@rokkit/chart 1.7.0 → 1.8.1
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 +13 -0
- package/dist/Plot/DataTable.svelte +54 -0
- package/dist/Plot/DataTable.svelte.d.ts +12 -0
- package/dist/Plot/SpecGeoms.svelte +78 -0
- package/dist/Plot/SpecGeoms.svelte.d.ts +11 -0
- package/dist/Plot.svelte +47 -151
- package/dist/Plot.svelte.d.ts +5 -0
- package/dist/PlotState.svelte.d.ts +115 -71
- package/dist/PlotState.svelte.js +174 -547
- package/dist/SparkState.svelte.d.ts +3 -1
- package/dist/SparkState.svelte.js +11 -41
- package/dist/geoms/Arc.svelte +1 -1
- package/dist/geoms/Bar.svelte +23 -32
- package/dist/geoms/Contour.svelte +88 -0
- package/dist/geoms/Contour.svelte.d.ts +19 -0
- package/dist/geoms/Hull.svelte +89 -0
- package/dist/geoms/Hull.svelte.d.ts +17 -0
- package/dist/geoms/Region.svelte +107 -0
- package/dist/geoms/Region.svelte.d.ts +27 -0
- package/dist/geoms/Rule.svelte +88 -26
- package/dist/geoms/Rule.svelte.d.ts +8 -0
- package/dist/geoms/lib/PlotAreaClip.svelte +18 -0
- package/dist/geoms/lib/PlotAreaClip.svelte.d.ts +6 -0
- package/dist/geoms/lib/bars.js +13 -20
- package/dist/geoms/lib/grouping.d.ts +35 -0
- package/dist/geoms/lib/grouping.js +59 -0
- package/dist/geoms/lib/marks/arc.d.ts +1 -0
- package/dist/geoms/lib/marks/contour.d.ts +26 -0
- package/dist/geoms/lib/marks/contour.js +67 -0
- package/dist/geoms/lib/marks/hull.d.ts +26 -0
- package/dist/geoms/lib/marks/hull.js +36 -0
- package/dist/geoms/lib/marks/waterfall.js +15 -20
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/lib/abline.d.ts +41 -0
- package/dist/lib/abline.js +51 -0
- package/dist/lib/brewing/marks/arcs.d.ts +6 -1
- package/dist/lib/brewing/marks/arcs.js +9 -1
- package/dist/lib/brewing/polar.js +8 -3
- package/dist/lib/hull.d.ts +18 -0
- package/dist/lib/hull.js +64 -0
- package/dist/lib/plot/domains.d.ts +61 -0
- package/dist/lib/plot/domains.js +130 -0
- package/dist/lib/plot/running.d.ts +23 -0
- package/dist/lib/plot/running.js +32 -0
- package/dist/lib/plot/spec.d.ts +59 -0
- package/dist/lib/plot/spec.js +131 -0
- package/dist/lib/plot/stacking.d.ts +32 -0
- package/dist/lib/plot/stacking.js +34 -0
- package/dist/lib/plot/types.d.ts +8 -1
- package/dist/lib/plot/types.js +5 -1
- package/dist/lib/preset.d.ts +2 -0
- package/dist/lib/preset.js +76 -41
- package/dist/lib/region.d.ts +63 -0
- package/dist/lib/region.js +127 -0
- package/dist/state/AestheticState.svelte.d.ts +43 -0
- package/dist/state/AestheticState.svelte.js +84 -0
- package/dist/state/AxisState.svelte.d.ts +26 -0
- package/dist/state/AxisState.svelte.js +56 -0
- package/dist/state/ChannelState.svelte.d.ts +34 -0
- package/dist/state/ChannelState.svelte.js +67 -0
- package/dist/state/GeomRegistry.svelte.d.ts +44 -0
- package/dist/state/GeomRegistry.svelte.js +86 -0
- package/dist/state/InteractionState.svelte.d.ts +30 -0
- package/dist/state/InteractionState.svelte.js +69 -0
- package/dist/state/OrientationState.svelte.d.ts +34 -0
- package/dist/state/OrientationState.svelte.js +64 -0
- package/dist/state/PlotConfig.svelte.d.ts +179 -0
- package/dist/state/PlotConfig.svelte.js +195 -0
- package/dist/state/PlotFrame.svelte.d.ts +17 -0
- package/dist/state/PlotFrame.svelte.js +18 -0
- package/dist/state/ScaleState.svelte.d.ts +36 -0
- package/dist/state/ScaleState.svelte.js +121 -0
- package/package.json +5 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How `PlotChart` reads its inputs: a `spec` object and component props, which overlap.
|
|
3
|
+
*
|
|
4
|
+
* The rule is "a spec value overrides its prop", with one declared exception — `orientation`,
|
|
5
|
+
* where the prop wins so a wrapper can force direction over a shared spec. Written as a table
|
|
6
|
+
* so the exception is visible rather than one `??` in twenty turned the other way. Pure: the
|
|
7
|
+
* component derives from these, and they are tested without rendering.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Where a config field comes from. A null/undefined value falls through to the other source. */
|
|
11
|
+
const SOURCES = {
|
|
12
|
+
spec: (key, spec) => spec?.[key],
|
|
13
|
+
prop: (key, _spec, props) => props[key],
|
|
14
|
+
specFirst: (key, spec, props) => spec?.[key] ?? props[key],
|
|
15
|
+
propFirst: (key, spec, props) => props[key] ?? spec?.[key]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The PlotState config fields read straight from spec and/or props, in config order.
|
|
20
|
+
* @type {Array<[string, keyof typeof SOURCES]>}
|
|
21
|
+
*/
|
|
22
|
+
export const PLOT_CONFIG_FIELDS = [
|
|
23
|
+
['data', 'specFirst'],
|
|
24
|
+
['width', 'specFirst'],
|
|
25
|
+
['height', 'specFirst'],
|
|
26
|
+
['mode', 'prop'],
|
|
27
|
+
['margin', 'prop'],
|
|
28
|
+
['helpers', 'prop'],
|
|
29
|
+
['xDomain', 'specFirst'],
|
|
30
|
+
['yDomain', 'specFirst'],
|
|
31
|
+
['colorDomain', 'spec'],
|
|
32
|
+
['colorScale', 'spec'],
|
|
33
|
+
['colorScheme', 'spec'],
|
|
34
|
+
['colorMidpoint', 'spec'],
|
|
35
|
+
['orientation', 'propFirst'],
|
|
36
|
+
['axisOrigin', 'specFirst'],
|
|
37
|
+
['axisOffset', 'specFirst'],
|
|
38
|
+
['sort', 'spec'],
|
|
39
|
+
['continuousCategory', 'spec'],
|
|
40
|
+
['onselect', 'prop'],
|
|
41
|
+
['selectable', 'prop']
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The config `PlotSurface` builds its `PlotState` from.
|
|
46
|
+
*
|
|
47
|
+
* @param {Record<string, any> | undefined} spec
|
|
48
|
+
* @param {Record<string, any>} props - the component's props
|
|
49
|
+
* @param {unknown} chartPreset - the resolved preset (context, else the default)
|
|
50
|
+
*/
|
|
51
|
+
export function resolvePlotConfig(spec, props, chartPreset) {
|
|
52
|
+
const config = Object.fromEntries(
|
|
53
|
+
PLOT_CONFIG_FIELDS.map(([key, source]) => [key, SOURCES[source](key, spec, props)])
|
|
54
|
+
)
|
|
55
|
+
// A spec names its channels; the prop-driven form has none here (children declare them).
|
|
56
|
+
config.channels = spec ? { x: spec.x, y: spec.y, color: spec.color ?? spec.fill } : {}
|
|
57
|
+
config.labels = spec?.labels ?? {}
|
|
58
|
+
config.chartPreset = chartPreset
|
|
59
|
+
return config
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** An axis label: the spec's label for the field on that axis, else blank. */
|
|
63
|
+
const axisLabel = (spec, field) => spec?.labels?.[field ?? ''] ?? ''
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The chrome around the marks: grid, legend, title, summary, axis labels, and the fields the
|
|
67
|
+
* trend / highlight overlays draw on.
|
|
68
|
+
*
|
|
69
|
+
* `grid`: `false` hides it; `true` is 'auto'; `'x' | 'y' | 'both'` names the lines.
|
|
70
|
+
*
|
|
71
|
+
* @param {Record<string, any> | undefined} spec
|
|
72
|
+
* @param {Record<string, any>} props
|
|
73
|
+
*/
|
|
74
|
+
export function resolveChrome(spec, props) {
|
|
75
|
+
const grid = spec?.grid ?? props.grid
|
|
76
|
+
return {
|
|
77
|
+
showGrid: grid !== false,
|
|
78
|
+
gridLines: typeof grid === 'boolean' ? 'auto' : grid,
|
|
79
|
+
showLegend: spec?.legend ?? props.legend,
|
|
80
|
+
title: spec?.title ?? props.title,
|
|
81
|
+
summary: spec?.summary ?? props.summary,
|
|
82
|
+
overlayX: spec?.x ?? props.x,
|
|
83
|
+
overlayY: spec?.y ?? props.y,
|
|
84
|
+
xLabel: axisLabel(spec, spec?.x),
|
|
85
|
+
yLabel: axisLabel(spec, spec?.y)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The accessible table's columns: the spec's channels (deduped — a table's keyed each needs
|
|
91
|
+
* unique names, and `x === color` is normal), else the first row's keys.
|
|
92
|
+
*
|
|
93
|
+
* @param {Record<string, any> | undefined} spec
|
|
94
|
+
* @param {Record<string, unknown>[]} rows
|
|
95
|
+
* @returns {string[]}
|
|
96
|
+
*/
|
|
97
|
+
export function tableColumns(spec, rows) {
|
|
98
|
+
const channels = [spec?.x, spec?.y, spec?.color ?? spec?.fill].filter(Boolean)
|
|
99
|
+
if (channels.length > 0) return [...new Set(channels)]
|
|
100
|
+
return rows[0] ? Object.keys(rows[0]) : []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Spec-level options every geom inherits — only when the spec sets them. */
|
|
104
|
+
function inheritedOptions(spec) {
|
|
105
|
+
const options = {}
|
|
106
|
+
if (spec?.stack !== undefined) options.stack = spec.stack
|
|
107
|
+
if (spec?.orientation !== undefined) options.orientation = spec.orientation
|
|
108
|
+
return options
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The props for one `spec.geoms` entry: the spec's channels unless the geom names its own,
|
|
113
|
+
* the spec's stack / orientation under the geom's options, and the geom's `props` over all.
|
|
114
|
+
*
|
|
115
|
+
* @param {Record<string, any>} geomSpec
|
|
116
|
+
* @param {Record<string, any> | undefined} spec
|
|
117
|
+
*/
|
|
118
|
+
export function specGeomProps(geomSpec, spec) {
|
|
119
|
+
return {
|
|
120
|
+
x: geomSpec.x ?? spec?.x,
|
|
121
|
+
y: geomSpec.y ?? spec?.y,
|
|
122
|
+
color: geomSpec.color ?? spec?.color,
|
|
123
|
+
fill: geomSpec.fill ?? spec?.fill,
|
|
124
|
+
pattern: geomSpec.pattern,
|
|
125
|
+
symbol: geomSpec.symbol,
|
|
126
|
+
stat: geomSpec.stat,
|
|
127
|
+
label: geomSpec.label,
|
|
128
|
+
options: { ...inheritedOptions(spec), ...(geomSpec.options ?? {}) },
|
|
129
|
+
...(geomSpec.props ?? {})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The distinct non-x fields among [group, color, pattern] — the fields that put more than one
|
|
3
|
+
* bar in a single x band (dodge) or more than one segment in a column (stack). `group`, the
|
|
4
|
+
* explicit grouping channel, comes first. Literal colours are paint, not fields.
|
|
5
|
+
*
|
|
6
|
+
* @param {{ x?: string, group?: string, color?: string, pattern?: string }} channels
|
|
7
|
+
* @returns {string[]}
|
|
8
|
+
*/
|
|
9
|
+
export function subBandFields(channels: {
|
|
10
|
+
x?: string;
|
|
11
|
+
group?: string;
|
|
12
|
+
color?: string;
|
|
13
|
+
pattern?: string;
|
|
14
|
+
}): string[];
|
|
15
|
+
/**
|
|
16
|
+
* The field a stacked bar stacks by, from a bar's own channels — or null when there is none,
|
|
17
|
+
* in which case the builder draws plain bars. One rule for the builder that draws the stack
|
|
18
|
+
* and the domain that sizes the axis to it: two copies disagreed, and the axis ended below
|
|
19
|
+
* the tallest bar.
|
|
20
|
+
*
|
|
21
|
+
* `group` defaults to the interior (`fill ?? color`), exactly as the bar marks resolve it.
|
|
22
|
+
*
|
|
23
|
+
* @param {{ x?: string, group?: string, fill?: string, color?: string, pattern?: string }} channels
|
|
24
|
+
* @returns {string | null}
|
|
25
|
+
*/
|
|
26
|
+
export function stackFieldOf({ x, group, fill, color, pattern }: {
|
|
27
|
+
x?: string;
|
|
28
|
+
group?: string;
|
|
29
|
+
fill?: string;
|
|
30
|
+
color?: string;
|
|
31
|
+
pattern?: string;
|
|
32
|
+
}): string | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isLiteralColor } from '../brewing/colors.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The distinct non-x fields among [group, color, pattern] — the fields that put more than one
|
|
5
|
+
* bar in a single x band (dodge) or more than one segment in a column (stack). `group`, the
|
|
6
|
+
* explicit grouping channel, comes first. Literal colours are paint, not fields.
|
|
7
|
+
*
|
|
8
|
+
* @param {{ x?: string, group?: string, color?: string, pattern?: string }} channels
|
|
9
|
+
* @returns {string[]}
|
|
10
|
+
*/
|
|
11
|
+
export function subBandFields(channels) {
|
|
12
|
+
const { x: xf, group: gf, color: cf, pattern: pf } = channels
|
|
13
|
+
const out = []
|
|
14
|
+
for (const f of [gf, cf, pf]) {
|
|
15
|
+
if (f && f !== xf && !out.includes(f) && !isLiteralColor(f)) out.push(f)
|
|
16
|
+
}
|
|
17
|
+
return out
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The field a stacked bar stacks by, from a bar's own channels — or null when there is none,
|
|
22
|
+
* in which case the builder draws plain bars. One rule for the builder that draws the stack
|
|
23
|
+
* and the domain that sizes the axis to it: two copies disagreed, and the axis ended below
|
|
24
|
+
* the tallest bar.
|
|
25
|
+
*
|
|
26
|
+
* `group` defaults to the interior (`fill ?? color`), exactly as the bar marks resolve it.
|
|
27
|
+
*
|
|
28
|
+
* @param {{ x?: string, group?: string, fill?: string, color?: string, pattern?: string }} channels
|
|
29
|
+
* @returns {string | null}
|
|
30
|
+
*/
|
|
31
|
+
export function stackFieldOf({ x, group, fill, color, pattern }) {
|
|
32
|
+
const interior = fill ?? color
|
|
33
|
+
return subBandFields({ x, group: group ?? interior, color: interior, pattern })[0] ?? null
|
|
34
|
+
}
|
package/dist/lib/plot/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type GeomSpec = {
|
|
2
2
|
/**
|
|
3
|
-
* - Geom type: 'bar'|'line'|'area'|'point'|'box'|'violin'|'arc' or custom
|
|
3
|
+
* - Geom type: 'bar'|'line'|'area'|'point'|'box'|'violin'|'arc'|'hull'|'contour'|'rule'|'region' or custom
|
|
4
4
|
*/
|
|
5
5
|
type: string;
|
|
6
6
|
x?: string | undefined;
|
|
@@ -16,6 +16,13 @@ export type GeomSpec = {
|
|
|
16
16
|
stat?: string | undefined;
|
|
17
17
|
label?: string | boolean | undefined;
|
|
18
18
|
options?: Record<string, unknown> | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* - Geom-specific props spread onto the component
|
|
21
|
+
* (`Region`'s `points`/`label`, `Rule`'s `slope`, `Hull`'s `padding`). For the annotation
|
|
22
|
+
* geoms (`rule`, `region`) these are the ONLY props: they read no data, and a Region's `x`/`y`
|
|
23
|
+
* are ranges, so inheriting the spec's field names would be wrong.
|
|
24
|
+
*/
|
|
25
|
+
props?: Record<string, unknown> | undefined;
|
|
19
26
|
};
|
|
20
27
|
export type PlotSpec = {
|
|
21
28
|
data?: Record<string, unknown>[] | undefined;
|
package/dist/lib/plot/types.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {Object} GeomSpec
|
|
6
|
-
* @property {string} type - Geom type: 'bar'|'line'|'area'|'point'|'box'|'violin'|'arc' or custom
|
|
6
|
+
* @property {string} type - Geom type: 'bar'|'line'|'area'|'point'|'box'|'violin'|'arc'|'hull'|'contour'|'rule'|'region' or custom
|
|
7
7
|
* @property {string} [x]
|
|
8
8
|
* @property {string} [y]
|
|
9
9
|
* @property {string} [color]
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* @property {string} [stat] - Built-in or helpers.stats key
|
|
15
15
|
* @property {boolean | string} [label]
|
|
16
16
|
* @property {Record<string, unknown>} [options]
|
|
17
|
+
* @property {Record<string, unknown>} [props] - Geom-specific props spread onto the component
|
|
18
|
+
* (`Region`'s `points`/`label`, `Rule`'s `slope`, `Hull`'s `padding`). For the annotation
|
|
19
|
+
* geoms (`rule`, `region`) these are the ONLY props: they read no data, and a Region's `x`/`y`
|
|
20
|
+
* are ranges, so inheriting the spec's field names would be wrong.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
/**
|
package/dist/lib/preset.d.ts
CHANGED
package/dist/lib/preset.js
CHANGED
|
@@ -1,36 +1,71 @@
|
|
|
1
1
|
// packages/chart/src/lib/preset.js
|
|
2
2
|
|
|
3
3
|
export const defaultPreset = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
4
|
+
colors: [
|
|
5
|
+
'blue',
|
|
6
|
+
'emerald',
|
|
7
|
+
'rose',
|
|
8
|
+
'amber',
|
|
9
|
+
'violet',
|
|
10
|
+
'sky',
|
|
11
|
+
'pink',
|
|
12
|
+
'teal',
|
|
13
|
+
'orange',
|
|
14
|
+
'indigo',
|
|
15
|
+
'lime',
|
|
16
|
+
'cyan',
|
|
17
|
+
'gold',
|
|
18
|
+
'lavender'
|
|
19
|
+
],
|
|
20
|
+
shades: {
|
|
21
|
+
light: { fill: '300', stroke: '700' },
|
|
22
|
+
dark: { fill: '500', stroke: '200' }
|
|
23
|
+
},
|
|
24
|
+
// Per-geom default mark opacity. `alpha` (a fixed 0–1 prop on each geom / Plot.Root)
|
|
25
|
+
// overrides these; solid marks default to 1 so `alpha` is the single opacity knob.
|
|
26
|
+
opacity: {
|
|
27
|
+
area: 0.6,
|
|
28
|
+
bar: 1,
|
|
29
|
+
line: 1,
|
|
30
|
+
point: 0.8,
|
|
31
|
+
arc: 1,
|
|
32
|
+
box: 0.5,
|
|
33
|
+
violin: 0.5,
|
|
34
|
+
jitter: 0.8,
|
|
35
|
+
heatmap: 1,
|
|
36
|
+
candlestick: 1,
|
|
37
|
+
hexbin: 1,
|
|
38
|
+
ribbon: 0.5,
|
|
39
|
+
waterfall: 1,
|
|
40
|
+
rule: 1,
|
|
41
|
+
hull: 0.15,
|
|
42
|
+
contour: 0.5,
|
|
43
|
+
radar: 0.25
|
|
44
|
+
},
|
|
45
|
+
patterns: [
|
|
46
|
+
'diagonal',
|
|
47
|
+
'dots',
|
|
48
|
+
'triangles',
|
|
49
|
+
'hatch',
|
|
50
|
+
'lattice',
|
|
51
|
+
'swell',
|
|
52
|
+
'checkerboard',
|
|
53
|
+
'waves',
|
|
54
|
+
'petals',
|
|
55
|
+
'brick',
|
|
56
|
+
'diamonds',
|
|
57
|
+
'tile',
|
|
58
|
+
'scales',
|
|
59
|
+
'circles',
|
|
60
|
+
'pip',
|
|
61
|
+
'rings',
|
|
62
|
+
'chevrons',
|
|
63
|
+
'shards',
|
|
64
|
+
'wedge',
|
|
65
|
+
'argyle',
|
|
66
|
+
'shell'
|
|
67
|
+
],
|
|
68
|
+
symbols: ['circle', 'square', 'triangle', 'diamond', 'cross', 'star']
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
/**
|
|
@@ -40,15 +75,15 @@ export const defaultPreset = {
|
|
|
40
75
|
* @returns {typeof defaultPreset}
|
|
41
76
|
*/
|
|
42
77
|
export function createChartPreset(overrides = {}) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
return {
|
|
79
|
+
...defaultPreset,
|
|
80
|
+
...overrides,
|
|
81
|
+
shades: overrides.shades
|
|
82
|
+
? {
|
|
83
|
+
light: { ...defaultPreset.shades.light, ...overrides.shades.light },
|
|
84
|
+
dark: { ...defaultPreset.shades.dark, ...overrides.shades.dark }
|
|
85
|
+
}
|
|
86
|
+
: defaultPreset.shades,
|
|
87
|
+
opacity: { ...defaultPreset.opacity, ...overrides.opacity }
|
|
88
|
+
}
|
|
54
89
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen span of a [lo, hi] range on one axis. A band category contributes its whole band,
|
|
3
|
+
* so `x={['a', 'b']}` shades both columns edge to edge. Omitted → the full range; a `null`
|
|
4
|
+
* end runs to that edge of the axis, so `[p75, null]` is "the top quartile and beyond".
|
|
5
|
+
*
|
|
6
|
+
* @param {AnyScale} scale
|
|
7
|
+
* @param {unknown[] | undefined} range
|
|
8
|
+
* @returns {[number, number]}
|
|
9
|
+
*/
|
|
10
|
+
export function spanOf(scale: AnyScale, range: unknown[] | undefined): [number, number];
|
|
11
|
+
/**
|
|
12
|
+
* Position of a single data value — band categories resolve to the band centre, which is
|
|
13
|
+
* where a point in that category is drawn.
|
|
14
|
+
*
|
|
15
|
+
* @param {AnyScale} scale
|
|
16
|
+
* @param {unknown} v
|
|
17
|
+
*/
|
|
18
|
+
export function positionOf(scale: AnyScale, v: unknown): number;
|
|
19
|
+
/**
|
|
20
|
+
* The region's vertices in (u, v) plot coordinates — before orientation `place`.
|
|
21
|
+
*
|
|
22
|
+
* `points` (a polygon, three or more vertices) wins over `x`/`y` ranges. Fewer than three
|
|
23
|
+
* points is not an area, so it resolves to nothing rather than a degenerate sliver.
|
|
24
|
+
*
|
|
25
|
+
* @param {{ x?: unknown[], y?: unknown[], points?: unknown[][] }} shape
|
|
26
|
+
* @param {AnyScale} xScale
|
|
27
|
+
* @param {AnyScale} yScale
|
|
28
|
+
* @returns {Array<[number, number]>}
|
|
29
|
+
*/
|
|
30
|
+
export function regionVertices(shape: {
|
|
31
|
+
x?: unknown[];
|
|
32
|
+
y?: unknown[];
|
|
33
|
+
points?: unknown[][];
|
|
34
|
+
}, xScale: AnyScale, yScale: AnyScale): Array<[number, number]>;
|
|
35
|
+
/**
|
|
36
|
+
* Area centroid of a simple polygon — where its label goes. Falls back to the vertex mean for
|
|
37
|
+
* a zero-area (collinear) polygon, which has no area centroid.
|
|
38
|
+
*
|
|
39
|
+
* @param {Array<{ x: number, y: number }>} pts
|
|
40
|
+
* @returns {{ x: number, y: number }}
|
|
41
|
+
*/
|
|
42
|
+
export function centroid(pts: Array<{
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
}>): {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* SVG path data for a closed polygon.
|
|
51
|
+
*
|
|
52
|
+
* @param {Array<{ x: number, y: number }>} pts
|
|
53
|
+
*/
|
|
54
|
+
export function polygonPath(pts: Array<{
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
}>): string;
|
|
58
|
+
export type AnyScale = {
|
|
59
|
+
(v: unknown): number | undefined;
|
|
60
|
+
bandwidth?: () => number;
|
|
61
|
+
range: () => number[];
|
|
62
|
+
domain: () => unknown[];
|
|
63
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Region geometry — data-coordinate shapes resolved to plot (u, v) coordinates.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the component so the maths is testable without a DOM, and so every geom that
|
|
5
|
+
* needs "a shape in data space" (Region today, anything shaded tomorrow) resolves it the same
|
|
6
|
+
* way: continuous values map straight through the scale, band categories cover the WHOLE
|
|
7
|
+
* band, and an omitted range spans the axis.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @typedef {{ (v: unknown): number | undefined, bandwidth?: () => number, range: () => number[], domain: () => unknown[] }} AnyScale */
|
|
11
|
+
|
|
12
|
+
/** @param {AnyScale} scale */
|
|
13
|
+
const isBand = (scale) => typeof scale.bandwidth === 'function'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The data value an open (null/undefined) range end stands for: the low or high end of the
|
|
17
|
+
* axis's domain — the first or last category on a band axis.
|
|
18
|
+
*
|
|
19
|
+
* @param {AnyScale} scale
|
|
20
|
+
* @param {unknown} v
|
|
21
|
+
* @param {'lo' | 'hi'} side
|
|
22
|
+
*/
|
|
23
|
+
function endValue(scale, v, side) {
|
|
24
|
+
if (v !== null && v !== undefined) return v
|
|
25
|
+
const domain = scale.domain()
|
|
26
|
+
if (isBand(scale)) return side === 'lo' ? domain[0] : domain[domain.length - 1]
|
|
27
|
+
const nums = domain.map(Number)
|
|
28
|
+
return side === 'lo' ? Math.min(...nums) : Math.max(...nums)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Screen span of a [lo, hi] range on one axis. A band category contributes its whole band,
|
|
33
|
+
* so `x={['a', 'b']}` shades both columns edge to edge. Omitted → the full range; a `null`
|
|
34
|
+
* end runs to that edge of the axis, so `[p75, null]` is "the top quartile and beyond".
|
|
35
|
+
*
|
|
36
|
+
* @param {AnyScale} scale
|
|
37
|
+
* @param {unknown[] | undefined} range
|
|
38
|
+
* @returns {[number, number]}
|
|
39
|
+
*/
|
|
40
|
+
export function spanOf(scale, range) {
|
|
41
|
+
if (!range || range.length < 2) {
|
|
42
|
+
const r = scale.range()
|
|
43
|
+
return [Math.min(...r), Math.max(...r)]
|
|
44
|
+
}
|
|
45
|
+
const band = isBand(scale) ? /** @type {() => number} */ (scale.bandwidth)() : 0
|
|
46
|
+
const ends = [endValue(scale, range[0], 'lo'), endValue(scale, range[1], 'hi')].map((v) =>
|
|
47
|
+
Number(scale(v))
|
|
48
|
+
)
|
|
49
|
+
return [Math.min(...ends), Math.max(...ends) + band]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Position of a single data value — band categories resolve to the band centre, which is
|
|
54
|
+
* where a point in that category is drawn.
|
|
55
|
+
*
|
|
56
|
+
* @param {AnyScale} scale
|
|
57
|
+
* @param {unknown} v
|
|
58
|
+
*/
|
|
59
|
+
export function positionOf(scale, v) {
|
|
60
|
+
const base = Number(scale(v))
|
|
61
|
+
return isBand(scale) ? base + /** @type {() => number} */ (scale.bandwidth)() / 2 : base
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The region's vertices in (u, v) plot coordinates — before orientation `place`.
|
|
66
|
+
*
|
|
67
|
+
* `points` (a polygon, three or more vertices) wins over `x`/`y` ranges. Fewer than three
|
|
68
|
+
* points is not an area, so it resolves to nothing rather than a degenerate sliver.
|
|
69
|
+
*
|
|
70
|
+
* @param {{ x?: unknown[], y?: unknown[], points?: unknown[][] }} shape
|
|
71
|
+
* @param {AnyScale} xScale
|
|
72
|
+
* @param {AnyScale} yScale
|
|
73
|
+
* @returns {Array<[number, number]>}
|
|
74
|
+
*/
|
|
75
|
+
export function regionVertices(shape, xScale, yScale) {
|
|
76
|
+
if (shape.points) {
|
|
77
|
+
if (shape.points.length < 3) return []
|
|
78
|
+
return shape.points.map(([x, y]) => [positionOf(xScale, x), positionOf(yScale, y)])
|
|
79
|
+
}
|
|
80
|
+
if (!shape.x && !shape.y) return []
|
|
81
|
+
const [x0, x1] = spanOf(xScale, shape.x)
|
|
82
|
+
const [y0, y1] = spanOf(yScale, shape.y)
|
|
83
|
+
// Wound so the first vertex is the (lo-x, lo-y) corner in DATA terms: on a vertical chart
|
|
84
|
+
// the y range is inverted, so y1 (the larger pixel) is the lower value.
|
|
85
|
+
return [
|
|
86
|
+
[x0, y1],
|
|
87
|
+
[x1, y1],
|
|
88
|
+
[x1, y0],
|
|
89
|
+
[x0, y0]
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Area centroid of a simple polygon — where its label goes. Falls back to the vertex mean for
|
|
95
|
+
* a zero-area (collinear) polygon, which has no area centroid.
|
|
96
|
+
*
|
|
97
|
+
* @param {Array<{ x: number, y: number }>} pts
|
|
98
|
+
* @returns {{ x: number, y: number }}
|
|
99
|
+
*/
|
|
100
|
+
export function centroid(pts) {
|
|
101
|
+
let a = 0
|
|
102
|
+
let cx = 0
|
|
103
|
+
let cy = 0
|
|
104
|
+
for (let i = 0; i < pts.length; i++) {
|
|
105
|
+
const p = pts[i]
|
|
106
|
+
const q = pts[(i + 1) % pts.length]
|
|
107
|
+
const cross = p.x * q.y - q.x * p.y
|
|
108
|
+
a += cross
|
|
109
|
+
cx += (p.x + q.x) * cross
|
|
110
|
+
cy += (p.y + q.y) * cross
|
|
111
|
+
}
|
|
112
|
+
if (Math.abs(a) < 1e-9) {
|
|
113
|
+
const n = pts.length || 1
|
|
114
|
+
return { x: pts.reduce((s, p) => s + p.x, 0) / n, y: pts.reduce((s, p) => s + p.y, 0) / n }
|
|
115
|
+
}
|
|
116
|
+
return { x: cx / (3 * a), y: cy / (3 * a) }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* SVG path data for a closed polygon.
|
|
121
|
+
*
|
|
122
|
+
* @param {Array<{ x: number, y: number }>} pts
|
|
123
|
+
*/
|
|
124
|
+
export function polygonPath(pts) {
|
|
125
|
+
const fmt = (/** @type {number} */ n) => Math.round(n * 1000) / 1000
|
|
126
|
+
return `M${pts.map((p) => `${fmt(p.x)},${fmt(p.y)}`).join(' L')} Z`
|
|
127
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each category looks like: its palette entry, pattern and symbol — and, for a numeric
|
|
3
|
+
* colour field, the continuous (sequential or diverging) colour scale.
|
|
4
|
+
*/
|
|
5
|
+
export class AestheticState {
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('./PlotConfig.svelte.js').PlotConfig} config
|
|
8
|
+
* @param {import('./ChannelState.svelte.js').ChannelState} channels
|
|
9
|
+
*/
|
|
10
|
+
constructor(config: import("./PlotConfig.svelte.js").PlotConfig, channels: import("./ChannelState.svelte.js").ChannelState);
|
|
11
|
+
/** 'categorical' | 'sequential' | 'diverging' — inferred from the colour field and the spec. */
|
|
12
|
+
colorScaleType: any;
|
|
13
|
+
/** The sequential or diverging colour scale, or null for a categorical colour field. */
|
|
14
|
+
continuousColorScale: {
|
|
15
|
+
scale: (v: number) => string;
|
|
16
|
+
domain: [number, number];
|
|
17
|
+
type: "sequential";
|
|
18
|
+
} | {
|
|
19
|
+
scale: (v: number) => string;
|
|
20
|
+
domain: [number, number, number];
|
|
21
|
+
type: "diverging";
|
|
22
|
+
} | null;
|
|
23
|
+
/**
|
|
24
|
+
* Map<category, { fill, stroke }> — one shared palette across the colour and fill channels.
|
|
25
|
+
*
|
|
26
|
+
* A literal CSS colour is a singleton keyed by null, which every mark falls back to. A
|
|
27
|
+
* `colorDomain` (FacetPlot passes one so panels agree) wins over the local categories. With
|
|
28
|
+
* no colour field at all, a single series takes the first palette entry rather than grey.
|
|
29
|
+
*/
|
|
30
|
+
colors: Map<unknown, {
|
|
31
|
+
fill: string;
|
|
32
|
+
stroke: string;
|
|
33
|
+
}> | Map<null, {
|
|
34
|
+
fill: any;
|
|
35
|
+
stroke: any;
|
|
36
|
+
}>;
|
|
37
|
+
/** Map<category, pattern name> — only for a categorical pattern field. */
|
|
38
|
+
patterns: Map<unknown, string> | SvelteMap<any, any>;
|
|
39
|
+
/** Map<category, symbol shape>. */
|
|
40
|
+
symbols: Map<unknown, string> | SvelteMap<any, any>;
|
|
41
|
+
#private;
|
|
42
|
+
}
|
|
43
|
+
import { SvelteMap } from 'svelte/reactivity';
|