Skip to content

Commit 6bda3fc

Browse files
authored
feat(dashboards): Allow constraining the Y axis range to data min and max in TimeSeriesWidgetVisualization (#90247)
Closes [DAIN-377: Allow setting automatic Y axis min](https://linear.app/getsentry/issue/DAIN-377/allow-setting-automatic-y-axis-min) ~except for a _huge_ caveat with release bubbles~. Adds a new prop to `TimeSeriesWidgetVisualization`. If `axisRange` is set to `"axisRange"`, the Y axis doesn't start at 0, it starts at the data min. The story has an explanation of when this is useful and how to use it. **e.g.,** These two charts plot the same data series! <img width="756" alt="Screenshot 2025-05-23 at 2 43 39 PM" src="https://github.com/user-attachments/assets/52b32a86-68e4-48ef-a973-1aa6aebcc51d" />
1 parent 3c3a9c7 commit 6bda3fc

File tree

4 files changed

+287
-18
lines changed

4 files changed

+287
-18
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types';
2+
3+
export const sampleCrashFreeRateTimeSeries: TimeSeries = {
4+
yAxis: 'crash_free_rate()',
5+
meta: {
6+
valueType: 'percentage',
7+
valueUnit: null,
8+
interval: 1_800_000, // 30 minutes
9+
},
10+
values: [
11+
{
12+
value: 0.9997,
13+
timestamp: 1729796400000, // '2024-10-24T15:00:00-04:00'
14+
},
15+
{
16+
value: 0.9998,
17+
timestamp: 1729798200000, // '2024-10-24T15:30:00-04:00'
18+
},
19+
{
20+
value: 0.9996,
21+
timestamp: 1729800000000, // '2024-10-24T16:00:00-04:00'
22+
},
23+
{
24+
value: 0.9999,
25+
timestamp: 1729801800000, // '2024-10-24T16:30:00-04:00'
26+
},
27+
{
28+
value: 1.0,
29+
timestamp: 1729803600000, // '2024-10-24T17:00:00-04:00'
30+
},
31+
{
32+
value: 0.9997,
33+
timestamp: 1729805400000, // '2024-10-24T17:30:00-04:00'
34+
},
35+
{
36+
value: 0.9998,
37+
timestamp: 1729807200000, // '2024-10-24T18:00:00-04:00'
38+
},
39+
{
40+
value: 0.9999,
41+
timestamp: 1729809000000, // '2024-10-24T18:30:00-04:00'
42+
},
43+
{
44+
value: 0.9996,
45+
timestamp: 1729810800000, // '2024-10-24T19:00:00-04:00'
46+
},
47+
{
48+
value: 0.9997,
49+
timestamp: 1729812600000, // '2024-10-24T19:30:00-04:00'
50+
},
51+
{
52+
value: 0.9998,
53+
timestamp: 1729814400000, // '2024-10-24T20:00:00-04:00'
54+
},
55+
{
56+
value: 0.9998,
57+
timestamp: 1729816200000, // '2024-10-24T20:30:00-04:00'
58+
},
59+
{
60+
value: 0.9999,
61+
timestamp: 1729818000000, // '2024-10-24T21:00:00-04:00'
62+
},
63+
{
64+
value: 0.9996,
65+
timestamp: 1729819800000, // '2024-10-24T21:30:00-04:00'
66+
},
67+
{
68+
value: 0.9998,
69+
timestamp: 1729821600000, // '2024-10-24T22:00:00-04:00'
70+
},
71+
{
72+
value: 0.9997,
73+
timestamp: 1729823400000, // '2024-10-24T22:30:00-04:00'
74+
},
75+
{
76+
value: 1.0,
77+
timestamp: 1729825200000, // '2024-10-24T23:00:00-04:00'
78+
},
79+
{
80+
value: 0.9998,
81+
timestamp: 1729827000000, // '2024-10-24T23:30:00-04:00'
82+
},
83+
{
84+
value: 0.9996,
85+
timestamp: 1729828800000, // '2024-10-25T00:00:00-04:00'
86+
},
87+
{
88+
value: 0.999,
89+
timestamp: 1729830600000, // '2024-10-25T00:30:00-04:00'
90+
},
91+
{
92+
value: 0.9997,
93+
timestamp: 1729832400000, // '2024-10-25T01:00:00-04:00'
94+
},
95+
{
96+
value: 0.9998,
97+
timestamp: 1729834200000, // '2024-10-25T01:30:00-04:00'
98+
},
99+
{
100+
value: 1.0,
101+
timestamp: 1729836000000, // '2024-10-25T02:00:00-04:00'
102+
},
103+
{
104+
value: 0.9999,
105+
timestamp: 1729837800000, // '2024-10-25T02:30:00-04:00'
106+
},
107+
{
108+
value: 0.9997,
109+
timestamp: 1729839600000, // '2024-10-25T03:00:00-04:00'
110+
},
111+
{
112+
value: 0.9996,
113+
timestamp: 1729841400000, // '2024-10-25T03:30:00-04:00'
114+
},
115+
{
116+
value: 0.9998,
117+
timestamp: 1729843200000, // '2024-10-25T04:00:00-04:00'
118+
},
119+
{
120+
value: 0.9997,
121+
timestamp: 1729845000000, // '2024-10-25T04:30:00-04:00'
122+
},
123+
{
124+
value: 0.9999,
125+
timestamp: 1729846800000, // '2024-10-25T05:00:00-04:00'
126+
},
127+
{
128+
value: 0.9999,
129+
timestamp: 1729848600000, // '2024-10-25T05:30:00-04:00'
130+
},
131+
{
132+
value: 0.9996,
133+
timestamp: 1729850400000, // '2024-10-25T06:00:00-04:00'
134+
},
135+
{
136+
value: 0.9998,
137+
timestamp: 1729852200000, // '2024-10-25T06:30:00-04:00'
138+
},
139+
{
140+
value: 0.9997,
141+
timestamp: 1729854000000, // '2024-10-25T07:00:00-04:00'
142+
},
143+
{
144+
value: 1.0,
145+
timestamp: 1729855800000, // '2024-10-25T07:30:00-04:00'
146+
},
147+
{
148+
value: 0.9999,
149+
timestamp: 1729857600000, // '2024-10-25T08:00:00-04:00'
150+
},
151+
{
152+
value: 0.9996,
153+
timestamp: 1729859400000, // '2024-10-25T08:30:00-04:00'
154+
},
155+
{
156+
value: 0.9998,
157+
timestamp: 1729861200000, // '2024-10-25T09:00:00-04:00'
158+
},
159+
{
160+
value: 0.9997,
161+
timestamp: 1729863000000, // '2024-10-25T09:30:00-04:00'
162+
},
163+
{
164+
value: 0.9997,
165+
timestamp: 1729864800000, // '2024-10-25T10:00:00-04:00'
166+
},
167+
{
168+
value: 0.9996,
169+
timestamp: 1729866600000, // '2024-10-25T10:30:00-04:00'
170+
},
171+
{
172+
value: 0.9996,
173+
timestamp: 1729868400000, // '2024-10-25T11:00:00-04:00'
174+
},
175+
{
176+
value: 0.9998,
177+
timestamp: 1729870200000, // '2024-10-25T11:30:00-04:00'
178+
},
179+
{
180+
value: 0.9997,
181+
timestamp: 1729872000000, // '2024-10-25T12:00:00-04:00'
182+
},
183+
{
184+
value: 0.9999,
185+
timestamp: 1729873800000, // '2024-10-25T12:30:00-04:00'
186+
},
187+
{
188+
value: 0.9999,
189+
timestamp: 1729875600000, // '2024-10-25T13:00:00-04:00'
190+
},
191+
{
192+
value: 0.9996,
193+
timestamp: 1729877400000, // '2024-10-25T13:30:00-04:00'
194+
},
195+
{
196+
value: 0.9998,
197+
timestamp: 1729879200000, // '2024-10-25T14:00:00-04:00'
198+
},
199+
{
200+
value: 0.9997,
201+
timestamp: 1729881000000, // '2024-10-25T14:30:00-04:00'
202+
},
203+
{
204+
value: 0.9996,
205+
timestamp: 1729882800000, // '2024-10-25T15:00:00-04:00'
206+
},
207+
],
208+
};

static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.stories.tsx

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121

2222
import {shiftTabularDataToNow} from './__stories__/shiftTabularDataToNow';
2323
import {shiftTimeSeriesToNow} from './__stories__/shiftTimeSeriesToNow';
24+
import {sampleCrashFreeRateTimeSeries} from './fixtures/sampleCrashFreeRateTimeSeries';
2425
import {sampleDurationTimeSeries} from './fixtures/sampleDurationTimeSeries';
2526
import {sampleScoreTimeSeries} from './fixtures/sampleScoreTimeSeries';
2627
import {sampleThroughputTimeSeries} from './fixtures/sampleThroughputTimeSeries';
@@ -58,6 +59,19 @@ const sampleDurationTimeSeriesP75: TimeSeries = {
5859

5960
const shiftedSpanSamples = shiftTabularDataToNow(spanSamplesWithDurations);
6061

62+
const releases = [
63+
{
64+
version: 'ui@0.1.2',
65+
timestamp: new Date(sampleThroughputTimeSeries.values.at(2)!.timestamp).toISOString(),
66+
},
67+
{
68+
version: 'ui@0.1.3',
69+
timestamp: new Date(
70+
sampleThroughputTimeSeries.values.at(20)!.timestamp
71+
).toISOString(),
72+
},
73+
].filter(hasTimestamp);
74+
6175
export default Storybook.story('TimeSeriesWidgetVisualization', (story, APIReference) => {
6276
APIReference(types.TimeSeriesWidgetVisualization);
6377

@@ -405,6 +419,53 @@ export default Storybook.story('TimeSeriesWidgetVisualization', (story, APIRefer
405419
/>
406420
</SmallWidget>
407421
</Storybook.SideBySide>
422+
423+
<p>
424+
A common issue with Y axes is data ranges. Some time series, like crash rates
425+
tend to hover very close to 100%. In these cases, starting the Y axis at 0 can
426+
make it difficult to see the actual values. You can set the{' '}
427+
<code>axisRange</code> prop to <code>"dataMin"</code> to start the Y axis at the
428+
minimum value of the data.
429+
</p>
430+
431+
<p>
432+
In the charts below you can see an example. The left chart is not very useful,
433+
because it looks like a flat line at 100%. The chart in the middle shows the
434+
actual data much clearer, and a dip is visible.
435+
</p>
436+
437+
<Storybook.SideBySide>
438+
<SmallWidget>
439+
<TimeSeriesWidgetVisualization
440+
plottables={[new Line(sampleCrashFreeRateTimeSeries)]}
441+
/>
442+
</SmallWidget>
443+
<SmallWidget>
444+
<TimeSeriesWidgetVisualization
445+
plottables={[new Line(sampleCrashFreeRateTimeSeries)]}
446+
axisRange="dataMin"
447+
/>
448+
</SmallWidget>
449+
</Storybook.SideBySide>
450+
451+
<p>A few notes of caution:</p>
452+
<ol>
453+
<li>
454+
This only works well for line series. If you try this with area or bar
455+
plottables you will have a bad time because the chart will look weird and make
456+
no sense
457+
</li>
458+
<li>
459+
If your data range is very narrow (e.g., &lt;0.00001) you will have a bad time
460+
because the Y axis labels will become very long to accommodate the high
461+
precision
462+
</li>
463+
<li>
464+
Some customers find floating Y axis minimum disorienting. When they change the
465+
date range or environment, the floating Y axis minimum makes it harder to
466+
compare the data visually
467+
</li>
468+
</ol>
408469
</Fragment>
409470
);
410471
});
@@ -881,21 +942,6 @@ export default Storybook.story('TimeSeriesWidgetVisualization', (story, APIRefer
881942
});
882943

883944
story('Releases', () => {
884-
const releases = [
885-
{
886-
version: 'ui@0.1.2',
887-
timestamp: new Date(
888-
sampleThroughputTimeSeries.values.at(2)!.timestamp
889-
).toISOString(),
890-
},
891-
{
892-
version: 'ui@0.1.3',
893-
timestamp: new Date(
894-
sampleThroughputTimeSeries.values.at(20)!.timestamp
895-
).toISOString(),
896-
},
897-
].filter(hasTimestamp);
898-
899945
return (
900946
<Fragment>
901947
<p>

static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ export interface TimeSeriesWidgetVisualizationProps
6868
* An array of `Plottable` objects. This can be any object that implements the `Plottable` interface.
6969
*/
7070
plottables: Plottable[];
71+
/**
72+
* Sets the range of the Y axis.
73+
*
74+
* - `auto`: The Y axis starts at 0, and ends at the maximum value of the data.
75+
* - `dataMin`: The Y axis starts at the minimum value of the data, and ends at the maximum value of the data.
76+
* Default: `auto`
77+
*/
78+
axisRange?: 'auto' | 'dataMin';
7179
/**
7280
* A mapping of time series field name to boolean. If the value is `false`, the series is hidden from view
7381
*/
@@ -76,6 +84,7 @@ export interface TimeSeriesWidgetVisualizationProps
7684
* Callback that returns an updated `LegendSelection` after a user manipulations the selection via the legend
7785
*/
7886
onLegendSelectionChange?: (selection: LegendSelection) => void;
87+
7988
/**
8089
* Callback that returns an updated ECharts zoom selection. If omitted, the default behavior is to update the URL with updated `start` and `end` query parameters.
8190
*/
@@ -223,6 +232,8 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
223232
return FALLBACK_UNIT_FOR_FIELD_TYPE[type as AggregationOutputType];
224233
});
225234

235+
const axisRangeProp = props.axisRange ?? 'auto';
236+
226237
const leftYAxis: YAXisComponentOption = TimeSeriesWidgetYAxis(
227238
{
228239
axisLabel: {
@@ -231,7 +242,8 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
231242
},
232243
position: 'left',
233244
},
234-
leftYAxisType
245+
leftYAxisType,
246+
axisRangeProp
235247
);
236248

237249
const rightYAxis: YAXisComponentOption | undefined = rightYAxisType
@@ -247,7 +259,8 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
247259
},
248260
position: 'right',
249261
},
250-
rightYAxisType
262+
rightYAxisType,
263+
axisRangeProp
251264
)
252265
: undefined;
253266

static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetYAxis.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ type TimeSeriesWidgetYAxisProps = YAXisComponentOption;
77

88
export function TimeSeriesWidgetYAxis(
99
props: TimeSeriesWidgetYAxisProps,
10-
yAxisFieldType: string
10+
yAxisFieldType: string,
11+
yAxisRange: 'auto' | 'dataMin'
1112
): YAXisComponentOption {
1213
return merge(
1314
{
@@ -24,6 +25,7 @@ export function TimeSeriesWidgetYAxis(
2425
show: false,
2526
},
2627
},
28+
min: yAxisRange === 'auto' ? null : 'dataMin',
2729
// @ts-expect-error ECharts types are wrong here. Returning `undefined` from the `max` function is 100% allowed and is listed in the documentation. See https://github.com/apache/echarts/pull/12215/
2830
max: value => {
2931
// Handle a very specific edge case with percentage formatting.

0 commit comments

Comments
 (0)