Merge "ui: Display histogram in explore page." into main
diff --git a/ui/src/frontend/sql_table_tab.ts b/ui/src/frontend/sql_table_tab.ts
index 40f2012..b803148 100644
--- a/ui/src/frontend/sql_table_tab.ts
+++ b/ui/src/frontend/sql_table_tab.ts
@@ -28,6 +28,12 @@
 import {MenuItem, PopupMenu2} from '../widgets/menu';
 import {addEphemeralTab} from '../common/add_ephemeral_tab';
 import {Tab} from '../public/tab';
+import {addChartTab} from './widgets/charts/chart_tab';
+import {
+  ChartOption,
+  createChartConfigFromSqlTableState,
+} from './widgets/charts/chart';
+import {AddChartMenuItem} from './widgets/charts/add_chart_menu';
 
 export interface AddSqlTableTabParams {
   table: SqlTableDescription;
@@ -122,6 +128,16 @@
       },
       m(SqlTable, {
         state: this.state,
+        addColumnMenuItems: (column, columnAlias) =>
+          m(AddChartMenuItem, {
+            chartConfig: createChartConfigFromSqlTableState(
+              column,
+              columnAlias,
+              this.state,
+            ),
+            chartOptions: [ChartOption.HISTOGRAM],
+            addChart: (chart) => addChartTab(chart),
+          }),
       }),
     );
   }
diff --git a/ui/src/frontend/widgets/charts/add_chart_menu.ts b/ui/src/frontend/widgets/charts/add_chart_menu.ts
index 384ed9a..cb3bb17 100644
--- a/ui/src/frontend/widgets/charts/add_chart_menu.ts
+++ b/ui/src/frontend/widgets/charts/add_chart_menu.ts
@@ -15,12 +15,12 @@
 import m from 'mithril';
 import {MenuItem} from '../../../widgets/menu';
 import {Icons} from '../../../base/semantic_icons';
-import {ChartConfig, ChartOption, toTitleCase} from './chart';
+import {Chart, ChartConfig, ChartOption, toTitleCase} from './chart';
 
 interface AddChartMenuItemAttrs {
   readonly chartConfig: ChartConfig;
   readonly chartOptions: Array<ChartOption>;
-  readonly addChart: (option: ChartOption, config: ChartConfig) => void;
+  readonly addChart: (chart: Chart) => void;
 }
 
 export class AddChartMenuItem
@@ -29,12 +29,12 @@
   private renderAddChartOptions(
     config: ChartConfig,
     chartOptions: Array<ChartOption>,
-    addChart: (option: ChartOption, config: ChartConfig) => void,
+    addChart: (chart: Chart) => void,
   ): m.Children {
     return chartOptions.map((option) => {
       return m(MenuItem, {
         label: toTitleCase(option),
-        onclick: () => addChart(option, config),
+        onclick: () => addChart({option, config}),
       });
     });
   }
diff --git a/ui/src/frontend/widgets/charts/chart.ts b/ui/src/frontend/widgets/charts/chart.ts
index ccbf566..124a52e 100644
--- a/ui/src/frontend/widgets/charts/chart.ts
+++ b/ui/src/frontend/widgets/charts/chart.ts
@@ -16,6 +16,8 @@
 import {Engine} from '../../../trace_processor/engine';
 import {Filter, TableColumn, TableColumnSet} from '../sql/table/column';
 import {Histogram} from './histogram/histogram';
+import {SqlTableState} from '../sql/table/state';
+import {columnTitle} from '../sql/table/table';
 
 export interface VegaLiteChartSpec {
   $schema: string;
@@ -58,6 +60,11 @@
   readonly aggregationType?: 'nominal' | 'quantitative'; // Aggregation type.
 }
 
+export interface Chart {
+  readonly option: ChartOption;
+  readonly config: ChartConfig;
+}
+
 export interface ChartData {
   readonly rows: Row[];
   readonly error?: string;
@@ -85,11 +92,29 @@
 
 // renderChartComponent will take a chart option and config and map
 // to the corresponding chart class component.
-export function renderChartComponent(option: ChartOption, config: ChartConfig) {
-  switch (option) {
+export function renderChartComponent(chart: Chart) {
+  switch (chart.option) {
     case ChartOption.HISTOGRAM:
-      return m(Histogram, config);
+      return m(Histogram, chart.config);
     default:
       return;
   }
 }
+
+export function createChartConfigFromSqlTableState(
+  column: TableColumn,
+  columnAlias: string,
+  sqlTableState: SqlTableState,
+) {
+  return {
+    engine: sqlTableState.trace.engine,
+    columnTitle: columnTitle(column),
+    sqlColumn: [columnAlias],
+    filters: sqlTableState?.getFilters(),
+    tableDisplay: sqlTableState.config.displayName ?? sqlTableState.config.name,
+    query: sqlTableState.getSqlQuery(
+      Object.fromEntries([[columnAlias, column.primaryColumn()]]),
+    ),
+    aggregationType: column.aggregation?.().dataType,
+  };
+}
diff --git a/ui/src/frontend/widgets/charts/chart_tab.ts b/ui/src/frontend/widgets/charts/chart_tab.ts
index 0dcc6d9..6d802e6 100644
--- a/ui/src/frontend/widgets/charts/chart_tab.ts
+++ b/ui/src/frontend/widgets/charts/chart_tab.ts
@@ -17,25 +17,14 @@
 import {filterTitle} from '../sql/table/column';
 import {addEphemeralTab} from '../../../common/add_ephemeral_tab';
 import {Tab} from '../../../public/tab';
-import {
-  ChartConfig,
-  ChartOption,
-  renderChartComponent,
-  toTitleCase,
-} from './chart';
+import {Chart, renderChartComponent, toTitleCase} from './chart';
 
-export function addChartTab(
-  chartOption: ChartOption,
-  chartConfig: ChartConfig,
-): void {
-  addEphemeralTab('histogramTab', new ChartTab(chartOption, chartConfig));
+export function addChartTab(chart: Chart): void {
+  addEphemeralTab('histogramTab', new ChartTab(chart));
 }
 
 export class ChartTab implements Tab {
-  constructor(
-    private readonly chartOption: ChartOption,
-    private readonly chartConfig: ChartConfig,
-  ) {}
+  constructor(private readonly chart: Chart) {}
 
   render() {
     return m(
@@ -44,20 +33,20 @@
         title: this.getTitle(),
         description: this.getDescription(),
       },
-      renderChartComponent(this.chartOption, this.chartConfig),
+      renderChartComponent(this.chart),
     );
   }
 
   getTitle(): string {
-    return `${toTitleCase(this.chartConfig.columnTitle)} Histogram`;
+    return `${toTitleCase(this.chart.config.columnTitle)} Histogram`;
   }
 
   private getDescription(): string {
-    let desc = `Count distribution for ${this.chartConfig.tableDisplay ?? ''} table`;
+    let desc = `Count distribution for ${this.chart.config.tableDisplay ?? ''} table`;
 
-    if (this.chartConfig.filters && this.chartConfig.filters.length > 0) {
+    if (this.chart.config.filters && this.chart.config.filters.length > 0) {
       desc += ' where ';
-      desc += this.chartConfig.filters.map((f) => filterTitle(f)).join(', ');
+      desc += this.chart.config.filters.map((f) => filterTitle(f)).join(', ');
     }
 
     return desc;
diff --git a/ui/src/frontend/widgets/sql/table/table.ts b/ui/src/frontend/widgets/sql/table/table.ts
index 28cc81c..761a32c 100644
--- a/ui/src/frontend/widgets/sql/table/table.ts
+++ b/ui/src/frontend/widgets/sql/table/table.ts
@@ -40,16 +40,20 @@
 import {SqlTableState} from './state';
 import {SqlTableDescription} from './table_description';
 import {Intent} from '../../../../widgets/common';
-import {addChartTab} from '../../charts/chart_tab';
 import {Form} from '../../../../widgets/form';
 import {TextInput} from '../../../../widgets/text_input';
-import {AddChartMenuItem} from '../../charts/add_chart_menu';
-import {ChartConfig, ChartOption} from '../../charts/chart';
 
 export interface SqlTableConfig {
   readonly state: SqlTableState;
+  // For additional menu items to add to the column header menus
+  readonly addColumnMenuItems?: (
+    column: TableColumn,
+    columnAlias: string,
+  ) => m.Children;
 }
 
+type AdditionalColumnMenuItems = Record<string, m.Children>;
+
 function renderCell(
   column: TableColumn,
   row: Row,
@@ -276,7 +280,11 @@
     );
   }
 
-  renderColumnHeader(column: TableColumn, index: number) {
+  renderColumnHeader(
+    column: TableColumn,
+    index: number,
+    additionalColumnHeaderMenuItems?: m.Children,
+  ) {
     const sorted = this.state.isSortedBy(column);
     const icon =
       sorted === 'ASC'
@@ -285,22 +293,6 @@
           ? Icons.SortedDesc
           : Icons.ContextMenu;
 
-    const columnAlias =
-      this.state.getCurrentRequest().columns[
-        sqlColumnId(column.primaryColumn())
-      ];
-    const chartConfig: ChartConfig = {
-      engine: this.state.trace.engine,
-      columnTitle: columnTitle(column),
-      sqlColumn: [columnAlias],
-      filters: this.state.getFilters(),
-      tableDisplay: this.table.displayName ?? this.table.name,
-      query: this.state.getSqlQuery(
-        Object.fromEntries([[columnAlias, column.primaryColumn()]]),
-      ),
-      aggregationType: column.aggregation?.().dataType,
-    };
-
     return m(
       PopupMenu2,
       {
@@ -345,11 +337,7 @@
         {label: 'Add filter', icon: Icons.Filter},
         this.renderColumnFilterOptions(column),
       ),
-      m(AddChartMenuItem, {
-        chartConfig,
-        chartOptions: [ChartOption.HISTOGRAM],
-        addChart: (option, config) => addChartTab(option, config),
-      }),
+      additionalColumnHeaderMenuItems,
       // Menu items before divider apply to selected column
       m(MenuDivider),
       // Menu items after divider apply to entire table
@@ -357,13 +345,49 @@
     );
   }
 
-  view() {
+  getAdditionalColumnMenuItems(
+    addColumnMenuItems?: (
+      column: TableColumn,
+      columnAlias: string,
+    ) => m.Children,
+  ) {
+    if (addColumnMenuItems === undefined) return;
+
+    const additionalColumnMenuItems: AdditionalColumnMenuItems = {};
+    this.state.getSelectedColumns().forEach((column) => {
+      const columnAlias =
+        this.state.getCurrentRequest().columns[
+          sqlColumnId(column.primaryColumn())
+        ];
+
+      additionalColumnMenuItems[columnAlias] = addColumnMenuItems(
+        column,
+        columnAlias,
+      );
+    });
+
+    return additionalColumnMenuItems;
+  }
+
+  view({attrs}: m.Vnode<SqlTableConfig>) {
     const rows = this.state.getDisplayedRows();
+    const additionalColumnMenuItems = this.getAdditionalColumnMenuItems(
+      attrs.addColumnMenuItems,
+    );
 
     const columns = this.state.getSelectedColumns();
     const columnDescriptors = columns.map((column, i) => {
       return {
-        title: this.renderColumnHeader(column, i),
+        title: this.renderColumnHeader(
+          column,
+          i,
+          additionalColumnMenuItems &&
+            additionalColumnMenuItems[
+              this.state.getCurrentRequest().columns[
+                sqlColumnId(column.primaryColumn())
+              ]
+            ],
+        ),
         render: (row: Row) => renderCell(column, row, this.state),
       };
     });
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
index 5b1d0d4..1c60d4f 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
@@ -33,8 +33,15 @@
 import {Button} from '../../widgets/button';
 import {Icons} from '../../base/semantic_icons';
 import {DetailsShell} from '../../widgets/details_shell';
+import {
+  Chart,
+  ChartOption,
+  createChartConfigFromSqlTableState,
+  renderChartComponent,
+} from '../../frontend/widgets/charts/chart';
+import {AddChartMenuItem} from '../../frontend/widgets/charts/add_chart_menu';
 
-interface ExplorePageState {
+interface ExploreTableState {
   sqlTableState?: SqlTableState;
   selectedTable?: ExplorableTable;
 }
@@ -46,13 +53,12 @@
 }
 
 export class ExplorePage implements m.ClassComponent<PageWithTraceAttrs> {
-  private readonly state: ExplorePageState;
+  private readonly state: ExploreTableState;
+  private readonly charts: Chart[];
 
   constructor() {
-    this.state = {
-      sqlTableState: undefined,
-      selectedTable: undefined,
-    };
+    this.charts = [];
+    this.state = {};
   }
 
   // Show menu with standard library tables
@@ -115,7 +121,7 @@
 
           this.state.selectedTable = table;
 
-          const sqlTableState = new SqlTableState(
+          this.state.sqlTableState = new SqlTableState(
             trace,
             {
               name: table.name,
@@ -123,7 +129,6 @@
             },
             {imports: [table.module]},
           );
-          this.state.sqlTableState = sqlTableState;
         },
       });
     });
@@ -162,6 +167,16 @@
       },
       m(SqlTable, {
         state: sqlTableState,
+        addColumnMenuItems: (column, columnAlias) =>
+          m(AddChartMenuItem, {
+            chartConfig: createChartConfigFromSqlTableState(
+              column,
+              columnAlias,
+              sqlTableState,
+            ),
+            chartOptions: [ChartOption.HISTOGRAM],
+            addChart: (chart) => this.charts.push(chart),
+          }),
       }),
     );
   }
@@ -170,6 +185,7 @@
     return m(
       '.explore-page',
       m(Menu, this.renderSelectableTablesMenuItems(attrs.trace)),
+      this.charts.map((chart) => renderChartComponent(chart)),
       this.state.selectedTable && this.renderSqlTable(),
     );
   }