layerchart 2.0.0-next.4 → 2.0.0-next.40

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 (179) hide show
  1. package/dist/components/AnnotationLine.svelte +15 -2
  2. package/dist/components/AnnotationPoint.svelte +29 -11
  3. package/dist/components/AnnotationRange.svelte +18 -4
  4. package/dist/components/Arc.svelte +5 -5
  5. package/dist/components/Area.svelte +10 -2
  6. package/dist/components/Axis.svelte +175 -58
  7. package/dist/components/Axis.svelte.d.ts +23 -6
  8. package/dist/components/Bar.svelte +20 -15
  9. package/dist/components/Bar.svelte.d.ts +2 -2
  10. package/dist/components/Bars.svelte +4 -4
  11. package/dist/components/Blur.svelte +7 -6
  12. package/dist/components/Blur.svelte.d.ts +2 -5
  13. package/dist/components/BrushContext.svelte +45 -45
  14. package/dist/components/Calendar.svelte +31 -10
  15. package/dist/components/Calendar.svelte.d.ts +2 -1
  16. package/dist/components/Chart.svelte +76 -27
  17. package/dist/components/Chart.svelte.d.ts +26 -8
  18. package/dist/components/ChartClipPath.svelte +1 -1
  19. package/dist/components/Circle.svelte +44 -3
  20. package/dist/components/CircleClipPath.svelte +8 -1
  21. package/dist/components/ClipPath.svelte +1 -2
  22. package/dist/components/ColorRamp.svelte +1 -1
  23. package/dist/components/ComputedStyles.svelte +9 -2
  24. package/dist/components/Connector.svelte +3 -3
  25. package/dist/components/Connector.svelte.d.ts +1 -1
  26. package/dist/components/Ellipse.svelte +228 -0
  27. package/dist/components/Ellipse.svelte.d.ts +64 -0
  28. package/dist/components/ForceSimulation.svelte +184 -50
  29. package/dist/components/ForceSimulation.svelte.d.ts +95 -21
  30. package/dist/components/Frame.svelte +1 -1
  31. package/dist/components/GeoCircle.svelte +1 -1
  32. package/dist/components/GeoEdgeFade.svelte +1 -1
  33. package/dist/components/GeoPath.svelte +30 -8
  34. package/dist/components/GeoPoint.svelte +4 -5
  35. package/dist/components/GeoSpline.svelte +5 -5
  36. package/dist/components/GeoSpline.svelte.d.ts +1 -1
  37. package/dist/components/GeoTile.svelte +1 -1
  38. package/dist/components/Graticule.svelte +5 -5
  39. package/dist/components/Grid.svelte +60 -63
  40. package/dist/components/Group.svelte +13 -8
  41. package/dist/components/Group.svelte.d.ts +10 -3
  42. package/dist/components/Highlight.svelte +55 -28
  43. package/dist/components/Highlight.svelte.d.ts +4 -0
  44. package/dist/components/Hull.svelte +12 -5
  45. package/dist/components/Labels.svelte +24 -13
  46. package/dist/components/Labels.svelte.d.ts +12 -5
  47. package/dist/components/Legend.svelte +143 -70
  48. package/dist/components/Legend.svelte.d.ts +12 -8
  49. package/dist/components/Line.svelte +40 -3
  50. package/dist/components/LinearGradient.svelte +35 -4
  51. package/dist/components/Link.svelte +1 -1
  52. package/dist/components/Marker.svelte +37 -26
  53. package/dist/components/MonthPath.svelte +26 -12
  54. package/dist/components/MonthPath.svelte.d.ts +4 -3
  55. package/dist/components/MotionPath.svelte +1 -1
  56. package/dist/components/Pack.svelte.d.ts +10 -3
  57. package/dist/components/Partition.svelte.d.ts +10 -3
  58. package/dist/components/Pattern.svelte +5 -5
  59. package/dist/components/Pie.svelte +1 -2
  60. package/dist/components/Points.svelte +11 -72
  61. package/dist/components/Points.svelte.d.ts +1 -8
  62. package/dist/components/Polygon.svelte +309 -0
  63. package/dist/components/Polygon.svelte.d.ts +115 -0
  64. package/dist/components/RadialGradient.svelte +4 -6
  65. package/dist/components/Rect.svelte +55 -5
  66. package/dist/components/Rect.svelte.d.ts +2 -2
  67. package/dist/components/RectClipPath.svelte +4 -3
  68. package/dist/components/RectClipPath.svelte.d.ts +2 -2
  69. package/dist/components/Rule.svelte +167 -77
  70. package/dist/components/Rule.svelte.d.ts +7 -2
  71. package/dist/components/Spline.svelte +59 -28
  72. package/dist/components/Spline.svelte.d.ts +12 -4
  73. package/dist/components/Text.svelte +121 -73
  74. package/dist/components/Text.svelte.d.ts +6 -0
  75. package/dist/components/TileImage.svelte +19 -4
  76. package/dist/components/TransformContext.svelte +9 -3
  77. package/dist/components/TransformControls.svelte +89 -38
  78. package/dist/components/Tree.svelte.d.ts +10 -3
  79. package/dist/components/Treemap.svelte +63 -26
  80. package/dist/components/Treemap.svelte.d.ts +21 -14
  81. package/dist/components/Voronoi.svelte +12 -13
  82. package/dist/components/charts/ArcChart.svelte +43 -71
  83. package/dist/components/charts/ArcChart.svelte.d.ts +10 -3
  84. package/dist/components/charts/AreaChart.svelte +29 -59
  85. package/dist/components/charts/AreaChart.svelte.d.ts +10 -3
  86. package/dist/components/charts/BarChart.svelte +79 -71
  87. package/dist/components/charts/BarChart.svelte.d.ts +10 -3
  88. package/dist/components/charts/DefaultTooltip.svelte +3 -3
  89. package/dist/components/charts/DefaultTooltip.svelte.d.ts +1 -1
  90. package/dist/components/charts/LineChart.svelte +69 -75
  91. package/dist/components/charts/LineChart.svelte.d.ts +21 -8
  92. package/dist/components/charts/PieChart.svelte +44 -71
  93. package/dist/components/charts/PieChart.svelte.d.ts +10 -3
  94. package/dist/components/charts/ScatterChart.svelte +10 -39
  95. package/dist/components/charts/ScatterChart.svelte.d.ts +10 -3
  96. package/dist/components/charts/utils.svelte.d.ts +1 -19
  97. package/dist/components/charts/utils.svelte.js +7 -35
  98. package/dist/components/index.d.ts +4 -0
  99. package/dist/components/index.js +5 -1
  100. package/dist/components/layout/Canvas.svelte +96 -69
  101. package/dist/components/layout/Canvas.svelte.d.ts +6 -0
  102. package/dist/components/layout/Html.svelte +15 -9
  103. package/dist/components/layout/Layer.svelte +6 -4
  104. package/dist/components/layout/Layer.svelte.d.ts +6 -4
  105. package/dist/components/layout/Svg.svelte +19 -11
  106. package/dist/components/layout/WebGL.svelte +26 -6
  107. package/dist/components/layout/WebGL.svelte.d.ts +5 -2
  108. package/dist/components/tooltip/Tooltip.svelte +65 -27
  109. package/dist/components/tooltip/Tooltip.svelte.d.ts +10 -3
  110. package/dist/components/tooltip/TooltipContext.svelte +167 -54
  111. package/dist/components/tooltip/TooltipContext.svelte.d.ts +19 -5
  112. package/dist/components/tooltip/TooltipHeader.svelte +32 -18
  113. package/dist/components/tooltip/TooltipHeader.svelte.d.ts +3 -3
  114. package/dist/components/tooltip/TooltipItem.svelte +46 -37
  115. package/dist/components/tooltip/TooltipItem.svelte.d.ts +3 -3
  116. package/dist/components/tooltip/TooltipList.svelte +12 -10
  117. package/dist/components/tooltip/TooltipSeparator.svelte +18 -10
  118. package/dist/components/tooltip/tooltipMetaContext.d.ts +2 -2
  119. package/dist/docs/Blockquote.svelte +6 -4
  120. package/dist/docs/Blockquote.svelte.d.ts +4 -19
  121. package/dist/docs/Code.svelte +70 -28
  122. package/dist/docs/Code.svelte.d.ts +9 -24
  123. package/dist/docs/Header1.svelte +4 -2
  124. package/dist/docs/Header1.svelte.d.ts +4 -28
  125. package/dist/docs/Json.svelte +11 -3
  126. package/dist/docs/Json.svelte.d.ts +9 -21
  127. package/dist/docs/Layout.svelte +10 -7
  128. package/dist/docs/Layout.svelte.d.ts +4 -19
  129. package/dist/docs/Link.svelte +7 -3
  130. package/dist/docs/Link.svelte.d.ts +4 -38
  131. package/dist/docs/Preview.svelte +22 -23
  132. package/dist/docs/Preview.svelte.d.ts +5 -6
  133. package/dist/docs/TilesetField.svelte +20 -19
  134. package/dist/docs/TilesetField.svelte.d.ts +5 -22
  135. package/dist/docs/ViewSourceButton.svelte +10 -7
  136. package/dist/docs/ViewSourceButton.svelte.d.ts +7 -21
  137. package/dist/states/series.svelte.d.ts +30 -0
  138. package/dist/states/series.svelte.js +54 -0
  139. package/dist/styles/daisyui-5.css +6 -0
  140. package/dist/styles/shadcn-svelte.css +11 -0
  141. package/dist/styles/skeleton-3.css +15 -0
  142. package/dist/utils/arcText.svelte.js +4 -4
  143. package/dist/utils/array.d.ts +11 -0
  144. package/dist/utils/array.js +23 -0
  145. package/dist/utils/array.test.d.ts +1 -0
  146. package/dist/utils/array.test.js +200 -0
  147. package/dist/utils/attributes.d.ts +3 -13
  148. package/dist/utils/attributes.js +4 -18
  149. package/dist/utils/canvas.d.ts +77 -0
  150. package/dist/utils/canvas.js +105 -41
  151. package/dist/utils/common.d.ts +9 -0
  152. package/dist/utils/common.js +18 -1
  153. package/dist/utils/common.test.js +26 -1
  154. package/dist/utils/genData.d.ts +22 -8
  155. package/dist/utils/genData.js +34 -14
  156. package/dist/utils/graph/dagre.d.ts +4 -4
  157. package/dist/utils/graph/dagre.js +5 -7
  158. package/dist/utils/index.d.ts +1 -0
  159. package/dist/utils/index.js +1 -0
  160. package/dist/utils/math.d.ts +17 -0
  161. package/dist/utils/math.js +17 -0
  162. package/dist/utils/path.d.ts +10 -0
  163. package/dist/utils/path.js +30 -0
  164. package/dist/utils/rect.svelte.d.ts +2 -2
  165. package/dist/utils/rect.svelte.js +73 -1
  166. package/dist/utils/scales.svelte.d.ts +9 -3
  167. package/dist/utils/scales.svelte.js +47 -4
  168. package/dist/utils/shape.d.ts +43 -0
  169. package/dist/utils/shape.js +59 -0
  170. package/dist/utils/stack.js +1 -1
  171. package/dist/utils/string.d.ts +49 -0
  172. package/dist/utils/string.js +4 -2
  173. package/dist/utils/ticks.d.ts +15 -4
  174. package/dist/utils/ticks.js +140 -159
  175. package/dist/utils/ticks.test.js +16 -26
  176. package/dist/utils/treemap.d.ts +1 -1
  177. package/dist/utils/types.d.ts +15 -2
  178. package/package.json +35 -35
  179. package/dist/utils/object.js +0 -2
@@ -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
+ });
@@ -1,14 +1,4 @@
1
- /**
2
- * Creates a string containing a class name that can be used by
3
- * developers to target a specific layer/element within a LayerChart.
4
- *
5
- * This is a function so that the class names remain consistent and the
6
- * prefix/structure can be changed in the future if needed
7
- *
8
- * @param layerName - the name of the layer to be appended to the generated class name
9
- * @returns a string to be used as a class on an element
10
- */
11
- export declare function layerClass(layerName: string): string;
1
+ import type { ClassValue } from 'svelte/elements';
12
2
  type ExtractObjectType<T> = T extends object ? (T extends Function ? never : T) : never;
13
3
  type WithClass<T> = T & {
14
4
  class?: string;
@@ -21,9 +11,9 @@ type DefaultProps = WithClass<{
21
11
  * a class name to its class property to identify the layer for CSS targeting.
22
12
  *
23
13
  * @param props The props to be extracted, can be an object, function or any other type
24
- * @param layerName The name of the layer used to apply a layer classname for targeting styling
14
+ * @param className The class name to be applied to the layer for targeting styling (e.g. 'lc-layer')
25
15
  * @param extraClasses Additional classes to be applied to the layer if they don't exist in the props already
26
16
  * @returns a typed spreadable object with props for the layer
27
17
  */
28
- export declare function extractLayerProps<T>(props: T, layerName: string, extraClasses?: string): WithClass<ExtractObjectType<T> extends never ? DefaultProps : ExtractObjectType<T>>;
18
+ export declare function extractLayerProps<T>(props: T, className: string, ...extraClasses: ClassValue[]): WithClass<ExtractObjectType<T> extends never ? DefaultProps : ExtractObjectType<T>>;
29
19
  export {};
@@ -1,17 +1,4 @@
1
1
  import { cls } from '@layerstack/tailwind';
2
- /**
3
- * Creates a string containing a class name that can be used by
4
- * developers to target a specific layer/element within a LayerChart.
5
- *
6
- * This is a function so that the class names remain consistent and the
7
- * prefix/structure can be changed in the future if needed
8
- *
9
- * @param layerName - the name of the layer to be appended to the generated class name
10
- * @returns a string to be used as a class on an element
11
- */
12
- export function layerClass(layerName) {
13
- return `lc-${layerName}`;
14
- }
15
2
  // type guard to narrow props to an object with optional class
16
3
  // for extractLayerProps
17
4
  function isObjectWithClass(val) {
@@ -22,19 +9,18 @@ function isObjectWithClass(val) {
22
9
  * a class name to its class property to identify the layer for CSS targeting.
23
10
  *
24
11
  * @param props The props to be extracted, can be an object, function or any other type
25
- * @param layerName The name of the layer used to apply a layer classname for targeting styling
12
+ * @param className The class name to be applied to the layer for targeting styling (e.g. 'lc-layer')
26
13
  * @param extraClasses Additional classes to be applied to the layer if they don't exist in the props already
27
14
  * @returns a typed spreadable object with props for the layer
28
15
  */
29
- export function extractLayerProps(props, layerName, extraClasses) {
30
- const className = layerClass(layerName);
16
+ export function extractLayerProps(props, className, ...extraClasses) {
31
17
  if (isObjectWithClass(props)) {
32
18
  return {
33
19
  ...props,
34
- class: cls(className, props.class ?? '', extraClasses),
20
+ class: cls(className, ...extraClasses, props.class),
35
21
  };
36
22
  }
37
23
  return {
38
- class: cls(className, extraClasses),
24
+ class: cls(className, ...extraClasses),
39
25
  };
40
26
  }
@@ -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
  });
@@ -16,3 +16,12 @@ export declare function defaultChartPadding<TData, SeriesComponent extends Compo
16
16
  * Handles complex objects such as `Date` by invoking `.valueOf()`
17
17
  */
18
18
  export declare function findRelatedData(data: any[], original: any, accessor: Function): any;
19
+ /**
20
+ * Return the object if the value is an object, otherwise return null.
21
+ * Functions (including Snippet types) are treated as non-objects and return null.
22
+ */
23
+ export declare function getObjectOrNull<T>(value: T): T extends object ? T extends Function ? null : T : T extends null ? null : T extends undefined ? undefined : null;
24
+ /**
25
+ * Call with args if function, otherwise return the value.
26
+ */
27
+ export declare function resolveMaybeFn<T>(value: T | ((...args: any[]) => T), ...args: any[]): any;
@@ -40,7 +40,7 @@ export function defaultChartPadding(axis = true, legend = false) {
40
40
  return {
41
41
  top: axis === true || axis === 'y' ? 4 : 0,
42
42
  left: axis === true || axis === 'y' ? 20 : 0,
43
- bottom: (axis === true || axis === 'x' ? 20 : 0) + (legend === true ? 32 : 0),
43
+ bottom: (axis === true || axis === 'x' ? 20 : 0) + (legend ? 32 : 0),
44
44
  right: axis === true || axis === 'x' ? 4 : 0,
45
45
  };
46
46
  }
@@ -54,3 +54,20 @@ export function findRelatedData(data, original, accessor) {
54
54
  return accessor(d)?.valueOf() === accessor(original)?.valueOf();
55
55
  });
56
56
  }
57
+ /**
58
+ * Return the object if the value is an object, otherwise return null.
59
+ * Functions (including Snippet types) are treated as non-objects and return null.
60
+ */
61
+ export function getObjectOrNull(value) {
62
+ if (typeof value === 'object')
63
+ return value;
64
+ if (value === undefined)
65
+ return undefined;
66
+ return null;
67
+ }
68
+ /**
69
+ * Call with args if function, otherwise return the value.
70
+ */
71
+ export function resolveMaybeFn(value, ...args) {
72
+ return typeof value === 'function' ? value(...args) : value;
73
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { accessor } from './common.js';
2
+ import { accessor, resolveMaybeFn, getObjectOrNull } from './common.js';
3
3
  export const testData = {
4
4
  one: 1,
5
5
  two: 2,
@@ -36,3 +36,28 @@ describe('accessor', () => {
36
36
  expect(actual).toEqual(testData);
37
37
  });
38
38
  });
39
+ describe('getObjectOrNull', () => {
40
+ it('returns null for non-object values', () => {
41
+ expect(getObjectOrNull(5)).toBeNull();
42
+ expect(getObjectOrNull('string')).toBeNull();
43
+ expect(getObjectOrNull(null)).toBeNull();
44
+ expect(getObjectOrNull(undefined)).toBeUndefined();
45
+ });
46
+ it('returns null for functions', () => {
47
+ const fn = () => { };
48
+ expect(getObjectOrNull(fn)).toBeNull();
49
+ });
50
+ it('returns the object if value is an object', () => {
51
+ const obj = { a: 1 };
52
+ expect(getObjectOrNull(obj)).toBe(obj);
53
+ });
54
+ });
55
+ describe('resolveMaybeFn', () => {
56
+ it('returns value if not a function', () => {
57
+ expect(resolveMaybeFn(5)).toBe(5);
58
+ });
59
+ it('calls function with args', () => {
60
+ const fn = (a, b) => a + b;
61
+ expect(resolveMaybeFn(fn, 2, 3)).toBe(5);
62
+ });
63
+ });