layerchart 2.0.0-next.56 → 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/AnnotationRange.svelte +17 -6
- 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/Spline.svelte +2 -2
- 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 +38 -12
- package/dist/states/chart.svelte.test.js +227 -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
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { Without } from '../utils/types.js';
|
|
2
|
+
import type { Accessor } from '../utils/common.js';
|
|
2
3
|
export type VoronoiPropsWithoutHTML = {
|
|
3
4
|
/**
|
|
4
5
|
* Override data instead of using context
|
|
5
6
|
*/
|
|
6
7
|
data?: any;
|
|
8
|
+
/**
|
|
9
|
+
* Override the `x` accessor used to place each point. Useful when the
|
|
10
|
+
* chart's `x` accessor returns an array of values (e.g. `['start', 'end']`)
|
|
11
|
+
* and you want to use a specific one.
|
|
12
|
+
*/
|
|
13
|
+
x?: Accessor;
|
|
14
|
+
/** Override the `y` accessor used to place each point. See `x` above. */
|
|
15
|
+
y?: Accessor;
|
|
7
16
|
/** Radius to clip voronoi cells. `0` or `undefined` to disables clipping */
|
|
8
17
|
r?: number;
|
|
9
18
|
/**
|
|
Binary file
|
|
Binary file
|
|
@@ -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);
|
|
@@ -678,8 +694,13 @@ export class ChartState {
|
|
|
678
694
|
rDomain = $derived(calcDomain('r', this.extents, this.props.rDomain));
|
|
679
695
|
x1Domain = $derived.by(() => {
|
|
680
696
|
if (this.props.x1Domain) {
|
|
681
|
-
|
|
682
|
-
|
|
697
|
+
// Only filter by visible series when series are configured — otherwise the
|
|
698
|
+
// full x1Domain is used as-is (composable charts without series).
|
|
699
|
+
if (this.seriesState.series.length > 0) {
|
|
700
|
+
const visibleKeys = new Set(this.seriesState.visibleSeries.map((s) => s.key));
|
|
701
|
+
return this.props.x1Domain.filter((key) => visibleKeys.has(key));
|
|
702
|
+
}
|
|
703
|
+
return this.props.x1Domain;
|
|
683
704
|
}
|
|
684
705
|
// Auto-derive for grouped series when x is the category axis
|
|
685
706
|
if (this.props.seriesLayout === 'group' && this.valueAxis === 'y') {
|
|
@@ -692,8 +713,13 @@ export class ChartState {
|
|
|
692
713
|
});
|
|
693
714
|
y1Domain = $derived.by(() => {
|
|
694
715
|
if (this.props.y1Domain) {
|
|
695
|
-
|
|
696
|
-
|
|
716
|
+
// Only filter by visible series when series are configured — otherwise the
|
|
717
|
+
// full y1Domain is used as-is (composable charts without series).
|
|
718
|
+
if (this.seriesState.series.length > 0) {
|
|
719
|
+
const visibleKeys = new Set(this.seriesState.visibleSeries.map((s) => s.key));
|
|
720
|
+
return this.props.y1Domain.filter((key) => visibleKeys.has(key));
|
|
721
|
+
}
|
|
722
|
+
return this.props.y1Domain;
|
|
697
723
|
}
|
|
698
724
|
// Auto-derive for grouped series when y is the category axis
|
|
699
725
|
if (this.props.seriesLayout === 'group' && this.valueAxis === 'x') {
|
|
@@ -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 },
|
|
@@ -1406,3 +1587,49 @@ describe('ChartState group layout auto-derives x1/y1', () => {
|
|
|
1406
1587
|
}
|
|
1407
1588
|
});
|
|
1408
1589
|
});
|
|
1590
|
+
describe('ChartState x1Domain/y1Domain without series', () => {
|
|
1591
|
+
const longData = [
|
|
1592
|
+
{ year: 2019, fruit: 'apples', value: 3840 },
|
|
1593
|
+
{ year: 2019, fruit: 'bananas', value: 1920 },
|
|
1594
|
+
{ year: 2018, fruit: 'apples', value: 1600 },
|
|
1595
|
+
{ year: 2018, fruit: 'bananas', value: 1440 },
|
|
1596
|
+
];
|
|
1597
|
+
it('should pass through explicit x1Domain when no series are configured', () => {
|
|
1598
|
+
const { state, cleanup } = createChartState({
|
|
1599
|
+
data: longData,
|
|
1600
|
+
x: 'year',
|
|
1601
|
+
xScale: scaleBand(),
|
|
1602
|
+
y: 'value',
|
|
1603
|
+
x1: 'fruit',
|
|
1604
|
+
x1Domain: ['apples', 'bananas'],
|
|
1605
|
+
x1Range: ({ xScale }) => [0, xScale.bandwidth()],
|
|
1606
|
+
});
|
|
1607
|
+
try {
|
|
1608
|
+
expect(state.seriesState.series).toHaveLength(0);
|
|
1609
|
+
expect(state.x1Domain).toEqual(['apples', 'bananas']);
|
|
1610
|
+
expect(state.x1Scale.domain()).toEqual(['apples', 'bananas']);
|
|
1611
|
+
}
|
|
1612
|
+
finally {
|
|
1613
|
+
cleanup();
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
it('should pass through explicit y1Domain when no series are configured', () => {
|
|
1617
|
+
const { state, cleanup } = createChartState({
|
|
1618
|
+
data: longData,
|
|
1619
|
+
y: 'year',
|
|
1620
|
+
yScale: scaleBand(),
|
|
1621
|
+
x: 'value',
|
|
1622
|
+
y1: 'fruit',
|
|
1623
|
+
y1Domain: ['apples', 'bananas'],
|
|
1624
|
+
y1Range: ({ yScale }) => [0, yScale.bandwidth()],
|
|
1625
|
+
});
|
|
1626
|
+
try {
|
|
1627
|
+
expect(state.seriesState.series).toHaveLength(0);
|
|
1628
|
+
expect(state.y1Domain).toEqual(['apples', 'bananas']);
|
|
1629
|
+
expect(state.y1Scale.domain()).toEqual(['apples', 'bananas']);
|
|
1630
|
+
}
|
|
1631
|
+
finally {
|
|
1632
|
+
cleanup();
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
});
|
|
@@ -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 {};
|