@umriss-ui/charts 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/llms-full.md CHANGED
@@ -1,6 +1,6 @@
1
- # @umriss-ui/charts 0.8.2
1
+ # @umriss-ui/charts 0.9.0
2
2
 
3
- > React canvas chart library for data-dense dashboards - line, area, bar, scatter, control and Pareto charts. Few chart kinds, drawn well, fast.
3
+ > React canvas chart library for data-dense dashboards - line, area, bar, box plot, scatter, control and Pareto charts. Few chart kinds, drawn well, fast.
4
4
 
5
5
  Install with `pnpm add @umriss-ui/charts`. This text is generated from the package's demo (https://romanhaendler.github.io/umriss-ui/charts/): every page with its import line, its examples - the source exactly as it runs, with the package name where the demo imports its own source - its props tables generated from the code, and what it deliberately does not do. The pages index stands in https://romanhaendler.github.io/umriss-ui/charts/llms.txt.
6
6
 
@@ -1717,6 +1717,448 @@ It imports `logistics.ts` from beside itself; the file stands once, under "Files
1717
1717
 
1718
1718
  What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0032-what-umriss-is-not.md).
1719
1719
 
1720
+ ### BoxPlot
1721
+
1722
+ A distribution per position (a box plot, box-and-whisker): quartiles, median and whiskers, per service, per group or per hour. Reach for it to compare spreads, not single values.
1723
+
1724
+ ```ts
1725
+ import { BoxPlot } from "@umriss-ui/charts";
1726
+ ```
1727
+
1728
+ Demo page: https://romanhaendler.github.io/umriss-ui/charts/boxplot/
1729
+
1730
+ Every number is the caller's: `median`, the two quartiles and where each whisker ends. The library computes none of them and names no quartile method or whisker rule, so a box aggregated in a database draws as well as one computed in the browser.
1731
+
1732
+ A box stands on the numeric x axis as a bar does: a group is a position a `tickFormat` names, and a time axis carries a box per hour (ADR-0002). A missing median is a gap - no box is drawn there.
1733
+
1734
+ #### Examples
1735
+
1736
+ ##### One box per service
1737
+
1738
+ Five numbers per row, computed wherever the caller likes. Services are positions 0, 1, 2 on the numeric x axis; `tickFormat` names them.
1739
+
1740
+ ```tsx
1741
+ import { BoxPlot, Chart, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
1742
+
1743
+ interface ResponseTime {
1744
+ service: string;
1745
+ low: number;
1746
+ q1: number;
1747
+ median: number;
1748
+ q3: number;
1749
+ high: number;
1750
+ }
1751
+
1752
+ /* Milliseconds per request over the last hour, aggregated by the database. */
1753
+ const RESPONSE_TIMES: ResponseTime[] = [
1754
+ { service: "Checkout", low: 112, q1: 138, median: 149, q3: 161, high: 195 },
1755
+ { service: "Billing", low: 85, q1: 121, median: 140, q3: 173, high: 238 },
1756
+ { service: "Search", low: 140, q1: 152, median: 158, q3: 165, high: 181 },
1757
+ ];
1758
+
1759
+ export default function Minimal() {
1760
+ return (
1761
+ <Chart data={RESPONSE_TIMES} height={260} ariaLabel="Response time per service over the last hour">
1762
+ <XAxis accessor={(_: ResponseTime, i) => i} ticks={[0, 1, 2]} tickFormat={(v) => RESPONSE_TIMES[v]?.service ?? ""} />
1763
+ <YAxis accessor={(d: ResponseTime) => d.median} tickFormat={(v) => `${v} ms`} />
1764
+ <BoxPlot
1765
+ name="Response time"
1766
+ median={(d: ResponseTime) => d.median}
1767
+ lowerQuartile={(d: ResponseTime) => d.q1}
1768
+ upperQuartile={(d: ResponseTime) => d.q3}
1769
+ lowerWhisker={(d: ResponseTime) => d.low}
1770
+ upperWhisker={(d: ResponseTime) => d.high}
1771
+ />
1772
+ <Tooltip />
1773
+ </Chart>
1774
+ );
1775
+ }
1776
+ ```
1777
+
1778
+ ##### A box per hour on a time axis
1779
+
1780
+ Checkout's median latency, every five minutes, gathered into one box per hour. The quartiles and the whisker rule are the caller's - here the last value within 1.5 IQR - and the library draws what it is given.
1781
+
1782
+ ```tsx
1783
+ import { BoxPlot, Chart, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
1784
+ import { metrics } from "./operations";
1785
+
1786
+ const HOUR = 3_600_000;
1787
+
1788
+ interface HourBox {
1789
+ hour: number;
1790
+ low: number;
1791
+ q1: number;
1792
+ median: number;
1793
+ q3: number;
1794
+ high: number;
1795
+ }
1796
+
1797
+ /** The value at share p of the sorted values, between neighbours. */
1798
+ function quantile(sorted: readonly number[], p: number): number {
1799
+ const at = (sorted.length - 1) * p;
1800
+ const lo = Math.floor(at);
1801
+ return (sorted[lo] as number) + ((sorted[Math.ceil(at)] as number) - (sorted[lo] as number)) * (at - lo);
1802
+ }
1803
+
1804
+ function hourBoxes(): HourBox[] {
1805
+ const byHour = new Map<number, number[]>();
1806
+ for (const point of metrics("checkout")) {
1807
+ const hour = Math.floor(point.t / HOUR) * HOUR;
1808
+ byHour.set(hour, [...(byHour.get(hour) ?? []), point.p50]);
1809
+ }
1810
+ return [...byHour].map(([hour, values]) => {
1811
+ const sorted = values.sort((a, b) => a - b);
1812
+ const q1 = quantile(sorted, 0.25);
1813
+ const q3 = quantile(sorted, 0.75);
1814
+ const reach = 1.5 * (q3 - q1);
1815
+ const inside = sorted.filter((v) => v >= q1 - reach && v <= q3 + reach);
1816
+ return { hour, low: inside[0] as number, q1, median: quantile(sorted, 0.5), q3, high: inside[inside.length - 1] as number };
1817
+ });
1818
+ }
1819
+
1820
+ const BOXES = hourBoxes();
1821
+
1822
+ export default function OverTime() {
1823
+ return (
1824
+ <Chart data={BOXES} height={280} ariaLabel="Checkout median latency per hour today">
1825
+ <XAxis accessor={(d: HourBox) => d.hour} time />
1826
+ <YAxis accessor={(d: HourBox) => d.median} tickFormat={(v) => `${v} ms`} />
1827
+ <BoxPlot
1828
+ name="Checkout p50"
1829
+ median={(d: HourBox) => d.median}
1830
+ lowerQuartile={(d: HourBox) => d.q1}
1831
+ upperQuartile={(d: HourBox) => d.q3}
1832
+ lowerWhisker={(d: HourBox) => d.low}
1833
+ upperWhisker={(d: HourBox) => d.high}
1834
+ />
1835
+ <Tooltip />
1836
+ </Chart>
1837
+ );
1838
+ }
1839
+ ```
1840
+
1841
+ It imports `operations.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
1842
+
1843
+ ##### With outliers
1844
+
1845
+ `outliers` returns the values beyond the whiskers, a list per box. They are drawn with their box and read in its tooltip, the first five written out; one more than three IQR beyond its box is a ring.
1846
+
1847
+ ```tsx
1848
+ import { BoxPlot, Chart, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
1849
+
1850
+ interface ResponseTime {
1851
+ service: string;
1852
+ low: number;
1853
+ q1: number;
1854
+ median: number;
1855
+ q3: number;
1856
+ high: number;
1857
+ outliers: number[];
1858
+ }
1859
+
1860
+ /* Milliseconds per request over the last hour. The whiskers end at the last
1861
+ value within 1.5 IQR; the database returned everything beyond as outliers. */
1862
+ const RESPONSE_TIMES: ResponseTime[] = [
1863
+ { service: "Checkout", low: 112, q1: 138, median: 149, q3: 161, high: 195, outliers: [224, 98] },
1864
+ { service: "Billing", low: 85, q1: 121, median: 140, q3: 173, high: 238, outliers: [281, 395] },
1865
+ { service: "Search", low: 140, q1: 152, median: 158, q3: 165, high: 181, outliers: [189, 214, 104] },
1866
+ { service: "Images", low: 99, q1: 126, median: 137, q3: 150, high: 186, outliers: [193, 198, 204, 210, 222, 235, 259, 71] },
1867
+ ];
1868
+
1869
+ export default function WithOutliers() {
1870
+ return (
1871
+ <Chart data={RESPONSE_TIMES} height={300} ariaLabel="Response time per service over the last hour, with outliers">
1872
+ <XAxis accessor={(_: ResponseTime, i) => i} ticks={[0, 1, 2, 3]} tickFormat={(v) => RESPONSE_TIMES[v]?.service ?? ""} />
1873
+ <YAxis accessor={(d: ResponseTime) => d.median} tickFormat={(v) => `${v} ms`} />
1874
+ <BoxPlot
1875
+ name="Response time"
1876
+ median={(d: ResponseTime) => d.median}
1877
+ lowerQuartile={(d: ResponseTime) => d.q1}
1878
+ upperQuartile={(d: ResponseTime) => d.q3}
1879
+ lowerWhisker={(d: ResponseTime) => d.low}
1880
+ upperWhisker={(d: ResponseTime) => d.high}
1881
+ outliers={(d: ResponseTime) => d.outliers}
1882
+ />
1883
+ <Tooltip />
1884
+ </Chart>
1885
+ );
1886
+ }
1887
+ ```
1888
+
1889
+ ##### Mean and notches: do two medians differ?
1890
+
1891
+ Where two boxes' notches do not overlap, their medians differ. The bounds are yours - here the usual median ± 1.57 · IQR / √n. `mean` draws a small ×: beside the median it shows a skew. `count` is read, not drawn.
1892
+
1893
+ ```tsx
1894
+ import { BoxPlot, Chart, DataTable, Legend, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
1895
+
1896
+ interface Batch {
1897
+ supplier: string;
1898
+ low: number;
1899
+ q1: number;
1900
+ median: number;
1901
+ q3: number;
1902
+ high: number;
1903
+ mean: number;
1904
+ n: number;
1905
+ }
1906
+
1907
+ /* Breaking strength of tiles, in newtons, per clay supplier. */
1908
+ const BATCHES: Batch[] = [
1909
+ { supplier: "Hollen", low: 1310, q1: 1395, median: 1430, q3: 1468, high: 1540, mean: 1431, n: 120 },
1910
+ { supplier: "Marsk", low: 1290, q1: 1380, median: 1418, q3: 1462, high: 1555, mean: 1426, n: 64 },
1911
+ { supplier: "Velde", low: 1240, q1: 1322, median: 1352, q3: 1398, high: 1490, mean: 1371, n: 96 },
1912
+ ];
1913
+
1914
+ const notch = (d: Batch) => (1.57 * (d.q3 - d.q1)) / Math.sqrt(d.n);
1915
+
1916
+ export default function MeanAndNotches() {
1917
+ return (
1918
+ <Chart data={BATCHES} height={300} ariaLabel="Breaking strength per clay supplier, with mean and notches">
1919
+ <XAxis accessor={(_: Batch, i) => i} ticks={[0, 1, 2]} tickFormat={(v) => BATCHES[v]?.supplier ?? ""} />
1920
+ <YAxis accessor={(d: Batch) => d.median} tickFormat={(v) => `${v} N`} />
1921
+ <BoxPlot
1922
+ name="Breaking strength"
1923
+ median={(d: Batch) => d.median}
1924
+ lowerQuartile={(d: Batch) => d.q1}
1925
+ upperQuartile={(d: Batch) => d.q3}
1926
+ lowerWhisker={(d: Batch) => d.low}
1927
+ upperWhisker={(d: Batch) => d.high}
1928
+ mean={(d: Batch) => d.mean}
1929
+ notchLower={(d: Batch) => d.median - notch(d)}
1930
+ notchUpper={(d: Batch) => d.median + notch(d)}
1931
+ count={(d: Batch) => d.n}
1932
+ format={(v) => `${Math.round(v)} N`}
1933
+ boxWidth={0.5}
1934
+ />
1935
+ <Legend />
1936
+ <Tooltip />
1937
+ <DataTable />
1938
+ </Chart>
1939
+ );
1940
+ }
1941
+ ```
1942
+
1943
+ ##### Before and after, side by side
1944
+
1945
+ Box series on one x axis stand beside each other, as bars do. `hidden` is your state and `onToggle` hands you the clicked entry; a hidden series leaves the drawing and the y extent.
1946
+
1947
+ ```tsx
1948
+ import { useState } from "react";
1949
+ import { BoxPlot, Chart, Legend, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
1950
+
1951
+ interface Spread {
1952
+ service: number;
1953
+ low: number;
1954
+ q1: number;
1955
+ median: number;
1956
+ q3: number;
1957
+ high: number;
1958
+ }
1959
+
1960
+ const SERVICES = ["Checkout", "Billing", "Search", "Images"];
1961
+
1962
+ /* Milliseconds per request, the week before and the week after the release. */
1963
+ const BEFORE: Spread[] = [
1964
+ { service: 0, low: 112, q1: 138, median: 149, q3: 161, high: 195 },
1965
+ { service: 1, low: 85, q1: 121, median: 140, q3: 173, high: 238 },
1966
+ { service: 2, low: 140, q1: 152, median: 158, q3: 165, high: 181 },
1967
+ { service: 3, low: 99, q1: 126, median: 137, q3: 150, high: 186 },
1968
+ ];
1969
+ const AFTER: Spread[] = [
1970
+ { service: 0, low: 118, q1: 135, median: 143, q3: 152, high: 176 },
1971
+ { service: 1, low: 101, q1: 119, median: 128, q3: 139, high: 164 },
1972
+ { service: 2, low: 142, q1: 151, median: 157, q3: 164, high: 179 },
1973
+ { service: 3, low: 103, q1: 124, median: 135, q3: 146, high: 172 },
1974
+ ];
1975
+
1976
+ export default function Grouped() {
1977
+ const [hidden, setHidden] = useState<readonly string[]>([]);
1978
+ const toggle = (name: string) => setHidden((h) => (h.includes(name) ? h.filter((n) => n !== name) : [...h, name]));
1979
+ return (
1980
+ <Chart data={BEFORE} height={280} ariaLabel="Response time per service before and after the release">
1981
+ <XAxis accessor={(d: Spread) => d.service} ticks={[0, 1, 2, 3]} tickFormat={(v) => SERVICES[v] ?? ""} />
1982
+ <YAxis accessor={(d: Spread) => d.median} tickFormat={(v) => `${v} ms`} />
1983
+ {[
1984
+ { name: "Before", data: BEFORE },
1985
+ { name: "After", data: AFTER },
1986
+ ].map((one) => (
1987
+ <BoxPlot
1988
+ key={one.name}
1989
+ name={one.name}
1990
+ data={one.data}
1991
+ hidden={hidden.includes(one.name)}
1992
+ median={(d: Spread) => d.median}
1993
+ lowerQuartile={(d: Spread) => d.q1}
1994
+ upperQuartile={(d: Spread) => d.q3}
1995
+ lowerWhisker={(d: Spread) => d.low}
1996
+ upperWhisker={(d: Spread) => d.high}
1997
+ />
1998
+ ))}
1999
+ <Legend onToggle={toggle} />
2000
+ <Tooltip />
2001
+ </Chart>
2002
+ );
2003
+ }
2004
+ ```
2005
+
2006
+ ##### Against the objective
2007
+
2008
+ A `LimitLine` shows what was promised. Whether a box breaks it is your judgement, not the library's: this week's whiskers reach past the objective in every region, so its series carries `tone="alarm"`.
2009
+
2010
+ ```tsx
2011
+ import { BoxPlot, Chart, Legend, LimitLine, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
2012
+
2013
+ interface Spread {
2014
+ region: number;
2015
+ low: number;
2016
+ q1: number;
2017
+ median: number;
2018
+ q3: number;
2019
+ high: number;
2020
+ }
2021
+
2022
+ const REGIONS = ["Europe", "Americas", "Asia"];
2023
+
2024
+ /* Checkout's 95th percentile per five minutes, in ms, per region and week. */
2025
+ const LAST_WEEK: Spread[] = [
2026
+ { region: 0, low: 182, q1: 204, median: 216, q3: 229, high: 262 },
2027
+ { region: 1, low: 191, q1: 213, median: 226, q3: 240, high: 276 },
2028
+ { region: 2, low: 176, q1: 199, median: 211, q3: 224, high: 258 },
2029
+ ];
2030
+ const THIS_WEEK: Spread[] = [
2031
+ { region: 0, low: 196, q1: 231, median: 249, q3: 268, high: 318 },
2032
+ { region: 1, low: 205, q1: 242, median: 262, q3: 283, high: 339 },
2033
+ { region: 2, low: 190, q1: 226, median: 244, q3: 263, high: 311 },
2034
+ ];
2035
+
2036
+ export default function AgainstTheObjective() {
2037
+ return (
2038
+ <Chart data={LAST_WEEK} height={280} ariaLabel="Checkout 95th percentile per region and week against its objective">
2039
+ <XAxis accessor={(d: Spread) => d.region} ticks={[0, 1, 2]} tickFormat={(v) => REGIONS[v] ?? ""} />
2040
+ <YAxis accessor={(d: Spread) => d.median} tickFormat={(v) => `${v} ms`} />
2041
+ <BoxPlot
2042
+ name="Last week"
2043
+ median={(d: Spread) => d.median}
2044
+ lowerQuartile={(d: Spread) => d.q1}
2045
+ upperQuartile={(d: Spread) => d.q3}
2046
+ lowerWhisker={(d: Spread) => d.low}
2047
+ upperWhisker={(d: Spread) => d.high}
2048
+ />
2049
+ <BoxPlot
2050
+ name="This week"
2051
+ data={THIS_WEEK}
2052
+ tone="alarm"
2053
+ median={(d: Spread) => d.median}
2054
+ lowerQuartile={(d: Spread) => d.q1}
2055
+ upperQuartile={(d: Spread) => d.q3}
2056
+ lowerWhisker={(d: Spread) => d.low}
2057
+ upperWhisker={(d: Spread) => d.high}
2058
+ />
2059
+ <LimitLine value={300} severity="alarm" label="Objective" />
2060
+ <Legend />
2061
+ <Tooltip />
2062
+ </Chart>
2063
+ );
2064
+ }
2065
+ ```
2066
+
2067
+ ##### A box per shift, its medians as a line
2068
+
2069
+ The tiles measured in each of six shifts, one box each, and a `Line` through the medians to read the drift. The tooltip reads box and line together; `DataTable` lists every number as text.
2070
+
2071
+ ```tsx
2072
+ import { BoxPlot, Chart, DataTable, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
2073
+ import { plant } from "./plant";
2074
+
2075
+ const SHIFTS = ["Mon early", "Mon late", "Tue early", "Tue late", "Wed early", "Wed late"];
2076
+
2077
+ interface ShiftBox {
2078
+ shift: number;
2079
+ low: number;
2080
+ q1: number;
2081
+ median: number;
2082
+ q3: number;
2083
+ high: number;
2084
+ }
2085
+
2086
+ /** The value at share p of the sorted values, between neighbours. */
2087
+ function quantile(sorted: readonly number[], p: number): number {
2088
+ const at = (sorted.length - 1) * p;
2089
+ const lo = Math.floor(at);
2090
+ return (sorted[lo] as number) + ((sorted[Math.ceil(at)] as number) - (sorted[lo] as number)) * (at - lo);
2091
+ }
2092
+
2093
+ /* One shift per seed; the whiskers end at the last length within 1.5 IQR. */
2094
+ const BOXES: ShiftBox[] = SHIFTS.map((_, shift) => {
2095
+ const sorted = plant(shift + 1).samples.map((s) => s.length).sort((a, b) => a - b);
2096
+ const q1 = quantile(sorted, 0.25);
2097
+ const q3 = quantile(sorted, 0.75);
2098
+ const reach = 1.5 * (q3 - q1);
2099
+ const inside = sorted.filter((v) => v >= q1 - reach && v <= q3 + reach);
2100
+ return { shift, low: inside[0] as number, q1, median: quantile(sorted, 0.5), q3, high: inside[inside.length - 1] as number };
2101
+ });
2102
+
2103
+ const mm = (v: number) => `${v.toFixed(2)} mm`;
2104
+
2105
+ export default function Detailed() {
2106
+ return (
2107
+ <Chart data={BOXES} height={320} ariaLabel="Tile length per shift, with the medians as a line">
2108
+ <XAxis accessor={(d: ShiftBox) => d.shift} ticks={SHIFTS.map((_, i) => i)} tickFormat={(v) => SHIFTS[v] ?? ""} label="Shift" />
2109
+ <YAxis accessor={(d: ShiftBox) => d.median} tickFormat={mm} />
2110
+ <BoxPlot
2111
+ name="Tile length"
2112
+ median={(d: ShiftBox) => d.median}
2113
+ lowerQuartile={(d: ShiftBox) => d.q1}
2114
+ upperQuartile={(d: ShiftBox) => d.q3}
2115
+ lowerWhisker={(d: ShiftBox) => d.low}
2116
+ upperWhisker={(d: ShiftBox) => d.high}
2117
+ boxWidth={0.5}
2118
+ />
2119
+ <Line accessor={(d: ShiftBox) => d.median} name="Median" markers="always" />
2120
+ <Legend />
2121
+ <Tooltip />
2122
+ <DataTable />
2123
+ </Chart>
2124
+ );
2125
+ }
2126
+ ```
2127
+
2128
+ It imports `plant.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
2129
+
2130
+ #### API
2131
+
2132
+ ##### `BoxPlotProps<T>`
2133
+
2134
+ | Prop | Type | Default | Description |
2135
+ |---|---|---|---|
2136
+ | `median` (required) | `Accessor<T>` | — | The line across the box; null/undefined/NaN/±Infinity means a gap - no box is drawn there, whatever the other numbers say (R-2.5). |
2137
+ | `lowerQuartile` (required) | `Accessor<T>` | — | The box's lower edge. |
2138
+ | `upperQuartile` (required) | `Accessor<T>` | — | The box's upper edge. |
2139
+ | `lowerWhisker` (required) | `Accessor<T>` | — | Where the lower whisker ends - the caller's rule, not the minimum. |
2140
+ | `upperWhisker` (required) | `Accessor<T>` | — | Where the upper whisker ends - the caller's rule, not the maximum. |
2141
+ | `outliers` | `ListAccessor<T>` | — | The values beyond the whiskers, per box: an array, empty or missing where there are none. Drawn with their box in its colour - one beyond three IQR of the box's own quartiles as a ring -, read in its tooltip and table row, never hit on their own (ADR-0040). |
2142
+ | `mean` | `Accessor<T>` | — | The mean, drawn as a small ×: beside the median it shows a skew. |
2143
+ | `notchLower` | `Accessor<T>` | — | The notch's lower bound - usually of a confidence interval of the median. Both bounds or neither: one alone warns in DEV and is not drawn. |
2144
+ | `notchUpper` | `Accessor<T>` | — | The notch's upper bound; see `notchLower`. |
2145
+ | `count` | `Accessor<T>` | — | How many values stand behind the box; read in the tooltip and the table, not drawn. |
2146
+ | `xAxisId` | `string` | — | Binding to an x axis (R-4.12). |
2147
+ | `yAxisId` | `string` | — | Binding to a y axis (R-4.12). |
2148
+ | `data` | `readonly T[]` | — | Series-own data; overrides the container data (R-2.4). |
2149
+ | `name` | `string` | — | The name in legend and tooltip. Without one the series is called "Series n" and a warning stands in DEV. |
2150
+ | `hidden` | `boolean` | — | Not drawn, not hit and not counted for its axes' extent. Its legend entry stays, drawn back. Controlled, typically from `Legend onToggle`. |
2151
+ | `format` | `(value: number) => string` | — | Every number of the box as the tooltip and the table write it; without one the y axis' `tickFormat`, then the default. |
2152
+ | `color` | `string` | — | Any CSS colour value; without one the palette --uc-series-N. |
2153
+ | `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it. `color` beats it. Set by the caller: a box out of its specification is the caller's judgement, not the library's. |
2154
+ | `boxWidth` | `number` | — | Width as a fraction of the step, as a bar's: boxes and bars on one x axis share this fraction and stand side by side. |
2155
+
2156
+ #### Known limits
2157
+
2158
+ - No horizontal boxes, no violin or jittered points, no box width by count: a density is smoothing, a jitter a picture that does not repeat.
2159
+
2160
+ What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0032-what-umriss-is-not.md).
2161
+
1720
2162
  ### Scatter
1721
2163
 
1722
2164
  Single readings as points, joined by nothing (a scatter plot, dots). Reach for it for samples, checks and events - anything that is not a course.
@@ -3582,6 +4024,9 @@ interface MaterializedSeries {
3582
4024
  carry does not belong in the shared type with a comment (ADR-0011). null
3583
4025
  for every kind that does not use it. */
3584
4026
  w: Float64Array | null;
4027
+ /** A box's further numbers beside its median in y; null for every other
4028
+ kind (ADR-0011). */
4029
+ box: BoxChannels | null;
3585
4030
  length: number;
3586
4031
  }
3587
4032
  ```
@@ -3655,7 +4100,7 @@ interface SeriesBase<T = unknown> {
3655
4100
  ### `SeriesConfig`
3656
4101
 
3657
4102
  ```ts
3658
- type SeriesConfig<T = unknown> = LineSeriesConfig<T> | AreaSeriesConfig<T> | BarSeriesConfig<T> | ScatterSeriesConfig<T> | StateSeriesConfig<T> | MatrixSeriesConfig<T>;
4103
+ type SeriesConfig<T = unknown> = LineSeriesConfig<T> | AreaSeriesConfig<T> | BarSeriesConfig<T> | BoxSeriesConfig<T> | ScatterSeriesConfig<T> | StateSeriesConfig<T> | MatrixSeriesConfig<T>;
3659
4104
  ```
3660
4105
 
3661
4106
  ### `SeriesKind`
@@ -3702,6 +4147,12 @@ interface TooltipPoint<T = unknown> {
3702
4147
  to: number;
3703
4148
  label: string;
3704
4149
  };
4150
+ /** Only for a box: its further numbers; yValue is its median. Fields of
4151
+ their own for the reason `value` has one (ADR-0011). */
4152
+ box?: BoxNumbers & Partial<BoxExtras> & {
4153
+ /** As the caller gave them; only where the series has `outliers`. */
4154
+ outliers?: readonly number[];
4155
+ };
3705
4156
  }
3706
4157
  ```
3707
4158
 
@@ -3775,6 +4226,75 @@ interface DataTableGroup {
3775
4226
  }
3776
4227
  ```
3777
4228
 
4229
+ ### `BoxSeriesConfig`
4230
+
4231
+ ```ts
4232
+ /** A box plot (CONTEXT.md: Box). The base accessor is the median; the four
4233
+ further numbers are the caller's, computed by whatever method the caller
4234
+ chose - the library draws them and computes none. */
4235
+ interface BoxSeriesConfig<T = unknown> extends SeriesBase<T> {
4236
+ kind: "box";
4237
+ lowerQuartile: Accessor<T>;
4238
+ upperQuartile: Accessor<T>;
4239
+ lowerWhisker: Accessor<T>;
4240
+ upperWhisker: Accessor<T>;
4241
+ /** The values beyond the whiskers, per box (ADR-0040). */
4242
+ outliers?: ListAccessor<T>;
4243
+ mean?: Accessor<T>;
4244
+ /** Both bounds or neither (CONTEXT.md: Notch). */
4245
+ notchLower?: Accessor<T>;
4246
+ notchUpper?: Accessor<T>;
4247
+ /** How many values stand behind the box; no y value. */
4248
+ count?: Accessor<T>;
4249
+ /** Width as a fraction of the step; boxes and bars on one x axis share it. */
4250
+ boxWidth: number;
4251
+ }
4252
+ ```
4253
+
4254
+ ### `BoxChannels`
4255
+
4256
+ ```ts
4257
+ /** The named channels of a box: one number per point, NaN where the median
4258
+ is a gap; and its outliers (ADR-0040). */
4259
+ interface BoxChannels extends BoxNumbers<Float64Array> {
4260
+ /** Every box's outliers, flat, box after box; null without `outliers`. */
4261
+ outliers: Float64Array | null;
4262
+ /** Where each box's outliers begin, and one past the last: box i's stand
4263
+ from outlierOffsets[i] to outlierOffsets[i + 1]. null without
4264
+ `outliers`. */
4265
+ outlierOffsets: Uint32Array | null;
4266
+ /** The optional numbers, each null where not given. */
4267
+ mean: Float64Array | null;
4268
+ notchLower: Float64Array | null;
4269
+ notchUpper: Float64Array | null;
4270
+ count: Float64Array | null;
4271
+ }
4272
+ ```
4273
+
4274
+ ### `BoxNumbers`
4275
+
4276
+ ```ts
4277
+ /** A box's further numbers beside its median, by name. */
4278
+ interface BoxNumbers<V = number> {
4279
+ lowerQuartile: V;
4280
+ upperQuartile: V;
4281
+ lowerWhisker: V;
4282
+ upperWhisker: V;
4283
+ }
4284
+ ```
4285
+
4286
+ ### `BoxExtras`
4287
+
4288
+ ```ts
4289
+ /** The optional numbers of a box (box-plot 04), by name. */
4290
+ interface BoxExtras<V = number> {
4291
+ mean: V;
4292
+ notchLower: V;
4293
+ notchUpper: V;
4294
+ count: V;
4295
+ }
4296
+ ```
4297
+
3778
4298
  ## Files the examples show
3779
4299
 
3780
4300
  The demos' shared data, which some examples import from beside themselves. Copied with the example, it runs.
@@ -3792,7 +4312,10 @@ The demos' shared data, which some examples import from beside themselves. Copie
3792
4312
  * where the month is closed (January and February), the forecast for every
3793
4313
  * month; Marketing overspends, IT runs under;
3794
4314
  * - `INVOICES` - incoming invoices with their lines (quantity, unit price,
3795
- * discount, VAT rate), and `APPROVALS`, who signed which, or has yet to.
4315
+ * discount, VAT rate), and `APPROVALS`, who signed which, or has yet to;
4316
+ * - `TEAMS` - the teams of each cost centre with their headcount, full-time
4317
+ * equivalents and monthly staff cost, and `BUSINESS_LINES`, which cost
4318
+ * centres each of the three business lines holds.
3796
4319
  *
3797
4320
  * Amounts in euros. "Now" is Tuesday, 17 March 2026. Plain data and small pure
3798
4321
  * functions, no imports: copy the file beside an example and it runs.
@@ -3944,6 +4467,58 @@ export const APPROVALS: readonly Approval[] = [
3944
4467
  { invoice: "INV-26-0226", step: "finance", approver: "Martina Vogel", decision: "approved", at: on(3, 2) },
3945
4468
  { invoice: "INV-26-0221", step: "cost centre", approver: "Anika Sørensen", decision: "rejected", at: on(2, 24), comment: "Not ordered - the review was cancelled in January." },
3946
4469
  ];
4470
+
4471
+ export interface Team {
4472
+ /** The cost centre the team books to. */
4473
+ costCentre: string;
4474
+ name: string;
4475
+ /** People employed, whatever their hours. */
4476
+ headcount: number;
4477
+ /** Full-time equivalents: contracted hours over 40 a week. */
4478
+ fte: number;
4479
+ /** Staff cost of a month, in thousand euros. */
4480
+ monthlyCost: number;
4481
+ }
4482
+
4483
+ /** Every team, on 17 March: 102 people, 91.5 full-time equivalents. */
4484
+ export const TEAMS: readonly Team[] = [
4485
+ { costCentre: "CC-1100", name: "Field sales North", headcount: 9, fte: 8.6, monthlyCost: 61.2 },
4486
+ { costCentre: "CC-1100", name: "Field sales South", headcount: 8, fte: 7.5, monthlyCost: 53.9 },
4487
+ { costCentre: "CC-1100", name: "Inside sales", headcount: 6, fte: 5.2, monthlyCost: 29.8 },
4488
+ { costCentre: "CC-1100", name: "Key accounts", headcount: 4, fte: 4.0, monthlyCost: 33.6 },
4489
+ { costCentre: "CC-1200", name: "Brand", headcount: 3, fte: 2.8, monthlyCost: 19.4 },
4490
+ { costCentre: "CC-1200", name: "Online marketing", headcount: 4, fte: 3.5, monthlyCost: 24.1 },
4491
+ { costCentre: "CC-1200", name: "Trade fairs", headcount: 2, fte: 1.5, monthlyCost: 10.2 },
4492
+ { costCentre: "CC-3100", name: "Service desk", headcount: 7, fte: 5.9, monthlyCost: 31.3 },
4493
+ { costCentre: "CC-3100", name: "Returns", headcount: 3, fte: 2.5, monthlyCost: 12.6 },
4494
+ { costCentre: "CC-2100", name: "Seating", headcount: 6, fte: 5.8, monthlyCost: 43.5 },
4495
+ { costCentre: "CC-2100", name: "Desks", headcount: 7, fte: 6.5, monthlyCost: 48.1 },
4496
+ { costCentre: "CC-2100", name: "Storage", headcount: 5, fte: 4.6, monthlyCost: 33.9 },
4497
+ { costCentre: "CC-2100", name: "Test lab", headcount: 3, fte: 2.5, monthlyCost: 16.8 },
4498
+ { costCentre: "CC-2200", name: "Industrial design", headcount: 4, fte: 3.6, monthlyCost: 27.2 },
4499
+ { costCentre: "CC-2200", name: "Colour and material", headcount: 2, fte: 1.8, monthlyCost: 12.9 },
4500
+ { costCentre: "CC-4100", name: "Accounting", headcount: 5, fte: 4.4, monthlyCost: 27.5 },
4501
+ { costCentre: "CC-4100", name: "Controlling", headcount: 3, fte: 3.0, monthlyCost: 21.3 },
4502
+ { costCentre: "CC-4200", name: "Recruiting", headcount: 2, fte: 1.8, monthlyCost: 12.4 },
4503
+ { costCentre: "CC-4200", name: "Payroll", headcount: 2, fte: 1.5, monthlyCost: 9.6 },
4504
+ { costCentre: "CC-4300", name: "Workplace", headcount: 4, fte: 3.8, monthlyCost: 24.2 },
4505
+ { costCentre: "CC-4300", name: "Applications", headcount: 5, fte: 4.5, monthlyCost: 34.8 },
4506
+ { costCentre: "CC-4400", name: "Buildings", headcount: 3, fte: 3.0, monthlyCost: 16.9 },
4507
+ { costCentre: "CC-4400", name: "Canteen", headcount: 5, fte: 3.2, monthlyCost: 14.7 },
4508
+ ];
4509
+
4510
+ export interface BusinessLine {
4511
+ name: string;
4512
+ /** Its cost centres, in the order a report lists them. */
4513
+ costCentres: readonly string[];
4514
+ }
4515
+
4516
+ /** How the company reports: three business lines over the nine cost centres. */
4517
+ export const BUSINESS_LINES: readonly BusinessLine[] = [
4518
+ { name: "Commercial", costCentres: ["CC-1100", "CC-1200", "CC-3100"] },
4519
+ { name: "Product", costCentres: ["CC-2100", "CC-2200"] },
4520
+ { name: "Corporate services", costCentres: ["CC-4100", "CC-4200", "CC-4300", "CC-4400"] },
4521
+ ];
3947
4522
  ```
3948
4523
 
3949
4524
  ### `logistics.ts`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@umriss-ui/charts",
3
- "version": "0.8.2",
4
- "description": "React canvas chart library for data-dense dashboards - line, area, bar, scatter, control and Pareto charts. Few chart kinds, drawn well, fast.",
3
+ "version": "0.9.0",
4
+ "description": "React canvas chart library for data-dense dashboards - line, area, bar, box plot, scatter, control and Pareto charts. Few chart kinds, drawn well, fast.",
5
5
  "license": "MIT",
6
6
  "author": "Roman Händler",
7
7
  "homepage": "https://romanhaendler.github.io/umriss-ui/charts/",
@@ -21,6 +21,7 @@
21
21
  "canvas",
22
22
  "line-chart",
23
23
  "bar-chart",
24
+ "box-plot",
24
25
  "scatter-plot",
25
26
  "control-chart",
26
27
  "pareto",
@@ -70,7 +71,7 @@
70
71
  "vite": "^6.0.0",
71
72
  "vite-plugin-dts": "^4.3.0",
72
73
  "@umriss-ui/demo": "0.0.0",
73
- "@umriss-ui/core": "0.16.0"
74
+ "@umriss-ui/core": "0.19.0"
74
75
  },
75
76
  "scripts": {
76
77
  "props": "node --disable-warning=ExperimentalWarning --experimental-strip-types demo/props.ts",