@fundar/data-chart-telling 1.2.0 → 1.3.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 (38) hide show
  1. package/CHANGELOG.md +13 -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/legend/DiscreteSymbolIcon.svelte +38 -14
  7. package/dist/layout/legend/LegendState.svelte.js +3 -5
  8. package/dist/layout/margins/PlotMargins.svelte.js +31 -0
  9. package/dist/layout/plot/Plot.svelte +22 -2
  10. package/dist/layout/scales/ScalesLayout.svelte +21 -0
  11. package/dist/markers/delta/Delta.d.ts +6 -1
  12. package/dist/markers/delta/Delta.js +15 -0
  13. package/dist/markers/delta/DeltaMarker.svelte +9 -87
  14. package/dist/markers/delta/DeltaPoint.svelte +97 -0
  15. package/dist/markers/delta/DeltaPoint.svelte.d.ts +29 -0
  16. package/dist/markers/rule/RuleMarker.svelte +5 -2
  17. package/dist/phases/facet/FacetPhase.d.ts +2 -1
  18. package/dist/phases/facet/FacetPhase.js +19 -4
  19. package/dist/phases/statistics/StatisticsPhase.js +9 -1
  20. package/dist/plots/utils/segments.d.ts +4 -1
  21. package/dist/plots/utils/segments.js +19 -6
  22. package/dist/types/configuration/styling.d.ts +8 -0
  23. package/dist/types/layout/facet.d.ts +18 -0
  24. package/dist/types/layout/legend.d.ts +2 -0
  25. package/dist/types/layout/plot.d.ts +2 -0
  26. package/dist/types/markers/axis/delta.d.ts +15 -2
  27. package/dist/types/markers/axis/rule.d.ts +9 -2
  28. package/dist/types/phases/facet/phase.d.ts +4 -0
  29. package/dist/types/plots/props.d.ts +6 -3
  30. package/dist/types/plots/segments/config.d.ts +4 -2
  31. package/docs/facets.md +47 -0
  32. package/docs/markers.md +54 -26
  33. package/docs/styles-and-segments.md +1 -0
  34. package/docs/theming.md +1 -1
  35. package/examples/FacetsScoped.svelte +26 -0
  36. package/llms-full.txt +5 -3
  37. package/llms.txt +1 -0
  38. package/package.json +1 -1
@@ -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;
@@ -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;
package/docs/facets.md CHANGED
@@ -106,6 +106,53 @@ The data phases (grouping, transforms, statistics, shared scales) run once for a
106
106
 
107
107
  ![Panels for each combination of region and sector](images/facets-rows-cols.png)
108
108
 
109
+ ## Markers and segments in some panels
110
+
111
+ Every panel draws the plot's `markers` and `segments`. Add `facet` to one to limit it to the panels it names: the value of its `by` field, or a list of them. Without `facet` it applies to every panel, as before.
112
+
113
+ <!-- example: examples/FacetsScoped.svelte -->
114
+
115
+ ```svelte
116
+ <script lang="ts">
117
+ import { LinePlot } from '@fundar/data-chart-telling';
118
+ import { REGION_VALUES } from './datasets';
119
+
120
+ // Each region's value split into two sectors.
121
+ const SECTORS = [
122
+ { sector: 'Goods', share: 0.8 },
123
+ { sector: 'Services', share: 1.4 }
124
+ ];
125
+ const rows = REGION_VALUES.flatMap((row) =>
126
+ SECTORS.map(({ sector, share }) => ({ ...row, sector, value: row.value * share }))
127
+ );
128
+ </script>
129
+
130
+ <!-- One panel per region. `facet` on a marker or a segment limits it to the panels it names: the rule is drawn in GBA only, and Services is dashed in Cuyo only. -->
131
+ <LinePlot
132
+ width={900}
133
+ height={360}
134
+ data={rows}
135
+ x="date"
136
+ y="value"
137
+ color="sector"
138
+ facet={{ by: 'region', columns: 3 }}
139
+ markers={[{ type: 'rule', axis: 'y', value: 10, facet: 'GBA' }]}
140
+ segments={{ Services: [{ facet: 'Cuyo', styles: { stroke: { strokeDasharray: '4 4' } } }] }}
141
+ />
142
+ ```
143
+
144
+ <!-- /example -->
145
+
146
+ ![Markers and segments in some panels](images/facets-scoped.png)
147
+
148
+ | `facet` | Applies to |
149
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
150
+ | _not set_ | Every panel, and a plot with no facet |
151
+ | `'GBA'` or `['GBA', 'Cuyo']` | The panels with that `by` value (numbers and booleans match the text they print as) |
152
+ | `{ row: 'GBA' }`, `{ col: 'Goods' }` | In a `rows`/`cols` grid, the panels with that row or column value; `{ row, col }` takes both, and a side left out matches every value of it |
153
+
154
+ A marker or a segment that names a facet is not drawn by a plot that has none. It works on every plot kind: a `GeoPlot` map takes it the same way, on top of slicing its marker rows by the facet field.
155
+
109
156
  ## Alignment
110
157
 
111
158
  Panels line up: each plot reports the margins it settled on, and once all have, every column and row pins its widest ones, so the plot areas align whatever their tick labels need.
package/docs/markers.md CHANGED
@@ -58,32 +58,60 @@ Many markers also take channel fields (`color`, `fill`, `opacity`, `r`, `symbol`
58
58
 
59
59
  ## Catalog for axis-based plots
60
60
 
61
- | `type` | Draws |
62
- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63
- | `dot` | A point per row; `r`, `color`, `symbol`, `styles` (`DotStyle`) |
64
- | `text` | A label per row; `text` (string or function), `styles` (`FontStyle`), `declutter`, `edgeAware`, `fitMargins` |
65
- | `line` | A line through the rows |
66
- | `rule` | A reference line: `axis: 'x' \| 'y'`, `value` (or a function with `data`), `styles`; with `data`, `stroke`, `strokeWidth` and `strokeOpacity` also take a field or function per row |
67
- | `tick` | A short tick per row along one axis |
68
- | `bar` | A bar; `rect`, `cell`: rectangles and heatmap cells |
69
- | `rect` | A rectangle from `x1`/`x2`/`y1`/`y2`; `interval` turns `x`/`y` into ranges |
70
- | `area` | A filled area under a curve, or between `y1` and `y2`; `curve` |
71
- | `difference` | The area between two series, colored by which one is on top |
72
- | `arrow` | An arrow from `x1,y1` to `x2,y2`; `bend`, `headAngle`, `headLength`; with `data`, `stroke`, `strokeWidth` and `strokeOpacity` also take a field or function per row |
73
- | `trail` | A variable-width ribbon along a path, its width set by `r` |
74
- | `delta` | The distance between two series (or a series and a fixed value) at a position, as a line and a label |
75
- | `hull` | The convex hull around a cluster of points |
76
- | `density` | 2D density contour bands |
77
- | `box` | A box plot; sub-marks styled through `bar`, `rule`, `dot`, `tickMedian`, `tickMinMax`; `thickness` sets its width in pixels, `slot` its share of a band |
78
- | `violin` | A violin per category of its rows: the mirrored density of `y` (or `x`), `kernel`, `bandwidth`, `trim`, `peak`, `slot`; add a narrow `box` over the same rows for the box inside |
79
- | `regression`, `bollinger` | A fitted curve, a moving-average band |
80
- | `waffle` | The value as a grid of unit squares; `unit`, `multiple`, `gap` |
81
- | `voronoi`, `voronoiMesh`, `delaunayLink`, `delaunayMesh` | Voronoi cells and Delaunay triangulation |
82
- | `image` | An image per row; `src`, `width`, `height` |
83
- | `raster` | A field sampled over a rectangle, drawn to a canvas |
84
- | `brush` | A drag-to-select rectangle with `onbrushstart`, `onbrush`, `onbrushend` |
85
- | `hover` | The in-plot hover highlight (see below) |
86
- | `component` | Your own Svelte component (see below) |
61
+ | `type` | Draws |
62
+ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63
+ | `dot` | A point per row; `r`, `color`, `symbol`, `styles` (`DotStyle`) |
64
+ | `text` | A label per row; `text` (string or function), `styles` (`FontStyle`), `declutter`, `edgeAware`, `fitMargins` |
65
+ | `line` | A line through the rows |
66
+ | `rule` | A reference line: `axis: 'x' \| 'y'`, `value` (a list draws a line at each, or a function with `data`), `styles`; with `data`, `stroke`, `strokeWidth` and `strokeOpacity` also take a field or function per row |
67
+ | `tick` | A short tick per row along one axis |
68
+ | `bar` | A bar; `rect`, `cell`: rectangles and heatmap cells |
69
+ | `rect` | A rectangle from `x1`/`x2`/`y1`/`y2`; `interval` turns `x`/`y` into ranges |
70
+ | `area` | A filled area under a curve, or between `y1` and `y2`; `curve` |
71
+ | `difference` | The area between two series, colored by which one is on top |
72
+ | `arrow` | An arrow from `x1,y1` to `x2,y2`; `bend`, `headAngle`, `headLength`; with `data`, `stroke`, `strokeWidth` and `strokeOpacity` also take a field or function per row |
73
+ | `trail` | A variable-width ribbon along a path, its width set by `r` |
74
+ | `delta` | The distance between two series (or a series and a fixed value) at a position - or at each of a list of them, `at: [1980, 2000]` - as a line and a label |
75
+ | `hull` | The convex hull around a cluster of points |
76
+ | `density` | 2D density contour bands |
77
+ | `box` | A box plot; sub-marks styled through `bar`, `rule`, `dot`, `tickMedian`, `tickMinMax`; `thickness` sets its width in pixels, `slot` its share of a band |
78
+ | `violin` | A violin per category of its rows: the mirrored density of `y` (or `x`), `kernel`, `bandwidth`, `trim`, `peak`, `slot`; add a narrow `box` over the same rows for the box inside |
79
+ | `regression`, `bollinger` | A fitted curve, a moving-average band |
80
+ | `waffle` | The value as a grid of unit squares; `unit`, `multiple`, `gap` |
81
+ | `voronoi`, `voronoiMesh`, `delaunayLink`, `delaunayMesh` | Voronoi cells and Delaunay triangulation |
82
+ | `image` | An image per row; `src`, `width`, `height` |
83
+ | `raster` | A field sampled over a rectangle, drawn to a canvas |
84
+ | `brush` | A drag-to-select rectangle with `onbrushstart`, `onbrush`, `onbrushend` |
85
+ | `hover` | The in-plot hover highlight (see below) |
86
+ | `component` | Your own Svelte component (see below) |
87
+
88
+ ## Markers in some facets
89
+
90
+ On a plot with a [`facet`](facets.md#markers-and-segments-in-some-panels), every marker is drawn in every panel. A marker takes `facet`, a panel's value or a list of them, to be drawn only in the panels it names.
91
+
92
+ ## Several positions at once
93
+
94
+ A `rule` takes a list as its `value`, and a `delta` a list as its `at`. One marker then draws one line, or one delta, at each position, all with the same style and the same ends - instead of one marker for each.
95
+
96
+ ```ts
97
+ markers={[
98
+ // A rule at each of the three years.
99
+ { type: 'rule', axis: 'x', value: [1991, 2001, 2010], styles: { strokeDasharray: '4 4' } },
100
+ // The gap between the two series at each of the three.
101
+ { type: 'delta', axis: 'x', at: [1991, 2001, 2010], from: { type: 'series', series: 'female' }, to: { type: 'series', series: 'male' } }
102
+ ]}
103
+ ```
104
+
105
+ A delta's `line`, `text` and `dot` can be functions of the position, to style each one on its own - a label to the left of the last one, say, so it stays inside the plot:
106
+
107
+ ```ts
108
+ {
109
+ type: 'delta', axis: 'x', at: [1991, 2001, 2022], from: { type: 'series', series: 'female' }, to: { type: 'series', series: 'male' },
110
+ text: (at) => (at === 2022 ? { dx: -20, textAnchor: 'end' } : { dx: 20 })
111
+ }
112
+ ```
113
+
114
+ Every other marker draws from `data` rows, one mark per row, so it already takes many positions.
87
115
 
88
116
  ## Hover highlight
89
117
 
@@ -119,6 +119,7 @@ type Range = { from?: AxisValue; to?: AxisValue }; // inclusive on both ends
119
119
  - A segment without `areas` is a **catch-all**: it matches every point, is evaluated last, and is overridden by any area-specific segment. A catch-all placed first is a baseline that highlighted ranges override.
120
120
  - `'default'` applies to every series; a named key applies to that series only, and wins over `'default'` where both match.
121
121
  - `legend` names the segment on the automatic [legend](legend.md).
122
+ - `facet` limits the segment to the [facet panels](facets.md#markers-and-segments-in-some-panels) it names - a `by` value or a list of them. Without it the segment applies to every panel.
122
123
 
123
124
  The style shape depends on the plot:
124
125