Skip to content
Merged
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
13 changes: 9 additions & 4 deletions frontend/src/apexcharts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const axisTooltip =
const formatted = Array.isArray(measured)
? measured.map(format).join(" - ")
: format(measured);
if (axis === "y" && point.link)
if (axis === "y" && point?.link)
valueSpan.innerHTML = linkTooltipValue(formatted, point.link);
else valueSpan.innerText = formatted;
axisValue.appendChild(valueSpan);
Expand Down Expand Up @@ -141,7 +141,7 @@ const sqlpage_chart = (() => {
["gray", "#495057", "#adb5bd"],
["black", "#000000", "#000000"],
["white", "#ffffff", "#f8f9fa"],
];
] as const;
const colorNames = new Map(
tblrColors.flatMap(([name, dark, light]): [string, string][] => [
[name, dark],
Expand Down Expand Up @@ -200,8 +200,10 @@ const sqlpage_chart = (() => {

function build_sqlpage_chart(c: HTMLElement) {
const [data_element] = c.getElementsByTagName("data");
const chartContainer = c.querySelector<HTMLElement>(".chart");
if (!data_element || !chartContainer)
throw new Error("Chart component is missing its data or container");
const data = read_chart_data(data_element.textContent);
const chartContainer = c.querySelector(".chart") as HTMLElement;
chartContainer.innerHTML = "";
const is_timeseries = data.time;
const chart_type =
Expand Down Expand Up @@ -260,7 +262,10 @@ const sqlpage_chart = (() => {
: aligned_series;
const colors = is_pie
? points.map(
({ color }, i) => named_color(color) || palette[i % palette.length],
({ color }, i) =>
named_color(color) ??
palette[i % palette.length] ??
tblrColors[0][isDarkTheme ? 1 : 2],
)
: palette;

Expand Down
21 changes: 14 additions & 7 deletions frontend/src/chart_series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,21 @@ export function xaxis_type_for(
* ascending order where they diverge
*/
export function merged_x_values(series: ChartSeries[]): XValue[] {
const unread = series.map(({ data }) => data.map(({ x }) => x));
const unread = series.map(({ data }) => {
const iterator = data.values();
return { iterator, next: iterator.next() };
});
const merged = new Map<PlotValue, XValue>();
while (unread.some((xs) => xs.length > 0)) {
const with_lowest_x = unread
.filter((xs) => xs.length > 0)
.reduce((a, b) => (is_lower(b[0], a[0]) ? b : a));
const x = with_lowest_x.shift() as XValue;
merged.set(x_key(x), x);
while (true) {
let lowest: { stream: (typeof unread)[number]; x: XValue } | undefined;
for (const stream of unread) {
if (stream.next.done) continue;
const x = stream.next.value.x;
if (!lowest || is_lower(x, lowest.x)) lowest = { stream, x };
}
if (!lowest) break;
merged.set(x_key(lowest.x), lowest.x);
lowest.stream.next = lowest.stream.iterator.next();
}
return [...merged.values()];
}
Expand Down
18 changes: 13 additions & 5 deletions frontend/src/sqlpage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,9 @@ function apply_number_formatting(table_el: HTMLElement) {

for (const tr_el of table_el.querySelectorAll("tbody tr, tfoot tr")) {
const cells = tr_el.getElementsByTagName("td");
for (let idx = 0; idx < cells.length; idx++) {
for (const [idx, cell_el] of [...cells].entries()) {
const column_type = col_types[idx];
const is_raw_number = col_rawnums[idx];
const cell_el = cells[idx];
const text = cell_el.textContent;

if (column_type === "number" && !is_raw_number && text) {
Expand Down Expand Up @@ -166,6 +165,8 @@ function setup_sort_behavior(
items.sort((a, b) => {
const a_key = a.sort_keys[button_index];
const b_key = b.sort_keys[button_index];
if (!a_key || !b_key)
throw new Error("Table row is missing the selected column sort key");
return (
multiplier *
(Number.isNaN(a_key.num) || Number.isNaN(b_key.num)
Expand Down Expand Up @@ -243,14 +244,21 @@ function sqlpage_map(root: InitRoot) {
coords: string | undefined,
): Leaflet.LatLngTuple | undefined {
if (!coords) return undefined;
const parsed = coords.split(",", 2).map((c) => Number.parseFloat(c));
if (parsed.length !== 2 || !parsed.every(Number.isFinite)) {
const [latitude, longitude] = coords
.split(",", 2)
.map((c) => Number.parseFloat(c));
if (
latitude === undefined ||
longitude === undefined ||
!Number.isFinite(latitude) ||
!Number.isFinite(longitude)
) {
console.error(
`Invalid map coordinates: ${JSON.stringify(coords)}. Expected a "latitude,longitude" pair of numbers.`,
);
return undefined;
}
return [parsed[0], parsed[1]];
return [latitude, longitude];
}
function onLeafletLoad(map_root: InitRoot) {
is_leaflet_loaded = true;
Expand Down
45 changes: 29 additions & 16 deletions tests/end-to-end/fixtures/chart/test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from "node:assert/strict";
import { type ConsoleMessage, expect, type Page, test } from "../../fixture.ts";

const MARKS =
Expand Down Expand Up @@ -219,8 +220,11 @@ test("positions complete numeric bar series on an explicit numeric axis (#733)",
expect(chart.axisLabels.map(({ text }) => Number(text))).toEqual(xs);
expect(chart.dataLabels.map(Number)).toEqual([...xs, ...xs]);
expect(chart.barGroups).toHaveLength(xs.length);
for (const [index, label] of chart.axisLabels.entries())
expect(Math.abs(label.center - chart.barGroups[index])).toBeLessThan(1);
for (const [index, label] of chart.axisLabels.entries()) {
const barCenter = chart.barGroups[index];
assert.ok(barCenter !== undefined, `Bar group ${index} was drawn`);
expect(Math.abs(label.center - barCenter)).toBeLessThan(1);
}
});

test("keeps irregular numeric x values proportionately spaced", async ({
Expand All @@ -237,9 +241,9 @@ test("keeps irregular numeric x values proportionately spaced", async ({
"3.0",
]);
expect(chart.axisLabels.map(({ text }) => text)).not.toContain("2");
expect(chart.barGroups[2] - chart.barGroups[1]).toBeGreaterThan(
5 * (chart.barGroups[1] - chart.barGroups[0]),
);
const [first, second, third] = chart.barGroups;
assert.ok(first !== undefined && second !== undefined && third !== undefined);
expect(third - second).toBeGreaterThan(5 * (second - first));
});

test("keeps an explicit x interval count", async ({ page }) => {
Expand Down Expand Up @@ -299,7 +303,7 @@ test("gives a stacked series a zero at every x it did not measure", async ({

expect(chart.failures).toEqual([]);
expect(chart.series.map((s) => s.name)).toEqual(["CPU", "GPU"]);
expect(chart.series[1].points).toEqual([
expect(chart.series[1]?.points).toEqual([
["2024-01-01T00:00:00.000Z", 0],
["2024-01-01T00:01:00.000Z", 50],
["2024-01-01T00:02:00.000Z", 50],
Expand All @@ -310,10 +314,13 @@ test("gives a stacked series a zero at every x it did not measure", async ({
test("stacks a series above the one it shares an x with", async ({ page }) => {
const chart = await renderChart(page, "stacked-time-series");
const [cpu, gpu] = chart.drawnPerSeries;
assert.ok(cpu && gpu, "Both series were drawn");

expect(gpu.heights).toHaveLength(4);
expect(gpu.heights[0]).toBe(cpu.heights[0]);
expect(gpu.heights[1]).toBeLessThan(cpu.heights[1]);
const cpuHeight = cpu.heights[1];
assert.ok(cpuHeight !== undefined, "CPU has a second point");
expect(gpu.heights[1]).toBeLessThan(cpuHeight);
});

test("keeps a lone series in the order the query returned it (#930)", async ({
Expand All @@ -322,7 +329,7 @@ test("keeps a lone series in the order the query returned it (#930)", async ({
const chart = await renderChart(page, "out-of-order");

expect(chart.failures).toEqual([]);
expect(chart.series[0].points).toEqual([
expect(chart.series[0]?.points).toEqual([
["Q3", 3],
["Q1", 1],
["Q2", 2],
Expand All @@ -335,12 +342,12 @@ test("orders by name the categories two bar series do not share (#951)", async (
const chart = await renderChart(page, "disjoint-categories");

expect(chart.failures).toEqual([]);
expect(chart.series[0].points).toEqual([
expect(chart.series[0]?.points).toEqual([
["X1", 0],
["X2", 10],
["X3", 30],
]);
expect(chart.series[1].points).toEqual([
expect(chart.series[1]?.points).toEqual([
["X1", 25],
["X2", 20],
["X3", 0],
Expand All @@ -353,7 +360,7 @@ test("leaves the points of a chart that does not stack alone", async ({
const chart = await renderChart(page, "unstacked-time-series");

expect(chart.failures).toEqual([]);
expect(chart.series[1].points).toEqual([
expect(chart.series[1]?.points).toEqual([
["2024-01-01T00:01:00.000Z", 50],
["2024-01-01T00:02:00.000Z", 50],
["2024-01-01T00:03:00.000Z", 50],
Expand All @@ -364,7 +371,7 @@ test("stacks a bar series on the categories it skipped", async ({ page }) => {
const chart = await renderChart(page, "stacked-categories");

expect(chart.failures).toEqual([]);
expect(chart.series[1].points).toEqual([
expect(chart.series[1]?.points).toEqual([
["Q1", 0],
["Q2", 20],
["Q3", 30],
Expand All @@ -377,7 +384,7 @@ test("lines an unstacked series up with the categories it skipped", async ({
const chart = await renderChart(page, "line-categories");

expect(chart.failures).toEqual([]);
expect(chart.series[1].points).toEqual([
expect(chart.series[1]?.points).toEqual([
["Q1", null],
["Q2", 20],
["Q3", 30],
Expand All @@ -389,6 +396,7 @@ test("draws nothing where an unstacked series has no value", async ({
}) => {
const chart = await renderChart(page, "line-categories");
const [a, b] = chart.drawnPerSeries;
assert.ok(a && b, "Both series were drawn");

expect(a.lefts).toHaveLength(3);
expect(b.lefts).toEqual(a.lefts.slice(1));
Expand All @@ -397,8 +405,9 @@ test("draws nothing where an unstacked series has no value", async ({
test("keeps a measured zero apart from a missing value", async ({ page }) => {
const chart = await renderChart(page, "zero-and-missing");
const [a, b] = chart.drawnPerSeries;
assert.ok(a && b, "Both series were drawn");

expect(chart.series[1].points).toEqual([
expect(chart.series[1]?.points).toEqual([
["Q1", null],
["Q2", 0],
["Q3", 30],
Expand All @@ -413,7 +422,11 @@ for (const type of ["area", "scatter", "heatmap"]) {
const chart = await renderChart(page, `${type}-categories`);

expect(chart.failures).toEqual([]);
expect(chart.series[1].points.map((p) => p[0])).toEqual(["Q1", "Q2", "Q3"]);
expect(chart.series[1]?.points.map((p) => p[0])).toEqual([
"Q1",
"Q2",
"Q3",
]);
});
}

Expand All @@ -431,7 +444,7 @@ test("keeps the bubble size of the points it lined up", async ({ page }) => {
const chart = await renderChart(page, "bubble-categories");

expect(chart.failures).toEqual([]);
expect(chart.series[1].points).toEqual([
expect(chart.series[1]?.points).toEqual([
["Q1", null],
["Q2", 5],
]);
Expand Down
2 changes: 1 addition & 1 deletion tests/end-to-end/official-site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ test("stacked chart raises a series only where it has a value", async ({
const gpu = await drawnPoints(powerChart, "GPU");

expect([gpu[0], gpu[3]]).toEqual([cpu[0], cpu[3]]);
expect(Number(gpu[1].y)).toBeLessThan(Number(cpu[1].y));
expect(Number(gpu[1]?.y)).toBeLessThan(Number(cpu[1]?.y));
});

test("chart draws a yline as a line and a yline_end as a band", async ({
Expand Down
Loading
Loading