@cfasim-ui/charts 0.7.8 → 0.8.1
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/BarChart.d.ts +9 -26
- package/dist/ChartMenu/ChartMenu.d.ts +3 -2
- package/dist/ChartTooltip/ChartTooltip.d.ts +9 -12
- package/dist/ChoroplethMap/ChoroplethMap.d.ts +42 -190
- package/dist/ChoroplethMap/ChoroplethTooltip.d.ts +20 -27
- package/dist/ChoroplethMap/canvasLayer.d.ts +23 -6
- package/dist/ChoroplethMap/cityLayout.d.ts +3 -2
- package/dist/ChoroplethMap/mixedGeo.d.ts +74 -0
- package/dist/ChoroplethMap/mixedGeo.test.d.ts +1 -0
- package/dist/DataTable/DataTable.d.ts +3 -2
- package/dist/LineChart/LineChart.d.ts +9 -26
- package/dist/_shared/ChartAnnotations.d.ts +3 -2
- package/dist/_shared/ChartZoomControls.d.ts +3 -2
- package/dist/_shared/index.d.ts +2 -1
- package/dist/_shared/mapTheme.d.ts +159 -0
- package/dist/_shared/mapTheme.test.d.ts +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1821 -1462
- package/docs/BarChart.md +776 -0
- package/docs/ChoroplethMap.md +1394 -0
- package/docs/DataTable.md +386 -0
- package/docs/LineChart.md +1267 -0
- package/docs/index.json +56 -0
- package/package.json +25 -21
- package/src/BarChart/BarChart.md +743 -0
- package/src/BarChart/BarChart.vue +1901 -0
- package/src/ChartMenu/ChartMenu.vue +220 -0
- package/src/ChartMenu/download.ts +120 -0
- package/src/ChartTooltip/ChartTooltip.vue +97 -0
- package/src/ChoroplethMap/ChoroplethMap.md +1354 -0
- package/src/ChoroplethMap/ChoroplethMap.vue +3778 -0
- package/src/ChoroplethMap/ChoroplethTooltip.vue +103 -0
- package/src/ChoroplethMap/canvasLayer.ts +373 -0
- package/src/ChoroplethMap/cityLayout.ts +262 -0
- package/src/ChoroplethMap/hsaMapping.ts +4116 -0
- package/src/ChoroplethMap/mixedGeo.ts +201 -0
- package/src/DataTable/DataTable.md +372 -0
- package/src/DataTable/DataTable.vue +406 -0
- package/src/LineChart/LineChart.md +1225 -0
- package/src/LineChart/LineChart.vue +1555 -0
- package/src/_shared/ChartAnnotations.vue +420 -0
- package/src/_shared/ChartZoomControls.vue +138 -0
- package/src/_shared/annotations.ts +106 -0
- package/src/_shared/axes.ts +69 -0
- package/src/_shared/chartProps.ts +201 -0
- package/src/_shared/computeTicks.ts +42 -0
- package/src/_shared/contrast.ts +100 -0
- package/src/_shared/dateAxis.ts +501 -0
- package/src/_shared/index.ts +78 -0
- package/src/_shared/mapTheme.ts +551 -0
- package/src/_shared/scale.ts +86 -0
- package/src/_shared/seriesCsv.ts +68 -0
- package/src/_shared/touch.ts +8 -0
- package/src/_shared/useChartFoundation.ts +175 -0
- package/src/_shared/useChartFullscreen.ts +254 -0
- package/src/_shared/useChartMenu.ts +111 -0
- package/src/_shared/useChartPadding.ts +235 -0
- package/src/_shared/useChartSize.ts +58 -0
- package/src/_shared/useChartTooltip.ts +205 -0
- package/src/env.d.ts +4 -0
- package/src/hsa-mapping.ts +1 -0
- package/src/index.ts +41 -0
- package/src/tooltip-position.ts +55 -0
- package/src/us-cities/data.ts +1371 -0
- package/src/us-cities/index.ts +122 -0
- package/src/us-cities.ts +7 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers shared by chart components for axis tick math and value
|
|
3
|
+
* formatting. No Vue reactivity here; see `useAxisTicks` for the
|
|
4
|
+
* reactive wrapper.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Round to nearest half-pixel so 1px SVG strokes stay sharp. */
|
|
8
|
+
export function snap(v: number): number {
|
|
9
|
+
return Math.round(v) + 0.5;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function niceStep(range: number, targetTicks: number): number {
|
|
13
|
+
const rough = range / targetTicks;
|
|
14
|
+
const mag = Math.pow(10, Math.floor(Math.log10(rough)));
|
|
15
|
+
const norm = rough / mag;
|
|
16
|
+
let step: number;
|
|
17
|
+
if (norm <= 1.5) step = 1;
|
|
18
|
+
else if (norm <= 3) step = 2;
|
|
19
|
+
else if (norm <= 7) step = 5;
|
|
20
|
+
else step = 10;
|
|
21
|
+
return step * mag;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Generate interval-spaced values in [min, max], inclusive. */
|
|
25
|
+
export function intervalValues(
|
|
26
|
+
min: number,
|
|
27
|
+
max: number,
|
|
28
|
+
step: number,
|
|
29
|
+
): number[] {
|
|
30
|
+
if (!(step > 0) || !isFinite(step)) return [];
|
|
31
|
+
const out: number[] = [];
|
|
32
|
+
const start = Math.ceil(min / step) * step;
|
|
33
|
+
// Cap iteration to avoid runaway loops from pathological inputs.
|
|
34
|
+
const maxIterations = 1000;
|
|
35
|
+
for (
|
|
36
|
+
let i = 0, v = start;
|
|
37
|
+
v <= max + 1e-9 && i < maxIterations;
|
|
38
|
+
i++, v = start + i * step
|
|
39
|
+
) {
|
|
40
|
+
out.push(v);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const numFmt = new Intl.NumberFormat();
|
|
46
|
+
|
|
47
|
+
export function formatTick(v: number): string {
|
|
48
|
+
if (Math.abs(v) >= 1000) return numFmt.format(v);
|
|
49
|
+
if (Number.isInteger(v)) return v.toString();
|
|
50
|
+
return v.toFixed(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Numeric input accepted by chart components. `number[]` and any standard
|
|
55
|
+
* numeric typed array are supported, so the output of
|
|
56
|
+
* `ModelOutput.column('x')` (e.g. a `Float64Array`) can be passed
|
|
57
|
+
* directly without copying.
|
|
58
|
+
*/
|
|
59
|
+
export type ChartData =
|
|
60
|
+
| readonly number[]
|
|
61
|
+
| Float64Array
|
|
62
|
+
| Float32Array
|
|
63
|
+
| Int32Array
|
|
64
|
+
| Uint32Array
|
|
65
|
+
| Int16Array
|
|
66
|
+
| Uint16Array
|
|
67
|
+
| Int8Array
|
|
68
|
+
| Uint8Array
|
|
69
|
+
| Uint8ClampedArray;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prop / slot / emit types shared by LineChart and BarChart. Component
|
|
3
|
+
* authors compose these with chart-specific props via TypeScript
|
|
4
|
+
* intersection (e.g. `defineProps<ChartCommonProps & MyExtraProps>()`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NumberFormat } from "@cfasim-ui/shared";
|
|
8
|
+
import type { ChartAnnotation } from "./annotations.js";
|
|
9
|
+
import type { ChartPadding } from "./useChartPadding.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Visual styling for the chart title. All fields optional; unspecified
|
|
13
|
+
* fields fall back to the chart's defaults (font-size 14, line-height 18,
|
|
14
|
+
* font-weight 600, currentColor, align "left"). `lineHeight` is in pixels
|
|
15
|
+
* and controls spacing between lines when the title contains `\n`.
|
|
16
|
+
*/
|
|
17
|
+
export interface TitleStyle {
|
|
18
|
+
fontSize?: number;
|
|
19
|
+
lineHeight?: number;
|
|
20
|
+
color?: string;
|
|
21
|
+
fontWeight?: number | string;
|
|
22
|
+
/** Horizontal alignment of the title. Default: `"left"`. */
|
|
23
|
+
align?: "left" | "center" | "right";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Visual styling for axis labels, tick labels, and legend text. Each
|
|
28
|
+
* field is optional; unspecified fields fall back to that element's
|
|
29
|
+
* default. When `color` is set, the default fill-opacity (used by tick
|
|
30
|
+
* labels) is dropped so the exact color is rendered.
|
|
31
|
+
*/
|
|
32
|
+
export interface LabelStyle {
|
|
33
|
+
fontSize?: number;
|
|
34
|
+
color?: string;
|
|
35
|
+
fontWeight?: number | string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* CSS `mix-blend-mode` values usable on chart series. Drives how a
|
|
40
|
+
* series' pixels combine with what's painted behind them. Useful for
|
|
41
|
+
* overlapping series (e.g. `"multiply"` darkens light fills where bars
|
|
42
|
+
* overlap; `"screen"` lightens dark fills).
|
|
43
|
+
*/
|
|
44
|
+
export type BlendMode =
|
|
45
|
+
| "normal"
|
|
46
|
+
| "multiply"
|
|
47
|
+
| "screen"
|
|
48
|
+
| "overlay"
|
|
49
|
+
| "darken"
|
|
50
|
+
| "lighten"
|
|
51
|
+
| "color-dodge"
|
|
52
|
+
| "color-burn"
|
|
53
|
+
| "hard-light"
|
|
54
|
+
| "soft-light"
|
|
55
|
+
| "difference"
|
|
56
|
+
| "exclusion"
|
|
57
|
+
| "hue"
|
|
58
|
+
| "saturation"
|
|
59
|
+
| "color"
|
|
60
|
+
| "luminosity";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Visual styling shared by anything drawn as "a stroked line with
|
|
64
|
+
* optional dots" — used by `LineChart`'s `Series` and `BarChart`'s
|
|
65
|
+
* `BarSummaryLine`. Chart-specific data (y/x arrays, scaling overrides,
|
|
66
|
+
* tooltip/outline behavior) lives on the extending type.
|
|
67
|
+
*/
|
|
68
|
+
export interface LineMarkStyle {
|
|
69
|
+
color?: string;
|
|
70
|
+
strokeWidth?: number;
|
|
71
|
+
dashed?: boolean;
|
|
72
|
+
opacity?: number;
|
|
73
|
+
/**
|
|
74
|
+
* CSS `mix-blend-mode` applied to the line and dots. Lets overlapping
|
|
75
|
+
* marks combine their colors instead of one obscuring the other.
|
|
76
|
+
*/
|
|
77
|
+
blendMode?: BlendMode;
|
|
78
|
+
/** Draw a dot at each sample point. Default false. */
|
|
79
|
+
dots?: boolean;
|
|
80
|
+
/** Radius of each dot in pixels. */
|
|
81
|
+
dotRadius?: number;
|
|
82
|
+
/** Label shown in the inline legend. */
|
|
83
|
+
legend?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Whether this mark appears in the inline legend. Defaults to true.
|
|
86
|
+
* Has no effect when `legend` is unset.
|
|
87
|
+
*/
|
|
88
|
+
showInLegend?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Props common to every cartesian chart component. Anything specific to
|
|
93
|
+
* the chart type (series shape, layout, value-axis details) lives on the
|
|
94
|
+
* component itself.
|
|
95
|
+
*/
|
|
96
|
+
export interface ChartCommonProps {
|
|
97
|
+
width?: number;
|
|
98
|
+
height?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Chart title. `\n` in the string creates additional lines, each
|
|
101
|
+
* adding `titleStyle.lineHeight` (default 18px) of vertical space.
|
|
102
|
+
*/
|
|
103
|
+
title?: string;
|
|
104
|
+
/** Styling for the chart title. See `TitleStyle`. */
|
|
105
|
+
titleStyle?: TitleStyle;
|
|
106
|
+
/**
|
|
107
|
+
* ARIA role for the chart's `<svg>`. Defaults to `"img"` when an accessible
|
|
108
|
+
* name is present (from `ariaLabel` or `title`), so screen readers announce
|
|
109
|
+
* the chart as a single labeled image instead of exposing the individual
|
|
110
|
+
* marks. The menu and download controls sit outside the `<svg>`, so they
|
|
111
|
+
* stay reachable regardless. Pass your own role to override.
|
|
112
|
+
*/
|
|
113
|
+
role?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Accessible name for the chart, announced by screen readers via the
|
|
116
|
+
* `<svg>`'s `aria-label`. Defaults to the `title` prop. The individual
|
|
117
|
+
* marks aren't exposed to assistive tech, so set this to a short summary of
|
|
118
|
+
* what the chart shows.
|
|
119
|
+
*/
|
|
120
|
+
ariaLabel?: string;
|
|
121
|
+
/** Styling for axis labels (the `xLabel` / `yLabel` text). */
|
|
122
|
+
axisLabelStyle?: LabelStyle;
|
|
123
|
+
/** Styling for axis tick labels (the numbers/categories on each axis). */
|
|
124
|
+
tickLabelStyle?: LabelStyle;
|
|
125
|
+
/** Styling for the inline legend item labels. */
|
|
126
|
+
legendStyle?: LabelStyle;
|
|
127
|
+
xLabel?: string;
|
|
128
|
+
yLabel?: string;
|
|
129
|
+
debounce?: number;
|
|
130
|
+
menu?: boolean | string;
|
|
131
|
+
/**
|
|
132
|
+
* Custom per-index data forwarded to the `tooltip` slot. Accepts a
|
|
133
|
+
* plain array or any `ArrayLike` (e.g. a typed-array column).
|
|
134
|
+
*/
|
|
135
|
+
tooltipData?: ArrayLike<unknown>;
|
|
136
|
+
/** Tooltip activation mode. */
|
|
137
|
+
tooltipTrigger?: "hover" | "click";
|
|
138
|
+
/** Boundary for tooltip flip/clamp. Default: `"window"`. */
|
|
139
|
+
tooltipClamp?: "none" | "chart" | "window";
|
|
140
|
+
/**
|
|
141
|
+
* Formatter for numeric values shown in the default tooltip. Accepts a
|
|
142
|
+
* preset name, a printf-style format string, or a function. When
|
|
143
|
+
* omitted, the chart falls back to its value-axis tick formatter, then
|
|
144
|
+
* `formatTick`. See `formatNumber` in `@cfasim-ui/shared`.
|
|
145
|
+
*/
|
|
146
|
+
tooltipValueFormat?: NumberFormat;
|
|
147
|
+
/**
|
|
148
|
+
* Custom CSV content (string or function) for the Download CSV menu
|
|
149
|
+
* item. When omitted, CSV is generated from the chart's series.
|
|
150
|
+
*/
|
|
151
|
+
csv?: string | (() => string);
|
|
152
|
+
/** Filename (without extension) for SVG, PNG, and CSV downloads. */
|
|
153
|
+
filename?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Show a plain text link below the chart to download CSV. Pass `true`
|
|
156
|
+
* for the default label or a string to customize.
|
|
157
|
+
*/
|
|
158
|
+
downloadLink?: boolean | string;
|
|
159
|
+
/**
|
|
160
|
+
* Show a `<button>` below the chart to download CSV. Pass `true` for
|
|
161
|
+
* the default label or a string to customize. When set, the CSV menu
|
|
162
|
+
* item is suppressed. Mutually exclusive with `downloadLink`; if both
|
|
163
|
+
* are set, `downloadButton` wins.
|
|
164
|
+
*/
|
|
165
|
+
downloadButton?: boolean | string;
|
|
166
|
+
/**
|
|
167
|
+
* Where to teleport the chart while expanded (the Expand menu item). A
|
|
168
|
+
* CSS selector or element; defaults to `body`. The expanded chart is
|
|
169
|
+
* moved here so its `position: fixed` resolves against the viewport
|
|
170
|
+
* instead of being trapped by an ancestor's `transform`/`filter`/
|
|
171
|
+
* `contain`/`perspective` (which would otherwise become its containing
|
|
172
|
+
* block) or stacking context. Set this when your app doesn't mount at
|
|
173
|
+
* the document root (e.g. inside a shadow root or a dedicated overlay).
|
|
174
|
+
*/
|
|
175
|
+
fullscreenTarget?: string | HTMLElement;
|
|
176
|
+
/** Annotations rendered as the top layer of the chart. */
|
|
177
|
+
annotations?: readonly ChartAnnotation[];
|
|
178
|
+
/**
|
|
179
|
+
* Extra padding (pixels) added around the plot. Number = same on all
|
|
180
|
+
* sides; object = per-side. Useful for giving annotations or other
|
|
181
|
+
* overlays room to extend past the data area without clipping.
|
|
182
|
+
*/
|
|
183
|
+
chartPadding?: ChartPadding;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Payload emitted on `hover` from a cartesian chart. */
|
|
187
|
+
export type ChartHoverPayload = { index: number } | null;
|
|
188
|
+
|
|
189
|
+
/** One per-series value passed to the tooltip slot. */
|
|
190
|
+
export interface ChartTooltipValue {
|
|
191
|
+
value: number;
|
|
192
|
+
color: string;
|
|
193
|
+
seriesIndex: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Properties common to every chart's tooltip slot. */
|
|
197
|
+
export interface ChartTooltipBaseProps {
|
|
198
|
+
index: number;
|
|
199
|
+
values: ChartTooltipValue[];
|
|
200
|
+
data: unknown;
|
|
201
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { intervalValues, niceStep } from "./axes.js";
|
|
2
|
+
|
|
3
|
+
export interface TickValueOptions {
|
|
4
|
+
/** Data-space extent. */
|
|
5
|
+
min: number;
|
|
6
|
+
max: number;
|
|
7
|
+
/** Tick spec: number = interval, array = explicit values, undefined = auto. */
|
|
8
|
+
ticks?: number | number[];
|
|
9
|
+
/** Target tick count when auto-spacing (typically innerSize / 50 for y, /80 for x). */
|
|
10
|
+
targetTickCount?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Display-space offset added to user-supplied explicit/interval values
|
|
13
|
+
* before they are interpreted. Used by LineChart's `xMin` semantics so
|
|
14
|
+
* users supply tick values in display coordinates.
|
|
15
|
+
*/
|
|
16
|
+
displayOffset?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns tick values in data-space ([min, max]) according to the spec.
|
|
21
|
+
* Display-space conversions (for labels) happen at the call site so
|
|
22
|
+
* each chart can layer its own formatting / fallback behavior.
|
|
23
|
+
*/
|
|
24
|
+
export function computeTickValues(opts: TickValueOptions): number[] {
|
|
25
|
+
const { min, max, ticks } = opts;
|
|
26
|
+
if (min === max) return [];
|
|
27
|
+
const offset = opts.displayOffset ?? 0;
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(ticks)) {
|
|
30
|
+
return ticks.map((v) => v - offset).filter((v) => v >= min && v <= max);
|
|
31
|
+
}
|
|
32
|
+
if (typeof ticks === "number") {
|
|
33
|
+
return intervalValues(min + offset, max + offset, ticks).map(
|
|
34
|
+
(v) => v - offset,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const target = Math.max(3, Math.floor(opts.targetTickCount ?? 3));
|
|
38
|
+
const step = niceStep(max - min, target);
|
|
39
|
+
return intervalValues(min + offset, max + offset, step).map(
|
|
40
|
+
(v) => v - offset,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color helpers for picking a readable text color against a bar fill.
|
|
3
|
+
* The luminance math is pure and unit-testable; resolving CSS values
|
|
4
|
+
* that aren't plain hex/rgb (named colors, `var(--x)`) needs the DOM and
|
|
5
|
+
* is handled separately by {@link resolveColorToRgb}.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type Rgb = [number, number, number];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a hex (`#rgb`, `#rrggbb`, `#rrggbbaa`) or `rgb()/rgba()` string to
|
|
12
|
+
* `[r, g, b]` in 0..255. Returns null for anything else (named colors,
|
|
13
|
+
* `var(...)`, `hsl(...)`) — resolve those via {@link resolveColorToRgb}.
|
|
14
|
+
*/
|
|
15
|
+
export function parseRgb(color: string): Rgb | null {
|
|
16
|
+
const c = color.trim();
|
|
17
|
+
if (c.startsWith("#")) {
|
|
18
|
+
let hex = c.slice(1);
|
|
19
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
20
|
+
hex = hex
|
|
21
|
+
.slice(0, 3)
|
|
22
|
+
.split("")
|
|
23
|
+
.map((ch) => ch + ch)
|
|
24
|
+
.join("");
|
|
25
|
+
} else if (hex.length === 6 || hex.length === 8) {
|
|
26
|
+
hex = hex.slice(0, 6);
|
|
27
|
+
} else {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const n = Number.parseInt(hex, 16);
|
|
31
|
+
if (Number.isNaN(n)) return null;
|
|
32
|
+
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
33
|
+
}
|
|
34
|
+
const m = c.match(/^rgba?\(([^)]+)\)$/i);
|
|
35
|
+
if (m) {
|
|
36
|
+
const parts = m[1].split(/[,/\s]+/).filter(Boolean);
|
|
37
|
+
if (parts.length < 3) return null;
|
|
38
|
+
const rgb = parts.slice(0, 3).map((p) => {
|
|
39
|
+
if (p.endsWith("%")) return Math.round((parseFloat(p) / 100) * 255);
|
|
40
|
+
return Math.round(parseFloat(p));
|
|
41
|
+
});
|
|
42
|
+
if (rgb.some((v) => Number.isNaN(v))) return null;
|
|
43
|
+
return [rgb[0], rgb[1], rgb[2]];
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** WCAG relative luminance (0 = black, 1 = white) for an RGB triple. */
|
|
49
|
+
export function relativeLuminance([r, g, b]: Rgb): number {
|
|
50
|
+
const channel = (v: number) => {
|
|
51
|
+
const s = v / 255;
|
|
52
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
53
|
+
};
|
|
54
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let probe: HTMLSpanElement | null = null;
|
|
58
|
+
const resolveCache = new Map<string, Rgb | null>();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve any CSS color string (including named colors and `var(--x)`) to
|
|
62
|
+
* an RGB triple by letting the browser compute it on a hidden probe
|
|
63
|
+
* element. Cached per input. Returns null in non-DOM environments or when
|
|
64
|
+
* the value can't be resolved. Plain hex/rgb shortcut through
|
|
65
|
+
* {@link parseRgb} without touching the DOM.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveColorToRgb(color: string): Rgb | null {
|
|
68
|
+
const direct = parseRgb(color);
|
|
69
|
+
if (direct) return direct;
|
|
70
|
+
if (resolveCache.has(color)) return resolveCache.get(color) ?? null;
|
|
71
|
+
if (typeof document === "undefined") return null;
|
|
72
|
+
if (!probe) {
|
|
73
|
+
probe = document.createElement("span");
|
|
74
|
+
probe.style.cssText =
|
|
75
|
+
"position:absolute;width:0;height:0;visibility:hidden;pointer-events:none";
|
|
76
|
+
document.body.appendChild(probe);
|
|
77
|
+
}
|
|
78
|
+
probe.style.color = "";
|
|
79
|
+
probe.style.color = color;
|
|
80
|
+
const computed = getComputedStyle(probe).color;
|
|
81
|
+
const rgb = parseRgb(computed);
|
|
82
|
+
resolveCache.set(color, rgb);
|
|
83
|
+
return rgb;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Pick the more readable of `light`/`dark` text colors for a given fill.
|
|
88
|
+
* Uses the WCAG luminance threshold (0.179) that maximizes contrast
|
|
89
|
+
* against black-or-white text. When the fill can't be resolved (e.g. an
|
|
90
|
+
* unresolvable CSS var in a non-DOM context), falls back to `light`.
|
|
91
|
+
*/
|
|
92
|
+
export function pickContrastColor(
|
|
93
|
+
fill: string,
|
|
94
|
+
light = "#ffffff",
|
|
95
|
+
dark = "#1a1a1a",
|
|
96
|
+
): string {
|
|
97
|
+
const rgb = resolveColorToRgb(fill);
|
|
98
|
+
if (!rgb) return light;
|
|
99
|
+
return relativeLuminance(rgb) > 0.179 ? dark : light;
|
|
100
|
+
}
|