@fundar/data-chart-telling 1.13.0 → 1.15.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 +21 -0
- package/dist/layout/legend/AxisNumberFormat.d.ts +10 -1
- package/dist/layout/legend/AxisNumberFormat.js +25 -0
- package/dist/layout/legend/ContinuousSection.svelte +1 -1
- package/dist/layout/legend/SizeSection.svelte +1 -1
- package/dist/layout/styles/ValueLabels.d.ts +5 -2
- package/dist/layout/styles/ValueLabels.js +24 -6
- package/dist/markers/hover/HoverMarker.svelte +16 -1
- package/dist/markers/hover/HoverMarker.svelte.d.ts +1 -1
- package/dist/plots/ranking/bar/defaults.js +17 -7
- package/dist/plots/ranking/bar/markers.js +50 -51
- package/dist/plots/utils/segments.js +28 -5
- package/dist/types/layout/legend.d.ts +11 -5
- package/dist/types/layout/styles/values.d.ts +1 -1
- package/dist/types/markers/axis/delta.d.ts +2 -2
- package/dist/types/markers/axis/hover.d.ts +9 -3
- package/dist/types/plots/constants.d.ts +5 -0
- package/docs/legend.md +2 -2
- package/docs/markers.md +2 -2
- package/docs/styles-and-segments.md +13 -12
- package/docs/theming.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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.15.0] - 2026-11-04
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- A bar plot's value labels follow the bars that go below zero (or left of it): `'outside'` puts a label beyond its bar's tip on that side - to the left of a horizontal bar, under a vertical one - and the plot's margins make room for it. On a horizontal bar `'start'` is its left end and `'end'` its right one, both inside, and `'middle'` is its centre (it sat on the tip); a vertical bar's anchors are as they were.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- On a line, the piece that joins two runs of a series is drawn as the first area segment that covers both of its ends - so a dashed projection that starts at the last observed year dashes the piece leaving that year, even where a faded range that ends there owns the point. It was always the catch-all's stroke, which left a solid piece at the start of the projection. A piece no area covers both ends of keeps the catch-all's stroke.
|
|
14
|
+
|
|
15
|
+
## [1.14.0] - 2026-11-04
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- The `hover` marker takes `decimals`, `prefix`, `suffix` and `signDisplay` for its label, and its `format` gets a second argument, `number`, that writes a value as they ask. With `numbers.decimals` set, its label is written with that count.
|
|
20
|
+
- The `format` of `styles.values` gets `number` as its fourth argument, to write a value as the display fields ask while choosing which labels to show.
|
|
21
|
+
- The `continuous` and `size` legend sections (so `legend.color` too) take `decimals`, `prefix`, `suffix` and `signDisplay` for their values - without `decimals` they keep the axes' format, and `numbers.decimals` does not reach them. Their `format` gets `number` as a second argument, and can return a number (`v * 100` for a share kept as a ratio), which is written as those fields ask.
|
|
22
|
+
- A `delta`'s `formatDiff` can return a number: it is written as the marker's display fields ask.
|
|
23
|
+
|
|
5
24
|
## [1.13.0] - 2026-11-04
|
|
6
25
|
|
|
7
26
|
### Added
|
|
@@ -12,6 +31,7 @@ All notable changes to this project are documented here. The format follows [Kee
|
|
|
12
31
|
|
|
13
32
|
### Changed
|
|
14
33
|
|
|
34
|
+
- The `continuous` and `size` legend sections (so `legend.color` too) take `decimals`, `prefix`, `suffix` and `signDisplay` for their values - without `decimals` they keep the axes' format, and `numbers.decimals` does not reach them. Their `format` gets `number` as a second argument, and can return a number (`v * 100` for a share kept as a ratio), which is written as those fields ask.
|
|
15
35
|
- A `delta` writes its distance in `axis.locale` (`12,3`), not with a dot (`12.3`), and with the configuration's `numbers.decimals` when set. One decimal remains the default.
|
|
16
36
|
|
|
17
37
|
## [1.12.0] - 2026-11-04
|
|
@@ -77,6 +97,7 @@ All notable changes to this project are documented here. The format follows [Kee
|
|
|
77
97
|
|
|
78
98
|
### Fixed
|
|
79
99
|
|
|
100
|
+
- The `continuous` and `size` legend sections (so `legend.color` too) take `decimals`, `prefix`, `suffix` and `signDisplay` for their values - without `decimals` they keep the axes' format, and `numbers.decimals` does not reach them. Their `format` gets `number` as a second argument, and can return a number (`v * 100` for a share kept as a ratio), which is written as those fields ask.
|
|
80
101
|
- A `delta` marker at a `Date` position no longer turns a date `x` axis into a numeric one: it drew its dots, line and label at the position as a number, which svelteplot read next to the dates of the data as a linear scale, and the axis showed milliseconds.
|
|
81
102
|
|
|
82
103
|
## [1.6.0] - 2026-11-02
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PlotStylesConfig } from '../../types/configuration/styling';
|
|
2
|
-
import
|
|
2
|
+
import { NumberFormat } from '../../utils/NumberFormat';
|
|
3
|
+
import type { NumberDisplay } from '../../types/layout/numbers';
|
|
3
4
|
/** Numbers as the plots' axes write them, by `axis.locale` and `axis.compactDisplay`. */
|
|
4
5
|
export declare class AxisNumberFormat {
|
|
5
6
|
/**
|
|
@@ -8,6 +9,14 @@ export declare class AxisNumberFormat {
|
|
|
8
9
|
* spans more than one order of magnitude, plain otherwise.
|
|
9
10
|
*/
|
|
10
11
|
static of(domain: [number, number], cfg: Pick<PlotStylesConfig, 'axis'>, signDisplay?: ReturnType<typeof NumberFormat.sign>): (value: number) => string;
|
|
12
|
+
/**
|
|
13
|
+
* How a legend section writes its values: as its `decimals`, `prefix`, `suffix` and `signDisplay` ask, else as the
|
|
14
|
+
* axes write theirs over `domain` - the configuration's `numbers.decimals` does not reach it. Its own `format`
|
|
15
|
+
* writes by hand instead: it gets that writer as its second argument, and a number it returns is written by it.
|
|
16
|
+
*/
|
|
17
|
+
static legend(section: NumberDisplay & {
|
|
18
|
+
format?: (v: number, number: (v: number) => string) => string | number;
|
|
19
|
+
}, domain: [number, number], cfg: Pick<PlotStylesConfig, 'axis'>): (value: number) => string;
|
|
11
20
|
/** Whether ticks across `domain` read compact. */
|
|
12
21
|
static compact([min, max]: [number, number]): boolean;
|
|
13
22
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NumberFormat } from '../../utils/NumberFormat';
|
|
1
2
|
/** Numbers as the plots' axes write them, by `axis.locale` and `axis.compactDisplay`. */
|
|
2
3
|
export class AxisNumberFormat {
|
|
3
4
|
/**
|
|
@@ -14,6 +15,30 @@ export class AxisNumberFormat {
|
|
|
14
15
|
});
|
|
15
16
|
return (value) => format.format(value);
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* How a legend section writes its values: as its `decimals`, `prefix`, `suffix` and `signDisplay` ask, else as the
|
|
20
|
+
* axes write theirs over `domain` - the configuration's `numbers.decimals` does not reach it. Its own `format`
|
|
21
|
+
* writes by hand instead: it gets that writer as its second argument, and a number it returns is written by it.
|
|
22
|
+
*/
|
|
23
|
+
static legend(section, domain, cfg) {
|
|
24
|
+
const display = NumberFormat.displayOf(section);
|
|
25
|
+
const { prefix = '', suffix = '' } = display;
|
|
26
|
+
let number;
|
|
27
|
+
if (display.decimals !== undefined) {
|
|
28
|
+
number = NumberFormat.of(display, { ...cfg, numbers: {} });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const write = AxisNumberFormat.of(domain, cfg, NumberFormat.sign(display));
|
|
32
|
+
number = (value) => `${prefix}${write(value)}${suffix}`;
|
|
33
|
+
}
|
|
34
|
+
const { format } = section;
|
|
35
|
+
if (!format)
|
|
36
|
+
return number;
|
|
37
|
+
return (value) => {
|
|
38
|
+
const written = format(value, number);
|
|
39
|
+
return typeof written === 'number' ? number(written) : written;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
17
42
|
/** Whether ticks across `domain` read compact. */
|
|
18
43
|
static compact([min, max]) {
|
|
19
44
|
if (!Number.isFinite(min) || !Number.isFinite(max))
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
const checkerId = `dct-checker-${Math.random().toString(36).slice(2)}`;
|
|
14
14
|
const barWidth = $derived(section.width ?? 170);
|
|
15
15
|
|
|
16
|
-
const fmt = $derived(
|
|
16
|
+
const fmt = $derived(AxisNumberFormat.legend(section, [section.min, section.max], cfg));
|
|
17
17
|
const isInteractive = $derived(section.interactive?.enabled ?? false);
|
|
18
18
|
|
|
19
19
|
// How far the default handle bleeds past its 0/1 track position, so the
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
let { section }: { section: SizeLegendSection } = $props();
|
|
16
16
|
|
|
17
17
|
const cfg = $derived(getConfiguration());
|
|
18
|
-
const fmt = $derived(
|
|
18
|
+
const fmt = $derived(AxisNumberFormat.legend(section, section.domain, cfg));
|
|
19
19
|
/** A value's radius: `domain` onto `range` on a square-root scale. */
|
|
20
20
|
function radius(value: number): number {
|
|
21
21
|
const [d0, d1] = section.domain.map(Math.sqrt);
|
|
@@ -59,8 +59,11 @@ export declare class ValueLabels {
|
|
|
59
59
|
x?: number;
|
|
60
60
|
y?: number;
|
|
61
61
|
}, edges: MarginSide[]): EdgeBox;
|
|
62
|
-
/**
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Each series' value farthest from zero, read on `valueAxis` - the widest label it can show. With `'both'`,
|
|
64
|
+
* the farthest above zero and the farthest below it, whichever the series has.
|
|
65
|
+
*/
|
|
66
|
+
extremes<S extends Series<any>>(series: S[], valueAxis?: 'x' | 'y', sides?: 'farthest' | 'both'): {
|
|
64
67
|
series: S;
|
|
65
68
|
value: number;
|
|
66
69
|
}[];
|
|
@@ -34,8 +34,11 @@ export class ValueLabels {
|
|
|
34
34
|
// The value as the display fields ask (else as it is), which a plot kind's own text builds on.
|
|
35
35
|
const number = NumberFormat.maybe(NumberFormat.displayOf(values), cfg) ?? ((v) => `${v}`);
|
|
36
36
|
const kindFormat = options.format;
|
|
37
|
-
this.format =
|
|
38
|
-
|
|
37
|
+
this.format = values?.format
|
|
38
|
+
? (v, name, row) => values.format(v, name, row, number)
|
|
39
|
+
: kindFormat
|
|
40
|
+
? (v, name) => kindFormat(v, name, number)
|
|
41
|
+
: (v) => number(v);
|
|
39
42
|
this.hoverIsolate = values?.hoverIsolate ?? false;
|
|
40
43
|
this.font = values?.fontStyle;
|
|
41
44
|
this.fontSize = typeof this.font?.fontSize === 'number' ? this.font.fontSize : 12;
|
|
@@ -85,21 +88,36 @@ export class ValueLabels {
|
|
|
85
88
|
const box = this.textBox(text).beside({ x: at.x ?? 0, y: at.y ?? 0 }, side);
|
|
86
89
|
return { box, edges };
|
|
87
90
|
}
|
|
88
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Each series' value farthest from zero, read on `valueAxis` - the widest label it can show. With `'both'`,
|
|
93
|
+
* the farthest above zero and the farthest below it, whichever the series has.
|
|
94
|
+
*/
|
|
89
95
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
-
extremes(series, valueAxis = 'y') {
|
|
96
|
+
extremes(series, valueAxis = 'y', sides = 'farthest') {
|
|
91
97
|
return series.flatMap((s) => {
|
|
92
98
|
const accessor = s[valueAxis];
|
|
93
99
|
if (accessor === undefined)
|
|
94
100
|
return [];
|
|
95
101
|
const y = resolveAccessor(accessor);
|
|
96
102
|
let extreme;
|
|
103
|
+
let lowest;
|
|
104
|
+
let highest;
|
|
97
105
|
for (const d of s.data) {
|
|
98
106
|
const v = Number(y(d));
|
|
99
|
-
if (
|
|
107
|
+
if (Number.isNaN(v))
|
|
108
|
+
continue;
|
|
109
|
+
if (extreme === undefined || Math.abs(v) > Math.abs(extreme))
|
|
100
110
|
extreme = v;
|
|
111
|
+
if (v < 0 && (lowest === undefined || v < lowest))
|
|
112
|
+
lowest = v;
|
|
113
|
+
if (v >= 0 && (highest === undefined || v > highest))
|
|
114
|
+
highest = v;
|
|
101
115
|
}
|
|
102
|
-
|
|
116
|
+
if (extreme === undefined)
|
|
117
|
+
return [];
|
|
118
|
+
if (sides === 'farthest')
|
|
119
|
+
return [{ series: s, value: extreme }];
|
|
120
|
+
return [lowest, highest].flatMap((value) => value === undefined ? [] : [{ series: s, value }]);
|
|
103
121
|
});
|
|
104
122
|
}
|
|
105
123
|
/**
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import RuleMarker from '../rule/RuleMarker.svelte';
|
|
6
6
|
import BarMarker from '../bar/BarMarker.svelte';
|
|
7
7
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
8
|
+
import { NumberFormat } from '../../utils/NumberFormat';
|
|
8
9
|
import { BandNudge } from '../utils/BandNudge';
|
|
9
10
|
import type { BandSlice } from '../../types/layout/hover';
|
|
10
11
|
import type { HoverMarkerConfig } from '../../types/markers/common';
|
|
@@ -47,6 +48,10 @@
|
|
|
47
48
|
| 'area'
|
|
48
49
|
| 'dot'
|
|
49
50
|
| 'text'
|
|
51
|
+
| 'decimals'
|
|
52
|
+
| 'prefix'
|
|
53
|
+
| 'suffix'
|
|
54
|
+
| 'signDisplay'
|
|
50
55
|
>;
|
|
51
56
|
activePoint?: HoverDisplayPoint | null;
|
|
52
57
|
impliesCrosshairX?: boolean;
|
|
@@ -111,6 +116,16 @@
|
|
|
111
116
|
const areaY = $derived(marker.areaY === true ? (activePoint?.y ?? null) : null);
|
|
112
117
|
const showLabels = $derived(marker.showLabels ?? true);
|
|
113
118
|
const format = $derived(marker.format);
|
|
119
|
+
/** The hovered value as the marker's display fields ask; `undefined` while nothing does, so the plot's own label stays. */
|
|
120
|
+
const numberOf = $derived(NumberFormat.maybe(NumberFormat.displayOf(marker), cfg));
|
|
121
|
+
const number = $derived(numberOf ?? NumberFormat.of(undefined, cfg));
|
|
122
|
+
const labelOf = (d: HoverDisplayPoint): string => {
|
|
123
|
+
if (format) return format(d, number);
|
|
124
|
+
const value = Number(d.label);
|
|
125
|
+
return numberOf && d.label !== undefined && d.label !== '' && Number.isFinite(value)
|
|
126
|
+
? numberOf(value)
|
|
127
|
+
: (d.label ?? '');
|
|
128
|
+
};
|
|
114
129
|
const ruleStyle = $derived(marker.rule);
|
|
115
130
|
const areaStyle = $derived({ ...cfg.hover.area, ...marker.area });
|
|
116
131
|
const dotStyle = $derived(marker.dot);
|
|
@@ -242,7 +257,7 @@
|
|
|
242
257
|
data: [point],
|
|
243
258
|
x: (d: HoverDisplayPoint) => d.x,
|
|
244
259
|
y: (d: HoverDisplayPoint) => d.y,
|
|
245
|
-
text:
|
|
260
|
+
text: labelOf,
|
|
246
261
|
styles: resolveFontStyle(point)
|
|
247
262
|
}}
|
|
248
263
|
/>
|
|
@@ -25,7 +25,7 @@ import type { HoverDisplayPoint } from '../../types/layout/hover';
|
|
|
25
25
|
interface Props {
|
|
26
26
|
data?: HoverDisplayPoint[];
|
|
27
27
|
/** Narrowed to just the fields this component reads - `HoverMarkerConfig` also carries routing/scoping fields (`series`/`scope`/`strategy`/`sync`) it has no use for. */
|
|
28
|
-
marker: Pick<HoverMarkerConfig, 'ruleX' | 'ruleY' | 'areaX' | 'areaY' | 'showLabels' | 'format' | 'rule' | 'area' | 'dot' | 'text'>;
|
|
28
|
+
marker: Pick<HoverMarkerConfig, 'ruleX' | 'ruleY' | 'areaX' | 'areaY' | 'showLabels' | 'format' | 'rule' | 'area' | 'dot' | 'text' | 'decimals' | 'prefix' | 'suffix' | 'signDisplay'>;
|
|
29
29
|
activePoint?: HoverDisplayPoint | null;
|
|
30
30
|
impliesCrosshairX?: boolean;
|
|
31
31
|
impliesCrosshairY?: boolean;
|
|
@@ -68,15 +68,25 @@ export function buildBarDefaults() {
|
|
|
68
68
|
if (ProcessedPositions.span(state.processedData, axis).stacked)
|
|
69
69
|
return [];
|
|
70
70
|
if (!state.axes?.isHorizontal) {
|
|
71
|
-
const
|
|
72
|
-
|
|
71
|
+
const offset = labels.offset('y');
|
|
72
|
+
const boxes = [
|
|
73
|
+
labels.edgeBox('0', { x: 'middle', y: 'before' }, { y: -offset }, ['top'])
|
|
74
|
+
];
|
|
75
|
+
// A bar below zero has its label under its tip.
|
|
76
|
+
const hasNegative = labels
|
|
77
|
+
.extremes(state.resolvedSeries ?? [], PlotAxes.valueOf(state), 'both')
|
|
78
|
+
.some(({ value }) => value < 0);
|
|
79
|
+
if (hasNegative)
|
|
80
|
+
boxes.push(labels.edgeBox('0', { x: 'middle', y: 'after' }, { y: offset }, ['bottom']));
|
|
81
|
+
return boxes;
|
|
73
82
|
}
|
|
74
|
-
|
|
83
|
+
// A bar below zero reaches out to the left, with its label turned that way.
|
|
84
|
+
const offset = labels.offset('x');
|
|
75
85
|
return labels
|
|
76
|
-
.extremes(state.resolvedSeries ?? [], PlotAxes.valueOf(state))
|
|
77
|
-
.map(({ series, value }) =>
|
|
78
|
-
'
|
|
79
|
-
|
|
86
|
+
.extremes(state.resolvedSeries ?? [], PlotAxes.valueOf(state), 'both')
|
|
87
|
+
.map(({ series, value }) => value < 0
|
|
88
|
+
? labels.edgeBox(labels.format(value, series.name), { x: 'before', y: 'middle' }, { x: -offset }, ['left'])
|
|
89
|
+
: labels.edgeBox(labels.format(value, series.name), { x: 'after', y: 'middle' }, { x: offset }, ['right']));
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
},
|
|
@@ -82,60 +82,59 @@ export function buildValueLabels(labels, state, ctx) {
|
|
|
82
82
|
const geometry = buildBarGeometry(state, ctx.cfg.bar.seriesPadding);
|
|
83
83
|
const { font, anchor, format } = labels;
|
|
84
84
|
const valueOf = ValueLabels.valueOf(state);
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(
|
|
100
|
-
?
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
: 'middle'
|
|
105
|
-
: 'middle');
|
|
106
|
-
const lineAnchor = font?.lineAnchor ??
|
|
107
|
-
(isHorizontal
|
|
108
|
-
? 'middle'
|
|
109
|
-
: anchor === 'outside' || anchor === 'end'
|
|
110
|
-
? 'bottom'
|
|
111
|
-
: anchor === 'start'
|
|
112
|
-
? 'top'
|
|
113
|
-
: 'middle');
|
|
85
|
+
const spanOf = (d) => geometry.spanOf(d) ?? [0, 0];
|
|
86
|
+
const lowOf = (d) => Math.min(...spanOf(d));
|
|
87
|
+
const highOf = (d) => Math.max(...spanOf(d));
|
|
88
|
+
const below = (d) => Number(valueOf(d)) < 0;
|
|
89
|
+
const place = (negative) => {
|
|
90
|
+
const mid = (d) => (lowOf(d) + highOf(d)) / 2;
|
|
91
|
+
if (geometry.stacked)
|
|
92
|
+
return { at: mid, side: 0 };
|
|
93
|
+
if (anchor === 'middle')
|
|
94
|
+
return { at: mid, side: 0 };
|
|
95
|
+
if (anchor === 'outside')
|
|
96
|
+
return isHorizontal
|
|
97
|
+
? { at: negative ? lowOf : highOf, side: negative ? -1 : 1 }
|
|
98
|
+
: { at: negative ? lowOf : highOf, side: negative ? 1 : -1 };
|
|
99
|
+
if (anchor === 'start')
|
|
100
|
+
return isHorizontal ? { at: lowOf, side: 1 } : { at: highOf, side: 1 };
|
|
101
|
+
return isHorizontal ? { at: highOf, side: -1 } : { at: lowOf, side: -1 };
|
|
102
|
+
};
|
|
103
|
+
const gap = anchor === 'outside' ? 6 : 4;
|
|
114
104
|
const labelled = processedData.data.filter((d) => d[GAP_VALUE] === undefined && geometry.spanOf(d) !== undefined);
|
|
115
105
|
return (state.resolvedSeries ?? []).flatMap((s) => {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return [
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
106
|
+
const own = labelled.filter((d) => d[SERIES_ID_KEY] === seriesIdOf(s));
|
|
107
|
+
// Only a label beyond the tip turns with the sign of its value.
|
|
108
|
+
const turns = !geometry.stacked && anchor === 'outside';
|
|
109
|
+
return (turns ? [false, true] : [false]).flatMap((negative) => {
|
|
110
|
+
const data = turns ? own.filter((d) => below(d) === negative) : own;
|
|
111
|
+
if (data.length === 0)
|
|
112
|
+
return [];
|
|
113
|
+
const { at, side } = place(negative);
|
|
114
|
+
// A horizontal label reads away from its point to `side`; a vertical one sits on the far side of it.
|
|
115
|
+
const textAnchor = labels.textAnchor ??
|
|
116
|
+
(isHorizontal ? (side === 1 ? 'start' : side === -1 ? 'end' : 'middle') : 'middle');
|
|
117
|
+
const lineAnchor = font?.lineAnchor ??
|
|
118
|
+
(isHorizontal ? 'middle' : side === 1 ? 'top' : side === -1 ? 'bottom' : 'middle');
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
type: 'text',
|
|
122
|
+
data,
|
|
123
|
+
...(isHorizontal ? { x: at, y: geometry.categoryOf } : { x: geometry.categoryOf, y: at }),
|
|
124
|
+
bandSlice: geometry.sliceOf(s.name),
|
|
125
|
+
text: (d) => format(Number(valueOf(d)), s.name, d),
|
|
126
|
+
styles: {
|
|
127
|
+
...font,
|
|
128
|
+
fill: font?.fill ?? ctx.cfg.axis.color,
|
|
129
|
+
fontSize: font?.fontSize ?? 12,
|
|
130
|
+
dx: font?.dx ?? (isHorizontal ? side * gap : 0),
|
|
131
|
+
dy: font?.dy ?? (isHorizontal ? 0 : side * gap),
|
|
132
|
+
textAnchor,
|
|
133
|
+
lineAnchor
|
|
134
|
+
}
|
|
136
135
|
}
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
];
|
|
137
|
+
});
|
|
139
138
|
});
|
|
140
139
|
}
|
|
141
140
|
/**
|
|
@@ -128,11 +128,34 @@ function resolveSeriesSegments(series, segmentsOfSeries, connect, base) {
|
|
|
128
128
|
// the default palette color when the user has set a baseline via catch-all.
|
|
129
129
|
// Dots are excluded to avoid duplicate marks at the shared boundary points.
|
|
130
130
|
const catchAll = segs.find((s) => !s.areas || s.areas.length === 0);
|
|
131
|
-
const
|
|
132
|
-
const rest = { ...
|
|
131
|
+
const withoutDots = (style) => {
|
|
132
|
+
const rest = { ...style };
|
|
133
133
|
delete rest.dot;
|
|
134
134
|
return rest;
|
|
135
|
-
}
|
|
135
|
+
};
|
|
136
|
+
const connectorStyle = withoutDots((catchAll?.styles ?? baseStyle));
|
|
137
|
+
// A segment's style as its own run is drawn with: an area segment over the catch-all's.
|
|
138
|
+
const styleOf = (idx) => {
|
|
139
|
+
const seg = segs[idx];
|
|
140
|
+
const own = seg.styles ?? {};
|
|
141
|
+
const isAreaSpecific = seg.areas != null && seg.areas.length > 0;
|
|
142
|
+
return isAreaSpecific && catchAll?.styles
|
|
143
|
+
? merge(catchAll.styles, own)
|
|
144
|
+
: own;
|
|
145
|
+
};
|
|
146
|
+
// The line that joins two runs is drawn as the first area segment that covers both of its ends - a dashed
|
|
147
|
+
// projection that starts at the last observed year also dashes the piece leaving it, even where a faded
|
|
148
|
+
// range that ends there owns that year's point. Where none does, it is the catch-all's.
|
|
149
|
+
const covers = (idx, row) => {
|
|
150
|
+
const areas = segs[idx].areas;
|
|
151
|
+
return (areas != null &&
|
|
152
|
+
areas.length > 0 &&
|
|
153
|
+
areas.some((area) => areaMatchesXY(getX(row), getY(row), area)));
|
|
154
|
+
};
|
|
155
|
+
const joining = (from, to) => {
|
|
156
|
+
const covering = evalOrder.find(({ i }) => covers(i, from) && covers(i, to));
|
|
157
|
+
return covering ? withoutDots(styleOf(covering.i)) : connectorStyle;
|
|
158
|
+
};
|
|
136
159
|
const defaultVisual = [];
|
|
137
160
|
const connectors = [];
|
|
138
161
|
const userVisual = [];
|
|
@@ -161,7 +184,7 @@ function resolveSeriesSegments(series, segmentsOfSeries, connect, base) {
|
|
|
161
184
|
if (r < runs.length - 1) {
|
|
162
185
|
connectors.push({
|
|
163
186
|
data: [last, runs[r + 1].data[0]],
|
|
164
|
-
styles:
|
|
187
|
+
styles: joining(last, runs[r + 1].data[0]),
|
|
165
188
|
scales: undefined
|
|
166
189
|
});
|
|
167
190
|
}
|
|
@@ -188,7 +211,7 @@ function resolveSeriesSegments(series, segmentsOfSeries, connect, base) {
|
|
|
188
211
|
if (connect && nextRun && nextRun.segIdx !== -1) {
|
|
189
212
|
connectors.push({
|
|
190
213
|
data: [run.data[run.data.length - 1], nextRun.data[0]],
|
|
191
|
-
styles:
|
|
214
|
+
styles: joining(run.data[run.data.length - 1], nextRun.data[0]),
|
|
192
215
|
scales: undefined
|
|
193
216
|
});
|
|
194
217
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { SymbolType, LineStyle } from '../plots/constants';
|
|
3
3
|
import type { StrokeStyle, DotStyle } from '../plots/styling';
|
|
4
|
+
import type { NumberDisplay } from './numbers';
|
|
4
5
|
import type { ColorScaleConfig, OpacityScaleConfig, SymbolScaleConfig } from './scales';
|
|
5
6
|
export type LegendSectionType = 'discrete' | 'continuous' | 'size' | 'symbol';
|
|
6
7
|
/**
|
|
@@ -127,7 +128,11 @@ export type ContinuousInteractiveConfig = {
|
|
|
127
128
|
/** Custom renderer for the draggable lo/hi handles. */
|
|
128
129
|
handle?: LegendHandleSnippet;
|
|
129
130
|
};
|
|
130
|
-
|
|
131
|
+
/**
|
|
132
|
+
* A continuous color or opacity scale. Its values read as {@link NumberDisplay} asks - with `decimals`, exactly
|
|
133
|
+
* that many; without, as the axes write theirs - unless `format` writes them.
|
|
134
|
+
*/
|
|
135
|
+
export type ContinuousLegendSection = NumberDisplay & {
|
|
131
136
|
type: 'continuous';
|
|
132
137
|
enabled?: boolean;
|
|
133
138
|
title?: string;
|
|
@@ -146,7 +151,8 @@ export type ContinuousLegendSection = {
|
|
|
146
151
|
opacity?: [number, number];
|
|
147
152
|
ticks?: number[];
|
|
148
153
|
width?: number;
|
|
149
|
-
|
|
154
|
+
/** A value's text, in place of the value. A number it returns (`v * 100` for a share kept as a ratio) is written as the display fields above ask; a string is shown as it is. `number` writes a value that way by hand. */
|
|
155
|
+
format?: (v: number, number: (v: number) => string) => string | number;
|
|
150
156
|
interactive?: ContinuousInteractiveConfig;
|
|
151
157
|
};
|
|
152
158
|
/**
|
|
@@ -154,7 +160,7 @@ export type ContinuousLegendSection = {
|
|
|
154
160
|
* the radius each value draws at, its value under each. `domain` maps to
|
|
155
161
|
* `range` (px) on a square-root scale, so a dot's area follows its value.
|
|
156
162
|
*/
|
|
157
|
-
export type SizeLegendSection = {
|
|
163
|
+
export type SizeLegendSection = NumberDisplay & {
|
|
158
164
|
type: 'size';
|
|
159
165
|
enabled?: boolean;
|
|
160
166
|
title?: string;
|
|
@@ -171,8 +177,8 @@ export type SizeLegendSection = {
|
|
|
171
177
|
filled?: boolean;
|
|
172
178
|
/** The dots' shape. Defaults to `'circle'`. */
|
|
173
179
|
symbol?: SymbolType;
|
|
174
|
-
/** How each value reads
|
|
175
|
-
format?: (v: number) => string;
|
|
180
|
+
/** How each value reads, in place of the display fields' format (which defaults to the axes' own: `axis.locale`, `axis.compactDisplay`). A number it returns is written as those fields ask; `number` does the same by hand. */
|
|
181
|
+
format?: (v: number, number: (v: number) => string) => string | number;
|
|
176
182
|
};
|
|
177
183
|
/**
|
|
178
184
|
* A categorical shape scale - the `symbol` channel of a plot's dots: each
|
|
@@ -39,7 +39,7 @@ export type ValuesStyle = NumberDisplay & {
|
|
|
39
39
|
*/
|
|
40
40
|
color?: 'series' | (string & {});
|
|
41
41
|
/** A label's text, in place of the value as `decimals`, `prefix`, `suffix` and `signDisplay` write it, from its row's own value (a histogram bin's count) and its series - and, on the plots that label one row each, that row, for the other fields of the data. */
|
|
42
|
-
format?: (value: number, seriesName: string, row?: Record<string, unknown
|
|
42
|
+
format?: (value: number, seriesName: string, row?: Record<string, unknown>, number?: (value: number) => string) => string;
|
|
43
43
|
/** Hovering a label dims every other series, like toggling them off in the legend. */
|
|
44
44
|
hoverIsolate?: boolean;
|
|
45
45
|
fontStyle?: FontStyle;
|
|
@@ -18,8 +18,8 @@ export type DeltaConfig = NumberDisplay & {
|
|
|
18
18
|
at: AxisValue;
|
|
19
19
|
from: DeltaTarget;
|
|
20
20
|
to?: DeltaTarget;
|
|
21
|
-
/** The distance's text, in place of the number as the display fields above
|
|
22
|
-
formatDiff?: (diff: number) => string;
|
|
21
|
+
/** The distance's text, in place of the distance. A number it returns is written as the display fields above ask (a percentage of a baseline, say); a string is shown as it is. */
|
|
22
|
+
formatDiff?: (diff: number) => string | number;
|
|
23
23
|
stroke?: StrokeStyle;
|
|
24
24
|
text?: FontStyle;
|
|
25
25
|
dot?: DotStyle;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { AreaStyle, DotStyle, FontStyle, StrokeStyle } from '../../plots/styling';
|
|
2
2
|
import type { HoverDisplayPoint, HoverStrategy } from '../../layout/hover';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import type { NumberDisplay } from '../../layout/numbers';
|
|
4
|
+
/**
|
|
5
|
+
* In-SVG hover highlight: crosshair, dot and label. The label is the hovered value as `decimals`, `prefix`,
|
|
6
|
+
* `suffix` and `signDisplay` write it - as the plot writes it while none of them (nor `numbers.decimals`) is set -
|
|
7
|
+
* unless `format` writes it.
|
|
8
|
+
*/
|
|
9
|
+
export type HoverMarkerConfig = NumberDisplay & {
|
|
5
10
|
type: 'hover';
|
|
6
11
|
series?: string;
|
|
7
12
|
scope?: 'series' | 'segment';
|
|
@@ -13,7 +18,8 @@ export type HoverMarkerConfig = {
|
|
|
13
18
|
areaX?: boolean;
|
|
14
19
|
areaY?: boolean;
|
|
15
20
|
showLabels?: boolean;
|
|
16
|
-
|
|
21
|
+
/** The label's text for a point, in place of the value. `number` writes a value as the display fields above ask. */
|
|
22
|
+
format?: (point: HoverDisplayPoint, number: (value: number) => string) => string;
|
|
17
23
|
rule?: StrokeStyle;
|
|
18
24
|
area?: AreaStyle;
|
|
19
25
|
dot?: DotStyle;
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* of a pyramid bar from zero, right of a line's last point).
|
|
9
9
|
* - `'middle'` - centred on the mark.
|
|
10
10
|
* - `'end'` - at the baseline/zero end of the mark.
|
|
11
|
+
*
|
|
12
|
+
* On a bar plot the anchors read as the bars are drawn, whatever the sign of the value: `'outside'` is
|
|
13
|
+
* beyond the tip (left of a horizontal bar below zero, under a vertical one), `'start'` the left end of a
|
|
14
|
+
* horizontal bar or the top end of a vertical one, `'end'` the right or bottom end, both inside, and
|
|
15
|
+
* `'middle'` the centre.
|
|
11
16
|
*/
|
|
12
17
|
export type ValueAnchor = 'outside' | 'start' | 'middle' | 'end';
|
|
13
18
|
/**
|
package/docs/legend.md
CHANGED
|
@@ -187,8 +187,8 @@ A list replaces the automatic legend with exactly those sections:
|
|
|
187
187
|
Section types:
|
|
188
188
|
|
|
189
189
|
- `discrete` - `items` (`name`, `label`, `color`, `symbol`, `filled`, `opacity`, `lineStyle`), `columns`, `layout` (`{ type: 'vertical' }`, `{ type: 'horizontal', maxRows }` paginated, `{ type: 'flex' }`), `interactive`, `navButton`.
|
|
190
|
-
- `continuous` - `min`, `max`, `colorMin`, `colorMax`, `scheme`, `pivot`, `opacity`, `ticks`, `width`, `format`, `interactive`.
|
|
191
|
-
- `size` - `domain`, `range`, `values`, `color`, `filled`, `symbol`, `format`.
|
|
190
|
+
- `continuous` - `min`, `max`, `colorMin`, `colorMax`, `scheme`, `pivot`, `opacity`, `ticks`, `width`, `decimals`, `prefix`, `suffix`, `signDisplay`, `format`, `interactive`.
|
|
191
|
+
- `size` - `domain`, `range`, `values`, `color`, `filled`, `symbol`, `decimals`, `prefix`, `suffix`, `signDisplay`, `format`.
|
|
192
192
|
- `symbol` - `items` of `{ name, symbol }`, `color`, `filled`, `layout`.
|
|
193
193
|
|
|
194
194
|
## Placement and look
|
package/docs/markers.md
CHANGED
|
@@ -48,7 +48,7 @@ A `text` marker's `text` is a string, a number, the name of a field of its rows,
|
|
|
48
48
|
{ type: 'text', data: rows, x: 'year', y: 'share', text: 'share', decimals: 1, suffix: ' %' }
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
A `delta` writes its distance the same way, with one decimal unless `decimals` or `numbers.decimals` says otherwise. `formatDiff` writes it by hand instead.
|
|
51
|
+
A `delta` writes its distance the same way, with one decimal unless `decimals` or `numbers.decimals` says otherwise. `formatDiff` writes it by hand instead: a string it returns is shown as it is, and a number (a percentage of a baseline, say) is written as the display fields ask.
|
|
52
52
|
|
|
53
53
|
## Fields every marker shares
|
|
54
54
|
|
|
@@ -125,7 +125,7 @@ Every other marker draws from `data` rows, one mark per row, so it already takes
|
|
|
125
125
|
|
|
126
126
|
## Hover highlight
|
|
127
127
|
|
|
128
|
-
`{ type: 'hover' }` draws a crosshair, a dot and a label at the hovered point. It makes the plot hoverable on its own; add `tooltip` for a floating box too. Fields: `strategy`, `sync`, `ruleX`, `ruleY`, `areaX`, `areaY` (a band highlight on band scales), `showLabels`, `format`, and the styles `rule`, `area`, `dot`, `text`. See [Tooltip and hover](tooltip-and-hover.md).
|
|
128
|
+
`{ type: 'hover' }` draws a crosshair, a dot and a label at the hovered point. It makes the plot hoverable on its own; add `tooltip` for a floating box too. Fields: `strategy`, `sync`, `ruleX`, `ruleY`, `areaX`, `areaY` (a band highlight on band scales), `showLabels`, `format`, the number fields `decimals`, `prefix`, `suffix` and `signDisplay` (they write the label's value; `format` gets a `number` function that does the same), and the styles `rule`, `area`, `dot`, `text`. See [Tooltip and hover](tooltip-and-hover.md).
|
|
129
129
|
|
|
130
130
|
## Custom components
|
|
131
131
|
|
|
@@ -46,18 +46,18 @@
|
|
|
46
46
|
|
|
47
47
|

|
|
48
48
|
|
|
49
|
-
| Field | Meaning
|
|
50
|
-
| --------------------------------------------- |
|
|
51
|
-
| `show` | Draws the labels. Off by default, except on a heatmap
|
|
52
|
-
| `anchor` | `'outside'`, `'start'`, `'middle'`, `'end'` - where a label sits on its mark
|
|
53
|
-
| `select` | Which row of each group is labelled: `'first'`, `'last'`, `'min'`, `'max'`, or a list of them. A group is a series on a line or area plot (default `'last'`), a category on the plots that join dots (default: every dot)
|
|
54
|
-
| `series` | Labels only these series, by name
|
|
55
|
-
| `color` | `'series'` writes each label in its series' color; any other CSS color writes them all in it (`fontStyle.fill` wins)
|
|
56
|
-
| `decimals`, `prefix`, `suffix`, `signDisplay` | How the value reads, over `numbers.decimals` ([Number format](theming.md#number-format)); `format` replaces them
|
|
57
|
-
| `format` | `(value, seriesName, row) => string`; `row` is the labelled row, for its other fields
|
|
58
|
-
| `hoverIsolate` | Hovering a label dims every other series
|
|
59
|
-
| `fontStyle` | Text style: `fontSize`, `fill`, `dx`, `dy`, `textAnchor`, `lineAnchor`, `rotate`, `class`, ... Overrides the anchor's placement field by field
|
|
60
|
-
| `strokeStyle` | Style of the connector lines drawn from labels that had to move apart
|
|
49
|
+
| Field | Meaning |
|
|
50
|
+
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
51
|
+
| `show` | Draws the labels. Off by default, except on a heatmap |
|
|
52
|
+
| `anchor` | `'outside'`, `'start'`, `'middle'`, `'end'` - where a label sits on its mark. On a bar, `'outside'` is beyond its tip (on either side of zero), `'start'` / `'end'` its left / right end (top / bottom on a vertical bar), inside, and `'middle'` its centre |
|
|
53
|
+
| `select` | Which row of each group is labelled: `'first'`, `'last'`, `'min'`, `'max'`, or a list of them. A group is a series on a line or area plot (default `'last'`), a category on the plots that join dots (default: every dot) |
|
|
54
|
+
| `series` | Labels only these series, by name |
|
|
55
|
+
| `color` | `'series'` writes each label in its series' color; any other CSS color writes them all in it (`fontStyle.fill` wins) |
|
|
56
|
+
| `decimals`, `prefix`, `suffix`, `signDisplay` | How the value reads, over `numbers.decimals` ([Number format](theming.md#number-format)); `format` replaces them |
|
|
57
|
+
| `format` | `(value, seriesName, row) => string`; `row` is the labelled row, for its other fields |
|
|
58
|
+
| `hoverIsolate` | Hovering a label dims every other series |
|
|
59
|
+
| `fontStyle` | Text style: `fontSize`, `fill`, `dx`, `dy`, `textAnchor`, `lineAnchor`, `rotate`, `class`, ... Overrides the anchor's placement field by field |
|
|
60
|
+
| `strokeStyle` | Style of the connector lines drawn from labels that had to move apart |
|
|
61
61
|
|
|
62
62
|
Where the labels go depends on the plot: the end of each series on a line or area plot, one per bar on a bar, pyramid or histogram, one per point on a scatter, one per cell on a heatmap. On lines, overlapping labels spread apart, share one column and are joined to their points by elbow connectors; the right margin grows to fit them when it is `'auto'`. The spread is tuned by `text.declutter` in the [configuration](theming.md).
|
|
63
63
|
|
|
@@ -148,6 +148,7 @@ type Range = { from?: AxisValue; to?: AxisValue }; // inclusive on both ends
|
|
|
148
148
|
- A segment applies to a data point (or heatmap cell) inside **any** of its `areas`.
|
|
149
149
|
- An `Area` limits by `x`, `y` or both; leaving one out leaves that axis open.
|
|
150
150
|
- 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.
|
|
151
|
+
- On a line, the piece that joins two runs of a series is drawn as the first area segment that covers both of its ends (its stroke, not its dots): a dashed range that starts at a year dashes the piece leaving it, even where a faded range ending there owns that year's point. One no area covers both ends of is drawn as the catch-all.
|
|
151
152
|
- `'default'` applies to every series; a named key applies to that series only, and wins over `'default'` where both match.
|
|
152
153
|
- `legend` names the segment on the automatic [legend](legend.md).
|
|
153
154
|
- `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.
|
package/docs/theming.md
CHANGED
|
@@ -80,7 +80,7 @@ The same four fields set it where a plot needs something else, in `axis.locale`:
|
|
|
80
80
|
| `suffix` | Text after the number, spaces included: `' %'` |
|
|
81
81
|
| `signDisplay` | `'auto'` (default) writes only a minus, `'always'` also a plus on a positive number (never on zero), `'never'` no sign at all |
|
|
82
82
|
|
|
83
|
-
They are fields of `scales.x` and `scales.y` (the ticks), of `tooltip`, of `styles.values` (value labels), and of a `text` and a `delta` marker. A tooltip writes the value on the plot's value axis - the `x` of horizontal bars - and a heatmap's color value; a `text` marker writes a number that its `text` gives (see [Markers](markers.md#numbers-in-text-and-delta-markers)).
|
|
83
|
+
They are fields of `scales.x` and `scales.y` (the ticks), of `tooltip`, of `styles.values` (value labels), of a `hover` marker (its label), and of a `text` and a `delta` marker. A tooltip writes the value on the plot's value axis - the `x` of horizontal bars - and a heatmap's color value; a `text` marker writes a number that its `text` gives (see [Markers](markers.md#numbers-in-text-and-delta-markers)).
|
|
84
84
|
|
|
85
85
|
A `scales` axis keeps its automatic ticks (abbreviated when they span orders of magnitude) unless it sets `decimals`: `numbers.decimals` does not reach the ticks, since a fixed count would write `5,0` for `5` on every axis. `prefix` and `suffix` alone keep the automatic format. An axis with its own `tickFormat` or `percent` ignores them.
|
|
86
86
|
|