@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,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,69 +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
- return 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
- })
122
93
  </script>
123
94
 
124
95
  {#if points.length > 0}
125
96
  <g data-plot-geom="point">
126
97
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
127
- {#each points as pt, i (`${i}::${pt.data[x ?? '']}::${pt.data[y ?? '']}`)}
98
+ {#each points as pt (pt.key)}
128
99
  {#if pt.symbolPath}
129
100
  <path
130
101
  transform="translate({pt.cx},{pt.cy})"
@@ -132,7 +103,7 @@
132
103
  fill={pt.fill}
133
104
  stroke={pt.stroke}
134
105
  stroke-width="1"
135
- fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
106
+ fill-opacity={pt.alpha}
136
107
  data-plot-element="point"
137
108
  role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
138
109
  tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
@@ -154,7 +125,7 @@
154
125
  fill={pt.fill}
155
126
  stroke={pt.stroke}
156
127
  stroke-width="1"
157
- fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
128
+ fill-opacity={pt.alpha}
158
129
  data-plot-element="point"
159
130
  role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
160
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;
@@ -1,8 +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
-
5
- type Node = { name: string; y: number; height: number }
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildRibbonMarks } from './lib/marks/ribbon.js'
6
6
 
7
7
  type Options = {
8
8
  source?: string
@@ -15,141 +15,47 @@
15
15
  y?: string
16
16
  color?: string
17
17
  fill?: string
18
+ /** Fixed ribbon opacity 0–1; defaults to the per-geom preset (ribbon = 0.5). */
19
+ alpha?: number
18
20
  stat?: string
19
21
  options?: Options
20
22
  }
21
23
 
22
- let {
23
- x = undefined,
24
- y = undefined,
25
- color = undefined,
26
- fill: fillProp = undefined,
27
- stat = 'identity',
28
- options = {}
29
- }: Props = $props()
24
+ let { x, y, color, fill, alpha, stat = 'identity', options = {} }: Props = $props()
30
25
 
26
+ const colorField = $derived(fill ?? color)
31
27
  const sourceField = $derived(options.source ?? 'source')
32
28
  const targetField = $derived(options.target ?? 'target')
33
29
  const valueField = $derived(options.value ?? 'value')
34
- const colorChannel = $derived(fillProp ?? color)
35
-
36
30
  const plotState = getContext<PlotState>('plot-state')
37
- let id = $state<string | null>(null)
38
-
39
- onMount(() => {
40
- id = plotState.registerGeom({
41
- type: 'ribbon',
42
- channels: { x, y, color: colorChannel },
43
- stat
44
- })
45
- })
46
- onDestroy(() => {
47
- if (id) plotState.unregisterGeom(id)
48
- })
49
-
50
- $effect(() => {
51
- if (id)
52
- plotState.updateGeom(id, {
53
- channels: { x, y, color: colorChannel },
54
- stat
55
- })
56
- })
57
-
58
- const data = $derived(id ? plotState.geomData(id) : [])
59
- const colors = $derived(plotState.colors)
60
- const innerHeight = $derived(plotState.innerHeight)
61
- const innerWidth = $derived(plotState.innerWidth)
62
-
63
- // Compute Sankey-style layout
64
- // eslint-disable-next-line max-lines-per-function, complexity
65
- const ribbons = $derived.by(() => {
66
- if (!data?.length) return { links: [], sourceNodes: [], targetNodes: [] }
67
-
68
- // Aggregate flows
69
- const flows = data.map((d) => ({
70
- source: String(d[sourceField]),
71
- target: String(d[targetField]),
72
- value: Number(d[valueField]) || 0,
73
- data: d
74
- }))
75
-
76
- // Compute node positions
77
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
78
- const sourceMap = new Map<string, number>()
79
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
80
- const targetMap = new Map<string, number>()
81
- for (const f of flows) {
82
- sourceMap.set(f.source, (sourceMap.get(f.source) ?? 0) + f.value)
83
- targetMap.set(f.target, (targetMap.get(f.target) ?? 0) + f.value)
84
- }
85
-
86
- const totalSource = Math.max(1, [...sourceMap.values()].reduce((s, v) => s + v, 0))
87
- const totalTarget = Math.max(1, [...targetMap.values()].reduce((s, v) => s + v, 0))
88
- const padding = 4
89
- const availHeight = innerHeight - padding * (Math.max(sourceMap.size, targetMap.size) - 1)
90
-
91
- // Position source nodes on left
92
- let sourceY = 0
93
- const sourceNodes: Node[] = []
94
- for (const [name, value] of sourceMap) {
95
- const h = (value / totalSource) * availHeight
96
- sourceNodes.push({ name, y: sourceY, height: h })
97
- sourceY += h + padding
98
- }
99
-
100
- // Position target nodes on right
101
- let targetY = 0
102
- const targetNodes: Node[] = []
103
- for (const [name, value] of targetMap) {
104
- const h = (value / totalTarget) * availHeight
105
- targetNodes.push({ name, y: targetY, height: h })
106
- targetY += h + padding
107
- }
108
-
109
- // Build ribbon paths
110
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
111
- const sourceOffsets = new Map(sourceNodes.map((n) => [n.name, n.y]))
112
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
113
- const targetOffsets = new Map(targetNodes.map((n) => [n.name, n.y]))
114
- const x0 = 0
115
- const x1 = innerWidth
116
-
117
- // eslint-disable-next-line complexity
118
- const links = flows.map((f, i) => {
119
- const sy0 = sourceOffsets.get(f.source) ?? 0
120
- const ty0 = targetOffsets.get(f.target) ?? 0
121
- const sourceTotal = sourceMap.get(f.source) ?? 1
122
- const targetTotal = targetMap.get(f.target) ?? 1
123
- const sh = (f.value / sourceTotal) * (sourceNodes.find((n) => n.name === f.source)?.height ?? 0)
124
- const th = (f.value / targetTotal) * (targetNodes.find((n) => n.name === f.target)?.height ?? 0)
125
-
126
- const path = `M${x0},${sy0} C${innerWidth * 0.4},${sy0} ${innerWidth * 0.6},${ty0} ${x1},${ty0} L${x1},${ty0 + th} C${innerWidth * 0.6},${ty0 + th} ${innerWidth * 0.4},${sy0 + sh} ${x0},${sy0 + sh} Z`
127
-
128
- // Update offsets for stacking
129
- sourceOffsets.set(f.source, sy0 + sh)
130
- targetOffsets.set(f.target, ty0 + th)
131
-
132
- const fillColor = colors?.get(f.source)?.fill ?? colors?.get(f.target)?.fill ?? '#888'
133
-
134
- return {
135
- key: `ribbon-${i}`,
136
- d: path,
137
- fill: fillColor,
138
- data: f.data
139
- }
140
- })
141
31
 
142
- return { links, sourceNodes, targetNodes }
143
- })
32
+ const geom = new GeomState(plotState, () => ({
33
+ type: 'ribbon',
34
+ channels: { x, y, color: colorField },
35
+ stat,
36
+ options,
37
+ alpha,
38
+ build: buildRibbonMarks
39
+ }))
40
+ onMount(geom.register)
41
+ onDestroy(geom.destroy)
42
+ $effect(geom.sync)
43
+
44
+ // GeomState returns [] for empty data; buildRibbonMarks otherwise returns
45
+ // { links, sourceNodes, targetNodes } — normalize each to an array.
46
+ const marks = $derived(geom.marks)
47
+ const links = $derived(marks?.links ?? [])
48
+ const sourceNodes = $derived(marks?.sourceNodes ?? [])
49
+ const targetNodes = $derived(marks?.targetNodes ?? [])
144
50
  </script>
145
51
 
146
- {#if ribbons.links.length > 0}
52
+ {#if links.length > 0}
147
53
  <g data-plot-geom="ribbon">
148
- {#each ribbons.links as link (link.key)}
54
+ {#each links as link (link.key)}
149
55
  <path
150
56
  d={link.d}
151
57
  fill={link.fill}
152
- opacity="0.5"
58
+ opacity={link.alpha}
153
59
  data-plot-element="ribbon"
154
60
  role="img"
155
61
  onmouseenter={() => plotState.setHovered(link.data)}
@@ -159,7 +65,7 @@
159
65
  </path>
160
66
  {/each}
161
67
  <!-- Source node labels -->
162
- {#each ribbons.sourceNodes as node (node.name)}
68
+ {#each sourceNodes as node (node.name)}
163
69
  <text
164
70
  x="-4"
165
71
  y={node.y + node.height / 2}
@@ -171,9 +77,9 @@
171
77
  >{node.name}</text>
172
78
  {/each}
173
79
  <!-- Target node labels -->
174
- {#each ribbons.targetNodes as node (node.name)}
80
+ {#each targetNodes as node (node.name)}
175
81
  <text
176
- x={innerWidth + 4}
82
+ x={plotState.innerWidth + 4}
177
83
  y={node.y + node.height / 2}
178
84
  text-anchor="start"
179
85
  dominant-baseline="middle"
@@ -8,6 +8,8 @@ type Props = {
8
8
  y?: string;
9
9
  color?: string;
10
10
  fill?: string;
11
+ /** Fixed ribbon opacity 0–1; defaults to the per-geom preset (ribbon = 0.5). */
12
+ alpha?: number;
11
13
  stat?: string;
12
14
  options?: Options;
13
15
  };
@@ -0,0 +1,117 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte'
3
+ import type { PlotState } from '../PlotState.svelte.js'
4
+
5
+ type RuleValue = number | string
6
+ type Props = {
7
+ /** Vertical reference line(s) at these x-axis value(s) (a value, or a band category). */
8
+ x?: RuleValue | RuleValue[]
9
+ /** Horizontal reference line(s) at these y-axis value(s). */
10
+ y?: RuleValue | RuleValue[]
11
+ /** Line color (aligns with the other geoms' `color` aesthetic). */
12
+ color?: string
13
+ /** @deprecated use `color` — kept as an alias. */
14
+ stroke?: string
15
+ /** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
16
+ dash?: string
17
+ strokeWidth?: number
18
+ /** Optional label drawn at the line's end. String, or a fn of the value. */
19
+ label?: string | ((value: RuleValue) => string)
20
+ }
21
+
22
+ let {
23
+ x,
24
+ y,
25
+ color = undefined,
26
+ stroke = undefined,
27
+ dash = '4 4',
28
+ strokeWidth = 1,
29
+ label = undefined
30
+ }: Props = $props()
31
+
32
+ const plotState = getContext<PlotState>('plot-state')
33
+ const xScale = $derived(plotState.xScale)
34
+ const yScale = $derived(plotState.yScale)
35
+
36
+ const toArray = (v: RuleValue | RuleValue[] | undefined): RuleValue[] =>
37
+ v === undefined ? [] : Array.isArray(v) ? v : [v]
38
+
39
+ // Position along an axis: band scales resolve to the band centre, linear scales map directly.
40
+ function axisPos(scale: { (v: unknown): number | undefined; bandwidth?: () => number }, v: unknown) {
41
+ const base = scale(v) ?? 0
42
+ return typeof scale.bandwidth === 'function' ? base + scale.bandwidth() / 2 : base
43
+ }
44
+ // The full [min, max] screen extent of a scale's range (spans the perpendicular axis).
45
+ function axisExtent(scale: { range: () => number[] }) {
46
+ const r = scale.range()
47
+ return [Math.min(...r), Math.max(...r)]
48
+ }
49
+
50
+ const labelFor = (v: RuleValue) =>
51
+ typeof label === 'function' ? label(v) : typeof label === 'string' ? label : null
52
+
53
+ // Each rule is expressed in (x-channel, y-channel) space and placed to screen, so it
54
+ // transposes under orientation flip: a y-value reference is horizontal when vertical and
55
+ // vertical when flipped (it stays perpendicular to the value axis).
56
+ const rules = $derived.by(() => {
57
+ if (!xScale || !yScale) return []
58
+ const place = plotState.place.bind(plotState)
59
+ const out: Array<{
60
+ key: string
61
+ x1: number
62
+ y1: number
63
+ x2: number
64
+ y2: number
65
+ value: RuleValue
66
+ text: string | null
67
+ }> = []
68
+ const [xLo, xHi] = axisExtent(xScale)
69
+ const [yLo, yHi] = axisExtent(yScale)
70
+
71
+ for (const yv of toArray(y)) {
72
+ const yc = axisPos(yScale, yv)
73
+ const a = place(xLo, yc)
74
+ const b = place(xHi, yc)
75
+ out.push({ key: `y-${yv}`, x1: a.x, y1: a.y, x2: b.x, y2: b.y, value: yv, text: labelFor(yv) })
76
+ }
77
+ for (const xv of toArray(x)) {
78
+ const xc = axisPos(xScale, xv)
79
+ const a = place(xc, yLo)
80
+ const b = place(xc, yHi)
81
+ out.push({ key: `x-${xv}`, x1: a.x, y1: a.y, x2: b.x, y2: b.y, value: xv, text: labelFor(xv) })
82
+ }
83
+ return out
84
+ })
85
+
86
+ const strokeColor = $derived(color ?? stroke ?? 'currentColor')
87
+ </script>
88
+
89
+ {#if rules.length > 0}
90
+ <g data-plot-geom="rule">
91
+ {#each rules as rule (rule.key)}
92
+ <line
93
+ x1={rule.x1}
94
+ y1={rule.y1}
95
+ x2={rule.x2}
96
+ y2={rule.y2}
97
+ stroke={strokeColor}
98
+ stroke-width={strokeWidth}
99
+ stroke-dasharray={dash || undefined}
100
+ data-plot-element="rule"
101
+ data-plot-value={rule.value}
102
+ />
103
+ {#if rule.text}
104
+ <text
105
+ x={rule.x2}
106
+ y={rule.y2}
107
+ dx="-4"
108
+ dy="-4"
109
+ text-anchor="end"
110
+ font-size="10"
111
+ fill={strokeColor}
112
+ data-plot-element="rule-label"
113
+ >{rule.text}</text>
114
+ {/if}
115
+ {/each}
116
+ </g>
117
+ {/if}
@@ -0,0 +1,19 @@
1
+ type RuleValue = number | string;
2
+ type Props = {
3
+ /** Vertical reference line(s) at these x-axis value(s) (a value, or a band category). */
4
+ x?: RuleValue | RuleValue[];
5
+ /** Horizontal reference line(s) at these y-axis value(s). */
6
+ y?: RuleValue | RuleValue[];
7
+ /** Line color (aligns with the other geoms' `color` aesthetic). */
8
+ color?: string;
9
+ /** @deprecated use `color` — kept as an alias. */
10
+ stroke?: string;
11
+ /** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
12
+ dash?: string;
13
+ strokeWidth?: number;
14
+ /** Optional label drawn at the line's end. String, or a fn of the value. */
15
+ label?: string | ((value: RuleValue) => string);
16
+ };
17
+ declare const Rule: import("svelte").Component<Props, {}, "">;
18
+ type Rule = ReturnType<typeof Rule>;
19
+ export default Rule;
@@ -1,49 +1,50 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
- import { buildViolins } from '../lib/brewing/marks/violins.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildViolinMarks } from './lib/marks/violin.js'
5
6
 
6
7
  type Props = {
7
8
  x?: string
8
9
  y?: string
10
+ /** Violin interior aesthetic (field); defaults to `x`. */
9
11
  fill?: string
12
+ /** Outline aesthetic (field); overrides the silhouette stroke when set. */
13
+ color?: string
10
14
  stat?: string
11
- options?: { opacity?: number }
15
+ /** Half-violin: 'left'/'right' draw one side (flat edge at the band centre) for
16
+ * raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
17
+ side?: 'left' | 'right' | 'center'
18
+ /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
19
+ pattern?: string
20
+ /** Fixed violin opacity 0–1; defaults to the per-geom preset (violin = 0.5). */
21
+ alpha?: number
12
22
  }
13
23
 
14
- let { x, y, fill, stat = 'boxplot', options = {} }: Props = $props()
24
+ let { x, y, fill, color, stat = 'boxplot', side = 'center', pattern, alpha }: Props = $props()
15
25
 
16
26
  const plotState = getContext<PlotState>('plot-state')
17
- let id = $state<string | null>(null)
18
27
 
19
- // fill ?? x drives the colors map for both violin interior and outline
20
- const fillChannel = $derived(fill ?? x)
28
+ // Fall back to the root Plot channels when the geom omits x/y (composes under
29
+ // <Plot.Root x y> without silently NaN-ing).
30
+ const xf = $derived(x ?? plotState.channels?.x)
31
+ const yf = $derived(y ?? plotState.channels?.y)
32
+ // fill ?? effective-x drives the colors map for both violin interior and outline
33
+ const fillChannel = $derived(fill ?? xf)
21
34
 
22
- onMount(() => {
23
- id = plotState.registerGeom({
24
- type: 'violin',
25
- channels: { x, y, color: fillChannel },
26
- stat,
27
- options
28
- })
29
- })
30
- onDestroy(() => {
31
- if (id) plotState.unregisterGeom(id)
32
- })
35
+ const geom = new GeomState(plotState, () => ({
36
+ type: 'violin',
37
+ channels: { x: xf, y: yf, fill: fillChannel, color, pattern },
38
+ stat,
39
+ options: { side },
40
+ alpha,
41
+ build: buildViolinMarks
42
+ }))
43
+ onMount(geom.register)
44
+ onDestroy(geom.destroy)
45
+ $effect(geom.sync)
33
46
 
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 violins = $derived.by(() => {
44
- if (!data?.length || !xScale || !yScale) return []
45
- return buildViolins(data, { x, fill: fillChannel }, xScale, yScale, colors)
46
- })
47
+ const violins = $derived(geom.marks)
47
48
  </script>
48
49
 
49
50
  {#if violins.length > 0}
@@ -52,7 +53,7 @@
52
53
  <path
53
54
  d={v.d}
54
55
  fill={v.fill}
55
- fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.violin}
56
+ fill-opacity={v.alpha}
56
57
  stroke={v.stroke}
57
58
  stroke-width="1.5"
58
59
  data-plot-element="violin"
@@ -60,6 +61,10 @@
60
61
  onmouseenter={() => plotState.setHovered(v.data)}
61
62
  onmouseleave={() => plotState.clearHovered()}
62
63
  />
64
+ {#if v.patternId}
65
+ <!-- Texture fill overlay (matches the violin silhouette) -->
66
+ <path d={v.d} fill="url(#{v.patternId})" pointer-events="none" />
67
+ {/if}
63
68
  {/each}
64
69
  </g>
65
70
  {/if}
@@ -1,11 +1,18 @@
1
1
  type Props = {
2
2
  x?: string;
3
3
  y?: string;
4
+ /** Violin interior aesthetic (field); defaults to `x`. */
4
5
  fill?: string;
6
+ /** Outline aesthetic (field); overrides the silhouette stroke when set. */
7
+ color?: string;
5
8
  stat?: string;
6
- options?: {
7
- opacity?: number;
8
- };
9
+ /** Half-violin: 'left'/'right' draw one side (flat edge at the band centre) for
10
+ * raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
11
+ side?: 'left' | 'right' | 'center';
12
+ /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
13
+ pattern?: string;
14
+ /** Fixed violin opacity 0–1; defaults to the per-geom preset (violin = 0.5). */
15
+ alpha?: number;
9
16
  };
10
17
  declare const Violin: import("svelte").Component<Props, {}, "">;
11
18
  type Violin = ReturnType<typeof Violin>;