@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,131 @@
1
+ import { getContext, setContext } from 'svelte'
2
+ import { writable, derived } from 'svelte/store'
3
+ import * as d3 from 'd3'
4
+
5
+ const CHART_CONTEXT = 'chart-context'
6
+
7
+ const DEFAULT_OPTIONS = {
8
+ width: 600,
9
+ height: 400,
10
+ margin: { top: 20, right: 30, bottom: 40, left: 50 },
11
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
12
+ responsive: true,
13
+ animationDuration: 300,
14
+ data: []
15
+ }
16
+
17
+ /**
18
+ * @param {Object} config
19
+ * @returns {import('svelte/store').Writable}
20
+ */
21
+ function createDimensionsStore(config) {
22
+ return writable({
23
+ width: config.width,
24
+ height: config.height,
25
+ margin: { ...config.margin },
26
+ padding: { ...config.padding }
27
+ })
28
+ }
29
+
30
+ /**
31
+ * @param {import('svelte/store').Writable} dimensions
32
+ * @returns {import('svelte/store').Readable}
33
+ */
34
+ function createInnerDimensions(dimensions) {
35
+ return derived(dimensions, ($d) => ({
36
+ width: $d.width - $d.margin.left - $d.margin.right - $d.padding.left - $d.padding.right,
37
+ height: $d.height - $d.margin.top - $d.margin.bottom - $d.padding.top - $d.padding.bottom
38
+ }))
39
+ }
40
+
41
+ /**
42
+ * @param {import('svelte/store').Writable} plots
43
+ * @returns {(plot: unknown) => () => void}
44
+ */
45
+ function makeAddPlot(plots) {
46
+ return function addPlot(plot) {
47
+ plots.update((currentPlots) => [...currentPlots, plot])
48
+ return () => {
49
+ plots.update((currentPlots) => currentPlots.filter((p) => p !== plot))
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param {import('svelte/store').Writable} data
56
+ * @param {import('svelte/store').Readable} innerDimensions
57
+ * @returns {(xKey: string, yKey: string, colorKey?: string|null) => import('svelte/store').Readable}
58
+ */
59
+ function makeUpdateScales(data, innerDimensions) {
60
+ return function updateScales(xKey, yKey, colorKey = null) {
61
+ return derived([data, innerDimensions], ([$data, $innerDimensions]) => {
62
+ if (!$data || $data.length === 0) return null
63
+
64
+ const xScale = d3
65
+ .scaleBand()
66
+ .domain($data.map((d) => d[xKey]))
67
+ .range([0, $innerDimensions.width])
68
+ .padding(0.2)
69
+
70
+ const yScale = d3
71
+ .scaleLinear()
72
+ .domain([0, d3.max($data, (d) => d[yKey])])
73
+ .nice()
74
+ .range([$innerDimensions.height, 0])
75
+
76
+ let colorScale = null
77
+ if (colorKey) {
78
+ const uniqueCategories = [...new Set($data.map((d) => d[colorKey]))]
79
+ colorScale = d3.scaleOrdinal().domain(uniqueCategories).range(d3.schemeCategory10)
80
+ }
81
+
82
+ return { xScale, yScale, colorScale }
83
+ })
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Creates chart context and provides it to child components
89
+ *
90
+ * @param {Object} options Initial chart options
91
+ * @returns {Object} Chart context with all stores and methods
92
+ */
93
+ export function createChartContext(options = {}) {
94
+ const config = { ...DEFAULT_OPTIONS, ...options }
95
+
96
+ const dimensions = createDimensionsStore(config)
97
+ const data = writable(config.data)
98
+ const scales = writable({})
99
+ const innerDimensions = createInnerDimensions(dimensions)
100
+ const plots = writable([])
101
+ const axes = writable({ x: null, y: null })
102
+ const legend = writable({ enabled: false, items: [] })
103
+
104
+ const addPlot = makeAddPlot(plots)
105
+ const updateScales = makeUpdateScales(data, innerDimensions)
106
+
107
+ const chartContext = {
108
+ dimensions,
109
+ innerDimensions,
110
+ data,
111
+ scales,
112
+ plots,
113
+ axes,
114
+ legend,
115
+ addPlot,
116
+ updateScales
117
+ }
118
+
119
+ setContext(CHART_CONTEXT, chartContext)
120
+
121
+ return chartContext
122
+ }
123
+
124
+ /**
125
+ * Gets chart context provided by parent component
126
+ *
127
+ * @returns {Object} Chart context
128
+ */
129
+ export function getChartContext() {
130
+ return getContext(CHART_CONTEXT)
131
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @typedef GridPoint
3
+ * @property {number} x - x-coordinate of the point
4
+ * @property {number} y - y-coordinate of the point
5
+ * @property {number} r - radius of the point
6
+ */
7
+
8
+ /**
9
+ * @typedef SwatchGrid
10
+ * @property {number} width - width of the grid
11
+ * @property {number} height - height of the grid
12
+ * @property {GridPoint[]} data - data points of the grid
13
+ */
14
+
15
+ /**
16
+ * @tyoedef {Object} GridOptions
17
+ * @property {number} [pad=0] - The padding between the items
18
+ * @property {number} [columns=0] - The number of columns
19
+ * @property {number} [rows=0] - The number of rows
20
+ */
21
+
22
+ /**
23
+ * Calculates a grid of centres to fit a list of items of `size` within the number of `columns` and `rows`.
24
+ *
25
+ * - Attempts to find a best fit square if both columns and rows are not specified
26
+ * - Value in columns is prioritized over rows for recalculating the grid
27
+ * - Supports padding between the items
28
+ *
29
+ * @param {number} count - number of items
30
+ * @param {number} size - size of the items
31
+ * @param {GridOptions} options - options for the grid
32
+ * @returns {SwatchGrid}
33
+ */
34
+ export function swatchGrid(count, size, options) {
35
+ const { pad = 0 } = options || {}
36
+ let { columns = 0, rows = 0 } = options || {}
37
+ if (columns > 0) {
38
+ rows = Math.ceil(count / columns)
39
+ } else if (rows > 0) {
40
+ columns = Math.ceil(count / rows)
41
+ } else {
42
+ columns = Math.ceil(Math.sqrt(count))
43
+ rows = Math.ceil(count / columns)
44
+ }
45
+
46
+ const width = (size + pad) * columns + pad
47
+ const height = (size + pad) * rows + pad
48
+ const radius = size / 2
49
+ const data = [...Array(count).keys()].map((index) => ({
50
+ x: pad + radius + (index % columns) * (size + pad),
51
+ y: pad + radius + Math.floor(index / columns) * (size + pad),
52
+ r: radius
53
+ }))
54
+
55
+ return { width, height, data }
56
+ }
57
+
58
+ /**
59
+ * Spreads values as patterns with colors from a palette
60
+ *
61
+ * @param {number[]} values - values to spread
62
+ * @param {string[]} patterns - patterns to spread
63
+ * @param {string[]} palette - colors to spread
64
+ * @returns {Record<number, { id: string, pattern: string, color: string }>}
65
+ */
66
+ export function spreadValuesAsPatterns(values, patterns, palette) {
67
+ const result = values
68
+ .map((value, index) => ({
69
+ pattern: patterns[index % patterns.length],
70
+ color: palette[index % palette.length],
71
+ value
72
+ }))
73
+ .reduce(
74
+ (acc, { value, pattern, color }) => ({
75
+ ...acc,
76
+ [value]: {
77
+ id: `${pattern}_${color}`,
78
+ pattern,
79
+ color
80
+ }
81
+ }),
82
+ {}
83
+ )
84
+ return result
85
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Shared JSDoc type definitions for chart component props.
3
+ * Import these in chart components with:
4
+ * @type {import('./lib/plot/chartProps.js').ChartProps}
5
+ */
6
+
7
+ /**
8
+ * @typedef {Object} ChartProps
9
+ * Common props shared by all high-level chart wrappers.
10
+ *
11
+ * @property {Object[]} [data=[]] - Data array to visualize
12
+ * @property {string} [x] - Field name for the X axis
13
+ * @property {string} [y] - Field name for the Y axis
14
+ * @property {string} [fill] - Field name for fill color grouping (alias for color)
15
+ * @property {string} [color] - Field name for color grouping
16
+ * @property {string} [pattern] - Field name for pattern fill grouping
17
+ * @property {string} [stat='identity']- Aggregation stat: 'identity' | 'sum' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'boxplot'
18
+ * @property {number} [width=600] - SVG width in pixels
19
+ * @property {number} [height=400] - SVG height in pixels
20
+ * @property {string} [mode='light'] - Color mode: 'light' | 'dark'
21
+ * @property {boolean} [grid=true] - Whether to show grid lines
22
+ * @property {boolean} [legend=false] - Whether to show the legend
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} BarChartProps
27
+ * @extends ChartProps
28
+ * @property {boolean} [stack=false] - Stack bars (true) or group them (false)
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} PieChartProps
33
+ * @property {Object[]} [data=[]] - Data array to visualize
34
+ * @property {string} [label] - Field name for slice labels (drives color key)
35
+ * @property {string} [y] - Field name for slice values (theta)
36
+ * @property {string} [fill] - Alternative to label for color grouping
37
+ * @property {number} [innerRadius=0] - Inner radius as fraction of outer (0=pie, 0.5=donut)
38
+ * @property {string} [stat='sum'] - Aggregation stat (default sum for pie charts)
39
+ * @property {number} [width=400] - SVG width in pixels
40
+ * @property {number} [height=400] - SVG height in pixels
41
+ * @property {string} [mode='light'] - Color mode: 'light' | 'dark'
42
+ * @property {boolean} [legend=false] - Whether to show the legend
43
+ */
44
+
45
+ /**
46
+ * @typedef {Object} BoxViolinChartProps
47
+ * @property {Object[]} [data=[]] - Data array to visualize
48
+ * @property {string} [x] - Field name for the category axis (groups)
49
+ * @property {string} [y] - Field name for the value axis (raw observations)
50
+ * @property {string} [fill] - Field name for fill color; when different from x, sub-groups within each x-band (like grouped bars); lighter shade used for body, darker shade for whiskers/outline
51
+ * @property {number} [width=600] - SVG width in pixels
52
+ * @property {number} [height=400] - SVG height in pixels
53
+ * @property {string} [mode='light'] - Color mode: 'light' | 'dark'
54
+ * @property {boolean} [grid=true] - Whether to show grid lines
55
+ * @property {boolean} [legend=false] - Whether to show the legend
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} ScatterBubbleChartProps
60
+ * @property {Object[]} [data=[]] - Data array to visualize
61
+ * @property {string} [x] - Field name for X position
62
+ * @property {string} [y] - Field name for Y position
63
+ * @property {string} [color] - Field name for color grouping
64
+ * @property {string} [size] - Field name for point radius (BubbleChart: required)
65
+ * @property {number} [width=600] - SVG width in pixels
66
+ * @property {number} [height=400] - SVG height in pixels
67
+ * @property {string} [mode='light'] - Color mode: 'light' | 'dark'
68
+ * @property {boolean} [grid=true] - Whether to show grid lines
69
+ * @property {boolean} [legend=false] - Whether to show the legend
70
+ */
71
+
72
+ /**
73
+ * @typedef {Object} LineAreaChartProps
74
+ * @extends ChartProps
75
+ * @property {string} [curve] - Line interpolation: 'linear' | 'smooth' | 'step'
76
+ */
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Applies dimming state to data rows using crossfilter instance.
3
+ * Maps each row to { data: row, dimmed: boolean }.
4
+ *
5
+ * @param {Object[]} data - raw data array
6
+ * @param {Object} cf - crossfilter instance (from createCrossFilter)
7
+ * @param {Object} channels - { x, y, color, ... } field name mapping
8
+ * @returns {{ data: Object, dimmed: boolean }[]}
9
+ */
10
+ export function applyDimming(data, cf, channels) {
11
+ const fields = Object.values(channels).filter(Boolean)
12
+ return data.map((row) => {
13
+ const dimmed = fields.some((field) => cf.isDimmed(field, row[field]))
14
+ return { data: row, dimmed }
15
+ })
16
+ }
@@ -0,0 +1,58 @@
1
+ import { extent } from 'd3-array'
2
+
3
+ /**
4
+ * Splits data into a Map of panels keyed by facet field value.
5
+ * Preserves insertion order of first occurrence.
6
+ *
7
+ * @param {Object[]} data
8
+ * @param {string} field
9
+ * @returns {Map<unknown, Object[]>}
10
+ */
11
+ export function splitByField(data, field) {
12
+ const map = new Map()
13
+ for (const row of data) {
14
+ const key = row[field]
15
+ if (!map.has(key)) map.set(key, [])
16
+ map.get(key).push(row)
17
+ }
18
+ return map
19
+ }
20
+
21
+ /**
22
+ * Computes x/y domains for each panel.
23
+ *
24
+ * @param {Map<unknown, Object[]>} panels
25
+ * @param {{ x: string, y: string }} channels
26
+ * @param {'fixed'|'free'|'free_x'|'free_y'} scalesMode
27
+ * @returns {Map<unknown, { xDomain: unknown[], yDomain: [number, number] }>}
28
+ */
29
+ export function getFacetDomains(panels, channels, scalesMode = 'fixed') {
30
+ const { x: xf, y: yf } = channels
31
+ const allData = [...panels.values()].flat()
32
+
33
+ // Determine if x is categorical (string) or numeric
34
+ const sampleXVal = allData.find(d => d[xf] !== null && d[xf] !== undefined)?.[xf]
35
+ const xIsCategorical = typeof sampleXVal === 'string'
36
+
37
+ // Global domains (for fixed mode)
38
+ const globalXDomain = xIsCategorical
39
+ ? [...new Set(allData.map((d) => d[xf]))]
40
+ : extent(allData, (d) => Number(d[xf]))
41
+ const globalYDomain = extent(allData, (d) => Number(d[yf]))
42
+
43
+ const result = new Map()
44
+ for (const [key, rows] of panels.entries()) {
45
+ const freeX = scalesMode === 'free' || scalesMode === 'free_x'
46
+ const freeY = scalesMode === 'free' || scalesMode === 'free_y'
47
+
48
+ const xDomain = freeX
49
+ ? (xIsCategorical ? [...new Set(rows.map((d) => d[xf]))] : extent(rows, (d) => Number(d[xf])))
50
+ : globalXDomain
51
+ const yDomain = freeY
52
+ ? extent(rows, (d) => Number(d[yf]))
53
+ : globalYDomain
54
+
55
+ result.set(key, { xDomain, yDomain })
56
+ }
57
+ return result
58
+ }
@@ -0,0 +1,80 @@
1
+ import { extent } from 'd3-array'
2
+ import { dataset } from '@rokkit/data'
3
+
4
+ /**
5
+ * Extracts animation frames from data, keyed by time field value.
6
+ * Preserves insertion order of time values.
7
+ *
8
+ * @param {Object[]} data
9
+ * @param {string} timeField
10
+ * @returns {Map<unknown, Object[]>}
11
+ */
12
+ export function extractFrames(data, timeField) {
13
+ const map = new Map()
14
+ for (const row of data) {
15
+ const key = row[timeField]
16
+ if (!map.has(key)) map.set(key, [])
17
+ map.get(key).push(row)
18
+ }
19
+ return map
20
+ }
21
+
22
+ /**
23
+ * Ensures all frame values (byField) appear for every (x, color?) combination.
24
+ * Uses dataset alignBy to fill missing frame-value combos with y=0 so bars
25
+ * animate smoothly rather than disappearing between frames.
26
+ *
27
+ * Call after pre-aggregation. The result can be split directly by extractFrames
28
+ * with no further per-frame normalization needed.
29
+ *
30
+ * @param {Object[]} data - pre-aggregated rows, one per (x, color?, byField)
31
+ * @param {{ x?: string, y: string, color?: string }} channels
32
+ * @param {string} byField - the frame field (e.g. 'year')
33
+ * @returns {Object[]}
34
+ */
35
+ export function completeFrames(data, channels, byField) {
36
+ const { x: xf, y: yf, color: cf } = channels
37
+ const groupFields = [xf, ...(cf ? [cf] : [])].filter(Boolean)
38
+
39
+ if (groupFields.length === 0) return data
40
+
41
+ const nested = dataset(data)
42
+ .groupBy(...groupFields)
43
+ .alignBy(byField)
44
+ .usingTemplate({ [yf]: 0 })
45
+ .rollup()
46
+ .select()
47
+
48
+ return nested.flatMap((row) => {
49
+ const groupKey = groupFields.reduce((acc, f) => ({ ...acc, [f]: row[f] }), {})
50
+ // strip the actual_flag marker added by alignBy
51
+ return row.children.map(({ actual_flag: _af, ...child }) => ({ ...groupKey, ...child }))
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Computes static x/y domains from the full (pre-split) data array.
57
+ * These domains stay constant throughout the animation so values are
58
+ * always comparable across frames.
59
+ *
60
+ * NOTE: y domain is pinned to [0, max] — assumes bar chart semantics.
61
+ * Pass an explicit yDomain override for scatter/line charts where y can
62
+ * be negative.
63
+ *
64
+ * @param {Object[]} data - full dataset (before frame extraction)
65
+ * @param {{ x: string, y: string }} channels
66
+ * @returns {{ xDomain: unknown[], yDomain: [number, number] }}
67
+ */
68
+ export function computeStaticDomains(data, channels) {
69
+ const { x: xf, y: yf } = channels
70
+
71
+ const sampleX = data[0]?.[xf]
72
+ const xDomain = typeof sampleX === 'string'
73
+ ? [...new Set(data.map((d) => d[xf]))]
74
+ : extent(data, (d) => Number(d[xf]))
75
+
76
+ const [, yMax] = extent(data, (d) => Number(d[yf]))
77
+ const yDomain = [0, yMax ?? 0]
78
+
79
+ return { xDomain, yDomain }
80
+ }
@@ -0,0 +1,14 @@
1
+ const BUILT_IN_GEOMS = new Set(['bar', 'line', 'area', 'point', 'box', 'violin', 'arc'])
2
+
3
+ export function resolveFormat(field, helpers = {}) {
4
+ return helpers?.format?.[field] ?? ((v) => String(v))
5
+ }
6
+
7
+ export function resolveTooltip(helpers = {}) {
8
+ return helpers?.tooltip ?? null
9
+ }
10
+
11
+ export function resolveGeom(type, helpers = {}) {
12
+ if (BUILT_IN_GEOMS.has(type)) return null
13
+ return helpers?.geoms?.[type] ?? null
14
+ }
@@ -0,0 +1,53 @@
1
+ import palette from '../brewing/palette.json'
2
+ import { PATTERN_ORDER } from '../brewing/patterns.js'
3
+ import { SYMBOL_ORDER } from '../brewing/symbols.js'
4
+
5
+ /** @typedef {{ colors: string[], patterns: string[], symbols: string[] }} PlotPreset */
6
+
7
+ export const DEFAULT_PRESET = {
8
+ colors: palette.map((p) => p.shades.light.fill),
9
+ patterns: PATTERN_ORDER,
10
+ symbols: SYMBOL_ORDER
11
+ }
12
+
13
+ export const ACCESSIBLE_PRESET = {
14
+ colors: ['#66c2a5','#fc8d62','#8da0cb','#e78ac3','#a6d854','#ffd92f','#e5c494','#b3b3b3'],
15
+ patterns: PATTERN_ORDER,
16
+ symbols: SYMBOL_ORDER
17
+ }
18
+
19
+ export const PRINT_PRESET = {
20
+ colors: ['#f0f0f0','#bdbdbd','#969696','#737373','#525252','#252525','#000000'],
21
+ patterns: ['CrossHatch','DiagonalLines','Dots','Brick','Waves','Triangles','HorizontalLines'],
22
+ symbols: SYMBOL_ORDER
23
+ }
24
+
25
+ const BUILT_IN_PRESETS = {
26
+ default: DEFAULT_PRESET,
27
+ accessible: ACCESSIBLE_PRESET,
28
+ print: PRINT_PRESET
29
+ }
30
+
31
+ export function resolvePreset(name, helpers = {}) {
32
+ let resolved = null
33
+
34
+ if (name && BUILT_IN_PRESETS[name]) {
35
+ resolved = BUILT_IN_PRESETS[name]
36
+ } else if (name && helpers?.presets?.[name]) {
37
+ resolved = helpers.presets[name]
38
+ } else if (name) {
39
+ // eslint-disable-next-line no-console
40
+ console.warn(`[Plot] Unknown preset "${name}" — falling back to default. Add it to helpers.presets to suppress this warning.`)
41
+ resolved = DEFAULT_PRESET
42
+ } else if (helpers?.preset) {
43
+ resolved = helpers.preset
44
+ } else {
45
+ resolved = DEFAULT_PRESET
46
+ }
47
+
48
+ return {
49
+ colors: resolved.colors ?? DEFAULT_PRESET.colors,
50
+ patterns: resolved.patterns ?? DEFAULT_PRESET.patterns,
51
+ symbols: resolved.symbols ?? DEFAULT_PRESET.symbols
52
+ }
53
+ }
@@ -0,0 +1,56 @@
1
+ import { scaleBand, scaleLinear } from 'd3-scale'
2
+ import { extent } from 'd3-array'
3
+
4
+ export function inferFieldType(data, field) {
5
+ const values = data.map((d) => d[field]).filter((v) => v !== null && v !== undefined)
6
+ if (values.length === 0) return 'band'
7
+ const isNumeric = values.every((v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== ''))
8
+ return isNumeric ? 'continuous' : 'band'
9
+ }
10
+
11
+ export function inferOrientation(xType, yType) {
12
+ if (xType === 'band' && yType === 'continuous') return 'vertical'
13
+ if (yType === 'band' && xType === 'continuous') return 'horizontal'
14
+ return 'none'
15
+ }
16
+
17
+ export function buildUnifiedXScale(datasets, field, width, opts = {}) {
18
+ const allValues = datasets.flatMap((d) => d.map((r) => r[field]))
19
+ const isNumeric = allValues.every((v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== ''))
20
+
21
+ // opts.band forces scaleBand even for numeric data (e.g. bar charts with year on X).
22
+ if (opts.domain) {
23
+ const domainIsNumeric = opts.domain.every((v) => typeof v === 'number')
24
+ if (!opts.band && (domainIsNumeric || isNumeric)) {
25
+ return scaleLinear().domain(opts.domain).range([0, width]).nice()
26
+ }
27
+ return scaleBand().domain(opts.domain).range([0, width]).padding(opts.padding ?? 0.2)
28
+ }
29
+
30
+ if (isNumeric && !opts.band) {
31
+ const numericValues = allValues.map(Number)
32
+ const [minVal, maxVal] = extent(numericValues)
33
+ const domainMin = (opts.includeZero ?? false) ? 0 : (minVal ?? 0)
34
+ return scaleLinear().domain([domainMin, maxVal ?? 0]).range([0, width]).nice()
35
+ }
36
+
37
+ const domain = [...new Set(allValues)].filter((v) => v !== null && v !== undefined)
38
+ return scaleBand().domain(domain).range([0, width]).padding(opts.padding ?? 0.2)
39
+ }
40
+
41
+ export function buildUnifiedYScale(datasets, field, height, opts = {}) {
42
+ if (opts.domain) {
43
+ return scaleLinear().domain(opts.domain).range([height, 0]).nice()
44
+ }
45
+ const allValues = datasets.flatMap((d) => d.map((r) => Number(r[field]))).filter((v) => !isNaN(v))
46
+ const [minVal, maxVal] = extent(allValues)
47
+ const domainMin = (opts.includeZero ?? true) ? 0 : (minVal ?? 0)
48
+ return scaleLinear().domain([domainMin, maxVal ?? 0]).range([height, 0]).nice()
49
+ }
50
+
51
+ export function inferColorScaleType(data, field, spec = {}) {
52
+ if (spec.colorScale) return spec.colorScale
53
+ if (spec.colorMidpoint !== undefined) return 'diverging'
54
+ const type = inferFieldType(data, field)
55
+ return type === 'continuous' ? 'sequential' : 'categorical'
56
+ }
@@ -0,0 +1,92 @@
1
+ import { sum, mean, min, max, median } from 'd3-array'
2
+ import { applyAggregate, applyBoxStat } from '../brewing/stats.js'
3
+
4
+ const BUILT_IN_STATS = {
5
+ sum,
6
+ mean,
7
+ min,
8
+ max,
9
+ count: (values) => values.length,
10
+ median
11
+ }
12
+
13
+ /**
14
+ * Resolves a stat name to an aggregation function.
15
+ * Checks built-ins first, then helpers.stats, then warns and falls back to identity.
16
+ *
17
+ * @param {string} name
18
+ * @param {Object} helpers
19
+ * @returns {Function}
20
+ */
21
+ export function resolveStat(name, helpers = {}) {
22
+ if (name === 'identity') return (data) => data
23
+ if (BUILT_IN_STATS[name]) return BUILT_IN_STATS[name]
24
+ if (helpers?.stats?.[name]) return helpers.stats[name]
25
+ // eslint-disable-next-line no-console
26
+ console.warn(
27
+ `[Plot] Unknown stat "${name}" — falling back to identity. Add it to helpers.stats to suppress this warning.`
28
+ )
29
+ return (data) => data
30
+ }
31
+
32
+ /**
33
+ * Infers group-by fields from channels by excluding value fields.
34
+ * valueFields may contain channel keys (e.g. ['y', 'size']) OR field values (e.g. ['cty']).
35
+ * A channel's field is excluded if either the channel key OR the field value is in valueFields.
36
+ *
37
+ * @param {Record<string, string|undefined>} channels
38
+ * @param {string[]} valueFields
39
+ * @returns {string[]}
40
+ */
41
+ export function inferGroupByFields(channels, valueFields) {
42
+ const seen = new Set()
43
+ const result = []
44
+ for (const [key, field] of Object.entries(channels)) {
45
+ if (!field) continue
46
+ if (valueFields.includes(key) || valueFields.includes(field)) continue
47
+ if (seen.has(field)) continue
48
+ seen.add(field)
49
+ result.push(field)
50
+ }
51
+ return result
52
+ }
53
+
54
+ /**
55
+ * Applies a stat aggregation to data based on a geom config.
56
+ * Returns data unchanged for identity stat.
57
+ *
58
+ * @param {Object[]} data
59
+ * @param {{ stat?: string, channels?: Record<string, string> }} geomConfig
60
+ * @param {Object} helpers
61
+ * @returns {Object[]}
62
+ */
63
+ export function applyGeomStat(data, geomConfig, helpers = {}) {
64
+ const { stat = 'identity', channels = {} } = geomConfig
65
+ if (stat === 'identity') return data
66
+ if (stat === 'boxplot') return applyBoxStat(data, channels)
67
+
68
+ const statFn = resolveStat(stat, helpers)
69
+
70
+ const VALUE_CHANNEL_KEYS = ['y', 'size', 'theta']
71
+ const groupByFields = inferGroupByFields(channels, VALUE_CHANNEL_KEYS)
72
+ const primaryKey = VALUE_CHANNEL_KEYS.find((k) => channels[k])
73
+ if (!primaryKey) return data
74
+
75
+ let result = applyAggregate(data, {
76
+ by: groupByFields,
77
+ value: channels[primaryKey],
78
+ stat: statFn
79
+ })
80
+
81
+ for (const key of VALUE_CHANNEL_KEYS.filter((k) => k !== primaryKey && channels[k])) {
82
+ const extra = applyAggregate(data, { by: groupByFields, value: channels[key], stat: statFn })
83
+ const index = new Map(extra.map((r) => [groupByFields.map((f) => r[f]).join('|'), r]))
84
+ result = result.map((r) => {
85
+ const mapKey = groupByFields.map((f) => r[f]).join('|')
86
+ const extraRow = index.get(mapKey)
87
+ return extraRow ? { ...r, [channels[key]]: extraRow[channels[key]] } : r
88
+ })
89
+ }
90
+
91
+ return result
92
+ }