layerchart 2.0.0-next.57 → 2.0.0-next.59

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.
Files changed (28) hide show
  1. package/dist/components/AnnotationLine.svelte +112 -66
  2. package/dist/components/AnnotationLine.svelte.d.ts +10 -2
  3. package/dist/components/AnnotationPoint.svelte +97 -23
  4. package/dist/components/AnnotationPoint.svelte.d.ts +8 -1
  5. package/dist/components/GeoPath.svelte +4 -4
  6. package/dist/components/Legend.svelte +1 -0
  7. package/dist/components/Link.svelte +261 -75
  8. package/dist/components/Link.svelte.d.ts +69 -26
  9. package/dist/components/Text.svelte +1 -1
  10. package/dist/components/Voronoi.svelte +35 -6
  11. package/dist/components/Voronoi.svelte.d.ts +9 -0
  12. 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
  13. 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
  14. 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
  15. 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
  16. package/dist/components/index.d.ts +0 -2
  17. package/dist/components/index.js +0 -2
  18. package/dist/components/tooltip/TooltipContext.svelte +39 -10
  19. package/dist/components/tooltip/TooltipContext.svelte.d.ts +14 -0
  20. package/dist/states/brush.svelte.d.ts +1 -1
  21. package/dist/states/chart.svelte.js +24 -8
  22. package/dist/states/chart.svelte.test.js +181 -0
  23. package/dist/utils/linkUtils.d.ts +42 -0
  24. package/dist/utils/{connectorUtils.js → linkUtils.js} +56 -6
  25. package/package.json +1 -1
  26. package/dist/components/Connector.svelte +0 -167
  27. package/dist/components/Connector.svelte.d.ts +0 -56
  28. package/dist/utils/connectorUtils.d.ts +0 -34
@@ -5,12 +5,24 @@
5
5
  import type { SingleDomainType } from '../utils/scales.svelte.js';
6
6
 
7
7
  export type AnnotationLinePropsWithoutHTML = {
8
- /** x value of the point */
8
+ /** x value of the line (draws vertically across the full y range) */
9
9
  x?: SingleDomainType;
10
10
 
11
- /** y value of the point */
11
+ /** y value of the line (draws horizontally across the full x range) */
12
12
  y?: SingleDomainType;
13
13
 
14
+ /** x value of the line's start point. Takes precedence over `x`. Defaults to the start of the x range. */
15
+ x1?: SingleDomainType;
16
+
17
+ /** y value of the line's start point. Takes precedence over `y`. Defaults to the start of the y range. */
18
+ y1?: SingleDomainType;
19
+
20
+ /** x value of the line's end point. Takes precedence over `x`. Defaults to the end of the x range. */
21
+ x2?: SingleDomainType;
22
+
23
+ /** y value of the line's end point. Takes precedence over `y`. Defaults to the end of the y range. */
24
+ y2?: SingleDomainType;
25
+
14
26
  /** Label to display for line*/
15
27
  label?: string;
16
28
 
@@ -45,6 +57,10 @@
45
57
  const {
46
58
  x,
47
59
  y,
60
+ x1: x1Prop,
61
+ y1: y1Prop,
62
+ x2: x2Prop,
63
+ y2: y2Prop,
48
64
  label,
49
65
  labelPlacement = 'top-right',
50
66
  labelXOffset = 0,
@@ -54,74 +70,104 @@
54
70
 
55
71
  const ctx = getChartContext();
56
72
 
57
- const isVertical = $derived(x != null);
73
+ const isVertical = $derived(
74
+ x != null || (x1Prop != null && x2Prop != null && x1Prop === x2Prop)
75
+ );
58
76
 
59
77
  const line = $derived({
60
- x1: x ? ctx.xScale(x) : ctx.xRange[0],
61
- y1: y && !x ? ctx.yScale(y) : ctx.yRange[0],
62
- x2: x ? ctx.xScale(x) : ctx.xRange[1],
63
- y2: y ? ctx.yScale(y) : ctx.yRange[1],
78
+ x1: x1Prop != null ? ctx.xScale(x1Prop) : x != null ? ctx.xScale(x) : ctx.xRange[0],
79
+ y1:
80
+ y1Prop != null ? ctx.yScale(y1Prop) : y != null && x == null ? ctx.yScale(y) : ctx.yRange[0],
81
+ x2: x2Prop != null ? ctx.xScale(x2Prop) : x != null ? ctx.xScale(x) : ctx.xRange[1],
82
+ y2: y2Prop != null ? ctx.yScale(y2Prop) : y != null ? ctx.yScale(y) : ctx.yRange[1],
64
83
  });
65
84
 
66
- const labelProps = $derived<ComponentProps<typeof Text>>(
67
- isVertical
68
- ? {
69
- x: line.x1 + (labelPlacement.includes('left') ? -labelXOffset : labelXOffset),
70
- y:
71
- (labelPlacement.includes('top')
72
- ? line.y2
73
- : labelPlacement.includes('bottom')
74
- ? line.y1
75
- : (line.y1 - line.y2) / 2) +
76
- (['top', 'bottom-left', 'bottom-right'].includes(labelPlacement)
77
- ? -labelYOffset
78
- : labelYOffset),
79
- dy: -2, // adjust for smaller font size
80
- textAnchor: labelPlacement.includes('left')
81
- ? 'end'
82
- : labelPlacement.includes('right')
83
- ? 'start'
84
- : 'middle',
85
- verticalAnchor:
86
- labelPlacement === 'top'
87
- ? 'end' // place above line
88
- : labelPlacement === 'bottom'
89
- ? 'start' // place below line
90
- : labelPlacement.includes('top')
91
- ? 'start'
92
- : labelPlacement.includes('bottom')
93
- ? 'end'
94
- : 'middle',
95
- }
96
- : {
97
- x:
98
- (labelPlacement.includes('left')
99
- ? line.x1
100
- : labelPlacement.includes('right')
101
- ? line.x2
102
- : (line.x2 - line.x1) / 2) +
103
- (['left', 'top-right', 'bottom-right'].includes(labelPlacement)
104
- ? -labelXOffset
105
- : labelXOffset),
106
- y: line.y1 + (labelPlacement.includes('top') ? -labelYOffset : labelYOffset),
107
- dy: -2, // adjust for smaller font size
108
- textAnchor:
109
- labelPlacement === 'left'
110
- ? 'end' // place beside line
111
- : labelPlacement === 'right'
112
- ? 'start' // place beside line
113
- : labelPlacement.includes('left')
114
- ? 'start'
115
- : labelPlacement.includes('right')
116
- ? 'end'
117
- : 'middle',
118
- verticalAnchor: labelPlacement.includes('top')
119
- ? 'end'
120
- : labelPlacement.includes('bottom')
121
- ? 'start'
122
- : 'middle',
123
- }
124
- );
85
+ const isSloped = $derived(!isVertical && line.x1 !== line.x2 && line.y1 !== line.y2);
86
+
87
+ // Angle of the line in degrees, normalized to [-90, 90] so text stays upright
88
+ const slopeAngle = $derived.by(() => {
89
+ let angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1) * (180 / Math.PI);
90
+ if (angle > 90) angle -= 180;
91
+ else if (angle < -90) angle += 180;
92
+ return angle;
93
+ });
94
+
95
+ const labelProps = $derived.by<ComponentProps<typeof Text>>(() => {
96
+ const isLeft = labelPlacement.includes('left');
97
+ const isRight = labelPlacement.includes('right');
98
+ const isTop = labelPlacement.includes('top');
99
+ const isBottom = labelPlacement.includes('bottom');
100
+
101
+ if (isVertical) {
102
+ return {
103
+ x: line.x1 + (isLeft ? -labelXOffset : labelXOffset),
104
+ y:
105
+ (isTop ? line.y2 : isBottom ? line.y1 : (line.y1 - line.y2) / 2) +
106
+ (['top', 'bottom-left', 'bottom-right'].includes(labelPlacement)
107
+ ? -labelYOffset
108
+ : labelYOffset),
109
+ dy: -2, // adjust for smaller font size
110
+ textAnchor: isLeft ? 'end' : isRight ? 'start' : 'middle',
111
+ verticalAnchor:
112
+ labelPlacement === 'top'
113
+ ? 'end' // place above line
114
+ : labelPlacement === 'bottom'
115
+ ? 'start' // place below line
116
+ : isTop
117
+ ? 'start'
118
+ : isBottom
119
+ ? 'end'
120
+ : 'middle',
121
+ };
122
+ }
123
+
124
+ const x = isLeft ? line.x1 : isRight ? line.x2 : (line.x1 + line.x2) / 2;
125
+ const y = isLeft ? line.y1 : isRight ? line.y2 : (line.y1 + line.y2) / 2;
126
+ const textAnchor =
127
+ labelPlacement === 'left'
128
+ ? 'end' // place beside line
129
+ : labelPlacement === 'right'
130
+ ? 'start' // place beside line
131
+ : isLeft
132
+ ? 'start'
133
+ : isRight
134
+ ? 'end'
135
+ : 'middle';
136
+ const verticalAnchor = isTop ? 'end' : isBottom ? 'start' : 'middle';
137
+
138
+ if (isSloped) {
139
+ // Project along-line and perpendicular offsets onto screen dx/dy so
140
+ // labelXOffset/labelYOffset track the slope rather than the viewport axes.
141
+ const aSign = ['left', 'top-right', 'bottom-right'].includes(labelPlacement) ? -1 : 1;
142
+ const pSign = isTop ? 1 : -1;
143
+ const alongLine = aSign * labelXOffset;
144
+ const perpAbove = pSign * labelYOffset + 2; // +2 for font baseline
145
+ const theta = (slopeAngle * Math.PI) / 180;
146
+ const cosT = Math.cos(theta);
147
+ const sinT = Math.sin(theta);
148
+ return {
149
+ x,
150
+ y,
151
+ rotate: slopeAngle,
152
+ dx: alongLine * cosT + perpAbove * sinT,
153
+ dy: alongLine * sinT - perpAbove * cosT,
154
+ textAnchor,
155
+ verticalAnchor,
156
+ };
157
+ }
158
+
159
+ return {
160
+ x:
161
+ x +
162
+ (['left', 'top-right', 'bottom-right'].includes(labelPlacement)
163
+ ? -labelXOffset
164
+ : labelXOffset),
165
+ y: y + (isTop ? -labelYOffset : labelYOffset),
166
+ dy: -2, // adjust for smaller font size
167
+ textAnchor,
168
+ verticalAnchor,
169
+ };
170
+ });
125
171
  </script>
126
172
 
127
173
  <Line
@@ -3,10 +3,18 @@ import type { SVGAttributes } from 'svelte/elements';
3
3
  import type { CommonStyleProps, Without } from '../utils/types.js';
4
4
  import type { SingleDomainType } from '../utils/scales.svelte.js';
5
5
  export type AnnotationLinePropsWithoutHTML = {
6
- /** x value of the point */
6
+ /** x value of the line (draws vertically across the full y range) */
7
7
  x?: SingleDomainType;
8
- /** y value of the point */
8
+ /** y value of the line (draws horizontally across the full x range) */
9
9
  y?: SingleDomainType;
10
+ /** x value of the line's start point. Takes precedence over `x`. Defaults to the start of the x range. */
11
+ x1?: SingleDomainType;
12
+ /** y value of the line's start point. Takes precedence over `y`. Defaults to the start of the y range. */
13
+ y1?: SingleDomainType;
14
+ /** x value of the line's end point. Takes precedence over `x`. Defaults to the end of the x range. */
15
+ x2?: SingleDomainType;
16
+ /** y value of the line's end point. Takes precedence over `y`. Defaults to the end of the y range. */
17
+ y2?: SingleDomainType;
10
18
  /** Label to display for line*/
11
19
  label?: string;
12
20
  /** Placement of the label */
@@ -14,7 +14,7 @@
14
14
  /** Radius of the circle */
15
15
  r?: number;
16
16
 
17
- /** Label to display on circle*/
17
+ /** Label to display on circle */
18
18
  label?: string;
19
19
 
20
20
  /** Placement of the label */
@@ -26,6 +26,13 @@
26
26
  /** Y offset of the label */
27
27
  labelYOffset?: number;
28
28
 
29
+ /**
30
+ * Draw a `<Link>` from the ring edge to the label (d3-ring-note style).
31
+ * Pass `true` for a straight line, or an object to configure the `Link`
32
+ * (e.g. `{ type: 'beveled', radius: 20 }`).
33
+ */
34
+ link?: boolean | Partial<ComponentProps<typeof Link>>;
35
+
29
36
  /** Details (description, etc) useful to display in tooltip */
30
37
  details?: any;
31
38
 
@@ -42,7 +49,9 @@
42
49
 
43
50
  <script lang="ts">
44
51
  import { getChartContext } from '../contexts/chart.js';
52
+ import { getGeoContext } from '../contexts/geo.js';
45
53
  import Circle from './Circle.svelte';
54
+ import Link from './Link.svelte';
46
55
  import Text from './Text.svelte';
47
56
  import type { Placement } from './types.js';
48
57
 
@@ -56,39 +65,78 @@
56
65
  labelPlacement = 'center',
57
66
  labelXOffset = 0,
58
67
  labelYOffset = 0,
68
+ link,
59
69
  details,
60
70
  props,
61
71
  }: AnnotationPointProps = $props();
62
72
 
63
73
  const ctx = getChartContext();
74
+ const geo = getGeoContext();
64
75
 
65
- const point = $derived({
66
- x: x ? ctx.xScale(x) + (isScaleBand(ctx.xScale) ? ctx.xScale.bandwidth() / 2 : 0) : 0,
67
- y: y ? ctx.yScale(y) + (isScaleBand(ctx.yScale) ? ctx.yScale.bandwidth() / 2 : 0) : ctx.height,
76
+ const point = $derived.by(() => {
77
+ // Inside a geo chart, interpret `x`/`y` as `[lon, lat]` and project directly.
78
+ if (geo.projection && typeof x === 'number' && typeof y === 'number') {
79
+ const [px, py] = geo.projection([x, y]) ?? [0, 0];
80
+ return { x: px, y: py };
81
+ }
82
+ return {
83
+ x: x ? ctx.xScale(x) + (isScaleBand(ctx.xScale) ? ctx.xScale.bandwidth() / 2 : 0) : 0,
84
+ y: y ? ctx.yScale(y) + (isScaleBand(ctx.yScale) ? ctx.yScale.bandwidth() / 2 : 0) : ctx.height,
85
+ };
68
86
  });
69
87
 
70
- const labelProps = $derived<ComponentProps<typeof Text>>({
71
- x:
72
- point.x +
73
- ((['top', 'center', 'bottom'].includes(labelPlacement) ? 0 : r) + labelXOffset) *
74
- (labelPlacement.includes('left') ? -1 : 1),
75
- y:
76
- point.y +
77
- ((['left', 'center', 'right'].includes(labelPlacement) ? 0 : r) + labelYOffset) *
78
- (labelPlacement.includes('top') ? -1 : 1),
79
- dy: -2, // adjust for smaler font size
80
- textAnchor: labelPlacement.includes('left')
81
- ? 'end'
88
+ const labelProps = $derived.by<ComponentProps<typeof Text>>(() => {
89
+ // Unit vector from the ring center toward the placement direction — diagonals
90
+ // land on the ring at 45°, not at the bounding-box corner.
91
+ const dirX = labelPlacement.includes('left') ? -1 : labelPlacement.includes('right') ? 1 : 0;
92
+ const dirY = labelPlacement.includes('top') ? -1 : labelPlacement.includes('bottom') ? 1 : 0;
93
+ const mag = Math.hypot(dirX, dirY) || 1;
94
+ const signX = labelPlacement.includes('left') ? -1 : 1;
95
+ const signY = labelPlacement.includes('top') ? -1 : 1;
96
+
97
+ return {
98
+ x: point.x + (r * dirX) / mag + labelXOffset * signX,
99
+ y: point.y + (r * dirY) / mag + labelYOffset * signY,
100
+ dy: -2, // adjust for smaller font size
101
+ textAnchor: labelPlacement.includes('left')
102
+ ? 'end'
103
+ : labelPlacement.includes('right')
104
+ ? 'start'
105
+ : 'middle',
106
+ verticalAnchor: labelPlacement.includes('top')
107
+ ? 'end'
108
+ : labelPlacement.includes('bottom')
109
+ ? 'start'
110
+ : 'middle',
111
+ };
112
+ });
113
+
114
+ // Endpoints for the optional leader `<Link>`. Source sits on the ring edge in
115
+ // the direction dictated by `labelPlacement` — offsets don't rotate it.
116
+ const linkEndpoints = $derived.by(() => {
117
+ if (!link) return null;
118
+
119
+ const dirX = labelPlacement.includes('left')
120
+ ? -1
82
121
  : labelPlacement.includes('right')
83
- ? 'start'
84
- : 'middle',
85
- verticalAnchor: labelPlacement.includes('top')
86
- ? 'end'
122
+ ? 1
123
+ : 0;
124
+ const dirY = labelPlacement.includes('top')
125
+ ? -1
87
126
  : labelPlacement.includes('bottom')
88
- ? 'start'
89
- : 'middle',
127
+ ? 1
128
+ : 0;
129
+ if (dirX === 0 && dirY === 0) return null; // labelPlacement='center' — no line
130
+
131
+ const mag = Math.hypot(dirX, dirY);
132
+ return {
133
+ source: { x: point.x + (r * dirX) / mag, y: point.y + (r * dirY) / mag },
134
+ target: { x: labelProps.x as number, y: labelProps.y as number },
135
+ };
90
136
  });
91
137
 
138
+ const linkProps = $derived(typeof link === 'object' ? link : {});
139
+
92
140
  function onPointerMove(e: PointerEvent | MouseEvent | TouchEvent) {
93
141
  if (details) {
94
142
  e.stopPropagation();
@@ -115,9 +163,24 @@
115
163
  onmouseleave={onPointerLeave}
116
164
  ontouchend={onPointerLeave}
117
165
  {...props?.circle}
118
- class={cls('lc-annotation-point', props?.circle?.class)}
166
+ class={cls('lc-annotation-point', link && 'lc-annotation-point-ring', props?.circle?.class)}
119
167
  />
120
168
 
169
+ {#if linkEndpoints}
170
+ <Link
171
+ x1={linkEndpoints.source.x}
172
+ y1={linkEndpoints.source.y}
173
+ x2={linkEndpoints.target.x}
174
+ y2={linkEndpoints.target.y}
175
+ type="straight"
176
+ {...linkProps}
177
+ class={cls(
178
+ 'lc-annotation-point-link',
179
+ typeof linkProps.class === 'string' ? linkProps.class : undefined
180
+ )}
181
+ />
182
+ {/if}
183
+
121
184
  {#if label}
122
185
  <Text
123
186
  value={label}
@@ -133,5 +196,16 @@
133
196
  font-size: 12px;
134
197
  pointer-events: none;
135
198
  }
199
+
200
+ :global(:where(.lc-annotation-point-ring)) {
201
+ --fill-color: none;
202
+ --stroke-color: var(--color-surface-content, currentColor);
203
+ }
204
+
205
+ :global(:where(.lc-annotation-point-link)) {
206
+ --stroke-color: var(--color-surface-content, currentColor);
207
+ fill: none;
208
+ pointer-events: none;
209
+ }
136
210
  }
137
211
  </style>
@@ -9,7 +9,7 @@ export type AnnotationPointPropsWithoutHTML = {
9
9
  y?: SingleDomainType;
10
10
  /** Radius of the circle */
11
11
  r?: number;
12
- /** Label to display on circle*/
12
+ /** Label to display on circle */
13
13
  label?: string;
14
14
  /** Placement of the label */
15
15
  labelPlacement?: Placement;
@@ -17,6 +17,12 @@ export type AnnotationPointPropsWithoutHTML = {
17
17
  labelXOffset?: number;
18
18
  /** Y offset of the label */
19
19
  labelYOffset?: number;
20
+ /**
21
+ * Draw a `<Link>` from the ring edge to the label (d3-ring-note style).
22
+ * Pass `true` for a straight line, or an object to configure the `Link`
23
+ * (e.g. `{ type: 'beveled', radius: 20 }`).
24
+ */
25
+ link?: boolean | Partial<ComponentProps<typeof Link>>;
20
26
  /** Details (description, etc) useful to display in tooltip */
21
27
  details?: any;
22
28
  /** Classes for inner elements */
@@ -27,6 +33,7 @@ export type AnnotationPointPropsWithoutHTML = {
27
33
  } & CommonStyleProps;
28
34
  export type AnnotationPointProps = AnnotationPointPropsWithoutHTML & Without<SVGAttributes<Element>, AnnotationPointPropsWithoutHTML>;
29
35
  import Circle from './Circle.svelte';
36
+ import Link from './Link.svelte';
30
37
  import Text from './Text.svelte';
31
38
  import type { Placement } from './types.js';
32
39
  declare const AnnotationPoint: import("svelte").Component<AnnotationPointProps, {}, "">;
@@ -151,10 +151,10 @@
151
151
  <Path
152
152
  {pathData}
153
153
  {...restProps}
154
- onclick={onclick ? _onClick : undefined}
155
- onpointerenter={tooltip || onpointerenter ? _onPointerEnter : undefined}
156
- onpointermove={tooltip || onpointermove ? _onPointerMove : undefined}
157
- onpointerleave={tooltip || onpointerleave ? _onPointerLeave : undefined}
154
+ {...onclick && { onclick: _onClick }}
155
+ {...(tooltip || onpointerenter) && { onpointerenter: _onPointerEnter }}
156
+ {...(tooltip || onpointermove) && { onpointermove: _onPointerMove }}
157
+ {...(tooltip || onpointerleave) && { onpointerleave: _onPointerLeave }}
158
158
  class={cls('lc-geo-path', className)}
159
159
  pathRef={refProp}
160
160
  />
@@ -498,6 +498,7 @@
498
498
  <div class={cls('lc-legend-swatch-group', classes.items)} data-orientation={orientation}>
499
499
  {#each swatchItems as item}
500
500
  <button
501
+ type="button"
501
502
  class={cls('lc-legend-swatch-button', resolveMaybeFn(classes?.item, item))}
502
503
  style:opacity={selected.length === 0 || selected.includes(item.value) ? 1 : 0.3}
503
504
  onclick={(e) => onclickProp?.(e, item) ?? item.onclick?.(e)}