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.
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard.ts b/ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard.ts
index db15876..f0e91d4 100644
--- a/ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard.ts
+++ b/ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard.ts
@@ -43,10 +43,14 @@
 } from './dashboard_chart_view';
 import {ResizeHandle} from '../../../widgets/resize_handle';
 import {Card} from '../../../widgets/card';
-import {getDefaultChartLabel} from '../query_builder/nodes/visualisation_node';
+import {
+  ChartType,
+  getDefaultChartLabel,
+} from '../query_builder/nodes/visualisation_node';
 import {Popup, PopupPosition} from '../../../widgets/popup';
 import {renderChartConfigPopup} from '../query_builder/charts/chart_config_popup';
 import {RoundActionButton} from '../query_builder/widgets';
+import {renderChartTypePickerGrid} from '../query_builder/charts/chart_type_picker';
 
 // Default dimensions for dashboard chart cards (in pixels).
 const DEFAULT_CHART_WIDTH = 400;
@@ -205,26 +209,26 @@
     return m(
       '.pf-dashboard__add-button',
       m(
-        PopupMenu,
+        Popup,
         {
           trigger: RoundActionButton({
             icon: 'add',
             title: 'Add item',
           }),
+          fitContent: true,
         },
-        m(MenuItem, {
-          label: 'Chart',
-          icon: 'bar_chart',
-          disabled: lastSource === undefined,
-          onclick: () => {
-            if (lastSource !== undefined) {
-              this.addChartForSource(attrs, lastSource);
-            }
-          },
-        }),
+        lastSource !== undefined
+          ? renderChartTypePickerGrid((chartType: ChartType) => {
+              this.addChartForSource(attrs, lastSource, chartType);
+            })
+          : m(
+              '.pf-chart-type-picker__empty',
+              'Add a data source first to create charts',
+            ),
         m(MenuItem, {
           label: 'Label',
           icon: 'text_fields',
+          className: 'pf-dismiss-popup-group',
           onclick: () => {
             this.addLabel(attrs);
           },
@@ -232,6 +236,7 @@
         m(MenuItem, {
           label: 'Segment Divider',
           icon: 'horizontal_rule',
+          className: 'pf-dismiss-popup-group',
           onclick: () => {
             this.addDivider(attrs);
           },
@@ -1171,24 +1176,31 @@
             ),
           ),
         ),
-      m(Button, {
-        label: 'Add Chart',
-        icon: 'bar_chart',
-        compact: true,
-        className: 'pf-dashboard__add-chart-btn',
-        onclick: () => {
-          this.addChartForSource(attrs, source);
+      m(
+        Popup,
+        {
+          trigger: m(Button, {
+            label: 'Add Chart',
+            icon: 'bar_chart',
+            compact: true,
+            className: 'pf-dashboard__add-chart-btn',
+          }),
+          fitContent: true,
         },
-      }),
+        renderChartTypePickerGrid((chartType: ChartType) => {
+          this.addChartForSource(attrs, source, chartType);
+        }),
+      ),
     ];
   }
 
   private addChartForSource(
     attrs: DashboardAttrs,
     source: DashboardDataSource,
+    chartType: ChartType,
   ): void {
     const items = [...attrs.items];
-    const newConfig = createDefaultChartConfig(source.columns);
+    const newConfig = createDefaultChartConfig(source.columns, chartType);
     const candidate = getNextItemPosition(items);
     const pos = findNonOverlappingPosition(
       candidate.x,
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard_chart_view.ts b/ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard_chart_view.ts
index 7bf2558..18b363c 100644
--- a/ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard_chart_view.ts
+++ b/ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard_chart_view.ts
@@ -441,11 +441,12 @@
  */
 export function createDefaultChartConfig(
   columns: ReadonlyArray<{name: string}>,
+  chartType: ChartType = 'bar',
 ): ChartConfig {
   const column = columns.length > 0 ? columns[0].name : '';
   return {
     id: generateChartId(),
     column,
-    chartType: 'bar',
+    chartType,
   };
 }
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/charts/chart_type_picker.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/charts/chart_type_picker.ts
new file mode 100644
index 0000000..b2dfda9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/charts/chart_type_picker.ts
@@ -0,0 +1,269 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {ChartType} from '../nodes/visualisation_node';
+import {CHART_TYPES, ChartTypeDefinition} from '../nodes/chart_type_registry';
+
+// SVG preview renderers for each chart type. Each returns a small schematic
+// SVG that gives users a visual sense of what the chart looks like.
+const CHART_PREVIEW_RENDERERS: Record<ChartType, () => m.Children> = {
+  bar: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48', fill: 'currentColor'},
+      m('rect', {x: 6, y: 28, width: 10, height: 16, rx: 1, opacity: 0.5}),
+      m('rect', {x: 20, y: 12, width: 10, height: 32, rx: 1, opacity: 0.7}),
+      m('rect', {x: 34, y: 20, width: 10, height: 24, rx: 1, opacity: 0.85}),
+      m('rect', {x: 48, y: 6, width: 10, height: 38, rx: 1}),
+    ),
+
+  histogram: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48', fill: 'currentColor'},
+      m('rect', {x: 4, y: 30, width: 8, height: 14, opacity: 0.4}),
+      m('rect', {x: 12, y: 22, width: 8, height: 22, opacity: 0.55}),
+      m('rect', {x: 20, y: 10, width: 8, height: 34, opacity: 0.75}),
+      m('rect', {x: 28, y: 6, width: 8, height: 38, opacity: 0.9}),
+      m('rect', {x: 36, y: 14, width: 8, height: 30, opacity: 0.7}),
+      m('rect', {x: 44, y: 26, width: 8, height: 18, opacity: 0.5}),
+      m('rect', {x: 52, y: 34, width: 8, height: 10, opacity: 0.35}),
+    ),
+
+  line: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48', fill: 'none', stroke: 'currentColor'},
+      m('polyline', {
+        'points': '6,38 16,28 26,32 36,16 46,20 58,8',
+        'stroke-width': '2.5',
+        'stroke-linecap': 'round',
+        'stroke-linejoin': 'round',
+      }),
+      m('circle', {
+        cx: 6,
+        cy: 38,
+        r: 2.5,
+        fill: 'currentColor',
+        stroke: 'none',
+      }),
+      m('circle', {
+        cx: 26,
+        cy: 32,
+        r: 2.5,
+        fill: 'currentColor',
+        stroke: 'none',
+      }),
+      m('circle', {
+        cx: 46,
+        cy: 20,
+        r: 2.5,
+        fill: 'currentColor',
+        stroke: 'none',
+      }),
+      m('circle', {
+        cx: 58,
+        cy: 8,
+        r: 2.5,
+        fill: 'currentColor',
+        stroke: 'none',
+      }),
+    ),
+
+  scatter: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48', fill: 'currentColor'},
+      m('circle', {cx: 10, cy: 34, r: 3.5, opacity: 0.7}),
+      m('circle', {cx: 18, cy: 26, r: 2.5, opacity: 0.6}),
+      m('circle', {cx: 28, cy: 18, r: 4, opacity: 0.8}),
+      m('circle', {cx: 36, cy: 30, r: 3, opacity: 0.65}),
+      m('circle', {cx: 44, cy: 12, r: 3.5, opacity: 0.75}),
+      m('circle', {cx: 52, cy: 22, r: 2.5, opacity: 0.85}),
+      m('circle', {cx: 22, cy: 38, r: 2, opacity: 0.5}),
+      m('circle', {cx: 48, cy: 36, r: 2, opacity: 0.55}),
+    ),
+
+  pie: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48'},
+      // Larger slice (~60%)
+      m('path', {
+        d: 'M32,24 L32,8 A16,16 0 1,1 18.1,36.1 Z',
+        fill: 'currentColor',
+        opacity: 0.8,
+      }),
+      // Smaller slice (~40%)
+      m('path', {
+        d: 'M32,24 L18.1,36.1 A16,16 0 0,1 32,8 Z',
+        fill: 'currentColor',
+        opacity: 0.45,
+      }),
+    ),
+
+  treemap: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48', fill: 'currentColor'},
+      m('rect', {x: 4, y: 4, width: 34, height: 24, rx: 2, opacity: 0.8}),
+      m('rect', {x: 40, y: 4, width: 20, height: 24, rx: 2, opacity: 0.55}),
+      m('rect', {x: 4, y: 30, width: 20, height: 14, rx: 2, opacity: 0.65}),
+      m('rect', {x: 26, y: 30, width: 16, height: 14, rx: 2, opacity: 0.4}),
+      m('rect', {x: 44, y: 30, width: 16, height: 14, rx: 2, opacity: 0.5}),
+    ),
+
+  boxplot: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48', stroke: 'currentColor', fill: 'currentColor'},
+      // Whisker lines
+      m('line', {'x1': 20, 'y1': 6, 'x2': 20, 'y2': 14, 'stroke-width': 1.5}),
+      m('line', {'x1': 20, 'y1': 34, 'x2': 20, 'y2': 42, 'stroke-width': 1.5}),
+      // Caps
+      m('line', {'x1': 15, 'y1': 6, 'x2': 25, 'y2': 6, 'stroke-width': 1.5}),
+      m('line', {'x1': 15, 'y1': 42, 'x2': 25, 'y2': 42, 'stroke-width': 1.5}),
+      // Box
+      m('rect', {
+        'x': 13,
+        'y': 14,
+        'width': 14,
+        'height': 20,
+        'rx': 1,
+        'fill': 'none',
+        'stroke-width': 1.5,
+      }),
+      // Median line
+      m('line', {'x1': 13, 'y1': 22, 'x2': 27, 'y2': 22, 'stroke-width': 2}),
+      // Second boxplot (shorter)
+      m('line', {'x1': 44, 'y1': 12, 'x2': 44, 'y2': 18, 'stroke-width': 1.5}),
+      m('line', {'x1': 44, 'y1': 36, 'x2': 44, 'y2': 42, 'stroke-width': 1.5}),
+      m('line', {'x1': 39, 'y1': 12, 'x2': 49, 'y2': 12, 'stroke-width': 1.5}),
+      m('line', {'x1': 39, 'y1': 42, 'x2': 49, 'y2': 42, 'stroke-width': 1.5}),
+      m('rect', {
+        'x': 37,
+        'y': 18,
+        'width': 14,
+        'height': 18,
+        'rx': 1,
+        'fill': 'none',
+        'stroke-width': 1.5,
+      }),
+      m('line', {'x1': 37, 'y1': 28, 'x2': 51, 'y2': 28, 'stroke-width': 2}),
+    ),
+
+  heatmap: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48', fill: 'currentColor'},
+      // 4x3 grid of cells with varying opacity
+      m('rect', {x: 4, y: 4, width: 13, height: 12, rx: 1, opacity: 0.9}),
+      m('rect', {x: 19, y: 4, width: 13, height: 12, rx: 1, opacity: 0.4}),
+      m('rect', {x: 34, y: 4, width: 13, height: 12, rx: 1, opacity: 0.7}),
+      m('rect', {x: 49, y: 4, width: 13, height: 12, rx: 1, opacity: 0.3}),
+      m('rect', {x: 4, y: 18, width: 13, height: 12, rx: 1, opacity: 0.5}),
+      m('rect', {x: 19, y: 18, width: 13, height: 12, rx: 1, opacity: 0.8}),
+      m('rect', {x: 34, y: 18, width: 13, height: 12, rx: 1, opacity: 0.35}),
+      m('rect', {x: 49, y: 18, width: 13, height: 12, rx: 1, opacity: 0.65}),
+      m('rect', {x: 4, y: 32, width: 13, height: 12, rx: 1, opacity: 0.25}),
+      m('rect', {x: 19, y: 32, width: 13, height: 12, rx: 1, opacity: 0.6}),
+      m('rect', {x: 34, y: 32, width: 13, height: 12, rx: 1, opacity: 0.85}),
+      m('rect', {x: 49, y: 32, width: 13, height: 12, rx: 1, opacity: 0.45}),
+    ),
+
+  cdf: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48', fill: 'none', stroke: 'currentColor'},
+      m('polyline', {
+        'points': '6,42 14,40 22,36 28,28 34,18 40,12 48,9 56,8',
+        'stroke-width': '2.5',
+        'stroke-linecap': 'round',
+        'stroke-linejoin': 'round',
+      }),
+      // Dashed 50% line
+      m('line', {
+        'x1': 4,
+        'y1': 24,
+        'x2': 60,
+        'y2': 24,
+        'stroke-width': 1,
+        'stroke-dasharray': '3,3',
+        'opacity': 0.4,
+      }),
+    ),
+
+  scorecard: () =>
+    m(
+      'svg',
+      {viewBox: '0 0 64 48', fill: 'currentColor'},
+      m(
+        'text',
+        {
+          'x': 32,
+          'y': 28,
+          'text-anchor': 'middle',
+          'dominant-baseline': 'central',
+          'font-size': '18',
+          'font-weight': 'bold',
+        },
+        '42',
+      ),
+      m(
+        'text',
+        {
+          'x': 32,
+          'y': 41,
+          'text-anchor': 'middle',
+          'font-size': '7',
+          'opacity': 0.5,
+        },
+        'value',
+      ),
+    ),
+};
+
+/**
+ * Renders a grid of chart type cards. Each card fires `onSelect` when clicked
+ * and has the `pf-dismiss-popup-group` class so it auto-closes a parent Popup.
+ */
+export function renderChartTypePickerGrid(
+  onSelect: (type: ChartType) => void,
+): m.Children {
+  return m(
+    '.pf-chart-type-picker',
+    CHART_TYPES.map((def) => renderChartTypeCard(def, onSelect)),
+  );
+}
+
+function renderChartTypeCard(
+  def: ChartTypeDefinition,
+  onSelect: (type: ChartType) => void,
+): m.Children {
+  const description = def.description;
+
+  return m(
+    'button.pf-chart-type-picker__card.pf-dismiss-popup-group',
+    {
+      key: def.type,
+      title: description,
+      onclick: () => onSelect(def.type),
+    },
+    [
+      m('.pf-chart-type-picker__preview', CHART_PREVIEW_RENDERERS[def.type]()),
+      m('.pf-chart-type-picker__label', def.label),
+    ],
+  );
+}
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/charts/chart_view.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/charts/chart_view.ts
index 6f7375b..bfce456 100644
--- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/charts/chart_view.ts
+++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/charts/chart_view.ts
@@ -37,6 +37,7 @@
 import {EmptyState} from '../../../../widgets/empty_state';
 import {Card} from '../../../../widgets/card';
 import {AddItemPlaceholder} from '../widgets';
+import {renderChartTypePickerGrid} from './chart_type_picker';
 
 export interface ChartViewAttrs {
   trace: Trace;
@@ -175,14 +176,20 @@
             title: 'No charts configured',
             fillHeight: true,
           },
-          m(Button, {
-            label: 'Add first chart',
-            icon: 'add',
-            onclick: () => {
-              attrs.node.addChart();
-              attrs.onFilterChange?.();
+          m(
+            Popup,
+            {
+              trigger: m(Button, {
+                label: 'Add first chart',
+                icon: 'add',
+              }),
+              fitContent: true,
             },
-          }),
+            renderChartTypePickerGrid((chartType) => {
+              attrs.node.addChart(chartType);
+              attrs.onFilterChange?.();
+            }),
+          ),
         ),
       );
     }
@@ -210,15 +217,25 @@
       toolbar,
       m(`.pf-chart-view__charts.${gridClass}`, [
         ...configs.map((config) => this.renderSingleChart(attrs, config)),
-        m(AddItemPlaceholder, {
-          key: 'add-chart',
-          label: 'Add Chart',
-          icon: 'add',
-          onclick: () => {
-            attrs.node.addChart();
-            attrs.onFilterChange?.();
+        m(
+          Popup,
+          {
+            key: 'add-chart',
+            trigger: m(
+              'span',
+              m(AddItemPlaceholder, {
+                label: 'Add Chart',
+                icon: 'add',
+              }),
+            ),
+            showArrow: false,
+            fitContent: true,
           },
-        }),
+          renderChartTypePickerGrid((chartType) => {
+            attrs.node.addChart(chartType);
+            attrs.onFilterChange?.();
+          }),
+        ),
       ]),
     ]);
   }
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/chart_type_registry.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/chart_type_registry.ts
index b48880d..4cf26b8 100644
--- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/chart_type_registry.ts
+++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/chart_type_registry.ts
@@ -78,6 +78,9 @@
    * Example: Scatter plot bubble size.
    */
   readonly supportsSizeColumn: boolean;
+
+  /** Short description shown on hover in the chart type picker. */
+  readonly description: string;
 }
 
 /**
@@ -98,6 +101,7 @@
     supportsYColumn: false,
     supportsGroupColumn: false,
     supportsSizeColumn: false,
+    description: 'Compare categories using vertical or horizontal bars',
   },
   {
     type: 'histogram',
@@ -110,6 +114,7 @@
     supportsYColumn: false,
     supportsGroupColumn: false,
     supportsSizeColumn: false,
+    description: 'Show distribution of numeric values across bins',
   },
   {
     type: 'line',
@@ -122,6 +127,7 @@
     supportsYColumn: true,
     supportsGroupColumn: true,
     supportsSizeColumn: false,
+    description: 'Plot trends with connected data points over a numeric axis',
   },
   {
     type: 'scatter',
@@ -134,6 +140,7 @@
     supportsYColumn: true,
     supportsGroupColumn: true,
     supportsSizeColumn: true,
+    description: 'Reveal correlations between two numeric variables',
   },
   {
     type: 'pie',
@@ -146,6 +153,7 @@
     supportsYColumn: false,
     supportsGroupColumn: false,
     supportsSizeColumn: false,
+    description: 'Show proportions of a whole as slices',
   },
   {
     type: 'treemap',
@@ -158,6 +166,7 @@
     supportsYColumn: false,
     supportsGroupColumn: true,
     supportsSizeColumn: false,
+    description: 'Display hierarchical data as nested rectangles by size',
   },
   {
     type: 'boxplot',
@@ -170,6 +179,7 @@
     supportsYColumn: true,
     supportsGroupColumn: false,
     supportsSizeColumn: false,
+    description: 'Summarize data spread with quartiles and outliers',
   },
   {
     type: 'heatmap',
@@ -182,6 +192,7 @@
     supportsYColumn: true,
     supportsGroupColumn: false,
     supportsSizeColumn: false,
+    description: 'Visualize magnitude across two dimensions using color',
   },
   {
     type: 'cdf',
@@ -194,6 +205,8 @@
     supportsYColumn: false,
     supportsGroupColumn: true,
     supportsSizeColumn: false,
+    description:
+      'Cumulative distribution — proportion of values below a threshold',
   },
   {
     type: 'scorecard',
@@ -206,6 +219,7 @@
     supportsYColumn: false,
     supportsGroupColumn: false,
     supportsSizeColumn: false,
+    description: 'Display a single aggregated number prominently',
   },
 ] as const;
 
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/visualisation_node.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/visualisation_node.ts
index e928908..f108cde 100644
--- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/visualisation_node.ts
+++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/visualisation_node.ts
@@ -43,6 +43,8 @@
 import {Icon} from '../../../../widgets/icon';
 import {Card} from '../../../../widgets/card';
 import {classNames} from '../../../../base/classnames';
+import {renderChartTypePickerGrid} from '../charts/chart_type_picker';
+import {Popup} from '../../../../widgets/popup';
 
 /**
  * Chart type options.
@@ -488,14 +490,23 @@
       title: 'Charts',
       content: m('.pf-chart-cards-container', [
         ...chartCards,
-        m(AddItemPlaceholder, {
-          key: 'add-chart',
-          label: 'Add Chart',
-          icon: 'add',
-          onclick: () => {
-            this.addChart();
+        m(
+          Popup,
+          {
+            key: 'add-chart',
+            trigger: m(
+              'span',
+              m(AddItemPlaceholder, {
+                label: 'Add Chart',
+                icon: 'add',
+              }),
+            ),
+            fitContent: true,
           },
-        }),
+          renderChartTypePickerGrid((chartType: ChartType) => {
+            this.addChart(chartType);
+          }),
+        ),
       ]),
     });
 
@@ -526,7 +537,7 @@
    * Prefers string/categorical columns for bar charts, and avoids
    * columns already used by other charts when possible.
    */
-  addChart(): void {
+  addChart(chartType: ChartType = 'bar'): void {
     // Get columns already used by existing charts
     const usedColumns = new Set(
       this.state.chartConfigs.map((c) => c.column).filter(Boolean),
@@ -534,13 +545,13 @@
 
     // Find a good default column:
     // 1. Prefer unused columns
-    // 2. Prefer non-numeric columns (better for bar charts)
+    // 2. Prefer non-numeric columns (better for aggregation chart types)
     // 3. Fall back to first available column
     const availableCols = this.sourceCols;
     const unusedCols = availableCols.filter((c) => !usedColumns.has(c.name));
     const colsToCheck = unusedCols.length > 0 ? unusedCols : availableCols;
 
-    // Prefer string/categorical columns for bar charts
+    // Prefer string/categorical columns for aggregation charts
     const stringCol = colsToCheck.find(
       (c) => c.column.type === undefined || !isQuantitativeType(c.column.type),
     );
@@ -549,7 +560,7 @@
     const newChart: ChartConfig = {
       id: generateChartId(),
       column: defaultColumn,
-      chartType: 'bar',
+      chartType,
     };
     this.state.chartConfigs.push(newChart);
     this.state.onchange?.();
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/widgets.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/widgets.ts
index 269afa4..2094724 100644
--- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/widgets.ts
+++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/widgets.ts
@@ -754,7 +754,7 @@
 export interface AddItemPlaceholderAttrs {
   label: string;
   icon?: string;
-  onclick: () => void;
+  onclick?: () => void;
 }
 
 export class AddItemPlaceholder
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/styles.scss b/ui/src/plugins/dev.perfetto.DataExplorer/styles.scss
index d4b10f2..88904f5 100644
--- a/ui/src/plugins/dev.perfetto.DataExplorer/styles.scss
+++ b/ui/src/plugins/dev.perfetto.DataExplorer/styles.scss
@@ -608,3 +608,74 @@
     padding: 16px 8px;
   }
 }
+
+// Chart type picker — visual grid of chart types shown when adding a chart.
+.pf-chart-type-picker {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 6px;
+  padding: 4px;
+
+  &__card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 4px;
+    padding: 8px 6px;
+    border: 1px solid var(--pf-color-border);
+    border-radius: 6px;
+    background: var(--pf-color-surface);
+    cursor: pointer;
+    transition:
+      border-color 0.15s,
+      box-shadow 0.15s,
+      background 0.15s;
+    // Reset button defaults
+    font: inherit;
+    color: inherit;
+
+    &:hover {
+      border-color: var(--pf-color-primary);
+      background: var(--pf-color-surface-highlight);
+      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+    }
+
+    &:focus-visible {
+      outline: 2px solid var(--pf-color-primary);
+      outline-offset: 2px;
+    }
+
+    &:active {
+      transform: scale(0.97);
+    }
+  }
+
+  &__preview {
+    width: 48px;
+    height: 36px;
+    color: var(--pf-color-primary);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  &__label {
+    font-size: 11px;
+    font-weight: 500;
+    text-align: center;
+    color: var(--pf-color-text);
+    white-space: nowrap;
+  }
+
+  &__empty {
+    padding: 12px 16px;
+    font-size: 12px;
+    color: var(--pf-color-text-muted);
+    text-align: center;
+  }
+}