Skip to content

Commit d2aaedf

Browse files
authored
de: More modern charts picker (#5195)
Replace the simple "Add Chart" button/menu with a visual chart type picker grid that lets users choose the chart type upfront when adding a chart to the Data Explorer dashboard or query builder.
1 parent c9aa148 commit d2aaedf

File tree

8 files changed

+444
-49
lines changed

8 files changed

+444
-49
lines changed

ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,14 @@ import {
4343
} from './dashboard_chart_view';
4444
import {ResizeHandle} from '../../../widgets/resize_handle';
4545
import {Card} from '../../../widgets/card';
46-
import {getDefaultChartLabel} from '../query_builder/nodes/visualisation_node';
46+
import {
47+
ChartType,
48+
getDefaultChartLabel,
49+
} from '../query_builder/nodes/visualisation_node';
4750
import {Popup, PopupPosition} from '../../../widgets/popup';
4851
import {renderChartConfigPopup} from '../query_builder/charts/chart_config_popup';
4952
import {RoundActionButton} from '../query_builder/widgets';
53+
import {renderChartTypePickerGrid} from '../query_builder/charts/chart_type_picker';
5054

5155
// Default dimensions for dashboard chart cards (in pixels).
5256
const DEFAULT_CHART_WIDTH = 400;
@@ -205,33 +209,34 @@ export class Dashboard implements m.ClassComponent<DashboardAttrs> {
205209
return m(
206210
'.pf-dashboard__add-button',
207211
m(
208-
PopupMenu,
212+
Popup,
209213
{
210214
trigger: RoundActionButton({
211215
icon: 'add',
212216
title: 'Add item',
213217
}),
218+
fitContent: true,
214219
},
215-
m(MenuItem, {
216-
label: 'Chart',
217-
icon: 'bar_chart',
218-
disabled: lastSource === undefined,
219-
onclick: () => {
220-
if (lastSource !== undefined) {
221-
this.addChartForSource(attrs, lastSource);
222-
}
223-
},
224-
}),
220+
lastSource !== undefined
221+
? renderChartTypePickerGrid((chartType: ChartType) => {
222+
this.addChartForSource(attrs, lastSource, chartType);
223+
})
224+
: m(
225+
'.pf-chart-type-picker__empty',
226+
'Add a data source first to create charts',
227+
),
225228
m(MenuItem, {
226229
label: 'Label',
227230
icon: 'text_fields',
231+
className: 'pf-dismiss-popup-group',
228232
onclick: () => {
229233
this.addLabel(attrs);
230234
},
231235
}),
232236
m(MenuItem, {
233237
label: 'Segment Divider',
234238
icon: 'horizontal_rule',
239+
className: 'pf-dismiss-popup-group',
235240
onclick: () => {
236241
this.addDivider(attrs);
237242
},
@@ -1171,24 +1176,31 @@ export class Dashboard implements m.ClassComponent<DashboardAttrs> {
11711176
),
11721177
),
11731178
),
1174-
m(Button, {
1175-
label: 'Add Chart',
1176-
icon: 'bar_chart',
1177-
compact: true,
1178-
className: 'pf-dashboard__add-chart-btn',
1179-
onclick: () => {
1180-
this.addChartForSource(attrs, source);
1179+
m(
1180+
Popup,
1181+
{
1182+
trigger: m(Button, {
1183+
label: 'Add Chart',
1184+
icon: 'bar_chart',
1185+
compact: true,
1186+
className: 'pf-dashboard__add-chart-btn',
1187+
}),
1188+
fitContent: true,
11811189
},
1182-
}),
1190+
renderChartTypePickerGrid((chartType: ChartType) => {
1191+
this.addChartForSource(attrs, source, chartType);
1192+
}),
1193+
),
11831194
];
11841195
}
11851196

11861197
private addChartForSource(
11871198
attrs: DashboardAttrs,
11881199
source: DashboardDataSource,
1200+
chartType: ChartType,
11891201
): void {
11901202
const items = [...attrs.items];
1191-
const newConfig = createDefaultChartConfig(source.columns);
1203+
const newConfig = createDefaultChartConfig(source.columns, chartType);
11921204
const candidate = getNextItemPosition(items);
11931205
const pos = findNonOverlappingPosition(
11941206
candidate.x,

ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard_chart_view.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,12 @@ function buildWhereClause(
441441
*/
442442
export function createDefaultChartConfig(
443443
columns: ReadonlyArray<{name: string}>,
444+
chartType: ChartType = 'bar',
444445
): ChartConfig {
445446
const column = columns.length > 0 ? columns[0].name : '';
446447
return {
447448
id: generateChartId(),
448449
column,
449-
chartType: 'bar',
450+
chartType,
450451
};
451452
}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// Copyright (C) 2026 The Android Open Source Project
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import m from 'mithril';
16+
import {ChartType} from '../nodes/visualisation_node';
17+
import {CHART_TYPES, ChartTypeDefinition} from '../nodes/chart_type_registry';
18+
19+
// SVG preview renderers for each chart type. Each returns a small schematic
20+
// SVG that gives users a visual sense of what the chart looks like.
21+
const CHART_PREVIEW_RENDERERS: Record<ChartType, () => m.Children> = {
22+
bar: () =>
23+
m(
24+
'svg',
25+
{viewBox: '0 0 64 48', fill: 'currentColor'},
26+
m('rect', {x: 6, y: 28, width: 10, height: 16, rx: 1, opacity: 0.5}),
27+
m('rect', {x: 20, y: 12, width: 10, height: 32, rx: 1, opacity: 0.7}),
28+
m('rect', {x: 34, y: 20, width: 10, height: 24, rx: 1, opacity: 0.85}),
29+
m('rect', {x: 48, y: 6, width: 10, height: 38, rx: 1}),
30+
),
31+
32+
histogram: () =>
33+
m(
34+
'svg',
35+
{viewBox: '0 0 64 48', fill: 'currentColor'},
36+
m('rect', {x: 4, y: 30, width: 8, height: 14, opacity: 0.4}),
37+
m('rect', {x: 12, y: 22, width: 8, height: 22, opacity: 0.55}),
38+
m('rect', {x: 20, y: 10, width: 8, height: 34, opacity: 0.75}),
39+
m('rect', {x: 28, y: 6, width: 8, height: 38, opacity: 0.9}),
40+
m('rect', {x: 36, y: 14, width: 8, height: 30, opacity: 0.7}),
41+
m('rect', {x: 44, y: 26, width: 8, height: 18, opacity: 0.5}),
42+
m('rect', {x: 52, y: 34, width: 8, height: 10, opacity: 0.35}),
43+
),
44+
45+
line: () =>
46+
m(
47+
'svg',
48+
{viewBox: '0 0 64 48', fill: 'none', stroke: 'currentColor'},
49+
m('polyline', {
50+
'points': '6,38 16,28 26,32 36,16 46,20 58,8',
51+
'stroke-width': '2.5',
52+
'stroke-linecap': 'round',
53+
'stroke-linejoin': 'round',
54+
}),
55+
m('circle', {
56+
cx: 6,
57+
cy: 38,
58+
r: 2.5,
59+
fill: 'currentColor',
60+
stroke: 'none',
61+
}),
62+
m('circle', {
63+
cx: 26,
64+
cy: 32,
65+
r: 2.5,
66+
fill: 'currentColor',
67+
stroke: 'none',
68+
}),
69+
m('circle', {
70+
cx: 46,
71+
cy: 20,
72+
r: 2.5,
73+
fill: 'currentColor',
74+
stroke: 'none',
75+
}),
76+
m('circle', {
77+
cx: 58,
78+
cy: 8,
79+
r: 2.5,
80+
fill: 'currentColor',
81+
stroke: 'none',
82+
}),
83+
),
84+
85+
scatter: () =>
86+
m(
87+
'svg',
88+
{viewBox: '0 0 64 48', fill: 'currentColor'},
89+
m('circle', {cx: 10, cy: 34, r: 3.5, opacity: 0.7}),
90+
m('circle', {cx: 18, cy: 26, r: 2.5, opacity: 0.6}),
91+
m('circle', {cx: 28, cy: 18, r: 4, opacity: 0.8}),
92+
m('circle', {cx: 36, cy: 30, r: 3, opacity: 0.65}),
93+
m('circle', {cx: 44, cy: 12, r: 3.5, opacity: 0.75}),
94+
m('circle', {cx: 52, cy: 22, r: 2.5, opacity: 0.85}),
95+
m('circle', {cx: 22, cy: 38, r: 2, opacity: 0.5}),
96+
m('circle', {cx: 48, cy: 36, r: 2, opacity: 0.55}),
97+
),
98+
99+
pie: () =>
100+
m(
101+
'svg',
102+
{viewBox: '0 0 64 48'},
103+
// Larger slice (~60%)
104+
m('path', {
105+
d: 'M32,24 L32,8 A16,16 0 1,1 18.1,36.1 Z',
106+
fill: 'currentColor',
107+
opacity: 0.8,
108+
}),
109+
// Smaller slice (~40%)
110+
m('path', {
111+
d: 'M32,24 L18.1,36.1 A16,16 0 0,1 32,8 Z',
112+
fill: 'currentColor',
113+
opacity: 0.45,
114+
}),
115+
),
116+
117+
treemap: () =>
118+
m(
119+
'svg',
120+
{viewBox: '0 0 64 48', fill: 'currentColor'},
121+
m('rect', {x: 4, y: 4, width: 34, height: 24, rx: 2, opacity: 0.8}),
122+
m('rect', {x: 40, y: 4, width: 20, height: 24, rx: 2, opacity: 0.55}),
123+
m('rect', {x: 4, y: 30, width: 20, height: 14, rx: 2, opacity: 0.65}),
124+
m('rect', {x: 26, y: 30, width: 16, height: 14, rx: 2, opacity: 0.4}),
125+
m('rect', {x: 44, y: 30, width: 16, height: 14, rx: 2, opacity: 0.5}),
126+
),
127+
128+
boxplot: () =>
129+
m(
130+
'svg',
131+
{viewBox: '0 0 64 48', stroke: 'currentColor', fill: 'currentColor'},
132+
// Whisker lines
133+
m('line', {'x1': 20, 'y1': 6, 'x2': 20, 'y2': 14, 'stroke-width': 1.5}),
134+
m('line', {'x1': 20, 'y1': 34, 'x2': 20, 'y2': 42, 'stroke-width': 1.5}),
135+
// Caps
136+
m('line', {'x1': 15, 'y1': 6, 'x2': 25, 'y2': 6, 'stroke-width': 1.5}),
137+
m('line', {'x1': 15, 'y1': 42, 'x2': 25, 'y2': 42, 'stroke-width': 1.5}),
138+
// Box
139+
m('rect', {
140+
'x': 13,
141+
'y': 14,
142+
'width': 14,
143+
'height': 20,
144+
'rx': 1,
145+
'fill': 'none',
146+
'stroke-width': 1.5,
147+
}),
148+
// Median line
149+
m('line', {'x1': 13, 'y1': 22, 'x2': 27, 'y2': 22, 'stroke-width': 2}),
150+
// Second boxplot (shorter)
151+
m('line', {'x1': 44, 'y1': 12, 'x2': 44, 'y2': 18, 'stroke-width': 1.5}),
152+
m('line', {'x1': 44, 'y1': 36, 'x2': 44, 'y2': 42, 'stroke-width': 1.5}),
153+
m('line', {'x1': 39, 'y1': 12, 'x2': 49, 'y2': 12, 'stroke-width': 1.5}),
154+
m('line', {'x1': 39, 'y1': 42, 'x2': 49, 'y2': 42, 'stroke-width': 1.5}),
155+
m('rect', {
156+
'x': 37,
157+
'y': 18,
158+
'width': 14,
159+
'height': 18,
160+
'rx': 1,
161+
'fill': 'none',
162+
'stroke-width': 1.5,
163+
}),
164+
m('line', {'x1': 37, 'y1': 28, 'x2': 51, 'y2': 28, 'stroke-width': 2}),
165+
),
166+
167+
heatmap: () =>
168+
m(
169+
'svg',
170+
{viewBox: '0 0 64 48', fill: 'currentColor'},
171+
// 4x3 grid of cells with varying opacity
172+
m('rect', {x: 4, y: 4, width: 13, height: 12, rx: 1, opacity: 0.9}),
173+
m('rect', {x: 19, y: 4, width: 13, height: 12, rx: 1, opacity: 0.4}),
174+
m('rect', {x: 34, y: 4, width: 13, height: 12, rx: 1, opacity: 0.7}),
175+
m('rect', {x: 49, y: 4, width: 13, height: 12, rx: 1, opacity: 0.3}),
176+
m('rect', {x: 4, y: 18, width: 13, height: 12, rx: 1, opacity: 0.5}),
177+
m('rect', {x: 19, y: 18, width: 13, height: 12, rx: 1, opacity: 0.8}),
178+
m('rect', {x: 34, y: 18, width: 13, height: 12, rx: 1, opacity: 0.35}),
179+
m('rect', {x: 49, y: 18, width: 13, height: 12, rx: 1, opacity: 0.65}),
180+
m('rect', {x: 4, y: 32, width: 13, height: 12, rx: 1, opacity: 0.25}),
181+
m('rect', {x: 19, y: 32, width: 13, height: 12, rx: 1, opacity: 0.6}),
182+
m('rect', {x: 34, y: 32, width: 13, height: 12, rx: 1, opacity: 0.85}),
183+
m('rect', {x: 49, y: 32, width: 13, height: 12, rx: 1, opacity: 0.45}),
184+
),
185+
186+
cdf: () =>
187+
m(
188+
'svg',
189+
{viewBox: '0 0 64 48', fill: 'none', stroke: 'currentColor'},
190+
m('polyline', {
191+
'points': '6,42 14,40 22,36 28,28 34,18 40,12 48,9 56,8',
192+
'stroke-width': '2.5',
193+
'stroke-linecap': 'round',
194+
'stroke-linejoin': 'round',
195+
}),
196+
// Dashed 50% line
197+
m('line', {
198+
'x1': 4,
199+
'y1': 24,
200+
'x2': 60,
201+
'y2': 24,
202+
'stroke-width': 1,
203+
'stroke-dasharray': '3,3',
204+
'opacity': 0.4,
205+
}),
206+
),
207+
208+
scorecard: () =>
209+
m(
210+
'svg',
211+
{viewBox: '0 0 64 48', fill: 'currentColor'},
212+
m(
213+
'text',
214+
{
215+
'x': 32,
216+
'y': 28,
217+
'text-anchor': 'middle',
218+
'dominant-baseline': 'central',
219+
'font-size': '18',
220+
'font-weight': 'bold',
221+
},
222+
'42',
223+
),
224+
m(
225+
'text',
226+
{
227+
'x': 32,
228+
'y': 41,
229+
'text-anchor': 'middle',
230+
'font-size': '7',
231+
'opacity': 0.5,
232+
},
233+
'value',
234+
),
235+
),
236+
};
237+
238+
/**
239+
* Renders a grid of chart type cards. Each card fires `onSelect` when clicked
240+
* and has the `pf-dismiss-popup-group` class so it auto-closes a parent Popup.
241+
*/
242+
export function renderChartTypePickerGrid(
243+
onSelect: (type: ChartType) => void,
244+
): m.Children {
245+
return m(
246+
'.pf-chart-type-picker',
247+
CHART_TYPES.map((def) => renderChartTypeCard(def, onSelect)),
248+
);
249+
}
250+
251+
function renderChartTypeCard(
252+
def: ChartTypeDefinition,
253+
onSelect: (type: ChartType) => void,
254+
): m.Children {
255+
const description = def.description;
256+
257+
return m(
258+
'button.pf-chart-type-picker__card.pf-dismiss-popup-group',
259+
{
260+
key: def.type,
261+
title: description,
262+
onclick: () => onSelect(def.type),
263+
},
264+
[
265+
m('.pf-chart-type-picker__preview', CHART_PREVIEW_RENDERERS[def.type]()),
266+
m('.pf-chart-type-picker__label', def.label),
267+
],
268+
);
269+
}

0 commit comments

Comments
 (0)