diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx index cca5fc455d..26b54b53fe 100644 --- a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx +++ b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx @@ -16,6 +16,10 @@ export const locators = { const base = page.locator('path[data-ui-name="Area"]'); return typeof index === 'number' ? base.nth(index) : base; }, + areaLine: (page: Page, index?: number) => { + const base = page.locator('path[clip-path*="-animation"][d]'); + return typeof index === 'number' ? base.nth(index) : base; + }, legendItem: (page: Page, text?: string, index?: number) => { const base = text ? page.getByText(text) : page.locator('[data-ui-name="Legend.Item"]'); return typeof index === 'number' ? base.nth(index) : base; @@ -24,9 +28,63 @@ export const locators = { const base = text ? page.getByText(text) : page.locator('[data-ui-name="LegendFlex.LegendItem"]'); return typeof index === 'number' ? base.nth(index) : base; }, + highlightRing: (page: Page) => page.locator('circle[r="11.5"]'), + highlightHalo: (page: Page) => page.locator('circle[r="8.5"]'), tooltip: (page: Page) => page.locator('[data-ui-name="AreaChart.Tooltip"], [data-ui-name="Chart.Tooltip"], [data-ui-name="HoverLine.Tooltip"]'), }; +const AREA_STORY = 'stories/components/d3-chart/tests/examples/area-chart/basic-usage.tsx'; + +type SegmentKind = 'main' | 'forecast' | 'potential'; +type Segment = { kind: SegmentKind; fill: string; hasGradient: boolean }; + +const segments = (page: Page): Promise => + page.evaluate(() => { + const classify = (path: Element) => { + const mask = path.getAttribute('mask') ?? ''; + if (mask.includes('forecast')) return 'forecast' as const; + if (mask.includes('potential')) return 'potential' as const; + + // Only the filled area carries the name; lines, axes and grid paths do not. + return path.getAttribute('data-ui-name')?.endsWith('Area') ? ('main' as const) : null; + }; + + return Array.from(document.querySelectorAll('svg[data-ui-name="Plot"] path')) + .filter((path) => (path.getAttribute('d') ?? '').length > 0) + .flatMap((path) => { + const kind = classify(path); + if (kind === null) return []; + + const styles = getComputedStyle(path); + + return [{ + kind, + fill: styles.fill, + hasGradient: styles.maskImage !== 'none' && styles.maskImage !== '', + }]; + }); + }); + +const segmentsOfKind = async (page: Page, kind: SegmentKind) => { + const all = await segments(page); + const found = all.filter((segment) => segment.kind === kind); + + expect( + found, + `expected a "${kind}" segment, found kinds: [${all.map((s) => s.kind).join(', ') || 'none'}]`, + ).not.toHaveLength(0); + + return found; +}; + +const mainHasGradient = async (page: Page) => + (await segmentsOfKind(page, 'main')).every((segment) => segment.hasGradient); + +const openAreaStory = async (page: Page, props: Record) => { + await loadPage(page, AREA_STORY, 'en', { duration: 0, ...props }); + await locators.plot(page).first().waitFor({ state: 'visible' }); +}; + /* ===================================================== @visual Visual states, hover and focus styles, paddings, margins, and snapshots. @@ -90,6 +148,44 @@ test.describe(`${TAG.VISUAL}`, () => { patterns: false, duration: 0, }, + { + description: 'Highlighted dots, ordinary dots off', + highlightDots: 'mixed', + singleSeries: true, + showDots: false, + plotWidth: 500, + plotHeight: 300, + showTooltip: false, + duration: 0, + }, + { + description: 'Forecast segment', + dataType: 'forecast', + singleSeries: true, + plotWidth: 500, + plotHeight: 300, + showTooltip: false, + duration: 0, + }, + { + description: 'Potential segment', + dataType: 'potential', + singleSeries: true, + plotWidth: 500, + plotHeight: 300, + showTooltip: false, + duration: 0, + }, + { + description: 'Highlighted dots with a11y patterns', + highlightDots: 'mixed', + singleSeries: true, + patterns: true, + plotWidth: 500, + plotHeight: 300, + showTooltip: false, + duration: 0, + }, ]; variables.forEach((vars, index) => { @@ -361,7 +457,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await locators.plot(page).waitFor({ state: 'visible' }); await page.waitForTimeout(500); - await expect(locators.areaDots(page)).toHaveCount(10); + await expect(locators.areaDots(page)).toHaveCount(8); await expectEachToHaveAttribute(locators.areaDots(page), 'aria-hidden', 'true'); await expectEachToHaveAttribute(locators.areaDots(page), 'r', '3.5'); }); @@ -373,6 +469,16 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); }); + test('Verify area line stroke width is 2px', { + tag: [TAG.PRIORITY_MEDIUM, '@area-chart', '@d3-chart'], + }, async ({ page }) => { + await loadPage(page, 'stories/components/d3-chart/docs/examples/area-chart/area.tsx', 'en'); + await locators.plot(page).waitFor({ state: 'visible' }); + + await expect(locators.areaLine(page)).toHaveCount(1); + await expect(locators.areaLine(page)).toHaveCSS('stroke-width', '2px'); + }); + test('Verify interpolation attributes', { tag: [TAG.PRIORITY_MEDIUM, '@area-chart', '@d3-chart'], }, async ({ page }) => { @@ -429,4 +535,138 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await expectEachToHaveAttribute(checkboxes.locator('input'), 'aria-invalid', 'false'); }); }); + + const gradientRules = [ + { description: 'stacked, no patterns', props: { stacked: true }, gradient: true }, + { description: 'stacked, patterns', props: { stacked: true, patterns: true }, gradient: false }, + { + description: 'not stacked, one series, no patterns', + props: { stacked: false, singleSeries: true }, + gradient: true, + }, + { + description: 'not stacked, one series, patterns', + props: { stacked: false, singleSeries: true, patterns: true }, + gradient: false, + }, + { + description: 'not stacked, two series, no patterns', + props: { stacked: false }, + gradient: false, + }, + { + description: 'not stacked, two series, patterns', + props: { stacked: false, patterns: true }, + gradient: false, + }, + ]; + + gradientRules.forEach(({ description, props, gradient }) => { + test(`Verify area gradient is ${gradient ? 'applied' : 'suppressed'}: ${description}`, { + tag: [TAG.PRIORITY_HIGH, '@area-chart', '@d3-chart'], + }, async ({ page }) => { + await openAreaStory(page, props); + + expect(await mainHasGradient(page)).toBe(gradient); + }); + }); + + test('Verify the area gradient follows the legend rather than the data', { + tag: [TAG.PRIORITY_MEDIUM, '@mouse', '@area-chart', '@d3-chart'], + }, async ({ page }) => { + await openAreaStory(page, { + stacked: false, + data: [ + { time: new Date('2024-01-01').getTime(), a: 2, b: 3, c: 4 }, + { time: new Date('2024-01-06').getTime(), a: 4, b: 3, c: 2 }, + { time: new Date('2024-01-11').getTime(), a: 3, b: 5, c: 6 }, + ], + }); + + await test.step('three series checked: no gradient', async () => { + expect(await mainHasGradient(page)).toBe(false); + }); + + await test.step('one series left: gradient appears', async () => { + await locators.legendFlexItem(page, undefined, 1).click(); + await locators.legendFlexItem(page, undefined, 2).click(); + await page.waitForTimeout(300); + expect(await mainHasGradient(page)).toBe(true); + }); + + await test.step('series restored: gradient disappears again', async () => { + await locators.legendFlexItem(page, undefined, 1).click(); + await locators.legendFlexItem(page, undefined, 2).click(); + await page.waitForTimeout(300); + expect(await mainHasGradient(page)).toBe(false); + }); + }); + + test('Verify highlighted dots survive a hover', { + tag: [TAG.PRIORITY_MEDIUM, '@mouse', '@area-chart', '@d3-chart'], + }, async ({ page }) => { + await openAreaStory(page, { highlightDots: 'mixed', singleSeries: true, showDots: false }); + + const ringsBefore = await locators.highlightRing(page).count(); + expect(ringsBefore).toBeGreaterThan(0); + + const box = await locators.plot(page).first().boundingBox(); + if (box) await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.waitForTimeout(300); + + await test.step('Hover grows the ordinary dot without disturbing the decorations', async () => { + expect(await locators.highlightRing(page).count()).toBe(ringsBefore); + }); + }); + + test('Verify forecast does not use the pattern fill when patterns are enabled', { + tag: [TAG.PRIORITY_HIGH, '@area-chart', '@d3-chart', '@accessibility'], + }, async ({ page }) => { + await openAreaStory(page, { dataType: 'forecast', singleSeries: true, patterns: true }); + + const [forecast] = await segmentsOfKind(page, 'forecast'); + + expect(forecast.fill).not.toContain('pattern'); + }); + + test('Verify forecast segments differ in colour between two series', { + tag: [TAG.PRIORITY_HIGH, '@area-chart', '@d3-chart'], + }, async ({ page }) => { + await openAreaStory(page, { dataType: 'forecast' }); + + const stopColours = await page.evaluate(() => + Array.from(document.querySelectorAll('linearGradient[id*="forecast-gradient"]')).map( + (gradient) => getComputedStyle(gradient.querySelector('stop')!).stopColor, + ), + ); + + expect(stopColours.length).toBeGreaterThan(1); + expect(new Set(stopColours).size).toBeGreaterThan(1); + }); + + test('Verify interpolated points bridge the gap instead of breaking the series', { + tag: [TAG.PRIORITY_MEDIUM, '@area-chart', '@d3-chart'], + }, async ({ page }) => { + await openAreaStory(page, { + withInterpolatedGaps: true, + singleSeries: true, + showDots: true, + stacked: false, + }); + + const [main] = await segmentsOfKind(page, 'main'); + + expect(main.fill).not.toBe('none'); + + await test.step('The segment still draws, with no gap in its geometry', async () => { + const d = await locators.areaPath(page).first().getAttribute('d'); + + expect(d).toBeTruthy(); + expect(d).not.toContain('NaN'); + }); + + await test.step('Interpolated points carry no dot of their own', async () => { + await expect(locators.areaDots(page)).toHaveCount(7); + }); + }); }); diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Forecast-segment-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Forecast-segment-1-chromium-linux.png new file mode 100644 index 0000000000..7937c718b1 Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Forecast-segment-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Forecast-segment-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Forecast-segment-1-firefox-linux.png new file mode 100644 index 0000000000..9f829878c1 Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Forecast-segment-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Forecast-segment-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Forecast-segment-1-webkit-linux.png new file mode 100644 index 0000000000..b32406ee0f Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Forecast-segment-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-ordinary-dots-off-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-ordinary-dots-off-1-chromium-linux.png new file mode 100644 index 0000000000..cb83030d7a Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-ordinary-dots-off-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-ordinary-dots-off-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-ordinary-dots-off-1-firefox-linux.png new file mode 100644 index 0000000000..932000cc3e Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-ordinary-dots-off-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-ordinary-dots-off-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-ordinary-dots-off-1-webkit-linux.png new file mode 100644 index 0000000000..27bb4cec57 Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-ordinary-dots-off-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-with-a11y-patterns-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-with-a11y-patterns-1-chromium-linux.png new file mode 100644 index 0000000000..8bde0c692b Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-with-a11y-patterns-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-with-a11y-patterns-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-with-a11y-patterns-1-firefox-linux.png new file mode 100644 index 0000000000..356a9c5cb3 Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-with-a11y-patterns-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-with-a11y-patterns-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-with-a11y-patterns-1-webkit-linux.png new file mode 100644 index 0000000000..a393b4088f Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Highlighted-dots-with-a11y-patterns-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-chromium-linux.png index 6b6f5a1b97..6a8e7af2b0 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-firefox-linux.png index 11febc917d..5ad6a0c6d4 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-webkit-linux.png index a49236f361..c7bd37c525 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-webkit-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Inverted-axis-inversion-no-tooltip-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Potential-segment-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Potential-segment-1-chromium-linux.png new file mode 100644 index 0000000000..373be39fd5 Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Potential-segment-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Potential-segment-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Potential-segment-1-firefox-linux.png new file mode 100644 index 0000000000..f5f3c86b73 Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Potential-segment-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Potential-segment-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Potential-segment-1-webkit-linux.png new file mode 100644 index 0000000000..d93a0cdcff Binary files /dev/null and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Potential-segment-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-chromium-linux.png index 3c569e038e..84372f5b80 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-firefox-linux.png index 1a2a6e6873..3361b50571 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-webkit-linux.png index e7a5df11d9..8c0da4010a 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-webkit-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Stacked-patterns-enabled-minimal-UI-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-chromium-linux.png index 2a7914f79a..0492451582 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-firefox-linux.png index 28c52f928a..7d903c8c31 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-webkit-linux.png index 1cfab591d1..3f78943634 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-webkit-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-Standard-all-features-medium-size-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-chromium-linux.png index 37723d4e83..f582977170 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-firefox-linux.png index d3dd1c35d0..39d18c1621 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-webkit-linux.png index ef0a55af5f..97d74d5347 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-webkit-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-rendering-with-dots-and-hover-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-chromium-linux.png index 8db2f087f1..a0d09d1230 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-firefox-linux.png index 2da8e2763b..2a2796f239 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-webkit-linux.png index 2df8562b1f..e7eb032564 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-webkit-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-null-undefined-and-negative-values-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-chromium-linux.png index 7788168858..51e7f4df89 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-firefox-linux.png index 7862666a54..9180317428 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-webkit-linux.png index c73ab6933b..962cc6f1a2 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-webkit-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-area-chart-with-single-data-point-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-1-chromium-linux.png index dd0ecce7be..ea3936d278 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-1-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-1-firefox-linux.png index f3a18ce41a..6abe3a38e2 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-1-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-2-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-2-chromium-linux.png index a096ab1cde..2117061beb 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-2-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-2-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-2-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-2-firefox-linux.png index ca68a44072..e4624ed82b 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-2-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-2-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-3-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-3-chromium-linux.png index 6d1299fcdc..8731ba23ad 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-3-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-3-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-3-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-3-firefox-linux.png index 7341a05d86..d31173dc23 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-3-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-legend-and-pattern-fill-hover-and-focus-styles-3-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-chart-legend-hover-styles-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-chart-legend-hover-styles-1-chromium-linux.png index 43a35a4ece..adc14644a4 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-chart-legend-hover-styles-1-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-chart-legend-hover-styles-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-chart-legend-hover-styles-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-chart-legend-hover-styles-1-firefox-linux.png index ce99c3f9d0..f27bbeb8d2 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-chart-legend-hover-styles-1-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-chart-legend-hover-styles-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-chromium-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-chromium-linux.png index 0dab1ec934..e64ead97c1 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-chromium-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-firefox-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-firefox-linux.png index dd9786ab7d..f0593b968b 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-firefox-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-webkit-linux.png b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-webkit-linux.png index 7db383e9cf..363b459e79 100644 Binary files a/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-webkit-linux.png and b/semcore/d3-chart/__tests__/area-chart.browser-test.tsx-snapshots/-visual-Verify-stacked-area-when-part-of-the-chart-has-no-data-or-only-one-value-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-chromium-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-chromium-linux.png index e5579c68b3..6ebeb116d0 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-chromium-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-firefox-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-firefox-linux.png index c38a09b180..6252316db0 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-firefox-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-webkit-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-webkit-linux.png index 5b60aa0b5f..a86ae542d0 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-webkit-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Bar-chart---Legend-and-interactions-Verify-bar-legend-and-pattern-fill-mouse-interaction-2-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-chromium-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-chromium-linux.png index feb324b577..faa22311d4 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-chromium-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-firefox-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-firefox-linux.png index 642229e1a0..8a8d3c952a 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-firefox-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-webkit-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-webkit-linux.png index 7bfc981dba..8a084d200d 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-webkit-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-bar-chart-Patterns-minimal-axes-and-UI-multiline-ticks-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-chromium-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-chromium-linux.png index 21e21c2e5b..b4eb091ac3 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-chromium-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-firefox-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-firefox-linux.png index 7ac6f53871..18015bf45d 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-firefox-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-webkit-linux.png b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-webkit-linux.png index de41b473ad..b8d95251d6 100644 Binary files a/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-webkit-linux.png and b/semcore/d3-chart/__tests__/bar-chart.browser-test.tsx-snapshots/-visual-Basic-bar-chart-Verify-trend-for-chart-Horizontal-with-trend-2-webkit-linux.png differ 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__/chart-legend.browser-test.tsx b/semcore/d3-chart/__tests__/chart-legend.browser-test.tsx index 84229777bc..8e3e3d6bec 100644 --- a/semcore/d3-chart/__tests__/chart-legend.browser-test.tsx +++ b/semcore/d3-chart/__tests__/chart-legend.browser-test.tsx @@ -127,8 +127,7 @@ test.describe(`${TAG.VISUAL}`, () => { test.describe('LegendFlex', () => { flexPairwiseCases.forEach((c) => { test(`Verify pairwise ${c.name}`, { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend', - '@typography'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, FLEX_EXAMPLE, 'en', c.props); @@ -141,10 +140,7 @@ test.describe(`${TAG.VISUAL}`, () => { test.describe('LegendTable', () => { tablePairwiseCases.forEach((c) => { test(`Verify pairwise ${c.name}`, { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend', - '@base-components', - '@flex-box', - '@typography'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, TABLE_EXAMPLE, 'en', c.props); @@ -181,8 +177,7 @@ We verify states, visibility, and attributes. test.describe(`${TAG.FUNCTIONAL}`, () => { test.describe('LegendFlex', () => { test('Verify checkbox roles and attributes', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend', - '@typography'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, FLEX_EXAMPLE, 'en', { shape: 'Checkbox' }); @@ -190,8 +185,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify an item is toggled by its checkbox and by its label', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', '@chart-legend', - '@typography'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, FLEX_EXAMPLE, 'en', { shape: 'Checkbox' }); @@ -211,8 +205,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { (['Circle', 'Pattern'] as const).forEach((shape) => { test(`Verify ${shape} shape renders no checkbox`, { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend', - '@typography'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, FLEX_EXAMPLE, 'en', { shape, patterns: shape === 'Pattern' }); @@ -222,8 +215,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify highlightedItem dims every other item', { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend', - '@typography'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, FLEX_EXAMPLE, 'en', { highlightedItem: 0 }); @@ -238,8 +230,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify nothing is dimmed when highlightedItem is -1', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend', - '@typography'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, FLEX_EXAMPLE, 'en', { highlightedItem: -1 }); @@ -248,8 +239,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify the trend item renders next to the legend items', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend', - '@typography'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, FLEX_EXAMPLE, 'en', { withTrend: true, trendLabel: 'Trend line' }); @@ -257,8 +247,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify the suffix renders after the legend items', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend', - '@typography'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, FLEX_EXAMPLE, 'en', { withSuffix: true }); @@ -268,10 +257,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { test.describe('LegendTable', () => { test('Verify the design data renders in both columns', { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend', - '@base-components', - '@flex-box', - '@typography'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, TABLE_EXAMPLE, 'en', { columnsCount: 2 }); @@ -282,10 +268,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify a single value column renders one cell per row', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend', - '@base-components', - '@flex-box', - '@typography'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, TABLE_EXAMPLE, 'en', { columnsCount: 1 }); @@ -294,10 +277,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify a row is toggled by its checkbox', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', '@chart-legend', - '@base-components', - '@flex-box', - '@typography'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, TABLE_EXAMPLE, 'en', {}); @@ -309,10 +289,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify highlightedItem dims every other row label', { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend', - '@base-components', - '@flex-box', - '@typography'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, TABLE_EXAMPLE, 'en', { highlightedItem: 0 }); @@ -332,10 +309,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { * they are easy to let drift apart. */ test('Verify the value columns of a dimmed row are dimmed as well', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend', - '@base-components', - '@flex-box', - '@typography'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@chart-legend'], }, async ({ page }) => { await loadPage(page, TABLE_EXAMPLE, 'en', { highlightedItem: 0, columnsCount: 2 }); 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 d179d6cfc7..c7b4dcd764 100644 --- a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx +++ b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx @@ -170,8 +170,7 @@ Visual states, hover and focus styles, paddings, margins, and snapshots. test.describe(`${TAG.VISUAL}`, () => { test.describe('Chart Plot', () => { test('Verify paddings and margins apply to the plot', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', - '@line-chart'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart'], }, async ({ page }) => { await loadPage(page, 'stories/components/d3-chart/docs/examples/d3-chart/paddings-&-margins.tsx', 'en'); @@ -314,8 +313,7 @@ test.describe(`${TAG.VISUAL}`, () => { pairwiseCases.forEach((c) => { test(`Verify pairwise ${c.name}`, { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', - '@bar-chart'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart'], }, async ({ page }) => { await loadPage(page, GRID_AXIS_EXAMPLE, 'en', c.props); await locators.plot(page).waitFor({ state: 'visible' }); @@ -324,8 +322,7 @@ test.describe(`${TAG.VISUAL}`, () => { }); test('Verify vertical writing mode + primaryText ticks on both axes', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', - '@bar-chart'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart'], }, async ({ page }) => { await loadPage(page, GRID_AXIS_EXAMPLE, 'en', { yShowTitle: true, @@ -342,9 +339,7 @@ test.describe(`${TAG.VISUAL}`, () => { test.describe('Adaptive chart', () => { test('Verify chart looks good on small resolutions', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', - '@line-chart', - '@responsive'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart'], }, async ({ page }) => { await loadPage(page, 'stories/components/d3-chart/docs/examples/d3-chart/responsive-low-level-chart.tsx', 'en'); @@ -360,8 +355,7 @@ test.describe(`${TAG.VISUAL}`, () => { test.describe('Hover Line and Tooltip', () => { test('Verify Tooltip controlled appearing', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', '@base-components', '@flex-box', '@typography', - '@line-chart'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', '@base-components', '@flex-box', '@typography'], }, async ({ page }) => { await loadPage(page, 'stories/components/d3-chart/docs/examples/d3-chart/tooltip-control.tsx', 'en'); await page.waitForTimeout(500); // wait for finish animation @@ -371,9 +365,7 @@ test.describe(`${TAG.VISUAL}`, () => { }); test('Verify synscronous charts by EventEmitter', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@line-chart'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, 'stories/components/d3-chart/docs/examples/d3-chart/synchronous-charts.tsx', 'en'); @@ -387,8 +379,7 @@ test.describe(`${TAG.VISUAL}`, () => { test.describe('Pattern fills, dots and lines', () => { test('Verify enforcing patterns', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', - '@stacked-area-chart'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, 'stories/components/d3-chart/docs/examples/d3-chart/enforcing-patterns.tsx', 'en'); @@ -418,9 +409,7 @@ test.describe(`${TAG.VISUAL}`, () => { test.describe('Hovered tick', () => { (['Line', 'Rect'] as const).forEach((hoverType) => { test(`Verify the tick pill appears under the hovered tick for Hover${hoverType}`, { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, HOVERED_TICK_EXAMPLE, 'en', { hoverType }); @@ -435,10 +424,7 @@ test.describe(`${TAG.VISUAL}`, () => { test.describe('Tooltip percent delta', () => { test('Verify upward deltas render with the DiffUp icon', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@area-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, AREA_CHART_EXAMPLE, 'en', deltaProps); @@ -462,8 +448,7 @@ We verify states, visibility, and attributes. test.describe(`${TAG.FUNCTIONAL}`, () => { test.describe('Chart Plot', () => { test('Verify Plot roles and attributes', { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart'], }, async ({ page }) => { await loadPage(page, 'stories/components/d3-chart/tests/examples/d3-chart/plot-props.tsx', 'en'); @@ -481,9 +466,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 () => { @@ -548,10 +531,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'); }); }); }); @@ -560,8 +540,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { const GRID_AXIS_EXAMPLE = 'stories/components/d3-chart/tests/examples/d3-chart/grid-axis-props.tsx'; test('Verify attributes of all elements in the chart', { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', - '@bar-chart'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart'], }, async ({ page }) => { await loadPage(page, GRID_AXIS_EXAMPLE, 'en', { yShowTitle: true, @@ -588,8 +567,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify yHide=true hides at least one axis via display:none', { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', - '@bar-chart'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart'], }, async ({ page }) => { await loadPage(page, GRID_AXIS_EXAMPLE, 'en', { yHide: true }); await locators.plot(page).waitFor({ state: 'visible' }); @@ -607,8 +585,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify yTicksHide=true hides Y tick texts via display:none', { - tag: [TAG.PRIORITY_HIGH, '@d3-chart', - '@bar-chart'], + tag: [TAG.PRIORITY_HIGH, '@d3-chart'], }, async ({ page }) => { await loadPage(page, GRID_AXIS_EXAMPLE, 'en', { yTicksHide: true }); await locators.plot(page).waitFor({ state: 'visible' }); @@ -628,23 +605,15 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { test.describe('Pattern fills, dots and lines', () => { test('Verify pattern styles', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', - '@area-chart'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart'], }, async ({ page }) => { await loadPage(page, 'stories/components/d3-chart/docs/examples/d3-chart/pattern-fill.tsx', 'en'); - const patterns = page.locator('pattern'); - - const count = await patterns.count(); + const patterns = page.locator('pattern[viewBox]'); - for (let i = 0; i < count; i++) { - const pattern = patterns.nth(i); - await expect(pattern).toHaveAttribute('patternUnits', 'userSpaceOnUse'); - await expect(pattern).toHaveAttribute('width', '12'); - await expect(pattern).toHaveAttribute('height', '12'); - await expect(pattern).toHaveAttribute('x', '0'); - await expect(pattern).toHaveAttribute('y', '0'); - } + await expectEachToHaveAttribute(patterns, 'patternUnits', 'userSpaceOnUse'); + await expectEachToHaveAttribute(patterns, 'width', '12'); + await expectEachToHaveAttribute(patterns, 'height', '12'); // The example draws no dots at rest (`showDots` is not set), so the hovered point is // the only one that renders. With patterns on, a dot is a `` of the pattern @@ -660,8 +629,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { test.describe('Dots', () => { test('Verify dots radius', { - tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@base-components', '@flex-box', '@typography', - '@line-chart'], + tag: [TAG.PRIORITY_MEDIUM, '@d3-chart', '@base-components', '@flex-box', '@typography'], }, async ({ page }) => { await loadPage(page, 'stories/components/d3-chart/docs/examples/d3-chart/tooltip.tsx', 'en'); @@ -678,10 +646,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify hovered dot grows to the active radius', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', '@line-chart', - '@base-components', - '@flex-box', - '@typography'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', '@line-chart'], }, async ({ page }) => { await loadPage(page, 'stories/components/d3-chart/docs/examples/d3-chart/tooltip.tsx', 'en'); @@ -752,9 +717,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { test.describe('Hovered tick', () => { (['Line', 'Rect'] as const).forEach((hoverType) => { test(`Verify hideTickHover removes the tick pill for Hover${hoverType}`, { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, HOVERED_TICK_EXAMPLE, 'en', { hoverType, hideTickHover: true }); @@ -765,9 +728,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test(`Verify the tick pill is rendered on hover for Hover${hoverType}`, { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, HOVERED_TICK_EXAMPLE, 'en', { hoverType }); @@ -779,9 +740,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify the tick pill stays inside the plot on the first and the last tick', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage( page, @@ -815,9 +774,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify the hover line renders notch caps on both ends', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@line-chart'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage( page, @@ -875,10 +832,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify Chart.Bar hovers with a rect and gets no caps', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, BAR_CHART_EXAMPLE, 'en', { duration: 0 }); @@ -891,9 +845,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify hideHoverLine removes the hover line and the tick pill', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@line-chart'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage( page, @@ -909,9 +861,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify hovering the axis area below the plot keeps the tooltip open', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage( page, @@ -947,10 +897,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { */ test.describe('Tooltip percent delta', () => { test('Verify upward deltas render with the DiffUp icon', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@area-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, AREA_CHART_EXAMPLE, 'en', deltaProps); @@ -963,10 +910,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify downward deltas render with the DiffDown icon', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@area-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, AREA_CHART_EXAMPLE, 'en', deltaProps); @@ -987,10 +931,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { * that survives a palette tweak but still catches the two being swapped. */ test('Verify the diff is green upwards, red downwards and muted when stable', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@area-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, AREA_CHART_EXAMPLE, 'en', deltaProps); @@ -1120,18 +1061,11 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { /** * Pins the behaviour of the awkward inputs, all read off the edge-case example. * None of these is a crash, but two of them are judgement calls worth noticing if - * they ever change: a sub-0.05% move shows as "0.0%" yet still points upward, and a - * negative baseline keeps growth green because the formula divides by `Math.abs(prev)`. - * - * Both the grouping separator and the decimal place come from `defaultTooltipFormatter`, - * which runs `Intl.NumberFormat` under the chart's `locale` — these expectations are - * written for `en` and would need adjusting for a locale with other separators. + * they ever change: a sub-0.05% move collapses into a stable "0", and a negative + * baseline turns growth into a red decline because the formula divides by it. */ test('Verify how the diff handles extreme and awkward values', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, DELTA_EDGE_CASES_EXAMPLE, 'en', {}); @@ -1163,31 +1097,28 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { await test.step('Growth beyond 100% keeps its full value', async () => { expect(diffAfter('overHundred')).toMatchObject({ text: '400%', trend: 'upward' }); - expect(diffAfter('huge')).toMatchObject({ text: '9,900%', trend: 'upward' }); + expect(diffAfter('huge')).toMatchObject({ text: '9900%', trend: 'upward' }); }); await test.step('Fractions keep one decimal place', async () => { expect(diffAfter('fraction')).toMatchObject({ text: '0.5%', trend: 'upward' }); }); - await test.step('A move below 0.05% rounds to zero but keeps its direction', async () => { - // 10000 -> 10004 is +0.04%. Rounding happens on the way to the screen, not in the - // delta itself, so the number reads "0.0%" while the trend is still computed from - // the raw 0.04 and keeps the upward icon — a tiny move no longer passes for "no - // change", which it did while the delta was rounded with `toFixed(1)` first. - expect(diffAfter('roundsToZero')).toMatchObject({ text: '0.0%', trend: 'upward' }); + await test.step('A move below 0.05% collapses into a stable zero', async () => { + // 10000 -> 10004 is +0.04%, which rounds away and reads as no change at all. + // A stable delta still carries the percent sign, it just loses the icon. + expect(diffAfter('roundsToZero')).toMatchObject({ text: '0%', trend: 'stable' }); }); await test.step('A negative baseline keeps the sign of the actual move', async () => { + // -10 -> -5 is an improvement, and dividing by |prev| keeps it a green +50% + // instead of flipping it into a red decline. expect(diffAfter('negativeBase')).toMatchObject({ text: '50%', trend: 'upward' }); }); }); test('Verify an unchanged value renders a stable delta without an icon', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', - '@area-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, AREA_CHART_EXAMPLE, 'en', deltaProps); @@ -1199,10 +1130,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify the first data point renders no delta column', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', - '@area-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, AREA_CHART_EXAMPLE, 'en', deltaProps); @@ -1221,10 +1149,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { * is 0) while `line2` does, which is exactly the mixed case that would break. */ test('Verify rows stay aligned when only some series have a delta', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', - '@area-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, AREA_CHART_EXAMPLE, 'en', { ...deltaProps, withZeroValue: true }); @@ -1250,15 +1175,15 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); expect(grid.columns).toBe(3); + // A partially filled row would leave a hole, and the cell count would stop being a + // whole number of rows with everything after it shifted one column to the left. expect(grid.cellCount % grid.columns).toBe(0); + // Both series labels must still start their own row. expect(grid.firstColumnTexts).toEqual(['line', 'line2']); }); test('Verify no delta is rendered when showDeltaPercentInTooltip is off', { - tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart', - '@area-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_MEDIUM, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, AREA_CHART_EXAMPLE, 'en', { ...deltaProps, @@ -1299,10 +1224,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }; test('Verify HoverLine highlights a value the axis does not label', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@base-components', - '@flex-box', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, LINE_CHART_EXAMPLE, 'en', { duration: 0 }); @@ -1316,10 +1238,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify HoverLine formats the value the same way the axis does', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@area-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, AREA_CHART_EXAMPLE, 'en', { duration: 0 }); @@ -1333,20 +1252,24 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify HoverRect highlights the hovered category', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@bar-chart', - '@base-components', - '@flex-box'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, BAR_CHART_EXAMPLE, 'en', { duration: 0 }); await hoverPlotCenter(page); + // Bar charts hover through HoverRect, which has to highlight the tick all the same. await expect(page.locator('[data-ui-name="Tooltip.Trigger"]').first()).toHaveJSProperty('tagName', 'rect'); await expect(locators.hoveredTickText(page)).toHaveText('Category 2'); }); }); + /** + * The tooltip is inverted, and `chart-palette-order-1` is a dark neutral that matches + * its background, so the 1px ring is the only thing separating the two. The unit tests + * cover which offset each colour is given; these check the other half of the chain — + * that the rule applies and the relative colour actually resolves in the browser. + */ test.describe('Tooltip dot ring', () => { const readDotRings = (page: Page) => page.locator('[class*="SDotCircle"]').evaluateAll((els) => @@ -1364,10 +1287,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { ); test('Verify each dot is ringed by a lighter shade of its own colour', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@base-components', - '@flex-box', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage(page, LINE_CHART_EXAMPLE, 'en', { duration: 0 }); @@ -1390,10 +1310,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { test.describe('Tooltip default formatting', () => { test('Verify a Date group key is formatted through Intl for the given locale', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@base-components', - '@flex-box', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage( page, @@ -1417,10 +1334,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { }); test('Verify the default date carries the full weekday and month and no time', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@base-components', - '@flex-box', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage( page, @@ -1460,10 +1374,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { for (const [titleFormat, expected] of titleFormats) { test(`Verify tooltipTitleFormatter renders the ${titleFormat} title`, { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@base-components', - '@flex-box', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage( page, @@ -1479,10 +1390,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => { } test('Verify tooltipTitleFormatter leaves the series values alone', { - tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart', - '@base-components', - '@flex-box', - '@line-chart'], + tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@d3-chart'], }, async ({ page }) => { await loadPage( page, diff --git a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Pattern-fills-dots-and-lines-Verify-enforcing-patterns-1-chromium-linux.png b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Pattern-fills-dots-and-lines-Verify-enforcing-patterns-1-chromium-linux.png index 5e61d64bc2..b54df0ef29 100644 Binary files a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Pattern-fills-dots-and-lines-Verify-enforcing-patterns-1-chromium-linux.png and b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Pattern-fills-dots-and-lines-Verify-enforcing-patterns-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Pattern-fills-dots-and-lines-Verify-enforcing-patterns-1-firefox-linux.png b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Pattern-fills-dots-and-lines-Verify-enforcing-patterns-1-firefox-linux.png index 952cb66e74..62eee614ed 100644 Binary files a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Pattern-fills-dots-and-lines-Verify-enforcing-patterns-1-firefox-linux.png and b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Pattern-fills-dots-and-lines-Verify-enforcing-patterns-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-chromium-linux.png b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-chromium-linux.png index 139efcc32b..da2d9a1234 100644 Binary files a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-chromium-linux.png and b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-chromium-linux.png differ diff --git a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-firefox-linux.png b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-firefox-linux.png index ee0109ea64..d472f360c6 100644 Binary files a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-firefox-linux.png and b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-firefox-linux.png differ diff --git a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-webkit-linux.png b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-webkit-linux.png index 17bc99bdc1..eff88f6ecb 100644 Binary files a/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-webkit-linux.png and b/semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx-snapshots/-visual-Tooltip-percent-delta-Verify-upward-deltas-render-with-the-DiffUp-icon-1-webkit-linux.png differ diff --git a/semcore/d3-chart/__tests__/index.test.tsx b/semcore/d3-chart/__tests__/index.test.tsx index 9c45f038b5..5e56c14da9 100644 --- a/semcore/d3-chart/__tests__/index.test.tsx +++ b/semcore/d3-chart/__tests__/index.test.tsx @@ -7,7 +7,16 @@ import { scaleLinear, scaleBand } from 'd3-scale'; import React from 'react'; import { + Area, + BAD, + DATA_TYPE, + FORECAST, + GOOD, + HIGHLIGHT_DOT, + INSIGHTFUL, + interpolateValue, Plot, + POTENTIAL, YAxis, XAxis, makeDataHintsContainer, @@ -357,6 +366,43 @@ describe('Focus skip to content after plot', () => { }); }); +const PLOT = { plotWidth: 500, plotHeight: 300, duration: 0 } as const; + +const makeAreaData = (count = 6) => + Array.from({ length: count }, (_, i) => ({ + time: new Date(2024, 0, 1 + i * 5), + line: i + 1, + })); + +const areaDots = (container: HTMLElement) => + queryAllByAttribute('data-ui-name', container, (value: string) => + Boolean(value?.endsWith('Dots')), + ); + +const areaPaths = (container: HTMLElement) => + Array.from(container.querySelectorAll('path')).filter((path) => { + const mask = path.getAttribute('mask') ?? ''; + if (mask.includes('forecast') || mask.includes('potential')) return true; + + return path.getAttribute('data-ui-name')?.endsWith('Area') ?? false; + }); + +const drawnAreaPaths = (container: HTMLElement) => + areaPaths(container).filter((path) => (path.getAttribute('d') ?? '').length > 0); + +const mainAreaPaths = (container: HTMLElement) => + drawnAreaPaths(container).filter((path) => !path.getAttribute('mask')); + +const vertices = (path: Element | undefined) => (path?.getAttribute('d') ?? '').split('L').length; + +const rings = (container: HTMLElement) => container.querySelectorAll('circle[r="11.5"]'); +const halos = (container: HTMLElement) => container.querySelectorAll('circle[r="8.5"]'); + +const renderArea = (props: Record = {}) => + render( + , + ); + describe('Chart.Area', () => { beforeEach(cleanup); @@ -413,6 +459,212 @@ describe('Chart.Area', () => { await userEvent.click(dots[0]); }); + + describe('HIGHLIGHT_DOT equivalence classes', () => { + beforeEach(cleanup); + + const withHighlight = (highlight: unknown) => { + const data: any[] = makeAreaData(); + data[2] = { ...data[2], [HIGHLIGHT_DOT]: highlight }; + return data; + }; + + test.each([ + ['GOOD', GOOD], + ['BAD', BAD], + ['INSIGHTFUL', INSIGHTFUL], + ])('%s draws one ring and one halo around the marked point', (_name, highlight) => { + const { container } = renderArea({ data: withHighlight(highlight), showDots: false }); + + expect(rings(container)).toHaveLength(1); + expect(halos(container)).toHaveLength(1); + }); + + test('each class resolves to its own colour reference', () => { + const colourOf = (highlight: unknown) => { + cleanup(); + const { container } = renderArea({ data: withHighlight(highlight), showDots: false }); + return halos(container)[0]?.getAttribute('color'); + }; + + expect(colourOf(GOOD)).toContain('chart-data-success'); + expect(colourOf(BAD)).toContain('chart-data-critical'); + // INSIGHTFUL is the only class that points at a gradient rather than a flat token. + expect(colourOf(INSIGHTFUL)).toMatch(/^url\(#dotGradient_/); + }); + + test('an unmarked dataset draws no highlight decorations', () => { + const { container } = renderArea({ showDots: true }); + + expect(rings(container)).toHaveLength(0); + expect(halos(container)).toHaveLength(0); + }); + }); + + describe('DATA_TYPE equivalence classes', () => { + beforeEach(cleanup); + + const withTail = (marker: symbol) => { + const base = makeAreaData(); + const last = base[base.length - 1]; + return [ + ...base, + { ...last, [DATA_TYPE]: marker }, + { time: new Date(2024, 1, 5), line: 9, [DATA_TYPE]: marker }, + ] as any[]; + }; + + test.each([ + ['FORECAST', FORECAST], + ['POTENTIAL', POTENTIAL], + ])('%s data adds a second drawn segment', (_name, marker) => { + const { container } = renderArea({ data: withTail(marker) }); + + expect(drawnAreaPaths(container)).toHaveLength(2); + }); + + test('the forecast segment is dashed and masked', () => { + const { container } = renderArea({ data: withTail(FORECAST) }); + const dashed = container.querySelectorAll('path[stroke-dasharray="4 4"]'); + const masked = drawnAreaPaths(container).filter((path) => + path.getAttribute('mask')?.includes('forecast'), + ); + + expect(dashed.length).toBeGreaterThan(0); + expect(masked).toHaveLength(1); + }); + + test('marked points are excluded from the main segment', () => { + const plain = renderArea().container; + const plainVertices = vertices(mainAreaPaths(plain)[0]); + cleanup(); + + const marked: any[] = makeAreaData(); + marked[2] = { ...marked[2], [DATA_TYPE]: FORECAST }; + const { container } = renderArea({ data: marked }); + + expect(vertices(mainAreaPaths(container)[0])).toBe(plainVertices - 2); + }); + }); + + describe('interpolateValue gaps', () => { + beforeEach(cleanup); + + /** A gap in the middle of `line2`, with real values on both sides to bridge between. */ + const withGap = () => { + const data: any[] = makeAreaData().map((point) => ({ ...point, line2: point.line + 1 })); + data[2] = { ...data[2], line2: interpolateValue }; + return data; + }; + + test('a gap does not break the unstacked series', () => { + const { container } = renderArea({ data: withGap(), stacked: false, showDots: true }); + + mainAreaPaths(container).forEach((path) => { + expect(path.getAttribute('d') ?? '').not.toContain('NaN'); + }); + expect(mainAreaPaths(container).length).toBeGreaterThan(0); + }); + + test('the interpolated point gets no dot of its own', () => { + const plain = renderArea({ + data: makeAreaData().map((point) => ({ ...point, line2: point.line + 1 })), + stacked: false, + showDots: true, + }).container; + const plainDots = areaDots(plain).length; + cleanup(); + + const { container } = renderArea({ data: withGap(), stacked: false, showDots: true }); + + expect(areaDots(container)).toHaveLength(plainDots - 1); + }); + + test.fails('a gap does not break the stacked series', () => { + expect(() => renderArea({ data: withGap(), stacked: true })).not.toThrow(); + }); + }); + + describe('boundary values', () => { + beforeEach(cleanup); + + test.fails('renders an empty dataset without throwing', () => { + expect(() => renderArea({ data: [] })).not.toThrow(); + }); + + test.each([1, 2])('renders a %i-point dataset without producing NaN geometry', (count) => { + const { container } = renderArea({ data: makeAreaData(count), showDots: true }); + const paths = areaPaths(container); + + // Guard: an empty selection would make the loop below assert nothing at all. + expect(paths.length).toBeGreaterThan(0); + paths.forEach((path) => { + expect(path.getAttribute('d') ?? '').not.toContain('NaN'); + }); + }); + + test('a one-point forecast run produces no NaN in the segment geometry', () => { + const data: any[] = [ + ...makeAreaData(), + { time: new Date(2024, 1, 5), line: 9, [DATA_TYPE]: FORECAST }, + ]; + + const { container } = renderArea({ data }); + const paths = areaPaths(container); + + // Guard: an empty selection would make the loop below assert nothing at all. + expect(paths.length).toBeGreaterThan(0); + paths.forEach((path) => { + expect(path.getAttribute('d') ?? '').not.toContain('NaN'); + }); + }); + + test('a dataset marked FORECAST end to end leaves the main segment empty', () => { + const data = makeAreaData().map((point) => ({ ...point, [DATA_TYPE]: FORECAST })); + + const { container } = renderArea({ data }); + + expect(drawnAreaPaths(container)).toHaveLength(1); + }); + }); + + describe('stacked interaction', () => { + beforeEach(cleanup); + + test('Chart.Area is stacked by default', () => { + const { container } = renderArea(); + + expect(queryAllByAttribute('data-ui-name', container, 'StackedArea').length).toBeGreaterThan(0); + }); + + test('stacked={false} is deprecated but still switches the rendering', () => { + const { container } = renderArea({ stacked: false }); + + expect(queryAllByAttribute('data-ui-name', container, 'StackedArea')).toHaveLength(0); + }); + + test('marking a point FORECAST does not move the stack baseline of the other series', () => { + const base = [ + { time: new Date(2024, 0, 1), a: 1, b: 2 }, + { time: new Date(2024, 0, 6), a: 2, b: 2 }, + { time: new Date(2024, 0, 11), a: 3, b: 2 }, + { time: new Date(2024, 0, 16), a: 4, b: 2 }, + ]; + const plain = render( + , + ).container; + const plainPaths = mainAreaPaths(plain).length; + cleanup(); + + const marked: any[] = [...base]; + marked[2] = { ...marked[2], [DATA_TYPE]: FORECAST }; + const { container } = render( + , + ); + + expect(mainAreaPaths(container)).toHaveLength(plainPaths); + }); + }); }); describe('Chart.Bubble', () => { 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..3390f5000c 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..466ea99573 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..73afbb236d 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 23867b29d9..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 ad3f41d320..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 dd0bd7f717..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 f2d6f1ec18..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 2a38f916ca..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 02d9446da9..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 9c351e6e28..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 ac603e9be7..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 807e08dd5f..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 45ae897a8d..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 579597b1dd..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 6addc46ff3..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 da0eb751f8..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 d07f07fc7d..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 908386840d..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 87d7651523..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 c401422755..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 9560bcd39c..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 f2573d9516..8d80dd2f9f 100644 --- a/semcore/d3-chart/src/Area.jsx +++ b/semcore/d3-chart/src/Area.jsx @@ -6,10 +6,16 @@ import { area, curveLinear, line } from 'd3-shape'; 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 { PatternFill } from './Pattern'; +import { + PatternFill, + ForecastGradient, + PotentialGradient, + StrokeMask, +} from './Pattern'; import style from './style/area.shadow.css'; import { definedData, @@ -19,6 +25,7 @@ import { interpolateValue, eventToPoint, invert, + getDataOfType, } from './utils'; class AreaRoot extends Component { @@ -102,6 +109,101 @@ class AreaRoot extends Component { onClick(index, e); } + renderForecast() { + const SArea = SvgElement; + const SAreaLine = SvgElement; + const { + styles, + hide, + d3, + d3Line, + color, + uid, + duration, + y, + transparent, + resolveColor, + } = this.asProps; + const data = getDataOfType(this.asProps.data, FORECAST, y); + + return sstyled(styles)( + <> + + + + + , + ); + } + + renderPotential() { + const SArea = SvgElement; + const SAreaLine = SvgElement; + const { + styles, + hide, + d3, + d3Line, + uid, + duration, + y, + transparent, + } = this.asProps; + const data = getDataOfType(this.asProps.data, POTENTIAL, y); + + return sstyled(styles)( + <> + + + + + + + , + ); + } + render() { const SArea = this.Element; const SAreaLine = SvgElement; @@ -121,9 +223,10 @@ class AreaRoot extends Component { forcedAdvancedMode, resolveColor, patterns, + withGradient, } = this.asProps; const advancedMode = forcedAdvancedMode || !!findComponent(Children, [Area.Line.displayName]); - 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'); @@ -141,6 +244,8 @@ class AreaRoot extends Component { transparent={transparent} /> )} + {this.renderForecast()} + {this.renderPotential()} {duration && } + {duration && } {patterns && ( d[x]).center; const [activeIndex, setActiveIndex] = React.useState(null); const patternKey = color || getChartDefaultColorName(0); @@ -69,6 +76,22 @@ function Dots(props) { onClick(index, e); }, [scale, data, onClick]); + const resolveHighlightColor = React.useCallback((highlight) => { + switch (highlight) { + case GOOD: { + return resolveColor('--intergalactic-chart-data-success'); + } + case BAD: { + return resolveColor('--intergalactic-chart-data-critical'); + } + case INSIGHTFUL: { + return `url(#dotGradient_${uid})`; + } + default: + return ''; + } + }, [uid, resolveColor]); + React.useEffect(() => { const unsubscribeMouseMoveRoot = eventEmitter.subscribe('onMouseMoveChart', (e) => { e.persist(); @@ -97,8 +120,8 @@ function Dots(props) { const active = i === activeIndex; const visible = typeof display === 'function' - ? display(i, i === activeIndex, !isPrev && !isNext) - : display || i === activeIndex || (!isPrev && !isNext); + ? display(i, active, !isPrev && !isNext, d) + : display || active || (!isPrev && !isNext); const radius = active ? ACTIVE_RADIUS : radiusBase; if (!d3.defined()(d)) return acc; if (!visible) return acc; @@ -106,22 +129,56 @@ function Dots(props) { if (!patterns) { acc.push( sstyled(styles)( - , + + {d[HIGHLIGHT_DOT] && ( + <> + + + + )} + {transparent && ( + + )} + + , ), ); } else { @@ -149,6 +206,7 @@ function Dots(props) { const SDots = 'g'; return sstyled(styles)( + { patterns && ( - ) } + )} {dots} , ); 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()} { + return ( + + + + + + + ); +}; +export const LightStrokePattern = (props: { id: string }) => { + return ( + + + + + + ); +}; +export const StrokeMask = (props: { id: string; color: string }) => { + return ( + + + + + + + + + ); +}; +export const ForecastGradient = (props: { id: string; color: string }) => { + return ( + + + + + + + ); +}; +export const PotentialGradient = (props: { id: string; type?: 'line' | 'area' }) => { + const SLinearGradient = 'linearGradient'; + const SStopFrom = 'stop'; + const SStopTo = 'stop'; + const isLine = props.type === 'line'; + const isArea = props.type === 'area'; + + let gradientTransform = 'rotate(-90 0.5 0.5)'; + + if (isLine) { + gradientTransform = ''; + } else if (isArea) { + gradientTransform = 'rotate(-30 0.5 0.5)'; + } + + return sstyled(style)( + + + + + + , + ); +}; + export const PatternFill = ({ id, color, diff --git a/semcore/d3-chart/src/component/Chart/AbstractChart.tsx b/semcore/d3-chart/src/component/Chart/AbstractChart.tsx index 01f0f33c29..d758b34c3c 100644 --- a/semcore/d3-chart/src/component/Chart/AbstractChart.tsx +++ b/semcore/d3-chart/src/component/Chart/AbstractChart.tsx @@ -52,7 +52,7 @@ export abstract class AbstractChart< /** * Padding from the end's of chart to the container (except axis sides) */ - protected plotPadding = 6; + protected plotPadding = 10; protected dataHints = makeDataHintsContainer(); @@ -559,7 +559,9 @@ export abstract class AbstractChart< if (prev === 0) return curr === 0 ? 0 : null; - return ((curr - prev) / Math.abs(prev)) * 100; + const percent = ((curr - prev) / Math.abs(prev)) * 100; + + return Number(percent.toFixed(1)); } protected getTooltipChildren(options: { @@ -622,7 +624,6 @@ export abstract class AbstractChart< const trend = this.getTooltipPercentDeltaTrend(delta); const STooltipDeltaWrapper = Flex; const STooltipDeltaIcon = trend === 'upward' ? DiffUp : DiffDown; - const displayedDelta = delta !== null ? this.defaultTooltipFormatter(Math.abs(delta)) : null; return sstyled(styles)( {(trend === 'upward' || trend === 'downward') && } - {trend !== 'unknown' && displayedDelta && {displayedDelta}%} + {trend !== 'unknown' && {Math.abs(delta!)}%} , ); } diff --git a/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts b/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts index d6d05ccb92..0fada34290 100644 --- a/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts +++ b/semcore/d3-chart/src/component/Chart/AbstractChart.type.ts @@ -6,11 +6,20 @@ import type React from 'react'; import type { PatternsConfig } from '../../Pattern'; // @ts-ignore import type { PlotSummarizerConfig } from '../../Plot'; -import type { PlotEventEmitter } from '../../utils'; +import type { interpolateValue, PlotEventEmitter } from '../../utils'; import type { BaseChartLegendProps } from '../ChartLegend/BaseLegend.type'; import type { TrendProps } from '../ChartLegend/LegendFlex/LegendFlex.type'; import type { LegendItemKey } from '../ChartLegend/LegendItem/LegendItem.type'; +export const GOOD = Symbol('GOOD'); +export const BAD = Symbol('BAD'); +export const INSIGHTFUL = Symbol('INSIGHTFUL'); +export const HIGHLIGHT_DOT = Symbol('HIGHLIGHT_DOT'); + +export const DATA_TYPE = Symbol('DATA_TYPE'); +export const FORECAST = Symbol('FORECAST_DATA'); +export const POTENTIAL = Symbol('POTENTIAL_DATA'); + export type BaseLegendProps = BaseChartLegendProps & { /** * Disable hover (for transition items legend of each not hovered) @@ -44,7 +53,10 @@ export type BaseLegendProps = BaseChartLegendProps & { ); export type ObjectDataKey = string; -export type ObjectData = Record; +export type ObjectData = Record & { + [HIGHLIGHT_DOT]?: typeof GOOD | typeof BAD | typeof INSIGHTFUL; + [DATA_TYPE]?: typeof POTENTIAL | typeof FORECAST; +}; export type ListData = ObjectData[]; /** diff --git a/semcore/d3-chart/src/component/Chart/AreaChart.tsx b/semcore/d3-chart/src/component/Chart/AreaChart.tsx index ff8a38cc13..fb834f3498 100644 --- a/semcore/d3-chart/src/component/Chart/AreaChart.tsx +++ b/semcore/d3-chart/src/component/Chart/AreaChart.tsx @@ -7,7 +7,14 @@ import React from 'react'; import { Area, minMax, HoverLine, StackedArea } from '../..'; import type { ChartState } from './AbstractChart'; import { AbstractChart } from './AbstractChart'; -import type { AreaChartData, AreaChartProps, AreaChartType, AreaChartDefaultProps } from './AreaChart.type'; +import type { ObjectData } from './AbstractChart.type'; +import { HIGHLIGHT_DOT } from './AbstractChart.type'; +import type { + AreaChartData, + AreaChartProps, + AreaChartType, + AreaChartDefaultProps, +} from './AreaChart.type'; import { localizedMessages } from '../../translations/__intergalactic-dynamic-locales'; class AreaChartComponent extends AbstractChart< @@ -27,6 +34,7 @@ class AreaChartComponent extends AbstractChart< showXAxis: true, showYAxis: true, showTooltip: true, + stacked: true, locale: 'en', deltaPercentGrowthColor: 'success', } as const; @@ -85,8 +93,9 @@ class AreaChartComponent extends AbstractChart< transparent={highlightedItem !== -1 && highlightedItem !== index} curve={curve} onClick={onClickArea} + withGradient > - + ) ); @@ -95,6 +104,8 @@ class AreaChartComponent extends AbstractChart< ); } + const withGradient = dataDefinitions.filter((item) => item.checked).length === 1; + return dataDefinitions.map((item, index) => { return ( item.checked && ( @@ -106,8 +117,9 @@ class AreaChartComponent extends AbstractChart< transparent={highlightedItem !== -1 && highlightedItem !== index} curve={curve} onClick={onClickArea} + withGradient={withGradient} > - + ) ); @@ -137,6 +149,10 @@ class AreaChartComponent extends AbstractChart< protected getLegendAriaLabel(): string { return this.asProps.getI18nText('legendForChart', { chartType: 'Area' }); } + + 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/component/Chart/AreaChart.type.ts b/semcore/d3-chart/src/component/Chart/AreaChart.type.ts index 4ac48e792a..b78e8895e5 100644 --- a/semcore/d3-chart/src/component/Chart/AreaChart.type.ts +++ b/semcore/d3-chart/src/component/Chart/AreaChart.type.ts @@ -3,10 +3,9 @@ import type { Intergalactic } from '@semcore/core'; import type { ScaleLinear, ScaleTime } from 'd3-scale'; import type { CurveFactory } from 'd3-shape'; -import type { AriaNameProps, BaseChartProps } from './AbstractChart.type'; -import type { interpolateValue } from '../../utils'; +import type { AriaNameProps, BaseChartProps, ObjectData } from './AbstractChart.type'; -export type AreaChartData = Array>; +export type AreaChartData = Array; export type AreaChartProps = BaseChartProps & { /** Field name that groups the data points */ @@ -19,7 +18,11 @@ export type AreaChartProps = BaseChartProps & { showDots?: boolean; /** D3 curve factory for line interpolation (e.g., curveLinear, curveCardinal) */ curve?: CurveFactory; - /** Enables stacked area chart mode */ + /** + * Enables stacked area chart mode + * @deprecated True by default. AreaChart should be only stacked. + * @default true + */ stacked?: boolean; /** Callback triggered when a user clicks on a chart at a position corresponding to a data item */ onClickArea?: (index: number, event: React.SyntheticEvent) => void; diff --git a/semcore/d3-chart/src/component/Chart/LineChart.tsx b/semcore/d3-chart/src/component/Chart/LineChart.tsx index 780be4bcc7..73287cf379 100644 --- a/semcore/d3-chart/src/component/Chart/LineChart.tsx +++ b/semcore/d3-chart/src/component/Chart/LineChart.tsx @@ -3,6 +3,8 @@ import i18nEnhance from '@semcore/core/lib/utils/enhances/i18nEnhance'; import { type ScaleLinear, scaleLinear, scaleTime } from 'd3-scale'; import React from 'react'; +import type { ObjectData } from './AbstractChart.type'; +import { HIGHLIGHT_DOT } from './AbstractChart.type'; import type { LineChartData, LineChartDefaultProps, LineChartProps, LineChartType } from './LineChart.type'; // @ts-ignore import { Line, minMax, HoverLine } from '../..'; @@ -84,7 +86,7 @@ class LineChartComponent extends AbstractChart< curve={curve} onClick={onClickLine} > - + {area?.[item.id] && ( )} @@ -117,6 +119,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/component/Chart/index.ts b/semcore/d3-chart/src/component/Chart/index.ts index cd3ee5bbc3..0afd4d430c 100644 --- a/semcore/d3-chart/src/component/Chart/index.ts +++ b/semcore/d3-chart/src/component/Chart/index.ts @@ -1,3 +1,4 @@ +import { GOOD, BAD, INSIGHTFUL, HIGHLIGHT_DOT, DATA_TYPE, POTENTIAL, FORECAST } from './AbstractChart.type'; import { AreaChart } from './AreaChart'; import { BarChart } from './BarChart'; import { BubbleChart } from './BubbleChart'; @@ -23,3 +24,13 @@ export default { Cigarette: CigaretteChart, CompactHorizontalBar: CompactHorizontalBarChart, }; + +export { + GOOD, + BAD, + INSIGHTFUL, + HIGHLIGHT_DOT, + DATA_TYPE, + FORECAST, + POTENTIAL, +}; diff --git a/semcore/d3-chart/src/index.js b/semcore/d3-chart/src/index.js index e97384de3f..d2b3b57c61 100644 --- a/semcore/d3-chart/src/index.js +++ b/semcore/d3-chart/src/index.js @@ -1,6 +1,7 @@ export { default as Plot } from './Plot'; export { ChartLegend, ChartLegendTable } from './component/ChartLegend'; export { default as Chart } from './component/Chart'; +export * from './component/Chart'; export { default as StackGroupBar } from './component/StackGroupBar/StackGroupBar'; export { SvgElement } from './component/SvgElement'; export { XAxis, YAxis } from './Axis'; diff --git a/semcore/d3-chart/src/style/area.shadow.css b/semcore/d3-chart/src/style/area.shadow.css index 27072527fa..009aad758e 100644 --- a/semcore/d3-chart/src/style/area.shadow.css +++ b/semcore/d3-chart/src/style/area.shadow.css @@ -3,6 +3,9 @@ SArea { fill: var(--intergalactic-chart-palette-order-1, oklch(0.23 0.01 140)); fill-opacity: 0.1; +} + +SArea[withGradient] { mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.48) 60%, transparent 100%); } @@ -12,7 +15,7 @@ SArea[color] { SArea[pattern] { fill: var(--pattern); - fill-opacity: 0.7; + fill-opacity: 0.5; } SArea[transparent] { @@ -48,4 +51,4 @@ SNull { SNull[hide] { display: none; -} \ No newline at end of file +} diff --git a/semcore/d3-chart/src/style/dot.shadow.css b/semcore/d3-chart/src/style/dot.shadow.css index 18534242fa..65751b9e66 100644 --- a/semcore/d3-chart/src/style/dot.shadow.css +++ b/semcore/d3-chart/src/style/dot.shadow.css @@ -28,6 +28,41 @@ SDot[color] { fill: var(--color); } +SDotHighlight { + &[color] { + opacity: 0.4; + fill: var(--color); + } + + &[transparent] { + opacity: 0.12; + } +} + +SDotBorder { + fill: none; + stroke-width: 1px; + stroke: var(--intergalactic-chart-grid-dot-outer-border, oklch(from oklch(0.23 0.01 140) l c h / 0.1)); + stroke-dasharray: 2; + + &[transparent] { + opacity: 0.03; + } +} + SDot[transparent] { opacity: 0.3; -} \ No newline at end of file +} + +SDotBackdrop { + stroke-width: 1px; + stroke: var(--intergalactic-chart-grid-border, #ffffff); + fill: var(--intergalactic-chart-grid-border, #ffffff); + transition-property: cx, cy; + transition-duration: var(--duration); + transition-timing-function: ease-in-out; + + &[hide] { + display: none + } +} diff --git a/semcore/d3-chart/src/style/pattern.shadow.css b/semcore/d3-chart/src/style/pattern.shadow.css new file mode 100644 index 0000000000..41859e8b33 --- /dev/null +++ b/semcore/d3-chart/src/style/pattern.shadow.css @@ -0,0 +1,10 @@ + +SLinearGradient { + SStopFrom { + stop-color: var(--violet-300, oklch(0.74 0.17 303)); + } + + SStopTo { + stop-color: var(--green-100, oklch(0.9 0.11 175)); + } +} 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__/area.ts b/stories/components/d3-chart/__mocks__/area.ts index 43376283a3..17050dfdcb 100644 --- a/stories/components/d3-chart/__mocks__/area.ts +++ b/stories/components/d3-chart/__mocks__/area.ts @@ -1,17 +1,15 @@ -import { interpolateValue } from '@semcore/ui/d3-chart'; +import { interpolateValue, GOOD, HIGHLIGHT_DOT, BAD, INSIGHTFUL, DATA_TYPE, FORECAST, POTENTIAL } from '@semcore/ui/d3-chart'; export default { Default: [ { time: new Date(2025, 0, 1), line: 3 }, { time: new Date(2025, 0, 5), line: 7 }, - { time: new Date(2025, 0, 10), line: 1 }, - { time: new Date(2025, 0, 15), line: 9 }, + { time: new Date(2025, 0, 10), line: 5 }, + { time: new Date(2025, 0, 15), line: 9, [HIGHLIGHT_DOT]: GOOD } as const, { time: new Date(2025, 0, 20), line: 4 }, { time: new Date(2025, 0, 25), line: 6 }, - { time: new Date(2025, 0, 30), line: 2 }, + { time: new Date(2025, 0, 30), line: 2, [HIGHLIGHT_DOT]: BAD } as const, { time: new Date(2025, 1, 4), line: 8 }, - { time: new Date(2025, 1, 9), line: 0 }, - { time: new Date(2025, 1, 14), line: 5 }, ], Interpolation: [ { time: new Date(2025, 0, 1), line1: 5, line2: 3 }, @@ -45,4 +43,24 @@ export default { { x: 4, y: 1 }, { x: 5, y: null }, ], + Forecast: [ + { time: new Date(2025, 0, 1), line: 3 }, + { time: new Date(2025, 0, 5), line: 7 }, + { time: new Date(2025, 0, 10), line: 1 }, + { time: new Date(2025, 0, 15), line: 9 }, + { time: new Date(2025, 0, 20), line: 4 }, + { time: new Date(2025, 0, 25), line: 6 }, + { time: new Date(2025, 0, 30), line: 2, [DATA_TYPE]: FORECAST } as const, + { time: new Date(2025, 1, 4), line: 8, [DATA_TYPE]: FORECAST } as const, + ], + Potential: [ + { time: new Date(2025, 0, 1), line: 3 }, + { time: new Date(2025, 0, 5), line: 7 }, + { time: new Date(2025, 0, 10), line: 1 }, + { time: new Date(2025, 0, 15), line: 9 }, + { time: new Date(2025, 0, 20), line: 4 }, + { time: new Date(2025, 1, 14), line: 5 }, + { time: new Date(2025, 1, 18), line: 7, [DATA_TYPE]: POTENTIAL } as const, + { time: new Date(2025, 1, 28), line: 8, [DATA_TYPE]: POTENTIAL, [HIGHLIGHT_DOT]: INSIGHTFUL } as const, + ], }; diff --git a/stories/components/d3-chart/__mocks__/effects/withAddedData.ts b/stories/components/d3-chart/__mocks__/effects/withAddedData.ts new file mode 100644 index 0000000000..018aa17593 --- /dev/null +++ b/stories/components/d3-chart/__mocks__/effects/withAddedData.ts @@ -0,0 +1,9 @@ +import type { ListData, ObjectData } from '@semcore/d3-chart'; + +export const withAddedData = +

(payload: P, at?: number) => + (base: ListData): ListData => { + if (at === undefined) return [...base, payload]; + + return [...base.slice(0, at), { ...payload }, ...base.slice(at)]; + }; diff --git a/stories/components/d3-chart/__mocks__/effects/withOmittedKey.ts b/stories/components/d3-chart/__mocks__/effects/withOmittedKey.ts new file mode 100644 index 0000000000..1d1af3fe5a --- /dev/null +++ b/stories/components/d3-chart/__mocks__/effects/withOmittedKey.ts @@ -0,0 +1,10 @@ +import type { ListData } from '@semcore/d3-chart'; + +export const withOmittedKey = + (key: string) => + (base: ListData): ListData => + base.map((i) => { + const { [key]: ommitedKey, ...rest } = i; + + return { ...rest }; + }); diff --git a/stories/components/d3-chart/__mocks__/effects/withUpdatedKeyAt.ts b/stories/components/d3-chart/__mocks__/effects/withUpdatedKeyAt.ts new file mode 100644 index 0000000000..256ed0a165 --- /dev/null +++ b/stories/components/d3-chart/__mocks__/effects/withUpdatedKeyAt.ts @@ -0,0 +1,5 @@ +import type { ListData, ObjectData } from '@semcore/ui/d3-chart'; + +export const withUpdatedKeyAt = +

(index: number, payload: P) => + (base: ListData): ListData => [...base.slice(0, index), { ...base[index], ...payload }, ...base.slice(index + 1)]; 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/__mocks__/utils/compose.ts b/stories/components/d3-chart/__mocks__/utils/compose.ts new file mode 100644 index 0000000000..0c002190d8 --- /dev/null +++ b/stories/components/d3-chart/__mocks__/utils/compose.ts @@ -0,0 +1 @@ +export const compose = (...fns: Array<(base: D) => D>) => (base: D) => fns.reduce((acc, fn) => fn(acc), base); diff --git a/stories/components/d3-chart/__mocks__/utils/when.ts b/stories/components/d3-chart/__mocks__/utils/when.ts new file mode 100644 index 0000000000..05473030e6 --- /dev/null +++ b/stories/components/d3-chart/__mocks__/utils/when.ts @@ -0,0 +1,2 @@ +export const identity = (data: D) => data; +export const when = (condition: boolean | undefined, effect: (data: D) => D) => condition ? effect : identity; diff --git a/stories/components/d3-chart/docs/examples/area-chart/basic-usage.tsx b/stories/components/d3-chart/docs/examples/area-chart/basic-usage.tsx index 8ca932ff25..c7d5ae76bd 100644 --- a/stories/components/d3-chart/docs/examples/area-chart/basic-usage.tsx +++ b/stories/components/d3-chart/docs/examples/area-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'; @@ -14,14 +15,32 @@ function formatDate(value: any) { const Demo = () => { return ( - + + + + + ); }; 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/area-chart.stories.tsx b/stories/components/d3-chart/tests/area-chart.stories.tsx index b03701f3a2..c4c4f6334f 100644 --- a/stories/components/d3-chart/tests/area-chart.stories.tsx +++ b/stories/components/d3-chart/tests/area-chart.stories.tsx @@ -13,12 +13,33 @@ export default meta; export const AnimatedDots: StoryObj = { render: AnimatedDotsExample, }; +const storyKnob = { table: { category: 'Story data knobs' } } as const; export const BasicUsage = { render: BasicUsageExample, argTypes: getChartArgTypes({ - useCustomValueFormatter: { control: 'boolean' }, - withZeroValue: { control: 'boolean' }, + showDots: { control: 'boolean' }, + stacked: { control: 'boolean' }, + + useCustomValueFormatter: { control: 'boolean', ...storyKnob }, + withZeroValue: { control: 'boolean', ...storyKnob }, + singleSeries: { control: 'boolean', ...storyKnob }, + withInterpolatedGaps: { control: 'boolean', ...storyKnob }, + curveName: { + control: 'select', + options: ['linear', 'cardinal', 'monotoneX', 'step', 'basis'], + ...storyKnob, + }, + highlightDots: { + control: 'select', + options: ['none', 'good', 'bad', 'insightful', 'mixed'], + ...storyKnob, + }, + dataType: { + control: 'select', + options: ['none', 'forecast', 'potential', 'both'], + ...storyKnob, + }, }), args: areaExampleProps, }; diff --git a/stories/components/d3-chart/tests/d3-chart-base.stories.tsx b/stories/components/d3-chart/tests/d3-chart-base.stories.tsx index a20866b11e..d0c30fce39 100644 --- a/stories/components/d3-chart/tests/d3-chart-base.stories.tsx +++ b/stories/components/d3-chart/tests/d3-chart-base.stories.tsx @@ -74,7 +74,7 @@ export const TooltipDeltaEdgeCases = { argTypes: getChartArgTypes({ deltaOverride: { control: 'select', - options: ['off', 'divideByZero'], + options: ['off', 'custom', 'divideByZero', 'includesFirstPoint'], }, }), args: TooltipDeltaEdgeCasesProps, diff --git a/stories/components/d3-chart/tests/examples/area-chart/__mocks__/index.ts b/stories/components/d3-chart/tests/examples/area-chart/__mocks__/index.ts new file mode 100644 index 0000000000..c7d809d077 --- /dev/null +++ b/stories/components/d3-chart/tests/examples/area-chart/__mocks__/index.ts @@ -0,0 +1,105 @@ +import { BAD, DATA_TYPE, FORECAST, GOOD, HIGHLIGHT_DOT, INSIGHTFUL, interpolateValue, POTENTIAL, type ListData } from '@semcore/d3-chart'; + +import { withAddedData } from '../../../../__mocks__/effects/withAddedData'; +import { withOmittedKey } from '../../../../__mocks__/effects/withOmittedKey'; +import { withUpdatedKeyAt } from '../../../../__mocks__/effects/withUpdatedKeyAt'; +import { compose } from '../../../../__mocks__/utils/compose'; +import { when } from '../../../../__mocks__/utils/when'; + +type PipelineOptions = { + withZeroValue?: boolean; + withSingleSeries?: boolean; + highlightDots?: 'none' | 'good' | 'bad' | 'insightful' | 'mixed'; + dataType?: 'none' | 'forecast' | 'potential' | 'both'; + withInterpolatedGaps?: boolean; +}; + +/** + * Points whose value is replaced with `interpolateValue`, which makes the chart bridge the + * gap instead of breaking the line. They sit in the middle of the run so there is a real + * value on each side to interpolate between. + */ +const INTERPOLATED_AT = [3, 4, 7]; + +export const base: ListData = [ + { time: new Date('2024-01-01'), line: 2, line2: 3 }, + { time: new Date('2024-01-06'), line: 4, line2: 3 }, + { time: new Date('2024-01-11'), line: 3, line2: 3 }, + { time: new Date('2024-01-16'), line: 6, line2: 4 }, + { time: new Date('2024-01-21'), line: 5, line2: 3 }, + { time: new Date('2024-01-26'), line: 7, line2: 5 }, + { time: new Date('2024-01-31'), line: 6, line2: 2 }, + { time: new Date('2024-02-05'), line: 8, line2: 5 }, + { time: new Date('2024-02-10'), line: 9, line2: 7 }, + { time: new Date('2024-02-15'), line: 10, line2: 8 }, +]; + +/** + * `source` defaults to `base` but stays overridable, so a caller that hands the story its + * own `data` (a browser test, or the Storybook control) keeps it — the knobs then apply on + * top of that dataset instead of being silently discarded. + */ +export default (props: PipelineOptions, source: ListData = base): ListData => { + return compose( + when(props.withZeroValue, withUpdatedKeyAt(2, { line: 0 })), + when(props.withSingleSeries, withOmittedKey('line2')), + // Applied after `withOmittedKey`, so with one series the gaps have to go on `line` — + // writing them to `line2` would put the dropped series back. + when( + props.withInterpolatedGaps, + compose( + ...INTERPOLATED_AT.map((index) => + withUpdatedKeyAt( + index, + props.withSingleSeries ? { line: interpolateValue } : { line2: interpolateValue }, + ), + ), + ), + ), + when(props.highlightDots === 'good', withUpdatedKeyAt(5, { [HIGHLIGHT_DOT]: GOOD })), + when(props.highlightDots === 'bad', withUpdatedKeyAt(6, { [HIGHLIGHT_DOT]: BAD })), + when(props.highlightDots === 'insightful', withUpdatedKeyAt(9, { [HIGHLIGHT_DOT]: INSIGHTFUL })), + when( + props.highlightDots === 'mixed', + compose( + withUpdatedKeyAt(5, { [HIGHLIGHT_DOT]: GOOD }), + withUpdatedKeyAt(6, { [HIGHLIGHT_DOT]: BAD }), + withUpdatedKeyAt(9, { [HIGHLIGHT_DOT]: INSIGHTFUL }), + ), + ), + when( + props.dataType === 'forecast' || props.dataType === 'both', + compose( + withAddedData({ + time: new Date('2024-02-20'), + line: 12, + [DATA_TYPE]: FORECAST, + ...(!props.withSingleSeries && { line2: 9.6 }), + }), + withAddedData({ + time: new Date('2024-02-25'), + line: 11, + [DATA_TYPE]: FORECAST, + ...(!props.withSingleSeries && { line2: 8.8 }), + }), + ), + ), + when( + props.dataType === 'potential' || props.dataType === 'both', + compose( + withAddedData({ + time: new Date('2024-03-01'), + line: 15.4, + [DATA_TYPE]: POTENTIAL, + ...(!props.withSingleSeries && { line2: 12.3 }), + }), + withAddedData({ + time: new Date('2024-03-06'), + line: 17.6, + [DATA_TYPE]: POTENTIAL, + ...(!props.withSingleSeries && { line2: 14.1 }), + }), + ), + ), + )(source); +}; diff --git a/stories/components/d3-chart/tests/examples/area-chart/basic-usage.tsx b/stories/components/d3-chart/tests/examples/area-chart/basic-usage.tsx index 675ff650e6..44fbd9da07 100644 --- a/stories/components/d3-chart/tests/examples/area-chart/basic-usage.tsx +++ b/stories/components/d3-chart/tests/examples/area-chart/basic-usage.tsx @@ -1,8 +1,10 @@ import { Box } from '@semcore/ui/base-components'; import type { AreaChartProps } from '@semcore/ui/d3-chart'; import { Chart } from '@semcore/ui/d3-chart'; +import { curveBasis, curveCardinal, curveLinear, curveMonotoneX, curveStep } from 'd3-shape'; import React from 'react'; +import dataPipeline, { base as baseData } from './__mocks__'; import { getChartProps, getPropsToChart } from '../stories_props_helper'; /** @@ -10,32 +12,98 @@ import { getChartProps, getPropsToChart } from '../stories_props_helper'; * to format numbers. The title is always rendered by the built-in formatter, which prints * the `Date` group key as a date. */ -function formatValue(value: any) { - return `${value} clicks`; +function formatValue(value: unknown) { + if (typeof value === 'number' && !Number.isNaN(value)) { + return `${value} clicks`; + } + + return String(value); } -type AreaChartStoryProps = AreaChartProps & { +/** + * Curves are picked by name rather than passed as a factory: browser tests hand props to + * the story as plain JSON through `loadPage`, so a function would not survive the trip. + */ +const curves = { + linear: curveLinear, + cardinal: curveCardinal, + monotoneX: curveMonotoneX, + step: curveStep, + basis: curveBasis, +}; + +export type CurveName = keyof typeof curves; +export type HighlightDotsMode = 'none' | 'good' | 'bad' | 'insightful' | 'mixed'; +export type DataTypeMode = 'none' | 'forecast' | 'potential' | 'both'; + +type AreaChartStoryProps = Omit & { + patterns?: AreaChartProps['patterns'] | false; /** Set to true to replace the built-in tooltip value formatter with `formatValue`. */ useCustomValueFormatter?: boolean; /** * Swaps in a dataset where `line` drops to 0 in the middle. + */ + withZeroValue?: boolean; + /** D3 curve factory, chosen by name — see `curves`. */ + curveName?: CurveName; + /** + * Marks data points with `HIGHLIGHT_DOT`, which makes `Dots` draw the dashed border ring + * and the translucent halo around them. * - * `getPercentDelta` cannot divide by a previous value of 0, so that series has no - * delta on the next point while `line2` still has one. Together with - * `showDeltaPercentInTooltip` this is the mixed row that no other dataset produces. + * `insightful` resolves to the violet→green gradient instead of a flat token color, so it + * is the only mode that exercises the `url(#dotGradient_*)` branch. */ - withZeroValue?: boolean; + highlightDots?: HighlightDotsMode; + /** + * Appends points marked with `DATA_TYPE`, which `Area` pulls out of the main path and + * renders as separate dashed + masked segments. + */ + dataType?: DataTypeMode; + /** + * Replaces three mid-run values with `interpolateValue`, so the chart bridges the gap + * instead of breaking the line. + */ + withInterpolatedGaps?: boolean; + /** + * Drops `line2` so only one series is left. + */ + singleSeries?: boolean; }; const Demo = (props: AreaChartStoryProps) => { + const { + plotWidth, + plotHeight, + useCustomValueFormatter, + withZeroValue, + curveName, + highlightDots, + dataType, + singleSeries, + withInterpolatedGaps, + data, + ...chartProps + } = getPropsToChart(props); + + const chartData = dataPipeline( + { + withZeroValue, + withSingleSeries: singleSeries, + highlightDots, + dataType, + withInterpolatedGaps, + }, + data, + ); + const onClickHandler = (index: number, event: React.SyntheticEvent) => { - const clickedItem = data[index]; + const clickedItem = chartData[index]; console.log('Clicked area chart point:'); console.log('→ Index:', index); console.log('→ Data item:', clickedItem); console.log('→ Event:', event); }; - const { plotWidth, plotHeight, useCustomValueFormatter, withZeroValue, ...chartProps } = getPropsToChart(props); + return ( { overflow='auto' > @@ -56,48 +126,22 @@ const Demo = (props: AreaChartStoryProps) => { ); }; -const data = [ - { time: new Date('2024-01-01'), line: 2, line2: 3 }, - { time: new Date('2024-01-06'), line: 4, line2: 3 }, - { time: new Date('2024-01-11'), line: 3, line2: 3 }, - { time: new Date('2024-01-16'), line: 6, line2: 4 }, - { time: new Date('2024-01-21'), line: 5, line2: 3 }, - { time: new Date('2024-01-26'), line: 7, line2: 5 }, - { time: new Date('2024-01-31'), line: 6, line2: 2 }, - { time: new Date('2024-02-05'), line: 8, line2: 5 }, - { time: new Date('2024-02-10'), line: 9, line2: 7 }, - { time: new Date('2024-02-15'), line: 10, line2: 8 }, -]; - -/** - * Same shape as `data`, but `line` sits at 0 on Jan 11. - * - * Hovering Jan 16 then gives `line` no delta (its previous value is 0) while `line2` - * still has one, so the tooltip switches to three columns and the `line` row only fills - * two of them. - */ -const dataWithZeroValue = [ - { time: new Date('2024-01-01'), line: 2, line2: 3 }, - { time: new Date('2024-01-06'), line: 4, line2: 3 }, - { time: new Date('2024-01-11'), line: 0, line2: 3 }, - { time: new Date('2024-01-16'), line: 6, line2: 4 }, - { time: new Date('2024-01-21'), line: 5, line2: 3 }, - { time: new Date('2024-01-26'), line: 7, line2: 5 }, - { time: new Date('2024-01-31'), line: 6, line2: 2 }, - { time: new Date('2024-02-05'), line: 8, line2: 5 }, - { time: new Date('2024-02-10'), line: 9, line2: 7 }, - { time: new Date('2024-02-15'), line: 10, line2: 8 }, -]; - export const defaultProps = getChartProps({ showDots: true, - stacked: false, + // Mirrors the component default. `stacked` is deprecated and always true by default + stacked: true, + patterns: false, groupKey: 'time', - data, + data: baseData, // Off by default: the tooltip then shows the date in the title and plain numbers in the // values, which is what the built-in formatter does. useCustomValueFormatter: false, withZeroValue: false, + curveName: 'linear', + highlightDots: 'none', + dataType: 'none', + singleSeries: false, + withInterpolatedGaps: false, }); Demo.defaultProps = defaultProps; diff --git a/stories/components/d3-chart/tests/examples/chart-legend/legend-table-data.tsx b/stories/components/d3-chart/tests/examples/chart-legend/legend-table-data.tsx index f1bcef9cc0..3a057c48a6 100644 --- a/stories/components/d3-chart/tests/examples/chart-legend/legend-table-data.tsx +++ b/stories/components/d3-chart/tests/examples/chart-legend/legend-table-data.tsx @@ -1,17 +1,8 @@ import { Box } from '@semcore/ui/base-components'; -import type { LegendTableProps } from '@semcore/ui/d3-chart'; import { ChartLegendTable } from '@semcore/ui/d3-chart'; import { Text } from '@semcore/ui/typography'; import React from 'react'; -/** - * `highlightedItem` is an internal prop of `BaseLegend` — it dims every row except the - * highlighted one, but it is declared on the component's inner props, not on the public - * `LegendTableProps`. Declaring it here keeps the story typed and the prop greppable, - * instead of silencing the whole JSX node with a suppression comment. - */ -type LegendTableWithHighlight = LegendTableProps & { highlightedItem?: number }; - type LegendTableStoryProps = { size?: 'm' | 'l'; /** Width of the legend, 185px in the design. */ @@ -64,17 +55,16 @@ const Demo = (props: LegendTableStoryProps) => { ], })); - const legendProps: LegendTableWithHighlight = { - items, - size, - onChangeVisibleItem, - 'aria-label': 'Chart legend', - highlightedItem, - }; - return ( - + ); }; diff --git a/stories/components/d3-chart/tests/examples/d3-chart/hovered-tick.tsx b/stories/components/d3-chart/tests/examples/d3-chart/hovered-tick.tsx index be081a2156..6909c373a5 100644 --- a/stories/components/d3-chart/tests/examples/d3-chart/hovered-tick.tsx +++ b/stories/components/d3-chart/tests/examples/d3-chart/hovered-tick.tsx @@ -36,12 +36,8 @@ const Demo = (props: HoveredTickStoryProps) => { } = props; // `hideHoverLine` is read by `HoverLine`/`HoverRect` at runtime, but it is missing from the - // public `HoverProps` type (it is only declared on `HoverLine.Tooltip`), so it is declared - // here instead of being cast away — both props must stay visible in the type. - const hoverProps: { hideTickHover?: boolean; hideHoverLine?: boolean } = { - hideTickHover, - hideHoverLine, - }; + // public `HoverProps` type (it is only declared on `HoverLine.Tooltip`), hence the cast. + const hoverProps = { hideTickHover, hideHoverLine } as { hideTickHover?: boolean }; const yScale = scaleLinear() .range([height - MARGIN, MARGIN]) diff --git a/stories/components/d3-chart/tests/examples/d3-chart/tooltip-default-format.tsx b/stories/components/d3-chart/tests/examples/d3-chart/tooltip-default-format.tsx index e182fdce74..ffef5d5095 100644 --- a/stories/components/d3-chart/tests/examples/d3-chart/tooltip-default-format.tsx +++ b/stories/components/d3-chart/tests/examples/d3-chart/tooltip-default-format.tsx @@ -56,8 +56,7 @@ type TooltipFormatStoryProps = LineChartProps & { }; const Demo = (props: TooltipFormatStoryProps) => { - const { plotWidth, plotHeight, titleFormat, ...chartProps } = - getPropsToChart(props); + const { plotWidth, plotHeight, titleFormat, ...chartProps } = getPropsToChart(props) as any; const options = titleFormat && titleFormat !== 'off' ? titleFormats[titleFormat as keyof typeof titleFormats] diff --git a/stories/components/d3-chart/tests/examples/d3-chart/tooltip-delta-edge-cases.tsx b/stories/components/d3-chart/tests/examples/d3-chart/tooltip-delta-edge-cases.tsx index 36018b4e4c..c4f8ec7bd8 100644 --- a/stories/components/d3-chart/tests/examples/d3-chart/tooltip-delta-edge-cases.tsx +++ b/stories/components/d3-chart/tests/examples/d3-chart/tooltip-delta-edge-cases.tsx @@ -1,85 +1,152 @@ import { Box } from '@semcore/ui/base-components'; -import type { BarChartData, BarChartProps } from '@semcore/ui/d3-chart'; +import type { BarChartProps } from '@semcore/ui/d3-chart'; import { Chart } from '@semcore/ui/d3-chart'; import React from 'react'; import { getChartProps, getPropsToChart } from '../stories_props_helper'; /** - * Edge cases of the tooltip percent delta, one per series, written as `[before, after]`. + * Edge cases of the tooltip percent delta, one per series. * - * `before` is the baseline and carries no delta at all — there is no previous point. - * `after` produces every case at once: hover it and read the tooltip top to bottom. + * Only two points: `before` is the baseline and carries no delta at all (there is no + * previous point), `after` produces every case at once. Hover `after` and read the + * tooltip top to bottom. + * + * series | before | after | expected diff + * ---------------|--------|--------|------------------------------------------------ + * overHundred | 1 | 5 | +400% — growth past 100% + * huge | 1 | 100 | +9900% — does the column still fit? + * fraction | 100 | 100.5 | +0.5% — one decimal place + * roundsToZero | 10000 | 10004 | +0.04% rounds to 0 -> shown as a stable "0%" + * shrank | 100 | 50 | 50% — declines show no minus, only DiffDown + * droppedToZero | 100 | 0 | 100% — a drop to zero, also downward + * fromZero | 0 | 7 | no diff — cannot divide by 0, cell stays empty + * zeroToZero | 0 | 0 | 0% — stable + * unchanged | 50 | 50 | 0% — stable + * negativeBase | -10 | -5 | +50% — dividing by |prev| keeps growth green + * + * Two rows are worth a decision: + * - `roundsToZero` turns a real change into "no change" once it rounds below 0.05%; + * - `fromZero` carries no percentage, because there is nothing to divide by. Its cell is + * still rendered, just empty, which is what keeps the three column grid aligned — + * every row below it has to start its own line. */ -const series = { - overHundred: [1, 5], // +400% growth past 100% - huge: [1, 100], // +9,900% grouped by locale — and does the column fit? - fraction: [100, 100.5], // +0.5% one decimal place - roundsToZero: [10000, 10004], // +0.04% prints "0.0%", still upward - shrank: [100, 50], // 50% declines show no minus, only DiffDown - droppedToZero: [100, 0], // 100% a drop to zero, also downward - fromZero: [0, 7], // no diff cannot divide by 0, cell stays empty - zeroToZero: [0, 0], // 0% stable - unchanged: [50, 50], // 0% stable - negativeBase: [-10, -5], // +50% dividing by |prev| keeps growth green -}; - -const valuesAt = (index: 0 | 1) => - Object.fromEntries(Object.entries(series).map(([key, values]) => [key, values[index]])); - const data = [ - { point: 'before', ...valuesAt(0) }, - { point: 'after', ...valuesAt(1) }, + { + point: 'before', + overHundred: 1, + huge: 1, + fraction: 100, + roundsToZero: 10000, + shrank: 100, + droppedToZero: 100, + fromZero: 0, + zeroToZero: 0, + unchanged: 50, + negativeBase: -10, + }, + { + point: 'after', + overHundred: 5, + huge: 100, + fraction: 100.5, + roundsToZero: 10004, + shrank: 50, + droppedToZero: 0, + fromZero: 7, + zeroToZero: 0, + unchanged: 50, + negativeBase: -5, + }, ]; -/** - * Overrides are typed against the chart's contract, not against this file's data: the - * chart hands them its own `BarChartData`, whose values are `string | number | Date`, so - * each one has to narrow to a number itself — exactly what a real consumer has to do. - */ -type DeltaOverride = NonNullable; - -const numberAt = (chartData: BarChartData, index: number, key: string) => { - const value = chartData[index]?.[key]; - - return typeof value === 'number' ? value : null; -}; +type DeltaData = (typeof data)[number]; /** - * The one override worth a control: it shows what the chart does with a return value that - * is valid yet unusable, which nothing in the `off` path can demonstrate. + * Variants for the `getPercentDelta` prop, which replaces the built-in delta calculation. + * + * Every variant honours the declared contract — each one is annotated `: number | null` + * and TypeScript accepts it. 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. * - * It is the built-in formula without its zero guard — the likeliest omission when someone - * reimplements it. `NaN` and `Infinity` are both of type `number`, so neither TypeScript - * nor the chart rejects them: on the second group `zeroToZero` (0 -> 0) yields `NaN` and - * `fromZero` (0 -> 7) yields `Infinity`. The tooltip prints both verbatim, and `NaN` is - * additionally classified as the `stable` trend, so it is greyed out as though nothing - * had changed. + * variant | what it demonstrates | hover + * -------------------|---------------------------------------------|------------- + * custom | a correct override, for reference | second group + * divideByZero | `NaN` and `Infinity` are valid numbers | second group + * includesFirstPoint | an override also runs for index 0 | FIRST group */ const deltaOverrides = { - divideByZero: (key, index, chartData) => { + custom: (key: string, index: number, chartData: DeltaData[]): number | null => { if (index === 0) return null; - const prev = numberAt(chartData, index - 1, key); - const curr = numberAt(chartData, index, key); + const prev = chartData[index - 1]?.[key as keyof DeltaData]; + const curr = chartData[index]?.[key as keyof DeltaData]; + + if (typeof prev !== 'number' || typeof curr !== 'number' || prev === 0) return null; - if (prev === null || curr === null) return null; + return ((curr - prev) / Math.abs(prev)) * 100; + }, + + /** + * The built-in formula written without the zero guard — the single most likely omission + * when someone reimplements it. The return type is still satisfied, because `NaN` and + * `Infinity` are both of type `number`, so neither TypeScript nor the chart rejects them. + * + * On the second group this dataset produces both: `zeroToZero` goes 0 -> 0, which is + * `0 / 0` -> `NaN`, and `fromZero` goes 0 -> 7, which is `7 / 0` -> `Infinity`. The + * tooltip prints them verbatim as `NaN%` and `Infinity%`, and `NaN` is additionally + * classified as the `stable` trend, so it is greyed out as though nothing had changed. + */ + divideByZero: (key: string, index: number, chartData: DeltaData[]): number | null => { + if (index === 0) return null; + + const prev = chartData[index - 1]?.[key as keyof DeltaData]; + const curr = chartData[index]?.[key as keyof DeltaData]; + + if (typeof prev !== 'number' || typeof curr !== 'number') return null; return ((curr - prev) / prev) * 100; }, -} satisfies Record; + + /** + * The built-in formula without its first-point guard, to show that the guard is not + * applied to overrides at all. + * + * The built-in path starts with `if (index === 0) return null`, but that line sits + * *after* the custom branch, so an override is called with `index === 0` too. Comparing + * the first point with itself yields a valid `0`, which is enough to make the difference + * visible: hover the FIRST group and every series shows `0%`, where `off` renders no + * delta column at all. + * + * On the second group this is identical to `off` by design — it is the same formula, and + * with a two point dataset there is nothing else it could differ on. The first group is + * the whole point of this variant. + */ + includesFirstPoint: (key: string, index: number, chartData: DeltaData[]): number | null => { + const previous = chartData[Math.max(index - 1, 0)]?.[key as keyof DeltaData]; + const curr = chartData[index]?.[key as keyof DeltaData]; + + if (typeof previous !== 'number' || typeof curr !== 'number' || previous === 0) return null; + + return ((curr - previous) / Math.abs(previous)) * 100; + }, +}; type DeltaEdgeCasesStoryProps = BarChartProps & { - /** `off` keeps the built-in calculation, the rest replace it — see `deltaOverrides`. */ + /** + * `off` keeps the built-in calculation, the rest replace it. See `deltaOverrides` for + * what each one demonstrates and which bar group to hover. + */ deltaOverride?: 'off' | keyof typeof deltaOverrides; }; const Demo = (props: DeltaEdgeCasesStoryProps) => { - const { plotWidth, plotHeight, deltaOverride, ...chartProps } = - getPropsToChart(props); + const { plotWidth, plotHeight, deltaOverride, ...chartProps } = getPropsToChart(props) as any; const getPercentDelta = - deltaOverride && deltaOverride !== 'off' ? deltaOverrides[deltaOverride] : undefined; + deltaOverride && deltaOverride !== 'off' + ? deltaOverrides[deltaOverride as keyof typeof deltaOverrides] + : undefined; return ( { ); @@ -107,10 +174,10 @@ export const defaultProps = getChartProps({ showLegend: false, duration: 0, deltaPercentGrowthColor: 'success', - // Off by default: the browser tests screenshot and assert the built-in calculation - // on this story, so the overrides stay opt-in from the Storybook panel. + // Off by default: the browser tests screenshot and assert this story, and the + // `returnsUndefined` variant deliberately throws. deltaOverride: 'off', -}); +} as DeltaEdgeCasesStoryProps); Demo.defaultProps = defaultProps; 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 new file mode 100644 index 0000000000..c67ef6b87d --- /dev/null +++ b/stories/components/d3-chart/tests/examples/line-chart/__mocks__/index.ts @@ -0,0 +1,56 @@ +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; +}; + +/** 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 179e29d4b9..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,10 +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 DataTypeMode, + type HighlightDotsMode, +} from './__mocks__'; import { getChartProps, getPropsToChart } from '../stories_props_helper'; +export type { DataTypeMode, HighlightDotsMode }; + type LineChartStoryProps = LineChartProps & { useExplicitPlotWidth?: boolean; + highlightDots?: HighlightDotsMode; + dataType?: DataTypeMode; + /** + * Which `tooltipTitleFormatter` to pass, or `off` to keep the built-in title. + * + * 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. + */ + 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) => { @@ -22,21 +105,54 @@ const Demo = (props: LineChartStoryProps) => { plotWidth, plotHeight, useExplicitPlotWidth, + highlightDots, + 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, withInterpolatedGaps, dataType }, + suppliedData ?? baseData, + ), + [suppliedData, highlightDots, withInterpolatedGaps, dataType], + ); + const handleResize: NonNullable = (size, entries) => { setMeasuredSize(size); 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; @@ -66,20 +182,17 @@ const Demo = (props: LineChartStoryProps) => { ); }; -const data = Array(20) - .fill({}) - .map((d, i) => ({ - x: i, - line1: Math.abs(Math.sin(Math.exp(i))) * 10, - line2: Math.abs(Math.cos(Math.exp(i))) * 10, - })); - export const defaultProps = getChartProps({ groupKey: 'x', - data, + data: baseData, showDots: true, 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 3b41913ab2..195e827e19 100644 --- a/stories/components/d3-chart/tests/line-chart.stories.tsx +++ b/stories/components/d3-chart/tests/line-chart.stories.tsx @@ -24,13 +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' }, + + 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, }; diff --git a/tools/theme/src/theme.ts b/tools/theme/src/theme.ts index 5977136482..cfb3693926 100644 --- a/tools/theme/src/theme.ts +++ b/tools/theme/src/theme.ts @@ -712,6 +712,10 @@ export const theme: Theme = { value: neutral.at(L_BORDER_SECONDARY), description: 'Grid and axis guide lines for charts.', }, + chart_grid_dot_outer_border: { + value: 'oklch(from {semanticTokens.colors.border.accent.DEFAULT} l c h / 0.1)', + description: 'Outer border for the dots highlighting forecast, bad or good insights.', + }, chart_grid_period_bg: { value: neutral.at(L_BG_LIGHT), description: 'Use for highlighting a period on the chart grid.', @@ -3510,6 +3514,9 @@ type SemanticColors = { }; grid: { line: Value; + dot: { + outer_border: Value; + }; line_forecast: Value; x: { axis: Value; diff --git a/website/docs/style/design-tokens/design-tokens.json b/website/docs/style/design-tokens/design-tokens.json index 9e288bee2b..54bbaba418 100644 --- a/website/docs/style/design-tokens/design-tokens.json +++ b/website/docs/style/design-tokens/design-tokens.json @@ -756,6 +756,14 @@ "d3-chart" ] }, + { + "name": "--intergalactic-chart-grid-dot-outer-border", + "value": "oklch(from oklch(0.23 0.01 140) l c h / 0.1)", + "description": "Outer border for the dots highlighting forecast, bad or good insights.", + "components": [ + "d3-chart" + ] + }, { "name": "--intergalactic-chart-grid-period-bg", "value": "oklch(0.96 0.001 180)",