@rokkit/chart 1.0.0-next.16 → 1.0.0-next.161
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/README.md +150 -46
- package/package.json +42 -45
- package/src/AnimatedPlot.svelte +383 -0
- package/src/Chart.svelte +95 -0
- package/src/ChartProvider.svelte +10 -0
- package/src/FacetPlot/Panel.svelte +37 -0
- package/src/FacetPlot.svelte +114 -0
- package/src/Plot/Arc.svelte +29 -0
- package/src/Plot/Area.svelte +32 -0
- package/src/Plot/Axis.svelte +95 -0
- package/src/Plot/Bar.svelte +54 -0
- package/src/Plot/Grid.svelte +34 -0
- package/src/Plot/Legend.svelte +233 -0
- package/src/Plot/Line.svelte +37 -0
- package/src/Plot/Point.svelte +40 -0
- package/src/Plot/Root.svelte +62 -0
- package/src/Plot/Timeline.svelte +95 -0
- package/src/Plot/Tooltip.svelte +87 -0
- package/src/Plot/index.js +9 -0
- package/src/Plot.svelte +297 -0
- package/src/PlotState.svelte.js +350 -0
- package/src/Sparkline.svelte +108 -0
- package/src/Symbol.svelte +21 -0
- package/src/Texture.svelte +18 -0
- package/src/charts/AreaChart.svelte +27 -0
- package/src/charts/BarChart.svelte +28 -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 +35 -0
- package/src/charts/ScatterPlot.svelte +26 -0
- package/src/charts/ViolinPlot.svelte +21 -0
- package/src/crossfilter/CrossFilter.svelte +42 -0
- package/src/crossfilter/FilterBar.svelte +24 -0
- package/src/crossfilter/FilterHistogram.svelte +290 -0
- package/src/crossfilter/FilterSlider.svelte +83 -0
- package/src/crossfilter/createCrossFilter.svelte.js +124 -0
- package/src/elements/Bar.svelte +22 -24
- package/src/elements/ColorRamp.svelte +20 -22
- package/src/elements/ContinuousLegend.svelte +20 -17
- package/src/elements/DefinePatterns.svelte +24 -0
- package/src/elements/DiscreteLegend.svelte +15 -15
- package/src/elements/Label.svelte +4 -8
- package/src/elements/SymbolGrid.svelte +22 -0
- package/src/elements/index.js +6 -0
- package/src/examples/BarChartExample.svelte +81 -0
- package/src/geoms/Arc.svelte +126 -0
- package/src/geoms/Area.svelte +78 -0
- package/src/geoms/Bar.svelte +200 -0
- package/src/geoms/Box.svelte +113 -0
- package/src/geoms/LabelPill.svelte +17 -0
- package/src/geoms/Line.svelte +123 -0
- package/src/geoms/Point.svelte +145 -0
- package/src/geoms/Violin.svelte +56 -0
- package/src/geoms/lib/areas.js +154 -0
- package/src/geoms/lib/bars.js +223 -0
- package/src/index.js +74 -16
- package/src/lib/brewer.js +25 -0
- package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
- package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
- package/src/lib/brewing/PieBrewer.svelte.js +14 -0
- package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
- package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
- package/src/lib/brewing/axes.svelte.js +270 -0
- package/src/lib/brewing/bars.svelte.js +201 -0
- package/src/lib/brewing/brewer.svelte.js +277 -0
- package/src/lib/brewing/colors.js +51 -0
- package/src/lib/brewing/dimensions.svelte.js +56 -0
- package/src/lib/brewing/index.svelte.js +205 -0
- package/src/lib/brewing/legends.svelte.js +137 -0
- package/src/lib/brewing/marks/arcs.js +43 -0
- package/src/lib/brewing/marks/areas.js +72 -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 +55 -0
- package/src/lib/brewing/marks/points.js +105 -0
- package/src/lib/brewing/marks/violins.js +90 -0
- package/src/lib/brewing/patterns.js +45 -0
- package/src/lib/brewing/scales.js +51 -0
- package/src/lib/brewing/scales.svelte.js +82 -0
- package/src/lib/brewing/stats.js +74 -0
- package/src/lib/brewing/symbols.js +10 -0
- package/src/lib/brewing/types.js +73 -0
- package/src/lib/chart.js +221 -0
- package/src/lib/context.js +131 -0
- package/src/lib/grid.js +85 -0
- package/src/lib/keyboard-nav.js +37 -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 +81 -0
- package/src/lib/plot/helpers.js +14 -0
- package/src/lib/plot/preset.js +67 -0
- package/src/lib/plot/scales.js +81 -0
- package/src/lib/plot/stat.js +92 -0
- package/src/lib/plot/types.js +65 -0
- package/src/lib/preset.js +41 -0
- package/src/lib/scales.svelte.js +151 -0
- package/src/lib/swatch.js +13 -0
- package/src/lib/ticks.js +46 -0
- package/src/lib/utils.js +111 -118
- 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 -0
- package/src/patterns/patterns.js +360 -0
- package/src/patterns/scale.js +116 -0
- package/src/spec/chart-spec.js +72 -0
- package/src/symbols/RoundedSquare.svelte +33 -0
- package/src/symbols/Shape.svelte +37 -0
- package/src/symbols/constants/index.js +4 -0
- package/src/symbols/index.js +9 -0
- package/src/symbols/outline.svelte +60 -0
- package/src/symbols/solid.svelte +60 -0
- package/LICENSE +0 -21
- package/src/chart/FacetGrid.svelte +0 -51
- package/src/chart/Grid.svelte +0 -34
- package/src/chart/Legend.svelte +0 -16
- package/src/chart/PatternDefs.svelte +0 -13
- package/src/chart/Swatch.svelte +0 -93
- package/src/chart/SwatchButton.svelte +0 -29
- package/src/chart/SwatchGrid.svelte +0 -55
- package/src/chart/Symbol.svelte +0 -37
- package/src/chart/Texture.svelte +0 -16
- package/src/chart/TexturedShape.svelte +0 -27
- package/src/chart/TimelapseChart.svelte +0 -97
- package/src/chart/Timer.svelte +0 -27
- package/src/chart.js +0 -9
- package/src/components/charts/Axis.svelte +0 -66
- package/src/components/charts/Chart.svelte +0 -35
- package/src/components/index.js +0 -23
- package/src/components/lib/axis.js +0 -0
- package/src/components/lib/chart.js +0 -187
- package/src/components/lib/color.js +0 -327
- package/src/components/lib/funnel.js +0 -204
- package/src/components/lib/index.js +0 -19
- package/src/components/lib/pattern.js +0 -190
- package/src/components/lib/rollup.js +0 -55
- package/src/components/lib/shape.js +0 -199
- package/src/components/lib/summary.js +0 -145
- package/src/components/lib/theme.js +0 -23
- package/src/components/lib/timer.js +0 -41
- package/src/components/lib/utils.js +0 -165
- package/src/components/plots/BarPlot.svelte +0 -36
- package/src/components/plots/BoxPlot.svelte +0 -54
- package/src/components/plots/ScatterPlot.svelte +0 -30
- package/src/components/store.js +0 -70
- package/src/constants.js +0 -66
- package/src/elements/PatternDefs.svelte +0 -13
- package/src/elements/PatternMask.svelte +0 -20
- package/src/elements/Symbol.svelte +0 -38
- package/src/elements/Tooltip.svelte +0 -23
- package/src/funnel.svelte +0 -35
- package/src/geom.js +0 -105
- package/src/lib/axis.js +0 -75
- package/src/lib/colors.js +0 -32
- package/src/lib/geom.js +0 -4
- package/src/lib/shapes.js +0 -144
- package/src/lib/timer.js +0 -44
- package/src/lookup.js +0 -29
- package/src/plots/BarPlot.svelte +0 -55
- package/src/plots/BoxPlot.svelte +0 -0
- package/src/plots/FunnelPlot.svelte +0 -33
- package/src/plots/HeatMap.svelte +0 -5
- package/src/plots/HeatMapCalendar.svelte +0 -129
- package/src/plots/LinePlot.svelte +0 -55
- package/src/plots/Plot.svelte +0 -25
- package/src/plots/RankBarPlot.svelte +0 -38
- package/src/plots/ScatterPlot.svelte +0 -20
- package/src/plots/ViolinPlot.svelte +0 -11
- package/src/plots/heatmap.js +0 -70
- package/src/plots/index.js +0 -10
- package/src/swatch.js +0 -11
package/src/lib/grid.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef GridPoint
|
|
3
|
+
* @property {number} x - x-coordinate of the point
|
|
4
|
+
* @property {number} y - y-coordinate of the point
|
|
5
|
+
* @property {number} r - radius of the point
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef SwatchGrid
|
|
10
|
+
* @property {number} width - width of the grid
|
|
11
|
+
* @property {number} height - height of the grid
|
|
12
|
+
* @property {GridPoint[]} data - data points of the grid
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @tyoedef {Object} GridOptions
|
|
17
|
+
* @property {number} [pad=0] - The padding between the items
|
|
18
|
+
* @property {number} [columns=0] - The number of columns
|
|
19
|
+
* @property {number} [rows=0] - The number of rows
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculates a grid of centres to fit a list of items of `size` within the number of `columns` and `rows`.
|
|
24
|
+
*
|
|
25
|
+
* - Attempts to find a best fit square if both columns and rows are not specified
|
|
26
|
+
* - Value in columns is prioritized over rows for recalculating the grid
|
|
27
|
+
* - Supports padding between the items
|
|
28
|
+
*
|
|
29
|
+
* @param {number} count - number of items
|
|
30
|
+
* @param {number} size - size of the items
|
|
31
|
+
* @param {GridOptions} options - options for the grid
|
|
32
|
+
* @returns {SwatchGrid}
|
|
33
|
+
*/
|
|
34
|
+
export function swatchGrid(count, size, options) {
|
|
35
|
+
const { pad = 0 } = options || {}
|
|
36
|
+
let { columns = 0, rows = 0 } = options || {}
|
|
37
|
+
if (columns > 0) {
|
|
38
|
+
rows = Math.ceil(count / columns)
|
|
39
|
+
} else if (rows > 0) {
|
|
40
|
+
columns = Math.ceil(count / rows)
|
|
41
|
+
} else {
|
|
42
|
+
columns = Math.ceil(Math.sqrt(count))
|
|
43
|
+
rows = Math.ceil(count / columns)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const width = (size + pad) * columns + pad
|
|
47
|
+
const height = (size + pad) * rows + pad
|
|
48
|
+
const radius = size / 2
|
|
49
|
+
const data = [...Array(count).keys()].map((index) => ({
|
|
50
|
+
x: pad + radius + (index % columns) * (size + pad),
|
|
51
|
+
y: pad + radius + Math.floor(index / columns) * (size + pad),
|
|
52
|
+
r: radius
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
return { width, height, data }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Spreads values as patterns with colors from a palette
|
|
60
|
+
*
|
|
61
|
+
* @param {number[]} values - values to spread
|
|
62
|
+
* @param {string[]} patterns - patterns to spread
|
|
63
|
+
* @param {string[]} palette - colors to spread
|
|
64
|
+
* @returns {Record<number, { id: string, pattern: string, color: string }>}
|
|
65
|
+
*/
|
|
66
|
+
export function spreadValuesAsPatterns(values, patterns, palette) {
|
|
67
|
+
const result = values
|
|
68
|
+
.map((value, index) => ({
|
|
69
|
+
pattern: patterns[index % patterns.length],
|
|
70
|
+
color: palette[index % palette.length],
|
|
71
|
+
value
|
|
72
|
+
}))
|
|
73
|
+
.reduce(
|
|
74
|
+
(acc, { value, pattern, color }) => ({
|
|
75
|
+
...acc,
|
|
76
|
+
[value]: {
|
|
77
|
+
id: `${pattern}_${color}`,
|
|
78
|
+
pattern,
|
|
79
|
+
color
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
{}
|
|
83
|
+
)
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action: arrow key navigation between sibling data elements within a geom.
|
|
3
|
+
*
|
|
4
|
+
* When applied to a focusable SVG element with `enabled=true`, ArrowLeft/ArrowRight
|
|
5
|
+
* move focus between elements sharing the same `[data-plot-geom]` container.
|
|
6
|
+
*
|
|
7
|
+
* Usage: `<circle use:keyboardNav={keyboard} ...>`
|
|
8
|
+
*
|
|
9
|
+
* @param {Element} node
|
|
10
|
+
* @param {boolean} enabled
|
|
11
|
+
*/
|
|
12
|
+
export function keyboardNav(node, enabled) {
|
|
13
|
+
function handleKeydown(e) {
|
|
14
|
+
if (!enabled) return
|
|
15
|
+
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return
|
|
16
|
+
const container = node.closest('[data-plot-geom]')
|
|
17
|
+
if (!container) return
|
|
18
|
+
const elements = [...container.querySelectorAll('[data-plot-element]')].filter(
|
|
19
|
+
(el) => /** @type {HTMLElement|SVGElement} */ (el).tabIndex >= 0
|
|
20
|
+
)
|
|
21
|
+
const idx = elements.indexOf(node)
|
|
22
|
+
if (idx === -1) return
|
|
23
|
+
const nextIdx =
|
|
24
|
+
e.key === 'ArrowRight' ? Math.min(idx + 1, elements.length - 1) : Math.max(idx - 1, 0)
|
|
25
|
+
if (nextIdx !== idx) {
|
|
26
|
+
e.preventDefault()
|
|
27
|
+
/** @type {HTMLElement|SVGElement} */ (elements[nextIdx]).focus()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
node.addEventListener('keydown', handleKeydown)
|
|
31
|
+
return {
|
|
32
|
+
update(newEnabled) {
|
|
33
|
+
enabled = newEnabled
|
|
34
|
+
},
|
|
35
|
+
destroy: () => node.removeEventListener('keydown', handleKeydown)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared JSDoc type definitions for chart component props.
|
|
3
|
+
* Import these in chart components with:
|
|
4
|
+
* @type {import('./lib/plot/chartProps.js').ChartProps}
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} ChartProps
|
|
9
|
+
* Common props shared by all high-level chart wrappers.
|
|
10
|
+
*
|
|
11
|
+
* @property {Object[]} [data=[]] - Data array to visualize
|
|
12
|
+
* @property {string} [x] - Field name for the X axis
|
|
13
|
+
* @property {string} [y] - Field name for the Y axis
|
|
14
|
+
* @property {string} [fill] - Field name for fill color grouping (alias for color)
|
|
15
|
+
* @property {string} [color] - Field name for color grouping
|
|
16
|
+
* @property {string} [pattern] - Field name for pattern fill grouping
|
|
17
|
+
* @property {string} [stat='identity']- Aggregation stat: 'identity' | 'sum' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'boxplot'
|
|
18
|
+
* @property {number} [width=600] - SVG width in pixels
|
|
19
|
+
* @property {number} [height=400] - SVG height in pixels
|
|
20
|
+
* @property {string} [mode='light'] - Color mode: 'light' | 'dark'
|
|
21
|
+
* @property {boolean} [grid=true] - Whether to show grid lines
|
|
22
|
+
* @property {boolean} [legend=false] - Whether to show the legend
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} BarChartProps
|
|
27
|
+
* @extends ChartProps
|
|
28
|
+
* @property {boolean} [stack=false] - Stack bars (true) or group them (false)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} PieChartProps
|
|
33
|
+
* @property {Object[]} [data=[]] - Data array to visualize
|
|
34
|
+
* @property {string} [label] - Field name for slice labels (drives color key)
|
|
35
|
+
* @property {string} [y] - Field name for slice values (theta)
|
|
36
|
+
* @property {string} [fill] - Alternative to label for color grouping
|
|
37
|
+
* @property {number} [innerRadius=0] - Inner radius as fraction of outer (0=pie, 0.5=donut)
|
|
38
|
+
* @property {string} [stat='sum'] - Aggregation stat (default sum for pie charts)
|
|
39
|
+
* @property {number} [width=400] - SVG width in pixels
|
|
40
|
+
* @property {number} [height=400] - SVG height in pixels
|
|
41
|
+
* @property {string} [mode='light'] - Color mode: 'light' | 'dark'
|
|
42
|
+
* @property {boolean} [legend=false] - Whether to show the legend
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} BoxViolinChartProps
|
|
47
|
+
* @property {Object[]} [data=[]] - Data array to visualize
|
|
48
|
+
* @property {string} [x] - Field name for the category axis (groups)
|
|
49
|
+
* @property {string} [y] - Field name for the value axis (raw observations)
|
|
50
|
+
* @property {string} [fill] - Field name for fill color; when different from x, sub-groups within each x-band (like grouped bars); lighter shade used for body, darker shade for whiskers/outline
|
|
51
|
+
* @property {number} [width=600] - SVG width in pixels
|
|
52
|
+
* @property {number} [height=400] - SVG height in pixels
|
|
53
|
+
* @property {string} [mode='light'] - Color mode: 'light' | 'dark'
|
|
54
|
+
* @property {boolean} [grid=true] - Whether to show grid lines
|
|
55
|
+
* @property {boolean} [legend=false] - Whether to show the legend
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {Object} ScatterBubbleChartProps
|
|
60
|
+
* @property {Object[]} [data=[]] - Data array to visualize
|
|
61
|
+
* @property {string} [x] - Field name for X position
|
|
62
|
+
* @property {string} [y] - Field name for Y position
|
|
63
|
+
* @property {string} [color] - Field name for color grouping
|
|
64
|
+
* @property {string} [size] - Field name for point radius (BubbleChart: required)
|
|
65
|
+
* @property {number} [width=600] - SVG width in pixels
|
|
66
|
+
* @property {number} [height=400] - SVG height in pixels
|
|
67
|
+
* @property {string} [mode='light'] - Color mode: 'light' | 'dark'
|
|
68
|
+
* @property {boolean} [grid=true] - Whether to show grid lines
|
|
69
|
+
* @property {boolean} [legend=false] - Whether to show the legend
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {Object} LineAreaChartProps
|
|
74
|
+
* @extends ChartProps
|
|
75
|
+
* @property {string} [curve] - Line interpolation: 'linear' | 'smooth' | 'step'
|
|
76
|
+
*/
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies dimming state to data rows using crossfilter instance.
|
|
3
|
+
* Maps each row to { data: row, dimmed: boolean }.
|
|
4
|
+
*
|
|
5
|
+
* @param {Object[]} data - raw data array
|
|
6
|
+
* @param {Object} cf - crossfilter instance (from createCrossFilter)
|
|
7
|
+
* @param {Object} channels - { x, y, color, ... } field name mapping
|
|
8
|
+
* @returns {{ data: Object, dimmed: boolean }[]}
|
|
9
|
+
*/
|
|
10
|
+
export function applyDimming(data, cf, channels) {
|
|
11
|
+
const fields = Object.values(channels).filter(Boolean)
|
|
12
|
+
return data.map((row) => {
|
|
13
|
+
const dimmed = fields.some((field) => cf.isDimmed(field, row[field]))
|
|
14
|
+
return { data: row, dimmed }
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { extent } from 'd3-array'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Splits data into a Map of panels keyed by facet field value.
|
|
5
|
+
* Preserves insertion order of first occurrence.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object[]} data
|
|
8
|
+
* @param {string} field
|
|
9
|
+
* @returns {Map<unknown, Object[]>}
|
|
10
|
+
*/
|
|
11
|
+
export function splitByField(data, field) {
|
|
12
|
+
const map = new Map()
|
|
13
|
+
for (const row of data) {
|
|
14
|
+
const key = row[field]
|
|
15
|
+
if (!map.has(key)) map.set(key, [])
|
|
16
|
+
map.get(key).push(row)
|
|
17
|
+
}
|
|
18
|
+
return map
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Computes x/y domains for each panel.
|
|
23
|
+
*
|
|
24
|
+
* @param {Map<unknown, Object[]>} panels
|
|
25
|
+
* @param {{ x: string, y: string }} channels
|
|
26
|
+
* @param {'fixed'|'free'|'free_x'|'free_y'} scalesMode
|
|
27
|
+
* @returns {Map<unknown, { xDomain: unknown[], yDomain: [number, number] }>}
|
|
28
|
+
*/
|
|
29
|
+
export function getFacetDomains(panels, channels, scalesMode = 'fixed') {
|
|
30
|
+
const { x: xf, y: yf } = channels
|
|
31
|
+
const allData = [...panels.values()].flat()
|
|
32
|
+
|
|
33
|
+
// Determine if x is categorical (string) or numeric
|
|
34
|
+
const sampleXVal = allData.find((d) => d[xf] !== null && d[xf] !== undefined)?.[xf]
|
|
35
|
+
const xIsCategorical = typeof sampleXVal === 'string'
|
|
36
|
+
|
|
37
|
+
// Global domains (for fixed mode)
|
|
38
|
+
const globalXDomain = xIsCategorical
|
|
39
|
+
? [...new Set(allData.map((d) => d[xf]))]
|
|
40
|
+
: extent(allData, (d) => Number(d[xf]))
|
|
41
|
+
const globalYDomain = extent(allData, (d) => Number(d[yf]))
|
|
42
|
+
|
|
43
|
+
const result = new Map()
|
|
44
|
+
for (const [key, rows] of panels.entries()) {
|
|
45
|
+
const freeX = scalesMode === 'free' || scalesMode === 'free_x'
|
|
46
|
+
const freeY = scalesMode === 'free' || scalesMode === 'free_y'
|
|
47
|
+
|
|
48
|
+
const xDomain = freeX
|
|
49
|
+
? xIsCategorical
|
|
50
|
+
? [...new Set(rows.map((d) => d[xf]))]
|
|
51
|
+
: extent(rows, (d) => Number(d[xf]))
|
|
52
|
+
: globalXDomain
|
|
53
|
+
const yDomain = freeY ? extent(rows, (d) => Number(d[yf])) : globalYDomain
|
|
54
|
+
|
|
55
|
+
result.set(key, { xDomain, yDomain })
|
|
56
|
+
}
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { extent } from 'd3-array'
|
|
2
|
+
import { dataset } from '@rokkit/data'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extracts animation frames from data, keyed by time field value.
|
|
6
|
+
* Preserves insertion order of time values.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object[]} data
|
|
9
|
+
* @param {string} timeField
|
|
10
|
+
* @returns {Map<unknown, Object[]>}
|
|
11
|
+
*/
|
|
12
|
+
export function extractFrames(data, timeField) {
|
|
13
|
+
const map = new Map()
|
|
14
|
+
for (const row of data) {
|
|
15
|
+
const key = row[timeField]
|
|
16
|
+
if (!map.has(key)) map.set(key, [])
|
|
17
|
+
map.get(key).push(row)
|
|
18
|
+
}
|
|
19
|
+
return map
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ensures all frame values (byField) appear for every (x, color?) combination.
|
|
24
|
+
* Uses dataset alignBy to fill missing frame-value combos with y=0 so bars
|
|
25
|
+
* animate smoothly rather than disappearing between frames.
|
|
26
|
+
*
|
|
27
|
+
* Call after pre-aggregation. The result can be split directly by extractFrames
|
|
28
|
+
* with no further per-frame normalization needed.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object[]} data - pre-aggregated rows, one per (x, color?, byField)
|
|
31
|
+
* @param {{ x?: string, y: string, color?: string }} channels
|
|
32
|
+
* @param {string} byField - the frame field (e.g. 'year')
|
|
33
|
+
* @returns {Object[]}
|
|
34
|
+
*/
|
|
35
|
+
export function completeFrames(data, channels, byField) {
|
|
36
|
+
const { x: xf, y: yf, color: cf } = channels
|
|
37
|
+
const groupFields = [xf, ...(cf ? [cf] : [])].filter(Boolean)
|
|
38
|
+
|
|
39
|
+
if (groupFields.length === 0) return data
|
|
40
|
+
|
|
41
|
+
const nested = dataset(data)
|
|
42
|
+
.groupBy(...groupFields)
|
|
43
|
+
.alignBy(byField)
|
|
44
|
+
.usingTemplate({ [yf]: 0 })
|
|
45
|
+
.rollup()
|
|
46
|
+
.select()
|
|
47
|
+
|
|
48
|
+
return nested.flatMap((row) => {
|
|
49
|
+
const groupKey = groupFields.reduce((acc, f) => ({ ...acc, [f]: row[f] }), {})
|
|
50
|
+
// strip the actual_flag marker added by alignBy
|
|
51
|
+
return row.children.map(({ actual_flag: _af, ...child }) => ({ ...groupKey, ...child }))
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Computes static x/y domains from the full (pre-split) data array.
|
|
57
|
+
* These domains stay constant throughout the animation so values are
|
|
58
|
+
* always comparable across frames.
|
|
59
|
+
*
|
|
60
|
+
* NOTE: y domain is pinned to [0, max] — assumes bar chart semantics.
|
|
61
|
+
* Pass an explicit yDomain override for scatter/line charts where y can
|
|
62
|
+
* be negative.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object[]} data - full dataset (before frame extraction)
|
|
65
|
+
* @param {{ x: string, y: string }} channels
|
|
66
|
+
* @returns {{ xDomain: unknown[], yDomain: [number, number] }}
|
|
67
|
+
*/
|
|
68
|
+
export function computeStaticDomains(data, channels) {
|
|
69
|
+
const { x: xf, y: yf } = channels
|
|
70
|
+
|
|
71
|
+
const sampleX = data[0]?.[xf]
|
|
72
|
+
const xDomain =
|
|
73
|
+
typeof sampleX === 'string'
|
|
74
|
+
? [...new Set(data.map((d) => d[xf]))]
|
|
75
|
+
: extent(data, (d) => Number(d[xf]))
|
|
76
|
+
|
|
77
|
+
const [, yMax] = extent(data, (d) => Number(d[yf]))
|
|
78
|
+
const yDomain = [0, yMax ?? 0]
|
|
79
|
+
|
|
80
|
+
return { xDomain, yDomain }
|
|
81
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const BUILT_IN_GEOMS = new Set(['bar', 'line', 'area', 'point', 'box', 'violin', 'arc'])
|
|
2
|
+
|
|
3
|
+
export function resolveFormat(field, helpers = {}) {
|
|
4
|
+
return helpers?.format?.[field] ?? ((v) => String(v))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveTooltip(helpers = {}) {
|
|
8
|
+
return helpers?.tooltip ?? null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveGeom(type, helpers = {}) {
|
|
12
|
+
if (BUILT_IN_GEOMS.has(type)) return null
|
|
13
|
+
return helpers?.geoms?.[type] ?? null
|
|
14
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import masterPalette from '../palette.json'
|
|
2
|
+
import { defaultPreset } from '../preset.js'
|
|
3
|
+
import { PATTERN_ORDER } from '../brewing/patterns.js'
|
|
4
|
+
import { SYMBOL_ORDER } from '../brewing/symbols.js'
|
|
5
|
+
|
|
6
|
+
/** @typedef {{ colors: string[], patterns: string[], symbols: string[] }} PlotPreset */
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_PRESET = {
|
|
9
|
+
colors: defaultPreset.colors.map((name) => {
|
|
10
|
+
const group = masterPalette[name]
|
|
11
|
+
return group?.[defaultPreset.shades.light.fill] ?? '#888'
|
|
12
|
+
}),
|
|
13
|
+
patterns: PATTERN_ORDER,
|
|
14
|
+
symbols: SYMBOL_ORDER
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ACCESSIBLE_PRESET = {
|
|
18
|
+
colors: ['#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', '#e5c494', '#b3b3b3'],
|
|
19
|
+
patterns: PATTERN_ORDER,
|
|
20
|
+
symbols: SYMBOL_ORDER
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const PRINT_PRESET = {
|
|
24
|
+
colors: ['#f0f0f0', '#bdbdbd', '#969696', '#737373', '#525252', '#252525', '#000000'],
|
|
25
|
+
patterns: [
|
|
26
|
+
'CrossHatch',
|
|
27
|
+
'DiagonalLines',
|
|
28
|
+
'Dots',
|
|
29
|
+
'Brick',
|
|
30
|
+
'Waves',
|
|
31
|
+
'Triangles',
|
|
32
|
+
'HorizontalLines'
|
|
33
|
+
],
|
|
34
|
+
symbols: SYMBOL_ORDER
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const BUILT_IN_PRESETS = {
|
|
38
|
+
default: DEFAULT_PRESET,
|
|
39
|
+
accessible: ACCESSIBLE_PRESET,
|
|
40
|
+
print: PRINT_PRESET
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolvePreset(name, helpers = {}) {
|
|
44
|
+
let resolved = null
|
|
45
|
+
|
|
46
|
+
if (name && BUILT_IN_PRESETS[name]) {
|
|
47
|
+
resolved = BUILT_IN_PRESETS[name]
|
|
48
|
+
} else if (name && helpers?.presets?.[name]) {
|
|
49
|
+
resolved = helpers.presets[name]
|
|
50
|
+
} else if (name) {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.warn(
|
|
53
|
+
`[Plot] Unknown preset "${name}" — falling back to default. Add it to helpers.presets to suppress this warning.`
|
|
54
|
+
)
|
|
55
|
+
resolved = DEFAULT_PRESET
|
|
56
|
+
} else if (helpers?.preset) {
|
|
57
|
+
resolved = helpers.preset
|
|
58
|
+
} else {
|
|
59
|
+
resolved = DEFAULT_PRESET
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
colors: resolved.colors ?? DEFAULT_PRESET.colors,
|
|
64
|
+
patterns: resolved.patterns ?? DEFAULT_PRESET.patterns,
|
|
65
|
+
symbols: resolved.symbols ?? DEFAULT_PRESET.symbols
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { scaleBand, scaleLinear } from 'd3-scale'
|
|
2
|
+
import { extent } from 'd3-array'
|
|
3
|
+
|
|
4
|
+
export function inferFieldType(data, field) {
|
|
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'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function inferOrientation(xType, yType) {
|
|
14
|
+
if (xType === 'band' && yType === 'continuous') return 'vertical'
|
|
15
|
+
if (yType === 'band' && xType === 'continuous') return 'horizontal'
|
|
16
|
+
return 'none'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildUnifiedXScale(datasets, field, width, opts = {}) {
|
|
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)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildUnifiedYScale(datasets, field, height, opts = {}) {
|
|
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()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function inferColorScaleType(data, field, spec = {}) {
|
|
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'
|
|
81
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { sum, mean, min, max, median } from 'd3-array'
|
|
2
|
+
import { applyAggregate, applyBoxStat } from '../brewing/stats.js'
|
|
3
|
+
|
|
4
|
+
const BUILT_IN_STATS = {
|
|
5
|
+
sum,
|
|
6
|
+
mean,
|
|
7
|
+
min,
|
|
8
|
+
max,
|
|
9
|
+
count: (values) => values.length,
|
|
10
|
+
median
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolves a stat name to an aggregation function.
|
|
15
|
+
* Checks built-ins first, then helpers.stats, then warns and falls back to identity.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} name
|
|
18
|
+
* @param {Object} helpers
|
|
19
|
+
* @returns {Function}
|
|
20
|
+
*/
|
|
21
|
+
export function resolveStat(name, helpers = {}) {
|
|
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
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Infers group-by fields from channels by excluding value fields.
|
|
34
|
+
* valueFields may contain channel keys (e.g. ['y', 'size']) OR field values (e.g. ['cty']).
|
|
35
|
+
* A channel's field is excluded if either the channel key OR the field value is in valueFields.
|
|
36
|
+
*
|
|
37
|
+
* @param {Record<string, string|undefined>} channels
|
|
38
|
+
* @param {string[]} valueFields
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
export function inferGroupByFields(channels, valueFields) {
|
|
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
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Applies a stat aggregation to data based on a geom config.
|
|
56
|
+
* Returns data unchanged for identity stat.
|
|
57
|
+
*
|
|
58
|
+
* @param {Object[]} data
|
|
59
|
+
* @param {{ stat?: string, channels?: Record<string, string> }} geomConfig
|
|
60
|
+
* @param {Object} helpers
|
|
61
|
+
* @returns {Object[]}
|
|
62
|
+
*/
|
|
63
|
+
export function applyGeomStat(data, geomConfig, helpers = {}) {
|
|
64
|
+
const { stat = 'identity', channels = {} } = geomConfig
|
|
65
|
+
if (stat === 'identity') return data
|
|
66
|
+
if (stat === 'boxplot') return applyBoxStat(data, channels)
|
|
67
|
+
|
|
68
|
+
const statFn = resolveStat(stat, helpers)
|
|
69
|
+
|
|
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
|
+
|
|
75
|
+
let result = applyAggregate(data, {
|
|
76
|
+
by: groupByFields,
|
|
77
|
+
value: channels[primaryKey],
|
|
78
|
+
stat: statFn
|
|
79
|
+
})
|
|
80
|
+
|
|
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
|
+
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// packages/chart/src/lib/plot/types.js
|
|
2
|
+
// JSDoc typedefs for the Plot system. No runtime code.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} GeomSpec
|
|
6
|
+
* @property {string} type - Geom type: 'bar'|'line'|'area'|'point'|'box'|'violin'|'arc' or custom
|
|
7
|
+
* @property {string} [x]
|
|
8
|
+
* @property {string} [y]
|
|
9
|
+
* @property {string} [color]
|
|
10
|
+
* @property {string} [fill]
|
|
11
|
+
* @property {string} [size]
|
|
12
|
+
* @property {string} [symbol]
|
|
13
|
+
* @property {string} [pattern]
|
|
14
|
+
* @property {string} [stat] - Built-in or helpers.stats key
|
|
15
|
+
* @property {Record<string, unknown>} [options]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} PlotSpec
|
|
20
|
+
* @property {Record<string, unknown>[]} data
|
|
21
|
+
* @property {string} [x]
|
|
22
|
+
* @property {string} [y]
|
|
23
|
+
* @property {string} [color]
|
|
24
|
+
* @property {string} [fill]
|
|
25
|
+
* @property {string} [size]
|
|
26
|
+
* @property {string} [symbol]
|
|
27
|
+
* @property {string} [pattern]
|
|
28
|
+
* @property {string} [theta]
|
|
29
|
+
* @property {Record<string, string>} [labels]
|
|
30
|
+
* @property {unknown[]} [xDomain]
|
|
31
|
+
* @property {number[]} [yDomain]
|
|
32
|
+
* @property {string} [xLabel]
|
|
33
|
+
* @property {string} [yLabel]
|
|
34
|
+
* @property {[number, number]} [axisOrigin]
|
|
35
|
+
* @property {'categorical'|'sequential'|'diverging'} [colorScale]
|
|
36
|
+
* @property {string} [colorScheme]
|
|
37
|
+
* @property {number} [colorMidpoint]
|
|
38
|
+
* @property {unknown[]} [colorDomain]
|
|
39
|
+
* @property {GeomSpec[]} geoms
|
|
40
|
+
* @property {{ by: string, cols?: number, scales?: 'fixed'|'free'|'free_x'|'free_y' }} [facet]
|
|
41
|
+
* @property {{ by: string, duration?: number, loop?: boolean }} [animate]
|
|
42
|
+
* @property {boolean} [grid]
|
|
43
|
+
* @property {boolean} [legend]
|
|
44
|
+
* @property {boolean} [tooltip]
|
|
45
|
+
* @property {string} [title]
|
|
46
|
+
* @property {string} [preset]
|
|
47
|
+
* @property {number} [width]
|
|
48
|
+
* @property {number} [height]
|
|
49
|
+
* @property {'light'|'dark'} [mode]
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {Object} PlotHelpers
|
|
54
|
+
* @property {Record<string, (values: unknown[]) => unknown>} [stats]
|
|
55
|
+
* @property {Record<string, (v: unknown) => string>} [format]
|
|
56
|
+
* @property {(d: Record<string, unknown>) => string} [tooltip]
|
|
57
|
+
* @property {Record<string, unknown>} [geoms] Svelte components keyed by type name
|
|
58
|
+
* @property {unknown} [colorScale] d3 scale override
|
|
59
|
+
* @property {{ colors: string[], patterns: string[], symbols: string[] }} [preset]
|
|
60
|
+
* @property {Record<string, { colors: string[], patterns: string[], symbols: string[] }>} [presets]
|
|
61
|
+
* @property {Record<string, unknown>} [patterns] custom SVG pattern components keyed by name
|
|
62
|
+
* @property {Record<string, unknown>} [symbols] custom symbol shape components keyed by name
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
export {}
|