@rokkit/chart 1.0.0-next.151 → 1.0.0-next.158

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 (86) hide show
  1. package/dist/PlotState.svelte.d.ts +26 -0
  2. package/dist/index.d.ts +6 -1
  3. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +3 -5
  4. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +9 -0
  5. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +3 -4
  6. package/dist/lib/brewing/colors.d.ts +10 -1
  7. package/dist/lib/brewing/marks/points.d.ts +17 -2
  8. package/dist/lib/keyboard-nav.d.ts +15 -0
  9. package/dist/lib/plot/preset.d.ts +1 -1
  10. package/dist/lib/preset.d.ts +30 -0
  11. package/package.json +2 -1
  12. package/src/AnimatedPlot.svelte +375 -207
  13. package/src/Chart.svelte +81 -84
  14. package/src/ChartProvider.svelte +10 -0
  15. package/src/FacetPlot/Panel.svelte +30 -16
  16. package/src/FacetPlot.svelte +100 -76
  17. package/src/Plot/Area.svelte +26 -19
  18. package/src/Plot/Axis.svelte +81 -59
  19. package/src/Plot/Bar.svelte +47 -89
  20. package/src/Plot/Grid.svelte +23 -19
  21. package/src/Plot/Legend.svelte +213 -147
  22. package/src/Plot/Line.svelte +31 -21
  23. package/src/Plot/Point.svelte +35 -22
  24. package/src/Plot/Root.svelte +46 -91
  25. package/src/Plot/Timeline.svelte +82 -82
  26. package/src/Plot/Tooltip.svelte +68 -62
  27. package/src/Plot.svelte +290 -174
  28. package/src/PlotState.svelte.js +338 -265
  29. package/src/Sparkline.svelte +95 -56
  30. package/src/charts/AreaChart.svelte +22 -20
  31. package/src/charts/BarChart.svelte +23 -21
  32. package/src/charts/BoxPlot.svelte +15 -15
  33. package/src/charts/BubbleChart.svelte +17 -17
  34. package/src/charts/LineChart.svelte +20 -20
  35. package/src/charts/PieChart.svelte +30 -20
  36. package/src/charts/ScatterPlot.svelte +20 -19
  37. package/src/charts/ViolinPlot.svelte +15 -15
  38. package/src/crossfilter/CrossFilter.svelte +33 -29
  39. package/src/crossfilter/FilterBar.svelte +17 -25
  40. package/src/crossfilter/FilterHistogram.svelte +290 -0
  41. package/src/crossfilter/FilterSlider.svelte +69 -65
  42. package/src/crossfilter/createCrossFilter.svelte.js +94 -90
  43. package/src/geoms/Arc.svelte +114 -69
  44. package/src/geoms/Area.svelte +67 -39
  45. package/src/geoms/Bar.svelte +184 -126
  46. package/src/geoms/Box.svelte +101 -91
  47. package/src/geoms/LabelPill.svelte +11 -11
  48. package/src/geoms/Line.svelte +110 -86
  49. package/src/geoms/Point.svelte +130 -90
  50. package/src/geoms/Violin.svelte +51 -41
  51. package/src/geoms/lib/areas.js +122 -99
  52. package/src/geoms/lib/bars.js +195 -144
  53. package/src/index.js +21 -14
  54. package/src/lib/brewing/BoxBrewer.svelte.js +8 -50
  55. package/src/lib/brewing/CartesianBrewer.svelte.js +11 -7
  56. package/src/lib/brewing/PieBrewer.svelte.js +5 -5
  57. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  58. package/src/lib/brewing/ViolinBrewer.svelte.js +8 -49
  59. package/src/lib/brewing/brewer.svelte.js +242 -195
  60. package/src/lib/brewing/colors.js +34 -5
  61. package/src/lib/brewing/marks/arcs.js +28 -28
  62. package/src/lib/brewing/marks/areas.js +54 -41
  63. package/src/lib/brewing/marks/bars.js +34 -34
  64. package/src/lib/brewing/marks/boxes.js +51 -51
  65. package/src/lib/brewing/marks/lines.js +37 -30
  66. package/src/lib/brewing/marks/points.js +74 -26
  67. package/src/lib/brewing/marks/violins.js +57 -57
  68. package/src/lib/brewing/patterns.js +25 -11
  69. package/src/lib/brewing/scales.js +17 -17
  70. package/src/lib/brewing/stats.js +37 -29
  71. package/src/lib/brewing/symbols.js +1 -1
  72. package/src/lib/chart.js +2 -1
  73. package/src/lib/keyboard-nav.js +37 -0
  74. package/src/lib/plot/crossfilter.js +5 -5
  75. package/src/lib/plot/facet.js +30 -30
  76. package/src/lib/plot/frames.js +30 -29
  77. package/src/lib/plot/helpers.js +4 -4
  78. package/src/lib/plot/preset.js +48 -34
  79. package/src/lib/plot/scales.js +64 -39
  80. package/src/lib/plot/stat.js +47 -47
  81. package/src/lib/preset.js +41 -0
  82. package/src/patterns/DefinePatterns.svelte +24 -24
  83. package/src/patterns/README.md +3 -0
  84. package/src/patterns/patterns.js +328 -176
  85. package/src/patterns/scale.js +61 -32
  86. package/src/spec/chart-spec.js +64 -21
@@ -1,103 +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
- const fillChannel = $derived(fill ?? x)
10
+ // fill ?? x drives the colors map for both box interior and whisker strokes
11
+ const fillChannel = $derived(fill ?? x)
12
12
 
13
- onMount(() => {
14
- id = plotState.registerGeom({ type: 'box', channels: { x, y, color: fillChannel }, stat, options })
15
- })
16
- onDestroy(() => { if (id) plotState.unregisterGeom(id) })
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
+ })
17
24
 
18
- $effect(() => {
19
- if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
20
- })
25
+ $effect(() => {
26
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
27
+ })
21
28
 
22
- const data = $derived(id ? plotState.geomData(id) : [])
23
- const xScale = $derived(plotState.xScale)
24
- const yScale = $derived(plotState.yScale)
25
- const colors = $derived(plotState.colors)
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)
26
33
 
27
- const boxes = $derived.by(() => {
28
- if (!data?.length || !xScale || !yScale) return []
29
- return buildBoxes(data, { x, fill: fillChannel }, xScale, yScale, colors)
30
- })
34
+ const boxes = $derived.by(() => {
35
+ if (!data?.length || !xScale || !yScale) return []
36
+ return buildBoxes(data, { x, fill: fillChannel }, xScale, yScale, colors)
37
+ })
31
38
  </script>
32
39
 
33
40
  {#if boxes.length > 0}
34
- <g data-plot-geom="box">
35
- {#each boxes as box, i (`${String(box.cx) }::${ i}`)}
36
- {@const x0 = box.cx - box.width / 2}
37
- {@const xMid = box.cx}
38
- {@const xCap0 = box.cx - box.whiskerWidth / 2}
39
- {@const xCap1 = box.cx + box.whiskerWidth / 2}
40
- <!-- Box body (IQR): lighter fill shade -->
41
- <rect
42
- x={x0}
43
- y={box.q3}
44
- width={box.width}
45
- height={Math.max(0, box.q1 - box.q3)}
46
- fill={box.fill}
47
- fill-opacity="0.5"
48
- stroke={box.stroke}
49
- stroke-width="1"
50
- data-plot-element="box-body"
51
- />
52
- <!-- Median line: darker stroke shade -->
53
- <line
54
- x1={x0}
55
- y1={box.median}
56
- x2={x0 + box.width}
57
- y2={box.median}
58
- stroke={box.stroke}
59
- stroke-width="2"
60
- data-plot-element="box-median"
61
- />
62
- <!-- Lower whisker (q1 to iqr_min) -->
63
- <line
64
- x1={xMid}
65
- y1={box.q1}
66
- x2={xMid}
67
- y2={box.iqr_min}
68
- stroke={box.stroke}
69
- stroke-width="1"
70
- data-plot-element="box-whisker"
71
- />
72
- <!-- Upper whisker (q3 to iqr_max) -->
73
- <line
74
- x1={xMid}
75
- y1={box.q3}
76
- x2={xMid}
77
- y2={box.iqr_max}
78
- stroke={box.stroke}
79
- stroke-width="1"
80
- data-plot-element="box-whisker"
81
- />
82
- <!-- Lower whisker cap -->
83
- <line
84
- x1={xCap0}
85
- y1={box.iqr_min}
86
- x2={xCap1}
87
- y2={box.iqr_min}
88
- stroke={box.stroke}
89
- stroke-width="1"
90
- />
91
- <!-- Upper whisker cap -->
92
- <line
93
- x1={xCap0}
94
- y1={box.iqr_max}
95
- x2={xCap1}
96
- y2={box.iqr_max}
97
- stroke={box.stroke}
98
- stroke-width="1"
99
- />
100
- <!-- Outlier rendering deferred: buildBoxes does not compute outliers yet -->
101
- {/each}
102
- </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>
103
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,99 +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
- return typeof label === 'string' ? String(data[label] ?? '') : null
18
- }
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
+ }
19
30
 
20
- const plotState = getContext('plot-state')
21
- let id = $state(null)
31
+ const plotState = getContext('plot-state')
32
+ let id = $state(null)
22
33
 
23
- onMount(() => {
24
- id = plotState.registerGeom({ type: 'line', channels: { x, y, color, symbol: symbolField }, stat, options })
25
- })
26
- 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
+ })
27
45
 
28
- $effect(() => {
29
- if (id) plotState.updateGeom(id, { channels: { x, y, color, symbol: symbolField }, stat })
30
- })
46
+ $effect(() => {
47
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, symbol: symbolField }, stat })
48
+ })
31
49
 
32
- const data = $derived(id ? plotState.geomData(id) : [])
33
- const xScale = $derived(plotState.xScale)
34
- const yScale = $derived(plotState.yScale)
35
- const colors = $derived(plotState.colors)
36
- 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)
37
55
 
38
- const lines = $derived.by(() => {
39
- if (!data?.length || !xScale || !yScale) return []
40
- return buildLines(data, { x, y, color }, xScale, yScale, colors, options.curve)
41
- })
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
+ })
42
60
 
43
- const markerRadius = $derived(options.markerRadius ?? 4)
61
+ const markerRadius = $derived(options.markerRadius ?? 4)
44
62
  </script>
45
63
 
46
64
  {#if lines.length > 0}
47
- <g data-plot-geom="line">
48
- {#each lines as seg (seg.key ?? seg.d)}
49
- <path
50
- d={seg.d}
51
- fill="none"
52
- stroke={seg.stroke}
53
- stroke-width={options.strokeWidth ?? 2}
54
- stroke-linejoin="round"
55
- stroke-linecap="round"
56
- data-plot-element="line"
57
- />
58
- {#if symbolField && symbolMap}
59
- {#each seg.points as pt (`${pt.x}::${pt.y}`)}
60
- <path
61
- transform="translate({pt.x},{pt.y})"
62
- d={buildSymbolPath(symbolMap.get(pt.data[symbolField]) ?? 'circle', markerRadius)}
63
- fill={seg.stroke}
64
- stroke={seg.stroke}
65
- stroke-width="1"
66
- data-plot-element="line-marker"
67
- />
68
- {/each}
69
- {/if}
70
- {#if label}
71
- {#each seg.points as pt (`label::${pt.x}::${pt.y}`)}
72
- {@const text = resolveLabel(pt.data)}
73
- {#if text}
74
- <LabelPill
75
- x={pt.x + (options.labelOffset?.x ?? 0)}
76
- y={pt.y + (options.labelOffset?.y ?? -12)}
77
- {text}
78
- color={seg.stroke ?? '#333'}
79
- />
80
- {/if}
81
- {/each}
82
- {/if}
83
- <!-- Invisible hit areas for tooltip -->
84
- {#each seg.points as pt (`hover::${pt.x}::${pt.y}`)}
85
- <circle
86
- cx={pt.x}
87
- cy={pt.y}
88
- r="8"
89
- fill="transparent"
90
- stroke="none"
91
- role="presentation"
92
- data-plot-element="line-hover"
93
- onmouseenter={() => plotState.setHovered(pt.data)}
94
- onmouseleave={() => plotState.clearHovered()}
95
- />
96
- {/each}
97
- {/each}
98
- </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>
99
123
  {/if}