layerchart 2.0.0-next.47 → 2.0.0-next.49
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/bench/PrimitiveBench.svelte +66 -0
- package/dist/bench/PrimitiveBench.svelte.d.ts +10 -0
- package/dist/bench/primitives.svelte.bench.d.ts +1 -0
- package/dist/bench/primitives.svelte.bench.js +42 -0
- package/dist/components/Axis.svelte +14 -3
- package/dist/components/Axis.svelte.d.ts +1 -1
- package/dist/components/Chart.svelte +110 -12
- package/dist/components/Circle.svelte +20 -17
- package/dist/components/Contour.svelte +90 -13
- package/dist/components/Contour.svelte.d.ts +8 -0
- package/dist/components/Ellipse.svelte +18 -16
- package/dist/components/GeoPath.svelte +1 -1
- package/dist/components/Group.svelte +14 -12
- package/dist/components/Image.svelte +18 -16
- package/dist/components/Labels.svelte +56 -11
- package/dist/components/Labels.svelte.d.ts +3 -2
- package/dist/components/Line.svelte +18 -16
- package/dist/components/LinearGradient.svelte +1 -1
- package/dist/components/Marker.svelte +8 -3
- package/dist/components/Marker.svelte.d.ts +1 -1
- package/dist/components/Month.svelte +273 -0
- package/dist/components/Month.svelte.d.ts +70 -0
- package/dist/components/Path.svelte +28 -12
- package/dist/components/Polygon.svelte +25 -23
- package/dist/components/RadialGradient.svelte +1 -1
- package/dist/components/Raster.svelte +117 -29
- package/dist/components/Raster.svelte.d.ts +8 -0
- package/dist/components/Rect.svelte +26 -20
- package/dist/components/Spline.svelte +123 -25
- package/dist/components/Spline.svelte.d.ts +18 -1
- package/dist/components/Text.svelte +45 -20
- package/dist/components/Text.svelte.d.ts +6 -0
- package/dist/components/TransformContext.svelte +8 -0
- package/dist/components/TransformContext.svelte.test.d.ts +1 -0
- package/dist/components/TransformContext.svelte.test.js +166 -0
- package/dist/components/Vector.svelte +14 -12
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/tests/TransformTestHarness.svelte +27 -0
- package/dist/components/tests/TransformTestHarness.svelte.d.ts +8 -0
- package/dist/states/__fixtures__/ComponentNodeLifecycleChild.svelte +1 -1
- package/dist/states/__screenshots__/chart.component-node.svelte.test.ts/ChartState-registerComponent-cleans-up-child-nodes-and-mark-registrations-when-components-unmount-1.png +0 -0
- package/dist/states/__screenshots__/chart.component-node.svelte.test.ts/ChartState-registerComponent-cleans-up-child-nodes-and-mark-registrations-when-components-unmount-2.png +0 -0
- package/dist/states/brush.svelte.d.ts +26 -17
- package/dist/states/brush.svelte.js +118 -25
- package/dist/states/brush.svelte.test.js +126 -1
- package/dist/states/chart.svelte.d.ts +6 -0
- package/dist/states/chart.svelte.js +100 -21
- package/dist/states/chart.svelte.test.js +16 -1
- package/dist/states/transform.svelte.js +3 -1
- package/dist/utils/dataProp.d.ts +2 -10
- package/dist/utils/dataProp.js +16 -5
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/motion.svelte.d.ts +12 -2
- package/dist/utils/motion.svelte.js +22 -0
- package/dist/utils/motion.test.js +49 -1
- package/dist/utils/rasterBounds.d.ts +18 -0
- package/dist/utils/rasterBounds.js +98 -0
- package/dist/utils/rasterBounds.test.d.ts +1 -0
- package/dist/utils/rasterBounds.test.js +63 -0
- package/dist/utils/scales.svelte.js +4 -2
- package/dist/utils/scales.svelte.test.d.ts +1 -0
- package/dist/utils/scales.svelte.test.js +67 -0
- package/dist/utils/ticks.js +7 -3
- package/dist/utils/ticks.test.js +13 -3
- package/package.json +3 -2
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { accessor, type Accessor } from '../utils/common.js';
|
|
7
7
|
import type { MotionProp } from '../utils/motion.svelte.js';
|
|
8
|
+
import type { ColorProp, StyleProp } from '../utils/dataProp.js';
|
|
8
9
|
|
|
9
10
|
export type SplinePropsWithoutHTML = {
|
|
10
11
|
/**
|
|
@@ -46,12 +47,31 @@
|
|
|
46
47
|
*/
|
|
47
48
|
curve?: CurveFactory | CurveFactoryLineOnly;
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Stroke color or function returning stroke per data point.
|
|
52
|
+
* When a function, consecutive points with the same value are grouped
|
|
53
|
+
* into separate path segments, enabling per-segment styling.
|
|
54
|
+
*/
|
|
55
|
+
stroke?: ColorProp;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fill color or function returning fill per data point.
|
|
59
|
+
* When a function, acts as a grouping channel like `stroke`.
|
|
60
|
+
*/
|
|
61
|
+
fill?: ColorProp;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Opacity or function returning opacity per data point.
|
|
65
|
+
* When a function, acts as a grouping channel like `stroke`.
|
|
66
|
+
*/
|
|
67
|
+
opacity?: StyleProp<number | undefined>;
|
|
68
|
+
|
|
49
69
|
/**
|
|
50
70
|
* Whether to animate the path using tweened interpolation.
|
|
51
71
|
* When set, the line will animate from the baseline (y=0) on mount.
|
|
52
72
|
*/
|
|
53
73
|
motion?: MotionProp;
|
|
54
|
-
} & Omit<PathProps, 'x' | 'y' | 'motion'>;
|
|
74
|
+
} & Omit<PathProps, 'x' | 'y' | 'motion' | 'stroke' | 'fill' | 'opacity'>;
|
|
55
75
|
|
|
56
76
|
export type SplineProps = SplinePropsWithoutHTML &
|
|
57
77
|
Without<SVGAttributes<SVGPathElement>, SplinePropsWithoutHTML>;
|
|
@@ -68,6 +88,7 @@
|
|
|
68
88
|
import { isScaleBand } from '../utils/scales.svelte.js';
|
|
69
89
|
import { getChartContext } from '../contexts/chart.js';
|
|
70
90
|
import { getGeoContext } from '../contexts/geo.js';
|
|
91
|
+
import { resolveColorProp, resolveStyleProp } from '../utils/dataProp.js';
|
|
71
92
|
import Path, { type PathProps } from './Path.svelte';
|
|
72
93
|
import {
|
|
73
94
|
createMotion,
|
|
@@ -78,12 +99,12 @@
|
|
|
78
99
|
const ctx = getChartContext();
|
|
79
100
|
const geo = getGeoContext();
|
|
80
101
|
|
|
81
|
-
let { data, x, y, seriesKey, defined, curve, stroke, motion, ...restProps }: SplineProps = $props();
|
|
102
|
+
let { data, x, y, seriesKey, defined, curve, stroke, fill, opacity, motion, ...restProps }: SplineProps = $props();
|
|
82
103
|
|
|
83
104
|
ctx.registerComponent({
|
|
84
105
|
name: 'Spline',
|
|
85
106
|
kind: 'mark',
|
|
86
|
-
markInfo: () => ({ data, x, y, seriesKey, color: stroke
|
|
107
|
+
markInfo: () => ({ data, x, y, seriesKey, color: typeof stroke === 'string' ? stroke : undefined }),
|
|
87
108
|
});
|
|
88
109
|
|
|
89
110
|
function getScaleValue(
|
|
@@ -119,7 +140,25 @@
|
|
|
119
140
|
const xOffset = $derived(isScaleBand(ctx.xScale) ? ctx.xScale.bandwidth() / 2 : 0);
|
|
120
141
|
const yOffset = $derived(isScaleBand(ctx.yScale) ? ctx.yScale.bandwidth() / 2 : 0);
|
|
121
142
|
|
|
143
|
+
function buildPath(resolvedData: any[]) {
|
|
144
|
+
const path = ctx.radial
|
|
145
|
+
? lineRadial()
|
|
146
|
+
.angle((d) => getScaleValue(d, ctx.xScale, xAccessor) + 0) // Never apply xOffset (LineChart radar, BarChart radial, ...)?
|
|
147
|
+
.radius((d) => getScaleValue(d, ctx.yScale, yAccessor) + yOffset)
|
|
148
|
+
: d3Line()
|
|
149
|
+
.x((d) => getScaleValue(d, ctx.xScale, xAccessor) + xOffset)
|
|
150
|
+
.y((d) => getScaleValue(d, ctx.yScale, yAccessor) + yOffset);
|
|
151
|
+
|
|
152
|
+
path.defined(defined ?? ((d) => xAccessor(d) != null && yAccessor(d) != null));
|
|
153
|
+
if (curve) path.curve(curve);
|
|
154
|
+
|
|
155
|
+
return path(resolvedData) ?? '';
|
|
156
|
+
}
|
|
157
|
+
|
|
122
158
|
const d = $derived.by(() => {
|
|
159
|
+
// Skip full-path computation when segments mode handles rendering
|
|
160
|
+
if (hasAnyStyleFn && !geo.projection) return '';
|
|
161
|
+
|
|
123
162
|
const resolvedData = data ?? series?.data ?? ctx.data;
|
|
124
163
|
|
|
125
164
|
// Geo mode: convert data to GeoJSON LineString and render via geoPath
|
|
@@ -135,19 +174,61 @@
|
|
|
135
174
|
return d3GeoPath(geo.projection)(lineString) ?? '';
|
|
136
175
|
}
|
|
137
176
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.angle((d) => getScaleValue(d, ctx.xScale, xAccessor) + 0) // Never apply xOffset (LineChart radar, BarChart radial, ...)?
|
|
177
|
+
return buildPath(resolvedData);
|
|
178
|
+
});
|
|
141
179
|
|
|
142
|
-
|
|
143
|
-
: d3Line()
|
|
144
|
-
.x((d) => getScaleValue(d, ctx.xScale, xAccessor) + xOffset)
|
|
145
|
-
.y((d) => getScaleValue(d, ctx.yScale, yAccessor) + yOffset);
|
|
180
|
+
type SegmentStyle = { stroke?: string; fill?: string; opacity?: number };
|
|
146
181
|
|
|
147
|
-
|
|
148
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Groups consecutive data points by a composite key derived from function-valued style props.
|
|
184
|
+
* The key at index `i` determines the style for the segment from point `i` to point `i+1`.
|
|
185
|
+
* Each group includes an overlap of 1 point at boundaries for curve continuity.
|
|
186
|
+
*/
|
|
187
|
+
function groupConsecutive(
|
|
188
|
+
data: any[],
|
|
189
|
+
keyFn: (d: any, index: number, data: any[]) => { key: string; style: SegmentStyle }
|
|
190
|
+
): Array<{ style: SegmentStyle; data: any[] }> {
|
|
191
|
+
if (data.length < 2) return [];
|
|
192
|
+
|
|
193
|
+
const groups: Array<{ style: SegmentStyle; data: any[] }> = [];
|
|
194
|
+
let current = keyFn(data[0], 0, data);
|
|
195
|
+
let startIdx = 0;
|
|
196
|
+
|
|
197
|
+
for (let i = 1; i < data.length; i++) {
|
|
198
|
+
const next = keyFn(data[i], i, data);
|
|
199
|
+
if (next.key !== current.key) {
|
|
200
|
+
groups.push({ style: current.style, data: data.slice(startIdx, i + 1) });
|
|
201
|
+
startIdx = i;
|
|
202
|
+
current = next;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (data.length - startIdx >= 2) {
|
|
206
|
+
groups.push({ style: current.style, data: data.slice(startIdx) });
|
|
207
|
+
}
|
|
149
208
|
|
|
150
|
-
return
|
|
209
|
+
return groups;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const hasAnyStyleFn = $derived(
|
|
213
|
+
typeof stroke === 'function' || typeof fill === 'function' || typeof opacity === 'function'
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const segments = $derived.by(() => {
|
|
217
|
+
if (!hasAnyStyleFn) return null;
|
|
218
|
+
const resolvedData = data ?? series?.data ?? ctx.data;
|
|
219
|
+
if (geo.projection) return null;
|
|
220
|
+
|
|
221
|
+
const groups = groupConsecutive(resolvedData, (d, i, arr) => {
|
|
222
|
+
const s = resolveColorProp(stroke, d, ctx.cScale, i, arr);
|
|
223
|
+
const f = resolveColorProp(fill, d, ctx.cScale, i, arr);
|
|
224
|
+
const o = resolveStyleProp(opacity, d, i, arr);
|
|
225
|
+
return { key: `${s}\0${f}\0${o}`, style: { stroke: s, fill: f, opacity: o } };
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return groups.map((group) => ({
|
|
229
|
+
...group.style,
|
|
230
|
+
d: buildPath(group.data),
|
|
231
|
+
}));
|
|
151
232
|
});
|
|
152
233
|
|
|
153
234
|
const extractedTween = extractTweenConfig(motion);
|
|
@@ -191,20 +272,37 @@
|
|
|
191
272
|
}
|
|
192
273
|
|
|
193
274
|
const tweenState = createMotion(defaultPathData(), () => d, tweenOptions);
|
|
275
|
+
|
|
276
|
+
const seriesOpacity = $derived(
|
|
277
|
+
series?.key == null ||
|
|
278
|
+
ctx.series.visibleSeries.length <= 1 ||
|
|
279
|
+
ctx.series.isHighlighted(series.key, true)
|
|
280
|
+
? 1
|
|
281
|
+
: 0.1
|
|
282
|
+
);
|
|
194
283
|
</script>
|
|
195
284
|
|
|
196
285
|
<!-- TODO: handle in LineChart/etc? -->
|
|
197
286
|
<!-- class: cls(props.spline?.class, s.props?.class), -->
|
|
198
287
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{
|
|
210
|
-
|
|
288
|
+
{#if segments}
|
|
289
|
+
{#each segments as seg, i (i)}
|
|
290
|
+
<Path
|
|
291
|
+
pathData={seg.d}
|
|
292
|
+
stroke={seg.stroke}
|
|
293
|
+
fill={seg.fill}
|
|
294
|
+
{...series?.props}
|
|
295
|
+
{...restProps}
|
|
296
|
+
opacity={seg.opacity ?? seriesOpacity}
|
|
297
|
+
/>
|
|
298
|
+
{/each}
|
|
299
|
+
{:else}
|
|
300
|
+
<Path
|
|
301
|
+
pathData={tweenOptions ? tweenState.current : d}
|
|
302
|
+
stroke={(typeof stroke === 'string' ? stroke : undefined) ?? series?.color}
|
|
303
|
+
fill={typeof fill === 'string' ? fill : undefined}
|
|
304
|
+
{...series?.props}
|
|
305
|
+
{...restProps}
|
|
306
|
+
opacity={(typeof opacity === 'number' ? opacity : undefined) ?? seriesOpacity}
|
|
307
|
+
/>
|
|
308
|
+
{/if}
|
|
@@ -3,6 +3,7 @@ import type { SVGAttributes } from 'svelte/elements';
|
|
|
3
3
|
import type { CurveFactory, CurveFactoryLineOnly, Line } from 'd3-shape';
|
|
4
4
|
import { type Accessor } from '../utils/common.js';
|
|
5
5
|
import type { MotionProp } from '../utils/motion.svelte.js';
|
|
6
|
+
import type { ColorProp, StyleProp } from '../utils/dataProp.js';
|
|
6
7
|
export type SplinePropsWithoutHTML = {
|
|
7
8
|
/**
|
|
8
9
|
* Override data instead of using context
|
|
@@ -37,12 +38,28 @@ export type SplinePropsWithoutHTML = {
|
|
|
37
38
|
* @type {CurveFactory | CurveFactoryLineOnly | undefined}
|
|
38
39
|
*/
|
|
39
40
|
curve?: CurveFactory | CurveFactoryLineOnly;
|
|
41
|
+
/**
|
|
42
|
+
* Stroke color or function returning stroke per data point.
|
|
43
|
+
* When a function, consecutive points with the same value are grouped
|
|
44
|
+
* into separate path segments, enabling per-segment styling.
|
|
45
|
+
*/
|
|
46
|
+
stroke?: ColorProp;
|
|
47
|
+
/**
|
|
48
|
+
* Fill color or function returning fill per data point.
|
|
49
|
+
* When a function, acts as a grouping channel like `stroke`.
|
|
50
|
+
*/
|
|
51
|
+
fill?: ColorProp;
|
|
52
|
+
/**
|
|
53
|
+
* Opacity or function returning opacity per data point.
|
|
54
|
+
* When a function, acts as a grouping channel like `stroke`.
|
|
55
|
+
*/
|
|
56
|
+
opacity?: StyleProp<number | undefined>;
|
|
40
57
|
/**
|
|
41
58
|
* Whether to animate the path using tweened interpolation.
|
|
42
59
|
* When set, the line will animate from the baseline (y=0) on mount.
|
|
43
60
|
*/
|
|
44
61
|
motion?: MotionProp;
|
|
45
|
-
} & Omit<PathProps, 'x' | 'y' | 'motion'>;
|
|
62
|
+
} & Omit<PathProps, 'x' | 'y' | 'motion' | 'stroke' | 'fill' | 'opacity'>;
|
|
46
63
|
export type SplineProps = SplinePropsWithoutHTML & Without<SVGAttributes<SVGPathElement>, SplinePropsWithoutHTML>;
|
|
47
64
|
import { type PathProps } from './Path.svelte';
|
|
48
65
|
declare const Spline: import("svelte").Component<SplineProps, {}, "">;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { DataDrivenStyleProps } from '../utils/dataProp.js';
|
|
4
4
|
import type { SVGAttributes } from 'svelte/elements';
|
|
5
5
|
import { createMotion, type MotionProp } from '../utils/motion.svelte.js';
|
|
6
|
+
import type { FormatType, FormatConfig } from '@layerstack/utils';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Check if a string looks like a CSS/SVG value (percentage, em, px, etc.)
|
|
@@ -163,6 +164,12 @@
|
|
|
163
164
|
*/
|
|
164
165
|
ref?: SVGTextElement;
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Format the displayed value. When set with `motion` and a numeric `value`,
|
|
169
|
+
* the number will tween smoothly and be formatted for display.
|
|
170
|
+
*/
|
|
171
|
+
format?: FormatType | FormatConfig;
|
|
172
|
+
|
|
166
173
|
/** Motion configuration (pixel mode only). */
|
|
167
174
|
motion?: MotionProp;
|
|
168
175
|
|
|
@@ -224,7 +231,7 @@
|
|
|
224
231
|
<script lang="ts">
|
|
225
232
|
import { untrack } from 'svelte';
|
|
226
233
|
import { cls } from '@layerstack/tailwind';
|
|
227
|
-
import { merge } from '@layerstack/utils';
|
|
234
|
+
import { merge, format as formatValue } from '@layerstack/utils';
|
|
228
235
|
|
|
229
236
|
import { getLayerContext } from '../contexts/layer.js';
|
|
230
237
|
import { getChartContext } from '../contexts/chart.js';
|
|
@@ -263,6 +270,7 @@
|
|
|
263
270
|
stroke,
|
|
264
271
|
fill,
|
|
265
272
|
fillOpacity,
|
|
273
|
+
format,
|
|
266
274
|
motion,
|
|
267
275
|
svgRef: svgRefProp = $bindable(),
|
|
268
276
|
ref: refProp = $bindable(),
|
|
@@ -318,18 +326,20 @@
|
|
|
318
326
|
// --- Data mode motion ---
|
|
319
327
|
const dataMotionMap = createDataMotionMap(motion);
|
|
320
328
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
if (dataMotionMap) {
|
|
330
|
+
$effect(() => {
|
|
331
|
+
if (!dataMode) return;
|
|
332
|
+
const activeKeys = new Set<any>();
|
|
333
|
+
for (let i = 0; i < resolvedData.length; i++) {
|
|
334
|
+
const d = resolvedData[i];
|
|
335
|
+
const key = keyFn(d, i);
|
|
336
|
+
activeKeys.add(key);
|
|
337
|
+
const resolved = resolveTextPosition(d);
|
|
338
|
+
untrack(() => dataMotionMap.update(key, resolved));
|
|
339
|
+
}
|
|
340
|
+
untrack(() => dataMotionMap.cleanup(activeKeys));
|
|
341
|
+
});
|
|
342
|
+
}
|
|
333
343
|
|
|
334
344
|
// Single source of truth: resolved values with animated overlay
|
|
335
345
|
const resolvedItems = $derived.by(() => {
|
|
@@ -387,11 +397,26 @@
|
|
|
387
397
|
};
|
|
388
398
|
});
|
|
389
399
|
|
|
390
|
-
//
|
|
391
|
-
const
|
|
392
|
-
typeof value
|
|
400
|
+
// Tween numeric values when motion is configured
|
|
401
|
+
const motionValue = createMotion(
|
|
402
|
+
typeof value === 'number' ? value : 0,
|
|
403
|
+
() => (typeof value === 'number' ? value : 0),
|
|
404
|
+
typeof value === 'number' && motion ? (typeof motion === 'object' && 'type' in motion ? motion : undefined) : undefined
|
|
393
405
|
);
|
|
394
406
|
|
|
407
|
+
// Handle null and convert `\n` strings back to newline characters
|
|
408
|
+
const rawText = $derived.by(() => {
|
|
409
|
+
if (typeof value === 'function' || value == null) return '';
|
|
410
|
+
if (typeof value === 'number' && motion) {
|
|
411
|
+
const v = motionValue.current;
|
|
412
|
+
// @ts-expect-error - improve format types
|
|
413
|
+
return format ? formatValue(v, format) : String(v);
|
|
414
|
+
}
|
|
415
|
+
// @ts-expect-error - improve format types
|
|
416
|
+
const text = format ? formatValue(value, format) : value.toString();
|
|
417
|
+
return text.replace(/\\n/g, '\n');
|
|
418
|
+
});
|
|
419
|
+
|
|
395
420
|
const textValue = $derived.by(() => {
|
|
396
421
|
if (!truncateConfig) return rawText;
|
|
397
422
|
return truncateText(rawText, truncateConfig);
|
|
@@ -628,8 +653,8 @@
|
|
|
628
653
|
}
|
|
629
654
|
|
|
630
655
|
// TODO: Use objectId to work around Svelte 4 reactivity issue (even when memoizing gradients)
|
|
631
|
-
const fillKey = createKey(() => fill);
|
|
632
|
-
const strokeKey = createKey(() => stroke);
|
|
656
|
+
const fillKey = layerCtx === 'canvas' ? createKey(() => fill) : undefined;
|
|
657
|
+
const strokeKey = layerCtx === 'canvas' ? createKey(() => stroke) : undefined;
|
|
633
658
|
|
|
634
659
|
chartCtx.registerComponent({
|
|
635
660
|
name: 'Text',
|
|
@@ -651,8 +676,8 @@
|
|
|
651
676
|
value,
|
|
652
677
|
motionX.current,
|
|
653
678
|
motionY.current,
|
|
654
|
-
fillKey
|
|
655
|
-
strokeKey
|
|
679
|
+
fillKey!.current,
|
|
680
|
+
strokeKey!.current,
|
|
656
681
|
strokeWidth,
|
|
657
682
|
opacity,
|
|
658
683
|
className,
|
|
@@ -2,6 +2,7 @@ import type { Without } from '../utils/types.js';
|
|
|
2
2
|
import type { DataDrivenStyleProps } from '../utils/dataProp.js';
|
|
3
3
|
import type { SVGAttributes } from 'svelte/elements';
|
|
4
4
|
import { type MotionProp } from '../utils/motion.svelte.js';
|
|
5
|
+
import type { FormatType, FormatConfig } from '@layerstack/utils';
|
|
5
6
|
/**
|
|
6
7
|
* Check if a Text prop value is a data-space prop.
|
|
7
8
|
* Functions are always data props.
|
|
@@ -122,6 +123,11 @@ export type TextPropsWithoutHTML = {
|
|
|
122
123
|
* @bindable
|
|
123
124
|
*/
|
|
124
125
|
ref?: SVGTextElement;
|
|
126
|
+
/**
|
|
127
|
+
* Format the displayed value. When set with `motion` and a numeric `value`,
|
|
128
|
+
* the number will tween smoothly and be formatted for display.
|
|
129
|
+
*/
|
|
130
|
+
format?: FormatType | FormatConfig;
|
|
125
131
|
/** Motion configuration (pixel mode only). */
|
|
126
132
|
motion?: MotionProp;
|
|
127
133
|
/**
|
|
@@ -110,6 +110,14 @@
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
$effect.pre(() => {
|
|
114
|
+
transformState.processTranslate = processTranslate;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
$effect.pre(() => {
|
|
118
|
+
transformState.disablePointer = disablePointer ?? false;
|
|
119
|
+
});
|
|
120
|
+
|
|
113
121
|
$effect.pre(() => {
|
|
114
122
|
transformState.constrain = constrain;
|
|
115
123
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { render } from 'vitest-browser-svelte';
|
|
3
|
+
import { tick } from 'svelte';
|
|
4
|
+
import TransformTestHarness from './tests/TransformTestHarness.svelte';
|
|
5
|
+
import { geoMercator, geoOrthographic } from 'd3-geo';
|
|
6
|
+
describe('TransformContext', () => {
|
|
7
|
+
describe('reactive prop syncing', () => {
|
|
8
|
+
it('should sync processTranslate when projection changes from flat to globe', async () => {
|
|
9
|
+
let chartContext;
|
|
10
|
+
const chartProps = $state({
|
|
11
|
+
height: 300,
|
|
12
|
+
geo: {
|
|
13
|
+
projection: geoMercator,
|
|
14
|
+
fitGeojson: { type: 'Sphere' },
|
|
15
|
+
},
|
|
16
|
+
transform: {
|
|
17
|
+
mode: 'projection',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
render(TransformTestHarness, {
|
|
21
|
+
chartProps,
|
|
22
|
+
oncontext: (ctx) => {
|
|
23
|
+
chartContext = ctx;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
await vi.waitFor(() => expect(chartContext).toBeDefined());
|
|
27
|
+
// Mercator is a flat projection — processTranslate should be undefined
|
|
28
|
+
expect(chartContext.transform.processTranslate).toBeUndefined();
|
|
29
|
+
// Switch to orthographic (globe)
|
|
30
|
+
chartProps.geo = {
|
|
31
|
+
projection: geoOrthographic,
|
|
32
|
+
fitGeojson: { type: 'Sphere' },
|
|
33
|
+
};
|
|
34
|
+
await tick();
|
|
35
|
+
// Orthographic is a globe — processTranslate should now be a function
|
|
36
|
+
await vi.waitFor(() => {
|
|
37
|
+
expect(chartContext.transform.processTranslate).toBeTypeOf('function');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it('should sync processTranslate when projection changes from globe to flat', async () => {
|
|
41
|
+
let chartContext;
|
|
42
|
+
const chartProps = $state({
|
|
43
|
+
height: 300,
|
|
44
|
+
geo: {
|
|
45
|
+
projection: geoOrthographic,
|
|
46
|
+
fitGeojson: { type: 'Sphere' },
|
|
47
|
+
},
|
|
48
|
+
transform: {
|
|
49
|
+
mode: 'projection',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
render(TransformTestHarness, {
|
|
53
|
+
chartProps,
|
|
54
|
+
oncontext: (ctx) => {
|
|
55
|
+
chartContext = ctx;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
await vi.waitFor(() => expect(chartContext).toBeDefined());
|
|
59
|
+
// Orthographic is a globe — processTranslate should be a function
|
|
60
|
+
expect(chartContext.transform.processTranslate).toBeTypeOf('function');
|
|
61
|
+
// Switch to Mercator (flat)
|
|
62
|
+
chartProps.geo = {
|
|
63
|
+
projection: geoMercator,
|
|
64
|
+
fitGeojson: { type: 'Sphere' },
|
|
65
|
+
};
|
|
66
|
+
await tick();
|
|
67
|
+
// Mercator is flat — processTranslate should be undefined
|
|
68
|
+
await vi.waitFor(() => {
|
|
69
|
+
expect(chartContext.transform.processTranslate).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it('should enable scale for globe projections so scroll zoom works', async () => {
|
|
73
|
+
let chartContext;
|
|
74
|
+
render(TransformTestHarness, {
|
|
75
|
+
chartProps: {
|
|
76
|
+
height: 300,
|
|
77
|
+
geo: {
|
|
78
|
+
projection: geoOrthographic,
|
|
79
|
+
fitGeojson: { type: 'Sphere' },
|
|
80
|
+
},
|
|
81
|
+
transform: {
|
|
82
|
+
mode: 'projection',
|
|
83
|
+
scrollMode: 'scale',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
oncontext: (ctx) => {
|
|
87
|
+
chartContext = ctx;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
await vi.waitFor(() => expect(chartContext).toBeDefined());
|
|
91
|
+
const initialScale = chartContext.transform.scale;
|
|
92
|
+
expect(initialScale).toBeGreaterThan(1); // fitSize should give a scale > 1
|
|
93
|
+
// Simulate zoom in
|
|
94
|
+
chartContext.transform.setScale(initialScale * 2, { instant: true });
|
|
95
|
+
await tick();
|
|
96
|
+
await vi.waitFor(() => {
|
|
97
|
+
expect(chartContext.transform.scale).toBeCloseTo(initialScale * 2, 0);
|
|
98
|
+
});
|
|
99
|
+
// Verify the projection scale also updated (transformApply.scale = true for globes)
|
|
100
|
+
expect(chartContext.geo.projection?.scale()).toBeCloseTo(initialScale * 2, 0);
|
|
101
|
+
});
|
|
102
|
+
it('should interpret scaleExtent as relative multipliers in projection mode', async () => {
|
|
103
|
+
let chartContext;
|
|
104
|
+
render(TransformTestHarness, {
|
|
105
|
+
chartProps: {
|
|
106
|
+
height: 300,
|
|
107
|
+
geo: {
|
|
108
|
+
projection: geoMercator,
|
|
109
|
+
fitGeojson: { type: 'Sphere' },
|
|
110
|
+
},
|
|
111
|
+
transform: {
|
|
112
|
+
mode: 'projection',
|
|
113
|
+
scrollMode: 'scale',
|
|
114
|
+
scaleExtent: [0.5, 2],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
oncontext: (ctx) => {
|
|
118
|
+
chartContext = ctx;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
await vi.waitFor(() => expect(chartContext).toBeDefined());
|
|
122
|
+
const initialScale = chartContext.transform.scale;
|
|
123
|
+
expect(initialScale).toBeGreaterThan(10); // Mercator fitSize scale is typically large
|
|
124
|
+
// Try to zoom way beyond 2x — should be clamped to 2x initial
|
|
125
|
+
chartContext.transform.setScale(initialScale * 5, { instant: true });
|
|
126
|
+
await tick();
|
|
127
|
+
await vi.waitFor(() => {
|
|
128
|
+
// Should be clamped to ~2x the initial scale
|
|
129
|
+
expect(chartContext.transform.scale).toBeCloseTo(initialScale * 2, 0);
|
|
130
|
+
});
|
|
131
|
+
// Try to zoom below 0.5x — should be clamped to 0.5x initial
|
|
132
|
+
chartContext.transform.setScale(initialScale * 0.1, { instant: true });
|
|
133
|
+
await tick();
|
|
134
|
+
await vi.waitFor(() => {
|
|
135
|
+
expect(chartContext.transform.scale).toBeCloseTo(initialScale * 0.5, 0);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
it('should sync disablePointer reactively', async () => {
|
|
139
|
+
let chartContext;
|
|
140
|
+
const chartProps = $state({
|
|
141
|
+
height: 300,
|
|
142
|
+
transform: {
|
|
143
|
+
mode: 'canvas',
|
|
144
|
+
disablePointer: false,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
render(TransformTestHarness, {
|
|
148
|
+
chartProps,
|
|
149
|
+
oncontext: (ctx) => {
|
|
150
|
+
chartContext = ctx;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
await vi.waitFor(() => expect(chartContext).toBeDefined());
|
|
154
|
+
expect(chartContext.transform.disablePointer).toBe(false);
|
|
155
|
+
// Enable disablePointer
|
|
156
|
+
chartProps.transform = {
|
|
157
|
+
mode: 'canvas',
|
|
158
|
+
disablePointer: true,
|
|
159
|
+
};
|
|
160
|
+
await tick();
|
|
161
|
+
await vi.waitFor(() => {
|
|
162
|
+
expect(chartContext.transform.disablePointer).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -208,18 +208,20 @@
|
|
|
208
208
|
// --- Data mode motion ---
|
|
209
209
|
const dataMotionMap = createDataMotionMap(motion);
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
211
|
+
if (dataMotionMap) {
|
|
212
|
+
$effect(() => {
|
|
213
|
+
if (!dataMode) return;
|
|
214
|
+
const activeKeys = new Set<any>();
|
|
215
|
+
for (let i = 0; i < resolvedData.length; i++) {
|
|
216
|
+
const d = resolvedData[i];
|
|
217
|
+
const key = keyFn(d, i);
|
|
218
|
+
activeKeys.add(key);
|
|
219
|
+
const resolved = resolveVector(d);
|
|
220
|
+
untrack(() => dataMotionMap.update(key, resolved));
|
|
221
|
+
}
|
|
222
|
+
untrack(() => dataMotionMap.cleanup(activeKeys));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
223
225
|
|
|
224
226
|
// Single source of truth: resolved values with animated overlay
|
|
225
227
|
const resolvedItems = $derived.by(() => {
|
|
@@ -104,6 +104,8 @@ export { default as Link } from './Link.svelte';
|
|
|
104
104
|
export * from './Link.svelte';
|
|
105
105
|
export { default as MotionPath } from './MotionPath.svelte';
|
|
106
106
|
export * from './MotionPath.svelte';
|
|
107
|
+
export { default as Month } from './Month.svelte';
|
|
108
|
+
export * from './Month.svelte';
|
|
107
109
|
export { default as Pack } from './Pack.svelte';
|
|
108
110
|
export * from './Pack.svelte';
|
|
109
111
|
export { default as Partition } from './Partition.svelte';
|
package/dist/components/index.js
CHANGED
|
@@ -104,6 +104,8 @@ export { default as Link } from './Link.svelte';
|
|
|
104
104
|
export * from './Link.svelte';
|
|
105
105
|
export { default as MotionPath } from './MotionPath.svelte';
|
|
106
106
|
export * from './MotionPath.svelte';
|
|
107
|
+
export { default as Month } from './Month.svelte';
|
|
108
|
+
export * from './Month.svelte';
|
|
107
109
|
export { default as Pack } from './Pack.svelte';
|
|
108
110
|
export * from './Pack.svelte';
|
|
109
111
|
export { default as Partition } from './Partition.svelte';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Chart from '../Chart.svelte';
|
|
3
|
+
import type { ChartState } from '../../states/chart.svelte.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
chartProps = {},
|
|
7
|
+
oncontext,
|
|
8
|
+
}: {
|
|
9
|
+
chartProps?: Record<string, any>;
|
|
10
|
+
oncontext?: (ctx: ChartState<any, any, any>) => void;
|
|
11
|
+
} = $props();
|
|
12
|
+
|
|
13
|
+
let chartContext = $state<ChartState<any, any, any>>();
|
|
14
|
+
|
|
15
|
+
$effect(() => {
|
|
16
|
+
if (chartContext) {
|
|
17
|
+
oncontext?.(chartContext);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const mergedChartProps = $derived({
|
|
22
|
+
height: 300,
|
|
23
|
+
...chartProps,
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<Chart {...mergedChartProps} bind:context={chartContext} />
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChartState } from '../../states/chart.svelte.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
chartProps?: Record<string, any>;
|
|
4
|
+
oncontext?: (ctx: ChartState<any, any, any>) => void;
|
|
5
|
+
};
|
|
6
|
+
declare const TransformTestHarness: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type TransformTestHarness = ReturnType<typeof TransformTestHarness>;
|
|
8
|
+
export default TransformTestHarness;
|