Skip to content

Commit 09310b6

Browse files
DominikB2014claude
andauthored
feat(chartcuterie): Support multiple display types in Explore chart rendering (#112610)
Refactors the Explore chartcuterie rendering to use the plottable infrastructure (`Line`, `Area`, `Bars` classes and their `toSeries()` methods) instead of hardcoding `LineSeries`. This ensures Slack unfurl charts match the frontend rendering for line, bar, and area display types. `ExploreChartData` now accepts an optional `type` field (`DisplayType`), defaulting to `LINE` for backward compatibility. The Python unfurl code can pass `"type": "bar"` or `"type": "area"` to render different chart types. Also extracts `createPlottableFromTimeSeries(displayType, timeSeries, config?)` as a reusable function that takes a `DisplayType` directly, and renames the `Widget`-dependent variant to `createPlottableFromTimeSeriesAndWidget`. Refs DAIN-1481 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b440f9b commit 09310b6

File tree

4 files changed

+59
-37
lines changed

4 files changed

+59
-37
lines changed

static/app/chartcuterie/explore.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import type {Theme} from '@emotion/react';
22

33
import {XAxis} from 'sentry/components/charts/components/xAxis';
4-
import {LineSeries} from 'sentry/components/charts/series/lineSeries';
5-
import {timeSeriesItemToEChartsDataPoint} from 'sentry/utils/timeSeries/timeSeriesItemToEChartsDataPoint';
4+
import {DisplayType} from 'sentry/views/dashboards/types';
65
import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types';
76
import {formatTimeSeriesLabel} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel';
7+
import type {ContinuousTimeSeriesPlottingOptions} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/continuousTimeSeries';
8+
import {createPlottableFromTimeSeries} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/createPlottableFromTimeSeries';
89

910
import {DEFAULT_FONT_FAMILY, makeSlackChartDefaults, slackChartSize} from './slack';
1011
import type {RenderDescriptor} from './types';
1112
import {ChartType} from './types';
1213

1314
type ExploreChartData = {
1415
timeSeries: TimeSeries[];
16+
type?: DisplayType;
1517
};
1618

1719
export const makeExploreCharts = (theme: Theme): Array<RenderDescriptor<ChartType>> => {
@@ -28,7 +30,7 @@ export const makeExploreCharts = (theme: Theme): Array<RenderDescriptor<ChartTyp
2830
exploreCharts.push({
2931
key: ChartType.SLACK_EXPLORE_LINE,
3032
getOption: (data: ExploreChartData) => {
31-
const {timeSeries} = data;
33+
const {timeSeries, type: displayType = DisplayType.LINE} = data;
3234

3335
if (timeSeries.length === 0) {
3436
return {
@@ -44,19 +46,21 @@ export const makeExploreCharts = (theme: Theme): Array<RenderDescriptor<ChartTyp
4446
if (!hasGroups) {
4547
const ts = timeSeries[0]!;
4648
const color = theme.chart.getColorPalette(0);
47-
const lineSeries = LineSeries({
48-
name: ts.yAxis,
49-
data: ts.values.map(timeSeriesItemToEChartsDataPoint),
50-
lineStyle: {color: color?.[0], opacity: 1},
51-
itemStyle: {color: color?.[0]},
49+
const plottingOptions: ContinuousTimeSeriesPlottingOptions = {
50+
color: color?.[0] ?? '',
51+
unit: ts.meta.valueUnit,
52+
yAxisPosition: 'left',
53+
};
54+
const plottable = createPlottableFromTimeSeries(displayType, ts, {
55+
color: color?.[0],
5256
});
5357

5458
return {
5559
...slackChartDefaults,
5660
xAxis: exploreXAxis,
5761
useUTC: true,
5862
color,
59-
series: [lineSeries],
63+
series: plottable?.toSeries(plottingOptions) ?? [],
6064
};
6165
}
6266

@@ -71,13 +75,17 @@ export const makeExploreCharts = (theme: Theme): Array<RenderDescriptor<ChartTyp
7175
color.push(theme.tokens.content.secondary);
7276
}
7377

74-
const series = sorted.map((ts, i) => {
75-
return LineSeries({
78+
const series = sorted.flatMap((ts, i) => {
79+
const plottingOptions: ContinuousTimeSeriesPlottingOptions = {
80+
color: color?.[i] ?? '',
81+
unit: ts.meta.valueUnit,
82+
yAxisPosition: 'left',
83+
};
84+
const plottable = createPlottableFromTimeSeries(displayType, ts, {
7685
name: formatTimeSeriesLabel(ts),
77-
data: ts.values.map(timeSeriesItemToEChartsDataPoint),
78-
lineStyle: {color: color?.[i], opacity: 1},
79-
itemStyle: {color: color?.[i]},
86+
color: color?.[i],
8087
});
88+
return plottable?.toSeries(plottingOptions) ?? [];
8189
});
8290

8391
return {

static/app/views/dashboards/widgetCard/visualizationWidget.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import type {
4242
TimeSeriesGroupBy,
4343
} from 'sentry/views/dashboards/widgets/common/types';
4444
import {formatBreakdownLegendValue} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatBreakdownLegendValue';
45-
import {createPlottableFromTimeSeries} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/createPlottableFromTimeSeries';
45+
import {createPlottableFromTimeSeriesAndWidget} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/createPlottableFromTimeSeries';
4646
import type {Plottable} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/plottable';
4747
import {Thresholds} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/thresholds';
4848
import {TimeSeriesWidgetVisualization} from 'sentry/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization';
@@ -232,7 +232,7 @@ function VisualizationWidgetContent({
232232
}
233233

234234
const {timeSeries, label, seriesName, widgetQuery} = transformed;
235-
const plottable = createPlottableFromTimeSeries(
235+
const plottable = createPlottableFromTimeSeriesAndWidget(
236236
timeSeries,
237237
widget,
238238
label,

static/app/views/dashboards/widgets/timeSeriesWidget/plottables/createPlottableFromTimeSeries.spec.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {Area} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/
55
import {Bars} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/bars';
66
import {Line} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/line';
77

8-
import {createPlottableFromTimeSeries} from './createPlottableFromTimeSeries';
8+
import {createPlottableFromTimeSeriesAndWidget} from './createPlottableFromTimeSeries';
99

10-
describe('createPlottableFromTimeSeries', () => {
10+
describe('createPlottableFromTimeSeriesAndWidget', () => {
1111
const mockTimeSeries = {
1212
yAxis: 'count()',
1313
values: [
@@ -23,28 +23,28 @@ describe('createPlottableFromTimeSeries', () => {
2323

2424
it('creates Line instance for LINE display type', () => {
2525
const widget = WidgetFixture({displayType: DisplayType.LINE});
26-
const plottable = createPlottableFromTimeSeries(mockTimeSeries, widget);
26+
const plottable = createPlottableFromTimeSeriesAndWidget(mockTimeSeries, widget);
2727

2828
expect(plottable).toBeInstanceOf(Line);
2929
});
3030

3131
it('creates Area instance for AREA display type', () => {
3232
const widget = WidgetFixture({displayType: DisplayType.AREA});
33-
const plottable = createPlottableFromTimeSeries(mockTimeSeries, widget);
33+
const plottable = createPlottableFromTimeSeriesAndWidget(mockTimeSeries, widget);
3434

3535
expect(plottable).toBeInstanceOf(Area);
3636
});
3737

3838
it('creates Bars instance for BAR display type', () => {
3939
const widget = WidgetFixture({displayType: DisplayType.BAR});
40-
const plottable = createPlottableFromTimeSeries(mockTimeSeries, widget);
40+
const plottable = createPlottableFromTimeSeriesAndWidget(mockTimeSeries, widget);
4141

4242
expect(plottable).toBeInstanceOf(Bars);
4343
});
4444

4545
it('returns null for TABLE display type', () => {
4646
const widget = WidgetFixture({displayType: DisplayType.TABLE});
47-
const plottable = createPlottableFromTimeSeries(mockTimeSeries, widget);
47+
const plottable = createPlottableFromTimeSeriesAndWidget(mockTimeSeries, widget);
4848

4949
expect(plottable).toBeNull();
5050
});

static/app/views/dashboards/widgets/timeSeriesWidget/plottables/createPlottableFromTimeSeries.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,43 @@ import {Bars} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/
66
import {Line} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/line';
77
import type {Plottable} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/plottable';
88

9+
type PlottableConfig = {
10+
alias?: string;
11+
color?: string;
12+
name?: string;
13+
stack?: string;
14+
};
15+
916
export function createPlottableFromTimeSeries(
17+
displayType: DisplayType,
1018
timeSeries: TimeSeries,
11-
widget: Widget,
12-
alias?: string,
13-
name?: string,
14-
color?: string
19+
config?: PlottableConfig
1520
): Plottable | null {
16-
const shouldStack = widget.queries[0]?.columns.length! > 0;
17-
18-
const {displayType, title} = widget;
1921
switch (displayType) {
2022
case DisplayType.LINE:
21-
return new Line(timeSeries, {alias, name, color});
23+
return new Line(timeSeries, config);
2224
case DisplayType.AREA:
23-
return new Area(timeSeries, {alias, name, color});
25+
return new Area(timeSeries, config);
2426
case DisplayType.BAR:
25-
return new Bars(timeSeries, {
26-
stack: shouldStack ? title : undefined,
27-
alias,
28-
name,
29-
color,
30-
});
27+
return new Bars(timeSeries, config);
3128
default:
3229
return null;
3330
}
3431
}
32+
33+
export function createPlottableFromTimeSeriesAndWidget(
34+
timeSeries: TimeSeries,
35+
widget: Widget,
36+
alias?: string,
37+
name?: string,
38+
color?: string
39+
): Plottable | null {
40+
const shouldStack = widget.queries[0]?.columns.length! > 0;
41+
42+
return createPlottableFromTimeSeries(widget.displayType, timeSeries, {
43+
alias,
44+
name,
45+
color,
46+
stack: shouldStack ? widget.title : undefined,
47+
});
48+
}

0 commit comments

Comments
 (0)