@rokkit/chart 1.0.0-next.147 → 1.0.0-next.148
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/Plot/index.d.ts +4 -0
- package/dist/PlotState.svelte.d.ts +47 -0
- package/dist/crossfilter/createCrossFilter.svelte.d.ts +15 -0
- package/dist/geoms/lib/areas.d.ts +52 -0
- package/dist/geoms/lib/bars.d.ts +3 -0
- package/dist/index.d.ts +38 -1
- package/dist/lib/brewing/BoxBrewer.svelte.d.ts +10 -0
- package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +8 -0
- package/dist/lib/brewing/PieBrewer.svelte.d.ts +8 -0
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +9 -0
- package/dist/lib/brewing/brewer.svelte.d.ts +145 -0
- package/dist/lib/brewing/colors.d.ts +17 -0
- package/dist/lib/brewing/marks/arcs.d.ts +17 -0
- package/dist/lib/brewing/marks/areas.d.ts +31 -0
- package/dist/lib/brewing/marks/bars.d.ts +1 -0
- package/dist/lib/brewing/marks/boxes.d.ts +24 -0
- package/dist/lib/brewing/marks/lines.d.ts +24 -0
- package/dist/lib/brewing/marks/points.d.ts +40 -0
- package/dist/lib/brewing/marks/violins.d.ts +20 -0
- package/dist/lib/brewing/patterns.d.ts +14 -0
- package/dist/lib/brewing/scales.d.ts +28 -0
- package/dist/lib/brewing/stats.d.ts +31 -0
- package/dist/lib/brewing/symbols.d.ts +7 -0
- package/dist/lib/plot/chartProps.d.ts +177 -0
- package/dist/lib/plot/crossfilter.d.ts +13 -0
- package/dist/lib/plot/facet.d.ts +24 -0
- package/dist/lib/plot/frames.d.ts +45 -0
- package/dist/lib/plot/helpers.d.ts +3 -0
- package/dist/lib/plot/preset.d.ts +29 -0
- package/dist/lib/plot/scales.d.ts +5 -0
- package/dist/lib/plot/stat.d.ts +32 -0
- package/dist/lib/plot/types.d.ts +89 -0
- package/dist/lib/scales.svelte.d.ts +1 -1
- package/dist/lib/swatch.d.ts +12 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/xscale.d.ts +11 -0
- package/dist/patterns/index.d.ts +4 -9
- package/dist/patterns/patterns.d.ts +72 -0
- package/dist/patterns/scale.d.ts +30 -0
- package/package.json +9 -3
- package/src/AnimatedPlot.svelte +194 -0
- package/src/Chart.svelte +101 -0
- package/src/FacetPlot/Panel.svelte +23 -0
- package/src/FacetPlot.svelte +90 -0
- package/src/Plot/Arc.svelte +29 -0
- package/src/Plot/Area.svelte +25 -0
- package/src/Plot/Axis.svelte +62 -84
- package/src/Plot/Grid.svelte +20 -58
- package/src/Plot/Legend.svelte +160 -120
- package/src/Plot/Line.svelte +27 -0
- package/src/Plot/Point.svelte +27 -0
- package/src/Plot/Timeline.svelte +95 -0
- package/src/Plot/Tooltip.svelte +81 -0
- package/src/Plot/index.js +4 -0
- package/src/Plot.svelte +189 -0
- package/src/PlotState.svelte.js +278 -0
- package/src/Sparkline.svelte +69 -0
- package/src/charts/AreaChart.svelte +25 -0
- package/src/charts/BarChart.svelte +26 -0
- package/src/charts/BoxPlot.svelte +21 -0
- package/src/charts/BubbleChart.svelte +23 -0
- package/src/charts/LineChart.svelte +26 -0
- package/src/charts/PieChart.svelte +25 -0
- package/src/charts/ScatterPlot.svelte +25 -0
- package/src/charts/ViolinPlot.svelte +21 -0
- package/src/crossfilter/CrossFilter.svelte +38 -0
- package/src/crossfilter/FilterBar.svelte +32 -0
- package/src/crossfilter/FilterSlider.svelte +79 -0
- package/src/crossfilter/createCrossFilter.svelte.js +113 -0
- package/src/elements/SymbolGrid.svelte +6 -7
- package/src/geoms/Arc.svelte +81 -0
- package/src/geoms/Area.svelte +50 -0
- package/src/geoms/Bar.svelte +142 -0
- package/src/geoms/Box.svelte +101 -0
- package/src/geoms/LabelPill.svelte +17 -0
- package/src/geoms/Line.svelte +100 -0
- package/src/geoms/Point.svelte +100 -0
- package/src/geoms/Violin.svelte +44 -0
- package/src/geoms/lib/areas.js +131 -0
- package/src/geoms/lib/bars.js +172 -0
- package/src/index.js +52 -3
- package/src/lib/brewing/BoxBrewer.svelte.js +56 -0
- package/src/lib/brewing/CartesianBrewer.svelte.js +16 -0
- package/src/lib/brewing/PieBrewer.svelte.js +14 -0
- package/src/lib/brewing/ViolinBrewer.svelte.js +55 -0
- package/src/lib/brewing/brewer.svelte.js +229 -0
- package/src/lib/brewing/colors.js +22 -0
- package/src/lib/brewing/marks/arcs.js +43 -0
- package/src/lib/brewing/marks/areas.js +59 -0
- package/src/lib/brewing/marks/bars.js +49 -0
- package/src/lib/brewing/marks/boxes.js +75 -0
- package/src/lib/brewing/marks/lines.js +48 -0
- package/src/lib/brewing/marks/points.js +57 -0
- package/src/lib/brewing/marks/violins.js +90 -0
- package/src/lib/brewing/patterns.js +31 -0
- package/src/lib/brewing/scales.js +51 -0
- package/src/lib/brewing/scales.svelte.js +2 -26
- package/src/lib/brewing/stats.js +62 -0
- package/src/lib/brewing/symbols.js +10 -0
- package/src/lib/plot/chartProps.js +76 -0
- package/src/lib/plot/crossfilter.js +16 -0
- package/src/lib/plot/facet.js +58 -0
- package/src/lib/plot/frames.js +90 -0
- package/src/lib/plot/helpers.js +14 -0
- package/src/lib/plot/preset.js +53 -0
- package/src/lib/plot/scales.js +56 -0
- package/src/lib/plot/stat.js +92 -0
- package/src/lib/plot/types.js +65 -0
- package/src/lib/scales.svelte.js +2 -26
- package/src/lib/swatch.js +13 -0
- package/src/lib/utils.js +9 -0
- package/src/lib/xscale.js +31 -0
- package/src/patterns/DefinePatterns.svelte +32 -0
- package/src/patterns/PatternDef.svelte +27 -0
- package/src/patterns/index.js +4 -14
- package/src/patterns/patterns.js +208 -0
- package/src/patterns/scale.js +87 -0
- package/src/spec/chart-spec.js +29 -0
- package/src/symbols/Shape.svelte +1 -1
- package/src/symbols/constants/index.js +1 -1
- package/dist/old_lib/index.d.ts +0 -4
- package/dist/old_lib/plots.d.ts +0 -3
- package/dist/old_lib/swatch.d.ts +0 -285
- package/dist/old_lib/utils.d.ts +0 -1
- package/dist/patterns/paths/constants.d.ts +0 -1
- package/dist/template/constants.d.ts +0 -43
- package/dist/template/shapes/index.d.ts +0 -4
- package/src/old_lib/index.js +0 -4
- package/src/old_lib/plots.js +0 -27
- package/src/old_lib/swatch.js +0 -16
- package/src/old_lib/utils.js +0 -8
- package/src/patterns/Brick.svelte +0 -15
- package/src/patterns/Circles.svelte +0 -18
- package/src/patterns/CrossHatch.svelte +0 -12
- package/src/patterns/CurvedWave.svelte +0 -7
- package/src/patterns/Dots.svelte +0 -20
- package/src/patterns/OutlineCircles.svelte +0 -13
- package/src/patterns/Tile.svelte +0 -16
- package/src/patterns/Triangles.svelte +0 -13
- package/src/patterns/Waves.svelte +0 -9
- package/src/patterns/paths/NamedPattern.svelte +0 -9
- package/src/patterns/paths/constants.js +0 -4
- package/src/template/Texture.svelte +0 -13
- package/src/template/constants.js +0 -43
- package/src/template/shapes/Circles.svelte +0 -15
- package/src/template/shapes/Lines.svelte +0 -16
- package/src/template/shapes/Path.svelte +0 -9
- package/src/template/shapes/Polygons.svelte +0 -15
- package/src/template/shapes/index.js +0 -4
- /package/dist/{old_lib → lib}/brewer.d.ts +0 -0
- /package/dist/{old_lib → lib}/chart.d.ts +0 -0
- /package/dist/{old_lib → lib}/grid.d.ts +0 -0
- /package/dist/{old_lib → lib}/ticks.d.ts +0 -0
- /package/src/{old_lib → lib}/brewer.js +0 -0
- /package/src/{old_lib → lib}/chart.js +0 -0
- /package/src/{old_lib → lib}/grid.js +0 -0
- /package/src/{old_lib → lib}/ticks.js +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { buildBoxes } from '../lib/brewing/marks/boxes.js'
|
|
4
|
+
|
|
5
|
+
let { x, y, fill, stat = 'boxplot', options = {} } = $props()
|
|
6
|
+
|
|
7
|
+
const plotState = getContext('plot-state')
|
|
8
|
+
let id = $state(null)
|
|
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) })
|
|
15
|
+
|
|
16
|
+
$effect(() => {
|
|
17
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color: fill ?? x }, stat })
|
|
18
|
+
})
|
|
19
|
+
|
|
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)
|
|
24
|
+
|
|
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
|
+
</script>
|
|
30
|
+
|
|
31
|
+
{#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>
|
|
101
|
+
{/if}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/** @type {{ x: number, y: number, text: string, color?: string }} */
|
|
3
|
+
let { x, y, text, color = '#333' } = $props()
|
|
4
|
+
|
|
5
|
+
const w = $derived(Math.max(36, String(text).length * 7 + 12))
|
|
6
|
+
</script>
|
|
7
|
+
|
|
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>
|
|
17
|
+
</g>
|
|
@@ -0,0 +1,100 @@
|
|
|
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'
|
|
6
|
+
|
|
7
|
+
let { x, y, color, symbol: symbolField, label = false, stat = 'identity', options = {} } = $props()
|
|
8
|
+
|
|
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
|
+
const plotState = getContext('plot-state')
|
|
22
|
+
let id = $state(null)
|
|
23
|
+
|
|
24
|
+
onMount(() => {
|
|
25
|
+
id = plotState.registerGeom({ type: 'line', channels: { x, y, color, symbol: symbolField }, stat, options })
|
|
26
|
+
})
|
|
27
|
+
onDestroy(() => { if (id) plotState.unregisterGeom(id) })
|
|
28
|
+
|
|
29
|
+
$effect(() => {
|
|
30
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color, symbol: symbolField }, stat })
|
|
31
|
+
})
|
|
32
|
+
|
|
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)
|
|
38
|
+
|
|
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
|
+
})
|
|
43
|
+
|
|
44
|
+
const markerRadius = $derived(options.markerRadius ?? 4)
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
{#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>
|
|
100
|
+
{/if}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { scaleSqrt } from 'd3-scale'
|
|
4
|
+
import { buildPoints } from '../lib/brewing/marks/points.js'
|
|
5
|
+
import LabelPill from './LabelPill.svelte'
|
|
6
|
+
|
|
7
|
+
let { x, y, color, size, symbol: symbolField, label = false, stat = 'identity', options = {} } = $props()
|
|
8
|
+
|
|
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
|
+
const plotState = getContext('plot-state')
|
|
22
|
+
let id = $state(null)
|
|
23
|
+
|
|
24
|
+
onMount(() => {
|
|
25
|
+
id = plotState.registerGeom({ type: 'point', channels: { x, y, color, size, symbol: symbolField }, stat, options })
|
|
26
|
+
})
|
|
27
|
+
onDestroy(() => { if (id) plotState.unregisterGeom(id) })
|
|
28
|
+
|
|
29
|
+
$effect(() => {
|
|
30
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color, size, symbol: symbolField }, stat })
|
|
31
|
+
})
|
|
32
|
+
|
|
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)
|
|
38
|
+
|
|
39
|
+
const sizeScale = $derived.by(() => {
|
|
40
|
+
if (!size || !data?.length) return null
|
|
41
|
+
const vals = data.map((d) => Number(d[size])).filter((v) => !isNaN(v))
|
|
42
|
+
if (!vals.length) return null
|
|
43
|
+
const maxVal = Math.max(...vals)
|
|
44
|
+
const minVal = Math.min(...vals)
|
|
45
|
+
return scaleSqrt().domain([minVal, maxVal]).range([options.minRadius ?? 3, options.maxRadius ?? 20])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const points = $derived.by(() => {
|
|
49
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
50
|
+
return buildPoints(data, { x, y, color, size, symbol: symbolField }, xScale, yScale, colors, sizeScale, symbolMap, options.radius ?? 4)
|
|
51
|
+
})
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
{#if points.length > 0}
|
|
55
|
+
<g data-plot-geom="point">
|
|
56
|
+
{#each points as pt, i (`${i}::${pt.data[x]}::${pt.data[y]}`)}
|
|
57
|
+
{#if pt.symbolPath}
|
|
58
|
+
<path
|
|
59
|
+
transform="translate({pt.cx},{pt.cy})"
|
|
60
|
+
d={pt.symbolPath}
|
|
61
|
+
fill={pt.fill}
|
|
62
|
+
stroke={pt.stroke}
|
|
63
|
+
stroke-width="1"
|
|
64
|
+
fill-opacity={options.opacity ?? 0.8}
|
|
65
|
+
data-plot-element="point"
|
|
66
|
+
role="graphics-symbol"
|
|
67
|
+
aria-label="{pt.data[x]}, {pt.data[y]}"
|
|
68
|
+
onmouseenter={() => plotState.setHovered(pt.data)}
|
|
69
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
70
|
+
/>
|
|
71
|
+
{:else}
|
|
72
|
+
<circle
|
|
73
|
+
cx={pt.cx}
|
|
74
|
+
cy={pt.cy}
|
|
75
|
+
r={pt.r}
|
|
76
|
+
fill={pt.fill}
|
|
77
|
+
stroke={pt.stroke}
|
|
78
|
+
stroke-width="1"
|
|
79
|
+
fill-opacity={options.opacity ?? 0.8}
|
|
80
|
+
data-plot-element="point"
|
|
81
|
+
role="graphics-symbol"
|
|
82
|
+
aria-label="{pt.data[x]}, {pt.data[y]}"
|
|
83
|
+
onmouseenter={() => plotState.setHovered(pt.data)}
|
|
84
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
85
|
+
/>
|
|
86
|
+
{/if}
|
|
87
|
+
{#if label}
|
|
88
|
+
{@const text = resolveLabel(pt.data)}
|
|
89
|
+
{#if text}
|
|
90
|
+
<LabelPill
|
|
91
|
+
x={pt.cx + (options.labelOffset?.x ?? 0)}
|
|
92
|
+
y={pt.cy - pt.r + (options.labelOffset?.y ?? -12)}
|
|
93
|
+
{text}
|
|
94
|
+
color={pt.stroke ?? '#333'}
|
|
95
|
+
/>
|
|
96
|
+
{/if}
|
|
97
|
+
{/if}
|
|
98
|
+
{/each}
|
|
99
|
+
</g>
|
|
100
|
+
{/if}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { buildViolins } from '../lib/brewing/marks/violins.js'
|
|
4
|
+
|
|
5
|
+
let { x, y, fill, stat = 'boxplot', options = {} } = $props()
|
|
6
|
+
|
|
7
|
+
const plotState = getContext('plot-state')
|
|
8
|
+
let id = $state(null)
|
|
9
|
+
|
|
10
|
+
// fill ?? x drives the colors map for both violin interior and outline
|
|
11
|
+
onMount(() => {
|
|
12
|
+
id = plotState.registerGeom({ type: 'violin', channels: { x, y, color: fill ?? x }, stat, options })
|
|
13
|
+
})
|
|
14
|
+
onDestroy(() => { if (id) plotState.unregisterGeom(id) })
|
|
15
|
+
|
|
16
|
+
$effect(() => {
|
|
17
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color: fill ?? x }, stat })
|
|
18
|
+
})
|
|
19
|
+
|
|
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)
|
|
24
|
+
|
|
25
|
+
const violins = $derived.by(() => {
|
|
26
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
27
|
+
return buildViolins(data, { x, fill: fill ?? x }, xScale, yScale, colors)
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
{#if violins.length > 0}
|
|
32
|
+
<g data-plot-geom="violin">
|
|
33
|
+
{#each violins as v, i (`${String(v.cx) }::${ i}`)}
|
|
34
|
+
<path
|
|
35
|
+
d={v.d}
|
|
36
|
+
fill={v.fill}
|
|
37
|
+
fill-opacity="0.5"
|
|
38
|
+
stroke={v.stroke}
|
|
39
|
+
stroke-width="1.5"
|
|
40
|
+
data-plot-element="violin"
|
|
41
|
+
/>
|
|
42
|
+
{/each}
|
|
43
|
+
</g>
|
|
44
|
+
{/if}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { area, stack, curveCatmullRom, curveStep } from 'd3-shape'
|
|
2
|
+
import { toPatternId } from '../../lib/brewing/patterns.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds area path geometry for multi-series area charts.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object[]} data
|
|
8
|
+
* @param {{ x: string, y: string, color?: string }} channels
|
|
9
|
+
* @param {Function} xScale
|
|
10
|
+
* @param {Function} yScale
|
|
11
|
+
* @param {Map<unknown, {fill: string, stroke: string}>} colors
|
|
12
|
+
* @param {'linear'|'smooth'|'step'} [curve]
|
|
13
|
+
* @param {Map<unknown, string>} [patterns]
|
|
14
|
+
* @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
|
|
15
|
+
*/
|
|
16
|
+
export function buildAreas(data, channels, xScale, yScale, colors, curve, patterns) {
|
|
17
|
+
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
18
|
+
const baseline = yScale.range()[0] // bottom of the chart (y pixel max)
|
|
19
|
+
|
|
20
|
+
const xPos = (d) => typeof xScale.bandwidth === 'function'
|
|
21
|
+
? xScale(d[xf]) + xScale.bandwidth() / 2
|
|
22
|
+
: xScale(d[xf])
|
|
23
|
+
|
|
24
|
+
const makeGen = () => {
|
|
25
|
+
const gen = area()
|
|
26
|
+
.x(xPos)
|
|
27
|
+
.y0(baseline)
|
|
28
|
+
.y1((d) => yScale(d[yf]))
|
|
29
|
+
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
30
|
+
else if (curve === 'step') gen.curve(curveStep)
|
|
31
|
+
return gen
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sortByX = (rows) => [...rows].sort((a, b) => a[xf] < b[xf] ? -1 : a[xf] > b[xf] ? 1 : 0)
|
|
35
|
+
|
|
36
|
+
if (!cf) {
|
|
37
|
+
const entry = colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
|
|
38
|
+
const patternKey = pf ? data[0]?.[pf] : null
|
|
39
|
+
const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
40
|
+
? toPatternId(String(patternKey)) : null
|
|
41
|
+
return [{ d: makeGen()(sortByX(data)), fill: entry.fill, stroke: 'none', key: null, patternId }]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Group by color field
|
|
45
|
+
const groups = new Map()
|
|
46
|
+
for (const d of data) {
|
|
47
|
+
const key = d[cf]
|
|
48
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
49
|
+
groups.get(key).push(d)
|
|
50
|
+
}
|
|
51
|
+
// For different-field patterns, assign positionally so each area gets a distinct pattern
|
|
52
|
+
const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
|
|
53
|
+
|
|
54
|
+
return [...groups.entries()].map(([key, rows], i) => {
|
|
55
|
+
const entry = colors?.get(key) ?? { fill: '#888', stroke: '#888' }
|
|
56
|
+
// Same field or no pf: look up by colorKey. Different field: assign positionally.
|
|
57
|
+
const patternKey = !pf ? key : pf === cf ? key : (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
|
|
58
|
+
const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
59
|
+
? toPatternId(String(patternKey)) : null
|
|
60
|
+
return { d: makeGen()(sortByX(rows)), fill: entry.fill, stroke: 'none', key, patternId }
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Builds stacked area paths using d3 stack layout.
|
|
66
|
+
*
|
|
67
|
+
* @param {Object[]} data
|
|
68
|
+
* @param {{ x: string, y: string, color: string }} channels
|
|
69
|
+
* @param {Function} xScale
|
|
70
|
+
* @param {Function} yScale
|
|
71
|
+
* @param {Map<unknown, {fill: string, stroke: string}>} colors
|
|
72
|
+
* @param {'linear'|'smooth'|'step'} [curve]
|
|
73
|
+
* @param {Map<unknown, string>} [patterns]
|
|
74
|
+
* @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
|
|
75
|
+
*/
|
|
76
|
+
export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns) {
|
|
77
|
+
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
78
|
+
if (!cf) return buildAreas(data, channels, xScale, yScale, colors, curve, patterns)
|
|
79
|
+
|
|
80
|
+
const xCategories = [...new Set(data.map((d) => d[xf]))]
|
|
81
|
+
.sort((a, b) => a < b ? -1 : a > b ? 1 : 0)
|
|
82
|
+
const colorCategories = [...new Set(data.map((d) => d[cf]))]
|
|
83
|
+
|
|
84
|
+
// Build wide-form lookup: xVal → { colorKey: yVal }
|
|
85
|
+
const lookup = new Map()
|
|
86
|
+
for (const d of data) {
|
|
87
|
+
if (!lookup.has(d[xf])) lookup.set(d[xf], {})
|
|
88
|
+
lookup.get(d[xf])[d[cf]] = Number(d[yf])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const wide = xCategories.map((xVal) => {
|
|
92
|
+
const row = { [xf]: xVal }
|
|
93
|
+
for (const c of colorCategories) row[c] = lookup.get(xVal)?.[c] ?? 0
|
|
94
|
+
return row
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const xPos = (d) => typeof xScale.bandwidth === 'function'
|
|
98
|
+
? xScale(d.data[xf]) + xScale.bandwidth() / 2
|
|
99
|
+
: xScale(d.data[xf])
|
|
100
|
+
|
|
101
|
+
const makeGen = () => {
|
|
102
|
+
const gen = area()
|
|
103
|
+
.x(xPos)
|
|
104
|
+
.y0((d) => yScale(d[0]))
|
|
105
|
+
.y1((d) => yScale(d[1]))
|
|
106
|
+
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
107
|
+
else if (curve === 'step') gen.curve(curveStep)
|
|
108
|
+
return gen
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const stackGen = stack().keys(colorCategories)
|
|
112
|
+
const layers = stackGen(wide)
|
|
113
|
+
|
|
114
|
+
const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
|
|
115
|
+
|
|
116
|
+
return layers.map((layer, i) => {
|
|
117
|
+
const colorKey = layer.key
|
|
118
|
+
const entry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
|
|
119
|
+
// Same field (or no pf): look up by colorKey. Different field: assign positionally.
|
|
120
|
+
const patternKey = !pf ? colorKey : pf === cf ? colorKey : (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
|
|
121
|
+
const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
122
|
+
? toPatternId(String(patternKey)) : null
|
|
123
|
+
return {
|
|
124
|
+
d: makeGen()(layer) ?? '',
|
|
125
|
+
fill: entry.fill,
|
|
126
|
+
stroke: 'none',
|
|
127
|
+
key: colorKey,
|
|
128
|
+
patternId
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { scaleBand } from 'd3-scale'
|
|
2
|
+
import { stack } from 'd3-shape'
|
|
3
|
+
import { toPatternId } from '../../lib/brewing/patterns.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a band scale suitable for bar x-positioning.
|
|
7
|
+
* When xScale is already a band scale, returns it unchanged.
|
|
8
|
+
* When xScale is a linear scale (numeric x field like year/month),
|
|
9
|
+
* derives a band scale from the distinct values in the data.
|
|
10
|
+
*/
|
|
11
|
+
function ensureBandX(xScale, data, xField) {
|
|
12
|
+
if (typeof xScale?.bandwidth === 'function') return xScale
|
|
13
|
+
const [r0, r1] = xScale.range()
|
|
14
|
+
const domain = [...new Set(data.map((d) => d[xField]))]
|
|
15
|
+
return scaleBand().domain(domain).range([r0, r1]).padding(0.2)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns the sub-band fields: distinct non-x fields among [color, pattern].
|
|
20
|
+
* These are the fields that cause multiple bars within a single x-band.
|
|
21
|
+
*/
|
|
22
|
+
function subBandFields(channels) {
|
|
23
|
+
const { x: xf, color: cf, pattern: pf } = channels
|
|
24
|
+
const seen = new Set()
|
|
25
|
+
const out = []
|
|
26
|
+
for (const f of [cf, pf]) {
|
|
27
|
+
if (f && f !== xf && !seen.has(f)) { seen.add(f); out.push(f) }
|
|
28
|
+
}
|
|
29
|
+
return out
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
|
|
33
|
+
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
34
|
+
|
|
35
|
+
const bandScale = ensureBandX(xScale, data, xf)
|
|
36
|
+
|
|
37
|
+
// Sub-banding: only fields that differ from x drive grouping within a band
|
|
38
|
+
const subFields = subBandFields(channels)
|
|
39
|
+
const getSubKey = (d) => subFields.map((f) => String(d[f])).join('::')
|
|
40
|
+
const subDomain = subFields.length > 0 ? [...new Set(data.map(getSubKey))] : []
|
|
41
|
+
const subScale = subDomain.length > 1
|
|
42
|
+
? scaleBand().domain(subDomain).range([0, bandScale.bandwidth()]).padding(0.05)
|
|
43
|
+
: null
|
|
44
|
+
|
|
45
|
+
return data.map((d, i) => {
|
|
46
|
+
const xVal = d[xf]
|
|
47
|
+
const colorKey = cf ? d[cf] : null
|
|
48
|
+
const patternKey = pf ? d[pf] : null
|
|
49
|
+
const subKey = getSubKey(d)
|
|
50
|
+
|
|
51
|
+
const colorEntry = colors?.get(colorKey) ?? colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
|
|
52
|
+
const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
53
|
+
? toPatternId(String(patternKey))
|
|
54
|
+
: null
|
|
55
|
+
|
|
56
|
+
const bandX = bandScale(xVal) ?? 0
|
|
57
|
+
const subX = subScale && subKey ? (subScale(subKey) ?? 0) : 0
|
|
58
|
+
const barX = bandX + subX
|
|
59
|
+
const barWidth = subScale ? subScale.bandwidth() : bandScale.bandwidth()
|
|
60
|
+
const barY = yScale(d[yf])
|
|
61
|
+
const barHeight = innerHeight - barY
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
data: d,
|
|
65
|
+
key: `${String(xVal)}::${subKey}::${i}`,
|
|
66
|
+
x: barX,
|
|
67
|
+
y: barY,
|
|
68
|
+
width: barWidth,
|
|
69
|
+
height: barHeight,
|
|
70
|
+
fill: colorEntry.fill,
|
|
71
|
+
stroke: colorEntry.stroke,
|
|
72
|
+
patternId
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
|
|
78
|
+
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
79
|
+
|
|
80
|
+
const bandScale = ensureBandX(xScale, data, xf)
|
|
81
|
+
|
|
82
|
+
// Stack dimension: first non-x grouping field (prefer pattern, then color)
|
|
83
|
+
const subFields = subBandFields(channels)
|
|
84
|
+
if (subFields.length === 0) {
|
|
85
|
+
return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
86
|
+
}
|
|
87
|
+
const stackField = subFields[0]
|
|
88
|
+
|
|
89
|
+
const xCategories = [...new Set(data.map((d) => d[xf]))]
|
|
90
|
+
const stackCategories = [...new Set(data.map((d) => d[stackField]))]
|
|
91
|
+
|
|
92
|
+
const lookup = new Map()
|
|
93
|
+
for (const d of data) {
|
|
94
|
+
if (!lookup.has(d[xf])) lookup.set(d[xf], {})
|
|
95
|
+
lookup.get(d[xf])[d[stackField]] = Number(d[yf])
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const wide = xCategories.map((xVal) => {
|
|
99
|
+
const row = { [xf]: xVal }
|
|
100
|
+
for (const sk of stackCategories) row[sk] = lookup.get(xVal)?.[sk] ?? 0
|
|
101
|
+
return row
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const stackGen = stack().keys(stackCategories)
|
|
105
|
+
const layers = stackGen(wide)
|
|
106
|
+
|
|
107
|
+
const bars = []
|
|
108
|
+
for (const layer of layers) {
|
|
109
|
+
const stackKey = layer.key
|
|
110
|
+
|
|
111
|
+
for (const point of layer) {
|
|
112
|
+
const [y0, y1] = point
|
|
113
|
+
const xVal = point.data[xf]
|
|
114
|
+
|
|
115
|
+
// Color lookup: cf may equal xf (= xVal) or stackField (= stackKey)
|
|
116
|
+
const colorKey = cf
|
|
117
|
+
? (cf === xf ? xVal : cf === stackField ? stackKey : null)
|
|
118
|
+
: null
|
|
119
|
+
const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
|
|
120
|
+
|
|
121
|
+
// Pattern lookup: pf may equal xf (= xVal) or stackField (= stackKey)
|
|
122
|
+
const patternKey = pf
|
|
123
|
+
? (pf === xf ? xVal : pf === stackField ? stackKey : null)
|
|
124
|
+
: null
|
|
125
|
+
const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
126
|
+
? toPatternId(String(patternKey))
|
|
127
|
+
: null
|
|
128
|
+
|
|
129
|
+
bars.push({
|
|
130
|
+
data: point.data,
|
|
131
|
+
key: `${String(xVal)}::${String(stackKey)}`,
|
|
132
|
+
x: bandScale(xVal) ?? 0,
|
|
133
|
+
y: yScale(y1),
|
|
134
|
+
width: bandScale.bandwidth(),
|
|
135
|
+
height: yScale(y0) - yScale(y1),
|
|
136
|
+
fill: colorEntry.fill,
|
|
137
|
+
stroke: colorEntry.stroke,
|
|
138
|
+
patternId
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return bars
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildHorizontalBars(data, channels, xScale, yScale, colors, _innerHeight) {
|
|
146
|
+
const { x: xf, y: yf, color: cf } = channels
|
|
147
|
+
const colorKeys = cf ? [...new Set(data.map((d) => d[cf]))] : []
|
|
148
|
+
const subScale = colorKeys.length > 1
|
|
149
|
+
? scaleBand().domain(colorKeys).range([0, yScale.bandwidth()]).padding(0.05)
|
|
150
|
+
: null
|
|
151
|
+
|
|
152
|
+
return data.map((d) => {
|
|
153
|
+
const yVal = d[yf]
|
|
154
|
+
const colorKey = cf ? d[cf] : null
|
|
155
|
+
const colorEntry = colors?.get(colorKey) ?? colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
|
|
156
|
+
|
|
157
|
+
const bandY = yScale(yVal) ?? 0
|
|
158
|
+
const subY = subScale ? (subScale(colorKey) ?? 0) : 0
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
data: d,
|
|
162
|
+
key: `${String(yVal)}::${String(colorKey ?? '')}`,
|
|
163
|
+
x: 0,
|
|
164
|
+
y: bandY + subY,
|
|
165
|
+
width: xScale(d[xf]),
|
|
166
|
+
height: subScale ? subScale.bandwidth() : yScale.bandwidth(),
|
|
167
|
+
fill: colorEntry.fill,
|
|
168
|
+
stroke: colorEntry.stroke,
|
|
169
|
+
patternId: null
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|