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

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 (90) hide show
  1. package/dist/PlotState.svelte.d.ts +31 -3
  2. package/dist/crossfilter/createCrossFilter.svelte.d.ts +13 -15
  3. package/dist/index.d.ts +6 -1
  4. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +3 -5
  5. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +9 -0
  6. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +3 -4
  7. package/dist/lib/brewing/brewer.svelte.d.ts +5 -36
  8. package/dist/lib/brewing/colors.d.ts +10 -1
  9. package/dist/lib/brewing/marks/points.d.ts +17 -2
  10. package/dist/lib/brewing/stats.d.ts +5 -13
  11. package/dist/lib/chart.d.ts +5 -7
  12. package/dist/lib/keyboard-nav.d.ts +15 -0
  13. package/dist/lib/plot/preset.d.ts +1 -1
  14. package/dist/lib/preset.d.ts +30 -0
  15. package/package.json +2 -1
  16. package/src/AnimatedPlot.svelte +375 -206
  17. package/src/Chart.svelte +81 -87
  18. package/src/ChartProvider.svelte +10 -0
  19. package/src/FacetPlot/Panel.svelte +30 -16
  20. package/src/FacetPlot.svelte +100 -76
  21. package/src/Plot/Area.svelte +26 -19
  22. package/src/Plot/Axis.svelte +81 -59
  23. package/src/Plot/Bar.svelte +47 -89
  24. package/src/Plot/Grid.svelte +23 -19
  25. package/src/Plot/Legend.svelte +213 -147
  26. package/src/Plot/Line.svelte +31 -21
  27. package/src/Plot/Point.svelte +35 -22
  28. package/src/Plot/Root.svelte +46 -91
  29. package/src/Plot/Timeline.svelte +82 -82
  30. package/src/Plot/Tooltip.svelte +68 -62
  31. package/src/Plot.svelte +290 -182
  32. package/src/PlotState.svelte.js +339 -267
  33. package/src/Sparkline.svelte +95 -56
  34. package/src/charts/AreaChart.svelte +22 -20
  35. package/src/charts/BarChart.svelte +23 -21
  36. package/src/charts/BoxPlot.svelte +15 -15
  37. package/src/charts/BubbleChart.svelte +17 -17
  38. package/src/charts/LineChart.svelte +20 -20
  39. package/src/charts/PieChart.svelte +30 -20
  40. package/src/charts/ScatterPlot.svelte +20 -19
  41. package/src/charts/ViolinPlot.svelte +15 -15
  42. package/src/crossfilter/CrossFilter.svelte +33 -29
  43. package/src/crossfilter/FilterBar.svelte +17 -25
  44. package/src/crossfilter/FilterHistogram.svelte +290 -0
  45. package/src/crossfilter/FilterSlider.svelte +69 -65
  46. package/src/crossfilter/createCrossFilter.svelte.js +100 -89
  47. package/src/geoms/Arc.svelte +114 -69
  48. package/src/geoms/Area.svelte +67 -39
  49. package/src/geoms/Bar.svelte +184 -126
  50. package/src/geoms/Box.svelte +102 -90
  51. package/src/geoms/LabelPill.svelte +11 -11
  52. package/src/geoms/Line.svelte +110 -87
  53. package/src/geoms/Point.svelte +132 -87
  54. package/src/geoms/Violin.svelte +45 -33
  55. package/src/geoms/lib/areas.js +122 -99
  56. package/src/geoms/lib/bars.js +195 -144
  57. package/src/index.js +21 -14
  58. package/src/lib/brewing/BoxBrewer.svelte.js +8 -50
  59. package/src/lib/brewing/CartesianBrewer.svelte.js +12 -7
  60. package/src/lib/brewing/PieBrewer.svelte.js +5 -5
  61. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  62. package/src/lib/brewing/ViolinBrewer.svelte.js +8 -49
  63. package/src/lib/brewing/brewer.svelte.js +249 -201
  64. package/src/lib/brewing/colors.js +34 -5
  65. package/src/lib/brewing/marks/arcs.js +28 -28
  66. package/src/lib/brewing/marks/areas.js +54 -41
  67. package/src/lib/brewing/marks/bars.js +34 -34
  68. package/src/lib/brewing/marks/boxes.js +51 -51
  69. package/src/lib/brewing/marks/lines.js +37 -30
  70. package/src/lib/brewing/marks/points.js +74 -26
  71. package/src/lib/brewing/marks/violins.js +57 -57
  72. package/src/lib/brewing/patterns.js +25 -11
  73. package/src/lib/brewing/scales.js +20 -20
  74. package/src/lib/brewing/stats.js +40 -28
  75. package/src/lib/brewing/symbols.js +1 -1
  76. package/src/lib/chart.js +12 -4
  77. package/src/lib/keyboard-nav.js +37 -0
  78. package/src/lib/plot/crossfilter.js +5 -5
  79. package/src/lib/plot/facet.js +30 -30
  80. package/src/lib/plot/frames.js +30 -29
  81. package/src/lib/plot/helpers.js +4 -4
  82. package/src/lib/plot/preset.js +48 -34
  83. package/src/lib/plot/scales.js +64 -39
  84. package/src/lib/plot/stat.js +47 -47
  85. package/src/lib/preset.js +41 -0
  86. package/src/patterns/DefinePatterns.svelte +24 -24
  87. package/src/patterns/PatternDef.svelte +1 -1
  88. package/src/patterns/patterns.js +328 -176
  89. package/src/patterns/scale.js +61 -32
  90. package/src/spec/chart-spec.js +64 -21
@@ -0,0 +1,290 @@
1
+ <script>
2
+ // @ts-nocheck
3
+ import { getContext } from 'svelte'
4
+ import { bin, extent, max } from 'd3-array'
5
+ import { scaleLinear } from 'd3-scale'
6
+ import { format } from 'd3-format'
7
+
8
+ /**
9
+ * Interactive histogram with brush selection for crossfilter range dimensions.
10
+ * Renders bins as bars; mouse drag selects a range and calls cf.setRange().
11
+ * Click without dragging clears the filter.
12
+ */
13
+ let {
14
+ data = [],
15
+ field = '',
16
+ label = '',
17
+ bins: binCount = 20,
18
+ width = 280,
19
+ height = 120,
20
+ color = 'rgb(var(--color-primary, 249 115 22))',
21
+ dimColor = 'rgb(var(--color-surface-z3, 203 213 225))'
22
+ } = $props()
23
+
24
+ const cf = getContext('crossfilter')
25
+
26
+ const margin = { top: 8, right: 10, bottom: 22, left: 32 }
27
+
28
+ const innerWidth = $derived(width - margin.left - margin.right)
29
+ const innerHeight = $derived(height - margin.top - margin.bottom)
30
+
31
+ // Compute histogram bins from data
32
+ const binner = $derived(
33
+ bin()
34
+ .value((d) => d[field])
35
+ .thresholds(binCount)
36
+ )
37
+ const binned = $derived(data.length > 0 ? binner(data) : [])
38
+ const domainExtent = $derived(data.length > 0 ? extent(data, (d) => d[field]) : [0, 1])
39
+
40
+ // Scales
41
+ const xScale = $derived(
42
+ scaleLinear()
43
+ .domain(domainExtent)
44
+ .range([0, innerWidth])
45
+ .nice()
46
+ )
47
+ const yScale = $derived(
48
+ binned.length > 0
49
+ ? scaleLinear()
50
+ .domain([0, max(binned, (d) => d.length) ?? 1])
51
+ .range([innerHeight, 0])
52
+ .nice()
53
+ : scaleLinear().domain([0, 1]).range([innerHeight, 0])
54
+ )
55
+
56
+ // Active range from crossfilter
57
+ const activeRange = $derived(cf?.filters?.get(field))
58
+
59
+ // Brush state
60
+ let brushStartPx = $state(null)
61
+ let brushEndPx = $state(null)
62
+ let brushing = $state(false)
63
+
64
+ // Brush rect in pixel space
65
+ const brushRect = $derived.by(() => {
66
+ // Show committed range if not currently brushing
67
+ if (brushStartPx === null && activeRange) {
68
+ const [lo, hi] = activeRange
69
+ return {
70
+ x: xScale(lo),
71
+ width: Math.max(1, xScale(hi) - xScale(lo))
72
+ }
73
+ }
74
+ if (brushStartPx === null) return null
75
+ const lo = Math.min(brushStartPx, brushEndPx ?? brushStartPx)
76
+ const hi = Math.max(brushStartPx, brushEndPx ?? brushStartPx)
77
+ return { x: lo, width: Math.max(1, hi - lo) }
78
+ })
79
+
80
+ function pixelToValue(px) {
81
+ return xScale.invert(Math.max(0, Math.min(innerWidth, px)))
82
+ }
83
+
84
+ function getLocalX(e) {
85
+ const svgEl = e.currentTarget.closest('svg')
86
+ const rect = svgEl.getBoundingClientRect()
87
+ return e.clientX - rect.left - margin.left
88
+ }
89
+
90
+ function onMouseDown(e) {
91
+ brushStartPx = getLocalX(e)
92
+ brushEndPx = brushStartPx
93
+ brushing = true
94
+ }
95
+
96
+ function onMouseMove(e) {
97
+ if (!brushing) return
98
+ brushEndPx = getLocalX(e)
99
+ }
100
+
101
+ function onMouseUp() {
102
+ if (!brushing) return
103
+ brushing = false
104
+ const lo = Math.min(brushStartPx, brushEndPx)
105
+ const hi = Math.max(brushStartPx, brushEndPx)
106
+ if (hi - lo < 3) {
107
+ // Treat as click — clear filter
108
+ cf?.clearFilter(field)
109
+ brushStartPx = null
110
+ brushEndPx = null
111
+ return
112
+ }
113
+ const loVal = pixelToValue(lo)
114
+ const hiVal = pixelToValue(hi)
115
+ cf?.setRange(field, [loVal, hiVal])
116
+ brushStartPx = null
117
+ brushEndPx = null
118
+ }
119
+
120
+ // Check if a bin is within active range
121
+ function binInRange(b) {
122
+ if (!activeRange) return true
123
+ const [lo, hi] = activeRange
124
+ return b.x1 > lo && b.x0 < hi
125
+ }
126
+
127
+ // Y-axis tick values (2-3 ticks)
128
+ const yTicks = $derived(yScale.ticks(3))
129
+ // X-axis tick values (4 ticks)
130
+ const xTicks = $derived(xScale.ticks(4))
131
+
132
+ const fmt = format('.2~s')
133
+ </script>
134
+
135
+ <div data-filter-histogram data-filter-field={field} style="width:{width}px">
136
+ {#if label}
137
+ <div data-filter-histogram-label>{label}</div>
138
+ {/if}
139
+ <svg
140
+ {width}
141
+ {height}
142
+ style="display:block;cursor:crosshair;user-select:none"
143
+ role="img"
144
+ aria-label="Histogram filter for {label || field}"
145
+ onmousedown={onMouseDown}
146
+ onmousemove={onMouseMove}
147
+ onmouseup={onMouseUp}
148
+ onmouseleave={onMouseUp}
149
+ >
150
+ <g transform="translate({margin.left},{margin.top})">
151
+ <!-- Y axis ticks -->
152
+ {#each yTicks as tick}
153
+ <line
154
+ x1={0}
155
+ x2={innerWidth}
156
+ y1={yScale(tick)}
157
+ y2={yScale(tick)}
158
+ stroke="currentColor"
159
+ stroke-opacity="0.1"
160
+ stroke-width="1"
161
+ />
162
+ <text
163
+ x={-4}
164
+ y={yScale(tick)}
165
+ text-anchor="end"
166
+ dominant-baseline="middle"
167
+ font-size="9"
168
+ fill="currentColor"
169
+ opacity="0.5">{tick}</text
170
+ >
171
+ {/each}
172
+
173
+ <!-- X axis line -->
174
+ <line
175
+ x1={0}
176
+ x2={innerWidth}
177
+ y1={innerHeight}
178
+ y2={innerHeight}
179
+ stroke="currentColor"
180
+ stroke-opacity="0.2"
181
+ />
182
+
183
+ <!-- X axis ticks -->
184
+ {#each xTicks as tick}
185
+ <text
186
+ x={xScale(tick)}
187
+ y={innerHeight + 12}
188
+ text-anchor="middle"
189
+ font-size="9"
190
+ fill="currentColor"
191
+ opacity="0.5">{fmt(tick)}</text
192
+ >
193
+ {/each}
194
+
195
+ <!-- Bars -->
196
+ {#each binned as b}
197
+ {@const bx = xScale(b.x0) + 1}
198
+ {@const bw = Math.max(0, xScale(b.x1) - xScale(b.x0) - 1)}
199
+ {@const by = yScale(b.length)}
200
+ {@const bh = innerHeight - by}
201
+ <rect
202
+ x={bx}
203
+ y={by}
204
+ width={bw}
205
+ height={bh}
206
+ fill={activeRange ? (binInRange(b) ? color : dimColor) : color}
207
+ opacity="0.85"
208
+ data-filter-histogram-bar
209
+ />
210
+ {/each}
211
+
212
+ <!-- Brush overlay rect -->
213
+ {#if brushRect}
214
+ <rect
215
+ x={brushRect.x}
216
+ y={0}
217
+ width={brushRect.width}
218
+ height={innerHeight}
219
+ fill={color}
220
+ opacity="0.15"
221
+ pointer-events="none"
222
+ />
223
+ <rect
224
+ x={brushRect.x}
225
+ y={0}
226
+ width={1}
227
+ height={innerHeight}
228
+ fill={color}
229
+ opacity="0.6"
230
+ pointer-events="none"
231
+ />
232
+ <rect
233
+ x={brushRect.x + brushRect.width - 1}
234
+ y={0}
235
+ width={1}
236
+ height={innerHeight}
237
+ fill={color}
238
+ opacity="0.6"
239
+ pointer-events="none"
240
+ />
241
+ {/if}
242
+ </g>
243
+ </svg>
244
+ {#if activeRange}
245
+ <div data-filter-histogram-range>
246
+ {fmt(activeRange[0])} – {fmt(activeRange[1])}
247
+ <button
248
+ data-filter-histogram-clear
249
+ onclick={() => cf?.clearFilter(field)}
250
+ aria-label="Clear {label || field} filter"
251
+ >✕</button>
252
+ </div>
253
+ {/if}
254
+ </div>
255
+
256
+ <style>
257
+ [data-filter-histogram] {
258
+ display: flex;
259
+ flex-direction: column;
260
+ gap: 4px;
261
+ font-size: 12px;
262
+ color: rgb(var(--color-surface-z6, 100 116 139));
263
+ }
264
+ [data-filter-histogram-label] {
265
+ font-weight: 600;
266
+ font-size: 11px;
267
+ text-transform: uppercase;
268
+ letter-spacing: 0.05em;
269
+ opacity: 0.7;
270
+ }
271
+ [data-filter-histogram-range] {
272
+ display: flex;
273
+ align-items: center;
274
+ gap: 6px;
275
+ font-size: 11px;
276
+ opacity: 0.8;
277
+ }
278
+ [data-filter-histogram-clear] {
279
+ background: none;
280
+ border: none;
281
+ cursor: pointer;
282
+ padding: 0 2px;
283
+ font-size: 10px;
284
+ opacity: 0.6;
285
+ line-height: 1;
286
+ }
287
+ [data-filter-histogram-clear]:hover {
288
+ opacity: 1;
289
+ }
290
+ </style>
@@ -1,79 +1,83 @@
1
1
  <script>
2
- import { getContext } from 'svelte'
2
+ import { getContext } from 'svelte'
3
3
 
4
- /**
5
- * Dual range slider for a continuous crossfilter dimension.
6
- * NOTE: Interim implementation using HTML range inputs.
7
- * The spec calls for a Plot+Point+brush architecture, deferred until brush geom is implemented.
8
- */
9
- let { field, min, max, step = 0.1, label = '' } = $props()
4
+ /**
5
+ * Dual range slider for a continuous crossfilter dimension.
6
+ * NOTE: Interim implementation using HTML range inputs.
7
+ * The spec calls for a Plot+Point+brush architecture, deferred until brush geom is implemented.
8
+ */
9
+ let { field, min, max, step = 0.1, label = '' } = $props()
10
10
 
11
- const cf = getContext('crossfilter')
11
+ const cf = getContext('crossfilter')
12
12
 
13
- // Initialize from props; $effect keeps in sync when min/max change
14
- let low = $state(0)
15
- let high = $state(100)
13
+ // Initialize from props; $effect keeps in sync when min/max change
14
+ let low = $state(0)
15
+ let high = $state(100)
16
16
 
17
- $effect(() => {
18
- low = min ?? 0
19
- high = max ?? 100
20
- })
17
+ $effect(() => {
18
+ low = min ?? 0
19
+ high = max ?? 100
20
+ })
21
21
 
22
- function handleLow(e) {
23
- low = Math.min(Number(e.currentTarget.value), high)
24
- cf?.setRange(field, [low, high])
25
- }
22
+ function handleLow(e) {
23
+ low = Math.min(Number(e.currentTarget.value), high)
24
+ cf?.setRange(field, [low, high])
25
+ }
26
26
 
27
- function handleHigh(e) {
28
- high = Math.max(Number(e.currentTarget.value), low)
29
- cf?.setRange(field, [low, high])
30
- }
27
+ function handleHigh(e) {
28
+ high = Math.max(Number(e.currentTarget.value), low)
29
+ cf?.setRange(field, [low, high])
30
+ }
31
31
  </script>
32
32
 
33
33
  <div data-filter-slider data-filter-field={field}>
34
- {#if label}
35
- <span data-filter-slider-label>{label}</span>
36
- {/if}
37
- <div data-filter-slider-inputs>
38
- <input
39
- type="range"
40
- {min} {max} {step}
41
- value={low}
42
- oninput={handleLow}
43
- aria-label="Minimum {label || field}"
44
- data-filter-slider-low
45
- />
46
- <input
47
- type="range"
48
- {min} {max} {step}
49
- value={high}
50
- oninput={handleHigh}
51
- aria-label="Maximum {label || field}"
52
- data-filter-slider-high
53
- />
54
- </div>
55
- <div data-filter-slider-display>
56
- {low} – {high}
57
- </div>
34
+ {#if label}
35
+ <span data-filter-slider-label>{label}</span>
36
+ {/if}
37
+ <div data-filter-slider-inputs>
38
+ <input
39
+ type="range"
40
+ {min}
41
+ {max}
42
+ {step}
43
+ value={low}
44
+ oninput={handleLow}
45
+ aria-label="Minimum {label || field}"
46
+ data-filter-slider-low
47
+ />
48
+ <input
49
+ type="range"
50
+ {min}
51
+ {max}
52
+ {step}
53
+ value={high}
54
+ oninput={handleHigh}
55
+ aria-label="Maximum {label || field}"
56
+ data-filter-slider-high
57
+ />
58
+ </div>
59
+ <div data-filter-slider-display>
60
+ {low} – {high}
61
+ </div>
58
62
  </div>
59
63
 
60
64
  <style>
61
- [data-filter-slider] {
62
- display: flex;
63
- flex-direction: column;
64
- gap: 4px;
65
- font-size: 12px;
66
- }
67
- [data-filter-slider-inputs] {
68
- display: flex;
69
- flex-direction: column;
70
- gap: 2px;
71
- }
72
- [data-filter-slider-label] {
73
- font-weight: 600;
74
- }
75
- [data-filter-slider-display] {
76
- color: currentColor;
77
- opacity: 0.7;
78
- }
65
+ [data-filter-slider] {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 4px;
69
+ font-size: 12px;
70
+ }
71
+ [data-filter-slider-inputs] {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 2px;
75
+ }
76
+ [data-filter-slider-label] {
77
+ font-weight: 600;
78
+ }
79
+ [data-filter-slider-display] {
80
+ color: currentColor;
81
+ opacity: 0.7;
82
+ }
79
83
  </style>
@@ -1,3 +1,5 @@
1
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity'
2
+
1
3
  /**
2
4
  * Creates a reactive cross-filter state object.
3
5
  *
@@ -12,102 +14,111 @@
12
14
  *
13
15
  * @returns {CrossFilter}
14
16
  */
17
+
18
+ function toggleCategoricalInMap(filters, dimension, value) {
19
+ const existing = filters.get(dimension)
20
+ const set = existing instanceof Set ? new SvelteSet(existing) : new SvelteSet()
21
+ if (set.has(value)) {
22
+ set.delete(value)
23
+ } else {
24
+ set.add(value)
25
+ }
26
+ if (set.size === 0) {
27
+ filters.delete(dimension)
28
+ } else {
29
+ filters.set(dimension, set)
30
+ }
31
+ }
32
+
15
33
  export function createCrossFilter() {
16
- // Map<dimension, Set<unknown> | [number, number]>
17
- const filters = $state(new Map())
34
+ // Map<dimension, Set<unknown> | [number, number]>
35
+ const filters = new SvelteMap()
18
36
 
19
- // Simple counter incremented on every mutation. Components read cf.version
20
- // inside $effect to reactively recompute when any filter changes.
21
- let version = $state(0)
37
+ // Simple counter incremented on every mutation. Components read cf.version
38
+ // inside $effect to reactively recompute when any filter changes.
39
+ let version = $state(0)
22
40
 
23
- /**
24
- * Returns true if any filter is active on this dimension.
25
- * @param {string} dimension
26
- */
27
- function isFiltered(dimension) {
28
- const f = filters.get(dimension)
29
- if (!f) return false
30
- if (f instanceof Set) return f.size > 0
31
- return true // range: always active if present
32
- }
41
+ /**
42
+ * Returns true if any filter is active on this dimension.
43
+ * @param {string} dimension
44
+ */
45
+ function isFiltered(dimension) {
46
+ const f = filters.get(dimension)
47
+ if (!f) return false
48
+ if (f instanceof Set) return f.size > 0
49
+ return true // range: always active if present
50
+ }
33
51
 
34
- /**
35
- * Returns true if a value on this dimension is NOT in the active filter.
36
- * Returns false when no filter is active on this dimension.
37
- *
38
- * @param {string} dimension
39
- * @param {unknown} value
40
- */
41
- function isDimmed(dimension, value) {
42
- const f = filters.get(dimension)
43
- if (!f) return false
44
- if (f instanceof Set) {
45
- return !f.has(value)
46
- }
47
- // [min, max] range
48
- const [lo, hi] = f
49
- return Number(value) < lo || Number(value) > hi
50
- }
52
+ /**
53
+ * Returns true if a value on this dimension is NOT in the active filter.
54
+ * Returns false when no filter is active on this dimension.
55
+ *
56
+ * @param {string} dimension
57
+ * @param {unknown} value
58
+ */
59
+ function isDimmed(dimension, value) {
60
+ const f = filters.get(dimension)
61
+ if (!f) return false
62
+ if (f instanceof Set) {
63
+ return !f.has(value)
64
+ }
65
+ // [min, max] range
66
+ const [lo, hi] = f
67
+ return Number(value) < lo || Number(value) > hi
68
+ }
51
69
 
52
- /**
53
- * Toggles a categorical value for a dimension.
54
- * Adds when absent, removes when present.
55
- * Clears the dimension filter when the last value is removed.
56
- *
57
- * @param {string} dimension
58
- * @param {unknown} value
59
- */
60
- function toggleCategorical(dimension, value) {
61
- const existing = filters.get(dimension)
62
- const set = existing instanceof Set ? new Set(existing) : new Set()
63
- if (set.has(value)) {
64
- set.delete(value)
65
- } else {
66
- set.add(value)
67
- }
68
- if (set.size === 0) {
69
- filters.delete(dimension)
70
- } else {
71
- filters.set(dimension, set)
72
- }
73
- version++
74
- }
70
+ /**
71
+ * Toggles a categorical value for a dimension.
72
+ * Adds when absent, removes when present.
73
+ * Clears the dimension filter when the last value is removed.
74
+ *
75
+ * @param {string} dimension
76
+ * @param {unknown} value
77
+ */
78
+ function toggleCategorical(dimension, value) {
79
+ toggleCategoricalInMap(filters, dimension, value)
80
+ version++
81
+ }
75
82
 
76
- /**
77
- * Sets a continuous range filter for a dimension.
78
- * @param {string} dimension
79
- * @param {[number, number]} range
80
- */
81
- function setRange(dimension, range) {
82
- filters.set(dimension, [range[0], range[1]])
83
- version++
84
- }
83
+ /**
84
+ * Sets a continuous range filter for a dimension.
85
+ * @param {string} dimension
86
+ * @param {[number, number]} range
87
+ */
88
+ function setRange(dimension, range) {
89
+ filters.set(dimension, [range[0], range[1]])
90
+ version++
91
+ }
85
92
 
86
- /**
87
- * Clears the filter for a single dimension.
88
- * @param {string} dimension
89
- */
90
- function clearFilter(dimension) {
91
- filters.delete(dimension)
92
- version++
93
- }
93
+ /**
94
+ * Clears the filter for a single dimension.
95
+ * @param {string} dimension
96
+ */
97
+ function clearFilter(dimension) {
98
+ filters.delete(dimension)
99
+ version++
100
+ }
94
101
 
95
- /** Clears all active filters. */
96
- function clearAll() {
97
- filters.clear()
98
- version++
99
- }
102
+ /** Clears all active filters. */
103
+ function clearAll() {
104
+ filters.clear()
105
+ version++
106
+ }
100
107
 
101
- return {
102
- /** @readonly — reactive Map of current filter state */
103
- get filters() { return filters },
104
- /** @readonly — increments on every mutation; read inside $effect to react to any filter change */
105
- get version() { return version },
106
- isFiltered,
107
- isDimmed,
108
- toggleCategorical,
109
- setRange,
110
- clearFilter,
111
- clearAll
112
- }
108
+ return {
109
+ /** @readonly — reactive Map of current filter state */
110
+ get filters() {
111
+ return filters
112
+ },
113
+ /** @readonly — increments on every mutation; read inside $effect to react to any filter change */
114
+ get version() {
115
+ return version
116
+ },
117
+ isFiltered,
118
+ isDimmed,
119
+ toggleCategorical,
120
+ setRange,
121
+ clearFilter,
122
+ clearAll
123
+ }
113
124
  }