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
@@ -0,0 +1,128 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { dodge } from './Dodge.shared.svelte.js';
3
+ /** Build inputs for the circular case (`rx === ry === r`). */
4
+ function input(arr) {
5
+ return arr.map((d, index) => ({
6
+ x: d.x,
7
+ rx: d.r,
8
+ ry: d.r,
9
+ data: { id: d.id },
10
+ index,
11
+ }));
12
+ }
13
+ /** Build inputs for rectangular cases (per-axis half-extents). */
14
+ function rectInput(arr) {
15
+ return arr.map((d, index) => ({
16
+ x: d.x,
17
+ rx: d.rx,
18
+ ry: d.ry,
19
+ data: { id: d.id },
20
+ index,
21
+ }));
22
+ }
23
+ describe('dodge() — circular packing', () => {
24
+ it('places a single item at the natural anchor position', () => {
25
+ const out = dodge(input([{ id: 'a', x: 50, r: 10 }]), {
26
+ axis: 'y',
27
+ anchor: 'bottom',
28
+ padding: 0,
29
+ baseline: 100,
30
+ });
31
+ expect(out).toHaveLength(1);
32
+ expect(out[0].x).toBe(50);
33
+ expect(out[0].y).toBe(90); // baseline - r
34
+ });
35
+ it('places non-overlapping items both at the anchor', () => {
36
+ // Two items 100px apart, r=10 each — no horizontal overlap, both can sit at the bottom
37
+ const out = dodge(input([
38
+ { id: 'a', x: 0, r: 10 },
39
+ { id: 'b', x: 100, r: 10 },
40
+ ]), { axis: 'y', anchor: 'bottom', padding: 0, baseline: 100 });
41
+ expect(out[0].y).toBe(90);
42
+ expect(out[1].y).toBe(90);
43
+ });
44
+ it('stacks overlapping items vertically', () => {
45
+ // Two items at the same x — must stack with a gap ≥ r1 + r2 + padding = 21.
46
+ // padding is also applied to the chart edge, so the first item sits 1px
47
+ // above the bottom (y=89, not 90).
48
+ const out = dodge(input([
49
+ { id: 'a', x: 50, r: 10 },
50
+ { id: 'b', x: 50, r: 10 },
51
+ ]), { axis: 'y', anchor: 'bottom', padding: 1, baseline: 100 });
52
+ expect(out[0].y).toBe(89);
53
+ expect(out[1].y).toBeCloseTo(89 - 21, 5); // gap of sumR+padding
54
+ });
55
+ it('returns items in original input order', () => {
56
+ const out = dodge(input([
57
+ { id: 'small', x: 50, r: 5 },
58
+ { id: 'large', x: 50, r: 20 },
59
+ ]), { axis: 'y', anchor: 'bottom', padding: 0, baseline: 100 });
60
+ expect(out[0].data.id).toBe('small');
61
+ expect(out[1].data.id).toBe('large');
62
+ // Algorithm processes input order: small placed first at bottom (y=95),
63
+ // then large stacks above.
64
+ expect(out[0].y).toBe(95);
65
+ expect(out[1].y).toBeLessThan(95);
66
+ });
67
+ it('respects anchor=top (stacks downward)', () => {
68
+ const out = dodge(input([
69
+ { id: 'a', x: 50, r: 10 },
70
+ { id: 'b', x: 50, r: 10 },
71
+ ]), { axis: 'y', anchor: 'top', padding: 0, baseline: 0 });
72
+ expect(out[0].y).toBe(10); // first at top
73
+ expect(out[1].y).toBeCloseTo(30, 5); // second below, gap = r1+r2 = 20
74
+ });
75
+ it('swaps axes for axis=x', () => {
76
+ const out = dodge(input([{ id: 'a', x: 30, r: 5 }]), {
77
+ axis: 'x',
78
+ anchor: 'left',
79
+ padding: 0,
80
+ baseline: 0,
81
+ });
82
+ expect(out[0].x).toBe(5); // dodged: at left edge
83
+ expect(out[0].y).toBe(30); // anchor y preserved
84
+ });
85
+ });
86
+ describe('dodge() — rectangular packing (rx + ry)', () => {
87
+ // ry = 8 ⇒ row spacing = 2 * ry = 16 (matches the old `rowHeight: 16` cases).
88
+ it('places non-overlapping items in row 0', () => {
89
+ const out = dodge(rectInput([
90
+ { id: 'a', x: 0, rx: 20, ry: 8 }, // spans -20 to 20
91
+ { id: 'b', x: 100, rx: 20, ry: 8 }, // spans 80 to 120
92
+ ]), { axis: 'y', anchor: 'bottom', padding: 0, baseline: 200, rectangular: true });
93
+ // Row 0 center from bottom = baseline - ry = 192
94
+ expect(out[0].y).toBe(192);
95
+ expect(out[1].y).toBe(192);
96
+ });
97
+ it('stacks horizontally-overlapping items into separate rows', () => {
98
+ const out = dodge(rectInput([
99
+ { id: 'a', x: 50, rx: 30, ry: 8 },
100
+ { id: 'b', x: 60, rx: 30, ry: 8 },
101
+ { id: 'c', x: 70, rx: 30, ry: 8 },
102
+ ]), { axis: 'y', anchor: 'bottom', padding: 0, baseline: 200, rectangular: true });
103
+ // Input order: a → row 0, b overlaps a → row 1, c overlaps both → row 2
104
+ // Row centers from bottom: 192, 176, 160.
105
+ expect(out[0].y).toBe(192);
106
+ expect(out[1].y).toBe(176);
107
+ expect(out[2].y).toBe(160);
108
+ });
109
+ it("reuses row 0 when later items don't overlap earlier ones in that row", () => {
110
+ const out = dodge(rectInput([
111
+ { id: 'a', x: 0, rx: 10, ry: 8 },
112
+ { id: 'b', x: 5, rx: 10, ry: 8 }, // overlaps a → row 1
113
+ { id: 'c', x: 100, rx: 10, ry: 8 }, // far from both → row 0
114
+ ]), { axis: 'y', anchor: 'bottom', padding: 0, baseline: 200, rectangular: true });
115
+ expect(out[0].y).toBe(192);
116
+ expect(out[1].y).toBe(176);
117
+ expect(out[2].y).toBe(192);
118
+ });
119
+ it('respects anchor=top for rectangular mode', () => {
120
+ const out = dodge(rectInput([
121
+ { id: 'a', x: 50, rx: 30, ry: 8 },
122
+ { id: 'b', x: 60, rx: 30, ry: 8 },
123
+ ]), { axis: 'y', anchor: 'top', padding: 0, baseline: 0, rectangular: true });
124
+ // Row 0 from top: 0 + ry = 8. Row 1: 24.
125
+ expect(out[0].y).toBe(8);
126
+ expect(out[1].y).toBe(24);
127
+ });
128
+ });
@@ -104,11 +104,3 @@
104
104
  {...rest as any}
105
105
  />
106
106
  {/if}
107
-
108
- <style>
109
- @layer base {
110
- :global(:where(.lc-image)) {
111
- pointer-events: none;
112
- }
113
- }
114
- </style>
@@ -127,12 +127,4 @@
127
127
  {opacity}
128
128
  class={cls('lc-image', className)}
129
129
  />
130
- {/if}
131
-
132
- <style>
133
- @layer base {
134
- :global(:where(.lc-image)) {
135
- pointer-events: none;
136
- }
137
- }
138
- </style>
130
+ {/if}
@@ -22,11 +22,14 @@
22
22
  height = size,
23
23
  lines: linesProp,
24
24
  circles: circlesProp,
25
+ rects: rectsProp,
25
26
  background,
26
27
  children,
27
28
  }: PatternProps = $props();
28
29
 
29
- const shapes = $derived(buildPatternShapes(linesProp, circlesProp, size, width, height));
30
+ const shapes = $derived(
31
+ buildPatternShapes(linesProp, circlesProp, size, width, height, rectsProp)
32
+ );
30
33
 
31
34
  let canvasPattern = $state<CanvasPattern | null>(null);
32
35
 
@@ -21,6 +21,22 @@ export type PatternCircleDef = {
21
21
  /** The opacity of the circle @default 1 */
22
22
  opacity?: number;
23
23
  };
24
+ export type PatternRectDef = {
25
+ /**
26
+ * Inset from each edge of the pattern tile, in pixels. Useful for cell
27
+ * grids — set to half the desired gap between cells.
28
+ * @default 0
29
+ */
30
+ inset?: number;
31
+ /** Horizontal corner radius, or `"100%"` for a full ellipse / circle. */
32
+ rx?: number | string;
33
+ /** Vertical corner radius. Defaults to `rx` if not provided. */
34
+ ry?: number | string;
35
+ /** Fill color @default 'var(--color-surface-content)' */
36
+ color?: string;
37
+ /** Opacity @default 1 */
38
+ opacity?: number;
39
+ };
24
40
  export type PatternPropsWithoutHTML = {
25
41
  /** The id of the pattern */
26
42
  id?: string;
@@ -34,6 +50,8 @@ export type PatternPropsWithoutHTML = {
34
50
  lines?: boolean | PatternLineDef | PatternLineDef[];
35
51
  /** The number of circles to render */
36
52
  circles?: boolean | PatternCircleDef | PatternCircleDef[];
53
+ /** Rect(s) to render in each pattern tile */
54
+ rects?: boolean | PatternRectDef | PatternRectDef[];
37
55
  /** The background color of the pattern */
38
56
  background?: string;
39
57
  /** Render as a child of the pattern. Note: only supported on the `<Svg>` layer. */
@@ -59,9 +77,20 @@ export type LineShape = {
59
77
  strokeWidth: string | number;
60
78
  opacity: number;
61
79
  };
62
- export type PatternShape = CircleShape | LineShape;
80
+ export type RectShape = {
81
+ type: 'rect';
82
+ x: number;
83
+ y: number;
84
+ width: number;
85
+ height: number;
86
+ rx?: number | string;
87
+ ry?: number | string;
88
+ fill: string;
89
+ opacity: number;
90
+ };
91
+ export type PatternShape = CircleShape | LineShape | RectShape;
63
92
  /**
64
93
  * Build the SVG/canvas shape descriptors for a pattern's lines/circles.
65
94
  * Pure function — no reactivity.
66
95
  */
67
- export declare function buildPatternShapes(linesProp: PatternPropsWithoutHTML['lines'], circlesProp: PatternPropsWithoutHTML['circles'], size: number, width: number, height: number): PatternShape[];
96
+ export declare function buildPatternShapes(linesProp: PatternPropsWithoutHTML['lines'], circlesProp: PatternPropsWithoutHTML['circles'], size: number, width: number, height: number, rectsProp?: PatternPropsWithoutHTML['rects']): PatternShape[];
@@ -2,7 +2,7 @@
2
2
  * Build the SVG/canvas shape descriptors for a pattern's lines/circles.
3
3
  * Pure function — no reactivity.
4
4
  */
5
- export function buildPatternShapes(linesProp, circlesProp, size, width, height) {
5
+ export function buildPatternShapes(linesProp, circlesProp, size, width, height, rectsProp) {
6
6
  const shapes = [];
7
7
  if (linesProp) {
8
8
  const lineDefs = Array.isArray(linesProp) ? linesProp : linesProp === true ? [{}] : [linesProp];
@@ -67,5 +67,24 @@ export function buildPatternShapes(linesProp, circlesProp, size, width, height)
67
67
  }
68
68
  }
69
69
  }
70
+ if (rectsProp) {
71
+ const rectDefs = Array.isArray(rectsProp) ? rectsProp : rectsProp === true ? [{}] : [rectsProp];
72
+ for (const rect of rectDefs) {
73
+ const inset = rect.inset ?? 0;
74
+ const fill = rect.color ?? 'var(--color-surface-content, currentColor)';
75
+ const opacity = rect.opacity ?? 1;
76
+ shapes.push({
77
+ type: 'rect',
78
+ x: inset,
79
+ y: inset,
80
+ width: Math.max(0, width - 2 * inset),
81
+ height: Math.max(0, height - 2 * inset),
82
+ rx: rect.rx,
83
+ ry: rect.ry,
84
+ fill,
85
+ opacity,
86
+ });
87
+ }
88
+ }
70
89
  return shapes;
71
90
  }
@@ -19,13 +19,16 @@
19
19
  height = size,
20
20
  lines: linesProp,
21
21
  circles: circlesProp,
22
+ rects: rectsProp,
22
23
  background,
23
24
  patternContent,
24
25
  children,
25
26
  ...rest
26
27
  }: PatternProps = $props();
27
28
 
28
- const shapes = $derived(buildPatternShapes(linesProp, circlesProp, size, width, height));
29
+ const shapes = $derived(
30
+ buildPatternShapes(linesProp, circlesProp, size, width, height, rectsProp)
31
+ );
29
32
  </script>
30
33
 
31
34
  <defs>
@@ -62,6 +65,19 @@
62
65
  opacity={circle.opacity}
63
66
  />
64
67
  {/each}
68
+
69
+ {#each shapes.filter((s) => s.type === 'rect') as rect}
70
+ <rect
71
+ x={rect.x}
72
+ y={rect.y}
73
+ width={rect.width}
74
+ height={rect.height}
75
+ rx={rect.rx as number | string | undefined}
76
+ ry={rect.ry as number | string | undefined}
77
+ fill={rect.fill}
78
+ opacity={rect.opacity}
79
+ />
80
+ {/each}
65
81
  {/if}
66
82
  </pattern>
67
83
  </defs>
@@ -16,11 +16,7 @@
16
16
  import { max, min } from 'd3-array';
17
17
  import { rgb } from 'd3-color';
18
18
 
19
- import {
20
- accessor as resolveAccessor,
21
- chartDataArray,
22
- type Accessor,
23
- } from '../../utils/common.js';
19
+ import { accessor as resolveAccessor, chartDataArray, type Accessor } from '../../utils/common.js';
24
20
  import { getChartContext } from '../../contexts/chart.js';
25
21
  import { getGeoContext } from '../../contexts/geo.js';
26
22
  import { gridCellCenterToBounds, resolveRasterBounds } from '../../utils/index.js';
@@ -89,9 +85,7 @@
89
85
  if (!ctx.width || !ctx.height) return new Float64Array(0);
90
86
 
91
87
  if (isGridMode) {
92
- return dataProp instanceof Float64Array
93
- ? dataProp
94
- : Float64Array.from(dataProp as number[]);
88
+ return dataProp instanceof Float64Array ? dataProp : Float64Array.from(dataProp as number[]);
95
89
  }
96
90
 
97
91
  if (typeof valueProp === 'function' && valueProp.length >= 2) {
@@ -1,8 +1,5 @@
1
1
  <script lang="ts" module>
2
- export type {
3
- RectProps,
4
- RectPropsWithoutHTML,
5
- } from './Rect.shared.svelte.js';
2
+ export type { RectProps, RectPropsWithoutHTML } from './Rect.shared.svelte.js';
6
3
  </script>
7
4
 
8
5
  <script lang="ts">
@@ -90,6 +87,7 @@
90
87
  height: item.height,
91
88
  rx: c.rx,
92
89
  ry: c.ry,
90
+ corners: c.resolveCorners(item.width, item.height),
93
91
  },
94
92
  styleOpts
95
93
  );
@@ -1,4 +1,4 @@
1
- export type { RectProps, RectPropsWithoutHTML, } from './Rect.shared.svelte.js';
1
+ export type { RectProps, RectPropsWithoutHTML } from './Rect.shared.svelte.js';
2
2
  import { type RectProps } from './Rect.shared.svelte.js';
3
3
  declare const Rect: import("svelte").Component<RectProps, {}, "">;
4
4
  type Rect = ReturnType<typeof Rect>;
@@ -1,8 +1,5 @@
1
1
  <script lang="ts" module>
2
- export type {
3
- RectProps,
4
- RectPropsWithoutHTML,
5
- } from './Rect.shared.svelte.js';
2
+ export type { RectProps, RectPropsWithoutHTML } from './Rect.shared.svelte.js';
6
3
  </script>
7
4
 
8
5
  <script lang="ts">
@@ -11,10 +8,7 @@
11
8
  import { resolveColorProp, resolveStyleProp } from '../../utils/dataProp.js';
12
9
  import { RectState, rectMarkInfo, type RectProps } from './Rect.shared.svelte.js';
13
10
 
14
- let {
15
- children,
16
- ...rest
17
- }: RectProps = $props();
11
+ let { children, ...rest }: RectProps = $props();
18
12
 
19
13
  const c = new RectState(() => rest as RectProps);
20
14
 
@@ -55,7 +49,7 @@
55
49
  style:border-width={resolvedBorderWidth}
56
50
  style:border-style={c.dashArrayResolved ? 'dashed' : 'solid'}
57
51
  style:border-color={resolvedStroke}
58
- style:border-radius="{c.rx}px"
52
+ style:border-radius={c.borderRadius(item.width, item.height) ?? `${c.rx}px`}
59
53
  class={cls('lc-rect', resolvedClass)}
60
54
  ></div>
61
55
  {/each}
@@ -1,4 +1,4 @@
1
- export type { RectProps, RectPropsWithoutHTML, } from './Rect.shared.svelte.js';
1
+ export type { RectProps, RectPropsWithoutHTML } from './Rect.shared.svelte.js';
2
2
  import { type RectProps } from './Rect.shared.svelte.js';
3
3
  declare const Rect: import("svelte").Component<RectProps, {}, "">;
4
4
  type Rect = ReturnType<typeof Rect>;
@@ -42,14 +42,14 @@ export type RectPropsWithoutHTML = {
42
42
  *
43
43
  * @default 0
44
44
  */
45
- width?: number;
45
+ width?: DataProp;
46
46
  initialWidth?: number;
47
47
  /**
48
48
  * The height of the rectangle (pixels).
49
49
  *
50
50
  * @default 0
51
51
  */
52
- height?: number;
52
+ height?: DataProp;
53
53
  initialHeight?: number;
54
54
  /**
55
55
  * Left/start x edge (data mode).
@@ -158,6 +158,9 @@ export declare class RectState {
158
158
  get motionY(): any;
159
159
  get motionWidth(): any;
160
160
  get motionHeight(): any;
161
+ resolveCorners(width: number, height: number): import("../../utils/rect.svelte.js").CornerRadii | undefined;
162
+ roundedRectPath(x: number, y: number, width: number, height: number): string | undefined;
163
+ borderRadius(width: number, height: number): string | undefined;
161
164
  resolvedCorners: import("../../utils/rect.svelte.js").CornerRadii | undefined;
162
165
  borderRadiusStyle: string | undefined;
163
166
  pixelPathData: string | undefined;
@@ -1,5 +1,5 @@
1
1
  import { untrack } from 'svelte';
2
- import { hasAnyDataProp, resolveDataProp, resolveGeoDataPair, } from '../../utils/dataProp.js';
2
+ import { hasAnyDataProp, resolveDataProp, resolveGeoDataPair } from '../../utils/dataProp.js';
3
3
  import { chartDataArray } from '../../utils/common.js';
4
4
  import { resolveCorners, cornersUniform, resolveInsets, } from '../../utils/rect.svelte.js';
5
5
  import { roundedRectPath, parseDashArray } from '../../utils/path.js';
@@ -32,7 +32,7 @@ export class RectState {
32
32
  geo = getGeoContext();
33
33
  // Data mode detection
34
34
  hasEdgeProps = $derived(hasAnyDataProp(this.#getProps().x0, this.#getProps().y0, this.#getProps().x1, this.#getProps().y1));
35
- dataMode = $derived(hasAnyDataProp(this.#getProps().x, this.#getProps().y) || this.hasEdgeProps);
35
+ dataMode = $derived(hasAnyDataProp(this.#getProps().x, this.#getProps().y, this.#getProps().width, this.#getProps().height) || this.hasEdgeProps);
36
36
  // Data resolution
37
37
  #resolvedData = $derived(this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : []);
38
38
  resolvedItems = $derived.by(() => {
@@ -92,8 +92,10 @@ export class RectState {
92
92
  return {
93
93
  x: resolvedX + resolvedInsets.left,
94
94
  y: resolvedY + resolvedInsets.top,
95
- width: Math.max(0, (props.width ?? 0) - resolvedInsets.left - resolvedInsets.right),
96
- height: Math.max(0, (props.height ?? 0) - resolvedInsets.top - resolvedInsets.bottom),
95
+ width: Math.max(0, resolveDataProp(props.width, d, undefined, 0) - resolvedInsets.left - resolvedInsets.right),
96
+ height: Math.max(0, resolveDataProp(props.height, d, undefined, 0) -
97
+ resolvedInsets.top -
98
+ resolvedInsets.bottom),
97
99
  };
98
100
  }
99
101
  }
@@ -133,11 +135,24 @@ export class RectState {
133
135
  return this.#motionHeight.current;
134
136
  }
135
137
  // Resolved per-corner radii (clamped to current bounds)
136
- resolvedCorners = $derived.by(() => {
138
+ resolveCorners(width, height) {
137
139
  const corners = this.#getProps().corners;
138
140
  if (corners === undefined)
139
141
  return undefined;
140
- return resolveCorners(corners, this.motionWidth, this.motionHeight);
142
+ return resolveCorners(corners, width, height);
143
+ }
144
+ roundedRectPath(x, y, width, height) {
145
+ const corners = this.resolveCorners(width, height);
146
+ if (!corners || !this.cornersNonUniform)
147
+ return undefined;
148
+ return roundedRectPath(x, y, width, height, corners);
149
+ }
150
+ borderRadius(width, height) {
151
+ const corners = this.resolveCorners(width, height);
152
+ return corners ? corners.map((c) => `${c}px`).join(' ') : undefined;
153
+ }
154
+ resolvedCorners = $derived.by(() => {
155
+ return this.resolveCorners(this.motionWidth, this.motionHeight);
141
156
  });
142
157
  borderRadiusStyle = $derived(this.resolvedCorners ? this.resolvedCorners.map((c) => `${c}px`).join(' ') : undefined);
143
158
  pixelPathData = $derived.by(() => {
@@ -158,9 +173,7 @@ export class RectState {
158
173
  staticStrokeWidth = $derived(typeof this.#getProps().strokeWidth === 'number'
159
174
  ? this.#getProps().strokeWidth
160
175
  : undefined);
161
- staticOpacity = $derived(typeof this.#getProps().opacity === 'number'
162
- ? this.#getProps().opacity
163
- : undefined);
176
+ staticOpacity = $derived(typeof this.#getProps().opacity === 'number' ? this.#getProps().opacity : undefined);
164
177
  staticClassName = $derived(typeof this.#getProps().class === 'string' ? this.#getProps().class : undefined);
165
178
  // Match SVG's implicit `stroke-width: 1` default
166
179
  staticBorderWidth = $derived.by(() => {
@@ -176,13 +189,13 @@ export class RectState {
176
189
  const initial = getProps();
177
190
  const initialX = initial.initialX ?? (typeof initial.x === 'number' ? initial.x : 0);
178
191
  const initialY = initial.initialY ?? (typeof initial.y === 'number' ? initial.y : 0);
179
- const initialWidth = initial.initialWidth ?? initial.width ?? 0;
180
- const initialHeight = initial.initialHeight ?? initial.height ?? 0;
192
+ const initialWidth = initial.initialWidth ?? (typeof initial.width === 'number' ? initial.width : 0);
193
+ const initialHeight = initial.initialHeight ?? (typeof initial.height === 'number' ? initial.height : 0);
181
194
  const motion = initial.motion;
182
195
  this.#motionX = createMotion(initialX, () => (typeof getProps().x === 'number' ? getProps().x : 0), motion === undefined ? undefined : parseMotionProp(motion, 'x'));
183
196
  this.#motionY = createMotion(initialY, () => (typeof getProps().y === 'number' ? getProps().y : 0), motion === undefined ? undefined : parseMotionProp(motion, 'y'));
184
- this.#motionWidth = createMotion(initialWidth, () => getProps().width ?? 0, motion === undefined ? undefined : parseMotionProp(motion, 'width'));
185
- this.#motionHeight = createMotion(initialHeight, () => getProps().height ?? 0, motion === undefined ? undefined : parseMotionProp(motion, 'height'));
197
+ this.#motionWidth = createMotion(initialWidth, () => (typeof getProps().width === 'number' ? getProps().width : 0), motion === undefined ? undefined : parseMotionProp(motion, 'width'));
198
+ this.#motionHeight = createMotion(initialHeight, () => (typeof getProps().height === 'number' ? getProps().height : 0), motion === undefined ? undefined : parseMotionProp(motion, 'height'));
186
199
  this.#dataMotionMap = createDataMotionMap(motion);
187
200
  if (this.#dataMotionMap) {
188
201
  const motionMap = this.#dataMotionMap;
@@ -47,6 +47,28 @@ describe('Rect', () => {
47
47
  const rects = page.getByTestId(componentTestId).elements();
48
48
  await expect.poll(() => rects.length).toBe(3);
49
49
  });
50
+ it('should resolve width and height from data', async () => {
51
+ render(TestHarness, {
52
+ component: Rect,
53
+ chartProps: {
54
+ data: [{ x: 20, y: 30, width: 80, height: 40 }],
55
+ x: 'x',
56
+ y: 'y',
57
+ xDomain: [0, 100],
58
+ yDomain: [0, 100],
59
+ },
60
+ componentProps: {
61
+ x: 'x',
62
+ y: 'y',
63
+ width: 'width',
64
+ height: 'height',
65
+ },
66
+ });
67
+ const rect = page.getByTestId(componentTestId);
68
+ await expect.element(rect).toBeInTheDocument();
69
+ expect(rect.element()?.getAttribute('width')).toBe('80');
70
+ expect(rect.element()?.getAttribute('height')).toBe('40');
71
+ });
50
72
  });
51
73
  describe('data mode - colors', () => {
52
74
  const data = [
@@ -153,6 +175,29 @@ describe('Rect', () => {
153
175
  const width = Number(firstRect?.getAttribute('width'));
154
176
  expect(width).toBeGreaterThan(0);
155
177
  });
178
+ it('should render non-uniform corners from edge props', async () => {
179
+ render(TestHarness, {
180
+ component: Rect,
181
+ chartProps: {
182
+ data: [data[0]],
183
+ x: ['x0', 'x1'],
184
+ y: 'count',
185
+ xDomain: [0, 100],
186
+ yDomain: [0, 15],
187
+ },
188
+ componentProps: {
189
+ x0: 'x0',
190
+ x1: 'x1',
191
+ y0: (d) => 0,
192
+ y1: 'count',
193
+ corners: [12, 4, 10, 2],
194
+ },
195
+ });
196
+ const rect = page.getByTestId(componentTestId);
197
+ await expect.element(rect).toBeInTheDocument();
198
+ expect(rect.element()?.tagName.toLowerCase()).toBe('path');
199
+ expect(rect.element()?.getAttribute('d')).toContain('a');
200
+ });
156
201
  it('should use explicit data prop over chart context data', async () => {
157
202
  const explicitData = [{ x0: 0, x1: 50, count: 5 }];
158
203
  render(TestHarness, {
@@ -1,8 +1,5 @@
1
1
  <script lang="ts" module>
2
- export type {
3
- RectProps,
4
- RectPropsWithoutHTML,
5
- } from './Rect.shared.svelte.js';
2
+ export type { RectProps, RectPropsWithoutHTML } from './Rect.shared.svelte.js';
6
3
  </script>
7
4
 
8
5
  <script lang="ts">
@@ -64,23 +61,41 @@
64
61
  {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)}
65
62
  {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)}
66
63
  {@const resolvedClass = resolveStyleProp(rest.class, item.d)}
67
- <rect
68
- {...rest as any}
69
- x={item.x}
70
- y={item.y}
71
- width={item.width}
72
- height={item.height}
73
- fill={resolvedFill}
74
- fill-opacity={resolvedFillOpacity}
75
- stroke={resolvedStroke}
76
- stroke-opacity={resolvedStrokeOpacity}
77
- stroke-width={resolvedStrokeWidth}
78
- opacity={resolvedOpacity}
79
- rx={c.rx}
80
- ry={c.ry}
81
- stroke-dasharray={c.dashArrayAttr}
82
- class={cls('lc-rect', resolvedClass)}
83
- />
64
+ {@const pathData = c.roundedRectPath(item.x, item.y, item.width, item.height)}
65
+ {#if pathData}
66
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
67
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
68
+ <path
69
+ {...rest as unknown as SVGAttributes<SVGPathElement>}
70
+ d={pathData}
71
+ fill={resolvedFill}
72
+ fill-opacity={resolvedFillOpacity}
73
+ stroke={resolvedStroke}
74
+ stroke-opacity={resolvedStrokeOpacity}
75
+ stroke-width={resolvedStrokeWidth}
76
+ opacity={resolvedOpacity}
77
+ stroke-dasharray={c.dashArrayAttr}
78
+ class={cls('lc-rect', resolvedClass)}
79
+ />
80
+ {:else}
81
+ <rect
82
+ {...rest as any}
83
+ x={item.x}
84
+ y={item.y}
85
+ width={item.width}
86
+ height={item.height}
87
+ fill={resolvedFill}
88
+ fill-opacity={resolvedFillOpacity}
89
+ stroke={resolvedStroke}
90
+ stroke-opacity={resolvedStrokeOpacity}
91
+ stroke-width={resolvedStrokeWidth}
92
+ opacity={resolvedOpacity}
93
+ rx={c.rx}
94
+ ry={c.ry}
95
+ stroke-dasharray={c.dashArrayAttr}
96
+ class={cls('lc-rect', resolvedClass)}
97
+ />
98
+ {/if}
84
99
  {/each}
85
100
  {:else if c.pixelPathData}
86
101
  <!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -1,4 +1,4 @@
1
- export type { RectProps, RectPropsWithoutHTML, } from './Rect.shared.svelte.js';
1
+ export type { RectProps, RectPropsWithoutHTML } from './Rect.shared.svelte.js';
2
2
  import { type RectProps } from './Rect.shared.svelte.js';
3
3
  declare const Rect: import("svelte").Component<RectProps, {}, "ref">;
4
4
  type Rect = ReturnType<typeof Rect>;
@@ -39,7 +39,7 @@
39
39
  pathData={seg.d}
40
40
  stroke={seg.stroke}
41
41
  fill={seg.fill}
42
- opacity={seg.opacity ?? c.seriesOpacity}
42
+ opacity={seg.opacity ?? (c.seriesOpacity === 1 ? undefined : c.seriesOpacity)}
43
43
  {...c.series?.props}
44
44
  {...restProps}
45
45
  />
@@ -49,7 +49,8 @@
49
49
  pathData={c.isTweened ? c.tweenedPath : c.d}
50
50
  stroke={(typeof stroke === 'string' ? stroke : undefined) ?? c.series?.color}
51
51
  fill={typeof fill === 'string' ? fill : undefined}
52
- opacity={(typeof opacity === 'number' ? opacity : undefined) ?? c.seriesOpacity}
52
+ opacity={(typeof opacity === 'number' ? opacity : undefined) ??
53
+ (c.seriesOpacity === 1 ? undefined : c.seriesOpacity)}
53
54
  {...c.series?.props}
54
55
  {...restProps}
55
56
  />