@rokkit/chart 1.0.0-next.16 → 1.0.0-next.160

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 (173) hide show
  1. package/README.md +150 -46
  2. package/package.json +42 -45
  3. package/src/AnimatedPlot.svelte +383 -0
  4. package/src/Chart.svelte +95 -0
  5. package/src/ChartProvider.svelte +10 -0
  6. package/src/FacetPlot/Panel.svelte +37 -0
  7. package/src/FacetPlot.svelte +114 -0
  8. package/src/Plot/Arc.svelte +29 -0
  9. package/src/Plot/Area.svelte +32 -0
  10. package/src/Plot/Axis.svelte +95 -0
  11. package/src/Plot/Bar.svelte +54 -0
  12. package/src/Plot/Grid.svelte +34 -0
  13. package/src/Plot/Legend.svelte +233 -0
  14. package/src/Plot/Line.svelte +37 -0
  15. package/src/Plot/Point.svelte +40 -0
  16. package/src/Plot/Root.svelte +62 -0
  17. package/src/Plot/Timeline.svelte +95 -0
  18. package/src/Plot/Tooltip.svelte +87 -0
  19. package/src/Plot/index.js +9 -0
  20. package/src/Plot.svelte +297 -0
  21. package/src/PlotState.svelte.js +350 -0
  22. package/src/Sparkline.svelte +108 -0
  23. package/src/Symbol.svelte +21 -0
  24. package/src/Texture.svelte +18 -0
  25. package/src/charts/AreaChart.svelte +27 -0
  26. package/src/charts/BarChart.svelte +28 -0
  27. package/src/charts/BoxPlot.svelte +21 -0
  28. package/src/charts/BubbleChart.svelte +23 -0
  29. package/src/charts/LineChart.svelte +26 -0
  30. package/src/charts/PieChart.svelte +35 -0
  31. package/src/charts/ScatterPlot.svelte +26 -0
  32. package/src/charts/ViolinPlot.svelte +21 -0
  33. package/src/crossfilter/CrossFilter.svelte +42 -0
  34. package/src/crossfilter/FilterBar.svelte +24 -0
  35. package/src/crossfilter/FilterHistogram.svelte +290 -0
  36. package/src/crossfilter/FilterSlider.svelte +83 -0
  37. package/src/crossfilter/createCrossFilter.svelte.js +124 -0
  38. package/src/elements/Bar.svelte +22 -24
  39. package/src/elements/ColorRamp.svelte +20 -22
  40. package/src/elements/ContinuousLegend.svelte +20 -17
  41. package/src/elements/DefinePatterns.svelte +24 -0
  42. package/src/elements/DiscreteLegend.svelte +15 -15
  43. package/src/elements/Label.svelte +4 -8
  44. package/src/elements/SymbolGrid.svelte +22 -0
  45. package/src/elements/index.js +6 -0
  46. package/src/examples/BarChartExample.svelte +81 -0
  47. package/src/geoms/Arc.svelte +126 -0
  48. package/src/geoms/Area.svelte +78 -0
  49. package/src/geoms/Bar.svelte +200 -0
  50. package/src/geoms/Box.svelte +113 -0
  51. package/src/geoms/LabelPill.svelte +17 -0
  52. package/src/geoms/Line.svelte +123 -0
  53. package/src/geoms/Point.svelte +145 -0
  54. package/src/geoms/Violin.svelte +56 -0
  55. package/src/geoms/lib/areas.js +154 -0
  56. package/src/geoms/lib/bars.js +223 -0
  57. package/src/index.js +74 -16
  58. package/src/lib/brewer.js +25 -0
  59. package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
  60. package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
  61. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  62. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  63. package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
  64. package/src/lib/brewing/axes.svelte.js +270 -0
  65. package/src/lib/brewing/bars.svelte.js +201 -0
  66. package/src/lib/brewing/brewer.svelte.js +277 -0
  67. package/src/lib/brewing/colors.js +51 -0
  68. package/src/lib/brewing/dimensions.svelte.js +56 -0
  69. package/src/lib/brewing/index.svelte.js +205 -0
  70. package/src/lib/brewing/legends.svelte.js +137 -0
  71. package/src/lib/brewing/marks/arcs.js +43 -0
  72. package/src/lib/brewing/marks/areas.js +72 -0
  73. package/src/lib/brewing/marks/bars.js +49 -0
  74. package/src/lib/brewing/marks/boxes.js +75 -0
  75. package/src/lib/brewing/marks/lines.js +55 -0
  76. package/src/lib/brewing/marks/points.js +105 -0
  77. package/src/lib/brewing/marks/violins.js +90 -0
  78. package/src/lib/brewing/patterns.js +45 -0
  79. package/src/lib/brewing/scales.js +51 -0
  80. package/src/lib/brewing/scales.svelte.js +82 -0
  81. package/src/lib/brewing/stats.js +74 -0
  82. package/src/lib/brewing/symbols.js +10 -0
  83. package/src/lib/brewing/types.js +73 -0
  84. package/src/lib/chart.js +221 -0
  85. package/src/lib/context.js +131 -0
  86. package/src/lib/grid.js +85 -0
  87. package/src/lib/keyboard-nav.js +37 -0
  88. package/src/lib/plot/chartProps.js +76 -0
  89. package/src/lib/plot/crossfilter.js +16 -0
  90. package/src/lib/plot/facet.js +58 -0
  91. package/src/lib/plot/frames.js +81 -0
  92. package/src/lib/plot/helpers.js +14 -0
  93. package/src/lib/plot/preset.js +67 -0
  94. package/src/lib/plot/scales.js +81 -0
  95. package/src/lib/plot/stat.js +92 -0
  96. package/src/lib/plot/types.js +65 -0
  97. package/src/lib/preset.js +41 -0
  98. package/src/lib/scales.svelte.js +151 -0
  99. package/src/lib/swatch.js +13 -0
  100. package/src/lib/ticks.js +46 -0
  101. package/src/lib/utils.js +111 -118
  102. package/src/lib/xscale.js +31 -0
  103. package/src/patterns/DefinePatterns.svelte +32 -0
  104. package/src/patterns/PatternDef.svelte +27 -0
  105. package/src/patterns/index.js +4 -0
  106. package/src/patterns/patterns.js +360 -0
  107. package/src/patterns/scale.js +116 -0
  108. package/src/spec/chart-spec.js +72 -0
  109. package/src/symbols/RoundedSquare.svelte +33 -0
  110. package/src/symbols/Shape.svelte +37 -0
  111. package/src/symbols/constants/index.js +4 -0
  112. package/src/symbols/index.js +9 -0
  113. package/src/symbols/outline.svelte +60 -0
  114. package/src/symbols/solid.svelte +60 -0
  115. package/LICENSE +0 -21
  116. package/src/chart/FacetGrid.svelte +0 -51
  117. package/src/chart/Grid.svelte +0 -34
  118. package/src/chart/Legend.svelte +0 -16
  119. package/src/chart/PatternDefs.svelte +0 -13
  120. package/src/chart/Swatch.svelte +0 -93
  121. package/src/chart/SwatchButton.svelte +0 -29
  122. package/src/chart/SwatchGrid.svelte +0 -55
  123. package/src/chart/Symbol.svelte +0 -37
  124. package/src/chart/Texture.svelte +0 -16
  125. package/src/chart/TexturedShape.svelte +0 -27
  126. package/src/chart/TimelapseChart.svelte +0 -97
  127. package/src/chart/Timer.svelte +0 -27
  128. package/src/chart.js +0 -9
  129. package/src/components/charts/Axis.svelte +0 -66
  130. package/src/components/charts/Chart.svelte +0 -35
  131. package/src/components/index.js +0 -23
  132. package/src/components/lib/axis.js +0 -0
  133. package/src/components/lib/chart.js +0 -187
  134. package/src/components/lib/color.js +0 -327
  135. package/src/components/lib/funnel.js +0 -204
  136. package/src/components/lib/index.js +0 -19
  137. package/src/components/lib/pattern.js +0 -190
  138. package/src/components/lib/rollup.js +0 -55
  139. package/src/components/lib/shape.js +0 -199
  140. package/src/components/lib/summary.js +0 -145
  141. package/src/components/lib/theme.js +0 -23
  142. package/src/components/lib/timer.js +0 -41
  143. package/src/components/lib/utils.js +0 -165
  144. package/src/components/plots/BarPlot.svelte +0 -36
  145. package/src/components/plots/BoxPlot.svelte +0 -54
  146. package/src/components/plots/ScatterPlot.svelte +0 -30
  147. package/src/components/store.js +0 -70
  148. package/src/constants.js +0 -66
  149. package/src/elements/PatternDefs.svelte +0 -13
  150. package/src/elements/PatternMask.svelte +0 -20
  151. package/src/elements/Symbol.svelte +0 -38
  152. package/src/elements/Tooltip.svelte +0 -23
  153. package/src/funnel.svelte +0 -35
  154. package/src/geom.js +0 -105
  155. package/src/lib/axis.js +0 -75
  156. package/src/lib/colors.js +0 -32
  157. package/src/lib/geom.js +0 -4
  158. package/src/lib/shapes.js +0 -144
  159. package/src/lib/timer.js +0 -44
  160. package/src/lookup.js +0 -29
  161. package/src/plots/BarPlot.svelte +0 -55
  162. package/src/plots/BoxPlot.svelte +0 -0
  163. package/src/plots/FunnelPlot.svelte +0 -33
  164. package/src/plots/HeatMap.svelte +0 -5
  165. package/src/plots/HeatMapCalendar.svelte +0 -129
  166. package/src/plots/LinePlot.svelte +0 -55
  167. package/src/plots/Plot.svelte +0 -25
  168. package/src/plots/RankBarPlot.svelte +0 -38
  169. package/src/plots/ScatterPlot.svelte +0 -20
  170. package/src/plots/ViolinPlot.svelte +0 -11
  171. package/src/plots/heatmap.js +0 -70
  172. package/src/plots/index.js +0 -10
  173. package/src/swatch.js +0 -11
@@ -1,24 +1,27 @@
1
1
  <script>
2
2
  import { scaleLinear } from 'd3-scale'
3
- import { uniqueId } from '../components/lib/utils'
4
3
 
5
- export let x = 0
6
- export let y = 0
7
- export let textSize = 5
8
- export let height = 10
9
- export let width = 100
10
- export let tickCount = 5
11
- export let scale
4
+ let {
5
+ x = 0,
6
+ y = 0,
7
+ textSize = 5,
8
+ height = 10,
9
+ width = 100,
10
+ tickCount = 5,
11
+ scale,
12
+ id = 'legend'
13
+ } = $props()
12
14
 
13
- $: scaleTicks = scaleLinear()
14
- .range([x, x + 100])
15
- .domain(scale.domain())
16
- $: ticks = scale.ticks
17
- .apply(scale, [tickCount])
18
- .map((d) => ({ x: scaleTicks(d), label: d }))
15
+ let scaleTicks = $derived(
16
+ scaleLinear()
17
+ .range([x, x + 100])
18
+ .domain(scale.domain())
19
+ )
20
+ let ticks = $derived(
21
+ scale.ticks.apply(scale, [tickCount]).map((d) => ({ x: scaleTicks(d), label: d }))
22
+ )
19
23
 
20
- $: colors = scale.range()
21
- $: id = uniqueId('legend-')
24
+ let colors = $derived(scale.range())
22
25
  </script>
23
26
 
24
27
  <defs>
@@ -28,7 +31,7 @@
28
31
  </linearGradient>
29
32
  </defs>
30
33
  <rect {x} y={y + height} {width} {height} fill="url(#{id})" />
31
- {#each ticks as { x, label }}
34
+ {#each ticks as { x, label }, index (index)}
32
35
  <line x1={x} y1={y + (2 * height) / 3} x2={x} y2={y + height * 2} />
33
36
  <text {x} y={y + height / 2} font-size={textSize}>{label}</text>
34
37
  {/each}
@@ -0,0 +1,24 @@
1
+ <script>
2
+ import { uniq } from 'ramda'
3
+
4
+ let {
5
+ size = 10,
6
+ patternUnits = 'userSpaceOnUse',
7
+ /** @type {Array<import('./types').Pattern>} */
8
+ patterns = []
9
+ } = $props()
10
+
11
+ let names = $derived(uniq(patterns.map(({ id }) => id)))
12
+ </script>
13
+
14
+ {#if names.length < patterns.length}
15
+ <error> Patterns should be an array and should have unique names for each pattern </error>
16
+ {:else if patterns.length > 0}
17
+ <defs>
18
+ {#each patterns as { id, component: Component, fill, stroke }, index (index)}
19
+ <pattern {id} {patternUnits} width={size} height={size}>
20
+ <Component {size} {fill} {stroke} />
21
+ </pattern>
22
+ {/each}
23
+ </defs>
24
+ {/if}
@@ -1,22 +1,22 @@
1
1
  <script>
2
- export let x = 0
3
- export let y = 0
4
- export let textSize = 5
5
- export let size = 10
6
- export let space = 2
7
- export let padding = 5
8
- export let scale
9
- export let tickCount
2
+ let {
3
+ x = 0,
4
+ y = 0,
5
+ textSize = 5,
6
+ size = 10,
7
+ space = 2,
8
+ padding = 5,
9
+ scale,
10
+ tickCount = 10
11
+ } = $props()
10
12
 
11
- $: sizeWithSpace = size + space
12
- $: ticks = scale.ticks.apply(scale, [tickCount])
13
+ let sizeWithSpace = $derived(size + space)
14
+ let ticks = $derived(scale.ticks.apply(scale, [tickCount]))
13
15
  </script>
14
16
 
15
- {#each ticks as tick, i}
16
- <text
17
- x={x + padding + i * sizeWithSpace + size / 2}
18
- y={y + size / 2}
19
- font-size={textSize}>{tick}</text
17
+ {#each ticks as tick, i (i)}
18
+ <text x={x + padding + i * sizeWithSpace + size / 2} y={y + size / 2} font-size={textSize}
19
+ >{tick}</text
20
20
  >
21
21
  <rect
22
22
  x={x + padding + i * sizeWithSpace}
@@ -1,12 +1,8 @@
1
1
  <script>
2
- export let small = false
3
- export let label
4
- export let angle = 0
5
- export let anchor = 'middle'
6
- export let x
7
- export let y
2
+ let { x, y, text, angle = 0, small = false, anchor = 'middle' } = $props()
8
3
 
9
- $: transform = `translate(${x},${y}) rotate(${angle})`
4
+ let transform = $derived(`translate(${x},${y}) rotate(${angle})`)
5
+ let validAnchor = $derived(['start', 'middle', 'end'].includes(anchor) ? anchor : 'middle')
10
6
  </script>
11
7
 
12
- <text class="label" class:small text-anchor={anchor} {transform}>{label}</text>
8
+ <text class="label" class:small text-anchor={validAnchor} {transform}>{text}</text>
@@ -0,0 +1,22 @@
1
+ <script>
2
+ import { swatch } from '../lib/swatch'
3
+ import { swatchGrid } from '../lib/grid'
4
+ import Symbol from '../Symbol.svelte'
5
+
6
+ let { base = 'teal', size = 4, shade = 600 } = $props()
7
+
8
+ let grid = $derived(swatchGrid(swatch.keys.symbol.length, size, 10))
9
+ </script>
10
+
11
+ <svg viewBox="0 0 {grid.width} {grid.height}">
12
+ {#each grid.data as { x, y, r }, index (index)}
13
+ <Symbol
14
+ {x}
15
+ {y}
16
+ size={r * 2}
17
+ name={swatch.keys.symbol[index]}
18
+ fill={swatch.palette[base][shade]}
19
+ stroke={swatch.palette[base][shade]}
20
+ />
21
+ {/each}
22
+ </svg>
@@ -0,0 +1,6 @@
1
+ export { default as Bar } from './Bar.svelte'
2
+ export { default as ColorRamp } from './ColorRamp.svelte'
3
+ export { default as ContinuousLegend } from './ContinuousLegend.svelte'
4
+ export { default as Label } from './Label.svelte'
5
+ export { default as DefinePatterns } from './DefinePatterns.svelte'
6
+ export { default as SymbolGrid } from './SymbolGrid.svelte'
@@ -0,0 +1,81 @@
1
+ <script>
2
+ import { Plot } from '../index.js'
3
+ import { dataset } from '@rokkit/data'
4
+
5
+ // Sample data
6
+ const sampleData = [
7
+ { model: 'Model A', name: 'Product 1', category: 'Electronics', count: 45 },
8
+ { model: 'Model B', name: 'Product 2', category: 'Clothing', count: 32 },
9
+ { model: 'Model C', name: 'Product 3', category: 'Electronics', count: 62 },
10
+ { model: 'Model D', name: 'Product 4', category: 'Home', count: 28 },
11
+ { model: 'Model E', name: 'Product 5', category: 'Electronics', count: 53 },
12
+ { model: 'Model F', name: 'Product 6', category: 'Clothing', count: 24 },
13
+ { model: 'Model G', name: 'Product 7', category: 'Home', count: 35 }
14
+ ]
15
+
16
+ // Use the dataset class to process the data
17
+ const data = dataset(sampleData)
18
+ .groupBy('category')
19
+ .summarize('name', { count: (values) => values.length })
20
+ .rollup()
21
+
22
+ // Chart dimensions
23
+ const width = 600
24
+ const height = 400
25
+ const margin = { top: 20, right: 100, bottom: 60, left: 60 }
26
+
27
+ // Click handler for bars
28
+ function handleBarClick(item) {
29
+ console.log('Bar clicked:', item)
30
+ alert(`Clicked on ${item.category} with count ${item.count}`)
31
+ }
32
+ </script>
33
+
34
+ <div class="example">
35
+ <h2>Bar Chart Example</h2>
36
+
37
+ <div class="chart-wrapper">
38
+ <Plot.Root {data} {width} {height} {margin} fill="category">
39
+ <Plot.Grid direction="y" lineStyle="dashed" />
40
+ <Plot.Axis type="x" field="category" label="Product Category" />
41
+ <Plot.Axis type="y" field="count" label="Number of Products" />
42
+ <Plot.Bar x="category" y="count" fill="category" onClick={handleBarClick} />
43
+ <Plot.Legend title="Categories" />
44
+ </Plot.Root>
45
+ </div>
46
+
47
+ <div class="code-example">
48
+ <h3>Example Code:</h3>
49
+ <pre>
50
+ <!-- &lt;script&gt;
51
+ import { Plot } from '@rokkit/chart';
52
+ import { dataset } from '@rokkit/data';
53
+
54
+ // Sample data
55
+ const sampleData = [
56
+ { model: 'Model A', name: 'Product 1', category: 'Electronics', count: 45 },
57
+ { model: 'Model B', name: 'Product 2', category: 'Clothing', count: 32 },
58
+ { model: 'Model C', name: 'Product 3', category: 'Electronics', count: 62 },
59
+ { model: 'Model D', name: 'Product 4', category: 'Home', count: 28 },
60
+ { model: 'Model E', name: 'Product 5', category: 'Electronics', count: 53 },
61
+ { model: 'Model F', name: 'Product 6', category: 'Clothing', count: 24 },
62
+ { model: 'Model G', name: 'Product 7', category: 'Home', count: 35 },
63
+ ];
64
+
65
+ // Use the dataset class to process the data
66
+ const data = dataset(sampleData)
67
+ .groupBy('category')
68
+ .summarize('name', { count: values => values.length })
69
+ .rollup();
70
+ &lt;/script&gt;
71
+
72
+ &lt;Plot.Root {data} width={600} height={400} margin={{ top: 20, right: 100, bottom: 60, left: 60 }} fill="category"&gt;
73
+ &lt;Plot.Grid direction="y" lineStyle="dashed" /&gt;
74
+ &lt;Plot.Axis type="x" field="category" label="Product Category" /&gt;
75
+ &lt;Plot.Axis type="y" field="count" label="Number of Products" /&gt;
76
+ &lt;Plot.Bar x="category" y="count" fill="category" onClick={handleBarClick} /&gt;
77
+ &lt;Plot.Legend title="Categories" /&gt;
78
+ &lt;/Plot.Root&gt; -->
79
+ </pre>
80
+ </div>
81
+ </div>
@@ -0,0 +1,126 @@
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 {
11
+ theta,
12
+ fill,
13
+ color,
14
+ pattern,
15
+ labelFn = undefined,
16
+ stat = 'identity',
17
+ options = {},
18
+ onselect = undefined
19
+ } = $props()
20
+
21
+ const fillField = $derived(fill ?? color)
22
+
23
+ const plotState = getContext('plot-state')
24
+ let id = $state(null)
25
+
26
+ onMount(() => {
27
+ id = plotState.registerGeom({
28
+ type: 'arc',
29
+ channels: { color: fillField, y: theta, pattern },
30
+ stat,
31
+ options
32
+ })
33
+ })
34
+ onDestroy(() => {
35
+ if (id) plotState.unregisterGeom(id)
36
+ })
37
+
38
+ $effect(() => {
39
+ if (id) plotState.updateGeom(id, { channels: { color: fillField, y: theta, pattern }, stat })
40
+ })
41
+
42
+ const data = $derived(id ? plotState.geomData(id) : [])
43
+ const colors = $derived(plotState.colors)
44
+ const patterns = $derived(plotState.patterns)
45
+ const w = $derived(plotState.innerWidth)
46
+ const h = $derived(plotState.innerHeight)
47
+
48
+ const arcs = $derived.by(() => {
49
+ if (!data?.length) return []
50
+ // Guard: skip until data catches up after a fill-field change.
51
+ // When fillField changes, the $effect updates the geom asynchronously, but
52
+ // this derived runs first with stale data whose rows don't have the new
53
+ // field — causing all keys to be undefined (duplicate key error).
54
+ if (fillField && !(fillField in data[0])) return []
55
+ const innerRadius = ((options.innerRadius ?? 0) * Math.min(w, h)) / 2
56
+ return buildArcs(
57
+ data,
58
+ { color: fillField, y: theta, pattern },
59
+ colors,
60
+ w,
61
+ h,
62
+ { innerRadius },
63
+ patterns
64
+ )
65
+ })
66
+ </script>
67
+
68
+ {#if arcs.length > 0}
69
+ <g data-plot-geom="arc" transform="translate({w / 2}, {h / 2})">
70
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
71
+ {#each arcs as arc (arc.key)}
72
+ <path
73
+ d={arc.d}
74
+ fill={arc.fill}
75
+ stroke={arc.stroke}
76
+ stroke-width="1"
77
+ role={onselect ? 'button' : 'presentation'}
78
+ tabindex={onselect ? 0 : undefined}
79
+ style:cursor={onselect ? 'pointer' : undefined}
80
+ data-plot-element="arc"
81
+ onmouseenter={() => plotState.setHovered({ ...arc.data, '%': `${arc.pct}%` })}
82
+ onmouseleave={() => plotState.clearHovered()}
83
+ onclick={onselect ? () => onselect({ ...arc.data, '%': `${arc.pct}%` }) : undefined}
84
+ onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect({ ...arc.data, '%': `${arc.pct}%` }) : undefined}
85
+ />
86
+ {#if arc.patternId}
87
+ <path
88
+ d={arc.d}
89
+ fill="url(#{arc.patternId})"
90
+ stroke={arc.stroke}
91
+ stroke-width="1"
92
+ pointer-events="none"
93
+ data-plot-element="arc"
94
+ />
95
+ {/if}
96
+ {#if arc.pct >= 5}
97
+ {@const labelText = labelFn ? String(labelFn(arc.data) ?? '') : `${arc.pct}%`}
98
+ {#if labelText}
99
+ {@const lw = Math.max(36, labelText.length * 7 + 12)}
100
+ <g
101
+ transform="translate({arc.centroid[0]},{arc.centroid[1]})"
102
+ pointer-events="none"
103
+ data-plot-element="arc-label"
104
+ >
105
+ <rect
106
+ x={-lw / 2}
107
+ y="-9"
108
+ width={lw}
109
+ height="18"
110
+ rx="4"
111
+ fill="white"
112
+ fill-opacity="0.82"
113
+ />
114
+ <text
115
+ text-anchor="middle"
116
+ dominant-baseline="central"
117
+ font-size="11"
118
+ font-weight="600"
119
+ fill={arc.stroke}>{labelText}</text
120
+ >
121
+ </g>
122
+ {/if}
123
+ {/if}
124
+ {/each}
125
+ </g>
126
+ {/if}
@@ -0,0 +1,78 @@
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({
12
+ type: 'area',
13
+ channels: { x, y, color, pattern },
14
+ stat,
15
+ options: { stack: options?.stack ?? false }
16
+ })
17
+ })
18
+ onDestroy(() => {
19
+ if (id) plotState.unregisterGeom(id)
20
+ })
21
+
22
+ $effect(() => {
23
+ if (id)
24
+ plotState.updateGeom(id, {
25
+ channels: { x, y, color, pattern },
26
+ stat,
27
+ options: { stack: options?.stack ?? false }
28
+ })
29
+ })
30
+
31
+ const data = $derived(id ? plotState.geomData(id) : [])
32
+ const xScale = $derived(plotState.xScale)
33
+ const yScale = $derived(plotState.yScale)
34
+ const colors = $derived(plotState.colors)
35
+ const patterns = $derived(plotState.patterns)
36
+
37
+ const areas = $derived.by(() => {
38
+ if (!data?.length || !xScale || !yScale) return []
39
+ const channels = { x, y, color, pattern }
40
+ if (options.stack) {
41
+ return buildStackedAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
42
+ }
43
+ return buildAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
44
+ })
45
+ </script>
46
+
47
+ {#if areas.length > 0}
48
+ <g data-plot-geom="area">
49
+ {#each areas as seg (seg.key ?? seg.d)}
50
+ <path
51
+ d={seg.d}
52
+ fill={seg.fill}
53
+ fill-opacity={seg.patternId ? 1 : (options.opacity ?? plotState.chartPreset.opacity.area)}
54
+ stroke={seg.stroke ?? 'none'}
55
+ data-plot-element="area"
56
+ />
57
+ {#if seg.patternId}
58
+ <path d={seg.d} fill="url(#{seg.patternId})" data-plot-element="area" />
59
+ {/if}
60
+ {/each}
61
+ <!-- Invisible hit circles for tooltip: one per data point -->
62
+ {#each data as d, i (`hover::${i}`)}
63
+ {@const px = typeof xScale?.bandwidth === 'function' ? (xScale(d[x]) ?? 0) + xScale.bandwidth() / 2 : (xScale?.(d[x]) ?? 0)}
64
+ {@const py = yScale?.(d[y]) ?? 0}
65
+ <circle
66
+ cx={px}
67
+ cy={py}
68
+ r="8"
69
+ fill="transparent"
70
+ stroke="none"
71
+ role="presentation"
72
+ data-plot-element="area-hover"
73
+ onmouseenter={() => plotState.setHovered(d)}
74
+ onmouseleave={() => plotState.clearHovered()}
75
+ />
76
+ {/each}
77
+ </g>
78
+ {/if}
@@ -0,0 +1,200 @@
1
+ <script>
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildGroupedBars, buildStackedBars, buildHorizontalBars } from './lib/bars.js'
4
+ import { keyboardNav } from '../lib/keyboard-nav.js'
5
+ import LabelPill from './LabelPill.svelte'
6
+
7
+ let {
8
+ x,
9
+ y,
10
+ color,
11
+ fill: fillProp,
12
+ pattern,
13
+ label = false,
14
+ stat = 'identity',
15
+ options = {},
16
+ filterable = false,
17
+ onselect = undefined,
18
+ keyboard = false
19
+ } = $props()
20
+
21
+ // `fill` is accepted as an alias for `color` (consistent with Arc.svelte)
22
+ const colorChannel = $derived(fillProp ?? color)
23
+
24
+ /**
25
+ * @param {Record<string, unknown>} data
26
+ * @param {string} defaultField
27
+ * @returns {string | null}
28
+ */
29
+ function resolveLabel(data, defaultField) {
30
+ if (!label) return null
31
+ if (label === true) return String(data[defaultField] ?? '')
32
+ if (typeof label === 'function') return String(label(data) ?? '')
33
+ if (typeof label === 'string') return String(data[label] ?? '')
34
+ return null
35
+ }
36
+
37
+ /**
38
+ * Pick white or dark text based on perceived luminance of a hex fill color.
39
+ * @param {string | undefined} hex
40
+ * @returns {string}
41
+ */
42
+ function contrastColor(hex) {
43
+ if (!hex || !hex.startsWith('#') || hex.length < 7) return 'white'
44
+ const r = parseInt(hex.slice(1, 3), 16) / 255
45
+ const g = parseInt(hex.slice(3, 5), 16) / 255
46
+ const b = parseInt(hex.slice(5, 7), 16) / 255
47
+ return 0.299 * r + 0.587 * g + 0.114 * b > 0.55 ? '#333' : 'white'
48
+ }
49
+
50
+ const plotState = getContext('plot-state')
51
+ const cf = getContext('crossfilter')
52
+ let id = $state(null)
53
+
54
+ onMount(() => {
55
+ id = plotState.registerGeom({
56
+ type: 'bar',
57
+ channels: { x, y, color: colorChannel, pattern },
58
+ stat,
59
+ options: { stack: options?.stack ?? false }
60
+ })
61
+ })
62
+ onDestroy(() => {
63
+ if (id) plotState.unregisterGeom(id)
64
+ })
65
+
66
+ $effect(() => {
67
+ if (id)
68
+ plotState.updateGeom(id, {
69
+ channels: { x, y, color: colorChannel, pattern },
70
+ stat,
71
+ options: { stack: options?.stack ?? false }
72
+ })
73
+ })
74
+
75
+ const data = $derived(id ? plotState.geomData(id) : [])
76
+ const xScale = $derived(plotState.xScale)
77
+ const yScale = $derived(plotState.yScale)
78
+ const colors = $derived(plotState.colors)
79
+ const patterns = $derived(plotState.patterns)
80
+ const effectiveOrientation = $derived(options.orientation ?? plotState.orientation)
81
+ const innerHeight = $derived(plotState.innerHeight)
82
+
83
+ const bars = $derived.by(() => {
84
+ if (!data?.length || !xScale || !yScale) return []
85
+ const channels = { x, y, color: colorChannel, pattern }
86
+ if (effectiveOrientation === 'horizontal') {
87
+ return buildHorizontalBars(data, channels, xScale, yScale, colors, innerHeight)
88
+ }
89
+ if (options.stack) {
90
+ return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
91
+ }
92
+ return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
93
+ })
94
+
95
+ /** @type {Record<string, boolean>} */
96
+ let dimmedByKey = $state({})
97
+
98
+ $effect(() => {
99
+ if (!cf) {
100
+ dimmedByKey = {}
101
+ return
102
+ }
103
+ // cf.version is a $state counter that increments on every filter mutation.
104
+ // Reading it here establishes a reactive dependency so the effect re-runs
105
+ // whenever any filter changes — including changes from sibling FilterBars.
106
+ void cf.version
107
+ const next = /** @type {Record<string, boolean>} */ ({})
108
+ for (const bar of bars) {
109
+ const dimmedByX = x ? cf.isDimmed(x, bar.data[x]) : false
110
+ const dimmedByY = y ? cf.isDimmed(y, bar.data[y]) : false
111
+ next[bar.key] = dimmedByX || dimmedByY
112
+ }
113
+ dimmedByKey = next
114
+ })
115
+
116
+ function handleBarClick(barX) {
117
+ if (!filterable || !x || !cf) return
118
+ cf.toggleCategorical(x, barX)
119
+ }
120
+ </script>
121
+
122
+ {#if bars.length > 0}
123
+ <g data-plot-geom="bar">
124
+ {#each bars as bar (bar.key)}
125
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
126
+ <rect
127
+ x={bar.x}
128
+ y={bar.y}
129
+ width={Math.max(0, bar.width)}
130
+ height={Math.max(0, bar.height)}
131
+ fill={bar.fill}
132
+ stroke={bar.stroke ?? 'none'}
133
+ stroke-width={bar.stroke ? 0.5 : 0}
134
+ data-plot-element="bar"
135
+ data-plot-value={bar.data[y]}
136
+ data-plot-category={bar.data[x]}
137
+ data-dimmed={dimmedByKey[bar.key] ? true : undefined}
138
+ style:cursor={filterable || onselect ? 'pointer' : undefined}
139
+ onclick={filterable && x ? () => { handleBarClick(bar.data[x]); onselect?.(bar.data) } : onselect ? () => onselect(bar.data) : undefined}
140
+ onkeydown={filterable && x
141
+ ? (e) => (e.key === 'Enter' || e.key === ' ') && handleBarClick(bar.data[x])
142
+ : onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(bar.data) : undefined}
143
+ role={filterable || onselect || keyboard ? 'button' : 'graphics-symbol'}
144
+ tabindex={filterable || onselect || keyboard ? 0 : undefined}
145
+ use:keyboardNav={keyboard}
146
+ aria-label="{bar.data[x]}: {bar.data[y]}"
147
+ onmouseenter={() => plotState.setHovered(bar.data)}
148
+ onmouseleave={() => plotState.clearHovered()}
149
+ >
150
+ <title>{bar.data[x]}: {bar.data[y]}</title>
151
+ </rect>
152
+ {#if bar.patternId}
153
+ <rect
154
+ x={bar.x}
155
+ y={bar.y}
156
+ width={Math.max(0, bar.width)}
157
+ height={Math.max(0, bar.height)}
158
+ fill="url(#{bar.patternId})"
159
+ pointer-events="none"
160
+ />
161
+ {/if}
162
+ {#if label}
163
+ {@const text = resolveLabel(bar.data, effectiveOrientation === 'horizontal' ? x : y)}
164
+ {#if text}
165
+ {#if effectiveOrientation === 'horizontal'}
166
+ {#if options.labelInside}
167
+ {@const estimatedWidth = text.length * 7 + 16}
168
+ {@const fitsInside = bar.width >= estimatedWidth}
169
+ <text
170
+ x={fitsInside ? bar.x + bar.width - 8 : bar.x + bar.width + 6}
171
+ y={bar.y + bar.height / 2}
172
+ dominant-baseline="central"
173
+ text-anchor={fitsInside ? 'end' : 'start'}
174
+ font-size="11"
175
+ font-weight="600"
176
+ fill={fitsInside ? contrastColor(bar.fill) : (bar.stroke ?? '#555')}
177
+ pointer-events="none"
178
+ data-plot-element="label"
179
+ >{text}</text>
180
+ {:else}
181
+ <LabelPill
182
+ x={bar.x + bar.width + (options.labelOffset ?? 8)}
183
+ y={bar.y + bar.height / 2}
184
+ {text}
185
+ color={bar.stroke ?? '#333'}
186
+ />
187
+ {/if}
188
+ {:else}
189
+ <LabelPill
190
+ x={bar.x + bar.width / 2}
191
+ y={bar.y + (options.labelOffset ?? -8)}
192
+ {text}
193
+ color={bar.stroke ?? '#333'}
194
+ />
195
+ {/if}
196
+ {/if}
197
+ {/if}
198
+ {/each}
199
+ </g>
200
+ {/if}