fix: Set better Chart Axis Bounds (#1585)

Closes HDX-3180

# Summary

This PR makes a couple of changes to TimeChart axis bounds

1. Y Axis lower bound is now always 0 unless a series is selected (partial revert of #1572)
2. X-Axis bounds have been adjusted so that
   1. There is no longer an empty partial interval's worth of space at the end of line charts
   2. The first bar of a bar chart no longer overlaps with the Y Axis


## Before

- The first Bar in bar charts overlaps the Y Axis

<img width="1600" height="495" alt="Screenshot 2026-01-09 at 2 55 27 PM" src="https://github.com/user-attachments/assets/13087084-4847-46d5-98d3-85340101d7f3" />

- There is an empty partial (or full) interval at the end of charts

<img width="1611" height="483" alt="Screenshot 2026-01-09 at 2 55 21 PM" src="https://github.com/user-attachments/assets/a286966b-ccfa-485c-8d26-7090f75120e0" />

- Y Axis lower bound is non-zero when no series are selected

<img width="1603" height="388" alt="Screenshot 2026-01-09 at 2 38 54 PM" src="https://github.com/user-attachments/assets/4ca2743a-d484-40b1-b2e6-352aad838070" />
<img width="1623" height="373" alt="Screenshot 2026-01-09 at 2 37 26 PM" src="https://github.com/user-attachments/assets/4a8b9490-9efb-4c75-93b4-edb3321ee0a2" />

## After

- Bars fit fully within the chart domain

<img width="1610" height="503" alt="Screenshot 2026-01-09 at 2 55 42 PM" src="https://github.com/user-attachments/assets/42ab1f62-4c1e-475f-b3be-4bf6ed147dc4" />

- There is no empty space at the end of the line charts
- Y Axis lower bound is always 0 when no series are selected

<img width="1613" height="490" alt="Screenshot 2026-01-09 at 2 55 50 PM" src="https://github.com/user-attachments/assets/ed723a99-2a24-47f2-8ac8-93901f7fbf88" />
This commit is contained in:
Drew Davis 2026-01-09 15:49:37 -05:00 committed by GitHub
parent 99863885d0
commit 1e6987e485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 43 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
fix: Set better Chart Axis Bounds

View file

@ -1,5 +1,6 @@
import { memo, useCallback, useId, useMemo, useRef, useState } from 'react';
import cx from 'classnames';
import { add, isSameSecond, sub } from 'date-fns';
import { withErrorBoundary } from 'react-error-boundary';
import {
Area,
@ -16,6 +17,7 @@ import {
XAxis,
YAxis,
} from 'recharts';
import { AxisDomain } from 'recharts/types/util/types';
import { DisplayType } from '@hyperdx/common-utils/dist/types';
import { Popover } from '@mantine/core';
import { notifications } from '@mantine/notifications';
@ -24,7 +26,11 @@ import { IconCaretDownFilled, IconCaretUpFilled } from '@tabler/icons-react';
import type { NumberFormat } from '@/types';
import { COLORS, formatNumber, truncateMiddle } from '@/utils';
import { LineData } from './ChartUtils';
import {
convertGranularityToSeconds,
LineData,
toStartOfInterval,
} from './ChartUtils';
import { FormatTime, useFormatTime } from './useFormatTime';
import styles from '../styles/HDXLineChart.module.scss';
@ -404,6 +410,8 @@ export const MemoChart = memo(function MemoChart({
previousPeriodOffsetSeconds,
selectedSeriesNames,
onToggleSeries,
granularity,
dateRangeEndInclusive = true,
}: {
graphResults: any[];
setIsClickActive: (v: any) => void;
@ -421,6 +429,8 @@ export const MemoChart = memo(function MemoChart({
previousPeriodOffsetSeconds?: number;
selectedSeriesNames?: Set<string>;
onToggleSeries?: (seriesName: string, isShiftKey?: boolean) => void;
granularity: string;
dateRangeEndInclusive?: boolean;
}) {
const _id = useId();
const id = _id.replace(/:/g, '');
@ -495,12 +505,12 @@ export const MemoChart = memo(function MemoChart({
});
}, [lineData, displayType, id, isHovered, selectedSeriesNames]);
const yAxisDomain = useMemo(() => {
const yAxisDomain: AxisDomain = useMemo(() => {
const hasSelection = selectedSeriesNames && selectedSeriesNames.size > 0;
if (!hasSelection) {
// No selection, let Recharts auto-calculate based on all data
return ['auto', 'auto'];
return [0, 'auto'];
}
// When series are selected, calculate domain based only on visible series
@ -574,6 +584,28 @@ export const MemoChart = memo(function MemoChart({
return map;
}, [lineData]);
const xAxisDomain: AxisDomain = useMemo(() => {
let startTime = toStartOfInterval(dateRange[0], granularity);
let endTime = toStartOfInterval(dateRange[1], granularity);
const endTimeIsBoundaryAligned = isSameSecond(dateRange[1], endTime);
if (endTimeIsBoundaryAligned && !dateRangeEndInclusive) {
endTime = sub(endTime, {
seconds: convertGranularityToSeconds(granularity),
});
}
// For bar charts, extend the domain in both directions by half a granularity unit
// so that the full bar width is within the bounds of the chart
if (displayType === DisplayType.StackedBar) {
const halfGranularitySeconds =
convertGranularityToSeconds(granularity) / 2;
startTime = sub(startTime, { seconds: halfGranularitySeconds });
endTime = add(endTime, { seconds: halfGranularitySeconds });
}
return [startTime.getTime() / 1000, endTime.getTime() / 1000];
}, [dateRange, granularity, dateRangeEndInclusive, displayType]);
return (
<ResponsiveContainer
width="100%"
@ -708,10 +740,7 @@ export const MemoChart = memo(function MemoChart({
)}
<XAxis
dataKey={timestampKey ?? 'ts_bucket'}
domain={[
dateRange[0].getTime() / 1000,
dateRange[1].getTime() / 1000,
]}
domain={xAxisDomain}
interval="preserveStartEnd"
scale="time"
type="number"

View file

@ -750,6 +750,8 @@ function DBTimeChartComponent({
previousPeriodOffsetSeconds={previousPeriodOffsetSeconds}
selectedSeriesNames={selectedSeriesSet}
onToggleSeries={handleToggleSeries}
granularity={granularity}
dateRangeEndInclusive={queriedConfig.dateRangeEndInclusive}
/>
</>
)}