/** * 06-charts.css * Data-driven charts from semantic HTML tables. * Absorbed from Charts.css — class API converted to data-attributes. * * Core vocabulary: * data-chart="bar|column|line|area|pie" — chart type * data-chart-labels — show axis labels (thead) * data-chart-spacing="1–5" — gap between bars (default 2) * data-chart-stacked — stacked multi-dataset mode * style="--size: 0.8" — data injection on (legal exception) * style="--color: #hex" — per-row color override on * * Pragmatic exception: style="--size: N" and style="--color: X" on table cells * are DATA injection, not presentation — they bind numeric values to CSS. * This is the one place ASW permits inline style attributes. * * Chart dimensions: * --chart-height Bar chart: bar thickness. Column chart: chart height. * --chart-bar-size Column chart: bar width. * --chart-gap Gap between data points. * * Lineage: Charts.css (MIT) — converted class API to data-attribute API. * Reference: chartscss.org */ @layer charts { /* ══════════════════════════════════════════════════════════════════════════ Shared chart tokens ══════════════════════════════════════════════════════════════════════════ */ [data-chart] { /* Data series colors — cycle via nth-child in each chart type */ --chart-color-1: var(--accent); /* green */ --chart-color-2: var(--accent-blue); /* blue */ --chart-color-3: var(--accent-orange); /* orange */ --chart-color-4: var(--accent-red); /* red */ --chart-color-5: var(--purple-5, #ae3ec9); --chart-color-6: var(--cyan-5, #15aabf); --chart-color-7: var(--pink-5, #e64980); --chart-color-8: var(--teal-5, #0ca678); /* Layout */ --chart-height: 200px; /* column chart area height */ --chart-bar-size: var(--space-6); /* column bar width / bar chart bar height */ --chart-gap: 6px; /* spacing between data points */ /* Axis / labels */ --chart-axis: var(--border); --chart-axis-width: var(--outline-width); --chart-label: var(--text-3); --chart-label-size: var(--text-xs); /* Bar styling */ --chart-radius: var(--radius-2); /* Reset table styles — is presentational structure here */ display: block; inline-size: 100%; border-collapse: collapse; border-spacing: 0; background: transparent; } [data-chart] caption { display: block; font-size: var(--text-sm); color: var(--text-3); text-align: start; padding-block-end: var(--size-3); caption-side: top; } /* thead: hidden by default, shown with data-chart-labels */ [data-chart] thead { display: none; } [data-chart][data-chart-labels] thead { display: block; } /* tbody: each chart type overrides this */ [data-chart] tbody { display: block; } /* ══════════════════════════════════════════════════════════════════════════ Bar chart — horizontal bars ══════════════════════════════════════════════════════════════════════════ Structure:
Title
Label 80%
The bar width = 100% × --size. Bar is a ::before pseudo on td. ══════════════════════════════════════════════════════════════════════════ */ [data-chart="bar"] tbody { display: flex; flex-direction: column; gap: var(--chart-gap); /* Left axis line */ border-inline-start: var(--chart-axis-width) solid var(--chart-axis); padding-inline-start: 0; } [data-chart="bar"] tr { display: flex; align-items: center; gap: var(--size-3); } /* Row label (th) */ [data-chart="bar"] th[scope="row"] { font-size: var(--chart-label-size); font-weight: 400; color: var(--chart-label); min-inline-size: 5rem; max-inline-size: 8rem; text-align: end; padding-block: 0; padding-inline: var(--size-2) 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 0; } /* Data cell — the track */ [data-chart="bar"] td { flex: 1; position: relative; block-size: var(--chart-bar-size); display: flex; align-items: center; padding: 0; overflow: visible; } /* The bar itself — ::before */ [data-chart="bar"] td::before { content: ""; display: block; block-size: 100%; inline-size: calc(100% * var(--size, 0.5)); background: var(--color, var(--chart-color-1)); border-radius: 0 var(--chart-radius) var(--chart-radius) 0; transition: opacity var(--ease), inline-size var(--duration-moderate-1) var(--ease-3, ease-out); } [data-chart="bar"] td:hover::before { opacity: 0.8; } /* Data label (text inside/after bar) */ [data-chart="bar"] td::after { content: attr(data-value); position: absolute; inset-inline-start: calc(100% * var(--size, 0.5) + 0.35rem); font-size: var(--chart-label-size); color: var(--text-3); white-space: nowrap; } /* Color cycling for multi-series */ [data-chart="bar"] tr:nth-child(1) td::before { background: var(--color, var(--chart-color-1)); } [data-chart="bar"] tr:nth-child(2) td::before { background: var(--color, var(--chart-color-2)); } [data-chart="bar"] tr:nth-child(3) td::before { background: var(--color, var(--chart-color-3)); } [data-chart="bar"] tr:nth-child(4) td::before { background: var(--color, var(--chart-color-4)); } [data-chart="bar"] tr:nth-child(5) td::before { background: var(--color, var(--chart-color-5)); } [data-chart="bar"] tr:nth-child(6) td::before { background: var(--color, var(--chart-color-6)); } [data-chart="bar"] tr:nth-child(7) td::before { background: var(--color, var(--chart-color-7)); } [data-chart="bar"] tr:nth-child(8) td::before { background: var(--color, var(--chart-color-8)); } [data-chart="bar"] tr:nth-child(n+9) td::before { background: var(--color, var(--chart-color-1)); } /* ── Spacing modifiers ──────────────────────────────────── */ [data-chart="bar"][data-chart-spacing="1"] tbody { gap: 2px; } [data-chart="bar"][data-chart-spacing="2"] tbody { gap: 6px; } [data-chart="bar"][data-chart-spacing="3"] tbody { gap: 10px; } [data-chart="bar"][data-chart-spacing="4"] tbody { gap: 16px; } [data-chart="bar"][data-chart-spacing="5"] tbody { gap: 24px; } /* ══════════════════════════════════════════════════════════════════════════ Column chart — vertical bars ══════════════════════════════════════════════════════════════════════════ Structure:
Title
Jan 60
The chart area is --chart-height. Each column height = --chart-height × --size. Columns sit at the bottom of the chart area (flex-end alignment). ══════════════════════════════════════════════════════════════════════════ */ [data-chart="column"] tbody { display: flex; flex-direction: row; align-items: flex-end; gap: var(--chart-gap); block-size: var(--chart-height); border-block-end: var(--chart-axis-width) solid var(--chart-axis); padding: 0; } [data-chart="column"] tr { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1; block-size: 100%; gap: var(--size-1); } /* Column label (th) at the bottom */ [data-chart="column"] th[scope="row"] { font-size: var(--chart-label-size); font-weight: 400; color: var(--chart-label); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-inline-size: 100%; padding: 0; padding-block-start: var(--size-1); /* Move below axis */ order: 2; margin-block-start: var(--size-2); } /* Data cell — the column bar */ [data-chart="column"] td { display: block; inline-size: 100%; block-size: calc(var(--chart-height) * var(--size, 0.5)); padding: 0; order: 1; transition: block-size var(--duration-moderate-1) var(--ease-3, ease-out); border-radius: var(--chart-radius) var(--chart-radius) 0 0; } /* Color cycling for columns */ [data-chart="column"] tr:nth-child(1) td { background: var(--color, var(--chart-color-1)); } [data-chart="column"] tr:nth-child(2) td { background: var(--color, var(--chart-color-2)); } [data-chart="column"] tr:nth-child(3) td { background: var(--color, var(--chart-color-3)); } [data-chart="column"] tr:nth-child(4) td { background: var(--color, var(--chart-color-4)); } [data-chart="column"] tr:nth-child(5) td { background: var(--color, var(--chart-color-5)); } [data-chart="column"] tr:nth-child(6) td { background: var(--color, var(--chart-color-6)); } [data-chart="column"] tr:nth-child(7) td { background: var(--color, var(--chart-color-7)); } [data-chart="column"] tr:nth-child(8) td { background: var(--color, var(--chart-color-8)); } [data-chart="column"] tr:nth-child(n+9) td { background: var(--color, var(--chart-color-1)); } [data-chart="column"] td:hover { opacity: 0.8; } /* ── Spacing modifiers ──────────────────────────────────── */ [data-chart="column"][data-chart-spacing="1"] tbody { gap: 2px; } [data-chart="column"][data-chart-spacing="2"] tbody { gap: 6px; } [data-chart="column"][data-chart-spacing="3"] tbody { gap: 12px; } [data-chart="column"][data-chart-spacing="4"] tbody { gap: 20px; } [data-chart="column"][data-chart-spacing="5"] tbody { gap: 32px; } /* ── Column chart labels ───────────────────────────────── */ /* When data-chart-labels present, show thead as axis header */ [data-chart="column"][data-chart-labels] thead { display: flex; justify-content: space-around; margin-block-end: var(--size-2); } [data-chart="column"][data-chart-labels] thead th { font-size: var(--chart-label-size); color: var(--chart-label); font-weight: 400; text-align: center; flex: 1; } /* ══════════════════════════════════════════════════════════════════════════ Area chart — filled area from baseline ══════════════════════════════════════════════════════════════════════════ CSS-only area charts use linear-gradient on the td background. Each point's area = --size fraction of the column height. Structure identical to column — but cells connect visually. The visual connection requires identical widths and no gap (or clip). ══════════════════════════════════════════════════════════════════════════ */ [data-chart="area"] tbody { display: flex; flex-direction: row; align-items: flex-end; block-size: var(--chart-height); border-block-end: var(--chart-axis-width) solid var(--chart-axis); gap: 0; /* no gap — cells must connect */ padding: 0; } [data-chart="area"] tr { display: flex; flex-direction: column; align-items: stretch; justify-content: flex-end; flex: 1; block-size: 100%; } [data-chart="area"] th[scope="row"] { font-size: var(--chart-label-size); font-weight: 400; color: var(--chart-label); text-align: center; order: 2; padding-block-start: var(--size-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Area cell — filled gradient from --size down to baseline */ [data-chart="area"] td { display: block; inline-size: 100%; block-size: calc(var(--chart-height) * var(--size, 0.5)); padding: 0; order: 1; background: linear-gradient( to bottom, var(--chart-color-1) 0%, color-mix(in oklch, var(--chart-color-1), transparent 70%) 100% ); } /* ══════════════════════════════════════════════════════════════════════════ Line chart — dots connected by a visual line ══════════════════════════════════════════════════════════════════════════ CSS-only: we use the column approach but mark the top with a dot (::after) and use a border-top line to simulate connection between points. True line interpolation requires JavaScript or SVG. What we ship: column bars in outline/transparent mode with an accent dot at the top — semantic, readable, no JS. ══════════════════════════════════════════════════════════════════════════ */ [data-chart="line"] tbody { display: flex; flex-direction: row; align-items: flex-end; block-size: var(--chart-height); border-block-end: var(--chart-axis-width) solid var(--chart-axis); gap: 0; padding: 0; position: relative; } [data-chart="line"] tr { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1; block-size: 100%; } [data-chart="line"] th[scope="row"] { font-size: var(--chart-label-size); font-weight: 400; color: var(--chart-label); text-align: center; order: 2; padding-block-start: var(--size-1); white-space: nowrap; } /* Line chart cell — transparent bar with accent top border + dot */ [data-chart="line"] td { display: block; inline-size: 100%; block-size: calc(var(--chart-height) * var(--size, 0.5)); padding: 0; order: 1; background: linear-gradient( to bottom, color-mix(in oklch, var(--chart-color-1), transparent 80%) 0%, transparent 60% ); border-block-start: var(--outline-width) solid var(--chart-color-1); position: relative; } /* Dot at data point */ [data-chart="line"] td::before { content: ""; display: block; position: absolute; inset-block-start: -5px; inset-inline-start: 50%; translate: -50% 0; inline-size: 8px; block-size: 8px; border-radius: 50%; background: var(--chart-color-1); border: var(--outline-width) solid var(--surface); z-index: 1; } /* ══════════════════════════════════════════════════════════════════════════ Pie chart — conic-gradient segments ══════════════════════════════════════════════════════════════════════════ CSS-only pie charts use conic-gradient on a single element. Each segment's arc = --size × 360deg. Requires stacking values in CSS — not practical to automate per-row. For agent use: pie charts work best with explicit conic-gradient set as a custom property. The data-chart="pie" wrapper provides the shape and size; the agent sets --pie-segments. ══════════════════════════════════════════════════════════════════════════ */ [data-chart="pie"] { --pie-size: min(200px, 100%); --pie-segments: conic-gradient( var(--chart-color-1) 0% 25%, var(--chart-color-2) 25% 50%, var(--chart-color-3) 50% 75%, var(--chart-color-4) 75% 100% ); } /* Pie uses a generated element — hide table structure visually */ [data-chart="pie"] tbody { display: none; } /* Show caption + legend from thead */ [data-chart="pie"] thead { display: flex; flex-wrap: wrap; gap: var(--size-2); justify-content: center; margin-block-end: var(--size-3); } [data-chart="pie"] thead th { font-size: var(--chart-label-size); color: var(--chart-label); font-weight: 400; } /* The pie rendered as ::before on the table element */ [data-chart="pie"]::before { content: ""; display: block; inline-size: var(--pie-size); block-size: var(--pie-size); border-radius: 50%; background: var(--pie-segments); margin-inline: auto; } /* ══════════════════════════════════════════════════════════════════════════ Stacked bars — data-chart-stacked modifier ══════════════════════════════════════════════════════════════════════════ When multiple in one , stack them. ══════════════════════════════════════════════════════════════════════════ */ [data-chart="bar"][data-chart-stacked] td { /* Multiple tds per row — share the bar track inline */ display: inline-block; inline-size: calc(100% * var(--size, 0.2)); border-radius: 0; } [data-chart="bar"][data-chart-stacked] td::before { display: none; /* td IS the bar in stacked mode */ } [data-chart="bar"][data-chart-stacked] td:first-of-type { border-radius: 0 0 0 0; } [data-chart="bar"][data-chart-stacked] td:last-of-type { border-radius: 0 var(--chart-radius) var(--chart-radius) 0; } /* Stacked color cycling */ [data-chart][data-chart-stacked] td:nth-of-type(1) { background: var(--chart-color-1); } [data-chart][data-chart-stacked] td:nth-of-type(2) { background: var(--chart-color-2); } [data-chart][data-chart-stacked] td:nth-of-type(3) { background: var(--chart-color-3); } [data-chart][data-chart-stacked] td:nth-of-type(4) { background: var(--chart-color-4); } /* ══════════════════════════════════════════════════════════════════════════ Accessibility ══════════════════════════════════════════════════════════════════════════ */ /* Ensure cell content (the data value) is readable for screen readers but visually hidden inside the bar — text is in aria / caption */ [data-chart="bar"] td, [data-chart="column"] td { font-size: var(--chart-label-size); color: transparent; /* data visible to SR, hidden visually */ } /* Respect user preference — no transitions */ @media (prefers-reduced-motion: reduce) { [data-chart] td, [data-chart] td::before { transition: none; } } /* ══════════════════════════════════════════════════════════════════════════ Radial chart — circular gauge ══════════════════════════════════════════════════════════════════════════ Structure:
Token budget used
72%
The gauge is a conic-gradient on the td element. --size (0–1) drives the arc: --size × 360deg = colored portion. ::before pseudo creates a donut hole cutout over the gradient. inside td floats the value text above the donut via z-index. ══════════════════════════════════════════════════════════════════════════ */ [data-chart="radial"] { display: inline-flex; flex-direction: column; align-items: center; gap: var(--size-2); } [data-chart="radial"] caption { font-size: var(--chart-label-size); color: var(--chart-label); text-align: center; caption-side: bottom; padding-block-start: var(--size-2); } [data-chart="radial"] tbody { display: flex; } [data-chart="radial"] tr { display: flex; } /* The gauge circle */ [data-chart="radial"] td { position: relative; width: var(--chart-radial-size); height: var(--chart-radial-size); border-radius: 50%; background: conic-gradient( var(--color, var(--chart-color-1)) 0deg calc(var(--size, 0.5) * 360deg), var(--surface-1, var(--gray-15)) 0deg ); display: flex; align-items: center; justify-content: center; padding: 0; border: none; color: transparent; /* data readable by SR, hidden visually */ } /* Donut hole */ [data-chart="radial"] td::before { content: ""; position: absolute; inset: var(--chart-radial-inset); border-radius: 50%; background: var(--surface); z-index: 0; } /* Value text centered in the donut hole */ [data-chart="radial"] td span { position: relative; z-index: 1; font-size: var(--text-xs); font-family: var(--font-mono); color: var(--text); font-weight: 600; } /* Status color variants */ [data-chart="radial"][data-status="warning"] td { background: conic-gradient( var(--accent-orange, #f08c00) 0deg calc(var(--size, 0.5) * 360deg), var(--surface-1, #111111) 0deg ); } [data-chart="radial"][data-status="danger"] td { background: conic-gradient( var(--accent-red, #e03131) 0deg calc(var(--size, 0.5) * 360deg), var(--surface-1, #111111) 0deg ); } /* ══════════════════════════════════════════════════════════════════════════ Burndown chart — sprint burndown with CSS ideal-line overlay ══════════════════════════════════════════════════════════════════════════ Structure: same as column chart, but: - Bars use --accent-red (remaining work = red) - tbody::after renders a diagonal linear-gradient as the ideal-line - Ideal line runs top-left to bottom-right: full work at start → zero at end ...
Sprint burndown
D119
══════════════════════════════════════════════════════════════════════════ */ [data-chart="burndown"] tbody { display: flex; flex-direction: row; align-items: flex-end; block-size: var(--chart-height); border-block-end: var(--chart-axis-width) solid var(--chart-axis); position: relative; /* required for ::after overlay */ gap: var(--chart-gap); padding: 0; } /* Ideal-line overlay — diagonal gradient = ideal burn velocity */ [data-chart="burndown"] tbody::after { content: ""; position: absolute; inset: 0; background: linear-gradient( to bottom right, color-mix(in oklch, var(--chart-color-2, var(--accent-blue, #4dabf7)), transparent 20%) 0%, transparent 100% ); pointer-events: none; z-index: 2; } [data-chart="burndown"] tr { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1; block-size: 100%; gap: var(--size-1); } /* Remaining-work bar — red, with ideal line overlay above it */ [data-chart="burndown"] td { display: block; inline-size: 100%; block-size: calc(var(--chart-height) * var(--size, 0.5)); background: color-mix(in oklch, var(--chart-color-4, var(--accent-red, #e03131)), transparent 25%); border-radius: var(--chart-radius) var(--chart-radius) 0 0; order: 1; padding: 0; border: none; color: transparent; position: relative; z-index: 1; transition: opacity var(--ease); } [data-chart="burndown"] td:hover { opacity: 0.85; } [data-chart="burndown"] th[scope="row"] { font-size: var(--chart-label-size); font-weight: 400; color: var(--chart-label); text-align: center; order: 2; padding-block-start: var(--size-1); white-space: nowrap; padding: 0; margin-block-start: var(--size-2); } /* ── Spacing modifiers for area and line (port from bar/column) ──── */ [data-chart="area"][data-chart-spacing="1"] tbody { gap: 0; } [data-chart="area"][data-chart-spacing="2"] tbody { gap: 2px; } [data-chart="area"][data-chart-spacing="3"] tbody { gap: 6px; } [data-chart="area"][data-chart-spacing="4"] tbody { gap: 12px; } [data-chart="area"][data-chart-spacing="5"] tbody { gap: 20px; } [data-chart="line"][data-chart-spacing="1"] tbody { gap: 0; } [data-chart="line"][data-chart-spacing="2"] tbody { gap: 2px; } [data-chart="line"][data-chart-spacing="3"] tbody { gap: 6px; } [data-chart="line"][data-chart-spacing="4"] tbody { gap: 12px; } [data-chart="line"][data-chart-spacing="5"] tbody { gap: 20px; } /* ── data-chart-reverse modifier ────────────────────────────────── */ [data-chart="bar"][data-chart-reverse] tbody { flex-direction: column-reverse; } [data-chart="column"][data-chart-reverse] tbody { flex-direction: row-reverse; } /* ── data-chart-stacked on column ───────────────────────────────── */ [data-chart="column"][data-chart-stacked] tr { flex-direction: row; align-items: flex-end; gap: 0; } [data-chart="column"][data-chart-stacked] td { flex: 1; border-radius: 0; block-size: calc(var(--chart-height) * var(--size, 0.2)); } [data-chart="column"][data-chart-stacked] td:first-of-type { border-radius: var(--chart-radius) 0 0 0; } [data-chart="column"][data-chart-stacked] td:last-of-type { border-radius: 0 var(--chart-radius) 0 0; } /* ── data-chart-labels on bar ────────────────────────────────────── */ [data-chart="bar"][data-chart-labels] thead { display: block; margin-block-end: var(--size-2); } [data-chart="bar"][data-chart-labels] thead th { font-size: var(--chart-label-size); color: var(--chart-label); font-weight: 400; } } /* end @layer charts */