Skip to content

feat(dashboards): Allow constraining the Y axis range to data min and max in TimeSeriesWidgetVisualization #90247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types';

export const sampleCrashFreeRateTimeSeries: TimeSeries = {
yAxis: 'crash_free_rate()',
meta: {
valueType: 'percentage',
valueUnit: null,
interval: 1_800_000, // 30 minutes
},
values: [
{
value: 0.9997,
timestamp: 1729796400000, // '2024-10-24T15:00:00-04:00'
},
{
value: 0.9998,
timestamp: 1729798200000, // '2024-10-24T15:30:00-04:00'
},
{
value: 0.9996,
timestamp: 1729800000000, // '2024-10-24T16:00:00-04:00'
},
{
value: 0.9999,
timestamp: 1729801800000, // '2024-10-24T16:30:00-04:00'
},
{
value: 1.0,
timestamp: 1729803600000, // '2024-10-24T17:00:00-04:00'
},
{
value: 0.9997,
timestamp: 1729805400000, // '2024-10-24T17:30:00-04:00'
},
{
value: 0.9998,
timestamp: 1729807200000, // '2024-10-24T18:00:00-04:00'
},
{
value: 0.9999,
timestamp: 1729809000000, // '2024-10-24T18:30:00-04:00'
},
{
value: 0.9996,
timestamp: 1729810800000, // '2024-10-24T19:00:00-04:00'
},
{
value: 0.9997,
timestamp: 1729812600000, // '2024-10-24T19:30:00-04:00'
},
{
value: 0.9998,
timestamp: 1729814400000, // '2024-10-24T20:00:00-04:00'
},
{
value: 0.9998,
timestamp: 1729816200000, // '2024-10-24T20:30:00-04:00'
},
{
value: 0.9999,
timestamp: 1729818000000, // '2024-10-24T21:00:00-04:00'
},
{
value: 0.9996,
timestamp: 1729819800000, // '2024-10-24T21:30:00-04:00'
},
{
value: 0.9998,
timestamp: 1729821600000, // '2024-10-24T22:00:00-04:00'
},
{
value: 0.9997,
timestamp: 1729823400000, // '2024-10-24T22:30:00-04:00'
},
{
value: 1.0,
timestamp: 1729825200000, // '2024-10-24T23:00:00-04:00'
},
{
value: 0.9998,
timestamp: 1729827000000, // '2024-10-24T23:30:00-04:00'
},
{
value: 0.9996,
timestamp: 1729828800000, // '2024-10-25T00:00:00-04:00'
},
{
value: 0.999,
timestamp: 1729830600000, // '2024-10-25T00:30:00-04:00'
},
{
value: 0.9997,
timestamp: 1729832400000, // '2024-10-25T01:00:00-04:00'
},
{
value: 0.9998,
timestamp: 1729834200000, // '2024-10-25T01:30:00-04:00'
},
{
value: 1.0,
timestamp: 1729836000000, // '2024-10-25T02:00:00-04:00'
},
{
value: 0.9999,
timestamp: 1729837800000, // '2024-10-25T02:30:00-04:00'
},
{
value: 0.9997,
timestamp: 1729839600000, // '2024-10-25T03:00:00-04:00'
},
{
value: 0.9996,
timestamp: 1729841400000, // '2024-10-25T03:30:00-04:00'
},
{
value: 0.9998,
timestamp: 1729843200000, // '2024-10-25T04:00:00-04:00'
},
{
value: 0.9997,
timestamp: 1729845000000, // '2024-10-25T04:30:00-04:00'
},
{
value: 0.9999,
timestamp: 1729846800000, // '2024-10-25T05:00:00-04:00'
},
{
value: 0.9999,
timestamp: 1729848600000, // '2024-10-25T05:30:00-04:00'
},
{
value: 0.9996,
timestamp: 1729850400000, // '2024-10-25T06:00:00-04:00'
},
{
value: 0.9998,
timestamp: 1729852200000, // '2024-10-25T06:30:00-04:00'
},
{
value: 0.9997,
timestamp: 1729854000000, // '2024-10-25T07:00:00-04:00'
},
{
value: 1.0,
timestamp: 1729855800000, // '2024-10-25T07:30:00-04:00'
},
{
value: 0.9999,
timestamp: 1729857600000, // '2024-10-25T08:00:00-04:00'
},
{
value: 0.9996,
timestamp: 1729859400000, // '2024-10-25T08:30:00-04:00'
},
{
value: 0.9998,
timestamp: 1729861200000, // '2024-10-25T09:00:00-04:00'
},
{
value: 0.9997,
timestamp: 1729863000000, // '2024-10-25T09:30:00-04:00'
},
{
value: 0.9997,
timestamp: 1729864800000, // '2024-10-25T10:00:00-04:00'
},
{
value: 0.9996,
timestamp: 1729866600000, // '2024-10-25T10:30:00-04:00'
},
{
value: 0.9996,
timestamp: 1729868400000, // '2024-10-25T11:00:00-04:00'
},
{
value: 0.9998,
timestamp: 1729870200000, // '2024-10-25T11:30:00-04:00'
},
{
value: 0.9997,
timestamp: 1729872000000, // '2024-10-25T12:00:00-04:00'
},
{
value: 0.9999,
timestamp: 1729873800000, // '2024-10-25T12:30:00-04:00'
},
{
value: 0.9999,
timestamp: 1729875600000, // '2024-10-25T13:00:00-04:00'
},
{
value: 0.9996,
timestamp: 1729877400000, // '2024-10-25T13:30:00-04:00'
},
{
value: 0.9998,
timestamp: 1729879200000, // '2024-10-25T14:00:00-04:00'
},
{
value: 0.9997,
timestamp: 1729881000000, // '2024-10-25T14:30:00-04:00'
},
{
value: 0.9996,
timestamp: 1729882800000, // '2024-10-25T15:00:00-04:00'
},
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {

import {shiftTabularDataToNow} from './__stories__/shiftTabularDataToNow';
import {shiftTimeSeriesToNow} from './__stories__/shiftTimeSeriesToNow';
import {sampleCrashFreeRateTimeSeries} from './fixtures/sampleCrashFreeRateTimeSeries';
import {sampleDurationTimeSeries} from './fixtures/sampleDurationTimeSeries';
import {sampleScoreTimeSeries} from './fixtures/sampleScoreTimeSeries';
import {sampleThroughputTimeSeries} from './fixtures/sampleThroughputTimeSeries';
Expand Down Expand Up @@ -58,6 +59,19 @@ const sampleDurationTimeSeriesP75: TimeSeries = {

const shiftedSpanSamples = shiftTabularDataToNow(spanSamplesWithDurations);

const releases = [
{
version: 'ui@0.1.2',
timestamp: new Date(sampleThroughputTimeSeries.values.at(2)!.timestamp).toISOString(),
},
{
version: 'ui@0.1.3',
timestamp: new Date(
sampleThroughputTimeSeries.values.at(20)!.timestamp
).toISOString(),
},
].filter(hasTimestamp);

export default Storybook.story('TimeSeriesWidgetVisualization', (story, APIReference) => {
APIReference(types.TimeSeriesWidgetVisualization);

Expand Down Expand Up @@ -405,6 +419,58 @@ export default Storybook.story('TimeSeriesWidgetVisualization', (story, APIRefer
/>
</SmallWidget>
</Storybook.SideBySide>

<p>
A common issue with Y axes is data ranges. Some time series, like crash rates
tend to hover very close to 100%. In these cases, starting the Y axis at 0 can
make it difficult to see the actual values. You can set the{' '}
<code>axisRange</code> prop to <code>"dataMin"</code> to start the Y axis at the
minimum value of the data.
</p>

<p>
In the charts below you can see an example. The left chart is not very useful,
because it looks like a flat line at 100%. The chart in the middle shows the
actual data much clearer, and a dip is visible.
</p>

<Storybook.SideBySide>
<SmallWidget>
<TimeSeriesWidgetVisualization
plottables={[new Line(sampleCrashFreeRateTimeSeries)]}
/>
</SmallWidget>
<SmallWidget>
<TimeSeriesWidgetVisualization
plottables={[new Line(sampleCrashFreeRateTimeSeries)]}
axisRange="dataMin"
/>
</SmallWidget>
</Storybook.SideBySide>

<p>A few notes of caution:</p>
<ol>
<li>
This feature is not compatible with release bubbles! The bubble release series
will always force the range to start at 0, since the release bubbles are a
series with values of 0.
</li>
<li>
This only works well for line series. If you try this with area or bar
plottables you will have a bad time because the chart will look weird and make
no sense
</li>
<li>
If your data range is very narrow (e.g., &lt;0.00001) you will have a bad time
because the Y axis labels will become very long to accommodate the high
precision
</li>
<li>
Some customers find floating Y axis minimum disorienting. When they change the
date range or environment, the floating Y axis minimum makes it harder to
compare the data visually
</li>
</ol>
</Fragment>
);
});
Expand Down Expand Up @@ -881,21 +947,6 @@ export default Storybook.story('TimeSeriesWidgetVisualization', (story, APIRefer
});

story('Releases', () => {
const releases = [
{
version: 'ui@0.1.2',
timestamp: new Date(
sampleThroughputTimeSeries.values.at(2)!.timestamp
).toISOString(),
},
{
version: 'ui@0.1.3',
timestamp: new Date(
sampleThroughputTimeSeries.values.at(20)!.timestamp
).toISOString(),
},
].filter(hasTimestamp);

return (
<Fragment>
<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export interface TimeSeriesWidgetVisualizationProps
* An array of `Plottable` objects. This can be any object that implements the `Plottable` interface.
*/
plottables: Plottable[];
/**
* Sets the range of the Y axis.
*
* - `auto`: The Y axis starts at 0, and ends at the maximum value of the data.
* - `dataMin`: The Y axis starts at the minimum value of the data, and ends at the maximum value of the data.
* Default: `auto`
*/
axisRange?: 'auto' | 'dataMin';
/**
* A mapping of time series field name to boolean. If the value is `false`, the series is hidden from view
*/
Expand All @@ -76,6 +84,7 @@ export interface TimeSeriesWidgetVisualizationProps
* Callback that returns an updated `LegendSelection` after a user manipulations the selection via the legend
*/
onLegendSelectionChange?: (selection: LegendSelection) => void;

/**
* 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.
*/
Expand Down Expand Up @@ -223,6 +232,8 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
return FALLBACK_UNIT_FOR_FIELD_TYPE[type as AggregationOutputType];
});

const axisRangeProp = props.axisRange ?? 'auto';

const leftYAxis: YAXisComponentOption = TimeSeriesWidgetYAxis(
{
axisLabel: {
Expand All @@ -231,7 +242,8 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
},
position: 'left',
},
leftYAxisType
leftYAxisType,
axisRangeProp
);

const rightYAxis: YAXisComponentOption | undefined = rightYAxisType
Expand All @@ -247,7 +259,8 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
},
position: 'right',
},
rightYAxisType
rightYAxisType,
axisRangeProp
)
: undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ type TimeSeriesWidgetYAxisProps = YAXisComponentOption;

export function TimeSeriesWidgetYAxis(
props: TimeSeriesWidgetYAxisProps,
yAxisFieldType: string
yAxisFieldType: string,
yAxisRange: 'auto' | 'dataMin'
): YAXisComponentOption {
return merge(
{
Expand All @@ -24,6 +25,7 @@ export function TimeSeriesWidgetYAxis(
show: false,
},
},
min: yAxisRange === 'auto' ? null : 'dataMin',
// @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/
max: value => {
// Handle a very specific edge case with percentage formatting.
Expand Down
Loading