1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14import m from 'mithril'; 15import { 16 ChartAttrs, 17 ChartType, 18 renderChart, 19} from '../../../components/widgets/charts/chart'; 20import {Trace} from '../../../public/trace'; 21import {Button} from '../../../widgets/button'; 22import {Icons} from '../../../base/semantic_icons'; 23import { 24 SplitPanel, 25 SplitPanelDrawerVisibility, 26} from '../../../widgets/split_panel'; 27import {VisViewSource} from './view_source'; 28import {AddChartMenuItem} from '../../../components/widgets/charts/add_chart_menu'; 29import {exists} from '../../../base/utils'; 30import {DetailsShell} from '../../../widgets/details_shell'; 31import {SqlTable} from '../../../components/widgets/sql/table/table'; 32import {sqlValueToSqliteString} from '../../../trace_processor/sql_utils'; 33import {renderFilters} from '../../../components/widgets/sql/table/filters'; 34import {ExplorePageState} from '../explore_page'; 35 36export interface DataVisualiserAttrs { 37 trace: Trace; 38 readonly state: ExplorePageState; 39} 40 41export class DataVisualiser implements m.ClassComponent<DataVisualiserAttrs> { 42 private visibility = SplitPanelDrawerVisibility.VISIBLE; 43 44 constructor({attrs}: m.Vnode<DataVisualiserAttrs>) { 45 if (attrs.state.selectedNode === undefined) return; 46 47 attrs.state.activeViewSource = new VisViewSource( 48 attrs.trace, 49 attrs.state.selectedNode, 50 ); 51 } 52 53 private renderSqlTable(state: ExplorePageState) { 54 const sqlTableViewState = state.activeViewSource?.visViews?.sqlTableState; 55 56 if (sqlTableViewState === undefined) return; 57 58 const range = sqlTableViewState.getDisplayedRange(); 59 const rowCount = sqlTableViewState.getTotalRowCount(); 60 61 const navigation = [ 62 exists(range) && 63 exists(rowCount) && 64 `Showing rows ${range.from}-${range.to} of ${rowCount}`, 65 m(Button, { 66 icon: Icons.GoBack, 67 disabled: !sqlTableViewState.canGoBack(), 68 onclick: () => sqlTableViewState!.goBack(), 69 }), 70 m(Button, { 71 icon: Icons.GoForward, 72 disabled: !sqlTableViewState.canGoForward(), 73 onclick: () => sqlTableViewState!.goForward(), 74 }), 75 ]; 76 77 return m( 78 DetailsShell, 79 { 80 title: 'Explore Table', 81 buttons: navigation, 82 fillParent: false, 83 }, 84 m('div', renderFilters(sqlTableViewState.filters)), 85 m(SqlTable, { 86 state: sqlTableViewState, 87 addColumnMenuItems: (_, columnAlias) => { 88 const chartAttrs = { 89 data: state.activeViewSource?.data, 90 columns: [columnAlias], 91 }; 92 93 return m(AddChartMenuItem, { 94 chartOptions: [ 95 { 96 chartType: ChartType.BAR_CHART, 97 ...chartAttrs, 98 onIntervalSelection: (value) => { 99 const range = `(${value[columnAlias].map(sqlValueToSqliteString).join(', ')})`; 100 state.activeViewSource?.filters.addFilter({ 101 op: (cols) => `${cols[0]} IN ${range}`, 102 columns: [columnAlias], 103 }); 104 }, 105 onPointSelection: (item) => { 106 const value = sqlValueToSqliteString(item.datum[columnAlias]); 107 state.activeViewSource?.filters.addFilter({ 108 op: (cols) => `${cols[0]} = ${value}`, 109 columns: [columnAlias], 110 }); 111 }, 112 }, 113 { 114 chartType: ChartType.HISTOGRAM, 115 ...chartAttrs, 116 onIntervalSelection: (value) => { 117 const range = `${value[columnAlias][0]} AND ${value[columnAlias][1]}`; 118 state.activeViewSource?.filters.addFilter({ 119 op: (cols) => `${cols[0]} BETWEEN ${range}`, 120 columns: [columnAlias], 121 }); 122 }, 123 onPointSelection: (item) => { 124 const minValue = item.datum[`bin_maxbins_10_${columnAlias}`]; 125 const maxValue = 126 item.datum[`bin_maxbins_10_${columnAlias}_end`]; 127 state.activeViewSource?.filters.addFilter({ 128 op: (cols) => 129 `${cols[0]} BETWEEN ${minValue} AND ${maxValue}`, 130 columns: [columnAlias], 131 }); 132 }, 133 }, 134 ], 135 addChart: (chart) => state.activeViewSource?.addChart(chart), 136 }); 137 }, 138 }), 139 ); 140 } 141 142 private renderRemovableChart(chart: ChartAttrs, state: ExplorePageState) { 143 return m( 144 '.pf-chart-card', 145 { 146 key: `${chart.chartType}-${chart.columns[0]}`, 147 }, 148 m(Button, { 149 className: 'pf-chart-card__button', 150 icon: Icons.Close, 151 onclick: () => { 152 state.activeViewSource?.removeChart(chart); 153 }, 154 }), 155 m('.pf-chart-card__chart', renderChart(chart)), 156 ); 157 } 158 159 view({attrs}: m.Vnode<DataVisualiserAttrs>) { 160 const {state} = attrs; 161 162 return m( 163 SplitPanel, 164 { 165 visibility: this.visibility, 166 onVisibilityChange: (visibility) => { 167 this.visibility = visibility; 168 }, 169 drawerContent: m( 170 '.pf-chart-container', 171 state.activeViewSource?.visViews !== undefined && 172 Array.from(state.activeViewSource?.visViews.charts.values()).map( 173 (chart) => this.renderRemovableChart(chart, state), 174 ), 175 ), 176 }, 177 m('.pf-chart-card', this.renderSqlTable(state)), 178 ); 179 } 180} 181