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
|
@@ -100,7 +100,9 @@ export class TransformState {
|
|
|
100
100
|
if (this.processTranslate)
|
|
101
101
|
return this.processTranslate(x, y, deltaX, deltaY);
|
|
102
102
|
if (this.mode === 'domain') {
|
|
103
|
-
// Negate deltaY because screen Y (top→bottom) is inverted vs data Y (bottom→top)
|
|
103
|
+
// Negate deltaY because screen Y (top→bottom) is inverted vs data Y (bottom→top).
|
|
104
|
+
// This works for both normal and reversed Y domains because _computeTransformDomain
|
|
105
|
+
// uses signed range, which naturally handles the reversal.
|
|
104
106
|
if (this.axis === 'x')
|
|
105
107
|
return { x: x + deltaX, y: 0 };
|
|
106
108
|
if (this.axis === 'y')
|
package/dist/utils/dataProp.d.ts
CHANGED
|
@@ -103,18 +103,10 @@ export type DataDrivenStyleProps<T = any> = {
|
|
|
103
103
|
*/
|
|
104
104
|
class?: StyleProp<string | undefined, T>;
|
|
105
105
|
};
|
|
106
|
-
|
|
107
|
-
* Resolves a ColorProp for a specific data item, optionally through a color scale.
|
|
108
|
-
*
|
|
109
|
-
* - `string`: checks if `get(d, value)` is defined → data property, passed through cScale.
|
|
110
|
-
* Otherwise returns the string as a literal CSS color.
|
|
111
|
-
* - `function`: called with data item, result passed through cScale.
|
|
112
|
-
* - `undefined`/`null`: returns undefined.
|
|
113
|
-
*/
|
|
114
|
-
export declare function resolveColorProp<T>(value: ColorProp<T> | undefined | null, d: T, cScale: AnyScale | null | undefined): string | undefined;
|
|
106
|
+
export declare function resolveColorProp<T>(value: ColorProp<T> | undefined | null, d: T, cScale: AnyScale | null | undefined, ...args: any[]): string | undefined;
|
|
115
107
|
/**
|
|
116
108
|
* Resolves a StyleProp for a specific data item.
|
|
117
109
|
* If the value is a function, calls it with the data item.
|
|
118
110
|
* Otherwise returns the static value.
|
|
119
111
|
*/
|
|
120
|
-
export declare function resolveStyleProp<V, T>(value: StyleProp<V, T> | undefined, d: T): V | undefined;
|
|
112
|
+
export declare function resolveStyleProp<V, T>(value: StyleProp<V, T> | undefined, d: T, ...args: any[]): V | undefined;
|
package/dist/utils/dataProp.js
CHANGED
|
@@ -80,22 +80,33 @@ export function resolveGeoDataPair(xProp, yProp, d, projection, defaults = [0, 0
|
|
|
80
80
|
* - `function`: called with data item, result passed through cScale.
|
|
81
81
|
* - `undefined`/`null`: returns undefined.
|
|
82
82
|
*/
|
|
83
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Returns true if the string looks like a CSS color value rather than a data property name.
|
|
85
|
+
* Matches: `#hex`, and functional notation like `rgb(...)`, `hsl(...)`, `var(...)`,
|
|
86
|
+
* `url(...)`, `color-mix(...)`, etc.
|
|
87
|
+
*/
|
|
88
|
+
function isCSSColor(value) {
|
|
89
|
+
return value.startsWith('#') || value.includes('(');
|
|
90
|
+
}
|
|
91
|
+
export function resolveColorProp(value, d, cScale, ...args) {
|
|
84
92
|
if (value === undefined || value === null)
|
|
85
93
|
return undefined;
|
|
86
94
|
if (typeof value === 'function') {
|
|
87
|
-
const rawValue = value(d);
|
|
95
|
+
const rawValue = value(d, ...args);
|
|
88
96
|
if (rawValue === undefined || rawValue === null)
|
|
89
97
|
return undefined;
|
|
90
98
|
return cScale ? String(cScale(rawValue)) : String(rawValue);
|
|
91
99
|
}
|
|
92
100
|
if (typeof value === 'string') {
|
|
101
|
+
// CSS color literals are never data property lookups
|
|
102
|
+
if (isCSSColor(value))
|
|
103
|
+
return value;
|
|
93
104
|
const dataValue = get(d, value);
|
|
94
105
|
if (dataValue !== undefined) {
|
|
95
106
|
// Data property — resolve through cScale
|
|
96
107
|
return cScale ? String(cScale(dataValue)) : String(dataValue);
|
|
97
108
|
}
|
|
98
|
-
// Not a data property — literal CSS color
|
|
109
|
+
// Not a data property — treat as literal CSS color (e.g. named colors like 'red')
|
|
99
110
|
return value;
|
|
100
111
|
}
|
|
101
112
|
return undefined;
|
|
@@ -105,10 +116,10 @@ export function resolveColorProp(value, d, cScale) {
|
|
|
105
116
|
* If the value is a function, calls it with the data item.
|
|
106
117
|
* Otherwise returns the static value.
|
|
107
118
|
*/
|
|
108
|
-
export function resolveStyleProp(value, d) {
|
|
119
|
+
export function resolveStyleProp(value, d, ...args) {
|
|
109
120
|
if (value === undefined)
|
|
110
121
|
return undefined;
|
|
111
122
|
if (typeof value === 'function')
|
|
112
|
-
return value(d);
|
|
123
|
+
return value(d, ...args);
|
|
113
124
|
return value;
|
|
114
125
|
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './ticks.js';
|
|
|
14
14
|
export * from './treemap.js';
|
|
15
15
|
export * from './threshold.js';
|
|
16
16
|
export * from './rasterInterpolate.js';
|
|
17
|
+
export * from './rasterBounds.js';
|
|
17
18
|
export * from './stats.js';
|
|
18
19
|
export * from './types.js';
|
|
19
20
|
export * from './graph/dagre.js';
|
package/dist/utils/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export * from './ticks.js';
|
|
|
14
14
|
export * from './treemap.js';
|
|
15
15
|
export * from './threshold.js';
|
|
16
16
|
export * from './rasterInterpolate.js';
|
|
17
|
+
export * from './rasterBounds.js';
|
|
17
18
|
export * from './stats.js';
|
|
18
19
|
export * from './types.js';
|
|
19
20
|
export * from './graph/dagre.js';
|
|
@@ -108,12 +108,22 @@ type InternalMotionOptions = {
|
|
|
108
108
|
*/
|
|
109
109
|
controlled?: boolean;
|
|
110
110
|
};
|
|
111
|
-
export declare function createMotion<T = any>(initialValue: T, getValue: () => T, motionProp: MotionOptions | undefined, options?: InternalMotionOptions): MotionSpring<T> | MotionTween<any> | MotionNone<T
|
|
111
|
+
export declare function createMotion<T = any>(initialValue: T, getValue: () => T, motionProp: MotionOptions | undefined, options?: InternalMotionOptions): MotionSpring<T> | MotionTween<any> | MotionNone<T> | {
|
|
112
|
+
type: "none";
|
|
113
|
+
readonly current: T;
|
|
114
|
+
target: T;
|
|
115
|
+
set(_value: T, _options?: any): Promise<void>;
|
|
116
|
+
};
|
|
112
117
|
/**
|
|
113
118
|
* Creates a controlled motion state that only updates when explicitly set
|
|
114
119
|
* rather than automatically tracking changes to the source value
|
|
115
120
|
*/
|
|
116
|
-
export declare function createControlledMotion<T = any>(initialValue: T, motionProp: MotionOptions | undefined): MotionTween<any> | MotionSpring<T> | MotionNone<T
|
|
121
|
+
export declare function createControlledMotion<T = any>(initialValue: T, motionProp: MotionOptions | undefined): MotionTween<any> | MotionSpring<T> | MotionNone<T> | {
|
|
122
|
+
type: "none";
|
|
123
|
+
readonly current: T;
|
|
124
|
+
target: T;
|
|
125
|
+
set(_value: T, _options?: any): Promise<void>;
|
|
126
|
+
};
|
|
117
127
|
/**
|
|
118
128
|
* Creates a state tracker for animation completion
|
|
119
129
|
* This helps track whether any motion transitions are currently in progress
|
|
@@ -74,6 +74,25 @@ function setupTracking(motion, getValue, options) {
|
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
76
|
export function createMotion(initialValue, getValue, motionProp, options = {}) {
|
|
77
|
+
// Fast path: when no motion is configured, skip all state/effect overhead
|
|
78
|
+
// and return a lightweight passthrough that reads directly from the getter.
|
|
79
|
+
if (motionProp === undefined) {
|
|
80
|
+
return {
|
|
81
|
+
type: 'none',
|
|
82
|
+
get current() {
|
|
83
|
+
return getValue();
|
|
84
|
+
},
|
|
85
|
+
get target() {
|
|
86
|
+
return getValue();
|
|
87
|
+
},
|
|
88
|
+
set target(v) {
|
|
89
|
+
// no-op for passthrough
|
|
90
|
+
},
|
|
91
|
+
set(_value, _options) {
|
|
92
|
+
return Promise.resolve();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
77
96
|
const motion = parseMotionProp(motionProp);
|
|
78
97
|
const motionState = motion.type === 'spring'
|
|
79
98
|
? new MotionSpring(initialValue, motion.options)
|
|
@@ -128,6 +147,9 @@ export function createMotionTracker() {
|
|
|
128
147
|
* Returns null if no motion is configured (type: 'none').
|
|
129
148
|
*/
|
|
130
149
|
export function createDataMotionMap(motionProp) {
|
|
150
|
+
// Fast path: skip parseMotionProp overhead when no motion is configured
|
|
151
|
+
if (motionProp === undefined)
|
|
152
|
+
return null;
|
|
131
153
|
const config = parseMotionProp(motionProp);
|
|
132
154
|
if (config.type === 'none')
|
|
133
155
|
return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseMotionProp, extractTweenConfig } from './motion.svelte.js';
|
|
2
|
+
import { createMotion, parseMotionProp, extractTweenConfig } from './motion.svelte.js';
|
|
3
3
|
describe('parseMotionProp', () => {
|
|
4
4
|
it('should return "none" type when config is undefined', () => {
|
|
5
5
|
const result = parseMotionProp(undefined);
|
|
@@ -117,6 +117,54 @@ describe('parseMotionProp', () => {
|
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
|
+
describe('createMotion', () => {
|
|
121
|
+
describe('fast path (motionProp === undefined)', () => {
|
|
122
|
+
it('should return a passthrough that reads from the getter', () => {
|
|
123
|
+
let value = 42;
|
|
124
|
+
const motion = createMotion(0, () => value, undefined);
|
|
125
|
+
expect(motion.current).toBe(42);
|
|
126
|
+
value = 99;
|
|
127
|
+
expect(motion.current).toBe(99);
|
|
128
|
+
});
|
|
129
|
+
it('should have type "none"', () => {
|
|
130
|
+
const motion = createMotion(0, () => 0, undefined);
|
|
131
|
+
expect(motion.type).toBe('none');
|
|
132
|
+
});
|
|
133
|
+
it('should return resolved promise from set()', async () => {
|
|
134
|
+
const motion = createMotion(0, () => 0, undefined);
|
|
135
|
+
const result = motion.set(10);
|
|
136
|
+
expect(result).toBeInstanceOf(Promise);
|
|
137
|
+
await expect(result).resolves.toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
it('target getter should read from getValue', () => {
|
|
140
|
+
let value = 5;
|
|
141
|
+
const motion = createMotion(0, () => value, undefined);
|
|
142
|
+
expect(motion.target).toBe(5);
|
|
143
|
+
value = 20;
|
|
144
|
+
expect(motion.target).toBe(20);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('with explicit "none" motion', () => {
|
|
148
|
+
it('should create a MotionNone state (not a passthrough)', () => {
|
|
149
|
+
const motion = createMotion(0, () => 42, 'none');
|
|
150
|
+
// MotionNone stores its own state, so current reflects the initial value
|
|
151
|
+
// until the tracking effect runs (which requires Svelte runtime)
|
|
152
|
+
expect(motion.type).toBe('none');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe('with spring motion', () => {
|
|
156
|
+
it('should create a spring motion state', () => {
|
|
157
|
+
const motion = createMotion(0, () => 42, 'spring');
|
|
158
|
+
expect(motion.type).toBe('spring');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe('with tween motion', () => {
|
|
162
|
+
it('should create a tween motion state', () => {
|
|
163
|
+
const motion = createMotion(0, () => 42, 'tween');
|
|
164
|
+
expect(motion.type).toBe('tween');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
120
168
|
describe('extractTweenConfig', () => {
|
|
121
169
|
// Test case 1: Undefined input
|
|
122
170
|
it('should return undefined when config is undefined', () => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { InterpolateMethod } from './rasterInterpolate.js';
|
|
2
|
+
export type RasterBounds = {
|
|
3
|
+
x1: number;
|
|
4
|
+
y1: number;
|
|
5
|
+
x2: number;
|
|
6
|
+
y2: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function resolveRasterBounds(width: number, height: number, x1?: number, y1?: number, x2?: number, y2?: number): RasterBounds;
|
|
9
|
+
export declare function gridPointToBounds(x: number, y: number, width: number, height: number, bounds: RasterBounds): {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function gridCellCenterToBounds(column: number, row: number, width: number, height: number, bounds: RasterBounds): {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function sampleGridAtBounds(values: ArrayLike<number>, width: number, height: number, bounds: RasterBounds, x: number, y: number, method?: InterpolateMethod): number;
|
|
18
|
+
export declare function blurGridIgnoringNaN(values: ArrayLike<number>, width: number, height: number, radius: number): Float64Array<ArrayBufferLike>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { blur2 } from 'd3-array';
|
|
2
|
+
export function resolveRasterBounds(width, height, x1, y1, x2, y2) {
|
|
3
|
+
return {
|
|
4
|
+
x1: x1 ?? 0,
|
|
5
|
+
y1: y1 ?? 0,
|
|
6
|
+
x2: x2 ?? width,
|
|
7
|
+
y2: y2 ?? height,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function gridPointToBounds(x, y, width, height, bounds) {
|
|
11
|
+
return {
|
|
12
|
+
x: bounds.x1 + (x / width) * (bounds.x2 - bounds.x1),
|
|
13
|
+
y: bounds.y1 + (y / height) * (bounds.y2 - bounds.y1),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function gridCellCenterToBounds(column, row, width, height, bounds) {
|
|
17
|
+
return {
|
|
18
|
+
x: bounds.x1 + ((column + 0.5) / width) * (bounds.x2 - bounds.x1),
|
|
19
|
+
y: bounds.y1 + ((row + 0.5) / height) * (bounds.y2 - bounds.y1),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function sampleGridAtBounds(values, width, height, bounds, x, y, method = 'barycentric') {
|
|
23
|
+
const dx = bounds.x2 - bounds.x1;
|
|
24
|
+
const dy = bounds.y2 - bounds.y1;
|
|
25
|
+
if (!width || !height || dx === 0 || dy === 0)
|
|
26
|
+
return NaN;
|
|
27
|
+
const fx = ((x - bounds.x1) / dx) * width - 0.5;
|
|
28
|
+
const fy = ((y - bounds.y1) / dy) * height - 0.5;
|
|
29
|
+
if (fx < -0.5 || fx > width - 0.5 || fy < -0.5 || fy > height - 0.5) {
|
|
30
|
+
return NaN;
|
|
31
|
+
}
|
|
32
|
+
switch (method) {
|
|
33
|
+
case 'barycentric':
|
|
34
|
+
return sampleBilinear(values, width, height, fx, fy);
|
|
35
|
+
case 'nearest':
|
|
36
|
+
case 'none':
|
|
37
|
+
default:
|
|
38
|
+
return sampleNearest(values, width, height, fx, fy);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function blurGridIgnoringNaN(values, width, height, radius) {
|
|
42
|
+
if (!radius || !values.length) {
|
|
43
|
+
return values instanceof Float64Array ? values : Float64Array.from(values);
|
|
44
|
+
}
|
|
45
|
+
const numerators = new Float64Array(values.length);
|
|
46
|
+
const weights = new Float64Array(values.length);
|
|
47
|
+
for (let index = 0; index < values.length; index++) {
|
|
48
|
+
const value = values[index] ?? NaN;
|
|
49
|
+
if (!Number.isFinite(value))
|
|
50
|
+
continue;
|
|
51
|
+
numerators[index] = value;
|
|
52
|
+
weights[index] = 1;
|
|
53
|
+
}
|
|
54
|
+
blur2({ data: numerators, width, height }, radius);
|
|
55
|
+
blur2({ data: weights, width, height }, radius);
|
|
56
|
+
const result = new Float64Array(values.length);
|
|
57
|
+
for (let index = 0; index < values.length; index++) {
|
|
58
|
+
const weight = weights[index];
|
|
59
|
+
result[index] = weight > 1e-12 ? numerators[index] / weight : NaN;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function sampleNearest(values, width, height, x, y) {
|
|
64
|
+
const column = clamp(Math.round(x), 0, width - 1);
|
|
65
|
+
const row = clamp(Math.round(y), 0, height - 1);
|
|
66
|
+
return values[row * width + column] ?? NaN;
|
|
67
|
+
}
|
|
68
|
+
function sampleBilinear(values, width, height, x, y) {
|
|
69
|
+
const x0 = Math.floor(x);
|
|
70
|
+
const y0 = Math.floor(y);
|
|
71
|
+
const x1 = x0 + 1;
|
|
72
|
+
const y1 = y0 + 1;
|
|
73
|
+
const tx = x - x0;
|
|
74
|
+
const ty = y - y0;
|
|
75
|
+
const weightedValues = [
|
|
76
|
+
[getSample(values, width, height, x0, y0), (1 - tx) * (1 - ty)],
|
|
77
|
+
[getSample(values, width, height, x1, y0), tx * (1 - ty)],
|
|
78
|
+
[getSample(values, width, height, x0, y1), (1 - tx) * ty],
|
|
79
|
+
[getSample(values, width, height, x1, y1), tx * ty],
|
|
80
|
+
];
|
|
81
|
+
let totalWeight = 0;
|
|
82
|
+
let totalValue = 0;
|
|
83
|
+
for (const [value, weight] of weightedValues) {
|
|
84
|
+
if (!Number.isFinite(value))
|
|
85
|
+
continue;
|
|
86
|
+
totalWeight += weight;
|
|
87
|
+
totalValue += value * weight;
|
|
88
|
+
}
|
|
89
|
+
return totalWeight > 0 ? totalValue / totalWeight : NaN;
|
|
90
|
+
}
|
|
91
|
+
function getSample(values, width, height, x, y) {
|
|
92
|
+
const column = clamp(x, 0, width - 1);
|
|
93
|
+
const row = clamp(y, 0, height - 1);
|
|
94
|
+
return values[row * width + column] ?? NaN;
|
|
95
|
+
}
|
|
96
|
+
function clamp(value, min, max) {
|
|
97
|
+
return Math.max(min, Math.min(max, value));
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { blurGridIgnoringNaN, gridCellCenterToBounds, gridPointToBounds, resolveRasterBounds, sampleGridAtBounds, } from './rasterBounds.js';
|
|
3
|
+
describe('resolveRasterBounds', () => {
|
|
4
|
+
it('defaults to grid dimensions', () => {
|
|
5
|
+
expect(resolveRasterBounds(360, 180)).toEqual({
|
|
6
|
+
x1: 0,
|
|
7
|
+
y1: 0,
|
|
8
|
+
x2: 360,
|
|
9
|
+
y2: 180,
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
it('uses explicit geographic bounds', () => {
|
|
13
|
+
expect(resolveRasterBounds(360, 180, -180, 90, 180, -90)).toEqual({
|
|
14
|
+
x1: -180,
|
|
15
|
+
y1: 90,
|
|
16
|
+
x2: 180,
|
|
17
|
+
y2: -90,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('gridPointToBounds', () => {
|
|
22
|
+
it('maps contour-space coordinates back into bound coordinates', () => {
|
|
23
|
+
expect(gridPointToBounds(180, 90, 360, 180, resolveRasterBounds(360, 180, -180, 90, 180, -90))).toEqual({
|
|
24
|
+
x: 0,
|
|
25
|
+
y: 0,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('gridCellCenterToBounds', () => {
|
|
30
|
+
it('maps grid cell centers into bound coordinates', () => {
|
|
31
|
+
expect(gridCellCenterToBounds(0, 0, 360, 180, resolveRasterBounds(360, 180, -180, 90, 180, -90))).toEqual({
|
|
32
|
+
x: -179.5,
|
|
33
|
+
y: 89.5,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('sampleGridAtBounds', () => {
|
|
38
|
+
const grid = [0, 1, 2, 3];
|
|
39
|
+
const bounds = resolveRasterBounds(2, 2, -1, 1, 1, -1);
|
|
40
|
+
it('samples the nearest grid cell center', () => {
|
|
41
|
+
expect(sampleGridAtBounds(grid, 2, 2, bounds, -0.5, 0.5, 'nearest')).toBe(0);
|
|
42
|
+
expect(sampleGridAtBounds(grid, 2, 2, bounds, 0.5, -0.5, 'nearest')).toBe(3);
|
|
43
|
+
});
|
|
44
|
+
it('interpolates between adjacent cells', () => {
|
|
45
|
+
expect(sampleGridAtBounds(grid, 2, 2, bounds, 0, 0, 'barycentric')).toBe(1.5);
|
|
46
|
+
});
|
|
47
|
+
it('returns NaN outside the raster bounds', () => {
|
|
48
|
+
expect(sampleGridAtBounds(grid, 2, 2, bounds, 4, 4, 'barycentric')).toBeNaN();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('blurGridIgnoringNaN', () => {
|
|
52
|
+
it('preserves nearby finite values when blurring through NaN gaps', () => {
|
|
53
|
+
const blurred = blurGridIgnoringNaN([1, NaN, 3], 3, 1, 1);
|
|
54
|
+
expect(blurred[0]).toBeGreaterThan(0);
|
|
55
|
+
expect(blurred[1]).toBeGreaterThan(1);
|
|
56
|
+
expect(blurred[1]).toBeLessThan(3);
|
|
57
|
+
expect(blurred[2]).toBeGreaterThan(0);
|
|
58
|
+
});
|
|
59
|
+
it('keeps fully empty regions as NaN', () => {
|
|
60
|
+
const blurred = blurGridIgnoringNaN([NaN, NaN, NaN], 3, 1, 1);
|
|
61
|
+
expect(Array.from(blurred).every((value) => Number.isNaN(value))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -60,9 +60,11 @@ export function createMotionScale(scale, motion, options) {
|
|
|
60
60
|
export function scaleBandInvert(scale) {
|
|
61
61
|
const domain = scale.domain();
|
|
62
62
|
const eachBand = scale.step();
|
|
63
|
-
const
|
|
63
|
+
const rangeStart = scale.range()[0];
|
|
64
|
+
const paddingOuter = scale.paddingOuter?.() ?? scale.padding(); // `scaleBand` uses paddingOuter(), while `scalePoint` uses padding() for outer paddding - https://github.com/d3/d3-scale#point_padding
|
|
64
65
|
return function (value) {
|
|
65
|
-
|
|
66
|
+
// band[i] = rangeStart + step * (paddingOuter + i), so: i = (value - rangeStart) / step - paddingOuter
|
|
67
|
+
const index = Math.floor((value - rangeStart) / eachBand - paddingOuter);
|
|
66
68
|
return domain[Math.max(0, Math.min(index, domain.length - 1))];
|
|
67
69
|
};
|
|
68
70
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { scaleBand } from 'd3-scale';
|
|
3
|
+
import { scaleBandInvert, scaleInvert } from './scales.svelte.js';
|
|
4
|
+
describe('scaleBandInvert', () => {
|
|
5
|
+
const domain = ['A', 'B', 'C', 'D', 'E'];
|
|
6
|
+
it('should return correct category for standard range [0, width]', () => {
|
|
7
|
+
const scale = scaleBand().domain(domain).range([0, 500]).padding(0.1);
|
|
8
|
+
const invert = scaleBandInvert(scale);
|
|
9
|
+
// Each category should map back from its center
|
|
10
|
+
for (const category of domain) {
|
|
11
|
+
const x = scale(category) + scale.bandwidth() / 2;
|
|
12
|
+
expect(invert(x)).toBe(category);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
it('should clamp to first category for values before range', () => {
|
|
16
|
+
const scale = scaleBand().domain(domain).range([0, 500]).padding(0.1);
|
|
17
|
+
const invert = scaleBandInvert(scale);
|
|
18
|
+
expect(invert(-50)).toBe('A');
|
|
19
|
+
});
|
|
20
|
+
it('should clamp to last category for values after range', () => {
|
|
21
|
+
const scale = scaleBand().domain(domain).range([0, 500]).padding(0.1);
|
|
22
|
+
const invert = scaleBandInvert(scale);
|
|
23
|
+
expect(invert(600)).toBe('E');
|
|
24
|
+
});
|
|
25
|
+
it('should handle offset range (non-zero start)', () => {
|
|
26
|
+
// Simulates a zoomed band scale where the range is shifted
|
|
27
|
+
const scale = scaleBand().domain(domain).range([-200, 800]).padding(0.1);
|
|
28
|
+
const invert = scaleBandInvert(scale);
|
|
29
|
+
// Each category should still map correctly
|
|
30
|
+
for (const category of domain) {
|
|
31
|
+
const x = scale(category) + scale.bandwidth() / 2;
|
|
32
|
+
expect(invert(x)).toBe(category);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
it('should return correct category at viewport edges with offset range', () => {
|
|
36
|
+
// Range [-400, 600] means categories span 1000px but viewport is [0, ~500]
|
|
37
|
+
const scale = scaleBand().domain(domain).range([-400, 600]).padding(0.4);
|
|
38
|
+
const invert = scaleBandInvert(scale);
|
|
39
|
+
// At x=0, we should get whichever category is nearest that pixel position
|
|
40
|
+
const categoryAtZero = invert(0);
|
|
41
|
+
expect(domain).toContain(categoryAtZero);
|
|
42
|
+
// The returned category should be close to x=0 (within one step)
|
|
43
|
+
const pos = scale(categoryAtZero);
|
|
44
|
+
expect(Math.abs(pos)).toBeLessThan(scale.step());
|
|
45
|
+
});
|
|
46
|
+
it('should work with no padding', () => {
|
|
47
|
+
const scale = scaleBand().domain(domain).range([0, 500]);
|
|
48
|
+
const invert = scaleBandInvert(scale);
|
|
49
|
+
// bandwidth = 100, each category is 100px wide
|
|
50
|
+
expect(invert(50)).toBe('A');
|
|
51
|
+
expect(invert(150)).toBe('B');
|
|
52
|
+
expect(invert(450)).toBe('E');
|
|
53
|
+
});
|
|
54
|
+
it('should work with single-element domain', () => {
|
|
55
|
+
const scale = scaleBand().domain(['X']).range([0, 500]).padding(0.1);
|
|
56
|
+
const invert = scaleBandInvert(scale);
|
|
57
|
+
expect(invert(250)).toBe('X');
|
|
58
|
+
expect(invert(0)).toBe('X');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('scaleInvert', () => {
|
|
62
|
+
it('should use scaleBandInvert for band scales', () => {
|
|
63
|
+
const scale = scaleBand().domain(['A', 'B', 'C']).range([0, 300]).padding(0.1);
|
|
64
|
+
const center = scale('B') + scale.bandwidth() / 2;
|
|
65
|
+
expect(scaleInvert(scale, center)).toBe('B');
|
|
66
|
+
});
|
|
67
|
+
});
|
package/dist/utils/ticks.js
CHANGED
|
@@ -126,9 +126,13 @@ export function autoTickVals(scale, ticks, count) {
|
|
|
126
126
|
}
|
|
127
127
|
// Band (use domain)
|
|
128
128
|
if (isScaleBand(scale)) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
const domain = scale.domain();
|
|
130
|
+
const resolvedCount = typeof ticks === 'number' ? ticks : count;
|
|
131
|
+
if (resolvedCount != null && resolvedCount < domain.length) {
|
|
132
|
+
const step = Math.max(1, Math.ceil(domain.length / resolvedCount));
|
|
133
|
+
return domain.filter((_, i) => i % step === 0);
|
|
134
|
+
}
|
|
135
|
+
return domain;
|
|
132
136
|
}
|
|
133
137
|
// Ticks from scale
|
|
134
138
|
if (scale.ticks && typeof scale.ticks === 'function') {
|
package/dist/utils/ticks.test.js
CHANGED
|
@@ -27,11 +27,21 @@ describe('autoTickVals', () => {
|
|
|
27
27
|
const scale = { ticks: mockTicksFn };
|
|
28
28
|
expect(autoTickVals(scale, ticksConfig)).toEqual([]);
|
|
29
29
|
});
|
|
30
|
-
it('filters band scale domain with number ticks', () => {
|
|
30
|
+
it('filters band scale domain with explicit number ticks', () => {
|
|
31
31
|
const scale = { domain: mockDomain, bandwidth: vi.fn() };
|
|
32
|
-
|
|
32
|
+
// ticks=2, domain has 5 items → step = ceil(5/2) = 3 → indices 0, 3
|
|
33
|
+
expect(autoTickVals(scale, 2)).toEqual(['a', 'd']);
|
|
33
34
|
});
|
|
34
|
-
it('
|
|
35
|
+
it('filters band scale domain using count parameter (from tickSpacing)', () => {
|
|
36
|
+
const scale = { domain: mockDomain, bandwidth: vi.fn() };
|
|
37
|
+
// count=3, domain has 5 items → step = ceil(5/3) = 2 → indices 0, 2, 4
|
|
38
|
+
expect(autoTickVals(scale, undefined, 3)).toEqual(['a', 'c', 'e']);
|
|
39
|
+
});
|
|
40
|
+
it('returns full domain for band scale when count >= domain length', () => {
|
|
41
|
+
const scale = { domain: mockDomain, bandwidth: vi.fn() };
|
|
42
|
+
expect(autoTickVals(scale, undefined, 10)).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
43
|
+
});
|
|
44
|
+
it('returns full domain for band scale without ticks or count', () => {
|
|
35
45
|
const scale = { domain: mockDomain, bandwidth: vi.fn() };
|
|
36
46
|
expect(autoTickVals(scale)).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
37
47
|
});
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "techniq/layerchart",
|
|
7
7
|
"homepage": "https://layerchart.com",
|
|
8
|
-
"version": "2.0.0-next.
|
|
8
|
+
"version": "2.0.0-next.49",
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@changesets/cli": "^2.30.0",
|
|
11
11
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
@@ -111,7 +111,8 @@
|
|
|
111
111
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
|
112
112
|
"test:unit": "TZ=UTC-5 vitest",
|
|
113
113
|
"test:ui": "TZ=UTC-5 vitest --ui",
|
|
114
|
-
"bench": "pnpm bench:linechart && pnpm bench:composable && pnpm bench:svg-vs-canvas",
|
|
114
|
+
"bench": "pnpm bench:primitives && pnpm bench:linechart && pnpm bench:composable && pnpm bench:svg-vs-canvas",
|
|
115
|
+
"bench:primitives": "TZ=UTC-5 vitest bench --project bench src/lib/bench/primitives.svelte.bench.ts",
|
|
115
116
|
"bench:linechart": "TZ=UTC-5 vitest bench --project bench src/lib/components/charts/LineChart.svelte.bench.ts",
|
|
116
117
|
"bench:composable": "TZ=UTC-5 vitest bench --project bench src/lib/bench/composable-vs-linechart.svelte.bench.ts",
|
|
117
118
|
"bench:svg-vs-canvas": "TZ=UTC-5 vitest bench --project bench src/lib/bench/svg-vs-canvas.svelte.bench.ts",
|