From b1051b6b8bd727e298e8f6eff926f66be84045e8 Mon Sep 17 00:00:00 2001 From: 81reap Date: Sun, 9 Aug 2026 14:32:35 -0400 Subject: [PATCH] feat(chart) :: turn a reference line into a band --- CHANGELOG.md | 3 +-- .../sqlpage/migrations/01_documentation.sql | 18 +++++++++++------- sqlpage/apexcharts.js | 12 ++++++++---- sqlpage/templates/chart.handlebars | 4 ++-- tests/end-to-end/official-site.spec.ts | 18 ++++++++++++++---- 5 files changed, 36 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf53785d..3b9c0a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,7 @@ - `sqlpage.fetch_with_meta` now correctly documents server JSON responses sent under `json_body`, not `body`. - Datagrid rows with an icon or image no longer display an unnecessary en-dash placeholder, and an explicitly empty description remains empty. - Tooltip title text is now inhertis the same colour as the tooltip text. - - Charts can display reference lines. A row with a `yline` is drawn as a line across the chart at that value of the y axis, with the row's `label` and `color` for its text and its color. Reference lines are rows, so a chart can have as many of them as the query returns. A line follows its axis, so on a `horizontal` bar chart a `yline` is drawn down the chart rather than across it. They are not added to the total of a `stacked` chart, and are not filled in an `area` chart. - - Charts can display reference lines. A row with a `yline` is drawn as a line across the chart at that value of the y axis, and a row with `xline` marks a position on the x axis. `label` and `color` set the line's text and its color. Reference lines are rows, so a chart can have as many of them as the query returns. A line follows its axis, so on a `horizontal` bar chart a `yline` is drawn down the chart rather than across it. They are not added to the total of a `stacked` chart, and are not filled in an `area` chart. + - Charts can display reference lines. A row with a `yline` is drawn as a line across the chart at that value of the y axis, and a row with an `xline` marks a position on the x axis. Adding `yline_end` or `xline_end` makes a line a band, and the row's `label` and `color` set its text and its color. Reference lines are rows, so a chart can have as many of them as the query returns. Each one follows its own axis, so on a `horizontal` bar chart a `yline` is drawn down the chart rather than across it. They are not added to the total of a `stacked` chart, and are not filled in an `area` chart. ## v0.45 diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index 97a960b7..30061923 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -689,7 +689,9 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('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), ('yline', 'Draws a reference line across the chart at this value of the y axis instead of plotting a point, to show a limit such as a quota or an alarm threshold. Not drawn if it falls outside of the axis, so set ymax when the limit is above the data.', 'REAL', FALSE, TRUE), + ('yline_end', 'Makes the yline a band instead of a line, reaching to this value.', 'REAL', FALSE, TRUE), ('xline', 'Draws a reference line across the chart at this position of the x axis instead of plotting a point, to mark an event such as a deployment. A date or a timestamp when time is set, otherwise one of the x values.', 'TEXT', FALSE, TRUE), + ('xline_end', 'Makes the xline a band instead of a line, reaching to this value.', 'TEXT', FALSE, TRUE), ('color', 'The name of a color for the reference line this row draws. Grey by default.', 'COLOR', FALSE, TRUE) ) x; INSERT INTO example(component, description, properties) VALUES @@ -800,7 +802,7 @@ The `color` property sets the color of each series separately, in order. A row with a `yline` is not plotted as a data point, but drawn as a line across the whole chart, at that value of the y axis. Use it for the limit that the data should be read against: a disk quota, an alarm threshold, a service level -objective. +objective. Add `yline_end` to make it a band instead of a line. Reference lines are rows, so they come from a query like everything else, and a chart can have as many of them as the query returns: @@ -820,7 +822,7 @@ so set `ymax` when the limit is above the data. {"component":"chart", "title": "CPU temperature", "type": "line", "time": true, "ytitle": "°C", "ymax": 100, "color": "azure", "marker": 4}, {"yline": 70, "label": "target", "color": "green"}, - {"yline": 90, "label": "throttling", "color": "red"}, + {"yline": 90, "yline_end": 100, "label": "throttling", "color": "red"}, {"x": "2024-05-01T08:00:00Z", "y": 52}, {"x": "2024-05-01T09:00:00Z", "y": 58}, {"x": "2024-05-01T10:00:00Z", "y": 71}, @@ -833,13 +835,14 @@ so set `ymax` when the limit is above the data. ## Marking events `xline` is the counterpart of `yline`: it marks a position on the x axis instead -of a value on the y axis, for a moment rather than a limit. A single query can -draw a whole log of them: +of a value on the y axis. On its own it marks a moment, like a deployment. +With `xline_end`, it covers everything in between, like an incident or a +maintenance window. A single query can draw a whole log of them: ```sql -select started_at as xline, summary as label, +select started_at as xline, ended_at as xline_end, summary as label, case severity when ''outage'' then ''red'' else ''orange'' end as color -from deployments where started_at > $since; +from incidents where started_at > $since; ``` When `time` is set, an `xline` is a date or a timestamp, written like the `x` of @@ -848,7 +851,8 @@ a data point. On a chart with text labels on the x axis, it is one of those labe {"component":"chart", "title": "Request latency", "type": "area", "time": true, "ytitle": "ms", "color": "blue-lt", "marker": 3}, {"xline": "2024-05-01T10:00:00Z", "label": "deploy", "color": "green"}, - {"xline": "2024-05-01T11:30:00Z", "label": "incident", "color": "red"}, + {"xline": "2024-05-01T11:30:00Z", "xline_end": "2024-05-01T13:00:00Z", + "label": "incident", "color": "red"}, {"x": "2024-05-01T08:00:00Z", "y": 120}, {"x": "2024-05-01T09:00:00Z", "y": 134}, {"x": "2024-05-01T10:00:00Z", "y": 128}, diff --git a/sqlpage/apexcharts.js b/sqlpage/apexcharts.js index 65db39cb..b078e346 100644 --- a/sqlpage/apexcharts.js +++ b/sqlpage/apexcharts.js @@ -134,15 +134,19 @@ sqlpage_chart = (() => { * @returns {object[]} apexcharts axis annotations */ function reference_lines(rows, column, axis, to_axis_value) { + const on_axis = (value) => { + if (value == null) return null; + const placed = to_axis_value(value); + return Number.isNaN(placed) ? null : placed; + }; return rows.flatMap((row) => { - const value = row[`${column}line`]; - if (value == null) return []; - const from = to_axis_value(value); - if (Number.isNaN(from)) return []; + const from = on_axis(row[`${column}line`]); + if (from == null) return []; const color = reference_color(row.color); return [ { [axis]: from, + [`${axis}2`]: on_axis(row[`${column}line_end`]), borderColor: color, fillColor: color, strokeDashArray: 4, diff --git a/sqlpage/templates/chart.handlebars b/sqlpage/templates/chart.handlebars index 337b433a..fcb7f9fe 100644 --- a/sqlpage/templates/chart.handlebars +++ b/sqlpage/templates/chart.handlebars @@ -42,8 +42,8 @@ {{~#if (gt @row_index 0)}},{{/if~}} {{~#if (or xline yline)~}} { - "xline": {{~stringify xline}}, - "yline": {{~stringify yline}}, + "xline": {{~stringify xline}}, "xline_end": {{~stringify xline_end}}, + "yline": {{~stringify yline}}, "yline_end": {{~stringify yline_end}}, "label": {{~stringify label}}, "color": {{~stringify color}} } {{~else~}} diff --git a/tests/end-to-end/official-site.spec.ts b/tests/end-to-end/official-site.spec.ts index e1a1bd3c..58d56956 100644 --- a/tests/end-to-end/official-site.spec.ts +++ b/tests/end-to-end/official-site.spec.ts @@ -76,7 +76,9 @@ test("stacked chart raises a series only where it has a value", async ({ expect(Number(gpu[1].y)).toBeLessThan(Number(cpu[1].y)); }); -test("chart draws a reference line for every yline", async ({ page }) => { +test("chart draws a yline as a line and a yline_end as a band", async ({ + page, +}) => { await page.goto(`${BASE}/documentation.sql?component=chart#component`); const temperature = page.locator(".card", { @@ -85,13 +87,18 @@ test("chart draws a reference line for every yline", async ({ page }) => { await expect(temperature.locator(".apexcharts-canvas")).toBeVisible(); const annotations = temperature.locator(".apexcharts-yaxis-annotations"); + const lines = annotations.locator("line"); + const bands = annotations.locator(".apexcharts-annotation-rect"); - await expect(annotations.locator("line")).toHaveCount(2); + await expect(lines).toHaveCount(1); + await expect(bands).toHaveCount(1); await expect(annotations.getByText("target")).toBeVisible(); await expect(annotations.getByText("throttling")).toBeVisible(); }); -test("chart draws a reference line for every xline", async ({ page }) => { +test("chart draws an xline as a line and an xline_end as a band", async ({ + page, +}) => { await page.goto(`${BASE}/documentation.sql?component=chart#component`); const latency = page.locator(".card", { @@ -100,8 +107,11 @@ test("chart draws a reference line for every xline", async ({ page }) => { await expect(latency.locator(".apexcharts-canvas")).toBeVisible(); const annotations = latency.locator(".apexcharts-xaxis-annotations"); + const lines = annotations.locator("line"); + const bands = annotations.locator(".apexcharts-annotation-rect"); - await expect(annotations.locator("line")).toHaveCount(2); + await expect(lines).toHaveCount(1); + await expect(bands).toHaveCount(1); await expect(annotations.getByText("deploy")).toBeVisible(); await expect(annotations.getByText("incident")).toBeVisible(); });