@rokkit/chart 1.3.8 → 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 (130) 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/PlotState.svelte.d.ts +14 -2
  7. package/dist/PlotState.svelte.js +46 -4
  8. package/dist/charts/AreaChart.svelte +8 -1
  9. package/dist/charts/AreaChart.svelte.d.ts +5 -0
  10. package/dist/charts/BarChart.svelte +8 -1
  11. package/dist/charts/BarChart.svelte.d.ts +5 -0
  12. package/dist/charts/BoxPlot.svelte +3 -1
  13. package/dist/charts/BoxPlot.svelte.d.ts +1 -0
  14. package/dist/charts/BubbleChart.svelte +3 -1
  15. package/dist/charts/BubbleChart.svelte.d.ts +1 -0
  16. package/dist/charts/LineChart.svelte +3 -1
  17. package/dist/charts/LineChart.svelte.d.ts +1 -0
  18. package/dist/charts/PieChart.svelte +4 -2
  19. package/dist/charts/PieChart.svelte.d.ts +1 -0
  20. package/dist/charts/ScatterPlot.svelte +3 -1
  21. package/dist/charts/ScatterPlot.svelte.d.ts +1 -0
  22. package/dist/charts/ViolinPlot.svelte +3 -1
  23. package/dist/charts/ViolinPlot.svelte.d.ts +1 -0
  24. package/dist/geoms/Arc.svelte +21 -55
  25. package/dist/geoms/Arc.svelte.d.ts +4 -4
  26. package/dist/geoms/Area.svelte +45 -44
  27. package/dist/geoms/Area.svelte.d.ts +8 -1
  28. package/dist/geoms/Bar.svelte +33 -56
  29. package/dist/geoms/Bar.svelte.d.ts +9 -0
  30. package/dist/geoms/Box.svelte +118 -131
  31. package/dist/geoms/Box.svelte.d.ts +5 -3
  32. package/dist/geoms/Candlestick.svelte +22 -77
  33. package/dist/geoms/Candlestick.svelte.d.ts +2 -0
  34. package/dist/geoms/Heatmap.svelte +21 -66
  35. package/dist/geoms/Heatmap.svelte.d.ts +2 -0
  36. package/dist/geoms/Hexbin.svelte +20 -100
  37. package/dist/geoms/Hexbin.svelte.d.ts +2 -0
  38. package/dist/geoms/Jitter.svelte +21 -36
  39. package/dist/geoms/Jitter.svelte.d.ts +5 -3
  40. package/dist/geoms/Line.svelte +27 -31
  41. package/dist/geoms/Line.svelte.d.ts +5 -0
  42. package/dist/geoms/Point.svelte +31 -66
  43. package/dist/geoms/Point.svelte.d.ts +5 -1
  44. package/dist/geoms/Ribbon.svelte +30 -124
  45. package/dist/geoms/Ribbon.svelte.d.ts +2 -0
  46. package/dist/geoms/Rule.svelte +5 -1
  47. package/dist/geoms/Rule.svelte.d.ts +3 -0
  48. package/dist/geoms/Violin.svelte +21 -40
  49. package/dist/geoms/Violin.svelte.d.ts +5 -3
  50. package/dist/geoms/Waterfall.svelte +19 -80
  51. package/dist/geoms/Waterfall.svelte.d.ts +2 -0
  52. package/dist/geoms/lib/GeomState.svelte.d.ts +51 -0
  53. package/dist/geoms/lib/GeomState.svelte.js +79 -0
  54. package/dist/geoms/lib/aesthetics.d.ts +36 -0
  55. package/dist/geoms/lib/aesthetics.js +45 -0
  56. package/dist/geoms/lib/areas.d.ts +1 -1
  57. package/dist/geoms/lib/areas.js +4 -2
  58. package/dist/geoms/lib/bars.d.ts +2 -2
  59. package/dist/geoms/lib/bars.js +13 -8
  60. package/dist/geoms/lib/marks/arc.d.ts +24 -0
  61. package/dist/geoms/lib/marks/arc.js +28 -0
  62. package/dist/geoms/lib/marks/area.d.ts +23 -0
  63. package/dist/geoms/lib/marks/area.js +38 -0
  64. package/dist/geoms/lib/marks/bar.d.ts +16 -0
  65. package/dist/geoms/lib/marks/bar.js +43 -0
  66. package/dist/geoms/lib/marks/box.d.ts +14 -0
  67. package/dist/geoms/lib/marks/box.js +30 -0
  68. package/dist/geoms/lib/marks/candlestick.d.ts +27 -0
  69. package/dist/geoms/lib/marks/candlestick.js +52 -0
  70. package/dist/geoms/lib/marks/heatmap.d.ts +24 -0
  71. package/dist/geoms/lib/marks/heatmap.js +41 -0
  72. package/dist/geoms/lib/marks/hexbin.d.ts +29 -0
  73. package/dist/geoms/lib/marks/hexbin.js +62 -0
  74. package/dist/geoms/lib/marks/jitter.d.ts +21 -0
  75. package/dist/geoms/lib/marks/jitter.js +36 -0
  76. package/dist/geoms/lib/marks/line.d.ts +26 -0
  77. package/dist/geoms/lib/marks/line.js +25 -0
  78. package/dist/geoms/lib/marks/point.d.ts +25 -0
  79. package/dist/geoms/lib/marks/point.js +58 -0
  80. package/dist/geoms/lib/marks/ribbon.d.ts +31 -0
  81. package/dist/geoms/lib/marks/ribbon.js +77 -0
  82. package/dist/geoms/lib/marks/violin.d.ts +13 -0
  83. package/dist/geoms/lib/marks/violin.js +31 -0
  84. package/dist/geoms/lib/marks/waterfall.d.ts +25 -0
  85. package/dist/geoms/lib/marks/waterfall.js +58 -0
  86. package/dist/index.d.ts +1 -8
  87. package/dist/index.js +1 -8
  88. package/dist/lib/preset.d.ts +11 -1
  89. package/dist/lib/preset.js +16 -4
  90. package/package.json +1 -1
  91. package/dist/Plot/Arc.svelte +0 -30
  92. package/dist/Plot/Arc.svelte.d.ts +0 -18
  93. package/dist/lib/brewer.d.ts +0 -9
  94. package/dist/lib/brewer.js +0 -25
  95. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +0 -8
  96. package/dist/lib/brewing/BoxBrewer.svelte.js +0 -14
  97. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +0 -8
  98. package/dist/lib/brewing/CartesianBrewer.svelte.js +0 -21
  99. package/dist/lib/brewing/PieBrewer.svelte.d.ts +0 -8
  100. package/dist/lib/brewing/PieBrewer.svelte.js +0 -14
  101. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +0 -9
  102. package/dist/lib/brewing/QuartileBrewer.svelte.js +0 -51
  103. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +0 -8
  104. package/dist/lib/brewing/ViolinBrewer.svelte.js +0 -14
  105. package/dist/lib/brewing/axes.svelte.d.ts +0 -66
  106. package/dist/lib/brewing/axes.svelte.js +0 -270
  107. package/dist/lib/brewing/bars.svelte.d.ts +0 -56
  108. package/dist/lib/brewing/bars.svelte.js +0 -201
  109. package/dist/lib/brewing/brewer.svelte.d.ts +0 -124
  110. package/dist/lib/brewing/brewer.svelte.js +0 -277
  111. package/dist/lib/brewing/dimensions.svelte.d.ts +0 -35
  112. package/dist/lib/brewing/dimensions.svelte.js +0 -56
  113. package/dist/lib/brewing/index.svelte.d.ts +0 -118
  114. package/dist/lib/brewing/index.svelte.js +0 -205
  115. package/dist/lib/brewing/legends.svelte.d.ts +0 -48
  116. package/dist/lib/brewing/legends.svelte.js +0 -137
  117. package/dist/lib/brewing/marks/areas.d.ts +0 -31
  118. package/dist/lib/brewing/marks/areas.js +0 -72
  119. package/dist/lib/brewing/marks/bars.d.ts +0 -1
  120. package/dist/lib/brewing/marks/bars.js +0 -49
  121. package/dist/lib/brewing/scales.svelte.d.ts +0 -24
  122. package/dist/lib/brewing/scales.svelte.js +0 -82
  123. package/dist/lib/context.d.ts +0 -13
  124. package/dist/lib/context.js +0 -131
  125. package/dist/lib/plot/chartProps.d.ts +0 -177
  126. package/dist/lib/plot/chartProps.js +0 -76
  127. package/dist/lib/scales.svelte.d.ts +0 -35
  128. package/dist/lib/scales.svelte.js +0 -151
  129. package/dist/lib/ticks.d.ts +0 -36
  130. 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,4 +1,4 @@
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
3
 
4
4
  /**
@@ -92,7 +92,7 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
92
92
  * @param {Map<unknown, string>} [patterns]
93
93
  * @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
94
94
  */
95
- export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns, place = (x, y) => ({ x, y })) {
95
+ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns, place = (x, y) => ({ x, y }), normalize = false) {
96
96
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
97
97
  if (!cf) return buildAreas(data, channels, xScale, yScale, colors, curve, patterns)
98
98
 
@@ -132,6 +132,8 @@ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve,
132
132
  }
133
133
 
134
134
  const stackGen = stack().keys(colorCategories)
135
+ // position='fill' → normalize each x column to [0,1] (100% stacked area).
136
+ if (normalize) stackGen.offset(stackOffsetExpand)
135
137
  const layers = stackGen(wide)
136
138
 
137
139
  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,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)
@@ -52,11 +53,13 @@ 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
58
60
 
59
- const subFields = continuousCategory ? [] : subBandFields(channels)
61
+ // No sub-banding for a continuous (race) axis or position='identity' (dodge=false → overlap).
62
+ const subFields = continuousCategory || !dodge ? [] : subBandFields(channels)
60
63
  const getSubKey = (d) => subFields.map((f) => String(d[f])).join('::')
61
64
 
62
65
  let positionOf
@@ -131,7 +134,7 @@ export function buildBars(
131
134
  })
132
135
  }
133
136
 
134
- export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns, place = (x, y) => ({ x, y })) {
137
+ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns, place = (x, y) => ({ x, y }), normalize = false) {
135
138
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
136
139
 
137
140
  const bandScale = ensureBandX(xScale, data, xf)
@@ -159,6 +162,8 @@ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHe
159
162
  })
160
163
 
161
164
  const stackGen = stack().keys(stackCategories)
165
+ // position='fill' → normalize each x column to [0,1] (100% stacked).
166
+ if (normalize) stackGen.offset(stackOffsetExpand)
162
167
  const layers = stackGen(wide)
163
168
 
164
169
  const bars = []
@@ -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
+ }[];
@@ -0,0 +1,62 @@
1
+ import { resolveAlpha } from '../aesthetics.js'
2
+
3
+ /** SVG path for a pointy-top hexagon of radius r, centered at the origin. */
4
+ export function hexPath(r) {
5
+ const angles = [0, 1, 2, 3, 4, 5].map((i) => ((i * 60 - 30) * Math.PI) / 180)
6
+ return `${angles.map((a, i) => `${i === 0 ? 'M' : 'L'}${r * Math.cos(a)},${r * Math.sin(a)}`).join('')}Z`
7
+ }
8
+
9
+ /** Group points into hexagonal bins and count per bin. */
10
+ function hexBin(rows, xField, yField, xScale, yScale, r) {
11
+ const dx = r * 2 * Math.sin(Math.PI / 3)
12
+ const dy = r * 1.5
13
+ const bins = new Map()
14
+ for (const d of rows) {
15
+ const px = xScale(d[xField ?? ''])
16
+ const py = yScale(d[yField ?? ''])
17
+ if (px === null || px === undefined || py === null || py === undefined) continue
18
+ const col = Math.round(px / dx)
19
+ const row = Math.round(py / dy)
20
+ const cx = col * dx + (row % 2 ? dx / 2 : 0)
21
+ const cy = row * dy
22
+ const key = `${col},${row}`
23
+ let bin = bins.get(key)
24
+ if (!bin) {
25
+ bin = { cx, cy, count: 0, points: [] }
26
+ bins.set(key, bin)
27
+ }
28
+ bin.count++
29
+ bin.points.push(d)
30
+ }
31
+ return [...bins.values()]
32
+ }
33
+
34
+ /**
35
+ * Build renderable hex bins. Fill is the continuous color scale (by count) or a density-ramp
36
+ * rgba fallback. `alpha` is a flat multiplier on top (default 1 → unchanged).
37
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
38
+ */
39
+ export function buildHexbinMarks({ data, plot, channels, options = {}, alpha, type = 'hexbin' }) {
40
+ const { xScale, yScale } = plot
41
+ if (!data?.length || !xScale || !yScale) return []
42
+
43
+ const radius = options.radius ?? 20
44
+ const bins = hexBin(data, channels.x, channels.y, xScale, yScale, radius)
45
+ const maxCount = Math.max(1, ...bins.map((b) => b.count))
46
+ const colorScale = plot.continuousColorScale?.scale
47
+ const a = resolveAlpha(alpha, type, plot.chartPreset)
48
+
49
+ return bins.map((bin, i) => ({
50
+ key: `hex-${i}`,
51
+ cx: bin.cx,
52
+ cy: bin.cy,
53
+ count: bin.count,
54
+ fill: colorScale
55
+ ? colorScale(bin.count)
56
+ : `rgba(66, 133, 244, ${0.1 + 0.9 * (bin.count / maxCount)})`,
57
+ alpha: a,
58
+ value: bin.count,
59
+ display: `${bin.count}`,
60
+ data: { count: bin.count, x: bin.cx, y: bin.cy }
61
+ }))
62
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Build renderable jitter/swarm points. Fill + outline come from `fill` (defaulting to
3
+ * `x`); an explicit `color` channel overrides the outline stroke. Adds fixed `alpha`.
4
+ * Horizontal orientation swaps each point's screen coords via place().
5
+ * @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
6
+ */
7
+ export function buildJitterMarks({ 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
+ cx: any;
16
+ cy: any;
17
+ stroke: any;
18
+ alpha: number;
19
+ fill: string;
20
+ data: Record<string, unknown>;
21
+ }[];