diff --git a/CHANGELOG.md b/CHANGELOG.md index d713ec79..10ad050a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## v0.47.0 (unreleased) - **Mac users:** the downloadable `sqlpage-macos.tgz` now runs natively on Apple silicon (M-series Macs) and no longer runs on Intel Macs. Homebrew remains the recommended and easiest installation method. On an Intel Mac, [install Homebrew](https://brew.sh/) if needed, then run `brew install sqlpage` (or `brew update` followed by `brew upgrade sqlpage` if you already installed it with Homebrew). Open Terminal in your existing website folder and run `sqlpage` instead of `./sqlpage.bin`; keep your SQL files, database, and `sqlpage` configuration folder in place. Intel installations may build from source and take longer; see the [macOS installation guide](https://sql-page.com/your-first-sql-website/?os=macos#download) for setup and older macOS requirements. -- Chart data points can now include a `link`. Clicking a point opens that URL; when the tooltip has a text x value as its title, that title links to it too. The tooltip remains open while the pointer moves onto the link. ApexCharts was updated to [v7.6.0](https://github.com/apexcharts/apexcharts.js/releases/tag/v7.6.0) to support this. +- Chart data points can now include a `link`. Clicking a point or its tooltip value opens that URL; a text x value in the tooltip title links to it too. The tooltip remains open while the pointer moves onto the link. ApexCharts was updated to [v7.6.0](https://github.com/apexcharts/apexcharts.js/releases/tag/v7.6.0) to support this. - Updated sqlx-oldapi to v0.6.57 to fix SQL Server fallback expressions such as `ISNULL($missing, 'default')` truncating defaults or failing for date values when the bound variable is `NULL`. - Fixed MSSQL `JSON_OBJECT('key': value)` expressions being rejected by SQLPage's parser, including when used in `SET` statements or nested in `sqlpage.*` function calls. - OIDC now checks both normalized request paths and their resolved SQL files against protected prefixes, closing authentication bypasses through path and clean-URL aliases. Nonce verification also rejects provider-returned Argon2 parameters outside SQLPage's fixed low-cost profile before hashing. diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index ccb180e4..632c1d2d 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -685,7 +685,7 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('x', 'The value of the point on the horizontal axis. Numeric values use continuous, proportionate positioning; text values are evenly spaced categories. Set the top-level time property for dates and timestamps.', 'REAL', FALSE, FALSE), ('y', 'The value of the point on the vertical axis', 'REAL', FALSE, FALSE), ('z', 'A third value carried by the point. Used as the bubble radius in a bubble chart, and shown in the tooltip under the name given by the top-level "ztitle".', 'REAL', FALSE, TRUE), - ('link', 'A URL to open when the data point is clicked. When the tooltip has a text x value as its title, that title also links to the point.', 'URL', FALSE, TRUE), + ('link', 'A URL to open when the data point is clicked. The tooltip value also links to the point, as does a text x value when shown as the tooltip title.', 'URL', FALSE, TRUE), ('label', 'An alias for parameter "x". On a row that draws a reference line, the text to display next to the line.', 'TEXT', 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), diff --git a/frontend/src/apexcharts.ts b/frontend/src/apexcharts.ts index eb676375..21af2f1f 100644 --- a/frontend/src/apexcharts.ts +++ b/frontend/src/apexcharts.ts @@ -25,7 +25,7 @@ type TooltipArgs = { w: any; }; -function formatTooltipX( +function linkTooltipValue( value: string | number | null, link: string | undefined, ) { @@ -332,21 +332,29 @@ const sqlpage_chart = (() => { formatter: has_point_links && text_x_values ? (value, args) => - formatTooltipX(value, args?.w && pointLink(args, points)) + linkTooltipValue(value, args?.w && pointLink(args, points)) : undefined, }, y: { - formatter: (value) => { + formatter: (value, args) => { if (value == null) return ""; + let formatted: string; if (is_timeseries && chart_type === "rangeBar") { const d = new Date(value); - if (d.getHours() === 0 && d.getMinutes() === 0) - return d.toLocaleDateString(); - return d.toLocaleString(); + formatted = + d.getHours() === 0 && d.getMinutes() === 0 + ? d.toLocaleDateString() + : d.toLocaleString(); + } else { + formatted = value.toLocaleString(undefined, { + maximumFractionDigits: 2, + }); } - return value.toLocaleString(undefined, { - maximumFractionDigits: 2, - }); + const w = args?.w || args; + return linkTooltipValue( + formatted, + w?.config && pointLink({ ...args, w }, points), + ); }, }, }, @@ -404,9 +412,11 @@ const sqlpage_chart = (() => { const formatter = axis === "y" && w.config.tooltip.y.formatter; const format = (v) => formatter ? formatter(v, { seriesIndex, dataPointIndex, w }) : v; - valueSpan.innerText = Array.isArray(value) + const formatted = Array.isArray(value) ? value.map(format).join(" - ") : format(value); + if (axis === "y" && point.link) valueSpan.innerHTML = formatted; + else valueSpan.innerText = formatted; axisValue.appendChild(valueSpan); tooltip.appendChild(axisValue); } diff --git a/tests/end-to-end/fixtures/chart/test.ts b/tests/end-to-end/fixtures/chart/test.ts index 11bff5fc..1beecb12 100644 --- a/tests/end-to-end/fixtures/chart/test.ts +++ b/tests/end-to-end/fixtures/chart/test.ts @@ -172,7 +172,10 @@ test("linked text x labels keep the native bar tooltip", async ({ page }) => { await expect( tooltip.locator(".apexcharts-tooltip-series-group.apexcharts-active"), ).toHaveCount(1); - await tooltip.locator("a").click(); + await expect( + tooltip.locator(".apexcharts-tooltip-text-y-value a"), + ).toHaveAttribute("href", "/linked.sql"); + await tooltip.locator(".apexcharts-tooltip-title a").click(); await expect(page).toHaveURL(/\/linked\.sql$/); }); @@ -188,6 +191,32 @@ test("linked date x values retain ApexCharts' native date formatting", async ({ await expect(title).toBeVisible(); await expect(title).not.toContainText(/\d{13}/); await expect(title.locator("a")).toHaveCount(0); + await expect( + page.locator("#test-chart .apexcharts-tooltip-text-y-value a"), + ).toHaveAttribute("href", "/linked.sql"); +}); + +test("pie values link to their respective points", async ({ page }) => { + await renderChart(page, "link-pie"); + const slices = page.locator("#test-chart .apexcharts-pie-area"); + const link = page.locator( + "#test-chart .apexcharts-tooltip-series-group.apexcharts-active .apexcharts-tooltip-text-y-value a", + ); + await slices.nth(0).hover(); + await expect(link).toHaveAttribute("href", "/linked.sql"); + await slices.nth(1).hover(); + await expect(link).toHaveAttribute("href", "/linked-too.sql"); +}); + +test("the custom scatter tooltip renders a linked value", async ({ page }) => { + await renderChart(page, "link-scatter"); + await page + .locator("#test-chart .apexcharts-marker") + .first() + .hover({ force: true }); + await expect( + page.locator("#test-chart .apexcharts-tooltip-text-y-value a"), + ).toHaveAttribute("href", "/linked.sql"); }); test("positions complete numeric bar series on an explicit numeric axis (#733)", async ({