@rokkit/chart 1.0.0-next.16 → 1.0.0-next.160

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 (173) hide show
  1. package/README.md +150 -46
  2. package/package.json +42 -45
  3. package/src/AnimatedPlot.svelte +383 -0
  4. package/src/Chart.svelte +95 -0
  5. package/src/ChartProvider.svelte +10 -0
  6. package/src/FacetPlot/Panel.svelte +37 -0
  7. package/src/FacetPlot.svelte +114 -0
  8. package/src/Plot/Arc.svelte +29 -0
  9. package/src/Plot/Area.svelte +32 -0
  10. package/src/Plot/Axis.svelte +95 -0
  11. package/src/Plot/Bar.svelte +54 -0
  12. package/src/Plot/Grid.svelte +34 -0
  13. package/src/Plot/Legend.svelte +233 -0
  14. package/src/Plot/Line.svelte +37 -0
  15. package/src/Plot/Point.svelte +40 -0
  16. package/src/Plot/Root.svelte +62 -0
  17. package/src/Plot/Timeline.svelte +95 -0
  18. package/src/Plot/Tooltip.svelte +87 -0
  19. package/src/Plot/index.js +9 -0
  20. package/src/Plot.svelte +297 -0
  21. package/src/PlotState.svelte.js +350 -0
  22. package/src/Sparkline.svelte +108 -0
  23. package/src/Symbol.svelte +21 -0
  24. package/src/Texture.svelte +18 -0
  25. package/src/charts/AreaChart.svelte +27 -0
  26. package/src/charts/BarChart.svelte +28 -0
  27. package/src/charts/BoxPlot.svelte +21 -0
  28. package/src/charts/BubbleChart.svelte +23 -0
  29. package/src/charts/LineChart.svelte +26 -0
  30. package/src/charts/PieChart.svelte +35 -0
  31. package/src/charts/ScatterPlot.svelte +26 -0
  32. package/src/charts/ViolinPlot.svelte +21 -0
  33. package/src/crossfilter/CrossFilter.svelte +42 -0
  34. package/src/crossfilter/FilterBar.svelte +24 -0
  35. package/src/crossfilter/FilterHistogram.svelte +290 -0
  36. package/src/crossfilter/FilterSlider.svelte +83 -0
  37. package/src/crossfilter/createCrossFilter.svelte.js +124 -0
  38. package/src/elements/Bar.svelte +22 -24
  39. package/src/elements/ColorRamp.svelte +20 -22
  40. package/src/elements/ContinuousLegend.svelte +20 -17
  41. package/src/elements/DefinePatterns.svelte +24 -0
  42. package/src/elements/DiscreteLegend.svelte +15 -15
  43. package/src/elements/Label.svelte +4 -8
  44. package/src/elements/SymbolGrid.svelte +22 -0
  45. package/src/elements/index.js +6 -0
  46. package/src/examples/BarChartExample.svelte +81 -0
  47. package/src/geoms/Arc.svelte +126 -0
  48. package/src/geoms/Area.svelte +78 -0
  49. package/src/geoms/Bar.svelte +200 -0
  50. package/src/geoms/Box.svelte +113 -0
  51. package/src/geoms/LabelPill.svelte +17 -0
  52. package/src/geoms/Line.svelte +123 -0
  53. package/src/geoms/Point.svelte +145 -0
  54. package/src/geoms/Violin.svelte +56 -0
  55. package/src/geoms/lib/areas.js +154 -0
  56. package/src/geoms/lib/bars.js +223 -0
  57. package/src/index.js +74 -16
  58. package/src/lib/brewer.js +25 -0
  59. package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
  60. package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
  61. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  62. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  63. package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
  64. package/src/lib/brewing/axes.svelte.js +270 -0
  65. package/src/lib/brewing/bars.svelte.js +201 -0
  66. package/src/lib/brewing/brewer.svelte.js +277 -0
  67. package/src/lib/brewing/colors.js +51 -0
  68. package/src/lib/brewing/dimensions.svelte.js +56 -0
  69. package/src/lib/brewing/index.svelte.js +205 -0
  70. package/src/lib/brewing/legends.svelte.js +137 -0
  71. package/src/lib/brewing/marks/arcs.js +43 -0
  72. package/src/lib/brewing/marks/areas.js +72 -0
  73. package/src/lib/brewing/marks/bars.js +49 -0
  74. package/src/lib/brewing/marks/boxes.js +75 -0
  75. package/src/lib/brewing/marks/lines.js +55 -0
  76. package/src/lib/brewing/marks/points.js +105 -0
  77. package/src/lib/brewing/marks/violins.js +90 -0
  78. package/src/lib/brewing/patterns.js +45 -0
  79. package/src/lib/brewing/scales.js +51 -0
  80. package/src/lib/brewing/scales.svelte.js +82 -0
  81. package/src/lib/brewing/stats.js +74 -0
  82. package/src/lib/brewing/symbols.js +10 -0
  83. package/src/lib/brewing/types.js +73 -0
  84. package/src/lib/chart.js +221 -0
  85. package/src/lib/context.js +131 -0
  86. package/src/lib/grid.js +85 -0
  87. package/src/lib/keyboard-nav.js +37 -0
  88. package/src/lib/plot/chartProps.js +76 -0
  89. package/src/lib/plot/crossfilter.js +16 -0
  90. package/src/lib/plot/facet.js +58 -0
  91. package/src/lib/plot/frames.js +81 -0
  92. package/src/lib/plot/helpers.js +14 -0
  93. package/src/lib/plot/preset.js +67 -0
  94. package/src/lib/plot/scales.js +81 -0
  95. package/src/lib/plot/stat.js +92 -0
  96. package/src/lib/plot/types.js +65 -0
  97. package/src/lib/preset.js +41 -0
  98. package/src/lib/scales.svelte.js +151 -0
  99. package/src/lib/swatch.js +13 -0
  100. package/src/lib/ticks.js +46 -0
  101. package/src/lib/utils.js +111 -118
  102. package/src/lib/xscale.js +31 -0
  103. package/src/patterns/DefinePatterns.svelte +32 -0
  104. package/src/patterns/PatternDef.svelte +27 -0
  105. package/src/patterns/index.js +4 -0
  106. package/src/patterns/patterns.js +360 -0
  107. package/src/patterns/scale.js +116 -0
  108. package/src/spec/chart-spec.js +72 -0
  109. package/src/symbols/RoundedSquare.svelte +33 -0
  110. package/src/symbols/Shape.svelte +37 -0
  111. package/src/symbols/constants/index.js +4 -0
  112. package/src/symbols/index.js +9 -0
  113. package/src/symbols/outline.svelte +60 -0
  114. package/src/symbols/solid.svelte +60 -0
  115. package/LICENSE +0 -21
  116. package/src/chart/FacetGrid.svelte +0 -51
  117. package/src/chart/Grid.svelte +0 -34
  118. package/src/chart/Legend.svelte +0 -16
  119. package/src/chart/PatternDefs.svelte +0 -13
  120. package/src/chart/Swatch.svelte +0 -93
  121. package/src/chart/SwatchButton.svelte +0 -29
  122. package/src/chart/SwatchGrid.svelte +0 -55
  123. package/src/chart/Symbol.svelte +0 -37
  124. package/src/chart/Texture.svelte +0 -16
  125. package/src/chart/TexturedShape.svelte +0 -27
  126. package/src/chart/TimelapseChart.svelte +0 -97
  127. package/src/chart/Timer.svelte +0 -27
  128. package/src/chart.js +0 -9
  129. package/src/components/charts/Axis.svelte +0 -66
  130. package/src/components/charts/Chart.svelte +0 -35
  131. package/src/components/index.js +0 -23
  132. package/src/components/lib/axis.js +0 -0
  133. package/src/components/lib/chart.js +0 -187
  134. package/src/components/lib/color.js +0 -327
  135. package/src/components/lib/funnel.js +0 -204
  136. package/src/components/lib/index.js +0 -19
  137. package/src/components/lib/pattern.js +0 -190
  138. package/src/components/lib/rollup.js +0 -55
  139. package/src/components/lib/shape.js +0 -199
  140. package/src/components/lib/summary.js +0 -145
  141. package/src/components/lib/theme.js +0 -23
  142. package/src/components/lib/timer.js +0 -41
  143. package/src/components/lib/utils.js +0 -165
  144. package/src/components/plots/BarPlot.svelte +0 -36
  145. package/src/components/plots/BoxPlot.svelte +0 -54
  146. package/src/components/plots/ScatterPlot.svelte +0 -30
  147. package/src/components/store.js +0 -70
  148. package/src/constants.js +0 -66
  149. package/src/elements/PatternDefs.svelte +0 -13
  150. package/src/elements/PatternMask.svelte +0 -20
  151. package/src/elements/Symbol.svelte +0 -38
  152. package/src/elements/Tooltip.svelte +0 -23
  153. package/src/funnel.svelte +0 -35
  154. package/src/geom.js +0 -105
  155. package/src/lib/axis.js +0 -75
  156. package/src/lib/colors.js +0 -32
  157. package/src/lib/geom.js +0 -4
  158. package/src/lib/shapes.js +0 -144
  159. package/src/lib/timer.js +0 -44
  160. package/src/lookup.js +0 -29
  161. package/src/plots/BarPlot.svelte +0 -55
  162. package/src/plots/BoxPlot.svelte +0 -0
  163. package/src/plots/FunnelPlot.svelte +0 -33
  164. package/src/plots/HeatMap.svelte +0 -5
  165. package/src/plots/HeatMapCalendar.svelte +0 -129
  166. package/src/plots/LinePlot.svelte +0 -55
  167. package/src/plots/Plot.svelte +0 -25
  168. package/src/plots/RankBarPlot.svelte +0 -38
  169. package/src/plots/ScatterPlot.svelte +0 -20
  170. package/src/plots/ViolinPlot.svelte +0 -11
  171. package/src/plots/heatmap.js +0 -70
  172. package/src/plots/index.js +0 -10
  173. package/src/swatch.js +0 -11
@@ -0,0 +1,113 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildBoxes } from '../lib/brewing/marks/boxes.js'
4
+
5
+ let { x, y, fill, stat = 'boxplot', options = {} } = $props()
6
+
7
+ const plotState = getContext('plot-state')
8
+ let id = $state(null)
9
+
10
+ // fill ?? x drives the colors map for both box interior and whisker strokes
11
+ const fillChannel = $derived(fill ?? x)
12
+
13
+ onMount(() => {
14
+ id = plotState.registerGeom({
15
+ type: 'box',
16
+ channels: { x, y, color: fillChannel },
17
+ stat,
18
+ options
19
+ })
20
+ })
21
+ onDestroy(() => {
22
+ if (id) plotState.unregisterGeom(id)
23
+ })
24
+
25
+ $effect(() => {
26
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
27
+ })
28
+
29
+ const data = $derived(id ? plotState.geomData(id) : [])
30
+ const xScale = $derived(plotState.xScale)
31
+ const yScale = $derived(plotState.yScale)
32
+ const colors = $derived(plotState.colors)
33
+
34
+ const boxes = $derived.by(() => {
35
+ if (!data?.length || !xScale || !yScale) return []
36
+ return buildBoxes(data, { x, fill: fillChannel }, xScale, yScale, colors)
37
+ })
38
+ </script>
39
+
40
+ {#if boxes.length > 0}
41
+ <g data-plot-geom="box">
42
+ {#each boxes as box, i (`${String(box.cx)}::${i}`)}
43
+ {@const x0 = box.cx - box.width / 2}
44
+ {@const xMid = box.cx}
45
+ {@const xCap0 = box.cx - box.whiskerWidth / 2}
46
+ {@const xCap1 = box.cx + box.whiskerWidth / 2}
47
+ <!-- Box body (IQR): lighter fill shade -->
48
+ <rect
49
+ x={x0}
50
+ y={box.q3}
51
+ width={box.width}
52
+ height={Math.max(0, box.q1 - box.q3)}
53
+ fill={box.fill}
54
+ fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.box}
55
+ stroke={box.stroke}
56
+ stroke-width="1"
57
+ data-plot-element="box-body"
58
+ role="presentation"
59
+ onmouseenter={() => plotState.setHovered(box.data)}
60
+ onmouseleave={() => plotState.clearHovered()}
61
+ />
62
+ <!-- Median line: darker stroke shade -->
63
+ <line
64
+ x1={x0}
65
+ y1={box.median}
66
+ x2={x0 + box.width}
67
+ y2={box.median}
68
+ stroke={box.stroke}
69
+ stroke-width="2"
70
+ data-plot-element="box-median"
71
+ />
72
+ <!-- Lower whisker (q1 to iqr_min) -->
73
+ <line
74
+ x1={xMid}
75
+ y1={box.q1}
76
+ x2={xMid}
77
+ y2={box.iqr_min}
78
+ stroke={box.stroke}
79
+ stroke-width="1"
80
+ data-plot-element="box-whisker"
81
+ />
82
+ <!-- Upper whisker (q3 to iqr_max) -->
83
+ <line
84
+ x1={xMid}
85
+ y1={box.q3}
86
+ x2={xMid}
87
+ y2={box.iqr_max}
88
+ stroke={box.stroke}
89
+ stroke-width="1"
90
+ data-plot-element="box-whisker"
91
+ />
92
+ <!-- Lower whisker cap -->
93
+ <line
94
+ x1={xCap0}
95
+ y1={box.iqr_min}
96
+ x2={xCap1}
97
+ y2={box.iqr_min}
98
+ stroke={box.stroke}
99
+ stroke-width="1"
100
+ />
101
+ <!-- Upper whisker cap -->
102
+ <line
103
+ x1={xCap0}
104
+ y1={box.iqr_max}
105
+ x2={xCap1}
106
+ y2={box.iqr_max}
107
+ stroke={box.stroke}
108
+ stroke-width="1"
109
+ />
110
+ <!-- Outlier rendering deferred: buildBoxes does not compute outliers yet -->
111
+ {/each}
112
+ </g>
113
+ {/if}
@@ -0,0 +1,17 @@
1
+ <script>
2
+ /** @type {{ x: number, y: number, text: string, color?: string }} */
3
+ let { x, y, text, color = '#333' } = $props()
4
+
5
+ const w = $derived(Math.max(36, String(text).length * 7 + 12))
6
+ </script>
7
+
8
+ <g transform="translate({x},{y})" pointer-events="none" data-plot-element="label">
9
+ <rect x={-w / 2} y="-9" width={w} height="18" rx="4" fill="white" fill-opacity="0.82" />
10
+ <text
11
+ text-anchor="middle"
12
+ dominant-baseline="central"
13
+ font-size="11"
14
+ font-weight="600"
15
+ fill={color}>{text}</text
16
+ >
17
+ </g>
@@ -0,0 +1,123 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildLines } from '../lib/brewing/marks/lines.js'
4
+ import { buildSymbolPath } from '../lib/brewing/marks/points.js'
5
+ import { keyboardNav } from '../lib/keyboard-nav.js'
6
+ import LabelPill from './LabelPill.svelte'
7
+
8
+ let {
9
+ x,
10
+ y,
11
+ color,
12
+ symbol: symbolField,
13
+ label = false,
14
+ stat = 'identity',
15
+ options = {},
16
+ onselect = undefined,
17
+ keyboard = false
18
+ } = $props()
19
+
20
+ /**
21
+ * @param {Record<string, unknown>} data
22
+ * @returns {string | null}
23
+ */
24
+ function resolveLabel(data) {
25
+ if (!label) return null
26
+ if (label === true) return String(data[y] ?? '')
27
+ if (typeof label === 'function') return String(label(data) ?? '')
28
+ return typeof label === 'string' ? String(data[label] ?? '') : null
29
+ }
30
+
31
+ const plotState = getContext('plot-state')
32
+ let id = $state(null)
33
+
34
+ onMount(() => {
35
+ id = plotState.registerGeom({
36
+ type: 'line',
37
+ channels: { x, y, color, symbol: symbolField },
38
+ stat,
39
+ options
40
+ })
41
+ })
42
+ onDestroy(() => {
43
+ if (id) plotState.unregisterGeom(id)
44
+ })
45
+
46
+ $effect(() => {
47
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, symbol: symbolField }, stat })
48
+ })
49
+
50
+ const data = $derived(id ? plotState.geomData(id) : [])
51
+ const xScale = $derived(plotState.xScale)
52
+ const yScale = $derived(plotState.yScale)
53
+ const colors = $derived(plotState.colors)
54
+ const symbolMap = $derived(plotState.symbols)
55
+
56
+ const lines = $derived.by(() => {
57
+ if (!data?.length || !xScale || !yScale) return []
58
+ return buildLines(data, { x, y, color }, xScale, yScale, colors, options.curve)
59
+ })
60
+
61
+ const markerRadius = $derived(options.markerRadius ?? 4)
62
+ </script>
63
+
64
+ {#if lines.length > 0}
65
+ <g data-plot-geom="line">
66
+ {#each lines as seg (seg.key ?? seg.d)}
67
+ <path
68
+ d={seg.d}
69
+ fill="none"
70
+ stroke={seg.stroke}
71
+ stroke-width={options.strokeWidth ?? 2}
72
+ stroke-linejoin="round"
73
+ stroke-linecap="round"
74
+ data-plot-element="line"
75
+ />
76
+ {#if symbolField && symbolMap}
77
+ {#each seg.points as pt (`${pt.x}::${pt.y}`)}
78
+ <path
79
+ transform="translate({pt.x},{pt.y})"
80
+ d={buildSymbolPath(symbolMap.get(pt.data[symbolField]) ?? 'circle', markerRadius)}
81
+ fill={seg.stroke}
82
+ stroke={seg.stroke}
83
+ stroke-width="1"
84
+ data-plot-element="line-marker"
85
+ />
86
+ {/each}
87
+ {/if}
88
+ {#if label}
89
+ {#each seg.points as pt (`label::${pt.x}::${pt.y}`)}
90
+ {@const text = resolveLabel(pt.data)}
91
+ {#if text}
92
+ <LabelPill
93
+ x={pt.x + (options.labelOffset?.x ?? 0)}
94
+ y={pt.y + (options.labelOffset?.y ?? -12)}
95
+ {text}
96
+ color={seg.stroke ?? '#333'}
97
+ />
98
+ {/if}
99
+ {/each}
100
+ {/if}
101
+ <!-- Invisible hit areas for tooltip and selection -->
102
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
103
+ {#each seg.points as pt (`hover::${pt.x}::${pt.y}`)}
104
+ <circle
105
+ cx={pt.x}
106
+ cy={pt.y}
107
+ r="8"
108
+ fill="transparent"
109
+ stroke="none"
110
+ role={onselect || keyboard ? 'button' : 'presentation'}
111
+ tabindex={onselect || keyboard ? 0 : undefined}
112
+ style:cursor={onselect ? 'pointer' : undefined}
113
+ data-plot-element="line-hover"
114
+ onmouseenter={() => plotState.setHovered(pt.data)}
115
+ onmouseleave={() => plotState.clearHovered()}
116
+ onclick={onselect ? () => onselect(pt.data) : undefined}
117
+ onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
118
+ use:keyboardNav={keyboard}
119
+ />
120
+ {/each}
121
+ {/each}
122
+ </g>
123
+ {/if}
@@ -0,0 +1,145 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { scaleSqrt } from 'd3-scale'
4
+ import { buildPoints } from '../lib/brewing/marks/points.js'
5
+ import { keyboardNav } from '../lib/keyboard-nav.js'
6
+ import LabelPill from './LabelPill.svelte'
7
+
8
+ let {
9
+ x,
10
+ y,
11
+ color,
12
+ size,
13
+ symbol: symbolField,
14
+ label = false,
15
+ stat = 'identity',
16
+ options = {},
17
+ onselect = undefined,
18
+ keyboard = false
19
+ } = $props()
20
+
21
+ /**
22
+ * @param {Record<string, unknown>} data
23
+ * @returns {string | null}
24
+ */
25
+ function resolveLabel(data) {
26
+ if (!label) return null
27
+ if (label === true) return String(data[y] ?? '')
28
+ if (typeof label === 'function') return String(label(data) ?? '')
29
+ return typeof label === 'string' ? String(data[label] ?? '') : null
30
+ }
31
+
32
+ const plotState = getContext('plot-state')
33
+ let id = $state(null)
34
+
35
+ onMount(() => {
36
+ id = plotState.registerGeom({
37
+ type: 'point',
38
+ channels: { x, y, color, size, symbol: symbolField },
39
+ stat,
40
+ options
41
+ })
42
+ })
43
+ onDestroy(() => {
44
+ if (id) plotState.unregisterGeom(id)
45
+ })
46
+
47
+ $effect(() => {
48
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, size, symbol: symbolField }, stat })
49
+ })
50
+
51
+ const data = $derived(id ? plotState.geomData(id) : [])
52
+ const xScale = $derived(plotState.xScale)
53
+ const yScale = $derived(plotState.yScale)
54
+ const colors = $derived(plotState.colors)
55
+ const symbolMap = $derived(plotState.symbols)
56
+
57
+ function buildSizeScale() {
58
+ if (!size || !data?.length) return null
59
+ const vals = data.map((d) => Number(d[size])).filter((v) => !isNaN(v))
60
+ if (!vals.length) return null
61
+ const minVal = Math.min(...vals)
62
+ const maxVal = Math.max(...vals)
63
+ const minRadius = options.minRadius ?? 3
64
+ const maxRadius = options.maxRadius ?? 20
65
+ return scaleSqrt().domain([minVal, maxVal]).range([minRadius, maxRadius])
66
+ }
67
+
68
+ const sizeScale = $derived.by(() => buildSizeScale())
69
+
70
+ const defaultRadius = $derived(options.radius ?? 4)
71
+
72
+ const points = $derived.by(() => {
73
+ if (!data?.length || !xScale || !yScale) return []
74
+ return buildPoints(
75
+ data,
76
+ { x, y, color, size, symbol: symbolField },
77
+ xScale,
78
+ yScale,
79
+ colors,
80
+ sizeScale,
81
+ symbolMap,
82
+ defaultRadius,
83
+ options?.jitter ?? null
84
+ )
85
+ })
86
+ </script>
87
+
88
+ {#if points.length > 0}
89
+ <g data-plot-geom="point">
90
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
91
+ {#each points as pt, i (`${i}::${pt.data[x]}::${pt.data[y]}`)}
92
+ {#if pt.symbolPath}
93
+ <path
94
+ transform="translate({pt.cx},{pt.cy})"
95
+ d={pt.symbolPath}
96
+ fill={pt.fill}
97
+ stroke={pt.stroke}
98
+ stroke-width="1"
99
+ fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
100
+ data-plot-element="point"
101
+ role={onselect || keyboard ? 'button' : 'graphics-symbol'}
102
+ tabindex={onselect || keyboard ? 0 : undefined}
103
+ style:cursor={onselect ? 'pointer' : undefined}
104
+ aria-label="{pt.data[x]}, {pt.data[y]}"
105
+ onmouseenter={() => plotState.setHovered(pt.data)}
106
+ onmouseleave={() => plotState.clearHovered()}
107
+ onclick={onselect ? () => onselect(pt.data) : undefined}
108
+ onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
109
+ use:keyboardNav={keyboard}
110
+ />
111
+ {:else}
112
+ <circle
113
+ cx={pt.cx}
114
+ cy={pt.cy}
115
+ r={pt.r}
116
+ fill={pt.fill}
117
+ stroke={pt.stroke}
118
+ stroke-width="1"
119
+ fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
120
+ data-plot-element="point"
121
+ role={onselect || keyboard ? 'button' : 'graphics-symbol'}
122
+ tabindex={onselect || keyboard ? 0 : undefined}
123
+ style:cursor={onselect ? 'pointer' : undefined}
124
+ aria-label="{pt.data[x]}, {pt.data[y]}"
125
+ onmouseenter={() => plotState.setHovered(pt.data)}
126
+ onmouseleave={() => plotState.clearHovered()}
127
+ onclick={onselect ? () => onselect(pt.data) : undefined}
128
+ onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
129
+ use:keyboardNav={keyboard}
130
+ />
131
+ {/if}
132
+ {#if label}
133
+ {@const text = resolveLabel(pt.data)}
134
+ {#if text}
135
+ <LabelPill
136
+ x={pt.cx + (options.labelOffset?.x ?? 0)}
137
+ y={pt.cy - pt.r + (options.labelOffset?.y ?? -12)}
138
+ {text}
139
+ color={pt.stroke ?? '#333'}
140
+ />
141
+ {/if}
142
+ {/if}
143
+ {/each}
144
+ </g>
145
+ {/if}
@@ -0,0 +1,56 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildViolins } from '../lib/brewing/marks/violins.js'
4
+
5
+ let { x, y, fill, stat = 'boxplot', options = {} } = $props()
6
+
7
+ const plotState = getContext('plot-state')
8
+ let id = $state(null)
9
+
10
+ // fill ?? x drives the colors map for both violin interior and outline
11
+ const fillChannel = $derived(fill ?? x)
12
+
13
+ onMount(() => {
14
+ id = plotState.registerGeom({
15
+ type: 'violin',
16
+ channels: { x, y, color: fillChannel },
17
+ stat,
18
+ options
19
+ })
20
+ })
21
+ onDestroy(() => {
22
+ if (id) plotState.unregisterGeom(id)
23
+ })
24
+
25
+ $effect(() => {
26
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
27
+ })
28
+
29
+ const data = $derived(id ? plotState.geomData(id) : [])
30
+ const xScale = $derived(plotState.xScale)
31
+ const yScale = $derived(plotState.yScale)
32
+ const colors = $derived(plotState.colors)
33
+
34
+ const violins = $derived.by(() => {
35
+ if (!data?.length || !xScale || !yScale) return []
36
+ return buildViolins(data, { x, fill: fillChannel }, xScale, yScale, colors)
37
+ })
38
+ </script>
39
+
40
+ {#if violins.length > 0}
41
+ <g data-plot-geom="violin">
42
+ {#each violins as v, i (`${String(v.cx)}::${i}`)}
43
+ <path
44
+ d={v.d}
45
+ fill={v.fill}
46
+ fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.violin}
47
+ stroke={v.stroke}
48
+ stroke-width="1.5"
49
+ data-plot-element="violin"
50
+ role="presentation"
51
+ onmouseenter={() => plotState.setHovered(v.data)}
52
+ onmouseleave={() => plotState.clearHovered()}
53
+ />
54
+ {/each}
55
+ </g>
56
+ {/if}
@@ -0,0 +1,154 @@
1
+ import { area, stack, curveCatmullRom, curveStep } from 'd3-shape'
2
+ import { toPatternId } from '../../lib/brewing/patterns.js'
3
+
4
+ /**
5
+ * Builds area path geometry for multi-series area charts.
6
+ *
7
+ * @param {Object[]} data
8
+ * @param {{ x: string, y: string, color?: string }} channels
9
+ * @param {Function} xScale
10
+ * @param {Function} yScale
11
+ * @param {Map<unknown, {fill: string, stroke: string}>} colors
12
+ * @param {'linear'|'smooth'|'step'} [curve]
13
+ * @param {Map<unknown, string>} [patterns]
14
+ * @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
15
+ */
16
+ export function buildAreas(data, channels, xScale, yScale, colors, curve, patterns) {
17
+ const { x: xf, y: yf, color: cf, pattern: pf } = channels
18
+ const baseline = yScale.range()[0] // bottom of the chart (y pixel max)
19
+
20
+ const xPos = (d) =>
21
+ typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
22
+
23
+ const makeGen = () => {
24
+ const gen = area()
25
+ .x(xPos)
26
+ .y0(baseline)
27
+ .y1((d) => yScale(d[yf]))
28
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
29
+ else if (curve === 'step') gen.curve(curveStep)
30
+ return gen
31
+ }
32
+
33
+ // For band (categorical) x scales, sort by domain index to preserve intended ordering.
34
+ // For continuous scales, sort numerically so the path draws left-to-right.
35
+ const sortByX = (rows) => {
36
+ if (typeof xScale.bandwidth === 'function') {
37
+ const domain = xScale.domain()
38
+ return [...rows].sort((a, b) => domain.indexOf(a[xf]) - domain.indexOf(b[xf]))
39
+ }
40
+ return [...rows].sort((a, b) => (a[xf] < b[xf] ? -1 : a[xf] > b[xf] ? 1 : 0))
41
+ }
42
+
43
+ if (!cf) {
44
+ const entry = colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
45
+ const patternKey = pf ? data[0]?.[pf] : null
46
+ const patternId =
47
+ patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
48
+ ? toPatternId(String(patternKey))
49
+ : null
50
+ return [{ d: makeGen()(sortByX(data)), fill: entry.fill, stroke: 'none', key: null, patternId }]
51
+ }
52
+
53
+ // Group by color field
54
+ const groups = new Map()
55
+ for (const d of data) {
56
+ const key = d[cf]
57
+ if (!groups.has(key)) groups.set(key, [])
58
+ groups.get(key).push(d)
59
+ }
60
+ // For different-field patterns, assign positionally so each area gets a distinct pattern
61
+ const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
62
+
63
+ return [...groups.entries()].map(([key, rows], i) => {
64
+ const entry = colors?.get(key) ?? { fill: '#888', stroke: '#888' }
65
+ // Same field or no pf: look up by colorKey. Different field: assign positionally.
66
+ const patternKey = !pf
67
+ ? key
68
+ : pf === cf
69
+ ? key
70
+ : (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
71
+ const patternId =
72
+ patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
73
+ ? toPatternId(String(patternKey))
74
+ : null
75
+ return { d: makeGen()(sortByX(rows)), fill: entry.fill, stroke: 'none', key, patternId }
76
+ })
77
+ }
78
+
79
+ /**
80
+ * Builds stacked area paths using d3 stack layout.
81
+ *
82
+ * @param {Object[]} data
83
+ * @param {{ x: string, y: string, color: string }} channels
84
+ * @param {Function} xScale
85
+ * @param {Function} yScale
86
+ * @param {Map<unknown, {fill: string, stroke: string}>} colors
87
+ * @param {'linear'|'smooth'|'step'} [curve]
88
+ * @param {Map<unknown, string>} [patterns]
89
+ * @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
90
+ */
91
+ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns) {
92
+ const { x: xf, y: yf, color: cf, pattern: pf } = channels
93
+ if (!cf) return buildAreas(data, channels, xScale, yScale, colors, curve, patterns)
94
+
95
+ const xCategories = [...new Set(data.map((d) => d[xf]))].sort((a, b) =>
96
+ a < b ? -1 : a > b ? 1 : 0
97
+ )
98
+ const colorCategories = [...new Set(data.map((d) => d[cf]))]
99
+
100
+ // Build wide-form lookup: xVal → { colorKey: yVal }
101
+ const lookup = new Map()
102
+ for (const d of data) {
103
+ if (!lookup.has(d[xf])) lookup.set(d[xf], {})
104
+ lookup.get(d[xf])[d[cf]] = Number(d[yf])
105
+ }
106
+
107
+ const wide = xCategories.map((xVal) => {
108
+ const row = { [xf]: xVal }
109
+ for (const c of colorCategories) row[c] = lookup.get(xVal)?.[c] ?? 0
110
+ return row
111
+ })
112
+
113
+ const xPos = (d) =>
114
+ typeof xScale.bandwidth === 'function'
115
+ ? xScale(d.data[xf]) + xScale.bandwidth() / 2
116
+ : xScale(d.data[xf])
117
+
118
+ const makeGen = () => {
119
+ const gen = area()
120
+ .x(xPos)
121
+ .y0((d) => yScale(d[0]))
122
+ .y1((d) => yScale(d[1]))
123
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
124
+ else if (curve === 'step') gen.curve(curveStep)
125
+ return gen
126
+ }
127
+
128
+ const stackGen = stack().keys(colorCategories)
129
+ const layers = stackGen(wide)
130
+
131
+ const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
132
+
133
+ return layers.map((layer, i) => {
134
+ const colorKey = layer.key
135
+ const entry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
136
+ // Same field (or no pf): look up by colorKey. Different field: assign positionally.
137
+ const patternKey = !pf
138
+ ? colorKey
139
+ : pf === cf
140
+ ? colorKey
141
+ : (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
142
+ const patternId =
143
+ patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
144
+ ? toPatternId(String(patternKey))
145
+ : null
146
+ return {
147
+ d: makeGen()(layer) ?? '',
148
+ fill: entry.fill,
149
+ stroke: 'none',
150
+ key: colorKey,
151
+ patternId
152
+ }
153
+ })
154
+ }