| // Copyright (C) 2026 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 type {Engine} from '../../../trace_processor/engine'; |
| import { |
| NUM, |
| NUM_NULL, |
| STR_NULL, |
| type QueryResult, |
| } from '../../../trace_processor/query_result'; |
| import { |
| ChartSource, |
| SQLChartLoader, |
| type QueryConfig, |
| type ChartLoaderResult, |
| type PointColumnSpec, |
| rangeFilters, |
| } from './chart_sql_source'; |
| import type { |
| ScatterChartData, |
| ScatterChartPoint, |
| ScatterChartSeries, |
| } from './scatterplot'; |
| |
| /** |
| * Configuration for SQLScatterChartLoader. |
| */ |
| export interface SQLScatterChartLoaderOpts { |
| /** The trace processor engine to run queries against. */ |
| readonly engine: Engine; |
| |
| /** |
| * SQL query that provides the raw data. |
| * Must include x and y columns (and optionally size, label, series columns). |
| */ |
| readonly query: string; |
| |
| /** Column name for the X axis (numeric). */ |
| readonly xColumn: string; |
| |
| /** Column name for the Y axis (numeric). */ |
| readonly yColumn: string; |
| |
| /** Optional column for bubble size (numeric). */ |
| readonly sizeColumn?: string; |
| |
| /** Optional column for per-point label. */ |
| readonly labelColumn?: string; |
| |
| /** |
| * Optional column for grouping points into separate series. |
| * When omitted, all points belong to a single series. |
| */ |
| readonly seriesColumn?: string; |
| } |
| |
| /** |
| * Per-use configuration for the scatter chart loader. |
| */ |
| export interface ScatterChartLoaderConfig { |
| /** Filter to only include points within this X range. */ |
| readonly xRange?: {readonly min: number; readonly max: number}; |
| |
| /** Filter to only include points within this Y range. */ |
| readonly yRange?: {readonly min: number; readonly max: number}; |
| |
| /** |
| * Maximum number of points per series. When exceeded, stride-based |
| * sampling is applied. Defaults to no limit. |
| */ |
| readonly maxPoints?: number; |
| } |
| |
| /** Result returned by the scatter chart loader. */ |
| export type ScatterChartLoaderResult = ChartLoaderResult<ScatterChartData>; |
| |
| /** |
| * SQL-based scatter chart loader with async loading and caching. |
| * |
| * Fetches (x, y) points with optional size and series grouping from SQL. |
| */ |
| export class SQLScatterChartLoader extends SQLChartLoader< |
| ScatterChartLoaderConfig, |
| ScatterChartData |
| > { |
| private readonly xCol: string; |
| private readonly yCol: string; |
| private readonly sizeCol: string | undefined; |
| private readonly labelCol: string | undefined; |
| private readonly seriesCol: string | undefined; |
| |
| constructor(opts: SQLScatterChartLoaderOpts) { |
| const xCol = opts.xColumn; |
| const yCol = opts.yColumn; |
| const sizeCol = opts.sizeColumn; |
| const labelCol = opts.labelColumn; |
| const seriesCol = opts.seriesColumn; |
| |
| const schema: Record<string, 'text' | 'real'> = { |
| [xCol]: 'real', |
| [yCol]: 'real', |
| }; |
| if (sizeCol !== undefined) schema[sizeCol] = 'real'; |
| if (labelCol !== undefined) schema[labelCol] = 'text'; |
| if (seriesCol !== undefined) schema[seriesCol] = 'text'; |
| |
| super(opts.engine, new ChartSource({query: opts.query, schema})); |
| this.xCol = xCol; |
| this.yCol = yCol; |
| this.sizeCol = sizeCol; |
| this.labelCol = labelCol; |
| this.seriesCol = seriesCol; |
| } |
| |
| protected buildQueryConfig(config: ScatterChartLoaderConfig): QueryConfig { |
| // Always include _size and _label columns (NULL when not configured) |
| // so that parseResult can use a single iter spec. |
| const columns: PointColumnSpec[] = [ |
| {column: this.xCol, alias: '_x', cast: 'real'}, |
| {column: this.yCol, alias: '_y', cast: 'real'}, |
| this.sizeCol !== undefined |
| ? {column: this.sizeCol, alias: '_size', cast: 'real'} |
| : {alias: '_size', cast: 'real'}, |
| this.labelCol !== undefined |
| ? {column: this.labelCol, alias: '_label', cast: 'text'} |
| : {alias: '_label', cast: 'text'}, |
| ]; |
| // When no breakdown column, add NULL AS _series so parseResult's |
| // iter spec always finds the column. |
| if (this.seriesCol === undefined) { |
| columns.push({alias: '_series', cast: 'text'}); |
| } |
| return { |
| type: 'points', |
| columns, |
| breakdown: this.seriesCol, |
| filters: rangeFilters(this.xCol, config.xRange).concat( |
| rangeFilters(this.yCol, config.yRange), |
| ), |
| orderBy: this.seriesCol !== undefined ? [{column: '_series'}] : undefined, |
| maxPointsPerSeries: config.maxPoints, |
| }; |
| } |
| |
| protected parseResult(queryResult: QueryResult): ScatterChartData { |
| const seriesMap = new Map<string, ScatterChartPoint[]>(); |
| const defaultName = this.seriesCol !== undefined ? '' : 'Points'; |
| |
| const iter = queryResult.iter({ |
| _x: NUM, |
| _y: NUM, |
| _size: NUM_NULL, |
| _label: STR_NULL, |
| _series: STR_NULL, |
| }); |
| |
| for (; iter.valid(); iter.next()) { |
| const name = iter._series ?? defaultName; |
| let points = seriesMap.get(name); |
| if (points === undefined) { |
| points = []; |
| seriesMap.set(name, points); |
| } |
| const point: ScatterChartPoint = { |
| x: iter._x, |
| y: iter._y, |
| ...(iter._size !== null && {size: iter._size}), |
| ...(iter._label !== null && {label: iter._label}), |
| }; |
| points.push(point); |
| } |
| |
| const series: ScatterChartSeries[] = []; |
| for (const [name, points] of seriesMap) { |
| series.push({name, points}); |
| } |
| return {series}; |
| } |
| } |