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(),
);
}