@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/CHANGELOG.md +33 -0
- package/README.md +3 -3
- package/dist/charts.css +1 -1
- package/dist/charts.js +2334 -2057
- package/dist/de.d.ts +16 -0
- package/dist/index.d.ts +139 -1
- package/dist/wording/de.js +17 -6
- package/docs/llms-full.md +579 -4
- package/package.json +4 -3
package/docs/llms-full.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# @umriss-ui/charts 0.
|
|
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.
|
|
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.
|
|
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",
|