@rokkit/chart 1.0.0-next.151 → 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 (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,96 +1,54 @@
1
1
  <script>
2
2
  import { getContext } from 'svelte'
3
- import { ChartBrewer } from '../lib/brewing/index.svelte.js'
4
3
 
5
- let {
6
- x = null,
7
- y = null,
8
- fill = null,
9
- color = '#4682b4',
10
- opacity = 1,
11
- animationDuration = 300,
12
- onClick = null
13
- } = $props()
14
-
15
- // Get brewer from context
16
- const brewer = getContext('chart-brewer')
17
-
18
- // Set field mappings in the brewer
19
- $effect(() => {
20
- brewer.setFields({
21
- x,
22
- y,
23
- color: fill
4
+ let { data = undefined, x = undefined, y = undefined, fill = 'steelblue', opacity = 1 } = $props()
5
+
6
+ const state = getContext('plot-state')
7
+
8
+ const bars = $derived.by(() => {
9
+ if (!state?.xScale || !state?.yScale) return []
10
+ const xScale = state.xScale
11
+ const yScale = state.yScale
12
+ const innerHeight = state.innerHeight
13
+ const src = data ?? []
14
+ if (!src.length) return []
15
+
16
+ const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 20
17
+ const padding = typeof xScale.bandwidth === 'function' ? bw * 0.05 : 0
18
+
19
+ // Baseline at y=0 when domain spans zero (supports negative bars)
20
+ const yDomain = typeof yScale.bandwidth !== 'function' ? yScale.domain?.() : null
21
+ const baseline =
22
+ yDomain && yDomain[0] <= 0 && yDomain[yDomain.length - 1] >= 0
23
+ ? (yScale(0) ?? innerHeight)
24
+ : innerHeight
25
+
26
+ return src.map((d) => {
27
+ const xVal = x ? d[x] : d
28
+ const yVal = y ? d[y] : d
29
+ const xPos = (xScale(xVal) ?? 0) + padding
30
+ const yPos = yScale(yVal) ?? baseline
31
+ return {
32
+ x: xPos,
33
+ y: Math.min(yPos, baseline),
34
+ width: bw * 0.9,
35
+ height: Math.abs(baseline - yPos),
36
+ label: `${xVal}: ${yVal}`
37
+ }
24
38
  })
25
-
26
- // Ensure scales are updated
27
- brewer.createScales()
28
39
  })
29
-
30
- // Compute bars whenever data or fields change
31
- let bars = $derived(brewer.createBars())
32
-
33
- // Animation transition values
34
- let initialY = $state(0)
35
- let initialHeight = $state(0)
36
-
37
- // Handle resetting animation state for new bars
38
- $effect(() => {
39
- if (bars && bars.length > 0) {
40
- initialY = brewer.getDimensions().innerHeight
41
- initialHeight = 0
42
-
43
- // Reset to actual positions after a delay
44
- setTimeout(() => {
45
- initialY = 0
46
- initialHeight = 0
47
- }, 10)
48
- }
49
- })
50
-
51
- // Handle bar click
52
- function handleClick(event, bar) {
53
- if (onClick) onClick(bar.data, event)
54
- }
55
40
  </script>
56
41
 
57
- {#if bars && bars.length > 0}
58
- <g class="chart-bars" data-plot-type="bar">
59
- {#each bars as bar, i (bar.data[x])}
60
- {@const barY = initialY > 0 ? brewer.getDimensions().innerHeight : bar.y}
61
- {@const barHeight = initialHeight > 0 ? 0 : bar.height}
62
-
63
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions, a11y_click_events_have_key_events -->
64
- <rect
65
- class="bar"
66
- x={bar.x}
67
- y={barY}
68
- width={bar.width}
69
- height={barHeight}
70
- fill={bar.color}
71
- {opacity}
72
- onclick={(event) => handleClick(event, bar)}
73
- onmouseenter={(event) => {
74
- event.target.setAttribute('opacity', Math.min(opacity + 0.2, 1))
75
- }}
76
- onmouseleave={(event) => {
77
- event.target.setAttribute('opacity', opacity)
78
- }}
79
- style="transition: y {animationDuration}ms ease, height {animationDuration}ms ease;"
80
- role="graphics-symbol"
81
- aria-label="Bar representing {bar.data[x]} with value {bar.data[y]}"
82
- data-plot-element="bar"
83
- data-plot-value={bar.data[y]}
84
- data-plot-category={bar.data[x]}
85
- >
86
- <title>{bar.data[x]}: {bar.data[y]}</title>
87
- </rect>
88
- {/each}
89
- </g>
90
- {/if}
91
-
92
- <style>
93
- .chart-bars .bar {
94
- cursor: pointer;
95
- }
96
- </style>
42
+ {#each bars as bar, i (i)}
43
+ <rect
44
+ x={bar.x}
45
+ y={bar.y}
46
+ width={bar.width}
47
+ height={bar.height}
48
+ {fill}
49
+ {opacity}
50
+ data-plot-element="bar"
51
+ role="graphics-symbol"
52
+ aria-label={bar.label}
53
+ />
54
+ {/each}
@@ -1,30 +1,34 @@
1
1
  <script>
2
- import { getContext } from 'svelte'
2
+ import { getContext } from 'svelte'
3
3
 
4
- const state = getContext('plot-state')
4
+ const state = getContext('plot-state')
5
5
 
6
- const xGridLines = $derived.by(() => {
7
- const s = state.xScale
8
- if (!s || typeof s.bandwidth !== 'function') return []
9
- return s.domain().map((val) => ({ pos: (s(val) ?? 0) + s.bandwidth() / 2 }))
10
- })
6
+ const xGridLines = $derived.by(() => {
7
+ const s = state.xScale
8
+ if (!s || typeof s.bandwidth !== 'function') return []
9
+ return s.domain().map((val) => ({ pos: (s(val) ?? 0) + s.bandwidth() / 2 }))
10
+ })
11
11
 
12
- const yGridLines = $derived.by(() => {
13
- const s = state.yScale
14
- if (!s || typeof s.ticks !== 'function') return []
15
- return s.ticks(6).map((val) => ({ pos: s(val) }))
16
- })
12
+ const yGridLines = $derived.by(() => {
13
+ const s = state.yScale
14
+ if (!s || typeof s.ticks !== 'function') return []
15
+ return s.ticks(6).map((val) => ({ pos: s(val) }))
16
+ })
17
17
  </script>
18
18
 
19
19
  <g class="grid" data-plot-grid>
20
- {#each yGridLines as line (line.pos)}
21
- <line x1="0" y1={line.pos} x2={state.innerWidth} y2={line.pos} data-plot-grid-line />
22
- {/each}
23
- {#each xGridLines as line (line.pos)}
24
- <line x1={line.pos} y1="0" x2={line.pos} y2={state.innerHeight} data-plot-grid-line="x" />
25
- {/each}
20
+ {#each yGridLines as line (line.pos)}
21
+ <line x1="0" y1={line.pos} x2={state.innerWidth} y2={line.pos} data-plot-grid-line />
22
+ {/each}
23
+ {#each xGridLines as line (line.pos)}
24
+ <line x1={line.pos} y1="0" x2={line.pos} y2={state.innerHeight} data-plot-grid-line="x" />
25
+ {/each}
26
26
  </g>
27
27
 
28
28
  <style>
29
- [data-plot-grid-line] { stroke: var(--chart-grid-color, currentColor); opacity: 0.15; stroke-dasharray: 2 4; }
29
+ [data-plot-grid-line] {
30
+ stroke: var(--chart-grid-color, currentColor);
31
+ opacity: 0.15;
32
+ stroke-dasharray: 2 4;
33
+ }
30
34
  </style>
@@ -1,167 +1,233 @@
1
1
  <script>
2
- import { getContext } from 'svelte'
3
- import { toPatternId } from '../lib/brewing/patterns.js'
4
- import { buildSymbolPath } from '../lib/brewing/marks/points.js'
2
+ import { getContext } from 'svelte'
3
+ import { toPatternId } from '../lib/brewing/patterns.js'
4
+ import { buildSymbolPath } from '../lib/brewing/marks/points.js'
5
5
 
6
- /** @type {Record<string, string>} */
7
- let { labels = {} } = $props()
6
+ /** @type {Record<string, string>} */
7
+ let { labels = {} } = $props()
8
8
 
9
- const state = getContext('plot-state')
9
+ const state = getContext('plot-state')
10
10
 
11
- const isCategorical = $derived(state.colorScaleType === 'categorical')
12
- const isLineGeom = $derived(state.geomTypes?.has('line') ?? false)
13
- const isPointGeom = $derived(state.geomTypes?.has('point') ?? false)
14
- const hasSymbols = $derived((state.symbols?.size ?? 0) > 0)
11
+ const isCategorical = $derived(state.colorScaleType === 'categorical')
12
+ const isLineGeom = $derived(state.geomTypes?.has('line') ?? false)
13
+ const isPointGeom = $derived(state.geomTypes?.has('point') ?? false)
14
+ const hasSymbols = $derived((state.symbols?.size ?? 0) > 0)
15
15
 
16
- // Split conditions
17
- const splitPattern = $derived(
18
- !!state.colorField && !!state.patternField && state.colorField !== state.patternField
19
- )
20
- const splitSymbol = $derived(
21
- hasSymbols && !!state.colorField && !!state.symbolField && state.colorField !== state.symbolField
22
- )
23
- const symbolOnly = $derived(hasSymbols && !state.colorField)
16
+ // Split conditions
17
+ const splitPattern = $derived(
18
+ !!state.colorField && !!state.patternField && state.colorField !== state.patternField
19
+ )
20
+ const splitSymbol = $derived(
21
+ hasSymbols &&
22
+ !!state.colorField &&
23
+ !!state.symbolField &&
24
+ state.colorField !== state.symbolField
25
+ )
26
+ const symbolOnly = $derived(hasSymbols && !state.colorField)
24
27
 
25
- // Color section items — combined with same-field pattern/symbol overlays
26
- const colorItems = $derived(
27
- [...(state.colors?.entries() ?? [])].map(([key, entry]) => ({
28
- key,
29
- label: labels[String(key)] ?? String(key),
30
- fill: entry.fill,
31
- stroke: entry.stroke,
32
- patternId: !splitPattern && state.patterns?.has(key) ? toPatternId(String(key)) : null,
33
- symbolShape: !splitSymbol && state.symbols?.get(key) ? state.symbols.get(key) : null
34
- }))
35
- )
28
+ // Color section items — combined with same-field pattern/symbol overlays
29
+ const colorItems = $derived(
30
+ [...(state.colors?.entries() ?? [])].map(([key, entry]) => ({
31
+ key,
32
+ label: labels[String(key)] ?? String(key),
33
+ fill: entry.fill,
34
+ stroke: entry.stroke,
35
+ patternId: !splitPattern && state.patterns?.has(key) ? toPatternId(String(key)) : null,
36
+ symbolShape: !splitSymbol && state.symbols?.get(key) ? state.symbols.get(key) : null
37
+ }))
38
+ )
36
39
 
37
- // Pattern section — only when pattern encodes a different field than color
38
- const patternItems = $derived(
39
- splitPattern
40
- ? [...(state.patterns?.entries() ?? [])].map(([key]) => ({
41
- key,
42
- label: labels[String(key)] ?? String(key),
43
- patternId: toPatternId(String(key))
44
- }))
45
- : []
46
- )
40
+ // Pattern section — only when pattern encodes a different field than color
41
+ const patternItems = $derived(
42
+ splitPattern
43
+ ? [...(state.patterns?.entries() ?? [])].map(([key]) => ({
44
+ key,
45
+ label: labels[String(key)] ?? String(key),
46
+ patternId: toPatternId(String(key))
47
+ }))
48
+ : []
49
+ )
47
50
 
48
- // Symbol section — only when symbol encodes a different field than color, or symbol-only
49
- const symbolItems = $derived(
50
- splitSymbol || symbolOnly
51
- ? [...(state.symbols?.entries() ?? [])].map(([key, shape]) => ({
52
- key,
53
- label: labels[String(key)] ?? String(key),
54
- shape,
55
- fill: state.colors?.get(key)?.fill ?? '#888',
56
- stroke: state.colors?.get(key)?.stroke ?? '#888'
57
- }))
58
- : []
59
- )
51
+ // Symbol section — only when symbol encodes a different field than color, or symbol-only
52
+ const symbolItems = $derived(
53
+ splitSymbol || symbolOnly
54
+ ? [...(state.symbols?.entries() ?? [])].map(([key, shape]) => ({
55
+ key,
56
+ label: labels[String(key)] ?? String(key),
57
+ shape,
58
+ fill: state.colors?.get(key)?.fill ?? '#888',
59
+ stroke: state.colors?.get(key)?.stroke ?? '#888'
60
+ }))
61
+ : []
62
+ )
60
63
 
61
- const gradientStyle = $derived.by(() => {
62
- if (isCategorical) return ''
63
- return `background: linear-gradient(to right, #cfe2f3, #084594)`
64
- })
64
+ const gradientStyle = $derived.by(() => {
65
+ if (isCategorical) return ''
66
+ return `background: linear-gradient(to right, #cfe2f3, #084594)`
67
+ })
65
68
  </script>
66
69
 
67
70
  {#if isCategorical}
68
- <div class="legend-root" data-plot-legend>
71
+ <div class="legend-root" data-plot-legend>
72
+ <!-- Symbol-only: no color field, just symbol shapes -->
73
+ {#if symbolOnly}
74
+ <div class="legend categorical">
75
+ {#each symbolItems as item (item.key)}
76
+ <div class="legend-item" data-plot-legend-item>
77
+ <svg width="14" height="14" data-plot-legend-swatch>
78
+ <path
79
+ transform="translate(7,7)"
80
+ d={buildSymbolPath(item.shape, 5)}
81
+ fill={item.fill}
82
+ />
83
+ </svg>
84
+ <span class="label" data-plot-legend-label>{item.label}</span>
85
+ </div>
86
+ {/each}
87
+ </div>
69
88
 
70
- <!-- Symbol-only: no color field, just symbol shapes -->
71
- {#if symbolOnly}
72
- <div class="legend categorical">
73
- {#each symbolItems as item (item.key)}
74
- <div class="legend-item" data-plot-legend-item>
75
- <svg width="14" height="14" data-plot-legend-swatch>
76
- <path transform="translate(7,7)" d={buildSymbolPath(item.shape, 5)} fill={item.fill} />
77
- </svg>
78
- <span class="label" data-plot-legend-label>{item.label}</span>
79
- </div>
80
- {/each}
81
- </div>
89
+ <!-- Color section (line, point, or fill swatches) -->
90
+ {:else if colorItems.length > 0}
91
+ <div class="legend categorical">
92
+ {#each colorItems as item (item.key)}
93
+ <div class="legend-item" data-plot-legend-item>
94
+ {#if isLineGeom}
95
+ <!-- Line swatch with optional combined symbol -->
96
+ <svg width="24" height="14" data-plot-legend-swatch>
97
+ <line
98
+ x1="2"
99
+ y1="7"
100
+ x2="22"
101
+ y2="7"
102
+ stroke={item.stroke}
103
+ stroke-width="2"
104
+ stroke-linecap="round"
105
+ />
106
+ {#if item.symbolShape}
107
+ <path
108
+ transform="translate(12,7)"
109
+ d={buildSymbolPath(item.symbolShape, 4)}
110
+ fill={item.stroke}
111
+ />
112
+ {/if}
113
+ </svg>
114
+ {:else if isPointGeom && item.symbolShape}
115
+ <!-- Symbol shape swatch for scatter -->
116
+ <svg width="14" height="14" data-plot-legend-swatch>
117
+ <path
118
+ transform="translate(7,7)"
119
+ d={buildSymbolPath(item.symbolShape, 5)}
120
+ fill={item.fill}
121
+ />
122
+ </svg>
123
+ {:else if item.patternId}
124
+ <!-- Fill + pattern overlay -->
125
+ <svg width="14" height="14" data-plot-legend-swatch>
126
+ <rect width="14" height="14" fill={item.fill} />
127
+ <rect width="14" height="14" fill="url(#{item.patternId})" />
128
+ </svg>
129
+ {:else}
130
+ <!-- Plain fill swatch -->
131
+ <span class="swatch" style:background-color={item.fill} data-plot-legend-swatch
132
+ ></span>
133
+ {/if}
134
+ <span class="label" data-plot-legend-label>{item.label}</span>
135
+ </div>
136
+ {/each}
137
+ </div>
138
+ {/if}
82
139
 
83
- <!-- Color section (line, point, or fill swatches) -->
84
- {:else if colorItems.length > 0}
85
- <div class="legend categorical">
86
- {#each colorItems as item (item.key)}
87
- <div class="legend-item" data-plot-legend-item>
88
- {#if isLineGeom}
89
- <!-- Line swatch with optional combined symbol -->
90
- <svg width="24" height="14" data-plot-legend-swatch>
91
- <line x1="2" y1="7" x2="22" y2="7" stroke={item.stroke} stroke-width="2" stroke-linecap="round" />
92
- {#if item.symbolShape}
93
- <path transform="translate(12,7)" d={buildSymbolPath(item.symbolShape, 4)} fill={item.stroke} />
94
- {/if}
95
- </svg>
96
- {:else if isPointGeom && item.symbolShape}
97
- <!-- Symbol shape swatch for scatter -->
98
- <svg width="14" height="14" data-plot-legend-swatch>
99
- <path transform="translate(7,7)" d={buildSymbolPath(item.symbolShape, 5)} fill={item.fill} />
100
- </svg>
101
- {:else if item.patternId}
102
- <!-- Fill + pattern overlay -->
103
- <svg width="14" height="14" data-plot-legend-swatch>
104
- <rect width="14" height="14" fill={item.fill} />
105
- <rect width="14" height="14" fill="url(#{item.patternId})" />
106
- </svg>
107
- {:else}
108
- <!-- Plain fill swatch -->
109
- <span class="swatch" style:background-color={item.fill} data-plot-legend-swatch></span>
110
- {/if}
111
- <span class="label" data-plot-legend-label>{item.label}</span>
112
- </div>
113
- {/each}
114
- </div>
115
- {/if}
140
+ <!-- Pattern section (different field from color) -->
141
+ {#if patternItems.length > 0}
142
+ <div class="legend categorical legend-section">
143
+ {#each patternItems as item (item.key)}
144
+ <div class="legend-item" data-plot-legend-item>
145
+ <svg width="14" height="14" data-plot-legend-swatch>
146
+ <rect width="14" height="14" fill="var(--color-surface-z2, #ccc)" />
147
+ <rect width="14" height="14" fill="url(#{item.patternId})" />
148
+ </svg>
149
+ <span class="label" data-plot-legend-label>{item.label}</span>
150
+ </div>
151
+ {/each}
152
+ </div>
153
+ {/if}
116
154
 
117
- <!-- Pattern section (different field from color) -->
118
- {#if patternItems.length > 0}
119
- <div class="legend categorical legend-section">
120
- {#each patternItems as item (item.key)}
121
- <div class="legend-item" data-plot-legend-item>
122
- <svg width="14" height="14" data-plot-legend-swatch>
123
- <rect width="14" height="14" fill="var(--color-surface-z2, #ccc)" />
124
- <rect width="14" height="14" fill="url(#{item.patternId})" />
125
- </svg>
126
- <span class="label" data-plot-legend-label>{item.label}</span>
127
- </div>
128
- {/each}
129
- </div>
130
- {/if}
131
-
132
- <!-- Symbol section (different field from color) -->
133
- {#if symbolItems.length > 0 && !symbolOnly}
134
- <div class="legend categorical legend-section">
135
- {#each symbolItems as item (item.key)}
136
- <div class="legend-item" data-plot-legend-item>
137
- {#if isLineGeom}
138
- <svg width="24" height="14" data-plot-legend-swatch>
139
- <line x1="2" y1="7" x2="22" y2="7" stroke={item.stroke ?? item.fill} stroke-width="2" stroke-linecap="round" stroke-dasharray="4 2" />
140
- <path transform="translate(12,7)" d={buildSymbolPath(item.shape, 4)} fill={item.stroke ?? item.fill} />
141
- </svg>
142
- {:else}
143
- <svg width="14" height="14" data-plot-legend-swatch>
144
- <path transform="translate(7,7)" d={buildSymbolPath(item.shape, 5)} fill={item.fill} />
145
- </svg>
146
- {/if}
147
- <span class="label" data-plot-legend-label>{item.label}</span>
148
- </div>
149
- {/each}
150
- </div>
151
- {/if}
152
-
153
- </div>
155
+ <!-- Symbol section (different field from color) -->
156
+ {#if symbolItems.length > 0 && !symbolOnly}
157
+ <div class="legend categorical legend-section">
158
+ {#each symbolItems as item (item.key)}
159
+ <div class="legend-item" data-plot-legend-item>
160
+ {#if isLineGeom}
161
+ <svg width="24" height="14" data-plot-legend-swatch>
162
+ <line
163
+ x1="2"
164
+ y1="7"
165
+ x2="22"
166
+ y2="7"
167
+ stroke={item.stroke ?? item.fill}
168
+ stroke-width="2"
169
+ stroke-linecap="round"
170
+ stroke-dasharray="4 2"
171
+ />
172
+ <path
173
+ transform="translate(12,7)"
174
+ d={buildSymbolPath(item.shape, 4)}
175
+ fill={item.stroke ?? item.fill}
176
+ />
177
+ </svg>
178
+ {:else}
179
+ <svg width="14" height="14" data-plot-legend-swatch>
180
+ <path
181
+ transform="translate(7,7)"
182
+ d={buildSymbolPath(item.shape, 5)}
183
+ fill={item.fill}
184
+ />
185
+ </svg>
186
+ {/if}
187
+ <span class="label" data-plot-legend-label>{item.label}</span>
188
+ </div>
189
+ {/each}
190
+ </div>
191
+ {/if}
192
+ </div>
154
193
  {:else}
155
- <div class="legend gradient" data-plot-legend>
156
- <div class="gradient-bar" style={gradientStyle} data-plot-legend-gradient></div>
157
- </div>
194
+ <div class="legend gradient" data-plot-legend>
195
+ <div class="gradient-bar" style={gradientStyle} data-plot-legend-gradient></div>
196
+ </div>
158
197
  {/if}
159
198
 
160
199
  <style>
161
- .legend-root { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
162
- .legend { display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px; }
163
- .legend-section { border-top: 1px solid var(--color-surface-z3, #e0e0e0); padding-top: 6px; }
164
- .legend-item { display: flex; align-items: center; gap: 4px; }
165
- .swatch { display: inline-block; width: 14px; height: 14px; border-radius: 2px; flex-shrink: 0; }
166
- .gradient-bar { width: 180px; height: 14px; border-radius: 2px; }
200
+ .legend-root {
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: 6px;
204
+ margin-top: 8px;
205
+ }
206
+ .legend {
207
+ display: flex;
208
+ flex-wrap: wrap;
209
+ gap: 8px;
210
+ font-size: 12px;
211
+ }
212
+ .legend-section {
213
+ border-top: 1px solid var(--color-surface-z3, #e0e0e0);
214
+ padding-top: 6px;
215
+ }
216
+ .legend-item {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 4px;
220
+ }
221
+ .swatch {
222
+ display: inline-block;
223
+ width: 14px;
224
+ height: 14px;
225
+ border-radius: 2px;
226
+ flex-shrink: 0;
227
+ }
228
+ .gradient-bar {
229
+ width: 180px;
230
+ height: 14px;
231
+ border-radius: 2px;
232
+ }
167
233
  </style>
@@ -1,27 +1,37 @@
1
1
  <script>
2
2
  import { getContext } from 'svelte'
3
+ import { line as d3Line } from 'd3-shape'
3
4
 
4
- const brewer = getContext('chart-brewer')
5
+ let {
6
+ data = [],
7
+ x = undefined,
8
+ y = undefined,
9
+ stroke = 'steelblue',
10
+ strokeWidth = 2,
11
+ curve = undefined
12
+ } = $props()
13
+
14
+ const state = getContext('plot-state')
15
+
16
+ const path = $derived.by(() => {
17
+ if (!state?.xScale || !state?.yScale || !data?.length) return null
18
+ const lineGen = d3Line()
19
+ .x((d) => state.xScale(x ? d[x] : d) ?? 0)
20
+ .y((d) => state.yScale(y ? d[y] : d) ?? 0)
21
+ .defined((d) => d != null)
22
+ if (curve) lineGen.curve(curve)
23
+ return lineGen(data)
24
+ })
5
25
  </script>
6
26
 
7
- {#if brewer && brewer.lines && brewer.lines.length > 0}
8
- <g class="chart-lines" data-plot-type="line">
9
- {#each brewer.lines as seg (seg.key ?? seg.d)}
10
- <path
11
- d={seg.d}
12
- fill={seg.fill}
13
- stroke={seg.stroke}
14
- stroke-width="2"
15
- stroke-linejoin="round"
16
- stroke-linecap="round"
17
- data-plot-element="line"
18
- />
19
- {/each}
20
- </g>
27
+ {#if path}
28
+ <path
29
+ d={path}
30
+ fill="none"
31
+ {stroke}
32
+ stroke-width={strokeWidth}
33
+ stroke-linejoin="round"
34
+ stroke-linecap="round"
35
+ data-plot-element="line"
36
+ />
21
37
  {/if}
22
-
23
- <style>
24
- .chart-lines {
25
- pointer-events: none;
26
- }
27
- </style>