Skip to content

Commit 3b2b238

Browse files
billyvgandrewshie-sentry
authored andcommitted
feat(releases): Add separate y-axis for release bubbles (#91813)
By default, release bubbles will use the default y-axis. This generally works except when the chart does not start at "y=0" as the bubbles are hard-coded to show below "y=0". With this change, the `useReleaseBubbles` hook returns a yaxis object that the hook consumer can use to pass to echarts -- you will also need to configure `yaxisIndex` to tell the release bubbles series which yaxis to use. This works by adding a new yaxis to decouple release bubbles series from the default yaxis. This new yaxis is configured to be hidden. This also works if you ignore the returned y-axis object and don't specify a `yaxisIndex` since it will use the default yaxis (assuming it starts at 0). Unblocks #90247
1 parent fbc9c71 commit 3b2b238

File tree

3 files changed

+118
-76
lines changed

3 files changed

+118
-76
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,19 @@ export default storyBook('TimeSeriesWidgetVisualization', (story, APIReference)
938938
/>
939939
</MediumWidget>
940940
</SideBySide>
941+
<SideBySide>
942+
<MediumWidget>
943+
<TimeSeriesWidgetVisualization
944+
plottables={[
945+
new Line(sampleThroughputTimeSeries),
946+
new Line(sampleDurationTimeSeries),
947+
new Line(sampleDurationTimeSeriesP50),
948+
]}
949+
releases={releases}
950+
showReleaseAs="bubble"
951+
/>
952+
</MediumWidget>
953+
</SideBySide>
941954
</Fragment>
942955
);
943956
});

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

Lines changed: 82 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -133,82 +133,6 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
133133
organization.features.includes('release-bubbles-ui') &&
134134
props.showReleaseAs === 'bubble';
135135

136-
// find min/max timestamp of *all* timeSeries
137-
const allBoundaries = props.plottables
138-
.flatMap(plottable => [plottable.start, plottable.end])
139-
.toSorted();
140-
const earliestTimeStamp = allBoundaries.at(0);
141-
const latestTimeStamp = allBoundaries.at(-1);
142-
143-
const {
144-
connectReleaseBubbleChartRef,
145-
releaseBubbleSeries,
146-
releaseBubbleXAxis,
147-
releaseBubbleGrid,
148-
} = useReleaseBubbles({
149-
chartId: props.id,
150-
minTime: earliestTimeStamp ? new Date(earliestTimeStamp).getTime() : undefined,
151-
maxTime: latestTimeStamp ? new Date(latestTimeStamp).getTime() : undefined,
152-
releases: hasReleaseBubbles
153-
? props.releases?.map(({timestamp, version}) => ({date: timestamp, version}))
154-
: [],
155-
});
156-
157-
const releaseSeries = props.releases
158-
? hasReleaseBubbles
159-
? releaseBubbleSeries
160-
: ReleaseSeries(
161-
theme,
162-
props.releases,
163-
function onReleaseClick(release: Release) {
164-
if (organization.features.includes('release-bubbles-ui')) {
165-
navigate(
166-
makeReleaseDrawerPathname({
167-
location,
168-
release: release.version,
169-
source: 'time-series-widget',
170-
})
171-
);
172-
return;
173-
}
174-
navigate(
175-
makeReleasesPathname({
176-
organization,
177-
path: `/${encodeURIComponent(release.version)}/`,
178-
})
179-
);
180-
},
181-
utc ?? false
182-
)
183-
: null;
184-
185-
const hasReleaseBubblesSeries = hasReleaseBubbles && releaseSeries;
186-
187-
const handleChartRef = useCallback(
188-
(e: ReactEchartsRef | null) => {
189-
if (!e?.getEchartsInstance) {
190-
return;
191-
}
192-
193-
for (const plottable of props.plottables) {
194-
plottable.handleChartRef?.(e);
195-
}
196-
197-
const echartsInstance = e.getEchartsInstance();
198-
registerWithWidgetSyncContext(echartsInstance);
199-
200-
if (hasReleaseBubblesSeries) {
201-
connectReleaseBubbleChartRef(e);
202-
}
203-
},
204-
[
205-
hasReleaseBubblesSeries,
206-
connectReleaseBubbleChartRef,
207-
registerWithWidgetSyncContext,
208-
props.plottables,
209-
]
210-
);
211-
212136
const {onDataZoom, ...chartZoomProps} = useChartZoom({
213137
saveOnZoom: true,
214138
});
@@ -431,6 +355,88 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
431355

432356
const yAxes: YAXisComponentOption[] = [leftYAxis, rightYAxis].filter(axis => !!axis);
433357

358+
// find min/max timestamp of *all* timeSeries
359+
const allBoundaries = props.plottables
360+
.flatMap(plottable => [plottable.start, plottable.end])
361+
.toSorted();
362+
const earliestTimeStamp = allBoundaries.at(0);
363+
const latestTimeStamp = allBoundaries.at(-1);
364+
365+
const {
366+
connectReleaseBubbleChartRef,
367+
releaseBubbleSeries,
368+
releaseBubbleXAxis,
369+
releaseBubbleGrid,
370+
releaseBubbleYAxis,
371+
} = useReleaseBubbles({
372+
chartId: props.id,
373+
minTime: earliestTimeStamp ? new Date(earliestTimeStamp).getTime() : undefined,
374+
maxTime: latestTimeStamp ? new Date(latestTimeStamp).getTime() : undefined,
375+
releases: hasReleaseBubbles
376+
? props.releases?.map(({timestamp, version}) => ({date: timestamp, version}))
377+
: [],
378+
yAxisIndex: yAxes.length,
379+
});
380+
381+
if (releaseBubbleYAxis) {
382+
yAxes.push(releaseBubbleYAxis);
383+
}
384+
385+
const releaseSeries = props.releases
386+
? hasReleaseBubbles
387+
? releaseBubbleSeries
388+
: ReleaseSeries(
389+
theme,
390+
props.releases,
391+
function onReleaseClick(release: Release) {
392+
if (organization.features.includes('release-bubbles-ui')) {
393+
navigate(
394+
makeReleaseDrawerPathname({
395+
location,
396+
release: release.version,
397+
source: 'time-series-widget',
398+
})
399+
);
400+
return;
401+
}
402+
navigate(
403+
makeReleasesPathname({
404+
organization,
405+
path: `/${encodeURIComponent(release.version)}/`,
406+
})
407+
);
408+
},
409+
utc ?? false
410+
)
411+
: null;
412+
413+
const hasReleaseBubblesSeries = hasReleaseBubbles && releaseSeries;
414+
415+
const handleChartRef = useCallback(
416+
(e: ReactEchartsRef | null) => {
417+
if (!e?.getEchartsInstance) {
418+
return;
419+
}
420+
421+
for (const plottable of props.plottables) {
422+
plottable.handleChartRef?.(e);
423+
}
424+
425+
const echartsInstance = e.getEchartsInstance();
426+
registerWithWidgetSyncContext(echartsInstance);
427+
428+
if (hasReleaseBubblesSeries) {
429+
connectReleaseBubbleChartRef(e);
430+
}
431+
},
432+
[
433+
hasReleaseBubblesSeries,
434+
connectReleaseBubbleChartRef,
435+
registerWithWidgetSyncContext,
436+
props.plottables,
437+
]
438+
);
439+
434440
const showXAxisProp = props.showXAxis ?? 'auto';
435441
const showXAxis = showXAxisProp === 'auto';
436442

static/app/views/releases/releaseBubbles/useReleaseBubbles.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ interface ReleaseBubbleSeriesProps {
6969
};
7070
releases: ReleaseMetaBasic[];
7171
theme: Theme;
72+
yAxisIndex?: number;
7273
}
7374

7475
/**
@@ -82,6 +83,7 @@ function ReleaseBubbleSeries({
8283
bubblePadding,
8384
dateFormatOptions,
8485
alignInMiddle,
86+
yAxisIndex,
8587
}: ReleaseBubbleSeriesProps): CustomSeriesOption | null {
8688
const totalReleases = buckets.reduce((acc, {releases}) => acc + releases.length, 0);
8789
const avgReleases = totalReleases / buckets.length;
@@ -190,6 +192,7 @@ function ReleaseBubbleSeries({
190192
return {
191193
id: BUBBLE_SERIES_ID,
192194
type: 'custom',
195+
yAxisIndex,
193196
renderItem: renderReleaseBubble,
194197
name: t('Releases'),
195198
data,
@@ -286,6 +289,10 @@ interface UseReleaseBubblesParams {
286289
* List of releases that will be grouped
287290
*/
288291
releases?: ReleaseMetaBasic[];
292+
/**
293+
* The index of the y-axis to use for the release bubbles
294+
*/
295+
yAxisIndex?: number;
289296
}
290297

291298
export function useReleaseBubbles({
@@ -297,6 +304,7 @@ export function useReleaseBubbles({
297304
environments,
298305
projects,
299306
legendSelected,
307+
yAxisIndex,
300308
alignInMiddle = false,
301309
bubbleSize = 4,
302310
bubblePadding = 2,
@@ -342,6 +350,17 @@ export function useReleaseBubbles({
342350
}),
343351
[bubbleSize, totalBubblePaddingY]
344352
);
353+
354+
const releaseBubbleYAxis = useMemo(
355+
() => ({
356+
type: 'value' as const,
357+
min: 0,
358+
max: 100,
359+
show: false,
360+
}),
361+
[]
362+
);
363+
345364
const releaseBubbleGrid = useMemo(
346365
() => ({
347366
// Moves bottom of grid "up" `bubbleSize` pixels so that bubbles are
@@ -600,6 +619,7 @@ export function useReleaseBubbles({
600619
ReleaseBubbleSeries: null,
601620
releaseBubbleXAxis: {},
602621
releaseBubbleGrid: {},
622+
releaseBubbleYAxis: null,
603623
};
604624
}
605625

@@ -610,6 +630,7 @@ export function useReleaseBubbles({
610630
* Series to append to a chart's existing `series`
611631
*/
612632
releaseBubbleSeries: ReleaseBubbleSeries({
633+
yAxisIndex,
613634
alignInMiddle,
614635
buckets,
615636
bubbleSize,
@@ -622,6 +643,8 @@ export function useReleaseBubbles({
622643
},
623644
}),
624645

646+
releaseBubbleYAxis,
647+
625648
/**
626649
* ECharts xAxis configuration. Spread/override charts `xAxis` prop.
627650
*

0 commit comments

Comments
 (0)