@rokkit/chart 1.0.0-next.147 → 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,25 @@
1
+ <script>
2
+ import Plot from '../Plot.svelte'
3
+ import Point from '../geoms/Point.svelte'
4
+
5
+ /** @type {import('../lib/plot/chartProps.js').ScatterBubbleChartProps} */
6
+ let {
7
+ data = [],
8
+ x = undefined,
9
+ y = undefined,
10
+ color = undefined,
11
+ symbol = undefined,
12
+ size = undefined,
13
+ label = false,
14
+ tooltip = false,
15
+ width = 600,
16
+ height = 400,
17
+ mode = 'light',
18
+ grid = true,
19
+ legend = false
20
+ } = $props()
21
+ </script>
22
+
23
+ <Plot {data} {width} {height} {mode} {grid} {legend} {tooltip}>
24
+ <Point {x} {y} {color} {size} {symbol} {label} />
25
+ </Plot>
@@ -0,0 +1,21 @@
1
+ <script>
2
+ import Plot from '../Plot.svelte'
3
+ import Violin from '../geoms/Violin.svelte'
4
+
5
+ /** @type {import('../lib/plot/chartProps.js').BoxViolinChartProps} */
6
+ let {
7
+ data = [],
8
+ x = undefined,
9
+ y = undefined,
10
+ fill = undefined,
11
+ width = 600,
12
+ height = 400,
13
+ mode = 'light',
14
+ grid = true,
15
+ legend = false
16
+ } = $props()
17
+ </script>
18
+
19
+ <Plot {data} {width} {height} {mode} {grid} {legend}>
20
+ <Violin {x} {y} {fill} />
21
+ </Plot>
@@ -0,0 +1,38 @@
1
+ <script>
2
+ import { setContext, untrack } from 'svelte'
3
+ import { createCrossFilter } from './createCrossFilter.svelte.js'
4
+
5
+ /**
6
+ * @type {{
7
+ * crossfilter?: ReturnType<typeof createCrossFilter>,
8
+ * mode?: 'dim' | 'hide',
9
+ * filters?: import('./createCrossFilter.svelte.js').FilterState,
10
+ * children?: import('svelte').Snippet
11
+ * }}
12
+ */
13
+ let {
14
+ crossfilter: externalCf = undefined,
15
+ mode = 'dim',
16
+ filters = $bindable(),
17
+ children
18
+ } = $props()
19
+
20
+ // Use an externally provided instance (spec/helpers API) or create one internally.
21
+ // untrack() suppresses "captures initial value" warning — intentional: the cf
22
+ // instance is locked in at construction time and must not recreate on prop changes.
23
+ const cf = untrack(() => externalCf ?? createCrossFilter())
24
+
25
+ // Expose the reactive filters Map to callers via bind:filters
26
+ $effect(() => {
27
+ filters = cf.filters
28
+ })
29
+
30
+ setContext('crossfilter', cf)
31
+ // Use a getter object so children can read .mode reactively
32
+ const modeRef = { get mode() { return mode } }
33
+ setContext('crossfilter-mode', modeRef)
34
+ </script>
35
+
36
+ <div data-crossfilter data-crossfilter-mode={mode}>
37
+ {@render children?.()}
38
+ </div>
@@ -0,0 +1,32 @@
1
+ <script>
2
+ import PlotChart from '../Plot.svelte'
3
+ import Bar from '../geoms/Bar.svelte'
4
+
5
+ let {
6
+ data = [],
7
+ field,
8
+ valueField,
9
+ stat = 'sum',
10
+ width = 300,
11
+ height = 120,
12
+ mode = 'light'
13
+ } = $props()
14
+
15
+ const spec = $derived({
16
+ x: field,
17
+ y: valueField
18
+ })
19
+ </script>
20
+
21
+ <!-- FilterBar must be used inside a <CrossFilter> parent. Does not create its own context. -->
22
+ <PlotChart
23
+ {data}
24
+ {spec}
25
+ {width}
26
+ {height}
27
+ {mode}
28
+ grid={false}
29
+ legend={false}
30
+ >
31
+ <Bar x={field} y={valueField} {stat} filterable={true} />
32
+ </PlotChart>
@@ -0,0 +1,79 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
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()
10
+
11
+ const cf = getContext('crossfilter')
12
+
13
+ // Initialize from props; $effect keeps in sync when min/max change
14
+ let low = $state(0)
15
+ let high = $state(100)
16
+
17
+ $effect(() => {
18
+ low = min ?? 0
19
+ high = max ?? 100
20
+ })
21
+
22
+ function handleLow(e) {
23
+ low = Math.min(Number(e.currentTarget.value), high)
24
+ cf?.setRange(field, [low, high])
25
+ }
26
+
27
+ function handleHigh(e) {
28
+ high = Math.max(Number(e.currentTarget.value), low)
29
+ cf?.setRange(field, [low, high])
30
+ }
31
+ </script>
32
+
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>
58
+ </div>
59
+
60
+ <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
+ }
79
+ </style>
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Creates a reactive cross-filter state object.
3
+ *
4
+ * Filter values follow the spec type:
5
+ * FilterState = Map<string, Set<unknown> | [number, number]>
6
+ * - categorical: raw Set of selected values
7
+ * - continuous: [min, max] tuple
8
+ *
9
+ * Exposes a `filters` getter so CrossFilter.svelte can bind to current state.
10
+ * Exposes a `version` counter that increments on every mutation, giving
11
+ * components a simple reactive signal to watch for filter changes.
12
+ *
13
+ * @returns {CrossFilter}
14
+ */
15
+ export function createCrossFilter() {
16
+ // Map<dimension, Set<unknown> | [number, number]>
17
+ const filters = $state(new Map())
18
+
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)
22
+
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
+ }
33
+
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
+ }
51
+
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
+ }
75
+
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
+ }
85
+
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
+ }
94
+
95
+ /** Clears all active filters. */
96
+ function clearAll() {
97
+ filters.clear()
98
+ version++
99
+ }
100
+
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
+ }
113
+ }
@@ -1,12 +1,11 @@
1
1
  <script>
2
- import { get } from 'svelte/store'
3
- import { swatch, swatchGrid } from '../old_lib'
2
+ import { swatch } from '../lib/swatch'
3
+ import { swatchGrid } from '../lib/grid'
4
4
  import Symbol from '../Symbol.svelte'
5
5
 
6
6
  let { base = 'teal', size = 4, shade = 600 } = $props()
7
7
 
8
- let swatchValue = $derived(get(swatch))
9
- let grid = $derived(swatchGrid(swatchValue.keys.symbol.length, size, 10))
8
+ let grid = $derived(swatchGrid(swatch.keys.symbol.length, size, 10))
10
9
  </script>
11
10
 
12
11
  <svg viewBox="0 0 {grid.width} {grid.height}">
@@ -15,9 +14,9 @@
15
14
  {x}
16
15
  {y}
17
16
  size={r * 2}
18
- name={swatchValue.keys.symbol[index]}
19
- fill={swatchValue.palette[base][shade]}
20
- stroke={swatchValue.palette[base][shade]}
17
+ name={swatch.keys.symbol[index]}
18
+ fill={swatch.palette[base][shade]}
19
+ stroke={swatch.palette[base][shade]}
21
20
  />
22
21
  {/each}
23
22
  </svg>
@@ -0,0 +1,81 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildArcs } from '../lib/brewing/marks/arcs.js'
4
+
5
+ /**
6
+ * `fill` is the primary prop name; `color` is accepted as an alias for
7
+ * spec-driven usage (Plot.svelte passes `color` to all geoms generically).
8
+ * @type {{ theta?: string, fill?: string, color?: string, pattern?: string, stat?: string, labelFn?: (data: Record<string, unknown>) => string, options?: { innerRadius?: number } }}
9
+ */
10
+ let { theta, fill, color, pattern, labelFn = undefined, stat = 'identity', options = {} } = $props()
11
+
12
+ const fillField = $derived(fill ?? color)
13
+
14
+ const plotState = getContext('plot-state')
15
+ let id = $state(null)
16
+
17
+ onMount(() => {
18
+ id = plotState.registerGeom({ type: 'arc', channels: { color: fillField, y: theta, pattern }, stat, options })
19
+ })
20
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
21
+
22
+ $effect(() => {
23
+ if (id) plotState.updateGeom(id, { channels: { color: fillField, y: theta, pattern }, stat })
24
+ })
25
+
26
+ const data = $derived(id ? plotState.geomData(id) : [])
27
+ const colors = $derived(plotState.colors)
28
+ const patterns = $derived(plotState.patterns)
29
+ const w = $derived(plotState.innerWidth)
30
+ const h = $derived(plotState.innerHeight)
31
+
32
+ const arcs = $derived.by(() => {
33
+ if (!data?.length) return []
34
+ // Guard: skip until data catches up after a fill-field change.
35
+ // When fillField changes, the $effect updates the geom asynchronously, but
36
+ // this derived runs first with stale data whose rows don't have the new
37
+ // field — causing all keys to be undefined (duplicate key error).
38
+ if (fillField && !(fillField in data[0])) return []
39
+ const innerRadius = (options.innerRadius ?? 0) * Math.min(w, h) / 2
40
+ return buildArcs(data, { color: fillField, y: theta, pattern }, colors, w, h, { innerRadius }, patterns)
41
+ })
42
+ </script>
43
+
44
+ {#if arcs.length > 0}
45
+ <g
46
+ data-plot-geom="arc"
47
+ transform="translate({w / 2}, {h / 2})"
48
+ >
49
+ {#each arcs as arc (arc.key)}
50
+ <path
51
+ d={arc.d}
52
+ fill={arc.fill}
53
+ stroke={arc.stroke}
54
+ stroke-width="1"
55
+ role="presentation"
56
+ data-plot-element="arc"
57
+ onmouseenter={() => plotState.setHovered({ ...arc.data, '%': `${arc.pct}%` })}
58
+ onmouseleave={() => plotState.clearHovered()}
59
+ />
60
+ {#if arc.patternId}
61
+ <path d={arc.d} fill="url(#{arc.patternId})" stroke={arc.stroke} stroke-width="1" pointer-events="none" data-plot-element="arc" />
62
+ {/if}
63
+ {#if arc.pct >= 5}
64
+ {@const labelText = labelFn ? String(labelFn(arc.data) ?? '') : `${arc.pct}%`}
65
+ {#if labelText}
66
+ {@const lw = Math.max(36, labelText.length * 7 + 12)}
67
+ <g transform="translate({arc.centroid[0]},{arc.centroid[1]})" pointer-events="none" data-plot-element="arc-label">
68
+ <rect x={-lw / 2} y="-9" width={lw} height="18" rx="4" fill="white" fill-opacity="0.82" />
69
+ <text
70
+ text-anchor="middle"
71
+ dominant-baseline="central"
72
+ font-size="11"
73
+ font-weight="600"
74
+ fill={arc.stroke}
75
+ >{labelText}</text>
76
+ </g>
77
+ {/if}
78
+ {/if}
79
+ {/each}
80
+ </g>
81
+ {/if}
@@ -0,0 +1,50 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildAreas, buildStackedAreas } from './lib/areas.js'
4
+
5
+ let { x, y, color, pattern, stat = 'identity', options = {} } = $props()
6
+
7
+ const plotState = getContext('plot-state')
8
+ let id = $state(null)
9
+
10
+ onMount(() => {
11
+ id = plotState.registerGeom({ type: 'area', channels: { x, y, color, pattern }, stat, options: { stack: options?.stack ?? false } })
12
+ })
13
+ onDestroy(() => { if (id) plotState.unregisterGeom(id) })
14
+
15
+ $effect(() => {
16
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, pattern }, stat, options: { stack: options?.stack ?? false } })
17
+ })
18
+
19
+ const data = $derived(id ? plotState.geomData(id) : [])
20
+ const xScale = $derived(plotState.xScale)
21
+ const yScale = $derived(plotState.yScale)
22
+ const colors = $derived(plotState.colors)
23
+ const patterns = $derived(plotState.patterns)
24
+
25
+ const areas = $derived.by(() => {
26
+ if (!data?.length || !xScale || !yScale) return []
27
+ const channels = { x, y, color, pattern }
28
+ if (options.stack) {
29
+ return buildStackedAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
30
+ }
31
+ return buildAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
32
+ })
33
+ </script>
34
+
35
+ {#if areas.length > 0}
36
+ <g data-plot-geom="area">
37
+ {#each areas as seg (seg.key ?? seg.d)}
38
+ <path
39
+ d={seg.d}
40
+ fill={seg.fill}
41
+ fill-opacity={seg.patternId ? 1 : (options.opacity ?? 0.6)}
42
+ stroke={seg.stroke ?? 'none'}
43
+ data-plot-element="area"
44
+ />
45
+ {#if seg.patternId}
46
+ <path d={seg.d} fill="url(#{seg.patternId})" data-plot-element="area" />
47
+ {/if}
48
+ {/each}
49
+ </g>
50
+ {/if}
@@ -0,0 +1,142 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildGroupedBars, buildStackedBars, buildHorizontalBars } from './lib/bars.js'
4
+ import LabelPill from './LabelPill.svelte'
5
+
6
+ let { x, y, color, fill: fillProp, pattern, label = false, stat = 'identity', options = {}, filterable = false } = $props()
7
+
8
+ // `fill` is accepted as an alias for `color` (consistent with Arc.svelte)
9
+ const colorChannel = $derived(fillProp ?? color)
10
+
11
+ /**
12
+ * @param {Record<string, unknown>} data
13
+ * @param {string} defaultField
14
+ * @returns {string | null}
15
+ */
16
+ function resolveLabel(data, defaultField) {
17
+ if (!label) return null
18
+ if (label === true) return String(data[defaultField] ?? '')
19
+ if (typeof label === 'function') return String(label(data) ?? '')
20
+ if (typeof label === 'string') return String(data[label] ?? '')
21
+ return null
22
+ }
23
+
24
+ const plotState = getContext('plot-state')
25
+ const cf = getContext('crossfilter')
26
+ let id = $state(null)
27
+
28
+ onMount(() => {
29
+ id = plotState.registerGeom({ type: 'bar', channels: { x, y, color: colorChannel, pattern }, stat, options: { stack: options?.stack ?? false } })
30
+ })
31
+ onDestroy(() => {
32
+ if (id) plotState.unregisterGeom(id)
33
+ })
34
+
35
+ $effect(() => {
36
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: colorChannel, pattern }, stat, options: { stack: options?.stack ?? false } })
37
+ })
38
+
39
+ const data = $derived(id ? plotState.geomData(id) : [])
40
+ const xScale = $derived(plotState.xScale)
41
+ const yScale = $derived(plotState.yScale)
42
+ const colors = $derived(plotState.colors)
43
+ const patterns = $derived(plotState.patterns)
44
+ const orientation = $derived(plotState.orientation)
45
+ const innerHeight = $derived(plotState.innerHeight)
46
+
47
+ const bars = $derived.by(() => {
48
+ if (!data?.length || !xScale || !yScale) return []
49
+ const channels = { x, y, color: colorChannel, pattern }
50
+ if (orientation === 'horizontal') {
51
+ return buildHorizontalBars(data, channels, xScale, yScale, colors, innerHeight)
52
+ }
53
+ if (options.stack) {
54
+ return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
55
+ }
56
+ return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
57
+ })
58
+
59
+ /** @type {Record<string, boolean>} */
60
+ let dimmedByKey = $state({})
61
+
62
+ $effect(() => {
63
+ if (!cf) { dimmedByKey = {}; return }
64
+ // cf.version is a $state counter that increments on every filter mutation.
65
+ // Reading it here establishes a reactive dependency so the effect re-runs
66
+ // whenever any filter changes — including changes from sibling FilterBars.
67
+ void cf.version
68
+ const next = /** @type {Record<string, boolean>} */ ({})
69
+ for (const bar of bars) {
70
+ const dimmedByX = x ? cf.isDimmed(x, bar.data[x]) : false
71
+ const dimmedByY = y ? cf.isDimmed(y, bar.data[y]) : false
72
+ next[bar.key] = dimmedByX || dimmedByY
73
+ }
74
+ dimmedByKey = next
75
+ })
76
+
77
+ function handleBarClick(barX) {
78
+ if (!filterable || !x || !cf) return
79
+ cf.toggleCategorical(x, barX)
80
+ }
81
+ </script>
82
+
83
+ {#if bars.length > 0}
84
+ <g data-plot-geom="bar">
85
+ {#each bars as bar (bar.key)}
86
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
87
+ <rect
88
+ x={bar.x}
89
+ y={bar.y}
90
+ width={Math.max(0, bar.width)}
91
+ height={Math.max(0, bar.height)}
92
+ fill={bar.fill}
93
+ stroke={bar.stroke ?? 'none'}
94
+ stroke-width={bar.stroke ? 0.5 : 0}
95
+ data-plot-element="bar"
96
+ data-plot-value={bar.data[y]}
97
+ data-plot-category={bar.data[x]}
98
+ data-dimmed={dimmedByKey[bar.key] ? true : undefined}
99
+ style:cursor={filterable ? 'pointer' : undefined}
100
+ onclick={filterable && x ? () => handleBarClick(bar.data[x]) : undefined}
101
+ onkeydown={filterable && x ? (e) => (e.key === 'Enter' || e.key === ' ') && handleBarClick(bar.data[x]) : undefined}
102
+ role={filterable ? 'button' : 'graphics-symbol'}
103
+ tabindex={filterable ? 0 : undefined}
104
+ aria-label="{bar.data[x]}: {bar.data[y]}"
105
+ onmouseenter={() => plotState.setHovered(bar.data)}
106
+ onmouseleave={() => plotState.clearHovered()}
107
+ >
108
+ <title>{bar.data[x]}: {bar.data[y]}</title>
109
+ </rect>
110
+ {#if bar.patternId}
111
+ <rect
112
+ x={bar.x}
113
+ y={bar.y}
114
+ width={Math.max(0, bar.width)}
115
+ height={Math.max(0, bar.height)}
116
+ fill="url(#{bar.patternId})"
117
+ pointer-events="none"
118
+ />
119
+ {/if}
120
+ {#if label}
121
+ {@const text = resolveLabel(bar.data, orientation === 'horizontal' ? x : y)}
122
+ {#if text}
123
+ {#if orientation === 'horizontal'}
124
+ <LabelPill
125
+ x={bar.x + bar.width + (options.labelOffset ?? 8)}
126
+ y={bar.y + bar.height / 2}
127
+ {text}
128
+ color={bar.stroke ?? '#333'}
129
+ />
130
+ {:else}
131
+ <LabelPill
132
+ x={bar.x + bar.width / 2}
133
+ y={bar.y + (options.labelOffset ?? -8)}
134
+ {text}
135
+ color={bar.stroke ?? '#333'}
136
+ />
137
+ {/if}
138
+ {/if}
139
+ {/if}
140
+ {/each}
141
+ </g>
142
+ {/if}