@rokkit/chart 1.3.8 → 1.3.10

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 (140) hide show
  1. package/dist/Chart.svelte +50 -64
  2. package/dist/Plot/Root.svelte +4 -1
  3. package/dist/Plot/Root.svelte.d.ts +2 -0
  4. package/dist/Plot/index.d.ts +1 -1
  5. package/dist/Plot/index.js +1 -1
  6. package/dist/Plot.svelte +4 -1
  7. package/dist/Plot.svelte.d.ts +2 -0
  8. package/dist/PlotState.svelte.d.ts +14 -2
  9. package/dist/PlotState.svelte.js +81 -5
  10. package/dist/charts/AreaChart.svelte +8 -1
  11. package/dist/charts/AreaChart.svelte.d.ts +5 -0
  12. package/dist/charts/BarChart.svelte +8 -1
  13. package/dist/charts/BarChart.svelte.d.ts +5 -0
  14. package/dist/charts/BoxPlot.svelte +3 -1
  15. package/dist/charts/BoxPlot.svelte.d.ts +1 -0
  16. package/dist/charts/BubbleChart.svelte +3 -1
  17. package/dist/charts/BubbleChart.svelte.d.ts +1 -0
  18. package/dist/charts/LineChart.svelte +3 -1
  19. package/dist/charts/LineChart.svelte.d.ts +1 -0
  20. package/dist/charts/PieChart.svelte +4 -2
  21. package/dist/charts/PieChart.svelte.d.ts +1 -0
  22. package/dist/charts/ScatterPlot.svelte +3 -1
  23. package/dist/charts/ScatterPlot.svelte.d.ts +1 -0
  24. package/dist/charts/ViolinPlot.svelte +3 -1
  25. package/dist/charts/ViolinPlot.svelte.d.ts +1 -0
  26. package/dist/geoms/Arc.svelte +21 -55
  27. package/dist/geoms/Arc.svelte.d.ts +4 -4
  28. package/dist/geoms/Area.svelte +45 -44
  29. package/dist/geoms/Area.svelte.d.ts +8 -1
  30. package/dist/geoms/Bar.svelte +33 -56
  31. package/dist/geoms/Bar.svelte.d.ts +9 -0
  32. package/dist/geoms/Box.svelte +118 -131
  33. package/dist/geoms/Box.svelte.d.ts +5 -3
  34. package/dist/geoms/Candlestick.svelte +22 -77
  35. package/dist/geoms/Candlestick.svelte.d.ts +2 -0
  36. package/dist/geoms/Heatmap.svelte +21 -66
  37. package/dist/geoms/Heatmap.svelte.d.ts +2 -0
  38. package/dist/geoms/Hexbin.svelte +20 -100
  39. package/dist/geoms/Hexbin.svelte.d.ts +2 -0
  40. package/dist/geoms/Jitter.svelte +21 -36
  41. package/dist/geoms/Jitter.svelte.d.ts +5 -3
  42. package/dist/geoms/Line.svelte +27 -31
  43. package/dist/geoms/Line.svelte.d.ts +5 -0
  44. package/dist/geoms/Point.svelte +31 -66
  45. package/dist/geoms/Point.svelte.d.ts +5 -1
  46. package/dist/geoms/Ribbon.svelte +30 -124
  47. package/dist/geoms/Ribbon.svelte.d.ts +2 -0
  48. package/dist/geoms/Rule.svelte +5 -1
  49. package/dist/geoms/Rule.svelte.d.ts +3 -0
  50. package/dist/geoms/Violin.svelte +21 -40
  51. package/dist/geoms/Violin.svelte.d.ts +5 -3
  52. package/dist/geoms/Waterfall.svelte +19 -80
  53. package/dist/geoms/Waterfall.svelte.d.ts +2 -0
  54. package/dist/geoms/lib/GeomState.svelte.d.ts +51 -0
  55. package/dist/geoms/lib/GeomState.svelte.js +79 -0
  56. package/dist/geoms/lib/aesthetics.d.ts +36 -0
  57. package/dist/geoms/lib/aesthetics.js +45 -0
  58. package/dist/geoms/lib/areas.d.ts +1 -1
  59. package/dist/geoms/lib/areas.js +18 -5
  60. package/dist/geoms/lib/bars.d.ts +2 -2
  61. package/dist/geoms/lib/bars.js +37 -17
  62. package/dist/geoms/lib/marks/arc.d.ts +24 -0
  63. package/dist/geoms/lib/marks/arc.js +28 -0
  64. package/dist/geoms/lib/marks/area.d.ts +23 -0
  65. package/dist/geoms/lib/marks/area.js +38 -0
  66. package/dist/geoms/lib/marks/bar.d.ts +16 -0
  67. package/dist/geoms/lib/marks/bar.js +43 -0
  68. package/dist/geoms/lib/marks/box.d.ts +14 -0
  69. package/dist/geoms/lib/marks/box.js +30 -0
  70. package/dist/geoms/lib/marks/candlestick.d.ts +27 -0
  71. package/dist/geoms/lib/marks/candlestick.js +52 -0
  72. package/dist/geoms/lib/marks/heatmap.d.ts +24 -0
  73. package/dist/geoms/lib/marks/heatmap.js +47 -0
  74. package/dist/geoms/lib/marks/hexbin.d.ts +29 -0
  75. package/dist/geoms/lib/marks/hexbin.js +62 -0
  76. package/dist/geoms/lib/marks/jitter.d.ts +21 -0
  77. package/dist/geoms/lib/marks/jitter.js +36 -0
  78. package/dist/geoms/lib/marks/line.d.ts +26 -0
  79. package/dist/geoms/lib/marks/line.js +25 -0
  80. package/dist/geoms/lib/marks/point.d.ts +25 -0
  81. package/dist/geoms/lib/marks/point.js +58 -0
  82. package/dist/geoms/lib/marks/ribbon.d.ts +32 -0
  83. package/dist/geoms/lib/marks/ribbon.js +80 -0
  84. package/dist/geoms/lib/marks/violin.d.ts +13 -0
  85. package/dist/geoms/lib/marks/violin.js +31 -0
  86. package/dist/geoms/lib/marks/waterfall.d.ts +25 -0
  87. package/dist/geoms/lib/marks/waterfall.js +58 -0
  88. package/dist/index.d.ts +1 -8
  89. package/dist/index.js +1 -8
  90. package/dist/lib/brewing/colors.d.ts +31 -1
  91. package/dist/lib/brewing/colors.js +34 -1
  92. package/dist/lib/brewing/marks/arcs.js +9 -4
  93. package/dist/lib/brewing/marks/boxes.js +8 -4
  94. package/dist/lib/brewing/marks/lines.js +13 -2
  95. package/dist/lib/brewing/marks/points.js +7 -2
  96. package/dist/lib/brewing/marks/swarm.js +6 -2
  97. package/dist/lib/brewing/marks/violins.js +8 -4
  98. package/dist/lib/preset.d.ts +11 -1
  99. package/dist/lib/preset.js +16 -4
  100. package/package.json +1 -1
  101. package/dist/Plot/Arc.svelte +0 -30
  102. package/dist/Plot/Arc.svelte.d.ts +0 -18
  103. package/dist/lib/brewer.d.ts +0 -9
  104. package/dist/lib/brewer.js +0 -25
  105. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +0 -8
  106. package/dist/lib/brewing/BoxBrewer.svelte.js +0 -14
  107. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +0 -8
  108. package/dist/lib/brewing/CartesianBrewer.svelte.js +0 -21
  109. package/dist/lib/brewing/PieBrewer.svelte.d.ts +0 -8
  110. package/dist/lib/brewing/PieBrewer.svelte.js +0 -14
  111. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +0 -9
  112. package/dist/lib/brewing/QuartileBrewer.svelte.js +0 -51
  113. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +0 -8
  114. package/dist/lib/brewing/ViolinBrewer.svelte.js +0 -14
  115. package/dist/lib/brewing/axes.svelte.d.ts +0 -66
  116. package/dist/lib/brewing/axes.svelte.js +0 -270
  117. package/dist/lib/brewing/bars.svelte.d.ts +0 -56
  118. package/dist/lib/brewing/bars.svelte.js +0 -201
  119. package/dist/lib/brewing/brewer.svelte.d.ts +0 -124
  120. package/dist/lib/brewing/brewer.svelte.js +0 -277
  121. package/dist/lib/brewing/dimensions.svelte.d.ts +0 -35
  122. package/dist/lib/brewing/dimensions.svelte.js +0 -56
  123. package/dist/lib/brewing/index.svelte.d.ts +0 -118
  124. package/dist/lib/brewing/index.svelte.js +0 -205
  125. package/dist/lib/brewing/legends.svelte.d.ts +0 -48
  126. package/dist/lib/brewing/legends.svelte.js +0 -137
  127. package/dist/lib/brewing/marks/areas.d.ts +0 -31
  128. package/dist/lib/brewing/marks/areas.js +0 -72
  129. package/dist/lib/brewing/marks/bars.d.ts +0 -1
  130. package/dist/lib/brewing/marks/bars.js +0 -49
  131. package/dist/lib/brewing/scales.svelte.d.ts +0 -24
  132. package/dist/lib/brewing/scales.svelte.js +0 -82
  133. package/dist/lib/context.d.ts +0 -13
  134. package/dist/lib/context.js +0 -131
  135. package/dist/lib/plot/chartProps.d.ts +0 -177
  136. package/dist/lib/plot/chartProps.js +0 -76
  137. package/dist/lib/scales.svelte.d.ts +0 -35
  138. package/dist/lib/scales.svelte.js +0 -151
  139. package/dist/lib/ticks.d.ts +0 -36
  140. package/dist/lib/ticks.js +0 -46
@@ -0,0 +1,45 @@
1
+ // Shared aesthetic resolution for all geoms. Done ONCE here so every geom's build
2
+ // function inherits the same ggplot-aligned fill/color and alpha rules.
3
+ // See docs/backlog/2026-08-17-chart-aesthetics-unification.md §2, §3, §11.
4
+
5
+ const GRAY = { fill: '#888', stroke: '#888' }
6
+
7
+ /**
8
+ * Resolve a mark's interior (fill) and outline (stroke) from the `fill` and `color`
9
+ * channels through the shared categorical `colors` palette (Map<value, {fill, stroke}>).
10
+ *
11
+ * ggplot fallback:
12
+ * - color-only → fill AND stroke from the color value's entry (backward compatible)
13
+ * - fill-only → fill AND stroke from the fill value's entry
14
+ * - both → interior from the fill value, outline from the color value
15
+ * - neither → the first palette entry (single-series)
16
+ *
17
+ * @param {Record<string, unknown>} row
18
+ * @param {{ fill?: string, color?: string }} channels
19
+ * @param {Map<unknown, { fill: string, stroke: string }>} colors
20
+ * @returns {{ fill: string, stroke: string }}
21
+ */
22
+ export function resolveFillStroke(row, channels, colors) {
23
+ const { fill, color } = channels ?? {}
24
+ const first = colors?.values?.().next?.().value ?? GRAY
25
+ const fillEntry = fill ? (colors?.get(row?.[fill]) ?? first) : null
26
+ const colorEntry = color ? (colors?.get(row?.[color]) ?? first) : null
27
+ const base = fillEntry ?? colorEntry ?? first
28
+ return {
29
+ fill: (fillEntry ?? base).fill,
30
+ stroke: (colorEntry ?? base).stroke
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Resolve a geom's mark opacity: an explicit fixed `alpha`, else the per-geom preset
36
+ * default, else fully opaque.
37
+ * @param {number|undefined} alpha
38
+ * @param {string} type - geom type key into preset.opacity
39
+ * @param {{ opacity?: Record<string, number> }} [preset]
40
+ * @returns {number}
41
+ */
42
+ export function resolveAlpha(alpha, type, preset) {
43
+ if (typeof alpha === 'number') return alpha
44
+ return preset?.opacity?.[type] ?? 1
45
+ }
@@ -49,7 +49,7 @@ export function buildStackedAreas(data: Object[], channels: {
49
49
  }>, curve?: "linear" | "smooth" | "step", patterns?: Map<unknown, string>, place?: (x: any, y: any) => {
50
50
  x: any;
51
51
  y: any;
52
- }): {
52
+ }, normalize?: boolean): {
53
53
  d: string;
54
54
  fill: string;
55
55
  stroke: string;
@@ -1,5 +1,6 @@
1
- import { area, stack, curveCatmullRom, curveStep } from 'd3-shape'
1
+ import { area, stack, stackOffsetExpand, curveCatmullRom, curveStep } from 'd3-shape'
2
2
  import { toPatternId } from '../../lib/brewing/patterns.js'
3
+ import { literalColor, markEntry } from '../../lib/brewing/colors.js'
3
4
 
4
5
  /**
5
6
  * Builds area path geometry for multi-series area charts.
@@ -15,16 +16,25 @@ import { toPatternId } from '../../lib/brewing/patterns.js'
15
16
  * @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
16
17
  */
17
18
  export function buildAreas(data, channels, xScale, yScale, colors, curve, patterns, place = (x, y) => ({ x, y })) {
18
- const { x: xf, y: yf, color: cf, pattern: pf } = channels
19
+ const { x: xf, y: yf, pattern: pf } = channels
20
+ // A literal color (var()/oklch()/hex/currentColor) isn't a data field — it's the exact
21
+ // fill the caller wants for THIS area. Don't group on it, and don't fall back to the
22
+ // shared color scale (one value across all geoms); use it directly.
23
+ const lit = literalColor(channels.color)
24
+ const cf = lit ? undefined : channels.color
19
25
  const baseline = yScale.range()[0] // bottom of the chart (y pixel max)
20
26
 
21
27
  const xPos = (d) =>
22
28
  typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
23
29
 
24
30
  // Each area edge (baseline + top) is placed so the area transposes under orientation.
25
- const toEdge = (d) => ({ base: place(xPos(d), baseline), top: place(xPos(d), yScale(d[yf])) })
31
+ // `v` carries the raw y-value so the generator can break the area at a gap (null).
32
+ const toEdge = (d) => ({ base: place(xPos(d), baseline), top: place(xPos(d), yScale(d[yf])), v: d[yf] })
26
33
  const makeGen = () => {
27
34
  const gen = area()
35
+ // A null y-value is a GAP (a missing period): break the area there instead of
36
+ // dropping it onto the 0 baseline. A genuine 0 stays defined and fills.
37
+ .defined((p) => Number.isFinite(p.top.y) && (yf === undefined || yf === null || (p.v !== undefined && p.v !== null)))
28
38
  .x0((p) => p.base.x)
29
39
  .y0((p) => p.base.y)
30
40
  .x1((p) => p.top.x)
@@ -45,7 +55,8 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
45
55
  }
46
56
 
47
57
  if (!cf) {
48
- const entry = colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
58
+ // A literal color IS the fill; a single-series field uses the shared shade.
59
+ const entry = markEntry(lit, undefined, colors, colors?.values().next().value ?? { fill: '#888', stroke: '#888' })
49
60
  const patternKey = pf ? data[0]?.[pf] : null
50
61
  const patternId =
51
62
  patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
@@ -92,7 +103,7 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
92
103
  * @param {Map<unknown, string>} [patterns]
93
104
  * @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
94
105
  */
95
- export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns, place = (x, y) => ({ x, y })) {
106
+ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns, place = (x, y) => ({ x, y }), normalize = false) {
96
107
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
97
108
  if (!cf) return buildAreas(data, channels, xScale, yScale, colors, curve, patterns)
98
109
 
@@ -132,6 +143,8 @@ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve,
132
143
  }
133
144
 
134
145
  const stackGen = stack().keys(colorCategories)
146
+ // position='fill' → normalize each x column to [0,1] (100% stacked area).
147
+ if (normalize) stackGen.offset(stackOffsetExpand)
135
148
  const layers = stackGen(wide)
136
149
 
137
150
  const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
@@ -11,8 +11,8 @@
11
11
  export function buildBars(data: any, channels: any, xScale: any, yScale: any, colors: any, innerHeight: any, patterns: any, place?: (x: any, y: any) => {
12
12
  x: any;
13
13
  y: any;
14
- }, continuousCategory?: boolean): any;
14
+ }, continuousCategory?: boolean, dodge?: boolean): any;
15
15
  export function buildStackedBars(data: any, channels: any, xScale: any, yScale: any, colors: any, innerHeight: any, patterns: any, place?: (x: any, y: any) => {
16
16
  x: any;
17
17
  y: any;
18
- }): any;
18
+ }, normalize?: boolean): any;
@@ -1,7 +1,7 @@
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
- import { isLiteralColor } from '../../lib/brewing/colors.js'
4
+ import { isLiteralColor, literalColor, markEntry } from '../../lib/brewing/colors.js'
5
5
 
6
6
  /**
7
7
  * Returns a band scale suitable for bar x-positioning.
@@ -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)
@@ -52,11 +53,16 @@ export function buildBars(
52
53
  innerHeight,
53
54
  patterns,
54
55
  place = (x, y) => ({ x, y }),
55
- continuousCategory = false
56
+ continuousCategory = false,
57
+ dodge = true
56
58
  ) {
57
59
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
60
+ // A literal color is the exact fill for every bar; a field indexes the shared color scale.
61
+ const lit = literalColor(cf)
62
+ const field = lit ? undefined : cf
58
63
 
59
- const subFields = continuousCategory ? [] : subBandFields(channels)
64
+ // No sub-banding for a continuous (race) axis or position='identity' (dodge=false → overlap).
65
+ const subFields = continuousCategory || !dodge ? [] : subBandFields(channels)
60
66
  const getSubKey = (d) => subFields.map((f) => String(d[f])).join('::')
61
67
 
62
68
  let positionOf
@@ -93,12 +99,16 @@ export function buildBars(
93
99
 
94
100
  return data.map((d, i) => {
95
101
  const xVal = d[xf]
96
- const colorKey = cf ? d[cf] : null
102
+ const colorKey = field ? d[field] : null
97
103
  const patternKey = pf ? d[pf] : null
98
104
  const subKey = getSubKey(d)
99
105
 
100
- const colorEntry = colors?.get(colorKey) ??
106
+ const colorEntry = markEntry(
107
+ lit,
108
+ colorKey,
109
+ colors,
101
110
  colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
111
+ )
102
112
  const patternId =
103
113
  patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
104
114
  ? toPatternId(String(patternKey))
@@ -131,8 +141,10 @@ export function buildBars(
131
141
  })
132
142
  }
133
143
 
134
- export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns, place = (x, y) => ({ x, y })) {
144
+ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns, place = (x, y) => ({ x, y }), normalize = false) {
135
145
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
146
+ const lit = literalColor(cf)
147
+ const field = lit ? undefined : cf
136
148
 
137
149
  const bandScale = ensureBandX(xScale, data, xf)
138
150
 
@@ -159,6 +171,8 @@ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHe
159
171
  })
160
172
 
161
173
  const stackGen = stack().keys(stackCategories)
174
+ // position='fill' → normalize each x column to [0,1] (100% stacked).
175
+ if (normalize) stackGen.offset(stackOffsetExpand)
162
176
  const layers = stackGen(wide)
163
177
 
164
178
  const bars = []
@@ -170,8 +184,8 @@ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHe
170
184
  const xVal = point.data[xf]
171
185
 
172
186
  // Color lookup: cf may equal xf (= xVal) or stackField (= stackKey)
173
- const colorKey = cf ? (cf === xf ? xVal : cf === stackField ? stackKey : null) : null
174
- const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
187
+ const colorKey = field ? (field === xf ? xVal : field === stackField ? stackKey : null) : null
188
+ const colorEntry = markEntry(lit, colorKey, colors, { fill: '#888', stroke: '#888' })
175
189
 
176
190
  // Pattern lookup: pf may equal xf (= xVal) or stackField (= stackKey)
177
191
  const patternKey = pf ? (pf === xf ? xVal : pf === stackField ? stackKey : null) : null
@@ -180,13 +194,19 @@ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHe
180
194
  ? toPatternId(String(patternKey))
181
195
  : null
182
196
 
197
+ // Two opposite corners in (band, value) space, mapped through place() so the
198
+ // stack transposes under orientation='horizontal' (identity default → unchanged).
199
+ // Without this a grouped stack/fill ignored place() and always drew vertically.
200
+ const band = bandScale(xVal) ?? 0
201
+ const c1 = place(band, yScale(y0))
202
+ const c2 = place(band + bandScale.bandwidth(), yScale(y1))
183
203
  bars.push({
184
204
  data: point.data,
185
205
  key: `${String(xVal)}::${String(stackKey)}`,
186
- x: bandScale(xVal) ?? 0,
187
- y: yScale(y1),
188
- width: bandScale.bandwidth(),
189
- height: yScale(y0) - yScale(y1),
206
+ x: Math.min(c1.x, c2.x),
207
+ y: Math.min(c1.y, c2.y),
208
+ width: Math.abs(c2.x - c1.x),
209
+ height: Math.abs(c2.y - c1.y),
190
210
  fill: colorEntry.fill,
191
211
  stroke: colorEntry.stroke,
192
212
  patternId
@@ -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,47 @@
1
+ import { resolveAlpha } from '../aesthetics.js'
2
+ import { literalColor } from '../../../lib/brewing/colors.js'
3
+
4
+ /**
5
+ * Build renderable heatmap cells. Cell fill is the continuous color scale (numeric color
6
+ * field) or the categorical palette entry, from `fill ?? color`. Adds fixed `alpha`.
7
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
8
+ */
9
+ export function buildHeatmapMarks({ data, plot, channels, alpha, type = 'heatmap' }) {
10
+ const { xScale, yScale, colors } = plot
11
+ if (!data?.length || !xScale || !yScale) return []
12
+
13
+ const colorField = channels.fill ?? channels.color
14
+ // A literal fill/color paints every cell directly, skipping the value/palette scale.
15
+ const lit = literalColor(colorField)
16
+ const field = lit ? undefined : colorField
17
+ const continuous = plot.continuousColorScale
18
+ const bwX = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 0
19
+ const bwY = typeof yScale.bandwidth === 'function' ? yScale.bandwidth() : 0
20
+ const a = resolveAlpha(alpha, type, plot.chartPreset)
21
+
22
+ return data.map((d, i) => {
23
+ const xVal = d[channels.x ?? '']
24
+ const yVal = d[channels.y ?? '']
25
+ const colorVal = field ? d[field] : null
26
+ let fill = lit ?? '#ccc'
27
+ if (!lit) {
28
+ if (continuous) fill = continuous.scale(Number(colorVal))
29
+ else {
30
+ const entry = colors?.get(colorVal)
31
+ if (entry) fill = entry.fill
32
+ }
33
+ }
34
+ return {
35
+ key: `${xVal}-${yVal}-${i}`,
36
+ x: xScale(xVal) ?? 0,
37
+ y: yScale(yVal) ?? 0,
38
+ width: bwX,
39
+ height: bwY,
40
+ fill,
41
+ alpha: a,
42
+ value: colorVal,
43
+ display: String(colorVal ?? ''),
44
+ data: d
45
+ }
46
+ })
47
+ }
@@ -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
+ }[];