@rokkit/chart 1.3.7 → 1.3.9

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 (162) hide show
  1. package/dist/AnimatedPlot.svelte +26 -8
  2. package/dist/AnimatedPlot.svelte.d.ts +5 -0
  3. package/dist/Chart.svelte +51 -65
  4. package/dist/FacetPlot.svelte +1 -1
  5. package/dist/Plot/Axis.svelte +19 -10
  6. package/dist/Plot/Root.svelte +67 -46
  7. package/dist/Plot/Root.svelte.d.ts +29 -2
  8. package/dist/Plot/Timeline.svelte +39 -13
  9. package/dist/Plot/index.d.ts +5 -5
  10. package/dist/Plot/index.js +5 -5
  11. package/dist/Plot.svelte +95 -178
  12. package/dist/Plot.svelte.d.ts +4 -0
  13. package/dist/PlotState.svelte.d.ts +29 -1
  14. package/dist/PlotState.svelte.js +163 -19
  15. package/dist/PlotSurface.svelte +162 -0
  16. package/dist/PlotSurface.svelte.d.ts +25 -0
  17. package/dist/charts/AreaChart.svelte +12 -2
  18. package/dist/charts/AreaChart.svelte.d.ts +6 -0
  19. package/dist/charts/BarChart.svelte +40 -4
  20. package/dist/charts/BarChart.svelte.d.ts +15 -1
  21. package/dist/charts/BoxPlot.svelte +7 -3
  22. package/dist/charts/BoxPlot.svelte.d.ts +2 -0
  23. package/dist/charts/BubbleChart.svelte +37 -4
  24. package/dist/charts/BubbleChart.svelte.d.ts +12 -1
  25. package/dist/charts/LineChart.svelte +4 -2
  26. package/dist/charts/LineChart.svelte.d.ts +1 -0
  27. package/dist/charts/PieChart.svelte +5 -3
  28. package/dist/charts/PieChart.svelte.d.ts +1 -0
  29. package/dist/charts/ScatterPlot.svelte +35 -4
  30. package/dist/charts/ScatterPlot.svelte.d.ts +11 -1
  31. package/dist/charts/ViolinPlot.svelte +7 -3
  32. package/dist/charts/ViolinPlot.svelte.d.ts +2 -0
  33. package/dist/examples/BarChartExample.svelte +1 -1
  34. package/dist/geoms/Arc.svelte +21 -54
  35. package/dist/geoms/Arc.svelte.d.ts +4 -4
  36. package/dist/geoms/Area.svelte +45 -44
  37. package/dist/geoms/Area.svelte.d.ts +8 -1
  38. package/dist/geoms/Bar.svelte +33 -45
  39. package/dist/geoms/Bar.svelte.d.ts +9 -0
  40. package/dist/geoms/Box.svelte +103 -61
  41. package/dist/geoms/Box.svelte.d.ts +12 -3
  42. package/dist/geoms/Candlestick.svelte +26 -75
  43. package/dist/geoms/Candlestick.svelte.d.ts +2 -0
  44. package/dist/geoms/Heatmap.svelte +21 -66
  45. package/dist/geoms/Heatmap.svelte.d.ts +2 -0
  46. package/dist/geoms/Hexbin.svelte +20 -100
  47. package/dist/geoms/Hexbin.svelte.d.ts +2 -0
  48. package/dist/geoms/Jitter.svelte +67 -0
  49. package/dist/geoms/Jitter.svelte.d.ts +17 -0
  50. package/dist/geoms/Line.svelte +27 -31
  51. package/dist/geoms/Line.svelte.d.ts +5 -0
  52. package/dist/geoms/Point.svelte +31 -60
  53. package/dist/geoms/Point.svelte.d.ts +5 -1
  54. package/dist/geoms/Ribbon.svelte +30 -124
  55. package/dist/geoms/Ribbon.svelte.d.ts +2 -0
  56. package/dist/geoms/Rule.svelte +117 -0
  57. package/dist/geoms/Rule.svelte.d.ts +19 -0
  58. package/dist/geoms/Violin.svelte +36 -31
  59. package/dist/geoms/Violin.svelte.d.ts +10 -3
  60. package/dist/geoms/Waterfall.svelte +25 -79
  61. package/dist/geoms/Waterfall.svelte.d.ts +2 -0
  62. package/dist/geoms/lib/GeomState.svelte.d.ts +51 -0
  63. package/dist/geoms/lib/GeomState.svelte.js +79 -0
  64. package/dist/geoms/lib/aesthetics.d.ts +36 -0
  65. package/dist/geoms/lib/aesthetics.js +45 -0
  66. package/dist/geoms/lib/areas.d.ts +6 -2
  67. package/dist/geoms/lib/areas.js +20 -12
  68. package/dist/geoms/lib/bars.d.ts +18 -3
  69. package/dist/geoms/lib/bars.js +80 -101
  70. package/dist/geoms/lib/marks/arc.d.ts +24 -0
  71. package/dist/geoms/lib/marks/arc.js +28 -0
  72. package/dist/geoms/lib/marks/area.d.ts +23 -0
  73. package/dist/geoms/lib/marks/area.js +38 -0
  74. package/dist/geoms/lib/marks/bar.d.ts +16 -0
  75. package/dist/geoms/lib/marks/bar.js +43 -0
  76. package/dist/geoms/lib/marks/box.d.ts +14 -0
  77. package/dist/geoms/lib/marks/box.js +30 -0
  78. package/dist/geoms/lib/marks/candlestick.d.ts +27 -0
  79. package/dist/geoms/lib/marks/candlestick.js +52 -0
  80. package/dist/geoms/lib/marks/heatmap.d.ts +24 -0
  81. package/dist/geoms/lib/marks/heatmap.js +41 -0
  82. package/dist/geoms/lib/marks/hexbin.d.ts +29 -0
  83. package/dist/geoms/lib/marks/hexbin.js +62 -0
  84. package/dist/geoms/lib/marks/jitter.d.ts +21 -0
  85. package/dist/geoms/lib/marks/jitter.js +36 -0
  86. package/dist/geoms/lib/marks/line.d.ts +26 -0
  87. package/dist/geoms/lib/marks/line.js +25 -0
  88. package/dist/geoms/lib/marks/point.d.ts +25 -0
  89. package/dist/geoms/lib/marks/point.js +58 -0
  90. package/dist/geoms/lib/marks/ribbon.d.ts +31 -0
  91. package/dist/geoms/lib/marks/ribbon.js +77 -0
  92. package/dist/geoms/lib/marks/violin.d.ts +13 -0
  93. package/dist/geoms/lib/marks/violin.js +31 -0
  94. package/dist/geoms/lib/marks/waterfall.d.ts +25 -0
  95. package/dist/geoms/lib/marks/waterfall.js +58 -0
  96. package/dist/index.d.ts +25 -12
  97. package/dist/index.js +26 -13
  98. package/dist/lib/brewing/marks/arcs.js +7 -1
  99. package/dist/lib/brewing/marks/boxes.d.ts +11 -2
  100. package/dist/lib/brewing/marks/boxes.js +32 -11
  101. package/dist/lib/brewing/marks/lines.d.ts +4 -1
  102. package/dist/lib/brewing/marks/lines.js +18 -15
  103. package/dist/lib/brewing/marks/swarm.d.ts +32 -0
  104. package/dist/lib/brewing/marks/swarm.js +125 -0
  105. package/dist/lib/brewing/marks/violins.d.ts +4 -20
  106. package/dist/lib/brewing/marks/violins.js +41 -25
  107. package/dist/lib/brewing/stats.d.ts +3 -1
  108. package/dist/lib/brewing/stats.js +28 -11
  109. package/dist/lib/plot/scales.js +12 -7
  110. package/dist/lib/plot/types.d.ts +9 -1
  111. package/dist/lib/plot/types.js +3 -1
  112. package/dist/lib/preset.d.ts +11 -1
  113. package/dist/lib/preset.js +16 -4
  114. package/package.json +1 -1
  115. package/dist/Plot/Arc.svelte +0 -30
  116. package/dist/Plot/Arc.svelte.d.ts +0 -18
  117. package/dist/Plot/Area.svelte +0 -54
  118. package/dist/Plot/Area.svelte.d.ts +0 -14
  119. package/dist/Plot/Bar.svelte +0 -69
  120. package/dist/Plot/Bar.svelte.d.ts +0 -11
  121. package/dist/Plot/Line.svelte +0 -54
  122. package/dist/Plot/Line.svelte.d.ts +0 -13
  123. package/dist/Plot/Point.svelte +0 -58
  124. package/dist/Plot/Point.svelte.d.ts +0 -13
  125. package/dist/lib/brewer.d.ts +0 -9
  126. package/dist/lib/brewer.js +0 -25
  127. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +0 -8
  128. package/dist/lib/brewing/BoxBrewer.svelte.js +0 -14
  129. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +0 -8
  130. package/dist/lib/brewing/CartesianBrewer.svelte.js +0 -21
  131. package/dist/lib/brewing/PieBrewer.svelte.d.ts +0 -8
  132. package/dist/lib/brewing/PieBrewer.svelte.js +0 -14
  133. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +0 -9
  134. package/dist/lib/brewing/QuartileBrewer.svelte.js +0 -51
  135. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +0 -8
  136. package/dist/lib/brewing/ViolinBrewer.svelte.js +0 -14
  137. package/dist/lib/brewing/axes.svelte.d.ts +0 -66
  138. package/dist/lib/brewing/axes.svelte.js +0 -270
  139. package/dist/lib/brewing/bars.svelte.d.ts +0 -56
  140. package/dist/lib/brewing/bars.svelte.js +0 -201
  141. package/dist/lib/brewing/brewer.svelte.d.ts +0 -124
  142. package/dist/lib/brewing/brewer.svelte.js +0 -277
  143. package/dist/lib/brewing/dimensions.svelte.d.ts +0 -35
  144. package/dist/lib/brewing/dimensions.svelte.js +0 -56
  145. package/dist/lib/brewing/index.svelte.d.ts +0 -118
  146. package/dist/lib/brewing/index.svelte.js +0 -205
  147. package/dist/lib/brewing/legends.svelte.d.ts +0 -48
  148. package/dist/lib/brewing/legends.svelte.js +0 -137
  149. package/dist/lib/brewing/marks/areas.d.ts +0 -31
  150. package/dist/lib/brewing/marks/areas.js +0 -72
  151. package/dist/lib/brewing/marks/bars.d.ts +0 -1
  152. package/dist/lib/brewing/marks/bars.js +0 -49
  153. package/dist/lib/brewing/scales.svelte.d.ts +0 -24
  154. package/dist/lib/brewing/scales.svelte.js +0 -82
  155. package/dist/lib/context.d.ts +0 -13
  156. package/dist/lib/context.js +0 -131
  157. package/dist/lib/plot/chartProps.d.ts +0 -177
  158. package/dist/lib/plot/chartProps.js +0 -76
  159. package/dist/lib/scales.svelte.d.ts +0 -35
  160. package/dist/lib/scales.svelte.js +0 -151
  161. package/dist/lib/ticks.d.ts +0 -36
  162. package/dist/lib/ticks.js +0 -46
@@ -1,5 +1,5 @@
1
1
  import { scaleBand } from 'd3-scale'
2
- import { stack } from 'd3-shape'
2
+ import { stack, stackOffsetExpand } from 'd3-shape'
3
3
  import { toPatternId } from '../../lib/brewing/patterns.js'
4
4
  import { isLiteralColor } from '../../lib/brewing/colors.js'
5
5
 
@@ -17,14 +17,15 @@ function ensureBandX(xScale, data, xField) {
17
17
  }
18
18
 
19
19
  /**
20
- * Returns the sub-band fields: distinct non-x fields among [color, pattern].
21
- * These are the fields that cause multiple bars within a single x-band.
20
+ * Returns the sub-band fields: distinct non-x fields among [group, color, pattern].
21
+ * These are the fields that cause multiple bars within a single x-band (dodge/grouping).
22
+ * `group` (the explicit grouping channel) takes precedence as the first sub-band.
22
23
  */
23
24
  function subBandFields(channels) {
24
- const { x: xf, color: cf, pattern: pf } = channels
25
+ const { x: xf, group: gf, color: cf, pattern: pf } = channels
25
26
  const seen = new Set()
26
27
  const out = []
27
- for (const f of [cf, pf]) {
28
+ for (const f of [gf, cf, pf]) {
28
29
  if (f && f !== xf && !seen.has(f) && !isLiteralColor(f)) {
29
30
  seen.add(f)
30
31
  out.push(f)
@@ -33,27 +34,65 @@ function subBandFields(channels) {
33
34
  return out
34
35
  }
35
36
 
36
- export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
37
+ /**
38
+ * The one bar builder. Expresses each bar in abstract (category, value) space and maps it to
39
+ * screen via `place()`, so it renders vertical or horizontal (flip) with the same code.
40
+ *
41
+ * Category (x) positioning has two modes:
42
+ * - band (default): a scaleBand slots each category; sub-bands split grouped series.
43
+ * - continuous (`continuousCategory`): a LINEAR position scale (e.g. a bar-chart race's tweened
44
+ * `_rank`). Each bar centres on `xScale(pos)` with a fixed thickness from the scale step, so a
45
+ * fractional position tweens smoothly (a band scale can only jump between slots). No sub-banding.
46
+ */
47
+ export function buildBars(
48
+ data,
49
+ channels,
50
+ xScale,
51
+ yScale,
52
+ colors,
53
+ innerHeight,
54
+ patterns,
55
+ place = (x, y) => ({ x, y }),
56
+ continuousCategory = false,
57
+ dodge = true
58
+ ) {
37
59
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
38
60
 
39
- const bandScale = ensureBandX(xScale, data, xf)
40
-
41
- // Sub-banding: only fields that differ from x drive grouping within a band
42
- const subFields = subBandFields(channels)
61
+ // No sub-banding for a continuous (race) axis or position='identity' (dodge=false → overlap).
62
+ const subFields = continuousCategory || !dodge ? [] : subBandFields(channels)
43
63
  const getSubKey = (d) => subFields.map((f) => String(d[f])).join('::')
44
- const subDomain = subFields.length > 0 ? [...new Set(data.map(getSubKey))] : []
45
- const subScale =
46
- subDomain.length > 1
47
- ? scaleBand().domain(subDomain).range([0, bandScale.bandwidth()]).padding(0.05)
48
- : null
49
-
50
- // Baseline for bars: at y=0 when domain spans zero (supports negative bars),
51
- // otherwise at the bottom of the chart area.
64
+
65
+ let positionOf
66
+ let thicknessOf
67
+ if (continuousCategory) {
68
+ const [r0, r1] = xScale.range()
69
+ const [d0, d1] = xScale.domain()
70
+ const step = Math.max(Math.abs(r0 - r1) / Math.max(d1 - d0, 1), 1)
71
+ const thickness = step * 0.9
72
+ thicknessOf = () => thickness
73
+ positionOf = (d) => (xScale(Number(d[xf])) ?? 0) - thickness / 2
74
+ } else {
75
+ const bandScale = ensureBandX(xScale, data, xf)
76
+ const subDomain = subFields.length > 0 ? [...new Set(data.map(getSubKey))] : []
77
+ const subScale =
78
+ subDomain.length > 1
79
+ ? scaleBand().domain(subDomain).range([0, bandScale.bandwidth()]).padding(0.05)
80
+ : null
81
+ thicknessOf = () => (subScale ? subScale.bandwidth() : bandScale.bandwidth())
82
+ positionOf = (d) => {
83
+ const bandX = bandScale(d[xf]) ?? 0
84
+ const subX = subScale ? (subScale(getSubKey(d)) ?? 0) : 0
85
+ return bandX + subX
86
+ }
87
+ }
88
+
89
+ // Value baseline: at value=0 when the domain spans zero (supports negative bars), else the
90
+ // value-axis origin (yScale.range()[0] — innerHeight when vertical, 0 when flipped).
52
91
  const yDomain = typeof yScale.bandwidth !== 'function' ? yScale.domain?.() : null
53
92
  const baseline =
54
93
  yDomain && yDomain[0] <= 0 && yDomain[yDomain.length - 1] >= 0
55
- ? (yScale(0) ?? innerHeight)
56
- : innerHeight
94
+ ? (yScale(0) ?? yScale.range()[0])
95
+ : yScale.range()[0]
57
96
 
58
97
  return data.map((d, i) => {
59
98
  const xVal = d[xf]
@@ -68,19 +107,26 @@ export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHe
68
107
  ? toPatternId(String(patternKey))
69
108
  : null
70
109
 
71
- const bandX = bandScale(xVal) ?? 0
72
- const subX = subScale && subKey ? (subScale(subKey) ?? 0) : 0
73
- const barX = bandX + subX
74
- const barWidth = subScale ? subScale.bandwidth() : bandScale.bandwidth()
75
- const barY = yScale(d[yf]) ?? baseline
110
+ const barBand = positionOf(d)
111
+ const barThickness = thicknessOf(d)
112
+ const barValue = yScale(d[yf]) ?? baseline
113
+ // Two opposite corners in (band, value) space, placed to screen — transposes under flip.
114
+ const c1 = place(barBand, baseline)
115
+ const c2 = place(barBand + barThickness, barValue)
116
+
117
+ // Continuous mode keys by _entity so Svelte reuses each bar as its position tweens (a
118
+ // race's rows reorder every frame); band mode keys by category + sub-band + index.
119
+ const key = continuousCategory
120
+ ? `${String(d._entity ?? xVal)}::${String(colorKey ?? '')}`
121
+ : `${String(xVal)}::${subKey}::${i}`
76
122
 
77
123
  return {
78
124
  data: d,
79
- key: `${String(xVal)}::${subKey}::${i}`,
80
- x: barX,
81
- y: Math.min(barY, baseline),
82
- width: barWidth,
83
- height: Math.abs(baseline - barY),
125
+ key,
126
+ x: Math.min(c1.x, c2.x),
127
+ y: Math.min(c1.y, c2.y),
128
+ width: Math.abs(c2.x - c1.x),
129
+ height: Math.abs(c2.y - c1.y),
84
130
  fill: colorEntry.fill,
85
131
  stroke: colorEntry.stroke,
86
132
  patternId
@@ -88,7 +134,7 @@ export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHe
88
134
  })
89
135
  }
90
136
 
91
- export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
137
+ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns, place = (x, y) => ({ x, y }), normalize = false) {
92
138
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
93
139
 
94
140
  const bandScale = ensureBandX(xScale, data, xf)
@@ -96,7 +142,7 @@ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHe
96
142
  // Stack dimension: first non-x grouping field (prefer pattern, then color)
97
143
  const subFields = subBandFields(channels)
98
144
  if (subFields.length === 0) {
99
- return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
145
+ return buildBars(data, channels, xScale, yScale, colors, innerHeight, patterns, place)
100
146
  }
101
147
  const stackField = subFields[0]
102
148
 
@@ -116,6 +162,8 @@ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHe
116
162
  })
117
163
 
118
164
  const stackGen = stack().keys(stackCategories)
165
+ // position='fill' → normalize each x column to [0,1] (100% stacked).
166
+ if (normalize) stackGen.offset(stackOffsetExpand)
119
167
  const layers = stackGen(wide)
120
168
 
121
169
  const bars = []
@@ -152,72 +200,3 @@ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHe
152
200
  }
153
201
  return bars
154
202
  }
155
-
156
- export function buildHorizontalBars(data, channels, xScale, yScale, colors, _innerHeight) {
157
- const { x: xf, y: yf, color: cf } = channels
158
- const isBand = typeof yScale?.bandwidth === 'function'
159
-
160
- if (!isBand) {
161
- // Linear y-scale: rank-based bar chart race.
162
- // Each row has a numeric _rank that tweens smoothly between frames.
163
- // No sub-grouping — each rank position holds exactly one entity.
164
- const [r0, r1] = yScale.range() // [innerHeight, 0]
165
- const [d0, d1] = yScale.domain() // [0, N-1]
166
- const step = Math.max(Math.abs(r0 - r1) / Math.max(d1 - d0, 1), 1)
167
- const barH = step * 0.9
168
-
169
- return data.map((d) => {
170
- const rankVal = Number(d[yf])
171
- const colorKey = cf ? d[cf] : null
172
- const colorEntry =
173
- colors?.get(colorKey) ??
174
- colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
175
-
176
- const centerY = yScale(rankVal) ?? 0
177
- return {
178
- data: d,
179
- // Key by entity name (_entity) so Svelte reuses elements as rank tweens
180
- key: `${String(d._entity ?? d[yf])}::${String(colorKey ?? '')}`,
181
- x: 0,
182
- y: centerY - barH / 2,
183
- width: xScale(d[xf]) ?? 0,
184
- height: barH,
185
- fill: colorEntry.fill,
186
- stroke: colorEntry.stroke,
187
- patternId: null
188
- }
189
- })
190
- }
191
-
192
- // Band scale: standard grouped horizontal bars.
193
- // Only create sub-bands when multiple entities share the same y-band (true grouping).
194
- const yVals = new Set(data.map((d) => d[yf]))
195
- const colorKeys = cf && !isLiteralColor(cf) ? [...new Set(data.map((d) => d[cf]))] : []
196
- const hasSubBands = colorKeys.length > 1 && data.length > yVals.size
197
- const subScale = hasSubBands
198
- ? scaleBand().domain(colorKeys).range([0, yScale.bandwidth()]).padding(0.05)
199
- : null
200
-
201
- return data.map((d) => {
202
- const yVal = d[yf]
203
- const colorKey = cf ? d[cf] : null
204
- const colorEntry =
205
- colors?.get(colorKey) ??
206
- colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
207
-
208
- const bandY = yScale(yVal) ?? 0
209
- const subY = subScale ? (subScale(colorKey) ?? 0) : 0
210
-
211
- return {
212
- data: d,
213
- key: `${String(yVal)}::${String(colorKey ?? '')}`,
214
- x: 0,
215
- y: bandY + subY,
216
- width: xScale(d[xf]) ?? 0,
217
- height: subScale ? subScale.bandwidth() : yScale.bandwidth(),
218
- fill: colorEntry.fill,
219
- stroke: colorEntry.stroke,
220
- patternId: null
221
- }
222
- })
223
- }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Build renderable pie/donut arcs. Slice interior + border come from `fill ?? color`
3
+ * through the shared palette (buildArcs already yields fill + a darker stroke). Adds the
4
+ * geom's fixed `alpha`.
5
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
6
+ */
7
+ export function buildArcMarks({ data, plot, channels, options, alpha, type }: {
8
+ data: any[];
9
+ plot: any;
10
+ channels: any;
11
+ options?: any;
12
+ alpha?: number;
13
+ type?: string;
14
+ }): {
15
+ alpha: number;
16
+ d: string | null;
17
+ fill: string;
18
+ stroke: string;
19
+ key: unknown;
20
+ patternId: string | null;
21
+ pct: number;
22
+ centroid: [number, number];
23
+ data: Record<string, unknown>;
24
+ }[];
@@ -0,0 +1,28 @@
1
+ import { buildArcs } from '../../../lib/brewing/marks/arcs.js'
2
+ import { resolveAlpha } from '../aesthetics.js'
3
+
4
+ /**
5
+ * Build renderable pie/donut arcs. Slice interior + border come from `fill ?? color`
6
+ * through the shared palette (buildArcs already yields fill + a darker stroke). Adds the
7
+ * geom's fixed `alpha`.
8
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
9
+ */
10
+ export function buildArcMarks({ data, plot, channels, options = {}, alpha, type = 'arc' }) {
11
+ if (!data?.length) return []
12
+ const interiorField = channels.fill ?? channels.color
13
+ // Guard: skip until data catches up after a fill-field change — otherwise a stale row set
14
+ // (missing the new field) produces all-undefined keys and duplicate-key errors.
15
+ if (interiorField && data[0] && !(interiorField in data[0])) return []
16
+
17
+ const arcs = buildArcs(
18
+ data,
19
+ { color: interiorField, y: channels.y, pattern: channels.pattern },
20
+ plot.colors,
21
+ plot.innerWidth,
22
+ plot.innerHeight,
23
+ { innerRadius: options.innerRadius ?? 0 },
24
+ plot.patterns
25
+ )
26
+ const a = resolveAlpha(alpha, type, plot.chartPreset)
27
+ return arcs.map((arc) => ({ ...arc, alpha: a }))
28
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Build renderable area segments. Interior is grouped/filled by `fill ?? color`
3
+ * (ggplot geom_area's fill). A border stroke is drawn only when BOTH `fill` and `color`
4
+ * are set — so an old `color`-only area stays border-less (backward compatible); the
5
+ * border uses the interior group's darker stroke shade. Per-field border coloring
6
+ * (fill ≠ color, two legends) is the deferred dual-scale follow-up.
7
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
8
+ */
9
+ export function buildAreaMarks({ data, plot, channels, options, alpha, type }: {
10
+ data: any[];
11
+ plot: any;
12
+ channels: any;
13
+ options?: any;
14
+ alpha?: number;
15
+ type?: string;
16
+ }): {
17
+ stroke: any;
18
+ alpha: number;
19
+ d: string;
20
+ fill: string;
21
+ key: unknown;
22
+ patternId: string | null;
23
+ }[];
@@ -0,0 +1,38 @@
1
+ import { buildAreas, buildStackedAreas } from '../areas.js'
2
+ import { resolveAlpha } from '../aesthetics.js'
3
+
4
+ /**
5
+ * Build renderable area segments. Interior is grouped/filled by `fill ?? color`
6
+ * (ggplot geom_area's fill). A border stroke is drawn only when BOTH `fill` and `color`
7
+ * are set — so an old `color`-only area stays border-less (backward compatible); the
8
+ * border uses the interior group's darker stroke shade. Per-field border coloring
9
+ * (fill ≠ color, two legends) is the deferred dual-scale follow-up.
10
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
11
+ */
12
+ export function buildAreaMarks({ data, plot, channels, options = {}, alpha, type = 'area' }) {
13
+ const { xScale, yScale, colors, patterns } = plot
14
+ if (!data?.length || !xScale || !yScale || !channels.x || !channels.y) return []
15
+
16
+ const interiorField = channels.fill ?? channels.color
17
+ const hasBorder = Boolean(channels.fill && channels.color)
18
+ const place = plot.place.bind(plot)
19
+ const areaChannels = {
20
+ x: channels.x,
21
+ y: channels.y,
22
+ color: interiorField,
23
+ pattern: channels.pattern
24
+ }
25
+ // Areas don't dodge; position selects stack | fill (100%) | identity (overlap, default).
26
+ const position = options.position ?? (options.stack ? 'stack' : 'identity')
27
+ const stacked = position === 'stack' || position === 'fill'
28
+ const raw = stacked
29
+ ? buildStackedAreas(data, areaChannels, xScale, yScale, colors, options.curve, patterns, place, position === 'fill')
30
+ : buildAreas(data, areaChannels, xScale, yScale, colors, options.curve, patterns, place)
31
+
32
+ const a = resolveAlpha(alpha, type, plot.chartPreset)
33
+ return raw.map((seg) => ({
34
+ ...seg,
35
+ stroke: hasBorder ? (colors.get(seg.key)?.stroke ?? 'none') : 'none',
36
+ alpha: a
37
+ }))
38
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Build renderable bar marks. Interior is grouped/filled by `fill ?? color`; the `group`
3
+ * channel (default `fill ?? color`) drives the sub-series partitioned by `position`:
4
+ * - stack: cumulative segments - dodge: side-by-side sub-bands (default)
5
+ * - fill: 100% (normalized) stack - identity: overlapping full-width bars
6
+ * `options.stack: true` maps to position 'stack' (back-compat). Adds the geom's fixed `alpha`.
7
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
8
+ */
9
+ export function buildBarMarks({ data, plot, channels, options, alpha, type }: {
10
+ data: any[];
11
+ plot: any;
12
+ channels: any;
13
+ options?: any;
14
+ alpha?: number;
15
+ type?: string;
16
+ }): any;
@@ -0,0 +1,43 @@
1
+ import { buildBars, buildStackedBars } from '../bars.js'
2
+ import { resolveAlpha } from '../aesthetics.js'
3
+
4
+ /**
5
+ * Build renderable bar marks. Interior is grouped/filled by `fill ?? color`; the `group`
6
+ * channel (default `fill ?? color`) drives the sub-series partitioned by `position`:
7
+ * - stack: cumulative segments - dodge: side-by-side sub-bands (default)
8
+ * - fill: 100% (normalized) stack - identity: overlapping full-width bars
9
+ * `options.stack: true` maps to position 'stack' (back-compat). Adds the geom's fixed `alpha`.
10
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
11
+ */
12
+ export function buildBarMarks({ data, plot, channels, options = {}, alpha, type = 'bar' }) {
13
+ const { xScale, yScale, colors, patterns } = plot
14
+ if (!data?.length || !xScale || !yScale) return []
15
+
16
+ const interiorField = channels.fill ?? channels.color
17
+ const groupField = channels.group ?? channels.fill ?? channels.color
18
+ const barChannels = {
19
+ x: channels.x,
20
+ y: channels.y,
21
+ color: interiorField,
22
+ pattern: channels.pattern,
23
+ group: groupField
24
+ }
25
+ const place = plot.place.bind(plot)
26
+ const ih = plot.innerHeight
27
+ const position = options.position ?? (options.stack ? 'stack' : 'dodge')
28
+
29
+ let raw
30
+ if (position === 'stack') {
31
+ raw = buildStackedBars(data, barChannels, xScale, yScale, colors, ih, patterns, place, false)
32
+ } else if (position === 'fill') {
33
+ raw = buildStackedBars(data, barChannels, xScale, yScale, colors, ih, patterns, place, true)
34
+ } else if (position === 'identity') {
35
+ raw = buildBars(data, barChannels, xScale, yScale, colors, ih, patterns, place, plot.continuousCategory, false)
36
+ } else {
37
+ // dodge (default): side-by-side sub-bands
38
+ raw = buildBars(data, barChannels, xScale, yScale, colors, ih, patterns, place, plot.continuousCategory, true)
39
+ }
40
+
41
+ const a = resolveAlpha(alpha, type, plot.chartPreset)
42
+ return raw.map((bar) => ({ ...bar, alpha: a }))
43
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build renderable box marks. Body fill + outline (whiskers/median/caps) come from the
3
+ * `fill` channel (defaulting to `x`); an explicit `color` channel overrides the outline
4
+ * stroke (interior stays from `fill`). Adds the geom's fixed `alpha`.
5
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
6
+ */
7
+ export function buildBoxMarks({ data, plot, channels, options, alpha, type }: {
8
+ data: any[];
9
+ plot: any;
10
+ channels: any;
11
+ options?: any;
12
+ alpha?: number;
13
+ type?: string;
14
+ }): any[];
@@ -0,0 +1,30 @@
1
+ import { buildBoxes } from '../../../lib/brewing/marks/boxes.js'
2
+ import { resolveAlpha } from '../aesthetics.js'
3
+
4
+ /**
5
+ * Build renderable box marks. Body fill + outline (whiskers/median/caps) come from the
6
+ * `fill` channel (defaulting to `x`); an explicit `color` channel overrides the outline
7
+ * stroke (interior stays from `fill`). Adds the geom's fixed `alpha`.
8
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
9
+ */
10
+ export function buildBoxMarks({ data, plot, channels, options = {}, alpha, type = 'box' }) {
11
+ const { xScale, yScale, colors } = plot
12
+ if (!data?.length || !xScale || !yScale) return []
13
+
14
+ const fillField = channels.fill ?? channels.x
15
+ const boxes = buildBoxes(
16
+ data,
17
+ { x: channels.x, fill: fillField, pattern: channels.pattern },
18
+ xScale,
19
+ yScale,
20
+ colors,
21
+ { side: options.side, width: options.width, patterns: plot.patterns }
22
+ )
23
+ const a = resolveAlpha(alpha, type, plot.chartPreset)
24
+ const colorField = channels.color
25
+ return boxes.map((box) => ({
26
+ ...box,
27
+ stroke: colorField ? (colors.get(box.data[colorField])?.stroke ?? box.stroke) : box.stroke,
28
+ alpha: a
29
+ }))
30
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Build renderable candlesticks. Body + wick color is semantic (up/down from options), not
3
+ * the shared palette. Adds fixed `alpha`. Places (band, value) corners through place() so the
4
+ * candle transposes under orientation flip.
5
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
6
+ */
7
+ export function buildCandleMarks({ data, plot, channels, options, alpha, type }: {
8
+ data: any[];
9
+ plot: any;
10
+ channels: any;
11
+ options?: any;
12
+ alpha?: number;
13
+ type?: string;
14
+ }): {
15
+ key: string;
16
+ bodyX: number;
17
+ bodyY: number;
18
+ bodyWidth: number;
19
+ bodyHeight: number;
20
+ fill: any;
21
+ alpha: number;
22
+ wickX1: any;
23
+ wickY1: any;
24
+ wickX2: any;
25
+ wickY2: any;
26
+ data: any;
27
+ }[];
@@ -0,0 +1,52 @@
1
+ import { resolveAlpha } from '../aesthetics.js'
2
+
3
+ /**
4
+ * Build renderable candlesticks. Body + wick color is semantic (up/down from options), not
5
+ * the shared palette. Adds fixed `alpha`. Places (band, value) corners through place() so the
6
+ * candle transposes under orientation flip.
7
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
8
+ */
9
+ export function buildCandleMarks({ data, plot, channels, options = {}, alpha, type = 'candlestick' }) {
10
+ const { xScale, yScale } = plot
11
+ if (!data?.length || !xScale || !yScale) return []
12
+
13
+ const open = options.open ?? 'open'
14
+ const high = options.high ?? 'high'
15
+ const low = options.low ?? 'low'
16
+ const close = options.close ?? 'close'
17
+ const upColor = options.upColor ?? '#22c55e'
18
+ const downColor = options.downColor ?? '#ef4444'
19
+ const a = resolveAlpha(alpha, type, plot.chartPreset)
20
+
21
+ const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 10
22
+ const bodyWidth = bw * 0.6
23
+ const bodyOffset = (bw - bodyWidth) / 2
24
+
25
+ return data.map((d, i) => {
26
+ const xPos = (xScale(d[channels.x ?? '']) ?? 0) + bodyOffset
27
+ const openVal = Number(d[open])
28
+ const closeVal = Number(d[close])
29
+ const isUp = closeVal >= openVal
30
+ const bodyTop = yScale(isUp ? closeVal : openVal)
31
+ const bodyBottom = yScale(isUp ? openVal : closeVal)
32
+
33
+ const b1 = plot.place(xPos, bodyTop)
34
+ const b2 = plot.place(xPos + bodyWidth, bodyBottom)
35
+ const wa = plot.place(xPos + bodyWidth / 2, yScale(Number(d[high])))
36
+ const wb = plot.place(xPos + bodyWidth / 2, yScale(Number(d[low])))
37
+ return {
38
+ key: `${d[channels.x ?? '']}-${i}`,
39
+ bodyX: Math.min(b1.x, b2.x),
40
+ bodyY: Math.min(b1.y, b2.y),
41
+ bodyWidth: Math.abs(b2.x - b1.x),
42
+ bodyHeight: Math.max(1, Math.abs(b2.y - b1.y)),
43
+ fill: isUp ? upColor : downColor,
44
+ alpha: a,
45
+ wickX1: wa.x,
46
+ wickY1: wa.y,
47
+ wickX2: wb.x,
48
+ wickY2: wb.y,
49
+ data: d
50
+ }
51
+ })
52
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Build renderable heatmap cells. Cell fill is the continuous color scale (numeric color
3
+ * field) or the categorical palette entry, from `fill ?? color`. Adds fixed `alpha`.
4
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
5
+ */
6
+ export function buildHeatmapMarks({ data, plot, channels, alpha, type }: {
7
+ data: any[];
8
+ plot: any;
9
+ channels: any;
10
+ options?: any;
11
+ alpha?: number;
12
+ type?: string;
13
+ }): {
14
+ key: string;
15
+ x: any;
16
+ y: any;
17
+ width: any;
18
+ height: any;
19
+ fill: string;
20
+ alpha: number;
21
+ value: any;
22
+ display: string;
23
+ data: any;
24
+ }[];
@@ -0,0 +1,41 @@
1
+ import { resolveAlpha } from '../aesthetics.js'
2
+
3
+ /**
4
+ * Build renderable heatmap cells. Cell fill is the continuous color scale (numeric color
5
+ * field) or the categorical palette entry, from `fill ?? color`. Adds fixed `alpha`.
6
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
7
+ */
8
+ export function buildHeatmapMarks({ data, plot, channels, alpha, type = 'heatmap' }) {
9
+ const { xScale, yScale, colors } = plot
10
+ if (!data?.length || !xScale || !yScale) return []
11
+
12
+ const colorField = channels.fill ?? channels.color
13
+ const continuous = plot.continuousColorScale
14
+ const bwX = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 0
15
+ const bwY = typeof yScale.bandwidth === 'function' ? yScale.bandwidth() : 0
16
+ const a = resolveAlpha(alpha, type, plot.chartPreset)
17
+
18
+ return data.map((d, i) => {
19
+ const xVal = d[channels.x ?? '']
20
+ const yVal = d[channels.y ?? '']
21
+ const colorVal = colorField ? d[colorField] : null
22
+ let fill = '#ccc'
23
+ if (continuous) fill = continuous.scale(Number(colorVal))
24
+ else {
25
+ const entry = colors?.get(colorVal)
26
+ if (entry) fill = entry.fill
27
+ }
28
+ return {
29
+ key: `${xVal}-${yVal}-${i}`,
30
+ x: xScale(xVal) ?? 0,
31
+ y: yScale(yVal) ?? 0,
32
+ width: bwX,
33
+ height: bwY,
34
+ fill,
35
+ alpha: a,
36
+ value: colorVal,
37
+ display: String(colorVal ?? ''),
38
+ data: d
39
+ }
40
+ })
41
+ }
@@ -0,0 +1,29 @@
1
+ /** SVG path for a pointy-top hexagon of radius r, centered at the origin. */
2
+ export function hexPath(r: any): string;
3
+ /**
4
+ * Build renderable hex bins. Fill is the continuous color scale (by count) or a density-ramp
5
+ * rgba fallback. `alpha` is a flat multiplier on top (default 1 → unchanged).
6
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
7
+ */
8
+ export function buildHexbinMarks({ data, plot, channels, options, alpha, type }: {
9
+ data: any[];
10
+ plot: any;
11
+ channels: any;
12
+ options?: any;
13
+ alpha?: number;
14
+ type?: string;
15
+ }): {
16
+ key: string;
17
+ cx: any;
18
+ cy: any;
19
+ count: any;
20
+ fill: any;
21
+ alpha: number;
22
+ value: any;
23
+ display: string;
24
+ data: {
25
+ count: any;
26
+ x: any;
27
+ y: any;
28
+ };
29
+ }[];