/**
* 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:
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:
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:
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
══════════════════════════════════════════════════════════════════════════ */
[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 */