@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
@@ -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;
@@ -1,23 +1,27 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
- import { buildSwarm } from '../lib/brewing/marks/swarm.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildJitterMarks } from './lib/marks/jitter.js'
5
6
 
6
7
  type Props = {
7
8
  x?: string
8
9
  y?: string
10
+ /** Point interior aesthetic (field); defaults to `x`. */
9
11
  fill?: string
12
+ /** Outline aesthetic (field); overrides the point stroke when set. */
13
+ color?: string
10
14
  r?: number
11
15
  method?: 'jitter' | 'swarm'
12
16
  /** Confine points to one half of the band (raincloud/split plots). Default 'center'. */
13
17
  side?: 'left' | 'right' | 'center'
14
- options?: { opacity?: number }
18
+ /** Fixed point opacity 0–1; defaults to the per-geom preset (jitter = 0.8). */
19
+ alpha?: number
15
20
  }
16
21
 
17
- let { x, y, fill, r = 2, method = 'jitter', side = 'center', options = {} }: Props = $props()
22
+ let { x, y, fill, color, r = 2, method = 'jitter', side = 'center', alpha }: Props = $props()
18
23
 
19
24
  const plotState = getContext<PlotState>('plot-state')
20
- let id = $state<string | null>(null)
21
25
 
22
26
  // Fall back to the root Plot channels when the geom omits x/y (composes under
23
27
  // <Plot.Root x y> without silently NaN-ing).
@@ -26,38 +30,19 @@
26
30
  // fill ?? effective-x drives the color lookup
27
31
  const fillChannel = $derived(fill ?? xf)
28
32
 
29
- onMount(() => {
30
- id = plotState.registerGeom({
31
- type: 'jitter',
32
- channels: { x: xf, y: yf, color: fillChannel },
33
- stat: 'identity',
34
- options
35
- })
36
- })
37
- onDestroy(() => {
38
- if (id) plotState.unregisterGeom(id)
39
- })
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)
40
44
 
41
- $effect(() => {
42
- if (id) plotState.updateGeom(id, { channels: { x: xf, y: yf, color: fillChannel }, stat: 'identity' })
43
- })
44
-
45
- const data = $derived(id ? plotState.geomData(id) : [])
46
- const xScale = $derived(plotState.xScale)
47
- const yScale = $derived(plotState.yScale)
48
- const colors = $derived(plotState.colors)
49
-
50
- const points = $derived.by(() => {
51
- if (!data?.length || !xScale || !yScale) return []
52
- const raw = buildSwarm(data, { x: xf, y: yf, fill: fillChannel }, xScale, yScale, colors, { method, r, side })
53
- if (!plotState.isFlipped) return raw
54
- // Horizontal: the band axis (swarm spread) stands up on screen-Y and the value
55
- // axis runs along screen-X — swap the computed coords through place().
56
- return raw.map((p) => {
57
- const s = plotState.place(p.cx, p.cy)
58
- return { ...p, cx: s.x, cy: s.y }
59
- })
60
- })
45
+ const points = $derived(geom.marks)
61
46
  </script>
62
47
 
63
48
  {#if points.length > 0}
@@ -68,7 +53,7 @@
68
53
  cy={pt.cy}
69
54
  {r}
70
55
  fill={pt.fill}
71
- fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.point}
56
+ fill-opacity={pt.alpha}
72
57
  stroke={pt.stroke}
73
58
  stroke-width="0.5"
74
59
  data-plot-element="jitter-point"
@@ -1,14 +1,16 @@
1
1
  type Props = {
2
2
  x?: string;
3
3
  y?: string;
4
+ /** Point interior aesthetic (field); defaults to `x`. */
4
5
  fill?: string;
6
+ /** Outline aesthetic (field); overrides the point stroke when set. */
7
+ color?: string;
5
8
  r?: number;
6
9
  method?: 'jitter' | 'swarm';
7
10
  /** Confine points to one half of the band (raincloud/split plots). Default 'center'. */
8
11
  side?: 'left' | 'right' | 'center';
9
- options?: {
10
- opacity?: number;
11
- };
12
+ /** Fixed point opacity 0–1; defaults to the per-geom preset (jitter = 0.8). */
13
+ alpha?: number;
12
14
  };
13
15
  declare const Jitter: import("svelte").Component<Props, {}, "">;
14
16
  type Jitter = ReturnType<typeof 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, plotState.place.bind(plotState))
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;
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
- import { scaleSqrt } from 'd3-scale'
4
3
  import type { PlotState } from '../PlotState.svelte.js'
5
- import { buildPoints } from '../lib/brewing/marks/points.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildPointMarks } from './lib/marks/point.js'
6
6
  import { keyboardNav } from '../lib/keyboard-nav.js'
7
7
  import { buildSelectDetail } from '../lib/select.js'
8
8
  import LabelPill from './LabelPill.svelte'
@@ -12,7 +12,6 @@
12
12
  minRadius?: number
13
13
  maxRadius?: number
14
14
  radius?: number
15
- opacity?: number
16
15
  jitter?: { width?: number; height?: number } | null
17
16
  labelOffset?: { x?: number; y?: number }
18
17
  }
@@ -20,9 +19,14 @@
20
19
  type Props = {
21
20
  x?: string
22
21
  y?: string
22
+ /** Outline / point-stroke aesthetic (field). */
23
23
  color?: string
24
+ /** Interior aesthetic (field); falls back to `color` when omitted. */
25
+ fill?: string
24
26
  size?: string
25
27
  symbol?: string
28
+ /** Fixed mark opacity 0–1; defaults to the per-geom preset (point = 0.8). */
29
+ alpha?: number
26
30
  label?: boolean | string | ((data: Row) => unknown)
27
31
  stat?: string
28
32
  options?: Options
@@ -34,8 +38,10 @@
34
38
  x,
35
39
  y,
36
40
  color,
41
+ fill,
37
42
  size,
38
43
  symbol: symbolField,
44
+ alpha,
39
45
  label = false,
40
46
  stat = 'identity',
41
47
  options = {},
@@ -51,7 +57,24 @@
51
57
  }
52
58
 
53
59
  const plotState = getContext<PlotState>('plot-state')
54
- let id = $state<string | null>(null)
60
+
61
+ // All plumbing (register/data/scales/colors) + mark building (geometry + fill/color/alpha)
62
+ // lives in GeomState + buildPointMarks. This component only renders + handles interactivity.
63
+ const geom = new GeomState(plotState, () => ({
64
+ type: 'point',
65
+ channels: { x, y, color, fill, size, symbol: symbolField },
66
+ stat,
67
+ options,
68
+ alpha,
69
+ build: buildPointMarks
70
+ }))
71
+ onMount(geom.register)
72
+ onDestroy(geom.destroy)
73
+ $effect(geom.sync)
74
+
75
+ const points = $derived(geom.marks)
76
+
77
+ const seriesField = $derived(fill ?? color)
55
78
 
56
79
  function selectPoint(pt: { data: Row }, event: MouseEvent | KeyboardEvent) {
57
80
  onselect?.(pt.data)
@@ -62,75 +85,17 @@
62
85
  plotState.data.indexOf(pt.data),
63
86
  { x, y },
64
87
  'point',
65
- color ? pt.data[color] : undefined,
88
+ seriesField ? pt.data[seriesField] : undefined,
66
89
  event
67
90
  )
68
91
  )
69
92
  }
70
-
71
- onMount(() => {
72
- id = plotState.registerGeom({
73
- type: 'point',
74
- channels: { x, y, color, size, symbol: symbolField },
75
- stat,
76
- options
77
- })
78
- })
79
- onDestroy(() => {
80
- if (id) plotState.unregisterGeom(id)
81
- })
82
-
83
- $effect(() => {
84
- if (id) plotState.updateGeom(id, { channels: { x, y, color, size, symbol: symbolField }, stat })
85
- })
86
-
87
- const data = $derived(id ? plotState.geomData(id) : [])
88
- const xScale = $derived(plotState.xScale)
89
- const yScale = $derived(plotState.yScale)
90
- const colors = $derived(plotState.colors)
91
- const symbolMap = $derived(plotState.symbols)
92
-
93
- function buildSizeScale() {
94
- if (!size || !data?.length) return null
95
- const vals = data.map((d) => Number(d[size])).filter((v) => !isNaN(v))
96
- if (!vals.length) return null
97
- const minVal = Math.min(...vals)
98
- const maxVal = Math.max(...vals)
99
- const minRadius = options.minRadius ?? 3
100
- const maxRadius = options.maxRadius ?? 20
101
- return scaleSqrt().domain([minVal, maxVal]).range([minRadius, maxRadius])
102
- }
103
-
104
- const sizeScale = $derived.by(() => buildSizeScale())
105
-
106
- const defaultRadius = $derived(options.radius ?? 4)
107
-
108
- const points = $derived.by(() => {
109
- if (!data?.length || !xScale || !yScale) return []
110
- const raw = buildPoints(
111
- data,
112
- { x, y, color, size, symbol: symbolField },
113
- xScale,
114
- yScale,
115
- colors,
116
- sizeScale,
117
- symbolMap,
118
- defaultRadius,
119
- options?.jitter ?? null
120
- )
121
- if (!plotState.isFlipped) return raw
122
- // Horizontal: swap each point's screen coords (value → screen-X, category → screen-Y).
123
- return raw.map((p) => {
124
- const s = plotState.place(p.cx, p.cy)
125
- return { ...p, cx: s.x, cy: s.y }
126
- })
127
- })
128
93
  </script>
129
94
 
130
95
  {#if points.length > 0}
131
96
  <g data-plot-geom="point">
132
97
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
133
- {#each points as pt, i (`${i}::${pt.data[x ?? '']}::${pt.data[y ?? '']}`)}
98
+ {#each points as pt (pt.key)}
134
99
  {#if pt.symbolPath}
135
100
  <path
136
101
  transform="translate({pt.cx},{pt.cy})"
@@ -138,7 +103,7 @@
138
103
  fill={pt.fill}
139
104
  stroke={pt.stroke}
140
105
  stroke-width="1"
141
- fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
106
+ fill-opacity={pt.alpha}
142
107
  data-plot-element="point"
143
108
  role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
144
109
  tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
@@ -160,7 +125,7 @@
160
125
  fill={pt.fill}
161
126
  stroke={pt.stroke}
162
127
  stroke-width="1"
163
- fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
128
+ fill-opacity={pt.alpha}
164
129
  data-plot-element="point"
165
130
  role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
166
131
  tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
@@ -3,7 +3,6 @@ type Options = {
3
3
  minRadius?: number;
4
4
  maxRadius?: number;
5
5
  radius?: number;
6
- opacity?: number;
7
6
  jitter?: {
8
7
  width?: number;
9
8
  height?: number;
@@ -16,9 +15,14 @@ type Options = {
16
15
  type Props = {
17
16
  x?: string;
18
17
  y?: string;
18
+ /** Outline / point-stroke aesthetic (field). */
19
19
  color?: string;
20
+ /** Interior aesthetic (field); falls back to `color` when omitted. */
21
+ fill?: string;
20
22
  size?: string;
21
23
  symbol?: string;
24
+ /** Fixed mark opacity 0–1; defaults to the per-geom preset (point = 0.8). */
25
+ alpha?: number;
22
26
  label?: boolean | string | ((data: Row) => unknown);
23
27
  stat?: string;
24
28
  options?: Options;