@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
@@ -1,101 +1,113 @@
1
1
  <script>
2
- import { getContext, onMount, onDestroy } from 'svelte'
3
- import { buildBoxes } from '../lib/brewing/marks/boxes.js'
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildBoxes } from '../lib/brewing/marks/boxes.js'
4
4
 
5
- let { x, y, fill, stat = 'boxplot', options = {} } = $props()
5
+ let { x, y, fill, stat = 'boxplot', options = {} } = $props()
6
6
 
7
- const plotState = getContext('plot-state')
8
- let id = $state(null)
7
+ const plotState = getContext('plot-state')
8
+ let id = $state(null)
9
9
 
10
- // fill ?? x drives the colors map for both box interior and whisker strokes
11
- onMount(() => {
12
- id = plotState.registerGeom({ type: 'box', channels: { x, y, color: fill ?? x }, stat, options })
13
- })
14
- onDestroy(() => { if (id) plotState.unregisterGeom(id) })
10
+ // fill ?? x drives the colors map for both box interior and whisker strokes
11
+ const fillChannel = $derived(fill ?? x)
15
12
 
16
- $effect(() => {
17
- if (id) plotState.updateGeom(id, { channels: { x, y, color: fill ?? x }, stat })
18
- })
13
+ onMount(() => {
14
+ id = plotState.registerGeom({
15
+ type: 'box',
16
+ channels: { x, y, color: fillChannel },
17
+ stat,
18
+ options
19
+ })
20
+ })
21
+ onDestroy(() => {
22
+ if (id) plotState.unregisterGeom(id)
23
+ })
19
24
 
20
- const data = $derived(id ? plotState.geomData(id) : [])
21
- const xScale = $derived(plotState.xScale)
22
- const yScale = $derived(plotState.yScale)
23
- const colors = $derived(plotState.colors)
25
+ $effect(() => {
26
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
27
+ })
24
28
 
25
- const boxes = $derived.by(() => {
26
- if (!data?.length || !xScale || !yScale) return []
27
- return buildBoxes(data, { x, fill: fill ?? x }, xScale, yScale, colors)
28
- })
29
+ const data = $derived(id ? plotState.geomData(id) : [])
30
+ const xScale = $derived(plotState.xScale)
31
+ const yScale = $derived(plotState.yScale)
32
+ const colors = $derived(plotState.colors)
33
+
34
+ const boxes = $derived.by(() => {
35
+ if (!data?.length || !xScale || !yScale) return []
36
+ return buildBoxes(data, { x, fill: fillChannel }, xScale, yScale, colors)
37
+ })
29
38
  </script>
30
39
 
31
40
  {#if boxes.length > 0}
32
- <g data-plot-geom="box">
33
- {#each boxes as box, i (`${String(box.cx) }::${ i}`)}
34
- {@const x0 = box.cx - box.width / 2}
35
- {@const xMid = box.cx}
36
- {@const xCap0 = box.cx - box.whiskerWidth / 2}
37
- {@const xCap1 = box.cx + box.whiskerWidth / 2}
38
- <!-- Box body (IQR): lighter fill shade -->
39
- <rect
40
- x={x0}
41
- y={box.q3}
42
- width={box.width}
43
- height={Math.max(0, box.q1 - box.q3)}
44
- fill={box.fill}
45
- fill-opacity="0.5"
46
- stroke={box.stroke}
47
- stroke-width="1"
48
- data-plot-element="box-body"
49
- />
50
- <!-- Median line: darker stroke shade -->
51
- <line
52
- x1={x0}
53
- y1={box.median}
54
- x2={x0 + box.width}
55
- y2={box.median}
56
- stroke={box.stroke}
57
- stroke-width="2"
58
- data-plot-element="box-median"
59
- />
60
- <!-- Lower whisker (q1 to iqr_min) -->
61
- <line
62
- x1={xMid}
63
- y1={box.q1}
64
- x2={xMid}
65
- y2={box.iqr_min}
66
- stroke={box.stroke}
67
- stroke-width="1"
68
- data-plot-element="box-whisker"
69
- />
70
- <!-- Upper whisker (q3 to iqr_max) -->
71
- <line
72
- x1={xMid}
73
- y1={box.q3}
74
- x2={xMid}
75
- y2={box.iqr_max}
76
- stroke={box.stroke}
77
- stroke-width="1"
78
- data-plot-element="box-whisker"
79
- />
80
- <!-- Lower whisker cap -->
81
- <line
82
- x1={xCap0}
83
- y1={box.iqr_min}
84
- x2={xCap1}
85
- y2={box.iqr_min}
86
- stroke={box.stroke}
87
- stroke-width="1"
88
- />
89
- <!-- Upper whisker cap -->
90
- <line
91
- x1={xCap0}
92
- y1={box.iqr_max}
93
- x2={xCap1}
94
- y2={box.iqr_max}
95
- stroke={box.stroke}
96
- stroke-width="1"
97
- />
98
- <!-- Outlier rendering deferred: buildBoxes does not compute outliers yet -->
99
- {/each}
100
- </g>
41
+ <g data-plot-geom="box">
42
+ {#each boxes as box, i (`${String(box.cx)}::${i}`)}
43
+ {@const x0 = box.cx - box.width / 2}
44
+ {@const xMid = box.cx}
45
+ {@const xCap0 = box.cx - box.whiskerWidth / 2}
46
+ {@const xCap1 = box.cx + box.whiskerWidth / 2}
47
+ <!-- Box body (IQR): lighter fill shade -->
48
+ <rect
49
+ x={x0}
50
+ y={box.q3}
51
+ width={box.width}
52
+ height={Math.max(0, box.q1 - box.q3)}
53
+ fill={box.fill}
54
+ fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.box}
55
+ stroke={box.stroke}
56
+ stroke-width="1"
57
+ data-plot-element="box-body"
58
+ role="presentation"
59
+ onmouseenter={() => plotState.setHovered(box.data)}
60
+ onmouseleave={() => plotState.clearHovered()}
61
+ />
62
+ <!-- Median line: darker stroke shade -->
63
+ <line
64
+ x1={x0}
65
+ y1={box.median}
66
+ x2={x0 + box.width}
67
+ y2={box.median}
68
+ stroke={box.stroke}
69
+ stroke-width="2"
70
+ data-plot-element="box-median"
71
+ />
72
+ <!-- Lower whisker (q1 to iqr_min) -->
73
+ <line
74
+ x1={xMid}
75
+ y1={box.q1}
76
+ x2={xMid}
77
+ y2={box.iqr_min}
78
+ stroke={box.stroke}
79
+ stroke-width="1"
80
+ data-plot-element="box-whisker"
81
+ />
82
+ <!-- Upper whisker (q3 to iqr_max) -->
83
+ <line
84
+ x1={xMid}
85
+ y1={box.q3}
86
+ x2={xMid}
87
+ y2={box.iqr_max}
88
+ stroke={box.stroke}
89
+ stroke-width="1"
90
+ data-plot-element="box-whisker"
91
+ />
92
+ <!-- Lower whisker cap -->
93
+ <line
94
+ x1={xCap0}
95
+ y1={box.iqr_min}
96
+ x2={xCap1}
97
+ y2={box.iqr_min}
98
+ stroke={box.stroke}
99
+ stroke-width="1"
100
+ />
101
+ <!-- Upper whisker cap -->
102
+ <line
103
+ x1={xCap0}
104
+ y1={box.iqr_max}
105
+ x2={xCap1}
106
+ y2={box.iqr_max}
107
+ stroke={box.stroke}
108
+ stroke-width="1"
109
+ />
110
+ <!-- Outlier rendering deferred: buildBoxes does not compute outliers yet -->
111
+ {/each}
112
+ </g>
101
113
  {/if}
@@ -1,17 +1,17 @@
1
1
  <script>
2
- /** @type {{ x: number, y: number, text: string, color?: string }} */
3
- let { x, y, text, color = '#333' } = $props()
2
+ /** @type {{ x: number, y: number, text: string, color?: string }} */
3
+ let { x, y, text, color = '#333' } = $props()
4
4
 
5
- const w = $derived(Math.max(36, String(text).length * 7 + 12))
5
+ const w = $derived(Math.max(36, String(text).length * 7 + 12))
6
6
  </script>
7
7
 
8
8
  <g transform="translate({x},{y})" pointer-events="none" data-plot-element="label">
9
- <rect x={-w / 2} y="-9" width={w} height="18" rx="4" fill="white" fill-opacity="0.82" />
10
- <text
11
- text-anchor="middle"
12
- dominant-baseline="central"
13
- font-size="11"
14
- font-weight="600"
15
- fill={color}
16
- >{text}</text>
9
+ <rect x={-w / 2} y="-9" width={w} height="18" rx="4" fill="white" fill-opacity="0.82" />
10
+ <text
11
+ text-anchor="middle"
12
+ dominant-baseline="central"
13
+ font-size="11"
14
+ font-weight="600"
15
+ fill={color}>{text}</text
16
+ >
17
17
  </g>
@@ -1,100 +1,123 @@
1
1
  <script>
2
- import { getContext, onMount, onDestroy } from 'svelte'
3
- import { buildLines } from '../lib/brewing/marks/lines.js'
4
- import { buildSymbolPath } from '../lib/brewing/marks/points.js'
5
- import LabelPill from './LabelPill.svelte'
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildLines } from '../lib/brewing/marks/lines.js'
4
+ import { buildSymbolPath } from '../lib/brewing/marks/points.js'
5
+ import { keyboardNav } from '../lib/keyboard-nav.js'
6
+ import LabelPill from './LabelPill.svelte'
6
7
 
7
- let { x, y, color, symbol: symbolField, label = false, stat = 'identity', options = {} } = $props()
8
+ let {
9
+ x,
10
+ y,
11
+ color,
12
+ symbol: symbolField,
13
+ label = false,
14
+ stat = 'identity',
15
+ options = {},
16
+ onselect = undefined,
17
+ keyboard = false
18
+ } = $props()
8
19
 
9
- /**
10
- * @param {Record<string, unknown>} data
11
- * @returns {string | null}
12
- */
13
- function resolveLabel(data) {
14
- if (!label) return null
15
- if (label === true) return String(data[y] ?? '')
16
- if (typeof label === 'function') return String(label(data) ?? '')
17
- if (typeof label === 'string') return String(data[label] ?? '')
18
- return null
19
- }
20
+ /**
21
+ * @param {Record<string, unknown>} data
22
+ * @returns {string | null}
23
+ */
24
+ function resolveLabel(data) {
25
+ if (!label) return null
26
+ if (label === true) return String(data[y] ?? '')
27
+ if (typeof label === 'function') return String(label(data) ?? '')
28
+ return typeof label === 'string' ? String(data[label] ?? '') : null
29
+ }
20
30
 
21
- const plotState = getContext('plot-state')
22
- let id = $state(null)
31
+ const plotState = getContext('plot-state')
32
+ let id = $state(null)
23
33
 
24
- onMount(() => {
25
- id = plotState.registerGeom({ type: 'line', channels: { x, y, color, symbol: symbolField }, stat, options })
26
- })
27
- onDestroy(() => { if (id) plotState.unregisterGeom(id) })
34
+ onMount(() => {
35
+ id = plotState.registerGeom({
36
+ type: 'line',
37
+ channels: { x, y, color, symbol: symbolField },
38
+ stat,
39
+ options
40
+ })
41
+ })
42
+ onDestroy(() => {
43
+ if (id) plotState.unregisterGeom(id)
44
+ })
28
45
 
29
- $effect(() => {
30
- if (id) plotState.updateGeom(id, { channels: { x, y, color, symbol: symbolField }, stat })
31
- })
46
+ $effect(() => {
47
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, symbol: symbolField }, stat })
48
+ })
32
49
 
33
- const data = $derived(id ? plotState.geomData(id) : [])
34
- const xScale = $derived(plotState.xScale)
35
- const yScale = $derived(plotState.yScale)
36
- const colors = $derived(plotState.colors)
37
- const symbolMap = $derived(plotState.symbols)
50
+ const data = $derived(id ? plotState.geomData(id) : [])
51
+ const xScale = $derived(plotState.xScale)
52
+ const yScale = $derived(plotState.yScale)
53
+ const colors = $derived(plotState.colors)
54
+ const symbolMap = $derived(plotState.symbols)
38
55
 
39
- const lines = $derived.by(() => {
40
- if (!data?.length || !xScale || !yScale) return []
41
- return buildLines(data, { x, y, color }, xScale, yScale, colors, options.curve)
42
- })
56
+ const lines = $derived.by(() => {
57
+ if (!data?.length || !xScale || !yScale) return []
58
+ return buildLines(data, { x, y, color }, xScale, yScale, colors, options.curve)
59
+ })
43
60
 
44
- const markerRadius = $derived(options.markerRadius ?? 4)
61
+ const markerRadius = $derived(options.markerRadius ?? 4)
45
62
  </script>
46
63
 
47
64
  {#if lines.length > 0}
48
- <g data-plot-geom="line">
49
- {#each lines as seg (seg.key ?? seg.d)}
50
- <path
51
- d={seg.d}
52
- fill="none"
53
- stroke={seg.stroke}
54
- stroke-width={options.strokeWidth ?? 2}
55
- stroke-linejoin="round"
56
- stroke-linecap="round"
57
- data-plot-element="line"
58
- />
59
- {#if symbolField && symbolMap}
60
- {#each seg.points as pt (`${pt.x}::${pt.y}`)}
61
- <path
62
- transform="translate({pt.x},{pt.y})"
63
- d={buildSymbolPath(symbolMap.get(pt.data[symbolField]) ?? 'circle', markerRadius)}
64
- fill={seg.stroke}
65
- stroke={seg.stroke}
66
- stroke-width="1"
67
- data-plot-element="line-marker"
68
- />
69
- {/each}
70
- {/if}
71
- {#if label}
72
- {#each seg.points as pt (`label::${pt.x}::${pt.y}`)}
73
- {@const text = resolveLabel(pt.data)}
74
- {#if text}
75
- <LabelPill
76
- x={pt.x + (options.labelOffset?.x ?? 0)}
77
- y={pt.y + (options.labelOffset?.y ?? -12)}
78
- {text}
79
- color={seg.stroke ?? '#333'}
80
- />
81
- {/if}
82
- {/each}
83
- {/if}
84
- <!-- Invisible hit areas for tooltip -->
85
- {#each seg.points as pt (`hover::${pt.x}::${pt.y}`)}
86
- <circle
87
- cx={pt.x}
88
- cy={pt.y}
89
- r="8"
90
- fill="transparent"
91
- stroke="none"
92
- role="presentation"
93
- data-plot-element="line-hover"
94
- onmouseenter={() => plotState.setHovered(pt.data)}
95
- onmouseleave={() => plotState.clearHovered()}
96
- />
97
- {/each}
98
- {/each}
99
- </g>
65
+ <g data-plot-geom="line">
66
+ {#each lines as seg (seg.key ?? seg.d)}
67
+ <path
68
+ d={seg.d}
69
+ fill="none"
70
+ stroke={seg.stroke}
71
+ stroke-width={options.strokeWidth ?? 2}
72
+ stroke-linejoin="round"
73
+ stroke-linecap="round"
74
+ data-plot-element="line"
75
+ />
76
+ {#if symbolField && symbolMap}
77
+ {#each seg.points as pt (`${pt.x}::${pt.y}`)}
78
+ <path
79
+ transform="translate({pt.x},{pt.y})"
80
+ d={buildSymbolPath(symbolMap.get(pt.data[symbolField]) ?? 'circle', markerRadius)}
81
+ fill={seg.stroke}
82
+ stroke={seg.stroke}
83
+ stroke-width="1"
84
+ data-plot-element="line-marker"
85
+ />
86
+ {/each}
87
+ {/if}
88
+ {#if label}
89
+ {#each seg.points as pt (`label::${pt.x}::${pt.y}`)}
90
+ {@const text = resolveLabel(pt.data)}
91
+ {#if text}
92
+ <LabelPill
93
+ x={pt.x + (options.labelOffset?.x ?? 0)}
94
+ y={pt.y + (options.labelOffset?.y ?? -12)}
95
+ {text}
96
+ color={seg.stroke ?? '#333'}
97
+ />
98
+ {/if}
99
+ {/each}
100
+ {/if}
101
+ <!-- Invisible hit areas for tooltip and selection -->
102
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
103
+ {#each seg.points as pt (`hover::${pt.x}::${pt.y}`)}
104
+ <circle
105
+ cx={pt.x}
106
+ cy={pt.y}
107
+ r="8"
108
+ fill="transparent"
109
+ stroke="none"
110
+ role={onselect || keyboard ? 'button' : 'presentation'}
111
+ tabindex={onselect || keyboard ? 0 : undefined}
112
+ style:cursor={onselect ? 'pointer' : undefined}
113
+ data-plot-element="line-hover"
114
+ onmouseenter={() => plotState.setHovered(pt.data)}
115
+ onmouseleave={() => plotState.clearHovered()}
116
+ onclick={onselect ? () => onselect(pt.data) : undefined}
117
+ onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
118
+ use:keyboardNav={keyboard}
119
+ />
120
+ {/each}
121
+ {/each}
122
+ </g>
100
123
  {/if}