@fundar/data-chart-telling 1.10.0 → 1.12.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [1.12.0] - 2026-11-04
6
+
7
+ ### Added
8
+
9
+ - `labels` on a discrete legend section (`legend.series.labels`, or on an explicit section): `prefix`, `suffix` and `replace` rewrite the label of every item - to add a unit (`' years'`) or shorten a word (`{ Quintile: 'Q' }`, or `[[/^Quartile /, 'Q: ']]`) - without removing the automatic items to write them again. The names are left as they are, so turning series off still goes by them.
10
+
11
+ ## [1.11.0] - 2026-11-04
12
+
13
+ ### Changed
14
+
15
+ - The dots of a `delta` take the color of the series their end measures, as the plot draws it. Before, they took the line's `stroke` and, with none set, a neutral color. The line's `stroke` still wins when it is set, and the delta's own `dot` wins over both; an end on an axis keeps the neutral color. `usePlot()` now also gives `colorForSeries`.
16
+ - **Breaking:** a delta's line is styled with `stroke`, not `line`, like the rest of the styles (`DeltaMarkerConfig.line`, also as a function of the position). Rename it where it is set.
17
+
5
18
  ## [1.10.0] - 2026-11-03
6
19
 
7
20
  ### Added
@@ -238,7 +238,8 @@
238
238
  scales: () => scales,
239
239
  axes: () => state.axes,
240
240
  data: () => data,
241
- size: () => ({ width, height })
241
+ size: () => ({ width, height }),
242
+ colorForSeries: () => state.colorForSeries
242
243
  });
243
244
 
244
245
  /** The domains `ScalesLayout` widens an axis to, for an edge tick's `'extend'` (see `EdgeTicks`). */
@@ -13,6 +13,7 @@
13
13
  } from '../../configuration/config.svelte';
14
14
  import { HoverState } from '../hover/HoverState.svelte';
15
15
  import { LegendState } from '../legend/LegendState.svelte';
16
+ import { LegendLabels } from '../legend/LegendLabels';
16
17
  import type { HoverStrategy } from '../../types/layout/hover';
17
18
  import type { FigureBoxProps, PlotChromeConfig } from '../../types/plots/props';
18
19
  import type { PlotBoxSnippet } from '../../types/layout/figure';
@@ -112,11 +113,14 @@
112
113
  : 'auto'
113
114
  );
114
115
  const sections = $derived(
115
- legendMode === 'explicit'
116
- ? legendState.colorize(legend as LegendSection[])
117
- : legendMode === 'auto'
118
- ? legendState.sections(extension)
119
- : []
116
+ LegendLabels.apply(
117
+ legendMode === 'explicit'
118
+ ? legendState.colorize(legend as LegendSection[])
119
+ : legendMode === 'auto'
120
+ ? legendState.sections(extension)
121
+ : [],
122
+ cfg
123
+ )
120
124
  );
121
125
 
122
126
  /**
@@ -0,0 +1,12 @@
1
+ import type { PlotStylesConfig } from '../../types/configuration/styling';
2
+ import type { LegendLabels as LegendLabelsConfig, LegendSection } from '../../types/layout/legend';
3
+ /** How a discrete section rewrites the labels its items show: swaps, then a prefix and a suffix. */
4
+ export declare class LegendLabels {
5
+ /**
6
+ * `sections`, with the labels of every discrete section that sets `labels` rewritten. An item is
7
+ * rewritten from the text it would show - its `label`, else its name as the legend writes it.
8
+ */
9
+ static apply(sections: LegendSection[], cfg: Pick<PlotStylesConfig, 'legend'>): LegendSection[];
10
+ /** `text` after `labels`' swaps, in order (each replaces every match), with its prefix and suffix around it. */
11
+ static rewrite(text: string, labels: LegendLabelsConfig): string;
12
+ }
@@ -0,0 +1,35 @@
1
+ import { LegendText } from './LegendText';
2
+ /** How a discrete section rewrites the labels its items show: swaps, then a prefix and a suffix. */
3
+ export class LegendLabels {
4
+ /**
5
+ * `sections`, with the labels of every discrete section that sets `labels` rewritten. An item is
6
+ * rewritten from the text it would show - its `label`, else its name as the legend writes it.
7
+ */
8
+ static apply(sections, cfg) {
9
+ return sections.map((section) => {
10
+ if (section.type !== 'discrete' || !section.labels)
11
+ return section;
12
+ const { labels } = section;
13
+ return {
14
+ ...section,
15
+ items: section.items.map((item) => ({
16
+ ...item,
17
+ label: LegendLabels.rewrite(item.label || LegendText.of(item.name, cfg), labels)
18
+ }))
19
+ };
20
+ });
21
+ }
22
+ /** `text` after `labels`' swaps, in order (each replaces every match), with its prefix and suffix around it. */
23
+ static rewrite(text, labels) {
24
+ const swaps = Array.isArray(labels.replace)
25
+ ? labels.replace
26
+ : Object.entries(labels.replace ?? {});
27
+ let result = text;
28
+ for (const [pattern, replacement] of swaps)
29
+ result =
30
+ typeof pattern === 'string'
31
+ ? result.replaceAll(pattern, () => replacement)
32
+ : result.replace(pattern.global ? pattern : new RegExp(pattern.source, `${pattern.flags}g`), replacement);
33
+ return `${labels.prefix ?? ''}${result}${labels.suffix ?? ''}`;
34
+ }
35
+ }
@@ -16,6 +16,8 @@ export type PlotContextParts<T extends Record<string, unknown>> = {
16
16
  width: number;
17
17
  height: number;
18
18
  };
19
+ /** Each series' color, by name - axis kinds only. */
20
+ colorForSeries?: () => ((name: string) => string) | undefined;
19
21
  };
20
22
  /**
21
23
  * Everything one plot is, gathered where what draws inside it can reach it:
@@ -36,6 +38,7 @@ export declare class PlotContext<T extends Record<string, unknown> = Record<stri
36
38
  get scales(): object | undefined;
37
39
  get axes(): PlotAxes | undefined;
38
40
  get data(): T[];
41
+ get colorForSeries(): ((name: string) => string) | undefined;
39
42
  get width(): number;
40
43
  get height(): number;
41
44
  }
@@ -37,6 +37,9 @@ export class PlotContext {
37
37
  get data() {
38
38
  return this.#parts.data();
39
39
  }
40
+ get colorForSeries() {
41
+ return this.#parts.colorForSeries?.();
42
+ }
40
43
  get width() {
41
44
  return this.#parts.size().width;
42
45
  }
@@ -126,6 +126,8 @@ export declare class PublicPlotContext<T extends Record<string, unknown> = Recor
126
126
  get axes(): PlotAxes | undefined;
127
127
  /** The rows the plot draws and hovers over. */
128
128
  get data(): T[];
129
+ /** The color a series is drawn in, by its name - `undefined` for a plot with no series (a map). */
130
+ get colorForSeries(): ((name: string) => string) | undefined;
129
131
  get width(): number;
130
132
  get height(): number;
131
133
  }
@@ -231,6 +231,10 @@ export class PublicPlotContext {
231
231
  get data() {
232
232
  return this.#context.data;
233
233
  }
234
+ /** The color a series is drawn in, by its name - `undefined` for a plot with no series (a map). */
235
+ get colorForSeries() {
236
+ return this.#context.colorForSeries;
237
+ }
234
238
  get width() {
235
239
  return this.#context.width;
236
240
  }
@@ -18,7 +18,7 @@ export declare class Delta {
18
18
  #private;
19
19
  /**
20
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.
21
+ * `stroke`, `text` and `dot` its style functions give for that position.
22
22
  */
23
23
  static points(marker: DeltaMarkerConfig): DeltaPointConfig[];
24
24
  /** The delta `config` asks for over `allSeries`, or `undefined` while an end has no value there. */
@@ -7,7 +7,7 @@ import { resolveAccessor } from '../../utils/accessors';
7
7
  export class Delta {
8
8
  /**
9
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.
10
+ * `stroke`, `text` and `dot` its style functions give for that position.
11
11
  */
12
12
  static points(marker) {
13
13
  const positions = Array.isArray(marker.at) ? marker.at : [marker.at];
@@ -15,7 +15,7 @@ export class Delta {
15
15
  return positions.map((at) => ({
16
16
  ...marker,
17
17
  at,
18
- line: own(marker.line, at),
18
+ stroke: own(marker.stroke, at),
19
19
  text: own(marker.text, at),
20
20
  dot: own(marker.dot, at)
21
21
  }));
@@ -6,7 +6,7 @@
6
6
 
7
7
  /**
8
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.
9
+ * same ends. `stroke`, `text` and `dot` can be functions of the position.
10
10
  */
11
11
  let { marker, data = [] }: { marker: DeltaMarkerConfig; data?: Series<TData>[] } = $props();
12
12
 
@@ -3,6 +3,7 @@
3
3
  import DotMarker from '../dot/DotMarker.svelte';
4
4
  import TextMarker from '../text/TextMarker.svelte';
5
5
  import { Delta } from './Delta';
6
+ import { usePlot } from '../../layout/plot/usePlot';
6
7
  import { getConfiguration } from '../../configuration/config.svelte';
7
8
  import type { Series } from '../../types/plots/data/common';
8
9
  import type { DeltaPointConfig } from '../../types/markers/axis/delta';
@@ -10,6 +11,8 @@
10
11
  /** One delta, at the one position `marker.at` names. See {@link DeltaMarker} for a list of them. */
11
12
  let { marker, data = [] }: { marker: DeltaPointConfig; data?: Series<TData>[] } = $props();
12
13
 
14
+ const plot = usePlot();
15
+
13
16
  const resolved = $derived(Delta.build(marker.axis, data, marker));
14
17
  const label = $derived(getConfiguration().delta.text[marker.axis]);
15
18
  const lineAnchor = $derived(marker.text?.lineAnchor ?? label.lineAnchor);
@@ -54,13 +57,22 @@
54
57
  };
55
58
  });
56
59
 
57
- const dotStyles = $derived({
58
- dotRadius: 4,
59
- dotFill: marker.line?.stroke,
60
- dotStroke: marker.line?.stroke,
61
- dotStrokeWidth: 2,
62
- ...marker.dot
63
- });
60
+ /**
61
+ * The dot of one end: in the delta's own `stroke` when it sets one, else in the color of the series the end
62
+ * measures - an axis end has none, and takes the dot's default. `dot` wins over both.
63
+ */
64
+ function dotStyles(target: DeltaPointConfig['from'] | undefined) {
65
+ const color =
66
+ marker.stroke?.stroke ??
67
+ (target?.type === 'series' ? plot.colorForSeries?.(target.series) : undefined);
68
+ return {
69
+ dotRadius: 4,
70
+ dotFill: color,
71
+ dotStroke: color,
72
+ dotStrokeWidth: 2,
73
+ ...marker.dot
74
+ };
75
+ }
64
76
  </script>
65
77
 
66
78
  {#if coords}
@@ -68,14 +80,18 @@
68
80
  data={coords.lineData}
69
81
  x={(p) => p.x}
70
82
  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'}
83
+ stroke={marker.stroke?.stroke ?? 'currentColor'}
84
+ strokeWidth={marker.stroke?.strokeWidth ?? 2}
85
+ strokeOpacity={marker.stroke?.strokeOpacity ?? 0.8}
86
+ strokeDasharray={marker.stroke?.strokeDasharray ?? '6 6'}
87
+ strokeLinecap={marker.stroke?.strokeLinecap ?? 'round'}
88
+ />
89
+ <DotMarker
90
+ marker={{ type: 'dot', x: coords.fromPt.x, y: coords.fromPt.y, styles: dotStyles(marker.from) }}
91
+ />
92
+ <DotMarker
93
+ marker={{ type: 'dot', x: coords.toPt.x, y: coords.toPt.y, styles: dotStyles(marker.to) }}
76
94
  />
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
95
  <TextMarker
80
96
  marker={{
81
97
  type: 'text',
@@ -86,7 +102,7 @@
86
102
  fontWeight: 'bold',
87
103
  fontSize: 14,
88
104
  textAnchor: label.textAnchor,
89
- fill: marker.line?.stroke ?? 'currentColor',
105
+ fill: marker.stroke?.stroke ?? 'currentColor',
90
106
  ...marker.text,
91
107
  dx: marker.text?.dx ?? label.dx,
92
108
  dy: marker.text?.dy ?? label.dy,
@@ -89,11 +89,27 @@ export type DiscreteInteractiveConfig = {
89
89
  /** Shows the symbol icon inside a `'badge'` pill. Defaults to `true`; ignored by `'basic'`, which always shows it. */
90
90
  showIcon?: boolean;
91
91
  };
92
+ /**
93
+ * How a discrete section rewrites the label of each of its items, to add a unit or shorten a word without
94
+ * writing the items again. `replace` goes first, then `prefix` and `suffix` go around the result.
95
+ */
96
+ export type LegendLabels = {
97
+ prefix?: string;
98
+ suffix?: string;
99
+ /**
100
+ * What to swap in every label, in order: a text for the text that replaces it (every match), or a list of
101
+ * `[pattern, replacement]` pairs where the pattern may be a regular expression - `{ Quintile: 'Q' }`,
102
+ * `[[/^Quartile /, 'Q: ']]`.
103
+ */
104
+ replace?: Record<string, string> | [string | RegExp, string][];
105
+ };
92
106
  export type DiscreteLegendSection = {
93
107
  type: 'discrete';
94
108
  enabled?: boolean;
95
109
  title?: string;
96
110
  items: LegendItem[];
111
+ /** Rewrites the label of each item - see {@link LegendLabels}. */
112
+ labels?: LegendLabels;
97
113
  columns?: number;
98
114
  /** Item flow direction. Defaults to horizontal for a lone section, vertical once there's more than one. */
99
115
  layout?: LegendItemsLayout;
@@ -15,7 +15,7 @@ export type DeltaConfig = {
15
15
  from: DeltaTarget;
16
16
  to?: DeltaTarget;
17
17
  formatDiff?: (diff: number) => string;
18
- line?: StrokeStyle;
18
+ stroke?: StrokeStyle;
19
19
  text?: FontStyle;
20
20
  dot?: DotStyle;
21
21
  };
@@ -29,12 +29,12 @@ export type DeltaPointConfig = {
29
29
  export type PerPosition<T> = T | ((at: AxisValue) => T);
30
30
  /**
31
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
32
+ * drawn at each, with the same ends. `stroke`, `text` and `dot` can be functions of the position, to style
33
33
  * each delta on its own - a label to the left of the last one, say, so it stays inside the plot.
34
34
  */
35
- export type DeltaMarkerConfig = Omit<DeltaPointConfig, 'at' | 'line' | 'text' | 'dot'> & {
35
+ export type DeltaMarkerConfig = Omit<DeltaPointConfig, 'at' | 'stroke' | 'text' | 'dot'> & {
36
36
  at: AxisValue | AxisValue[];
37
- line?: PerPosition<StrokeStyle>;
37
+ stroke?: PerPosition<StrokeStyle>;
38
38
  text?: PerPosition<FontStyle>;
39
39
  dot?: PerPosition<DotStyle>;
40
40
  };
package/docs/legend.md CHANGED
@@ -118,6 +118,18 @@ legend={{ color: { match: 'temperature', enabled: false } }} // not that ramp
118
118
  legend={{ removeItems: ['2017/18'] }}
119
119
  ```
120
120
 
121
+ ### Rewriting the labels
122
+
123
+ `labels` on a discrete section rewrites the label of every item without writing the items again - to add a unit or shorten a word. Each is optional; `replace` goes first, then `prefix` and `suffix` go around the result. An item is rewritten from the text it would show: its `label`, else its name as the legend writes it.
124
+
125
+ ```ts
126
+ legend={{ series: { labels: { suffix: ' years' } } }} // 25-29 → 25-29 years
127
+ legend={{ series: { labels: { replace: { Quintile: 'Q' }, suffix: '%' } } }} // Quintile 1 → Q 1%
128
+ legend={{ series: { labels: { replace: [[/^Quartile /, 'Q: ']] } } }} // Quartile 3 → Q: 3 (a pattern, in order)
129
+ ```
130
+
131
+ `replace` is a record of text → replacement (every match; a text with spaces or dashes is a quoted key, `{ 'Quintile ': 'Q: ' }`), or a list of `[pattern, replacement]` pairs where the pattern may be a regular expression. The names are left as they are, so `off` and the toggles still go by them. An explicit section takes `labels` too.
132
+
121
133
  ### Turning series off from code
122
134
 
123
135
  `off` turns series off as if the reader had clicked them, without a click: a list of names (each dimmed, to `legend.disabledOpacity`), or a name → opacity or name → action map, where an action is the same `{ mode: 'dim' | 'hide' | 'omit' }` a toggle does. `highlight` is its inverse: it keeps the series it names on and dims all the others. An `off` entry for a series wins over `highlight`.
package/docs/markers.md CHANGED
@@ -102,7 +102,7 @@ markers={[
102
102
  ]}
103
103
  ```
104
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:
105
+ A delta's `stroke`, `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
106
 
107
107
  ```ts
108
108
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Declarative charts, maps and small multiples for Svelte, built on svelteplot.",
5
5
  "license": "MIT",
6
6
  "author": "Ezequiel Puerta",