@paul-portfolio/react 0.4.3 → 0.5.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/dist/BarChart.d.ts +22 -0
- package/dist/BarChart.js +21 -0
- package/dist/DonutChart.d.ts +26 -0
- package/dist/DonutChart.js +19 -0
- package/dist/FunnelChart.d.ts +26 -0
- package/dist/FunnelChart.js +24 -0
- package/dist/GaugeChart.d.ts +37 -0
- package/dist/GaugeChart.js +44 -0
- package/dist/GradientBackground.d.ts +21 -0
- package/dist/GradientBackground.js +24 -0
- package/dist/HeatmapChart.d.ts +46 -0
- package/dist/HeatmapChart.js +53 -0
- package/dist/ParetoChart.d.ts +34 -0
- package/dist/ParetoChart.js +44 -0
- package/dist/RadarChart.d.ts +35 -0
- package/dist/RadarChart.js +55 -0
- package/dist/ScatterPlot.d.ts +37 -0
- package/dist/ScatterPlot.js +22 -0
- package/dist/Sparkline.d.ts +30 -0
- package/dist/Sparkline.js +21 -0
- package/dist/Spotlight.d.ts +17 -0
- package/dist/Spotlight.js +39 -0
- package/dist/StackedLineChart.d.ts +35 -0
- package/dist/StackedLineChart.js +38 -0
- package/dist/Ticker.d.ts +27 -0
- package/dist/Ticker.js +96 -0
- package/dist/TiltCard.d.ts +17 -0
- package/dist/TiltCard.js +45 -0
- package/dist/WordCloud.d.ts +38 -0
- package/dist/WordCloud.js +36 -0
- package/dist/chartGeometry.d.ts +229 -0
- package/dist/chartGeometry.js +450 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/usePrefersReducedMotion.d.ts +6 -0
- package/dist/usePrefersReducedMotion.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
type BarChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
3
|
+
/** One bar per value. */
|
|
4
|
+
data: number[];
|
|
5
|
+
/** Optional category labels, used to build the accessible summary. */
|
|
6
|
+
labels?: string[];
|
|
7
|
+
/** Bar direction. Defaults to vertical. */
|
|
8
|
+
orientation?: 'vertical' | 'horizontal';
|
|
9
|
+
/** Per-bar fill colors. Falls back to the token palette when omitted. */
|
|
10
|
+
colors?: string[];
|
|
11
|
+
/** Accessible name for the chart. Required. */
|
|
12
|
+
label: string;
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A categorical bar chart, vertical or horizontal. Pure SVG from
|
|
18
|
+
* `chartGeometry`, matching the Angular `PaulBarChart`. Colour is never the
|
|
19
|
+
* only signal — the values are summarised in the `role="img"` label.
|
|
20
|
+
*/
|
|
21
|
+
export declare function BarChart({ data, labels, orientation, colors, label, width, height, className, ...props }: BarChartProps): import("react").JSX.Element;
|
|
22
|
+
export {};
|
package/dist/BarChart.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './cx';
|
|
3
|
+
import { barRects, barRectsHorizontal } from './chartGeometry';
|
|
4
|
+
function summarise(data, labels) {
|
|
5
|
+
return data
|
|
6
|
+
.map((v, i) => (labels?.[i] ? `${labels[i]} ${v}` : `${v}`))
|
|
7
|
+
.join(', ');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A categorical bar chart, vertical or horizontal. Pure SVG from
|
|
11
|
+
* `chartGeometry`, matching the Angular `PaulBarChart`. Colour is never the
|
|
12
|
+
* only signal — the values are summarised in the `role="img"` label.
|
|
13
|
+
*/
|
|
14
|
+
export function BarChart({ data, labels, orientation = 'vertical', colors, label, width = 160, height = 100, className, ...props }) {
|
|
15
|
+
const horizontal = orientation === 'horizontal';
|
|
16
|
+
const box = { width, height, padding: 2 };
|
|
17
|
+
const rects = horizontal ? barRectsHorizontal(data, box) : barRects(data, box);
|
|
18
|
+
const name = data.length > 0 ? `${label}: ${summarise(data, labels)}` : label;
|
|
19
|
+
const fillFor = (i) => colors?.[i] ?? `var(--paul-chart-${(i % 6) + 1})`;
|
|
20
|
+
return (_jsx("div", { role: "img", "aria-label": name, className: cx('paul-chart', 'paul-chart--bar', className), ...props, children: _jsx("svg", { className: cx('paul-chart__svg', horizontal && 'paul-chart__svg--horizontal'), viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "none", "aria-hidden": "true", focusable: "false", children: rects.map((r, i) => (_jsx("rect", { className: "paul-chart__bar", x: r.x, y: r.y, width: r.width, height: r.height, rx: 2, fill: fillFor(i) }, i))) }) }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type DonutDatum = {
|
|
3
|
+
label: string;
|
|
4
|
+
value: number;
|
|
5
|
+
/** Slice colour. Falls back to the token palette when omitted. */
|
|
6
|
+
color?: string;
|
|
7
|
+
};
|
|
8
|
+
type DonutChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
9
|
+
data: DonutDatum[];
|
|
10
|
+
/** Accessible name for the chart. Required. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Show the swatch + value legend beside the ring. Defaults to true. */
|
|
13
|
+
legend?: boolean;
|
|
14
|
+
/** Outer diameter, in coordinate units. Defaults to 120. */
|
|
15
|
+
size?: number;
|
|
16
|
+
/** Ring thickness, in coordinate units. Defaults to 28. */
|
|
17
|
+
thickness?: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A donut (ring) chart with an optional legend — the shape behind fleet-health
|
|
21
|
+
* and revenue-mix breakdowns. Pure SVG from `chartGeometry`, matching the
|
|
22
|
+
* Angular `PaulDonutChart`. The ring is `role="img"`; the legend sits outside
|
|
23
|
+
* it so its rows stay in the accessibility tree.
|
|
24
|
+
*/
|
|
25
|
+
export declare function DonutChart({ data, label, legend, size, thickness, className, ...props }: DonutChartProps): import("react").JSX.Element;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './cx';
|
|
3
|
+
import { donutSegments } from './chartGeometry';
|
|
4
|
+
/**
|
|
5
|
+
* A donut (ring) chart with an optional legend — the shape behind fleet-health
|
|
6
|
+
* and revenue-mix breakdowns. Pure SVG from `chartGeometry`, matching the
|
|
7
|
+
* Angular `PaulDonutChart`. The ring is `role="img"`; the legend sits outside
|
|
8
|
+
* it so its rows stay in the accessibility tree.
|
|
9
|
+
*/
|
|
10
|
+
export function DonutChart({ data, label, legend = true, size = 120, thickness = 28, className, ...props }) {
|
|
11
|
+
const values = data.map((d) => d.value);
|
|
12
|
+
const segments = donutSegments(values, { size, thickness });
|
|
13
|
+
const summary = data.map((d) => `${d.label} ${d.value}`).join(', ');
|
|
14
|
+
const name = data.length > 0 ? `${label}: ${summary}` : label;
|
|
15
|
+
const colorFor = (i) => data[i]?.color ?? `var(--paul-chart-${(i % 6) + 1})`;
|
|
16
|
+
return (_jsxs("div", { className: cx('paul-chart', 'paul-chart--donut', className), ...props, children: [_jsx("div", { role: "img", "aria-label": name, className: "paul-chart__figure", children: _jsx("svg", { className: "paul-chart__svg", viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", focusable: "false", children: segments
|
|
17
|
+
.filter((seg) => seg.percent > 0)
|
|
18
|
+
.map((seg) => (_jsx("path", { className: "paul-chart__slice", d: seg.path, fill: colorFor(seg.index) }, seg.index))) }) }), legend && data.length > 0 && (_jsx("ul", { className: "paul-chart__legend", children: data.map((d, i) => (_jsxs("li", { className: "paul-chart__legend-item", children: [_jsx("span", { className: "paul-chart__swatch", "aria-hidden": "true", style: { backgroundColor: colorFor(i) } }), _jsx("span", { className: "paul-chart__legend-label", children: d.label }), _jsx("span", { className: "paul-chart__legend-value", children: d.value })] }, d.label))) }))] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type FunnelDatum = {
|
|
3
|
+
label: string;
|
|
4
|
+
value: number;
|
|
5
|
+
};
|
|
6
|
+
type FunnelChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
7
|
+
data: FunnelDatum[];
|
|
8
|
+
/** Accessible name for the chart. Required. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Show the per-stage drop-off beside each row. Defaults to true. */
|
|
11
|
+
showDropOff?: boolean;
|
|
12
|
+
/** viewBox width in coordinate units. Defaults to 200. */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** viewBox height in coordinate units. Defaults to 140. */
|
|
15
|
+
height?: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Stage-to-stage conversion, drawn as narrowing bands. Pure SVG from
|
|
19
|
+
* `chartGeometry`, matching the Angular `PaulFunnelChart`.
|
|
20
|
+
*
|
|
21
|
+
* The drop-off is what a funnel is read for, so it is rendered as text rather
|
|
22
|
+
* than left to the taper — the widths carry the shape, the labels carry the
|
|
23
|
+
* number.
|
|
24
|
+
*/
|
|
25
|
+
export declare function FunnelChart({ data, label, showDropOff, width, height, className, ...props }: FunnelChartProps): import("react").JSX.Element;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './cx';
|
|
3
|
+
import { funnelStages } from './chartGeometry';
|
|
4
|
+
/** Stage colour comes from the SEQUENTIAL ramp: funnel stages are ordered. */
|
|
5
|
+
const stageColor = (i, total) => {
|
|
6
|
+
const step = total <= 1 ? 3 : Math.round(1 + (i / (total - 1)) * 3);
|
|
7
|
+
return `var(--paul-chart-seq-${step + 1})`;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Stage-to-stage conversion, drawn as narrowing bands. Pure SVG from
|
|
11
|
+
* `chartGeometry`, matching the Angular `PaulFunnelChart`.
|
|
12
|
+
*
|
|
13
|
+
* The drop-off is what a funnel is read for, so it is rendered as text rather
|
|
14
|
+
* than left to the taper — the widths carry the shape, the labels carry the
|
|
15
|
+
* number.
|
|
16
|
+
*/
|
|
17
|
+
export function FunnelChart({ data, label, showDropOff = true, width = 200, height = 140, className, ...props }) {
|
|
18
|
+
const stages = funnelStages(data.map((d) => d.value), { width, height, padding: 2 });
|
|
19
|
+
const summary = data
|
|
20
|
+
.map((d, i) => `${d.label} ${d.value} (${stages[i]?.percent ?? 0}%)`)
|
|
21
|
+
.join(', ');
|
|
22
|
+
const name = data.length > 0 ? `${label}: ${summary}` : label;
|
|
23
|
+
return (_jsxs("div", { className: cx('paul-chart', 'paul-chart--funnel', className), ...props, children: [_jsx("div", { role: "img", "aria-label": name, className: "paul-chart__figure", children: data.length > 0 ? (_jsx("svg", { className: "paul-chart__svg", viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "none", "aria-hidden": "true", focusable: "false", children: stages.map((stage) => (_jsx("path", { className: "paul-chart__stage", d: stage.path, fill: stageColor(stage.index, stages.length) }, stage.index))) })) : (_jsx("span", { className: "paul-chart__empty", "aria-hidden": "true", children: "No data" })) }), data.length > 0 && (_jsx("ol", { className: "paul-chart__legend", children: data.map((d, i) => (_jsxs("li", { className: "paul-chart__legend-item", children: [_jsx("span", { className: "paul-chart__swatch", "aria-hidden": "true", style: { backgroundColor: stageColor(i, data.length) } }), _jsx("span", { className: "paul-chart__legend-label", children: d.label }), _jsx("span", { className: "paul-chart__legend-value", children: d.value }), showDropOff && i > 0 && (_jsxs("span", { className: "paul-chart__delta", children: ["\u2212", stages[i]?.dropOff ?? 0, "%"] }))] }, d.label))) }))] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type GaugeTone = 'default' | 'good' | 'warning' | 'critical';
|
|
3
|
+
type GaugeChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
4
|
+
/** The reading. Clamped into [min, max] for the arc; shown verbatim as text. */
|
|
5
|
+
value: number;
|
|
6
|
+
/** Accessible name for the chart. Required. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Bottom of the range. Defaults to 0. */
|
|
9
|
+
min?: number;
|
|
10
|
+
/** Top of the range. Defaults to 100. */
|
|
11
|
+
max?: number;
|
|
12
|
+
/** Suffix beside the hero number, e.g. "%" or "GB". */
|
|
13
|
+
unit?: string;
|
|
14
|
+
/** Outer diameter, in coordinate units. Defaults to 120. */
|
|
15
|
+
size?: number;
|
|
16
|
+
/** Arc thickness, in coordinate units. Defaults to 20. */
|
|
17
|
+
thickness?: number;
|
|
18
|
+
/** Total sweep in degrees. Defaults to 270 — a dial, not a full ring. */
|
|
19
|
+
sweep?: number;
|
|
20
|
+
/** Status colour for the fill. Defaults to the neutral series colour. */
|
|
21
|
+
tone?: GaugeTone;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* One ratio against a limit, drawn as a radial dial. Pure SVG from
|
|
25
|
+
* `chartGeometry`, matching the Angular `PaulGaugeChart`.
|
|
26
|
+
*
|
|
27
|
+
* The number is the point, so it is a hero figure inside the arc rather than a
|
|
28
|
+
* label hanging off it — the arc gives the reading a position in its range, the
|
|
29
|
+
* text gives it precision. Text wears the text tokens and never the series
|
|
30
|
+
* colour; the track is recessive and the fill carries the tone. No legend: a
|
|
31
|
+
* single value has nothing to key.
|
|
32
|
+
*
|
|
33
|
+
* When `tone` is anything but `default` the tone name is rendered as text next
|
|
34
|
+
* to the value, because status must never be carried by colour alone.
|
|
35
|
+
*/
|
|
36
|
+
export declare function GaugeChart({ value, label, min, max, unit, size, thickness, sweep, tone, className, ...props }: GaugeChartProps): import("react").JSX.Element;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './cx';
|
|
3
|
+
import { gaugeArc } from './chartGeometry';
|
|
4
|
+
/**
|
|
5
|
+
* Status is never colour alone: a tone other than `default` also renders its
|
|
6
|
+
* name as visible text beside the value, so the reading survives greyscale,
|
|
7
|
+
* a colour-vision deficiency, and a printout.
|
|
8
|
+
*/
|
|
9
|
+
const TONE_LABEL = {
|
|
10
|
+
default: '',
|
|
11
|
+
good: 'Good',
|
|
12
|
+
warning: 'Warning',
|
|
13
|
+
critical: 'Critical',
|
|
14
|
+
};
|
|
15
|
+
/** Tones borrow the SEMANTIC status colours, not the categorical series ramp. */
|
|
16
|
+
const TONE_COLOR = {
|
|
17
|
+
default: 'var(--paul-chart-1)',
|
|
18
|
+
good: 'var(--paul-color-success-600)',
|
|
19
|
+
warning: 'var(--paul-color-warning-600)',
|
|
20
|
+
critical: 'var(--paul-color-error-600)',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* One ratio against a limit, drawn as a radial dial. Pure SVG from
|
|
24
|
+
* `chartGeometry`, matching the Angular `PaulGaugeChart`.
|
|
25
|
+
*
|
|
26
|
+
* The number is the point, so it is a hero figure inside the arc rather than a
|
|
27
|
+
* label hanging off it — the arc gives the reading a position in its range, the
|
|
28
|
+
* text gives it precision. Text wears the text tokens and never the series
|
|
29
|
+
* colour; the track is recessive and the fill carries the tone. No legend: a
|
|
30
|
+
* single value has nothing to key.
|
|
31
|
+
*
|
|
32
|
+
* When `tone` is anything but `default` the tone name is rendered as text next
|
|
33
|
+
* to the value, because status must never be carried by colour alone.
|
|
34
|
+
*/
|
|
35
|
+
export function GaugeChart({ value, label, min = 0, max = 100, unit, size = 120, thickness = 20, sweep = 270, tone = 'default', className, ...props }) {
|
|
36
|
+
const geo = gaugeArc(value, { size, thickness, min, max, sweep });
|
|
37
|
+
const toneLabel = TONE_LABEL[tone];
|
|
38
|
+
// Name the whole range, not just its top: a gauge running 20–60 that says
|
|
39
|
+
// "of 60" invites the reader to compute 41.5/60 when the arc means
|
|
40
|
+
// (41.5-20)/(60-20). Only mention `min` when it isn't the assumed zero.
|
|
41
|
+
const range = min === 0 ? `${max}` : `${min} to ${max}`;
|
|
42
|
+
const name = `${label}: ${value} of ${range} (${geo.percent}%)${toneLabel ? `, ${toneLabel}` : ''}`;
|
|
43
|
+
return (_jsxs("div", { className: cx('paul-chart', 'paul-chart--gauge', className), ...props, children: [_jsx("div", { role: "img", "aria-label": name, className: "paul-chart__figure", children: _jsxs("svg", { className: "paul-chart__svg", viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", focusable: "false", children: [_jsx("path", { className: "paul-chart__gauge-track", d: geo.track }), geo.fill !== '' && (_jsx("path", { className: "paul-chart__gauge-fill", d: geo.fill, fill: TONE_COLOR[tone] }))] }) }), _jsxs("div", { className: "paul-chart__readout", children: [_jsxs("p", { className: "paul-chart__value", children: [value, unit && _jsx("span", { className: "paul-chart__unit", children: unit })] }), toneLabel && _jsx("p", { className: "paul-chart__tone", children: toneLabel }), _jsxs("p", { className: "paul-chart__caption", children: ["of ", range] })] })] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
type GradientBackgroundProps = HTMLAttributes<HTMLDivElement> & {
|
|
3
|
+
/** Gradient stops, in order. Defaults to the design-token brand palette. */
|
|
4
|
+
colors?: string[];
|
|
5
|
+
/** Gradient angle in degrees. */
|
|
6
|
+
angle?: number;
|
|
7
|
+
/** Ambient flow speed. */
|
|
8
|
+
speed?: 'slow' | 'normal' | 'fast';
|
|
9
|
+
/** Run the ambient flow animation. Defaults to true. */
|
|
10
|
+
animate?: boolean;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A decorative surface painted with a flowing multi-stop gradient. Content is
|
|
15
|
+
* passed as children and renders on top, so the strip stays content-agnostic.
|
|
16
|
+
* The ambient flow is pure CSS and is gated behind prefers-reduced-motion, so
|
|
17
|
+
* it goes static for users who ask for reduced motion — no JS needed. When
|
|
18
|
+
* `colors` is omitted it falls back to the token brand palette.
|
|
19
|
+
*/
|
|
20
|
+
export declare function GradientBackground({ colors, angle, speed, animate, className, style, children, ...rest }: GradientBackgroundProps): import("react").JSX.Element;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './cx';
|
|
3
|
+
const DURATION = {
|
|
4
|
+
slow: '18s',
|
|
5
|
+
normal: '12s',
|
|
6
|
+
fast: '6s',
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A decorative surface painted with a flowing multi-stop gradient. Content is
|
|
10
|
+
* passed as children and renders on top, so the strip stays content-agnostic.
|
|
11
|
+
* The ambient flow is pure CSS and is gated behind prefers-reduced-motion, so
|
|
12
|
+
* it goes static for users who ask for reduced motion — no JS needed. When
|
|
13
|
+
* `colors` is omitted it falls back to the token brand palette.
|
|
14
|
+
*/
|
|
15
|
+
export function GradientBackground({ colors, angle = 120, speed = 'normal', animate = true, className, style, children, ...rest }) {
|
|
16
|
+
const gradientStyle = {
|
|
17
|
+
...(colors && colors.length > 0
|
|
18
|
+
? { ['--paul-gradient-image']: `linear-gradient(${angle}deg, ${colors.join(', ')})` }
|
|
19
|
+
: {}),
|
|
20
|
+
['--paul-gradient-duration']: DURATION[speed],
|
|
21
|
+
...style,
|
|
22
|
+
};
|
|
23
|
+
return (_jsx("div", { className: cx('gradient-bg', className), "data-animate": animate ? 'true' : undefined, style: gradientStyle, ...rest, children: children }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type HeatmapRow = {
|
|
3
|
+
/** Rendered down the left gutter. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** One value per column. */
|
|
6
|
+
values: number[];
|
|
7
|
+
};
|
|
8
|
+
type HeatmapChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
9
|
+
/**
|
|
10
|
+
* One entry per row, each carrying its own label.
|
|
11
|
+
*
|
|
12
|
+
* A row and its label travel together on purpose: as separate `matrix` and
|
|
13
|
+
* `rowLabels` arrays a length mismatch is representable, and it degrades
|
|
14
|
+
* quietly — the label for the missing row just vanishes and every other row
|
|
15
|
+
* still looks right.
|
|
16
|
+
*/
|
|
17
|
+
rows: HeatmapRow[];
|
|
18
|
+
/** One label per column, rendered across the top gutter. */
|
|
19
|
+
colLabels: string[];
|
|
20
|
+
/** Accessible name for the chart. Required. */
|
|
21
|
+
label: string;
|
|
22
|
+
/**
|
|
23
|
+
* Print each cell's value inside the cell. Defaults to true.
|
|
24
|
+
*
|
|
25
|
+
* Direct labelling is only honest on a small grid — past roughly 8×8 the text
|
|
26
|
+
* is smaller than the cell can carry and the numbers turn into texture. Turn
|
|
27
|
+
* it off for large cohorts and let the ramp plus the scale legend do the work.
|
|
28
|
+
*/
|
|
29
|
+
showValues?: boolean;
|
|
30
|
+
/** viewBox width in coordinate units. Defaults to 220. */
|
|
31
|
+
width?: number;
|
|
32
|
+
/** viewBox height in coordinate units. Defaults to 140. */
|
|
33
|
+
height?: number;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A cohort-retention grid: one cell per matrix value, shaded by magnitude. Pure
|
|
37
|
+
* SVG from `chartGeometry`, matching the Angular `PaulHeatmapChart`.
|
|
38
|
+
*
|
|
39
|
+
* The scale legend is not optional. A sequential ramp with no key tells a reader
|
|
40
|
+
* that one cell is bigger than another but never by how much, so the five steps
|
|
41
|
+
* are rendered with the matrix min and max as end labels. The `role="img"`
|
|
42
|
+
* summary names every row and its values, so the grid is readable with the
|
|
43
|
+
* colour thrown away entirely.
|
|
44
|
+
*/
|
|
45
|
+
export declare function HeatmapChart({ rows, colLabels, label, showValues, width, height, className, ...props }: HeatmapChartProps): import("react").JSX.Element;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './cx';
|
|
3
|
+
import { heatmapCells } from './chartGeometry';
|
|
4
|
+
/** Gutter reserved for the row labels, in coordinate units. */
|
|
5
|
+
const ROW_GUTTER = 40;
|
|
6
|
+
/** Gutter reserved for the column labels, in coordinate units. */
|
|
7
|
+
const COL_GUTTER = 14;
|
|
8
|
+
/**
|
|
9
|
+
* Cell colour comes from the SEQUENTIAL ramp: a heatmap encodes MAGNITUDE, so
|
|
10
|
+
* intensity maps onto one hue light-to-dark. The categorical --paul-chart-N
|
|
11
|
+
* palette would encode the value as identity, which is a different claim.
|
|
12
|
+
*/
|
|
13
|
+
const cellColor = (intensity) => `var(--paul-chart-seq-${Math.round(intensity * 4) + 1})`;
|
|
14
|
+
/** The top two ramp steps are dark enough that ink-on-cell has to flip. */
|
|
15
|
+
const valueColor = (intensity) => Math.round(intensity * 4) + 1 >= 4 ? 'var(--paul-color-surface)' : 'var(--paul-color-foreground)';
|
|
16
|
+
/**
|
|
17
|
+
* A cohort-retention grid: one cell per matrix value, shaded by magnitude. Pure
|
|
18
|
+
* SVG from `chartGeometry`, matching the Angular `PaulHeatmapChart`.
|
|
19
|
+
*
|
|
20
|
+
* The scale legend is not optional. A sequential ramp with no key tells a reader
|
|
21
|
+
* that one cell is bigger than another but never by how much, so the five steps
|
|
22
|
+
* are rendered with the matrix min and max as end labels. The `role="img"`
|
|
23
|
+
* summary names every row and its values, so the grid is readable with the
|
|
24
|
+
* colour thrown away entirely.
|
|
25
|
+
*/
|
|
26
|
+
export function HeatmapChart({ rows, colLabels, label, showValues = true, width = 220, height = 140, className, ...props }) {
|
|
27
|
+
const matrix = rows.map((row) => row.values);
|
|
28
|
+
const cells = heatmapCells(matrix, {
|
|
29
|
+
width: width - ROW_GUTTER,
|
|
30
|
+
height: height - COL_GUTTER,
|
|
31
|
+
});
|
|
32
|
+
const values = matrix.flat();
|
|
33
|
+
const min = values.length > 0 ? Math.min(...values) : 0;
|
|
34
|
+
const max = values.length > 0 ? Math.max(...values) : 0;
|
|
35
|
+
const summary = rows.map((row) => `${row.label} ${row.values.join(', ')}`).join('; ');
|
|
36
|
+
const name = cells.length > 0 ? `${label}: ${summary}` : label;
|
|
37
|
+
// Column and row label positions ride on the cells themselves, so the text
|
|
38
|
+
// never drifts out of alignment with the grid it annotates.
|
|
39
|
+
const firstInCol = (c) => cells.find((cell) => cell.col === c);
|
|
40
|
+
const firstInRow = (r) => cells.find((cell) => cell.row === r);
|
|
41
|
+
return (_jsxs("div", { className: cx('paul-chart', 'paul-chart--heatmap', className), ...props, children: [_jsx("div", { role: "img", "aria-label": name, className: "paul-chart__figure", children: cells.length > 0 ? (_jsxs("svg", { className: "paul-chart__svg", viewBox: `0 0 ${width} ${height}`, "aria-hidden": "true", focusable: "false", children: [colLabels.map((text, c) => {
|
|
42
|
+
const cell = firstInCol(c);
|
|
43
|
+
if (!cell)
|
|
44
|
+
return null;
|
|
45
|
+
return (_jsx("text", { className: "paul-chart__col-label", x: ROW_GUTTER + cell.x + cell.width / 2, y: COL_GUTTER - 5, textAnchor: "middle", children: text }, `col-${text}`));
|
|
46
|
+
}), rows.map((row, r) => {
|
|
47
|
+
const cell = firstInRow(r);
|
|
48
|
+
if (!cell)
|
|
49
|
+
return null;
|
|
50
|
+
return (_jsx("text", { className: "paul-chart__row-label", x: ROW_GUTTER - 5, y: COL_GUTTER + cell.y + cell.height / 2, textAnchor: "end", dominantBaseline: "middle", children: row.label }, `row-${row.label}`));
|
|
51
|
+
}), _jsxs("g", { transform: `translate(${ROW_GUTTER} ${COL_GUTTER})`, children: [cells.map((cell) => (_jsx("rect", { className: "paul-chart__cell", x: cell.x, y: cell.y, width: cell.width, height: cell.height, fill: cellColor(cell.intensity) }, `${cell.row}-${cell.col}`))), showValues &&
|
|
52
|
+
cells.map((cell) => (_jsx("text", { className: "paul-chart__cell-value", x: cell.x + cell.width / 2, y: cell.y + cell.height / 2, textAnchor: "middle", dominantBaseline: "middle", fill: valueColor(cell.intensity), children: cell.value }, `v-${cell.row}-${cell.col}`)))] })] })) : (_jsx("span", { className: "paul-chart__empty", "aria-hidden": "true", children: "No data" })) }), cells.length > 0 && (_jsxs("div", { className: "paul-chart__scale", children: [_jsx("span", { className: "paul-chart__scale-end", children: min }), [0, 1, 2, 3, 4].map((step) => (_jsx("span", { className: "paul-chart__scale-step", "aria-hidden": "true", style: { backgroundColor: `var(--paul-chart-seq-${step + 1})` } }, step))), _jsx("span", { className: "paul-chart__scale-end", children: max })] }))] }));
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type ParetoDatum = {
|
|
3
|
+
label: string;
|
|
4
|
+
value: number;
|
|
5
|
+
};
|
|
6
|
+
type ParetoChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
7
|
+
data: ParetoDatum[];
|
|
8
|
+
/** Accessible name for the chart. Required. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Cumulative percent to draw the reference line at. Defaults to 80. */
|
|
11
|
+
threshold?: number;
|
|
12
|
+
/** viewBox width in coordinate units. Defaults to 200. */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** viewBox height in coordinate units. Defaults to 140. */
|
|
15
|
+
height?: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Sorted bars plus a cumulative line, on ONE y-axis.
|
|
19
|
+
*
|
|
20
|
+
* The textbook Pareto puts raw counts on the left axis and cumulative percent
|
|
21
|
+
* on the right. Two y-scales on one plot invent a relationship the data doesn't
|
|
22
|
+
* have — where the line appears to cross the bars is a function of how the two
|
|
23
|
+
* scales were aligned, and that alignment is arbitrary. So there is one axis
|
|
24
|
+
* here: the bars are percent-of-total and the line is cumulative percent, both
|
|
25
|
+
* on 0–100, and the crossing point means something. Nothing is labelled with a
|
|
26
|
+
* raw count as if it had a scale of its own.
|
|
27
|
+
*
|
|
28
|
+
* One series, one colour. Ramping the bars by value would encode magnitude
|
|
29
|
+
* twice — the height already says it, and the categories are nominal.
|
|
30
|
+
*
|
|
31
|
+
* Pure SVG from `chartGeometry`, matching the Angular `PaulParetoChart`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function ParetoChart({ data, label, threshold, width, height, className, ...props }: ParetoChartProps): import("react").JSX.Element;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './cx';
|
|
3
|
+
import { paretoLayout } from './chartGeometry';
|
|
4
|
+
/** Height reserved under the plot for the category labels. */
|
|
5
|
+
const LABEL_BAND = 16;
|
|
6
|
+
const PADDING = 2;
|
|
7
|
+
/**
|
|
8
|
+
* Sorted bars plus a cumulative line, on ONE y-axis.
|
|
9
|
+
*
|
|
10
|
+
* The textbook Pareto puts raw counts on the left axis and cumulative percent
|
|
11
|
+
* on the right. Two y-scales on one plot invent a relationship the data doesn't
|
|
12
|
+
* have — where the line appears to cross the bars is a function of how the two
|
|
13
|
+
* scales were aligned, and that alignment is arbitrary. So there is one axis
|
|
14
|
+
* here: the bars are percent-of-total and the line is cumulative percent, both
|
|
15
|
+
* on 0–100, and the crossing point means something. Nothing is labelled with a
|
|
16
|
+
* raw count as if it had a scale of its own.
|
|
17
|
+
*
|
|
18
|
+
* One series, one colour. Ramping the bars by value would encode magnitude
|
|
19
|
+
* twice — the height already says it, and the categories are nominal.
|
|
20
|
+
*
|
|
21
|
+
* Pure SVG from `chartGeometry`, matching the Angular `PaulParetoChart`.
|
|
22
|
+
*/
|
|
23
|
+
export function ParetoChart({ data, label, threshold = 80, width = 200, height = 140, className, ...props }) {
|
|
24
|
+
// `paretoLayout` filters out non-positive values and sorts descending, so the
|
|
25
|
+
// incoming order is NOT the bar order. Pair labels to values and put them
|
|
26
|
+
// through the identical filter + sort, or every bar gets the wrong name.
|
|
27
|
+
// `Array.prototype.sort` is stable, so ties keep their original order in both.
|
|
28
|
+
const ranked = data.filter((d) => d.value > 0).sort((a, b) => b.value - a.value);
|
|
29
|
+
const plotHeight = Math.max(0, height - LABEL_BAND);
|
|
30
|
+
const box = { width, height: plotHeight, padding: PADDING };
|
|
31
|
+
// The geometry takes the threshold too, so the cut it reports and the rule
|
|
32
|
+
// drawn below are the same number by construction.
|
|
33
|
+
const { bars, cumulative, percents, cutIndex: cut } = paretoLayout(data.map((d) => d.value), box, { threshold });
|
|
34
|
+
// Same mapping the geometry uses for the cumulative points, so the reference
|
|
35
|
+
// line and the line it references are on one scale by construction.
|
|
36
|
+
const inner = { bottom: plotHeight - PADDING, height: Math.max(0, plotHeight - PADDING * 2) };
|
|
37
|
+
const thresholdY = Math.round((inner.bottom - threshold * (inner.height / 100)) * 1000) / 1000;
|
|
38
|
+
const summary = ranked.map((d, i) => `${d.label} ${percents[i] ?? 0}%`).join(', ');
|
|
39
|
+
const crossing = cut >= 0
|
|
40
|
+
? `${threshold}% of the total is reached by the first ${cut + 1} of ${ranked.length}.`
|
|
41
|
+
: `The cumulative total never reaches ${threshold}%.`;
|
|
42
|
+
const name = ranked.length > 0 ? `${label}: ${summary}. ${crossing}` : label;
|
|
43
|
+
return (_jsx("div", { className: cx('paul-chart', 'paul-chart--pareto', className), ...props, children: _jsx("div", { role: "img", "aria-label": name, className: "paul-chart__figure", children: ranked.length > 0 ? (_jsxs("svg", { className: "paul-chart__svg", viewBox: `0 0 ${width} ${height}`, "aria-hidden": "true", focusable: "false", children: [bars.map((bar, i) => (_jsx("rect", { className: "paul-chart__bar", x: bar.x, y: bar.y, width: bar.width, height: bar.height, rx: 1, fill: "var(--paul-chart-1)" }, ranked[i]?.label ?? i))), _jsx("line", { className: "paul-chart__threshold", x1: PADDING, x2: width - PADDING, y1: thresholdY, y2: thresholdY, stroke: "var(--paul-color-border)", strokeDasharray: "4 3" }), _jsx("polyline", { className: "paul-chart__cumulative", points: cumulative.map((p) => `${p.x},${p.y}`).join(' '), fill: "none", stroke: "var(--paul-chart-4)", strokeWidth: 2 }), cut >= 0 && cumulative[cut] && (_jsx("circle", { className: "paul-chart__cut", cx: cumulative[cut].x, cy: cumulative[cut].y, r: 3, fill: "var(--paul-chart-4)" })), bars.map((bar, i) => (_jsx("text", { className: "paul-chart__category", x: bar.x + bar.width / 2, y: height - 5, textAnchor: "middle", children: ranked[i]?.label ?? '' }, `label-${ranked[i]?.label ?? i}`)))] })) : (_jsx("span", { className: "paul-chart__empty", "aria-hidden": "true", children: "No data" })) }) }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
export type RadarSeries = {
|
|
3
|
+
label: string;
|
|
4
|
+
/** One value per axis, in axis order. Missing entries read as 0. */
|
|
5
|
+
values: number[];
|
|
6
|
+
};
|
|
7
|
+
type RadarChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
8
|
+
data: RadarSeries[];
|
|
9
|
+
/** Axis names, in order. The polygon has one vertex per entry. */
|
|
10
|
+
axes: string[];
|
|
11
|
+
/** Accessible name for the chart. Required. */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Shared ceiling for every series. Defaults to the max across all series. */
|
|
14
|
+
max?: number;
|
|
15
|
+
/** Width and height of the square viewBox, in coordinate units. Defaults to 160. */
|
|
16
|
+
size?: number;
|
|
17
|
+
/** Show the series legend. Only consulted when there are ≥2 series. Defaults to true. */
|
|
18
|
+
showLegend?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Several measures compared on one frame — the shape behind skill profiles and
|
|
22
|
+
* capability scorecards. Pure SVG from `chartGeometry`, matching the Angular
|
|
23
|
+
* `PaulRadarChart`.
|
|
24
|
+
*
|
|
25
|
+
* Two rules make the picture honest. Every series is scaled against ONE ceiling
|
|
26
|
+
* (`max`, defaulting to the max across all series), so the polygons are
|
|
27
|
+
* comparable rather than each filling the frame; and only the first three
|
|
28
|
+
* series are drawn — anything past that is dropped, because overlapping
|
|
29
|
+
* translucent polygons stop resolving into distinct shapes.
|
|
30
|
+
*
|
|
31
|
+
* The frame — spokes and rings — is recessive ink, never a series colour, so
|
|
32
|
+
* the data reads on top of it instead of competing with it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function RadarChart({ data, axes, label, max, size, showLegend, className, ...props }: RadarChartProps): import("react").JSX.Element;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './cx';
|
|
3
|
+
import { radarAxes, radarPolygon } from './chartGeometry';
|
|
4
|
+
/** At most three series — a fourth overlapping polygon stops being readable. */
|
|
5
|
+
const MAX_SERIES = 3;
|
|
6
|
+
function warnTruncated(count, label) {
|
|
7
|
+
// Declared locally rather than pulling @types/node into a browser package.
|
|
8
|
+
if (typeof process !== 'undefined' && process?.env?.NODE_ENV === 'production')
|
|
9
|
+
return;
|
|
10
|
+
// eslint-disable-next-line no-console
|
|
11
|
+
console.warn(`RadarChart ("${label}"): ${count} series given, ${MAX_SERIES} drawn. ` +
|
|
12
|
+
'Overlapping polygons stop being readable past three — facet into small ' +
|
|
13
|
+
'multiples, or fold the tail into one series, rather than relying on this cap.');
|
|
14
|
+
}
|
|
15
|
+
/** Series colour comes from the CATEGORICAL palette: series are identities. */
|
|
16
|
+
const seriesColor = (i) => `var(--paul-chart-${i + 1})`;
|
|
17
|
+
/**
|
|
18
|
+
* Several measures compared on one frame — the shape behind skill profiles and
|
|
19
|
+
* capability scorecards. Pure SVG from `chartGeometry`, matching the Angular
|
|
20
|
+
* `PaulRadarChart`.
|
|
21
|
+
*
|
|
22
|
+
* Two rules make the picture honest. Every series is scaled against ONE ceiling
|
|
23
|
+
* (`max`, defaulting to the max across all series), so the polygons are
|
|
24
|
+
* comparable rather than each filling the frame; and only the first three
|
|
25
|
+
* series are drawn — anything past that is dropped, because overlapping
|
|
26
|
+
* translucent polygons stop resolving into distinct shapes.
|
|
27
|
+
*
|
|
28
|
+
* The frame — spokes and rings — is recessive ink, never a series colour, so
|
|
29
|
+
* the data reads on top of it instead of competing with it.
|
|
30
|
+
*/
|
|
31
|
+
export function RadarChart({ data, axes, label, max, size = 160, showLegend = true, className, ...props }) {
|
|
32
|
+
const series = data.slice(0, MAX_SERIES);
|
|
33
|
+
if (data.length > MAX_SERIES)
|
|
34
|
+
warnTruncated(data.length, label);
|
|
35
|
+
const box = { width: size, height: size, padding: 18 };
|
|
36
|
+
const empty = series.length === 0 || axes.length === 0;
|
|
37
|
+
const geo = radarAxes(Math.max(axes.length, 1), box);
|
|
38
|
+
/** Pad or trim each series to the axis count so the polygon always closes. */
|
|
39
|
+
const valuesFor = (s) => axes.map((_, i) => s.values[i] ?? 0);
|
|
40
|
+
const ceiling = max ?? Math.max(0, ...series.flatMap((s) => valuesFor(s)));
|
|
41
|
+
const polygons = series.map((s, i) => ({
|
|
42
|
+
label: s.label,
|
|
43
|
+
color: seriesColor(i),
|
|
44
|
+
points: radarPolygon(valuesFor(s), box, ceiling)
|
|
45
|
+
.map((p) => `${p.x},${p.y}`)
|
|
46
|
+
.join(' '),
|
|
47
|
+
}));
|
|
48
|
+
const summary = series
|
|
49
|
+
.map((s) => `${s.label} ${axes.map((a, i) => `${a} ${s.values[i] ?? 0}`).join(', ')}`)
|
|
50
|
+
.join('; ');
|
|
51
|
+
const name = empty ? label : `${label}: ${summary}`;
|
|
52
|
+
return (_jsxs("div", { className: cx('paul-chart', 'paul-chart--radar', className), ...props, children: [_jsx("div", { role: "img", "aria-label": name, className: "paul-chart__figure", children: !empty ? (_jsxs("svg", { className: "paul-chart__svg", viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", focusable: "false", children: [geo.rings.map((r) => (_jsx("circle", { className: "paul-chart__ring", cx: geo.cx, cy: geo.cy, r: r }, r))), geo.axes.map((axis) => (_jsx("line", { className: "paul-chart__axis", x1: geo.cx, y1: geo.cy, x2: axis.x, y2: axis.y }, axis.index))), polygons.map((poly) => (_jsx("polygon", { className: "paul-chart__radar-area", points: poly.points, fill: poly.color, stroke: poly.color }, poly.label))), geo.axes.map((axis) => (_jsx("text", { className: "paul-chart__axis-label",
|
|
53
|
+
// Nudged past the spoke end so the name clears the outer ring.
|
|
54
|
+
x: axis.x + (axis.x - geo.cx) * 0.12, y: axis.y + (axis.y - geo.cy) * 0.12, textAnchor: axis.x > geo.cx + 1 ? 'start' : axis.x < geo.cx - 1 ? 'end' : 'middle', dominantBaseline: "middle", children: axes[axis.index] }, axis.index)))] })) : (_jsx("span", { className: "paul-chart__empty", "aria-hidden": "true", children: "No data" })) }), showLegend && series.length >= 2 && (_jsx("ul", { className: "paul-chart__legend", children: series.map((s, i) => (_jsxs("li", { className: "paul-chart__legend-item", children: [_jsx("span", { className: "paul-chart__swatch", "aria-hidden": "true", style: { backgroundColor: seriesColor(i) } }), _jsx("span", { className: "paul-chart__legend-label", children: s.label })] }, s.label))) }))] }));
|
|
55
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { type ScatterDomain } from './chartGeometry';
|
|
3
|
+
export type ScatterSeries = {
|
|
4
|
+
label: string;
|
|
5
|
+
points: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
type ScatterPlotProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
11
|
+
series: ScatterSeries[];
|
|
12
|
+
/** Accessible name for the chart. Required. */
|
|
13
|
+
label: string;
|
|
14
|
+
/**
|
|
15
|
+
* Fixes the scale. Pass it when several plots have to be read against each
|
|
16
|
+
* other — without it every plot rescales to its own extent and two charts
|
|
17
|
+
* side by side stop being comparable.
|
|
18
|
+
*/
|
|
19
|
+
domain?: ScatterDomain;
|
|
20
|
+
/** viewBox width in coordinate units. Defaults to 200. */
|
|
21
|
+
width?: number;
|
|
22
|
+
/** viewBox height in coordinate units. Defaults to 140. */
|
|
23
|
+
height?: number;
|
|
24
|
+
/** Mark radius in coordinate units. Defaults to 4 — an 8px target. */
|
|
25
|
+
radius?: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Two measures against each other, one mark per observation. Pure SVG from
|
|
29
|
+
* `chartGeometry`, matching the Angular `PaulScatterPlot`.
|
|
30
|
+
*
|
|
31
|
+
* Every mark carries a surface-coloured ring (in CSS, not inline) so points
|
|
32
|
+
* that land on top of each other still read as two points rather than one blob.
|
|
33
|
+
* The whole plot is one `role="img"`; a per-point accessibility tree would be
|
|
34
|
+
* noise, so the summary counts observations per series instead.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ScatterPlot({ series, label, domain, width, height, radius, className, ...props }: ScatterPlotProps): import("react").JSX.Element;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './cx';
|
|
3
|
+
import { scatterPoints } from './chartGeometry';
|
|
4
|
+
/** Series colour comes from the CATEGORICAL palette: series are identities. */
|
|
5
|
+
const seriesColor = (i) => `var(--paul-chart-${(i % 6) + 1})`;
|
|
6
|
+
/**
|
|
7
|
+
* Two measures against each other, one mark per observation. Pure SVG from
|
|
8
|
+
* `chartGeometry`, matching the Angular `PaulScatterPlot`.
|
|
9
|
+
*
|
|
10
|
+
* Every mark carries a surface-coloured ring (in CSS, not inline) so points
|
|
11
|
+
* that land on top of each other still read as two points rather than one blob.
|
|
12
|
+
* The whole plot is one `role="img"`; a per-point accessibility tree would be
|
|
13
|
+
* noise, so the summary counts observations per series instead.
|
|
14
|
+
*/
|
|
15
|
+
export function ScatterPlot({ series, label, domain, width = 200, height = 140, radius = 4, className, ...props }) {
|
|
16
|
+
// Inset by the radius so marks on the extremes sit inside the box.
|
|
17
|
+
const box = { width, height, padding: radius };
|
|
18
|
+
const total = series.reduce((n, s) => n + s.points.length, 0);
|
|
19
|
+
const summary = series.map((s) => `${s.label} ${s.points.length} points`).join(', ');
|
|
20
|
+
const name = total > 0 ? `${label}: ${summary}` : label;
|
|
21
|
+
return (_jsxs("div", { className: cx('paul-chart', 'paul-chart--scatter', className), ...props, children: [_jsx("div", { role: "img", "aria-label": name, className: "paul-chart__figure", children: total > 0 ? (_jsx("svg", { className: "paul-chart__svg", viewBox: `0 0 ${width} ${height}`, "aria-hidden": "true", focusable: "false", children: series.map((s, i) => (_jsx("g", { className: "paul-chart__series", children: scatterPoints(s.points, box, domain).map((point) => (_jsx("circle", { className: "paul-chart__mark", cx: point.x, cy: point.y, r: radius, fill: seriesColor(i) }, point.index))) }, s.label))) })) : (_jsx("span", { className: "paul-chart__empty", "aria-hidden": "true", children: "No data" })) }), series.length > 1 && total > 0 && (_jsx("ul", { className: "paul-chart__legend", children: series.map((s, i) => (_jsxs("li", { className: "paul-chart__legend-item", children: [_jsx("span", { className: "paul-chart__swatch", "aria-hidden": "true", style: { backgroundColor: seriesColor(i) } }), _jsx("span", { className: "paul-chart__legend-label", children: s.label })] }, s.label))) }))] }));
|
|
22
|
+
}
|