@faintshadow/flarecharts 26.3.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.
Files changed (85) hide show
  1. package/LICENSE +40 -0
  2. package/README.md +103 -0
  3. package/dist/charts/AreaChart.svelte +150 -0
  4. package/dist/charts/AreaChart.svelte.d.ts +60 -0
  5. package/dist/charts/BarChart.svelte +142 -0
  6. package/dist/charts/BarChart.svelte.d.ts +58 -0
  7. package/dist/charts/BoxPlotChart.svelte +138 -0
  8. package/dist/charts/BoxPlotChart.svelte.d.ts +56 -0
  9. package/dist/charts/DonutChart.svelte +129 -0
  10. package/dist/charts/DonutChart.svelte.d.ts +73 -0
  11. package/dist/charts/LineChart.svelte +149 -0
  12. package/dist/charts/LineChart.svelte.d.ts +63 -0
  13. package/dist/charts/Sparkline.svelte +87 -0
  14. package/dist/charts/Sparkline.svelte.d.ts +40 -0
  15. package/dist/charts/StackChart.svelte +157 -0
  16. package/dist/charts/StackChart.svelte.d.ts +69 -0
  17. package/dist/components/Arc.svelte +202 -0
  18. package/dist/components/Arc.svelte.d.ts +50 -0
  19. package/dist/components/Area.svelte +264 -0
  20. package/dist/components/Area.svelte.d.ts +54 -0
  21. package/dist/components/Axis.svelte +139 -0
  22. package/dist/components/Axis.svelte.d.ts +26 -0
  23. package/dist/components/Bars.svelte +192 -0
  24. package/dist/components/Bars.svelte.d.ts +55 -0
  25. package/dist/components/Box.svelte +287 -0
  26. package/dist/components/Box.svelte.d.ts +48 -0
  27. package/dist/components/Chart.svelte +207 -0
  28. package/dist/components/Chart.svelte.d.ts +23 -0
  29. package/dist/components/Crosshair.svelte +67 -0
  30. package/dist/components/Crosshair.svelte.d.ts +14 -0
  31. package/dist/components/Grid.svelte +38 -0
  32. package/dist/components/Grid.svelte.d.ts +14 -0
  33. package/dist/components/Labels.svelte +61 -0
  34. package/dist/components/Labels.svelte.d.ts +35 -0
  35. package/dist/components/Legend.svelte +81 -0
  36. package/dist/components/Legend.svelte.d.ts +12 -0
  37. package/dist/components/Line.svelte +192 -0
  38. package/dist/components/Line.svelte.d.ts +47 -0
  39. package/dist/components/PlotBand.svelte +68 -0
  40. package/dist/components/PlotBand.svelte.d.ts +14 -0
  41. package/dist/components/PlotLine.svelte +54 -0
  42. package/dist/components/PlotLine.svelte.d.ts +16 -0
  43. package/dist/components/Points.svelte +179 -0
  44. package/dist/components/Points.svelte.d.ts +53 -0
  45. package/dist/components/Svg.svelte +36 -0
  46. package/dist/components/Svg.svelte.d.ts +8 -0
  47. package/dist/components/Tooltip.svelte +211 -0
  48. package/dist/components/Tooltip.svelte.d.ts +44 -0
  49. package/dist/core/bisect.d.ts +5 -0
  50. package/dist/core/bisect.js +23 -0
  51. package/dist/core/context.svelte.d.ts +140 -0
  52. package/dist/core/context.svelte.js +294 -0
  53. package/dist/core/curves.d.ts +4 -0
  54. package/dist/core/curves.js +13 -0
  55. package/dist/core/hit.d.ts +34 -0
  56. package/dist/core/hit.js +43 -0
  57. package/dist/core/keynav.d.ts +20 -0
  58. package/dist/core/keynav.js +41 -0
  59. package/dist/core/labels.d.ts +39 -0
  60. package/dist/core/labels.js +27 -0
  61. package/dist/core/merge.d.ts +17 -0
  62. package/dist/core/merge.js +46 -0
  63. package/dist/core/motion.svelte.d.ts +31 -0
  64. package/dist/core/motion.svelte.js +129 -0
  65. package/dist/core/normalize.d.ts +35 -0
  66. package/dist/core/normalize.js +97 -0
  67. package/dist/core/options.d.ts +113 -0
  68. package/dist/core/options.js +36 -0
  69. package/dist/core/palette.d.ts +8 -0
  70. package/dist/core/palette.js +24 -0
  71. package/dist/core/responsive.d.ts +6 -0
  72. package/dist/core/responsive.js +19 -0
  73. package/dist/core/scales.d.ts +31 -0
  74. package/dist/core/scales.js +89 -0
  75. package/dist/core/stack.d.ts +19 -0
  76. package/dist/core/stack.js +133 -0
  77. package/dist/core/stats.d.ts +45 -0
  78. package/dist/core/stats.js +114 -0
  79. package/dist/core/symbols.d.ts +8 -0
  80. package/dist/core/symbols.js +31 -0
  81. package/dist/core/types.d.ts +28 -0
  82. package/dist/core/types.js +1 -0
  83. package/dist/index.d.ts +52 -0
  84. package/dist/index.js +42 -0
  85. package/package.json +81 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Options precedence merge — Data chain, as one tiny utility:
3
+ *
4
+ * chart defaults < chart-level plotOptions < per-series options < per-point options
5
+ *
6
+ * Call it with layers in ascending precedence; later layers win:
7
+ *
8
+ * mergeOptions(defaults, plotOptions, series.options, point.options)
9
+ *
10
+ * Rules:
11
+ * - plain objects merge recursively
12
+ * - everything else (arrays, Dates, functions, class instances, scalars) replaces
13
+ * - `undefined` values are skipped (do not overwrite)
14
+ * - `null` values DO overwrite (explicit "clear this option")
15
+ * - inputs are never mutated; nested plain objects are cloned
16
+ */
17
+ function isPlainObject(value) {
18
+ if (typeof value !== 'object' || value === null)
19
+ return false;
20
+ const proto = Object.getPrototypeOf(value);
21
+ return proto === Object.prototype || proto === null;
22
+ }
23
+ function mergeInto(target, layer) {
24
+ for (const key of Object.keys(layer)) {
25
+ const value = layer[key];
26
+ if (value === undefined)
27
+ continue;
28
+ const previous = target[key];
29
+ if (isPlainObject(value)) {
30
+ target[key] = mergeInto(isPlainObject(previous) ? previous : {}, value);
31
+ }
32
+ else {
33
+ target[key] = value;
34
+ }
35
+ }
36
+ return target;
37
+ }
38
+ export function mergeOptions(...layers) {
39
+ const out = {};
40
+ for (const layer of layers) {
41
+ if (layer == null)
42
+ continue;
43
+ mergeInto(out, layer);
44
+ }
45
+ return out;
46
+ }
@@ -0,0 +1,31 @@
1
+ /** True only in the browser when the user HAS requested reduced motion. */
2
+ export declare function prefersReducedMotion(): boolean;
3
+ /** Parse a CSS duration token ("500ms" | "0.5s" | "") to milliseconds. Pure. */
4
+ export declare function parseDuration(value: string, fallback?: number): number;
5
+ /** Effective enter/update duration for an element: reads --fc-duration, 0 when
6
+ * reduced motion or no element (server). Client-only. */
7
+ export declare function motionDuration(el: Element | undefined | null, fallback?: number): number;
8
+ /** Phase factors for the box entrance, given overall progress p (0..1):
9
+ * frame (caps+whiskers) leads, box grows in the middle, median trails. Pure. */
10
+ export declare function boxEnterPhases(p: number): {
11
+ frame: number;
12
+ grow: number;
13
+ median: number;
14
+ };
15
+ type Tweenable = number | number[] | Record<string, number>;
16
+ /**
17
+ * A tweened value. On the server and first paint it equals `target()` (no
18
+ * motion). On mount, if motion is enabled it snaps to `collapsed()` then
19
+ * animates to `target()` (enter). Later `target()` changes animate (update).
20
+ */
21
+ export declare function motionValue<T extends Tweenable>(target: () => T, collapsed: () => T, getEl: () => Element | undefined): {
22
+ readonly current: T;
23
+ };
24
+ /**
25
+ * Morphs an SVG path `d` on update via d3-interpolate-path. Enter is handled by
26
+ * the component's CSS (draw-on), so the first paint just shows `target()`.
27
+ */
28
+ export declare function motionPath(target: () => string, getEl: () => Element | undefined): {
29
+ readonly d: string;
30
+ };
31
+ export {};
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Motion helpers — pure math + two reactive tween wrappers.
3
+ *
4
+ * Philosophy mirrors the rest of the library: Svelte + CSS for motion, never an
5
+ * imperative animation engine. The animation loop/easing is Svelte's first-party
6
+ * `Tween`; path morphing is `d3-interpolate-path`. Everything reads its timing
7
+ * from the `--fc-duration` CSS token and respects `prefers-reduced-motion`.
8
+ */
9
+ import { Tween } from 'svelte/motion';
10
+ import { cubicOut } from 'svelte/easing';
11
+ import { interpolatePath } from 'd3-interpolate-path';
12
+ /** True only in the browser when the user HAS requested reduced motion. */
13
+ export function prefersReducedMotion() {
14
+ return (typeof window !== 'undefined' &&
15
+ typeof window.matchMedia === 'function' &&
16
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches);
17
+ }
18
+ /** Parse a CSS duration token ("500ms" | "0.5s" | "") to milliseconds. Pure. */
19
+ export function parseDuration(value, fallback = 500) {
20
+ const v = (value ?? '').trim();
21
+ const m = /^([\d.]+)(ms|s)?$/.exec(v);
22
+ if (!m)
23
+ return fallback;
24
+ const n = parseFloat(m[1]);
25
+ if (!Number.isFinite(n))
26
+ return fallback;
27
+ return m[2] === 's' ? n * 1000 : n;
28
+ }
29
+ /** Effective enter/update duration for an element: reads --fc-duration, 0 when
30
+ * reduced motion or no element (server). Client-only. */
31
+ export function motionDuration(el, fallback = 500) {
32
+ if (!el || typeof window === 'undefined')
33
+ return 0;
34
+ if (prefersReducedMotion())
35
+ return 0;
36
+ const raw = getComputedStyle(el).getPropertyValue('--fc-duration');
37
+ return parseDuration(raw, fallback);
38
+ }
39
+ /** Phase factors for the box entrance, given overall progress p (0..1):
40
+ * frame (caps+whiskers) leads, box grows in the middle, median trails. Pure. */
41
+ export function boxEnterPhases(p) {
42
+ const c = (x) => Math.max(0, Math.min(1, x));
43
+ return { frame: c(p / 0.2), grow: c((p - 0.15) / 0.6), median: c((p - 0.75) / 0.25) };
44
+ }
45
+ const arrLen = (v) => (Array.isArray(v) ? v.length : -1);
46
+ /**
47
+ * A tweened value. On the server and first paint it equals `target()` (no
48
+ * motion). On mount, if motion is enabled it snaps to `collapsed()` then
49
+ * animates to `target()` (enter). Later `target()` changes animate (update).
50
+ */
51
+ export function motionValue(target, collapsed, getEl) {
52
+ // Duration read live so runtime --fc-duration / reduced-motion changes apply.
53
+ const tw = new Tween(target(), { duration: () => motionDuration(getEl()), easing: cubicOut });
54
+ let entered = false;
55
+ let prevLen = arrLen(target());
56
+ $effect(() => {
57
+ const tgt = target();
58
+ const len = arrLen(tgt);
59
+ if (!entered) {
60
+ entered = true;
61
+ prevLen = len;
62
+ if (motionDuration(getEl()) > 0) {
63
+ tw.set(collapsed(), { duration: 0 });
64
+ tw.set(tgt);
65
+ }
66
+ else {
67
+ tw.set(tgt, { duration: 0 });
68
+ }
69
+ }
70
+ else if (len !== prevLen) {
71
+ // Structural change (point count differs) — snap, don't interpolate mismatched arrays.
72
+ prevLen = len;
73
+ tw.set(tgt, { duration: 0 });
74
+ }
75
+ else {
76
+ tw.target = tgt;
77
+ }
78
+ });
79
+ return {
80
+ get current() {
81
+ return tw.current;
82
+ }
83
+ };
84
+ }
85
+ /**
86
+ * Morphs an SVG path `d` on update via d3-interpolate-path. Enter is handled by
87
+ * the component's CSS (draw-on), so the first paint just shows `target()`.
88
+ */
89
+ export function motionPath(target, getEl) {
90
+ const t = new Tween(1, { duration: () => motionDuration(getEl()), easing: cubicOut });
91
+ let entered = false;
92
+ let animating = $state(false);
93
+ let interp = () => target();
94
+ // `displayed` mirrors `shown` but is non-reactive, so the morph-trigger effect
95
+ // depends only on target() — never on the per-frame `shown` writes (no loop).
96
+ let displayed = target();
97
+ let shown = $state(target());
98
+ $effect(() => {
99
+ const tgt = target();
100
+ if (!entered) {
101
+ entered = true;
102
+ shown = tgt;
103
+ displayed = tgt;
104
+ return;
105
+ }
106
+ if (motionDuration(getEl()) > 0) {
107
+ interp = interpolatePath(displayed, tgt);
108
+ animating = true;
109
+ t.set(0, { duration: 0 });
110
+ t.set(1);
111
+ }
112
+ else {
113
+ animating = false;
114
+ shown = tgt;
115
+ displayed = tgt;
116
+ }
117
+ });
118
+ $effect(() => {
119
+ if (animating) {
120
+ shown = interp(t.current);
121
+ displayed = shown;
122
+ }
123
+ });
124
+ return {
125
+ get d() {
126
+ return shown;
127
+ }
128
+ };
129
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Series data normalizer.
3
+ *
4
+ * Every mark component (`<Line>`, `<Area>`, `<Points>`, …) accepts the same
5
+ * input shapes:
6
+ *
7
+ * - `number[]` → y values, x is the array index
8
+ * - `[x, y][]` → tuples; x may be number | string | Date
9
+ * - `{ x, y, ... }[]` → point objects (extra keys pass through)
10
+ * - `T[]` + `x`/`y` accessor fns → arbitrary data, you tell us where x/y live
11
+ *
12
+ * `null` (or `NaN`/`±Infinity`) y values become gaps, never zeros.
13
+ * One normalizer, used everywhere, heavily tested.
14
+ */
15
+ /** Any value usable on the x axis. Strings imply a band (category) scale. */
16
+ export type XValue = number | string | Date;
17
+ export interface NormalizedPoint<T = unknown> {
18
+ x: XValue;
19
+ y: number | null;
20
+ /** The original datum, untouched — flows into snippets (tooltips, labels). */
21
+ datum: T;
22
+ index: number;
23
+ }
24
+ export interface PointAccessors<T = unknown> {
25
+ x?: (datum: T, index: number) => XValue;
26
+ y?: (datum: T, index: number) => number | null | undefined;
27
+ }
28
+ /**
29
+ * Normalize any supported data shape into `NormalizedPoint[]`.
30
+ * Accessors win over inference; partial accessors are fine
31
+ * (e.g. only `y` for `number[]`-like data with index x).
32
+ */
33
+ export declare function normalizePoints<T>(data: readonly T[], accessors?: PointAccessors<T>): NormalizedPoint<T>[];
34
+ /** What kind of x values a point list carries — drives x scale type inference. */
35
+ export declare function xKindOf(points: readonly NormalizedPoint[]): 'number' | 'date' | 'category';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Series data normalizer.
3
+ *
4
+ * Every mark component (`<Line>`, `<Area>`, `<Points>`, …) accepts the same
5
+ * input shapes:
6
+ *
7
+ * - `number[]` → y values, x is the array index
8
+ * - `[x, y][]` → tuples; x may be number | string | Date
9
+ * - `{ x, y, ... }[]` → point objects (extra keys pass through)
10
+ * - `T[]` + `x`/`y` accessor fns → arbitrary data, you tell us where x/y live
11
+ *
12
+ * `null` (or `NaN`/`±Infinity`) y values become gaps, never zeros.
13
+ * One normalizer, used everywhere, heavily tested.
14
+ */
15
+ function toX(value, index) {
16
+ if (value == null)
17
+ return index;
18
+ if (typeof value === 'number' || typeof value === 'string' || value instanceof Date) {
19
+ return value;
20
+ }
21
+ throw new Error(`flarechart: x value at index ${index} is not a number, string or Date (got ${typeof value}). Provide an \`x\` accessor.`);
22
+ }
23
+ function toY(value, index) {
24
+ if (value == null)
25
+ return null;
26
+ if (typeof value === 'number') {
27
+ return Number.isFinite(value) ? value : null;
28
+ }
29
+ throw new Error(`flarechart: y value at index ${index} is not a number (got ${typeof value}). Provide a \`y\` accessor.`);
30
+ }
31
+ function inferPoint(item, index, needX, needY) {
32
+ // number[] (null = gap)
33
+ if (item == null)
34
+ return { x: index, y: null };
35
+ if (typeof item === 'number')
36
+ return { x: index, y: toY(item, index) };
37
+ // [x, y][]
38
+ if (Array.isArray(item)) {
39
+ if (item.length < 2) {
40
+ throw new Error(`flarechart: tuple at index ${index} needs [x, y], got length ${item.length}.`);
41
+ }
42
+ return { x: toX(item[0], index), y: toY(item[1], index) };
43
+ }
44
+ // { x?, y }[]
45
+ if (typeof item === 'object') {
46
+ const o = item;
47
+ const hasY = 'y' in o;
48
+ if (needY && !hasY) {
49
+ throw new Error(`flarechart: cannot infer y from datum at index ${index} — no \`y\` key. Provide a \`y\` accessor.`);
50
+ }
51
+ return {
52
+ x: 'x' in o ? toX(o.x, index) : index,
53
+ y: hasY ? toY(o.y, index) : null
54
+ };
55
+ }
56
+ if (needX || needY) {
57
+ throw new Error(`flarechart: cannot infer a point from datum at index ${index} (got ${typeof item}). Provide x/y accessors.`);
58
+ }
59
+ return { x: index, y: null };
60
+ }
61
+ /**
62
+ * Normalize any supported data shape into `NormalizedPoint[]`.
63
+ * Accessors win over inference; partial accessors are fine
64
+ * (e.g. only `y` for `number[]`-like data with index x).
65
+ */
66
+ export function normalizePoints(data, accessors = {}) {
67
+ const out = new Array(data.length);
68
+ for (let i = 0; i < data.length; i++) {
69
+ const datum = data[i];
70
+ let x;
71
+ let y;
72
+ if (accessors.x)
73
+ x = toX(accessors.x(datum, i), i);
74
+ if (accessors.y)
75
+ y = toY(accessors.y(datum, i), i);
76
+ if (x === undefined || y === undefined) {
77
+ const inferred = inferPoint(datum, i, x === undefined, y === undefined);
78
+ if (x === undefined)
79
+ x = inferred.x;
80
+ if (y === undefined)
81
+ y = inferred.y;
82
+ }
83
+ out[i] = { x, y, datum, index: i };
84
+ }
85
+ return out;
86
+ }
87
+ /** What kind of x values a point list carries — drives x scale type inference. */
88
+ export function xKindOf(points) {
89
+ const p = points.find((pt) => pt.x != null);
90
+ if (!p)
91
+ return 'number';
92
+ if (p.x instanceof Date)
93
+ return 'date';
94
+ if (typeof p.x === 'string')
95
+ return 'category';
96
+ return 'number';
97
+ }
@@ -0,0 +1,113 @@
1
+ import type { XValue } from './normalize.js';
2
+ import type { CurveName } from './curves.js';
3
+ import type { StackOffset, StackOrder } from './stack.js';
4
+ import type { AxisOptions } from './types.js';
5
+ import type { TooltipMode } from './hit.js';
6
+ import type { WhiskerMode } from './stats.js';
7
+ import type { SymbolName } from './symbols.js';
8
+ export type MarkerMode = 'always' | 'hover' | 'none';
9
+ export interface SeriesOptions<T = unknown> {
10
+ name?: string;
11
+ data: readonly T[];
12
+ x?: (datum: T, index: number) => XValue;
13
+ y?: (datum: T, index: number) => number | null | undefined;
14
+ /** Any CSS color string — passed through untouched. */
15
+ color?: string;
16
+ /** Per-point color override — the end of the precedence chain. */
17
+ colorFor?: (datum: T, index: number) => string | undefined;
18
+ /** Accessible series description (announced when keyboard focus enters the series). */
19
+ description?: string;
20
+ curve?: CurveName;
21
+ strokeWidth?: number;
22
+ /** Areas only. */
23
+ fillOpacity?: number;
24
+ /** Bars/areas. normal/percent anchor at zero; stream/silhouette float (areas). */
25
+ stacking?: StackOffset;
26
+ /** Layer ordering within the stack group. */
27
+ order?: StackOrder;
28
+ stack?: string;
29
+ /** Bars only. */
30
+ barPadding?: number;
31
+ rx?: number;
32
+ /** Box plot accessors (BoxPlotChart). Raw `samples` wins over the precomputed five. */
33
+ low?: (datum: T, index: number) => number | null | undefined;
34
+ q1?: (datum: T, index: number) => number | null | undefined;
35
+ median?: (datum: T, index: number) => number | null | undefined;
36
+ q3?: (datum: T, index: number) => number | null | undefined;
37
+ high?: (datum: T, index: number) => number | null | undefined;
38
+ outliers?: (datum: T, index: number) => readonly number[] | undefined;
39
+ samples?: (datum: T, index: number) => readonly number[] | undefined;
40
+ whisker?: WhiskerMode;
41
+ whiskerK?: number;
42
+ boxPadding?: number;
43
+ /** Marker visibility: 'always' (all dots visible), 'hover' (dot at hovered x), 'none' (no markers). */
44
+ markers?: MarkerMode;
45
+ /** Symbol shape for point markers. */
46
+ symbol?: SymbolName;
47
+ /** Symbol area in px² (default 64 ≈ 4.5px radius circle). */
48
+ symbolSize?: number;
49
+ /** Draw value labels. */
50
+ labels?: boolean;
51
+ }
52
+ /** Chart-level series defaults — everything but data/name. */
53
+ export type PlotOptions<T = unknown> = Omit<Partial<SeriesOptions<T>>, 'data' | 'name'>;
54
+ export declare const SERIES_DEFAULTS: {
55
+ curve: CurveName;
56
+ strokeWidth: number;
57
+ fillOpacity: number;
58
+ barPadding: number;
59
+ rx: number;
60
+ markers: MarkerMode;
61
+ symbolSize: number;
62
+ labels: boolean;
63
+ whisker: WhiskerMode;
64
+ whiskerK: number;
65
+ boxPadding: number;
66
+ };
67
+ export type ResolvedSeries<T> = SeriesOptions<T> & typeof SERIES_DEFAULTS;
68
+ /** chart defaults < plotOptions < series. (Per-point colorFor applies at render.) */
69
+ export declare function resolveSeries<T>(plotOptions: PlotOptions<T> | undefined, series: SeriesOptions<T>): ResolvedSeries<T>;
70
+ /** Axis prop for L2 charts: scale options + display options in one bag. */
71
+ export interface SimpleAxisOptions extends AxisOptions {
72
+ title?: string;
73
+ ticks?: number;
74
+ format?: (value: XValue, index: number) => string;
75
+ rotate?: number;
76
+ /** false hides the axis entirely. */
77
+ visible?: boolean;
78
+ }
79
+ export interface SplitAxis {
80
+ scale: AxisOptions | undefined;
81
+ display: {
82
+ title?: string;
83
+ ticks?: number;
84
+ format?: (value: XValue, index: number) => string;
85
+ rotate?: number;
86
+ };
87
+ visible: boolean;
88
+ }
89
+ /** Separate what <Chart> needs (scale) from what <Axis> needs (display). */
90
+ export declare function splitAxisOptions(options: SimpleAxisOptions | undefined): SplitAxis;
91
+ export interface BandSpec {
92
+ axis?: 'x' | 'y';
93
+ from: XValue;
94
+ to: XValue;
95
+ color?: string;
96
+ label?: string;
97
+ }
98
+ export interface PlotLineSpec {
99
+ axis?: 'x' | 'y';
100
+ value: XValue;
101
+ color?: string;
102
+ dash?: string;
103
+ label?: string;
104
+ }
105
+ export interface TooltipSpec {
106
+ mode?: TooltipMode;
107
+ shared?: boolean;
108
+ formatX?: (x: XValue) => string;
109
+ formatY?: (y: number) => string;
110
+ }
111
+ export interface LegendSpec {
112
+ position?: 'top' | 'bottom';
113
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * L2 simple-chart option types + resolution.
3
+ *
4
+ * The precedence chain, end to end (later wins):
5
+ *
6
+ * SERIES_DEFAULTS < chart plotOptions < per-series options < per-point
7
+ *
8
+ * The first three layers resolve through mergeOptions in resolveSeries(); the
9
+ * per-point layer is `colorFor(datum, index)`, applied at render time by the
10
+ * marks (its result wins over the resolved series color).
11
+ */
12
+ import { mergeOptions } from './merge.js';
13
+ export const SERIES_DEFAULTS = {
14
+ curve: 'linear',
15
+ strokeWidth: 2,
16
+ fillOpacity: 0.2,
17
+ barPadding: 0.08,
18
+ rx: 0,
19
+ markers: 'hover',
20
+ symbolSize: 64,
21
+ labels: false,
22
+ whisker: 'tukey',
23
+ whiskerK: 1.5,
24
+ boxPadding: 0.15
25
+ };
26
+ /** chart defaults < plotOptions < series. (Per-point colorFor applies at render.) */
27
+ export function resolveSeries(plotOptions, series) {
28
+ return mergeOptions(SERIES_DEFAULTS, plotOptions, series);
29
+ }
30
+ /** Separate what <Chart> needs (scale) from what <Axis> needs (display). */
31
+ export function splitAxisOptions(options) {
32
+ if (!options)
33
+ return { scale: undefined, display: {}, visible: true };
34
+ const { title, ticks, format, rotate, visible, ...scale } = options;
35
+ return { scale, display: { title, ticks, format, rotate }, visible: visible !== false };
36
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Styled mode: every color resolves from a CSS custom property.
3
+ * The hex literals below are the ONLY literal colors in the library, and they
4
+ * exist solely as var() fallbacks — consumers theme by defining --fc-series-N.
5
+ */
6
+ export declare const DEFAULT_PALETTE: readonly string[];
7
+ /** `var(--fc-series-N, fallback)` for a zero-based series index (wraps past 12). */
8
+ export declare function seriesColorVar(index: number): string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Styled mode: every color resolves from a CSS custom property.
3
+ * The hex literals below are the ONLY literal colors in the library, and they
4
+ * exist solely as var() fallbacks — consumers theme by defining --fc-series-N.
5
+ */
6
+ export const DEFAULT_PALETTE = [
7
+ '#4f46e5', // indigo
8
+ '#0ea5e9', // sky
9
+ '#10b981', // emerald
10
+ '#f59e0b', // amber
11
+ '#f43f5e', // rose
12
+ '#8b5cf6', // violet
13
+ '#14b8a6', // teal
14
+ '#f97316', // orange
15
+ '#84cc16', // lime
16
+ '#d946ef', // fuchsia
17
+ '#06b6d4', // cyan
18
+ '#64748b' // slate
19
+ ];
20
+ /** `var(--fc-series-N, fallback)` for a zero-based series index (wraps past 12). */
21
+ export function seriesColorVar(index) {
22
+ const slot = ((index % DEFAULT_PALETTE.length) + DEFAULT_PALETTE.length) % DEFAULT_PALETTE.length;
23
+ return `var(--fc-series-${slot + 1}, ${DEFAULT_PALETTE[slot]})`;
24
+ }
@@ -0,0 +1,6 @@
1
+ export interface ResponsiveRule<P = Record<string, unknown>> {
2
+ minWidth?: number;
3
+ maxWidth?: number;
4
+ options: Partial<P>;
5
+ }
6
+ export declare function applyResponsive<P extends object>(base: P, rules: readonly ResponsiveRule<P>[] | undefined, width: number): P;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Container-driven responsive rules for L2 charts: each rule's options are
3
+ * merged over the base props (precedence utility, later rules win) when the
4
+ * measured container width matches. Width 0 (SSR / unmeasured) applies none.
5
+ */
6
+ import { mergeOptions } from './merge.js';
7
+ export function applyResponsive(base, rules, width) {
8
+ if (!rules?.length || width <= 0)
9
+ return base;
10
+ let out = base;
11
+ for (const rule of rules) {
12
+ const matchesMax = rule.maxWidth == null || width <= rule.maxWidth;
13
+ const matchesMin = rule.minWidth == null || width >= rule.minWidth;
14
+ if (matchesMax && matchesMin) {
15
+ out = mergeOptions(out, rule.options);
16
+ }
17
+ }
18
+ return out;
19
+ }
@@ -0,0 +1,31 @@
1
+ import type { ScaleBand, ScaleLinear, ScaleTime } from 'd3-scale';
2
+ import type { XValue } from './normalize.js';
3
+ export type ScaleKind = 'linear' | 'time' | 'band';
4
+ export type AnyScale = ScaleLinear<number, number> | ScaleTime<number, number> | ScaleBand<string>;
5
+ export type ScaleDomain = [number, number] | [Date, Date] | string[];
6
+ export interface ScaleSpec {
7
+ kind: ScaleKind;
8
+ domain: ScaleDomain;
9
+ range: [number, number];
10
+ /** `true` (default) → d3 nice(); a number → nice(count) tick hint; `false` → exact domain. */
11
+ nice?: boolean | number;
12
+ /** Band scales only: inner padding ratio (default 0.1). */
13
+ bandPadding?: number;
14
+ }
15
+ export declare function createScale(spec: ScaleSpec): AnyScale;
16
+ export declare function isBandScale(scale: AnyScale): scale is ScaleBand<string>;
17
+ /**
18
+ * Map a value to a pixel position. Band scales position at the band CENTER
19
+ * (right for lines/points over categories). Unmappable values yield NaN —
20
+ * callers filter via d3-shape `defined()` or render guards.
21
+ */
22
+ export declare function scalePos(scale: AnyScale, value: XValue): number;
23
+ /** Tick values: d3 ticks for continuous scales, the full domain for band scales. */
24
+ export declare function scaleTicks(scale: AnyScale, count?: number): XValue[];
25
+ /** Default tick label formatter: d3's precision/locale-aware one when available. */
26
+ export declare function scaleTickFormat(scale: AnyScale, count?: number): (value: XValue) => string;
27
+ /**
28
+ * Invert a pixel position to the band category whose center is closest.
29
+ * Band scales have no native invert; this is the tooltip "band" mode lookup.
30
+ */
31
+ export declare function bandInvert(scale: ScaleBand<string>, px: number): string | null;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Scale factory + scale helpers. d3 does math only — it never touches the DOM.
3
+ * All scale-type branching lives here so components stay scale-agnostic.
4
+ */
5
+ import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
6
+ export function createScale(spec) {
7
+ const { kind, range } = spec;
8
+ if (kind === 'band') {
9
+ const padding = spec.bandPadding ?? 0.1;
10
+ return scaleBand()
11
+ .domain(spec.domain)
12
+ .range(range)
13
+ .paddingInner(padding)
14
+ .paddingOuter(padding / 2);
15
+ }
16
+ if (kind === 'time') {
17
+ const scale = scaleTime()
18
+ .domain(spec.domain)
19
+ .range(range);
20
+ if (spec.nice !== false) {
21
+ if (typeof spec.nice === 'number')
22
+ scale.nice(spec.nice);
23
+ else
24
+ scale.nice();
25
+ }
26
+ return scale;
27
+ }
28
+ const scale = scaleLinear()
29
+ .domain(spec.domain)
30
+ .range(range);
31
+ if (spec.nice !== false) {
32
+ if (typeof spec.nice === 'number')
33
+ scale.nice(spec.nice);
34
+ else
35
+ scale.nice();
36
+ }
37
+ return scale;
38
+ }
39
+ export function isBandScale(scale) {
40
+ return 'bandwidth' in scale;
41
+ }
42
+ /**
43
+ * Map a value to a pixel position. Band scales position at the band CENTER
44
+ * (right for lines/points over categories). Unmappable values yield NaN —
45
+ * callers filter via d3-shape `defined()` or render guards.
46
+ */
47
+ export function scalePos(scale, value) {
48
+ if (isBandScale(scale)) {
49
+ const start = scale(String(value));
50
+ return start === undefined ? NaN : start + scale.bandwidth() / 2;
51
+ }
52
+ const input = value instanceof Date ? value : typeof value === 'number' ? value : NaN;
53
+ if (typeof input === 'number' && Number.isNaN(input))
54
+ return NaN;
55
+ const px = scale(input);
56
+ return px == null || Number.isNaN(px) ? NaN : px;
57
+ }
58
+ /** Tick values: d3 ticks for continuous scales, the full domain for band scales. */
59
+ export function scaleTicks(scale, count = 6) {
60
+ if (isBandScale(scale))
61
+ return scale.domain();
62
+ return scale.ticks(count);
63
+ }
64
+ /** Default tick label formatter: d3's precision/locale-aware one when available. */
65
+ export function scaleTickFormat(scale, count = 6) {
66
+ if (isBandScale(scale))
67
+ return (value) => String(value);
68
+ const format = scale.tickFormat(count);
69
+ return (value) => format(value);
70
+ }
71
+ /**
72
+ * Invert a pixel position to the band category whose center is closest.
73
+ * Band scales have no native invert; this is the tooltip "band" mode lookup.
74
+ */
75
+ export function bandInvert(scale, px) {
76
+ let best = null;
77
+ let bestDistance = Infinity;
78
+ for (const category of scale.domain()) {
79
+ const start = scale(category);
80
+ if (start === undefined)
81
+ continue;
82
+ const distance = Math.abs(start + scale.bandwidth() / 2 - px);
83
+ if (distance < bestDistance) {
84
+ bestDistance = distance;
85
+ best = category;
86
+ }
87
+ }
88
+ return best;
89
+ }