ui: Created "Add chart" menu.
"Add Histogram" in the table viewer column header is now replaced with "Add chart".
This CL:
1. Adds an "Add chart" menu class component to display a list of charts options. Click handlers are passed down by the parent class.
2. Changes the "addHistogramTab" to be "addChartTab" so that any chart can be added to a tab.
3. Provides an enum for chart types and a function to convert between the enum and class component.
Change-Id: I0d8c6c9782c7a4b94ef97df045239b1728ed6317
diff --git a/ui/src/frontend/widgets/charts/add_chart_menu.ts b/ui/src/frontend/widgets/charts/add_chart_menu.ts
new file mode 100644
index 0000000..384ed9a
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/add_chart_menu.ts
@@ -0,0 +1,53 @@
+// Copyright (C) 2024 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 {MenuItem} from '../../../widgets/menu';
+import {Icons} from '../../../base/semantic_icons';
+import {ChartConfig, ChartOption, toTitleCase} from './chart';
+
+interface AddChartMenuItemAttrs {
+ readonly chartConfig: ChartConfig;
+ readonly chartOptions: Array<ChartOption>;
+ readonly addChart: (option: ChartOption, config: ChartConfig) => void;
+}
+
+export class AddChartMenuItem
+ implements m.ClassComponent<AddChartMenuItemAttrs>
+{
+ private renderAddChartOptions(
+ config: ChartConfig,
+ chartOptions: Array<ChartOption>,
+ addChart: (option: ChartOption, config: ChartConfig) => void,
+ ): m.Children {
+ return chartOptions.map((option) => {
+ return m(MenuItem, {
+ label: toTitleCase(option),
+ onclick: () => addChart(option, config),
+ });
+ });
+ }
+
+ view({attrs}: m.Vnode<AddChartMenuItemAttrs>) {
+ return m(
+ MenuItem,
+ {label: 'Add chart', icon: Icons.Chart},
+ this.renderAddChartOptions(
+ attrs.chartConfig,
+ attrs.chartOptions,
+ attrs.addChart,
+ ),
+ );
+ }
+}
diff --git a/ui/src/frontend/widgets/charts/chart.ts b/ui/src/frontend/widgets/charts/chart.ts
index 1a78c41..ccbf566 100644
--- a/ui/src/frontend/widgets/charts/chart.ts
+++ b/ui/src/frontend/widgets/charts/chart.ts
@@ -11,9 +11,11 @@
// 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 {Row} from '../../../trace_processor/query_result';
import {Engine} from '../../../trace_processor/engine';
-import {TableColumn, TableColumnSet} from '../sql/table/column';
+import {Filter, TableColumn, TableColumnSet} from '../sql/table/column';
+import {Histogram} from './histogram/histogram';
export interface VegaLiteChartSpec {
$schema: string;
@@ -41,6 +43,21 @@
};
}
+// Holds the various chart types and human readable string
+export enum ChartOption {
+ HISTOGRAM = 'histogram',
+}
+
+export interface ChartConfig {
+ readonly engine: Engine;
+ readonly columnTitle: string; // Human readable column name (ex: Duration)
+ readonly sqlColumn: string[]; // SQL column name (ex: dur)
+ readonly filters?: Filter[]; // Filters applied to SQL table
+ readonly tableDisplay?: string; // Human readable table name (ex: slices)
+ readonly query: string; // SQL query for the underlying data
+ readonly aggregationType?: 'nominal' | 'quantitative'; // Aggregation type.
+}
+
export interface ChartData {
readonly rows: Row[];
readonly error?: string;
@@ -65,3 +82,14 @@
return words.join(' ');
}
+
+// 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) {
+ case ChartOption.HISTOGRAM:
+ return m(Histogram, config);
+ default:
+ return;
+ }
+}
diff --git a/ui/src/frontend/widgets/charts/chart_tab.ts b/ui/src/frontend/widgets/charts/chart_tab.ts
new file mode 100644
index 0000000..0dcc6d9
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/chart_tab.ts
@@ -0,0 +1,65 @@
+// Copyright (C) 2024 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 {DetailsShell} from '../../../widgets/details_shell';
+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';
+
+export function addChartTab(
+ chartOption: ChartOption,
+ chartConfig: ChartConfig,
+): void {
+ addEphemeralTab('histogramTab', new ChartTab(chartOption, chartConfig));
+}
+
+export class ChartTab implements Tab {
+ constructor(
+ private readonly chartOption: ChartOption,
+ private readonly chartConfig: ChartConfig,
+ ) {}
+
+ render() {
+ return m(
+ DetailsShell,
+ {
+ title: this.getTitle(),
+ description: this.getDescription(),
+ },
+ renderChartComponent(this.chartOption, this.chartConfig),
+ );
+ }
+
+ getTitle(): string {
+ return `${toTitleCase(this.chartConfig.columnTitle)} Histogram`;
+ }
+
+ private getDescription(): string {
+ let desc = `Count distribution for ${this.chartConfig.tableDisplay ?? ''} table`;
+
+ if (this.chartConfig.filters && this.chartConfig.filters.length > 0) {
+ desc += ' where ';
+ desc += this.chartConfig.filters.map((f) => filterTitle(f)).join(', ');
+ }
+
+ return desc;
+ }
+}
diff --git a/ui/src/frontend/widgets/charts/histogram/histogram.ts b/ui/src/frontend/widgets/charts/histogram/histogram.ts
index e4dd0d0..38f4a65 100644
--- a/ui/src/frontend/widgets/charts/histogram/histogram.ts
+++ b/ui/src/frontend/widgets/charts/histogram/histogram.ts
@@ -15,25 +15,14 @@
import m from 'mithril';
import {stringifyJsonWithBigints} from '../../../../base/json_utils';
import {VegaView} from '../../../../widgets/vega_view';
-import {Filter} from '../../../widgets/sql/table/column';
import {HistogramState} from './state';
import {Spinner} from '../../../../widgets/spinner';
-import {Engine} from '../../../../trace_processor/engine';
+import {ChartConfig} from '../chart';
-export interface HistogramConfig {
- engine: Engine;
- columnTitle: string; // Human readable column name (ex: Duration)
- sqlColumn: string[]; // SQL column name (ex: dur)
- filters?: Filter[]; // Filters applied to SQL table
- tableDisplay?: string; // Human readable table name (ex: slices)
- query: string; // SQL query for the underlying data
- aggregationType?: 'nominal' | 'quantitative'; // Aggregation type.
-}
-
-export class Histogram implements m.ClassComponent<HistogramConfig> {
+export class Histogram implements m.ClassComponent<ChartConfig> {
private readonly state: HistogramState;
- constructor({attrs}: m.Vnode<HistogramConfig>) {
+ constructor({attrs}: m.Vnode<ChartConfig>) {
this.state = new HistogramState(
attrs.engine,
attrs.query,
diff --git a/ui/src/frontend/widgets/charts/histogram/tab.ts b/ui/src/frontend/widgets/charts/histogram/tab.ts
deleted file mode 100644
index 096245e..0000000
--- a/ui/src/frontend/widgets/charts/histogram/tab.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) 2024 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 {DetailsShell} from '../../../../widgets/details_shell';
-import {filterTitle} from '../../../widgets/sql/table/column';
-import {addEphemeralTab} from '../../../../common/add_ephemeral_tab';
-import {Tab} from '../../../../public/tab';
-import {Histogram, HistogramConfig} from './histogram';
-import {toTitleCase} from '../chart';
-
-export function addHistogramTab(config: HistogramConfig): void {
- addEphemeralTab('histogramTab', new HistogramTab(config));
-}
-
-export class HistogramTab implements Tab {
- constructor(private readonly config: HistogramConfig) {}
-
- render() {
- return m(
- DetailsShell,
- {
- title: this.getTitle(),
- description: this.getDescription(),
- },
- m(Histogram, this.config),
- );
- }
-
- getTitle(): string {
- return `${toTitleCase(this.config.columnTitle)} Histogram`;
- }
-
- private getDescription(): string {
- let desc = `Count distribution for ${this.config.tableDisplay ?? ''} table`;
-
- if (this.config.filters && this.config.filters.length > 0) {
- desc += ' where ';
- desc += this.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 acd5ffe..28cc81c 100644
--- a/ui/src/frontend/widgets/sql/table/table.ts
+++ b/ui/src/frontend/widgets/sql/table/table.ts
@@ -40,9 +40,11 @@
import {SqlTableState} from './state';
import {SqlTableDescription} from './table_description';
import {Intent} from '../../../../widgets/common';
-import {addHistogramTab} from '../../charts/histogram/tab';
+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;
@@ -65,7 +67,7 @@
return column.renderCell(sqlValue, getTableManager(state), additionalValues);
}
-function columnTitle(column: TableColumn): string {
+export function columnTitle(column: TableColumn): string {
if (column.getTitle !== undefined) {
const title = column.getTitle();
if (title !== undefined) return title;
@@ -283,6 +285,22 @@
? 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,
{
@@ -327,26 +345,10 @@
{label: 'Add filter', icon: Icons.Filter},
this.renderColumnFilterOptions(column),
),
- m(MenuItem, {
- label: 'Create histogram',
- icon: Icons.Chart,
- onclick: () => {
- const columnAlias =
- this.state.getCurrentRequest().columns[
- sqlColumnId(column.primaryColumn())
- ];
- addHistogramTab({
- engine: this.state.trace.engine,
- sqlColumn: [columnAlias],
- columnTitle: columnTitle(column),
- filters: this.state.getFilters(),
- tableDisplay: this.table.displayName ?? this.table.name,
- query: this.state.getSqlQuery(
- Object.fromEntries([[columnAlias, column.primaryColumn()]]),
- ),
- aggregationType: column.aggregation?.().dataType,
- });
- },
+ m(AddChartMenuItem, {
+ chartConfig,
+ chartOptions: [ChartOption.HISTOGRAM],
+ addChart: (option, config) => addChartTab(option, config),
}),
// Menu items before divider apply to selected column
m(MenuDivider),