layerchart 2.0.0-next.57 → 2.0.0-next.58
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/components/AnnotationLine.svelte +112 -66
- package/dist/components/AnnotationLine.svelte.d.ts +10 -2
- package/dist/components/AnnotationPoint.svelte +97 -23
- package/dist/components/AnnotationPoint.svelte.d.ts +8 -1
- package/dist/components/GeoPath.svelte +4 -4
- package/dist/components/Link.svelte +261 -75
- package/dist/components/Link.svelte.d.ts +69 -26
- package/dist/components/Text.svelte +1 -1
- package/dist/components/Voronoi.svelte +35 -6
- package/dist/components/Voronoi.svelte.d.ts +9 -0
- package/dist/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-separate-data-per-series-should-render-stacked-series-with-separate-data-arrays-1.png +0 -0
- package/dist/components/charts/__screenshots__/BarChart.svelte.test.ts/BarChart-separate-data-per-series-should-render-stacked-series-with-separate-data-arrays-2.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-ScatterChart--single-point--quadtree-mode--should-show-series-header-for-multi-series-1.png +0 -0
- package/dist/components/charts/__screenshots__/DefaultTooltip.svelte.test.ts/DefaultTooltip-ScatterChart--single-point--quadtree-mode--should-show-series-header-for-multi-series-2.png +0 -0
- package/dist/components/index.d.ts +0 -2
- package/dist/components/index.js +0 -2
- package/dist/components/tooltip/TooltipContext.svelte +39 -10
- package/dist/components/tooltip/TooltipContext.svelte.d.ts +14 -0
- package/dist/states/brush.svelte.d.ts +1 -1
- package/dist/states/chart.svelte.js +24 -8
- package/dist/states/chart.svelte.test.js +181 -0
- package/dist/utils/linkUtils.d.ts +42 -0
- package/dist/utils/{connectorUtils.js → linkUtils.js} +56 -6
- package/package.json +1 -1
- package/dist/components/Connector.svelte +0 -167
- package/dist/components/Connector.svelte.d.ts +0 -56
- package/dist/utils/connectorUtils.d.ts +0 -34
|
@@ -46,8 +46,6 @@ export { default as ClipPath } from './ClipPath.svelte';
|
|
|
46
46
|
export * from './ClipPath.svelte';
|
|
47
47
|
export { default as ColorRamp } from './ColorRamp.svelte';
|
|
48
48
|
export * from './ColorRamp.svelte';
|
|
49
|
-
export { default as Connector } from './Connector.svelte';
|
|
50
|
-
export * from './Connector.svelte';
|
|
51
49
|
export { default as Contour } from './Contour.svelte';
|
|
52
50
|
export * from './Contour.svelte';
|
|
53
51
|
export { default as Dagre } from './Dagre.svelte';
|
package/dist/components/index.js
CHANGED
|
@@ -46,8 +46,6 @@ export { default as ClipPath } from './ClipPath.svelte';
|
|
|
46
46
|
export * from './ClipPath.svelte';
|
|
47
47
|
export { default as ColorRamp } from './ColorRamp.svelte';
|
|
48
48
|
export * from './ColorRamp.svelte';
|
|
49
|
-
export { default as Connector } from './Connector.svelte';
|
|
50
|
-
export * from './Connector.svelte';
|
|
51
49
|
export { default as Contour } from './Contour.svelte';
|
|
52
50
|
export * from './Contour.svelte';
|
|
53
51
|
export { default as Dagre } from './Dagre.svelte';
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import { asAny, type Without } from '../../utils/types.js';
|
|
4
4
|
import type { TooltipState as TooltipStateType } from '../../states/tooltip.svelte.js';
|
|
5
|
+
import type { Accessor } from '../../utils/common.js';
|
|
5
6
|
|
|
6
7
|
export type TooltipMode =
|
|
7
8
|
| 'bisect-x' // requires values to be sorted
|
|
@@ -55,6 +56,21 @@
|
|
|
55
56
|
*/
|
|
56
57
|
radius?: number;
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Override the `x` accessor used for quadtree/voronoi hit detection.
|
|
61
|
+
* Useful when the chart's `x` accessor returns an array of values and you
|
|
62
|
+
* want hit detection at a specific endpoint.
|
|
63
|
+
*
|
|
64
|
+
* Accepts a string property name (e.g. `'POP_2015'`) or a function.
|
|
65
|
+
*/
|
|
66
|
+
x?: Accessor;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Override the `y` accessor used for quadtree/voronoi hit detection.
|
|
70
|
+
* See `x` above.
|
|
71
|
+
*/
|
|
72
|
+
y?: Accessor;
|
|
73
|
+
|
|
58
74
|
/**
|
|
59
75
|
* Enable debug view (show hit targets, etc)
|
|
60
76
|
* @default false
|
|
@@ -131,6 +147,8 @@
|
|
|
131
147
|
radius = Infinity,
|
|
132
148
|
raiseTarget = false,
|
|
133
149
|
state: stateProp = $bindable() as TooltipStateType<TData>,
|
|
150
|
+
x: xProp,
|
|
151
|
+
y: yProp,
|
|
134
152
|
children,
|
|
135
153
|
}: TooltipContextProps<TData> = $props();
|
|
136
154
|
|
|
@@ -411,6 +429,9 @@
|
|
|
411
429
|
}, hideDelay);
|
|
412
430
|
}
|
|
413
431
|
|
|
432
|
+
const xAccessorOverride = $derived(xProp != null ? accessor(xProp) : undefined);
|
|
433
|
+
const yAccessorOverride = $derived(yProp != null ? accessor(yProp) : undefined);
|
|
434
|
+
|
|
414
435
|
const quadtree: Quadtree<[number, number]> | undefined = $derived.by(() => {
|
|
415
436
|
if (['quadtree', 'quadtree-x', 'quadtree-y'].includes(mode)) {
|
|
416
437
|
return d3Quadtree()
|
|
@@ -419,6 +440,11 @@
|
|
|
419
440
|
return 0;
|
|
420
441
|
}
|
|
421
442
|
|
|
443
|
+
if (xAccessorOverride) {
|
|
444
|
+
const scaled = ctx.xScale(xAccessorOverride(d));
|
|
445
|
+
return typeof scaled === 'number' ? scaled : 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
422
448
|
if (geo.projection) {
|
|
423
449
|
const lat = ctx.x(d);
|
|
424
450
|
const long = ctx.y(d);
|
|
@@ -429,11 +455,10 @@
|
|
|
429
455
|
const value = ctx.xGet(d);
|
|
430
456
|
|
|
431
457
|
if (Array.isArray(value)) {
|
|
432
|
-
// `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
return min(value);
|
|
458
|
+
// `x` accessor with multiple properties (ex. `x={['start', 'end']})`).
|
|
459
|
+
// Default to the max (typically the "target"/"end" endpoint); override
|
|
460
|
+
// via the `x` prop for explicit control.
|
|
461
|
+
return max(value);
|
|
437
462
|
} else {
|
|
438
463
|
return value;
|
|
439
464
|
}
|
|
@@ -443,6 +468,11 @@
|
|
|
443
468
|
return 0;
|
|
444
469
|
}
|
|
445
470
|
|
|
471
|
+
if (yAccessorOverride) {
|
|
472
|
+
const scaled = ctx.yScale(yAccessorOverride(d));
|
|
473
|
+
return typeof scaled === 'number' ? scaled : 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
446
476
|
if (geo.projection) {
|
|
447
477
|
const lat = ctx.x(d);
|
|
448
478
|
const long = ctx.y(d);
|
|
@@ -453,11 +483,8 @@
|
|
|
453
483
|
const value = ctx.yGet(d);
|
|
454
484
|
|
|
455
485
|
if (Array.isArray(value)) {
|
|
456
|
-
// `
|
|
457
|
-
|
|
458
|
-
// const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2);
|
|
459
|
-
// return midpoint;
|
|
460
|
-
return min(value);
|
|
486
|
+
// `y` accessor with multiple properties — default to max endpoint.
|
|
487
|
+
return max(value);
|
|
461
488
|
} else {
|
|
462
489
|
return value;
|
|
463
490
|
}
|
|
@@ -676,6 +703,8 @@
|
|
|
676
703
|
{#if mode === 'voronoi'}
|
|
677
704
|
<Svg>
|
|
678
705
|
<Voronoi
|
|
706
|
+
x={xProp}
|
|
707
|
+
y={yProp}
|
|
679
708
|
r={radius}
|
|
680
709
|
onpointerenter={(e, { data }) => {
|
|
681
710
|
showTooltip(e, data);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
2
|
import { type Without } from '../../utils/types.js';
|
|
3
3
|
import type { TooltipState as TooltipStateType } from '../../states/tooltip.svelte.js';
|
|
4
|
+
import type { Accessor } from '../../utils/common.js';
|
|
4
5
|
export type TooltipMode = 'bisect-x' | 'bisect-y' | 'band' | 'bisect-band' | 'bounds' | 'voronoi' | 'quadtree' | 'quadtree-x' | 'quadtree-y' | 'manual';
|
|
5
6
|
type TooltipContextPropsWithoutHTML<T = any> = {
|
|
6
7
|
/**
|
|
@@ -36,6 +37,19 @@ type TooltipContextPropsWithoutHTML<T = any> = {
|
|
|
36
37
|
* @default Infinity
|
|
37
38
|
*/
|
|
38
39
|
radius?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Override the `x` accessor used for quadtree/voronoi hit detection.
|
|
42
|
+
* Useful when the chart's `x` accessor returns an array of values and you
|
|
43
|
+
* want hit detection at a specific endpoint.
|
|
44
|
+
*
|
|
45
|
+
* Accepts a string property name (e.g. `'POP_2015'`) or a function.
|
|
46
|
+
*/
|
|
47
|
+
x?: Accessor;
|
|
48
|
+
/**
|
|
49
|
+
* Override the `y` accessor used for quadtree/voronoi hit detection.
|
|
50
|
+
* See `x` above.
|
|
51
|
+
*/
|
|
52
|
+
y?: Accessor;
|
|
39
53
|
/**
|
|
40
54
|
* Enable debug view (show hit targets, etc)
|
|
41
55
|
* @default false
|
|
@@ -35,7 +35,7 @@ export declare class BrushState {
|
|
|
35
35
|
x: BrushDomainType;
|
|
36
36
|
y: BrushDomainType;
|
|
37
37
|
active: boolean | undefined;
|
|
38
|
-
axis: "
|
|
38
|
+
axis: "x" | "y" | "both";
|
|
39
39
|
handleSize: number;
|
|
40
40
|
constructor(ctx: typeof this.ctx, options?: {
|
|
41
41
|
x?: BrushDomainType;
|
|
@@ -174,6 +174,13 @@ export class ChartState {
|
|
|
174
174
|
// Use the value axis accessor (y for horizontal charts, x for vertical).
|
|
175
175
|
const valueAxis = this.valueAxis;
|
|
176
176
|
const chartValueProp = valueAxis === 'y' ? this.props.y : this.props.x;
|
|
177
|
+
const chartValueKeys = Array.isArray(chartValueProp)
|
|
178
|
+
? chartValueProp.filter((k) => typeof k === 'string')
|
|
179
|
+
: typeof chartValueProp === 'string'
|
|
180
|
+
? [chartValueProp]
|
|
181
|
+
: [];
|
|
182
|
+
const chartHasOwnData = this.props.data != null &&
|
|
183
|
+
(!Array.isArray(this.props.data) || this.props.data.length > 0);
|
|
177
184
|
const implicitSeries = [];
|
|
178
185
|
for (const { info } of this._markInfos) {
|
|
179
186
|
const valueAccessor = valueAxis === 'y' ? info.y : info.x;
|
|
@@ -181,10 +188,12 @@ export class ChartState {
|
|
|
181
188
|
(typeof valueAccessor === 'string' ? valueAccessor : undefined);
|
|
182
189
|
if (!key)
|
|
183
190
|
continue;
|
|
184
|
-
// Skip if the mark
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
191
|
+
// Skip if the mark's key matches one of the chart's axis accessors.
|
|
192
|
+
// When the chart has its own data, any mark using that accessor is just
|
|
193
|
+
// decorating (e.g. a filtered label subset), not defining a new series.
|
|
194
|
+
// Without explicit chart data, still skip unless the mark has its own
|
|
195
|
+
// dataset (multi-dataset scenario).
|
|
196
|
+
if (chartValueKeys.includes(key) && (chartHasOwnData || !info.data))
|
|
188
197
|
continue;
|
|
189
198
|
if (implicitSeries.some((s) => s.key === key))
|
|
190
199
|
continue;
|
|
@@ -316,14 +325,21 @@ export class ChartState {
|
|
|
316
325
|
// Use $derived fields instead of getters for caching
|
|
317
326
|
containerWidth = $derived(this.props.width ?? this._containerWidth);
|
|
318
327
|
containerHeight = $derived(this.props.height ?? this._containerHeight);
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
328
|
+
// When `<Chart data>` is passed with a non-empty dataset, it's canonical —
|
|
329
|
+
// marks with their own `data` (e.g. filtered label subsets) still contribute
|
|
330
|
+
// to `flatData` for domain calculation but don't replace iteration data.
|
|
331
|
+
// Otherwise fall back to `visibleSeriesData` so simplified charts that pass
|
|
332
|
+
// data via series definitions still work, with reactive recomputation when
|
|
333
|
+
// series are shown/hidden via legend.
|
|
322
334
|
data = $derived.by(() => {
|
|
335
|
+
const propsData = this.props.data;
|
|
336
|
+
if (propsData != null && (!Array.isArray(propsData) || propsData.length > 0)) {
|
|
337
|
+
return propsData;
|
|
338
|
+
}
|
|
323
339
|
if (this.seriesState?.visibleSeriesData?.length) {
|
|
324
340
|
return this.seriesState.visibleSeriesData;
|
|
325
341
|
}
|
|
326
|
-
return
|
|
342
|
+
return [];
|
|
327
343
|
});
|
|
328
344
|
flatData = $derived.by(() => {
|
|
329
345
|
const base = (this.props.flatData ?? this.data);
|
|
@@ -534,6 +534,187 @@ describe('ChartState mark registration', () => {
|
|
|
534
534
|
}
|
|
535
535
|
});
|
|
536
536
|
});
|
|
537
|
+
describe('ChartState data vs visibleSeriesData', () => {
|
|
538
|
+
it('should return props.data when explicit, even if a mark registers a filtered subset', () => {
|
|
539
|
+
const fullData = [
|
|
540
|
+
{ date: '2024-01', value: 10 },
|
|
541
|
+
{ date: '2024-02', value: 20 },
|
|
542
|
+
{ date: '2024-03', value: 30 },
|
|
543
|
+
];
|
|
544
|
+
const highlighted = [fullData[0]]; // filtered subset
|
|
545
|
+
const { state, cleanup } = createChartState({
|
|
546
|
+
data: fullData,
|
|
547
|
+
x: 'date',
|
|
548
|
+
y: 'value',
|
|
549
|
+
});
|
|
550
|
+
try {
|
|
551
|
+
// Simulate a decorative mark (e.g. <Text data={highlighted}>) registering
|
|
552
|
+
// its own filtered dataset with the same value accessor as the chart.
|
|
553
|
+
state.registerMark({ y: 'value', data: highlighted });
|
|
554
|
+
flushSync();
|
|
555
|
+
// ctx.data (used by sibling marks for iteration) should remain the full
|
|
556
|
+
// chart data, not be replaced by the filtered subset.
|
|
557
|
+
expect(state.data).toBe(fullData);
|
|
558
|
+
expect(state.data).toHaveLength(3);
|
|
559
|
+
}
|
|
560
|
+
finally {
|
|
561
|
+
cleanup();
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
it('should fall back to visibleSeriesData when props.data is not provided', () => {
|
|
565
|
+
const applesData = [
|
|
566
|
+
{ date: '2024-01', value: 10 },
|
|
567
|
+
{ date: '2024-02', value: 20 },
|
|
568
|
+
];
|
|
569
|
+
const bananasData = [
|
|
570
|
+
{ date: '2024-01', value: 15 },
|
|
571
|
+
{ date: '2024-02', value: 25 },
|
|
572
|
+
];
|
|
573
|
+
const { state, cleanup } = createChartState({
|
|
574
|
+
x: 'date',
|
|
575
|
+
y: 'value',
|
|
576
|
+
series: [
|
|
577
|
+
{ key: 'apples', data: applesData },
|
|
578
|
+
{ key: 'bananas', data: bananasData },
|
|
579
|
+
],
|
|
580
|
+
});
|
|
581
|
+
try {
|
|
582
|
+
// No props.data — ctx.data should flatten series data for iteration.
|
|
583
|
+
expect(state.data).toHaveLength(4);
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
cleanup();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
it('should not create an implicit series for a decorative mark when chart has own data', () => {
|
|
590
|
+
// Scenario: <Chart data={full}> + <Text data={highlighted} y="value"> (labels)
|
|
591
|
+
// The Text mark shouldn't create an implicit series that narrows the domain.
|
|
592
|
+
const fullData = [
|
|
593
|
+
{ date: '2024-01', value: 10 },
|
|
594
|
+
{ date: '2024-02', value: 50 },
|
|
595
|
+
{ date: '2024-03', value: 100 },
|
|
596
|
+
];
|
|
597
|
+
const highlighted = [fullData[1]];
|
|
598
|
+
const { state, cleanup } = createChartState({
|
|
599
|
+
data: fullData,
|
|
600
|
+
x: 'date',
|
|
601
|
+
y: 'value',
|
|
602
|
+
});
|
|
603
|
+
try {
|
|
604
|
+
state.registerMark({ y: 'value', data: highlighted });
|
|
605
|
+
flushSync();
|
|
606
|
+
// Decorative mark shouldn't turn this into a multi-series chart.
|
|
607
|
+
expect(state.seriesState.isDefaultSeries).toBe(true);
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
cleanup();
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
it('should not create an implicit series when chart uses array y accessor matching the mark key', () => {
|
|
614
|
+
const fullData = [
|
|
615
|
+
{ date: '2024-01', v1: 10, v2: 20 },
|
|
616
|
+
{ date: '2024-02', v1: 30, v2: 40 },
|
|
617
|
+
];
|
|
618
|
+
const { state, cleanup } = createChartState({
|
|
619
|
+
data: fullData,
|
|
620
|
+
x: 'date',
|
|
621
|
+
y: ['v1', 'v2'],
|
|
622
|
+
});
|
|
623
|
+
try {
|
|
624
|
+
state.registerMark({ y: 'v2', data: [fullData[0]] });
|
|
625
|
+
flushSync();
|
|
626
|
+
expect(state.seriesState.isDefaultSeries).toBe(true);
|
|
627
|
+
}
|
|
628
|
+
finally {
|
|
629
|
+
cleanup();
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
it('should compute yDomain from full chart data when a decorative mark has a filtered subset', () => {
|
|
633
|
+
// Regression: Text labeling highlighted rows shouldn't narrow the y domain.
|
|
634
|
+
const fullData = [
|
|
635
|
+
{ date: '2024-01', value: 10 },
|
|
636
|
+
{ date: '2024-02', value: 50 },
|
|
637
|
+
{ date: '2024-03', value: 100 },
|
|
638
|
+
];
|
|
639
|
+
const highlighted = [fullData[1]]; // only value=50
|
|
640
|
+
const { state, cleanup } = createChartState({
|
|
641
|
+
data: fullData,
|
|
642
|
+
x: 'date',
|
|
643
|
+
y: 'value',
|
|
644
|
+
});
|
|
645
|
+
try {
|
|
646
|
+
state.registerMark({ y: 'value', data: highlighted });
|
|
647
|
+
flushSync();
|
|
648
|
+
// yDomain should reflect the full data extent [10, 100], not just [50, 50].
|
|
649
|
+
expect(state.yDomain).toEqual([10, 100]);
|
|
650
|
+
}
|
|
651
|
+
finally {
|
|
652
|
+
cleanup();
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
it('should compute yDomain across all array y accessors on Chart', () => {
|
|
656
|
+
const data = [
|
|
657
|
+
{ date: '2024-01', v1: 3, v2: 5 },
|
|
658
|
+
{ date: '2024-02', v1: 2, v2: 8 },
|
|
659
|
+
{ date: '2024-03', v1: 4, v2: 9 },
|
|
660
|
+
];
|
|
661
|
+
const { state, cleanup } = createChartState({
|
|
662
|
+
data,
|
|
663
|
+
x: 'date',
|
|
664
|
+
y: ['v1', 'v2'],
|
|
665
|
+
});
|
|
666
|
+
try {
|
|
667
|
+
// Domain should span min(v1) = 2 to max(v2) = 9
|
|
668
|
+
expect(state.yDomain).toEqual([2, 9]);
|
|
669
|
+
}
|
|
670
|
+
finally {
|
|
671
|
+
cleanup();
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
it('should keep full yDomain when decorative mark + array y both present', () => {
|
|
675
|
+
const data = [
|
|
676
|
+
{ date: '2024-01', v1: 3, v2: 5 },
|
|
677
|
+
{ date: '2024-02', v1: 2, v2: 8 },
|
|
678
|
+
{ date: '2024-03', v1: 4, v2: 9 },
|
|
679
|
+
];
|
|
680
|
+
const { state, cleanup } = createChartState({
|
|
681
|
+
data,
|
|
682
|
+
x: 'date',
|
|
683
|
+
y: ['v1', 'v2'],
|
|
684
|
+
});
|
|
685
|
+
try {
|
|
686
|
+
// Decorative Text mark with subset data and y matching one of chart's keys
|
|
687
|
+
state.registerMark({ y: 'v2', data: [data[0]] });
|
|
688
|
+
flushSync();
|
|
689
|
+
// Still the full range [2, 9], not narrowed to [5, 5]
|
|
690
|
+
expect(state.yDomain).toEqual([2, 9]);
|
|
691
|
+
expect(state.seriesState.isDefaultSeries).toBe(true);
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
cleanup();
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
it('should fall back to visibleSeriesData when props.data is an empty array', () => {
|
|
698
|
+
// Composite charts (BarChart, etc.) default `data = []` when not passed.
|
|
699
|
+
const applesData = [{ date: '2024-01', value: 10 }];
|
|
700
|
+
const bananasData = [{ date: '2024-01', value: 15 }];
|
|
701
|
+
const { state, cleanup } = createChartState({
|
|
702
|
+
data: [],
|
|
703
|
+
x: 'date',
|
|
704
|
+
y: 'value',
|
|
705
|
+
series: [
|
|
706
|
+
{ key: 'apples', data: applesData },
|
|
707
|
+
{ key: 'bananas', data: bananasData },
|
|
708
|
+
],
|
|
709
|
+
});
|
|
710
|
+
try {
|
|
711
|
+
expect(state.data).toHaveLength(2);
|
|
712
|
+
}
|
|
713
|
+
finally {
|
|
714
|
+
cleanup();
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
});
|
|
537
718
|
describe('ChartState geo projection skips markInfo', () => {
|
|
538
719
|
const geoData = [
|
|
539
720
|
{ name: 'New York', longitude: -74.006, latitude: 40.7128 },
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type CurveFactory } from 'd3-shape';
|
|
2
|
+
export type LinkCoords = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
};
|
|
6
|
+
export type PresetLinkType = 'straight' | 'square' | 'beveled' | 'rounded' | 'swoop';
|
|
7
|
+
export type LinkType = PresetLinkType | 'd3';
|
|
8
|
+
export type LinkSweep = 'horizontal-vertical' | 'vertical-horizontal' | 'none';
|
|
9
|
+
type GetLinkPresetPathProps = {
|
|
10
|
+
source: LinkCoords;
|
|
11
|
+
target: LinkCoords;
|
|
12
|
+
radius: number;
|
|
13
|
+
type: PresetLinkType;
|
|
14
|
+
sweep: LinkSweep;
|
|
15
|
+
/** Bend angle in degrees, used by 'swoop' type. Default 22.5. */
|
|
16
|
+
bend?: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function getLinkPresetPath(opts: GetLinkPresetPathProps): string;
|
|
19
|
+
type GetLinkD3PathProps = Omit<GetLinkPresetPathProps, 'radius' | 'type'> & {
|
|
20
|
+
curve: CurveFactory;
|
|
21
|
+
/**
|
|
22
|
+
* Cartesian orientation hint for axis-dependent curves (d3 step variants step
|
|
23
|
+
* along x by default; for 'vertical' we step along y to match the natural flow).
|
|
24
|
+
*/
|
|
25
|
+
orientation?: 'horizontal' | 'vertical';
|
|
26
|
+
};
|
|
27
|
+
export declare function getLinkD3Path({ source, target, sweep, curve, orientation, }: GetLinkD3PathProps): string;
|
|
28
|
+
type GetLinkRadialPresetPathProps = {
|
|
29
|
+
source: LinkCoords;
|
|
30
|
+
target: LinkCoords;
|
|
31
|
+
type: PresetLinkType;
|
|
32
|
+
radius: number;
|
|
33
|
+
bend?: number;
|
|
34
|
+
};
|
|
35
|
+
export declare function getLinkRadialPresetPath({ source, target, type, radius, bend, }: GetLinkRadialPresetPathProps): string;
|
|
36
|
+
type GetLinkRadialD3PathProps = {
|
|
37
|
+
source: LinkCoords;
|
|
38
|
+
target: LinkCoords;
|
|
39
|
+
curve?: CurveFactory;
|
|
40
|
+
};
|
|
41
|
+
export declare function getLinkRadialD3Path({ source, target, curve, }: GetLinkRadialD3PathProps): string;
|
|
42
|
+
export {};
|
|
@@ -58,27 +58,63 @@ function createRoundedPath(opts) {
|
|
|
58
58
|
return `M ${source.x} ${source.y} L ${pBeforeCorner.x} ${pBeforeCorner.y} A ${effectiveRadius} ${effectiveRadius} 0 0 ${sweepFlag} ${pAfterCorner.x} ${pAfterCorner.y} L ${target.x} ${target.y}`;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Swoop: circular arc between source and target. Equivalent to ObservablePlot's
|
|
63
|
+
* Arrow `bend` option — positive angle bends right (clockwise from source to
|
|
64
|
+
* target), negative bends left, 0 is a straight line.
|
|
65
|
+
*/
|
|
66
|
+
function createSwoopPath({ source, target, dx, dy, bend = 22.5 }) {
|
|
67
|
+
const chordLen = Math.hypot(dx, dy);
|
|
68
|
+
const bendRad = (bend * Math.PI) / 180;
|
|
69
|
+
if (Math.abs(bendRad) < 1e-6 || chordLen < 1e-6) {
|
|
70
|
+
return createDirectPath(source, target);
|
|
71
|
+
}
|
|
72
|
+
// Half-chord subtends `bend` at the arc center, so radius = chord / (2 * sin(bend))
|
|
73
|
+
const arcRadius = chordLen / (2 * Math.sin(Math.abs(bendRad)));
|
|
74
|
+
const largeArc = Math.abs(bend) > 90 ? 1 : 0;
|
|
75
|
+
const sweepFlag = bend > 0 ? 1 : 0;
|
|
76
|
+
return `M${source.x},${source.y}A${arcRadius},${arcRadius} 0 ${largeArc} ${sweepFlag} ${target.x},${target.y}`;
|
|
77
|
+
}
|
|
61
78
|
const pathStrategies = {
|
|
62
79
|
square: createSquarePath,
|
|
63
80
|
beveled: createBeveledPath,
|
|
64
81
|
rounded: createRoundedPath,
|
|
82
|
+
swoop: createSwoopPath,
|
|
65
83
|
};
|
|
66
|
-
export function
|
|
84
|
+
export function getLinkPresetPath(opts) {
|
|
67
85
|
const { source, target, type } = opts;
|
|
68
86
|
if (isSamePoint(source, target))
|
|
69
87
|
return '';
|
|
70
88
|
const dx = target.x - source.x;
|
|
71
89
|
const dy = target.y - source.y;
|
|
72
|
-
// straight line cases
|
|
73
|
-
if (type === 'straight' || isNearZero(dx) || isNearZero(dy)) {
|
|
90
|
+
// straight line cases (swoop still bends even when axis-aligned)
|
|
91
|
+
if (type === 'straight' || (type !== 'swoop' && (isNearZero(dx) || isNearZero(dy)))) {
|
|
74
92
|
return createDirectPath(source, target);
|
|
75
93
|
}
|
|
76
94
|
return (pathStrategies[type] || pathStrategies.square)({ ...opts, dx, dy });
|
|
77
95
|
}
|
|
78
96
|
const FALLBACK_PATH = 'M0,0L0,0';
|
|
79
|
-
export function
|
|
97
|
+
export function getLinkD3Path({ source, target, sweep, curve, orientation = 'horizontal', }) {
|
|
80
98
|
const dx = target.x - source.x;
|
|
81
99
|
const dy = target.y - source.y;
|
|
100
|
+
// d3 step curves always step along x. For vertical orientation, emit a
|
|
101
|
+
// y-axis step manually so the step sits between parent/child along depth.
|
|
102
|
+
if (orientation === 'vertical' && sweep === 'none') {
|
|
103
|
+
const { x: sx, y: sy } = source;
|
|
104
|
+
const { x: tx, y: ty } = target;
|
|
105
|
+
if (curve === curveStep) {
|
|
106
|
+
const my = (sy + ty) / 2;
|
|
107
|
+
return `M${sx},${sy}L${sx},${my}L${tx},${my}L${tx},${ty}`;
|
|
108
|
+
}
|
|
109
|
+
if (curve === curveStepBefore) {
|
|
110
|
+
// Bump near source: sibling (x) changes first, then depth (y)
|
|
111
|
+
return `M${sx},${sy}L${tx},${sy}L${tx},${ty}`;
|
|
112
|
+
}
|
|
113
|
+
if (curve === curveStepAfter) {
|
|
114
|
+
// Bump near target: depth (y) changes first, then sibling (x)
|
|
115
|
+
return `M${sx},${sy}L${sx},${ty}L${tx},${ty}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
82
118
|
const line = d3Line().curve(curve);
|
|
83
119
|
let points = [];
|
|
84
120
|
const isAligned = isNearZero(dx) || isNearZero(dy);
|
|
@@ -135,12 +171,26 @@ function radialGeometry(source, target) {
|
|
|
135
171
|
sweepFlag,
|
|
136
172
|
};
|
|
137
173
|
}
|
|
138
|
-
export function
|
|
174
|
+
export function getLinkRadialPresetPath({ source, target, type, radius, bend = 22.5, }) {
|
|
139
175
|
const g = radialGeometry(source, target);
|
|
140
176
|
const { sr, ta, tr, sc, ss, tc, ts, sx, sy, tx, ty, sweepFlag } = g;
|
|
141
177
|
if (type === 'straight') {
|
|
142
178
|
return `M${sx},${sy}L${tx},${ty}`;
|
|
143
179
|
}
|
|
180
|
+
if (type === 'swoop') {
|
|
181
|
+
// Circular arc in cartesian space between the polar-converted endpoints.
|
|
182
|
+
const dx = tx - sx;
|
|
183
|
+
const dy = ty - sy;
|
|
184
|
+
const chordLen = Math.hypot(dx, dy);
|
|
185
|
+
const bendRad = (bend * Math.PI) / 180;
|
|
186
|
+
if (Math.abs(bendRad) < 1e-6 || chordLen < 1e-6) {
|
|
187
|
+
return `M${sx},${sy}L${tx},${ty}`;
|
|
188
|
+
}
|
|
189
|
+
const arcRadius = chordLen / (2 * Math.sin(Math.abs(bendRad)));
|
|
190
|
+
const largeArc = Math.abs(bend) > 90 ? 1 : 0;
|
|
191
|
+
const arcSweep = bend > 0 ? 1 : 0;
|
|
192
|
+
return `M${sx},${sy}A${arcRadius},${arcRadius} 0 ${largeArc} ${arcSweep} ${tx},${ty}`;
|
|
193
|
+
}
|
|
144
194
|
if (type === 'rounded') {
|
|
145
195
|
// visx LinkRadialCurve: cubic Bezier with rotated offset (percent controls tension)
|
|
146
196
|
const percent = 0.2;
|
|
@@ -183,7 +233,7 @@ export function getConnectorRadialPresetPath({ source, target, type, radius, })
|
|
|
183
233
|
const p2y = cornerY + radialDir * r * ts;
|
|
184
234
|
return `M${sx},${sy}L${p1x},${p1y}L${p2x},${p2y}L${tx},${ty}`;
|
|
185
235
|
}
|
|
186
|
-
export function
|
|
236
|
+
export function getLinkRadialD3Path({ source, target, curve, }) {
|
|
187
237
|
const g = radialGeometry(source, target);
|
|
188
238
|
const { sr, tr, sc, ss, tc, ts, sx, sy, tx, ty, sweepFlag } = g;
|
|
189
239
|
// Step curves render as polar arcs/radials rather than cartesian stairs.
|
package/package.json
CHANGED