diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-line-1-webkit-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-line-1-webkit-linux.png index 0c089c92d1..56069e233b 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-line-1-webkit-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-line-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx index 9276a880cc..609c6eb233 100644 --- a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx +++ b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx @@ -445,9 +445,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await expect(svg).toHaveAttribute(attr, value); } - const path = svg.locator('path'); - - await expect(path).toHaveAttribute('aria-hidden', 'true'); + await expectEachToHaveAttribute(svg.locator('path'), 'aria-hidden', 'true'); }); await test.step('Verify data attributes and accessibility structure', async () => { @@ -512,10 +510,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { for (const [attr, value] of svgAttributes) { await expect(svg).toHaveAttribute(attr, value); } - - const path = svg.locator('path'); - - await expect(path).toHaveAttribute('aria-hidden', 'true'); + await expectEachToHaveAttribute(svg.locator('path'), 'aria-hidden', 'true'); }); }); }); diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx index 093947f576..b1d7bcd920 100644 --- a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx +++ b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx @@ -3,15 +3,6 @@ import { expect, test } from '@semcore/testing-utils/playwright'; import { loadPage } from '@semcore/testing-utils/shared/helpers'; import { TAG } from '@semcore/testing-utils/shared/tags'; -/** - * Asserts every match is kept out of the accessibility tree. - * - * `aria-hidden` on an ancestor hides the whole subtree, so checking the attribute on each - * element itself would fail the moment decorative shapes get wrapped in a group — which is - * exactly what happened when the hover line became a `` holding three `` children - * plus its end caps. What matters is that nothing here reaches a screen reader, not which - * node carries the attribute. - */ const expectEachToBeHiddenFromA11y = async (locator: Locator) => { await expect(locator).not.toHaveCount(0); @@ -49,6 +40,29 @@ export const locators = { tooltip: (page: Page) => page.locator('[data-ui-name="Line.Tooltip"], [data-ui-name="HoverLine.Tooltip"]'), }; +const BASIC_USAGE_STORY = 'stories/components/d3-chart/tests/examples/line-chart/basic-usage.tsx'; + +/** Hovers the middle of the plot, which lands on some point of the series. */ +const hoverPlotCentre = async (page: Page) => { + const plot = locators.plot(page).first(); + await plot.waitFor({ state: 'visible' }); + + const box = await plot.boundingBox(); + if (!box) throw new Error('Plot bounding box not found'); + + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); +}; + +const hoverFirstPoint = async (page: Page) => { + const plot = locators.plot(page).first(); + await plot.waitFor({ state: 'visible' }); + + const dot = await locators.dots(page, 0).boundingBox(); + if (!dot) throw new Error('First dot bounding box not found'); + + await page.mouse.move(dot.x + dot.width / 2, dot.y + dot.height / 2); +}; + /* ===================================================== @visual Visual states, hover and focus styles, paddings, margins, and snapshots. @@ -442,6 +456,33 @@ test.describe(`${TAG.VISUAL}`, () => { await expect(page).toHaveScreenshot(); }); }); + + /** + * The redesign's three data-level decorations in one shot: the dashed forecast tail, the + * gradient-filled potential tail, and the highlight rings. + * + * The functional tests next door already assert the wiring — that the dash attribute is + * there and that the gradient reference resolves to a real `linearGradient`. What no + * attribute can say is whether the gradient actually paints a visible ramp, whether the + * dash rhythm reads as a forecast, and whether the ring sits concentric with its dot. + * That is what this baseline is for. + * + * Deliberately one test rather than three, so the branch gains 3 PNGs instead of 9. + */ + test('Verify forecast, potential and highlighted dots render', { + tag: [TAG.PRIORITY_MEDIUM, '@line-chart', '@d3-chart'], + }, async ({ page }) => { + await loadPage( + page, + 'stories/components/d3-chart/tests/examples/line-chart/basic-usage.tsx', + 'en', + { dataType: 'both', highlightDots: 'mixed', duration: 0 }, + ); + await locators.plot(page).first().waitFor({ state: 'visible' }); + await page.waitForTimeout(500); + + await expect(page).toHaveScreenshot(); + }); }); /* ===================================================== @@ -466,6 +507,125 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); }); + /** + * Guards the redesign value rather than just letting the screenshots carry it: the data + * line went from 3 to 2. Snapshots do notice a change in line weight, but only as pixels + * that moved, and this branch regenerated 427 of them at once — exactly the setting in + * which a wrong stroke width gets baked into the baselines unnoticed. + * + * `stroke-width: 2` in the stylesheet is unitless, which in SVG means user units. The + * Plot carries no viewBox, so one user unit is one pixel and `toHaveCSS` reads back + * '2px'. + * + * Asserted on every line, not just the first: the forecast and potential segments added + * by the redesign are separate `SLine` paths, and they have to keep the same weight as + * the main line or the series visibly changes thickness partway along. + */ + test('Verify data line stroke width is 2px', { + tag: [TAG.PRIORITY_MEDIUM, '@line-chart', '@d3-chart'], + }, async ({ page }) => { + await loadPage( + page, + 'stories/components/d3-chart/tests/examples/line-chart/basic-usage.tsx', + 'en', + { dataType: 'both' }, + ); + await locators.plot(page).first().waitFor({ state: 'visible' }); + + const lines = page.locator('path[data-ui-name="Line"]'); + await expect(lines).not.toHaveCount(0); + + for (let i = 0; i < (await lines.count()); i++) { + await expect(lines.nth(i)).toHaveCSS('stroke-width', '2px'); + } + }); + + /** + * `DATA_TYPE` markers pull points out of the main line into their own segments + * (`Line.renderForecast` / `Line.renderPotential`), which is what lets them be dashed + * while the rest of the series stays solid. + * + * The point count is the part worth asserting: it is the only thing that distinguishes + * "the tail was moved into its own path" from "the tail is drawn twice, once in the main + * path and once on top of it". The two look identical in a screenshot. + * + * Only the dash is checked on the forecast segment, not its colour — see the note on + * `renderForecast` about the gradient it defines but never references. + */ + test('Verify forecast and potential render as separate dashed segments', { + tag: [TAG.PRIORITY_MEDIUM, '@line-chart', '@d3-chart'], + }, async ({ page }) => { + await loadPage( + page, + 'stories/components/d3-chart/tests/examples/line-chart/basic-usage.tsx', + 'en', + { dataType: 'both', duration: 0 }, + ); + await locators.plot(page).first().waitFor({ state: 'visible' }); + + const lines = page.locator('svg[data-ui-name="Plot"] path'); + + await test.step('Each series renders a main, a forecast and a potential path', async () => { + // Two series in the story dataset, three paths each. + await expect(lines).toHaveCount(6); + }); + + await test.step('The tail segments are dashed and the main line is not', async () => { + const dashed = await lines.evaluateAll((paths) => + paths.map((path) => path.getAttribute('stroke-dasharray')), + ); + + expect(dashed.filter((value) => value === '4 4')).toHaveLength(4); + expect(dashed.filter((value) => value === null)).toHaveLength(2); + }); + + await test.step('The main line stops before the tail instead of drawing it twice', async () => { + // The generator is curveLinear, so `d` is "M x,y L x,y …" and the number of `L` + // commands is one less than the number of points on that path. + const pointCounts = await lines.evaluateAll((paths) => + paths + .map((path) => path.getAttribute('d')) + .filter((d): d is string => Boolean(d)) + .map((d) => (d.match(/L/g)?.length ?? 0) + 1), + ); + + expect(pointCounts.filter((count) => count === 15)).toHaveLength(2); + expect(pointCounts.filter((count) => count === 3)).toHaveLength(2); + expect(pointCounts.filter((count) => count === 4)).toHaveLength(2); + }); + }); + + /** + * The potential segment is the one place in Line that paints with a gradient rather than + * the series colour, so the reference has to resolve — a typo in the id would leave the + * path silently unpainted, which a screenshot shows as "the line is missing" without + * saying why. + * + * The id is built from a generated `uid`, so it cannot be hardcoded: the reference is + * read off the path and the gradient is then looked up by it. + */ + test('Verify the potential segment is painted by a gradient that exists', { + tag: [TAG.PRIORITY_MEDIUM, '@line-chart', '@d3-chart'], + }, async ({ page }) => { + await loadPage( + page, + 'stories/components/d3-chart/tests/examples/line-chart/basic-usage.tsx', + 'en', + { dataType: 'potential', duration: 0 }, + ); + await locators.plot(page).first().waitFor({ state: 'visible' }); + + const potential = page + .locator('svg[data-ui-name="Plot"] path[stroke-dasharray="4 4"][d]') + .first(); + const stroke = await potential.evaluate((el) => getComputedStyle(el).stroke); + + expect(stroke).toMatch(/^url\(".*-potential-gradient-line"\)$/); + + const gradientId = stroke.slice('url("#'.length, -'")'.length); + await expect(page.locator(`linearGradient[id="${gradientId}"]`)).toHaveCount(1); + }); + test('Verify Line.Null attributes', { tag: [TAG.PRIORITY_MEDIUM, '@line-chart', '@d3-chart'], }, async ({ page }) => { @@ -483,8 +643,54 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); }); + test('Verify Line.Null is painted with the null-series palette colour', { + tag: [TAG.PRIORITY_MEDIUM, '@line-chart', '@d3-chart'], + }, async ({ page }) => { + await loadPage( + page, + 'stories/components/d3-chart/tests/examples/line-chart/line-area-with-empty.tsx', + 'en', + ); + await locators.plot(page).first().waitFor({ state: 'visible' }); + + await expect(locators.lineNull(page).first()).toHaveCSS('stroke', 'oklch(0.9 0.002 177)'); + await expect(locators.lineNull(page).first()).toHaveCSS('stroke-dasharray', '4px'); + }); + + test('Verify interpolated points are dropped from the series without breaking it', { + tag: [TAG.PRIORITY_MEDIUM, '@line-chart', '@d3-chart'], + }, async ({ page }) => { + await loadPage(page, BASIC_USAGE_STORY, 'en', { withInterpolatedGaps: true, duration: 0 }); + await locators.plot(page).first().waitFor({ state: 'visible' }); + + const lines = page.locator('path[data-ui-name="Line"]'); + await expect(lines).toHaveCount(2); + + const pointCounts = await lines.evaluateAll((paths) => + paths + .map((path) => path.getAttribute('d')) + .filter((d): d is string => Boolean(d)) + .map((d) => (d.match(/L/g)?.length ?? 0) + 1), + ); + + await test.step('The untouched series keeps all 20 points', async () => { + expect(pointCounts).toContain(20); + }); + + await test.step('The gapped series is short by exactly the three marked points', async () => { + // A gap drops the point from the path; the line closes over it rather than breaking, + // so this stays a single path of 17 points instead of several segments. + expect(pointCounts).toContain(17); + }); + + await test.step('No gap leaks into the geometry as NaN', async () => { + const ds = await lines.evaluateAll((paths) => paths.map((path) => path.getAttribute('d') ?? '')); + + expect(ds.filter((d) => d.includes('NaN'))).toEqual([]); + }); + }); + /* ----------------------------------------------------- - Responsiveness — high-level Chart.* are now responsive by default. plotWidth/plotHeight are optional: when omitted the chart fills its parent and auto-derives the plot size. `aspect` sets height = width / aspect, `hMin`/`hMax` clamp it, `onResize` reports the measured size. We assert on diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-forecast-potential-and-highlighted-dots-render-1-chromium-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-forecast-potential-and-highlighted-dots-render-1-chromium-linux.png new file mode 100644 index 0000000000..a071ddcf8a Binary files /dev/null and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-forecast-potential-and-highlighted-dots-render-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-forecast-potential-and-highlighted-dots-render-1-firefox-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-forecast-potential-and-highlighted-dots-render-1-firefox-linux.png new file mode 100644 index 0000000000..6433c31119 Binary files /dev/null and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-forecast-potential-and-highlighted-dots-render-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-forecast-potential-and-highlighted-dots-render-1-webkit-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-forecast-potential-and-highlighted-dots-render-1-webkit-linux.png new file mode 100644 index 0000000000..d74ab2ef5c Binary files /dev/null and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-forecast-potential-and-highlighted-dots-render-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-chromium-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-chromium-linux.png index ac25bc2907..c5c36e03be 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-chromium-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-firefox-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-firefox-linux.png index 01cfa1170e..813e562542 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-firefox-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-webkit-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-webkit-linux.png index 76531be927..c7ec9e28d5 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-webkit-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-All-features-enabled-standard-size-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-chromium-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-chromium-linux.png index f799bf2cc9..dcb5101141 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-chromium-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-firefox-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-firefox-linux.png index 0ce5ce742e..3781d69999 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-firefox-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-webkit-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-webkit-linux.png index 101de2b309..1835d4b9fc 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-webkit-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-axis-small-size-no-tooltip-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-chromium-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-chromium-linux.png index 9536ac087c..02cec27f97 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-chromium-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-firefox-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-firefox-linux.png index 47eff4d98c..5aa8062a88 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-firefox-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-webkit-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-webkit-linux.png index 750f9090f1..97349dc3cb 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-webkit-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Inverted-large-margins-patterns-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-chromium-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-chromium-linux.png index cb7b62450c..f269e2551e 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-chromium-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-firefox-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-firefox-linux.png index 9fbcffc9fc..e3ca39b705 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-firefox-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-webkit-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-webkit-linux.png index 160f3add52..075f45b7cd 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-webkit-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Large-size-minimal-margins-no-axes-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-chromium-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-chromium-linux.png index 742a968f54..faca9e50ee 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-chromium-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-firefox-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-firefox-linux.png index 4d055cbbc1..8986ad1aeb 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-firefox-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-webkit-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-webkit-linux.png index 8aba9f006a..6590aacbf2 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-webkit-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-Minimal-features-medium-size-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-chromium-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-chromium-linux.png index 1071a0f609..1c0b713710 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-chromium-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-firefox-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-firefox-linux.png index a2a221dd9f..07dd2ecabb 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-firefox-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-webkit-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-webkit-linux.png index 6e268f3c0d..cec3d8d1d7 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-webkit-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-line-chart-with-config-No-legend-mixed-features-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-patterns-and-symbols-for-dots-keyboard-interactions-1-chromium-linux.png b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-patterns-and-symbols-for-dots-keyboard-interactions-1-chromium-linux.png index 38d4659369..09767b626e 100644 Binary files a/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-patterns-and-symbols-for-dots-keyboard-interactions-1-chromium-linux.png and b/semcore/d3-chart/__tests__/line-chart.browser-test.tsx-snapshots/-visual-Verify-patterns-and-symbols-for-dots-keyboard-interactions-1-chromium-linux.png differ diff --git a/semcore/d3-chart/src/Area.jsx b/semcore/d3-chart/src/Area.jsx index e82e692467..76c50e30ac 100644 --- a/semcore/d3-chart/src/Area.jsx +++ b/semcore/d3-chart/src/Area.jsx @@ -6,7 +6,7 @@ import { area, curveLinear, line } from 'd3-shape'; import React from 'react'; import AnimatedClipPath from './AnimatedClipPath'; -import { DATA_TYPE, FORECAST, POTENTIAL } from './component/Chart/AbstractChart.type'; +import { DATA_TYPE, FORECAST, POTENTIAL } from './component/Chart'; import { SvgElement } from './component/SvgElement'; import createElement from './createElement'; import Dots from './Dots'; @@ -25,32 +25,9 @@ import { interpolateValue, eventToPoint, invert, + getDataOfType, } from './utils'; -function getDataOfType(data, type, y) { - const result = []; - let prevPoint = null; - let insideRun = false; - - data.forEach((item) => { - const valid = item[y] !== interpolateValue; - - if (valid && item[DATA_TYPE] === type) { - if (!insideRun && prevPoint !== null) result.push(prevPoint); - - result.push(item); - - insideRun = true; - } else { - insideRun = false; - } - - if (valid) prevPoint = item; - }); - - return result; -} - class AreaRoot extends Component { static displayName = 'Area'; static style = style; @@ -188,7 +165,6 @@ class AreaRoot extends Component { d3, d3Line, uid, - size, duration, y, transparent, diff --git a/semcore/d3-chart/src/Line.jsx b/semcore/d3-chart/src/Line.jsx index fccf60e5d6..833dda6586 100644 --- a/semcore/d3-chart/src/Line.jsx +++ b/semcore/d3-chart/src/Line.jsx @@ -5,9 +5,11 @@ import { curveLinear, line as d3Line, area as d3Area, curveCardinal } from 'd3-s import React from 'react'; import AnimatedClipPath from './AnimatedClipPath'; +import { DATA_TYPE, FORECAST, POTENTIAL } from './component/Chart'; +import { SvgElement } from './component/SvgElement'; import createElement from './createElement'; import Dots from './Dots'; -import { resolvePatternDasharray } from './Pattern'; +import { ForecastGradient, PotentialGradient, resolvePatternDasharray, StrokeMask } from './Pattern'; import style from './style/line.shadow.css'; import { definedData, @@ -18,6 +20,7 @@ import { getChartDefaultColorName, eventToPoint, invert, + getDataOfType, } from './utils'; class LineRoot extends Component { @@ -94,6 +97,75 @@ class LineRoot extends Component { onClick(index, e); } + renderForecast() { + const SLine = SvgElement; + const { + styles, + hide, + color, + resolveColor, + uid, + d3, + duration, + y, + transparent, + } = this.asProps; + const data = getDataOfType(this.asProps.data, FORECAST, y); + + return sstyled(styles)( + <> + + + + , + ); + } + + renderPotential() { + const SLine = SvgElement; + const { + styles, + hide, + uid, + d3, + duration, + y, + transparent, + } = this.asProps; + const data = getDataOfType(this.asProps.data, POTENTIAL, y); + + return sstyled(styles)( + <> + + + , + ); + } + render() { const SLine = this.Element; const { @@ -110,7 +182,7 @@ class LineRoot extends Component { transparent, patterns, } = this.asProps; - const data = this.asProps.data.filter((item) => item[y] !== interpolateValue); + const data = this.asProps.data.filter((item) => item[y] !== interpolateValue && item[DATA_TYPE] !== FORECAST && item[DATA_TYPE] !== POTENTIAL); this.asProps.dataHintsHandler.specifyDataRowFields(x, y); this.asProps.dataHintsHandler.establishDataType('time-series'); @@ -119,6 +191,8 @@ class LineRoot extends Component { return sstyled(styles)( <> + {this.renderForecast()} + {this.renderPotential()} - + {area?.[item.id] && ( )} @@ -116,6 +118,10 @@ class LineChartComponent extends AbstractChart< protected getLegendAriaLabel(): string { return this.asProps.getI18nText('legendForChart', { chartType: 'Line' }); } + + protected displayDots = (i: number, isActive: boolean, noAround: boolean, data: ObjectData): boolean => { + return isActive || noAround || Boolean(data[HIGHLIGHT_DOT]); + }; } /** diff --git a/semcore/d3-chart/src/utils.ts b/semcore/d3-chart/src/utils.ts index 87c754f4a6..99d4b7bb65 100644 --- a/semcore/d3-chart/src/utils.ts +++ b/semcore/d3-chart/src/utils.ts @@ -13,6 +13,10 @@ import { } from 'd3-scale'; import type React from 'react'; +import type { FORECAST, POTENTIAL } from './component/Chart'; +import { DATA_TYPE } from './component/Chart'; +import type { ObjectData, ObjectDataKey } from './component/Chart/AbstractChart.type'; + export const eventToPoint = (event: React.MouseEvent, svgRoot: SVGElement) => { const node = (event.currentTarget || event.target) as HTMLElement; const rect = svgRoot.getBoundingClientRect(); @@ -341,3 +345,27 @@ type Events = { }; export const PlotEventEmitter = EventEmitter; + +export function getDataOfType(data: ObjectData[], type: typeof FORECAST | typeof POTENTIAL, y: ObjectDataKey) { + const result: ObjectData[] = []; + let prevPoint: ObjectData | null = null; + let insideRun = false; + + data.forEach((item) => { + const valid = item[y] !== interpolateValue; + + if (valid && item[DATA_TYPE] === type) { + if (!insideRun && prevPoint !== null) result.push(prevPoint); + + result.push(item); + + insideRun = true; + } else { + insideRun = false; + } + + if (valid) prevPoint = item; + }); + + return result; +} diff --git a/stories/components/d3-chart/__mocks__/line.ts b/stories/components/d3-chart/__mocks__/line.ts index 7731a9e4e6..aff40efad2 100644 --- a/stories/components/d3-chart/__mocks__/line.ts +++ b/stories/components/d3-chart/__mocks__/line.ts @@ -1,4 +1,4 @@ -import { interpolateValue } from '@semcore/ui/d3-chart'; +import { DATA_TYPE, FORECAST, GOOD, HIGHLIGHT_DOT, interpolateValue, POTENTIAL, INSIGHTFUL } from '@semcore/ui/d3-chart'; export default { Default: [ @@ -23,6 +23,50 @@ export default { { x: 18, y: 1 }, { x: 19, y: 10 }, ], + Forecast: [ + { x: 0, y: 2 }, + { x: 1, y: 4 }, + { x: 2, y: 6 }, + { x: 3, y: 8 }, + { x: 4, y: 5 }, + { x: 5, y: 7 }, + { x: 6, y: 3 }, + { x: 7, y: 9 }, + { x: 8, y: 7 }, + { x: 9, y: 10, [HIGHLIGHT_DOT]: GOOD } as const, + { x: 10, y: 2 }, + { x: 11, y: 4 }, + { x: 12, y: 6 }, + { x: 13, y: 8 }, + { x: 14, y: 5 }, + { x: 15, y: 7 }, + { x: 16, y: 3, [DATA_TYPE]: FORECAST } as const, + { x: 17, y: 9, [DATA_TYPE]: FORECAST } as const, + { x: 18, y: 1, [DATA_TYPE]: FORECAST } as const, + { x: 19, y: 10, [DATA_TYPE]: FORECAST } as const, + ], + Potential: [ + { x: 0, y: 2 }, + { x: 1, y: 4 }, + { x: 2, y: 6 }, + { x: 3, y: 8 }, + { x: 4, y: 5 }, + { x: 5, y: 7 }, + { x: 6, y: 3 }, + { x: 7, y: 9 }, + { x: 8, y: 7 }, + { x: 9, y: 10 }, + { x: 10, y: 2 }, + { x: 11, y: 4 }, + { x: 12, y: 6 }, + { x: 13, y: 8 }, + { x: 14, y: 5 }, + { x: 15, y: 7 }, + { x: 16, y: 3 }, + { x: 17, y: 4, [DATA_TYPE]: POTENTIAL } as const, + { x: 18, y: 6, [DATA_TYPE]: POTENTIAL } as const, + { x: 19, y: 8, [DATA_TYPE]: POTENTIAL, [HIGHLIGHT_DOT]: INSIGHTFUL } as const, + ], TwoLines: [ { x: 0, line1: 2, line2: 5 }, { x: 1, line1: 4, line2: 7 }, diff --git a/stories/components/d3-chart/docs/examples/line-chart/basic-usage.tsx b/stories/components/d3-chart/docs/examples/line-chart/basic-usage.tsx index 05650ce66a..af3a4af58e 100644 --- a/stories/components/d3-chart/docs/examples/line-chart/basic-usage.tsx +++ b/stories/components/d3-chart/docs/examples/line-chart/basic-usage.tsx @@ -1,3 +1,4 @@ +import { Flex } from '@semcore/ui/base-components'; import { Chart } from '@semcore/ui/d3-chart'; import React from 'react'; @@ -5,14 +6,33 @@ import LineMockData from '../../../__mocks__/line'; const Demo = () => { return ( - + + + + + ); }; diff --git a/stories/components/d3-chart/tests/examples/line-chart/__mocks__/index.ts b/stories/components/d3-chart/tests/examples/line-chart/__mocks__/index.ts index 80f87ec66d..c67ef6b87d 100644 --- a/stories/components/d3-chart/tests/examples/line-chart/__mocks__/index.ts +++ b/stories/components/d3-chart/tests/examples/line-chart/__mocks__/index.ts @@ -1,28 +1,56 @@ -import { BAD, GOOD, HIGHLIGHT_DOT, INSIGHTFUL, type LineChartProps, type ListData } from '@semcore/ui/d3-chart'; +import { + BAD, + DATA_TYPE, + FORECAST, + GOOD, + HIGHLIGHT_DOT, + INSIGHTFUL, + interpolateValue, + type LineChartProps, + type ListData, + POTENTIAL, +} from '@semcore/ui/d3-chart'; import { withUpdatedKeyAt } from '../../../../__mocks__/effects/withUpdatedKeyAt'; +import LineMockData from '../../../../__mocks__/line'; import { compose } from '../../../../__mocks__/utils/compose'; import { when } from '../../../../__mocks__/utils/when'; export type HighlightDotsMode = 'none' | 'good' | 'bad' | 'insightful' | 'mixed'; +export type DataTypeMode = 'none' | 'forecast' | 'potential' | 'both'; type PipelineOptions = { highlightDots?: HighlightDotsMode; + withInterpolatedGaps?: boolean; + dataType?: DataTypeMode; }; -export const base: LineChartProps['data'] = Array.from({ length: 20 }, (_, i) => ({ - x: i, - line1: Math.abs(Math.sin(Math.exp(i))) * 10, - line2: Math.abs(Math.cos(Math.exp(i))) * 10, -})); +/** The shared two-series dataset the docs line examples use: `x = 0..19`, `line1`/`line2`. */ +export const base: LineChartProps['data'] = LineMockData.TwoLines; + +const INTERPOLATED_AT = [6, 7, 12]; const withGoodDot = withUpdatedKeyAt(4, { [HIGHLIGHT_DOT]: GOOD }); const withBadDot = withUpdatedKeyAt(9, { [HIGHLIGHT_DOT]: BAD }); const withInsightfulDot = withUpdatedKeyAt(14, { [HIGHLIGHT_DOT]: INSIGHTFUL }); +const FORECAST_AT = [15, 16]; +const POTENTIAL_AT = [17, 18, 19]; + +const mark = (indices: number[], marker: typeof FORECAST | typeof POTENTIAL) => + compose(...indices.map((index) => withUpdatedKeyAt(index, { [DATA_TYPE]: marker }))); + export default (props: PipelineOptions, source: ListData = base): LineChartProps['data'] => compose( when(props.highlightDots === 'good' || props.highlightDots === 'mixed', withGoodDot), when(props.highlightDots === 'bad' || props.highlightDots === 'mixed', withBadDot), when(props.highlightDots === 'insightful' || props.highlightDots === 'mixed', withInsightfulDot), + when( + props.withInterpolatedGaps, + compose( + ...INTERPOLATED_AT.map((index) => withUpdatedKeyAt(index, { line2: interpolateValue })), + ), + ), + when(props.dataType === 'forecast' || props.dataType === 'both', mark(FORECAST_AT, FORECAST)), + when(props.dataType === 'potential' || props.dataType === 'both', mark(POTENTIAL_AT, POTENTIAL)), )(source) as LineChartProps['data']; diff --git a/stories/components/d3-chart/tests/examples/line-chart/basic-usage.tsx b/stories/components/d3-chart/tests/examples/line-chart/basic-usage.tsx index 8ee1928049..7d5f762060 100644 --- a/stories/components/d3-chart/tests/examples/line-chart/basic-usage.tsx +++ b/stories/components/d3-chart/tests/examples/line-chart/basic-usage.tsx @@ -3,23 +3,93 @@ import type { LineChartProps } from '@semcore/ui/d3-chart'; import { Chart } from '@semcore/ui/d3-chart'; import React from 'react'; -import dataPipeline, { base as baseData, type HighlightDotsMode } from './__mocks__'; +import dataPipeline, { + base as baseData, + type DataTypeMode, + type HighlightDotsMode, +} from './__mocks__'; import { getChartProps, getPropsToChart } from '../stories_props_helper'; -export type { HighlightDotsMode }; +export type { DataTypeMode, HighlightDotsMode }; type LineChartStoryProps = LineChartProps & { - /** Hands `plotWidth` to the chart instead of letting it measure its container. */ useExplicitPlotWidth?: boolean; + highlightDots?: HighlightDotsMode; + dataType?: DataTypeMode; /** - * Marks points with `HIGHLIGHT_DOT`, the same data-level marker `AreaChart` uses. + * Which `tooltipTitleFormatter` to pass, or `off` to keep the built-in title. * - * `Dots` is shared between Area, Line and Radar, so Line renders the ring and the halo - * too — but only while the dot itself is visible. `LineChart` passes `display={showDots}` - * with no fallback, so turning `showDots` off hides the highlight as well, while - * `AreaChart` keeps it. Flip both controls to see the two charts disagree. + * The variants label the bare index, because that is what this story's group key is. + * Date-shaped titles are not demonstrated here on purpose — `tooltip-default-format.tsx` + * is the story for those, and it is Date-keyed to begin with. */ - highlightDots?: HighlightDotsMode; + titleFormat?: 'off' | keyof typeof titleFormats; + /** + * Which `getPercentDelta` override to pass, or `off` to keep the built-in calculation. + * + * Needs `showDeltaPercentInTooltip` to be on to show up at all. See `deltaOverrides` + * for what each variant demonstrates. + */ + deltaOverride?: 'off' | keyof typeof deltaOverrides; + withInterpolatedGaps?: boolean; +}; + +/** + * Variants for `tooltipTitleFormatter`, which formats the tooltip title only and leaves the + * series values to `tooltipValueFormatter`. + * + */ +const titleFormats = { + point: (value: unknown) => `Point ${value}`, + week: (value: unknown) => `Week ${value}`, + padded: (value: unknown) => `#${String(value).padStart(2, '0')}`, +} as const; + +/** + * Variants for `getPercentDelta`, which replaces the built-in delta calculation. + * + * Every variant honours the declared `number | null` contract — what they show is what the + * chart does with returns that are valid yet unusable, and where an override stops matching + * the built-in behaviour. + * + * variant | what it demonstrates + * -------------------|------------------------------------------------------------ + * custom | a correct override, for reference + * divideByZero | the built-in formula without its zero guard — `NaN`/`Infinity` + * | are valid `number`s, so they reach the tooltip verbatim + * includesFirstPoint | an override also runs for index 0, unlike the built-in path + */ +const deltaOverrides = { + custom: (key: string, index: number, chartData: LineChartProps['data']): number | null => { + if (index === 0) return null; + + const prev = chartData[index - 1]?.[key]; + const curr = chartData[index]?.[key]; + + if (typeof prev !== 'number' || typeof curr !== 'number' || prev === 0) return null; + + return ((curr - prev) / Math.abs(prev)) * 100; + }, + + divideByZero: (key: string, index: number, chartData: LineChartProps['data']): number | null => { + if (index === 0) return null; + + const prev = chartData[index - 1]?.[key]; + const curr = chartData[index]?.[key]; + + if (typeof prev !== 'number' || typeof curr !== 'number') return null; + + return ((curr - prev) / prev) * 100; + }, + + includesFirstPoint: (key: string, index: number, chartData: LineChartProps['data']): number | null => { + const prev = chartData[Math.max(index - 1, 0)]?.[key]; + const curr = chartData[index]?.[key]; + + if (typeof prev !== 'number' || typeof curr !== 'number' || prev === 0) return null; + + return ((curr - prev) / Math.abs(prev)) * 100; + }, }; const Demo = (props: LineChartStoryProps) => { @@ -36,12 +106,25 @@ const Demo = (props: LineChartStoryProps) => { plotHeight, useExplicitPlotWidth, highlightDots, - data, + dataType, + titleFormat, + deltaOverride, + withInterpolatedGaps, ...chartProps } = getPropsToChart(props); + + // A caller-supplied dataset (browser tests pass one through `loadPage`) wins over the + // built-in one, so the knobs reshape what was handed in rather than replacing it. + const suppliedData = (chartProps as LineChartProps).data; + const groupKey = (chartProps as LineChartProps).groupKey ?? 'x'; + const chartData = React.useMemo( - () => dataPipeline({ highlightDots }, data), - [data, highlightDots], + () => + dataPipeline( + { highlightDots, withInterpolatedGaps, dataType }, + suppliedData ?? baseData, + ), + [suppliedData, highlightDots, withInterpolatedGaps, dataType], ); const handleResize: NonNullable = (size, entries) => { @@ -49,14 +132,27 @@ const Demo = (props: LineChartStoryProps) => { onResize?.(size, entries); }; + const tooltipTitleFormatter = + titleFormat && titleFormat !== 'off' + ? titleFormats[titleFormat as keyof typeof titleFormats] + : undefined; + + const getPercentDelta = + deltaOverride && deltaOverride !== 'off' + ? deltaOverrides[deltaOverride as keyof typeof deltaOverrides] + : undefined; + const responsiveChartProps: LineChartProps = { ...(chartProps as LineChartProps), + groupKey, data: chartData, aspect, hMax, hMin, onResize: handleResize, ...(useExplicitPlotWidth ? { plotWidth } : {}), + ...(tooltipTitleFormatter ? { tooltipTitleFormatter } : {}), + ...(getPercentDelta ? { getPercentDelta } : {}), }; const showResponsiveInfo = aspect || hMin || hMax || useExplicitPlotWidth; @@ -93,6 +189,10 @@ export const defaultProps = getChartProps({ showLegend: true, useExplicitPlotWidth: false, highlightDots: 'none', + dataType: 'none', + withInterpolatedGaps: false, + titleFormat: 'off', + deltaOverride: 'off', }); Demo.defaultProps = defaultProps; diff --git a/stories/components/d3-chart/tests/line-chart.stories.tsx b/stories/components/d3-chart/tests/line-chart.stories.tsx index a6b25d39f3..195e827e19 100644 --- a/stories/components/d3-chart/tests/line-chart.stories.tsx +++ b/stories/components/d3-chart/tests/line-chart.stories.tsx @@ -24,15 +24,41 @@ export const Lines: StoryObj = { render: LinesExample, }; +/** + * Knobs that belong to the story, not to `LineChart`. They reshape the dataset or pick a + * value the component takes as data rather than as a prop, so they are grouped apart in the + * controls panel — mixed in with the real props they read as API that does not exist. + */ +const storyKnob = { table: { category: 'Story data knobs' } } as const; + export const BasicUsage = { render: BasicUsageExample, argTypes: getChartArgTypes({ aspect: { control: { type: 'number', min: 0, max: 2, step: 0.1 } }, hMin: { control: { type: 'number', min: 0, max: 600, step: 10 } }, hMax: { control: { type: 'number', min: 0, max: 600, step: 10 } }, - useExplicitPlotWidth: { control: 'boolean' }, showDots: { control: 'boolean' }, - highlightDots: { control: 'select', options: ['none', 'good', 'bad', 'insightful', 'mixed'] }, + + useExplicitPlotWidth: { control: 'boolean', ...storyKnob }, + highlightDots: { + control: 'select', + options: ['none', 'good', 'bad', 'insightful', 'mixed'], + ...storyKnob, + }, + dataType: { + control: 'select', + options: ['none', 'forecast', 'potential', 'both'], + ...storyKnob, + }, + withInterpolatedGaps: { control: 'boolean', ...storyKnob }, + // Variants label the numeric group key. Date-shaped titles live in the Tooltip-Format story. + titleFormat: { control: 'select', options: ['off', 'point', 'week', 'padded'], ...storyKnob }, + // Needs showDeltaPercentInTooltip to be on. + deltaOverride: { + control: 'select', + options: ['off', 'custom', 'divideByZero', 'includesFirstPoint'], + ...storyKnob, + }, }), args: BasicUsageProps, };