@rokkit/chart 1.3.8 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/Chart.svelte +50 -64
  2. package/dist/Plot/Root.svelte +4 -1
  3. package/dist/Plot/Root.svelte.d.ts +2 -0
  4. package/dist/Plot/index.d.ts +1 -1
  5. package/dist/Plot/index.js +1 -1
  6. package/dist/PlotState.svelte.d.ts +14 -2
  7. package/dist/PlotState.svelte.js +46 -4
  8. package/dist/charts/AreaChart.svelte +8 -1
  9. package/dist/charts/AreaChart.svelte.d.ts +5 -0
  10. package/dist/charts/BarChart.svelte +8 -1
  11. package/dist/charts/BarChart.svelte.d.ts +5 -0
  12. package/dist/charts/BoxPlot.svelte +3 -1
  13. package/dist/charts/BoxPlot.svelte.d.ts +1 -0
  14. package/dist/charts/BubbleChart.svelte +3 -1
  15. package/dist/charts/BubbleChart.svelte.d.ts +1 -0
  16. package/dist/charts/LineChart.svelte +3 -1
  17. package/dist/charts/LineChart.svelte.d.ts +1 -0
  18. package/dist/charts/PieChart.svelte +4 -2
  19. package/dist/charts/PieChart.svelte.d.ts +1 -0
  20. package/dist/charts/ScatterPlot.svelte +3 -1
  21. package/dist/charts/ScatterPlot.svelte.d.ts +1 -0
  22. package/dist/charts/ViolinPlot.svelte +3 -1
  23. package/dist/charts/ViolinPlot.svelte.d.ts +1 -0
  24. package/dist/geoms/Arc.svelte +21 -55
  25. package/dist/geoms/Arc.svelte.d.ts +4 -4
  26. package/dist/geoms/Area.svelte +45 -44
  27. package/dist/geoms/Area.svelte.d.ts +8 -1
  28. package/dist/geoms/Bar.svelte +33 -56
  29. package/dist/geoms/Bar.svelte.d.ts +9 -0
  30. package/dist/geoms/Box.svelte +118 -131
  31. package/dist/geoms/Box.svelte.d.ts +5 -3
  32. package/dist/geoms/Candlestick.svelte +22 -77
  33. package/dist/geoms/Candlestick.svelte.d.ts +2 -0
  34. package/dist/geoms/Heatmap.svelte +21 -66
  35. package/dist/geoms/Heatmap.svelte.d.ts +2 -0
  36. package/dist/geoms/Hexbin.svelte +20 -100
  37. package/dist/geoms/Hexbin.svelte.d.ts +2 -0
  38. package/dist/geoms/Jitter.svelte +21 -36
  39. package/dist/geoms/Jitter.svelte.d.ts +5 -3
  40. package/dist/geoms/Line.svelte +27 -31
  41. package/dist/geoms/Line.svelte.d.ts +5 -0
  42. package/dist/geoms/Point.svelte +31 -66
  43. package/dist/geoms/Point.svelte.d.ts +5 -1
  44. package/dist/geoms/Ribbon.svelte +30 -124
  45. package/dist/geoms/Ribbon.svelte.d.ts +2 -0
  46. package/dist/geoms/Rule.svelte +5 -1
  47. package/dist/geoms/Rule.svelte.d.ts +3 -0
  48. package/dist/geoms/Violin.svelte +21 -40
  49. package/dist/geoms/Violin.svelte.d.ts +5 -3
  50. package/dist/geoms/Waterfall.svelte +19 -80
  51. package/dist/geoms/Waterfall.svelte.d.ts +2 -0
  52. package/dist/geoms/lib/GeomState.svelte.d.ts +51 -0
  53. package/dist/geoms/lib/GeomState.svelte.js +79 -0
  54. package/dist/geoms/lib/aesthetics.d.ts +36 -0
  55. package/dist/geoms/lib/aesthetics.js +45 -0
  56. package/dist/geoms/lib/areas.d.ts +1 -1
  57. package/dist/geoms/lib/areas.js +4 -2
  58. package/dist/geoms/lib/bars.d.ts +2 -2
  59. package/dist/geoms/lib/bars.js +13 -8
  60. package/dist/geoms/lib/marks/arc.d.ts +24 -0
  61. package/dist/geoms/lib/marks/arc.js +28 -0
  62. package/dist/geoms/lib/marks/area.d.ts +23 -0
  63. package/dist/geoms/lib/marks/area.js +38 -0
  64. package/dist/geoms/lib/marks/bar.d.ts +16 -0
  65. package/dist/geoms/lib/marks/bar.js +43 -0
  66. package/dist/geoms/lib/marks/box.d.ts +14 -0
  67. package/dist/geoms/lib/marks/box.js +30 -0
  68. package/dist/geoms/lib/marks/candlestick.d.ts +27 -0
  69. package/dist/geoms/lib/marks/candlestick.js +52 -0
  70. package/dist/geoms/lib/marks/heatmap.d.ts +24 -0
  71. package/dist/geoms/lib/marks/heatmap.js +41 -0
  72. package/dist/geoms/lib/marks/hexbin.d.ts +29 -0
  73. package/dist/geoms/lib/marks/hexbin.js +62 -0
  74. package/dist/geoms/lib/marks/jitter.d.ts +21 -0
  75. package/dist/geoms/lib/marks/jitter.js +36 -0
  76. package/dist/geoms/lib/marks/line.d.ts +26 -0
  77. package/dist/geoms/lib/marks/line.js +25 -0
  78. package/dist/geoms/lib/marks/point.d.ts +25 -0
  79. package/dist/geoms/lib/marks/point.js +58 -0
  80. package/dist/geoms/lib/marks/ribbon.d.ts +31 -0
  81. package/dist/geoms/lib/marks/ribbon.js +77 -0
  82. package/dist/geoms/lib/marks/violin.d.ts +13 -0
  83. package/dist/geoms/lib/marks/violin.js +31 -0
  84. package/dist/geoms/lib/marks/waterfall.d.ts +25 -0
  85. package/dist/geoms/lib/marks/waterfall.js +58 -0
  86. package/dist/index.d.ts +1 -8
  87. package/dist/index.js +1 -8
  88. package/dist/lib/preset.d.ts +11 -1
  89. package/dist/lib/preset.js +16 -4
  90. package/package.json +1 -1
  91. package/dist/Plot/Arc.svelte +0 -30
  92. package/dist/Plot/Arc.svelte.d.ts +0 -18
  93. package/dist/lib/brewer.d.ts +0 -9
  94. package/dist/lib/brewer.js +0 -25
  95. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +0 -8
  96. package/dist/lib/brewing/BoxBrewer.svelte.js +0 -14
  97. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +0 -8
  98. package/dist/lib/brewing/CartesianBrewer.svelte.js +0 -21
  99. package/dist/lib/brewing/PieBrewer.svelte.d.ts +0 -8
  100. package/dist/lib/brewing/PieBrewer.svelte.js +0 -14
  101. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +0 -9
  102. package/dist/lib/brewing/QuartileBrewer.svelte.js +0 -51
  103. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +0 -8
  104. package/dist/lib/brewing/ViolinBrewer.svelte.js +0 -14
  105. package/dist/lib/brewing/axes.svelte.d.ts +0 -66
  106. package/dist/lib/brewing/axes.svelte.js +0 -270
  107. package/dist/lib/brewing/bars.svelte.d.ts +0 -56
  108. package/dist/lib/brewing/bars.svelte.js +0 -201
  109. package/dist/lib/brewing/brewer.svelte.d.ts +0 -124
  110. package/dist/lib/brewing/brewer.svelte.js +0 -277
  111. package/dist/lib/brewing/dimensions.svelte.d.ts +0 -35
  112. package/dist/lib/brewing/dimensions.svelte.js +0 -56
  113. package/dist/lib/brewing/index.svelte.d.ts +0 -118
  114. package/dist/lib/brewing/index.svelte.js +0 -205
  115. package/dist/lib/brewing/legends.svelte.d.ts +0 -48
  116. package/dist/lib/brewing/legends.svelte.js +0 -137
  117. package/dist/lib/brewing/marks/areas.d.ts +0 -31
  118. package/dist/lib/brewing/marks/areas.js +0 -72
  119. package/dist/lib/brewing/marks/bars.d.ts +0 -1
  120. package/dist/lib/brewing/marks/bars.js +0 -49
  121. package/dist/lib/brewing/scales.svelte.d.ts +0 -24
  122. package/dist/lib/brewing/scales.svelte.js +0 -82
  123. package/dist/lib/context.d.ts +0 -13
  124. package/dist/lib/context.js +0 -131
  125. package/dist/lib/plot/chartProps.d.ts +0 -177
  126. package/dist/lib/plot/chartProps.js +0 -76
  127. package/dist/lib/scales.svelte.d.ts +0 -35
  128. package/dist/lib/scales.svelte.js +0 -151
  129. package/dist/lib/ticks.d.ts +0 -36
  130. package/dist/lib/ticks.js +0 -46
@@ -1,12 +1,16 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
- import { buildBoxes } from '../lib/brewing/marks/boxes.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildBoxMarks } from './lib/marks/box.js'
5
6
 
6
7
  type Props = {
7
8
  x?: string
8
9
  y?: string
10
+ /** Box interior aesthetic (field); defaults to `x`. */
9
11
  fill?: string
12
+ /** Outline aesthetic (field); overrides whisker/median/border stroke when set. */
13
+ color?: string
10
14
  stat?: string
11
15
  /** Offset the box to one half of the band (raincloud/split plots). Default 'center'. */
12
16
  side?: 'left' | 'right' | 'center'
@@ -15,13 +19,14 @@
15
19
  width?: number
16
20
  /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
17
21
  pattern?: string
18
- options?: { opacity?: number }
22
+ /** Fixed box opacity 0–1; defaults to the per-geom preset (box = 0.5). */
23
+ alpha?: number
19
24
  }
20
25
 
21
- let { x, y, fill, stat = 'boxplot', side = 'center', width, pattern, options = {} }: Props = $props()
26
+ let { x, y, fill, color, stat = 'boxplot', side = 'center', width, pattern, alpha }: Props =
27
+ $props()
22
28
 
23
29
  const plotState = getContext<PlotState>('plot-state')
24
- let id = $state<string | null>(null)
25
30
 
26
31
  // Fall back to the root Plot channels when the geom's own x/y are omitted, so
27
32
  // <Plot.Box> composes under <Plot.Root x y> without silently NaN-ing.
@@ -30,148 +35,130 @@
30
35
  // fill ?? effective-x drives the colors map for both box interior and whisker strokes
31
36
  const fillChannel = $derived(fill ?? xf)
32
37
 
33
- onMount(() => {
34
- id = plotState.registerGeom({
35
- type: 'box',
36
- channels: { x: xf, y: yf, color: fillChannel, pattern },
37
- stat,
38
- options
39
- })
40
- })
41
- onDestroy(() => {
42
- if (id) plotState.unregisterGeom(id)
43
- })
38
+ const geom = new GeomState(plotState, () => ({
39
+ type: 'box',
40
+ channels: { x: xf, y: yf, fill: fillChannel, color, pattern },
41
+ stat,
42
+ options: { side, width },
43
+ alpha,
44
+ build: buildBoxMarks
45
+ }))
46
+ onMount(geom.register)
47
+ onDestroy(geom.destroy)
48
+ $effect(geom.sync)
44
49
 
45
- $effect(() => {
46
- if (id)
47
- plotState.updateGeom(id, { channels: { x: xf, y: yf, color: fillChannel, pattern }, stat })
48
- })
49
-
50
- const data = $derived(id ? plotState.geomData(id) : [])
51
- const xScale = $derived(plotState.xScale)
52
- const yScale = $derived(plotState.yScale)
53
- const colors = $derived(plotState.colors)
54
- const patterns = $derived(plotState.patterns)
55
-
56
- const boxes = $derived.by(() => {
57
- if (!data?.length || !xScale || !yScale) return []
58
- return buildBoxes(data, { x: xf, fill: fillChannel, pattern }, xScale, yScale, colors, {
59
- side,
60
- width,
61
- patterns
62
- })
63
- })
50
+ const boxes = $derived(geom.marks)
64
51
  </script>
65
52
 
66
53
  {#if boxes.length > 0}
67
54
  <g data-plot-geom="box">
68
- {#each boxes as box, i (`${String(box.cx)}::${i}`)}
69
- <!-- Each primitive is placed through plotState.place(band, value) so the whole
70
- box transposes correctly under orientation='horizontal' (flip). -->
71
- {@const c1 = plotState.place(box.cx - box.width / 2, box.q3)}
72
- {@const c2 = plotState.place(box.cx + box.width / 2, box.q1)}
73
- {@const mA = plotState.place(box.cx - box.width / 2, box.median)}
74
- {@const mB = plotState.place(box.cx + box.width / 2, box.median)}
75
- {@const wLoA = plotState.place(box.cx, box.q1)}
76
- {@const wLoB = plotState.place(box.cx, box.iqr_min)}
77
- {@const wHiA = plotState.place(box.cx, box.q3)}
78
- {@const wHiB = plotState.place(box.cx, box.iqr_max)}
79
- {@const capLoA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_min)}
80
- {@const capLoB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_min)}
81
- {@const capHiA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_max)}
82
- {@const capHiB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_max)}
83
- <!-- Box body (IQR): lighter fill shade -->
55
+ {#each boxes as box, i (`${String(box.cx)}::${i}`)}
56
+ <!-- Each primitive is placed through plotState.place(band, value) so the whole
57
+ box transposes correctly under orientation='horizontal' (flip). -->
58
+ {@const c1 = plotState.place(box.cx - box.width / 2, box.q3)}
59
+ {@const c2 = plotState.place(box.cx + box.width / 2, box.q1)}
60
+ {@const mA = plotState.place(box.cx - box.width / 2, box.median)}
61
+ {@const mB = plotState.place(box.cx + box.width / 2, box.median)}
62
+ {@const wLoA = plotState.place(box.cx, box.q1)}
63
+ {@const wLoB = plotState.place(box.cx, box.iqr_min)}
64
+ {@const wHiA = plotState.place(box.cx, box.q3)}
65
+ {@const wHiB = plotState.place(box.cx, box.iqr_max)}
66
+ {@const capLoA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_min)}
67
+ {@const capLoB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_min)}
68
+ {@const capHiA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_max)}
69
+ {@const capHiB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_max)}
70
+ <!-- Box body (IQR): lighter fill shade -->
71
+ <rect
72
+ x={Math.min(c1.x, c2.x)}
73
+ y={Math.min(c1.y, c2.y)}
74
+ width={Math.abs(c2.x - c1.x)}
75
+ height={Math.abs(c2.y - c1.y)}
76
+ fill={box.fill}
77
+ fill-opacity={box.alpha}
78
+ stroke={box.stroke}
79
+ stroke-width="1"
80
+ data-plot-element="box-body"
81
+ role="graphics-symbol"
82
+ aria-label={`${String(box.data[xf ?? ''] ?? 'box')}: median ${box.data.median}, IQR ${box.data.q1}–${box.data.q3}`}
83
+ onmouseenter={() => plotState.setHovered(box.data)}
84
+ onmouseleave={() => plotState.clearHovered()}
85
+ />
86
+ {#if box.patternId}
87
+ <!-- Texture fill overlay (matches the box body) -->
84
88
  <rect
85
89
  x={Math.min(c1.x, c2.x)}
86
90
  y={Math.min(c1.y, c2.y)}
87
91
  width={Math.abs(c2.x - c1.x)}
88
92
  height={Math.abs(c2.y - c1.y)}
89
- fill={box.fill}
90
- fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.box}
93
+ fill="url(#{box.patternId})"
94
+ pointer-events="none"
95
+ />
96
+ {/if}
97
+ <!-- Median line: darker stroke shade -->
98
+ <line
99
+ x1={mA.x}
100
+ y1={mA.y}
101
+ x2={mB.x}
102
+ y2={mB.y}
103
+ stroke={box.stroke}
104
+ stroke-width="2"
105
+ data-plot-element="box-median"
106
+ />
107
+ <!-- Lower whisker (q1 to iqr_min) -->
108
+ <line
109
+ x1={wLoA.x}
110
+ y1={wLoA.y}
111
+ x2={wLoB.x}
112
+ y2={wLoB.y}
113
+ stroke={box.stroke}
114
+ stroke-width="1"
115
+ data-plot-element="box-whisker"
116
+ />
117
+ <!-- Upper whisker (q3 to iqr_max) -->
118
+ <line
119
+ x1={wHiA.x}
120
+ y1={wHiA.y}
121
+ x2={wHiB.x}
122
+ y2={wHiB.y}
123
+ stroke={box.stroke}
124
+ stroke-width="1"
125
+ data-plot-element="box-whisker"
126
+ />
127
+ <!-- Lower whisker cap -->
128
+ <line
129
+ x1={capLoA.x}
130
+ y1={capLoA.y}
131
+ x2={capLoB.x}
132
+ y2={capLoB.y}
133
+ stroke={box.stroke}
134
+ stroke-width="1"
135
+ />
136
+ <!-- Upper whisker cap -->
137
+ <line
138
+ x1={capHiA.x}
139
+ y1={capHiA.y}
140
+ x2={capHiB.x}
141
+ y2={capHiB.y}
142
+ stroke={box.stroke}
143
+ stroke-width="1"
144
+ />
145
+ <!-- Outliers: individual points beyond the 1.5·IQR fence -->
146
+ {#each box.outliers as o, oi (`${i}-outlier-${oi}`)}
147
+ {@const op = plotState.place(box.cx, o.cy)}
148
+ <circle
149
+ cx={op.x}
150
+ cy={op.y}
151
+ r="2"
152
+ fill="none"
91
153
  stroke={box.stroke}
92
154
  stroke-width="1"
93
- data-plot-element="box-body"
155
+ data-plot-element="box-outlier"
94
156
  role="graphics-symbol"
95
- aria-label={`${String(box.data[xf ?? ''] ?? 'box')}: median ${box.data.median}, IQR ${box.data.q1}–${box.data.q3}`}
96
- onmouseenter={() => plotState.setHovered(box.data)}
157
+ aria-label={`outlier: ${o.value}`}
158
+ onmouseenter={() => plotState.setHovered({ ...box.data, value: o.value })}
97
159
  onmouseleave={() => plotState.clearHovered()}
98
160
  />
99
- {#if box.patternId}
100
- <!-- Texture fill overlay (matches the box body) -->
101
- <rect
102
- x={Math.min(c1.x, c2.x)}
103
- y={Math.min(c1.y, c2.y)}
104
- width={Math.abs(c2.x - c1.x)}
105
- height={Math.abs(c2.y - c1.y)}
106
- fill="url(#{box.patternId})"
107
- pointer-events="none"
108
- />
109
- {/if}
110
- <!-- Median line: darker stroke shade -->
111
- <line
112
- x1={mA.x}
113
- y1={mA.y}
114
- x2={mB.x}
115
- y2={mB.y}
116
- stroke={box.stroke}
117
- stroke-width="2"
118
- data-plot-element="box-median"
119
- />
120
- <!-- Lower whisker (q1 to iqr_min) -->
121
- <line
122
- x1={wLoA.x}
123
- y1={wLoA.y}
124
- x2={wLoB.x}
125
- y2={wLoB.y}
126
- stroke={box.stroke}
127
- stroke-width="1"
128
- data-plot-element="box-whisker"
129
- />
130
- <!-- Upper whisker (q3 to iqr_max) -->
131
- <line
132
- x1={wHiA.x}
133
- y1={wHiA.y}
134
- x2={wHiB.x}
135
- y2={wHiB.y}
136
- stroke={box.stroke}
137
- stroke-width="1"
138
- data-plot-element="box-whisker"
139
- />
140
- <!-- Lower whisker cap -->
141
- <line
142
- x1={capLoA.x}
143
- y1={capLoA.y}
144
- x2={capLoB.x}
145
- y2={capLoB.y}
146
- stroke={box.stroke}
147
- stroke-width="1"
148
- />
149
- <!-- Upper whisker cap -->
150
- <line
151
- x1={capHiA.x}
152
- y1={capHiA.y}
153
- x2={capHiB.x}
154
- y2={capHiB.y}
155
- stroke={box.stroke}
156
- stroke-width="1"
157
- />
158
- <!-- Outliers: individual points beyond the 1.5·IQR fence -->
159
- {#each box.outliers as o, oi (`${i}-outlier-${oi}`)}
160
- {@const op = plotState.place(box.cx, o.cy)}
161
- <circle
162
- cx={op.x}
163
- cy={op.y}
164
- r="2"
165
- fill="none"
166
- stroke={box.stroke}
167
- stroke-width="1"
168
- data-plot-element="box-outlier"
169
- role="graphics-symbol"
170
- aria-label={`outlier: ${o.value}`}
171
- onmouseenter={() => plotState.setHovered({ ...box.data, value: o.value })}
172
- onmouseleave={() => plotState.clearHovered()}
173
- />
174
- {/each}
175
161
  {/each}
162
+ {/each}
176
163
  </g>
177
164
  {/if}
@@ -1,7 +1,10 @@
1
1
  type Props = {
2
2
  x?: string;
3
3
  y?: string;
4
+ /** Box interior aesthetic (field); defaults to `x`. */
4
5
  fill?: string;
6
+ /** Outline aesthetic (field); overrides whisker/median/border stroke when set. */
7
+ color?: string;
5
8
  stat?: string;
6
9
  /** Offset the box to one half of the band (raincloud/split plots). Default 'center'. */
7
10
  side?: 'left' | 'right' | 'center';
@@ -10,9 +13,8 @@ type Props = {
10
13
  width?: number;
11
14
  /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
12
15
  pattern?: string;
13
- options?: {
14
- opacity?: number;
15
- };
16
+ /** Fixed box opacity 0–1; defaults to the per-geom preset (box = 0.5). */
17
+ alpha?: number;
16
18
  };
17
19
  declare const Box: import("svelte").Component<Props, {}, "">;
18
20
  type Box = ReturnType<typeof Box>;
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildCandleMarks } from './lib/marks/candlestick.js'
4
6
 
5
7
  type Options = {
6
8
  open?: string
@@ -17,92 +19,33 @@
17
19
  y?: string
18
20
  color?: string
19
21
  fill?: string
22
+ /** Fixed opacity 0–1; defaults to the per-geom preset (candlestick = 1). */
23
+ alpha?: number
20
24
  stat?: string
21
25
  options?: Options
22
26
  }
23
27
 
24
- let {
25
- x,
26
- y: _y = undefined,
27
- color = undefined,
28
- fill: fillProp = undefined,
29
- stat = 'identity',
30
- options = {}
31
- }: Props = $props()
28
+ let { x, y: _y = undefined, color, fill, alpha, stat = 'identity', options = {} }: Props = $props()
32
29
 
33
- const open = $derived(options.open ?? 'open')
30
+ const colorField = $derived(fill ?? color)
34
31
  const high = $derived(options.high ?? 'high')
35
- const low = $derived(options.low ?? 'low')
36
- const close = $derived(options.close ?? 'close')
37
- const upColor = $derived(options.upColor ?? '#22c55e')
38
- const downColor = $derived(options.downColor ?? '#ef4444')
39
32
  const wickWidth = $derived(options.wickWidth ?? 1)
40
-
41
33
  const plotState = getContext<PlotState>('plot-state')
42
- let id = $state<string | null>(null)
43
-
44
- onMount(() => {
45
- // Register with y pointing to 'high' so the y scale covers the full range
46
- id = plotState.registerGeom({
47
- type: 'candlestick',
48
- channels: { x, y: high, color: fillProp ?? color },
49
- stat
50
- })
51
- })
52
- onDestroy(() => {
53
- if (id) plotState.unregisterGeom(id)
54
- })
55
-
56
- $effect(() => {
57
- if (id)
58
- plotState.updateGeom(id, {
59
- channels: { x, y: high, color: fillProp ?? color },
60
- stat
61
- })
62
- })
63
-
64
- const data = $derived(id ? plotState.geomData(id) : [])
65
- const xScale = $derived(plotState.xScale)
66
- const yScale = $derived(plotState.yScale)
67
-
68
- const candles = $derived.by(() => {
69
- if (!data?.length || !xScale || !yScale) return []
70
- const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 10
71
- const bodyWidth = bw * 0.6
72
- const bodyOffset = (bw - bodyWidth) / 2
73
34
 
74
- return data.map((d, i) => {
75
- const xPos = (xScale(d[x ?? '']) ?? 0) + bodyOffset
76
- const openVal = Number(d[open])
77
- const closeVal = Number(d[close])
78
- const highVal = Number(d[high])
79
- const lowVal = Number(d[low])
80
- const isUp = closeVal >= openVal
81
- const bodyTop = yScale(isUp ? closeVal : openVal)
82
- const bodyBottom = yScale(isUp ? openVal : closeVal)
35
+ // Register with y pointing to 'high' so the y scale covers the full range.
36
+ const geom = new GeomState(plotState, () => ({
37
+ type: 'candlestick',
38
+ channels: { x, y: high, color: colorField },
39
+ stat,
40
+ options,
41
+ alpha,
42
+ build: buildCandleMarks
43
+ }))
44
+ onMount(geom.register)
45
+ onDestroy(geom.destroy)
46
+ $effect(geom.sync)
83
47
 
84
- // Place (band, value) points so the candle transposes under orientation flip.
85
- const b1 = plotState.place(xPos, bodyTop)
86
- const b2 = plotState.place(xPos + bodyWidth, bodyBottom)
87
- const wa = plotState.place(xPos + bodyWidth / 2, yScale(highVal))
88
- const wb = plotState.place(xPos + bodyWidth / 2, yScale(lowVal))
89
- return {
90
- key: `${d[x ?? '']}-${i}`,
91
- // Body
92
- bodyX: Math.min(b1.x, b2.x),
93
- bodyY: Math.min(b1.y, b2.y),
94
- bodyWidth: Math.abs(b2.x - b1.x),
95
- bodyHeight: Math.max(1, Math.abs(b2.y - b1.y)),
96
- fill: isUp ? upColor : downColor,
97
- // Wick
98
- wickX1: wa.x,
99
- wickY1: wa.y,
100
- wickX2: wb.x,
101
- wickY2: wb.y,
102
- data: d
103
- }
104
- })
105
- })
48
+ const candles = $derived(geom.marks)
106
49
  </script>
107
50
 
108
51
  {#if candles.length > 0}
@@ -115,6 +58,7 @@
115
58
  x2={c.wickX2}
116
59
  y2={c.wickY2}
117
60
  stroke={c.fill}
61
+ stroke-opacity={c.alpha}
118
62
  stroke-width={wickWidth}
119
63
  data-plot-element="wick"
120
64
  />
@@ -125,12 +69,13 @@
125
69
  width={c.bodyWidth}
126
70
  height={c.bodyHeight}
127
71
  fill={c.fill}
72
+ fill-opacity={c.alpha}
128
73
  data-plot-element="candle"
129
74
  role="img"
130
75
  onmouseenter={() => plotState.setHovered(c.data)}
131
76
  onmouseleave={() => plotState.clearHovered()}
132
77
  >
133
- <title>{c.data[x ?? '']}: O={c.data[open]} H={c.data[high]} L={c.data[low]} C={c.data[close]}</title>
78
+ <title>{c.data[x ?? '']}: O={c.data[options.open ?? 'open']} H={c.data[options.high ?? 'high']} L={c.data[options.low ?? 'low']} C={c.data[options.close ?? 'close']}</title>
134
79
  </rect>
135
80
  {/each}
136
81
  </g>
@@ -12,6 +12,8 @@ type Props = {
12
12
  y?: string;
13
13
  color?: string;
14
14
  fill?: string;
15
+ /** Fixed opacity 0–1; defaults to the per-geom preset (candlestick = 1). */
16
+ alpha?: number;
15
17
  stat?: string;
16
18
  options?: Options;
17
19
  };
@@ -1,85 +1,39 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildHeatmapMarks } from './lib/marks/heatmap.js'
4
6
 
5
7
  type Props = {
6
8
  x?: string
7
9
  y?: string
8
10
  color?: string
9
11
  fill?: string
12
+ /** Fixed cell opacity 0–1; defaults to the per-geom preset (heatmap = 1). */
13
+ alpha?: number
10
14
  stat?: string
11
15
  options?: { rounded?: number }
12
16
  }
13
17
 
14
- let {
15
- x,
16
- y,
17
- color,
18
- fill: fillProp,
19
- stat = 'identity',
20
- options = {}
21
- }: Props = $props()
22
-
23
- const colorChannel = $derived(fillProp ?? color)
18
+ let { x, y, color, fill, alpha, stat = 'identity', options = {} }: Props = $props()
24
19
 
20
+ const colorField = $derived(fill ?? color)
25
21
  const plotState = getContext<PlotState>('plot-state')
26
- let id = $state<string | null>(null)
27
-
28
- onMount(() => {
29
- id = plotState.registerGeom({
30
- type: 'heatmap',
31
- channels: { x, y, color: colorChannel },
32
- stat
33
- })
34
- })
35
- onDestroy(() => {
36
- if (id) plotState.unregisterGeom(id)
37
- })
38
22
 
39
- $effect(() => {
40
- if (id)
41
- plotState.updateGeom(id, {
42
- channels: { x, y, color: colorChannel },
43
- stat
44
- })
45
- })
46
-
47
- const data = $derived(id ? plotState.geomData(id) : [])
48
- const xScale = $derived(plotState.xScale)
49
- const yScale = $derived(plotState.yScale)
50
- const continuousColor = $derived(plotState.continuousColorScale)
51
- const categoricalColors = $derived(plotState.colors)
23
+ const geom = new GeomState(plotState, () => ({
24
+ type: 'heatmap',
25
+ channels: { x, y, color: colorField },
26
+ stat,
27
+ options,
28
+ alpha,
29
+ build: buildHeatmapMarks
30
+ }))
31
+ onMount(geom.register)
32
+ onDestroy(geom.destroy)
33
+ $effect(geom.sync)
34
+
35
+ const cells = $derived(geom.marks)
52
36
  const rx = $derived(options.rounded ?? 0)
53
-
54
- const cells = $derived.by(() => {
55
- if (!data?.length || !xScale || !yScale) return []
56
- const bwX = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 0
57
- const bwY = typeof yScale.bandwidth === 'function' ? yScale.bandwidth() : 0
58
-
59
- return data.map((d, i) => {
60
- const xVal = d[x ?? '']
61
- const yVal = d[y ?? '']
62
- const colorVal = colorChannel ? d[colorChannel] : null
63
- let cellFill = '#ccc'
64
-
65
- if (continuousColor) {
66
- cellFill = continuousColor.scale(Number(colorVal))
67
- } else {
68
- const entry = categoricalColors?.get(colorVal)
69
- if (entry) cellFill = entry.fill
70
- }
71
-
72
- return {
73
- key: `${xVal}-${yVal}-${i}`,
74
- x: xScale(xVal) ?? 0,
75
- y: yScale(yVal) ?? 0,
76
- width: bwX,
77
- height: bwY,
78
- fill: cellFill,
79
- data: d
80
- }
81
- })
82
- })
83
37
  </script>
84
38
 
85
39
  {#if cells.length > 0}
@@ -91,13 +45,14 @@
91
45
  width={Math.max(0, cell.width)}
92
46
  height={Math.max(0, cell.height)}
93
47
  fill={cell.fill}
48
+ fill-opacity={cell.alpha}
94
49
  rx={rx}
95
50
  data-plot-element="cell"
96
51
  role="img"
97
52
  onmouseenter={() => plotState.setHovered(cell.data)}
98
53
  onmouseleave={() => plotState.clearHovered()}
99
54
  >
100
- <title>{cell.data[x ?? '']}, {cell.data[y ?? '']}: {colorChannel ? cell.data[colorChannel] : ''}</title>
55
+ <title>{cell.data[x ?? '']}, {cell.data[y ?? '']}: {colorField ? cell.data[colorField] : ''}</title>
101
56
  </rect>
102
57
  {/each}
103
58
  </g>
@@ -3,6 +3,8 @@ type Props = {
3
3
  y?: string;
4
4
  color?: string;
5
5
  fill?: string;
6
+ /** Fixed cell opacity 0–1; defaults to the per-geom preset (heatmap = 1). */
7
+ alpha?: number;
6
8
  stat?: string;
7
9
  options?: {
8
10
  rounded?: number;