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

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 (222) 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 +49 -0
  5. package/dist/crossfilter/createCrossFilter.svelte.d.ts +13 -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 +114 -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 +23 -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 +38 -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 +215 -0
  59. package/src/Chart.svelte +98 -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 +181 -0
  75. package/src/PlotState.svelte.js +277 -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 +120 -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 +103 -0
  104. package/src/geoms/LabelPill.svelte +17 -0
  105. package/src/geoms/Line.svelte +99 -0
  106. package/src/geoms/Point.svelte +105 -0
  107. package/src/geoms/Violin.svelte +46 -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 +17 -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 +230 -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 +66 -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 +220 -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/index.js +4 -0
  156. package/src/patterns/patterns.js +208 -0
  157. package/src/patterns/scale.js +87 -0
  158. package/src/spec/chart-spec.js +29 -0
  159. package/src/symbols/RoundedSquare.svelte +33 -0
  160. package/src/symbols/Shape.svelte +37 -0
  161. package/src/symbols/constants/index.js +4 -0
  162. package/src/symbols/index.js +9 -0
  163. package/src/symbols/outline.svelte +60 -0
  164. package/src/symbols/solid.svelte +60 -0
  165. package/src/chart/FacetGrid.svelte +0 -51
  166. package/src/chart/Grid.svelte +0 -34
  167. package/src/chart/Legend.svelte +0 -16
  168. package/src/chart/PatternDefs.svelte +0 -13
  169. package/src/chart/Swatch.svelte +0 -93
  170. package/src/chart/SwatchButton.svelte +0 -29
  171. package/src/chart/SwatchGrid.svelte +0 -55
  172. package/src/chart/Symbol.svelte +0 -37
  173. package/src/chart/Texture.svelte +0 -16
  174. package/src/chart/TexturedShape.svelte +0 -27
  175. package/src/chart/TimelapseChart.svelte +0 -97
  176. package/src/chart/Timer.svelte +0 -27
  177. package/src/chart.js +0 -9
  178. package/src/components/charts/Axis.svelte +0 -66
  179. package/src/components/charts/Chart.svelte +0 -35
  180. package/src/components/index.js +0 -23
  181. package/src/components/lib/axis.js +0 -0
  182. package/src/components/lib/chart.js +0 -187
  183. package/src/components/lib/color.js +0 -327
  184. package/src/components/lib/funnel.js +0 -204
  185. package/src/components/lib/index.js +0 -19
  186. package/src/components/lib/pattern.js +0 -190
  187. package/src/components/lib/rollup.js +0 -55
  188. package/src/components/lib/shape.js +0 -199
  189. package/src/components/lib/summary.js +0 -145
  190. package/src/components/lib/theme.js +0 -23
  191. package/src/components/lib/timer.js +0 -41
  192. package/src/components/lib/utils.js +0 -165
  193. package/src/components/plots/BarPlot.svelte +0 -36
  194. package/src/components/plots/BoxPlot.svelte +0 -54
  195. package/src/components/plots/ScatterPlot.svelte +0 -30
  196. package/src/components/store.js +0 -70
  197. package/src/constants.js +0 -66
  198. package/src/elements/PatternDefs.svelte +0 -13
  199. package/src/elements/PatternMask.svelte +0 -20
  200. package/src/elements/Symbol.svelte +0 -38
  201. package/src/elements/Tooltip.svelte +0 -23
  202. package/src/funnel.svelte +0 -35
  203. package/src/geom.js +0 -105
  204. package/src/lib/axis.js +0 -75
  205. package/src/lib/colors.js +0 -32
  206. package/src/lib/geom.js +0 -4
  207. package/src/lib/shapes.js +0 -144
  208. package/src/lib/timer.js +0 -44
  209. package/src/lookup.js +0 -29
  210. package/src/plots/BarPlot.svelte +0 -55
  211. package/src/plots/BoxPlot.svelte +0 -0
  212. package/src/plots/FunnelPlot.svelte +0 -33
  213. package/src/plots/HeatMap.svelte +0 -5
  214. package/src/plots/HeatMapCalendar.svelte +0 -129
  215. package/src/plots/LinePlot.svelte +0 -55
  216. package/src/plots/Plot.svelte +0 -25
  217. package/src/plots/RankBarPlot.svelte +0 -38
  218. package/src/plots/ScatterPlot.svelte +0 -20
  219. package/src/plots/ViolinPlot.svelte +0 -11
  220. package/src/plots/heatmap.js +0 -70
  221. package/src/plots/index.js +0 -10
  222. package/src/swatch.js +0 -11
@@ -0,0 +1,22 @@
1
+ import palette from './palette.json'
2
+
3
+ /**
4
+ * Extracts distinct values for a given field from the data array.
5
+ * @param {Object[]} data
6
+ * @param {string|null} field
7
+ * @returns {unknown[]}
8
+ */
9
+ export function distinct(data, field) {
10
+ if (!field) return []
11
+ return [...new Set(data.map((d) => d[field]))].filter((v) => v !== null && v !== undefined)
12
+ }
13
+
14
+ /**
15
+ * Assigns palette colors to an array of distinct values.
16
+ * @param {unknown[]} values
17
+ * @param {'light'|'dark'} mode
18
+ * @returns {Map<unknown, {fill: string, stroke: string}>}
19
+ */
20
+ export function assignColors(values, mode = 'light') {
21
+ return new Map(values.map((v, i) => [v, palette[i % palette.length].shades[mode]]))
22
+ }
@@ -0,0 +1,56 @@
1
+ import {} from './types.js'
2
+
3
+ /**
4
+ * @typedef {import('./types').ChartMargin} ChartMargin
5
+ * @typedef {import('./types').ChartDimensions} ChartDimensions
6
+ */
7
+
8
+ /**
9
+ * Default chart margin
10
+ * @type {ChartMargin}
11
+ */
12
+ export const DEFAULT_MARGIN = { top: 20, right: 30, bottom: 40, left: 50 }
13
+
14
+ /**
15
+ * Creates chart dimensions based on width, height and margins
16
+ *
17
+ * @param {number} width - Total chart width
18
+ * @param {number} height - Total chart height
19
+ * @param {ChartMargin} margin - Chart margins
20
+ * @returns {ChartDimensions} Chart dimensions
21
+ */
22
+ export function createDimensions(width = 600, height = 400, margin = DEFAULT_MARGIN) {
23
+ return {
24
+ width,
25
+ height,
26
+ margin: { ...margin },
27
+ innerWidth: width - margin.left - margin.right,
28
+ innerHeight: height - margin.top - margin.bottom
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Updates existing dimensions with new values
34
+ *
35
+ * @param {ChartDimensions} dimensions - Current dimensions
36
+ * @param {Object} updates - Values to update
37
+ * @param {number} [updates.width] - New width
38
+ * @param {number} [updates.height] - New height
39
+ * @param {ChartMargin} [updates.margin] - New margin
40
+ * @returns {ChartDimensions} Updated dimensions
41
+ */
42
+ export function updateDimensions(dimensions, updates = {}) {
43
+ const newDimensions = { ...dimensions }
44
+
45
+ if (updates.width !== undefined) newDimensions.width = updates.width
46
+ if (updates.height !== undefined) newDimensions.height = updates.height
47
+ if (updates.margin !== undefined) newDimensions.margin = { ...updates.margin }
48
+
49
+ // Recalculate inner dimensions
50
+ newDimensions.innerWidth =
51
+ newDimensions.width - newDimensions.margin.left - newDimensions.margin.right
52
+ newDimensions.innerHeight =
53
+ newDimensions.height - newDimensions.margin.top - newDimensions.margin.bottom
54
+
55
+ return newDimensions
56
+ }
@@ -0,0 +1,205 @@
1
+ import { createDimensions, updateDimensions } from './dimensions.svelte.js'
2
+ import { createScales, getOrigin } from './scales.svelte.js'
3
+ import { createBars, filterBars, createGroupedBars } from './bars.svelte.js'
4
+ import { createXAxis, createYAxis, createGrid, createTickAttributes } from './axes.svelte.js'
5
+ import { createLegend, filterByLegend, createLegendItemAttributes } from './legends.svelte.js'
6
+
7
+ /**
8
+ * Chart Brewing - A collection of pure functions for chart data preparation
9
+ */
10
+ export {
11
+ // Dimensions
12
+ createDimensions,
13
+ updateDimensions,
14
+
15
+ // Scales
16
+ createScales,
17
+ getOrigin,
18
+
19
+ // Bars
20
+ createBars,
21
+ filterBars,
22
+ createGroupedBars,
23
+
24
+ // Axes
25
+ createXAxis,
26
+ createYAxis,
27
+ createGrid,
28
+ createTickAttributes,
29
+
30
+ // Legends
31
+ createLegend,
32
+ filterByLegend,
33
+ createLegendItemAttributes
34
+ }
35
+
36
+ /**
37
+ * Main class that manages chart state and provides access to all brewing functions
38
+ */
39
+ export class ChartBrewer {
40
+ #data = []
41
+ #dimensions = createDimensions()
42
+ #scales = { x: null, y: null, color: null }
43
+ #fields = { x: null, y: null, color: null }
44
+ #options = {
45
+ padding: 0.2,
46
+ animationDuration: 300
47
+ }
48
+
49
+ /**
50
+ * Creates a new ChartBrewer instance
51
+ *
52
+ * @param {Object} options Configuration options
53
+ */
54
+ constructor(options = {}) {
55
+ this.#dimensions = createDimensions(options.width, options.height, options.margin)
56
+
57
+ if (options.padding !== undefined) this.#options.padding = options.padding
58
+ if (options.animationDuration !== undefined)
59
+ this.#options.animationDuration = options.animationDuration
60
+ }
61
+
62
+ /**
63
+ * Sets the data for the chart
64
+ *
65
+ * @param {Array} data Data array
66
+ * @returns {ChartBrewer} this for method chaining
67
+ */
68
+ setData(data) {
69
+ this.#data = Array.isArray(data) ? data : []
70
+ return this
71
+ }
72
+
73
+ /**
74
+ * Sets the field mappings for axes and color
75
+ *
76
+ * @param {Object} fields Field mappings
77
+ * @returns {ChartBrewer} this for method chaining
78
+ */
79
+ setFields({ x, y, color }) {
80
+ if (x !== undefined) this.#fields.x = x
81
+ if (y !== undefined) this.#fields.y = y
82
+ if (color !== undefined) this.#fields.color = color
83
+ return this
84
+ }
85
+
86
+ /**
87
+ * Sets the dimensions of the chart
88
+ *
89
+ * @param {Object} dimensions Chart dimensions
90
+ * @returns {ChartBrewer} this for method chaining
91
+ */
92
+ setDimensions({ width, height, margin }) {
93
+ this.#dimensions = updateDimensions(this.#dimensions, { width, height, margin })
94
+ return this
95
+ }
96
+
97
+ /**
98
+ * Creates scales based on data and dimensions
99
+ *
100
+ * @returns {ChartBrewer} this for method chaining
101
+ */
102
+ createScales() {
103
+ this.#scales = createScales(this.#data, this.#fields, this.#dimensions, {
104
+ padding: this.#options.padding
105
+ })
106
+ return this
107
+ }
108
+
109
+ /**
110
+ * Creates bar data for rendering
111
+ *
112
+ * @returns {Array} Data for rendering bars
113
+ */
114
+ createBars() {
115
+ return createBars(this.#data, this.#fields, this.#scales, { dimensions: this.#dimensions })
116
+ }
117
+
118
+ /**
119
+ * Creates x-axis tick data for rendering
120
+ *
121
+ * @param {Object} options Axis options
122
+ * @returns {Object} Axis rendering data
123
+ */
124
+ createXAxis(options = {}) {
125
+ return createXAxis(this.#scales, this.#dimensions, options)
126
+ }
127
+
128
+ /**
129
+ * Creates y-axis tick data for rendering
130
+ *
131
+ * @param {Object} options Axis options
132
+ * @returns {Object} Axis rendering data
133
+ */
134
+ createYAxis(options = {}) {
135
+ return createYAxis(this.#scales, this.#dimensions, options)
136
+ }
137
+
138
+ /**
139
+ * Creates grid line data for rendering
140
+ *
141
+ * @param {Object} options Grid options
142
+ * @returns {Object} Grid rendering data
143
+ */
144
+ createGrid(options = {}) {
145
+ return createGrid(this.#scales, this.#dimensions, options)
146
+ }
147
+
148
+ /**
149
+ * Creates legend data for rendering
150
+ *
151
+ * @param {Object} options Legend options
152
+ * @returns {Object} Legend rendering data
153
+ */
154
+ createLegend(options = {}) {
155
+ return createLegend(this.#data, this.#fields, this.#scales, {
156
+ ...options,
157
+ dimensions: this.#dimensions
158
+ })
159
+ }
160
+
161
+ /**
162
+ * Gets all chart dimensions
163
+ *
164
+ * @returns {Object} Chart dimensions
165
+ */
166
+ getDimensions() {
167
+ return { ...this.#dimensions }
168
+ }
169
+
170
+ /**
171
+ * Gets all chart scales
172
+ *
173
+ * @returns {Object} Chart scales
174
+ */
175
+ getScales() {
176
+ return { ...this.#scales }
177
+ }
178
+
179
+ /**
180
+ * Gets the animation duration
181
+ *
182
+ * @returns {number} Animation duration in ms
183
+ */
184
+ getAnimationDuration() {
185
+ return this.#options.animationDuration
186
+ }
187
+
188
+ /**
189
+ * Gets the data being used
190
+ *
191
+ * @returns {Array} Chart data
192
+ */
193
+ getData() {
194
+ return [...this.#data]
195
+ }
196
+
197
+ /**
198
+ * Gets the fields configuration
199
+ *
200
+ * @returns {Object} Fields configuration
201
+ */
202
+ getFields() {
203
+ return { ...this.#fields }
204
+ }
205
+ }
@@ -0,0 +1,137 @@
1
+ import { SvelteSet } from 'svelte/reactivity'
2
+ import {} from './types.js'
3
+
4
+ /**
5
+ * @typedef {import('./types').LegendItem} LegendItem
6
+ * @typedef {import('./types').LegendData} LegendData
7
+ * @typedef {import('./types').ScaleFields} ScaleFields
8
+ * @typedef {import('./types').ChartScales} ChartScales
9
+ * @typedef {import('./types').ChartDimensions} ChartDimensions
10
+ */
11
+
12
+ const LEGEND_DEFAULTS = { title: '', align: 'right', shape: 'rect', markerSize: 10 }
13
+
14
+ /**
15
+ * Compute the x-position for legend alignment
16
+ * @param {string} align
17
+ * @param {number} innerWidth
18
+ * @param {number} approximateWidth
19
+ * @returns {number}
20
+ */
21
+ function legendX(align, innerWidth, approximateWidth) {
22
+ if (align === 'right') return innerWidth - approximateWidth
23
+ if (align === 'center') return (innerWidth - approximateWidth) / 2
24
+ return 0
25
+ }
26
+
27
+ /**
28
+ * @param {Array} colorValues
29
+ * @param {Function} colorScale
30
+ * @param {{ shape: string, markerSize: number, titleOffset: number }} style
31
+ * @returns {LegendItem[]}
32
+ */
33
+ function buildLegendItems(colorValues, colorScale, style) {
34
+ const { shape, markerSize, titleOffset } = style
35
+ return colorValues.map((value, index) => ({
36
+ value,
37
+ color: colorScale(value),
38
+ y: index * (markerSize + 5) + titleOffset,
39
+ shape,
40
+ markerSize
41
+ }))
42
+ }
43
+
44
+ /**
45
+ * @param {Array} colorValues
46
+ * @param {number} markerSize
47
+ * @returns {number}
48
+ */
49
+ function approximateLegendWidth(colorValues, markerSize) {
50
+ return Math.max(...colorValues.map((v) => v.toString().length)) * 8 + markerSize + 10
51
+ }
52
+
53
+ /**
54
+ * @param {Object} options
55
+ * @returns {{ dimensions: Object, title: string, align: string, shape: string, markerSize: number }}
56
+ */
57
+ function parseLegendOptions(options) {
58
+ const merged = Object.assign({}, LEGEND_DEFAULTS, options || {})
59
+ return {
60
+ dimensions: merged.dimensions,
61
+ title: merged.title,
62
+ align: merged.align,
63
+ shape: merged.shape,
64
+ markerSize: merged.markerSize
65
+ }
66
+ }
67
+
68
+ /**
69
+ * @param {Object|undefined} dimensions
70
+ * @returns {number}
71
+ */
72
+ function innerWidth(dimensions) {
73
+ return dimensions ? dimensions.innerWidth : 0
74
+ }
75
+
76
+ /**
77
+ * Creates legend data for rendering
78
+ *
79
+ * @param {Array} data - Chart data
80
+ * @param {Object} fields - Field mappings
81
+ * @param {string} fields.color - Color field
82
+ * @param {Object} scales - Chart scales
83
+ * @param {Function} scales.color - Color scale
84
+ * @param {Object} options - Legend options including dimensions
85
+ * @param {Object} options.dimensions - Chart dimensions
86
+ * @param {string} [options.title=''] - Legend title
87
+ * @param {string} [options.align='right'] - Legend alignment ('left', 'center', or 'right')
88
+ * @param {string} [options.shape='rect'] - Legend marker shape ('rect' or 'circle')
89
+ * @param {number} [options.markerSize=10] - Size of legend markers
90
+ * @returns {LegendData} Legend rendering data
91
+ */
92
+ export function createLegend(data, fields, scales, options) {
93
+ if (!data || !fields.color || !scales.color) {
94
+ return { items: [], title: '', transform: 'translate(0, 0)' }
95
+ }
96
+
97
+ const { dimensions, title, align, shape, markerSize } = parseLegendOptions(options)
98
+ const colorValues = [...new SvelteSet(data.map((d) => d[fields.color]))]
99
+ const titleOffset = title ? 15 : 0
100
+ const style = { shape, markerSize, titleOffset }
101
+ const items = buildLegendItems(colorValues, scales.color, style)
102
+ const approxWidth = approximateLegendWidth(colorValues, markerSize)
103
+ const x = legendX(align, innerWidth(dimensions), approxWidth)
104
+
105
+ return { items, title, transform: `translate(${x}, 0)` }
106
+ }
107
+
108
+ /**
109
+ * Filter data based on legend selection
110
+ *
111
+ * @param {Array} data - Chart data
112
+ * @param {string} colorField - Field used for color mapping
113
+ * @param {Array} selectedValues - Selected legend values
114
+ * @returns {Array} Filtered data
115
+ */
116
+ export function filterByLegend(data, colorField, selectedValues) {
117
+ if (!selectedValues || selectedValues.length === 0) {
118
+ return data
119
+ }
120
+
121
+ return data.filter((d) => selectedValues.includes(d[colorField]))
122
+ }
123
+
124
+ /**
125
+ * Create attributes for legend items
126
+ *
127
+ * @param {LegendItem} item - Legend item
128
+ * @returns {Object} Attributes for the legend item
129
+ */
130
+ export function createLegendItemAttributes(item) {
131
+ return {
132
+ 'data-plot-legend-item': '',
133
+ transform: `translate(0, ${item.y})`,
134
+ role: 'img',
135
+ 'aria-label': `Legend item for ${item.value}`
136
+ }
137
+ }
@@ -0,0 +1,43 @@
1
+ import { pie, arc } from 'd3-shape'
2
+ import { toPatternId } from '../../brewing/patterns.js'
3
+
4
+ /**
5
+ * Builds arc geometry for pie/donut charts.
6
+ * @param {Object[]} data
7
+ * @param {{ color: string, y: string, pattern?: string }} channels
8
+ * @param {Map} colors
9
+ * @param {number} width
10
+ * @param {number} height
11
+ * @param {{ innerRadius?: number }} opts
12
+ * @param {Map<unknown, string>} [patterns]
13
+ */
14
+ export function buildArcs(data, channels, colors, width, height, opts = {}, patterns) {
15
+ const { color: lf, y: yf } = channels
16
+ const radius = Math.min(width, height) / 2
17
+ const innerRadius = opts.innerRadius ?? 0
18
+ const pieGen = pie().value((d) => Number(d[yf]))
19
+ const arcGen = arc().innerRadius(innerRadius).outerRadius(radius)
20
+ const slices = pieGen(data)
21
+ const total = slices.reduce((s, sl) => s + (sl.endAngle - sl.startAngle), 0)
22
+ // Label radius: midpoint between inner and outer (or 70% out for solid pie)
23
+ const labelRadius = innerRadius > 0 ? (innerRadius + radius) / 2 : radius * 0.65
24
+ const labelArc = arc().innerRadius(labelRadius).outerRadius(labelRadius)
25
+ return slices.map((slice) => {
26
+ const key = slice.data[lf]
27
+ const colorEntry = colors?.get(key) ?? { fill: '#888', stroke: '#fff' }
28
+ const patternId = key !== null && key !== undefined && patterns?.has(key)
29
+ ? toPatternId(String(key)) : null
30
+ const pct = Math.round(((slice.endAngle - slice.startAngle) / total) * 100)
31
+ const [cx, cy] = labelArc.centroid(slice)
32
+ return {
33
+ d: arcGen(slice),
34
+ fill: colorEntry.fill,
35
+ stroke: colorEntry.stroke,
36
+ key,
37
+ patternId,
38
+ pct,
39
+ centroid: [cx, cy],
40
+ data: slice.data
41
+ }
42
+ })
43
+ }
@@ -0,0 +1,59 @@
1
+ import { area, curveCatmullRom, curveStep } from 'd3-shape'
2
+ import { toPatternId } from '../patterns.js'
3
+
4
+ /**
5
+ * @param {Object[]} data
6
+ * @param {{ x: string, y: string, fill?: string, pattern?: string }} channels
7
+ * `fill` is the primary aesthetic — drives grouping and interior color.
8
+ * @param {Function} xScale
9
+ * @param {Function} yScale
10
+ * @param {Map} colors
11
+ * @param {'linear'|'smooth'|'step'} [curve]
12
+ * @param {Map} [patternMap]
13
+ */
14
+ export function buildAreas(data, channels, xScale, yScale, colors, curve, patternMap) {
15
+ const { x: xf, y: yf, pattern: pf } = channels
16
+ const cf = channels.fill // fill is the primary aesthetic for area charts
17
+ const innerHeight = yScale.range()[0]
18
+ const xPos = (d) => typeof xScale.bandwidth === 'function'
19
+ ? xScale(d[xf]) + xScale.bandwidth() / 2
20
+ : xScale(d[xf])
21
+ const makeGen = () => {
22
+ const gen = area().x(xPos).y0(innerHeight).y1((d) => yScale(d[yf]))
23
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
24
+ else if (curve === 'step') gen.curve(curveStep)
25
+ return gen
26
+ }
27
+ if (!cf) {
28
+ const colorEntry = colors?.values().next().value ?? { fill: '#888', stroke: '#444' }
29
+ return [{ d: makeGen()(data), fill: colorEntry.fill, stroke: 'none', colorKey: null, patternKey: null, patternId: null }]
30
+ }
31
+ const groups = groupBy(data, cf)
32
+ return [...groups.entries()].map(([key, rows]) => {
33
+ const colorEntry = colors?.get(key) ?? { fill: '#888', stroke: '#444' }
34
+ const patternKey = pf ? (pf === cf ? key : rows[0]?.[pf]) : null
35
+ const patternName = patternKey !== null && patternKey !== undefined ? patternMap?.get(patternKey) : null
36
+ const compositePatternKey = (cf && pf && cf !== pf && patternKey !== null && patternKey !== undefined)
37
+ ? `${key}::${patternKey}`
38
+ : patternKey
39
+ return {
40
+ d: makeGen()(rows),
41
+ fill: colorEntry.fill,
42
+ stroke: 'none',
43
+ key,
44
+ colorKey: key,
45
+ patternKey,
46
+ patternId: patternName ? toPatternId(compositePatternKey) : null
47
+ }
48
+ })
49
+ }
50
+
51
+ function groupBy(arr, field) {
52
+ const map = new Map()
53
+ for (const item of arr) {
54
+ const key = item[field]
55
+ if (!map.has(key)) map.set(key, [])
56
+ map.get(key).push(item)
57
+ }
58
+ return map
59
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @param {Object[]} data
3
+ * @param {{ x: string, y: string, fill?: string, color?: string, pattern?: string }} channels
4
+ * `fill` drives the bar interior color. `color` drives the border stroke; falls back to fill.
5
+ * @param {import('d3-scale').ScaleBand|import('d3-scale').ScaleLinear} xScale
6
+ * @param {import('d3-scale').ScaleLinear} yScale
7
+ * @param {Map} colors - value→{fill,stroke}
8
+ * @param {Map} [patternMap] - value→patternName
9
+ * @returns {Array}
10
+ */
11
+ import { toPatternId } from '../patterns.js'
12
+
13
+ export function buildBars(data, channels, xScale, yScale, colors, patternMap) {
14
+ const { x: xf, y: yf, fill: ff, color: cf, pattern: pf } = channels
15
+ const barWidth = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 10
16
+ const innerHeight = yScale.range()[0]
17
+
18
+ return data.map((d) => {
19
+ const xVal = d[xf]
20
+ const fillKey = ff ? d[ff] : xVal // fill channel drives interior color
21
+ const strokeKey = cf ? d[cf] : null // color channel drives border; null = no border
22
+ const colorEntry = colors?.get(fillKey) ?? { fill: '#888', stroke: '#444' }
23
+ const strokeEntry = colors?.get(strokeKey) ?? colorEntry
24
+ const patternKey = pf ? d[pf] : null
25
+ const patternName = patternKey !== null && patternKey !== undefined ? patternMap?.get(patternKey) : null
26
+ // When fill and pattern are different fields, bars need a composite pattern def id
27
+ // so each (region, category) pair gets its uniquely colored+textured pattern.
28
+ const compositePatternKey = (ff && pf && ff !== pf && patternKey !== null && patternKey !== undefined)
29
+ ? `${d[ff]}::${patternKey}`
30
+ : patternKey
31
+ const barX = typeof xScale.bandwidth === 'function'
32
+ ? xScale(xVal)
33
+ : xScale(xVal) - barWidth / 2
34
+ const barY = yScale(d[yf])
35
+ return {
36
+ data: d,
37
+ key: `${xVal}::${fillKey ?? ''}::${patternKey ?? ''}`,
38
+ x: barX,
39
+ y: barY,
40
+ width: barWidth,
41
+ height: innerHeight - barY,
42
+ fill: colorEntry.fill,
43
+ stroke: strokeKey !== null ? strokeEntry.stroke : null,
44
+ colorKey: fillKey,
45
+ patternKey,
46
+ patternId: patternName ? toPatternId(compositePatternKey) : null
47
+ }
48
+ })
49
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Builds box geometry for box plot charts.
3
+ * Input data rows must already contain { q1, median, q3, iqr_min, iqr_max } —
4
+ * computed by applyBoxStat before reaching this function.
5
+ *
6
+ * When `fill` differs from `x`, boxes are sub-grouped within each x-band
7
+ * (one narrower box per fill value per x category, like grouped bars).
8
+ * Box body uses the lighter fill shade; whiskers and median use the darker stroke shade.
9
+ *
10
+ * @param {Object[]} data - Pre-aggregated rows with quartile fields
11
+ * @param {{ x: string, fill?: string }} channels
12
+ * `fill` drives the box and whisker color (defaults to x-field).
13
+ * @param {import('d3-scale').ScaleBand} xScale
14
+ * @param {import('d3-scale').ScaleLinear} yScale
15
+ * @param {Map<unknown, {fill:string, stroke:string}>} colors
16
+ * @returns {Array}
17
+ */
18
+ export function buildBoxes(data, channels, xScale, yScale, colors) {
19
+ const { x: xf, fill: ff } = channels
20
+ const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 20
21
+ const grouped = ff && ff !== xf
22
+
23
+ if (grouped) {
24
+ const fillValues = [...new Set(data.map((d) => d[ff]))]
25
+ const n = fillValues.length
26
+ const subBandWidth = bw / n
27
+ const boxWidth = subBandWidth * 0.75
28
+ const whiskerWidth = subBandWidth * 0.4
29
+
30
+ return data.map((d) => {
31
+ const fillVal = d[ff]
32
+ const subIndex = fillValues.indexOf(fillVal)
33
+ const bandStart = xScale(d[xf]) ?? 0
34
+ const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2
35
+ const colorEntry = colors?.get(fillVal) ?? { fill: '#aaa', stroke: '#666' }
36
+
37
+ return {
38
+ data: d,
39
+ cx,
40
+ q1: yScale(d.q1),
41
+ median: yScale(d.median),
42
+ q3: yScale(d.q3),
43
+ iqr_min: yScale(d.iqr_min),
44
+ iqr_max: yScale(d.iqr_max),
45
+ width: boxWidth,
46
+ whiskerWidth,
47
+ fill: colorEntry.fill,
48
+ stroke: colorEntry.stroke
49
+ }
50
+ })
51
+ }
52
+
53
+ // Non-grouped: one box per x category
54
+ const boxWidth = bw * 0.6
55
+ const whiskerWidth = bw * 0.3
56
+
57
+ return data.map((d) => {
58
+ const fillKey = ff ? d[ff] : d[xf]
59
+ const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
60
+ const cx = (xScale(d[xf]) ?? 0) + (typeof xScale.bandwidth === 'function' ? bw / 2 : 0)
61
+ return {
62
+ data: d,
63
+ cx,
64
+ q1: yScale(d.q1),
65
+ median: yScale(d.median),
66
+ q3: yScale(d.q3),
67
+ iqr_min: yScale(d.iqr_min),
68
+ iqr_max: yScale(d.iqr_max),
69
+ width: boxWidth,
70
+ whiskerWidth,
71
+ fill: colorEntry.fill,
72
+ stroke: colorEntry.stroke
73
+ }
74
+ })
75
+ }
@@ -0,0 +1,48 @@
1
+ import { line, curveCatmullRom, curveStep } from 'd3-shape'
2
+
3
+ /**
4
+ * @param {Object[]} data
5
+ * @param {{ x: string, y: string, color?: string }} channels
6
+ * @param {Function} xScale
7
+ * @param {Function} yScale
8
+ * @param {Map} colors
9
+ * @param {'linear'|'smooth'|'step'} [curve]
10
+ * @returns {{ d: string, fill: string, stroke: string, points: {x:number, y:number, data:Object}[], key?: unknown }[]}
11
+ */
12
+ export function buildLines(data, channels, xScale, yScale, colors, curve) {
13
+ const { x: xf, y: yf, color: cf } = channels
14
+ const xPos = (d) => typeof xScale.bandwidth === 'function'
15
+ ? xScale(d[xf]) + xScale.bandwidth() / 2
16
+ : xScale(d[xf])
17
+ const makeGen = () => {
18
+ const gen = line().x(xPos).y((d) => yScale(d[yf]))
19
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
20
+ else if (curve === 'step') gen.curve(curveStep)
21
+ return gen
22
+ }
23
+ const toPoints = (rows) => rows.map((d) => ({ x: xPos(d), y: yScale(d[yf]), data: d }))
24
+
25
+ const sortByX = (rows) => [...rows].sort((a, b) => a[xf] < b[xf] ? -1 : a[xf] > b[xf] ? 1 : 0)
26
+
27
+ if (!cf) {
28
+ const sorted = sortByX(data)
29
+ const stroke = colors?.values().next().value?.stroke ?? '#888'
30
+ return [{ d: makeGen()(sorted), fill: 'none', stroke, points: toPoints(sorted) }]
31
+ }
32
+ const groups = groupBy(data, cf)
33
+ return [...groups.entries()].map(([key, rows]) => {
34
+ const sorted = sortByX(rows)
35
+ const colorEntry = colors?.get(key) ?? { fill: 'none', stroke: '#888' }
36
+ return { d: makeGen()(sorted), fill: 'none', stroke: colorEntry.stroke, points: toPoints(sorted), key }
37
+ })
38
+ }
39
+
40
+ function groupBy(arr, field) {
41
+ const map = new Map()
42
+ for (const item of arr) {
43
+ const key = item[field]
44
+ if (!map.has(key)) map.set(key, [])
45
+ map.get(key).push(item)
46
+ }
47
+ return map
48
+ }