@rokkit/chart 1.0.0-next.150 → 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 +31 -3
- package/dist/crossfilter/createCrossFilter.svelte.d.ts +13 -15
- 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/brewer.svelte.d.ts +5 -36
- package/dist/lib/brewing/colors.d.ts +10 -1
- package/dist/lib/brewing/marks/points.d.ts +17 -2
- package/dist/lib/brewing/stats.d.ts +5 -13
- package/dist/lib/chart.d.ts +5 -7
- 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 -206
- package/src/Chart.svelte +81 -87
- 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 -182
- package/src/PlotState.svelte.js +339 -267
- 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 +100 -89
- 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 +102 -90
- package/src/geoms/LabelPill.svelte +11 -11
- package/src/geoms/Line.svelte +110 -87
- package/src/geoms/Point.svelte +132 -87
- package/src/geoms/Violin.svelte +45 -33
- 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 +12 -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 +249 -201
- 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 +20 -20
- package/src/lib/brewing/stats.js +40 -28
- package/src/lib/brewing/symbols.js +1 -1
- package/src/lib/chart.js +12 -4
- 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/PatternDef.svelte +1 -1
- package/src/patterns/patterns.js +328 -176
- package/src/patterns/scale.js +61 -32
- package/src/spec/chart-spec.js +64 -21
package/src/lib/plot/scales.js
CHANGED
|
@@ -2,55 +2,80 @@ import { scaleBand, scaleLinear } from 'd3-scale'
|
|
|
2
2
|
import { extent } from 'd3-array'
|
|
3
3
|
|
|
4
4
|
export function inferFieldType(data, field) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const values = data.map((d) => d[field]).filter((v) => v !== null && v !== undefined)
|
|
6
|
+
if (values.length === 0) return 'band'
|
|
7
|
+
const isNumeric = values.every(
|
|
8
|
+
(v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== '')
|
|
9
|
+
)
|
|
10
|
+
return isNumeric ? 'continuous' : 'band'
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function inferOrientation(xType, yType) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
if (xType === 'band' && yType === 'continuous') return 'vertical'
|
|
15
|
+
if (yType === 'band' && xType === 'continuous') return 'horizontal'
|
|
16
|
+
return 'none'
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export function buildUnifiedXScale(datasets, field, width, opts = {}) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
20
|
+
const allValues = datasets.flatMap((d) => d.map((r) => r[field]))
|
|
21
|
+
const isNumeric = allValues.every(
|
|
22
|
+
(v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== '')
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// opts.band forces scaleBand even for numeric data (e.g. bar charts with year on X).
|
|
26
|
+
if (opts.domain) {
|
|
27
|
+
const domainIsNumeric = opts.domain.every((v) => typeof v === 'number')
|
|
28
|
+
if (!opts.band && (domainIsNumeric || isNumeric)) {
|
|
29
|
+
return scaleLinear().domain(opts.domain).range([0, width]).nice()
|
|
30
|
+
}
|
|
31
|
+
return scaleBand()
|
|
32
|
+
.domain(opts.domain)
|
|
33
|
+
.range([0, width])
|
|
34
|
+
.padding(opts.padding ?? 0.2)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isNumeric && !opts.band) {
|
|
38
|
+
const numericValues = allValues.map(Number)
|
|
39
|
+
const [minVal, maxVal] = extent(numericValues)
|
|
40
|
+
const domainMin = (opts.includeZero ?? false) ? 0 : (minVal ?? 0)
|
|
41
|
+
return scaleLinear()
|
|
42
|
+
.domain([domainMin, maxVal ?? 0])
|
|
43
|
+
.range([0, width])
|
|
44
|
+
.nice()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const domain = [...new Set(allValues)].filter((v) => v !== null && v !== undefined)
|
|
48
|
+
return scaleBand()
|
|
49
|
+
.domain(domain)
|
|
50
|
+
.range([0, width])
|
|
51
|
+
.padding(opts.padding ?? 0.2)
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
export function buildUnifiedYScale(datasets, field, height, opts = {}) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
if (opts.domain) {
|
|
56
|
+
return scaleLinear().domain(opts.domain).range([height, 0]).nice()
|
|
57
|
+
}
|
|
58
|
+
const rawValues = datasets.flatMap((d) => d.map((r) => r[field])).filter((v) => v !== null && v !== undefined)
|
|
59
|
+
const isNumeric = rawValues.length > 0 && rawValues.every(
|
|
60
|
+
(v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== '')
|
|
61
|
+
)
|
|
62
|
+
if (!isNumeric) {
|
|
63
|
+
// Categorical y-axis (e.g. horizontal bar chart) — use scaleBand
|
|
64
|
+
const domain = [...new Set(rawValues.map(String))]
|
|
65
|
+
return scaleBand().domain(domain).range([height, 0]).padding(0.2)
|
|
66
|
+
}
|
|
67
|
+
const numericValues = rawValues.map(Number)
|
|
68
|
+
const [minVal, maxVal] = extent(numericValues)
|
|
69
|
+
const domainMin = (opts.includeZero ?? true) ? 0 : (minVal ?? 0)
|
|
70
|
+
return scaleLinear()
|
|
71
|
+
.domain([domainMin, maxVal ?? 0])
|
|
72
|
+
.range([height, 0])
|
|
73
|
+
.nice()
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
export function inferColorScaleType(data, field, spec = {}) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
if (spec.colorScale) return spec.colorScale
|
|
78
|
+
if (spec.colorMidpoint !== undefined) return 'diverging'
|
|
79
|
+
const type = inferFieldType(data, field)
|
|
80
|
+
return type === 'continuous' ? 'sequential' : 'categorical'
|
|
56
81
|
}
|
package/src/lib/plot/stat.js
CHANGED
|
@@ -2,12 +2,12 @@ import { sum, mean, min, max, median } from 'd3-array'
|
|
|
2
2
|
import { applyAggregate, applyBoxStat } from '../brewing/stats.js'
|
|
3
3
|
|
|
4
4
|
const BUILT_IN_STATS = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
sum,
|
|
6
|
+
mean,
|
|
7
|
+
min,
|
|
8
|
+
max,
|
|
9
|
+
count: (values) => values.length,
|
|
10
|
+
median
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -19,14 +19,14 @@ const BUILT_IN_STATS = {
|
|
|
19
19
|
* @returns {Function}
|
|
20
20
|
*/
|
|
21
21
|
export function resolveStat(name, helpers = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
if (name === 'identity') return (data) => data
|
|
23
|
+
if (BUILT_IN_STATS[name]) return BUILT_IN_STATS[name]
|
|
24
|
+
if (helpers?.stats?.[name]) return helpers.stats[name]
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
console.warn(
|
|
27
|
+
`[Plot] Unknown stat "${name}" — falling back to identity. Add it to helpers.stats to suppress this warning.`
|
|
28
|
+
)
|
|
29
|
+
return (data) => data
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -39,16 +39,16 @@ export function resolveStat(name, helpers = {}) {
|
|
|
39
39
|
* @returns {string[]}
|
|
40
40
|
*/
|
|
41
41
|
export function inferGroupByFields(channels, valueFields) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
const seen = new Set()
|
|
43
|
+
const result = []
|
|
44
|
+
for (const [key, field] of Object.entries(channels)) {
|
|
45
|
+
if (!field) continue
|
|
46
|
+
if (valueFields.includes(key) || valueFields.includes(field)) continue
|
|
47
|
+
if (seen.has(field)) continue
|
|
48
|
+
seen.add(field)
|
|
49
|
+
result.push(field)
|
|
50
|
+
}
|
|
51
|
+
return result
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
@@ -61,32 +61,32 @@ export function inferGroupByFields(channels, valueFields) {
|
|
|
61
61
|
* @returns {Object[]}
|
|
62
62
|
*/
|
|
63
63
|
export function applyGeomStat(data, geomConfig, helpers = {}) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const { stat = 'identity', channels = {} } = geomConfig
|
|
65
|
+
if (stat === 'identity') return data
|
|
66
|
+
if (stat === 'boxplot') return applyBoxStat(data, channels)
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
const statFn = resolveStat(stat, helpers)
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
const VALUE_CHANNEL_KEYS = ['y', 'size', 'theta']
|
|
71
|
+
const groupByFields = inferGroupByFields(channels, VALUE_CHANNEL_KEYS)
|
|
72
|
+
const primaryKey = VALUE_CHANNEL_KEYS.find((k) => channels[k])
|
|
73
|
+
if (!primaryKey) return data
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
let result = applyAggregate(data, {
|
|
76
|
+
by: groupByFields,
|
|
77
|
+
value: channels[primaryKey],
|
|
78
|
+
stat: statFn
|
|
79
|
+
})
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
for (const key of VALUE_CHANNEL_KEYS.filter((k) => k !== primaryKey && channels[k])) {
|
|
82
|
+
const extra = applyAggregate(data, { by: groupByFields, value: channels[key], stat: statFn })
|
|
83
|
+
const index = new Map(extra.map((r) => [groupByFields.map((f) => r[f]).join('|'), r]))
|
|
84
|
+
result = result.map((r) => {
|
|
85
|
+
const mapKey = groupByFields.map((f) => r[f]).join('|')
|
|
86
|
+
const extraRow = index.get(mapKey)
|
|
87
|
+
return extraRow ? { ...r, [channels[key]]: extraRow[channels[key]] } : r
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
return result
|
|
92
92
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// packages/chart/src/lib/preset.js
|
|
2
|
+
|
|
3
|
+
export const defaultPreset = {
|
|
4
|
+
colors: ['blue', 'emerald', 'rose', 'amber', 'violet', 'sky', 'pink', 'teal',
|
|
5
|
+
'orange', 'indigo', 'lime', 'cyan', 'gold', 'lavender'],
|
|
6
|
+
shades: {
|
|
7
|
+
light: { fill: '300', stroke: '700' },
|
|
8
|
+
dark: { fill: '500', stroke: '200' }
|
|
9
|
+
},
|
|
10
|
+
opacity: {
|
|
11
|
+
area: 0.6,
|
|
12
|
+
box: 0.5,
|
|
13
|
+
violin: 0.5,
|
|
14
|
+
point: 0.8
|
|
15
|
+
},
|
|
16
|
+
patterns: ['diagonal', 'dots', 'triangles', 'hatch', 'lattice', 'swell',
|
|
17
|
+
'checkerboard', 'waves', 'petals', 'brick', 'diamonds', 'tile',
|
|
18
|
+
'scales', 'circles', 'pip', 'rings', 'chevrons', 'shards',
|
|
19
|
+
'wedge', 'argyle', 'shell'],
|
|
20
|
+
symbols: ['circle', 'square', 'triangle', 'diamond', 'cross', 'star']
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a chart preset by deep-merging overrides with the default preset.
|
|
25
|
+
* All fields are optional. `opacity` is merged key-by-key so partial overrides work.
|
|
26
|
+
* @param {Partial<typeof defaultPreset>} [overrides]
|
|
27
|
+
* @returns {typeof defaultPreset}
|
|
28
|
+
*/
|
|
29
|
+
export function createChartPreset(overrides = {}) {
|
|
30
|
+
return {
|
|
31
|
+
...defaultPreset,
|
|
32
|
+
...overrides,
|
|
33
|
+
shades: overrides.shades
|
|
34
|
+
? {
|
|
35
|
+
light: { ...defaultPreset.shades.light, ...overrides.shades.light },
|
|
36
|
+
dark: { ...defaultPreset.shades.dark, ...overrides.shades.dark }
|
|
37
|
+
}
|
|
38
|
+
: defaultPreset.shades,
|
|
39
|
+
opacity: { ...defaultPreset.opacity, ...overrides.opacity }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { getContext } from 'svelte'
|
|
3
|
+
import { toPatternId } from '../lib/brewing/patterns.js'
|
|
4
|
+
import { PATTERNS } from './patterns.js'
|
|
5
|
+
import PatternDef from './PatternDef.svelte'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/** @type {{ patterns?: Record<string, import('./patterns.js').PatternMark[]> }} */
|
|
8
|
+
let { patterns = PATTERNS } = $props()
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const state = getContext('plot-state')
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
const patternDefs = $derived.by(() => {
|
|
13
|
+
const defs = []
|
|
14
|
+
for (const [key, patternName] of (state.patterns ?? new Map()).entries()) {
|
|
15
|
+
const colorEntry = state.colors?.get(key) ?? { stroke: '#444' }
|
|
16
|
+
defs.push({
|
|
17
|
+
id: toPatternId(String(key)),
|
|
18
|
+
marks: patterns[patternName] ?? [],
|
|
19
|
+
stroke: colorEntry.stroke ?? '#444'
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
return defs
|
|
23
|
+
})
|
|
24
24
|
</script>
|
|
25
25
|
|
|
26
26
|
{#if patternDefs.length > 0}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
<defs data-plot-pattern-defs>
|
|
28
|
+
{#each patternDefs as def (def.id)}
|
|
29
|
+
<PatternDef id={def.id} marks={def.marks} stroke={def.stroke} />
|
|
30
|
+
{/each}
|
|
31
|
+
</defs>
|
|
32
32
|
{/if}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
<pattern {id} patternUnits="userSpaceOnUse" width={size} height={size}>
|
|
13
13
|
<rect width={size} height={size} fill="none" />
|
|
14
|
-
{#each resolvedMarks as { type, attrs }}
|
|
14
|
+
{#each resolvedMarks as { type, attrs }, i (i)}
|
|
15
15
|
{#if type === 'line'}
|
|
16
16
|
<line {...attrs} />
|
|
17
17
|
{:else if type === 'circle'}
|