@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.
Files changed (74) hide show
  1. package/README.md +13 -0
  2. package/dist/Plot/DataTable.svelte +54 -0
  3. package/dist/Plot/DataTable.svelte.d.ts +12 -0
  4. package/dist/Plot/SpecGeoms.svelte +78 -0
  5. package/dist/Plot/SpecGeoms.svelte.d.ts +11 -0
  6. package/dist/Plot.svelte +47 -151
  7. package/dist/Plot.svelte.d.ts +5 -0
  8. package/dist/PlotState.svelte.d.ts +115 -71
  9. package/dist/PlotState.svelte.js +174 -547
  10. package/dist/SparkState.svelte.d.ts +3 -1
  11. package/dist/SparkState.svelte.js +11 -41
  12. package/dist/geoms/Arc.svelte +1 -1
  13. package/dist/geoms/Bar.svelte +23 -32
  14. package/dist/geoms/Contour.svelte +88 -0
  15. package/dist/geoms/Contour.svelte.d.ts +19 -0
  16. package/dist/geoms/Hull.svelte +89 -0
  17. package/dist/geoms/Hull.svelte.d.ts +17 -0
  18. package/dist/geoms/Region.svelte +107 -0
  19. package/dist/geoms/Region.svelte.d.ts +27 -0
  20. package/dist/geoms/Rule.svelte +88 -26
  21. package/dist/geoms/Rule.svelte.d.ts +8 -0
  22. package/dist/geoms/lib/PlotAreaClip.svelte +18 -0
  23. package/dist/geoms/lib/PlotAreaClip.svelte.d.ts +6 -0
  24. package/dist/geoms/lib/bars.js +13 -20
  25. package/dist/geoms/lib/grouping.d.ts +35 -0
  26. package/dist/geoms/lib/grouping.js +59 -0
  27. package/dist/geoms/lib/marks/arc.d.ts +1 -0
  28. package/dist/geoms/lib/marks/contour.d.ts +26 -0
  29. package/dist/geoms/lib/marks/contour.js +67 -0
  30. package/dist/geoms/lib/marks/hull.d.ts +26 -0
  31. package/dist/geoms/lib/marks/hull.js +36 -0
  32. package/dist/geoms/lib/marks/waterfall.js +15 -20
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.js +9 -0
  35. package/dist/lib/abline.d.ts +41 -0
  36. package/dist/lib/abline.js +51 -0
  37. package/dist/lib/brewing/marks/arcs.d.ts +6 -1
  38. package/dist/lib/brewing/marks/arcs.js +9 -1
  39. package/dist/lib/brewing/polar.js +8 -3
  40. package/dist/lib/hull.d.ts +18 -0
  41. package/dist/lib/hull.js +64 -0
  42. package/dist/lib/plot/domains.d.ts +61 -0
  43. package/dist/lib/plot/domains.js +130 -0
  44. package/dist/lib/plot/running.d.ts +23 -0
  45. package/dist/lib/plot/running.js +32 -0
  46. package/dist/lib/plot/spec.d.ts +59 -0
  47. package/dist/lib/plot/spec.js +131 -0
  48. package/dist/lib/plot/stacking.d.ts +32 -0
  49. package/dist/lib/plot/stacking.js +34 -0
  50. package/dist/lib/plot/types.d.ts +8 -1
  51. package/dist/lib/plot/types.js +5 -1
  52. package/dist/lib/preset.d.ts +2 -0
  53. package/dist/lib/preset.js +76 -41
  54. package/dist/lib/region.d.ts +63 -0
  55. package/dist/lib/region.js +127 -0
  56. package/dist/state/AestheticState.svelte.d.ts +43 -0
  57. package/dist/state/AestheticState.svelte.js +84 -0
  58. package/dist/state/AxisState.svelte.d.ts +26 -0
  59. package/dist/state/AxisState.svelte.js +56 -0
  60. package/dist/state/ChannelState.svelte.d.ts +34 -0
  61. package/dist/state/ChannelState.svelte.js +67 -0
  62. package/dist/state/GeomRegistry.svelte.d.ts +44 -0
  63. package/dist/state/GeomRegistry.svelte.js +86 -0
  64. package/dist/state/InteractionState.svelte.d.ts +30 -0
  65. package/dist/state/InteractionState.svelte.js +69 -0
  66. package/dist/state/OrientationState.svelte.d.ts +34 -0
  67. package/dist/state/OrientationState.svelte.js +64 -0
  68. package/dist/state/PlotConfig.svelte.d.ts +179 -0
  69. package/dist/state/PlotConfig.svelte.js +195 -0
  70. package/dist/state/PlotFrame.svelte.d.ts +17 -0
  71. package/dist/state/PlotFrame.svelte.js +18 -0
  72. package/dist/state/ScaleState.svelte.d.ts +36 -0
  73. package/dist/state/ScaleState.svelte.js +121 -0
  74. 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
+ }
@@ -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;
@@ -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
  /**
@@ -34,6 +34,8 @@ export namespace defaultPreset {
34
34
  let ribbon: number;
35
35
  let waterfall: number;
36
36
  let rule: number;
37
+ let hull: number;
38
+ let contour: number;
37
39
  let radar: number;
38
40
  }
39
41
  let patterns: string[];
@@ -1,36 +1,71 @@
1
1
  // packages/chart/src/lib/preset.js
2
2
 
3
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
- // Per-geom default mark opacity. `alpha` (a fixed 0–1 prop on each geom / Plot.Root)
11
- // overrides these; solid marks default to 1 so `alpha` is the single opacity knob.
12
- opacity: {
13
- area: 0.6,
14
- bar: 1,
15
- line: 1,
16
- point: 0.8,
17
- arc: 1,
18
- box: 0.5,
19
- violin: 0.5,
20
- jitter: 0.8,
21
- heatmap: 1,
22
- candlestick: 1,
23
- hexbin: 1,
24
- ribbon: 0.5,
25
- waterfall: 1,
26
- rule: 1,
27
- radar: 0.25
28
- },
29
- patterns: ['diagonal', 'dots', 'triangles', 'hatch', 'lattice', 'swell',
30
- 'checkerboard', 'waves', 'petals', 'brick', 'diamonds', 'tile',
31
- 'scales', 'circles', 'pip', 'rings', 'chevrons', 'shards',
32
- 'wedge', 'argyle', 'shell'],
33
- symbols: ['circle', 'square', 'triangle', 'diamond', 'cross', 'star']
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
- return {
44
- ...defaultPreset,
45
- ...overrides,
46
- shades: overrides.shades
47
- ? {
48
- light: { ...defaultPreset.shades.light, ...overrides.shades.light },
49
- dark: { ...defaultPreset.shades.dark, ...overrides.shades.dark }
50
- }
51
- : defaultPreset.shades,
52
- opacity: { ...defaultPreset.opacity, ...overrides.opacity }
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';