@rokkit/chart 1.0.0-next.16 → 1.0.0-next.161

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 (173) hide show
  1. package/README.md +150 -46
  2. package/package.json +42 -45
  3. package/src/AnimatedPlot.svelte +383 -0
  4. package/src/Chart.svelte +95 -0
  5. package/src/ChartProvider.svelte +10 -0
  6. package/src/FacetPlot/Panel.svelte +37 -0
  7. package/src/FacetPlot.svelte +114 -0
  8. package/src/Plot/Arc.svelte +29 -0
  9. package/src/Plot/Area.svelte +32 -0
  10. package/src/Plot/Axis.svelte +95 -0
  11. package/src/Plot/Bar.svelte +54 -0
  12. package/src/Plot/Grid.svelte +34 -0
  13. package/src/Plot/Legend.svelte +233 -0
  14. package/src/Plot/Line.svelte +37 -0
  15. package/src/Plot/Point.svelte +40 -0
  16. package/src/Plot/Root.svelte +62 -0
  17. package/src/Plot/Timeline.svelte +95 -0
  18. package/src/Plot/Tooltip.svelte +87 -0
  19. package/src/Plot/index.js +9 -0
  20. package/src/Plot.svelte +297 -0
  21. package/src/PlotState.svelte.js +350 -0
  22. package/src/Sparkline.svelte +108 -0
  23. package/src/Symbol.svelte +21 -0
  24. package/src/Texture.svelte +18 -0
  25. package/src/charts/AreaChart.svelte +27 -0
  26. package/src/charts/BarChart.svelte +28 -0
  27. package/src/charts/BoxPlot.svelte +21 -0
  28. package/src/charts/BubbleChart.svelte +23 -0
  29. package/src/charts/LineChart.svelte +26 -0
  30. package/src/charts/PieChart.svelte +35 -0
  31. package/src/charts/ScatterPlot.svelte +26 -0
  32. package/src/charts/ViolinPlot.svelte +21 -0
  33. package/src/crossfilter/CrossFilter.svelte +42 -0
  34. package/src/crossfilter/FilterBar.svelte +24 -0
  35. package/src/crossfilter/FilterHistogram.svelte +290 -0
  36. package/src/crossfilter/FilterSlider.svelte +83 -0
  37. package/src/crossfilter/createCrossFilter.svelte.js +124 -0
  38. package/src/elements/Bar.svelte +22 -24
  39. package/src/elements/ColorRamp.svelte +20 -22
  40. package/src/elements/ContinuousLegend.svelte +20 -17
  41. package/src/elements/DefinePatterns.svelte +24 -0
  42. package/src/elements/DiscreteLegend.svelte +15 -15
  43. package/src/elements/Label.svelte +4 -8
  44. package/src/elements/SymbolGrid.svelte +22 -0
  45. package/src/elements/index.js +6 -0
  46. package/src/examples/BarChartExample.svelte +81 -0
  47. package/src/geoms/Arc.svelte +126 -0
  48. package/src/geoms/Area.svelte +78 -0
  49. package/src/geoms/Bar.svelte +200 -0
  50. package/src/geoms/Box.svelte +113 -0
  51. package/src/geoms/LabelPill.svelte +17 -0
  52. package/src/geoms/Line.svelte +123 -0
  53. package/src/geoms/Point.svelte +145 -0
  54. package/src/geoms/Violin.svelte +56 -0
  55. package/src/geoms/lib/areas.js +154 -0
  56. package/src/geoms/lib/bars.js +223 -0
  57. package/src/index.js +74 -16
  58. package/src/lib/brewer.js +25 -0
  59. package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
  60. package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
  61. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  62. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  63. package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
  64. package/src/lib/brewing/axes.svelte.js +270 -0
  65. package/src/lib/brewing/bars.svelte.js +201 -0
  66. package/src/lib/brewing/brewer.svelte.js +277 -0
  67. package/src/lib/brewing/colors.js +51 -0
  68. package/src/lib/brewing/dimensions.svelte.js +56 -0
  69. package/src/lib/brewing/index.svelte.js +205 -0
  70. package/src/lib/brewing/legends.svelte.js +137 -0
  71. package/src/lib/brewing/marks/arcs.js +43 -0
  72. package/src/lib/brewing/marks/areas.js +72 -0
  73. package/src/lib/brewing/marks/bars.js +49 -0
  74. package/src/lib/brewing/marks/boxes.js +75 -0
  75. package/src/lib/brewing/marks/lines.js +55 -0
  76. package/src/lib/brewing/marks/points.js +105 -0
  77. package/src/lib/brewing/marks/violins.js +90 -0
  78. package/src/lib/brewing/patterns.js +45 -0
  79. package/src/lib/brewing/scales.js +51 -0
  80. package/src/lib/brewing/scales.svelte.js +82 -0
  81. package/src/lib/brewing/stats.js +74 -0
  82. package/src/lib/brewing/symbols.js +10 -0
  83. package/src/lib/brewing/types.js +73 -0
  84. package/src/lib/chart.js +221 -0
  85. package/src/lib/context.js +131 -0
  86. package/src/lib/grid.js +85 -0
  87. package/src/lib/keyboard-nav.js +37 -0
  88. package/src/lib/plot/chartProps.js +76 -0
  89. package/src/lib/plot/crossfilter.js +16 -0
  90. package/src/lib/plot/facet.js +58 -0
  91. package/src/lib/plot/frames.js +81 -0
  92. package/src/lib/plot/helpers.js +14 -0
  93. package/src/lib/plot/preset.js +67 -0
  94. package/src/lib/plot/scales.js +81 -0
  95. package/src/lib/plot/stat.js +92 -0
  96. package/src/lib/plot/types.js +65 -0
  97. package/src/lib/preset.js +41 -0
  98. package/src/lib/scales.svelte.js +151 -0
  99. package/src/lib/swatch.js +13 -0
  100. package/src/lib/ticks.js +46 -0
  101. package/src/lib/utils.js +111 -118
  102. package/src/lib/xscale.js +31 -0
  103. package/src/patterns/DefinePatterns.svelte +32 -0
  104. package/src/patterns/PatternDef.svelte +27 -0
  105. package/src/patterns/index.js +4 -0
  106. package/src/patterns/patterns.js +360 -0
  107. package/src/patterns/scale.js +116 -0
  108. package/src/spec/chart-spec.js +72 -0
  109. package/src/symbols/RoundedSquare.svelte +33 -0
  110. package/src/symbols/Shape.svelte +37 -0
  111. package/src/symbols/constants/index.js +4 -0
  112. package/src/symbols/index.js +9 -0
  113. package/src/symbols/outline.svelte +60 -0
  114. package/src/symbols/solid.svelte +60 -0
  115. package/LICENSE +0 -21
  116. package/src/chart/FacetGrid.svelte +0 -51
  117. package/src/chart/Grid.svelte +0 -34
  118. package/src/chart/Legend.svelte +0 -16
  119. package/src/chart/PatternDefs.svelte +0 -13
  120. package/src/chart/Swatch.svelte +0 -93
  121. package/src/chart/SwatchButton.svelte +0 -29
  122. package/src/chart/SwatchGrid.svelte +0 -55
  123. package/src/chart/Symbol.svelte +0 -37
  124. package/src/chart/Texture.svelte +0 -16
  125. package/src/chart/TexturedShape.svelte +0 -27
  126. package/src/chart/TimelapseChart.svelte +0 -97
  127. package/src/chart/Timer.svelte +0 -27
  128. package/src/chart.js +0 -9
  129. package/src/components/charts/Axis.svelte +0 -66
  130. package/src/components/charts/Chart.svelte +0 -35
  131. package/src/components/index.js +0 -23
  132. package/src/components/lib/axis.js +0 -0
  133. package/src/components/lib/chart.js +0 -187
  134. package/src/components/lib/color.js +0 -327
  135. package/src/components/lib/funnel.js +0 -204
  136. package/src/components/lib/index.js +0 -19
  137. package/src/components/lib/pattern.js +0 -190
  138. package/src/components/lib/rollup.js +0 -55
  139. package/src/components/lib/shape.js +0 -199
  140. package/src/components/lib/summary.js +0 -145
  141. package/src/components/lib/theme.js +0 -23
  142. package/src/components/lib/timer.js +0 -41
  143. package/src/components/lib/utils.js +0 -165
  144. package/src/components/plots/BarPlot.svelte +0 -36
  145. package/src/components/plots/BoxPlot.svelte +0 -54
  146. package/src/components/plots/ScatterPlot.svelte +0 -30
  147. package/src/components/store.js +0 -70
  148. package/src/constants.js +0 -66
  149. package/src/elements/PatternDefs.svelte +0 -13
  150. package/src/elements/PatternMask.svelte +0 -20
  151. package/src/elements/Symbol.svelte +0 -38
  152. package/src/elements/Tooltip.svelte +0 -23
  153. package/src/funnel.svelte +0 -35
  154. package/src/geom.js +0 -105
  155. package/src/lib/axis.js +0 -75
  156. package/src/lib/colors.js +0 -32
  157. package/src/lib/geom.js +0 -4
  158. package/src/lib/shapes.js +0 -144
  159. package/src/lib/timer.js +0 -44
  160. package/src/lookup.js +0 -29
  161. package/src/plots/BarPlot.svelte +0 -55
  162. package/src/plots/BoxPlot.svelte +0 -0
  163. package/src/plots/FunnelPlot.svelte +0 -33
  164. package/src/plots/HeatMap.svelte +0 -5
  165. package/src/plots/HeatMapCalendar.svelte +0 -129
  166. package/src/plots/LinePlot.svelte +0 -55
  167. package/src/plots/Plot.svelte +0 -25
  168. package/src/plots/RankBarPlot.svelte +0 -38
  169. package/src/plots/ScatterPlot.svelte +0 -20
  170. package/src/plots/ViolinPlot.svelte +0 -11
  171. package/src/plots/heatmap.js +0 -70
  172. package/src/plots/index.js +0 -10
  173. package/src/swatch.js +0 -11
@@ -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 =
29
+ key !== null && key !== undefined && patterns?.has(key) ? 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,72 @@
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) =>
19
+ typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
20
+ const makeGen = () => {
21
+ const gen = area()
22
+ .x(xPos)
23
+ .y0(innerHeight)
24
+ .y1((d) => yScale(d[yf]))
25
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
26
+ else if (curve === 'step') gen.curve(curveStep)
27
+ return gen
28
+ }
29
+ if (!cf) {
30
+ const colorEntry = colors?.values().next().value ?? { fill: '#888', stroke: '#444' }
31
+ return [
32
+ {
33
+ d: makeGen()(data),
34
+ fill: colorEntry.fill,
35
+ stroke: 'none',
36
+ colorKey: null,
37
+ patternKey: null,
38
+ patternId: null
39
+ }
40
+ ]
41
+ }
42
+ const groups = groupBy(data, cf)
43
+ return [...groups.entries()].map(([key, rows]) => {
44
+ const colorEntry = colors?.get(key) ?? { fill: '#888', stroke: '#444' }
45
+ const patternKey = pf ? (pf === cf ? key : rows[0]?.[pf]) : null
46
+ const patternName =
47
+ patternKey !== null && patternKey !== undefined ? patternMap?.get(patternKey) : null
48
+ const compositePatternKey =
49
+ cf && pf && cf !== pf && patternKey !== null && patternKey !== undefined
50
+ ? `${key}::${patternKey}`
51
+ : patternKey
52
+ return {
53
+ d: makeGen()(rows),
54
+ fill: colorEntry.fill,
55
+ stroke: 'none',
56
+ key,
57
+ colorKey: key,
58
+ patternKey,
59
+ patternId: patternName ? toPatternId(compositePatternKey) : null
60
+ }
61
+ })
62
+ }
63
+
64
+ function groupBy(arr, field) {
65
+ const map = new Map()
66
+ for (const item of arr) {
67
+ const key = item[field]
68
+ if (!map.has(key)) map.set(key, [])
69
+ map.get(key).push(item)
70
+ }
71
+ return map
72
+ }
@@ -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 =
26
+ patternKey !== null && patternKey !== undefined ? patternMap?.get(patternKey) : null
27
+ // When fill and pattern are different fields, bars need a composite pattern def id
28
+ // so each (region, category) pair gets its uniquely colored+textured pattern.
29
+ const compositePatternKey =
30
+ ff && pf && ff !== pf && patternKey !== null && patternKey !== undefined
31
+ ? `${d[ff]}::${patternKey}`
32
+ : patternKey
33
+ const barX = typeof xScale.bandwidth === 'function' ? xScale(xVal) : 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,55 @@
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) =>
15
+ typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
16
+ const makeGen = () => {
17
+ const gen = line()
18
+ .x(xPos)
19
+ .y((d) => yScale(d[yf]))
20
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
21
+ else if (curve === 'step') gen.curve(curveStep)
22
+ return gen
23
+ }
24
+ const toPoints = (rows) => rows.map((d) => ({ x: xPos(d), y: yScale(d[yf]), data: d }))
25
+
26
+ const sortByX = (rows) => [...rows].sort((a, b) => xPos(a) - xPos(b))
27
+
28
+ if (!cf) {
29
+ const sorted = sortByX(data)
30
+ const stroke = colors?.values().next().value?.stroke ?? '#888'
31
+ return [{ d: makeGen()(sorted), fill: 'none', stroke, points: toPoints(sorted) }]
32
+ }
33
+ const groups = groupBy(data, cf)
34
+ return [...groups.entries()].map(([key, rows]) => {
35
+ const sorted = sortByX(rows)
36
+ const colorEntry = colors?.get(key) ?? { fill: 'none', stroke: '#888' }
37
+ return {
38
+ d: makeGen()(sorted),
39
+ fill: 'none',
40
+ stroke: colorEntry.stroke,
41
+ points: toPoints(sorted),
42
+ key
43
+ }
44
+ })
45
+ }
46
+
47
+ function groupBy(arr, field) {
48
+ const map = new Map()
49
+ for (const item of arr) {
50
+ const key = item[field]
51
+ if (!map.has(key)) map.set(key, [])
52
+ map.get(key).push(item)
53
+ }
54
+ return map
55
+ }
@@ -0,0 +1,105 @@
1
+ import {
2
+ symbol,
3
+ symbolCircle,
4
+ symbolSquare,
5
+ symbolTriangle,
6
+ symbolDiamond,
7
+ symbolCross,
8
+ symbolStar
9
+ } from 'd3-shape'
10
+ import { defaultPreset } from '../../preset.js'
11
+
12
+ const SYMBOL_TYPES = [
13
+ symbolCircle,
14
+ symbolSquare,
15
+ symbolTriangle,
16
+ symbolDiamond,
17
+ symbolCross,
18
+ symbolStar
19
+ ]
20
+ const SYMBOL_NAMES = ['circle', 'square', 'triangle', 'diamond', 'cross', 'star']
21
+
22
+ /**
23
+ * Returns a Map assigning shape names to distinct values, cycling through available shapes.
24
+ * @param {unknown[]} values
25
+ * @param {typeof defaultPreset} preset
26
+ * @returns {Map<unknown, string>}
27
+ */
28
+ export function assignSymbols(values, preset = defaultPreset) {
29
+ const names = preset.symbols
30
+ return new Map(values.map((v, i) => [v, names[i % names.length]]))
31
+ }
32
+
33
+ /**
34
+ * Builds an SVG path string for a given shape name and radius.
35
+ * @param {string} shapeName
36
+ * @param {number} r
37
+ * @returns {string}
38
+ */
39
+ export function buildSymbolPath(shapeName, r) {
40
+ const idx = SYMBOL_NAMES.indexOf(shapeName)
41
+ const type = idx >= 0 ? SYMBOL_TYPES[idx] : symbolCircle
42
+ return (
43
+ symbol()
44
+ .type(type)
45
+ .size(Math.PI * r * r)() ?? ''
46
+ )
47
+ }
48
+
49
+ /**
50
+ * Returns a stable pseudo-random offset for a given index.
51
+ * Uses a linear congruential generator seeded by index — no external dependency,
52
+ * stable across re-renders.
53
+ * @param {number} i - row index (seed)
54
+ * @param {number} range - total spread (jitter is ±range/2)
55
+ * @returns {number}
56
+ */
57
+ export function jitterOffset(i, range) {
58
+ const r = ((i * 1664525 + 1013904223) >>> 0) / 0xffffffff
59
+ return (r - 0.5) * range
60
+ }
61
+
62
+ /**
63
+ * Builds point geometry for scatter/bubble charts.
64
+ * @param {Object[]} data
65
+ * @param {{ x: string, y: string, color?: string, size?: string, symbol?: string }} channels
66
+ * @param {Function} xScale
67
+ * @param {Function} yScale
68
+ * @param {Map} colors
69
+ * @param {Function|null} sizeScale
70
+ * @param {Map<unknown, string>|null} symbolMap — maps symbol field value → shape name
71
+ * @param {number} defaultRadius
72
+ * @param {{ width?: number, height?: number }|null} jitter
73
+ */
74
+ export function buildPoints(
75
+ data,
76
+ channels,
77
+ xScale,
78
+ yScale,
79
+ colors,
80
+ sizeScale,
81
+ symbolMap,
82
+ defaultRadius = 5,
83
+ jitter = null
84
+ ) {
85
+ const { x: xf, y: yf, color: cf, size: sf, symbol: symf } = channels
86
+ return data.map((d, i) => {
87
+ const colorKey = cf ? d[cf] : null
88
+ const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#444' }
89
+ const r = sf && sizeScale ? sizeScale(d[sf]) : defaultRadius
90
+ const shapeName = symf && symbolMap ? (symbolMap.get(d[symf]) ?? 'circle') : null
91
+ const symbolPath = shapeName ? buildSymbolPath(shapeName, r) : null
92
+ const jx = jitter?.width ? jitterOffset(i, jitter.width) : 0
93
+ const jy = jitter?.height ? jitterOffset(i + 100000, jitter.height) : 0
94
+ return {
95
+ data: d,
96
+ cx: xScale(d[xf]) + jx,
97
+ cy: yScale(d[yf]) + jy,
98
+ r,
99
+ fill: colorEntry.fill,
100
+ stroke: colorEntry.stroke,
101
+ symbolPath,
102
+ key: colorKey
103
+ }
104
+ })
105
+ }
@@ -0,0 +1,90 @@
1
+ import { line, curveCatmullRom } from 'd3-shape'
2
+
3
+ // Relative widths at each stat anchor (fraction of max half-width)
4
+ const DENSITY_AT = { iqr_min: 0.08, q1: 0.55, median: 1.0, q3: 0.55, iqr_max: 0.08 }
5
+ const ANCHOR_ORDER = ['iqr_max', 'q3', 'median', 'q1', 'iqr_min']
6
+
7
+ /**
8
+ * Builds a closed violin shape path for each group.
9
+ * Input rows must have { q1, median, q3, iqr_min, iqr_max } from applyBoxStat.
10
+ *
11
+ * When `fill` differs from `x`, violins are sub-grouped within each x-band
12
+ * (one narrower violin per fill value per x category, like grouped bars).
13
+ * Violin body uses the lighter fill shade; outline uses the darker stroke shade.
14
+ *
15
+ * @param {Object[]} data
16
+ * @param {{ x: string, fill?: string }} channels
17
+ * `fill` drives violin interior and outline (defaults to x-field).
18
+ * @param {import('d3-scale').ScaleBand} xScale
19
+ * @param {import('d3-scale').ScaleLinear} yScale
20
+ * @param {Map} colors
21
+ * @returns {Array}
22
+ */
23
+ export function buildViolins(data, channels, xScale, yScale, colors) {
24
+ const { x: xf, fill: ff } = channels
25
+ const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 40
26
+ const grouped = ff && ff !== xf
27
+
28
+ const pathGen = line()
29
+ .x((pt) => pt.x)
30
+ .y((pt) => pt.y)
31
+ .curve(curveCatmullRom.alpha(0.5))
32
+
33
+ if (grouped) {
34
+ const fillValues = [...new Set(data.map((d) => d[ff]))]
35
+ const n = fillValues.length
36
+ const subBandWidth = bw / n
37
+ const halfMax = subBandWidth * 0.45
38
+
39
+ return data.map((d) => {
40
+ const fillVal = d[ff]
41
+ const subIndex = fillValues.indexOf(fillVal)
42
+ const bandStart = xScale(d[xf]) ?? 0
43
+ const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2
44
+ const colorEntry = colors?.get(fillVal) ?? { fill: '#aaa', stroke: '#666' }
45
+
46
+ const rightPts = ANCHOR_ORDER.map((key) => ({
47
+ x: cx + halfMax * DENSITY_AT[key],
48
+ y: yScale(d[key])
49
+ }))
50
+ const leftPts = [...ANCHOR_ORDER].reverse().map((key) => ({
51
+ x: cx - halfMax * DENSITY_AT[key],
52
+ y: yScale(d[key])
53
+ }))
54
+
55
+ return {
56
+ data: d,
57
+ cx,
58
+ d: pathGen([...rightPts, ...leftPts, rightPts[0]]),
59
+ fill: colorEntry.fill,
60
+ stroke: colorEntry.stroke
61
+ }
62
+ })
63
+ }
64
+
65
+ // Non-grouped: one violin per x category
66
+ const halfMax = bw * 0.45
67
+
68
+ return data.map((d) => {
69
+ const fillKey = ff ? d[ff] : d[xf]
70
+ const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
71
+ const cx = (xScale(d[xf]) ?? 0) + (typeof xScale.bandwidth === 'function' ? bw / 2 : 0)
72
+
73
+ const rightPts = ANCHOR_ORDER.map((key) => ({
74
+ x: cx + halfMax * DENSITY_AT[key],
75
+ y: yScale(d[key])
76
+ }))
77
+ const leftPts = [...ANCHOR_ORDER].reverse().map((key) => ({
78
+ x: cx - halfMax * DENSITY_AT[key],
79
+ y: yScale(d[key])
80
+ }))
81
+
82
+ return {
83
+ data: d,
84
+ cx,
85
+ d: pathGen([...rightPts, ...leftPts, rightPts[0]]),
86
+ fill: colorEntry.fill,
87
+ stroke: colorEntry.stroke
88
+ }
89
+ })
90
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Converts a data key to a safe SVG element ID for pattern references.
3
+ * Spaces and non-word characters are replaced to avoid broken url(#...) refs.
4
+ * @param {unknown} key
5
+ * @returns {string}
6
+ */
7
+ export function toPatternId(key) {
8
+ return `chart-pat-${String(key)
9
+ .replace(/\s+/g, '-')
10
+ .replace(/[^\w-]/g, '_')}`
11
+ }
12
+
13
+ // Keys must match the keys in packages/chart/src/patterns/patterns.js
14
+ export const PATTERN_ORDER = [
15
+ 'diagonal',
16
+ 'dots',
17
+ 'triangles',
18
+ 'hatch',
19
+ 'lattice',
20
+ 'swell',
21
+ 'checkerboard',
22
+ 'waves',
23
+ 'petals',
24
+ 'brick',
25
+ 'diamonds',
26
+ 'tile',
27
+ 'scales',
28
+ 'circles',
29
+ 'pip',
30
+ 'rings',
31
+ 'chevrons',
32
+ 'shards',
33
+ 'wedge',
34
+ 'argyle',
35
+ 'shell'
36
+ ]
37
+
38
+ /**
39
+ * Assigns patterns from PATTERN_ORDER to an array of distinct values.
40
+ * @param {unknown[]} values
41
+ * @returns {Map<unknown, string>}
42
+ */
43
+ export function assignPatterns(values) {
44
+ return new Map(values.map((v, i) => [v, PATTERN_ORDER[i % PATTERN_ORDER.length]]))
45
+ }