From 6db4b0ce4746c57f80a0703eca4fb3bb88b9aa70 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Tue, 4 Aug 2026 10:39:09 +0100 Subject: [PATCH] docs(charts): the recipe documents the styling surface it has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five ChartStyle settings were named nowhere on the page — the three text styles, the donut centre style and the bar width ratio — four of them already load-bearing in the flagship examples that put a chart on a dark card. The value-label halo was filed under line charts, the one place its default white chip is least likely to be wrong, rather than described as the backing behind value and slice labels alike. And ChartTheme was presented as a layer an author styles through, though no authoring API accepts one; the low-level ChartLayoutResolver.resolve(...) does, and the page now draws that line rather than leaving it out. The page now carries every setting with its default and what it affects, plus compiled examples for typography and for the halo. ChartStyleDocumentationGuardTest fails the build when a setter reaches the builder without reaching the page. --- CHANGELOG.md | 15 ++ .../ChartStyleDocumentationGuardTest.java | 75 +++++++++ docs/recipes/charts.md | 158 ++++++++++++++++-- 3 files changed, 236 insertions(+), 12 deletions(-) create mode 100644 core/src/test/java/com/demcha/documentation/ChartStyleDocumentationGuardTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index fbcb0e080..187b07f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -211,6 +211,21 @@ follow semantic versioning; release dates are ISO 8601. ### Documentation +- **The chart recipe documents the styling surface it has.** Five settings were named + nowhere on the page — the three text styles, the donut centre style and the bar width + ratio — four of them already load-bearing in the flagship examples that put a chart on + a dark card, and discoverable only by reading the builder's source. The + value-label halo was filed under line charts, the one place its default white chip is + least likely to be wrong, rather than described as what it is: the backing behind + value and slice labels alike, and the first thing that must move when a chart leaves a + white page. And `ChartTheme` was presented as a layer an author styles through, though + no authoring API accepts one — a chart resolves its geometry after the document's theme + is out of reach, so `ChartStyle` is the author-facing whole of it. The low-level + `ChartLayoutResolver.resolve(...)` does take a `ChartTheme`, and the page now says so + rather than leaving it out. The page carries every setting + with its default and what it affects, worked examples for typography and for the halo, + and `ChartStyleDocumentationGuardTest` fails the build when a setter reaches the builder + without reaching the page. - **One recipe catalogue instead of two.** The cookbook page and the folder index each carried a hand-maintained table of all twenty-two recipe pages. They happened to agree on which pages exist and disagreed on ten of the descriptions — the folder index named diff --git a/core/src/test/java/com/demcha/documentation/ChartStyleDocumentationGuardTest.java b/core/src/test/java/com/demcha/documentation/ChartStyleDocumentationGuardTest.java new file mode 100644 index 000000000..152d7fe3a --- /dev/null +++ b/core/src/test/java/com/demcha/documentation/ChartStyleDocumentationGuardTest.java @@ -0,0 +1,75 @@ +package com.demcha.documentation; + +import com.demcha.compose.document.chart.ChartStyle; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.TreeSet; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Every knob on {@link ChartStyle.Builder} is named in the chart recipe. + * + *

A styling setter nobody documents is a setter nobody finds. The recipe is + * the only page that describes chart styling, and a new field added to the + * builder does not disturb it — the page keeps rendering, every guard keeps + * passing, and the option stays invisible until someone reads the source. Five + * had accumulated that way: the three text styles, the donut centre style, and + * the bar width ratio, all of them used by the flagship examples and named + * nowhere a reader would look.

+ * + *

The check is deliberately shallow — it asserts the setter's name occurs on + * the page, not that the prose around it is any good. That is enough to make + * the omission loud at the moment it is introduced, which is the only moment + * it is cheap to fix.

+ */ +class ChartStyleDocumentationGuardTest { + + private static final Path RECIPE = RepoRoot.get().resolve("docs/recipes/charts.md"); + + @Test + void everyChartStyleSetterIsNamedInTheChartRecipe() throws IOException { + assertThat(RECIPE) + .describedAs("the chart recipe moved; this guard is no longer reading the page it protects") + .exists(); + + String page = Files.readString(RECIPE, StandardCharsets.UTF_8); + + List setters = builderSetterNames(); + assertThat(setters) + .describedAs("no setters found on ChartStyle.Builder — the guard would cover nothing") + .isNotEmpty(); + + TreeSet undocumented = new TreeSet<>(); + for (String setter : setters) { + if (!page.contains(setter + "(")) { + undocumented.add(setter); + } + } + + assertThat(undocumented) + .describedAs("ChartStyle.Builder setters missing from docs/recipes/charts.md — a " + + "styling option that is not on the page is one readers cannot discover") + .isEmpty(); + } + + /** Public builder methods that configure the style, i.e. everything but {@code build}. */ + private static List builderSetterNames() { + return Arrays.stream(ChartStyle.Builder.class.getDeclaredMethods()) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .filter(m -> !m.isSynthetic()) + .map(Method::getName) + .filter(name -> !"build".equals(name)) + .distinct() + .sorted() + .toList(); + } +} diff --git a/docs/recipes/charts.md b/docs/recipes/charts.md index 54674556c..5d4800376 100644 --- a/docs/recipes/charts.md +++ b/docs/recipes/charts.md @@ -12,8 +12,8 @@ The API is split into independent layers so nothing is baked in: |---|---|---| | Data | `ChartData` | *what numbers* — categories + series, knows nothing about type or colour | | Spec | `ChartSpec` (sealed: `bar()` / `line()` / `pie()`) | *what to show* — orientation, axes, legend, labels, sizing | -| Style | `ChartStyle` over `ChartTheme` tokens | *how it looks* — cascading nullable fields, CSS-style merge | -| Geometry | `ChartLayoutResolver` | internal pure function `(data, spec, style) → primitives` | +| Style | `ChartStyle` | *how it looks* — nullable fields merged CSS-style over the built-in `ChartTheme` | +| Geometry | `ChartLayoutResolver` | *where the shapes come from* — a pure `(spec, style, theme, size, metrics) → primitives` function | All chart types live in `com.demcha.compose.document.chart`. @@ -57,6 +57,9 @@ section.chart(ChartSpec.bar() reading order, values grow right, labels sit at the bar ends. - `AxisSpec.min(...)` / `max(...)` pin the axis to explicit bounds; ticks still land on nice 1/2/5 values. +- `ChartStyle.barWidthRatio(...)` sets how much of a category slot the bar + group fills (default `0.72`). Lower it for airy, editorial bars; raise it + toward `1.0` to close the gaps. ## Line, smooth, and area charts @@ -90,11 +93,13 @@ section.chart(lineSpec, ChartStyle.builder() Markers are ellipses (`PointMarker.circle(d)` / `ellipse(w, h)`) drawn **above every stroke**, so joints where lines meet stay readable; the white -ring is the classic separator. Per-point value labels draw above markers -behind a halo chip (`ChartStyle.valueLabelHalo`, themed white — match it to -your card colour on tinted surfaces, including translucent paints via -`DocumentColor.rgba(...)`). When two series' labels would collide at the same -category, the lower one automatically flips below its point. +ring is the classic separator. Per-point value labels draw above their marker, +each behind a halo chip — see [the value-label halo](#the-value-label-halo). +When two series' labels would collide at the same category, the lower one +automatically flips below its point. + +`valueLabelOffset` (default `2`) is the gap between a label and the thing it +labels — the marker here, the bar end on a bar chart, the rim on a pie or donut. ## Pie and donut @@ -111,9 +116,36 @@ section.chart(ChartSpec.pie() .build()); ``` -Slices are arc-tessellated vector polygons. `sliceStroke` (themed white 1pt) -separates adjacent slices; `startAngleDegrees` / `clockwise(false)` control -layout. Negative values and multi-series data are rejected loudly. +Slices are arc-tessellated vector polygons. `sliceStroke` (white 1pt by +default) separates adjacent slices; `startAngleDegrees` / `clockwise(false)` +control layout. Negative values and multi-series data are rejected loudly. + +`centerText` is the KPI in the hole, and `ChartStyle.donutCenterTextStyle(...)` +is what sizes and colours it — 13pt bold dark grey unless you say otherwise. +It is a plain `DocumentTextStyle`, so a bigger figure in the brand colour is +one call: + + + +```java +import com.demcha.compose.document.chart.ChartStyle; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.font.FontName; + +ChartStyle kpiDonut = ChartStyle.builder() + .donutCenterTextStyle(DocumentTextStyle.builder() + .fontName(FontName.HELVETICA) + .decoration(DocumentTextDecoration.BOLD) + .size(22) + .color(DocumentColor.rgb(20, 80, 95)) + .build()) + .build(); +``` + +Slice labels use `valueLabelTextStyle` and the same halo as every other value +label. ## Hiding chrome: down to "just the bars" @@ -133,8 +165,9 @@ ChartSpec.bar().data(revenue) ## Styling: the cascade -`ChartTheme` tokens → document `ChartStyle` → per-series override, merged like -CSS (every `ChartStyle` field is nullable = inherit): +Every `ChartStyle` field is nullable, and null means *inherit*. The style you +pass to `chart(spec, style)` is merged CSS-style over `ChartDefaults.DEFAULT_THEME`, +so you set the handful of things you care about and the rest stays consistent: ```java ChartStyle.builder() @@ -148,6 +181,107 @@ ChartStyle.builder() The palette cycles by modulo, so a chart never runs out of colours. +`ChartTheme` is that base set of tokens, and the authoring API does not currently +expose a way to swap it: a chart resolves its geometry during the layout pass, +after the document's theme is out of reach, so every chart placed through the DSL +starts from `ChartDefaults.DEFAULT_THEME` and `ChartStyle` is the author-facing +override. Give charts that must match a brand a shared `ChartStyle` constant and +pass it to each one. + +`ChartLayoutResolver.resolve(...)` does take an explicit `ChartTheme`, but it +returns raw primitives rather than placing a chart in a document — that is the +geometry seam, useful for tooling and tests, not a second way to author. + +### Typography + +Three text styles cover the chrome, all plain `DocumentTextStyle` (the fourth, +[`donutCenterTextStyle`](#pie-and-donut), belongs to the donut hole): + + + +```java +import com.demcha.compose.document.chart.ChartStyle; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.font.FontName; + +ChartStyle onDark = ChartStyle.builder() + .axisTextStyle(label(7.5, DocumentColor.rgb(150, 160, 175))) // ticks + categories + .legendTextStyle(label(8, DocumentColor.rgb(150, 160, 175))) // series names + .valueLabelTextStyle(label(9, DocumentColor.WHITE)) // numbers on the data + .build(); + +static DocumentTextStyle label(double size, DocumentColor color) { + return DocumentTextStyle.builder() + .fontName(FontName.HELVETICA) + .size(size) + .color(color) + .build(); +} +``` + +`axisTextStyle` covers both the numeric ticks and the category labels — they +are the same chrome and read best when they match. Defaults are 8pt/9pt/8pt +grey, tuned for a white page: on a dark card you will want to set all three, +and the halo below along with them. + +### The value-label halo + +`valueLabelHalo` is the chip drawn *behind* a value or slice label so the digits +stay legible where the chart's own graphics run under them — a grid line, a +series stroke, a slice edge. It is a `DocumentPaint`, white by default, which is +right on a white page and wrong everywhere else: on a tinted card an unset halo +paints white rectangles across your background. + +Set it to the surface the chart sits on: + + + +```java +import com.demcha.compose.document.chart.ChartStyle; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentPaint; + +static final DocumentColor CARD = DocumentColor.rgb(18, 24, 38); + +ChartStyle onCard = ChartStyle.builder() + .valueLabelHalo(DocumentPaint.solid(CARD)) // match the card + .build(); + +ChartStyle softened = ChartStyle.builder() + .valueLabelHalo(DocumentPaint.solid(CARD.withOpacity(0.6))) // let the grid show + .build(); +``` + +`withOpacity(...)` makes the chip genuinely translucent — real graphics-state +alpha, so the grid stays faintly visible through it instead of being punched +out. Useful when a solid chip reads as a sticker. (`DocumentColor.rgba(r, g, b, a)` +does the same with an integer 0–255 alpha.) + +### Every `ChartStyle` field + +| Setting | Default | Applies to | +|---|---|---| +| `palette(...)` / `seriesPaint(i, ...)` | 8-colour Tableau-inspired palette † | all | +| `lineWidth(double)` | `1.5` | line | +| `pointMarker(PointMarker)` | none | line | +| `areaOpacity(double)` | `0.35` | line with `area(true)` | +| `barCornerRadius(DocumentCornerRadius)` | square corners | bar | +| `barWidthRatio(double)` | `0.72` | bar | +| `grid(GridStyle)` | horizontal, 0.5pt `#E0E0E0` † | bar, line | +| `axisTextStyle(DocumentTextStyle)` | 8pt `#5A5A5A` † | tick + category labels | +| `legendTextStyle(DocumentTextStyle)` | 9pt `#3C3C3C` † | legend | +| `valueLabelTextStyle(DocumentTextStyle)` | 8pt `#3C3C3C` † | value + slice labels | +| `valueLabelHalo(DocumentPaint)` | white † | chip behind those labels | +| `valueLabelOffset(double)` | `2` | gap from a line marker, bar end or pie edge | +| `sliceStroke(DocumentStroke)` | white 1pt | pie, donut | +| `sliceGapDegrees(double)` | `0` | pie, donut | +| `donutCenterTextStyle(DocumentTextStyle)` | 13pt bold `#2D2D2D` | donut centre | + +† inherited from `ChartDefaults.DEFAULT_THEME`; unmarked rows are fixed engine +defaults, some of them constants in `ChartDefaults` and some — `pointMarker`, +square corners — simply the absence of an override. + ## Inline sparklines Mini-charts that sit on the text baseline like any other inline shape — a