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
20 changes: 19 additions & 1 deletion examples/official-site/sqlpage/migrations/01_documentation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,8 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S
('y', 'The value of the point on the vertical axis', 'REAL', FALSE, FALSE),
('label', 'An alias for parameter "x"', 'REAL', FALSE, TRUE),
('value', 'An alias for parameter "y"', 'REAL', FALSE, TRUE),
('series', 'If multiple series are represented and share the same y-axis, this parameter can be used to distinguish between them.', 'TEXT', FALSE, TRUE)
('series', 'If multiple series are represented and share the same y-axis, this parameter can be used to distinguish between them.', 'TEXT', FALSE, TRUE),
('link', 'URL opened when user clicks this datapoint or slice.', 'URL', FALSE, TRUE)
) x;
INSERT INTO example(component, description, properties) VALUES
('chart', 'An area chart representing a time series, using the top-level property `time`.
Expand All @@ -692,6 +693,23 @@ INSERT INTO example(component, description, properties) VALUES
('chart', 'A basic bar chart', json('[
{"component":"chart", "type": "bar", "title": "Quarterly Results", "horizontal": true, "labels": true},
{"label": "Tom", "value": 35}, {"label": "Olive", "value": 15}]')),
('chart', 'A bar chart with clickable datapoints. Each row can set a `link` URL; clicking a datapoint opens that URL.', json('[
{"component":"chart", "type": "bar", "title": "Linked Sales", "labels": true},
{"label": "North", "value": 35, "link": "/documentation.sql?component=table"},
{"label": "South", "value": 22},
{"label": "West", "value": 41, "link": "/documentation.sql?component=map"}
]')),
('chart', 'A pie chart with per-slice links.', json('[
{"component":"chart", "title": "Linked Answers", "type": "pie", "labels": true},
{"label": "Yes", "value": 65, "link": "/documentation.sql?component=form"},
{"label": "No", "value": 35, "link": "/documentation.sql?component=table"}
]')),
('chart', 'A bubble chart demonstrating `z` and `link` metadata together on points.',
json('[
{"component":"chart", "title": "Bubbles with links", "type": "bubble", "ztitle": "Population", "marker": 8},
{"series": "Europe", "x": 2.1, "y": 18.5, "z": 742, "link": "/documentation.sql?component=chart"},
{"series": "Asia", "x": 5.2, "y": 24.1, "z": 4700, "link": "/documentation.sql?component=map"}
]')),
Comment on lines +696 to +712
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding new examples, add links to the existing examples where it makes the most sense. Update the tests accordingly and run them

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex Instead of adding new examples, add links to the existing examples where it makes the most sense. Update the tests accordingly and run them

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Folded chart link usage into the existing Answers pie chart and Quarterly Results bar chart documentation examples, instead of keeping separate linked-example entries. examples/official-site/sqlpage/migrations/01_documentation.sqlL690-L696
  • Updated chart point parsing to throw a TypeError immediately when the 4th tuple slot is not an object, matching the review request to error instead of warn. sqlpage/apexcharts.jsL121-L135
  • Retargeted the end-to-end coverage to those existing chart examples and disambiguated the duplicate Quarterly Results headings with exact role matching so the updated tests pass. tests/end-to-end/official-site.spec.tsL27-L60
  • Committed the follow-up changes on the current branch as 2adc7e1 and recorded a follow-up PR title/body with the requested summary.

Testing

  • npx biome format --write sqlpage/apexcharts.js tests/end-to-end/official-site.spec.ts

  • npx biome check sqlpage/apexcharts.js tests/end-to-end/official-site.spec.ts

  • cargo run (from examples/official-site)

  • npx playwright install chromium (from tests/end-to-end)

  • npx playwright install-deps chromium (from tests/end-to-end)

  • npm run test -- official-site.spec.ts -g 'chart point links|chart links - no-link datapoint' (from tests/end-to-end)

  • ⚠️ Screenshot not captured because the browser screenshot tool was not available in this environment.

View task →

('chart', 'A TreeMap Chart allows you to display hierarchical data in a nested layout. This is useful for visualizing the proportion of each part to the whole.',
json('[
{"component":"chart", "type": "treemap", "title": "Quarterly Results By Region (in k$)", "labels": true },
Expand Down
77 changes: 68 additions & 9 deletions sqlpage/apexcharts.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ sqlpage_chart = (() => {
);
const isDarkTheme = document.body?.dataset?.bsTheme === "dark";

/** @typedef { { [name:string]: {data:{x:number|string|Date,y:number}[], name:string} } } Series */
/** @typedef { { [name:string]: {data:{x:number|string|Date,y:number,z?:number|string,l?:string}[], name:string} } } Series */

/**
* Aligns series data points by their x-axis categories, ensuring all series have data points
Expand Down Expand Up @@ -78,19 +78,37 @@ sqlpage_chart = (() => {
series_idxs.splice(series_idxs.indexOf(idx_of_xmin), 1);
}
}
// Create a map of category -> value for each series and rebuild
// Create a map of category -> point for each series and rebuild
return series.map((s) => {
const valueMap = new Map(s.data.map((point) => [point.x, point.y]));
const valueMap = new Map(s.data.map((point) => [point.x, point]));
return {
name: s.name,
data: Array.from(categoriesSet, (category) => ({
x: category,
y: valueMap.get(category) || 0,
})),
data: Array.from(
categoriesSet,
(category) =>
valueMap.get(category) || {
x: category,
y: 0,
},
),
};
});
}

function resolvePointLink(data, opts, pieLinks) {
if (opts.dataPointIndex == null || opts.dataPointIndex < 0)
return undefined;
if (data.type === "pie") return pieLinks[opts.dataPointIndex];
if (opts.seriesIndex == null || opts.seriesIndex < 0) return undefined;
const series = opts.w?.config?.series?.[opts.seriesIndex];
const point = series?.data?.[opts.dataPointIndex];
return point?.l;
}

function navigateIfLink(link) {
if (typeof link === "string" && link) window.location.href = link;
}

/** @param {HTMLElement} c */
function build_sqlpage_chart(c) {
const [data_element] = c.getElementsByTagName("data");
Expand All @@ -100,7 +118,27 @@ sqlpage_chart = (() => {
const is_timeseries = !!data.time;
/** @type { Series } */
const series_map = {};
for (const [name, old_x, old_y, z] of data.points) {
const pieLinks = [];
let warnedLegacyPointMetadata = false;
for (const point of data.points) {
const [name, old_x, old_y] = point;
let meta;
if (point.length === 4) {
if (
typeof point[3] === "object" &&
point[3] != null &&
!Array.isArray(point[3])
) {
meta = point[3];
} else if (!warnedLegacyPointMetadata) {
warnedLegacyPointMetadata = true;
console.warn(
"Chart point metadata must be an object in the 4th tuple slot. Legacy formats are ignored.",
);
}
Comment on lines +133 to +138
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no warning just throw an error

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex no warning, just throw

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Updated chart point metadata parsing so a 4th tuple slot must be an object; otherwise the chart code now throws a TypeError immediately instead of logging a one-time warning and continuing. sqlpage/apexcharts.jsL121-L139
  • Removed the legacy warning-state bookkeeping because invalid metadata now fails fast on the offending datapoint. sqlpage/apexcharts.jsL121-L135
  • Committed the follow-up change on the current branch as bca97e7 and created a PR message titled Follow-up: throw on invalid chart metadata tuples.

Testing

  • npm run format
  • npm test

View task →

}
const z = meta?.z;
const link = meta?.l;
series_map[name] = series_map[name] || { name, data: [] };
let x = old_x;
let y = old_y;
Expand All @@ -110,7 +148,11 @@ sqlpage_chart = (() => {
y = y.map((y) => new Date(y).getTime());
else x = new Date(x);
}
series_map[name].data.push({ x, y, z });
const seriesPoint = { x, y };
if (z != null) seriesPoint.z = z;
if (link != null) seriesPoint.l = link;
series_map[name].data.push(seriesPoint);
pieLinks.push(link);
}
if (data.xmin == null) data.xmin = undefined;
if (data.xmax == null) data.xmax = undefined;
Expand All @@ -135,6 +177,16 @@ sqlpage_chart = (() => {
series = align_categories(series);

const chart_type = data.type || "line";
let skipNextChartClick = false;
const onDataPointInteraction = (_event, _chartContext, opts) => {
const link = resolvePointLink(data, opts, pieLinks);
if (!link) return;
skipNextChartClick = true;
navigateIfLink(link);
setTimeout(() => {
skipNextChartClick = false;
}, 0);
};
const options = {
chart: {
type: chart_type,
Expand All @@ -151,6 +203,13 @@ sqlpage_chart = (() => {
zoom: {
enabled: false,
},
events: {
dataPointSelection: onDataPointInteraction,
click: (event, chartContext, opts) => {
if (skipNextChartClick) return;
onDataPointInteraction(event, chartContext, opts);
},
},
},
theme: {
palette: "palette4",
Expand Down
10 changes: 9 additions & 1 deletion sqlpage/templates/chart.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@
{{~ stringify (default series (default ../title "")) ~}},
{{~ stringify (default x label) ~}},
{{~ stringify (default y value) ~}}
{{~#if z}}, {{~ stringify z ~}} {{~/if~}}
{{~#if (or (or z (eq z 0)) link)~}}, {
{{~#if (or z (eq z 0))~}}
"z": {{~ stringify z ~}}
{{~#if link~}},{{/if~}}
{{~/if~}}
{{~#if link~}}
"l": {{~ stringify link ~}}
{{~/if~}}
}{{~/if~}}
]
{{~/each_row~}}
]
Expand Down
37 changes: 37 additions & 0 deletions tests/end-to-end/official-site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,43 @@ test("chart", async ({ page }) => {
await expect(page.locator(".apexcharts-canvas").first()).toBeVisible();
});

test("chart point links - bar", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=chart`);
const linkedBarCard = page.locator(".card", {
has: page.getByRole("heading", { name: "Linked Sales" }),
});
await expect(linkedBarCard.locator(".apexcharts-canvas")).toBeVisible();
await linkedBarCard
.locator(".apexcharts-series path, .apexcharts-series rect")
.first()
.click();
await expect(page).toHaveURL(/component=table/);
});

test("chart point links - pie", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=chart`);
const linkedPieCard = page.locator(".card", {
has: page.getByRole("heading", { name: "Linked Answers" }),
});
await expect(linkedPieCard.locator(".apexcharts-canvas")).toBeVisible();
await linkedPieCard.locator(".apexcharts-pie-series path").first().click();
await expect(page).toHaveURL(/component=form/);
});

test("chart links - no-link datapoint", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=chart`);
const linkedBarCard = page.locator(".card", {
has: page.getByRole("heading", { name: "Linked Sales" }),
});
await expect(linkedBarCard.locator(".apexcharts-canvas")).toBeVisible();
const initialUrl = page.url();
await linkedBarCard
.locator(".apexcharts-series path, .apexcharts-series rect")
.nth(1)
.click();
await expect(page).toHaveURL(initialUrl);
});

test("map", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=map#component`);
await expect(page.getByText("Loading...")).not.toBeVisible();
Expand Down
Loading