@rokkit/chart 1.0.0-next.146 → 1.0.0-next.148

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 (157) hide show
  1. package/dist/Plot/index.d.ts +4 -0
  2. package/dist/PlotState.svelte.d.ts +47 -0
  3. package/dist/crossfilter/createCrossFilter.svelte.d.ts +15 -0
  4. package/dist/geoms/lib/areas.d.ts +52 -0
  5. package/dist/geoms/lib/bars.d.ts +3 -0
  6. package/dist/index.d.ts +38 -1
  7. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +10 -0
  8. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +8 -0
  9. package/dist/lib/brewing/PieBrewer.svelte.d.ts +8 -0
  10. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +9 -0
  11. package/dist/lib/brewing/brewer.svelte.d.ts +145 -0
  12. package/dist/lib/brewing/colors.d.ts +17 -0
  13. package/dist/lib/brewing/marks/arcs.d.ts +17 -0
  14. package/dist/lib/brewing/marks/areas.d.ts +31 -0
  15. package/dist/lib/brewing/marks/bars.d.ts +1 -0
  16. package/dist/lib/brewing/marks/boxes.d.ts +24 -0
  17. package/dist/lib/brewing/marks/lines.d.ts +24 -0
  18. package/dist/lib/brewing/marks/points.d.ts +40 -0
  19. package/dist/lib/brewing/marks/violins.d.ts +20 -0
  20. package/dist/lib/brewing/patterns.d.ts +14 -0
  21. package/dist/lib/brewing/scales.d.ts +28 -0
  22. package/dist/lib/brewing/stats.d.ts +31 -0
  23. package/dist/lib/brewing/symbols.d.ts +7 -0
  24. package/dist/lib/plot/chartProps.d.ts +177 -0
  25. package/dist/lib/plot/crossfilter.d.ts +13 -0
  26. package/dist/lib/plot/facet.d.ts +24 -0
  27. package/dist/lib/plot/frames.d.ts +45 -0
  28. package/dist/lib/plot/helpers.d.ts +3 -0
  29. package/dist/lib/plot/preset.d.ts +29 -0
  30. package/dist/lib/plot/scales.d.ts +5 -0
  31. package/dist/lib/plot/stat.d.ts +32 -0
  32. package/dist/lib/plot/types.d.ts +89 -0
  33. package/dist/lib/scales.svelte.d.ts +1 -1
  34. package/dist/lib/swatch.d.ts +12 -0
  35. package/dist/lib/utils.d.ts +1 -0
  36. package/dist/lib/xscale.d.ts +11 -0
  37. package/dist/patterns/index.d.ts +4 -9
  38. package/dist/patterns/patterns.d.ts +72 -0
  39. package/dist/patterns/scale.d.ts +30 -0
  40. package/package.json +9 -3
  41. package/src/AnimatedPlot.svelte +194 -0
  42. package/src/Chart.svelte +101 -0
  43. package/src/FacetPlot/Panel.svelte +23 -0
  44. package/src/FacetPlot.svelte +90 -0
  45. package/src/Plot/Arc.svelte +29 -0
  46. package/src/Plot/Area.svelte +25 -0
  47. package/src/Plot/Axis.svelte +62 -84
  48. package/src/Plot/Grid.svelte +20 -58
  49. package/src/Plot/Legend.svelte +160 -120
  50. package/src/Plot/Line.svelte +27 -0
  51. package/src/Plot/Point.svelte +27 -0
  52. package/src/Plot/Timeline.svelte +95 -0
  53. package/src/Plot/Tooltip.svelte +81 -0
  54. package/src/Plot/index.js +4 -0
  55. package/src/Plot.svelte +189 -0
  56. package/src/PlotState.svelte.js +278 -0
  57. package/src/Sparkline.svelte +69 -0
  58. package/src/charts/AreaChart.svelte +25 -0
  59. package/src/charts/BarChart.svelte +26 -0
  60. package/src/charts/BoxPlot.svelte +21 -0
  61. package/src/charts/BubbleChart.svelte +23 -0
  62. package/src/charts/LineChart.svelte +26 -0
  63. package/src/charts/PieChart.svelte +25 -0
  64. package/src/charts/ScatterPlot.svelte +25 -0
  65. package/src/charts/ViolinPlot.svelte +21 -0
  66. package/src/crossfilter/CrossFilter.svelte +38 -0
  67. package/src/crossfilter/FilterBar.svelte +32 -0
  68. package/src/crossfilter/FilterSlider.svelte +79 -0
  69. package/src/crossfilter/createCrossFilter.svelte.js +113 -0
  70. package/src/elements/SymbolGrid.svelte +6 -7
  71. package/src/geoms/Arc.svelte +81 -0
  72. package/src/geoms/Area.svelte +50 -0
  73. package/src/geoms/Bar.svelte +142 -0
  74. package/src/geoms/Box.svelte +101 -0
  75. package/src/geoms/LabelPill.svelte +17 -0
  76. package/src/geoms/Line.svelte +100 -0
  77. package/src/geoms/Point.svelte +100 -0
  78. package/src/geoms/Violin.svelte +44 -0
  79. package/src/geoms/lib/areas.js +131 -0
  80. package/src/geoms/lib/bars.js +172 -0
  81. package/src/index.js +52 -3
  82. package/src/lib/brewing/BoxBrewer.svelte.js +56 -0
  83. package/src/lib/brewing/CartesianBrewer.svelte.js +16 -0
  84. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  85. package/src/lib/brewing/ViolinBrewer.svelte.js +55 -0
  86. package/src/lib/brewing/brewer.svelte.js +229 -0
  87. package/src/lib/brewing/colors.js +22 -0
  88. package/src/lib/brewing/marks/arcs.js +43 -0
  89. package/src/lib/brewing/marks/areas.js +59 -0
  90. package/src/lib/brewing/marks/bars.js +49 -0
  91. package/src/lib/brewing/marks/boxes.js +75 -0
  92. package/src/lib/brewing/marks/lines.js +48 -0
  93. package/src/lib/brewing/marks/points.js +57 -0
  94. package/src/lib/brewing/marks/violins.js +90 -0
  95. package/src/lib/brewing/patterns.js +31 -0
  96. package/src/lib/brewing/scales.js +51 -0
  97. package/src/lib/brewing/scales.svelte.js +2 -26
  98. package/src/lib/brewing/stats.js +62 -0
  99. package/src/lib/brewing/symbols.js +10 -0
  100. package/src/lib/plot/chartProps.js +76 -0
  101. package/src/lib/plot/crossfilter.js +16 -0
  102. package/src/lib/plot/facet.js +58 -0
  103. package/src/lib/plot/frames.js +90 -0
  104. package/src/lib/plot/helpers.js +14 -0
  105. package/src/lib/plot/preset.js +53 -0
  106. package/src/lib/plot/scales.js +56 -0
  107. package/src/lib/plot/stat.js +92 -0
  108. package/src/lib/plot/types.js +65 -0
  109. package/src/lib/scales.svelte.js +2 -26
  110. package/src/lib/swatch.js +13 -0
  111. package/src/lib/utils.js +9 -0
  112. package/src/lib/xscale.js +31 -0
  113. package/src/patterns/DefinePatterns.svelte +32 -0
  114. package/src/patterns/PatternDef.svelte +27 -0
  115. package/src/patterns/index.js +4 -14
  116. package/src/patterns/patterns.js +208 -0
  117. package/src/patterns/scale.js +87 -0
  118. package/src/spec/chart-spec.js +29 -0
  119. package/src/symbols/Shape.svelte +1 -1
  120. package/src/symbols/constants/index.js +1 -1
  121. package/dist/old_lib/index.d.ts +0 -4
  122. package/dist/old_lib/plots.d.ts +0 -3
  123. package/dist/old_lib/swatch.d.ts +0 -285
  124. package/dist/old_lib/utils.d.ts +0 -1
  125. package/dist/patterns/paths/constants.d.ts +0 -1
  126. package/dist/template/constants.d.ts +0 -43
  127. package/dist/template/shapes/index.d.ts +0 -4
  128. package/src/old_lib/index.js +0 -4
  129. package/src/old_lib/plots.js +0 -27
  130. package/src/old_lib/swatch.js +0 -16
  131. package/src/old_lib/utils.js +0 -8
  132. package/src/patterns/Brick.svelte +0 -15
  133. package/src/patterns/Circles.svelte +0 -18
  134. package/src/patterns/CrossHatch.svelte +0 -12
  135. package/src/patterns/CurvedWave.svelte +0 -7
  136. package/src/patterns/Dots.svelte +0 -20
  137. package/src/patterns/OutlineCircles.svelte +0 -13
  138. package/src/patterns/Tile.svelte +0 -16
  139. package/src/patterns/Triangles.svelte +0 -13
  140. package/src/patterns/Waves.svelte +0 -9
  141. package/src/patterns/paths/NamedPattern.svelte +0 -9
  142. package/src/patterns/paths/constants.js +0 -4
  143. package/src/template/Texture.svelte +0 -13
  144. package/src/template/constants.js +0 -43
  145. package/src/template/shapes/Circles.svelte +0 -15
  146. package/src/template/shapes/Lines.svelte +0 -16
  147. package/src/template/shapes/Path.svelte +0 -9
  148. package/src/template/shapes/Polygons.svelte +0 -15
  149. package/src/template/shapes/index.js +0 -4
  150. /package/dist/{old_lib → lib}/brewer.d.ts +0 -0
  151. /package/dist/{old_lib → lib}/chart.d.ts +0 -0
  152. /package/dist/{old_lib → lib}/grid.d.ts +0 -0
  153. /package/dist/{old_lib → lib}/ticks.d.ts +0 -0
  154. /package/src/{old_lib → lib}/brewer.js +0 -0
  155. /package/src/{old_lib → lib}/chart.js +0 -0
  156. /package/src/{old_lib → lib}/grid.js +0 -0
  157. /package/src/{old_lib → lib}/ticks.js +0 -0
@@ -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
+ }
@@ -0,0 +1,172 @@
1
+ import { scaleBand } from 'd3-scale'
2
+ import { stack } from 'd3-shape'
3
+ import { toPatternId } from '../../lib/brewing/patterns.js'
4
+
5
+ /**
6
+ * Returns a band scale suitable for bar x-positioning.
7
+ * When xScale is already a band scale, returns it unchanged.
8
+ * When xScale is a linear scale (numeric x field like year/month),
9
+ * derives a band scale from the distinct values in the data.
10
+ */
11
+ function ensureBandX(xScale, data, xField) {
12
+ if (typeof xScale?.bandwidth === 'function') return xScale
13
+ const [r0, r1] = xScale.range()
14
+ const domain = [...new Set(data.map((d) => d[xField]))]
15
+ return scaleBand().domain(domain).range([r0, r1]).padding(0.2)
16
+ }
17
+
18
+ /**
19
+ * Returns the sub-band fields: distinct non-x fields among [color, pattern].
20
+ * These are the fields that cause multiple bars within a single x-band.
21
+ */
22
+ function subBandFields(channels) {
23
+ const { x: xf, color: cf, pattern: pf } = channels
24
+ const seen = new Set()
25
+ const out = []
26
+ for (const f of [cf, pf]) {
27
+ if (f && f !== xf && !seen.has(f)) { seen.add(f); out.push(f) }
28
+ }
29
+ return out
30
+ }
31
+
32
+ export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
33
+ const { x: xf, y: yf, color: cf, pattern: pf } = channels
34
+
35
+ const bandScale = ensureBandX(xScale, data, xf)
36
+
37
+ // Sub-banding: only fields that differ from x drive grouping within a band
38
+ const subFields = subBandFields(channels)
39
+ const getSubKey = (d) => subFields.map((f) => String(d[f])).join('::')
40
+ const subDomain = subFields.length > 0 ? [...new Set(data.map(getSubKey))] : []
41
+ const subScale = subDomain.length > 1
42
+ ? scaleBand().domain(subDomain).range([0, bandScale.bandwidth()]).padding(0.05)
43
+ : null
44
+
45
+ return data.map((d, i) => {
46
+ const xVal = d[xf]
47
+ const colorKey = cf ? d[cf] : null
48
+ const patternKey = pf ? d[pf] : null
49
+ const subKey = getSubKey(d)
50
+
51
+ const colorEntry = colors?.get(colorKey) ?? colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
52
+ const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
53
+ ? toPatternId(String(patternKey))
54
+ : null
55
+
56
+ const bandX = bandScale(xVal) ?? 0
57
+ const subX = subScale && subKey ? (subScale(subKey) ?? 0) : 0
58
+ const barX = bandX + subX
59
+ const barWidth = subScale ? subScale.bandwidth() : bandScale.bandwidth()
60
+ const barY = yScale(d[yf])
61
+ const barHeight = innerHeight - barY
62
+
63
+ return {
64
+ data: d,
65
+ key: `${String(xVal)}::${subKey}::${i}`,
66
+ x: barX,
67
+ y: barY,
68
+ width: barWidth,
69
+ height: barHeight,
70
+ fill: colorEntry.fill,
71
+ stroke: colorEntry.stroke,
72
+ patternId
73
+ }
74
+ })
75
+ }
76
+
77
+ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
78
+ const { x: xf, y: yf, color: cf, pattern: pf } = channels
79
+
80
+ const bandScale = ensureBandX(xScale, data, xf)
81
+
82
+ // Stack dimension: first non-x grouping field (prefer pattern, then color)
83
+ const subFields = subBandFields(channels)
84
+ if (subFields.length === 0) {
85
+ return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
86
+ }
87
+ const stackField = subFields[0]
88
+
89
+ const xCategories = [...new Set(data.map((d) => d[xf]))]
90
+ const stackCategories = [...new Set(data.map((d) => d[stackField]))]
91
+
92
+ const lookup = new Map()
93
+ for (const d of data) {
94
+ if (!lookup.has(d[xf])) lookup.set(d[xf], {})
95
+ lookup.get(d[xf])[d[stackField]] = Number(d[yf])
96
+ }
97
+
98
+ const wide = xCategories.map((xVal) => {
99
+ const row = { [xf]: xVal }
100
+ for (const sk of stackCategories) row[sk] = lookup.get(xVal)?.[sk] ?? 0
101
+ return row
102
+ })
103
+
104
+ const stackGen = stack().keys(stackCategories)
105
+ const layers = stackGen(wide)
106
+
107
+ const bars = []
108
+ for (const layer of layers) {
109
+ const stackKey = layer.key
110
+
111
+ for (const point of layer) {
112
+ const [y0, y1] = point
113
+ const xVal = point.data[xf]
114
+
115
+ // Color lookup: cf may equal xf (= xVal) or stackField (= stackKey)
116
+ const colorKey = cf
117
+ ? (cf === xf ? xVal : cf === stackField ? stackKey : null)
118
+ : null
119
+ const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
120
+
121
+ // Pattern lookup: pf may equal xf (= xVal) or stackField (= stackKey)
122
+ const patternKey = pf
123
+ ? (pf === xf ? xVal : pf === stackField ? stackKey : null)
124
+ : null
125
+ const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
126
+ ? toPatternId(String(patternKey))
127
+ : null
128
+
129
+ bars.push({
130
+ data: point.data,
131
+ key: `${String(xVal)}::${String(stackKey)}`,
132
+ x: bandScale(xVal) ?? 0,
133
+ y: yScale(y1),
134
+ width: bandScale.bandwidth(),
135
+ height: yScale(y0) - yScale(y1),
136
+ fill: colorEntry.fill,
137
+ stroke: colorEntry.stroke,
138
+ patternId
139
+ })
140
+ }
141
+ }
142
+ return bars
143
+ }
144
+
145
+ export function buildHorizontalBars(data, channels, xScale, yScale, colors, _innerHeight) {
146
+ const { x: xf, y: yf, color: cf } = channels
147
+ const colorKeys = cf ? [...new Set(data.map((d) => d[cf]))] : []
148
+ const subScale = colorKeys.length > 1
149
+ ? scaleBand().domain(colorKeys).range([0, yScale.bandwidth()]).padding(0.05)
150
+ : null
151
+
152
+ return data.map((d) => {
153
+ const yVal = d[yf]
154
+ const colorKey = cf ? d[cf] : null
155
+ const colorEntry = colors?.get(colorKey) ?? colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
156
+
157
+ const bandY = yScale(yVal) ?? 0
158
+ const subY = subScale ? (subScale(colorKey) ?? 0) : 0
159
+
160
+ return {
161
+ data: d,
162
+ key: `${String(yVal)}::${String(colorKey ?? '')}`,
163
+ x: 0,
164
+ y: bandY + subY,
165
+ width: xScale(d[xf]),
166
+ height: subScale ? subScale.bandwidth() : yScale.bandwidth(),
167
+ fill: colorEntry.fill,
168
+ stroke: colorEntry.stroke,
169
+ patternId: null
170
+ }
171
+ })
172
+ }