@rokkit/chart 1.0.0-next.147 → 1.0.0-next.149
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 +47 -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 +214 -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 +80 -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,25 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import Plot from '../Plot.svelte'
|
|
3
|
+
import Point from '../geoms/Point.svelte'
|
|
4
|
+
|
|
5
|
+
/** @type {import('../lib/plot/chartProps.js').ScatterBubbleChartProps} */
|
|
6
|
+
let {
|
|
7
|
+
data = [],
|
|
8
|
+
x = undefined,
|
|
9
|
+
y = undefined,
|
|
10
|
+
color = undefined,
|
|
11
|
+
symbol = undefined,
|
|
12
|
+
size = undefined,
|
|
13
|
+
label = false,
|
|
14
|
+
tooltip = false,
|
|
15
|
+
width = 600,
|
|
16
|
+
height = 400,
|
|
17
|
+
mode = 'light',
|
|
18
|
+
grid = true,
|
|
19
|
+
legend = false
|
|
20
|
+
} = $props()
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<Plot {data} {width} {height} {mode} {grid} {legend} {tooltip}>
|
|
24
|
+
<Point {x} {y} {color} {size} {symbol} {label} />
|
|
25
|
+
</Plot>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import Plot from '../Plot.svelte'
|
|
3
|
+
import Violin from '../geoms/Violin.svelte'
|
|
4
|
+
|
|
5
|
+
/** @type {import('../lib/plot/chartProps.js').BoxViolinChartProps} */
|
|
6
|
+
let {
|
|
7
|
+
data = [],
|
|
8
|
+
x = undefined,
|
|
9
|
+
y = undefined,
|
|
10
|
+
fill = undefined,
|
|
11
|
+
width = 600,
|
|
12
|
+
height = 400,
|
|
13
|
+
mode = 'light',
|
|
14
|
+
grid = true,
|
|
15
|
+
legend = false
|
|
16
|
+
} = $props()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<Plot {data} {width} {height} {mode} {grid} {legend}>
|
|
20
|
+
<Violin {x} {y} {fill} />
|
|
21
|
+
</Plot>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { setContext, untrack } from 'svelte'
|
|
3
|
+
import { createCrossFilter } from './createCrossFilter.svelte.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @type {{
|
|
7
|
+
* crossfilter?: ReturnType<typeof createCrossFilter>,
|
|
8
|
+
* mode?: 'dim' | 'hide',
|
|
9
|
+
* filters?: import('./createCrossFilter.svelte.js').FilterState,
|
|
10
|
+
* children?: import('svelte').Snippet
|
|
11
|
+
* }}
|
|
12
|
+
*/
|
|
13
|
+
let {
|
|
14
|
+
crossfilter: externalCf = undefined,
|
|
15
|
+
mode = 'dim',
|
|
16
|
+
filters = $bindable(),
|
|
17
|
+
children
|
|
18
|
+
} = $props()
|
|
19
|
+
|
|
20
|
+
// Use an externally provided instance (spec/helpers API) or create one internally.
|
|
21
|
+
// untrack() suppresses "captures initial value" warning — intentional: the cf
|
|
22
|
+
// instance is locked in at construction time and must not recreate on prop changes.
|
|
23
|
+
const cf = untrack(() => externalCf ?? createCrossFilter())
|
|
24
|
+
|
|
25
|
+
// Expose the reactive filters Map to callers via bind:filters
|
|
26
|
+
$effect(() => {
|
|
27
|
+
filters = cf.filters
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
setContext('crossfilter', cf)
|
|
31
|
+
// Use a getter object so children can read .mode reactively
|
|
32
|
+
const modeRef = { get mode() { return mode } }
|
|
33
|
+
setContext('crossfilter-mode', modeRef)
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div data-crossfilter data-crossfilter-mode={mode}>
|
|
37
|
+
{@render children?.()}
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import PlotChart from '../Plot.svelte'
|
|
3
|
+
import Bar from '../geoms/Bar.svelte'
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
data = [],
|
|
7
|
+
field,
|
|
8
|
+
valueField,
|
|
9
|
+
stat = 'sum',
|
|
10
|
+
width = 300,
|
|
11
|
+
height = 120,
|
|
12
|
+
mode = 'light'
|
|
13
|
+
} = $props()
|
|
14
|
+
|
|
15
|
+
const spec = $derived({
|
|
16
|
+
x: field,
|
|
17
|
+
y: valueField
|
|
18
|
+
})
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<!-- FilterBar must be used inside a <CrossFilter> parent. Does not create its own context. -->
|
|
22
|
+
<PlotChart
|
|
23
|
+
{data}
|
|
24
|
+
{spec}
|
|
25
|
+
{width}
|
|
26
|
+
{height}
|
|
27
|
+
{mode}
|
|
28
|
+
grid={false}
|
|
29
|
+
legend={false}
|
|
30
|
+
>
|
|
31
|
+
<Bar x={field} y={valueField} {stat} filterable={true} />
|
|
32
|
+
</PlotChart>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from 'svelte'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dual range slider for a continuous crossfilter dimension.
|
|
6
|
+
* NOTE: Interim implementation using HTML range inputs.
|
|
7
|
+
* The spec calls for a Plot+Point+brush architecture, deferred until brush geom is implemented.
|
|
8
|
+
*/
|
|
9
|
+
let { field, min, max, step = 0.1, label = '' } = $props()
|
|
10
|
+
|
|
11
|
+
const cf = getContext('crossfilter')
|
|
12
|
+
|
|
13
|
+
// Initialize from props; $effect keeps in sync when min/max change
|
|
14
|
+
let low = $state(0)
|
|
15
|
+
let high = $state(100)
|
|
16
|
+
|
|
17
|
+
$effect(() => {
|
|
18
|
+
low = min ?? 0
|
|
19
|
+
high = max ?? 100
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function handleLow(e) {
|
|
23
|
+
low = Math.min(Number(e.currentTarget.value), high)
|
|
24
|
+
cf?.setRange(field, [low, high])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function handleHigh(e) {
|
|
28
|
+
high = Math.max(Number(e.currentTarget.value), low)
|
|
29
|
+
cf?.setRange(field, [low, high])
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div data-filter-slider data-filter-field={field}>
|
|
34
|
+
{#if label}
|
|
35
|
+
<span data-filter-slider-label>{label}</span>
|
|
36
|
+
{/if}
|
|
37
|
+
<div data-filter-slider-inputs>
|
|
38
|
+
<input
|
|
39
|
+
type="range"
|
|
40
|
+
{min} {max} {step}
|
|
41
|
+
value={low}
|
|
42
|
+
oninput={handleLow}
|
|
43
|
+
aria-label="Minimum {label || field}"
|
|
44
|
+
data-filter-slider-low
|
|
45
|
+
/>
|
|
46
|
+
<input
|
|
47
|
+
type="range"
|
|
48
|
+
{min} {max} {step}
|
|
49
|
+
value={high}
|
|
50
|
+
oninput={handleHigh}
|
|
51
|
+
aria-label="Maximum {label || field}"
|
|
52
|
+
data-filter-slider-high
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
<div data-filter-slider-display>
|
|
56
|
+
{low} – {high}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
[data-filter-slider] {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 4px;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
}
|
|
67
|
+
[data-filter-slider-inputs] {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
gap: 2px;
|
|
71
|
+
}
|
|
72
|
+
[data-filter-slider-label] {
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
}
|
|
75
|
+
[data-filter-slider-display] {
|
|
76
|
+
color: currentColor;
|
|
77
|
+
opacity: 0.7;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a reactive cross-filter state object.
|
|
3
|
+
*
|
|
4
|
+
* Filter values follow the spec type:
|
|
5
|
+
* FilterState = Map<string, Set<unknown> | [number, number]>
|
|
6
|
+
* - categorical: raw Set of selected values
|
|
7
|
+
* - continuous: [min, max] tuple
|
|
8
|
+
*
|
|
9
|
+
* Exposes a `filters` getter so CrossFilter.svelte can bind to current state.
|
|
10
|
+
* Exposes a `version` counter that increments on every mutation, giving
|
|
11
|
+
* components a simple reactive signal to watch for filter changes.
|
|
12
|
+
*
|
|
13
|
+
* @returns {CrossFilter}
|
|
14
|
+
*/
|
|
15
|
+
export function createCrossFilter() {
|
|
16
|
+
// Map<dimension, Set<unknown> | [number, number]>
|
|
17
|
+
const filters = $state(new Map())
|
|
18
|
+
|
|
19
|
+
// Simple counter incremented on every mutation. Components read cf.version
|
|
20
|
+
// inside $effect to reactively recompute when any filter changes.
|
|
21
|
+
let version = $state(0)
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns true if any filter is active on this dimension.
|
|
25
|
+
* @param {string} dimension
|
|
26
|
+
*/
|
|
27
|
+
function isFiltered(dimension) {
|
|
28
|
+
const f = filters.get(dimension)
|
|
29
|
+
if (!f) return false
|
|
30
|
+
if (f instanceof Set) return f.size > 0
|
|
31
|
+
return true // range: always active if present
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns true if a value on this dimension is NOT in the active filter.
|
|
36
|
+
* Returns false when no filter is active on this dimension.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} dimension
|
|
39
|
+
* @param {unknown} value
|
|
40
|
+
*/
|
|
41
|
+
function isDimmed(dimension, value) {
|
|
42
|
+
const f = filters.get(dimension)
|
|
43
|
+
if (!f) return false
|
|
44
|
+
if (f instanceof Set) {
|
|
45
|
+
return !f.has(value)
|
|
46
|
+
}
|
|
47
|
+
// [min, max] range
|
|
48
|
+
const [lo, hi] = f
|
|
49
|
+
return Number(value) < lo || Number(value) > hi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Toggles a categorical value for a dimension.
|
|
54
|
+
* Adds when absent, removes when present.
|
|
55
|
+
* Clears the dimension filter when the last value is removed.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} dimension
|
|
58
|
+
* @param {unknown} value
|
|
59
|
+
*/
|
|
60
|
+
function toggleCategorical(dimension, value) {
|
|
61
|
+
const existing = filters.get(dimension)
|
|
62
|
+
const set = existing instanceof Set ? new Set(existing) : new Set()
|
|
63
|
+
if (set.has(value)) {
|
|
64
|
+
set.delete(value)
|
|
65
|
+
} else {
|
|
66
|
+
set.add(value)
|
|
67
|
+
}
|
|
68
|
+
if (set.size === 0) {
|
|
69
|
+
filters.delete(dimension)
|
|
70
|
+
} else {
|
|
71
|
+
filters.set(dimension, set)
|
|
72
|
+
}
|
|
73
|
+
version++
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sets a continuous range filter for a dimension.
|
|
78
|
+
* @param {string} dimension
|
|
79
|
+
* @param {[number, number]} range
|
|
80
|
+
*/
|
|
81
|
+
function setRange(dimension, range) {
|
|
82
|
+
filters.set(dimension, [range[0], range[1]])
|
|
83
|
+
version++
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clears the filter for a single dimension.
|
|
88
|
+
* @param {string} dimension
|
|
89
|
+
*/
|
|
90
|
+
function clearFilter(dimension) {
|
|
91
|
+
filters.delete(dimension)
|
|
92
|
+
version++
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Clears all active filters. */
|
|
96
|
+
function clearAll() {
|
|
97
|
+
filters.clear()
|
|
98
|
+
version++
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
/** @readonly — reactive Map of current filter state */
|
|
103
|
+
get filters() { return filters },
|
|
104
|
+
/** @readonly — increments on every mutation; read inside $effect to react to any filter change */
|
|
105
|
+
get version() { return version },
|
|
106
|
+
isFiltered,
|
|
107
|
+
isDimmed,
|
|
108
|
+
toggleCategorical,
|
|
109
|
+
setRange,
|
|
110
|
+
clearFilter,
|
|
111
|
+
clearAll
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { swatch } from '../lib/swatch'
|
|
3
|
+
import { swatchGrid } from '../lib/grid'
|
|
4
4
|
import Symbol from '../Symbol.svelte'
|
|
5
5
|
|
|
6
6
|
let { base = 'teal', size = 4, shade = 600 } = $props()
|
|
7
7
|
|
|
8
|
-
let
|
|
9
|
-
let grid = $derived(swatchGrid(swatchValue.keys.symbol.length, size, 10))
|
|
8
|
+
let grid = $derived(swatchGrid(swatch.keys.symbol.length, size, 10))
|
|
10
9
|
</script>
|
|
11
10
|
|
|
12
11
|
<svg viewBox="0 0 {grid.width} {grid.height}">
|
|
@@ -15,9 +14,9 @@
|
|
|
15
14
|
{x}
|
|
16
15
|
{y}
|
|
17
16
|
size={r * 2}
|
|
18
|
-
name={
|
|
19
|
-
fill={
|
|
20
|
-
stroke={
|
|
17
|
+
name={swatch.keys.symbol[index]}
|
|
18
|
+
fill={swatch.palette[base][shade]}
|
|
19
|
+
stroke={swatch.palette[base][shade]}
|
|
21
20
|
/>
|
|
22
21
|
{/each}
|
|
23
22
|
</svg>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { buildArcs } from '../lib/brewing/marks/arcs.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `fill` is the primary prop name; `color` is accepted as an alias for
|
|
7
|
+
* spec-driven usage (Plot.svelte passes `color` to all geoms generically).
|
|
8
|
+
* @type {{ theta?: string, fill?: string, color?: string, pattern?: string, stat?: string, labelFn?: (data: Record<string, unknown>) => string, options?: { innerRadius?: number } }}
|
|
9
|
+
*/
|
|
10
|
+
let { theta, fill, color, pattern, labelFn = undefined, stat = 'identity', options = {} } = $props()
|
|
11
|
+
|
|
12
|
+
const fillField = $derived(fill ?? color)
|
|
13
|
+
|
|
14
|
+
const plotState = getContext('plot-state')
|
|
15
|
+
let id = $state(null)
|
|
16
|
+
|
|
17
|
+
onMount(() => {
|
|
18
|
+
id = plotState.registerGeom({ type: 'arc', channels: { color: fillField, y: theta, pattern }, stat, options })
|
|
19
|
+
})
|
|
20
|
+
onDestroy(() => { if (id) plotState.unregisterGeom(id) })
|
|
21
|
+
|
|
22
|
+
$effect(() => {
|
|
23
|
+
if (id) plotState.updateGeom(id, { channels: { color: fillField, y: theta, pattern }, stat })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const data = $derived(id ? plotState.geomData(id) : [])
|
|
27
|
+
const colors = $derived(plotState.colors)
|
|
28
|
+
const patterns = $derived(plotState.patterns)
|
|
29
|
+
const w = $derived(plotState.innerWidth)
|
|
30
|
+
const h = $derived(plotState.innerHeight)
|
|
31
|
+
|
|
32
|
+
const arcs = $derived.by(() => {
|
|
33
|
+
if (!data?.length) return []
|
|
34
|
+
// Guard: skip until data catches up after a fill-field change.
|
|
35
|
+
// When fillField changes, the $effect updates the geom asynchronously, but
|
|
36
|
+
// this derived runs first with stale data whose rows don't have the new
|
|
37
|
+
// field — causing all keys to be undefined (duplicate key error).
|
|
38
|
+
if (fillField && !(fillField in data[0])) return []
|
|
39
|
+
const innerRadius = (options.innerRadius ?? 0) * Math.min(w, h) / 2
|
|
40
|
+
return buildArcs(data, { color: fillField, y: theta, pattern }, colors, w, h, { innerRadius }, patterns)
|
|
41
|
+
})
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
{#if arcs.length > 0}
|
|
45
|
+
<g
|
|
46
|
+
data-plot-geom="arc"
|
|
47
|
+
transform="translate({w / 2}, {h / 2})"
|
|
48
|
+
>
|
|
49
|
+
{#each arcs as arc (arc.key)}
|
|
50
|
+
<path
|
|
51
|
+
d={arc.d}
|
|
52
|
+
fill={arc.fill}
|
|
53
|
+
stroke={arc.stroke}
|
|
54
|
+
stroke-width="1"
|
|
55
|
+
role="presentation"
|
|
56
|
+
data-plot-element="arc"
|
|
57
|
+
onmouseenter={() => plotState.setHovered({ ...arc.data, '%': `${arc.pct}%` })}
|
|
58
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
59
|
+
/>
|
|
60
|
+
{#if arc.patternId}
|
|
61
|
+
<path d={arc.d} fill="url(#{arc.patternId})" stroke={arc.stroke} stroke-width="1" pointer-events="none" data-plot-element="arc" />
|
|
62
|
+
{/if}
|
|
63
|
+
{#if arc.pct >= 5}
|
|
64
|
+
{@const labelText = labelFn ? String(labelFn(arc.data) ?? '') : `${arc.pct}%`}
|
|
65
|
+
{#if labelText}
|
|
66
|
+
{@const lw = Math.max(36, labelText.length * 7 + 12)}
|
|
67
|
+
<g transform="translate({arc.centroid[0]},{arc.centroid[1]})" pointer-events="none" data-plot-element="arc-label">
|
|
68
|
+
<rect x={-lw / 2} y="-9" width={lw} height="18" rx="4" fill="white" fill-opacity="0.82" />
|
|
69
|
+
<text
|
|
70
|
+
text-anchor="middle"
|
|
71
|
+
dominant-baseline="central"
|
|
72
|
+
font-size="11"
|
|
73
|
+
font-weight="600"
|
|
74
|
+
fill={arc.stroke}
|
|
75
|
+
>{labelText}</text>
|
|
76
|
+
</g>
|
|
77
|
+
{/if}
|
|
78
|
+
{/if}
|
|
79
|
+
{/each}
|
|
80
|
+
</g>
|
|
81
|
+
{/if}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { buildAreas, buildStackedAreas } from './lib/areas.js'
|
|
4
|
+
|
|
5
|
+
let { x, y, color, pattern, stat = 'identity', options = {} } = $props()
|
|
6
|
+
|
|
7
|
+
const plotState = getContext('plot-state')
|
|
8
|
+
let id = $state(null)
|
|
9
|
+
|
|
10
|
+
onMount(() => {
|
|
11
|
+
id = plotState.registerGeom({ type: 'area', channels: { x, y, color, pattern }, stat, options: { stack: options?.stack ?? false } })
|
|
12
|
+
})
|
|
13
|
+
onDestroy(() => { if (id) plotState.unregisterGeom(id) })
|
|
14
|
+
|
|
15
|
+
$effect(() => {
|
|
16
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color, pattern }, stat, options: { stack: options?.stack ?? false } })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const data = $derived(id ? plotState.geomData(id) : [])
|
|
20
|
+
const xScale = $derived(plotState.xScale)
|
|
21
|
+
const yScale = $derived(plotState.yScale)
|
|
22
|
+
const colors = $derived(plotState.colors)
|
|
23
|
+
const patterns = $derived(plotState.patterns)
|
|
24
|
+
|
|
25
|
+
const areas = $derived.by(() => {
|
|
26
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
27
|
+
const channels = { x, y, color, pattern }
|
|
28
|
+
if (options.stack) {
|
|
29
|
+
return buildStackedAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
|
|
30
|
+
}
|
|
31
|
+
return buildAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
|
|
32
|
+
})
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
{#if areas.length > 0}
|
|
36
|
+
<g data-plot-geom="area">
|
|
37
|
+
{#each areas as seg (seg.key ?? seg.d)}
|
|
38
|
+
<path
|
|
39
|
+
d={seg.d}
|
|
40
|
+
fill={seg.fill}
|
|
41
|
+
fill-opacity={seg.patternId ? 1 : (options.opacity ?? 0.6)}
|
|
42
|
+
stroke={seg.stroke ?? 'none'}
|
|
43
|
+
data-plot-element="area"
|
|
44
|
+
/>
|
|
45
|
+
{#if seg.patternId}
|
|
46
|
+
<path d={seg.d} fill="url(#{seg.patternId})" data-plot-element="area" />
|
|
47
|
+
{/if}
|
|
48
|
+
{/each}
|
|
49
|
+
</g>
|
|
50
|
+
{/if}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { buildGroupedBars, buildStackedBars, buildHorizontalBars } from './lib/bars.js'
|
|
4
|
+
import LabelPill from './LabelPill.svelte'
|
|
5
|
+
|
|
6
|
+
let { x, y, color, fill: fillProp, pattern, label = false, stat = 'identity', options = {}, filterable = false } = $props()
|
|
7
|
+
|
|
8
|
+
// `fill` is accepted as an alias for `color` (consistent with Arc.svelte)
|
|
9
|
+
const colorChannel = $derived(fillProp ?? color)
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {Record<string, unknown>} data
|
|
13
|
+
* @param {string} defaultField
|
|
14
|
+
* @returns {string | null}
|
|
15
|
+
*/
|
|
16
|
+
function resolveLabel(data, defaultField) {
|
|
17
|
+
if (!label) return null
|
|
18
|
+
if (label === true) return String(data[defaultField] ?? '')
|
|
19
|
+
if (typeof label === 'function') return String(label(data) ?? '')
|
|
20
|
+
if (typeof label === 'string') return String(data[label] ?? '')
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const plotState = getContext('plot-state')
|
|
25
|
+
const cf = getContext('crossfilter')
|
|
26
|
+
let id = $state(null)
|
|
27
|
+
|
|
28
|
+
onMount(() => {
|
|
29
|
+
id = plotState.registerGeom({ type: 'bar', channels: { x, y, color: colorChannel, pattern }, stat, options: { stack: options?.stack ?? false } })
|
|
30
|
+
})
|
|
31
|
+
onDestroy(() => {
|
|
32
|
+
if (id) plotState.unregisterGeom(id)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color: colorChannel, pattern }, stat, options: { stack: options?.stack ?? false } })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const data = $derived(id ? plotState.geomData(id) : [])
|
|
40
|
+
const xScale = $derived(plotState.xScale)
|
|
41
|
+
const yScale = $derived(plotState.yScale)
|
|
42
|
+
const colors = $derived(plotState.colors)
|
|
43
|
+
const patterns = $derived(plotState.patterns)
|
|
44
|
+
const orientation = $derived(plotState.orientation)
|
|
45
|
+
const innerHeight = $derived(plotState.innerHeight)
|
|
46
|
+
|
|
47
|
+
const bars = $derived.by(() => {
|
|
48
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
49
|
+
const channels = { x, y, color: colorChannel, pattern }
|
|
50
|
+
if (orientation === 'horizontal') {
|
|
51
|
+
return buildHorizontalBars(data, channels, xScale, yScale, colors, innerHeight)
|
|
52
|
+
}
|
|
53
|
+
if (options.stack) {
|
|
54
|
+
return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
55
|
+
}
|
|
56
|
+
return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
/** @type {Record<string, boolean>} */
|
|
60
|
+
let dimmedByKey = $state({})
|
|
61
|
+
|
|
62
|
+
$effect(() => {
|
|
63
|
+
if (!cf) { dimmedByKey = {}; return }
|
|
64
|
+
// cf.version is a $state counter that increments on every filter mutation.
|
|
65
|
+
// Reading it here establishes a reactive dependency so the effect re-runs
|
|
66
|
+
// whenever any filter changes — including changes from sibling FilterBars.
|
|
67
|
+
void cf.version
|
|
68
|
+
const next = /** @type {Record<string, boolean>} */ ({})
|
|
69
|
+
for (const bar of bars) {
|
|
70
|
+
const dimmedByX = x ? cf.isDimmed(x, bar.data[x]) : false
|
|
71
|
+
const dimmedByY = y ? cf.isDimmed(y, bar.data[y]) : false
|
|
72
|
+
next[bar.key] = dimmedByX || dimmedByY
|
|
73
|
+
}
|
|
74
|
+
dimmedByKey = next
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
function handleBarClick(barX) {
|
|
78
|
+
if (!filterable || !x || !cf) return
|
|
79
|
+
cf.toggleCategorical(x, barX)
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
{#if bars.length > 0}
|
|
84
|
+
<g data-plot-geom="bar">
|
|
85
|
+
{#each bars as bar (bar.key)}
|
|
86
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
87
|
+
<rect
|
|
88
|
+
x={bar.x}
|
|
89
|
+
y={bar.y}
|
|
90
|
+
width={Math.max(0, bar.width)}
|
|
91
|
+
height={Math.max(0, bar.height)}
|
|
92
|
+
fill={bar.fill}
|
|
93
|
+
stroke={bar.stroke ?? 'none'}
|
|
94
|
+
stroke-width={bar.stroke ? 0.5 : 0}
|
|
95
|
+
data-plot-element="bar"
|
|
96
|
+
data-plot-value={bar.data[y]}
|
|
97
|
+
data-plot-category={bar.data[x]}
|
|
98
|
+
data-dimmed={dimmedByKey[bar.key] ? true : undefined}
|
|
99
|
+
style:cursor={filterable ? 'pointer' : undefined}
|
|
100
|
+
onclick={filterable && x ? () => handleBarClick(bar.data[x]) : undefined}
|
|
101
|
+
onkeydown={filterable && x ? (e) => (e.key === 'Enter' || e.key === ' ') && handleBarClick(bar.data[x]) : undefined}
|
|
102
|
+
role={filterable ? 'button' : 'graphics-symbol'}
|
|
103
|
+
tabindex={filterable ? 0 : undefined}
|
|
104
|
+
aria-label="{bar.data[x]}: {bar.data[y]}"
|
|
105
|
+
onmouseenter={() => plotState.setHovered(bar.data)}
|
|
106
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
107
|
+
>
|
|
108
|
+
<title>{bar.data[x]}: {bar.data[y]}</title>
|
|
109
|
+
</rect>
|
|
110
|
+
{#if bar.patternId}
|
|
111
|
+
<rect
|
|
112
|
+
x={bar.x}
|
|
113
|
+
y={bar.y}
|
|
114
|
+
width={Math.max(0, bar.width)}
|
|
115
|
+
height={Math.max(0, bar.height)}
|
|
116
|
+
fill="url(#{bar.patternId})"
|
|
117
|
+
pointer-events="none"
|
|
118
|
+
/>
|
|
119
|
+
{/if}
|
|
120
|
+
{#if label}
|
|
121
|
+
{@const text = resolveLabel(bar.data, orientation === 'horizontal' ? x : y)}
|
|
122
|
+
{#if text}
|
|
123
|
+
{#if orientation === 'horizontal'}
|
|
124
|
+
<LabelPill
|
|
125
|
+
x={bar.x + bar.width + (options.labelOffset ?? 8)}
|
|
126
|
+
y={bar.y + bar.height / 2}
|
|
127
|
+
{text}
|
|
128
|
+
color={bar.stroke ?? '#333'}
|
|
129
|
+
/>
|
|
130
|
+
{:else}
|
|
131
|
+
<LabelPill
|
|
132
|
+
x={bar.x + bar.width / 2}
|
|
133
|
+
y={bar.y + (options.labelOffset ?? -8)}
|
|
134
|
+
{text}
|
|
135
|
+
color={bar.stroke ?? '#333'}
|
|
136
|
+
/>
|
|
137
|
+
{/if}
|
|
138
|
+
{/if}
|
|
139
|
+
{/if}
|
|
140
|
+
{/each}
|
|
141
|
+
</g>
|
|
142
|
+
{/if}
|