@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,264 @@
1
+ <script lang="ts" generics="T">
2
+ import { onDestroy, onMount } from 'svelte';
3
+ import { area as d3area, line as d3line } from 'd3-shape';
4
+ import { getChartContext } from '../core/context.svelte.js';
5
+ import { normalizePoints } from '../core/normalize.js';
6
+ import type { NormalizedPoint, XValue } from '../core/normalize.js';
7
+ import { curveFor } from '../core/curves.js';
8
+ import type { CurveName } from '../core/curves.js';
9
+ import { isBandScale } from '../core/scales.js';
10
+ import { stackKey } from '../core/stack.js';
11
+ import type { StackOffset, StackOrder } from '../core/stack.js';
12
+ import { motionPath } from '../core/motion.svelte.js';
13
+ import { hitBisectX } from '../core/hit.js';
14
+ import { symbolFor, symbolByIndex, symbolPath } from '../core/symbols.js';
15
+ import type { SymbolName } from '../core/symbols.js';
16
+ import type { SymbolType } from 'd3-shape';
17
+ import type { MarkerMode } from '../core/options.js';
18
+
19
+ interface Props {
20
+ data: readonly T[];
21
+ x?: (datum: T, index: number) => XValue;
22
+ y?: (datum: T, index: number) => number | null | undefined;
23
+ color?: string;
24
+ name?: string;
25
+ description?: string;
26
+ describePoint?: (point: NormalizedPoint<T>) => string | undefined;
27
+ curve?: CurveName;
28
+ stacking?: StackOffset;
29
+ order?: StackOrder;
30
+ stack?: string;
31
+ baseline?: number;
32
+ fillOpacity?: number;
33
+ line?: boolean;
34
+ strokeWidth?: number;
35
+ index?: number;
36
+ /** Marker visibility: 'always' | 'hover' | 'none'. */
37
+ markers?: MarkerMode;
38
+ /** Symbol shape for markers. */
39
+ symbol?: SymbolName | SymbolType;
40
+ /** Symbol area in px² (default 64). */
41
+ symbolSize?: number;
42
+ class?: string;
43
+ }
44
+
45
+ let {
46
+ data,
47
+ x,
48
+ y,
49
+ color,
50
+ name,
51
+ description,
52
+ describePoint,
53
+ curve = 'linear',
54
+ stacking,
55
+ order,
56
+ stack,
57
+ baseline = 0,
58
+ fillOpacity = 0.2,
59
+ line = true,
60
+ strokeWidth = 2,
61
+ index: indexProp,
62
+ markers = 'none',
63
+ symbol: symbolProp,
64
+ symbolSize = 64,
65
+ class: klass = ''
66
+ }: Props = $props();
67
+
68
+ const ctx = getChartContext();
69
+
70
+ const points = $derived(normalizePoints(data, { x, y }));
71
+ const stackedMeta = $derived(
72
+ stacking ? { id: stack ?? 'fc-stack', offset: stacking, order } : undefined
73
+ );
74
+ const registration = ctx.registerSeries(() => ({
75
+ points,
76
+ name,
77
+ color: resolvedColor,
78
+ stacked: stackedMeta,
79
+ description,
80
+ describePoint: describePoint as ((p: NormalizedPoint<unknown>) => string | undefined) | undefined
81
+ }));
82
+ onDestroy(registration.unregister);
83
+
84
+ const seriesIndex = $derived(indexProp ?? registration.index);
85
+ const resolvedColor = $derived(ctx.seriesColor(seriesIndex, color));
86
+ const visible = $derived(ctx.isSeriesVisible(registration.index));
87
+
88
+ const extents = $derived(stackedMeta ? ctx.stackFor(registration.index) : undefined);
89
+
90
+ const baselinePx = $derived.by(() => {
91
+ if (isBandScale(ctx.yScale)) return ctx.innerHeight;
92
+ const [lo, hi] = ctx.yScale.domain() as [number, number];
93
+ return ctx.yPos(Math.max(Math.min(lo, hi), Math.min(Math.max(lo, hi), baseline)));
94
+ });
95
+
96
+ const defined = (p: NormalizedPoint<T>) => {
97
+ if (p.y == null || !Number.isFinite(ctx.xPos(p.x))) return false;
98
+ if (extents && !extents.has(stackKey(p.x))) return false;
99
+ return true;
100
+ };
101
+
102
+ const y0Of = (p: NormalizedPoint<T>) =>
103
+ extents ? ctx.yPos(extents.get(stackKey(p.x))![0]) : baselinePx;
104
+ const y1Of = (p: NormalizedPoint<T>) =>
105
+ extents ? ctx.yPos(extents.get(stackKey(p.x))![1]) : ctx.yPos(p.y as number);
106
+
107
+ const areaPath = $derived.by(() => {
108
+ const generator = d3area<NormalizedPoint<T>>()
109
+ .defined(defined)
110
+ .x((p) => ctx.xPos(p.x))
111
+ .y0(y0Of)
112
+ .y1(y1Of)
113
+ .curve(curveFor(curve));
114
+ return generator(points) ?? '';
115
+ });
116
+
117
+ const linePath = $derived.by(() => {
118
+ if (!line) return '';
119
+ const generator = d3line<NormalizedPoint<T>>()
120
+ .defined(defined)
121
+ .x((p) => ctx.xPos(p.x))
122
+ .y(y1Of)
123
+ .curve(curveFor(curve));
124
+ return generator(points) ?? '';
125
+ });
126
+
127
+ // --- markers ---
128
+ const resolvedSymbol = $derived(
129
+ symbolProp ? symbolFor(symbolProp) : symbolByIndex(seriesIndex)
130
+ );
131
+ const markerPathD = $derived(markers !== 'none' ? symbolPath(resolvedSymbol, symbolSize) : '');
132
+
133
+ interface MarkerDot {
134
+ cx: number;
135
+ cy: number;
136
+ index: number;
137
+ }
138
+ const markerDots = $derived.by((): MarkerDot[] => {
139
+ if (markers === 'none') return [];
140
+ const out: MarkerDot[] = [];
141
+ for (const p of points) {
142
+ if (!defined(p)) continue;
143
+ out.push({ cx: ctx.xPos(p.x), cy: y1Of(p), index: p.index });
144
+ }
145
+ return out;
146
+ });
147
+
148
+ const hoveredIndex = $derived.by(() => {
149
+ if (markers !== 'hover') return -1;
150
+ const ptr = ctx.hoverPointer;
151
+ if (!ptr) return -1;
152
+ const hit = hitBisectX(points, ctx.xPos, ctx.yPos, ptr.x);
153
+ return hit ? hit.point.index : -1;
154
+ });
155
+
156
+ let rootEl: SVGGElement | undefined = $state();
157
+ let mounted = $state(false);
158
+ onMount(() => {
159
+ mounted = true;
160
+ });
161
+ const areaMorph = motionPath(() => areaPath, () => rootEl);
162
+ const lineMorph = motionPath(() => linePath, () => rootEl);
163
+ </script>
164
+
165
+ {#if visible}
166
+ <g class="fc-series fc-area {klass}" data-name={name} bind:this={rootEl}>
167
+ <path
168
+ class="fc-area-fill"
169
+ class:fc-fade={mounted}
170
+ d={areaMorph.d}
171
+ fill={resolvedColor}
172
+ fill-opacity={fillOpacity}
173
+ />
174
+ {#if line && linePath}
175
+ <path
176
+ class="fc-area-line"
177
+ class:fc-draw={mounted}
178
+ d={lineMorph.d}
179
+ pathLength="1"
180
+ fill="none"
181
+ stroke={resolvedColor}
182
+ stroke-width={strokeWidth}
183
+ stroke-linecap="round"
184
+ stroke-linejoin="round"
185
+ />
186
+ {/if}
187
+ {#if markers !== 'none'}
188
+ {#each markerDots as d (d.index)}
189
+ <g transform="translate({d.cx},{d.cy})">
190
+ <path
191
+ class="fc-marker"
192
+ class:fc-marker-active={markers === 'always' || d.index === hoveredIndex}
193
+ d={markerPathD}
194
+ fill={resolvedColor}
195
+ />
196
+ </g>
197
+ {/each}
198
+ {/if}
199
+ </g>
200
+ {/if}
201
+
202
+ <style>
203
+ .fc-area-fill,
204
+ .fc-area-line {
205
+ transition: opacity var(--fc-duration-hover, 120ms) var(--fc-ease, ease);
206
+ }
207
+ .fc-fade {
208
+ animation: fc-fade-in var(--fc-duration, 500ms) var(--fc-ease, ease);
209
+ }
210
+ @keyframes fc-fade-in {
211
+ from {
212
+ opacity: 0;
213
+ }
214
+ to {
215
+ opacity: 1;
216
+ }
217
+ }
218
+ .fc-draw {
219
+ stroke-dasharray: 1 1;
220
+ animation: fc-draw-on var(--fc-duration, 500ms) var(--fc-ease, ease) both;
221
+ }
222
+ @keyframes fc-draw-on {
223
+ from {
224
+ stroke-dashoffset: 1;
225
+ }
226
+ to {
227
+ stroke-dashoffset: 0;
228
+ }
229
+ }
230
+ :global(.fc-svg:has(.fc-series:hover)) .fc-series:not(:hover) .fc-area-fill,
231
+ :global(.fc-svg:has(.fc-series:hover)) .fc-series:not(:hover) .fc-area-line {
232
+ opacity: 0.25;
233
+ }
234
+
235
+ /* markers */
236
+ .fc-marker {
237
+ opacity: 0;
238
+ transform: scale(0.4);
239
+ transform-box: fill-box;
240
+ transform-origin: center;
241
+ transition:
242
+ opacity var(--fc-marker-out, 300ms) var(--fc-ease, ease),
243
+ transform var(--fc-marker-out, 300ms) var(--fc-ease, ease);
244
+ }
245
+ .fc-marker-active {
246
+ opacity: 1;
247
+ transform: scale(1);
248
+ transition:
249
+ opacity var(--fc-marker-in, 250ms) var(--fc-ease, ease),
250
+ transform var(--fc-marker-in, 250ms) var(--fc-ease, ease);
251
+ }
252
+
253
+ @media (prefers-reduced-motion: reduce) {
254
+ .fc-fade,
255
+ .fc-draw {
256
+ animation: none;
257
+ }
258
+ .fc-area-fill,
259
+ .fc-area-line,
260
+ .fc-marker {
261
+ transition: none;
262
+ }
263
+ }
264
+ </style>
@@ -0,0 +1,54 @@
1
+ import type { NormalizedPoint, XValue } from '../core/normalize.js';
2
+ import type { CurveName } from '../core/curves.js';
3
+ import type { StackOffset, StackOrder } from '../core/stack.js';
4
+ import type { SymbolName } from '../core/symbols.js';
5
+ import type { SymbolType } from 'd3-shape';
6
+ import type { MarkerMode } from '../core/options.js';
7
+ declare function $$render<T>(): {
8
+ props: {
9
+ data: readonly T[];
10
+ x?: (datum: T, index: number) => XValue;
11
+ y?: (datum: T, index: number) => number | null | undefined;
12
+ color?: string;
13
+ name?: string;
14
+ description?: string;
15
+ describePoint?: (point: NormalizedPoint<T>) => string | undefined;
16
+ curve?: CurveName;
17
+ stacking?: StackOffset;
18
+ order?: StackOrder;
19
+ stack?: string;
20
+ baseline?: number;
21
+ fillOpacity?: number;
22
+ line?: boolean;
23
+ strokeWidth?: number;
24
+ index?: number;
25
+ /** Marker visibility: 'always' | 'hover' | 'none'. */
26
+ markers?: MarkerMode;
27
+ /** Symbol shape for markers. */
28
+ symbol?: SymbolName | SymbolType;
29
+ /** Symbol area in px² (default 64). */
30
+ symbolSize?: number;
31
+ class?: string;
32
+ };
33
+ exports: {};
34
+ bindings: "";
35
+ slots: {};
36
+ events: {};
37
+ };
38
+ declare class __sveltets_Render<T> {
39
+ props(): ReturnType<typeof $$render<T>>['props'];
40
+ events(): ReturnType<typeof $$render<T>>['events'];
41
+ slots(): ReturnType<typeof $$render<T>>['slots'];
42
+ bindings(): "";
43
+ exports(): {};
44
+ }
45
+ interface $$IsomorphicComponent {
46
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
47
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
48
+ } & ReturnType<__sveltets_Render<T>['exports']>;
49
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
50
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
51
+ }
52
+ declare const Area: $$IsomorphicComponent;
53
+ type Area<T> = InstanceType<typeof Area<T>>;
54
+ export default Area;
@@ -0,0 +1,139 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { getChartContext } from '../core/context.svelte.js';
4
+ import type { XValue } from '../core/normalize.js';
5
+
6
+ type Placement = 'top' | 'bottom' | 'left' | 'right';
7
+
8
+ interface Props {
9
+ /** bottom/top render the x scale; left/right render the y scale. */
10
+ placement?: Placement;
11
+ /** Tick count hint (continuous scales). Defaults to a density-based count. */
12
+ ticks?: number;
13
+ /** Tick label formatter; defaults to the scale's own (locale/precision aware). */
14
+ format?: (value: XValue, index: number) => string;
15
+ /** Axis title. */
16
+ title?: string;
17
+ tickSize?: number;
18
+ /** Rotate tick labels by N degrees (opt-in, e.g. 45 for crowded x axes). */
19
+ rotate?: number;
20
+ class?: string;
21
+ /** Replace the default tick label text entirely. */
22
+ tick?: Snippet<[{ value: XValue; formatted: string; index: number }]>;
23
+ }
24
+
25
+ let {
26
+ placement = 'bottom',
27
+ ticks,
28
+ format,
29
+ title,
30
+ tickSize = 6,
31
+ rotate = 0,
32
+ class: klass = '',
33
+ tick
34
+ }: Props = $props();
35
+
36
+ const ctx = getChartContext();
37
+
38
+ const isX = $derived(placement === 'top' || placement === 'bottom');
39
+ const values = $derived(isX ? ctx.xTicks(ticks) : ctx.yTicks(ticks));
40
+ const fmt = $derived.by(() => {
41
+ if (format) return format;
42
+ const f = isX ? ctx.xTickFormat(ticks) : ctx.yTickFormat(ticks);
43
+ return (value: XValue) => f(value);
44
+ });
45
+ const pos = $derived(isX ? ctx.xPos : ctx.yPos);
46
+
47
+ const offset = $derived.by(() => {
48
+ if (placement === 'bottom') return `translate(0, ${ctx.innerHeight})`;
49
+ if (placement === 'right') return `translate(${ctx.innerWidth}, 0)`;
50
+ return '';
51
+ });
52
+
53
+ // Outward direction: bottom/right grow positive, top/left negative.
54
+ const dir = $derived(placement === 'bottom' || placement === 'right' ? 1 : -1);
55
+ const labelGap = $derived((tickSize + 3) * dir);
56
+ </script>
57
+
58
+ <g class="fc-axis fc-axis-{isX ? 'x' : 'y'} fc-axis-{placement} {klass}" transform={offset}>
59
+ {#if isX}
60
+ <line class="fc-axis-line" x1="0" x2={ctx.innerWidth} y1="0" y2="0" />
61
+ {:else}
62
+ <line class="fc-axis-line" x1="0" x2="0" y1="0" y2={ctx.innerHeight} />
63
+ {/if}
64
+
65
+ {#each values as value, index (String(value))}
66
+ {@const p = pos(value)}
67
+ {#if Number.isFinite(p)}
68
+ <g class="fc-tick" transform={isX ? `translate(${p}, 0)` : `translate(0, ${p})`}>
69
+ {#if isX}
70
+ <line y2={tickSize * dir} />
71
+ {:else}
72
+ <line x2={tickSize * dir} />
73
+ {/if}
74
+ {#if tick}
75
+ {@render tick({ value, formatted: fmt(value, index), index })}
76
+ {:else if isX}
77
+ <text
78
+ class="fc-tick-label"
79
+ y={rotate ? labelGap - 2 : labelGap}
80
+ dy={dir === 1 ? '0.71em' : '0'}
81
+ text-anchor={rotate ? (dir === 1 ? 'end' : 'start') : 'middle'}
82
+ transform={rotate ? `rotate(${-rotate}, 0, ${labelGap})` : undefined}
83
+ >
84
+ {fmt(value, index)}
85
+ </text>
86
+ {:else}
87
+ <text
88
+ class="fc-tick-label"
89
+ x={labelGap}
90
+ dy="0.32em"
91
+ text-anchor={dir === 1 ? 'start' : 'end'}
92
+ >
93
+ {fmt(value, index)}
94
+ </text>
95
+ {/if}
96
+ </g>
97
+ {/if}
98
+ {/each}
99
+
100
+ {#if title}
101
+ {#if isX}
102
+ <text
103
+ class="fc-axis-title"
104
+ x={ctx.innerWidth / 2}
105
+ y={(tickSize + 26) * dir}
106
+ text-anchor="middle"
107
+ >
108
+ {title}
109
+ </text>
110
+ {:else}
111
+ <text
112
+ class="fc-axis-title"
113
+ transform="rotate(-90)"
114
+ x={-ctx.innerHeight / 2}
115
+ y={(tickSize + 30) * dir}
116
+ text-anchor="middle"
117
+ >
118
+ {title}
119
+ </text>
120
+ {/if}
121
+ {/if}
122
+ </g>
123
+
124
+ <style>
125
+ .fc-axis-line,
126
+ .fc-tick line {
127
+ stroke: var(--fc-axis, #94a3b8);
128
+ fill: none;
129
+ }
130
+ .fc-tick-label {
131
+ fill: var(--fc-axis-label, #64748b);
132
+ font-size: 11px;
133
+ }
134
+ .fc-axis-title {
135
+ fill: var(--fc-axis-label, #64748b);
136
+ font-size: 12px;
137
+ font-weight: 500;
138
+ }
139
+ </style>
@@ -0,0 +1,26 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { XValue } from '../core/normalize.js';
3
+ type Placement = 'top' | 'bottom' | 'left' | 'right';
4
+ interface Props {
5
+ /** bottom/top render the x scale; left/right render the y scale. */
6
+ placement?: Placement;
7
+ /** Tick count hint (continuous scales). Defaults to a density-based count. */
8
+ ticks?: number;
9
+ /** Tick label formatter; defaults to the scale's own (locale/precision aware). */
10
+ format?: (value: XValue, index: number) => string;
11
+ /** Axis title. */
12
+ title?: string;
13
+ tickSize?: number;
14
+ /** Rotate tick labels by N degrees (opt-in, e.g. 45 for crowded x axes). */
15
+ rotate?: number;
16
+ class?: string;
17
+ /** Replace the default tick label text entirely. */
18
+ tick?: Snippet<[{
19
+ value: XValue;
20
+ formatted: string;
21
+ index: number;
22
+ }]>;
23
+ }
24
+ declare const Axis: import("svelte").Component<Props, {}, "">;
25
+ type Axis = ReturnType<typeof Axis>;
26
+ export default Axis;
@@ -0,0 +1,192 @@
1
+ <script lang="ts" generics="T">
2
+ import { onDestroy } from 'svelte';
3
+ import { getChartContext } from '../core/context.svelte.js';
4
+ import { normalizePoints } from '../core/normalize.js';
5
+ import type { NormalizedPoint, XValue } from '../core/normalize.js';
6
+ import { isBandScale } from '../core/scales.js';
7
+ import { stackKey } from '../core/stack.js';
8
+ import type { StackOffset, StackOrder } from '../core/stack.js';
9
+ import { motionValue } from '../core/motion.svelte.js';
10
+
11
+ interface Props {
12
+ data: readonly T[];
13
+ x?: (datum: T, index: number) => XValue;
14
+ y?: (datum: T, index: number) => number | null | undefined;
15
+ color?: string;
16
+ /** Per-point color override (wins over color/palette) — end of the precedence chain. */
17
+ colorFor?: (datum: T, index: number) => string | undefined;
18
+ name?: string;
19
+ /** Accessible series description (announced when keyboard focus enters the series). */
20
+ description?: string;
21
+ /** Per-point announcement override for keyboard navigation. */
22
+ describePoint?: (point: NormalizedPoint<T>) => string | undefined;
23
+ /**
24
+ * Off (default) → side-by-side grouping with other bar series.
25
+ * 'normal' → stack values; 'percent' → stack normalized to ±100.
26
+ * (stream/silhouette also accepted but are intended for areas.)
27
+ */
28
+ stacking?: StackOffset;
29
+ /** Layer ordering within the stack group. */
30
+ order?: StackOrder;
31
+ /** Stack group id — series sharing it stack together (default one shared group). */
32
+ stack?: string;
33
+ /** Fraction of the slot width trimmed from EACH side of a bar (0–0.5). */
34
+ barPadding?: number;
35
+ /** Corner radius. */
36
+ rx?: number;
37
+ /** Override the palette slot. */
38
+ index?: number;
39
+ class?: string;
40
+ }
41
+
42
+ let {
43
+ data,
44
+ x,
45
+ y,
46
+ color,
47
+ colorFor,
48
+ name,
49
+ description,
50
+ describePoint,
51
+ stacking,
52
+ order,
53
+ stack,
54
+ barPadding = 0.08,
55
+ rx = 0,
56
+ index: indexProp,
57
+ class: klass = ''
58
+ }: Props = $props();
59
+
60
+ const ctx = getChartContext();
61
+
62
+ const points = $derived(normalizePoints(data, { x, y }));
63
+ const stackedMeta = $derived(
64
+ stacking ? { id: stack ?? 'fc-stack', offset: stacking, order } : undefined
65
+ );
66
+ // Getter runs lazily after init, so referencing deriveds declared below is safe.
67
+ const registration = ctx.registerSeries(() => ({
68
+ points,
69
+ name,
70
+ color: resolvedColor,
71
+ isBar: true,
72
+ stacked: stackedMeta,
73
+ description,
74
+ describePoint: describePoint as ((p: NormalizedPoint<unknown>) => string | undefined) | undefined
75
+ }));
76
+ onDestroy(registration.unregister);
77
+
78
+ const seriesIndex = $derived(indexProp ?? registration.index);
79
+ const resolvedColor = $derived(ctx.seriesColor(seriesIndex, color));
80
+ const visible = $derived(ctx.isSeriesVisible(registration.index));
81
+
82
+ /** Bar width basis on continuous x scales: smallest gap between point centers. */
83
+ function continuousStep(centers: number[]): number {
84
+ if (centers.length < 2) return 24;
85
+ const sorted = [...centers].sort((a, b) => a - b);
86
+ let minDiff = Infinity;
87
+ for (let i = 1; i < sorted.length; i++) {
88
+ const diff = sorted[i] - sorted[i - 1];
89
+ if (diff > 0 && diff < minDiff) minDiff = diff;
90
+ }
91
+ return Number.isFinite(minDiff) ? minDiff * 0.8 : 24;
92
+ }
93
+
94
+ interface BarRect {
95
+ x: number;
96
+ y: number;
97
+ width: number;
98
+ height: number;
99
+ point: NormalizedPoint<T>;
100
+ }
101
+
102
+ const bars = $derived.by((): BarRect[] => {
103
+ if (!visible) return [];
104
+ const slot = ctx.barSlots.bySeries.get(registration.index);
105
+ if (slot === undefined) return [];
106
+ const slotCount = ctx.barSlots.count;
107
+ const extents = stackedMeta ? ctx.stackFor(registration.index) : undefined;
108
+ const xScale = ctx.xScale;
109
+ const band = isBandScale(xScale);
110
+ const step = band
111
+ ? 0
112
+ : continuousStep(points.map((p) => ctx.xPos(p.x)).filter(Number.isFinite));
113
+ const clampY = (v: number) => Math.max(0, Math.min(ctx.innerHeight, v));
114
+
115
+ const out: BarRect[] = [];
116
+ for (const point of points) {
117
+ if (point.y == null) continue;
118
+
119
+ let groupStart: number;
120
+ let groupWidth: number;
121
+ if (band) {
122
+ const start = xScale(String(point.x));
123
+ if (start === undefined) continue;
124
+ groupStart = start;
125
+ groupWidth = xScale.bandwidth();
126
+ } else {
127
+ const center = ctx.xPos(point.x);
128
+ if (!Number.isFinite(center)) continue;
129
+ groupWidth = step;
130
+ groupStart = center - groupWidth / 2;
131
+ }
132
+
133
+ let v0 = 0;
134
+ let v1 = point.y;
135
+ if (extents) {
136
+ const extent = extents.get(stackKey(point.x));
137
+ if (!extent) continue;
138
+ [v0, v1] = extent;
139
+ }
140
+ const py0 = clampY(ctx.yPos(v0));
141
+ const py1 = clampY(ctx.yPos(v1));
142
+
143
+ const slotWidth = groupWidth / slotCount;
144
+ const inset = slotWidth * Math.max(0, Math.min(0.5, barPadding));
145
+ out.push({
146
+ x: groupStart + slot * slotWidth + inset,
147
+ y: Math.min(py0, py1),
148
+ width: Math.max(0, slotWidth - 2 * inset),
149
+ height: Math.abs(py1 - py0),
150
+ point
151
+ });
152
+ }
153
+ return out;
154
+ });
155
+
156
+ /** Pixel of value 0 — bars grow from this baseline. */
157
+ const baselinePx = $derived(Math.max(0, Math.min(ctx.innerHeight, ctx.yPos(0))));
158
+ const flatTarget = $derived(bars.flatMap((b) => [b.y, b.height]));
159
+ const flatCollapsed = $derived(bars.flatMap(() => [baselinePx, 0]));
160
+
161
+ let rootEl: SVGGElement | undefined = $state();
162
+ const anim = motionValue(
163
+ () => flatTarget,
164
+ () => flatCollapsed,
165
+ () => rootEl
166
+ );
167
+ </script>
168
+
169
+ {#if visible}
170
+ <g class="fc-series fc-bars {klass}" data-name={name} bind:this={rootEl}>
171
+ {#each bars as bar, i (bar.point.index)}
172
+ <rect
173
+ class="fc-bar"
174
+ x={bar.x}
175
+ y={anim.current[i * 2]}
176
+ width={bar.width}
177
+ height={anim.current[i * 2 + 1]}
178
+ {rx}
179
+ fill={colorFor?.(bar.point.datum, bar.point.index) ?? resolvedColor}
180
+ />
181
+ {/each}
182
+ </g>
183
+ {/if}
184
+
185
+ <style>
186
+ .fc-bar {
187
+ transition: opacity var(--fc-duration-hover, 120ms) var(--fc-ease, ease);
188
+ }
189
+ :global(.fc-svg:has(.fc-series:hover)) .fc-series:not(:hover) .fc-bar {
190
+ opacity: 0.35;
191
+ }
192
+ </style>