layerchart 2.0.0-next.3 → 2.0.0-next.31

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 (112) hide show
  1. package/dist/components/AnnotationPoint.svelte +16 -9
  2. package/dist/components/AnnotationRange.svelte +3 -3
  3. package/dist/components/Arc.svelte +2 -2
  4. package/dist/components/Axis.svelte +83 -29
  5. package/dist/components/Axis.svelte.d.ts +13 -3
  6. package/dist/components/Bar.svelte +12 -8
  7. package/dist/components/Blur.svelte +5 -3
  8. package/dist/components/Blur.svelte.d.ts +2 -5
  9. package/dist/components/BrushContext.svelte +1 -1
  10. package/dist/components/Calendar.svelte +10 -6
  11. package/dist/components/Calendar.svelte.d.ts +2 -1
  12. package/dist/components/Chart.svelte +39 -3
  13. package/dist/components/Chart.svelte.d.ts +11 -0
  14. package/dist/components/Connector.svelte +2 -2
  15. package/dist/components/Connector.svelte.d.ts +1 -1
  16. package/dist/components/Ellipse.svelte +187 -0
  17. package/dist/components/Ellipse.svelte.d.ts +64 -0
  18. package/dist/components/ForceSimulation.svelte +184 -50
  19. package/dist/components/ForceSimulation.svelte.d.ts +88 -21
  20. package/dist/components/GeoPath.svelte +12 -5
  21. package/dist/components/GeoPoint.svelte +1 -2
  22. package/dist/components/GeoSpline.svelte +4 -4
  23. package/dist/components/GeoSpline.svelte.d.ts +1 -1
  24. package/dist/components/Group.svelte +2 -2
  25. package/dist/components/Highlight.svelte +9 -6
  26. package/dist/components/Hull.svelte +1 -1
  27. package/dist/components/Labels.svelte +3 -2
  28. package/dist/components/Labels.svelte.d.ts +2 -2
  29. package/dist/components/Legend.svelte +19 -12
  30. package/dist/components/Legend.svelte.d.ts +5 -5
  31. package/dist/components/MonthPath.svelte +14 -11
  32. package/dist/components/MonthPath.svelte.d.ts +4 -3
  33. package/dist/components/Polygon.svelte +285 -0
  34. package/dist/components/Polygon.svelte.d.ts +115 -0
  35. package/dist/components/RadialGradient.svelte +1 -3
  36. package/dist/components/Spline.svelte +30 -18
  37. package/dist/components/Spline.svelte.d.ts +12 -4
  38. package/dist/components/Text.svelte +62 -60
  39. package/dist/components/Text.svelte.d.ts +6 -0
  40. package/dist/components/TransformControls.svelte +16 -20
  41. package/dist/components/Treemap.svelte +63 -26
  42. package/dist/components/Treemap.svelte.d.ts +11 -11
  43. package/dist/components/Voronoi.svelte +51 -33
  44. package/dist/components/Voronoi.svelte.d.ts +3 -1
  45. package/dist/components/charts/ArcChart.svelte +5 -3
  46. package/dist/components/charts/AreaChart.svelte +11 -11
  47. package/dist/components/charts/BarChart.svelte +72 -53
  48. package/dist/components/charts/DefaultTooltip.svelte +1 -1
  49. package/dist/components/charts/LineChart.svelte +10 -6
  50. package/dist/components/charts/PieChart.svelte +5 -3
  51. package/dist/components/charts/ScatterChart.svelte +2 -3
  52. package/dist/components/charts/utils.svelte.d.ts +2 -2
  53. package/dist/components/charts/utils.svelte.js +5 -1
  54. package/dist/components/index.d.ts +4 -0
  55. package/dist/components/index.js +5 -1
  56. package/dist/components/layout/Canvas.svelte +67 -49
  57. package/dist/components/layout/Canvas.svelte.d.ts +6 -0
  58. package/dist/components/layout/Layer.svelte +6 -4
  59. package/dist/components/layout/Layer.svelte.d.ts +6 -4
  60. package/dist/components/tooltip/Tooltip.svelte +14 -7
  61. package/dist/components/tooltip/TooltipContext.svelte +136 -43
  62. package/dist/components/tooltip/TooltipContext.svelte.d.ts +3 -3
  63. package/dist/components/tooltip/TooltipHeader.svelte +5 -4
  64. package/dist/components/tooltip/TooltipHeader.svelte.d.ts +3 -3
  65. package/dist/components/tooltip/TooltipItem.svelte +5 -4
  66. package/dist/components/tooltip/TooltipItem.svelte.d.ts +3 -3
  67. package/dist/components/tooltip/TooltipList.svelte +1 -1
  68. package/dist/components/tooltip/tooltipMetaContext.d.ts +2 -2
  69. package/dist/docs/Blockquote.svelte +6 -4
  70. package/dist/docs/Blockquote.svelte.d.ts +4 -19
  71. package/dist/docs/Code.svelte +20 -12
  72. package/dist/docs/Code.svelte.d.ts +9 -23
  73. package/dist/docs/Header1.svelte +4 -2
  74. package/dist/docs/Header1.svelte.d.ts +4 -28
  75. package/dist/docs/Json.svelte +11 -3
  76. package/dist/docs/Json.svelte.d.ts +9 -21
  77. package/dist/docs/Layout.svelte +10 -7
  78. package/dist/docs/Layout.svelte.d.ts +4 -19
  79. package/dist/docs/Link.svelte +7 -3
  80. package/dist/docs/Link.svelte.d.ts +4 -38
  81. package/dist/docs/Preview.svelte +6 -3
  82. package/dist/docs/TilesetField.svelte +20 -19
  83. package/dist/docs/TilesetField.svelte.d.ts +5 -22
  84. package/dist/docs/ViewSourceButton.svelte +9 -6
  85. package/dist/docs/ViewSourceButton.svelte.d.ts +7 -21
  86. package/dist/utils/arcText.svelte.js +4 -4
  87. package/dist/utils/array.d.ts +11 -0
  88. package/dist/utils/array.js +23 -0
  89. package/dist/utils/array.test.d.ts +1 -0
  90. package/dist/utils/array.test.js +200 -0
  91. package/dist/utils/canvas.d.ts +77 -0
  92. package/dist/utils/canvas.js +105 -41
  93. package/dist/utils/genData.d.ts +14 -0
  94. package/dist/utils/genData.js +24 -6
  95. package/dist/utils/index.d.ts +1 -0
  96. package/dist/utils/index.js +1 -0
  97. package/dist/utils/path.d.ts +10 -0
  98. package/dist/utils/path.js +30 -0
  99. package/dist/utils/rect.svelte.d.ts +2 -2
  100. package/dist/utils/rect.svelte.js +69 -1
  101. package/dist/utils/scales.svelte.d.ts +3 -2
  102. package/dist/utils/scales.svelte.js +7 -3
  103. package/dist/utils/shape.d.ts +43 -0
  104. package/dist/utils/shape.js +59 -0
  105. package/dist/utils/string.d.ts +49 -0
  106. package/dist/utils/string.js +4 -2
  107. package/dist/utils/ticks.d.ts +15 -4
  108. package/dist/utils/ticks.js +144 -158
  109. package/dist/utils/ticks.test.js +11 -16
  110. package/dist/utils/treemap.d.ts +1 -1
  111. package/package.json +27 -25
  112. package/dist/utils/object.js +0 -2
@@ -216,7 +216,7 @@ export function createArcTextProps(props, opts = {}, position) {
216
216
  x: x,
217
217
  y: y,
218
218
  textAnchor,
219
- 'dominant-baseline': 'middle',
219
+ dominantBaseline: 'middle',
220
220
  };
221
221
  });
222
222
  const current = $derived.by(() => {
@@ -224,21 +224,21 @@ export function createArcTextProps(props, opts = {}, position) {
224
224
  return {
225
225
  path: innerPath.current,
226
226
  ...sharedProps,
227
- 'dominant-baseline': innerDominantBaseline,
227
+ dominantBaseline: innerDominantBaseline,
228
228
  };
229
229
  }
230
230
  else if (position === 'outer') {
231
231
  return {
232
232
  path: outerPath.current,
233
233
  ...sharedProps,
234
- 'dominant-baseline': outerDominantBaseline,
234
+ dominantBaseline: outerDominantBaseline,
235
235
  };
236
236
  }
237
237
  else if (position === 'middle') {
238
238
  return {
239
239
  path: middlePath.current,
240
240
  ...sharedProps,
241
- 'dominant-baseline': 'middle',
241
+ dominantBaseline: 'middle',
242
242
  };
243
243
  }
244
244
  else if (position === 'centroid') {
@@ -1,5 +1,6 @@
1
1
  import type { Numeric } from 'd3-array';
2
2
  import { extent as d3extent } from 'd3-array';
3
+ import { type Accessor } from './common.js';
3
4
  /**
4
5
  * Wrapper around d3-array's `extent()` but remove [undefined, undefined] return type
5
6
  */
@@ -11,3 +12,13 @@ export declare function extent<T extends Numeric>(iterable: Parameters<typeof d3
11
12
  * of making a set
12
13
  */
13
14
  export declare function arraysEqual(arr1: unknown[], arr2: unknown[]): boolean;
15
+ /**
16
+ * Add `lanes` property to each element in the data array support densely packing.
17
+ * This is useful for visualizing overlapping events in a timeline / Gantt chart.
18
+ */
19
+ export declare function applyLanes<T extends Record<string, any>>(data: T[], options?: {
20
+ start: Accessor<T>;
21
+ end: Accessor<T>;
22
+ }): (T & {
23
+ lane: number;
24
+ })[];
@@ -1,4 +1,5 @@
1
1
  import { extent as d3extent } from 'd3-array';
2
+ import { accessor } from './common.js';
2
3
  /**
3
4
  * Wrapper around d3-array's `extent()` but remove [undefined, undefined] return type
4
5
  */
@@ -18,3 +19,25 @@ export function arraysEqual(arr1, arr2) {
18
19
  return arr2.includes(k);
19
20
  });
20
21
  }
22
+ /**
23
+ * Add `lanes` property to each element in the data array support densely packing.
24
+ * This is useful for visualizing overlapping events in a timeline / Gantt chart.
25
+ */
26
+ export function applyLanes(data, options = {
27
+ start: 'start',
28
+ end: 'end',
29
+ }) {
30
+ const result = [];
31
+ let stack = [];
32
+ const startAccessor = accessor(options.start);
33
+ const endAccessor = accessor(options.end);
34
+ for (const d of data) {
35
+ let lane = stack.findIndex((s) => endAccessor(s) <= startAccessor(d) && startAccessor(s) < startAccessor(d));
36
+ if (lane === -1) {
37
+ lane = stack.length;
38
+ }
39
+ result.push({ ...d, lane });
40
+ stack[lane] = d;
41
+ }
42
+ return result;
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,200 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { applyLanes } from './array.js';
3
+ describe('applyLanes', () => {
4
+ it('should assign same lane to non-overlapping events', () => {
5
+ const data = [
6
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') },
7
+ { id: 2, start: new Date('2023-01-03'), end: new Date('2023-01-05') },
8
+ { id: 3, start: new Date('2023-01-06'), end: new Date('2023-01-08') },
9
+ ];
10
+ const result = applyLanes(data);
11
+ expect(result).toEqual([
12
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 },
13
+ { id: 2, start: new Date('2023-01-03'), end: new Date('2023-01-05'), lane: 0 },
14
+ { id: 3, start: new Date('2023-01-06'), end: new Date('2023-01-08'), lane: 0 },
15
+ ]);
16
+ });
17
+ it('should assign different lanes to overlapping events', () => {
18
+ const data = [
19
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-03') },
20
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04') },
21
+ { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-05') },
22
+ ];
23
+ const result = applyLanes(data);
24
+ expect(result).toEqual([
25
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-03'), lane: 0 },
26
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04'), lane: 1 },
27
+ { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-05'), lane: 2 },
28
+ ]);
29
+ });
30
+ it('should reuse lanes when events no longer overlap', () => {
31
+ const data = [
32
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') },
33
+ { id: 2, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03') },
34
+ { id: 3, start: new Date('2023-01-04'), end: new Date('2023-01-06') }, // starts after id: 1 ends
35
+ { id: 4, start: new Date('2023-01-05'), end: new Date('2023-01-07') }, // starts after id: 2 ends
36
+ ];
37
+ const result = applyLanes(data);
38
+ expect(result).toEqual([
39
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 },
40
+ { id: 2, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03'), lane: 1 },
41
+ { id: 3, start: new Date('2023-01-04'), end: new Date('2023-01-06'), lane: 0 }, // reuses lane 0
42
+ { id: 4, start: new Date('2023-01-05'), end: new Date('2023-01-07'), lane: 1 }, // reuses lane 1
43
+ ]);
44
+ });
45
+ it('should handle events that start exactly when another ends', () => {
46
+ const data = [
47
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') },
48
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04') }, // starts exactly when id: 1 ends
49
+ { id: 3, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03') }, // overlaps with both
50
+ ];
51
+ const result = applyLanes(data);
52
+ expect(result).toEqual([
53
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 },
54
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04'), lane: 0 }, // can reuse lane 0
55
+ { id: 3, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03'), lane: 1 }, // overlaps, needs new lane
56
+ ]);
57
+ });
58
+ it('should work with string keys for start and end', () => {
59
+ const data = [
60
+ { name: 'Task 1', startTime: new Date('2023-01-01'), endTime: new Date('2023-01-03') },
61
+ { name: 'Task 2', startTime: new Date('2023-01-02'), endTime: new Date('2023-01-04') },
62
+ ];
63
+ const result = applyLanes(data, { start: 'startTime', end: 'endTime' });
64
+ expect(result).toEqual([
65
+ {
66
+ name: 'Task 1',
67
+ startTime: new Date('2023-01-01'),
68
+ endTime: new Date('2023-01-03'),
69
+ lane: 0,
70
+ },
71
+ {
72
+ name: 'Task 2',
73
+ startTime: new Date('2023-01-02'),
74
+ endTime: new Date('2023-01-04'),
75
+ lane: 1,
76
+ },
77
+ ]);
78
+ });
79
+ it('should work with nested string keys for start and end', () => {
80
+ const data = [
81
+ { name: 'Task 1', duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') } },
82
+ { name: 'Task 2', duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') } },
83
+ ];
84
+ const result = applyLanes(data, { start: 'duration.start', end: 'duration.end' });
85
+ expect(result).toEqual([
86
+ {
87
+ name: 'Task 1',
88
+ duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') },
89
+ lane: 0,
90
+ },
91
+ {
92
+ name: 'Task 2',
93
+ duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') },
94
+ lane: 0,
95
+ },
96
+ ]);
97
+ });
98
+ it('should work with function accessors for start and end', () => {
99
+ const data = [
100
+ { name: 'Task 1', duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') } },
101
+ { name: 'Task 2', duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') } },
102
+ ];
103
+ const result = applyLanes(data, { start: (d) => d.duration.start, end: (d) => d.duration.end });
104
+ expect(result).toEqual([
105
+ {
106
+ name: 'Task 1',
107
+ duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') },
108
+ lane: 0,
109
+ },
110
+ {
111
+ name: 'Task 2',
112
+ duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') },
113
+ lane: 0,
114
+ },
115
+ ]);
116
+ });
117
+ it('should handle empty array', () => {
118
+ const data = [];
119
+ const result = applyLanes(data);
120
+ expect(result).toEqual([]);
121
+ });
122
+ it('should handle single event', () => {
123
+ const data = [{ id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') }];
124
+ const result = applyLanes(data);
125
+ expect(result).toEqual([
126
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 },
127
+ ]);
128
+ });
129
+ it('should handle complex overlapping scenario', () => {
130
+ const data = [
131
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-05') }, // long event
132
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-03') }, // short event inside
133
+ { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-04') }, // overlaps with both
134
+ { id: 4, start: new Date('2023-01-03'), end: new Date('2023-01-04T12:00:00') }, // overlaps with 1 and 3
135
+ { id: 5, start: new Date('2023-01-06'), end: new Date('2023-01-08') }, // separate event
136
+ ];
137
+ const result = applyLanes(data);
138
+ expect(result).toEqual([
139
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-05'), lane: 0 },
140
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-03'), lane: 1 },
141
+ { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-04'), lane: 2 },
142
+ { id: 4, start: new Date('2023-01-03'), end: new Date('2023-01-04T12:00:00'), lane: 1 }, // can reuse lane 1 since id: 2 ended
143
+ { id: 5, start: new Date('2023-01-06'), end: new Date('2023-01-08'), lane: 0 }, // can reuse lane 0 since id: 1 ended
144
+ ]);
145
+ });
146
+ it('should preserve all original properties', () => {
147
+ const data = [
148
+ {
149
+ id: 1,
150
+ start: new Date('2023-01-01'),
151
+ end: new Date('2023-01-02'),
152
+ name: 'First',
153
+ priority: 'high',
154
+ metadata: { foo: 'bar' },
155
+ },
156
+ {
157
+ id: 2,
158
+ start: new Date('2023-01-01T12:00:00'),
159
+ end: new Date('2023-01-03'),
160
+ name: 'Second',
161
+ priority: 'low',
162
+ metadata: { baz: 'qux' },
163
+ },
164
+ ];
165
+ const result = applyLanes(data);
166
+ expect(result).toEqual([
167
+ {
168
+ id: 1,
169
+ start: new Date('2023-01-01'),
170
+ end: new Date('2023-01-02'),
171
+ name: 'First',
172
+ priority: 'high',
173
+ metadata: { foo: 'bar' },
174
+ lane: 0,
175
+ },
176
+ {
177
+ id: 2,
178
+ start: new Date('2023-01-01T12:00:00'),
179
+ end: new Date('2023-01-03'),
180
+ name: 'Second',
181
+ priority: 'low',
182
+ metadata: { baz: 'qux' },
183
+ lane: 1,
184
+ },
185
+ ]);
186
+ });
187
+ it('should work with numeric values', () => {
188
+ const data = [
189
+ { id: 1, start: 0, end: 3 },
190
+ { id: 2, start: 1, end: 4 },
191
+ { id: 3, start: 5, end: 7 },
192
+ ];
193
+ const result = applyLanes(data);
194
+ expect(result).toEqual([
195
+ { id: 1, start: 0, end: 3, lane: 0 },
196
+ { id: 2, start: 1, end: 4, lane: 1 },
197
+ { id: 3, start: 5, end: 7, lane: 0 }, // can reuse lane 0 since id: 1 ended
198
+ ]);
199
+ });
200
+ });
@@ -0,0 +1,77 @@
1
+ import type { ClassValue } from 'svelte/elements';
2
+ import type { PatternShape } from '../components/Pattern.svelte';
3
+ export declare const DEFAULT_FILL = "rgb(0, 0, 0)";
4
+ type StyleOptions = Partial<Omit<CSSStyleDeclaration, 'fillOpacity' | 'strokeWidth' | 'opacity'> & {
5
+ fillOpacity?: number | string;
6
+ strokeWidth?: number | string;
7
+ opacity?: number | string;
8
+ }>;
9
+ export type ComputedStylesOptions = {
10
+ styles?: StyleOptions;
11
+ classes?: ClassValue | null;
12
+ };
13
+ /**
14
+ * Appends or reuses `<svg>` element below `<canvas>` to resolve CSS variables and classes (ex. `stroke: var(--color-primary)` => `stroke: rgb(...)` )
15
+ */
16
+ export declare function _getComputedStyles(canvas: HTMLCanvasElement, { styles, classes }?: ComputedStylesOptions): CSSStyleDeclaration;
17
+ export declare const getComputedStyles: any;
18
+ /** Render SVG path data onto canvas context. Supports CSS variables and classes by tranferring to hidden `<svg>` element before retrieval) */
19
+ export declare function renderPathData(ctx: CanvasRenderingContext2D, pathData: string | null | undefined, styleOptions?: ComputedStylesOptions): void;
20
+ export declare function renderText(ctx: CanvasRenderingContext2D, text: string | number | null | undefined, coords: {
21
+ x: number;
22
+ y: number;
23
+ }, styleOptions?: ComputedStylesOptions): void;
24
+ export declare function renderRect(ctx: CanvasRenderingContext2D, coords: {
25
+ x: number;
26
+ y: number;
27
+ width: number;
28
+ height: number;
29
+ }, styleOptions?: ComputedStylesOptions): void;
30
+ export declare function renderCircle(ctx: CanvasRenderingContext2D, coords: {
31
+ cx: number;
32
+ cy: number;
33
+ r: number;
34
+ }, styleOptions?: ComputedStylesOptions): void;
35
+ export declare function renderEllipse(ctx: CanvasRenderingContext2D, coords: {
36
+ cx: number;
37
+ cy: number;
38
+ rx: number;
39
+ ry: number;
40
+ }, styleOptions?: ComputedStylesOptions): void;
41
+ /** Clear canvas accounting for Canvas `context.translate(...)` */
42
+ export declare function clearCanvasContext(ctx: CanvasRenderingContext2D, options: {
43
+ containerWidth: number;
44
+ containerHeight: number;
45
+ padding: {
46
+ top: number;
47
+ bottom: number;
48
+ left: number;
49
+ right: number;
50
+ };
51
+ }): void;
52
+ /**
53
+ Scales a canvas for high DPI / retina displays.
54
+ @see: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#examples
55
+ @see: https://web.dev/articles/canvas-hidipi
56
+ */
57
+ export declare function scaleCanvas(ctx: CanvasRenderingContext2D, width: number, height: number): {
58
+ width: number;
59
+ height: number;
60
+ };
61
+ /** Get pixel color (r,g,b,a) at canvas coordinates */
62
+ export declare function getPixelColor(ctx: CanvasRenderingContext2D, x: number, y: number): {
63
+ r: number;
64
+ g: number;
65
+ b: number;
66
+ a: number;
67
+ };
68
+ export declare function _createLinearGradient(ctx: CanvasRenderingContext2D, x0: number, y0: number, x1: number, y1: number, stops: {
69
+ offset: number;
70
+ color: string;
71
+ }[]): CanvasGradient;
72
+ /** Create linear gradient and memoize result to fix reactivity */
73
+ export declare const createLinearGradient: any;
74
+ export declare function _createPattern(ctx: CanvasRenderingContext2D, width: number, height: number, shapes: PatternShape[], background?: string): CanvasPattern | null;
75
+ /** Create pattern and memoize result to fix reactivity */
76
+ export declare const createPattern: any;
77
+ export {};
@@ -1,11 +1,25 @@
1
+ import memoize from 'memoize';
1
2
  import { cls } from '@layerstack/tailwind';
2
- import { memoize } from 'lodash-es';
3
3
  export const DEFAULT_FILL = 'rgb(0, 0, 0)';
4
4
  const CANVAS_STYLES_ELEMENT_ID = '__layerchart_canvas_styles_id';
5
+ const supportedStyles = [
6
+ 'fill',
7
+ 'fillOpacity',
8
+ 'stroke',
9
+ 'strokeWidth',
10
+ 'opacity',
11
+ 'fontWeight',
12
+ 'fontSize',
13
+ 'fontFamily',
14
+ 'textAnchor',
15
+ 'textAlign',
16
+ 'paintOrder',
17
+ ];
5
18
  /**
6
19
  * Appends or reuses `<svg>` element below `<canvas>` to resolve CSS variables and classes (ex. `stroke: var(--color-primary)` => `stroke: rgb(...)` )
7
20
  */
8
- export function getComputedStyles(canvas, { styles, classes } = {}) {
21
+ export function _getComputedStyles(canvas, { styles, classes } = {}) {
22
+ // console.count(`getComputedStyles: ${getComputedStylesKey(canvas, { styles, classes })}`);
9
23
  try {
10
24
  // Get or create `<svg>` below `<canvas>`
11
25
  let svg = document.getElementById(CANVAS_STYLES_ELEMENT_ID);
@@ -24,13 +38,19 @@ export function getComputedStyles(canvas, { styles, classes } = {}) {
24
38
  if (styles) {
25
39
  Object.assign(svg.style, styles);
26
40
  }
41
+ // Make sure `<svg>` is not visible
42
+ svg.style.display = 'none';
27
43
  if (classes) {
28
44
  svg.setAttribute('class', cls(classes)
29
45
  .split(' ')
30
46
  .filter((s) => !s.startsWith('transition-'))
31
47
  .join(' '));
32
48
  }
33
- const computedStyles = window.getComputedStyle(svg);
49
+ // Capture copy to enable memoization and avoid capturing all styles (which is very slow)
50
+ const computedStyles = supportedStyles.reduce((acc, style) => {
51
+ acc[style] = window.getComputedStyle(svg)[style];
52
+ return acc;
53
+ }, {});
34
54
  return computedStyles;
35
55
  }
36
56
  catch (e) {
@@ -38,38 +58,71 @@ export function getComputedStyles(canvas, { styles, classes } = {}) {
38
58
  return {};
39
59
  }
40
60
  }
61
+ function getComputedStylesKey(canvas, { styles, classes } = {}) {
62
+ return JSON.stringify({ canvasId: canvas.id, styles, classes });
63
+ }
64
+ export const getComputedStyles = memoize(_getComputedStyles, {
65
+ cacheKey: ([canvas, styleOptions]) => {
66
+ return getComputedStylesKey(canvas, styleOptions);
67
+ },
68
+ });
41
69
  /** Render onto canvas context. Supports CSS variables and classes by tranferring to hidden `<svg>` element before retrieval) */
42
- function render(ctx, render, styleOptions = {}) {
70
+ function render(ctx, render, styleOptions = {}, { applyText, } = {}) {
43
71
  // console.count('render');
44
72
  // TODO: Consider memoizing? How about reactiving to CSS variable changes (light/dark mode toggle)
45
- const computedStyles = getComputedStyles(ctx.canvas, styleOptions);
46
- // Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order
47
- const paintOrder = computedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke'];
48
- if (computedStyles?.opacity) {
49
- ctx.globalAlpha = Number(computedStyles?.opacity);
73
+ let resolvedStyles;
74
+ if (styleOptions.classes == null &&
75
+ !Object.values(styleOptions.styles ?? {}).some((v) => typeof v === 'string' && v.includes('var('))) {
76
+ // Skip resolving styles if no classes are provided and no styles are using CSS variables
77
+ resolvedStyles = styleOptions.styles ?? {};
50
78
  }
51
- // Text properties
52
- ctx.font = `${computedStyles.fontWeight} ${computedStyles.fontSize} ${computedStyles.fontFamily}`; // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null`
53
- // TODO: Hack to handle `textAnchor` with canvas. Try to find a better approach
54
- if (computedStyles.textAnchor === 'middle') {
55
- ctx.textAlign = 'center';
79
+ else {
80
+ // Remove constant non-css variable properties (ex. `strokeWidth: 0.5`, `fill: #123456`) as not needed and improves memoization cache hit
81
+ const { constantStyles, variableStyles } = Object.entries(styleOptions.styles ?? {}).reduce((acc, [key, value]) => {
82
+ if (typeof value === 'number' || (typeof value === 'string' && !value.includes('var('))) {
83
+ acc.constantStyles[key] = value;
84
+ }
85
+ else if (typeof value === 'string' && value.includes('var(')) {
86
+ acc.variableStyles[key] = value;
87
+ }
88
+ return acc;
89
+ }, { constantStyles: {}, variableStyles: {} });
90
+ const computedStyles = getComputedStyles(ctx.canvas, {
91
+ styles: variableStyles,
92
+ classes: styleOptions.classes,
93
+ });
94
+ resolvedStyles = { ...computedStyles, ...constantStyles };
56
95
  }
57
- else if (computedStyles.textAnchor === 'end') {
58
- ctx.textAlign = 'right';
96
+ // Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order
97
+ const paintOrder = resolvedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke'];
98
+ if (resolvedStyles?.opacity) {
99
+ ctx.globalAlpha = Number(resolvedStyles?.opacity);
59
100
  }
60
- else {
61
- ctx.textAlign = computedStyles.textAlign; // TODO: Handle/map `justify` and `match-parent`?
101
+ // font/text properties can be expensive to set (not sure why), so only apply if needed (renderText())
102
+ if (applyText) {
103
+ // Text properties
104
+ ctx.font = `${resolvedStyles.fontWeight} ${resolvedStyles.fontSize} ${resolvedStyles.fontFamily}`; // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null`
105
+ // TODO: Hack to handle `textAnchor` with canvas. Try to find a better approach
106
+ if (resolvedStyles.textAnchor === 'middle') {
107
+ ctx.textAlign = 'center';
108
+ }
109
+ else if (resolvedStyles.textAnchor === 'end') {
110
+ ctx.textAlign = 'right';
111
+ }
112
+ else {
113
+ ctx.textAlign = resolvedStyles.textAlign; // TODO: Handle/map `justify` and `match-parent`?
114
+ }
115
+ // TODO: Handle `textBaseline` / `verticalAnchor` (Text)
116
+ // ctx.textBaseline = 'top';
117
+ // ctx.textBaseline = 'middle';
118
+ // ctx.textBaseline = 'bottom';
119
+ // ctx.textBaseline = 'alphabetic';
120
+ // ctx.textBaseline = 'hanging';
121
+ // ctx.textBaseline = 'ideographic';
62
122
  }
63
- // TODO: Handle `textBaseline` / `verticalAnchor` (Text)
64
- // ctx.textBaseline = 'top';
65
- // ctx.textBaseline = 'middle';
66
- // ctx.textBaseline = 'bottom';
67
- // ctx.textBaseline = 'alphabetic';
68
- // ctx.textBaseline = 'hanging';
69
- // ctx.textBaseline = 'ideographic';
70
123
  // Dashed lines
71
- if (computedStyles.strokeDasharray.includes(',')) {
72
- const dashArray = computedStyles.strokeDasharray
124
+ if (resolvedStyles.strokeDasharray?.includes(',')) {
125
+ const dashArray = resolvedStyles.strokeDasharray
73
126
  .split(',')
74
127
  .map((s) => Number(s.replace('px', '')));
75
128
  ctx.setLineDash(dashArray);
@@ -81,11 +134,11 @@ function render(ctx, render, styleOptions = {}) {
81
134
  styleOptions.styles?.fill instanceof CanvasPattern ||
82
135
  !styleOptions.styles?.fill?.includes('var'))
83
136
  ? styleOptions.styles.fill
84
- : computedStyles?.fill;
137
+ : resolvedStyles?.fill;
85
138
  if (fill && !['none', DEFAULT_FILL].includes(fill)) {
86
139
  const currentGlobalAlpha = ctx.globalAlpha;
87
- const fillOpacity = Number(computedStyles?.fillOpacity);
88
- const opacity = Number(computedStyles?.opacity);
140
+ const fillOpacity = Number(resolvedStyles?.fillOpacity);
141
+ const opacity = Number(resolvedStyles?.opacity);
89
142
  ctx.globalAlpha = fillOpacity * opacity;
90
143
  ctx.fillStyle = fill;
91
144
  render.fill(ctx);
@@ -98,12 +151,12 @@ function render(ctx, render, styleOptions = {}) {
98
151
  (styleOptions.styles?.stroke instanceof CanvasGradient ||
99
152
  !styleOptions.styles?.stroke?.includes('var'))
100
153
  ? styleOptions.styles?.stroke
101
- : computedStyles?.stroke;
154
+ : resolvedStyles?.stroke;
102
155
  if (stroke && !['none'].includes(stroke)) {
103
156
  ctx.lineWidth =
104
- typeof computedStyles?.strokeWidth === 'string'
105
- ? Number(computedStyles?.strokeWidth?.replace('px', ''))
106
- : (computedStyles?.strokeWidth ?? 1);
157
+ typeof resolvedStyles?.strokeWidth === 'string'
158
+ ? Number(resolvedStyles?.strokeWidth?.replace('px', ''))
159
+ : (resolvedStyles?.strokeWidth ?? 1);
107
160
  ctx.strokeStyle = stroke;
108
161
  render.stroke(ctx);
109
162
  }
@@ -123,7 +176,7 @@ export function renderText(ctx, text, coords, styleOptions = {}) {
123
176
  render(ctx, {
124
177
  fill: (ctx) => ctx.fillText(text.toString(), coords.x, coords.y),
125
178
  stroke: (ctx) => ctx.strokeText(text.toString(), coords.x, coords.y),
126
- }, styleOptions);
179
+ }, styleOptions, { applyText: true });
127
180
  }
128
181
  }
129
182
  export function renderRect(ctx, coords, styleOptions = {}) {
@@ -145,6 +198,19 @@ export function renderCircle(ctx, coords, styleOptions = {}) {
145
198
  }, styleOptions);
146
199
  ctx.closePath();
147
200
  }
201
+ export function renderEllipse(ctx, coords, styleOptions = {}) {
202
+ ctx.beginPath();
203
+ ctx.ellipse(coords.cx, coords.cy, coords.rx, coords.ry, 0, 0, 2 * Math.PI);
204
+ render(ctx, {
205
+ fill: (ctx) => {
206
+ ctx.fill();
207
+ },
208
+ stroke: (ctx) => {
209
+ ctx.stroke();
210
+ },
211
+ }, styleOptions);
212
+ ctx.closePath();
213
+ }
148
214
  /** Clear canvas accounting for Canvas `context.translate(...)` */
149
215
  export function clearCanvasContext(ctx, options) {
150
216
  // Clear with negative offset due to Canvas `context.translate(...)`
@@ -179,9 +245,8 @@ export function _createLinearGradient(ctx, x0, y0, x1, y1, stops) {
179
245
  return gradient;
180
246
  }
181
247
  /** Create linear gradient and memoize result to fix reactivity */
182
- export const createLinearGradient = memoize(_createLinearGradient, (ctx, x0, y0, x1, y1, stops) => {
183
- const key = JSON.stringify({ x0, y0, x1, y1, stops });
184
- return key;
248
+ export const createLinearGradient = memoize(_createLinearGradient, {
249
+ cacheKey: (args) => JSON.stringify(args.slice(1)), // Ignore `ctx` argument
185
250
  });
186
251
  export function _createPattern(ctx, width, height, shapes, background) {
187
252
  const patternCanvas = document.createElement('canvas');
@@ -214,7 +279,6 @@ export function _createPattern(ctx, width, height, shapes, background) {
214
279
  return pattern;
215
280
  }
216
281
  /** Create pattern and memoize result to fix reactivity */
217
- export const createPattern = memoize(_createPattern, (ctx, width, height, shapes, background) => {
218
- const key = JSON.stringify({ width, height, shapes, background });
219
- return key;
282
+ export const createPattern = memoize(_createPattern, {
283
+ cacheKey: (args) => JSON.stringify(args.slice(1)), // Ignore `ctx` argument
220
284
  });
@@ -75,3 +75,17 @@ export declare function getSpiral({ angle, radius, count, width, height, }: {
75
75
  x: number;
76
76
  y: number;
77
77
  }[];
78
+ interface SineWaveOptions {
79
+ numPoints: number;
80
+ frequency?: number;
81
+ amplitude?: number;
82
+ noiseLevel?: number;
83
+ phase?: number;
84
+ xMin?: number;
85
+ xMax?: number;
86
+ }
87
+ export declare function generateSineWave(options: SineWaveOptions): {
88
+ x: number;
89
+ y: number;
90
+ }[];
91
+ export {};