Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
73 changes: 34 additions & 39 deletions frontend/src/apexcharts.js → frontend/src/apexcharts.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import type { ApexOptions } from "apexcharts";
import ApexCharts from "apexcharts";
import { align_series_for, xaxis_type_for } from "./chart_series.js";
import { add_init_fn } from "./init.js";

/**
* @typedef {import("./chart_series.js").ChartSeries} ChartSeries
* @typedef {import("./chart_series.js").Series} Series
*/
import {
align_series_for,
type ChartSeries,
type Series,
xaxis_type_for,
} from "./chart_series.ts";
import { add_init_fn } from "./init.ts";

const sqlpage_chart = (() => {
function sqlpage_chart() {
/** @type {NodeListOf<HTMLElement>} */
const charts = document.querySelectorAll("[data-pre-init=chart]");
const charts = document.querySelectorAll<HTMLElement>(
"[data-pre-init=chart]",
);
for (const c of charts) {
try {
build_sqlpage_chart(c);
Expand Down Expand Up @@ -50,24 +52,21 @@ const sqlpage_chart = (() => {

const referenceColor = colorNames[isDarkTheme ? "gray-lt" : "gray"];

/** @typedef { {[property:string]: string|number|null} } ReferenceLine */
type ReferenceLine = { [property: string]: string | number | null };

/** @param {unknown} name @returns {string|undefined} */
const named_color = (name) =>
const named_color = (name: unknown): string | undefined =>
typeof name === "string" ? colorNames[name] : undefined;

/** @param {string|number|null} name */
const reference_color = (name) => named_color(name) || referenceColor;
const reference_color = (name: string | number | null) =>
named_color(name) || referenceColor;

/**
* @param {ReferenceLine[]} rows - the rows that carry an xline or a yline
* @param {"x"|"y"} column - the column the reference is written in
* @param {"x"|"y"} axis - the apexcharts axis that column is drawn on
* @param {(value: any) => any} to_axis_value - puts a SQL value on the axis
* @returns {object[]} apexcharts axis annotations
*/
function reference_lines(rows, column, axis, to_axis_value) {
const on_axis = (value) => {
function reference_lines(
rows: ReferenceLine[],
column: "x" | "y",
axis: "x" | "y",
to_axis_value: (value: string | number) => unknown,
): object[] {
const on_axis = (value: string | number | null) => {
if (value == null) return null;
const placed = to_axis_value(value);
return Number.isNaN(placed) ? null : placed;
Expand All @@ -94,13 +93,10 @@ const sqlpage_chart = (() => {
});
}

/** @param {HTMLElement} c */
function build_sqlpage_chart(c) {
function build_sqlpage_chart(c: HTMLElement) {
const [data_element] = c.getElementsByTagName("data");
const data = JSON.parse(data_element.textContent);
const chartContainer = /** @type {HTMLElement} */ (
c.querySelector(".chart")
);
const chartContainer = c.querySelector(".chart") as HTMLElement;
chartContainer.innerHTML = "";
const is_timeseries = !!data.time;
const chart_type =
Expand All @@ -109,11 +105,12 @@ const sqlpage_chart = (() => {
!!data.stacked && STACKABLE_CHART_TYPES.includes(chart_type);
const points = data.points.filter(Array.isArray);
const reference_rows = data.points.filter((row) => !Array.isArray(row));
/** @type { Series } */
const series_map = new Map();
const series_map: Series = new Map();
for (const [name, old_x, old_y, color, z] of points) {
/** @type {ChartSeries} */
const point_series = series_map.get(name) ?? { name, data: [] };
const point_series: ChartSeries = series_map.get(name) ?? {
name,
data: [],
};
series_map.set(name, point_series);
let x = old_x;
let y = old_y;
Expand Down Expand Up @@ -145,7 +142,7 @@ const sqlpage_chart = (() => {
!!data.horizontal,
);

let labels;
let labels: (number | string | Date)[] | undefined;
if (chart_type === "pie") {
labels = points.map(([name, x, _y]) => x || name);
series = points.map(([_name, _x, y]) => Number.parseFloat(y));
Expand Down Expand Up @@ -297,14 +294,12 @@ const sqlpage_chart = (() => {
colors,
series,
};
if (labels) options.labels = labels;
if (labels) (options as { labels?: unknown }).labels = labels;
// Numeric axes count intervals; category and time axes use tickAmount as a
// target for label density.
if (data.xticks) options.xaxis.tickAmount = data.xticks;
const chart = new ApexCharts(
chartContainer,
/** @type {import("apexcharts").ApexOptions} */ (options),
);
if (data.xticks)
(options.xaxis as { tickAmount?: number }).tickAmount = data.xticks;
const chart = new ApexCharts(chartContainer, options as ApexOptions);
chart.render().catch(console.error);
if (window.charts) window.charts.push(chart);
else window.charts = [chart];
Expand Down
65 changes: 34 additions & 31 deletions frontend/src/chart_series.js → frontend/src/chart_series.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
/** @typedef {number|string|Date} XValue */
/** @typedef { {x:XValue, y:number|string|number[]|null, z?:number, fillColor?:string} } ChartPoint */
/** @typedef { {name:string, data:ChartPoint[]} } ChartSeries */
/** @typedef { Map<string, ChartSeries> } Series */
export type XValue = number | string | Date;
export type ChartPoint = {
x: XValue;
y: number | string | number[] | null;
z?: number;
fillColor?: string;
};
export type ChartSeries = { name: string; data: ChartPoint[] };
export type Series = Map<string, ChartSeries>;

const NUMERIC_X_CHART_TYPES = ["line", "area", "bar", "scatter", "bubble"];

const Y_WHEN_A_SERIES_SKIPS_A_LABEL = {
const Y_WHEN_A_SERIES_SKIPS_A_LABEL: Record<string, number | null> = {
bar: 0,
line: null,
area: null,
Expand All @@ -14,18 +19,18 @@ const Y_WHEN_A_SERIES_SKIPS_A_LABEL = {
heatmap: null,
};

/** @param {XValue} x @returns {number|string} equal x values share a key */
const x_key = (x) => (x instanceof Date ? x.getTime() : x);
/** equal x values share a key */
const x_key = (x: XValue): number | string =>
x instanceof Date ? x.getTime() : x;

/** @param {ChartSeries[]} series */
const x_is_text = (series) => typeof series[0]?.data?.[0]?.x === "string";
const x_is_text = (series: ChartSeries[]) =>
typeof series[0]?.data?.[0]?.x === "string";

/** @param {ChartSeries[]} series @param {string} chart_type */
export function xaxis_type_for(
series,
chart_type,
is_timeseries,
is_horizontal,
series: ChartSeries[],
chart_type: string,
is_timeseries: boolean,
is_horizontal: boolean,
) {
if (is_timeseries) return "datetime";
if (x_is_text(series)) return "category";
Expand All @@ -38,18 +43,17 @@ export function xaxis_type_for(
}

/**
* @param {ChartSeries[]} series
* @returns {XValue[]} every x the series hold, in their own order where they
* agree and in ascending order where they diverge
* @returns every x the series hold, in their own order where they agree and in
* ascending order where they diverge
*/
export function merged_x_values(series) {
export function merged_x_values(series: ChartSeries[]): XValue[] {
const unread = series.map(({ data }) => data.map(({ x }) => x));
const merged = new Map();
while (unread.some((xs) => xs.length > 0)) {
const with_lowest_x = unread
.filter((xs) => xs.length > 0)
.reduce((a, b) => (b[0] < a[0] ? b : a));
const x = /** @type {XValue} */ (with_lowest_x.shift());
const x = with_lowest_x.shift() as XValue;
merged.set(x_key(x), x);
}
return [...merged.values()];
Expand All @@ -60,12 +64,13 @@ export function merged_x_values(series) {
* series that skips an x lands on the wrong one. Give every series the same
* amount of x values.
*
* @param {ChartSeries[]} series
* @param {number|null} y_when_missing what a series with no value at an x is
* worth there: zero to add nothing to a stack, null to leave a gap.
* @returns {ChartSeries[]}
* @param y_when_missing what a series with no value at an x is worth there:
* zero to add nothing to a stack, null to leave a gap.
*/
export function align_series(series, y_when_missing) {
export function align_series(
series: ChartSeries[],
y_when_missing: number | null,
): ChartSeries[] {
const all_x = merged_x_values(series);
return series.map(({ name, data }) => {
const by_x = new Map(data.map((point) => [x_key(point.x), point]));
Expand All @@ -79,13 +84,11 @@ export function align_series(series, y_when_missing) {
});
}

/**
* @param {ChartSeries[]} series
* @param {string} chart_type
* @param {boolean} is_stacked
* @returns {ChartSeries[]}
*/
export function align_series_for(series, chart_type, is_stacked) {
export function align_series_for(
series: ChartSeries[],
chart_type: string,
is_stacked: boolean,
): ChartSeries[] {
if (is_stacked) return align_series(series, 0);
if (x_is_text(series) && chart_type in Y_WHEN_A_SERIES_SKIPS_A_LABEL)
return align_series(series, Y_WHEN_A_SERIES_SKIPS_A_LABEL[chart_type]);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/init.js → frontend/src/init.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function add_init_fn(f) {
export function add_init_fn(f: () => void) {
document.addEventListener("DOMContentLoaded", f);
document.addEventListener("fragment-loaded", f);
if (document.readyState !== "loading") setTimeout(f, 0);
Expand Down
Loading
Loading