Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/number-tile-background-chart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@hyperdx/app': minor
'@hyperdx/common-utils': minor
---

feat(dashboards): add a background trend sparkline to number tiles

Number tiles can now render a faint line or area sparkline behind the value,
derived from a time-bucketed version of the same query, so the value's trend
over the selected range is visible at a glance. This is handy for SLO /
error-budget tiles where the burn over time matters as much as the current
number. The sparkline inherits the tile's color by default and can be
overridden to any palette token. Configure it under Display Settings >
Background chart on a number tile. Available on builder number tiles (raw SQL
number tiles return a single value with no time dimension to bucket).
72 changes: 72 additions & 0 deletions packages/app/src/components/BackgroundChartInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { BackgroundChart } from '@hyperdx/common-utils/dist/types';
import { Box, Select, Text } from '@mantine/core';

import { ColorSwatchInput } from './ColorSwatchInput';

const TYPE_OPTIONS = [
{ value: 'none', label: 'None' },
{ value: 'line', label: 'Line' },
{ value: 'area', label: 'Area' },
];

/**
* Editor for a number tile's optional background sparkline. The type select
* drives the `backgroundChart` config object: "None" clears it, "Line" /
* "Area" set the shape. The color swatch is an optional palette-token
* override; when unset the sparkline inherits the tile's static color.
*
* `disabled` is set for raw SQL number tiles, which have no time dimension to
* bucket: the control stays visible but inert with a hint, so the option is
* discoverable rather than missing.
*/
export function BackgroundChartInput({
value,
onChange,
disabled = false,
}: {
value?: BackgroundChart;
onChange: (value: BackgroundChart | undefined) => void;
disabled?: boolean;
}) {
return (
<Box>
<Text size="xs" c="dimmed" mb={4}>
Background chart
</Text>
<Select
size="xs"
data={TYPE_OPTIONS}
value={value?.type ?? 'none'}
disabled={disabled}
allowDeselect={false}
comboboxProps={{ withinPortal: false }}
aria-label="Number tile background chart type"
onChange={next => {
if (next === 'line' || next === 'area') {
onChange({ ...value, type: next });
} else {
onChange(undefined);
}
}}
/>
{disabled ? (
<Text size="xs" c="dimmed" mt={4}>
Available on query-builder number tiles.
</Text>
) : (
value && (
<Box mt="xs">
<Text size="xs" c="dimmed" mb={4}>
Background color
</Text>
<ColorSwatchInput
value={value.color}
onChange={color => onChange({ ...value, color })}
ariaLabel="Number tile background chart color"
/>
</Box>
)
)}
</Box>
);
}
28 changes: 28 additions & 0 deletions packages/app/src/components/ChartDisplaySettingsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { shouldFillNullsWithZero } from '@/ChartUtils';
import { DEFAULT_SERIES_LIMIT } from '@/defaults';
import { FormatTime } from '@/useFormatTime';

import { BackgroundChartInput } from './BackgroundChartInput';
import {
attachLocalIds,
ColorRulesEditor,
Expand All @@ -41,6 +42,7 @@ export type ChartConfigDisplaySettings = Pick<
| 'fitYAxisToData'
| 'color'
| 'colorRules'
| 'backgroundChart'
> & {
groupByColumnsOnLeft?: boolean;
// Per-tile cap on the number of series fetched for a group-by time chart.
Expand Down Expand Up @@ -95,6 +97,7 @@ function applyDefaultSettings(
colorRules: settings.colorRules
? attachLocalIds(settings.colorRules)
: undefined,
backgroundChart: settings.backgroundChart,
};
}

Expand Down Expand Up @@ -168,6 +171,14 @@ export default function ChartDisplaySettingsDrawer({
// `select[i].color`.
const showTileColor = displayType === DisplayType.Number;

// The background sparkline is derived from a time-bucketed version of the
// tile's query, so it only applies to builder number tiles: raw SQL number
// tiles return a single value with no time dimension to bucket. On a SQL
// number tile the control is shown disabled with a hint rather than hidden,
// so the option stays discoverable.
const showBackgroundChart = displayType === DisplayType.Number;
const isBackgroundChartDisabled = configType === 'sql';

return (
<Drawer
title="Display Settings"
Expand Down Expand Up @@ -286,6 +297,23 @@ export default function ChartDisplaySettingsDrawer({
</>
)}

{showBackgroundChart && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ux: maybe if it's a sql chart we can just show a disabled state with a message instead of disappearing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Done in 4fa65c9. On a SQL number tile the control now stays visible but disabled, with a hint ("Available on query-builder number tiles"), instead of disappearing. Keeps it discoverable, and the slot is already in place for when the SQL background query lands.

<>
<Controller
control={control}
name="backgroundChart"
render={({ field: { onChange, value } }) => (
<BackgroundChartInput
value={value}
onChange={onChange}
disabled={isBackgroundChartDisabled}
/>
)}
/>
<Divider />
</>
)}

<NumberFormatForm
control={control}
setValue={setValue}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ export default function EditTimeChartForm({
seriesLimit,
color,
colorRules,
backgroundChart,
] = useWatch({
control,
name: [
Expand All @@ -258,6 +259,7 @@ export default function EditTimeChartForm({
'seriesLimit',
'color',
'colorRules',
'backgroundChart',
],
});

Expand All @@ -281,6 +283,7 @@ export default function EditTimeChartForm({
seriesLimit,
color,
colorRules,
backgroundChart,
}),
[
alignDateRangeToGranularity,
Expand All @@ -292,6 +295,7 @@ export default function EditTimeChartForm({
seriesLimit,
color,
colorRules,
backgroundChart,
],
);

Expand Down Expand Up @@ -586,6 +590,7 @@ export default function EditTimeChartForm({
seriesLimit,
color,
colorRules,
backgroundChart,
}: ChartConfigDisplaySettings) => {
setValue('numberFormat', numberFormat);
setValue('alignDateRangeToGranularity', alignDateRangeToGranularity);
Expand All @@ -594,11 +599,12 @@ export default function EditTimeChartForm({
setValue('fitYAxisToData', fitYAxisToData);
setValue('groupByColumnsOnLeft', groupByColumnsOnLeft);
// Persist `null` (not undefined) when cleared so the disabled state
// survives JSON round-tripping through the URL query state — otherwise
// survives JSON round-tripping through the URL query state; otherwise
// the dropped key lets RHF's `values` sync restore the stale value.
setValue('seriesLimit', seriesLimit ?? null);
setValue('color', color);
setValue('colorRules', colorRules);
setValue('backgroundChart', backgroundChart);
onSubmit();
},
[setValue, onSubmit],
Expand Down
19 changes: 15 additions & 4 deletions packages/app/src/components/DBNumberChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import ChartErrorState, {
ChartErrorStateVariant,
} from './charts/ChartErrorState';
import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator';
import NumberTileBackgroundChart from './NumberTileBackgroundChart';

const NUMBER_TILE_MIN_FONT_SIZE = 10;
const NUMBER_TILE_MAX_FONT_SIZE = 72;
Expand Down Expand Up @@ -181,7 +182,7 @@ function AutoSizeNumber({
}

// Wraps AutoSizeNumber in an error boundary so a runtime failure in the
// measurement / ResizeObserver pipeline never blanks out the tile —
// measurement / ResizeObserver pipeline never blanks out the tile;
// instead the dashboard falls back to the original fixed-size rendering.
function SafeAutoSizeNumber({
children,
Expand Down Expand Up @@ -362,9 +363,19 @@ export default function DBNumberChart({
No data found within time range.
</div>
) : (
<SafeAutoSizeNumber color={tileColor}>
{formattedValue ?? 'N/A'}
</SafeAutoSizeNumber>
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
{config.backgroundChart && (
<NumberTileBackgroundChart
config={config}
backgroundChart={config.backgroundChart}
/>
)}
<div style={{ position: 'relative', zIndex: 1, height: '100%' }}>
<SafeAutoSizeNumber color={tileColor}>
{formattedValue ?? 'N/A'}
</SafeAutoSizeNumber>
</div>
</div>
)}
</ChartContainer>
);
Expand Down
Loading
Loading