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
75 changes: 43 additions & 32 deletions frontend/src/apexcharts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type DataPoint = {
link?: string;
};

type AxisTitles = Record<"x" | "y" | "z", string | undefined>;

type TooltipArgs = {
seriesIndex: number;
dataPointIndex: number;
Expand All @@ -29,13 +31,25 @@ function formatTooltipX(
value: string | number | null,
link: string | undefined,
) {
if (!link || !value) return value;
const text = value == null ? "" : String(value);
if (!link || !value) return text;
const anchor = document.createElement("a");
anchor.setAttribute("href", link);
anchor.textContent = String(value);
anchor.textContent = text;
return anchor.outerHTML;
}

const rangeBarLabel = (_value: string | number, args?: TooltipArgs) =>
args ? args.w.config.series[args.seriesIndex].name : "";

const pieLabel = (value: string | number, args?: TooltipArgs) =>
args
? `${args.w.config.labels[args.seriesIndex]}: ${Number(value).toFixed()}%`
: "";

const numberLabel = (value: string | number) =>
value == null ? "" : value.toLocaleString?.() || String(value);

const sqlpage_chart = (() => {
function sqlpage_chart() {
const charts = document.querySelectorAll<HTMLElement>(
Expand Down Expand Up @@ -195,7 +209,9 @@ const sqlpage_chart = (() => {
);

const labels =
chart_type === "pie" ? points.map(({ name, x }) => x || name) : undefined;
chart_type === "pie"
? points.map(({ name, x }) => String(x || name))
: undefined;
const series =
chart_type === "pie"
? points.map(({ y }) => Number.parseFloat(String(y)))
Expand All @@ -217,11 +233,16 @@ const sqlpage_chart = (() => {
chart_type === "rangeBar" || (chart_type === "bar" && !!data.horizontal);
const value_axis = inverted ? "x" : "y";
const category_axis = inverted ? "y" : "x";
const axis_titles: AxisTitles = {
x: data.xtitle || undefined,
y: data.ytitle || undefined,
z: data.ztitle || undefined,
};
const has_point_links = points.some((point) => point.link);
const text_x_values = chart_series.every(({ data }) =>
data.every(({ x }) => x == null || typeof x === "string"),
);
const options = {
const options: ApexOptions = {
annotations: {
[`${value_axis}axis`]: reference_lines(
reference_rows,
Expand Down Expand Up @@ -254,7 +275,7 @@ const sqlpage_chart = (() => {
},
events: {
dataPointSelection: (_event, _chart, args) => {
const link = pointLink(args, points);
const link = args && pointLink(args, points);
if (link) window.location.assign(link);
},
},
Expand All @@ -274,11 +295,10 @@ const sqlpage_chart = (() => {
},
formatter:
chart_type === "rangeBar"
? (_val, { seriesIndex, w }) => w.config.series[seriesIndex].name
? rangeBarLabel
: chart_type === "pie"
? (value, { seriesIndex, w }) =>
`${w.config.labels[seriesIndex]}: ${value.toFixed()}%`
: (value) => value?.toLocaleString?.() || value,
? pieLabel
: numberLabel,
},
fill: {
type: chart_type === "area" ? "gradient" : "solid",
Expand All @@ -295,12 +315,15 @@ const sqlpage_chart = (() => {
min: data.xmin,
max: data.xmax,
title: {
text: data.xtitle || undefined,
text: axis_titles.x,
},
type: xaxis_type,
labels: {
datetimeUTC: false,
},
// Numeric axes count intervals; category and time axes use tickAmount
// as a target for label density.
tickAmount: data.xticks || undefined,
},
yaxis: {
logarithmic: !!data.logarithmic,
Expand All @@ -309,12 +332,7 @@ const sqlpage_chart = (() => {
stepSize: data.ystep,
tickAmount: data.yticks,
title: {
text: data.ytitle || undefined,
},
},
zaxis: {
title: {
text: data.ztitle || undefined,
text: axis_titles.y,
},
},
markers: {
Expand All @@ -329,7 +347,7 @@ const sqlpage_chart = (() => {
interactive: has_point_links,
custom:
chart_type === "bubble" || chart_type === "scatter"
? bubbleTooltip
? (args: TooltipArgs) => chartTooltip(args, axis_titles)
: undefined,
x: {
formatter:
Expand Down Expand Up @@ -363,19 +381,18 @@ const sqlpage_chart = (() => {
colors,
series,
};
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 as { tickAmount?: number }).tickAmount = data.xticks;
const chart = new ApexCharts(chartContainer, options as ApexOptions);
if (labels) options.labels = labels;
const chart = new ApexCharts(chartContainer, options);
chart.render().catch(console.error);
if (window.charts) window.charts.push(chart);
else window.charts = [chart];
c.removeAttribute("data-pre-init");
}

function chartTooltip({ seriesIndex, dataPointIndex, w }: TooltipArgs) {
function chartTooltip(
{ seriesIndex, dataPointIndex, w }: TooltipArgs,
titles: AxisTitles,
) {
const series = w.config.series[seriesIndex];
const name = series?.name || "";
const point = series?.data[dataPointIndex];
Expand All @@ -390,14 +407,12 @@ const sqlpage_chart = (() => {
seriesName.innerText = name;
tooltip.appendChild(seriesName);

for (const axis of ["x", "y", "z"]) {
for (const axis of ["x", "y", "z"] as const) {
const value = point[axis];
if (value == null) continue;
const axisValue = document.createElement("div");
axisValue.className = "apexcharts-tooltip-y-group";
let axis_conf = w.config[`${axis}axis`];
if (axis_conf.length) axis_conf = axis_conf[0];
const title = axis_conf.title.text || axis;
const title = titles[axis] || axis;
const labelSpan = document.createElement("span");
labelSpan.className = "apexcharts-tooltip-text-y-label";
labelSpan.innerText = `${title}: `;
Expand Down Expand Up @@ -426,10 +441,6 @@ const sqlpage_chart = (() => {
: points[seriesIndex]?.link;
}

function bubbleTooltip(args: TooltipArgs) {
return chartTooltip(args);
}

return sqlpage_chart;
})();

Expand Down
39 changes: 39 additions & 0 deletions tests/end-to-end/fixtures/chart/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ declare global {
chart: { type: string; stacked: boolean };
xaxis: { type?: string; tickAmount?: number };
series: { name: string | number; data?: ChartPoint[] }[];
tooltip: {
custom?: (args: {
seriesIndex: number;
dataPointIndex: number;
w: unknown;
}) => string;
};
};
globals: { labels: (string | number)[] };
};
Expand Down Expand Up @@ -582,3 +589,35 @@ test("keeps coloring reference lines from their own row", async ({ page }) => {
expect(chart.failures).toEqual([]);
expect(chart.referenceLines.strokes).toEqual([GREEN]);
});

test("labels each axis of a bubble tooltip with its own title", async ({
page,
}) => {
await renderChart(page, "titled-bubble");

const rows = await page.evaluate(() => {
const chart = window.charts?.[0];
if (!chart) throw new Error("Chart fixture did not render");
const custom = chart.w.config.tooltip.custom;
if (!custom) throw new Error("A bubble chart needs the custom tooltip");
const holder = document.createElement("div");
holder.innerHTML = custom({
seriesIndex: 0,
dataPointIndex: 1,
w: chart.w,
});
const values = holder.querySelectorAll(".apexcharts-tooltip-text-y-value");
return [...holder.querySelectorAll(".apexcharts-tooltip-text-y-label")].map(
(label, i): [string, string] => [
label.textContent?.trim() ?? "",
values[i]?.textContent?.trim() ?? "",
],
);
});

expect(rows).toEqual([
["Weekday:", "Tue"],
["Hours:", "4"],
["Weight:", "30"],
]);
});
3 changes: 3 additions & 0 deletions tests/end-to-end/fixtures/chart/titled-bubble.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SELECT 'chart' AS component, 'test-chart' AS id, 'Chart test fixture' AS title, 'bubble' AS type,
'Weekday' AS xtitle, 'Hours' AS ytitle, 'Weight' AS ztitle;
WITH points(series, x, y, z) AS (VALUES ('Coding', 'Mon', 6, 30), ('Coding', 'Tue', 4, 30)) SELECT * FROM points;
Loading