@fundar/data-chart-telling 1.2.0 → 1.4.0

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 (42) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +2 -1
  3. package/dist/configuration/config.svelte.js +12 -0
  4. package/dist/layout/facet/scope.d.ts +40 -0
  5. package/dist/layout/facet/scope.js +104 -0
  6. package/dist/layout/figure/Figure.svelte +5 -0
  7. package/dist/layout/legend/DiscreteSymbolIcon.svelte +38 -14
  8. package/dist/layout/legend/LegendState.svelte.d.ts +11 -0
  9. package/dist/layout/legend/LegendState.svelte.js +56 -6
  10. package/dist/layout/margins/PlotMargins.svelte.d.ts +9 -5
  11. package/dist/layout/margins/PlotMargins.svelte.js +65 -6
  12. package/dist/layout/plot/Plot.svelte +22 -2
  13. package/dist/layout/scales/ScalesLayout.svelte +21 -0
  14. package/dist/markers/delta/Delta.d.ts +6 -1
  15. package/dist/markers/delta/Delta.js +15 -0
  16. package/dist/markers/delta/DeltaMarker.svelte +9 -87
  17. package/dist/markers/delta/DeltaPoint.svelte +97 -0
  18. package/dist/markers/delta/DeltaPoint.svelte.d.ts +29 -0
  19. package/dist/markers/rule/RuleMarker.svelte +5 -2
  20. package/dist/phases/facet/FacetPhase.d.ts +2 -1
  21. package/dist/phases/facet/FacetPhase.js +19 -4
  22. package/dist/phases/statistics/StatisticsPhase.js +9 -1
  23. package/dist/plots/utils/segments.d.ts +4 -1
  24. package/dist/plots/utils/segments.js +19 -6
  25. package/dist/types/configuration/styling.d.ts +8 -0
  26. package/dist/types/layout/facet.d.ts +18 -0
  27. package/dist/types/layout/legend.d.ts +16 -0
  28. package/dist/types/layout/plot.d.ts +2 -0
  29. package/dist/types/markers/axis/delta.d.ts +15 -2
  30. package/dist/types/markers/axis/rule.d.ts +9 -2
  31. package/dist/types/phases/facet/phase.d.ts +4 -0
  32. package/dist/types/plots/props.d.ts +6 -3
  33. package/dist/types/plots/segments/config.d.ts +4 -2
  34. package/docs/facets.md +47 -0
  35. package/docs/legend.md +14 -0
  36. package/docs/markers.md +54 -26
  37. package/docs/styles-and-segments.md +1 -0
  38. package/docs/theming.md +1 -1
  39. package/examples/FacetsScoped.svelte +26 -0
  40. package/llms-full.txt +5 -3
  41. package/llms.txt +1 -0
  42. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import type { Series } from '../../types/plots/data/common';
2
- import type { DeltaConfig } from '../../types/markers/axis/delta';
2
+ import type { DeltaConfig, DeltaMarkerConfig, DeltaPointConfig } from '../../types/markers/axis/delta';
3
3
  /** A delta, measured: where it sits (`at`), its two ends and what lies between. */
4
4
  export type ResolvedDelta = {
5
5
  cfg: DeltaConfig;
@@ -16,6 +16,11 @@ export type ResolvedDelta = {
16
16
  */
17
17
  export declare class Delta {
18
18
  #private;
19
+ /**
20
+ * The deltas a marker draws: one for its `at`, or one for each position of a list, each with the
21
+ * `line`, `text` and `dot` its style functions give for that position.
22
+ */
23
+ static points(marker: DeltaMarkerConfig): DeltaPointConfig[];
19
24
  /** The delta `config` asks for over `allSeries`, or `undefined` while an end has no value there. */
20
25
  static build<T extends Record<string, unknown>>(axis: 'x' | 'y', allSeries: Series<T>[], config: DeltaConfig): ResolvedDelta | undefined;
21
26
  }
@@ -5,6 +5,21 @@ import { resolveAccessor } from '../../utils/accessors';
5
5
  * it crosses that position, interpolated linearly between its two nearest points.
6
6
  */
7
7
  export class Delta {
8
+ /**
9
+ * The deltas a marker draws: one for its `at`, or one for each position of a list, each with the
10
+ * `line`, `text` and `dot` its style functions give for that position.
11
+ */
12
+ static points(marker) {
13
+ const positions = Array.isArray(marker.at) ? marker.at : [marker.at];
14
+ const own = (style, at) => typeof style === 'function' ? style(at) : style;
15
+ return positions.map((at) => ({
16
+ ...marker,
17
+ at,
18
+ line: own(marker.line, at),
19
+ text: own(marker.text, at),
20
+ dot: own(marker.dot, at)
21
+ }));
22
+ }
8
23
  /** The delta `config` asks for over `allSeries`, or `undefined` while an end has no value there. */
9
24
  static build(axis, allSeries, config) {
10
25
  const at = Number(config.at);
@@ -1,96 +1,18 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
- import { Line } from 'svelteplot';
3
- import DotMarker from '../dot/DotMarker.svelte';
4
- import TextMarker from '../text/TextMarker.svelte';
2
+ import DeltaPoint from './DeltaPoint.svelte';
5
3
  import { Delta } from './Delta';
6
- import { getConfiguration } from '../../configuration/config.svelte';
7
4
  import type { Series } from '../../types/plots/data/common';
8
5
  import type { DeltaMarkerConfig } from '../../types/markers/axis/delta';
9
6
 
7
+ /**
8
+ * A delta annotation: one at `marker.at`, or, when it is a list, one at each position - all with the
9
+ * same ends. `line`, `text` and `dot` can be functions of the position.
10
+ */
10
11
  let { marker, data = [] }: { marker: DeltaMarkerConfig; data?: Series<TData>[] } = $props();
11
12
 
12
- const resolved = $derived(Delta.build(marker.axis, data, marker));
13
- const label = $derived(getConfiguration().delta.text[marker.axis]);
14
- const lineAnchor = $derived(marker.text?.lineAnchor ?? label.lineAnchor);
15
-
16
- const clampRange = $derived.by((): [number, number] | undefined => {
17
- const domain = marker.axis === 'x' ? marker.yDomain : marker.xDomain;
18
- if (!domain || domain.length < 2) return undefined;
19
- const lo = Number(domain[0]);
20
- const hi = Number(domain[domain.length - 1]);
21
- return isNaN(lo) || isNaN(hi) ? undefined : [Math.min(lo, hi), Math.max(lo, hi)];
22
- });
23
-
24
- function clamp(v: number): number {
25
- return clampRange ? Math.max(clampRange[0], Math.min(clampRange[1], v)) : v;
26
- }
27
-
28
- function pt(fixed: number, varying: number) {
29
- return marker.axis === 'x' ? { x: fixed, y: varying } : { x: varying, y: fixed };
30
- }
31
-
32
- function textVaryCoord(fromVis: number, toVis: number) {
33
- return lineAnchor === 'top'
34
- ? Math.max(fromVis, toVis)
35
- : lineAnchor === 'bottom'
36
- ? Math.min(fromVis, toVis)
37
- : (fromVis + toVis) / 2;
38
- }
39
-
40
- // Pre-derive all coordinates as script-level signals so prop getters read safe
41
- // values during Svelte 5 teardown, before the {#if} guard finishes unmounting.
42
- const coords = $derived.by(() => {
43
- if (!resolved) return null;
44
- const fromVis = clamp(resolved.from ?? 0);
45
- const toVis = clamp(resolved.to ?? 0);
46
- const at = resolved.at ?? 0;
47
- return {
48
- fromPt: pt(at, fromVis),
49
- toPt: pt(at, toVis),
50
- textPt: pt(at, textVaryCoord(fromVis, toVis)),
51
- lineData: [pt(at, fromVis), pt(at, toVis)],
52
- diff: resolved.diff ?? 0
53
- };
54
- });
55
-
56
- const dotStyles = $derived({
57
- dotRadius: 4,
58
- dotFill: marker.line?.stroke,
59
- dotStroke: marker.line?.stroke,
60
- dotStrokeWidth: 2,
61
- ...marker.dot
62
- });
13
+ const points = $derived(Delta.points(marker));
63
14
  </script>
64
15
 
65
- {#if coords}
66
- <Line
67
- data={coords.lineData}
68
- x={(p) => p.x}
69
- y={(p) => p.y}
70
- stroke={marker.line?.stroke ?? 'currentColor'}
71
- strokeWidth={marker.line?.strokeWidth ?? 2}
72
- strokeOpacity={marker.line?.strokeOpacity ?? 0.8}
73
- strokeDasharray={marker.line?.strokeDasharray ?? '6 6'}
74
- strokeLinecap={marker.line?.strokeLinecap ?? 'round'}
75
- />
76
- <DotMarker marker={{ type: 'dot', x: coords.fromPt.x, y: coords.fromPt.y, styles: dotStyles }} />
77
- <DotMarker marker={{ type: 'dot', x: coords.toPt.x, y: coords.toPt.y, styles: dotStyles }} />
78
- <TextMarker
79
- marker={{
80
- type: 'text',
81
- x: coords.textPt.x,
82
- y: coords.textPt.y,
83
- text: (marker.formatDiff ?? ((v: number) => v.toFixed(1)))(coords.diff),
84
- styles: {
85
- fontWeight: 'bold',
86
- fontSize: 14,
87
- textAnchor: label.textAnchor,
88
- fill: marker.line?.stroke ?? 'currentColor',
89
- ...marker.text,
90
- dx: marker.text?.dx ?? label.dx,
91
- dy: marker.text?.dy ?? label.dy,
92
- lineAnchor
93
- }
94
- }}
95
- />
96
- {/if}
16
+ {#each points as point (String(point.at))}
17
+ <DeltaPoint marker={point} {data} />
18
+ {/each}
@@ -0,0 +1,97 @@
1
+ <script lang="ts" generics="TData extends Record<string, unknown>">
2
+ import { Line } from 'svelteplot';
3
+ import DotMarker from '../dot/DotMarker.svelte';
4
+ import TextMarker from '../text/TextMarker.svelte';
5
+ import { Delta } from './Delta';
6
+ import { getConfiguration } from '../../configuration/config.svelte';
7
+ import type { Series } from '../../types/plots/data/common';
8
+ import type { DeltaPointConfig } from '../../types/markers/axis/delta';
9
+
10
+ /** One delta, at the one position `marker.at` names. See {@link DeltaMarker} for a list of them. */
11
+ let { marker, data = [] }: { marker: DeltaPointConfig; data?: Series<TData>[] } = $props();
12
+
13
+ const resolved = $derived(Delta.build(marker.axis, data, marker));
14
+ const label = $derived(getConfiguration().delta.text[marker.axis]);
15
+ const lineAnchor = $derived(marker.text?.lineAnchor ?? label.lineAnchor);
16
+
17
+ const clampRange = $derived.by((): [number, number] | undefined => {
18
+ const domain = marker.axis === 'x' ? marker.yDomain : marker.xDomain;
19
+ if (!domain || domain.length < 2) return undefined;
20
+ const lo = Number(domain[0]);
21
+ const hi = Number(domain[domain.length - 1]);
22
+ return isNaN(lo) || isNaN(hi) ? undefined : [Math.min(lo, hi), Math.max(lo, hi)];
23
+ });
24
+
25
+ function clamp(v: number): number {
26
+ return clampRange ? Math.max(clampRange[0], Math.min(clampRange[1], v)) : v;
27
+ }
28
+
29
+ function pt(fixed: number, varying: number) {
30
+ return marker.axis === 'x' ? { x: fixed, y: varying } : { x: varying, y: fixed };
31
+ }
32
+
33
+ function textVaryCoord(fromVis: number, toVis: number) {
34
+ return lineAnchor === 'top'
35
+ ? Math.max(fromVis, toVis)
36
+ : lineAnchor === 'bottom'
37
+ ? Math.min(fromVis, toVis)
38
+ : (fromVis + toVis) / 2;
39
+ }
40
+
41
+ // Pre-derive all coordinates as script-level signals so prop getters read safe
42
+ // values during Svelte 5 teardown, before the {#if} guard finishes unmounting.
43
+ const coords = $derived.by(() => {
44
+ if (!resolved) return null;
45
+ const fromVis = clamp(resolved.from ?? 0);
46
+ const toVis = clamp(resolved.to ?? 0);
47
+ const at = resolved.at ?? 0;
48
+ return {
49
+ fromPt: pt(at, fromVis),
50
+ toPt: pt(at, toVis),
51
+ textPt: pt(at, textVaryCoord(fromVis, toVis)),
52
+ lineData: [pt(at, fromVis), pt(at, toVis)],
53
+ diff: resolved.diff ?? 0
54
+ };
55
+ });
56
+
57
+ const dotStyles = $derived({
58
+ dotRadius: 4,
59
+ dotFill: marker.line?.stroke,
60
+ dotStroke: marker.line?.stroke,
61
+ dotStrokeWidth: 2,
62
+ ...marker.dot
63
+ });
64
+ </script>
65
+
66
+ {#if coords}
67
+ <Line
68
+ data={coords.lineData}
69
+ x={(p) => p.x}
70
+ y={(p) => p.y}
71
+ stroke={marker.line?.stroke ?? 'currentColor'}
72
+ strokeWidth={marker.line?.strokeWidth ?? 2}
73
+ strokeOpacity={marker.line?.strokeOpacity ?? 0.8}
74
+ strokeDasharray={marker.line?.strokeDasharray ?? '6 6'}
75
+ strokeLinecap={marker.line?.strokeLinecap ?? 'round'}
76
+ />
77
+ <DotMarker marker={{ type: 'dot', x: coords.fromPt.x, y: coords.fromPt.y, styles: dotStyles }} />
78
+ <DotMarker marker={{ type: 'dot', x: coords.toPt.x, y: coords.toPt.y, styles: dotStyles }} />
79
+ <TextMarker
80
+ marker={{
81
+ type: 'text',
82
+ x: coords.textPt.x,
83
+ y: coords.textPt.y,
84
+ text: (marker.formatDiff ?? ((v: number) => v.toFixed(1)))(coords.diff),
85
+ styles: {
86
+ fontWeight: 'bold',
87
+ fontSize: 14,
88
+ textAnchor: label.textAnchor,
89
+ fill: marker.line?.stroke ?? 'currentColor',
90
+ ...marker.text,
91
+ dx: marker.text?.dx ?? label.dx,
92
+ dy: marker.text?.dy ?? label.dy,
93
+ lineAnchor
94
+ }
95
+ }}
96
+ />
97
+ {/if}
@@ -0,0 +1,29 @@
1
+ import type { Series } from '../../types/plots/data/common';
2
+ import type { DeltaPointConfig } from '../../types/markers/axis/delta';
3
+ declare function $$render<TData extends Record<string, unknown>>(): {
4
+ props: {
5
+ marker: DeltaPointConfig;
6
+ data?: Series<TData>[];
7
+ };
8
+ exports: {};
9
+ bindings: "";
10
+ slots: {};
11
+ events: {};
12
+ };
13
+ declare class __sveltets_Render<TData extends Record<string, unknown>> {
14
+ props(): ReturnType<typeof $$render<TData>>['props'];
15
+ events(): ReturnType<typeof $$render<TData>>['events'];
16
+ slots(): ReturnType<typeof $$render<TData>>['slots'];
17
+ bindings(): "";
18
+ exports(): {};
19
+ }
20
+ interface $$IsomorphicComponent {
21
+ new <TData extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TData>['props']>, ReturnType<__sveltets_Render<TData>['events']>, ReturnType<__sveltets_Render<TData>['slots']>> & {
22
+ $$bindings?: ReturnType<__sveltets_Render<TData>['bindings']>;
23
+ } & ReturnType<__sveltets_Render<TData>['exports']>;
24
+ <TData extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<TData>['props']> & {}): ReturnType<__sveltets_Render<TData>['exports']>;
25
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
26
+ }
27
+ declare const DeltaPoint: $$IsomorphicComponent;
28
+ type DeltaPoint<TData extends Record<string, unknown>> = InstanceType<typeof DeltaPoint<TData>>;
29
+ export default DeltaPoint;
@@ -6,7 +6,7 @@
6
6
 
7
7
  /**
8
8
  * A single vertical (`axis: 'x'`) or horizontal (`axis: 'y'`) reference
9
- * line - or, with `data`, one line per row. Shared by author-configured
9
+ * line - or, with `data`, one line per row, or, with a list as `value`, one per value. Shared by author-configured
10
10
  * `'rule'` markers and `HoverMarker`'s crosshair (which always calls it
11
11
  * with a literal `value` and no `data`). Falls back to `'currentColor'`/`1`
12
12
  * when the caller supplies no style. See `RuleMarkerConfig`'s own doc
@@ -21,7 +21,10 @@
21
21
  const styles = $derived(marker.styles);
22
22
 
23
23
  const RuleMark = $derived(axis === 'x' ? RuleX : RuleY);
24
- const rows = $derived(data ?? [{ v: value }]);
24
+ /** Without `data`, one row per position: a list of values draws a line at each. */
25
+ const rows = $derived(
26
+ data ?? (Array.isArray(value) ? value : [value]).map((v) => ({ v: v as AxisValue }))
27
+ );
25
28
  /**
26
29
  * With `data`, `value` is forwarded raw (a field key resolves per row via
27
30
  * svelteplot's own `resolve()`, same as every other mark, and lets its
@@ -3,7 +3,8 @@ import type { FacetStageState, PhaseStage } from '../../types/phases';
3
3
  * The "facet" phase: the panels the plot's `facet` lays its rows out in, and
4
4
  * which panel each row belongs to. It runs first, so the "data" phase builds
5
5
  * every series already split by panel. A plot drawn from pre-built `series`
6
- * has no rows to split, so its facet is ignored.
6
+ * has no rows to split, so its facet is ignored. The plot's `segments` and markers that name a facet
7
+ * are handed to the panels they name here, once, for every later phase.
7
8
  */
8
9
  export declare class FacetPhase<TState extends FacetStageState> implements PhaseStage<TState> {
9
10
  readonly phase: "facet";
@@ -1,18 +1,26 @@
1
1
  import { buildFacetKeyOf, buildFacetPanels } from '../../layout/facet/config';
2
2
  import { PANEL_KEY } from './FacetPanels';
3
+ import { FacetScope } from '../../layout/facet/scope';
3
4
  /**
4
5
  * The "facet" phase: the panels the plot's `facet` lays its rows out in, and
5
6
  * which panel each row belongs to. It runs first, so the "data" phase builds
6
7
  * every series already split by panel. A plot drawn from pre-built `series`
7
- * has no rows to split, so its facet is ignored.
8
+ * has no rows to split, so its facet is ignored. The plot's `segments` and markers that name a facet
9
+ * are handed to the panels they name here, once, for every later phase.
8
10
  */
9
11
  export class FacetPhase {
10
12
  phase = 'facet';
11
13
  run = (state) => {
12
14
  const { data, series } = state;
13
15
  const facet = state.facet;
14
- if (!facet || series || !data)
15
- return state;
16
+ // Without panels, a segment or a marker that names a facet has none to be drawn in.
17
+ if (!facet || series || !data) {
18
+ return {
19
+ ...state,
20
+ segments: FacetScope.scopeSegments(state.segments, undefined),
21
+ authorMarkers: FacetScope.scope(state.authorMarkers, undefined)
22
+ };
23
+ }
16
24
  const panels = buildFacetPanels(facet, data).map(({ rows: _rows, ...panel }) => panel);
17
25
  const keys = new Set(panels.map((p) => p.key));
18
26
  const keyOf = buildFacetKeyOf(facet);
@@ -23,6 +31,13 @@ export class FacetPhase {
23
31
  const key = keyOf(row);
24
32
  return keys.has(key) ? key : undefined;
25
33
  };
26
- return { ...state, panels, panelOf };
34
+ // Every later phase reads each panel's own segments and markers: nothing past here knows of `facet`.
35
+ return {
36
+ ...state,
37
+ panels,
38
+ panelOf,
39
+ segments: FacetScope.distributeSegments(state.segments, panels),
40
+ authorMarkers: FacetScope.distributeMarkers(state.authorMarkers, panels)
41
+ };
27
42
  };
28
43
  }
@@ -24,9 +24,17 @@ export class StatisticsPhase {
24
24
  run = (state) => {
25
25
  if (!state.panels)
26
26
  return { ...state, markers: this.#resolve(state) };
27
- const markers = state.panels.flatMap((panel) => this.#resolve(FacetPanels.narrowToPanel(state, panel.key)).map((marker) => ({ ...marker, [PANEL_KEY]: panel.key })));
27
+ const markers = state.panels.flatMap((panel) => this.#resolve(this.#ownTo(FacetPanels.narrowToPanel(state, panel.key), panel.key)).map((marker) => ({ ...marker, [PANEL_KEY]: panel.key })));
28
28
  return { ...state, markers };
29
29
  };
30
+ /** `state` with the author's markers that the "facet" phase tagged for another panel left out. */
31
+ #ownTo(state, panel) {
32
+ const authorMarkers = state.authorMarkers?.filter((marker) => {
33
+ const tagged = marker[PANEL_KEY];
34
+ return tagged === undefined || tagged === panel;
35
+ });
36
+ return { ...state, authorMarkers };
37
+ }
30
38
  #resolve(state) {
31
39
  const options = this.#options;
32
40
  const { resolvedSeries = [], flat = [], colorForSeries, statistics, authorMarkers = [] } = state;
@@ -8,8 +8,11 @@ import type { ScopedMarkerGroup } from '../../types/layout/geometry';
8
8
  * Returns the flat {@link Segment} list for one series.
9
9
  * Series-specific segments are added before `'default'` segments so that
10
10
  * series-specific catch-alls win over default catch-alls on tie-breaks.
11
+ * A series split by facet panel also reads the segments its panel was given
12
+ * for it - under its id - and for every series of the panel - under the
13
+ * panel's own `'default'` - ahead of the plot's own, as the more specific.
11
14
  */
12
- export declare function resolveSegmentsForSeries<TStyle>(segments: SegmentsConfig<TStyle>, seriesName: string): Segment<TStyle>[];
15
+ export declare function resolveSegmentsForSeries<TStyle>(segments: SegmentsConfig<TStyle>, series: Pick<Series, 'name' | 'id' | 'panel'>): Segment<TStyle>[];
13
16
  /** Returns true when `xVal` falls inside any of a segment's x ranges. */
14
17
  export declare function matchesSegmentX<TStyle>(xVal: AxisValue, seg: Segment<TStyle>): boolean;
15
18
  /**
@@ -1,15 +1,28 @@
1
1
  import { resolveAccessor } from '../../utils/accessors';
2
2
  import { merge } from '../../utils/merge';
3
+ import { seriesIdOf } from '../../utils/grouping';
4
+ import { FacetPanels } from '../../phases/facet/FacetPanels';
3
5
  const DEFAULT_KEY = 'default';
4
6
  /**
5
7
  * Returns the flat {@link Segment} list for one series.
6
8
  * Series-specific segments are added before `'default'` segments so that
7
9
  * series-specific catch-alls win over default catch-alls on tie-breaks.
10
+ * A series split by facet panel also reads the segments its panel was given
11
+ * for it - under its id - and for every series of the panel - under the
12
+ * panel's own `'default'` - ahead of the plot's own, as the more specific.
8
13
  */
9
- export function resolveSegmentsForSeries(segments, seriesName) {
10
- const specific = segments[seriesName] ?? [];
11
- const defaults = segments[DEFAULT_KEY] ?? [];
12
- return [...specific, ...defaults];
14
+ export function resolveSegmentsForSeries(segments, series) {
15
+ const noPanel = series.panel === undefined;
16
+ const own = noPanel ? [] : (segments[seriesIdOf(series)] ?? []);
17
+ const panelDefaults = noPanel
18
+ ? []
19
+ : (segments[FacetPanels.buildSeriesId(series.panel, DEFAULT_KEY)] ?? []);
20
+ return [
21
+ ...own,
22
+ ...(segments[series.name] ?? []),
23
+ ...panelDefaults,
24
+ ...(segments[DEFAULT_KEY] ?? [])
25
+ ];
13
26
  }
14
27
  /** Returns true when `xVal` falls inside any of a segment's x ranges. */
15
28
  export function matchesSegmentX(xVal, seg) {
@@ -176,7 +189,7 @@ function readChannel(accessor) {
176
189
  export function buildGroupedSeries(seriesList, segments = {}, connect = true) {
177
190
  return seriesList.map((s) => ({
178
191
  series: s,
179
- visualSegments: resolveSeriesSegments(s, resolveSegmentsForSeries(segments, s.name), connect)
192
+ visualSegments: resolveSeriesSegments(s, resolveSegmentsForSeries(segments, s), connect)
180
193
  }));
181
194
  }
182
195
  /**
@@ -193,7 +206,7 @@ export function buildScopedMarkerGroups(args) {
193
206
  return groupedSeries.map((group, seriesIndex) => {
194
207
  const defaultColor = colorFor(group.series, seriesIndex);
195
208
  const xFor = readChannel(group.series.x);
196
- const segs = resolveSegmentsForSeries(segments, group.series.name);
209
+ const segs = resolveSegmentsForSeries(segments, group.series);
197
210
  return {
198
211
  name: group.series.name,
199
212
  ...(group.series.id !== undefined ? { id: group.series.id } : {}),
@@ -1,6 +1,7 @@
1
1
  import type { StrokeStyle, DotStyle, FontStyle, AreaStyle } from '../plots/styling';
2
2
  import type { LineAnchor, SymbolType, TextAnchor } from '../plots/constants';
3
3
  import type { PlotChromeEntry } from '../plots/props';
4
+ import type { LegendMark } from '../layout/legend';
4
5
  import type { EdgeTicksOptions } from '../layout/scales';
5
6
  import type { CurveName } from '../layout/styles/curve';
6
7
  export type TextStyle = {
@@ -133,6 +134,13 @@ export type PlotStylesConfig = {
133
134
  gap: string;
134
135
  /** swatch square size */
135
136
  swatchSize: string;
137
+ /** The icon a discrete legend item draws for its series. */
138
+ icon: {
139
+ /** Draws the icons with rounded corners. */
140
+ rounded: boolean;
141
+ /** The shape a series icon takes by how the series draws, unless the series sets its own `symbol`. */
142
+ marks: Record<LegendMark, SymbolType>;
143
+ };
136
144
  /** Fallback opacity for a legend-disabled series/value when a section doesn't set its own style. */
137
145
  disabledOpacity: number;
138
146
  /**
@@ -40,6 +40,24 @@ export type FacetCellContext<TRow extends Record<string, unknown>> = {
40
40
  export type FacetCellSnippet<TRow extends Record<string, unknown>> = Snippet<[
41
41
  FacetCellContext<TRow>
42
42
  ]>;
43
+ /** A facet value as the panel's label reads it: a number or a boolean matches by the text it prints as. */
44
+ export type FacetValue = string | number | boolean;
45
+ /**
46
+ * The facet panels a segment or a marker applies to. A value, or a list of them, names panels by their
47
+ * label - the `by` value; in a `rows`/`cols` grid, `{ row, col }` name the panels by the grid's own
48
+ * values, and a side left out matches every value of it.
49
+ */
50
+ export type FacetTarget = FacetValue | FacetValue[] | {
51
+ row?: FacetValue | FacetValue[];
52
+ col?: FacetValue | FacetValue[];
53
+ };
54
+ /**
55
+ * A segment or a marker that can be limited to some facet panels. Without `facet` it applies to every
56
+ * panel, as before; with it, only to the panels it names - and to none of a plot that has no facet.
57
+ */
58
+ export type FacetScoped = {
59
+ facet?: FacetTarget;
60
+ };
43
61
  export type FacetIndicatorContext = {
44
62
  current: number;
45
63
  total: number;
@@ -33,6 +33,8 @@ export type LegendItem = {
33
33
  name: string;
34
34
  label?: string;
35
35
  color?: string;
36
+ /** How the series draws; with no `symbol`, `legend.icon.marks` picks the icon from it. */
37
+ mark?: LegendMark;
36
38
  symbol?: SymbolType;
37
39
  /** Whether the symbol is filled, or drawn as an outline. Defaults to `true`. */
38
40
  filled?: boolean;
@@ -290,6 +292,20 @@ export type ColorOverride = Omit<ContinuousLegendSection, 'type' | 'min' | 'max'
290
292
  * ramp `interactive`, and only its first ramp is.
291
293
  */
292
294
  export type LegendExtension = {
295
+ /**
296
+ * Series to turn off from code, as if the reader had clicked them: a list
297
+ * of names (each dimmed, to `legend.disabledOpacity`), or a name → what
298
+ * turning it off does map - an opacity to dim to, or a full
299
+ * {@link LegendDisabledAction} (`dim`, `hide` or `omit`). It follows the
300
+ * value it is given: a series left out is turned back on. One the reader
301
+ * turned off themselves is left as it is.
302
+ */
303
+ off?: string[] | Record<string, number | LegendDisabledAction>;
304
+ /**
305
+ * The series to keep on: every other series is dimmed, the inverse of
306
+ * listing the rest in `off`. An entry of `off` for a series wins.
307
+ */
308
+ highlight?: string | string[];
293
309
  items?: LegendItem[];
294
310
  sections?: LegendSection[];
295
311
  series?: Omit<DiscreteLegendSection, 'type' | 'items'>;
@@ -29,6 +29,8 @@ export type PlotCellContext<TRow extends Record<string, unknown>, TScales extend
29
29
  scales?: TScales;
30
30
  /** This slice's facet panel, as a {@link Pipeline.runData} over every panel resolved it. */
31
31
  panel?: object;
32
+ /** Where this slice sits among the facet panels - `undefined` when the plot has no facet. */
33
+ facet?: NonNullable<SlicePlace['facet']>;
32
34
  /** Whether any row - the plot's own or a marker's - belongs in this slice; rows without a slicing field belong in all. */
33
35
  keep: (row: TRow) => boolean;
34
36
  };
@@ -19,9 +19,22 @@ export type DeltaConfig = {
19
19
  text?: FontStyle;
20
20
  dot?: DotStyle;
21
21
  };
22
- /** A delta annotation between two points on a line chart. */
23
- export type DeltaMarkerConfig = {
22
+ /** One delta annotation, at one position, between two points on a line chart. */
23
+ export type DeltaPointConfig = {
24
24
  type: 'delta';
25
25
  xDomain?: (number | string | Date)[];
26
26
  yDomain?: (number | string | Date)[];
27
27
  } & DeltaConfig;
28
+ /** A style as it is, or as a function of the position the delta is drawn at. */
29
+ export type PerPosition<T> = T | ((at: AxisValue) => T);
30
+ /**
31
+ * A delta annotation between two points on a line chart. `at` can be a list of positions: one delta is
32
+ * drawn at each, with the same ends. `line`, `text` and `dot` can be functions of the position, to style
33
+ * each delta on its own - a label to the left of the last one, say, so it stays inside the plot.
34
+ */
35
+ export type DeltaMarkerConfig = Omit<DeltaPointConfig, 'at' | 'line' | 'text' | 'dot'> & {
36
+ at: AxisValue | AxisValue[];
37
+ line?: PerPosition<StrokeStyle>;
38
+ text?: PerPosition<FontStyle>;
39
+ dot?: PerPosition<DotStyle>;
40
+ };
@@ -1,11 +1,18 @@
1
1
  import type { AxisValue } from '../../layout/mapping';
2
2
  import type { StrokeStyle } from '../../plots/styling';
3
3
  import type { CommonMarkerConfigWithInterval } from '../channels/commonMarkerConfigWithInterval';
4
- /** A vertical or horizontal reference line, or one line per row when `data` is given. */
4
+ /**
5
+ * A vertical or horizontal reference line, one line per row when `data` is given, or one per value
6
+ * when `value` is a list - all with the same style.
7
+ */
5
8
  export type RuleMarkerConfig = {
6
9
  type: 'rule';
7
10
  axis: 'x' | 'y';
8
- value: AxisValue | string | ((d: any) => AxisValue);
11
+ /**
12
+ * Where the line goes along `axis`. A list draws a line at each value (without `data`); with `data`,
13
+ * a field key or a function of the row gives each row's own.
14
+ */
15
+ value: AxisValue | string | (AxisValue | string)[] | ((d: any) => AxisValue);
9
16
  inset?: number;
10
17
  insetTop?: number;
11
18
  insetBottom?: number;
@@ -15,4 +15,8 @@ export type FacetStageState = {
15
15
  panels?: FacetPanel[];
16
16
  /** The panel a row belongs to: its own panel tag, or its facet values. `undefined` for a row no panel shows. */
17
17
  panelOf?: (row: Record<string | symbol, unknown>) => string | undefined;
18
+ /** The plot's `segments`. The phase moves those that name a facet to the panels they name, or drops them without panels. */
19
+ segments?: Record<string, object[]>;
20
+ /** The plot's own markers. The phase tags those that name a facet with the panels they name, or drops them without panels. */
21
+ authorMarkers?: object[];
18
22
  };
@@ -3,7 +3,7 @@ import type { Snippet } from 'svelte';
3
3
  import type { TooltipOptions, TooltipProp } from '../layout/tooltip';
4
4
  import type { HoverPoint, HoverStrategy } from '../layout/hover';
5
5
  import type { LegendExtension, LegendSection } from '../layout/legend';
6
- import type { FacetConfig } from '../layout/facet';
6
+ import type { FacetConfig, FacetScoped } from '../layout/facet';
7
7
  import type { GridPlacement } from '../layout/grid';
8
8
  import type { TimelineConfig } from '../layout/timeline';
9
9
  import type { Segment } from './segments/config';
@@ -14,8 +14,11 @@ import type { MarkerTransform } from '../transforms';
14
14
  /** The `styles` prop accepted by every plot kind - its own
15
15
  * styles shape ({@link AxisBasedStylesConfig}, `GeoStylesConfig`, …). */
16
16
  export type StylesConfig<TStyles extends object> = TStyles;
17
- /** The `markers` prop accepted by every plot kind - a list of that kind's own marker union. */
18
- export type MarkersConfig<TMarker> = TMarker[];
17
+ /**
18
+ * The `markers` prop accepted by every plot kind - a list of that kind's own marker union, each of
19
+ * which can be limited to some facet panels with `facet`.
20
+ */
21
+ export type MarkersConfig<TMarker> = (TMarker & FacetScoped)[];
19
22
  /**
20
23
  * The `segments` prop - series name (or `'default'`) to its {@link Segment}s.
21
24
  * Area-specific segments win over catch-all ones.
@@ -3,6 +3,7 @@ import type { AxisValue } from '../../layout/mapping';
3
3
  import type { Series } from '../data/common';
4
4
  import type { LineSegmentStyle } from './common';
5
5
  import type { MarkLegend } from '../../layout/legend';
6
+ import type { FacetScoped } from '../../layout/facet';
6
7
  /** A half-open interval on one axis. Both bounds are inclusive; omit either to leave that end open. */
7
8
  export type Range = {
8
9
  from?: AxisValue;
@@ -22,7 +23,8 @@ export type Area = {
22
23
  };
23
24
  /**
24
25
  * One styled rule for a plot. A data point (or cell) is styled when it falls
25
- * inside **any** of the listed `areas`. Omitting `areas` (or passing an empty
26
+ * inside **any** of the listed `areas`. With a `facet`, the rule exists only in
27
+ * the facet panels it names. Omitting `areas` (or passing an empty
26
28
  * array) makes the segment a catch-all that matches every data point - useful
27
29
  * as a baseline style. `TStyle` is the per-mark shape ({@link LineSegmentStyle},
28
30
  * {@link BarSegmentStyle}, {@link CellSegmentStyle}, …).
@@ -31,7 +33,7 @@ export type Area = {
31
33
  * priority, regardless of their position in the list. A catch-all placed first
32
34
  * therefore acts as a fallback baseline that area-specific segments override.
33
35
  */
34
- export type Segment<TStyle> = {
36
+ export type Segment<TStyle> = FacetScoped & {
35
37
  areas?: Area[];
36
38
  scales?: ChannelScalesConfig;
37
39
  styles?: TStyle;