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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Chart from '../components/Chart.svelte';
|
|
3
|
+
import Layer from '../components/layers/Layer.svelte';
|
|
4
|
+
import Rect from '../components/Rect.svelte';
|
|
5
|
+
import Circle from '../components/Circle.svelte';
|
|
6
|
+
import Ellipse from '../components/Ellipse.svelte';
|
|
7
|
+
import Line from '../components/Line.svelte';
|
|
8
|
+
import Group from '../components/Group.svelte';
|
|
9
|
+
import Text from '../components/Text.svelte';
|
|
10
|
+
import Path from '../components/Path.svelte';
|
|
11
|
+
|
|
12
|
+
type Primitive = 'rect' | 'circle' | 'ellipse' | 'line' | 'group' | 'text' | 'path';
|
|
13
|
+
type Mode = 'layerchart' | 'native';
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
primitive: Primitive;
|
|
17
|
+
mode: Mode;
|
|
18
|
+
count?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let { primitive, mode, count = 100 }: Props = $props();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
{#if mode === 'layerchart'}
|
|
25
|
+
<Chart width={500} height={300}>
|
|
26
|
+
<Layer type="svg">
|
|
27
|
+
{#each Array(count) as _, i (i)}
|
|
28
|
+
{#if primitive === 'rect'}
|
|
29
|
+
<Rect x={10} y={10} width={50} height={30} fill="steelblue" />
|
|
30
|
+
{:else if primitive === 'circle'}
|
|
31
|
+
<Circle cx={30} cy={30} r={15} fill="steelblue" />
|
|
32
|
+
{:else if primitive === 'ellipse'}
|
|
33
|
+
<Ellipse cx={30} cy={30} rx={20} ry={10} fill="steelblue" />
|
|
34
|
+
{:else if primitive === 'line'}
|
|
35
|
+
<Line x1={0} y1={0} x2={50} y2={50} stroke="steelblue" strokeWidth={2} />
|
|
36
|
+
{:else if primitive === 'group'}
|
|
37
|
+
<Group x={10} y={10} />
|
|
38
|
+
{:else if primitive === 'text'}
|
|
39
|
+
<Text x={10} y={20} value="Hello" fill="steelblue" />
|
|
40
|
+
{:else if primitive === 'path'}
|
|
41
|
+
<Path pathData="M0,0 L50,50 L100,0 Z" fill="none" stroke="steelblue" strokeWidth={2} />
|
|
42
|
+
{/if}
|
|
43
|
+
{/each}
|
|
44
|
+
</Layer>
|
|
45
|
+
</Chart>
|
|
46
|
+
{:else}
|
|
47
|
+
<svg width={500} height={300}>
|
|
48
|
+
{#each Array(count) as _, i (i)}
|
|
49
|
+
{#if primitive === 'rect'}
|
|
50
|
+
<rect x={10} y={10} width={50} height={30} fill="steelblue" />
|
|
51
|
+
{:else if primitive === 'circle'}
|
|
52
|
+
<circle cx={30} cy={30} r={15} fill="steelblue" />
|
|
53
|
+
{:else if primitive === 'ellipse'}
|
|
54
|
+
<ellipse cx={30} cy={30} rx={20} ry={10} fill="steelblue" />
|
|
55
|
+
{:else if primitive === 'line'}
|
|
56
|
+
<line x1={0} y1={0} x2={50} y2={50} stroke="steelblue" stroke-width={2} />
|
|
57
|
+
{:else if primitive === 'group'}
|
|
58
|
+
<g transform="translate(10,10)"></g>
|
|
59
|
+
{:else if primitive === 'text'}
|
|
60
|
+
<text x={10} y={20} fill="steelblue">Hello</text>
|
|
61
|
+
{:else if primitive === 'path'}
|
|
62
|
+
<path d="M0,0 L50,50 L100,0 Z" fill="none" stroke="steelblue" stroke-width={2} />
|
|
63
|
+
{/if}
|
|
64
|
+
{/each}
|
|
65
|
+
</svg>
|
|
66
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Primitive = 'rect' | 'circle' | 'ellipse' | 'line' | 'group' | 'text' | 'path';
|
|
2
|
+
type Mode = 'layerchart' | 'native';
|
|
3
|
+
type Props = {
|
|
4
|
+
primitive: Primitive;
|
|
5
|
+
mode: Mode;
|
|
6
|
+
count?: number;
|
|
7
|
+
};
|
|
8
|
+
declare const PrimitiveBench: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type PrimitiveBench = ReturnType<typeof PrimitiveBench>;
|
|
10
|
+
export default PrimitiveBench;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, bench, afterEach } from 'vitest';
|
|
2
|
+
import { render, cleanup } from 'vitest-browser-svelte';
|
|
3
|
+
import PrimitiveBench from './PrimitiveBench.svelte';
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
cleanup();
|
|
6
|
+
});
|
|
7
|
+
const COUNT = 100;
|
|
8
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
9
|
+
// LayerChart primitives vs native SVG elements — 100 instances each
|
|
10
|
+
//
|
|
11
|
+
// Representative subset: rect (shape), circle (shape), group (container),
|
|
12
|
+
// text (complex), path (path-based). Ellipse/line follow the same
|
|
13
|
+
// architecture as rect/circle and would show the same ratio.
|
|
14
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
15
|
+
const PRIMITIVES = ['rect', 'circle', 'group', 'text', 'path'];
|
|
16
|
+
for (const primitive of PRIMITIVES) {
|
|
17
|
+
describe(`${primitive} — ${COUNT} instances`, () => {
|
|
18
|
+
bench(`Native <${primitive}>`, () => {
|
|
19
|
+
cleanup();
|
|
20
|
+
render(PrimitiveBench, { primitive, mode: 'native', count: COUNT });
|
|
21
|
+
});
|
|
22
|
+
bench(`LayerChart <${primitive[0].toUpperCase()}${primitive.slice(1)}>`, () => {
|
|
23
|
+
cleanup();
|
|
24
|
+
render(PrimitiveBench, { primitive, mode: 'layerchart', count: COUNT });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
29
|
+
// Scaling: rect at 10, 100, 250 instances
|
|
30
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
31
|
+
describe('rect — scaling', () => {
|
|
32
|
+
for (const count of [10, 100, 250]) {
|
|
33
|
+
bench(`Native <rect> × ${count}`, () => {
|
|
34
|
+
cleanup();
|
|
35
|
+
render(PrimitiveBench, { primitive: 'rect', mode: 'native', count });
|
|
36
|
+
});
|
|
37
|
+
bench(`LayerChart <Rect> × ${count}`, () => {
|
|
38
|
+
cleanup();
|
|
39
|
+
render(PrimitiveBench, { primitive: 'rect', mode: 'layerchart', count });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* Width or height of each tick in pixels (enabling responsive count)
|
|
48
48
|
* @default 80 (top|bottom|angle) or 50 (left|right|radius)
|
|
49
49
|
*/
|
|
50
|
-
tickSpacing?: number;
|
|
50
|
+
tickSpacing?: number | null;
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Whether to render tick labels on multiple lines for additional context
|
|
@@ -220,11 +220,22 @@
|
|
|
220
220
|
: null
|
|
221
221
|
);
|
|
222
222
|
|
|
223
|
+
// For band scales with domain-mode transform, scale up effective size by the zoom factor
|
|
224
|
+
// so that more tick labels appear as bands get wider when zoomed in
|
|
225
|
+
const effectiveSize = $derived.by(() => {
|
|
226
|
+
if (!ctxSize) return ctxSize;
|
|
227
|
+
const ts = ctx.transformState;
|
|
228
|
+
if (ts?.mode === 'domain' && isScaleBand(scale) && ts.scale > 1) {
|
|
229
|
+
return ctxSize * ts.scale;
|
|
230
|
+
}
|
|
231
|
+
return ctxSize;
|
|
232
|
+
});
|
|
233
|
+
|
|
223
234
|
const tickCount = $derived(
|
|
224
235
|
typeof ticks === 'number'
|
|
225
236
|
? ticks
|
|
226
|
-
: tickSpacing &&
|
|
227
|
-
? Math.round(
|
|
237
|
+
: tickSpacing && effectiveSize
|
|
238
|
+
? Math.round(effectiveSize / tickSpacing)
|
|
228
239
|
: undefined
|
|
229
240
|
);
|
|
230
241
|
const tickVals = $derived.by(() => {
|
|
@@ -40,7 +40,7 @@ export type AxisPropsWithoutHTML<In extends Transition = Transition> = {
|
|
|
40
40
|
* Width or height of each tick in pixels (enabling responsive count)
|
|
41
41
|
* @default 80 (top|bottom|angle) or 50 (left|right|radius)
|
|
42
42
|
*/
|
|
43
|
-
tickSpacing?: number;
|
|
43
|
+
tickSpacing?: number | null;
|
|
44
44
|
/**
|
|
45
45
|
* Whether to render tick labels on multiple lines for additional context
|
|
46
46
|
*
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { getObjectOrNull, type Accessor } from '../utils/common.js';
|
|
9
9
|
import type { MotionProp } from '../utils/motion.svelte.js';
|
|
10
|
-
import { type AnyScale, type DomainType } from '../utils/scales.svelte.js';
|
|
10
|
+
import { type AnyScale, type DomainType, isScaleBand } from '../utils/scales.svelte.js';
|
|
11
11
|
import type {
|
|
12
12
|
BaseRange,
|
|
13
13
|
Nice,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { geoFitObjectTransform } from '../utils/geo.js';
|
|
23
23
|
import TransformContext from './TransformContext.svelte';
|
|
24
24
|
import BrushContext from './BrushContext.svelte';
|
|
25
|
-
import { type BrushDomainType, type BrushState } from '../states/brush.svelte.js';
|
|
25
|
+
import { type BrushDomainType, type BrushState, expandBandBrushDomain } from '../states/brush.svelte.js';
|
|
26
26
|
|
|
27
27
|
import { setChartContext } from '../contexts/chart.js';
|
|
28
28
|
import { ChartState } from '../states/chart.svelte.js';
|
|
@@ -751,7 +751,7 @@
|
|
|
751
751
|
}
|
|
752
752
|
|
|
753
753
|
const defaults = isGlobe
|
|
754
|
-
? { rotation: true, scale:
|
|
754
|
+
? { rotation: true, scale: true, translate: false }
|
|
755
755
|
: { rotation: false, scale: true, translate: true };
|
|
756
756
|
|
|
757
757
|
// User overrides win; enforce mutual exclusion
|
|
@@ -833,6 +833,9 @@
|
|
|
833
833
|
|
|
834
834
|
const d0 = baseDomain[0] as unknown;
|
|
835
835
|
const d1 = baseDomain[1] as unknown;
|
|
836
|
+
|
|
837
|
+
// Skip domain extent constraint for categorical scales (range clamping handles boundaries)
|
|
838
|
+
if (typeof d0 === 'string') return axisTranslate;
|
|
836
839
|
const isDate = d0 instanceof Date;
|
|
837
840
|
const rawD0 = isDate ? (d0 as Date).getTime() : (d0 as number);
|
|
838
841
|
const rawD1 = isDate ? (d1 as Date).getTime() : (d1 as number);
|
|
@@ -925,15 +928,104 @@
|
|
|
925
928
|
};
|
|
926
929
|
});
|
|
927
930
|
|
|
928
|
-
//
|
|
931
|
+
// Whether this is a band scale domain transform (affects scaleExtent and constrain defaults)
|
|
932
|
+
const isBandDomainTransform = $derived(
|
|
933
|
+
transform?.mode === 'domain' && (
|
|
934
|
+
((transform.axis ?? 'both') !== 'y' && isScaleBand(chartState._xScaleProp)) ||
|
|
935
|
+
((transform.axis ?? 'both') !== 'x' && isScaleBand(chartState._yScaleProp))
|
|
936
|
+
)
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
// For projection mode, scaleExtent is relative to the initial fitted scale (like d3-zoom).
|
|
940
|
+
// e.g. [0.5, 8] means 0.5x to 8x of the fitted projection scale.
|
|
941
|
+
// For band scale domain transforms, enforce minimum of 1 (can't zoom out past initial view).
|
|
942
|
+
const resolvedScaleExtent = $derived.by(() => {
|
|
943
|
+
if (transform?.mode === 'projection' && transform?.scaleExtent && initialTransform) {
|
|
944
|
+
const baseScale = initialTransform.scale;
|
|
945
|
+
return [
|
|
946
|
+
transform.scaleExtent[0] * baseScale,
|
|
947
|
+
transform.scaleExtent[1] * baseScale,
|
|
948
|
+
] as [number, number];
|
|
949
|
+
}
|
|
950
|
+
if (!isBandDomainTransform) return transform?.scaleExtent;
|
|
951
|
+
const userExtent = transform?.scaleExtent;
|
|
952
|
+
return [Math.max(1, userExtent?.[0] ?? 1), userExtent?.[1] ?? Infinity] as [number, number];
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
// For projection mode with flat projections, translateExtent defines the pannable world bounds
|
|
956
|
+
// at the initial (1x) zoom level, similar to d3-zoom. The allowed translate range scales with
|
|
957
|
+
// the zoom ratio so you can pan more when zoomed in.
|
|
958
|
+
// For rotation mode (globes), translateExtent is passed through as degrees (yaw/pitch).
|
|
959
|
+
const resolvedTranslateExtent = $derived.by(() => {
|
|
960
|
+
if (transform?.mode === 'projection' && transform?.translateExtent) {
|
|
961
|
+
if (resolvedApply.rotation) {
|
|
962
|
+
// Rotation mode: values are degrees (yaw/pitch), pass through as-is
|
|
963
|
+
return transform.translateExtent;
|
|
964
|
+
}
|
|
965
|
+
// Flat projection translate mode: handled via projectionTranslateConstrain below
|
|
966
|
+
return undefined;
|
|
967
|
+
}
|
|
968
|
+
return transform?.translateExtent;
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// For flat projection mode, implement d3-zoom-style translate constraining:
|
|
972
|
+
// The viewport (at current zoom) must overlap with the translateExtent world bounds.
|
|
973
|
+
// As zoom increases, the allowed translate range grows proportionally.
|
|
974
|
+
const projectionTranslateConstrain = $derived.by(() => {
|
|
975
|
+
if (transform?.mode !== 'projection' || !transform?.translateExtent || !initialTransform || resolvedApply.rotation) {
|
|
976
|
+
return undefined;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const baseScale = initialTransform.scale;
|
|
980
|
+
const baseTranslate = initialTransform.translate;
|
|
981
|
+
const [[x0, y0], [x1, y1]] = transform.translateExtent;
|
|
982
|
+
|
|
983
|
+
return (t: { scale: number; translate: { x: number; y: number } }) => {
|
|
984
|
+
let { scale, translate } = t;
|
|
985
|
+
// Zoom ratio relative to fitted scale
|
|
986
|
+
const k = scale / baseScale;
|
|
987
|
+
|
|
988
|
+
// Allowed translate range scales with zoom ratio
|
|
989
|
+
translate = {
|
|
990
|
+
x: Math.max(baseTranslate.x + x0 * k, Math.min(baseTranslate.x + x1 * k, translate.x)),
|
|
991
|
+
y: Math.max(baseTranslate.y + y0 * k, Math.min(baseTranslate.y + y1 * k, translate.y)),
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
return { scale, translate };
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
// Default constrain for band scale domain transforms: prevent panning past data boundaries
|
|
999
|
+
const bandScaleConstrain = $derived.by(() => {
|
|
1000
|
+
if (!isBandDomainTransform) return undefined;
|
|
1001
|
+
const xIsBand =
|
|
1002
|
+
(transform!.axis ?? 'both') !== 'y' && isScaleBand(chartState._xScaleProp);
|
|
1003
|
+
const yIsBand =
|
|
1004
|
+
(transform!.axis ?? 'both') !== 'x' && isScaleBand(chartState._yScaleProp);
|
|
1005
|
+
|
|
1006
|
+
return (t: { scale: number; translate: { x: number; y: number } }) => {
|
|
1007
|
+
let { scale, translate } = t;
|
|
1008
|
+
let tx = translate.x;
|
|
1009
|
+
let ty = translate.y;
|
|
1010
|
+
if (xIsBand) {
|
|
1011
|
+
// translate.x must be in [width * (1 - scale), 0]
|
|
1012
|
+
tx = Math.max(chartState.width * (1 - scale), Math.min(0, tx));
|
|
1013
|
+
}
|
|
1014
|
+
if (yIsBand) {
|
|
1015
|
+
ty = Math.max(chartState.height * (1 - scale), Math.min(0, ty));
|
|
1016
|
+
}
|
|
1017
|
+
return { scale, translate: { x: tx, y: ty } };
|
|
1018
|
+
};
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
// Compose user-provided constrain with domainExtent constrain and band scale constrain
|
|
929
1022
|
const composedConstrain = $derived.by(() => {
|
|
930
1023
|
const userConstrain = transform?.constrain;
|
|
931
|
-
|
|
932
|
-
if (
|
|
933
|
-
if (
|
|
934
|
-
// Domain extent first, then user constrain
|
|
1024
|
+
const constrains = [bandScaleConstrain, domainExtentConstrain, projectionTranslateConstrain, userConstrain].filter(Boolean) as Array<(t: { scale: number; translate: { x: number; y: number } }) => { scale: number; translate: { x: number; y: number } }>;
|
|
1025
|
+
if (constrains.length === 0) return undefined;
|
|
1026
|
+
if (constrains.length === 1) return constrains[0];
|
|
935
1027
|
return (t: { scale: number; translate: { x: number; y: number } }) => {
|
|
936
|
-
return
|
|
1028
|
+
return constrains.reduce((acc, fn) => fn(acc), t);
|
|
937
1029
|
};
|
|
938
1030
|
});
|
|
939
1031
|
|
|
@@ -954,8 +1046,12 @@
|
|
|
954
1046
|
chartState.zoomToBrush(e.brush, userProps.axis ?? 'x');
|
|
955
1047
|
} else if (zoomOnBrush) {
|
|
956
1048
|
const axis = userProps.axis ?? 'x';
|
|
957
|
-
if (axis === 'x' || axis === 'both')
|
|
958
|
-
|
|
1049
|
+
if (axis === 'x' || axis === 'both') {
|
|
1050
|
+
brushXDomain = expandBandBrushDomain(e.brush.x, chartState._baseXDomain);
|
|
1051
|
+
}
|
|
1052
|
+
if (axis === 'y' || axis === 'both') {
|
|
1053
|
+
brushYDomain = expandBandBrushDomain(e.brush.y, chartState._baseYDomain);
|
|
1054
|
+
}
|
|
959
1055
|
}
|
|
960
1056
|
userOnBrushEnd?.(e);
|
|
961
1057
|
e.brush.reset();
|
|
@@ -993,7 +1089,7 @@
|
|
|
993
1089
|
>
|
|
994
1090
|
{#key chartState.isMounted}
|
|
995
1091
|
<!-- svelte-ignore ownership_invalid_binding -->
|
|
996
|
-
{@const { domainExtent: _de, constrain: _uc, apply: _apply, ...transformProps } = transform ?? {}}
|
|
1092
|
+
{@const { domainExtent: _de, constrain: _uc, apply: _apply, scaleExtent: _se, translateExtent: _te, ...transformProps } = transform ?? {}}
|
|
997
1093
|
<TransformContext
|
|
998
1094
|
bind:state={chartState.transformState}
|
|
999
1095
|
mode={transform?.mode ?? 'none'}
|
|
@@ -1001,6 +1097,8 @@
|
|
|
1001
1097
|
initialScale={resolvedApply.scale ? initialTransform?.scale : undefined}
|
|
1002
1098
|
{processTranslate}
|
|
1003
1099
|
{...transformProps}
|
|
1100
|
+
scaleExtent={resolvedScaleExtent}
|
|
1101
|
+
translateExtent={resolvedTranslateExtent}
|
|
1004
1102
|
constrain={composedConstrain}
|
|
1005
1103
|
disablePointer={(brush === true || (typeof brush === 'object' && !brush.disabled)) || transform?.disablePointer}
|
|
1006
1104
|
{ondragstart}
|
|
@@ -154,19 +154,21 @@
|
|
|
154
154
|
// --- Data mode: resolved items with optional motion ---
|
|
155
155
|
const dataMotionMap = createDataMotionMap(motion);
|
|
156
156
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
157
|
+
// Only create the data motion tracking effect when motion is actually configured
|
|
158
|
+
if (dataMotionMap) {
|
|
159
|
+
$effect(() => {
|
|
160
|
+
if (!dataMode) return;
|
|
161
|
+
const activeKeys = new Set<any>();
|
|
162
|
+
for (let i = 0; i < resolvedData.length; i++) {
|
|
163
|
+
const d = resolvedData[i];
|
|
164
|
+
const key = keyFn(d, i);
|
|
165
|
+
activeKeys.add(key);
|
|
166
|
+
const resolved = resolveCircle(d);
|
|
167
|
+
untrack(() => dataMotionMap.update(key, resolved));
|
|
168
|
+
}
|
|
169
|
+
untrack(() => dataMotionMap.cleanup(activeKeys));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
170
172
|
|
|
171
173
|
// Single source of truth: resolved values with animated overlay
|
|
172
174
|
// Reading Spring .current here makes this reactive to animation frames
|
|
@@ -266,8 +268,9 @@
|
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
// TODO: Use objectId to work around Svelte 4 reactivity issue (even when memoizing gradients)
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
+
// Only create key trackers when in canvas mode (they're only used for canvas dep tracking)
|
|
272
|
+
const fillKey = layerCtx === 'canvas' ? createKey(() => fill) : undefined;
|
|
273
|
+
const strokeKey = layerCtx === 'canvas' ? createKey(() => stroke) : undefined;
|
|
271
274
|
|
|
272
275
|
chartCtx.registerComponent({
|
|
273
276
|
name: 'Circle',
|
|
@@ -296,9 +299,9 @@
|
|
|
296
299
|
motionCx.current,
|
|
297
300
|
motionCy.current,
|
|
298
301
|
motionR.current,
|
|
299
|
-
fillKey
|
|
302
|
+
fillKey!.current,
|
|
300
303
|
fillOpacity,
|
|
301
|
-
strokeKey
|
|
304
|
+
strokeKey!.current,
|
|
302
305
|
strokeWidth,
|
|
303
306
|
opacity,
|
|
304
307
|
className,
|
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
/** Grid height (rows). When set, `data` is treated as a flat grid array. */
|
|
18
18
|
height?: number;
|
|
19
19
|
|
|
20
|
+
/** Left bound of the raster in data coordinates. Defaults to `0` in grid mode. */
|
|
21
|
+
x1?: number;
|
|
22
|
+
/** Top bound of the raster in data coordinates. Defaults to `0` in grid mode. */
|
|
23
|
+
y1?: number;
|
|
24
|
+
/** Right bound of the raster in data coordinates. Defaults to `width` in grid mode. */
|
|
25
|
+
x2?: number;
|
|
26
|
+
/** Bottom bound of the raster in data coordinates. Defaults to `height` in grid mode. */
|
|
27
|
+
y2?: number;
|
|
28
|
+
|
|
20
29
|
/**
|
|
21
30
|
* Value channel. Interpretation depends on the type:
|
|
22
31
|
* - `(x, y) => number`: continuous function evaluated at each pixel (function sampling mode)
|
|
@@ -51,20 +60,32 @@
|
|
|
51
60
|
import { geoPath, geoTransform } from 'd3-geo';
|
|
52
61
|
import { scaleSequential } from 'd3-scale';
|
|
53
62
|
import { interpolateYlGnBu } from 'd3-scale-chromatic';
|
|
54
|
-
import { max, min
|
|
63
|
+
import { max, min } from 'd3-array';
|
|
55
64
|
|
|
56
65
|
import Group from './Group.svelte';
|
|
57
66
|
import Path from './Path.svelte';
|
|
58
67
|
import { accessor as resolveAccessor, chartDataArray } from '../utils/common.js';
|
|
59
68
|
import { getChartContext } from '../contexts/chart.js';
|
|
69
|
+
import { getGeoContext } from '../contexts/geo.js';
|
|
70
|
+
import {
|
|
71
|
+
blurGridIgnoringNaN,
|
|
72
|
+
gridCellCenterToBounds,
|
|
73
|
+
gridPointToBounds,
|
|
74
|
+
resolveRasterBounds,
|
|
75
|
+
} from '../utils/index.js';
|
|
60
76
|
import { interpolateGrid } from '../utils/rasterInterpolate.js';
|
|
61
77
|
|
|
62
78
|
const ctx = getChartContext();
|
|
79
|
+
const geo = getGeoContext();
|
|
63
80
|
|
|
64
81
|
let {
|
|
65
82
|
data: dataProp,
|
|
66
83
|
width: widthProp,
|
|
67
84
|
height: heightProp,
|
|
85
|
+
x1: x1Prop,
|
|
86
|
+
y1: y1Prop,
|
|
87
|
+
x2: x2Prop,
|
|
88
|
+
y2: y2Prop,
|
|
68
89
|
value: valueProp,
|
|
69
90
|
x: xProp,
|
|
70
91
|
y: yProp,
|
|
@@ -83,6 +104,12 @@
|
|
|
83
104
|
|
|
84
105
|
// Detect grid mode: data + width/height
|
|
85
106
|
const isGridMode = $derived(!!(dataProp && widthProp && heightProp));
|
|
107
|
+
const hasExplicitBounds = $derived(
|
|
108
|
+
x1Prop !== undefined || y1Prop !== undefined || x2Prop !== undefined || y2Prop !== undefined
|
|
109
|
+
);
|
|
110
|
+
const gridBounds = $derived(
|
|
111
|
+
resolveRasterBounds(widthProp ?? 0, heightProp ?? 0, x1Prop, y1Prop, x2Prop, y2Prop)
|
|
112
|
+
);
|
|
86
113
|
|
|
87
114
|
// Register as composite-mark with markInfo for grid mode domain participation
|
|
88
115
|
ctx.registerComponent({
|
|
@@ -92,8 +119,8 @@
|
|
|
92
119
|
if (!isGridMode) return {};
|
|
93
120
|
return {
|
|
94
121
|
data: [
|
|
95
|
-
{ x:
|
|
96
|
-
{ x:
|
|
122
|
+
{ x: gridBounds.x1, y: gridBounds.y1 },
|
|
123
|
+
{ x: gridBounds.x2, y: gridBounds.y2 },
|
|
97
124
|
],
|
|
98
125
|
x: 'x',
|
|
99
126
|
y: 'y',
|
|
@@ -108,6 +135,9 @@
|
|
|
108
135
|
// Scale factors from grid coordinates to chart pixel coordinates
|
|
109
136
|
const scaleX = $derived(ctx.width / gridW);
|
|
110
137
|
const scaleY = $derived(ctx.height / gridH);
|
|
138
|
+
const contourScaleX = $derived(gridW / ctx.width);
|
|
139
|
+
const contourScaleY = $derived(gridH / ctx.height);
|
|
140
|
+
const useProjectedGridSampling = $derived(!!(geo.projection && isGridMode && hasExplicitBounds));
|
|
111
141
|
|
|
112
142
|
// Resolve grid values from one of three input modes
|
|
113
143
|
const gridValues = $derived.by(() => {
|
|
@@ -115,9 +145,7 @@
|
|
|
115
145
|
|
|
116
146
|
// Mode 1: Grid data (flat array + width/height)
|
|
117
147
|
if (isGridMode) {
|
|
118
|
-
return dataProp instanceof Float64Array
|
|
119
|
-
? dataProp
|
|
120
|
-
: Float64Array.from(dataProp as number[]);
|
|
148
|
+
return dataProp instanceof Float64Array ? dataProp : Float64Array.from(dataProp as number[]);
|
|
121
149
|
}
|
|
122
150
|
|
|
123
151
|
// Mode 2: Continuous function — evaluate at each grid cell
|
|
@@ -144,7 +172,7 @@
|
|
|
144
172
|
|
|
145
173
|
const xAcc = xProp ? resolveAccessor(xProp) : ctx.x;
|
|
146
174
|
const yAcc = yProp ? resolveAccessor(yProp) : ctx.y;
|
|
147
|
-
const valAcc = resolveAccessor(valueProp ?? 'value');
|
|
175
|
+
const valAcc = resolveAccessor((valueProp ?? 'value') as Accessor);
|
|
148
176
|
|
|
149
177
|
const points: [number, number, number][] = chartData.map((d: any) => [
|
|
150
178
|
ctx.xScale(xAcc(d)) / scaleX,
|
|
@@ -155,12 +183,39 @@
|
|
|
155
183
|
return interpolateGrid(points, gridW, gridH, interpolateMethod);
|
|
156
184
|
});
|
|
157
185
|
|
|
186
|
+
const projectedGridPoints = $derived.by(() => {
|
|
187
|
+
if (!useProjectedGridSampling || !widthProp || !heightProp || !geo.projection) return [];
|
|
188
|
+
|
|
189
|
+
const points: [number, number, number][] = [];
|
|
190
|
+
for (let row = 0; row < heightProp; row++) {
|
|
191
|
+
for (let column = 0; column < widthProp; column++) {
|
|
192
|
+
const value = gridValues[row * widthProp + column];
|
|
193
|
+
if (!Number.isFinite(value)) continue;
|
|
194
|
+
|
|
195
|
+
const point = gridCellCenterToBounds(column, row, widthProp, heightProp, gridBounds);
|
|
196
|
+
const projected = geo.projection([point.x, point.y]);
|
|
197
|
+
if (!projected || !Number.isFinite(projected[0]) || !Number.isFinite(projected[1]))
|
|
198
|
+
continue;
|
|
199
|
+
|
|
200
|
+
points.push([projected[0] * contourScaleX, projected[1] * contourScaleY, value]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return points;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const contourGridValues = $derived.by(() => {
|
|
208
|
+
if (useProjectedGridSampling) {
|
|
209
|
+
return interpolateGrid(projectedGridPoints, gridW, gridH, interpolateMethod);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return gridValues;
|
|
213
|
+
});
|
|
214
|
+
|
|
158
215
|
// Apply optional blur
|
|
159
216
|
const blurredValues = $derived.by(() => {
|
|
160
|
-
if (!blurRadius ||
|
|
161
|
-
|
|
162
|
-
blur2({ data: copy, width: gridW, height: gridH }, blurRadius);
|
|
163
|
-
return copy;
|
|
217
|
+
if (!blurRadius || contourGridValues.length === 0) return contourGridValues;
|
|
218
|
+
return blurGridIgnoringNaN(contourGridValues, gridW, gridH, blurRadius);
|
|
164
219
|
});
|
|
165
220
|
|
|
166
221
|
// Run d3 contour generator
|
|
@@ -171,8 +226,29 @@
|
|
|
171
226
|
return generator(Array.from(blurredValues));
|
|
172
227
|
});
|
|
173
228
|
|
|
174
|
-
// Path generator that
|
|
229
|
+
// Path generator that maps contour geometry into either data or geo space.
|
|
175
230
|
const pathGenerator = $derived.by(() => {
|
|
231
|
+
if (useProjectedGridSampling) {
|
|
232
|
+
return geoPath(
|
|
233
|
+
geoTransform({
|
|
234
|
+
point(x, y) {
|
|
235
|
+
this.stream.point(x * scaleX, y * scaleY);
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (isGridMode && hasExplicitBounds) {
|
|
242
|
+
return geoPath(
|
|
243
|
+
geoTransform({
|
|
244
|
+
point(x, y) {
|
|
245
|
+
const point = gridPointToBounds(x, y, gridW, gridH, gridBounds);
|
|
246
|
+
this.stream.point(ctx.xScale(point.x), ctx.yScale(point.y));
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
176
252
|
if (scaleX === 1 && scaleY === 1) return geoPath();
|
|
177
253
|
return geoPath(
|
|
178
254
|
geoTransform({
|
|
@@ -189,7 +265,8 @@
|
|
|
189
265
|
const minValue = min(contourData, (d) => d.value) ?? 0;
|
|
190
266
|
const maxValue = max(contourData, (d) => d.value) ?? 1;
|
|
191
267
|
if (ctx.cScale) {
|
|
192
|
-
|
|
268
|
+
const scale = ctx.cScale.copy();
|
|
269
|
+
return ctx.props.cDomain ? scale : scale.domain([minValue, maxValue]);
|
|
193
270
|
}
|
|
194
271
|
return scaleSequential([minValue, maxValue], interpolateYlGnBu);
|
|
195
272
|
});
|
|
@@ -14,6 +14,14 @@ export type ContourPropsWithoutHTML = {
|
|
|
14
14
|
width?: number;
|
|
15
15
|
/** Grid height (rows). When set, `data` is treated as a flat grid array. */
|
|
16
16
|
height?: number;
|
|
17
|
+
/** Left bound of the raster in data coordinates. Defaults to `0` in grid mode. */
|
|
18
|
+
x1?: number;
|
|
19
|
+
/** Top bound of the raster in data coordinates. Defaults to `0` in grid mode. */
|
|
20
|
+
y1?: number;
|
|
21
|
+
/** Right bound of the raster in data coordinates. Defaults to `width` in grid mode. */
|
|
22
|
+
x2?: number;
|
|
23
|
+
/** Bottom bound of the raster in data coordinates. Defaults to `height` in grid mode. */
|
|
24
|
+
y2?: number;
|
|
17
25
|
/**
|
|
18
26
|
* Value channel. Interpretation depends on the type:
|
|
19
27
|
* - `(x, y) => number`: continuous function evaluated at each pixel (function sampling mode)
|
|
@@ -170,18 +170,20 @@
|
|
|
170
170
|
// --- Data mode motion ---
|
|
171
171
|
const dataMotionMap = createDataMotionMap(motion);
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
173
|
+
if (dataMotionMap) {
|
|
174
|
+
$effect(() => {
|
|
175
|
+
if (!dataMode) return;
|
|
176
|
+
const activeKeys = new Set<any>();
|
|
177
|
+
for (let i = 0; i < resolvedData.length; i++) {
|
|
178
|
+
const d = resolvedData[i];
|
|
179
|
+
const key = keyFn(d, i);
|
|
180
|
+
activeKeys.add(key);
|
|
181
|
+
const resolved = resolveEllipse(d);
|
|
182
|
+
untrack(() => dataMotionMap.update(key, resolved));
|
|
183
|
+
}
|
|
184
|
+
untrack(() => dataMotionMap.cleanup(activeKeys));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
185
187
|
|
|
186
188
|
// Single source of truth: resolved values with animated overlay
|
|
187
189
|
const resolvedItems = $derived.by(() => {
|
|
@@ -290,8 +292,8 @@
|
|
|
290
292
|
}
|
|
291
293
|
|
|
292
294
|
// TODO: Use objectId to work around Svelte 4 reactivity issue (even when memoizing gradients)
|
|
293
|
-
const fillKey = createKey(() => fill);
|
|
294
|
-
const strokeKey = createKey(() => stroke);
|
|
295
|
+
const fillKey = layerCtx === 'canvas' ? createKey(() => fill) : undefined;
|
|
296
|
+
const strokeKey = layerCtx === 'canvas' ? createKey(() => stroke) : undefined;
|
|
295
297
|
|
|
296
298
|
chartCtx.registerComponent({
|
|
297
299
|
name: 'Ellipse',
|
|
@@ -321,9 +323,9 @@
|
|
|
321
323
|
motionCy.current,
|
|
322
324
|
motionRx.current,
|
|
323
325
|
motionRy.current,
|
|
324
|
-
fillKey
|
|
326
|
+
fillKey!.current,
|
|
325
327
|
fillOpacity,
|
|
326
|
-
strokeKey
|
|
328
|
+
strokeKey!.current,
|
|
327
329
|
strokeWidth,
|
|
328
330
|
opacity,
|
|
329
331
|
className,
|