@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,73 +1,74 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
- import { buildAreas, buildStackedAreas } from './lib/areas.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildAreaMarks } from './lib/marks/area.js'
5
6
  import { buildSelectDetail } from '../lib/select.js'
6
7
 
7
8
  type Row = Record<string, unknown>
8
-
9
9
  type Options = {
10
+ /** @deprecated use `position="stack"` — kept as an alias. */
10
11
  stack?: boolean
11
12
  curve?: 'linear' | 'smooth' | 'step'
12
- opacity?: number
13
13
  }
14
14
 
15
15
  type Props = {
16
16
  x?: string
17
17
  y?: string
18
+ /** Border aesthetic (field); a border is drawn only when both fill and color are set. */
18
19
  color?: string
20
+ /** Interior aesthetic (field); falls back to `color`. */
21
+ fill?: string
19
22
  pattern?: string
23
+ /** Multi-series arrangement: 'stack' | 'fill' (100%) | 'identity' (overlap, default). */
24
+ position?: 'stack' | 'fill' | 'identity'
25
+ /** Fixed area opacity 0–1; defaults to the per-geom preset (area = 0.6). */
26
+ alpha?: number
20
27
  stat?: string
21
28
  options?: Options
22
29
  }
23
30
 
24
- let { x, y, color, pattern, stat = 'identity', options = {} }: Props = $props()
31
+ let { x, y, color, fill, pattern, position, alpha, stat = 'identity', options = {} }: Props =
32
+ $props()
33
+
34
+ // `options.stack: true` is the back-compat alias for position='stack'.
35
+ const resolvedPosition = $derived(position ?? (options?.stack ? 'stack' : 'identity'))
25
36
 
26
37
  const plotState = getContext<PlotState>('plot-state')
27
- let id = $state<string | null>(null)
38
+
39
+ const geom = new GeomState(plotState, () => ({
40
+ type: 'area',
41
+ channels: { x, y, color, fill, pattern },
42
+ stat,
43
+ options: { stack: options?.stack ?? false, position: resolvedPosition, curve: options?.curve },
44
+ alpha,
45
+ build: buildAreaMarks
46
+ }))
47
+ onMount(geom.register)
48
+ onDestroy(geom.destroy)
49
+ $effect(geom.sync)
50
+
51
+ const areas = $derived(geom.marks)
52
+ // Per-row tooltip hit-circles are rendered from the geom's rows + shared scales.
53
+ const rows = $derived(geom.data)
54
+ const xScale = $derived(plotState.xScale)
55
+ const yScale = $derived(plotState.yScale)
56
+
57
+ const seriesField = $derived(fill ?? color)
28
58
 
29
59
  function selectPoint(d: Row, event: MouseEvent | KeyboardEvent) {
30
60
  if (plotState.interactive)
31
61
  plotState.handleSelect(
32
- buildSelectDetail(d, plotState.data.indexOf(d), { x, y }, 'area', color ? d[color] : undefined, event)
62
+ buildSelectDetail(
63
+ d,
64
+ plotState.data.indexOf(d),
65
+ { x, y },
66
+ 'area',
67
+ seriesField ? d[seriesField] : undefined,
68
+ event
69
+ )
33
70
  )
34
71
  }
35
-
36
- onMount(() => {
37
- id = plotState.registerGeom({
38
- type: 'area',
39
- channels: { x, y, color, pattern },
40
- stat,
41
- options: { stack: options?.stack ?? false }
42
- })
43
- })
44
- onDestroy(() => {
45
- if (id) plotState.unregisterGeom(id)
46
- })
47
-
48
- $effect(() => {
49
- if (id)
50
- plotState.updateGeom(id, {
51
- channels: { x, y, color, pattern },
52
- stat,
53
- options: { stack: options?.stack ?? false }
54
- })
55
- })
56
-
57
- const data = $derived(id ? plotState.geomData(id) : [])
58
- const xScale = $derived(plotState.xScale)
59
- const yScale = $derived(plotState.yScale)
60
- const colors = $derived(plotState.colors)
61
- const patterns = $derived(plotState.patterns)
62
-
63
- const areas = $derived.by(() => {
64
- if (!data?.length || !xScale || !yScale || !x || !y) return []
65
- const channels = { x, y, color, pattern }
66
- if (options.stack) {
67
- return buildStackedAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
68
- }
69
- return buildAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
70
- })
71
72
  </script>
72
73
 
73
74
  {#if areas.length > 0}
@@ -76,7 +77,7 @@
76
77
  <path
77
78
  d={seg.d}
78
79
  fill={seg.fill}
79
- fill-opacity={seg.patternId ? 1 : (options.opacity ?? plotState.chartPreset.opacity.area)}
80
+ fill-opacity={seg.patternId ? 1 : seg.alpha}
80
81
  stroke={seg.stroke ?? 'none'}
81
82
  data-plot-element="area"
82
83
  />
@@ -86,7 +87,7 @@
86
87
  {/each}
87
88
  <!-- Invisible hit circles for tooltip: one per data point -->
88
89
  {#if x && y}
89
- {#each data as d, i (`hover::${i}`)}
90
+ {#each rows as d, i (`hover::${i}`)}
90
91
  {@const px = typeof xScale?.bandwidth === 'function' ? (xScale(d[x]) ?? 0) + xScale.bandwidth() / 2 : (xScale?.(d[x]) ?? 0)}
91
92
  {@const py = yScale?.(d[y]) ?? 0}
92
93
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
@@ -1,13 +1,20 @@
1
1
  type Options = {
2
+ /** @deprecated use `position="stack"` — kept as an alias. */
2
3
  stack?: boolean;
3
4
  curve?: 'linear' | 'smooth' | 'step';
4
- opacity?: number;
5
5
  };
6
6
  type Props = {
7
7
  x?: string;
8
8
  y?: string;
9
+ /** Border aesthetic (field); a border is drawn only when both fill and color are set. */
9
10
  color?: string;
11
+ /** Interior aesthetic (field); falls back to `color`. */
12
+ fill?: string;
10
13
  pattern?: string;
14
+ /** Multi-series arrangement: 'stack' | 'fill' (100%) | 'identity' (overlap, default). */
15
+ position?: 'stack' | 'fill' | 'identity';
16
+ /** Fixed area opacity 0–1; defaults to the per-geom preset (area = 0.6). */
17
+ alpha?: number;
11
18
  stat?: string;
12
19
  options?: Options;
13
20
  };
@@ -2,7 +2,8 @@
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
4
  import type { createCrossFilter } from '../crossfilter/createCrossFilter.svelte.js'
5
- import { buildGroupedBars, buildStackedBars, buildHorizontalBars } from './lib/bars.js'
5
+ import { GeomState } from './lib/GeomState.svelte.js'
6
+ import { buildBarMarks } from './lib/marks/bar.js'
6
7
  import { keyboardNav } from '../lib/keyboard-nav.js'
7
8
  import { buildSelectDetail } from '../lib/select.js'
8
9
  import LabelPill from './LabelPill.svelte'
@@ -10,6 +11,7 @@
10
11
  type Row = Record<string, unknown>
11
12
  type CrossFilter = ReturnType<typeof createCrossFilter>
12
13
  type Options = {
14
+ /** @deprecated use `position="stack"` — kept as an alias. */
13
15
  stack?: boolean
14
16
  orientation?: 'horizontal' | 'vertical'
15
17
  labelInside?: boolean
@@ -19,9 +21,17 @@
19
21
  type Props = {
20
22
  x?: string
21
23
  y?: string
24
+ /** Border/outline aesthetic (field). */
22
25
  color?: string
26
+ /** Interior aesthetic (field); falls back to `color`. */
23
27
  fill?: string
24
28
  pattern?: string
29
+ /** Multi-series arrangement: 'stack' | 'dodge' (default) | 'fill' (100%) | 'identity' (overlap). */
30
+ position?: 'stack' | 'dodge' | 'fill' | 'identity'
31
+ /** Sub-series field for stack/dodge; defaults to `fill ?? color`. */
32
+ group?: string
33
+ /** Fixed bar opacity 0–1; defaults to the per-geom preset (bar = 1). */
34
+ alpha?: number
25
35
  label?: boolean | string | ((data: Row) => unknown)
26
36
  stat?: string
27
37
  options?: Options
@@ -34,8 +44,11 @@
34
44
  x,
35
45
  y,
36
46
  color,
37
- fill: fillProp,
47
+ fill,
38
48
  pattern,
49
+ position,
50
+ group,
51
+ alpha,
39
52
  label = false,
40
53
  stat = 'identity',
41
54
  options = {},
@@ -44,8 +57,8 @@
44
57
  keyboard = false
45
58
  }: Props = $props()
46
59
 
47
- // `fill` is accepted as an alias for `color` (consistent with Arc.svelte)
48
- const colorChannel = $derived(fillProp ?? color)
60
+ // `options.stack: true` is the back-compat alias for position='stack'.
61
+ const resolvedPosition = $derived(position ?? (options?.stack ? 'stack' : 'dodge'))
49
62
 
50
63
  function resolveLabel(data: Row, defaultField: string | undefined): string | null {
51
64
  if (!label) return null
@@ -68,48 +81,22 @@
68
81
 
69
82
  const plotState = getContext<PlotState>('plot-state')
70
83
  const cf = getContext<CrossFilter | undefined>('crossfilter')
71
- let id = $state<string | null>(null)
72
-
73
- onMount(() => {
74
- id = plotState.registerGeom({
75
- type: 'bar',
76
- channels: { x, y, color: colorChannel, pattern },
77
- stat,
78
- options: { stack: options?.stack ?? false }
79
- })
80
- })
81
- onDestroy(() => {
82
- if (id) plotState.unregisterGeom(id)
83
- })
84
-
85
- $effect(() => {
86
- if (id)
87
- plotState.updateGeom(id, {
88
- channels: { x, y, color: colorChannel, pattern },
89
- stat,
90
- options: { stack: options?.stack ?? false }
91
- })
92
- })
93
84
 
94
- const data = $derived(id ? plotState.geomData(id) : [])
95
- const xScale = $derived(plotState.xScale)
96
- const yScale = $derived(plotState.yScale)
97
- const colors = $derived(plotState.colors)
98
- const patterns = $derived(plotState.patterns)
85
+ const geom = new GeomState(plotState, () => ({
86
+ type: 'bar',
87
+ channels: { x, y, color, fill, pattern, group },
88
+ stat,
89
+ options: { stack: options?.stack ?? false, position: resolvedPosition },
90
+ alpha,
91
+ build: buildBarMarks
92
+ }))
93
+ onMount(geom.register)
94
+ onDestroy(geom.destroy)
95
+ $effect(geom.sync)
96
+
97
+ const bars = $derived(geom.marks)
99
98
  const effectiveOrientation = $derived(options.orientation ?? plotState.orientation)
100
- const innerHeight = $derived(plotState.innerHeight)
101
-
102
- const bars = $derived.by(() => {
103
- if (!data?.length || !xScale || !yScale) return []
104
- const channels = { x, y, color: colorChannel, pattern }
105
- if (effectiveOrientation === 'horizontal') {
106
- return buildHorizontalBars(data, channels, xScale, yScale, colors, innerHeight)
107
- }
108
- if (options.stack) {
109
- return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
110
- }
111
- return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
112
- })
99
+ const seriesField = $derived(fill ?? color)
113
100
 
114
101
  let dimmedByKey = $state<Record<string, boolean>>({})
115
102
 
@@ -144,7 +131,7 @@
144
131
  plotState.data.indexOf(bar.data),
145
132
  { x, y },
146
133
  'bar',
147
- colorChannel ? bar.data[colorChannel] : undefined,
134
+ seriesField ? bar.data[seriesField] : undefined,
148
135
  event
149
136
  )
150
137
  )
@@ -177,6 +164,7 @@
177
164
  width={Math.max(0, bar.width)}
178
165
  height={Math.max(0, bar.height)}
179
166
  fill={bar.fill}
167
+ fill-opacity={bar.alpha}
180
168
  stroke={bar.stroke ?? 'none'}
181
169
  stroke-width={bar.stroke ? 0.5 : 0}
182
170
  data-plot-element="bar"
@@ -1,5 +1,6 @@
1
1
  type Row = Record<string, unknown>;
2
2
  type Options = {
3
+ /** @deprecated use `position="stack"` — kept as an alias. */
3
4
  stack?: boolean;
4
5
  orientation?: 'horizontal' | 'vertical';
5
6
  labelInside?: boolean;
@@ -8,9 +9,17 @@ type Options = {
8
9
  type Props = {
9
10
  x?: string;
10
11
  y?: string;
12
+ /** Border/outline aesthetic (field). */
11
13
  color?: string;
14
+ /** Interior aesthetic (field); falls back to `color`. */
12
15
  fill?: string;
13
16
  pattern?: string;
17
+ /** Multi-series arrangement: 'stack' | 'dodge' (default) | 'fill' (100%) | 'identity' (overlap). */
18
+ position?: 'stack' | 'dodge' | 'fill' | 'identity';
19
+ /** Sub-series field for stack/dodge; defaults to `fill ?? color`. */
20
+ group?: string;
21
+ /** Fixed bar opacity 0–1; defaults to the per-geom preset (bar = 1). */
22
+ alpha?: number;
14
23
  label?: boolean | string | ((data: Row) => unknown);
15
24
  stat?: string;
16
25
  options?: Options;
@@ -1,122 +1,164 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
- import { buildBoxes } from '../lib/brewing/marks/boxes.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildBoxMarks } from './lib/marks/box.js'
5
6
 
6
7
  type Props = {
7
8
  x?: string
8
9
  y?: string
10
+ /** Box interior aesthetic (field); defaults to `x`. */
9
11
  fill?: string
12
+ /** Outline aesthetic (field); overrides whisker/median/border stroke when set. */
13
+ color?: string
10
14
  stat?: string
11
- options?: { opacity?: number }
15
+ /** Offset the box to one half of the band (raincloud/split plots). Default 'center'. */
16
+ side?: 'left' | 'right' | 'center'
17
+ /** Box thickness as a fraction of the (sub-)band — use a small value (e.g. 0.16) for a
18
+ * thin raincloud box. Defaults to the full box width. */
19
+ width?: number
20
+ /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
21
+ pattern?: string
22
+ /** Fixed box opacity 0–1; defaults to the per-geom preset (box = 0.5). */
23
+ alpha?: number
12
24
  }
13
25
 
14
- let { x, y, fill, stat = 'boxplot', options = {} }: Props = $props()
26
+ let { x, y, fill, color, stat = 'boxplot', side = 'center', width, pattern, alpha }: Props =
27
+ $props()
15
28
 
16
29
  const plotState = getContext<PlotState>('plot-state')
17
- let id = $state<string | null>(null)
18
30
 
19
- // fill ?? x drives the colors map for both box interior and whisker strokes
20
- const fillChannel = $derived(fill ?? x)
31
+ // Fall back to the root Plot channels when the geom's own x/y are omitted, so
32
+ // <Plot.Box> composes under <Plot.Root x y> without silently NaN-ing.
33
+ const xf = $derived(x ?? plotState.channels?.x)
34
+ const yf = $derived(y ?? plotState.channels?.y)
35
+ // fill ?? effective-x drives the colors map for both box interior and whisker strokes
36
+ const fillChannel = $derived(fill ?? xf)
21
37
 
22
- onMount(() => {
23
- id = plotState.registerGeom({
24
- type: 'box',
25
- channels: { x, y, color: fillChannel },
26
- stat,
27
- options
28
- })
29
- })
30
- onDestroy(() => {
31
- if (id) plotState.unregisterGeom(id)
32
- })
38
+ const geom = new GeomState(plotState, () => ({
39
+ type: 'box',
40
+ channels: { x: xf, y: yf, fill: fillChannel, color, pattern },
41
+ stat,
42
+ options: { side, width },
43
+ alpha,
44
+ build: buildBoxMarks
45
+ }))
46
+ onMount(geom.register)
47
+ onDestroy(geom.destroy)
48
+ $effect(geom.sync)
33
49
 
34
- $effect(() => {
35
- if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
36
- })
37
-
38
- const data = $derived(id ? plotState.geomData(id) : [])
39
- const xScale = $derived(plotState.xScale)
40
- const yScale = $derived(plotState.yScale)
41
- const colors = $derived(plotState.colors)
42
-
43
- const boxes = $derived.by(() => {
44
- if (!data?.length || !xScale || !yScale) return []
45
- return buildBoxes(data, { x, fill: fillChannel }, xScale, yScale, colors)
46
- })
50
+ const boxes = $derived(geom.marks)
47
51
  </script>
48
52
 
49
53
  {#if boxes.length > 0}
50
54
  <g data-plot-geom="box">
51
55
  {#each boxes as box, i (`${String(box.cx)}::${i}`)}
52
- {@const x0 = box.cx - box.width / 2}
53
- {@const xMid = box.cx}
54
- {@const xCap0 = box.cx - box.whiskerWidth / 2}
55
- {@const xCap1 = box.cx + box.whiskerWidth / 2}
56
+ <!-- Each primitive is placed through plotState.place(band, value) so the whole
57
+ box transposes correctly under orientation='horizontal' (flip). -->
58
+ {@const c1 = plotState.place(box.cx - box.width / 2, box.q3)}
59
+ {@const c2 = plotState.place(box.cx + box.width / 2, box.q1)}
60
+ {@const mA = plotState.place(box.cx - box.width / 2, box.median)}
61
+ {@const mB = plotState.place(box.cx + box.width / 2, box.median)}
62
+ {@const wLoA = plotState.place(box.cx, box.q1)}
63
+ {@const wLoB = plotState.place(box.cx, box.iqr_min)}
64
+ {@const wHiA = plotState.place(box.cx, box.q3)}
65
+ {@const wHiB = plotState.place(box.cx, box.iqr_max)}
66
+ {@const capLoA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_min)}
67
+ {@const capLoB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_min)}
68
+ {@const capHiA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_max)}
69
+ {@const capHiB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_max)}
56
70
  <!-- Box body (IQR): lighter fill shade -->
57
71
  <rect
58
- x={x0}
59
- y={box.q3}
60
- width={box.width}
61
- height={Math.max(0, box.q1 - box.q3)}
72
+ x={Math.min(c1.x, c2.x)}
73
+ y={Math.min(c1.y, c2.y)}
74
+ width={Math.abs(c2.x - c1.x)}
75
+ height={Math.abs(c2.y - c1.y)}
62
76
  fill={box.fill}
63
- fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.box}
77
+ fill-opacity={box.alpha}
64
78
  stroke={box.stroke}
65
79
  stroke-width="1"
66
80
  data-plot-element="box-body"
67
- role="presentation"
81
+ role="graphics-symbol"
82
+ aria-label={`${String(box.data[xf ?? ''] ?? 'box')}: median ${box.data.median}, IQR ${box.data.q1}–${box.data.q3}`}
68
83
  onmouseenter={() => plotState.setHovered(box.data)}
69
84
  onmouseleave={() => plotState.clearHovered()}
70
85
  />
86
+ {#if box.patternId}
87
+ <!-- Texture fill overlay (matches the box body) -->
88
+ <rect
89
+ x={Math.min(c1.x, c2.x)}
90
+ y={Math.min(c1.y, c2.y)}
91
+ width={Math.abs(c2.x - c1.x)}
92
+ height={Math.abs(c2.y - c1.y)}
93
+ fill="url(#{box.patternId})"
94
+ pointer-events="none"
95
+ />
96
+ {/if}
71
97
  <!-- Median line: darker stroke shade -->
72
98
  <line
73
- x1={x0}
74
- y1={box.median}
75
- x2={x0 + box.width}
76
- y2={box.median}
99
+ x1={mA.x}
100
+ y1={mA.y}
101
+ x2={mB.x}
102
+ y2={mB.y}
77
103
  stroke={box.stroke}
78
104
  stroke-width="2"
79
105
  data-plot-element="box-median"
80
106
  />
81
107
  <!-- Lower whisker (q1 to iqr_min) -->
82
108
  <line
83
- x1={xMid}
84
- y1={box.q1}
85
- x2={xMid}
86
- y2={box.iqr_min}
109
+ x1={wLoA.x}
110
+ y1={wLoA.y}
111
+ x2={wLoB.x}
112
+ y2={wLoB.y}
87
113
  stroke={box.stroke}
88
114
  stroke-width="1"
89
115
  data-plot-element="box-whisker"
90
116
  />
91
117
  <!-- Upper whisker (q3 to iqr_max) -->
92
118
  <line
93
- x1={xMid}
94
- y1={box.q3}
95
- x2={xMid}
96
- y2={box.iqr_max}
119
+ x1={wHiA.x}
120
+ y1={wHiA.y}
121
+ x2={wHiB.x}
122
+ y2={wHiB.y}
97
123
  stroke={box.stroke}
98
124
  stroke-width="1"
99
125
  data-plot-element="box-whisker"
100
126
  />
101
127
  <!-- Lower whisker cap -->
102
128
  <line
103
- x1={xCap0}
104
- y1={box.iqr_min}
105
- x2={xCap1}
106
- y2={box.iqr_min}
129
+ x1={capLoA.x}
130
+ y1={capLoA.y}
131
+ x2={capLoB.x}
132
+ y2={capLoB.y}
107
133
  stroke={box.stroke}
108
134
  stroke-width="1"
109
135
  />
110
136
  <!-- Upper whisker cap -->
111
137
  <line
112
- x1={xCap0}
113
- y1={box.iqr_max}
114
- x2={xCap1}
115
- y2={box.iqr_max}
138
+ x1={capHiA.x}
139
+ y1={capHiA.y}
140
+ x2={capHiB.x}
141
+ y2={capHiB.y}
116
142
  stroke={box.stroke}
117
143
  stroke-width="1"
118
144
  />
119
- <!-- Outlier rendering deferred: buildBoxes does not compute outliers yet -->
145
+ <!-- Outliers: individual points beyond the 1.5·IQR fence -->
146
+ {#each box.outliers as o, oi (`${i}-outlier-${oi}`)}
147
+ {@const op = plotState.place(box.cx, o.cy)}
148
+ <circle
149
+ cx={op.x}
150
+ cy={op.y}
151
+ r="2"
152
+ fill="none"
153
+ stroke={box.stroke}
154
+ stroke-width="1"
155
+ data-plot-element="box-outlier"
156
+ role="graphics-symbol"
157
+ aria-label={`outlier: ${o.value}`}
158
+ onmouseenter={() => plotState.setHovered({ ...box.data, value: o.value })}
159
+ onmouseleave={() => plotState.clearHovered()}
160
+ />
161
+ {/each}
120
162
  {/each}
121
163
  </g>
122
164
  {/if}
@@ -1,11 +1,20 @@
1
1
  type Props = {
2
2
  x?: string;
3
3
  y?: string;
4
+ /** Box interior aesthetic (field); defaults to `x`. */
4
5
  fill?: string;
6
+ /** Outline aesthetic (field); overrides whisker/median/border stroke when set. */
7
+ color?: string;
5
8
  stat?: string;
6
- options?: {
7
- opacity?: number;
8
- };
9
+ /** Offset the box to one half of the band (raincloud/split plots). Default 'center'. */
10
+ side?: 'left' | 'right' | 'center';
11
+ /** Box thickness as a fraction of the (sub-)band — use a small value (e.g. 0.16) for a
12
+ * thin raincloud box. Defaults to the full box width. */
13
+ width?: number;
14
+ /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
15
+ pattern?: string;
16
+ /** Fixed box opacity 0–1; defaults to the per-geom preset (box = 0.5). */
17
+ alpha?: number;
9
18
  };
10
19
  declare const Box: import("svelte").Component<Props, {}, "">;
11
20
  type Box = ReturnType<typeof Box>;