@humanforest/charts 0.1.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.
@@ -0,0 +1,61 @@
1
+ // Chart palette helpers for Unovis. Returns `categories` maps whose colours are
2
+ // var(--dataviz-*) STRINGS, not hexes — CSS resolves them, so dark mode flips charts with zero JS
3
+ // and the engine stays the only place values live. Engine hex imports are allowed in chart pages
4
+ // ONLY for CVD-simulation demos (simulateCvd needs real hexes).
5
+ import type { BulletLegendItemInterface } from '@unovis/ts';
6
+ import { DATAVIZ, DATAVIZ_PAIRS, DATAVIZ_CATEGORICAL, DATAVIZ_SEQUENTIAL, DATAVIZ_DIVERGING } from './engine';
7
+
8
+ type Categories = Record<string, BulletLegendItemInterface>;
9
+ export interface SeriesDef { key: string; name: string }
10
+
11
+ // Each accessor is named after the palette it returns, matching the tier names on the docs page.
12
+ // There is deliberately NO generic `categorical()` — the generic name used to return the VIVID set,
13
+ // which meant the one palette that is not colour-blind-safe owned the most authoritative-sounding name.
14
+ const SAFE = Object.keys(DATAVIZ.light); // 1..6 — brand-led CVD-safe, consumed by index
15
+ const VIVID = DATAVIZ_CATEGORICAL.names; // green, amber, red, … (first 6 = the curated 6)
16
+ const PAIRS = Object.keys(DATAVIZ_PAIRS.light); // forest-deep, forest-soft, warm-deep, … (family-grouped)
17
+
18
+ export const safeVar = (i: number) => `var(--dataviz-categorical-safe-${SAFE[i % SAFE.length]})`;
19
+ export const vividVar = (i: number) => `var(--dataviz-categorical-vivid-${VIVID[i % VIVID.length]})`;
20
+ export const pairsVar = (i: number) => `var(--dataviz-categorical-pairs-${PAIRS[i % PAIRS.length]})`;
21
+
22
+ /** The three categorical sets, named. Only for callers that choose a set at runtime. */
23
+ export type CategoricalPalette = 'cvdSafe' | 'vivid' | 'pairs';
24
+ const CATEGORICAL_VARS: Record<CategoricalPalette, (i: number) => string> = {
25
+ cvdSafe: safeVar,
26
+ vivid: vividVar,
27
+ pairs: pairsVar,
28
+ };
29
+ export const categoricalVar = (p: CategoricalPalette, i: number) => CATEGORICAL_VARS[p](i);
30
+ export const sequentialVar = (name: keyof typeof DATAVIZ_SEQUENTIAL, i: number) => `var(--dataviz-sequential-${name}-${i})`;
31
+ const kebab = (s: string) => s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
32
+ export const divergingVar = (name: keyof typeof DATAVIZ_DIVERGING, i: number) => `var(--dataviz-diverging-${kebab(name)}-${i})`;
33
+
34
+ const build = (series: SeriesDef[], color: (i: number) => string): Categories =>
35
+ Object.fromEntries(series.map((s, i) => [s.key, { name: s.name, color: color(i) }]));
36
+
37
+ export function useChartPalette() {
38
+ return {
39
+ /** The 6-colour brand-led CVD-safe set — what the Unovis adapter already ships on --vis-color0..5.
40
+ * The default: colour alone separates the series, so it needs no second channel. */
41
+ cvdSafe: (series: SeriesDef[]) => build(series, safeVar),
42
+ /** Vivid categorical (palette order). ≤6 where you can; 7–12 are label-only.
43
+ * NOT CVD-safe — the caller MUST add a second channel (legend, direct labels, dash or marker). */
44
+ vivid: (series: SeriesDef[]) => build(series, vividVar),
45
+ /** Brand pairs — 3 brand hues × deep/soft, family-grouped. Brand-forward work: decks, marketing.
46
+ * NOT CVD-safe — the caller MUST add a second channel (legend, direct labels, dash or marker). */
47
+ pairs: (series: SeriesDef[]) => build(series, pairsVar),
48
+ /** One of the three sets, chosen at runtime. For the docs' palette picker — production code
49
+ * should name the set it means, so the CVD contract above is visible at the call site. */
50
+ byName: (p: CategoricalPalette, series: SeriesDef[]) =>
51
+ build(series, (i) => categoricalVar(p, i)),
52
+ /** n evenly-spaced stops from a sequential ramp (low → high). */
53
+ sequential: (name: keyof typeof DATAVIZ_SEQUENTIAL, n: number) => {
54
+ const len = DATAVIZ_SEQUENTIAL[name].length;
55
+ return Array.from({ length: n }, (_, i) => sequentialVar(name, Math.round((i * (len - 1)) / Math.max(n - 1, 1))));
56
+ },
57
+ /** All 9 stops of a diverging ramp (low → centre → high). */
58
+ diverging: (name: keyof typeof DATAVIZ_DIVERGING) =>
59
+ DATAVIZ_DIVERGING[name].map((_, i) => divergingVar(name, i)),
60
+ };
61
+ }
@@ -0,0 +1,22 @@
1
+ import { computed, toValue, type ComputedRef, type MaybeRefOrGetter } from 'vue';
2
+
3
+ /**
4
+ * Remount key for Unovis charts whose series colours change identity at runtime.
5
+ *
6
+ * The bug it works around: Unovis caches series colours on mount — Bar, Donut and Bubble keep
7
+ * painting the colours they mounted with even after `categories` hands them new ones (Line
8
+ * re-reads and repaints without help, which is why the trap is easy to miss). Bind the returned
9
+ * key as `:key` on the chart component so a flip of any signal remounts it:
10
+ *
11
+ * ```ts
12
+ * const chartKey = useChartRepaintKey(mode, vision, chartSet);
13
+ * // <VisSingleContainer :key="chartKey" …>
14
+ * ```
15
+ *
16
+ * Signals are passed in as refs or getters — colour mode, CVD simulation, palette choice —
17
+ * rather than read from an app composable, so the package carries no app internals; the docs
18
+ * app passes its own colour-mode ref.
19
+ */
20
+ export function useChartRepaintKey(...signals: MaybeRefOrGetter<unknown>[]): ComputedRef<string> {
21
+ return computed(() => signals.map((s) => String(toValue(s))).join('-'));
22
+ }
@@ -0,0 +1,17 @@
1
+ // Frame-owned repaint signal: bumps when the documentElement's class or data-theme attribute
2
+ // changes (i.e. on a colour-mode flip). Unovis Bar/Donut/Bubble cache resolved colours on mount,
3
+ // so a var(--dataviz-*) colour does NOT repaint them when dark mode lands — remounting on this
4
+ // signal is the workaround, owned by the frame instead of every call site.
5
+ import { onScopeDispose, ref, type Ref } from 'vue';
6
+
7
+ export function useThemeVersion(): Ref<number> {
8
+ const version = ref(0);
9
+ if (typeof window === 'undefined' || typeof MutationObserver === 'undefined') return version;
10
+ const observer = new MutationObserver(() => { version.value++; });
11
+ observer.observe(document.documentElement, {
12
+ attributes: true,
13
+ attributeFilter: ['class', 'data-theme'],
14
+ });
15
+ onScopeDispose(() => observer.disconnect());
16
+ return version;
17
+ }