xref: /aosp_15_r20/external/perfetto/ui/src/frontend/pivot_table.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2022 The Android Open Source Project
2*6dbdd20aSAndroid Build Coastguard Worker//
3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License.
5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*6dbdd20aSAndroid Build Coastguard Worker//
7*6dbdd20aSAndroid Build Coastguard Worker//      http://www.apache.org/licenses/LICENSE-2.0
8*6dbdd20aSAndroid Build Coastguard Worker//
9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License.
14*6dbdd20aSAndroid Build Coastguard Worker
15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril';
16*6dbdd20aSAndroid Build Coastguard Workerimport {SortDirection} from '../base/comparison_utils';
17*6dbdd20aSAndroid Build Coastguard Workerimport {sqliteString} from '../base/string_utils';
18*6dbdd20aSAndroid Build Coastguard Workerimport {DropDirection} from '../core/pivot_table_manager';
19*6dbdd20aSAndroid Build Coastguard Workerimport {
20*6dbdd20aSAndroid Build Coastguard Worker  PivotTableResult,
21*6dbdd20aSAndroid Build Coastguard Worker  Aggregation,
22*6dbdd20aSAndroid Build Coastguard Worker  AggregationFunction,
23*6dbdd20aSAndroid Build Coastguard Worker  columnKey,
24*6dbdd20aSAndroid Build Coastguard Worker  PivotTree,
25*6dbdd20aSAndroid Build Coastguard Worker  TableColumn,
26*6dbdd20aSAndroid Build Coastguard Worker  COUNT_AGGREGATION,
27*6dbdd20aSAndroid Build Coastguard Worker} from '../core/pivot_table_types';
28*6dbdd20aSAndroid Build Coastguard Workerimport {AreaSelection} from '../public/selection';
29*6dbdd20aSAndroid Build Coastguard Workerimport {raf} from '../core/raf_scheduler';
30*6dbdd20aSAndroid Build Coastguard Workerimport {ColumnType} from '../trace_processor/query_result';
31*6dbdd20aSAndroid Build Coastguard Workerimport {
32*6dbdd20aSAndroid Build Coastguard Worker  aggregationIndex,
33*6dbdd20aSAndroid Build Coastguard Worker  areaFilters,
34*6dbdd20aSAndroid Build Coastguard Worker  sliceAggregationColumns,
35*6dbdd20aSAndroid Build Coastguard Worker  tables,
36*6dbdd20aSAndroid Build Coastguard Worker} from '../core/pivot_table_query_generator';
37*6dbdd20aSAndroid Build Coastguard Workerimport {ReorderableCell, ReorderableCellGroup} from './reorderable_cells';
38*6dbdd20aSAndroid Build Coastguard Workerimport {AttributeModalHolder} from './tables/attribute_modal_holder';
39*6dbdd20aSAndroid Build Coastguard Workerimport {DurationWidget} from '../components/widgets/duration';
40*6dbdd20aSAndroid Build Coastguard Workerimport {getSqlTableDescription} from '../components/widgets/sql/table/sql_table_registry';
41*6dbdd20aSAndroid Build Coastguard Workerimport {assertExists, assertFalse} from '../base/logging';
42*6dbdd20aSAndroid Build Coastguard Workerimport {Filter, SqlColumn} from '../components/widgets/sql/table/column';
43*6dbdd20aSAndroid Build Coastguard Workerimport {argSqlColumn} from '../components/widgets/sql/table/well_known_columns';
44*6dbdd20aSAndroid Build Coastguard Workerimport {TraceImpl} from '../core/trace_impl';
45*6dbdd20aSAndroid Build Coastguard Workerimport {PivotTableManager} from '../core/pivot_table_manager';
46*6dbdd20aSAndroid Build Coastguard Workerimport {extensions} from '../components/extensions';
47*6dbdd20aSAndroid Build Coastguard Workerimport {MenuItem, PopupMenu2} from '../widgets/menu';
48*6dbdd20aSAndroid Build Coastguard Workerimport {Button} from '../widgets/button';
49*6dbdd20aSAndroid Build Coastguard Workerimport {popupMenuIcon} from '../widgets/table';
50*6dbdd20aSAndroid Build Coastguard Worker
51*6dbdd20aSAndroid Build Coastguard Workerinterface PathItem {
52*6dbdd20aSAndroid Build Coastguard Worker  tree: PivotTree;
53*6dbdd20aSAndroid Build Coastguard Worker  nextKey: ColumnType;
54*6dbdd20aSAndroid Build Coastguard Worker}
55*6dbdd20aSAndroid Build Coastguard Worker
56*6dbdd20aSAndroid Build Coastguard Workerinterface PivotTableAttrs {
57*6dbdd20aSAndroid Build Coastguard Worker  trace: TraceImpl;
58*6dbdd20aSAndroid Build Coastguard Worker  selectionArea: AreaSelection;
59*6dbdd20aSAndroid Build Coastguard Worker}
60*6dbdd20aSAndroid Build Coastguard Worker
61*6dbdd20aSAndroid Build Coastguard Workerinterface DrillFilter {
62*6dbdd20aSAndroid Build Coastguard Worker  column: TableColumn;
63*6dbdd20aSAndroid Build Coastguard Worker  value: ColumnType;
64*6dbdd20aSAndroid Build Coastguard Worker}
65*6dbdd20aSAndroid Build Coastguard Worker
66*6dbdd20aSAndroid Build Coastguard Workerfunction drillFilterColumnName(column: TableColumn): SqlColumn {
67*6dbdd20aSAndroid Build Coastguard Worker  switch (column.kind) {
68*6dbdd20aSAndroid Build Coastguard Worker    case 'argument':
69*6dbdd20aSAndroid Build Coastguard Worker      return argSqlColumn('arg_set_id', column.argument);
70*6dbdd20aSAndroid Build Coastguard Worker    case 'regular':
71*6dbdd20aSAndroid Build Coastguard Worker      return `${column.column}`;
72*6dbdd20aSAndroid Build Coastguard Worker  }
73*6dbdd20aSAndroid Build Coastguard Worker}
74*6dbdd20aSAndroid Build Coastguard Worker
75*6dbdd20aSAndroid Build Coastguard Worker// Convert DrillFilter to SQL condition to be used in WHERE clause.
76*6dbdd20aSAndroid Build Coastguard Workerfunction renderDrillFilter(filter: DrillFilter): Filter {
77*6dbdd20aSAndroid Build Coastguard Worker  const column = drillFilterColumnName(filter.column);
78*6dbdd20aSAndroid Build Coastguard Worker  const value = filter.value;
79*6dbdd20aSAndroid Build Coastguard Worker  if (value === null) {
80*6dbdd20aSAndroid Build Coastguard Worker    return {op: (cols) => `${cols[0]} IS NULL`, columns: [column]};
81*6dbdd20aSAndroid Build Coastguard Worker  } else if (typeof value === 'number' || typeof value === 'bigint') {
82*6dbdd20aSAndroid Build Coastguard Worker    return {op: (cols) => `${cols[0]} = ${filter.value}`, columns: [column]};
83*6dbdd20aSAndroid Build Coastguard Worker  } else if (value instanceof Uint8Array) {
84*6dbdd20aSAndroid Build Coastguard Worker    throw new Error(`BLOB as DrillFilter not implemented`);
85*6dbdd20aSAndroid Build Coastguard Worker  }
86*6dbdd20aSAndroid Build Coastguard Worker  return {
87*6dbdd20aSAndroid Build Coastguard Worker    op: (cols) => `${cols[0]} = ${sqliteString(value)}`,
88*6dbdd20aSAndroid Build Coastguard Worker    columns: [column],
89*6dbdd20aSAndroid Build Coastguard Worker  };
90*6dbdd20aSAndroid Build Coastguard Worker}
91*6dbdd20aSAndroid Build Coastguard Worker
92*6dbdd20aSAndroid Build Coastguard Workerfunction readableColumnName(column: TableColumn) {
93*6dbdd20aSAndroid Build Coastguard Worker  switch (column.kind) {
94*6dbdd20aSAndroid Build Coastguard Worker    case 'argument':
95*6dbdd20aSAndroid Build Coastguard Worker      return `Argument ${column.argument}`;
96*6dbdd20aSAndroid Build Coastguard Worker    case 'regular':
97*6dbdd20aSAndroid Build Coastguard Worker      return `${column.column}`;
98*6dbdd20aSAndroid Build Coastguard Worker  }
99*6dbdd20aSAndroid Build Coastguard Worker}
100*6dbdd20aSAndroid Build Coastguard Worker
101*6dbdd20aSAndroid Build Coastguard Workerexport function markFirst(index: number) {
102*6dbdd20aSAndroid Build Coastguard Worker  if (index === 0) {
103*6dbdd20aSAndroid Build Coastguard Worker    return '.first';
104*6dbdd20aSAndroid Build Coastguard Worker  }
105*6dbdd20aSAndroid Build Coastguard Worker  return '';
106*6dbdd20aSAndroid Build Coastguard Worker}
107*6dbdd20aSAndroid Build Coastguard Worker
108*6dbdd20aSAndroid Build Coastguard Workerexport class PivotTable implements m.ClassComponent<PivotTableAttrs> {
109*6dbdd20aSAndroid Build Coastguard Worker  private pivotMgr: PivotTableManager;
110*6dbdd20aSAndroid Build Coastguard Worker
111*6dbdd20aSAndroid Build Coastguard Worker  constructor({attrs}: m.CVnode<PivotTableAttrs>) {
112*6dbdd20aSAndroid Build Coastguard Worker    this.pivotMgr = attrs.trace.pivotTable;
113*6dbdd20aSAndroid Build Coastguard Worker    this.attributeModalHolder = new AttributeModalHolder((arg) =>
114*6dbdd20aSAndroid Build Coastguard Worker      this.pivotMgr.setPivotSelected({
115*6dbdd20aSAndroid Build Coastguard Worker        column: {kind: 'argument', argument: arg},
116*6dbdd20aSAndroid Build Coastguard Worker        selected: true,
117*6dbdd20aSAndroid Build Coastguard Worker      }),
118*6dbdd20aSAndroid Build Coastguard Worker    );
119*6dbdd20aSAndroid Build Coastguard Worker  }
120*6dbdd20aSAndroid Build Coastguard Worker
121*6dbdd20aSAndroid Build Coastguard Worker  get pivotState() {
122*6dbdd20aSAndroid Build Coastguard Worker    return this.pivotMgr.state;
123*6dbdd20aSAndroid Build Coastguard Worker  }
124*6dbdd20aSAndroid Build Coastguard Worker
125*6dbdd20aSAndroid Build Coastguard Worker  renderDrillDownCell(attrs: PivotTableAttrs, filters: DrillFilter[]) {
126*6dbdd20aSAndroid Build Coastguard Worker    return m(
127*6dbdd20aSAndroid Build Coastguard Worker      'td',
128*6dbdd20aSAndroid Build Coastguard Worker      m(
129*6dbdd20aSAndroid Build Coastguard Worker        'button',
130*6dbdd20aSAndroid Build Coastguard Worker        {
131*6dbdd20aSAndroid Build Coastguard Worker          title: 'All corresponding slices',
132*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
133*6dbdd20aSAndroid Build Coastguard Worker            const queryFilters = filters.map(renderDrillFilter);
134*6dbdd20aSAndroid Build Coastguard Worker            if (this.pivotState.constrainToArea) {
135*6dbdd20aSAndroid Build Coastguard Worker              queryFilters.push(...areaFilters(attrs.selectionArea));
136*6dbdd20aSAndroid Build Coastguard Worker            }
137*6dbdd20aSAndroid Build Coastguard Worker            extensions.addSqlTableTab(attrs.trace, {
138*6dbdd20aSAndroid Build Coastguard Worker              table: assertExists(getSqlTableDescription('slice')),
139*6dbdd20aSAndroid Build Coastguard Worker              // TODO(altimin): this should properly reference the required columns, but it works for now (until the pivot table is going to be rewritten to be more flexible).
140*6dbdd20aSAndroid Build Coastguard Worker              filters: queryFilters,
141*6dbdd20aSAndroid Build Coastguard Worker            });
142*6dbdd20aSAndroid Build Coastguard Worker          },
143*6dbdd20aSAndroid Build Coastguard Worker        },
144*6dbdd20aSAndroid Build Coastguard Worker        m('i.material-icons', 'arrow_right'),
145*6dbdd20aSAndroid Build Coastguard Worker      ),
146*6dbdd20aSAndroid Build Coastguard Worker    );
147*6dbdd20aSAndroid Build Coastguard Worker  }
148*6dbdd20aSAndroid Build Coastguard Worker
149*6dbdd20aSAndroid Build Coastguard Worker  renderSectionRow(
150*6dbdd20aSAndroid Build Coastguard Worker    attrs: PivotTableAttrs,
151*6dbdd20aSAndroid Build Coastguard Worker    path: PathItem[],
152*6dbdd20aSAndroid Build Coastguard Worker    tree: PivotTree,
153*6dbdd20aSAndroid Build Coastguard Worker    result: PivotTableResult,
154*6dbdd20aSAndroid Build Coastguard Worker  ): m.Vnode {
155*6dbdd20aSAndroid Build Coastguard Worker    const renderedCells = [];
156*6dbdd20aSAndroid Build Coastguard Worker    for (let j = 0; j + 1 < path.length; j++) {
157*6dbdd20aSAndroid Build Coastguard Worker      renderedCells.push(m('td', m('span.indent', ' '), `${path[j].nextKey}`));
158*6dbdd20aSAndroid Build Coastguard Worker    }
159*6dbdd20aSAndroid Build Coastguard Worker
160*6dbdd20aSAndroid Build Coastguard Worker    const treeDepth = result.metadata.pivotColumns.length;
161*6dbdd20aSAndroid Build Coastguard Worker    const colspan = treeDepth - path.length + 1;
162*6dbdd20aSAndroid Build Coastguard Worker    const button = m(
163*6dbdd20aSAndroid Build Coastguard Worker      'button',
164*6dbdd20aSAndroid Build Coastguard Worker      {
165*6dbdd20aSAndroid Build Coastguard Worker        onclick: () => {
166*6dbdd20aSAndroid Build Coastguard Worker          tree.isCollapsed = !tree.isCollapsed;
167*6dbdd20aSAndroid Build Coastguard Worker          raf.scheduleFullRedraw();
168*6dbdd20aSAndroid Build Coastguard Worker        },
169*6dbdd20aSAndroid Build Coastguard Worker      },
170*6dbdd20aSAndroid Build Coastguard Worker      m('i.material-icons', tree.isCollapsed ? 'expand_more' : 'expand_less'),
171*6dbdd20aSAndroid Build Coastguard Worker    );
172*6dbdd20aSAndroid Build Coastguard Worker
173*6dbdd20aSAndroid Build Coastguard Worker    renderedCells.push(
174*6dbdd20aSAndroid Build Coastguard Worker      m('td', {colspan}, button, `${path[path.length - 1].nextKey}`),
175*6dbdd20aSAndroid Build Coastguard Worker    );
176*6dbdd20aSAndroid Build Coastguard Worker
177*6dbdd20aSAndroid Build Coastguard Worker    for (let i = 0; i < result.metadata.aggregationColumns.length; i++) {
178*6dbdd20aSAndroid Build Coastguard Worker      const renderedValue = this.renderCell(
179*6dbdd20aSAndroid Build Coastguard Worker        result.metadata.aggregationColumns[i].column,
180*6dbdd20aSAndroid Build Coastguard Worker        tree.aggregates[i],
181*6dbdd20aSAndroid Build Coastguard Worker      );
182*6dbdd20aSAndroid Build Coastguard Worker      renderedCells.push(m('td' + markFirst(i), renderedValue));
183*6dbdd20aSAndroid Build Coastguard Worker    }
184*6dbdd20aSAndroid Build Coastguard Worker
185*6dbdd20aSAndroid Build Coastguard Worker    const drillFilters: DrillFilter[] = [];
186*6dbdd20aSAndroid Build Coastguard Worker    for (let i = 0; i < path.length; i++) {
187*6dbdd20aSAndroid Build Coastguard Worker      drillFilters.push({
188*6dbdd20aSAndroid Build Coastguard Worker        value: `${path[i].nextKey}`,
189*6dbdd20aSAndroid Build Coastguard Worker        column: result.metadata.pivotColumns[i],
190*6dbdd20aSAndroid Build Coastguard Worker      });
191*6dbdd20aSAndroid Build Coastguard Worker    }
192*6dbdd20aSAndroid Build Coastguard Worker
193*6dbdd20aSAndroid Build Coastguard Worker    renderedCells.push(this.renderDrillDownCell(attrs, drillFilters));
194*6dbdd20aSAndroid Build Coastguard Worker    return m('tr', renderedCells);
195*6dbdd20aSAndroid Build Coastguard Worker  }
196*6dbdd20aSAndroid Build Coastguard Worker
197*6dbdd20aSAndroid Build Coastguard Worker  renderCell(column: TableColumn, value: ColumnType): m.Children {
198*6dbdd20aSAndroid Build Coastguard Worker    if (
199*6dbdd20aSAndroid Build Coastguard Worker      column.kind === 'regular' &&
200*6dbdd20aSAndroid Build Coastguard Worker      (column.column === 'dur' || column.column === 'thread_dur')
201*6dbdd20aSAndroid Build Coastguard Worker    ) {
202*6dbdd20aSAndroid Build Coastguard Worker      if (typeof value === 'bigint') {
203*6dbdd20aSAndroid Build Coastguard Worker        return m(DurationWidget, {dur: value});
204*6dbdd20aSAndroid Build Coastguard Worker      } else if (typeof value === 'number') {
205*6dbdd20aSAndroid Build Coastguard Worker        return m(DurationWidget, {dur: BigInt(Math.round(value))});
206*6dbdd20aSAndroid Build Coastguard Worker      }
207*6dbdd20aSAndroid Build Coastguard Worker    }
208*6dbdd20aSAndroid Build Coastguard Worker    return `${value}`;
209*6dbdd20aSAndroid Build Coastguard Worker  }
210*6dbdd20aSAndroid Build Coastguard Worker
211*6dbdd20aSAndroid Build Coastguard Worker  renderTree(
212*6dbdd20aSAndroid Build Coastguard Worker    attrs: PivotTableAttrs,
213*6dbdd20aSAndroid Build Coastguard Worker    path: PathItem[],
214*6dbdd20aSAndroid Build Coastguard Worker    tree: PivotTree,
215*6dbdd20aSAndroid Build Coastguard Worker    result: PivotTableResult,
216*6dbdd20aSAndroid Build Coastguard Worker    sink: m.Vnode[],
217*6dbdd20aSAndroid Build Coastguard Worker  ) {
218*6dbdd20aSAndroid Build Coastguard Worker    if (tree.isCollapsed) {
219*6dbdd20aSAndroid Build Coastguard Worker      sink.push(this.renderSectionRow(attrs, path, tree, result));
220*6dbdd20aSAndroid Build Coastguard Worker      return;
221*6dbdd20aSAndroid Build Coastguard Worker    }
222*6dbdd20aSAndroid Build Coastguard Worker    if (tree.children.size > 0) {
223*6dbdd20aSAndroid Build Coastguard Worker      // Avoid rendering the intermediate results row for the root of tree
224*6dbdd20aSAndroid Build Coastguard Worker      // and in case there's only one child subtree.
225*6dbdd20aSAndroid Build Coastguard Worker      if (!tree.isCollapsed && path.length > 0 && tree.children.size !== 1) {
226*6dbdd20aSAndroid Build Coastguard Worker        sink.push(this.renderSectionRow(attrs, path, tree, result));
227*6dbdd20aSAndroid Build Coastguard Worker      }
228*6dbdd20aSAndroid Build Coastguard Worker      for (const [key, childTree] of tree.children.entries()) {
229*6dbdd20aSAndroid Build Coastguard Worker        path.push({tree: childTree, nextKey: key});
230*6dbdd20aSAndroid Build Coastguard Worker        this.renderTree(attrs, path, childTree, result, sink);
231*6dbdd20aSAndroid Build Coastguard Worker        path.pop();
232*6dbdd20aSAndroid Build Coastguard Worker      }
233*6dbdd20aSAndroid Build Coastguard Worker      return;
234*6dbdd20aSAndroid Build Coastguard Worker    }
235*6dbdd20aSAndroid Build Coastguard Worker
236*6dbdd20aSAndroid Build Coastguard Worker    // Avoid rendering the intermediate results row if it has only one leaf
237*6dbdd20aSAndroid Build Coastguard Worker    // row.
238*6dbdd20aSAndroid Build Coastguard Worker    if (!tree.isCollapsed && path.length > 0 && tree.rows.length > 1) {
239*6dbdd20aSAndroid Build Coastguard Worker      sink.push(this.renderSectionRow(attrs, path, tree, result));
240*6dbdd20aSAndroid Build Coastguard Worker    }
241*6dbdd20aSAndroid Build Coastguard Worker    for (const row of tree.rows) {
242*6dbdd20aSAndroid Build Coastguard Worker      const renderedCells = [];
243*6dbdd20aSAndroid Build Coastguard Worker      const drillFilters: DrillFilter[] = [];
244*6dbdd20aSAndroid Build Coastguard Worker      const treeDepth = result.metadata.pivotColumns.length;
245*6dbdd20aSAndroid Build Coastguard Worker      for (let j = 0; j < treeDepth; j++) {
246*6dbdd20aSAndroid Build Coastguard Worker        const value = this.renderCell(result.metadata.pivotColumns[j], row[j]);
247*6dbdd20aSAndroid Build Coastguard Worker        if (j < path.length) {
248*6dbdd20aSAndroid Build Coastguard Worker          renderedCells.push(m('td', m('span.indent', ' '), value));
249*6dbdd20aSAndroid Build Coastguard Worker        } else {
250*6dbdd20aSAndroid Build Coastguard Worker          renderedCells.push(m(`td`, value));
251*6dbdd20aSAndroid Build Coastguard Worker        }
252*6dbdd20aSAndroid Build Coastguard Worker        drillFilters.push({
253*6dbdd20aSAndroid Build Coastguard Worker          column: result.metadata.pivotColumns[j],
254*6dbdd20aSAndroid Build Coastguard Worker          value: row[j],
255*6dbdd20aSAndroid Build Coastguard Worker        });
256*6dbdd20aSAndroid Build Coastguard Worker      }
257*6dbdd20aSAndroid Build Coastguard Worker      for (let j = 0; j < result.metadata.aggregationColumns.length; j++) {
258*6dbdd20aSAndroid Build Coastguard Worker        const value = row[aggregationIndex(treeDepth, j)];
259*6dbdd20aSAndroid Build Coastguard Worker        const renderedValue = this.renderCell(
260*6dbdd20aSAndroid Build Coastguard Worker          result.metadata.aggregationColumns[j].column,
261*6dbdd20aSAndroid Build Coastguard Worker          value,
262*6dbdd20aSAndroid Build Coastguard Worker        );
263*6dbdd20aSAndroid Build Coastguard Worker        renderedCells.push(m('td.aggregation' + markFirst(j), renderedValue));
264*6dbdd20aSAndroid Build Coastguard Worker      }
265*6dbdd20aSAndroid Build Coastguard Worker
266*6dbdd20aSAndroid Build Coastguard Worker      renderedCells.push(this.renderDrillDownCell(attrs, drillFilters));
267*6dbdd20aSAndroid Build Coastguard Worker      sink.push(m('tr', renderedCells));
268*6dbdd20aSAndroid Build Coastguard Worker    }
269*6dbdd20aSAndroid Build Coastguard Worker  }
270*6dbdd20aSAndroid Build Coastguard Worker
271*6dbdd20aSAndroid Build Coastguard Worker  renderTotalsRow(queryResult: PivotTableResult) {
272*6dbdd20aSAndroid Build Coastguard Worker    const overallValuesRow = [
273*6dbdd20aSAndroid Build Coastguard Worker      m(
274*6dbdd20aSAndroid Build Coastguard Worker        'td.total-values',
275*6dbdd20aSAndroid Build Coastguard Worker        {colspan: queryResult.metadata.pivotColumns.length},
276*6dbdd20aSAndroid Build Coastguard Worker        m('strong', 'Total values:'),
277*6dbdd20aSAndroid Build Coastguard Worker      ),
278*6dbdd20aSAndroid Build Coastguard Worker    ];
279*6dbdd20aSAndroid Build Coastguard Worker    for (let i = 0; i < queryResult.metadata.aggregationColumns.length; i++) {
280*6dbdd20aSAndroid Build Coastguard Worker      overallValuesRow.push(
281*6dbdd20aSAndroid Build Coastguard Worker        m(
282*6dbdd20aSAndroid Build Coastguard Worker          'td' + markFirst(i),
283*6dbdd20aSAndroid Build Coastguard Worker          this.renderCell(
284*6dbdd20aSAndroid Build Coastguard Worker            queryResult.metadata.aggregationColumns[i].column,
285*6dbdd20aSAndroid Build Coastguard Worker            queryResult.tree.aggregates[i],
286*6dbdd20aSAndroid Build Coastguard Worker          ),
287*6dbdd20aSAndroid Build Coastguard Worker        ),
288*6dbdd20aSAndroid Build Coastguard Worker      );
289*6dbdd20aSAndroid Build Coastguard Worker    }
290*6dbdd20aSAndroid Build Coastguard Worker    overallValuesRow.push(m('td'));
291*6dbdd20aSAndroid Build Coastguard Worker    return m('tr', overallValuesRow);
292*6dbdd20aSAndroid Build Coastguard Worker  }
293*6dbdd20aSAndroid Build Coastguard Worker
294*6dbdd20aSAndroid Build Coastguard Worker  sortingItem(aggregationIndex: number, order: SortDirection): m.Child {
295*6dbdd20aSAndroid Build Coastguard Worker    const pivotMgr = this.pivotMgr;
296*6dbdd20aSAndroid Build Coastguard Worker    return m(MenuItem, {
297*6dbdd20aSAndroid Build Coastguard Worker      label: order === 'DESC' ? 'Highest first' : 'Lowest first',
298*6dbdd20aSAndroid Build Coastguard Worker      onclick: () => {
299*6dbdd20aSAndroid Build Coastguard Worker        pivotMgr.setSortColumn(aggregationIndex, order);
300*6dbdd20aSAndroid Build Coastguard Worker      },
301*6dbdd20aSAndroid Build Coastguard Worker    });
302*6dbdd20aSAndroid Build Coastguard Worker  }
303*6dbdd20aSAndroid Build Coastguard Worker
304*6dbdd20aSAndroid Build Coastguard Worker  readableAggregationName(aggregation: Aggregation) {
305*6dbdd20aSAndroid Build Coastguard Worker    if (aggregation.aggregationFunction === 'COUNT') {
306*6dbdd20aSAndroid Build Coastguard Worker      return 'Count';
307*6dbdd20aSAndroid Build Coastguard Worker    }
308*6dbdd20aSAndroid Build Coastguard Worker    return `${aggregation.aggregationFunction}(${readableColumnName(
309*6dbdd20aSAndroid Build Coastguard Worker      aggregation.column,
310*6dbdd20aSAndroid Build Coastguard Worker    )})`;
311*6dbdd20aSAndroid Build Coastguard Worker  }
312*6dbdd20aSAndroid Build Coastguard Worker
313*6dbdd20aSAndroid Build Coastguard Worker  aggregationPopupItem(
314*6dbdd20aSAndroid Build Coastguard Worker    aggregation: Aggregation,
315*6dbdd20aSAndroid Build Coastguard Worker    index: number,
316*6dbdd20aSAndroid Build Coastguard Worker    nameOverride?: string,
317*6dbdd20aSAndroid Build Coastguard Worker  ): m.Child {
318*6dbdd20aSAndroid Build Coastguard Worker    return m(MenuItem, {
319*6dbdd20aSAndroid Build Coastguard Worker      label: nameOverride ?? readableColumnName(aggregation.column),
320*6dbdd20aSAndroid Build Coastguard Worker      onclick: () => {
321*6dbdd20aSAndroid Build Coastguard Worker        this.pivotMgr.addAggregation(aggregation, index);
322*6dbdd20aSAndroid Build Coastguard Worker      },
323*6dbdd20aSAndroid Build Coastguard Worker    });
324*6dbdd20aSAndroid Build Coastguard Worker  }
325*6dbdd20aSAndroid Build Coastguard Worker
326*6dbdd20aSAndroid Build Coastguard Worker  aggregationPopupTableGroup(
327*6dbdd20aSAndroid Build Coastguard Worker    table: string,
328*6dbdd20aSAndroid Build Coastguard Worker    columns: string[],
329*6dbdd20aSAndroid Build Coastguard Worker    index: number,
330*6dbdd20aSAndroid Build Coastguard Worker  ): m.Child | undefined {
331*6dbdd20aSAndroid Build Coastguard Worker    const items: m.Child[] = [];
332*6dbdd20aSAndroid Build Coastguard Worker    for (const column of columns) {
333*6dbdd20aSAndroid Build Coastguard Worker      const tableColumn: TableColumn = {kind: 'regular', table, column};
334*6dbdd20aSAndroid Build Coastguard Worker      items.push(
335*6dbdd20aSAndroid Build Coastguard Worker        this.aggregationPopupItem(
336*6dbdd20aSAndroid Build Coastguard Worker          {aggregationFunction: 'SUM', column: tableColumn},
337*6dbdd20aSAndroid Build Coastguard Worker          index,
338*6dbdd20aSAndroid Build Coastguard Worker        ),
339*6dbdd20aSAndroid Build Coastguard Worker      );
340*6dbdd20aSAndroid Build Coastguard Worker    }
341*6dbdd20aSAndroid Build Coastguard Worker
342*6dbdd20aSAndroid Build Coastguard Worker    if (items.length === 0) {
343*6dbdd20aSAndroid Build Coastguard Worker      return undefined;
344*6dbdd20aSAndroid Build Coastguard Worker    }
345*6dbdd20aSAndroid Build Coastguard Worker
346*6dbdd20aSAndroid Build Coastguard Worker    return m(MenuItem, {label: `Add ${table} aggregation`}, items);
347*6dbdd20aSAndroid Build Coastguard Worker  }
348*6dbdd20aSAndroid Build Coastguard Worker
349*6dbdd20aSAndroid Build Coastguard Worker  renderAggregationHeaderCell(
350*6dbdd20aSAndroid Build Coastguard Worker    aggregation: Aggregation,
351*6dbdd20aSAndroid Build Coastguard Worker    index: number,
352*6dbdd20aSAndroid Build Coastguard Worker    removeItem: boolean,
353*6dbdd20aSAndroid Build Coastguard Worker  ): ReorderableCell {
354*6dbdd20aSAndroid Build Coastguard Worker    const popupItems: m.Child[] = [];
355*6dbdd20aSAndroid Build Coastguard Worker    if (aggregation.sortDirection === undefined) {
356*6dbdd20aSAndroid Build Coastguard Worker      popupItems.push(
357*6dbdd20aSAndroid Build Coastguard Worker        this.sortingItem(index, 'DESC'),
358*6dbdd20aSAndroid Build Coastguard Worker        this.sortingItem(index, 'ASC'),
359*6dbdd20aSAndroid Build Coastguard Worker      );
360*6dbdd20aSAndroid Build Coastguard Worker    } else {
361*6dbdd20aSAndroid Build Coastguard Worker      // Table is already sorted by the same column, return one item with
362*6dbdd20aSAndroid Build Coastguard Worker      // opposite direction.
363*6dbdd20aSAndroid Build Coastguard Worker      popupItems.push(
364*6dbdd20aSAndroid Build Coastguard Worker        this.sortingItem(
365*6dbdd20aSAndroid Build Coastguard Worker          index,
366*6dbdd20aSAndroid Build Coastguard Worker          aggregation.sortDirection === 'DESC' ? 'ASC' : 'DESC',
367*6dbdd20aSAndroid Build Coastguard Worker        ),
368*6dbdd20aSAndroid Build Coastguard Worker      );
369*6dbdd20aSAndroid Build Coastguard Worker    }
370*6dbdd20aSAndroid Build Coastguard Worker    const otherAggs: AggregationFunction[] = ['SUM', 'MAX', 'MIN', 'AVG'];
371*6dbdd20aSAndroid Build Coastguard Worker    if (aggregation.aggregationFunction !== 'COUNT') {
372*6dbdd20aSAndroid Build Coastguard Worker      for (const otherAgg of otherAggs) {
373*6dbdd20aSAndroid Build Coastguard Worker        if (aggregation.aggregationFunction === otherAgg) {
374*6dbdd20aSAndroid Build Coastguard Worker          continue;
375*6dbdd20aSAndroid Build Coastguard Worker        }
376*6dbdd20aSAndroid Build Coastguard Worker        const pivotMgr = this.pivotMgr;
377*6dbdd20aSAndroid Build Coastguard Worker        popupItems.push(
378*6dbdd20aSAndroid Build Coastguard Worker          m(MenuItem, {
379*6dbdd20aSAndroid Build Coastguard Worker            label: otherAgg,
380*6dbdd20aSAndroid Build Coastguard Worker            onclick: () => {
381*6dbdd20aSAndroid Build Coastguard Worker              pivotMgr.setAggregationFunction(index, otherAgg);
382*6dbdd20aSAndroid Build Coastguard Worker            },
383*6dbdd20aSAndroid Build Coastguard Worker          }),
384*6dbdd20aSAndroid Build Coastguard Worker        );
385*6dbdd20aSAndroid Build Coastguard Worker      }
386*6dbdd20aSAndroid Build Coastguard Worker    }
387*6dbdd20aSAndroid Build Coastguard Worker
388*6dbdd20aSAndroid Build Coastguard Worker    if (removeItem) {
389*6dbdd20aSAndroid Build Coastguard Worker      popupItems.push(
390*6dbdd20aSAndroid Build Coastguard Worker        m(MenuItem, {
391*6dbdd20aSAndroid Build Coastguard Worker          label: 'Remove',
392*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
393*6dbdd20aSAndroid Build Coastguard Worker            this.pivotMgr.removeAggregation(index);
394*6dbdd20aSAndroid Build Coastguard Worker          },
395*6dbdd20aSAndroid Build Coastguard Worker        }),
396*6dbdd20aSAndroid Build Coastguard Worker      );
397*6dbdd20aSAndroid Build Coastguard Worker    }
398*6dbdd20aSAndroid Build Coastguard Worker
399*6dbdd20aSAndroid Build Coastguard Worker    let hasCount = false;
400*6dbdd20aSAndroid Build Coastguard Worker    for (const agg of this.pivotState.selectedAggregations.values()) {
401*6dbdd20aSAndroid Build Coastguard Worker      if (agg.aggregationFunction === 'COUNT') {
402*6dbdd20aSAndroid Build Coastguard Worker        hasCount = true;
403*6dbdd20aSAndroid Build Coastguard Worker      }
404*6dbdd20aSAndroid Build Coastguard Worker    }
405*6dbdd20aSAndroid Build Coastguard Worker
406*6dbdd20aSAndroid Build Coastguard Worker    if (!hasCount) {
407*6dbdd20aSAndroid Build Coastguard Worker      popupItems.push(
408*6dbdd20aSAndroid Build Coastguard Worker        this.aggregationPopupItem(
409*6dbdd20aSAndroid Build Coastguard Worker          COUNT_AGGREGATION,
410*6dbdd20aSAndroid Build Coastguard Worker          index,
411*6dbdd20aSAndroid Build Coastguard Worker          'Add count aggregation',
412*6dbdd20aSAndroid Build Coastguard Worker        ),
413*6dbdd20aSAndroid Build Coastguard Worker      );
414*6dbdd20aSAndroid Build Coastguard Worker    }
415*6dbdd20aSAndroid Build Coastguard Worker
416*6dbdd20aSAndroid Build Coastguard Worker    const sliceAggregationsItem = this.aggregationPopupTableGroup(
417*6dbdd20aSAndroid Build Coastguard Worker      assertExists(getSqlTableDescription('slice')).name,
418*6dbdd20aSAndroid Build Coastguard Worker      sliceAggregationColumns,
419*6dbdd20aSAndroid Build Coastguard Worker      index,
420*6dbdd20aSAndroid Build Coastguard Worker    );
421*6dbdd20aSAndroid Build Coastguard Worker    if (sliceAggregationsItem !== undefined) {
422*6dbdd20aSAndroid Build Coastguard Worker      popupItems.push(sliceAggregationsItem);
423*6dbdd20aSAndroid Build Coastguard Worker    }
424*6dbdd20aSAndroid Build Coastguard Worker
425*6dbdd20aSAndroid Build Coastguard Worker    return {
426*6dbdd20aSAndroid Build Coastguard Worker      extraClass: '.aggregation' + markFirst(index),
427*6dbdd20aSAndroid Build Coastguard Worker      content: [
428*6dbdd20aSAndroid Build Coastguard Worker        this.readableAggregationName(aggregation),
429*6dbdd20aSAndroid Build Coastguard Worker        m(
430*6dbdd20aSAndroid Build Coastguard Worker          PopupMenu2,
431*6dbdd20aSAndroid Build Coastguard Worker          {
432*6dbdd20aSAndroid Build Coastguard Worker            trigger: m(Button, {
433*6dbdd20aSAndroid Build Coastguard Worker              icon: popupMenuIcon(aggregation.sortDirection),
434*6dbdd20aSAndroid Build Coastguard Worker            }),
435*6dbdd20aSAndroid Build Coastguard Worker          },
436*6dbdd20aSAndroid Build Coastguard Worker          popupItems,
437*6dbdd20aSAndroid Build Coastguard Worker        ),
438*6dbdd20aSAndroid Build Coastguard Worker      ],
439*6dbdd20aSAndroid Build Coastguard Worker    };
440*6dbdd20aSAndroid Build Coastguard Worker  }
441*6dbdd20aSAndroid Build Coastguard Worker
442*6dbdd20aSAndroid Build Coastguard Worker  attributeModalHolder: AttributeModalHolder;
443*6dbdd20aSAndroid Build Coastguard Worker
444*6dbdd20aSAndroid Build Coastguard Worker  renderPivotColumnHeader(
445*6dbdd20aSAndroid Build Coastguard Worker    queryResult: PivotTableResult,
446*6dbdd20aSAndroid Build Coastguard Worker    pivot: TableColumn,
447*6dbdd20aSAndroid Build Coastguard Worker    selectedPivots: Set<string>,
448*6dbdd20aSAndroid Build Coastguard Worker  ): ReorderableCell {
449*6dbdd20aSAndroid Build Coastguard Worker    const pivotMgr = this.pivotMgr;
450*6dbdd20aSAndroid Build Coastguard Worker    const items: m.Child[] = [
451*6dbdd20aSAndroid Build Coastguard Worker      m(MenuItem, {
452*6dbdd20aSAndroid Build Coastguard Worker        label: 'Add argument pivot',
453*6dbdd20aSAndroid Build Coastguard Worker        onclick: () => {
454*6dbdd20aSAndroid Build Coastguard Worker          this.attributeModalHolder.start();
455*6dbdd20aSAndroid Build Coastguard Worker        },
456*6dbdd20aSAndroid Build Coastguard Worker      }),
457*6dbdd20aSAndroid Build Coastguard Worker    ];
458*6dbdd20aSAndroid Build Coastguard Worker    if (queryResult.metadata.pivotColumns.length > 1) {
459*6dbdd20aSAndroid Build Coastguard Worker      items.push(
460*6dbdd20aSAndroid Build Coastguard Worker        m(MenuItem, {
461*6dbdd20aSAndroid Build Coastguard Worker          label: 'Remove',
462*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
463*6dbdd20aSAndroid Build Coastguard Worker            pivotMgr.setPivotSelected({column: pivot, selected: false});
464*6dbdd20aSAndroid Build Coastguard Worker          },
465*6dbdd20aSAndroid Build Coastguard Worker        }),
466*6dbdd20aSAndroid Build Coastguard Worker      );
467*6dbdd20aSAndroid Build Coastguard Worker    }
468*6dbdd20aSAndroid Build Coastguard Worker
469*6dbdd20aSAndroid Build Coastguard Worker    for (const table of tables) {
470*6dbdd20aSAndroid Build Coastguard Worker      const group: m.Child[] = [];
471*6dbdd20aSAndroid Build Coastguard Worker      for (const columnName of table.columns) {
472*6dbdd20aSAndroid Build Coastguard Worker        const column: TableColumn = {
473*6dbdd20aSAndroid Build Coastguard Worker          kind: 'regular',
474*6dbdd20aSAndroid Build Coastguard Worker          table: table.name,
475*6dbdd20aSAndroid Build Coastguard Worker          column: columnName,
476*6dbdd20aSAndroid Build Coastguard Worker        };
477*6dbdd20aSAndroid Build Coastguard Worker        if (selectedPivots.has(columnKey(column))) {
478*6dbdd20aSAndroid Build Coastguard Worker          continue;
479*6dbdd20aSAndroid Build Coastguard Worker        }
480*6dbdd20aSAndroid Build Coastguard Worker        group.push(
481*6dbdd20aSAndroid Build Coastguard Worker          m(MenuItem, {
482*6dbdd20aSAndroid Build Coastguard Worker            label: columnName,
483*6dbdd20aSAndroid Build Coastguard Worker            onclick: () => {
484*6dbdd20aSAndroid Build Coastguard Worker              pivotMgr.setPivotSelected({column, selected: true});
485*6dbdd20aSAndroid Build Coastguard Worker            },
486*6dbdd20aSAndroid Build Coastguard Worker          }),
487*6dbdd20aSAndroid Build Coastguard Worker        );
488*6dbdd20aSAndroid Build Coastguard Worker      }
489*6dbdd20aSAndroid Build Coastguard Worker      items.push(
490*6dbdd20aSAndroid Build Coastguard Worker        m(
491*6dbdd20aSAndroid Build Coastguard Worker          MenuItem,
492*6dbdd20aSAndroid Build Coastguard Worker          {
493*6dbdd20aSAndroid Build Coastguard Worker            label: `Add ${table.displayName} pivot`,
494*6dbdd20aSAndroid Build Coastguard Worker          },
495*6dbdd20aSAndroid Build Coastguard Worker          group,
496*6dbdd20aSAndroid Build Coastguard Worker        ),
497*6dbdd20aSAndroid Build Coastguard Worker      );
498*6dbdd20aSAndroid Build Coastguard Worker    }
499*6dbdd20aSAndroid Build Coastguard Worker
500*6dbdd20aSAndroid Build Coastguard Worker    return {
501*6dbdd20aSAndroid Build Coastguard Worker      content: [
502*6dbdd20aSAndroid Build Coastguard Worker        readableColumnName(pivot),
503*6dbdd20aSAndroid Build Coastguard Worker        m(PopupMenu2, {trigger: m(Button, {icon: 'more_horiz'})}, items),
504*6dbdd20aSAndroid Build Coastguard Worker      ],
505*6dbdd20aSAndroid Build Coastguard Worker    };
506*6dbdd20aSAndroid Build Coastguard Worker  }
507*6dbdd20aSAndroid Build Coastguard Worker
508*6dbdd20aSAndroid Build Coastguard Worker  renderResultsTable(attrs: PivotTableAttrs) {
509*6dbdd20aSAndroid Build Coastguard Worker    const state = this.pivotState;
510*6dbdd20aSAndroid Build Coastguard Worker    const queryResult = state.queryResult;
511*6dbdd20aSAndroid Build Coastguard Worker    if (queryResult === undefined) {
512*6dbdd20aSAndroid Build Coastguard Worker      return m('div', 'Loading...');
513*6dbdd20aSAndroid Build Coastguard Worker    }
514*6dbdd20aSAndroid Build Coastguard Worker
515*6dbdd20aSAndroid Build Coastguard Worker    const renderedRows: m.Vnode[] = [];
516*6dbdd20aSAndroid Build Coastguard Worker
517*6dbdd20aSAndroid Build Coastguard Worker    // We should not even be showing the tab if there's no results.
518*6dbdd20aSAndroid Build Coastguard Worker    const tree = queryResult.tree;
519*6dbdd20aSAndroid Build Coastguard Worker    assertFalse(tree.children.size === 0 && tree.rows.length === 0);
520*6dbdd20aSAndroid Build Coastguard Worker
521*6dbdd20aSAndroid Build Coastguard Worker    this.renderTree(attrs, [], tree, queryResult, renderedRows);
522*6dbdd20aSAndroid Build Coastguard Worker
523*6dbdd20aSAndroid Build Coastguard Worker    const selectedPivots = new Set(
524*6dbdd20aSAndroid Build Coastguard Worker      this.pivotState.selectedPivots.map(columnKey),
525*6dbdd20aSAndroid Build Coastguard Worker    );
526*6dbdd20aSAndroid Build Coastguard Worker    const pivotTableHeaders = state.selectedPivots.map((pivot) =>
527*6dbdd20aSAndroid Build Coastguard Worker      this.renderPivotColumnHeader(queryResult, pivot, selectedPivots),
528*6dbdd20aSAndroid Build Coastguard Worker    );
529*6dbdd20aSAndroid Build Coastguard Worker
530*6dbdd20aSAndroid Build Coastguard Worker    const removeItem = queryResult.metadata.aggregationColumns.length > 1;
531*6dbdd20aSAndroid Build Coastguard Worker    const aggregationTableHeaders = queryResult.metadata.aggregationColumns.map(
532*6dbdd20aSAndroid Build Coastguard Worker      (aggregation, index) =>
533*6dbdd20aSAndroid Build Coastguard Worker        this.renderAggregationHeaderCell(aggregation, index, removeItem),
534*6dbdd20aSAndroid Build Coastguard Worker    );
535*6dbdd20aSAndroid Build Coastguard Worker
536*6dbdd20aSAndroid Build Coastguard Worker    return m(
537*6dbdd20aSAndroid Build Coastguard Worker      'table.pivot-table',
538*6dbdd20aSAndroid Build Coastguard Worker      m(
539*6dbdd20aSAndroid Build Coastguard Worker        'thead',
540*6dbdd20aSAndroid Build Coastguard Worker        // First row of the table, containing names of pivot and aggregation
541*6dbdd20aSAndroid Build Coastguard Worker        // columns, as well as popup menus to modify the columns. Last cell
542*6dbdd20aSAndroid Build Coastguard Worker        // is empty because of an extra column with "drill down" button for
543*6dbdd20aSAndroid Build Coastguard Worker        // each pivot table row.
544*6dbdd20aSAndroid Build Coastguard Worker        m(
545*6dbdd20aSAndroid Build Coastguard Worker          'tr.header',
546*6dbdd20aSAndroid Build Coastguard Worker          m(ReorderableCellGroup, {
547*6dbdd20aSAndroid Build Coastguard Worker            cells: pivotTableHeaders,
548*6dbdd20aSAndroid Build Coastguard Worker            onReorder: (from: number, to: number, direction: DropDirection) => {
549*6dbdd20aSAndroid Build Coastguard Worker              this.pivotMgr.setOrder(from, to, direction);
550*6dbdd20aSAndroid Build Coastguard Worker            },
551*6dbdd20aSAndroid Build Coastguard Worker          }),
552*6dbdd20aSAndroid Build Coastguard Worker          m(ReorderableCellGroup, {
553*6dbdd20aSAndroid Build Coastguard Worker            cells: aggregationTableHeaders,
554*6dbdd20aSAndroid Build Coastguard Worker            onReorder: (from: number, to: number, direction: DropDirection) => {
555*6dbdd20aSAndroid Build Coastguard Worker              this.pivotMgr.setAggregationOrder(from, to, direction);
556*6dbdd20aSAndroid Build Coastguard Worker            },
557*6dbdd20aSAndroid Build Coastguard Worker          }),
558*6dbdd20aSAndroid Build Coastguard Worker          m(
559*6dbdd20aSAndroid Build Coastguard Worker            'td.menu',
560*6dbdd20aSAndroid Build Coastguard Worker            m(
561*6dbdd20aSAndroid Build Coastguard Worker              PopupMenu2,
562*6dbdd20aSAndroid Build Coastguard Worker              {
563*6dbdd20aSAndroid Build Coastguard Worker                trigger: m(Button, {icon: 'menu'}),
564*6dbdd20aSAndroid Build Coastguard Worker              },
565*6dbdd20aSAndroid Build Coastguard Worker              m(MenuItem, {
566*6dbdd20aSAndroid Build Coastguard Worker                label: state.constrainToArea
567*6dbdd20aSAndroid Build Coastguard Worker                  ? 'Query data for the whole timeline'
568*6dbdd20aSAndroid Build Coastguard Worker                  : 'Constrain to selected area',
569*6dbdd20aSAndroid Build Coastguard Worker                onclick: () => {
570*6dbdd20aSAndroid Build Coastguard Worker                  this.pivotMgr.setConstrainedToArea(!state.constrainToArea);
571*6dbdd20aSAndroid Build Coastguard Worker                },
572*6dbdd20aSAndroid Build Coastguard Worker              }),
573*6dbdd20aSAndroid Build Coastguard Worker            ),
574*6dbdd20aSAndroid Build Coastguard Worker          ),
575*6dbdd20aSAndroid Build Coastguard Worker        ),
576*6dbdd20aSAndroid Build Coastguard Worker      ),
577*6dbdd20aSAndroid Build Coastguard Worker      m('tbody', this.renderTotalsRow(queryResult), renderedRows),
578*6dbdd20aSAndroid Build Coastguard Worker    );
579*6dbdd20aSAndroid Build Coastguard Worker  }
580*6dbdd20aSAndroid Build Coastguard Worker
581*6dbdd20aSAndroid Build Coastguard Worker  view({attrs}: m.Vnode<PivotTableAttrs>): m.Children {
582*6dbdd20aSAndroid Build Coastguard Worker    return m('.pivot-table', this.renderResultsTable(attrs));
583*6dbdd20aSAndroid Build Coastguard Worker  }
584*6dbdd20aSAndroid Build Coastguard Worker}
585