@rokkit/chart 1.0.0-next.15 → 1.0.0-next.150

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 (223) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +150 -46
  3. package/dist/Plot/index.d.ts +9 -0
  4. package/dist/PlotState.svelte.d.ts +47 -0
  5. package/dist/crossfilter/createCrossFilter.svelte.d.ts +15 -0
  6. package/dist/elements/index.d.ts +6 -0
  7. package/dist/geoms/lib/areas.d.ts +52 -0
  8. package/dist/geoms/lib/bars.d.ts +3 -0
  9. package/dist/index.d.ts +51 -0
  10. package/dist/lib/brewer.d.ts +9 -0
  11. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +10 -0
  12. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +8 -0
  13. package/dist/lib/brewing/PieBrewer.svelte.d.ts +8 -0
  14. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +9 -0
  15. package/dist/lib/brewing/axes.svelte.d.ts +66 -0
  16. package/dist/lib/brewing/bars.svelte.d.ts +56 -0
  17. package/dist/lib/brewing/brewer.svelte.d.ts +145 -0
  18. package/dist/lib/brewing/colors.d.ts +17 -0
  19. package/dist/lib/brewing/dimensions.svelte.d.ts +35 -0
  20. package/dist/lib/brewing/index.svelte.d.ts +118 -0
  21. package/dist/lib/brewing/legends.svelte.d.ts +48 -0
  22. package/dist/lib/brewing/marks/arcs.d.ts +17 -0
  23. package/dist/lib/brewing/marks/areas.d.ts +31 -0
  24. package/dist/lib/brewing/marks/bars.d.ts +1 -0
  25. package/dist/lib/brewing/marks/boxes.d.ts +24 -0
  26. package/dist/lib/brewing/marks/lines.d.ts +24 -0
  27. package/dist/lib/brewing/marks/points.d.ts +40 -0
  28. package/dist/lib/brewing/marks/violins.d.ts +20 -0
  29. package/dist/lib/brewing/patterns.d.ts +14 -0
  30. package/dist/lib/brewing/scales.d.ts +28 -0
  31. package/dist/lib/brewing/scales.svelte.d.ts +24 -0
  32. package/dist/lib/brewing/stats.d.ts +31 -0
  33. package/dist/lib/brewing/symbols.d.ts +7 -0
  34. package/dist/lib/brewing/types.d.ts +162 -0
  35. package/dist/lib/chart.d.ts +40 -0
  36. package/dist/lib/context.d.ts +13 -0
  37. package/dist/lib/grid.d.ts +72 -0
  38. package/dist/lib/plot/chartProps.d.ts +177 -0
  39. package/dist/lib/plot/crossfilter.d.ts +13 -0
  40. package/dist/lib/plot/facet.d.ts +24 -0
  41. package/dist/lib/plot/frames.d.ts +47 -0
  42. package/dist/lib/plot/helpers.d.ts +3 -0
  43. package/dist/lib/plot/preset.d.ts +29 -0
  44. package/dist/lib/plot/scales.d.ts +5 -0
  45. package/dist/lib/plot/stat.d.ts +32 -0
  46. package/dist/lib/plot/types.d.ts +89 -0
  47. package/dist/lib/scales.svelte.d.ts +35 -0
  48. package/dist/lib/swatch.d.ts +12 -0
  49. package/dist/lib/ticks.d.ts +36 -0
  50. package/dist/lib/utils.d.ts +61 -0
  51. package/dist/lib/xscale.d.ts +11 -0
  52. package/dist/patterns/index.d.ts +4 -0
  53. package/dist/patterns/patterns.d.ts +72 -0
  54. package/dist/patterns/scale.d.ts +30 -0
  55. package/dist/symbols/constants/index.d.ts +1 -0
  56. package/dist/symbols/index.d.ts +5 -0
  57. package/package.json +41 -45
  58. package/src/AnimatedPlot.svelte +214 -0
  59. package/src/Chart.svelte +101 -0
  60. package/src/FacetPlot/Panel.svelte +23 -0
  61. package/src/FacetPlot.svelte +90 -0
  62. package/src/Plot/Arc.svelte +29 -0
  63. package/src/Plot/Area.svelte +25 -0
  64. package/src/Plot/Axis.svelte +73 -0
  65. package/src/Plot/Bar.svelte +96 -0
  66. package/src/Plot/Grid.svelte +30 -0
  67. package/src/Plot/Legend.svelte +167 -0
  68. package/src/Plot/Line.svelte +27 -0
  69. package/src/Plot/Point.svelte +27 -0
  70. package/src/Plot/Root.svelte +107 -0
  71. package/src/Plot/Timeline.svelte +95 -0
  72. package/src/Plot/Tooltip.svelte +81 -0
  73. package/src/Plot/index.js +9 -0
  74. package/src/Plot.svelte +189 -0
  75. package/src/PlotState.svelte.js +278 -0
  76. package/src/Sparkline.svelte +69 -0
  77. package/src/Symbol.svelte +21 -0
  78. package/src/Texture.svelte +18 -0
  79. package/src/charts/AreaChart.svelte +25 -0
  80. package/src/charts/BarChart.svelte +26 -0
  81. package/src/charts/BoxPlot.svelte +21 -0
  82. package/src/charts/BubbleChart.svelte +23 -0
  83. package/src/charts/LineChart.svelte +26 -0
  84. package/src/charts/PieChart.svelte +25 -0
  85. package/src/charts/ScatterPlot.svelte +25 -0
  86. package/src/charts/ViolinPlot.svelte +21 -0
  87. package/src/crossfilter/CrossFilter.svelte +38 -0
  88. package/src/crossfilter/FilterBar.svelte +32 -0
  89. package/src/crossfilter/FilterSlider.svelte +79 -0
  90. package/src/crossfilter/createCrossFilter.svelte.js +113 -0
  91. package/src/elements/Bar.svelte +22 -24
  92. package/src/elements/ColorRamp.svelte +20 -22
  93. package/src/elements/ContinuousLegend.svelte +20 -17
  94. package/src/elements/DefinePatterns.svelte +24 -0
  95. package/src/elements/DiscreteLegend.svelte +15 -15
  96. package/src/elements/Label.svelte +4 -8
  97. package/src/elements/SymbolGrid.svelte +22 -0
  98. package/src/elements/index.js +6 -0
  99. package/src/examples/BarChartExample.svelte +81 -0
  100. package/src/geoms/Arc.svelte +81 -0
  101. package/src/geoms/Area.svelte +50 -0
  102. package/src/geoms/Bar.svelte +142 -0
  103. package/src/geoms/Box.svelte +101 -0
  104. package/src/geoms/LabelPill.svelte +17 -0
  105. package/src/geoms/Line.svelte +100 -0
  106. package/src/geoms/Point.svelte +100 -0
  107. package/src/geoms/Violin.svelte +44 -0
  108. package/src/geoms/lib/areas.js +131 -0
  109. package/src/geoms/lib/bars.js +172 -0
  110. package/src/index.js +67 -16
  111. package/src/lib/brewer.js +25 -0
  112. package/src/lib/brewing/BoxBrewer.svelte.js +56 -0
  113. package/src/lib/brewing/CartesianBrewer.svelte.js +16 -0
  114. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  115. package/src/lib/brewing/ViolinBrewer.svelte.js +55 -0
  116. package/src/lib/brewing/axes.svelte.js +270 -0
  117. package/src/lib/brewing/bars.svelte.js +201 -0
  118. package/src/lib/brewing/brewer.svelte.js +229 -0
  119. package/src/lib/brewing/colors.js +22 -0
  120. package/src/lib/brewing/dimensions.svelte.js +56 -0
  121. package/src/lib/brewing/index.svelte.js +205 -0
  122. package/src/lib/brewing/legends.svelte.js +137 -0
  123. package/src/lib/brewing/marks/arcs.js +43 -0
  124. package/src/lib/brewing/marks/areas.js +59 -0
  125. package/src/lib/brewing/marks/bars.js +49 -0
  126. package/src/lib/brewing/marks/boxes.js +75 -0
  127. package/src/lib/brewing/marks/lines.js +48 -0
  128. package/src/lib/brewing/marks/points.js +57 -0
  129. package/src/lib/brewing/marks/violins.js +90 -0
  130. package/src/lib/brewing/patterns.js +31 -0
  131. package/src/lib/brewing/scales.js +51 -0
  132. package/src/lib/brewing/scales.svelte.js +82 -0
  133. package/src/lib/brewing/stats.js +62 -0
  134. package/src/lib/brewing/symbols.js +10 -0
  135. package/src/lib/brewing/types.js +73 -0
  136. package/src/lib/chart.js +213 -0
  137. package/src/lib/context.js +131 -0
  138. package/src/lib/grid.js +85 -0
  139. package/src/lib/plot/chartProps.js +76 -0
  140. package/src/lib/plot/crossfilter.js +16 -0
  141. package/src/lib/plot/facet.js +58 -0
  142. package/src/lib/plot/frames.js +80 -0
  143. package/src/lib/plot/helpers.js +14 -0
  144. package/src/lib/plot/preset.js +53 -0
  145. package/src/lib/plot/scales.js +56 -0
  146. package/src/lib/plot/stat.js +92 -0
  147. package/src/lib/plot/types.js +65 -0
  148. package/src/lib/scales.svelte.js +151 -0
  149. package/src/lib/swatch.js +13 -0
  150. package/src/lib/ticks.js +46 -0
  151. package/src/lib/utils.js +111 -118
  152. package/src/lib/xscale.js +31 -0
  153. package/src/patterns/DefinePatterns.svelte +32 -0
  154. package/src/patterns/PatternDef.svelte +27 -0
  155. package/src/patterns/README.md +3 -0
  156. package/src/patterns/index.js +4 -0
  157. package/src/patterns/patterns.js +208 -0
  158. package/src/patterns/scale.js +87 -0
  159. package/src/spec/chart-spec.js +29 -0
  160. package/src/symbols/RoundedSquare.svelte +33 -0
  161. package/src/symbols/Shape.svelte +37 -0
  162. package/src/symbols/constants/index.js +4 -0
  163. package/src/symbols/index.js +9 -0
  164. package/src/symbols/outline.svelte +60 -0
  165. package/src/symbols/solid.svelte +60 -0
  166. package/src/chart/FacetGrid.svelte +0 -51
  167. package/src/chart/Grid.svelte +0 -34
  168. package/src/chart/Legend.svelte +0 -16
  169. package/src/chart/PatternDefs.svelte +0 -13
  170. package/src/chart/Swatch.svelte +0 -93
  171. package/src/chart/SwatchButton.svelte +0 -29
  172. package/src/chart/SwatchGrid.svelte +0 -55
  173. package/src/chart/Symbol.svelte +0 -37
  174. package/src/chart/Texture.svelte +0 -16
  175. package/src/chart/TexturedShape.svelte +0 -27
  176. package/src/chart/TimelapseChart.svelte +0 -97
  177. package/src/chart/Timer.svelte +0 -27
  178. package/src/chart.js +0 -9
  179. package/src/components/charts/Axis.svelte +0 -66
  180. package/src/components/charts/Chart.svelte +0 -35
  181. package/src/components/index.js +0 -23
  182. package/src/components/lib/axis.js +0 -0
  183. package/src/components/lib/chart.js +0 -187
  184. package/src/components/lib/color.js +0 -327
  185. package/src/components/lib/funnel.js +0 -204
  186. package/src/components/lib/index.js +0 -19
  187. package/src/components/lib/pattern.js +0 -190
  188. package/src/components/lib/rollup.js +0 -55
  189. package/src/components/lib/shape.js +0 -199
  190. package/src/components/lib/summary.js +0 -145
  191. package/src/components/lib/theme.js +0 -23
  192. package/src/components/lib/timer.js +0 -41
  193. package/src/components/lib/utils.js +0 -165
  194. package/src/components/plots/BarPlot.svelte +0 -36
  195. package/src/components/plots/BoxPlot.svelte +0 -54
  196. package/src/components/plots/ScatterPlot.svelte +0 -30
  197. package/src/components/store.js +0 -70
  198. package/src/constants.js +0 -66
  199. package/src/elements/PatternDefs.svelte +0 -13
  200. package/src/elements/PatternMask.svelte +0 -20
  201. package/src/elements/Symbol.svelte +0 -38
  202. package/src/elements/Tooltip.svelte +0 -23
  203. package/src/funnel.svelte +0 -35
  204. package/src/geom.js +0 -105
  205. package/src/lib/axis.js +0 -75
  206. package/src/lib/colors.js +0 -32
  207. package/src/lib/geom.js +0 -4
  208. package/src/lib/shapes.js +0 -144
  209. package/src/lib/timer.js +0 -44
  210. package/src/lookup.js +0 -29
  211. package/src/plots/BarPlot.svelte +0 -55
  212. package/src/plots/BoxPlot.svelte +0 -0
  213. package/src/plots/FunnelPlot.svelte +0 -33
  214. package/src/plots/HeatMap.svelte +0 -5
  215. package/src/plots/HeatMapCalendar.svelte +0 -129
  216. package/src/plots/LinePlot.svelte +0 -55
  217. package/src/plots/Plot.svelte +0 -25
  218. package/src/plots/RankBarPlot.svelte +0 -38
  219. package/src/plots/ScatterPlot.svelte +0 -20
  220. package/src/plots/ViolinPlot.svelte +0 -11
  221. package/src/plots/heatmap.js +0 -70
  222. package/src/plots/index.js +0 -10
  223. package/src/swatch.js +0 -11
@@ -0,0 +1,270 @@
1
+ // import { scaleBand } from 'd3-scale'
2
+
3
+ import {} from './types.js'
4
+
5
+ /**
6
+ * @typedef {import('./types').TickData} TickData
7
+ * @typedef {import('./types').AxisData} AxisData
8
+ * @typedef {import('./types').ChartScales} ChartScales
9
+ * @typedef {import('./types').ChartDimensions} ChartDimensions
10
+ */
11
+
12
+ const IDENTITY = (v) => v
13
+
14
+ /**
15
+ * @param {Object} xScale
16
+ * @param {number|null} tickCount
17
+ * @returns {Array}
18
+ */
19
+ function xTicksFromContinuous(xScale, tickCount) {
20
+ return tickCount !== null ? xScale.ticks(tickCount) : xScale.ticks()
21
+ }
22
+
23
+ /**
24
+ * @param {Array} domain
25
+ * @param {number} tickCount
26
+ * @returns {Array}
27
+ */
28
+ function downsampleDomain(domain, tickCount) {
29
+ const step = Math.max(1, Math.floor(domain.length / tickCount))
30
+ return domain.filter((_, i) => i % step === 0)
31
+ }
32
+
33
+ /**
34
+ * @param {Object} xScale
35
+ * @param {number|null} tickCount
36
+ * @returns {Array}
37
+ */
38
+ function xTicksFromBand(xScale, tickCount) {
39
+ const domain = xScale.domain()
40
+ if (tickCount === null || tickCount >= domain.length) return domain
41
+ return downsampleDomain(domain, tickCount)
42
+ }
43
+
44
+ /**
45
+ * @param {Object} xScale
46
+ * @param {number|null} tickCount
47
+ * @returns {Array}
48
+ */
49
+ function resolveXTicks(xScale, tickCount) {
50
+ return xScale.ticks ? xTicksFromContinuous(xScale, tickCount) : xTicksFromBand(xScale, tickCount)
51
+ }
52
+
53
+ /**
54
+ * @param {Object} xScale
55
+ * @param {unknown} value
56
+ * @returns {number}
57
+ */
58
+ function xPosition(xScale, value) {
59
+ return xScale.bandwidth ? xScale(value) + xScale.bandwidth() / 2 : xScale(value)
60
+ }
61
+
62
+ /**
63
+ * @param {Object} scale
64
+ * @param {Array} values
65
+ * @param {Function} formatter
66
+ * @param {(scale: Object, v: unknown) => number} getPosition
67
+ * @returns {Array}
68
+ */
69
+ function mapTicks(scale, values, formatter, getPosition) {
70
+ return values.map((value) => ({
71
+ value,
72
+ position: getPosition(scale, value),
73
+ formattedValue: formatter(value)
74
+ }))
75
+ }
76
+
77
+ /**
78
+ * @param {Object} options
79
+ * @returns {{ tickCount: number|null, formatter: Function, label: string }}
80
+ */
81
+ function parseAxisOptions(options) {
82
+ const opts = options || {}
83
+ return {
84
+ tickCount: opts.tickCount !== undefined ? opts.tickCount : null,
85
+ formatter: opts.tickFormat ? opts.tickFormat : IDENTITY,
86
+ label: opts.label ? opts.label : ''
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Creates x-axis tick data for rendering
92
+ *
93
+ * @param {Object} scales - Chart scales
94
+ * @param {Function} scales.x - X-axis scale
95
+ * @param {Object} dimensions - Chart dimensions
96
+ * @param {Object} options - Axis options
97
+ * @param {number} [options.tickCount] - Number of ticks to show
98
+ * @param {Function} [options.tickFormat] - Tick formatting function
99
+ * @param {string} [options.label] - Axis label
100
+ * @returns {AxisData} Axis rendering data
101
+ */
102
+ export function createXAxis(scales, dimensions, options) {
103
+ if (!scales.x) return { ticks: [] }
104
+ const { tickCount, formatter, label } = parseAxisOptions(options)
105
+ const ticks = mapTicks(scales.x, resolveXTicks(scales.x, tickCount), formatter, xPosition)
106
+ return {
107
+ ticks,
108
+ label,
109
+ transform: `translate(0, ${dimensions.innerHeight})`,
110
+ labelTransform: `translate(${dimensions.innerWidth / 2}, 35)`
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @param {Object} yScale
116
+ * @param {number|null} tickCount
117
+ * @returns {Array}
118
+ */
119
+ function yTicksFromDomain(yScale, tickCount) {
120
+ const domain = yScale.domain()
121
+ if (tickCount === null || domain.length <= tickCount) return domain
122
+ return downsampleDomain(domain, tickCount)
123
+ }
124
+
125
+ /**
126
+ * @param {Object} yScale
127
+ * @param {number|null} tickCount
128
+ * @returns {Array}
129
+ */
130
+ function resolveYTicks(yScale, tickCount) {
131
+ if (yScale.ticks) return tickCount !== null ? yScale.ticks(tickCount) : yScale.ticks()
132
+ if (yScale.domain) return yTicksFromDomain(yScale, tickCount)
133
+ return []
134
+ }
135
+
136
+ /**
137
+ * @param {Object} scale
138
+ * @param {unknown} value
139
+ * @returns {unknown}
140
+ */
141
+ function yPositionPassthrough(scale, value) {
142
+ return scale(value)
143
+ }
144
+
145
+ /**
146
+ * Creates y-axis tick data for rendering
147
+ *
148
+ * @param {Object} scales - Chart scales
149
+ * @param {Function} scales.y - Y-axis scale
150
+ * @param {Object} dimensions - Chart dimensions
151
+ * @param {Object} options - Axis options
152
+ * @param {number} [options.tickCount] - Number of ticks to show
153
+ * @param {Function} [options.tickFormat] - Tick formatting function
154
+ * @param {string} [options.label] - Axis label
155
+ * @returns {AxisData} Axis rendering data
156
+ */
157
+ export function createYAxis(scales, dimensions, options) {
158
+ if (!scales.y) return { ticks: [] }
159
+ const { tickCount, formatter, label } = parseAxisOptions(options)
160
+ const ticks = mapTicks(
161
+ scales.y,
162
+ resolveYTicks(scales.y, tickCount),
163
+ formatter,
164
+ yPositionPassthrough
165
+ )
166
+ return {
167
+ ticks,
168
+ label,
169
+ transform: 'translate(0, 0)',
170
+ labelTransform: `translate(-40, ${dimensions.innerHeight / 2}) rotate(-90)`
171
+ }
172
+ }
173
+
174
+ /**
175
+ * @param {Object} scales
176
+ * @param {Object} dimensions
177
+ * @param {number|null} xTickCount
178
+ * @returns {Array}
179
+ */
180
+ function buildXLines(scales, dimensions, xTickCount) {
181
+ return createXAxis(scales, dimensions, { tickCount: xTickCount }).ticks.map((tick) => ({
182
+ x1: tick.position,
183
+ y1: 0,
184
+ x2: tick.position,
185
+ y2: dimensions.innerHeight
186
+ }))
187
+ }
188
+
189
+ /**
190
+ * @param {Object} scales
191
+ * @param {Object} dimensions
192
+ * @param {number|null} yTickCount
193
+ * @returns {Array}
194
+ */
195
+ function buildYLines(scales, dimensions, yTickCount) {
196
+ return createYAxis(scales, dimensions, { tickCount: yTickCount }).ticks.map((tick) => ({
197
+ x1: 0,
198
+ y1: tick.position,
199
+ x2: dimensions.innerWidth,
200
+ y2: tick.position
201
+ }))
202
+ }
203
+
204
+ /**
205
+ * @param {Object} opts
206
+ * @returns {{ direction: string, xTickCount: number|null, yTickCount: number|null }}
207
+ */
208
+ function parseGridOptions(opts) {
209
+ const o = opts || {}
210
+ return {
211
+ direction: o.direction !== undefined ? o.direction : 'both',
212
+ xTickCount: o.xTickCount !== undefined ? o.xTickCount : null,
213
+ yTickCount: o.yTickCount !== undefined ? o.yTickCount : null
214
+ }
215
+ }
216
+
217
+ /**
218
+ * @param {string} direction
219
+ * @returns {{ showX: boolean, showY: boolean }}
220
+ */
221
+ function gridDirections(direction) {
222
+ return {
223
+ showX: direction === 'x' || direction === 'both',
224
+ showY: direction === 'y' || direction === 'both'
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Creates grid line data for rendering
230
+ *
231
+ * @param {Object} scales - Chart scales
232
+ * @param {Object} dimensions - Chart dimensions
233
+ * @param {Object} options - Grid options
234
+ * @param {string} [options.direction='both'] - Grid direction ('x', 'y', or 'both')
235
+ * @param {number} [options.xTickCount] - Number of x-axis ticks
236
+ * @param {number} [options.yTickCount] - Number of y-axis ticks
237
+ * @returns {Object} Grid rendering data
238
+ */
239
+ export function createGrid(scales, dimensions, options) {
240
+ const { direction, xTickCount, yTickCount } = parseGridOptions(options)
241
+ const { showX, showY } = gridDirections(direction)
242
+ return {
243
+ xLines: showX && scales.x ? buildXLines(scales, dimensions, xTickCount) : [],
244
+ yLines: showY && scales.y ? buildYLines(scales, dimensions, yTickCount) : []
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Creates DOM attributes for a tick element
250
+ *
251
+ * @param {TickData} tick - Tick data
252
+ * @param {string} axis - Axis type ('x' or 'y')
253
+ * @returns {Object} Attributes for the tick
254
+ */
255
+ export function createTickAttributes(tick, axis) {
256
+ if (axis === 'x') {
257
+ return {
258
+ 'data-plot-tick': 'major',
259
+ transform: `translate(${tick.position}, 0)`,
260
+ 'text-anchor': 'middle'
261
+ }
262
+ } else {
263
+ return {
264
+ 'data-plot-tick': 'major',
265
+ transform: `translate(0, ${tick.position})`,
266
+ 'text-anchor': 'end',
267
+ 'dominant-baseline': 'middle'
268
+ }
269
+ }
270
+ }
@@ -0,0 +1,201 @@
1
+ import { SvelteSet } from 'svelte/reactivity'
2
+ import {} from './types.js'
3
+
4
+ /**
5
+ * @typedef {import('./types').BarData} BarData
6
+ * @typedef {import('./types').ScaleFields} ScaleFields
7
+ * @typedef {import('./types').ChartScales} ChartScales
8
+ * @typedef {import('./types').ChartDimensions} ChartDimensions
9
+ */
10
+
11
+ const DEFAULT_COLOR = '#4682b4'
12
+
13
+ /**
14
+ * @param {Object} d - data item
15
+ * @param {{ fields: Object, scales: Object, dimensions: Object, defaultColor: string }} ctx
16
+ * @returns {BarData}
17
+ */
18
+ function buildBar(d, ctx) {
19
+ const { fields, scales, dimensions, defaultColor } = ctx
20
+ const { x: xField, y: yField, color: colorField } = fields
21
+ const barWidth = scales.x.bandwidth ? scales.x.bandwidth() : 10
22
+ const barX = scales.x.bandwidth ? scales.x(d[xField]) : scales.x(d[xField]) - barWidth / 2
23
+ return {
24
+ data: d,
25
+ x: barX,
26
+ y: scales.y(d[yField]),
27
+ width: barWidth,
28
+ height: dimensions.innerHeight - scales.y(d[yField]),
29
+ color: colorField && scales.color ? scales.color(d[colorField]) : defaultColor
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @param {Object} options
35
+ * @param {Object} fields
36
+ * @param {Object} scales
37
+ * @returns {{ ctx: Object }|null} null if input is invalid
38
+ */
39
+ function parseBarsInput(options, fields, scales) {
40
+ if (!scales.x || !scales.y) return null
41
+ const opts = options || {}
42
+ return {
43
+ fields,
44
+ scales,
45
+ dimensions: opts.dimensions,
46
+ defaultColor: opts.defaultColor !== undefined ? opts.defaultColor : DEFAULT_COLOR
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Creates bar data for rendering
52
+ *
53
+ * @param {Array} data - Chart data
54
+ * @param {Object} fields - Field mappings
55
+ * @param {string} fields.x - X-axis field
56
+ * @param {string} fields.y - Y-axis field
57
+ * @param {string} fields.color - Color field (optional)
58
+ * @param {Object} scales - Chart scales
59
+ * @param {Function} scales.x - X-axis scale
60
+ * @param {Function} scales.y - Y-axis scale
61
+ * @param {Function} scales.color - Color scale
62
+ * @param {Object} options - Options including dimensions and defaultColor
63
+ * @param {Object} options.dimensions - Chart dimensions
64
+ * @param {string} [options.defaultColor='#4682b4'] - Default color if no color scale
65
+ * @returns {BarData[]} Bar data for rendering
66
+ */
67
+ export function createBars(data, fields, scales, options) {
68
+ if (!data || !data.length) return []
69
+ const ctx = parseBarsInput(options, fields, scales)
70
+ if (!ctx) return []
71
+ return data.map((d) => buildBar(d, ctx))
72
+ }
73
+
74
+ /**
75
+ * Filter bars based on a selection criteria
76
+ *
77
+ * @param {BarData[]} bars - Bar data array
78
+ * @param {Object} selection - Selection criteria
79
+ * @returns {BarData[]} Filtered bars
80
+ */
81
+ export function filterBars(bars, selection) {
82
+ if (!selection) return bars
83
+
84
+ return bars.filter((bar) => {
85
+ for (const [key, value] of Object.entries(selection)) {
86
+ if (bar.data[key] !== value) return false
87
+ }
88
+ return true
89
+ })
90
+ }
91
+
92
+ /**
93
+ * @param {Object} item - data item
94
+ * @param {Object} scales - chart scales
95
+ * @param {{ yField: string, colorField: string, group: unknown, barX: number, barWidth: number, dimensions: Object }} ctx
96
+ * @returns {BarData}
97
+ */
98
+ function buildGroupBar(item, scales, ctx) {
99
+ const { yField, colorField, group, barX, barWidth, dimensions } = ctx
100
+ return {
101
+ data: item,
102
+ group,
103
+ x: barX,
104
+ y: scales.y(item[yField]),
105
+ width: barWidth,
106
+ height: dimensions.innerHeight - scales.y(item[yField]),
107
+ color: scales.color ? scales.color(item[colorField]) : DEFAULT_COLOR
108
+ }
109
+ }
110
+
111
+ /**
112
+ * @param {Object} scales
113
+ * @param {{ groupItems: Array, groupField: string, group: unknown, i: number, yField: string, colorField: string, barWidth: number, padding: number, dimensions: Object, xPos: number }} ctx
114
+ * @returns {BarData|null}
115
+ */
116
+ function buildOneGroupBar(scales, ctx) {
117
+ const {
118
+ groupItems,
119
+ groupField,
120
+ group,
121
+ i,
122
+ yField,
123
+ colorField,
124
+ barWidth,
125
+ padding,
126
+ dimensions,
127
+ xPos
128
+ } = ctx
129
+ const item = groupItems.find((d) => d[groupField] === group)
130
+ if (!item) return null
131
+ const barX = xPos + i * (barWidth + padding)
132
+ return buildGroupBar(item, scales, { yField, colorField, group, barX, barWidth, dimensions })
133
+ }
134
+
135
+ /**
136
+ * @param {Object} fields
137
+ * @param {Object} options
138
+ * @returns {{ xField: string, yField: string, groupField: string, colorField: string, dimensions: Object, padding: number }}
139
+ */
140
+ function parseGroupedBarsConfig(fields, options) {
141
+ const opts = options || {}
142
+ return {
143
+ xField: fields.x,
144
+ yField: fields.y,
145
+ groupField: fields.group,
146
+ colorField: fields.color !== undefined ? fields.color : fields.group,
147
+ dimensions: opts.dimensions,
148
+ padding: opts.padding !== undefined ? opts.padding : 0.1
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Creates a grouped bars layout
154
+ *
155
+ * @param {Array} data - Chart data
156
+ * @param {Object} fields - Field mappings
157
+ * @param {Object} scales - Chart scales
158
+ * @param {Object} options - Options including dimensions and padding
159
+ * @param {Object} options.dimensions - Chart dimensions
160
+ * @param {number} [options.padding=0.1] - Padding between bars in a group
161
+ * @returns {Object} Grouped bar data
162
+ */
163
+ export function createGroupedBars(data, fields, scales, options) {
164
+ if (!data || !data.length || !fields.group) return { groups: [], bars: [] }
165
+
166
+ const { xField, yField, groupField, colorField, dimensions, padding } = parseGroupedBarsConfig(
167
+ fields,
168
+ options
169
+ )
170
+
171
+ const groups = [...new SvelteSet(data.map((d) => d[groupField]))]
172
+ const xValues = [...new SvelteSet(data.map((d) => d[xField]))]
173
+
174
+ const xScale = scales.x
175
+ const groupWidth = xScale.bandwidth ? xScale.bandwidth() : 20
176
+ const barWidth = (groupWidth - padding * (groups.length - 1)) / groups.length
177
+
178
+ const bars = []
179
+ xValues.forEach((xValue) => {
180
+ const groupItems = data.filter((d) => d[xField] === xValue)
181
+ const xPos = xScale(xValue)
182
+ groups.forEach((group, i) => {
183
+ const ctx = {
184
+ groupItems,
185
+ groupField,
186
+ group,
187
+ i,
188
+ yField,
189
+ colorField,
190
+ barWidth,
191
+ padding,
192
+ dimensions,
193
+ xPos
194
+ }
195
+ const bar = buildOneGroupBar(scales, ctx)
196
+ if (bar) bars.push(bar)
197
+ })
198
+ })
199
+
200
+ return { groups, bars }
201
+ }
@@ -0,0 +1,229 @@
1
+ import { distinct, assignColors } from './colors.js'
2
+ import { assignPatterns, toPatternId, PATTERN_ORDER } from './patterns.js'
3
+ import { assignSymbols } from './symbols.js'
4
+ import { buildXScale, buildYScale, buildSizeScale } from './scales.js'
5
+ import { buildBars } from './marks/bars.js'
6
+ import { buildLines } from './marks/lines.js'
7
+ import { buildAreas } from './marks/areas.js'
8
+ import { buildArcs } from './marks/arcs.js'
9
+ import { buildPoints } from './marks/points.js'
10
+
11
+ const DEFAULT_MARGIN = { top: 20, right: 20, bottom: 40, left: 50 }
12
+
13
+ /**
14
+ * Groups aesthetic channel mappings by field name, merging aesthetics that
15
+ * share the same field into one legend section.
16
+ *
17
+ * @param {{ fill?: string, color?: string, pattern?: string, symbol?: string }} channels
18
+ * `fill` takes precedence over `color` for polygon charts (bars, areas, pie slices).
19
+ * @param {Map<unknown, {fill:string, stroke:string}>} colorMap
20
+ * @param {Map<unknown, string>} patternMap
21
+ * @param {Map<unknown, string>} symbolMap
22
+ * @returns {{ field: string, items: { label: string, fill: string|null, stroke: string|null, patternId: string|null, shape: string|null }[] }[]}
23
+ */
24
+ export function buildLegendGroups(channels, colorMap, patternMap, symbolMap) {
25
+ const cf = channels.fill ?? channels.color
26
+ const { pattern: pf, symbol: sf } = channels
27
+ const byField = new Map()
28
+
29
+ if (cf) {
30
+ byField.set(cf, { aesthetics: ['color'], keys: [...colorMap.keys()] })
31
+ }
32
+ if (pf) {
33
+ if (byField.has(pf)) {
34
+ byField.get(pf).aesthetics.push('pattern')
35
+ } else {
36
+ byField.set(pf, { aesthetics: ['pattern'], keys: [...patternMap.keys()] })
37
+ }
38
+ }
39
+ if (sf) {
40
+ if (byField.has(sf)) {
41
+ byField.get(sf).aesthetics.push('symbol')
42
+ } else {
43
+ byField.set(sf, { aesthetics: ['symbol'], keys: [...symbolMap.keys()] })
44
+ }
45
+ }
46
+
47
+ return [...byField.entries()].map(([field, { aesthetics, keys }]) => ({
48
+ field,
49
+ items: keys.filter((k) => k !== null && k !== undefined).map((key) => ({
50
+ label: String(key),
51
+ fill: aesthetics.includes('color') ? (colorMap.get(key)?.fill ?? null) : null,
52
+ stroke: aesthetics.includes('color') ? (colorMap.get(key)?.stroke ?? null) : null,
53
+ patternId:
54
+ aesthetics.includes('pattern') && patternMap.has(key) ? toPatternId(key) : null,
55
+ shape: aesthetics.includes('symbol') ? (symbolMap.get(key) ?? 'circle') : null
56
+ }))
57
+ })).filter((group) => group.items.length > 0)
58
+ }
59
+
60
+ export class ChartBrewer {
61
+ #rawData = $state([])
62
+ #channels = $state({})
63
+ #width = $state(600)
64
+ #height = $state(400)
65
+ #mode = $state('light')
66
+ #margin = $state(DEFAULT_MARGIN)
67
+ #layers = $state([])
68
+ #curve = $state(/** @type {'linear'|'smooth'|'step'|undefined} */(undefined))
69
+ #stat = $state('identity')
70
+
71
+ /**
72
+ * Override in subclasses to apply stat aggregation.
73
+ * @param {Object[]} data
74
+ * @param {Object} channels
75
+ * @param {string|Function} stat
76
+ * @returns {Object[]}
77
+ */
78
+ transform(data, _channels, _stat) {
79
+ return data
80
+ }
81
+
82
+ /** Aggregated data — all derived marks read this, not #rawData */
83
+ processedData = $derived(this.transform(this.#rawData, this.#channels, this.#stat))
84
+
85
+ /** Exposes channels to subclasses for use in their own $derived properties */
86
+ get channels() { return this.#channels }
87
+
88
+ // Maps are built from rawData so the legend always reflects the full set of
89
+ // original values — independent of whichever stat aggregation is applied.
90
+ // e.g. pattern=quarter with stat=sum still shows all 8 quarters in the legend.
91
+
92
+ /** @type {Map<unknown, {fill:string,stroke:string}>} */
93
+ colorMap = $derived(
94
+ (this.#channels.fill ?? this.#channels.color)
95
+ ? assignColors(distinct(this.#rawData, this.#channels.fill ?? this.#channels.color), this.#mode)
96
+ : new Map()
97
+ )
98
+
99
+ /** @type {Map<unknown, string>} */
100
+ patternMap = $derived(
101
+ this.#channels.pattern
102
+ ? assignPatterns(distinct(this.#rawData, this.#channels.pattern))
103
+ : new Map()
104
+ )
105
+
106
+ /**
107
+ * Unified pattern defs for ChartPatternDefs.
108
+ * When fill and pattern map the same field, pattern key = color key (simple case).
109
+ * When they differ, each unique (fillKey, patternKey) pair gets its own pattern def
110
+ * so bars/areas can have distinct colors per region AND distinct textures per category.
111
+ * @type {Array<{ id: string, name: string, fill: string, stroke: string }>}
112
+ */
113
+ patternDefs = $derived((() => {
114
+ const pf = this.#channels.pattern
115
+ const ff = this.#channels.fill ?? this.#channels.color
116
+ if (!pf || this.patternMap.size === 0) return []
117
+ if (!ff || pf === ff) {
118
+ // Same field: pattern key = fill key — simple 1:1 lookup
119
+ return Array.from(this.patternMap.entries()).map(([key, name]) => {
120
+ const color = this.colorMap.get(key) ?? { fill: '#ddd', stroke: '#666' }
121
+ return { id: toPatternId(key), name, fill: color.fill, stroke: color.stroke }
122
+ })
123
+ }
124
+ // Different fields: need two sets of defs in the SVG:
125
+ // 1. Simple defs (neutral background) — referenced by legend swatches via toPatternId(patternKey)
126
+ // 2. Composite defs (fill-colored background) — referenced by bars via toPatternId(fillKey::patternKey)
127
+ const defs = []
128
+ for (const [pk, name] of this.patternMap.entries()) {
129
+ defs.push({ id: toPatternId(pk), name, fill: '#ddd', stroke: '#666' })
130
+ }
131
+ const seenComposite = new Set()
132
+ for (const d of this.processedData) {
133
+ const fk = d[ff]
134
+ const pk = d[pf]
135
+ if (pk === null || pk === undefined) continue
136
+ const compositeKey = `${fk}::${pk}`
137
+ if (seenComposite.has(compositeKey)) continue
138
+ seenComposite.add(compositeKey)
139
+ const name = this.patternMap.get(pk) ?? PATTERN_ORDER[0]
140
+ const color = this.colorMap.get(fk) ?? { fill: '#ddd', stroke: '#666' }
141
+ defs.push({ id: toPatternId(compositeKey), name, fill: color.fill, stroke: color.stroke })
142
+ }
143
+ return defs
144
+ })())
145
+
146
+ /** @type {Map<unknown, string>} */
147
+ symbolMap = $derived(
148
+ this.#channels.symbol
149
+ ? assignSymbols(distinct(this.#rawData, this.#channels.symbol))
150
+ : new Map()
151
+ )
152
+
153
+ get innerWidth() { return this.#width - this.#margin.left - this.#margin.right }
154
+ get innerHeight() { return this.#height - this.#margin.top - this.#margin.bottom }
155
+
156
+ xScale = $derived(
157
+ this.#channels.x
158
+ ? buildXScale(this.processedData, this.#channels.x, this.innerWidth)
159
+ : null
160
+ )
161
+
162
+ yScale = $derived(
163
+ this.#channels.y
164
+ ? buildYScale(this.processedData, this.#channels.y, this.innerHeight, this.#layers)
165
+ : null
166
+ )
167
+
168
+ sizeScale = $derived(
169
+ this.#channels.size
170
+ ? buildSizeScale(this.processedData, this.#channels.size)
171
+ : null
172
+ )
173
+
174
+ bars = $derived(
175
+ this.xScale && this.yScale
176
+ ? buildBars(this.processedData, this.#channels, this.xScale, this.yScale, this.colorMap, this.patternMap)
177
+ : []
178
+ )
179
+
180
+ lines = $derived(
181
+ this.xScale && this.yScale
182
+ ? buildLines(this.processedData, this.#channels, this.xScale, this.yScale, this.colorMap, this.#curve)
183
+ : []
184
+ )
185
+
186
+ areas = $derived(
187
+ this.xScale && this.yScale
188
+ ? buildAreas(this.processedData, this.#channels, this.xScale, this.yScale, this.colorMap, this.#curve, this.patternMap)
189
+ : []
190
+ )
191
+
192
+ arcs = $derived(
193
+ this.#channels.y
194
+ ? buildArcs(this.processedData, this.#channels, this.colorMap, this.#width, this.#height)
195
+ : []
196
+ )
197
+
198
+ points = $derived(
199
+ this.xScale && this.yScale
200
+ ? buildPoints(this.processedData, this.#channels, this.xScale, this.yScale, this.colorMap, this.sizeScale, this.symbolMap)
201
+ : []
202
+ )
203
+
204
+ legendGroups = $derived(
205
+ buildLegendGroups(this.#channels, this.colorMap, this.patternMap, this.symbolMap)
206
+ )
207
+
208
+ get margin() { return this.#margin }
209
+ get width() { return this.#width }
210
+ get height() { return this.#height }
211
+ get mode() { return this.#mode }
212
+
213
+ /**
214
+ * @param {{ data?: Object[], channels?: Object, width?: number, height?: number, mode?: string, margin?: Object, layers?: Object[], curve?: string, stat?: string|Function }} opts
215
+ * Supported channel keys: `x`, `y`, `fill`, `color`, `pattern`, `symbol`, `size`, `label`.
216
+ * `frame` is reserved for future animation use (no-op).
217
+ */
218
+ update(opts = {}) {
219
+ if (opts.data !== undefined) this.#rawData = opts.data
220
+ if (opts.channels !== undefined) this.#channels = opts.channels
221
+ if (opts.width !== undefined) this.#width = opts.width
222
+ if (opts.height !== undefined) this.#height = opts.height
223
+ if (opts.mode !== undefined) this.#mode = opts.mode
224
+ if (opts.margin !== undefined) this.#margin = { ...DEFAULT_MARGIN, ...opts.margin }
225
+ if (opts.layers !== undefined) this.#layers = opts.layers
226
+ if (opts.curve !== undefined) this.#curve = opts.curve
227
+ if (opts.stat !== undefined) this.#stat = opts.stat
228
+ }
229
+ }