layerchart 2.0.0-next.62 → 2.0.0-next.64

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 (54) hide show
  1. package/dist/canvas.d.ts +4 -0
  2. package/dist/canvas.js +4 -0
  3. package/dist/components/Arc/Arc.shared.svelte.d.ts +2 -0
  4. package/dist/components/ArcLabel/ArcLabel.shared.svelte.d.ts +1 -0
  5. package/dist/components/Circle/Circle.shared.svelte.js +24 -5
  6. package/dist/components/Circle/Circle.svelte.test.js +70 -0
  7. package/dist/components/Dodge/Dodge.shared.svelte.d.ts +132 -0
  8. package/dist/components/Dodge/Dodge.shared.svelte.js +240 -0
  9. package/dist/components/Dodge/Dodge.svelte +88 -0
  10. package/dist/components/Dodge/Dodge.svelte.d.ts +27 -0
  11. package/dist/components/Dodge/Dodge.test.d.ts +1 -0
  12. package/dist/components/Dodge/Dodge.test.js +128 -0
  13. package/dist/components/Image/Image.html.svelte +0 -8
  14. package/dist/components/Image/Image.svg.svelte +1 -9
  15. package/dist/components/Pattern/Pattern.canvas.svelte +4 -1
  16. package/dist/components/Pattern/Pattern.shared.svelte.d.ts +31 -2
  17. package/dist/components/Pattern/Pattern.shared.svelte.js +20 -1
  18. package/dist/components/Pattern/Pattern.svg.svelte +17 -1
  19. package/dist/components/Raster/Raster.base.svelte +2 -8
  20. package/dist/components/Rect/Rect.canvas.svelte +2 -4
  21. package/dist/components/Rect/Rect.canvas.svelte.d.ts +1 -1
  22. package/dist/components/Rect/Rect.html.svelte +3 -9
  23. package/dist/components/Rect/Rect.html.svelte.d.ts +1 -1
  24. package/dist/components/Rect/Rect.shared.svelte.d.ts +5 -2
  25. package/dist/components/Rect/Rect.shared.svelte.js +26 -13
  26. package/dist/components/Rect/Rect.svelte.test.js +45 -0
  27. package/dist/components/Rect/Rect.svg.svelte +36 -21
  28. package/dist/components/Rect/Rect.svg.svelte.d.ts +1 -1
  29. package/dist/components/Spline/Spline.base.svelte +3 -2
  30. package/dist/components/Text/Text.canvas.svelte +9 -0
  31. package/dist/components/Text/Text.html.svelte +6 -0
  32. package/dist/components/Text/Text.shared.svelte.d.ts +25 -2
  33. package/dist/components/Text/Text.shared.svelte.js +36 -5
  34. package/dist/components/Text/Text.svelte.test.js +40 -0
  35. package/dist/components/Text/Text.svg.svelte +7 -1
  36. package/dist/components/Waffle/Waffle.shared.svelte.d.ts +182 -0
  37. package/dist/components/Waffle/Waffle.shared.svelte.js +300 -0
  38. package/dist/components/Waffle/Waffle.svelte +148 -0
  39. package/dist/components/Waffle/Waffle.svelte.d.ts +5 -0
  40. package/dist/components/charts/ArcChart/ArcChart.base.svelte +1 -0
  41. package/dist/components/index.d.ts +4 -0
  42. package/dist/components/index.js +4 -0
  43. package/dist/html.d.ts +4 -0
  44. package/dist/html.js +4 -0
  45. package/dist/states/chart.svelte.js +8 -4
  46. package/dist/states/chart.svelte.test.js +53 -0
  47. package/dist/svg.d.ts +4 -0
  48. package/dist/svg.js +4 -0
  49. package/dist/utils/canvas.js +54 -13
  50. package/dist/utils/canvas.svelte.test.js +44 -0
  51. package/dist/utils/download.d.ts +5 -3
  52. package/dist/utils/download.js +36 -16
  53. package/dist/utils/stack.js +10 -2
  54. package/package.json +1 -1
@@ -50,6 +50,14 @@
50
50
  strokeWidth: itemStrokeWidth ?? c.staticStrokeWidth,
51
51
  opacity: itemOpacity ?? c.staticOpacity,
52
52
  paintOrder: 'stroke',
53
+ ...(rest.fontSize != null
54
+ ? {
55
+ fontSize:
56
+ typeof rest.fontSize === 'number'
57
+ ? `${rest.fontSize}px`
58
+ : rest.fontSize,
59
+ }
60
+ : {}),
53
61
  ...((rest.textAnchor ?? 'start') !== 'start'
54
62
  ? { textAnchor: rest.textAnchor }
55
63
  : {}),
@@ -195,6 +203,7 @@
195
203
  rest.class,
196
204
  c.truncateConfig,
197
205
  rest.rotate,
206
+ rest.fontSize,
198
207
  rest.lineHeight,
199
208
  rest.textAnchor,
200
209
  rest.verticalAnchor,
@@ -46,6 +46,9 @@
46
46
  {textAnchor === 'middle' ? 'center' : textAnchor === 'end' ? 'right' : 'left'}"
47
47
  style:white-space="pre-wrap"
48
48
  style:line-height={rest.lineHeight ?? '1em'}
49
+ style:font-size={typeof rest.fontSize === 'number'
50
+ ? `${rest.fontSize}px`
51
+ : rest.fontSize}
49
52
  style:color={resolvedFill}
50
53
  style:opacity={resolvedOpacity ?? resolvedFillOpacity}
51
54
  class={['lc-text', resolvedClass]}
@@ -74,6 +77,9 @@
74
77
  {textAnchor === 'middle' ? 'center' : textAnchor === 'end' ? 'right' : 'left'}"
75
78
  style:white-space="pre-wrap"
76
79
  style:line-height={rest.lineHeight ?? '1em'}
80
+ style:font-size={typeof rest.fontSize === 'number'
81
+ ? `${rest.fontSize}px`
82
+ : rest.fontSize}
77
83
  style:color={c.staticFill}
78
84
  style:opacity={c.staticOpacity ?? c.staticFillOpacity}
79
85
  class={['lc-text', c.staticClassName]}
@@ -88,8 +88,20 @@ export type TextPropsWithoutHTML = {
88
88
  */
89
89
  lineHeight?: string;
90
90
  /**
91
- * Cap height of the text
92
- * @default '0.71em'
91
+ * Font size of the text. A number is treated as pixels; a string passes
92
+ * through (e.g. `'12px'`, `'1.25em'`). When set, vertical centering math
93
+ * derives `capHeight` from this value automatically (as `fontSize * 0.71`),
94
+ * so per-item scaled labels with `verticalAnchor="middle"` align correctly
95
+ * without an explicit `capHeight` override.
96
+ */
97
+ fontSize?: number | string;
98
+ /**
99
+ * Cap height of the text — used by vertical anchor math to align the text
100
+ * to its visual center (as opposed to the font box). Defaults to `0.71em`,
101
+ * but if `fontSize` is set, defaults to `fontSize * 0.71` so centering
102
+ * stays correct as text scales.
103
+ *
104
+ * @default '0.71em' (or `fontSize * 0.71` when `fontSize` is set)
93
105
  */
94
106
  capHeight?: string;
95
107
  /**
@@ -182,6 +194,17 @@ export declare function getPathLength(pathRef: SVGPathElement | undefined): numb
182
194
  * Convert css value to pixel value (ex. 0.71em => 11.36)
183
195
  */
184
196
  export declare function getPixelValue(cssValue: number | string): number;
197
+ /**
198
+ * Resolve the cap-height used by vertical-anchor math.
199
+ *
200
+ * Priority:
201
+ * 1. Explicit `capHeight` prop
202
+ * 2. `fontSize * 0.71` when `fontSize` is set (keeps centering correct as
203
+ * labels scale per-item)
204
+ * 3. `'0.71em'` (legacy default — only correct for ~16px text since
205
+ * `getPixelValue` resolves `em` against 16, not the actual font-size)
206
+ */
207
+ export declare function resolveCapHeight(capHeight: string | undefined, fontSize: number | string | undefined): number | string;
185
208
  export declare function isValidXOrY(xOrY: string | number | undefined): boolean;
186
209
  /** Build the standard `markInfo` payload used by every Text variant. */
187
210
  export declare function textMarkInfo(props: TextProps, dataMode: boolean): {
@@ -58,6 +58,23 @@ export function getPixelValue(cssValue) {
58
58
  return 0;
59
59
  }
60
60
  }
61
+ /**
62
+ * Resolve the cap-height used by vertical-anchor math.
63
+ *
64
+ * Priority:
65
+ * 1. Explicit `capHeight` prop
66
+ * 2. `fontSize * 0.71` when `fontSize` is set (keeps centering correct as
67
+ * labels scale per-item)
68
+ * 3. `'0.71em'` (legacy default — only correct for ~16px text since
69
+ * `getPixelValue` resolves `em` against 16, not the actual font-size)
70
+ */
71
+ export function resolveCapHeight(capHeight, fontSize) {
72
+ if (capHeight != null)
73
+ return capHeight;
74
+ if (fontSize != null)
75
+ return getPixelValue(fontSize) * 0.71;
76
+ return '0.71em';
77
+ }
61
78
  export function isValidXOrY(xOrY) {
62
79
  return ((typeof xOrY === 'number' && Number.isFinite(xOrY)) ||
63
80
  typeof xOrY === 'string');
@@ -89,7 +106,9 @@ export class TextState {
89
106
  // Path measurement (only meaningful for SVG layer where the textPath element exists)
90
107
  pathRef = $state();
91
108
  // Data mode detection
92
- dataMode = $derived(isTextDataProp(this.#getProps().x) || isTextDataProp(this.#getProps().y));
109
+ dataMode = $derived(this.#getProps().data != null ||
110
+ isTextDataProp(this.#getProps().x) ||
111
+ isTextDataProp(this.#getProps().y));
93
112
  // Data resolution
94
113
  #resolvedData = $derived(this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : []);
95
114
  resolvedItems = $derived.by(() => {
@@ -115,9 +134,21 @@ export class TextState {
115
134
  const [projX, projY] = resolveGeoDataPair(props.x, props.y, d, this.geo.projection);
116
135
  return { x: projX, y: projY };
117
136
  }
137
+ // When x/y are omitted, fall back to the chart's accessors (xGet/yGet) —
138
+ // mirroring the Circle / Points / Dodge pattern.
139
+ const xDefault = typeof props.x === 'number'
140
+ ? props.x
141
+ : props.x == null && this.chartCtx.config.x != null
142
+ ? Number(this.chartCtx.xGet(d)) || 0
143
+ : 0;
144
+ const yDefault = typeof props.y === 'number'
145
+ ? props.y
146
+ : props.y == null && this.chartCtx.config.y != null
147
+ ? Number(this.chartCtx.yGet(d)) || 0
148
+ : 0;
118
149
  return {
119
- x: resolveDataProp(props.x, d, this.chartCtx.xScale, 0),
120
- y: resolveDataProp(props.y, d, this.chartCtx.yScale, 0),
150
+ x: resolveDataProp(props.x, d, this.chartCtx.xScale, xDefault),
151
+ y: resolveDataProp(props.y, d, this.chartCtx.yScale, yDefault),
121
152
  };
122
153
  }
123
154
  resolveTextValue(d) {
@@ -220,7 +251,7 @@ export class TextState {
220
251
  const props = this.#getProps();
221
252
  const verticalAnchor = props.verticalAnchor ?? 'end';
222
253
  const lineHeight = props.lineHeight ?? '1em';
223
- const capHeight = props.capHeight ?? '0.71em';
254
+ const capHeight = resolveCapHeight(props.capHeight, props.fontSize);
224
255
  if (verticalAnchor === 'start') {
225
256
  return getPixelValue(lineHeight);
226
257
  }
@@ -233,7 +264,7 @@ export class TextState {
233
264
  const props = this.#getProps();
234
265
  const verticalAnchor = props.verticalAnchor ?? 'end';
235
266
  const lineHeight = props.lineHeight ?? '1em';
236
- const capHeight = props.capHeight ?? '0.71em';
267
+ const capHeight = resolveCapHeight(props.capHeight, props.fontSize);
237
268
  if (verticalAnchor === 'start')
238
269
  return getPixelValue(lineHeight);
239
270
  if (verticalAnchor === 'middle')
@@ -148,5 +148,45 @@ describe('Text', () => {
148
148
  const texts = page.getByTestId(componentTestId).elements();
149
149
  await expect.poll(() => texts.length).toBe(1);
150
150
  });
151
+ it('should enter data mode when only `data` prop is set, using chart accessors', async () => {
152
+ render(TestHarness, {
153
+ component: Text,
154
+ chartProps: {
155
+ data,
156
+ x: 'date',
157
+ y: 'value',
158
+ yDomain: [0, 100],
159
+ },
160
+ componentProps: {
161
+ data,
162
+ value: 'label',
163
+ },
164
+ });
165
+ const texts = page.getByTestId(componentTestId).elements();
166
+ await expect.poll(() => texts.length).toBe(3);
167
+ const ys = texts.map((t) => Number(t.getAttribute('y')));
168
+ expect(ys[0]).toBeGreaterThan(ys[1]);
169
+ expect(ys[1]).toBeGreaterThan(ys[2]);
170
+ });
171
+ it('should fall back to chart x accessor when only y is omitted', async () => {
172
+ render(TestHarness, {
173
+ component: Text,
174
+ chartProps: {
175
+ data,
176
+ x: 'date',
177
+ y: 'value',
178
+ yDomain: [0, 100],
179
+ },
180
+ componentProps: {
181
+ data,
182
+ x: 'date',
183
+ value: 'label',
184
+ },
185
+ });
186
+ const texts = page.getByTestId(componentTestId).elements();
187
+ await expect.poll(() => texts.length).toBe(3);
188
+ const ys = texts.map((t) => Number(t.getAttribute('y')));
189
+ expect(ys[0]).toBeGreaterThan(ys[2]);
190
+ });
151
191
  });
152
192
  });
@@ -30,10 +30,13 @@
30
30
  rotate,
31
31
  dx,
32
32
  dy,
33
+ // `fontSize` is a typed prop (drives `capHeight` defaults), but on the
34
+ // DOM it must be rendered as the kebab-case `font-size` attribute.
35
+ fontSize,
33
36
  ...rest
34
37
  }: TextProps = $props();
35
38
 
36
- const c = new TextState(() => ({ rotate, dx, dy, ...rest } as TextProps));
39
+ const c = new TextState(() => ({ rotate, dx, dy, fontSize, ...rest } as TextProps));
37
40
 
38
41
  let ref = $state<SVGTextElement>();
39
42
  let svgRef = $state<SVGElement>();
@@ -78,6 +81,7 @@
78
81
  transform={(rest.transform as string | undefined) ?? dataRotateTransform}
79
82
  text-anchor={rest.textAnchor ?? 'start'}
80
83
  dominant-baseline={rest.dominantBaseline ?? 'auto'}
84
+ font-size={fontSize}
81
85
  fill={resolvedFill}
82
86
  fill-opacity={resolvedFillOpacity}
83
87
  stroke={resolvedStroke}
@@ -109,6 +113,7 @@
109
113
  {...rest as any}
110
114
  bind:this={ref}
111
115
  dy={dy ?? 0}
116
+ font-size={fontSize}
112
117
  fill={c.staticFill}
113
118
  fill-opacity={c.staticFillOpacity}
114
119
  stroke={c.staticStroke}
@@ -140,6 +145,7 @@
140
145
  transform={c.transform}
141
146
  text-anchor={rest.textAnchor ?? 'start'}
142
147
  dominant-baseline={rest.dominantBaseline ?? 'auto'}
148
+ font-size={fontSize}
143
149
  fill={c.staticFill}
144
150
  fill-opacity={c.staticFillOpacity}
145
151
  stroke={c.staticStroke}
@@ -0,0 +1,182 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { SVGAttributes } from 'svelte/elements';
3
+ import type { ChartState } from '../../states/chart.svelte.js';
4
+ import { type Accessor } from '../../utils/common.js';
5
+ import { type Insets } from '../../utils/rect.svelte.js';
6
+ import type { CommonStyleProps, Without } from '../../utils/types.js';
7
+ export type WaffleRound = boolean | ((n: number) => number);
8
+ export type WafflePropsWithoutHTML = {
9
+ /** Override chart context data. */
10
+ data?: any[];
11
+ /** Override `x` from context. @default ctx.x */
12
+ x?: Accessor;
13
+ /** Override `y` from context. @default ctx.y */
14
+ y?: Accessor;
15
+ /** Override `x1` from context. @default ctx.x1 */
16
+ x1?: Accessor;
17
+ /** Override `y1` from context. @default ctx.y1 */
18
+ y1?: Accessor;
19
+ /**
20
+ * Axis the waffle extends along (the value axis).
21
+ *
22
+ * - `'y'` (default): vertical waffle, like Plot's `waffleY`. Cells stack
23
+ * upward from the value=0 baseline.
24
+ * - `'x'`: horizontal waffle, like Plot's `waffleX`. Cells extend rightward.
25
+ *
26
+ * Falls back to the chart's `valueAxis`.
27
+ */
28
+ axis?: 'x' | 'y';
29
+ /**
30
+ * The quantity each cell represents. Larger units produce fewer cells.
31
+ * @default 1
32
+ */
33
+ unit?: number;
34
+ /**
35
+ * The number of cells per row (along the anchor axis). When omitted,
36
+ * computed automatically from the bar width and unit so cells stay
37
+ * approximately square.
38
+ */
39
+ multiple?: number;
40
+ /**
41
+ * Pixel separation between adjacent cells.
42
+ * @default 1
43
+ */
44
+ gap?: number;
45
+ /**
46
+ * How to handle non-integer cell counts.
47
+ *
48
+ * - `false` (default) — keep the partial cell as a fractional cut-off
49
+ * - `true` — `Math.round`
50
+ * - function — custom rounding (e.g. `Math.floor`, `Math.ceil`)
51
+ */
52
+ round?: WaffleRound;
53
+ /** Cell horizontal corner radius (number of pixels, or "100%" for circles). */
54
+ rx?: number | string;
55
+ /** Cell vertical corner radius (number of pixels, or "100%" for circles). */
56
+ ry?: number | string;
57
+ /** Series key for stacked-waffle support. */
58
+ seriesKey?: string;
59
+ /** Insets to shrink each waffle's bounding band. */
60
+ insets?: Insets;
61
+ /** Fixed band-axis size in pixels. Override the band width / height. */
62
+ width?: number;
63
+ /** Fixed band-axis size in pixels. Override the band width / height. */
64
+ height?: number;
65
+ /** Default `(d, i) => i` */
66
+ key?: (d: any, index: number) => any;
67
+ /** Setup pointer events to show tooltip for the hovered datum. */
68
+ tooltip?: boolean;
69
+ /** Click handler invoked with `(event, { data })` for the hovered waffle. */
70
+ onWaffleClick?: (e: MouseEvent, detail: {
71
+ data: any;
72
+ }) => void;
73
+ /**
74
+ * Render a custom symbol per cell instead of the default rect. The snippet
75
+ * receives the cell's `width`/`height` and the resolved `color` for the
76
+ * cell. The CSS `color` is applied to the wrapping element, so any nested
77
+ * SVG using `fill="currentColor"` (or stroke) inherits it automatically.
78
+ *
79
+ * SVG layer only — canvas falls back to the default rect.
80
+ */
81
+ symbol?: Snippet<[
82
+ {
83
+ width: number;
84
+ height: number;
85
+ datum: any;
86
+ color: string;
87
+ }
88
+ ]>;
89
+ } & CommonStyleProps;
90
+ export type WaffleProps = WafflePropsWithoutHTML & Without<Omit<SVGAttributes<SVGElement>, 'width' | 'height' | 'x' | 'y'>, WafflePropsWithoutHTML>;
91
+ /** Per-datum, fully-resolved waffle layout — pixel coords ready to render. */
92
+ export type WaffleItem = {
93
+ data: any;
94
+ index: number;
95
+ /** SVG path data for the waffle outline (relative to translate). */
96
+ pathData: string;
97
+ /** Pixel translate origin — apply to the path/pattern as `translate(tx, ty)`. */
98
+ tx: number;
99
+ ty: number;
100
+ /** Cell box width in pixels (pattern tile width). */
101
+ cx: number;
102
+ /** Cell box height in pixels (pattern tile height). */
103
+ cy: number;
104
+ /** Cell centroid in pixel coords (translated). */
105
+ centroid: {
106
+ x: number;
107
+ y: number;
108
+ };
109
+ /** Resolved fill color. */
110
+ fill: string | null;
111
+ };
112
+ export type WaffleLayoutOptions = {
113
+ axis: 'x' | 'y';
114
+ unit: number;
115
+ multiple?: number;
116
+ round: (n: number) => number;
117
+ /** Anchor-axis size in pixels (the bar's other-axis extent). */
118
+ barSize: number;
119
+ /** Anchor-axis pixel position (the bar's other-axis start). */
120
+ barStart: number;
121
+ /** Pixels per data unit on the value axis (signed; negative = inverted). */
122
+ pixelsPerUnit: number;
123
+ /** Pixel position of the value=0 baseline along the value axis. */
124
+ valueZero: number;
125
+ /** Value range in data units. */
126
+ v1: number;
127
+ v2: number;
128
+ };
129
+ /**
130
+ * Shared reactive state for Waffle. Resolves accessors, computes per-datum
131
+ * dimensions and waffle layouts (pattern tile size, polygon path, centroid),
132
+ * and exposes them via `items`.
133
+ */
134
+ export declare class WaffleState {
135
+ #private;
136
+ ctx: ChartState;
137
+ constructor(getProps: () => WaffleProps);
138
+ axis: "x" | "y";
139
+ unit: number;
140
+ gap: number;
141
+ round: (n: number) => number;
142
+ multipleProp: number | undefined;
143
+ series: import("../index.js").SeriesData<any, any> | undefined;
144
+ /** Opacity multiplier — fades to 0.1 when another series is highlighted. */
145
+ seriesOpacity: number;
146
+ seriesAccessor: string | number | ((d: any) => any) | Accessor<any>[] | undefined;
147
+ stackAccessors: {
148
+ y0: (d: any) => number;
149
+ y1: (d: any) => number;
150
+ value: (d: any) => [number, number] | null;
151
+ } | null;
152
+ data: any[];
153
+ x: Accessor;
154
+ y: Accessor;
155
+ getDimensions: (item: any) => {
156
+ x: any;
157
+ y: any;
158
+ width: number;
159
+ height: number;
160
+ } | undefined;
161
+ /** Pixel slope of the value-axis scale (signed). */
162
+ pixelsPerUnit: number;
163
+ /** Pixel position of value=0 along the value axis. */
164
+ valueZero: number;
165
+ items: WaffleItem[];
166
+ /** Resolved gap. */
167
+ resolvedGap: number;
168
+ }
169
+ /**
170
+ * Generate the polygon outline of a waffle covering the cell interval
171
+ * `[i1, i2)` on a grid of `columns` columns. The shape is approximately
172
+ * rectangular but may have one or two corner cuts when the start or end
173
+ * value is not aligned to a row boundary, plus extra cuts for fractional
174
+ * intervals. The last point is the centroid (popped by callers for tooltips
175
+ * and tip placement).
176
+ *
177
+ * Coordinate space is `(column, row)` in cell units — callers transform to
178
+ * pixel space (and may negate the row axis for screen y).
179
+ *
180
+ * @see https://github.com/observablehq/plot/blob/main/src/marks/waffle.js
181
+ */
182
+ export declare function wafflePoints(i1: number, i2: number, columns: number): [number, number][];