blob: 559e9c02f843c5a60af5adffa28e968fbd3a1e2d [file]
// 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 {AggregateFunction} from '../datagrid/model';
/**
* Aggregation functions available for charts.
* Extends the datagrid's AggregateFunction with COUNT, which is
* field-independent (counts rows rather than aggregating a column).
*/
export type ChartAggregation = AggregateFunction | 'COUNT';
/**
* Format a number for display on chart axes.
*/
export function formatNumber(value: number): string {
if (Number.isInteger(value)) {
return value.toLocaleString();
}
if (Math.abs(value) >= 1000) {
return value.toLocaleString(undefined, {maximumFractionDigits: 0});
}
if (Math.abs(value) >= 1) {
return value.toLocaleString(undefined, {maximumFractionDigits: 2});
}
// For very small numbers, use more precision
return value.toPrecision(3);
}
/**
* Whether an aggregation always produces integer results.
*/
export function isIntegerAggregation(agg: ChartAggregation): boolean {
return agg === 'COUNT' || agg === 'COUNT_DISTINCT';
}
/**
* Compute the p-th percentile of a numeric array using linear interpolation.
* Returns NaN for empty arrays.
*/
export function percentile(values: number[], p: number): number {
if (values.length === 0) return NaN;
const sorted = [...values].sort((a, b) => a - b);
const idx = (p / 100) * (sorted.length - 1);
const lower = Math.floor(idx);
const upper = Math.ceil(idx);
if (lower === upper) return sorted[lower];
return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
}
// ---------------------------------------------------------------------------
// SQL helpers shared across chart loaders
// ---------------------------------------------------------------------------
// Valid SQL column name: identifier chars only (letters, digits, underscore).
const VALID_COLUMN_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
/**
* Validate that a string is a safe SQL column identifier.
* Throws if the name contains non-identifier characters.
*/
export function validateColumnName(name: string): void {
if (!VALID_COLUMN_RE.test(name)) {
throw new Error(`Invalid SQL column name: '${name}'`);
}
}
// ---------------------------------------------------------------------------
// ECharts brush helpers
// ---------------------------------------------------------------------------
// Re-export EChartBrushEndParams for use by other chart modules.
export {type EChartBrushEndParams} from './echart_view';
/**
* Extract the numeric brush range from an ECharts brushEnd event.
* Returns [min, max] if a valid range was selected, undefined otherwise.
*
* This utility centralizes the brush range extraction logic used across
* different chart types (line, bar, histogram).
*/
export function extractBrushRange(
params: unknown,
): [number, number] | undefined {
const p = params as {
areas?: ReadonlyArray<{coordRange?: [number, number]}>;
};
const areas = p.areas;
if (areas !== undefined && areas.length > 0 && areas[0].coordRange) {
const [a, b] = areas[0].coordRange;
return [Math.min(a, b), Math.max(a, b)];
}
return undefined;
}
/**
* Extract the 2D brush rect from an ECharts `brushEnd` event (rect brush).
* Returns {xMin, xMax, yMin, yMax} if a valid rect was selected, undefined
* otherwise.
*
* For a rect brush, ECharts puts [[xMin, xMax], [yMin, yMax]] in coordRange.
*/
export function extractBrushRect(
params: unknown,
): {xMin: number; xMax: number; yMin: number; yMax: number} | undefined {
const p = params as {
areas?: ReadonlyArray<{
coordRange?: [[number, number], [number, number]];
}>;
};
const areas = p.areas;
if (areas === undefined || areas.length === 0) return undefined;
const coordRange = areas[0].coordRange;
if (coordRange === undefined) return undefined;
const [[x1, x2], [y1, y2]] = coordRange;
return {
xMin: Math.min(x1, x2),
xMax: Math.max(x1, x2),
yMin: Math.min(y1, y2),
yMax: Math.max(y1, y2),
};
}