@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 +13 -0
- package/dist/layout/base/BasePlotLayout.svelte +2 -1
- package/dist/layout/figure/Figure.svelte +9 -5
- package/dist/layout/legend/LegendLabels.d.ts +12 -0
- package/dist/layout/legend/LegendLabels.js +35 -0
- package/dist/layout/plot/PlotContext.d.ts +3 -0
- package/dist/layout/plot/PlotContext.js +3 -0
- package/dist/layout/plot/PublicPlotContext.d.ts +2 -0
- package/dist/layout/plot/PublicPlotContext.js +4 -0
- package/dist/markers/delta/Delta.d.ts +1 -1
- package/dist/markers/delta/Delta.js +2 -2
- package/dist/markers/delta/DeltaMarker.svelte +1 -1
- package/dist/markers/delta/DeltaPoint.svelte +31 -15
- package/dist/types/layout/legend.d.ts +16 -0
- package/dist/types/markers/axis/delta.d.ts +4 -4
- package/docs/legend.md +12 -0
- package/docs/markers.md +1 -1
- package/package.json +1 -1
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
}
|
|
@@ -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
|
-
* `
|
|
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
|
-
* `
|
|
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
|
-
|
|
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. `
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
72
|
-
strokeWidth={marker.
|
|
73
|
-
strokeOpacity={marker.
|
|
74
|
-
strokeDasharray={marker.
|
|
75
|
-
strokeLinecap={marker.
|
|
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.
|
|
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
|
-
|
|
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. `
|
|
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' | '
|
|
35
|
+
export type DeltaMarkerConfig = Omit<DeltaPointConfig, 'at' | 'stroke' | 'text' | 'dot'> & {
|
|
36
36
|
at: AxisValue | AxisValue[];
|
|
37
|
-
|
|
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 `
|
|
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
|
{
|