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

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 (222) 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 +49 -0
  5. package/dist/crossfilter/createCrossFilter.svelte.d.ts +13 -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 +114 -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 +23 -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 +38 -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 +215 -0
  59. package/src/Chart.svelte +98 -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 +181 -0
  75. package/src/PlotState.svelte.js +277 -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 +120 -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 +103 -0
  104. package/src/geoms/LabelPill.svelte +17 -0
  105. package/src/geoms/Line.svelte +99 -0
  106. package/src/geoms/Point.svelte +105 -0
  107. package/src/geoms/Violin.svelte +46 -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 +17 -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 +230 -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 +66 -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 +220 -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/index.js +4 -0
  156. package/src/patterns/patterns.js +208 -0
  157. package/src/patterns/scale.js +87 -0
  158. package/src/spec/chart-spec.js +29 -0
  159. package/src/symbols/RoundedSquare.svelte +33 -0
  160. package/src/symbols/Shape.svelte +37 -0
  161. package/src/symbols/constants/index.js +4 -0
  162. package/src/symbols/index.js +9 -0
  163. package/src/symbols/outline.svelte +60 -0
  164. package/src/symbols/solid.svelte +60 -0
  165. package/src/chart/FacetGrid.svelte +0 -51
  166. package/src/chart/Grid.svelte +0 -34
  167. package/src/chart/Legend.svelte +0 -16
  168. package/src/chart/PatternDefs.svelte +0 -13
  169. package/src/chart/Swatch.svelte +0 -93
  170. package/src/chart/SwatchButton.svelte +0 -29
  171. package/src/chart/SwatchGrid.svelte +0 -55
  172. package/src/chart/Symbol.svelte +0 -37
  173. package/src/chart/Texture.svelte +0 -16
  174. package/src/chart/TexturedShape.svelte +0 -27
  175. package/src/chart/TimelapseChart.svelte +0 -97
  176. package/src/chart/Timer.svelte +0 -27
  177. package/src/chart.js +0 -9
  178. package/src/components/charts/Axis.svelte +0 -66
  179. package/src/components/charts/Chart.svelte +0 -35
  180. package/src/components/index.js +0 -23
  181. package/src/components/lib/axis.js +0 -0
  182. package/src/components/lib/chart.js +0 -187
  183. package/src/components/lib/color.js +0 -327
  184. package/src/components/lib/funnel.js +0 -204
  185. package/src/components/lib/index.js +0 -19
  186. package/src/components/lib/pattern.js +0 -190
  187. package/src/components/lib/rollup.js +0 -55
  188. package/src/components/lib/shape.js +0 -199
  189. package/src/components/lib/summary.js +0 -145
  190. package/src/components/lib/theme.js +0 -23
  191. package/src/components/lib/timer.js +0 -41
  192. package/src/components/lib/utils.js +0 -165
  193. package/src/components/plots/BarPlot.svelte +0 -36
  194. package/src/components/plots/BoxPlot.svelte +0 -54
  195. package/src/components/plots/ScatterPlot.svelte +0 -30
  196. package/src/components/store.js +0 -70
  197. package/src/constants.js +0 -66
  198. package/src/elements/PatternDefs.svelte +0 -13
  199. package/src/elements/PatternMask.svelte +0 -20
  200. package/src/elements/Symbol.svelte +0 -38
  201. package/src/elements/Tooltip.svelte +0 -23
  202. package/src/funnel.svelte +0 -35
  203. package/src/geom.js +0 -105
  204. package/src/lib/axis.js +0 -75
  205. package/src/lib/colors.js +0 -32
  206. package/src/lib/geom.js +0 -4
  207. package/src/lib/shapes.js +0 -144
  208. package/src/lib/timer.js +0 -44
  209. package/src/lookup.js +0 -29
  210. package/src/plots/BarPlot.svelte +0 -55
  211. package/src/plots/BoxPlot.svelte +0 -0
  212. package/src/plots/FunnelPlot.svelte +0 -33
  213. package/src/plots/HeatMap.svelte +0 -5
  214. package/src/plots/HeatMapCalendar.svelte +0 -129
  215. package/src/plots/LinePlot.svelte +0 -55
  216. package/src/plots/Plot.svelte +0 -25
  217. package/src/plots/RankBarPlot.svelte +0 -38
  218. package/src/plots/ScatterPlot.svelte +0 -20
  219. package/src/plots/ViolinPlot.svelte +0 -11
  220. package/src/plots/heatmap.js +0 -70
  221. package/src/plots/index.js +0 -10
  222. 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,103 @@
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({ type: 'box', channels: { x, y, color: fillChannel }, stat, options })
15
+ })
16
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
17
+
18
+ $effect(() => {
19
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
20
+ })
21
+
22
+ const data = $derived(id ? plotState.geomData(id) : [])
23
+ const xScale = $derived(plotState.xScale)
24
+ const yScale = $derived(plotState.yScale)
25
+ const colors = $derived(plotState.colors)
26
+
27
+ const boxes = $derived.by(() => {
28
+ if (!data?.length || !xScale || !yScale) return []
29
+ return buildBoxes(data, { x, fill: fillChannel }, xScale, yScale, colors)
30
+ })
31
+ </script>
32
+
33
+ {#if boxes.length > 0}
34
+ <g data-plot-geom="box">
35
+ {#each boxes as box, i (`${String(box.cx) }::${ i}`)}
36
+ {@const x0 = box.cx - box.width / 2}
37
+ {@const xMid = box.cx}
38
+ {@const xCap0 = box.cx - box.whiskerWidth / 2}
39
+ {@const xCap1 = box.cx + box.whiskerWidth / 2}
40
+ <!-- Box body (IQR): lighter fill shade -->
41
+ <rect
42
+ x={x0}
43
+ y={box.q3}
44
+ width={box.width}
45
+ height={Math.max(0, box.q1 - box.q3)}
46
+ fill={box.fill}
47
+ fill-opacity="0.5"
48
+ stroke={box.stroke}
49
+ stroke-width="1"
50
+ data-plot-element="box-body"
51
+ />
52
+ <!-- Median line: darker stroke shade -->
53
+ <line
54
+ x1={x0}
55
+ y1={box.median}
56
+ x2={x0 + box.width}
57
+ y2={box.median}
58
+ stroke={box.stroke}
59
+ stroke-width="2"
60
+ data-plot-element="box-median"
61
+ />
62
+ <!-- Lower whisker (q1 to iqr_min) -->
63
+ <line
64
+ x1={xMid}
65
+ y1={box.q1}
66
+ x2={xMid}
67
+ y2={box.iqr_min}
68
+ stroke={box.stroke}
69
+ stroke-width="1"
70
+ data-plot-element="box-whisker"
71
+ />
72
+ <!-- Upper whisker (q3 to iqr_max) -->
73
+ <line
74
+ x1={xMid}
75
+ y1={box.q3}
76
+ x2={xMid}
77
+ y2={box.iqr_max}
78
+ stroke={box.stroke}
79
+ stroke-width="1"
80
+ data-plot-element="box-whisker"
81
+ />
82
+ <!-- Lower whisker cap -->
83
+ <line
84
+ x1={xCap0}
85
+ y1={box.iqr_min}
86
+ x2={xCap1}
87
+ y2={box.iqr_min}
88
+ stroke={box.stroke}
89
+ stroke-width="1"
90
+ />
91
+ <!-- Upper whisker cap -->
92
+ <line
93
+ x1={xCap0}
94
+ y1={box.iqr_max}
95
+ x2={xCap1}
96
+ y2={box.iqr_max}
97
+ stroke={box.stroke}
98
+ stroke-width="1"
99
+ />
100
+ <!-- Outlier rendering deferred: buildBoxes does not compute outliers yet -->
101
+ {/each}
102
+ </g>
103
+ {/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,99 @@
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
+ return typeof label === 'string' ? String(data[label] ?? '') : null
18
+ }
19
+
20
+ const plotState = getContext('plot-state')
21
+ let id = $state(null)
22
+
23
+ onMount(() => {
24
+ id = plotState.registerGeom({ type: 'line', channels: { x, y, color, symbol: symbolField }, stat, options })
25
+ })
26
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
27
+
28
+ $effect(() => {
29
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, symbol: symbolField }, stat })
30
+ })
31
+
32
+ const data = $derived(id ? plotState.geomData(id) : [])
33
+ const xScale = $derived(plotState.xScale)
34
+ const yScale = $derived(plotState.yScale)
35
+ const colors = $derived(plotState.colors)
36
+ const symbolMap = $derived(plotState.symbols)
37
+
38
+ const lines = $derived.by(() => {
39
+ if (!data?.length || !xScale || !yScale) return []
40
+ return buildLines(data, { x, y, color }, xScale, yScale, colors, options.curve)
41
+ })
42
+
43
+ const markerRadius = $derived(options.markerRadius ?? 4)
44
+ </script>
45
+
46
+ {#if lines.length > 0}
47
+ <g data-plot-geom="line">
48
+ {#each lines as seg (seg.key ?? seg.d)}
49
+ <path
50
+ d={seg.d}
51
+ fill="none"
52
+ stroke={seg.stroke}
53
+ stroke-width={options.strokeWidth ?? 2}
54
+ stroke-linejoin="round"
55
+ stroke-linecap="round"
56
+ data-plot-element="line"
57
+ />
58
+ {#if symbolField && symbolMap}
59
+ {#each seg.points as pt (`${pt.x}::${pt.y}`)}
60
+ <path
61
+ transform="translate({pt.x},{pt.y})"
62
+ d={buildSymbolPath(symbolMap.get(pt.data[symbolField]) ?? 'circle', markerRadius)}
63
+ fill={seg.stroke}
64
+ stroke={seg.stroke}
65
+ stroke-width="1"
66
+ data-plot-element="line-marker"
67
+ />
68
+ {/each}
69
+ {/if}
70
+ {#if label}
71
+ {#each seg.points as pt (`label::${pt.x}::${pt.y}`)}
72
+ {@const text = resolveLabel(pt.data)}
73
+ {#if text}
74
+ <LabelPill
75
+ x={pt.x + (options.labelOffset?.x ?? 0)}
76
+ y={pt.y + (options.labelOffset?.y ?? -12)}
77
+ {text}
78
+ color={seg.stroke ?? '#333'}
79
+ />
80
+ {/if}
81
+ {/each}
82
+ {/if}
83
+ <!-- Invisible hit areas for tooltip -->
84
+ {#each seg.points as pt (`hover::${pt.x}::${pt.y}`)}
85
+ <circle
86
+ cx={pt.x}
87
+ cy={pt.y}
88
+ r="8"
89
+ fill="transparent"
90
+ stroke="none"
91
+ role="presentation"
92
+ data-plot-element="line-hover"
93
+ onmouseenter={() => plotState.setHovered(pt.data)}
94
+ onmouseleave={() => plotState.clearHovered()}
95
+ />
96
+ {/each}
97
+ {/each}
98
+ </g>
99
+ {/if}
@@ -0,0 +1,105 @@
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
+ return typeof label === 'string' ? String(data[label] ?? '') : null
18
+ }
19
+
20
+ const plotState = getContext('plot-state')
21
+ let id = $state(null)
22
+
23
+ onMount(() => {
24
+ id = plotState.registerGeom({ type: 'point', channels: { x, y, color, size, symbol: symbolField }, stat, options })
25
+ })
26
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
27
+
28
+ $effect(() => {
29
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, size, symbol: symbolField }, stat })
30
+ })
31
+
32
+ const data = $derived(id ? plotState.geomData(id) : [])
33
+ const xScale = $derived(plotState.xScale)
34
+ const yScale = $derived(plotState.yScale)
35
+ const colors = $derived(plotState.colors)
36
+ const symbolMap = $derived(plotState.symbols)
37
+
38
+ function buildSizeScale() {
39
+ if (!size || !data?.length) return null
40
+ const vals = data.map((d) => Number(d[size])).filter((v) => !isNaN(v))
41
+ if (!vals.length) return null
42
+ const minVal = Math.min(...vals)
43
+ const maxVal = Math.max(...vals)
44
+ const minRadius = options.minRadius ?? 3
45
+ const maxRadius = options.maxRadius ?? 20
46
+ return scaleSqrt().domain([minVal, maxVal]).range([minRadius, maxRadius])
47
+ }
48
+
49
+ const sizeScale = $derived.by(() => buildSizeScale())
50
+
51
+ const defaultRadius = $derived(options.radius ?? 4)
52
+
53
+ const points = $derived.by(() => {
54
+ if (!data?.length || !xScale || !yScale) return []
55
+ return buildPoints(data, { x, y, color, size, symbol: symbolField }, xScale, yScale, colors, sizeScale, symbolMap, defaultRadius)
56
+ })
57
+ </script>
58
+
59
+ {#if points.length > 0}
60
+ <g data-plot-geom="point">
61
+ {#each points as pt, i (`${i}::${pt.data[x]}::${pt.data[y]}`)}
62
+ {#if pt.symbolPath}
63
+ <path
64
+ transform="translate({pt.cx},{pt.cy})"
65
+ d={pt.symbolPath}
66
+ fill={pt.fill}
67
+ stroke={pt.stroke}
68
+ stroke-width="1"
69
+ fill-opacity={options.opacity ?? 0.8}
70
+ data-plot-element="point"
71
+ role="graphics-symbol"
72
+ aria-label="{pt.data[x]}, {pt.data[y]}"
73
+ onmouseenter={() => plotState.setHovered(pt.data)}
74
+ onmouseleave={() => plotState.clearHovered()}
75
+ />
76
+ {:else}
77
+ <circle
78
+ cx={pt.cx}
79
+ cy={pt.cy}
80
+ r={pt.r}
81
+ fill={pt.fill}
82
+ stroke={pt.stroke}
83
+ stroke-width="1"
84
+ fill-opacity={options.opacity ?? 0.8}
85
+ data-plot-element="point"
86
+ role="graphics-symbol"
87
+ aria-label="{pt.data[x]}, {pt.data[y]}"
88
+ onmouseenter={() => plotState.setHovered(pt.data)}
89
+ onmouseleave={() => plotState.clearHovered()}
90
+ />
91
+ {/if}
92
+ {#if label}
93
+ {@const text = resolveLabel(pt.data)}
94
+ {#if text}
95
+ <LabelPill
96
+ x={pt.cx + (options.labelOffset?.x ?? 0)}
97
+ y={pt.cy - pt.r + (options.labelOffset?.y ?? -12)}
98
+ {text}
99
+ color={pt.stroke ?? '#333'}
100
+ />
101
+ {/if}
102
+ {/if}
103
+ {/each}
104
+ </g>
105
+ {/if}
@@ -0,0 +1,46 @@
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({ type: 'violin', channels: { x, y, color: fillChannel }, stat, options })
15
+ })
16
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
17
+
18
+ $effect(() => {
19
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
20
+ })
21
+
22
+ const data = $derived(id ? plotState.geomData(id) : [])
23
+ const xScale = $derived(plotState.xScale)
24
+ const yScale = $derived(plotState.yScale)
25
+ const colors = $derived(plotState.colors)
26
+
27
+ const violins = $derived.by(() => {
28
+ if (!data?.length || !xScale || !yScale) return []
29
+ return buildViolins(data, { x, fill: fillChannel }, xScale, yScale, colors)
30
+ })
31
+ </script>
32
+
33
+ {#if violins.length > 0}
34
+ <g data-plot-geom="violin">
35
+ {#each violins as v, i (`${String(v.cx) }::${ i}`)}
36
+ <path
37
+ d={v.d}
38
+ fill={v.fill}
39
+ fill-opacity="0.5"
40
+ stroke={v.stroke}
41
+ stroke-width="1.5"
42
+ data-plot-element="violin"
43
+ />
44
+ {/each}
45
+ </g>
46
+ {/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
+ }