@rokkit/chart 1.0.0-next.15 → 1.0.0-next.150

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 (223) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +150 -46
  3. package/dist/Plot/index.d.ts +9 -0
  4. package/dist/PlotState.svelte.d.ts +47 -0
  5. package/dist/crossfilter/createCrossFilter.svelte.d.ts +15 -0
  6. package/dist/elements/index.d.ts +6 -0
  7. package/dist/geoms/lib/areas.d.ts +52 -0
  8. package/dist/geoms/lib/bars.d.ts +3 -0
  9. package/dist/index.d.ts +51 -0
  10. package/dist/lib/brewer.d.ts +9 -0
  11. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +10 -0
  12. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +8 -0
  13. package/dist/lib/brewing/PieBrewer.svelte.d.ts +8 -0
  14. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +9 -0
  15. package/dist/lib/brewing/axes.svelte.d.ts +66 -0
  16. package/dist/lib/brewing/bars.svelte.d.ts +56 -0
  17. package/dist/lib/brewing/brewer.svelte.d.ts +145 -0
  18. package/dist/lib/brewing/colors.d.ts +17 -0
  19. package/dist/lib/brewing/dimensions.svelte.d.ts +35 -0
  20. package/dist/lib/brewing/index.svelte.d.ts +118 -0
  21. package/dist/lib/brewing/legends.svelte.d.ts +48 -0
  22. package/dist/lib/brewing/marks/arcs.d.ts +17 -0
  23. package/dist/lib/brewing/marks/areas.d.ts +31 -0
  24. package/dist/lib/brewing/marks/bars.d.ts +1 -0
  25. package/dist/lib/brewing/marks/boxes.d.ts +24 -0
  26. package/dist/lib/brewing/marks/lines.d.ts +24 -0
  27. package/dist/lib/brewing/marks/points.d.ts +40 -0
  28. package/dist/lib/brewing/marks/violins.d.ts +20 -0
  29. package/dist/lib/brewing/patterns.d.ts +14 -0
  30. package/dist/lib/brewing/scales.d.ts +28 -0
  31. package/dist/lib/brewing/scales.svelte.d.ts +24 -0
  32. package/dist/lib/brewing/stats.d.ts +31 -0
  33. package/dist/lib/brewing/symbols.d.ts +7 -0
  34. package/dist/lib/brewing/types.d.ts +162 -0
  35. package/dist/lib/chart.d.ts +40 -0
  36. package/dist/lib/context.d.ts +13 -0
  37. package/dist/lib/grid.d.ts +72 -0
  38. package/dist/lib/plot/chartProps.d.ts +177 -0
  39. package/dist/lib/plot/crossfilter.d.ts +13 -0
  40. package/dist/lib/plot/facet.d.ts +24 -0
  41. package/dist/lib/plot/frames.d.ts +47 -0
  42. package/dist/lib/plot/helpers.d.ts +3 -0
  43. package/dist/lib/plot/preset.d.ts +29 -0
  44. package/dist/lib/plot/scales.d.ts +5 -0
  45. package/dist/lib/plot/stat.d.ts +32 -0
  46. package/dist/lib/plot/types.d.ts +89 -0
  47. package/dist/lib/scales.svelte.d.ts +35 -0
  48. package/dist/lib/swatch.d.ts +12 -0
  49. package/dist/lib/ticks.d.ts +36 -0
  50. package/dist/lib/utils.d.ts +61 -0
  51. package/dist/lib/xscale.d.ts +11 -0
  52. package/dist/patterns/index.d.ts +4 -0
  53. package/dist/patterns/patterns.d.ts +72 -0
  54. package/dist/patterns/scale.d.ts +30 -0
  55. package/dist/symbols/constants/index.d.ts +1 -0
  56. package/dist/symbols/index.d.ts +5 -0
  57. package/package.json +41 -45
  58. package/src/AnimatedPlot.svelte +214 -0
  59. package/src/Chart.svelte +101 -0
  60. package/src/FacetPlot/Panel.svelte +23 -0
  61. package/src/FacetPlot.svelte +90 -0
  62. package/src/Plot/Arc.svelte +29 -0
  63. package/src/Plot/Area.svelte +25 -0
  64. package/src/Plot/Axis.svelte +73 -0
  65. package/src/Plot/Bar.svelte +96 -0
  66. package/src/Plot/Grid.svelte +30 -0
  67. package/src/Plot/Legend.svelte +167 -0
  68. package/src/Plot/Line.svelte +27 -0
  69. package/src/Plot/Point.svelte +27 -0
  70. package/src/Plot/Root.svelte +107 -0
  71. package/src/Plot/Timeline.svelte +95 -0
  72. package/src/Plot/Tooltip.svelte +81 -0
  73. package/src/Plot/index.js +9 -0
  74. package/src/Plot.svelte +189 -0
  75. package/src/PlotState.svelte.js +278 -0
  76. package/src/Sparkline.svelte +69 -0
  77. package/src/Symbol.svelte +21 -0
  78. package/src/Texture.svelte +18 -0
  79. package/src/charts/AreaChart.svelte +25 -0
  80. package/src/charts/BarChart.svelte +26 -0
  81. package/src/charts/BoxPlot.svelte +21 -0
  82. package/src/charts/BubbleChart.svelte +23 -0
  83. package/src/charts/LineChart.svelte +26 -0
  84. package/src/charts/PieChart.svelte +25 -0
  85. package/src/charts/ScatterPlot.svelte +25 -0
  86. package/src/charts/ViolinPlot.svelte +21 -0
  87. package/src/crossfilter/CrossFilter.svelte +38 -0
  88. package/src/crossfilter/FilterBar.svelte +32 -0
  89. package/src/crossfilter/FilterSlider.svelte +79 -0
  90. package/src/crossfilter/createCrossFilter.svelte.js +113 -0
  91. package/src/elements/Bar.svelte +22 -24
  92. package/src/elements/ColorRamp.svelte +20 -22
  93. package/src/elements/ContinuousLegend.svelte +20 -17
  94. package/src/elements/DefinePatterns.svelte +24 -0
  95. package/src/elements/DiscreteLegend.svelte +15 -15
  96. package/src/elements/Label.svelte +4 -8
  97. package/src/elements/SymbolGrid.svelte +22 -0
  98. package/src/elements/index.js +6 -0
  99. package/src/examples/BarChartExample.svelte +81 -0
  100. package/src/geoms/Arc.svelte +81 -0
  101. package/src/geoms/Area.svelte +50 -0
  102. package/src/geoms/Bar.svelte +142 -0
  103. package/src/geoms/Box.svelte +101 -0
  104. package/src/geoms/LabelPill.svelte +17 -0
  105. package/src/geoms/Line.svelte +100 -0
  106. package/src/geoms/Point.svelte +100 -0
  107. package/src/geoms/Violin.svelte +44 -0
  108. package/src/geoms/lib/areas.js +131 -0
  109. package/src/geoms/lib/bars.js +172 -0
  110. package/src/index.js +67 -16
  111. package/src/lib/brewer.js +25 -0
  112. package/src/lib/brewing/BoxBrewer.svelte.js +56 -0
  113. package/src/lib/brewing/CartesianBrewer.svelte.js +16 -0
  114. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  115. package/src/lib/brewing/ViolinBrewer.svelte.js +55 -0
  116. package/src/lib/brewing/axes.svelte.js +270 -0
  117. package/src/lib/brewing/bars.svelte.js +201 -0
  118. package/src/lib/brewing/brewer.svelte.js +229 -0
  119. package/src/lib/brewing/colors.js +22 -0
  120. package/src/lib/brewing/dimensions.svelte.js +56 -0
  121. package/src/lib/brewing/index.svelte.js +205 -0
  122. package/src/lib/brewing/legends.svelte.js +137 -0
  123. package/src/lib/brewing/marks/arcs.js +43 -0
  124. package/src/lib/brewing/marks/areas.js +59 -0
  125. package/src/lib/brewing/marks/bars.js +49 -0
  126. package/src/lib/brewing/marks/boxes.js +75 -0
  127. package/src/lib/brewing/marks/lines.js +48 -0
  128. package/src/lib/brewing/marks/points.js +57 -0
  129. package/src/lib/brewing/marks/violins.js +90 -0
  130. package/src/lib/brewing/patterns.js +31 -0
  131. package/src/lib/brewing/scales.js +51 -0
  132. package/src/lib/brewing/scales.svelte.js +82 -0
  133. package/src/lib/brewing/stats.js +62 -0
  134. package/src/lib/brewing/symbols.js +10 -0
  135. package/src/lib/brewing/types.js +73 -0
  136. package/src/lib/chart.js +213 -0
  137. package/src/lib/context.js +131 -0
  138. package/src/lib/grid.js +85 -0
  139. package/src/lib/plot/chartProps.js +76 -0
  140. package/src/lib/plot/crossfilter.js +16 -0
  141. package/src/lib/plot/facet.js +58 -0
  142. package/src/lib/plot/frames.js +80 -0
  143. package/src/lib/plot/helpers.js +14 -0
  144. package/src/lib/plot/preset.js +53 -0
  145. package/src/lib/plot/scales.js +56 -0
  146. package/src/lib/plot/stat.js +92 -0
  147. package/src/lib/plot/types.js +65 -0
  148. package/src/lib/scales.svelte.js +151 -0
  149. package/src/lib/swatch.js +13 -0
  150. package/src/lib/ticks.js +46 -0
  151. package/src/lib/utils.js +111 -118
  152. package/src/lib/xscale.js +31 -0
  153. package/src/patterns/DefinePatterns.svelte +32 -0
  154. package/src/patterns/PatternDef.svelte +27 -0
  155. package/src/patterns/README.md +3 -0
  156. package/src/patterns/index.js +4 -0
  157. package/src/patterns/patterns.js +208 -0
  158. package/src/patterns/scale.js +87 -0
  159. package/src/spec/chart-spec.js +29 -0
  160. package/src/symbols/RoundedSquare.svelte +33 -0
  161. package/src/symbols/Shape.svelte +37 -0
  162. package/src/symbols/constants/index.js +4 -0
  163. package/src/symbols/index.js +9 -0
  164. package/src/symbols/outline.svelte +60 -0
  165. package/src/symbols/solid.svelte +60 -0
  166. package/src/chart/FacetGrid.svelte +0 -51
  167. package/src/chart/Grid.svelte +0 -34
  168. package/src/chart/Legend.svelte +0 -16
  169. package/src/chart/PatternDefs.svelte +0 -13
  170. package/src/chart/Swatch.svelte +0 -93
  171. package/src/chart/SwatchButton.svelte +0 -29
  172. package/src/chart/SwatchGrid.svelte +0 -55
  173. package/src/chart/Symbol.svelte +0 -37
  174. package/src/chart/Texture.svelte +0 -16
  175. package/src/chart/TexturedShape.svelte +0 -27
  176. package/src/chart/TimelapseChart.svelte +0 -97
  177. package/src/chart/Timer.svelte +0 -27
  178. package/src/chart.js +0 -9
  179. package/src/components/charts/Axis.svelte +0 -66
  180. package/src/components/charts/Chart.svelte +0 -35
  181. package/src/components/index.js +0 -23
  182. package/src/components/lib/axis.js +0 -0
  183. package/src/components/lib/chart.js +0 -187
  184. package/src/components/lib/color.js +0 -327
  185. package/src/components/lib/funnel.js +0 -204
  186. package/src/components/lib/index.js +0 -19
  187. package/src/components/lib/pattern.js +0 -190
  188. package/src/components/lib/rollup.js +0 -55
  189. package/src/components/lib/shape.js +0 -199
  190. package/src/components/lib/summary.js +0 -145
  191. package/src/components/lib/theme.js +0 -23
  192. package/src/components/lib/timer.js +0 -41
  193. package/src/components/lib/utils.js +0 -165
  194. package/src/components/plots/BarPlot.svelte +0 -36
  195. package/src/components/plots/BoxPlot.svelte +0 -54
  196. package/src/components/plots/ScatterPlot.svelte +0 -30
  197. package/src/components/store.js +0 -70
  198. package/src/constants.js +0 -66
  199. package/src/elements/PatternDefs.svelte +0 -13
  200. package/src/elements/PatternMask.svelte +0 -20
  201. package/src/elements/Symbol.svelte +0 -38
  202. package/src/elements/Tooltip.svelte +0 -23
  203. package/src/funnel.svelte +0 -35
  204. package/src/geom.js +0 -105
  205. package/src/lib/axis.js +0 -75
  206. package/src/lib/colors.js +0 -32
  207. package/src/lib/geom.js +0 -4
  208. package/src/lib/shapes.js +0 -144
  209. package/src/lib/timer.js +0 -44
  210. package/src/lookup.js +0 -29
  211. package/src/plots/BarPlot.svelte +0 -55
  212. package/src/plots/BoxPlot.svelte +0 -0
  213. package/src/plots/FunnelPlot.svelte +0 -33
  214. package/src/plots/HeatMap.svelte +0 -5
  215. package/src/plots/HeatMapCalendar.svelte +0 -129
  216. package/src/plots/LinePlot.svelte +0 -55
  217. package/src/plots/Plot.svelte +0 -25
  218. package/src/plots/RankBarPlot.svelte +0 -38
  219. package/src/plots/ScatterPlot.svelte +0 -20
  220. package/src/plots/ViolinPlot.svelte +0 -11
  221. package/src/plots/heatmap.js +0 -70
  222. package/src/plots/index.js +0 -10
  223. package/src/swatch.js +0 -11
@@ -0,0 +1,50 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildAreas, buildStackedAreas } from './lib/areas.js'
4
+
5
+ let { x, y, color, pattern, stat = 'identity', options = {} } = $props()
6
+
7
+ const plotState = getContext('plot-state')
8
+ let id = $state(null)
9
+
10
+ onMount(() => {
11
+ id = plotState.registerGeom({ type: 'area', channels: { x, y, color, pattern }, stat, options: { stack: options?.stack ?? false } })
12
+ })
13
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
14
+
15
+ $effect(() => {
16
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, pattern }, stat, options: { stack: options?.stack ?? false } })
17
+ })
18
+
19
+ const data = $derived(id ? plotState.geomData(id) : [])
20
+ const xScale = $derived(plotState.xScale)
21
+ const yScale = $derived(plotState.yScale)
22
+ const colors = $derived(plotState.colors)
23
+ const patterns = $derived(plotState.patterns)
24
+
25
+ const areas = $derived.by(() => {
26
+ if (!data?.length || !xScale || !yScale) return []
27
+ const channels = { x, y, color, pattern }
28
+ if (options.stack) {
29
+ return buildStackedAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
30
+ }
31
+ return buildAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
32
+ })
33
+ </script>
34
+
35
+ {#if areas.length > 0}
36
+ <g data-plot-geom="area">
37
+ {#each areas as seg (seg.key ?? seg.d)}
38
+ <path
39
+ d={seg.d}
40
+ fill={seg.fill}
41
+ fill-opacity={seg.patternId ? 1 : (options.opacity ?? 0.6)}
42
+ stroke={seg.stroke ?? 'none'}
43
+ data-plot-element="area"
44
+ />
45
+ {#if seg.patternId}
46
+ <path d={seg.d} fill="url(#{seg.patternId})" data-plot-element="area" />
47
+ {/if}
48
+ {/each}
49
+ </g>
50
+ {/if}
@@ -0,0 +1,142 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildGroupedBars, buildStackedBars, buildHorizontalBars } from './lib/bars.js'
4
+ import LabelPill from './LabelPill.svelte'
5
+
6
+ let { x, y, color, fill: fillProp, pattern, label = false, stat = 'identity', options = {}, filterable = false } = $props()
7
+
8
+ // `fill` is accepted as an alias for `color` (consistent with Arc.svelte)
9
+ const colorChannel = $derived(fillProp ?? color)
10
+
11
+ /**
12
+ * @param {Record<string, unknown>} data
13
+ * @param {string} defaultField
14
+ * @returns {string | null}
15
+ */
16
+ function resolveLabel(data, defaultField) {
17
+ if (!label) return null
18
+ if (label === true) return String(data[defaultField] ?? '')
19
+ if (typeof label === 'function') return String(label(data) ?? '')
20
+ if (typeof label === 'string') return String(data[label] ?? '')
21
+ return null
22
+ }
23
+
24
+ const plotState = getContext('plot-state')
25
+ const cf = getContext('crossfilter')
26
+ let id = $state(null)
27
+
28
+ onMount(() => {
29
+ id = plotState.registerGeom({ type: 'bar', channels: { x, y, color: colorChannel, pattern }, stat, options: { stack: options?.stack ?? false } })
30
+ })
31
+ onDestroy(() => {
32
+ if (id) plotState.unregisterGeom(id)
33
+ })
34
+
35
+ $effect(() => {
36
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: colorChannel, pattern }, stat, options: { stack: options?.stack ?? false } })
37
+ })
38
+
39
+ const data = $derived(id ? plotState.geomData(id) : [])
40
+ const xScale = $derived(plotState.xScale)
41
+ const yScale = $derived(plotState.yScale)
42
+ const colors = $derived(plotState.colors)
43
+ const patterns = $derived(plotState.patterns)
44
+ const orientation = $derived(plotState.orientation)
45
+ const innerHeight = $derived(plotState.innerHeight)
46
+
47
+ const bars = $derived.by(() => {
48
+ if (!data?.length || !xScale || !yScale) return []
49
+ const channels = { x, y, color: colorChannel, pattern }
50
+ if (orientation === 'horizontal') {
51
+ return buildHorizontalBars(data, channels, xScale, yScale, colors, innerHeight)
52
+ }
53
+ if (options.stack) {
54
+ return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
55
+ }
56
+ return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
57
+ })
58
+
59
+ /** @type {Record<string, boolean>} */
60
+ let dimmedByKey = $state({})
61
+
62
+ $effect(() => {
63
+ if (!cf) { dimmedByKey = {}; return }
64
+ // cf.version is a $state counter that increments on every filter mutation.
65
+ // Reading it here establishes a reactive dependency so the effect re-runs
66
+ // whenever any filter changes — including changes from sibling FilterBars.
67
+ void cf.version
68
+ const next = /** @type {Record<string, boolean>} */ ({})
69
+ for (const bar of bars) {
70
+ const dimmedByX = x ? cf.isDimmed(x, bar.data[x]) : false
71
+ const dimmedByY = y ? cf.isDimmed(y, bar.data[y]) : false
72
+ next[bar.key] = dimmedByX || dimmedByY
73
+ }
74
+ dimmedByKey = next
75
+ })
76
+
77
+ function handleBarClick(barX) {
78
+ if (!filterable || !x || !cf) return
79
+ cf.toggleCategorical(x, barX)
80
+ }
81
+ </script>
82
+
83
+ {#if bars.length > 0}
84
+ <g data-plot-geom="bar">
85
+ {#each bars as bar (bar.key)}
86
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
87
+ <rect
88
+ x={bar.x}
89
+ y={bar.y}
90
+ width={Math.max(0, bar.width)}
91
+ height={Math.max(0, bar.height)}
92
+ fill={bar.fill}
93
+ stroke={bar.stroke ?? 'none'}
94
+ stroke-width={bar.stroke ? 0.5 : 0}
95
+ data-plot-element="bar"
96
+ data-plot-value={bar.data[y]}
97
+ data-plot-category={bar.data[x]}
98
+ data-dimmed={dimmedByKey[bar.key] ? true : undefined}
99
+ style:cursor={filterable ? 'pointer' : undefined}
100
+ onclick={filterable && x ? () => handleBarClick(bar.data[x]) : undefined}
101
+ onkeydown={filterable && x ? (e) => (e.key === 'Enter' || e.key === ' ') && handleBarClick(bar.data[x]) : undefined}
102
+ role={filterable ? 'button' : 'graphics-symbol'}
103
+ tabindex={filterable ? 0 : undefined}
104
+ aria-label="{bar.data[x]}: {bar.data[y]}"
105
+ onmouseenter={() => plotState.setHovered(bar.data)}
106
+ onmouseleave={() => plotState.clearHovered()}
107
+ >
108
+ <title>{bar.data[x]}: {bar.data[y]}</title>
109
+ </rect>
110
+ {#if bar.patternId}
111
+ <rect
112
+ x={bar.x}
113
+ y={bar.y}
114
+ width={Math.max(0, bar.width)}
115
+ height={Math.max(0, bar.height)}
116
+ fill="url(#{bar.patternId})"
117
+ pointer-events="none"
118
+ />
119
+ {/if}
120
+ {#if label}
121
+ {@const text = resolveLabel(bar.data, orientation === 'horizontal' ? x : y)}
122
+ {#if text}
123
+ {#if orientation === 'horizontal'}
124
+ <LabelPill
125
+ x={bar.x + bar.width + (options.labelOffset ?? 8)}
126
+ y={bar.y + bar.height / 2}
127
+ {text}
128
+ color={bar.stroke ?? '#333'}
129
+ />
130
+ {:else}
131
+ <LabelPill
132
+ x={bar.x + bar.width / 2}
133
+ y={bar.y + (options.labelOffset ?? -8)}
134
+ {text}
135
+ color={bar.stroke ?? '#333'}
136
+ />
137
+ {/if}
138
+ {/if}
139
+ {/if}
140
+ {/each}
141
+ </g>
142
+ {/if}
@@ -0,0 +1,101 @@
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
+ onMount(() => {
12
+ id = plotState.registerGeom({ type: 'box', channels: { x, y, color: fill ?? x }, stat, options })
13
+ })
14
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
15
+
16
+ $effect(() => {
17
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: fill ?? x }, stat })
18
+ })
19
+
20
+ const data = $derived(id ? plotState.geomData(id) : [])
21
+ const xScale = $derived(plotState.xScale)
22
+ const yScale = $derived(plotState.yScale)
23
+ const colors = $derived(plotState.colors)
24
+
25
+ const boxes = $derived.by(() => {
26
+ if (!data?.length || !xScale || !yScale) return []
27
+ return buildBoxes(data, { x, fill: fill ?? x }, xScale, yScale, colors)
28
+ })
29
+ </script>
30
+
31
+ {#if boxes.length > 0}
32
+ <g data-plot-geom="box">
33
+ {#each boxes as box, i (`${String(box.cx) }::${ i}`)}
34
+ {@const x0 = box.cx - box.width / 2}
35
+ {@const xMid = box.cx}
36
+ {@const xCap0 = box.cx - box.whiskerWidth / 2}
37
+ {@const xCap1 = box.cx + box.whiskerWidth / 2}
38
+ <!-- Box body (IQR): lighter fill shade -->
39
+ <rect
40
+ x={x0}
41
+ y={box.q3}
42
+ width={box.width}
43
+ height={Math.max(0, box.q1 - box.q3)}
44
+ fill={box.fill}
45
+ fill-opacity="0.5"
46
+ stroke={box.stroke}
47
+ stroke-width="1"
48
+ data-plot-element="box-body"
49
+ />
50
+ <!-- Median line: darker stroke shade -->
51
+ <line
52
+ x1={x0}
53
+ y1={box.median}
54
+ x2={x0 + box.width}
55
+ y2={box.median}
56
+ stroke={box.stroke}
57
+ stroke-width="2"
58
+ data-plot-element="box-median"
59
+ />
60
+ <!-- Lower whisker (q1 to iqr_min) -->
61
+ <line
62
+ x1={xMid}
63
+ y1={box.q1}
64
+ x2={xMid}
65
+ y2={box.iqr_min}
66
+ stroke={box.stroke}
67
+ stroke-width="1"
68
+ data-plot-element="box-whisker"
69
+ />
70
+ <!-- Upper whisker (q3 to iqr_max) -->
71
+ <line
72
+ x1={xMid}
73
+ y1={box.q3}
74
+ x2={xMid}
75
+ y2={box.iqr_max}
76
+ stroke={box.stroke}
77
+ stroke-width="1"
78
+ data-plot-element="box-whisker"
79
+ />
80
+ <!-- Lower whisker cap -->
81
+ <line
82
+ x1={xCap0}
83
+ y1={box.iqr_min}
84
+ x2={xCap1}
85
+ y2={box.iqr_min}
86
+ stroke={box.stroke}
87
+ stroke-width="1"
88
+ />
89
+ <!-- Upper whisker cap -->
90
+ <line
91
+ x1={xCap0}
92
+ y1={box.iqr_max}
93
+ x2={xCap1}
94
+ y2={box.iqr_max}
95
+ stroke={box.stroke}
96
+ stroke-width="1"
97
+ />
98
+ <!-- Outlier rendering deferred: buildBoxes does not compute outliers yet -->
99
+ {/each}
100
+ </g>
101
+ {/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}
16
+ >{text}</text>
17
+ </g>
@@ -0,0 +1,100 @@
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 LabelPill from './LabelPill.svelte'
6
+
7
+ let { x, y, color, symbol: symbolField, label = false, stat = 'identity', options = {} } = $props()
8
+
9
+ /**
10
+ * @param {Record<string, unknown>} data
11
+ * @returns {string | null}
12
+ */
13
+ function resolveLabel(data) {
14
+ if (!label) return null
15
+ if (label === true) return String(data[y] ?? '')
16
+ if (typeof label === 'function') return String(label(data) ?? '')
17
+ if (typeof label === 'string') return String(data[label] ?? '')
18
+ return null
19
+ }
20
+
21
+ const plotState = getContext('plot-state')
22
+ let id = $state(null)
23
+
24
+ onMount(() => {
25
+ id = plotState.registerGeom({ type: 'line', channels: { x, y, color, symbol: symbolField }, stat, options })
26
+ })
27
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
28
+
29
+ $effect(() => {
30
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, symbol: symbolField }, stat })
31
+ })
32
+
33
+ const data = $derived(id ? plotState.geomData(id) : [])
34
+ const xScale = $derived(plotState.xScale)
35
+ const yScale = $derived(plotState.yScale)
36
+ const colors = $derived(plotState.colors)
37
+ const symbolMap = $derived(plotState.symbols)
38
+
39
+ const lines = $derived.by(() => {
40
+ if (!data?.length || !xScale || !yScale) return []
41
+ return buildLines(data, { x, y, color }, xScale, yScale, colors, options.curve)
42
+ })
43
+
44
+ const markerRadius = $derived(options.markerRadius ?? 4)
45
+ </script>
46
+
47
+ {#if lines.length > 0}
48
+ <g data-plot-geom="line">
49
+ {#each lines as seg (seg.key ?? seg.d)}
50
+ <path
51
+ d={seg.d}
52
+ fill="none"
53
+ stroke={seg.stroke}
54
+ stroke-width={options.strokeWidth ?? 2}
55
+ stroke-linejoin="round"
56
+ stroke-linecap="round"
57
+ data-plot-element="line"
58
+ />
59
+ {#if symbolField && symbolMap}
60
+ {#each seg.points as pt (`${pt.x}::${pt.y}`)}
61
+ <path
62
+ transform="translate({pt.x},{pt.y})"
63
+ d={buildSymbolPath(symbolMap.get(pt.data[symbolField]) ?? 'circle', markerRadius)}
64
+ fill={seg.stroke}
65
+ stroke={seg.stroke}
66
+ stroke-width="1"
67
+ data-plot-element="line-marker"
68
+ />
69
+ {/each}
70
+ {/if}
71
+ {#if label}
72
+ {#each seg.points as pt (`label::${pt.x}::${pt.y}`)}
73
+ {@const text = resolveLabel(pt.data)}
74
+ {#if text}
75
+ <LabelPill
76
+ x={pt.x + (options.labelOffset?.x ?? 0)}
77
+ y={pt.y + (options.labelOffset?.y ?? -12)}
78
+ {text}
79
+ color={seg.stroke ?? '#333'}
80
+ />
81
+ {/if}
82
+ {/each}
83
+ {/if}
84
+ <!-- Invisible hit areas for tooltip -->
85
+ {#each seg.points as pt (`hover::${pt.x}::${pt.y}`)}
86
+ <circle
87
+ cx={pt.x}
88
+ cy={pt.y}
89
+ r="8"
90
+ fill="transparent"
91
+ stroke="none"
92
+ role="presentation"
93
+ data-plot-element="line-hover"
94
+ onmouseenter={() => plotState.setHovered(pt.data)}
95
+ onmouseleave={() => plotState.clearHovered()}
96
+ />
97
+ {/each}
98
+ {/each}
99
+ </g>
100
+ {/if}
@@ -0,0 +1,100 @@
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 LabelPill from './LabelPill.svelte'
6
+
7
+ let { x, y, color, size, symbol: symbolField, label = false, stat = 'identity', options = {} } = $props()
8
+
9
+ /**
10
+ * @param {Record<string, unknown>} data
11
+ * @returns {string | null}
12
+ */
13
+ function resolveLabel(data) {
14
+ if (!label) return null
15
+ if (label === true) return String(data[y] ?? '')
16
+ if (typeof label === 'function') return String(label(data) ?? '')
17
+ if (typeof label === 'string') return String(data[label] ?? '')
18
+ return null
19
+ }
20
+
21
+ const plotState = getContext('plot-state')
22
+ let id = $state(null)
23
+
24
+ onMount(() => {
25
+ id = plotState.registerGeom({ type: 'point', channels: { x, y, color, size, symbol: symbolField }, stat, options })
26
+ })
27
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
28
+
29
+ $effect(() => {
30
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, size, symbol: symbolField }, stat })
31
+ })
32
+
33
+ const data = $derived(id ? plotState.geomData(id) : [])
34
+ const xScale = $derived(plotState.xScale)
35
+ const yScale = $derived(plotState.yScale)
36
+ const colors = $derived(plotState.colors)
37
+ const symbolMap = $derived(plotState.symbols)
38
+
39
+ const sizeScale = $derived.by(() => {
40
+ if (!size || !data?.length) return null
41
+ const vals = data.map((d) => Number(d[size])).filter((v) => !isNaN(v))
42
+ if (!vals.length) return null
43
+ const maxVal = Math.max(...vals)
44
+ const minVal = Math.min(...vals)
45
+ return scaleSqrt().domain([minVal, maxVal]).range([options.minRadius ?? 3, options.maxRadius ?? 20])
46
+ })
47
+
48
+ const points = $derived.by(() => {
49
+ if (!data?.length || !xScale || !yScale) return []
50
+ return buildPoints(data, { x, y, color, size, symbol: symbolField }, xScale, yScale, colors, sizeScale, symbolMap, options.radius ?? 4)
51
+ })
52
+ </script>
53
+
54
+ {#if points.length > 0}
55
+ <g data-plot-geom="point">
56
+ {#each points as pt, i (`${i}::${pt.data[x]}::${pt.data[y]}`)}
57
+ {#if pt.symbolPath}
58
+ <path
59
+ transform="translate({pt.cx},{pt.cy})"
60
+ d={pt.symbolPath}
61
+ fill={pt.fill}
62
+ stroke={pt.stroke}
63
+ stroke-width="1"
64
+ fill-opacity={options.opacity ?? 0.8}
65
+ data-plot-element="point"
66
+ role="graphics-symbol"
67
+ aria-label="{pt.data[x]}, {pt.data[y]}"
68
+ onmouseenter={() => plotState.setHovered(pt.data)}
69
+ onmouseleave={() => plotState.clearHovered()}
70
+ />
71
+ {:else}
72
+ <circle
73
+ cx={pt.cx}
74
+ cy={pt.cy}
75
+ r={pt.r}
76
+ fill={pt.fill}
77
+ stroke={pt.stroke}
78
+ stroke-width="1"
79
+ fill-opacity={options.opacity ?? 0.8}
80
+ data-plot-element="point"
81
+ role="graphics-symbol"
82
+ aria-label="{pt.data[x]}, {pt.data[y]}"
83
+ onmouseenter={() => plotState.setHovered(pt.data)}
84
+ onmouseleave={() => plotState.clearHovered()}
85
+ />
86
+ {/if}
87
+ {#if label}
88
+ {@const text = resolveLabel(pt.data)}
89
+ {#if text}
90
+ <LabelPill
91
+ x={pt.cx + (options.labelOffset?.x ?? 0)}
92
+ y={pt.cy - pt.r + (options.labelOffset?.y ?? -12)}
93
+ {text}
94
+ color={pt.stroke ?? '#333'}
95
+ />
96
+ {/if}
97
+ {/if}
98
+ {/each}
99
+ </g>
100
+ {/if}
@@ -0,0 +1,44 @@
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
+ onMount(() => {
12
+ id = plotState.registerGeom({ type: 'violin', channels: { x, y, color: fill ?? x }, stat, options })
13
+ })
14
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
15
+
16
+ $effect(() => {
17
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: fill ?? x }, stat })
18
+ })
19
+
20
+ const data = $derived(id ? plotState.geomData(id) : [])
21
+ const xScale = $derived(plotState.xScale)
22
+ const yScale = $derived(plotState.yScale)
23
+ const colors = $derived(plotState.colors)
24
+
25
+ const violins = $derived.by(() => {
26
+ if (!data?.length || !xScale || !yScale) return []
27
+ return buildViolins(data, { x, fill: fill ?? x }, xScale, yScale, colors)
28
+ })
29
+ </script>
30
+
31
+ {#if violins.length > 0}
32
+ <g data-plot-geom="violin">
33
+ {#each violins as v, i (`${String(v.cx) }::${ i}`)}
34
+ <path
35
+ d={v.d}
36
+ fill={v.fill}
37
+ fill-opacity="0.5"
38
+ stroke={v.stroke}
39
+ stroke-width="1.5"
40
+ data-plot-element="violin"
41
+ />
42
+ {/each}
43
+ </g>
44
+ {/if}
@@ -0,0 +1,131 @@
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) => typeof xScale.bandwidth === 'function'
21
+ ? xScale(d[xf]) + xScale.bandwidth() / 2
22
+ : xScale(d[xf])
23
+
24
+ const makeGen = () => {
25
+ const gen = area()
26
+ .x(xPos)
27
+ .y0(baseline)
28
+ .y1((d) => yScale(d[yf]))
29
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
30
+ else if (curve === 'step') gen.curve(curveStep)
31
+ return gen
32
+ }
33
+
34
+ const sortByX = (rows) => [...rows].sort((a, b) => a[xf] < b[xf] ? -1 : a[xf] > b[xf] ? 1 : 0)
35
+
36
+ if (!cf) {
37
+ const entry = colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
38
+ const patternKey = pf ? data[0]?.[pf] : null
39
+ const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
40
+ ? toPatternId(String(patternKey)) : null
41
+ return [{ d: makeGen()(sortByX(data)), fill: entry.fill, stroke: 'none', key: null, patternId }]
42
+ }
43
+
44
+ // Group by color field
45
+ const groups = new Map()
46
+ for (const d of data) {
47
+ const key = d[cf]
48
+ if (!groups.has(key)) groups.set(key, [])
49
+ groups.get(key).push(d)
50
+ }
51
+ // For different-field patterns, assign positionally so each area gets a distinct pattern
52
+ const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
53
+
54
+ return [...groups.entries()].map(([key, rows], i) => {
55
+ const entry = colors?.get(key) ?? { fill: '#888', stroke: '#888' }
56
+ // Same field or no pf: look up by colorKey. Different field: assign positionally.
57
+ const patternKey = !pf ? key : pf === cf ? key : (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
58
+ const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
59
+ ? toPatternId(String(patternKey)) : null
60
+ return { d: makeGen()(sortByX(rows)), fill: entry.fill, stroke: 'none', key, patternId }
61
+ })
62
+ }
63
+
64
+ /**
65
+ * Builds stacked area paths using d3 stack layout.
66
+ *
67
+ * @param {Object[]} data
68
+ * @param {{ x: string, y: string, color: string }} channels
69
+ * @param {Function} xScale
70
+ * @param {Function} yScale
71
+ * @param {Map<unknown, {fill: string, stroke: string}>} colors
72
+ * @param {'linear'|'smooth'|'step'} [curve]
73
+ * @param {Map<unknown, string>} [patterns]
74
+ * @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
75
+ */
76
+ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns) {
77
+ const { x: xf, y: yf, color: cf, pattern: pf } = channels
78
+ if (!cf) return buildAreas(data, channels, xScale, yScale, colors, curve, patterns)
79
+
80
+ const xCategories = [...new Set(data.map((d) => d[xf]))]
81
+ .sort((a, b) => a < b ? -1 : a > b ? 1 : 0)
82
+ const colorCategories = [...new Set(data.map((d) => d[cf]))]
83
+
84
+ // Build wide-form lookup: xVal → { colorKey: yVal }
85
+ const lookup = new Map()
86
+ for (const d of data) {
87
+ if (!lookup.has(d[xf])) lookup.set(d[xf], {})
88
+ lookup.get(d[xf])[d[cf]] = Number(d[yf])
89
+ }
90
+
91
+ const wide = xCategories.map((xVal) => {
92
+ const row = { [xf]: xVal }
93
+ for (const c of colorCategories) row[c] = lookup.get(xVal)?.[c] ?? 0
94
+ return row
95
+ })
96
+
97
+ const xPos = (d) => typeof xScale.bandwidth === 'function'
98
+ ? xScale(d.data[xf]) + xScale.bandwidth() / 2
99
+ : xScale(d.data[xf])
100
+
101
+ const makeGen = () => {
102
+ const gen = area()
103
+ .x(xPos)
104
+ .y0((d) => yScale(d[0]))
105
+ .y1((d) => yScale(d[1]))
106
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
107
+ else if (curve === 'step') gen.curve(curveStep)
108
+ return gen
109
+ }
110
+
111
+ const stackGen = stack().keys(colorCategories)
112
+ const layers = stackGen(wide)
113
+
114
+ const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
115
+
116
+ return layers.map((layer, i) => {
117
+ const colorKey = layer.key
118
+ const entry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
119
+ // Same field (or no pf): look up by colorKey. Different field: assign positionally.
120
+ const patternKey = !pf ? colorKey : pf === cf ? colorKey : (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
121
+ const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
122
+ ? toPatternId(String(patternKey)) : null
123
+ return {
124
+ d: makeGen()(layer) ?? '',
125
+ fill: entry.fill,
126
+ stroke: 'none',
127
+ key: colorKey,
128
+ patternId
129
+ }
130
+ })
131
+ }