From c0177e1137bcd703404bcb6c4801a8add0ecdc39 Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:08:29 +0200 Subject: [PATCH 1/5] test(design): specify chart labels, gaps and mandatory table twin Test-First: src/design/data/charts.test.tsx::the table twin shows the same values as the chart Test-First: src/design/data/charts.test.tsx::each bar has a text label and value beyond colour Test-First: src/design/data/charts.test.tsx::sparkline preserves gaps and renders single and constant samples Test-First: src/design/data/charts.test.tsx::empty and unknown data are words in graphics and table twins Co-Authored-By: GPT-6 --- src/design/data/BarList.tsx | 2 ++ src/design/data/ChartFrame.tsx | 3 ++ src/design/data/Sparkline.tsx | 2 ++ src/design/data/charts.test.tsx | 51 +++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 src/design/data/BarList.tsx create mode 100644 src/design/data/ChartFrame.tsx create mode 100644 src/design/data/Sparkline.tsx create mode 100644 src/design/data/charts.test.tsx diff --git a/src/design/data/BarList.tsx b/src/design/data/BarList.tsx new file mode 100644 index 00000000..ce52ce23 --- /dev/null +++ b/src/design/data/BarList.tsx @@ -0,0 +1,2 @@ +import type { ChartProps } from "./ChartFrame"; +export function BarList(_props: ChartProps) { return null; } diff --git a/src/design/data/ChartFrame.tsx b/src/design/data/ChartFrame.tsx new file mode 100644 index 00000000..eafcdbac --- /dev/null +++ b/src/design/data/ChartFrame.tsx @@ -0,0 +1,3 @@ +export interface ChartDatum { label: string; value: number | null } +export interface ChartProps { data: readonly ChartDatum[] | null; label: string } +export function ChartFrame(_props: { title: string; caption: string; kind: "sparkline" | "bars"; data: readonly ChartDatum[] | null }) { return null; } diff --git a/src/design/data/Sparkline.tsx b/src/design/data/Sparkline.tsx new file mode 100644 index 00000000..f128cfe8 --- /dev/null +++ b/src/design/data/Sparkline.tsx @@ -0,0 +1,2 @@ +import type { ChartProps } from "./ChartFrame"; +export function Sparkline(_props: ChartProps) { return null; } diff --git a/src/design/data/charts.test.tsx b/src/design/data/charts.test.tsx new file mode 100644 index 00000000..82971507 --- /dev/null +++ b/src/design/data/charts.test.tsx @@ -0,0 +1,51 @@ +import { cleanup, render, screen, within, fireEvent } from "@testing-library/react"; +import { afterEach, expect, it } from "vitest"; +import { ChartFrame } from "./ChartFrame"; +import { Sparkline } from "./Sparkline"; +import { BarList } from "./BarList"; + +afterEach(cleanup); +const data = [{ label: "Mo", value: -2 }, { label: "Di", value: 0 }, { label: "Mi", value: 4 }]; + +it("the table twin shows the same values as the chart", () => { + render(); + expect(screen.getByRole("heading", { name: "Aufträge" })).toBeTruthy(); + expect(screen.getByText("Diese Woche")).toBeTruthy(); + const summary = screen.getByText("Als Tabelle"); + summary.focus(); + expect(document.activeElement).toBe(summary); + fireEvent.click(summary); + const table = screen.getByRole("table", { name: "Aufträge" }); + expect(within(table).getAllByRole("row").map(row => row.textContent)).toEqual(["BezeichnungWert", "Mo-2", "Di0", "Mi4"]); + expect(screen.getByRole("list", { name: "Aufträge" }).textContent).toBe("Mo-2Di0Mi4"); + expect(screen.getByRole("list", { name: "Legende" }).textContent).toBe("MoDiMi"); +}); + +it("each bar has a text label and value beyond colour", () => { + const { container } = render(); + expect(screen.getByRole("list", { name: "Messung" }).textContent).toBe("Mo-2Di0Mi4DoUnbekannt"); + const rects = container.querySelectorAll("rect"); + expect([...rects].map(r => [r.getAttribute("x"), r.getAttribute("width")])).toEqual([["26", "24"], ["50", "0"], ["50", "48"]]); +}); + +it("sparkline preserves gaps and renders single and constant samples", () => { + const { container, rerender } = render(); + expect(screen.getByRole("img", { name: "Verlauf" })).toBeTruthy(); + expect(container.querySelector("path")?.getAttribute("d")).toBe("M2,30 M118,2"); + for (const values of [[0], [2, 2], [-Number.MAX_VALUE, Number.MAX_VALUE]]) { + rerender( ({ label: String(i), value }))} />); + expect(container.querySelectorAll("circle")).toHaveLength(values.length); + expect(container.innerHTML).not.toMatch(/NaN|Infinity/); + } +}); + +it("empty and unknown data are words in graphics and table twins", () => { + for (const kind of ["sparkline", "bars"] as const) { + for (const [data, word] of [[[], "Keine Daten"], [null, "Unbekannt"], [[{ label: "Mo", value: Infinity }], "Unbekannt"]] as const) { + const { unmount } = render(); + expect(screen.getAllByText(word).length).toBeGreaterThanOrEqual(2); + expect(screen.getByText("Als Tabelle").tagName).toBe("SUMMARY"); + unmount(); + } + } +}); From 53235f4b38f03f96af12bf7885e4274aeebcae78 Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:13:32 +0200 Subject: [PATCH 2/5] feat(design): add static charts with a required table twin Test-First: src/design/data/charts.test.tsx::the table twin shows the same values as the chart Test-First: src/design/data/charts.test.tsx::each bar has a text label and value beyond colour Test-First: src/design/data/charts.test.tsx::sparkline preserves gaps and renders single and constant samples Test-First: src/design/data/charts.test.tsx::empty and unknown data are words in graphics and table twins Co-Authored-By: gpt-6.1-sol --- src/design/data/BarList.tsx | 20 +++++++++++++++-- src/design/data/ChartFrame.tsx | 39 +++++++++++++++++++++++++++++++--- src/design/data/Sparkline.tsx | 24 +++++++++++++++++++-- src/design/data/charts.css | 18 ++++++++++++++++ src/design/data/charts.ts | 8 +++++++ src/design/registry.ts | 15 +++++++++++++ src/i18n/de.ts | 6 ++++++ 7 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 src/design/data/charts.css create mode 100644 src/design/data/charts.ts diff --git a/src/design/data/BarList.tsx b/src/design/data/BarList.tsx index ce52ce23..b55426b7 100644 --- a/src/design/data/BarList.tsx +++ b/src/design/data/BarList.tsx @@ -1,2 +1,18 @@ -import type { ChartProps } from "./ChartFrame"; -export function BarList(_props: ChartProps) { return null; } +import { chartColour, chartEmpty, chartValue, known, type ChartProps } from "./charts"; +import "./charts.css"; + +export function BarList({ label, data }: ChartProps) { + if (!data?.length) return

{chartEmpty(data)}

; + const max = Math.max(...data.map(row => known(row.value) ? Math.abs(row.value) : 0)) || 1; + return
    + {data.map((row, i) => { + const width = known(row.value) ? row.value / max * 48 : null; + return
  • + {row.label}{chartValue(row.value)} + +
  • ; + })} +
; +} diff --git a/src/design/data/ChartFrame.tsx b/src/design/data/ChartFrame.tsx index eafcdbac..e4e68112 100644 --- a/src/design/data/ChartFrame.tsx +++ b/src/design/data/ChartFrame.tsx @@ -1,3 +1,36 @@ -export interface ChartDatum { label: string; value: number | null } -export interface ChartProps { data: readonly ChartDatum[] | null; label: string } -export function ChartFrame(_props: { title: string; caption: string; kind: "sparkline" | "bars"; data: readonly ChartDatum[] | null }) { return null; } +import { useId } from "react"; +import { de } from "../../i18n/de"; +import { BarList } from "./BarList"; +import { Sparkline } from "./Sparkline"; +import "./data.css"; +import { chartColour, chartEmpty, chartValue, type ChartProps } from "./charts"; +import "./charts.css"; + +export interface ChartFrameProps { + title: string; + caption: string; + kind: "sparkline" | "bars"; + data: ChartProps["data"]; +} + +/** One dataset drives both the graphic and its mandatory table twin. */ +export function ChartFrame({ title, caption, kind, data }: ChartFrameProps) { + const id = useId(); + const legend = kind === "sparkline" ? [title] : (data ?? []).map(row => row.label); + return
+

{title}

{caption}

+
    + {legend.map((label, i) =>