Skip to content
Draft
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
18 changes: 18 additions & 0 deletions .changeset/timechart-display-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@hyperdx/app': patch
'@hyperdx/common-utils': patch
---

feat: Per-tile display settings for time charts

Line and area time charts gain three settings in the tile Display Settings
drawer, persisted on the tile config:

- Show Legend: hide or show the series legend.
- Hover Tooltip: Auto, Single series, All series, or Hidden. Auto keeps the
density-based behavior (a single-series tooltip on dense charts, the full
list otherwise); the other values pin the mode.
- Line Style: Linear, Smooth, or Step interpolation for the drawn series.

All three are optional and fall back to the current behavior (legend on, Auto
tooltip, Smooth line) when unset, so existing tiles are unchanged.
59 changes: 53 additions & 6 deletions packages/app/src/HDXMultiSeriesTimeChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ const NEAREST_SERIES_MAX_DISTANCE_PX = 30;

type TooltipMode = 'single' | 'all' | 'hidden';

// The persisted hover-tooltip setting from the tile config. 'auto' defers to
// the density-based default (resolveTooltipMode); the other values are the
// explicit TooltipMode overrides.
export type TooltipModeSetting = 'auto' | TooltipMode;

// Curve interpolation for the drawn series, matching Recharts' `type`. Persisted
// on the tile config; defaults to 'monotone' (smooth) when unset.
export type LineInterpolation = 'linear' | 'monotone' | 'step';

// Above this many visible series the hover tooltip collapses to just the series
// under the cursor: a list of dozens of rows is unreadable and never the one
// being pointed at. At or below it, the full sorted list stays useful.
Expand Down Expand Up @@ -192,6 +201,22 @@ export function resolveTooltipMode(
: 'all';
}

/**
* The tooltip mode a chart actually renders with, folding the persisted tile
* setting over the density-based default. An explicit setting ('single' /
* 'all' / 'hidden') always wins; 'auto' (or an unset setting) falls back to
* `resolveTooltipMode`, so a tile keeps its adaptive behavior unless the user
* pins a mode in Display Settings.
*/
export function resolveEffectiveTooltipMode(
setting: TooltipModeSetting | undefined,
displayType: DisplayType | undefined,
visibleSeriesCount: number,
): TooltipMode {
if (setting != null && setting !== 'auto') return setting;
return resolveTooltipMode(displayType, visibleSeriesCount);
}

/**
* Which rows the hover tooltip renders, given its mode and hover state. Returns
* `null` to render nothing (a synced follower with no matching series, where
Expand Down Expand Up @@ -826,6 +851,8 @@ export const MemoChart = memo(function MemoChart({
granularity,
dateRangeEndInclusive = true,
fitYAxisToData = false,
tooltipMode: tooltipModeSetting,
lineInterpolation = 'monotone',
}: {
graphResults: any[];
setIsClickActive: (v: ActiveClickPayload | undefined) => void;
Expand Down Expand Up @@ -859,6 +886,13 @@ export const MemoChart = memo(function MemoChart({
* (with padding) instead of zero.
**/
fitYAxisToData?: boolean;
/**
* Persisted hover-tooltip setting from the tile config. 'auto' (or unset)
* derives the mode from the chart's density; the explicit values override it.
*/
tooltipMode?: TooltipModeSetting;
/** Curve interpolation for the drawn series. Defaults to 'monotone' (smooth). */
lineInterpolation?: LineInterpolation;
}) {
const _id = useId();
const id = _id.replace(/:/g, '');
Expand Down Expand Up @@ -928,8 +962,14 @@ export const MemoChart = memo(function MemoChart({

// Dense line/area charts collapse the hover tooltip to the series under the
// cursor; small charts and stacked bars keep the full list. Derived from the
// drawn series so legend filtering restores the full tooltip.
const tooltipMode = resolveTooltipMode(displayType, visibleLineData.length);
// drawn series so legend filtering restores the full tooltip. A tile can pin
// an explicit mode via Display Settings (tooltipModeSetting), which overrides
// this density-based default.
const tooltipMode = resolveEffectiveTooltipMode(
tooltipModeSetting,
displayType,
visibleLineData.length,
);

// The series to emphasize: the line nearest the cursor. Cheap to recompute;
// drives the overlay + the dim-others CSS class only, never the base lines.
Expand Down Expand Up @@ -974,7 +1014,7 @@ export const MemoChart = memo(function MemoChart({
<Area
key={key}
dataKey={key}
type="monotone"
type={lineInterpolation}
stroke={color}
fillOpacity={1}
activeDot={<CaptureActiveDot onCapture={captureActivePointY} />}
Expand All @@ -990,7 +1030,14 @@ export const MemoChart = memo(function MemoChart({
/>
);
});
}, [visibleLineData, displayType, id, isHovered, captureActivePointY]);
}, [
visibleLineData,
displayType,
id,
isHovered,
captureActivePointY,
lineInterpolation,
]);

// The emphasized series redrawn thick and on top. recharts paints graphical
// items in mount order and ignores a reorder of existing children, so an
Expand All @@ -1008,7 +1055,7 @@ export const MemoChart = memo(function MemoChart({
key="__hdx_emphasis_overlay__"
className="hdx-emphasis-overlay"
dataKey={emphasizedKey}
type="monotone"
type={lineInterpolation}
stroke={ld.color}
strokeWidth={2.5}
strokeOpacity={1}
Expand All @@ -1021,7 +1068,7 @@ export const MemoChart = memo(function MemoChart({
name={getSeriesDisplayName(ld)}
/>
);
}, [emphasizedKey, visibleLineData, displayType]);
}, [emphasizedKey, visibleLineData, displayType, lineInterpolation]);

const yAxisDomain: AxisDomain = useMemo(() => {
const hasSelection = selectedSeriesNames && selectedSeriesNames.size > 0;
Expand Down
50 changes: 50 additions & 0 deletions packages/app/src/__tests__/HDXMultiSeriesTimeChart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
collectMemoChartGradientHexes,
getVisibleLineData,
HARD_LINES_LIMIT,
resolveEffectiveTooltipMode,
resolveTooltipMode,
selectTooltipRows,
} from '@/HDXMultiSeriesTimeChart';
Expand Down Expand Up @@ -397,3 +398,52 @@ describe('resolveTooltipMode', () => {
expect(resolveTooltipMode(DisplayType.Line, 3)).toBe('all');
});
});

// A tile can pin an explicit tooltip mode in Display Settings, which overrides
// the density-based default. 'auto' (or an unset setting) keeps the adaptive
// behavior. Pins that precedence so the two never get crossed.
describe('resolveEffectiveTooltipMode', () => {
it('honors an explicit mode over the density default', () => {
// Sparse chart would auto-resolve to 'all'; an explicit 'single' wins.
expect(resolveEffectiveTooltipMode('single', DisplayType.Line, 2)).toBe(
'single',
);
// Dense chart would auto-resolve to 'single'; an explicit 'all' wins.
expect(resolveEffectiveTooltipMode('all', DisplayType.Line, 50)).toBe(
'all',
);
// 'hidden' has no auto equivalent; it only comes from an explicit setting.
expect(resolveEffectiveTooltipMode('hidden', DisplayType.Line, 2)).toBe(
'hidden',
);
});

it("falls back to the density default when the setting is 'auto'", () => {
expect(resolveEffectiveTooltipMode('auto', DisplayType.Line, 50)).toBe(
'single',
);
expect(resolveEffectiveTooltipMode('auto', DisplayType.Line, 2)).toBe(
'all',
);
});

it('falls back to the density default when the setting is unset', () => {
expect(resolveEffectiveTooltipMode(undefined, DisplayType.Line, 50)).toBe(
'single',
);
expect(resolveEffectiveTooltipMode(undefined, DisplayType.Line, 2)).toBe(
'all',
);
});

it('still forces the full tooltip for stacked bars under auto', () => {
// Stacked bars ignore the density collapse; auto must keep that.
expect(
resolveEffectiveTooltipMode('auto', DisplayType.StackedBar, 50),
).toBe('all');
// But an explicit 'hidden' setting still overrides even a stacked bar.
expect(
resolveEffectiveTooltipMode('hidden', DisplayType.StackedBar, 50),
).toBe('hidden');
});
});
67 changes: 67 additions & 0 deletions packages/app/src/components/ChartDisplaySettingsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Drawer,
Group,
NumberInput,
SegmentedControl,
Stack,
Text,
} from '@mantine/core';
Expand All @@ -32,6 +33,7 @@ import {
import { ColorSwatchInput } from './ColorSwatchInput';
import { CheckBoxControlled } from './InputControlled';
import { DEFAULT_NUMBER_FORMAT, NumberFormatForm } from './NumberFormat';
import SelectControlled from './SelectControlled';

export type ChartConfigDisplaySettings = Pick<
ChartConfigWithDateRange,
Expand All @@ -43,6 +45,9 @@ export type ChartConfigDisplaySettings = Pick<
| 'color'
| 'colorRules'
| 'backgroundChart'
| 'showLegend'
| 'tooltipMode'
| 'lineInterpolation'
> & {
groupByColumnsOnLeft?: boolean;
alternateRowBackground?: boolean;
Expand Down Expand Up @@ -102,6 +107,11 @@ function applyDefaultSettings(
? attachLocalIds(settings.colorRules)
: undefined,
backgroundChart: settings.backgroundChart,
// Line/area time-chart controls. Defaults match the renderer's fallbacks:
// legend on, tooltip mode auto (density-driven), smooth interpolation.
showLegend: settings.showLegend ?? true,
tooltipMode: settings.tooltipMode ?? 'auto',
lineInterpolation: settings.lineInterpolation ?? 'monotone',
};
}

Expand Down Expand Up @@ -181,6 +191,12 @@ export default function ChartDisplaySettingsDrawer({
const isTimeChart =
displayType === DisplayType.Line || displayType === DisplayType.StackedBar;

// Legend / tooltip / line-style controls apply to line/area time charts
// (DisplayType.Line renders as an area chart). Stacked bars keep the full
// tooltip and have no line curve, so this section is Line-only, mirroring the
// other displayType-gated sections below.
const showLineChartOptions = displayType === DisplayType.Line;

// The series-limit CTE is only emitted for builder group-by time charts;
// raw SQL configs author their own LIMIT logic directly.
const showSeriesLimit =
Expand Down Expand Up @@ -288,6 +304,57 @@ export default function ChartDisplaySettingsDrawer({
</>
)}

{showLineChartOptions && (
<>
<CheckBoxControlled
control={control}
name="showLegend"
size="xs"
label="Show Legend"
/>
<Box>
<SelectControlled
control={control}
name="tooltipMode"
size="xs"
label="Hover Tooltip"
description="How the hover tooltip lists series. Auto shows a single series on dense charts and the full list otherwise."
allowDeselect={false}
comboboxProps={{ withinPortal: false }}
data={[
{ label: 'Auto', value: 'auto' },
{ label: 'Single series', value: 'single' },
{ label: 'All series', value: 'all' },
{ label: 'Hidden', value: 'hidden' },
]}
/>
</Box>
<Box>
<Text size="xs" c="dimmed" mb={4}>
Line Style
</Text>
<Controller
control={control}
name="lineInterpolation"
render={({ field: { onChange, value } }) => (
<SegmentedControl
size="xs"
fullWidth
value={value ?? 'monotone'}
onChange={onChange}
data={[
{ label: 'Linear', value: 'linear' },
{ label: 'Smooth', value: 'monotone' },
{ label: 'Step', value: 'step' },
]}
/>
)}
/>
</Box>
<Divider />
</>
)}

{showCategoricalLimit && (
<>
<Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ export default function EditTimeChartForm({
color,
colorRules,
backgroundChart,
showLegend,
tooltipMode,
lineInterpolation,
] = useWatch({
control,
name: [
Expand All @@ -290,6 +293,9 @@ export default function EditTimeChartForm({
'color',
'colorRules',
'backgroundChart',
'showLegend',
'tooltipMode',
'lineInterpolation',
],
});

Expand Down Expand Up @@ -320,6 +326,9 @@ export default function EditTimeChartForm({
color,
colorRules,
backgroundChart,
showLegend,
tooltipMode,
lineInterpolation,
}),
[
alignDateRangeToGranularity,
Expand All @@ -333,6 +342,9 @@ export default function EditTimeChartForm({
color,
colorRules,
backgroundChart,
showLegend,
tooltipMode,
lineInterpolation,
],
);

Expand Down Expand Up @@ -633,6 +645,9 @@ export default function EditTimeChartForm({
color,
colorRules,
backgroundChart,
showLegend,
tooltipMode,
lineInterpolation,
}: ChartConfigDisplaySettings,
isDirty: boolean,
) => {
Expand All @@ -655,6 +670,9 @@ export default function EditTimeChartForm({
setValue('color', color);
setValue('colorRules', colorRules);
setValue('backgroundChart', backgroundChart);
setValue('showLegend', showLegend);
setValue('tooltipMode', tooltipMode);
setValue('lineInterpolation', lineInterpolation);
Comment on lines +673 to +675

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.

P1 Raw SQL settings are discarded

When a user applies these settings to a Raw SQL or PromQL line chart, the form records them here but the corresponding saved-config conversion branches omit all three fields, causing the selected legend, tooltip, and interpolation behavior to revert after saving.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

// Display settings live in a separate drawer form, so RHF can't track
// them. Latch dirty state only when the drawer reports actual changes.
if (isDirty) {
Expand Down
8 changes: 7 additions & 1 deletion packages/app/src/components/DBTimeChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -840,14 +840,20 @@ function DBTimeChartComponent({
referenceLines={referenceLines}
annotations={annotations}
setIsClickActive={setPinnedPayload}
showLegend={showLegend}
// The tile config can hide the legend via Display Settings. Combine
// it with the context prop (AND) so a surface that never shows a
// legend (e.g. compact search charts passing showLegend={false})
// still wins, and existing tiles (config unset) are unchanged.
showLegend={showLegend && (queriedConfig.showLegend ?? true)}
timestampKey={timestampColumn?.name}
previousPeriodOffsetSeconds={previousPeriodOffsetSeconds}
selectedSeriesNames={selectedSeriesSet}
onToggleSeries={handleToggleSeries}
granularity={granularity}
dateRangeEndInclusive={queriedConfig.dateRangeEndInclusive}
fitYAxisToData={queriedConfig.fitYAxisToData}
tooltipMode={queriedConfig.tooltipMode}
lineInterpolation={queriedConfig.lineInterpolation}
/>
</>
)}
Expand Down
Loading