@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,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildCandleMarks } from './lib/marks/candlestick.js'
4
6
 
5
7
  type Options = {
6
8
  open?: string
@@ -17,86 +19,33 @@
17
19
  y?: string
18
20
  color?: string
19
21
  fill?: string
22
+ /** Fixed opacity 0–1; defaults to the per-geom preset (candlestick = 1). */
23
+ alpha?: number
20
24
  stat?: string
21
25
  options?: Options
22
26
  }
23
27
 
24
- let {
25
- x,
26
- y: _y = undefined,
27
- color = undefined,
28
- fill: fillProp = undefined,
29
- stat = 'identity',
30
- options = {}
31
- }: Props = $props()
28
+ let { x, y: _y = undefined, color, fill, alpha, stat = 'identity', options = {} }: Props = $props()
32
29
 
33
- const open = $derived(options.open ?? 'open')
30
+ const colorField = $derived(fill ?? color)
34
31
  const high = $derived(options.high ?? 'high')
35
- const low = $derived(options.low ?? 'low')
36
- const close = $derived(options.close ?? 'close')
37
- const upColor = $derived(options.upColor ?? '#22c55e')
38
- const downColor = $derived(options.downColor ?? '#ef4444')
39
32
  const wickWidth = $derived(options.wickWidth ?? 1)
40
-
41
33
  const plotState = getContext<PlotState>('plot-state')
42
- let id = $state<string | null>(null)
43
-
44
- onMount(() => {
45
- // Register with y pointing to 'high' so the y scale covers the full range
46
- id = plotState.registerGeom({
47
- type: 'candlestick',
48
- channels: { x, y: high, color: fillProp ?? color },
49
- stat
50
- })
51
- })
52
- onDestroy(() => {
53
- if (id) plotState.unregisterGeom(id)
54
- })
55
-
56
- $effect(() => {
57
- if (id)
58
- plotState.updateGeom(id, {
59
- channels: { x, y: high, color: fillProp ?? color },
60
- stat
61
- })
62
- })
63
-
64
- const data = $derived(id ? plotState.geomData(id) : [])
65
- const xScale = $derived(plotState.xScale)
66
- const yScale = $derived(plotState.yScale)
67
-
68
- const candles = $derived.by(() => {
69
- if (!data?.length || !xScale || !yScale) return []
70
- const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 10
71
- const bodyWidth = bw * 0.6
72
- const bodyOffset = (bw - bodyWidth) / 2
73
34
 
74
- return data.map((d, i) => {
75
- const xPos = (xScale(d[x ?? '']) ?? 0) + bodyOffset
76
- const openVal = Number(d[open])
77
- const closeVal = Number(d[close])
78
- const highVal = Number(d[high])
79
- const lowVal = Number(d[low])
80
- const isUp = closeVal >= openVal
81
- const bodyTop = yScale(isUp ? closeVal : openVal)
82
- const bodyBottom = yScale(isUp ? openVal : closeVal)
35
+ // Register with y pointing to 'high' so the y scale covers the full range.
36
+ const geom = new GeomState(plotState, () => ({
37
+ type: 'candlestick',
38
+ channels: { x, y: high, color: colorField },
39
+ stat,
40
+ options,
41
+ alpha,
42
+ build: buildCandleMarks
43
+ }))
44
+ onMount(geom.register)
45
+ onDestroy(geom.destroy)
46
+ $effect(geom.sync)
83
47
 
84
- return {
85
- key: `${d[x ?? '']}-${i}`,
86
- // Body
87
- bodyX: xPos,
88
- bodyY: bodyTop,
89
- bodyWidth,
90
- bodyHeight: Math.max(1, bodyBottom - bodyTop),
91
- fill: isUp ? upColor : downColor,
92
- // Wick
93
- wickX: xPos + bodyWidth / 2,
94
- wickTop: yScale(highVal),
95
- wickBottom: yScale(lowVal),
96
- data: d
97
- }
98
- })
99
- })
48
+ const candles = $derived(geom.marks)
100
49
  </script>
101
50
 
102
51
  {#if candles.length > 0}
@@ -104,11 +53,12 @@
104
53
  {#each candles as c (c.key)}
105
54
  <!-- Wick (high to low) -->
106
55
  <line
107
- x1={c.wickX}
108
- y1={c.wickTop}
109
- x2={c.wickX}
110
- y2={c.wickBottom}
56
+ x1={c.wickX1}
57
+ y1={c.wickY1}
58
+ x2={c.wickX2}
59
+ y2={c.wickY2}
111
60
  stroke={c.fill}
61
+ stroke-opacity={c.alpha}
112
62
  stroke-width={wickWidth}
113
63
  data-plot-element="wick"
114
64
  />
@@ -119,12 +69,13 @@
119
69
  width={c.bodyWidth}
120
70
  height={c.bodyHeight}
121
71
  fill={c.fill}
72
+ fill-opacity={c.alpha}
122
73
  data-plot-element="candle"
123
74
  role="img"
124
75
  onmouseenter={() => plotState.setHovered(c.data)}
125
76
  onmouseleave={() => plotState.clearHovered()}
126
77
  >
127
- <title>{c.data[x ?? '']}: O={c.data[open]} H={c.data[high]} L={c.data[low]} C={c.data[close]}</title>
78
+ <title>{c.data[x ?? '']}: O={c.data[options.open ?? 'open']} H={c.data[options.high ?? 'high']} L={c.data[options.low ?? 'low']} C={c.data[options.close ?? 'close']}</title>
128
79
  </rect>
129
80
  {/each}
130
81
  </g>
@@ -12,6 +12,8 @@ type Props = {
12
12
  y?: string;
13
13
  color?: string;
14
14
  fill?: string;
15
+ /** Fixed opacity 0–1; defaults to the per-geom preset (candlestick = 1). */
16
+ alpha?: number;
15
17
  stat?: string;
16
18
  options?: Options;
17
19
  };
@@ -1,85 +1,39 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildHeatmapMarks } from './lib/marks/heatmap.js'
4
6
 
5
7
  type Props = {
6
8
  x?: string
7
9
  y?: string
8
10
  color?: string
9
11
  fill?: string
12
+ /** Fixed cell opacity 0–1; defaults to the per-geom preset (heatmap = 1). */
13
+ alpha?: number
10
14
  stat?: string
11
15
  options?: { rounded?: number }
12
16
  }
13
17
 
14
- let {
15
- x,
16
- y,
17
- color,
18
- fill: fillProp,
19
- stat = 'identity',
20
- options = {}
21
- }: Props = $props()
22
-
23
- const colorChannel = $derived(fillProp ?? color)
18
+ let { x, y, color, fill, alpha, stat = 'identity', options = {} }: Props = $props()
24
19
 
20
+ const colorField = $derived(fill ?? color)
25
21
  const plotState = getContext<PlotState>('plot-state')
26
- let id = $state<string | null>(null)
27
-
28
- onMount(() => {
29
- id = plotState.registerGeom({
30
- type: 'heatmap',
31
- channels: { x, y, color: colorChannel },
32
- stat
33
- })
34
- })
35
- onDestroy(() => {
36
- if (id) plotState.unregisterGeom(id)
37
- })
38
22
 
39
- $effect(() => {
40
- if (id)
41
- plotState.updateGeom(id, {
42
- channels: { x, y, color: colorChannel },
43
- stat
44
- })
45
- })
46
-
47
- const data = $derived(id ? plotState.geomData(id) : [])
48
- const xScale = $derived(plotState.xScale)
49
- const yScale = $derived(plotState.yScale)
50
- const continuousColor = $derived(plotState.continuousColorScale)
51
- const categoricalColors = $derived(plotState.colors)
23
+ const geom = new GeomState(plotState, () => ({
24
+ type: 'heatmap',
25
+ channels: { x, y, color: colorField },
26
+ stat,
27
+ options,
28
+ alpha,
29
+ build: buildHeatmapMarks
30
+ }))
31
+ onMount(geom.register)
32
+ onDestroy(geom.destroy)
33
+ $effect(geom.sync)
34
+
35
+ const cells = $derived(geom.marks)
52
36
  const rx = $derived(options.rounded ?? 0)
53
-
54
- const cells = $derived.by(() => {
55
- if (!data?.length || !xScale || !yScale) return []
56
- const bwX = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 0
57
- const bwY = typeof yScale.bandwidth === 'function' ? yScale.bandwidth() : 0
58
-
59
- return data.map((d, i) => {
60
- const xVal = d[x ?? '']
61
- const yVal = d[y ?? '']
62
- const colorVal = colorChannel ? d[colorChannel] : null
63
- let cellFill = '#ccc'
64
-
65
- if (continuousColor) {
66
- cellFill = continuousColor.scale(Number(colorVal))
67
- } else {
68
- const entry = categoricalColors?.get(colorVal)
69
- if (entry) cellFill = entry.fill
70
- }
71
-
72
- return {
73
- key: `${xVal}-${yVal}-${i}`,
74
- x: xScale(xVal) ?? 0,
75
- y: yScale(yVal) ?? 0,
76
- width: bwX,
77
- height: bwY,
78
- fill: cellFill,
79
- data: d
80
- }
81
- })
82
- })
83
37
  </script>
84
38
 
85
39
  {#if cells.length > 0}
@@ -91,13 +45,14 @@
91
45
  width={Math.max(0, cell.width)}
92
46
  height={Math.max(0, cell.height)}
93
47
  fill={cell.fill}
48
+ fill-opacity={cell.alpha}
94
49
  rx={rx}
95
50
  data-plot-element="cell"
96
51
  role="img"
97
52
  onmouseenter={() => plotState.setHovered(cell.data)}
98
53
  onmouseleave={() => plotState.clearHovered()}
99
54
  >
100
- <title>{cell.data[x ?? '']}, {cell.data[y ?? '']}: {colorChannel ? cell.data[colorChannel] : ''}</title>
55
+ <title>{cell.data[x ?? '']}, {cell.data[y ?? '']}: {colorField ? cell.data[colorField] : ''}</title>
101
56
  </rect>
102
57
  {/each}
103
58
  </g>
@@ -3,6 +3,8 @@ type Props = {
3
3
  y?: string;
4
4
  color?: string;
5
5
  fill?: string;
6
+ /** Fixed cell opacity 0–1; defaults to the per-geom preset (heatmap = 1). */
7
+ alpha?: number;
6
8
  stat?: string;
7
9
  options?: {
8
10
  rounded?: number;
@@ -1,120 +1,39 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
-
5
- type Row = Record<string, unknown>
6
- type Scale = (value: unknown) => number | undefined
7
- type Bin = { cx: number; cy: number; count: number; points: Row[] }
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildHexbinMarks, hexPath } from './lib/marks/hexbin.js'
8
6
 
9
7
  type Props = {
10
8
  x?: string
11
9
  y?: string
12
10
  color?: string
13
11
  fill?: string
12
+ /** Fixed hex opacity 0–1; defaults to the per-geom preset (hexbin = 1). */
13
+ alpha?: number
14
14
  stat?: string
15
15
  options?: { radius?: number }
16
16
  }
17
17
 
18
- let {
19
- x,
20
- y,
21
- color = undefined,
22
- fill: fillProp = undefined,
23
- stat = 'identity',
24
- options = {}
25
- }: Props = $props()
18
+ let { x, y, color, fill, alpha, stat = 'identity', options = {} }: Props = $props()
26
19
 
20
+ const colorField = $derived(fill ?? color)
27
21
  const radius = $derived(options.radius ?? 20)
28
-
29
22
  const plotState = getContext<PlotState>('plot-state')
30
- let id = $state<string | null>(null)
31
-
32
- onMount(() => {
33
- id = plotState.registerGeom({
34
- type: 'hexbin',
35
- channels: { x, y, color: fillProp ?? color },
36
- stat
37
- })
38
- })
39
- onDestroy(() => {
40
- if (id) plotState.unregisterGeom(id)
41
- })
42
-
43
- $effect(() => {
44
- if (id)
45
- plotState.updateGeom(id, {
46
- channels: { x, y, color: fillProp ?? color },
47
- stat
48
- })
49
- })
50
-
51
- const data = $derived(id ? plotState.geomData(id) : [])
52
- const xScale = $derived(plotState.xScale)
53
- const yScale = $derived(plotState.yScale)
54
- const continuousColor = $derived(plotState.continuousColorScale)
55
-
56
- // Hex binning: group data points into hexagonal cells
57
- // eslint-disable-next-line max-params, complexity
58
- function hexBin(
59
- rows: Row[],
60
- xField: string | undefined,
61
- yField: string | undefined,
62
- xScale: Scale,
63
- yScale: Scale,
64
- r: number
65
- ): Bin[] {
66
- const dx = r * 2 * Math.sin(Math.PI / 3) // horizontal spacing
67
- const dy = r * 1.5 // vertical spacing
68
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
69
- const bins = new Map<string, Bin>()
70
-
71
- for (const d of rows) {
72
- const px = xScale(d[xField ?? ''])
73
- const py = yScale(d[yField ?? ''])
74
- if (px === null || px === undefined || py === null || py === undefined) continue
75
-
76
- // Find nearest hex center
77
- const col = Math.round(px / dx)
78
- const row = Math.round(py / dy)
79
- const cx = col * dx + (row % 2 ? dx / 2 : 0)
80
- const cy = row * dy
81
- const key = `${col},${row}`
82
-
83
- let bin = bins.get(key)
84
- if (!bin) {
85
- bin = { cx, cy, count: 0, points: [] }
86
- bins.set(key, bin)
87
- }
88
- bin.count++
89
- bin.points.push(d)
90
- }
91
- return [...bins.values()]
92
- }
93
-
94
- // Build hexagon path for a given radius
95
- function hexPath(r: number) {
96
- const angles = [0, 1, 2, 3, 4, 5].map((i) => ((i * 60 - 30) * Math.PI) / 180)
97
- return `${angles.map((a, i) => `${i === 0 ? 'M' : 'L'}${r * Math.cos(a)},${r * Math.sin(a)}`).join('') }Z`
98
- }
99
-
100
- const hexes = $derived.by(() => {
101
- if (!data?.length || !xScale || !yScale) return []
102
- const bins = hexBin(data, x, y, xScale, yScale, radius)
103
- const maxCount = Math.max(1, ...bins.map((b) => b.count))
104
- const colorScale = continuousColor?.scale
105
-
106
- return bins.map((bin, i) => ({
107
- key: `hex-${i}`,
108
- cx: bin.cx,
109
- cy: bin.cy,
110
- count: bin.count,
111
- fill: colorScale
112
- ? colorScale(bin.count)
113
- : `rgba(66, 133, 244, ${0.1 + 0.9 * (bin.count / maxCount)})`,
114
- data: { count: bin.count, x: bin.cx, y: bin.cy }
115
- }))
116
- })
117
23
 
24
+ const geom = new GeomState(plotState, () => ({
25
+ type: 'hexbin',
26
+ channels: { x, y, color: colorField },
27
+ stat,
28
+ options,
29
+ alpha,
30
+ build: buildHexbinMarks
31
+ }))
32
+ onMount(geom.register)
33
+ onDestroy(geom.destroy)
34
+ $effect(geom.sync)
35
+
36
+ const hexes = $derived(geom.marks)
118
37
  const hex = $derived(hexPath(radius))
119
38
  </script>
120
39
 
@@ -125,6 +44,7 @@
125
44
  transform="translate({h.cx},{h.cy})"
126
45
  d={hex}
127
46
  fill={h.fill}
47
+ fill-opacity={h.alpha}
128
48
  stroke="white"
129
49
  stroke-width="0.5"
130
50
  data-plot-element="hex"
@@ -3,6 +3,8 @@ type Props = {
3
3
  y?: string;
4
4
  color?: string;
5
5
  fill?: string;
6
+ /** Fixed hex opacity 0–1; defaults to the per-geom preset (hexbin = 1). */
7
+ alpha?: number;
6
8
  stat?: string;
7
9
  options?: {
8
10
  radius?: number;
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import type { PlotState } from '../PlotState.svelte.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildJitterMarks } from './lib/marks/jitter.js'
6
+
7
+ type Props = {
8
+ x?: string
9
+ y?: string
10
+ /** Point interior aesthetic (field); defaults to `x`. */
11
+ fill?: string
12
+ /** Outline aesthetic (field); overrides the point stroke when set. */
13
+ color?: string
14
+ r?: number
15
+ method?: 'jitter' | 'swarm'
16
+ /** Confine points to one half of the band (raincloud/split plots). Default 'center'. */
17
+ side?: 'left' | 'right' | 'center'
18
+ /** Fixed point opacity 0–1; defaults to the per-geom preset (jitter = 0.8). */
19
+ alpha?: number
20
+ }
21
+
22
+ let { x, y, fill, color, r = 2, method = 'jitter', side = 'center', alpha }: Props = $props()
23
+
24
+ const plotState = getContext<PlotState>('plot-state')
25
+
26
+ // Fall back to the root Plot channels when the geom omits x/y (composes under
27
+ // <Plot.Root x y> without silently NaN-ing).
28
+ const xf = $derived(x ?? plotState.channels?.x)
29
+ const yf = $derived(y ?? plotState.channels?.y)
30
+ // fill ?? effective-x drives the color lookup
31
+ const fillChannel = $derived(fill ?? xf)
32
+
33
+ const geom = new GeomState(plotState, () => ({
34
+ type: 'jitter',
35
+ channels: { x: xf, y: yf, fill: fillChannel, color },
36
+ stat: 'identity',
37
+ options: { method, r, side },
38
+ alpha,
39
+ build: buildJitterMarks
40
+ }))
41
+ onMount(geom.register)
42
+ onDestroy(geom.destroy)
43
+ $effect(geom.sync)
44
+
45
+ const points = $derived(geom.marks)
46
+ </script>
47
+
48
+ {#if points.length > 0}
49
+ <g data-plot-geom="jitter">
50
+ {#each points as pt, i (`${String(pt.cx)}::${i}`)}
51
+ <circle
52
+ cx={pt.cx}
53
+ cy={pt.cy}
54
+ {r}
55
+ fill={pt.fill}
56
+ fill-opacity={pt.alpha}
57
+ stroke={pt.stroke}
58
+ stroke-width="0.5"
59
+ data-plot-element="jitter-point"
60
+ role="graphics-symbol"
61
+ aria-label={`(${String(pt.data[xf ?? ''])}, ${String(pt.data[yf ?? ''])})`}
62
+ onmouseenter={() => plotState.setHovered(pt.data)}
63
+ onmouseleave={() => plotState.clearHovered()}
64
+ />
65
+ {/each}
66
+ </g>
67
+ {/if}
@@ -0,0 +1,17 @@
1
+ type Props = {
2
+ x?: string;
3
+ y?: string;
4
+ /** Point interior aesthetic (field); defaults to `x`. */
5
+ fill?: string;
6
+ /** Outline aesthetic (field); overrides the point stroke when set. */
7
+ color?: string;
8
+ r?: number;
9
+ method?: 'jitter' | 'swarm';
10
+ /** Confine points to one half of the band (raincloud/split plots). Default 'center'. */
11
+ side?: 'left' | 'right' | 'center';
12
+ /** Fixed point opacity 0–1; defaults to the per-geom preset (jitter = 0.8). */
13
+ alpha?: number;
14
+ };
15
+ declare const Jitter: import("svelte").Component<Props, {}, "">;
16
+ type Jitter = ReturnType<typeof Jitter>;
17
+ export default Jitter;
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
- import { buildLines } from '../lib/brewing/marks/lines.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildLineMarks } from './lib/marks/line.js'
5
6
  import { buildSymbolPath } from '../lib/brewing/marks/points.js'
6
7
  import { keyboardNav } from '../lib/keyboard-nav.js'
7
8
  import { buildSelectDetail } from '../lib/select.js'
@@ -18,8 +19,13 @@
18
19
  type Props = {
19
20
  x?: string
20
21
  y?: string
22
+ /** Line/stroke aesthetic (field). */
21
23
  color?: string
24
+ /** Accepted as an alias for `color` (a line has no interior). */
25
+ fill?: string
22
26
  symbol?: string
27
+ /** Fixed line opacity 0–1; defaults to the per-geom preset (line = 1). */
28
+ alpha?: number
23
29
  label?: boolean | string | ((data: Row) => unknown)
24
30
  stat?: string
25
31
  options?: Options
@@ -31,7 +37,9 @@
31
37
  x,
32
38
  y,
33
39
  color,
40
+ fill,
34
41
  symbol: symbolField,
42
+ alpha,
35
43
  label = false,
36
44
  stat = 'identity',
37
45
  options = {},
@@ -47,7 +55,23 @@
47
55
  }
48
56
 
49
57
  const plotState = getContext<PlotState>('plot-state')
50
- let id = $state<string | null>(null)
58
+
59
+ const geom = new GeomState(plotState, () => ({
60
+ type: 'line',
61
+ channels: { x, y, color, fill, symbol: symbolField },
62
+ stat,
63
+ options,
64
+ alpha,
65
+ build: buildLineMarks
66
+ }))
67
+ onMount(geom.register)
68
+ onDestroy(geom.destroy)
69
+ $effect(geom.sync)
70
+
71
+ const lines = $derived(geom.marks)
72
+ // Markers are a rendering detail resolved from the shared symbol scale.
73
+ const symbolMap = $derived(plotState.symbols)
74
+ const markerRadius = $derived(options.markerRadius ?? 4)
51
75
 
52
76
  function selectPoint(
53
77
  pt: { data: Row },
@@ -60,35 +84,6 @@
60
84
  buildSelectDetail(pt.data, plotState.data.indexOf(pt.data), { x, y }, 'line', seg?.key, event)
61
85
  )
62
86
  }
63
-
64
- onMount(() => {
65
- id = plotState.registerGeom({
66
- type: 'line',
67
- channels: { x, y, color, symbol: symbolField },
68
- stat,
69
- options
70
- })
71
- })
72
- onDestroy(() => {
73
- if (id) plotState.unregisterGeom(id)
74
- })
75
-
76
- $effect(() => {
77
- if (id) plotState.updateGeom(id, { channels: { x, y, color, symbol: symbolField }, stat })
78
- })
79
-
80
- const data = $derived(id ? plotState.geomData(id) : [])
81
- const xScale = $derived(plotState.xScale)
82
- const yScale = $derived(plotState.yScale)
83
- const colors = $derived(plotState.colors)
84
- const symbolMap = $derived(plotState.symbols)
85
-
86
- const lines = $derived.by(() => {
87
- if (!data?.length || !xScale || !yScale) return []
88
- return buildLines(data, { x, y, color }, xScale, yScale, colors, options.curve)
89
- })
90
-
91
- const markerRadius = $derived(options.markerRadius ?? 4)
92
87
  </script>
93
88
 
94
89
  {#if lines.length > 0}
@@ -101,6 +96,7 @@
101
96
  stroke-width={options.strokeWidth ?? 2}
102
97
  stroke-linejoin="round"
103
98
  stroke-linecap="round"
99
+ stroke-opacity={seg.alpha}
104
100
  data-plot-element="line"
105
101
  />
106
102
  {#if symbolField && symbolMap}
@@ -11,8 +11,13 @@ type Options = {
11
11
  type Props = {
12
12
  x?: string;
13
13
  y?: string;
14
+ /** Line/stroke aesthetic (field). */
14
15
  color?: string;
16
+ /** Accepted as an alias for `color` (a line has no interior). */
17
+ fill?: string;
15
18
  symbol?: string;
19
+ /** Fixed line opacity 0–1; defaults to the per-geom preset (line = 1). */
20
+ alpha?: number;
16
21
  label?: boolean | string | ((data: Row) => unknown);
17
22
  stat?: string;
18
23
  options?: Options;