@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.
- package/dist/PlotState.svelte.d.ts +26 -0
- package/dist/index.d.ts +6 -1
- package/dist/lib/brewing/BoxBrewer.svelte.d.ts +3 -5
- package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +9 -0
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +3 -4
- package/dist/lib/brewing/colors.d.ts +10 -1
- package/dist/lib/brewing/marks/points.d.ts +17 -2
- package/dist/lib/keyboard-nav.d.ts +15 -0
- package/dist/lib/plot/preset.d.ts +1 -1
- package/dist/lib/preset.d.ts +30 -0
- package/package.json +2 -1
- package/src/AnimatedPlot.svelte +375 -207
- package/src/Chart.svelte +81 -84
- package/src/ChartProvider.svelte +10 -0
- package/src/FacetPlot/Panel.svelte +30 -16
- package/src/FacetPlot.svelte +100 -76
- package/src/Plot/Area.svelte +26 -19
- package/src/Plot/Axis.svelte +81 -59
- package/src/Plot/Bar.svelte +47 -89
- package/src/Plot/Grid.svelte +23 -19
- package/src/Plot/Legend.svelte +213 -147
- package/src/Plot/Line.svelte +31 -21
- package/src/Plot/Point.svelte +35 -22
- package/src/Plot/Root.svelte +46 -91
- package/src/Plot/Timeline.svelte +82 -82
- package/src/Plot/Tooltip.svelte +68 -62
- package/src/Plot.svelte +290 -174
- package/src/PlotState.svelte.js +338 -265
- package/src/Sparkline.svelte +95 -56
- package/src/charts/AreaChart.svelte +22 -20
- package/src/charts/BarChart.svelte +23 -21
- package/src/charts/BoxPlot.svelte +15 -15
- package/src/charts/BubbleChart.svelte +17 -17
- package/src/charts/LineChart.svelte +20 -20
- package/src/charts/PieChart.svelte +30 -20
- package/src/charts/ScatterPlot.svelte +20 -19
- package/src/charts/ViolinPlot.svelte +15 -15
- package/src/crossfilter/CrossFilter.svelte +33 -29
- package/src/crossfilter/FilterBar.svelte +17 -25
- package/src/crossfilter/FilterHistogram.svelte +290 -0
- package/src/crossfilter/FilterSlider.svelte +69 -65
- package/src/crossfilter/createCrossFilter.svelte.js +94 -90
- package/src/geoms/Arc.svelte +114 -69
- package/src/geoms/Area.svelte +67 -39
- package/src/geoms/Bar.svelte +184 -126
- package/src/geoms/Box.svelte +101 -91
- package/src/geoms/LabelPill.svelte +11 -11
- package/src/geoms/Line.svelte +110 -86
- package/src/geoms/Point.svelte +130 -90
- package/src/geoms/Violin.svelte +51 -41
- package/src/geoms/lib/areas.js +122 -99
- package/src/geoms/lib/bars.js +195 -144
- package/src/index.js +21 -14
- package/src/lib/brewing/BoxBrewer.svelte.js +8 -50
- package/src/lib/brewing/CartesianBrewer.svelte.js +11 -7
- package/src/lib/brewing/PieBrewer.svelte.js +5 -5
- package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
- package/src/lib/brewing/ViolinBrewer.svelte.js +8 -49
- package/src/lib/brewing/brewer.svelte.js +242 -195
- package/src/lib/brewing/colors.js +34 -5
- package/src/lib/brewing/marks/arcs.js +28 -28
- package/src/lib/brewing/marks/areas.js +54 -41
- package/src/lib/brewing/marks/bars.js +34 -34
- package/src/lib/brewing/marks/boxes.js +51 -51
- package/src/lib/brewing/marks/lines.js +37 -30
- package/src/lib/brewing/marks/points.js +74 -26
- package/src/lib/brewing/marks/violins.js +57 -57
- package/src/lib/brewing/patterns.js +25 -11
- package/src/lib/brewing/scales.js +17 -17
- package/src/lib/brewing/stats.js +37 -29
- package/src/lib/brewing/symbols.js +1 -1
- package/src/lib/chart.js +2 -1
- package/src/lib/keyboard-nav.js +37 -0
- package/src/lib/plot/crossfilter.js +5 -5
- package/src/lib/plot/facet.js +30 -30
- package/src/lib/plot/frames.js +30 -29
- package/src/lib/plot/helpers.js +4 -4
- package/src/lib/plot/preset.js +48 -34
- package/src/lib/plot/scales.js +64 -39
- package/src/lib/plot/stat.js +47 -47
- package/src/lib/preset.js +41 -0
- package/src/patterns/DefinePatterns.svelte +24 -24
- package/src/patterns/README.md +3 -0
- package/src/patterns/patterns.js +328 -176
- package/src/patterns/scale.js +61 -32
- package/src/spec/chart-spec.js +64 -21
package/src/Plot/Bar.svelte
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
{#
|
|
58
|
-
<
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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}
|
package/src/Plot/Grid.svelte
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
2
|
+
import { getContext } from 'svelte'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const state = getContext('plot-state')
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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>
|
package/src/Plot/Legend.svelte
CHANGED
|
@@ -1,167 +1,233 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
6
|
+
/** @type {Record<string, string>} */
|
|
7
|
+
let { labels = {} } = $props()
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const state = getContext('plot-state')
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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>
|
package/src/Plot/Line.svelte
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { getContext } from 'svelte'
|
|
3
|
+
import { line as d3Line } from 'd3-shape'
|
|
3
4
|
|
|
4
|
-
|
|
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
|
|
8
|
-
<
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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>
|