Skip to content

Commit e9e32d0

Browse files
gggritsoclaude
andcommitted
fix(dashboards): Rebalance chart interval options and defaults
Replace 15m with 10m, add 6h, and remove 1d from interval options to create a more even step progression (alternating 2x/3x multipliers). Rebalance MINIMUM and MAXIMUM interval ladders with new thresholds at 12h, 2d, and 4d for tighter duration bands. Default to second-biggest interval on dashboards so charts are smooth out of the box while still offering a hyper-granular option for power users. Previously, 90d at 3h produced 720 data points per chart, causing UI jank and moiré artifacts on bar charts. Now defaults stay in the 60–360 range across all durations. Refs DAIN-1376 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d538b47 commit e9e32d0

File tree

6 files changed

+49
-36
lines changed

6 files changed

+49
-36
lines changed

static/app/components/charts/utils.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ export const SIXTY_DAYS = 86400;
2727
export const THIRTY_DAYS = 43200;
2828
export const TWO_WEEKS = 20160;
2929
export const ONE_WEEK = 10080;
30+
export const FOUR_DAYS = 5760;
3031
export const FORTY_EIGHT_HOURS = 2880;
3132
export const TWENTY_FOUR_HOURS = 1440;
33+
export const TWELVE_HOURS = 720;
3234
export const SIX_HOURS = 360;
3335
const THREE_HOURS = 180;
3436
export const ONE_HOUR = 60;

static/app/utils/useChartInterval.spec.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ describe('useChartInterval', () => {
2626
expect(intervalOptions).toEqual([
2727
{value: '1h', label: '1 hour'},
2828
{value: '3h', label: '3 hours'},
29-
{value: '12h', label: '12 hours'},
30-
{value: '1d', label: '1 day'},
29+
{value: '6h', label: '6 hours'},
3130
]);
3231
expect(chartInterval).toBe('1h'); // default
3332

@@ -47,12 +46,11 @@ describe('useChartInterval', () => {
4746
expect(intervalOptions).toEqual([
4847
{value: '1m', label: '1 minute'},
4948
{value: '5m', label: '5 minutes'},
50-
{value: '15m', label: '15 minutes'},
5149
]);
5250
act(() => {
53-
setChartInterval('15m');
51+
setChartInterval('5m');
5452
});
55-
expect(chartInterval).toBe('15m');
53+
expect(chartInterval).toBe('5m');
5654
});
5755
});
5856

static/app/utils/useChartInterval.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import type {Location} from 'history';
44
import {
55
FIVE_MINUTES,
66
FORTY_EIGHT_HOURS,
7+
FOUR_DAYS,
78
getDiffInMinutes,
89
GranularityLadder,
910
ONE_HOUR,
10-
ONE_WEEK,
1111
SIX_HOURS,
1212
THIRTY_DAYS,
13+
TWELVE_HOURS,
1314
TWO_WEEKS,
1415
} from 'sentry/components/charts/utils';
1516
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
@@ -20,7 +21,7 @@ import {decodeScalar} from 'sentry/utils/queryString';
2021
import {useLocation} from 'sentry/utils/useLocation';
2122
import {useNavigate} from 'sentry/utils/useNavigate';
2223

23-
enum ChartIntervalUnspecifiedStrategy {
24+
export enum ChartIntervalUnspecifiedStrategy {
2425
/** Use the second biggest possible interval (e.g., pretty big buckets) */
2526
USE_SECOND_BIGGEST = 'use_second_biggest',
2627
/** Use the smallest possible interval (e.g., the smallest possible buckets) */
@@ -104,12 +105,12 @@ function useChartIntervalImpl({
104105
const ALL_INTERVAL_OPTIONS = [
105106
{value: '1m', label: t('1 minute')},
106107
{value: '5m', label: t('5 minutes')},
107-
{value: '15m', label: t('15 minutes')},
108+
{value: '10m', label: t('10 minutes')},
108109
{value: '30m', label: t('30 minutes')},
109110
{value: '1h', label: t('1 hour')},
110111
{value: '3h', label: t('3 hours')},
112+
{value: '6h', label: t('6 hours')},
111113
{value: '12h', label: t('12 hours')},
112-
{value: '1d', label: t('1 day')},
113114
];
114115

115116
/**
@@ -119,19 +120,21 @@ const ALL_INTERVAL_OPTIONS = [
119120
const MINIMUM_INTERVAL = new GranularityLadder([
120121
[THIRTY_DAYS, '3h'],
121122
[TWO_WEEKS, '1h'],
122-
[ONE_WEEK, '30m'],
123-
[FORTY_EIGHT_HOURS, '15m'],
124-
[SIX_HOURS, '5m'],
123+
[FOUR_DAYS, '30m'],
124+
[FORTY_EIGHT_HOURS, '10m'],
125+
[TWELVE_HOURS, '5m'],
126+
[SIX_HOURS, '1m'],
125127
[0, '1m'],
126128
]);
127129

128130
const MAXIMUM_INTERVAL = new GranularityLadder([
129-
[THIRTY_DAYS, '1d'],
130-
[TWO_WEEKS, '1d'],
131-
[ONE_WEEK, '12h'],
132-
[FORTY_EIGHT_HOURS, '4h'],
133-
[SIX_HOURS, '1h'],
134-
[ONE_HOUR, '15m'],
131+
[THIRTY_DAYS, '12h'],
132+
[TWO_WEEKS, '6h'],
133+
[FOUR_DAYS, '3h'],
134+
[FORTY_EIGHT_HOURS, '1h'],
135+
[TWELVE_HOURS, '30m'],
136+
[SIX_HOURS, '10m'],
137+
[ONE_HOUR, '5m'],
135138
[FIVE_MINUTES, '5m'],
136139
[0, '1m'],
137140
]);

static/app/views/dashboards/dashboard.spec.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -548,11 +548,11 @@ describe('Dashboards > Dashboard', () => {
548548
body: [],
549549
match: [MockApiClient.matchQuery({interval: '5m'})],
550550
});
551-
const hourlyIntervalMock = MockApiClient.addMockResponse({
551+
const thirtyMinuteMock = MockApiClient.addMockResponse({
552552
url: '/organizations/org-slug/events-stats/',
553553
method: 'GET',
554554
body: [],
555-
match: [MockApiClient.matchQuery({interval: '1h'})],
555+
match: [MockApiClient.matchQuery({interval: '30m'})],
556556
});
557557

558558
// No interval in the URL — the 5m default is derived purely from the
@@ -564,16 +564,16 @@ describe('Dashboards > Dashboard', () => {
564564

565565
await screen.findByText('Test Spans Widget');
566566
await waitFor(() => expect(fiveMinuteMock).toHaveBeenCalled());
567-
expect(hourlyIntervalMock).not.toHaveBeenCalled();
567+
expect(thirtyMinuteMock).not.toHaveBeenCalled();
568568

569-
// Click the interval selector and choose '1 hour'. FiltersBar writes
570-
// interval=1h to the URL, DashboardWithIntervalSelector re-renders with
569+
// Click the interval selector and choose '30 minutes'. FiltersBar writes
570+
// interval=30m to the URL, DashboardWithIntervalSelector re-renders with
571571
// the new widgetInterval, and the widget re-fetches with the new interval.
572572
await userEvent.click(screen.getByRole('button', {name: '5 minutes'}));
573-
await userEvent.click(screen.getByRole('option', {name: '1 hour'}));
573+
await userEvent.click(screen.getByRole('option', {name: '30 minutes'}));
574574

575-
await waitFor(() => expect(hourlyIntervalMock).toHaveBeenCalled());
576-
expect(router.location.query.interval).toBe('1h');
575+
await waitFor(() => expect(thirtyMinuteMock).toHaveBeenCalled());
576+
expect(router.location.query.interval).toBe('30m');
577577
});
578578
});
579579

@@ -592,11 +592,11 @@ describe('Dashboards > Dashboard', () => {
592592
match: [MockApiClient.matchQuery({interval: '5m'})],
593593
});
594594

595-
const hourlyIntervalMock = MockApiClient.addMockResponse({
595+
const tenMinuteMock = MockApiClient.addMockResponse({
596596
url: '/organizations/org-slug/events-stats/',
597597
method: 'GET',
598598
body: [],
599-
match: [MockApiClient.matchQuery({interval: '1h'})],
599+
match: [MockApiClient.matchQuery({interval: '10m'})],
600600
});
601601

602602
const {router} = render(<DashboardWithIntervalSelector />, {
@@ -615,16 +615,16 @@ describe('Dashboards > Dashboard', () => {
615615

616616
// Selecting a new interval updates the URL and triggers a re-fetch.
617617
await userEvent.click(screen.getByRole('button', {name: '30 minutes'}));
618-
await userEvent.click(screen.getByRole('option', {name: '1 hour'}));
618+
await userEvent.click(screen.getByRole('option', {name: '10 minutes'}));
619619

620-
await waitFor(() => expect(hourlyIntervalMock).toHaveBeenCalled());
621-
expect(router.location.query.interval).toBe('1h');
620+
await waitFor(() => expect(tenMinuteMock).toHaveBeenCalled());
621+
expect(router.location.query.interval).toBe('10m');
622622
});
623623
});
624624

625625
describe('URL interval not valid for the dashboard period', () => {
626626
beforeEach(() => {
627-
// Override the outer 24h store setup — valid intervals for 30d are 3h, 12h, 1d.
627+
// Override the outer 24h store setup — valid intervals for 30d are 3h, 6h, 12h.
628628
PageFiltersStore.init();
629629
PageFiltersStore.onInitializeUrlState(
630630
getSavedFiltersAsPageFilters(thirtyDayDashboard)

static/app/views/dashboards/detail.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ import {OnRouteLeave} from 'sentry/utils/reactRouter6Compat/onRouteLeave';
5353
import {scheduleMicroTask} from 'sentry/utils/scheduleMicroTask';
5454
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
5555
import {useApi} from 'sentry/utils/useApi';
56-
import {useChartInterval} from 'sentry/utils/useChartInterval';
56+
import {
57+
ChartIntervalUnspecifiedStrategy,
58+
useChartInterval,
59+
} from 'sentry/utils/useChartInterval';
5760
import {useLocation} from 'sentry/utils/useLocation';
5861
import type {ReactRouter3Navigate} from 'sentry/utils/useNavigate';
5962
import {useNavigate} from 'sentry/utils/useNavigate';
@@ -1462,7 +1465,9 @@ export function DashboardDetailWithInjectedProps(
14621465
const location = useLocation();
14631466
const params = useParams<RouteParams>();
14641467
const router = useRouter();
1465-
const [chartInterval] = useChartInterval();
1468+
const [chartInterval] = useChartInterval({
1469+
unspecifiedStrategy: ChartIntervalUnspecifiedStrategy.USE_SECOND_BIGGEST,
1470+
});
14661471
const queryClient = useQueryClient();
14671472
// Always use the validated chart interval so the UI dropdown and widget
14681473
// requests stay in sync. chartInterval is validated against the current page

static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import {useState} from 'react';
33
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
44
import {dedupeArray} from 'sentry/utils/dedupeArray';
55
import type {Sort} from 'sentry/utils/discover/fields';
6-
import {useChartInterval} from 'sentry/utils/useChartInterval';
6+
import {
7+
ChartIntervalUnspecifiedStrategy,
8+
useChartInterval,
9+
} from 'sentry/utils/useChartInterval';
710
import {useLocation} from 'sentry/utils/useLocation';
811
import {useNavigate} from 'sentry/utils/useNavigate';
912
import {useOrganization} from 'sentry/utils/useOrganization';
@@ -44,7 +47,9 @@ export function WidgetPreview({
4447
const location = useLocation();
4548
const navigate = useNavigate();
4649
const pageFilters = usePageFilters();
47-
const [chartInterval] = useChartInterval();
50+
const [chartInterval] = useChartInterval({
51+
unspecifiedStrategy: ChartIntervalUnspecifiedStrategy.USE_SECOND_BIGGEST,
52+
});
4853

4954
const {state, dispatch} = useWidgetBuilderContext();
5055
const [tableWidths, setTableWidths] = useState<number[]>();

0 commit comments

Comments
 (0)