asw/assets/css/layers/06-charts.css
Vigilio Desto 880a17f33a
Add dorveille article and full Hugo site scaffold
- content/dorveille.md: 'On the Craft of Invisible Systems'
- assets/css/: ASW layer system (00-reset through 09-landing + Open Props)
- layouts/: baseof, single, list, index — semantic HTML, no classes
- hugo.toml: baseURL asw.trentuna.com, PostCSS + minify pipeline
- package.json: postcss-import, postcss-custom-media, cssnano
- .gitignore: excludes public/, node_modules, build artifacts

Site builds to public/ via hugo --minify. nginx serves public/ statically.
2026-04-10 17:23:51 +02:00

761 lines
28 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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="15" — gap between bars (default 2)
* data-chart-stacked — stacked multi-dataset mode
* style="--size: 0.8" — data injection on <td> (legal exception)
* style="--color: #hex" — per-row color override on <tr>
*
* 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: 2rem; /* column bar width / bar chart bar height */
--chart-gap: 6px; /* spacing between data points */
/* Axis / labels */
--chart-axis: var(--border);
--chart-axis-width: 2px;
--chart-label: var(--text-3);
--chart-label-size: var(--text-xs);
/* Bar styling */
--chart-radius: var(--radius-2);
/* Reset table styles — <table> 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:
<table data-chart="bar">
<caption>Title</caption>
<tbody>
<tr>
<th scope="row">Label</th>
<td style="--size: 0.8">80%</td>
</tr>
</tbody>
</table>
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:
<table data-chart="column">
<caption>Title</caption>
<tbody>
<tr>
<th scope="row">Jan</th>
<td style="--size: 0.6">60</td>
</tr>
</tbody>
</table>
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: 2px 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: 2px 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 <td> in one <tr>, 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:
<table data-chart="radial" style="--size: 0.72">
<caption>Token budget used</caption>
<tbody><tr><td><span>72%</span></td></tr></tbody>
</table>
The gauge is a conic-gradient on the td element.
--size (01) drives the arc: --size × 360deg = colored portion.
::before pseudo creates a donut hole cutout over the gradient.
<span> 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
<table data-chart="burndown">
<caption>Sprint burndown</caption>
<tbody>
<tr><th scope="row">D1</th><td style="--size: 0.95">19</td></tr>
...
</tbody>
</table>
══════════════════════════════════════════════════════════════════════════ */
[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 */