@rokkit/chart 1.7.0 → 1.8.1

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 (74) hide show
  1. package/README.md +13 -0
  2. package/dist/Plot/DataTable.svelte +54 -0
  3. package/dist/Plot/DataTable.svelte.d.ts +12 -0
  4. package/dist/Plot/SpecGeoms.svelte +78 -0
  5. package/dist/Plot/SpecGeoms.svelte.d.ts +11 -0
  6. package/dist/Plot.svelte +47 -151
  7. package/dist/Plot.svelte.d.ts +5 -0
  8. package/dist/PlotState.svelte.d.ts +115 -71
  9. package/dist/PlotState.svelte.js +174 -547
  10. package/dist/SparkState.svelte.d.ts +3 -1
  11. package/dist/SparkState.svelte.js +11 -41
  12. package/dist/geoms/Arc.svelte +1 -1
  13. package/dist/geoms/Bar.svelte +23 -32
  14. package/dist/geoms/Contour.svelte +88 -0
  15. package/dist/geoms/Contour.svelte.d.ts +19 -0
  16. package/dist/geoms/Hull.svelte +89 -0
  17. package/dist/geoms/Hull.svelte.d.ts +17 -0
  18. package/dist/geoms/Region.svelte +107 -0
  19. package/dist/geoms/Region.svelte.d.ts +27 -0
  20. package/dist/geoms/Rule.svelte +88 -26
  21. package/dist/geoms/Rule.svelte.d.ts +8 -0
  22. package/dist/geoms/lib/PlotAreaClip.svelte +18 -0
  23. package/dist/geoms/lib/PlotAreaClip.svelte.d.ts +6 -0
  24. package/dist/geoms/lib/bars.js +13 -20
  25. package/dist/geoms/lib/grouping.d.ts +35 -0
  26. package/dist/geoms/lib/grouping.js +59 -0
  27. package/dist/geoms/lib/marks/arc.d.ts +1 -0
  28. package/dist/geoms/lib/marks/contour.d.ts +26 -0
  29. package/dist/geoms/lib/marks/contour.js +67 -0
  30. package/dist/geoms/lib/marks/hull.d.ts +26 -0
  31. package/dist/geoms/lib/marks/hull.js +36 -0
  32. package/dist/geoms/lib/marks/waterfall.js +15 -20
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.js +9 -0
  35. package/dist/lib/abline.d.ts +41 -0
  36. package/dist/lib/abline.js +51 -0
  37. package/dist/lib/brewing/marks/arcs.d.ts +6 -1
  38. package/dist/lib/brewing/marks/arcs.js +9 -1
  39. package/dist/lib/brewing/polar.js +8 -3
  40. package/dist/lib/hull.d.ts +18 -0
  41. package/dist/lib/hull.js +64 -0
  42. package/dist/lib/plot/domains.d.ts +61 -0
  43. package/dist/lib/plot/domains.js +130 -0
  44. package/dist/lib/plot/running.d.ts +23 -0
  45. package/dist/lib/plot/running.js +32 -0
  46. package/dist/lib/plot/spec.d.ts +59 -0
  47. package/dist/lib/plot/spec.js +131 -0
  48. package/dist/lib/plot/stacking.d.ts +32 -0
  49. package/dist/lib/plot/stacking.js +34 -0
  50. package/dist/lib/plot/types.d.ts +8 -1
  51. package/dist/lib/plot/types.js +5 -1
  52. package/dist/lib/preset.d.ts +2 -0
  53. package/dist/lib/preset.js +76 -41
  54. package/dist/lib/region.d.ts +63 -0
  55. package/dist/lib/region.js +127 -0
  56. package/dist/state/AestheticState.svelte.d.ts +43 -0
  57. package/dist/state/AestheticState.svelte.js +84 -0
  58. package/dist/state/AxisState.svelte.d.ts +26 -0
  59. package/dist/state/AxisState.svelte.js +56 -0
  60. package/dist/state/ChannelState.svelte.d.ts +34 -0
  61. package/dist/state/ChannelState.svelte.js +67 -0
  62. package/dist/state/GeomRegistry.svelte.d.ts +44 -0
  63. package/dist/state/GeomRegistry.svelte.js +86 -0
  64. package/dist/state/InteractionState.svelte.d.ts +30 -0
  65. package/dist/state/InteractionState.svelte.js +69 -0
  66. package/dist/state/OrientationState.svelte.d.ts +34 -0
  67. package/dist/state/OrientationState.svelte.js +64 -0
  68. package/dist/state/PlotConfig.svelte.d.ts +179 -0
  69. package/dist/state/PlotConfig.svelte.js +195 -0
  70. package/dist/state/PlotFrame.svelte.d.ts +17 -0
  71. package/dist/state/PlotFrame.svelte.js +18 -0
  72. package/dist/state/ScaleState.svelte.d.ts +36 -0
  73. package/dist/state/ScaleState.svelte.js +121 -0
  74. package/package.json +5 -3
@@ -111,6 +111,8 @@ export class SparkState {
111
111
  ribbon: number;
112
112
  waterfall: number;
113
113
  rule: number;
114
+ hull: number;
115
+ contour: number;
114
116
  radar: number;
115
117
  };
116
118
  patterns: string[];
@@ -133,6 +135,6 @@ export class SparkState {
133
135
  registerGeom(config: any): string;
134
136
  updateGeom(id: any, config: any): void;
135
137
  unregisterGeom(id: any): void;
136
- geomData(id: any): any[];
138
+ geomData(id: any): Object[];
137
139
  #private;
138
140
  }
@@ -1,12 +1,9 @@
1
- import { untrack } from 'svelte'
2
1
  import { buildUnifiedXScale, buildUnifiedYScale } from './lib/plot/scales.js'
3
2
  import { defaultPreset } from './lib/preset.js'
4
- import { applyGeomStat } from './lib/plot/stat.js'
3
+ import { GeomRegistry } from './state/GeomRegistry.svelte.js'
5
4
  import { distinct, assignColors } from './lib/brewing/colors.js'
6
5
  import { PATTERNS } from './patterns/patterns.js'
7
6
 
8
- let nextGeomId = 0
9
-
10
7
  /**
11
8
  * The full set of members a geom reads off its `'plot-state'` context — derived by
12
9
  * scanning every geom for `plotState.*` / `plot.*` / `state.*` reads (`geoms/*.svelte`,
@@ -117,7 +114,6 @@ export class SparkState {
117
114
  #max = $state(undefined)
118
115
  #baseline = $state(undefined)
119
116
  #pattern = $state(undefined)
120
- #geoms = $state([])
121
117
 
122
118
  // Mirrors Sparkline's current yMin/yMax logic: an explicit min/max wins; otherwise
123
119
  // the domain is the raw data extent, widened to include the baseline when one is
@@ -167,7 +163,12 @@ export class SparkState {
167
163
  return assignColors(values, 'light', defaultPreset)
168
164
  })
169
165
 
166
+ #registry
167
+
170
168
  constructor(config = {}) {
169
+ // The registry reads rows and container channels live, through this state's own
170
+ // `data` / `channels` getters.
171
+ this.#registry = new GeomRegistry(this, 'spark-geom')
171
172
  this.update(config)
172
173
  }
173
174
 
@@ -284,48 +285,17 @@ export class SparkState {
284
285
  clearHovered() {}
285
286
  handleSelect() {}
286
287
 
287
- // Mirrors PlotState.registerGeom at smaller scope: GeomState.register() (called
288
- // from a geom's onMount) calls this once per mounted geom and keeps the returned id.
288
+ // The same registry PlotState composes — one lifecycle, so a fix to it lands in both.
289
289
  registerGeom(config) {
290
- const id = `spark-geom-${nextGeomId++}`
291
- this.#geoms = [...this.#geoms, { id, ...config }]
292
- return id
290
+ return this.#registry.register(config)
293
291
  }
294
-
295
- // untrack the read of #geoms, same as PlotState: GeomState.sync() calls this from
296
- // a geom's `$effect`, which would otherwise track #geoms as a dependency and retrigger
297
- // itself on every update (effect_update_depth_exceeded).
298
292
  updateGeom(id, config) {
299
- this.#geoms = untrack(() => this.#geoms).map((g) => (g.id === id ? { ...g, ...config } : g))
293
+ this.#registry.update(id, config)
300
294
  }
301
-
302
- // No untrack needed here (unlike updateGeom above): this only ever runs from a geom's
303
- // onDestroy, which Svelte always executes outside any reactive tracking scope — there's
304
- // no enclosing effect for a #geoms read to register against, so no self-retrigger risk
305
- // exists. Mirrors PlotState.unregisterGeom, which is untrack-free for the same reason.
306
295
  unregisterGeom(id) {
307
- this.#geoms = this.#geoms.filter((g) => g.id !== id)
296
+ this.#registry.unregister(id)
308
297
  }
309
-
310
- // Identity stat returns `this.#data` itself, not a copy — geoms look up rows via
311
- // `plotState.data.indexOf(row)` (see e.g. Line.svelte's selectPoint), which only
312
- // works if geomData's rows are the exact same object instances as `data`.
313
298
  geomData(id) {
314
- const geom = this.#geoms.find((g) => g.id === id)
315
- if (!geom) return []
316
- const stat = geom.stat ?? 'identity'
317
- if (stat === 'identity') return this.#data
318
- // Strip explicit `undefined` values before spreading: geom components always pass
319
- // every channel key (e.g. Line.svelte's `channels: { x, y, color, fill, symbol }`),
320
- // so a geom that omits x/y to inherit them from the container sends
321
- // `{ x: undefined, y: undefined }`, not `{}`. Left unstripped, that `undefined`
322
- // becomes an own property that overrides the container's value in the spread below,
323
- // silently clobbering an inherited channel (applyGeomStat's primary-key lookup then
324
- // finds nothing and falls back to identity with no warning).
325
- const geomChannels = Object.fromEntries(
326
- Object.entries(geom.channels ?? {}).filter(([, v]) => v !== undefined)
327
- )
328
- const mergedChannels = { ...this.#channels, ...geomChannels }
329
- return applyGeomStat(this.#data, { stat, channels: mergedChannels })
299
+ return this.#registry.data(id)
330
300
  }
331
301
  }
@@ -55,7 +55,7 @@
55
55
  {#if arcs.length > 0}
56
56
  <g data-plot-geom="arc" transform="translate({w / 2}, {h / 2})">
57
57
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
58
- {#each arcs as arc (arc.key)}
58
+ {#each arcs as arc (arc.id)}
59
59
  <path
60
60
  d={arc.d}
61
61
  fill={arc.fill}
@@ -91,26 +91,22 @@
91
91
  const effectiveOrientation = $derived(options.orientation ?? plotState.orientation)
92
92
  const seriesField = $derived(fill ?? color)
93
93
 
94
- let dimmedByKey = $state<Record<string, boolean>>({})
95
-
96
- $effect(() => {
97
- if (!cf) {
98
- dimmedByKey = {}
99
- return
100
- }
101
- // cf.version is a $state counter that increments on every filter mutation.
102
- // Reading it here establishes a reactive dependency so the effect re-runs
103
- // whenever any filter changes — including changes from sibling FilterBars.
94
+ /** Bars the crossfilter dims, by key. A derivation, so it needs no effect writing state. */
95
+ const dimmedByKey = $derived.by((): Record<string, boolean> => {
96
+ if (!cf) return {}
97
+ // cf.version is a $state counter that increments on every filter mutation. Reading it
98
+ // makes this re-derive whenever any filter changes — including a sibling FilterBar's.
104
99
  void cf.version
105
- const next: Record<string, boolean> = {}
100
+ const dimmed: Record<string, boolean> = {}
106
101
  for (const bar of bars) {
107
- const dimmedByX = x ? cf.isDimmed(x, bar.data[x]) : false
108
- const dimmedByY = y ? cf.isDimmed(y, bar.data[y]) : false
109
- next[bar.key] = dimmedByX || dimmedByY
102
+ dimmed[bar.key] = (x ? cf.isDimmed(x, bar.data[x]) : false) || (y ? cf.isDimmed(y, bar.data[y]) : false)
110
103
  }
111
- dimmedByKey = next
104
+ return dimmed
112
105
  })
113
106
 
107
+ /** Whether a bar takes clicks and keys at all. */
108
+ const activatable = $derived(filterable || Boolean(onselect) || keyboard || plotState.interactive)
109
+
114
110
  function handleBarClick(barX: unknown) {
115
111
  if (!filterable || !x || !cf) return
116
112
  cf.toggleCategorical(x, barX)
@@ -130,20 +126,19 @@
130
126
  )
131
127
  }
132
128
 
133
- function handleBarActivate(bar: { data: Row }, event: MouseEvent) {
129
+ /**
130
+ * A bar activated by click, Enter or Space. A filterable bar FILTERS — it does not also emit
131
+ * `onselect`, the rule the keyboard path always had and a click did not; otherwise it
132
+ * selects. The plot-level selection (`selectBar`) runs either way.
133
+ */
134
+ function activate(bar: { data: Row }, event: MouseEvent | KeyboardEvent) {
134
135
  if (filterable && x) handleBarClick(bar.data[x])
135
- onselect?.(bar.data)
136
+ else onselect?.(bar.data)
136
137
  selectBar(bar, event)
137
138
  }
138
139
 
139
140
  function handleBarKeyActivate(bar: { data: Row }, event: KeyboardEvent) {
140
- if (event.key !== 'Enter' && event.key !== ' ') return
141
- if (filterable && x) {
142
- handleBarClick(bar.data[x])
143
- } else {
144
- onselect?.(bar.data)
145
- }
146
- selectBar(bar, event)
141
+ if (event.key === 'Enter' || event.key === ' ') activate(bar, event)
147
142
  }
148
143
  </script>
149
144
 
@@ -165,14 +160,10 @@
165
160
  data-plot-category={bar.data[x ?? '']}
166
161
  data-dimmed={dimmedByKey[bar.key] ? true : undefined}
167
162
  style:cursor={filterable || onselect || plotState.interactive ? 'pointer' : undefined}
168
- onclick={filterable || onselect || keyboard || plotState.interactive
169
- ? (e) => handleBarActivate(bar, e)
170
- : undefined}
171
- onkeydown={filterable || onselect || keyboard || plotState.interactive
172
- ? (e) => handleBarKeyActivate(bar, e)
173
- : undefined}
174
- role={filterable || onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
175
- tabindex={filterable || onselect || keyboard || plotState.interactive ? 0 : undefined}
163
+ onclick={activatable ? (e) => activate(bar, e) : undefined}
164
+ onkeydown={activatable ? (e) => handleBarKeyActivate(bar, e) : undefined}
165
+ role={activatable ? 'button' : 'graphics-symbol'}
166
+ tabindex={activatable ? 0 : undefined}
176
167
  use:keyboardNav={keyboard}
177
168
  aria-label="{bar.data[x ?? '']}: {bar.data[y ?? '']}"
178
169
  onmouseenter={() => plotState.setHovered(bar.data)}
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Kernel-density contours — where points pile up, drawn as nested rings (or filled bands).
4
+ *
5
+ * A scatter of a few hundred modules hides its density: on a main-sequence plot most of
6
+ * them sit on the A = 0 edge on top of each other. Contours show the pile-up without
7
+ * binning it into cells the way `Hexbin` does, and they overlay the points instead of
8
+ * replacing them. Grouped by `fill` (else `color`), each group contours separately.
9
+ */
10
+ import { getContext, onMount, onDestroy } from 'svelte'
11
+ import type { PlotState } from '../PlotState.svelte.js'
12
+ import { GeomState } from './lib/GeomState.svelte.js'
13
+ import { buildContourMarks } from './lib/marks/contour.js'
14
+ import PlotAreaClip from './lib/PlotAreaClip.svelte'
15
+
16
+ type Props = {
17
+ x?: string
18
+ y?: string
19
+ /** Group field (and its colour). Wins over `color`. */
20
+ fill?: string
21
+ /** Group field (and its colour) when `fill` is not set. */
22
+ color?: string
23
+ /** Kernel bandwidth in pixels — larger is smoother. */
24
+ bandwidth?: number
25
+ /** Approximate number of density levels. */
26
+ thresholds?: number
27
+ /** Fill the bands instead of stroking the rings. */
28
+ filled?: boolean
29
+ /** Opacity of the densest band when filled, or of every ring when stroked. */
30
+ alpha?: number
31
+ }
32
+
33
+ let { x, y, fill, color, bandwidth = 20, thresholds = 8, filled = false, alpha }: Props = $props()
34
+
35
+ const plotState = getContext<PlotState>('plot-state')
36
+ const uid = $props.id()
37
+ const clipId = `plot-contour-clip-${uid}`
38
+ const geom = new GeomState(plotState, () => ({
39
+ type: 'contour',
40
+ channels: { x, y, fill, color },
41
+ options: { bandwidth, thresholds },
42
+ alpha,
43
+ build: buildContourMarks
44
+ }))
45
+ onMount(geom.register)
46
+ onDestroy(geom.destroy)
47
+ $effect(geom.sync)
48
+
49
+ const rings = $derived(geom.marks)
50
+ const grouped = $derived(Boolean(fill ?? color))
51
+ </script>
52
+
53
+ {#if rings.length > 0}
54
+ <PlotAreaClip id={clipId} />
55
+ <g
56
+ data-plot-geom="contour"
57
+ data-plot-contour-mode={filled ? 'filled' : 'lines'}
58
+ clip-path="url(#{clipId})"
59
+ >
60
+ {#each rings as r (r.key)}
61
+ <path
62
+ d={r.d}
63
+ fill={filled ? r.fill : 'none'}
64
+ fill-opacity={filled ? r.fillAlpha : undefined}
65
+ stroke={filled ? 'none' : grouped ? r.stroke : undefined}
66
+ stroke-opacity={!filled && alpha !== undefined ? alpha : undefined}
67
+ data-plot-element="contour"
68
+ data-plot-contour={r.group}
69
+ data-plot-contour-level={r.level}
70
+ />
71
+ {/each}
72
+ </g>
73
+ {/if}
74
+
75
+ <style>
76
+ [data-plot-geom='contour'] {
77
+ pointer-events: none;
78
+ }
79
+ [data-plot-contour-mode='lines'] [data-plot-element='contour'] {
80
+ stroke-width: var(--chart-contour-width, 1);
81
+ }
82
+ [data-plot-contour-mode='lines'] [data-plot-element='contour']:not([stroke-opacity]) {
83
+ stroke-opacity: var(--chart-contour-opacity, 0.7);
84
+ }
85
+ [data-plot-contour-mode='lines'] [data-plot-element='contour']:not([stroke]) {
86
+ stroke: var(--chart-contour-color, currentColor);
87
+ }
88
+ </style>
@@ -0,0 +1,19 @@
1
+ type Props = {
2
+ x?: string;
3
+ y?: string;
4
+ /** Group field (and its colour). Wins over `color`. */
5
+ fill?: string;
6
+ /** Group field (and its colour) when `fill` is not set. */
7
+ color?: string;
8
+ /** Kernel bandwidth in pixels — larger is smoother. */
9
+ bandwidth?: number;
10
+ /** Approximate number of density levels. */
11
+ thresholds?: number;
12
+ /** Fill the bands instead of stroking the rings. */
13
+ filled?: boolean;
14
+ /** Opacity of the densest band when filled, or of every ring when stroked. */
15
+ alpha?: number;
16
+ };
17
+ declare const Contour: import("svelte").Component<Props, {}, "">;
18
+ type Contour = ReturnType<typeof Contour>;
19
+ export default Contour;
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ /**
3
+ * An outline around each group of points — the convex hull of the `fill` (else `color`)
4
+ * field's rows, padded so the outline clears the points it encloses.
5
+ *
6
+ * Answers "how spread out is this group": a package whose modules span the zone of pain to
7
+ * the zone of uselessness has a hull the size of the chart. The group field joins the
8
+ * shared palette, so a hull and the points it encloses take the same colour — or, with
9
+ * different fields, hulls group by package while points colour by community.
10
+ *
11
+ * Place it BEFORE the points so the fill sits behind them.
12
+ */
13
+ import { getContext, onMount, onDestroy } from 'svelte'
14
+ import type { PlotState } from '../PlotState.svelte.js'
15
+ import { GeomState } from './lib/GeomState.svelte.js'
16
+ import { buildHullMarks } from './lib/marks/hull.js'
17
+
18
+ type Props = {
19
+ x?: string
20
+ y?: string
21
+ /** Group field (and its colour). Wins over `color`. */
22
+ fill?: string
23
+ /** Group field (and its colour) when `fill` is not set. */
24
+ color?: string
25
+ /** Pixels between the outermost points and the outline. */
26
+ padding?: number
27
+ /** Opacity 0–1 of the whole hull; defaults to the preset's `hull` opacity. */
28
+ alpha?: number
29
+ /** Draw the group's name at its centroid. */
30
+ label?: boolean
31
+ }
32
+
33
+ let { x, y, fill, color, padding = 10, alpha, label = false }: Props = $props()
34
+
35
+ const plotState = getContext<PlotState>('plot-state')
36
+ const geom = new GeomState(plotState, () => ({
37
+ type: 'hull',
38
+ channels: { x, y, fill, color },
39
+ alpha,
40
+ build: buildHullMarks
41
+ }))
42
+ onMount(geom.register)
43
+ onDestroy(geom.destroy)
44
+ $effect(geom.sync)
45
+
46
+ const hulls = $derived(geom.marks)
47
+ </script>
48
+
49
+ {#if hulls.length > 0}
50
+ <g data-plot-geom="hull">
51
+ {#each hulls as h (h.key)}
52
+ <!-- `opacity`, not fill-/stroke-opacity: the padding is a stroke that overlaps the
53
+ fill, and only element opacity composites the two as one shape. -->
54
+ <path
55
+ d={h.d}
56
+ fill={h.fill}
57
+ stroke={h.fill}
58
+ stroke-width={padding * 2}
59
+ stroke-linejoin="round"
60
+ stroke-linecap="round"
61
+ opacity={h.alpha}
62
+ data-plot-element="hull"
63
+ data-plot-hull={h.group}
64
+ />
65
+ {/each}
66
+ {#if label}
67
+ {#each hulls as h (h.key)}
68
+ <text
69
+ x={h.at.x}
70
+ y={h.at.y}
71
+ text-anchor="middle"
72
+ dominant-baseline="middle"
73
+ data-plot-element="hull-label">{h.group}</text
74
+ >
75
+ {/each}
76
+ {/if}
77
+ </g>
78
+ {/if}
79
+
80
+ <style>
81
+ [data-plot-geom='hull'] {
82
+ pointer-events: none;
83
+ }
84
+ [data-plot-element='hull-label'] {
85
+ fill: var(--chart-hull-label-color, currentColor);
86
+ font-size: var(--chart-hull-label-size, 11px);
87
+ font-weight: 600;
88
+ }
89
+ </style>
@@ -0,0 +1,17 @@
1
+ type Props = {
2
+ x?: string;
3
+ y?: string;
4
+ /** Group field (and its colour). Wins over `color`. */
5
+ fill?: string;
6
+ /** Group field (and its colour) when `fill` is not set. */
7
+ color?: string;
8
+ /** Pixels between the outermost points and the outline. */
9
+ padding?: number;
10
+ /** Opacity 0–1 of the whole hull; defaults to the preset's `hull` opacity. */
11
+ alpha?: number;
12
+ /** Draw the group's name at its centroid. */
13
+ label?: boolean;
14
+ };
15
+ declare const Hull: import("svelte").Component<Props, {}, "">;
16
+ type Hull = ReturnType<typeof Hull>;
17
+ export default Hull;
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A shaded region in DATA coordinates — a band (`x` and/or `y` as `[lo, hi]`) or a polygon
4
+ * (`points`).
5
+ *
6
+ * The zones on a main-sequence diagram, the quadrants of a hotspot map, the threshold box
7
+ * of a code-smell detection rule: each is a statement about the metric PLANE, not about
8
+ * any row, so a region reads no data and registers no geom. It never moves the scales —
9
+ * declare `xDomain`/`yDomain` on the chart when the region, not the data, defines the frame.
10
+ *
11
+ * SVG paints in document order, so place a Region BEFORE the marks it sits behind.
12
+ */
13
+ import { getContext } from 'svelte'
14
+ import type { PlotState } from '../PlotState.svelte.js'
15
+ import { regionVertices, positionOf, centroid, polygonPath } from '../lib/region.js'
16
+ import PlotAreaClip from './lib/PlotAreaClip.svelte'
17
+
18
+ type Value = number | string
19
+ type Props = {
20
+ /**
21
+ * `[lo, hi]` on the x axis. Omit to span the whole axis; a `null` end runs to that edge,
22
+ * so `[p75, null]` is "the top quartile and beyond". A band category covers its band.
23
+ */
24
+ x?: [Value | null, Value | null]
25
+ /** `[lo, hi]` on the y axis, with the same open-end rule. */
26
+ y?: [Value | null, Value | null]
27
+ /** Polygon vertices as `[x, y]` pairs in data coordinates. Wins over `x`/`y`. */
28
+ points?: Array<[Value, Value]>
29
+ /** Theming hook — rendered as `data-plot-region="<name>"`. */
30
+ name?: string
31
+ /** Text drawn at the region's centroid (or at `labelAt`). */
32
+ label?: string
33
+ /** `[x, y]` in data coordinates for the label, when the centroid is the wrong spot. */
34
+ labelAt?: [Value, Value]
35
+ /** Literal fill colour. Defaults to `--chart-region-fill`. */
36
+ fill?: string
37
+ /** Fill opacity 0–1. Defaults to `--chart-region-opacity`. */
38
+ alpha?: number
39
+ /** Optional outline colour. */
40
+ stroke?: string
41
+ }
42
+
43
+ let {
44
+ x = undefined,
45
+ y = undefined,
46
+ points = undefined,
47
+ name = undefined,
48
+ label = undefined,
49
+ labelAt = undefined,
50
+ fill = undefined,
51
+ alpha = undefined,
52
+ stroke = undefined
53
+ }: Props = $props()
54
+
55
+ const plotState = getContext<PlotState>('plot-state')
56
+ const uid = $props.id()
57
+ const clipId = `plot-region-clip-${uid}`
58
+
59
+ const shape = $derived.by(() => {
60
+ const xs = plotState.xScale
61
+ const ys = plotState.yScale
62
+ if (!xs || !ys) return null
63
+ const uv = regionVertices({ x, y, points }, xs, ys)
64
+ if (uv.length === 0) return null
65
+ const pts = uv.map(([u, v]) => plotState.place(u, v))
66
+ const at = labelAt
67
+ ? plotState.place(positionOf(xs, labelAt[0]), positionOf(ys, labelAt[1]))
68
+ : centroid(pts)
69
+ return { d: polygonPath(pts), at }
70
+ })
71
+ </script>
72
+
73
+ {#if shape}
74
+ <PlotAreaClip id={clipId} />
75
+ <g data-plot-geom="region" data-plot-region={name} clip-path="url(#{clipId})">
76
+ <path d={shape.d} {fill} fill-opacity={alpha} {stroke} data-plot-element="region" />
77
+ {#if label}
78
+ <text
79
+ x={shape.at.x}
80
+ y={shape.at.y}
81
+ text-anchor="middle"
82
+ dominant-baseline="middle"
83
+ data-plot-element="region-label">{label}</text
84
+ >
85
+ {/if}
86
+ </g>
87
+ {/if}
88
+
89
+ <style>
90
+ [data-plot-geom='region'] {
91
+ pointer-events: none;
92
+ }
93
+ [data-plot-element='region']:not([fill]) {
94
+ fill: var(--chart-region-fill, currentColor);
95
+ }
96
+ [data-plot-element='region']:not([fill-opacity]) {
97
+ fill-opacity: var(--chart-region-opacity, 0.08);
98
+ }
99
+ [data-plot-element='region']:not([stroke]) {
100
+ stroke: none;
101
+ }
102
+ [data-plot-element='region-label'] {
103
+ fill: var(--chart-region-label-color, currentColor);
104
+ font-size: var(--chart-region-label-size, 11px);
105
+ opacity: var(--chart-region-label-opacity, 0.75);
106
+ }
107
+ </style>
@@ -0,0 +1,27 @@
1
+ type Value = number | string;
2
+ type Props = {
3
+ /**
4
+ * `[lo, hi]` on the x axis. Omit to span the whole axis; a `null` end runs to that edge,
5
+ * so `[p75, null]` is "the top quartile and beyond". A band category covers its band.
6
+ */
7
+ x?: [Value | null, Value | null];
8
+ /** `[lo, hi]` on the y axis, with the same open-end rule. */
9
+ y?: [Value | null, Value | null];
10
+ /** Polygon vertices as `[x, y]` pairs in data coordinates. Wins over `x`/`y`. */
11
+ points?: Array<[Value, Value]>;
12
+ /** Theming hook — rendered as `data-plot-region="<name>"`. */
13
+ name?: string;
14
+ /** Text drawn at the region's centroid (or at `labelAt`). */
15
+ label?: string;
16
+ /** `[x, y]` in data coordinates for the label, when the centroid is the wrong spot. */
17
+ labelAt?: [Value, Value];
18
+ /** Literal fill colour. Defaults to `--chart-region-fill`. */
19
+ fill?: string;
20
+ /** Fill opacity 0–1. Defaults to `--chart-region-opacity`. */
21
+ alpha?: number;
22
+ /** Optional outline colour. */
23
+ stroke?: string;
24
+ };
25
+ declare const Region: import("svelte").Component<Props, {}, "">;
26
+ type Region = ReturnType<typeof Region>;
27
+ export default Region;