@fundar/data-chart-telling 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +2 -1
- package/dist/configuration/config.svelte.js +12 -0
- package/dist/layout/facet/scope.d.ts +40 -0
- package/dist/layout/facet/scope.js +104 -0
- package/dist/layout/figure/Figure.svelte +5 -0
- package/dist/layout/legend/DiscreteSymbolIcon.svelte +38 -14
- package/dist/layout/legend/LegendState.svelte.d.ts +11 -0
- package/dist/layout/legend/LegendState.svelte.js +56 -6
- package/dist/layout/margins/PlotMargins.svelte.d.ts +9 -5
- package/dist/layout/margins/PlotMargins.svelte.js +65 -6
- package/dist/layout/plot/Plot.svelte +22 -2
- package/dist/layout/scales/ScalesLayout.svelte +21 -0
- package/dist/markers/delta/Delta.d.ts +6 -1
- package/dist/markers/delta/Delta.js +15 -0
- package/dist/markers/delta/DeltaMarker.svelte +9 -87
- package/dist/markers/delta/DeltaPoint.svelte +97 -0
- package/dist/markers/delta/DeltaPoint.svelte.d.ts +29 -0
- package/dist/markers/rule/RuleMarker.svelte +5 -2
- package/dist/phases/facet/FacetPhase.d.ts +2 -1
- package/dist/phases/facet/FacetPhase.js +19 -4
- package/dist/phases/statistics/StatisticsPhase.js +9 -1
- package/dist/plots/utils/segments.d.ts +4 -1
- package/dist/plots/utils/segments.js +19 -6
- package/dist/types/configuration/styling.d.ts +8 -0
- package/dist/types/layout/facet.d.ts +18 -0
- package/dist/types/layout/legend.d.ts +16 -0
- package/dist/types/layout/plot.d.ts +2 -0
- package/dist/types/markers/axis/delta.d.ts +15 -2
- package/dist/types/markers/axis/rule.d.ts +9 -2
- package/dist/types/phases/facet/phase.d.ts +4 -0
- package/dist/types/plots/props.d.ts +6 -3
- package/dist/types/plots/segments/config.d.ts +4 -2
- package/docs/facets.md +47 -0
- package/docs/legend.md +14 -0
- package/docs/markers.md +54 -26
- package/docs/styles-and-segments.md +1 -0
- package/docs/theming.md +1 -1
- package/examples/FacetsScoped.svelte +26 -0
- package/llms-full.txt +5 -3
- package/llms.txt +1 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Series } from '../../types/plots/data/common';
|
|
2
|
-
import type { DeltaConfig } from '../../types/markers/axis/delta';
|
|
2
|
+
import type { DeltaConfig, DeltaMarkerConfig, DeltaPointConfig } from '../../types/markers/axis/delta';
|
|
3
3
|
/** A delta, measured: where it sits (`at`), its two ends and what lies between. */
|
|
4
4
|
export type ResolvedDelta = {
|
|
5
5
|
cfg: DeltaConfig;
|
|
@@ -16,6 +16,11 @@ export type ResolvedDelta = {
|
|
|
16
16
|
*/
|
|
17
17
|
export declare class Delta {
|
|
18
18
|
#private;
|
|
19
|
+
/**
|
|
20
|
+
* The deltas a marker draws: one for its `at`, or one for each position of a list, each with the
|
|
21
|
+
* `line`, `text` and `dot` its style functions give for that position.
|
|
22
|
+
*/
|
|
23
|
+
static points(marker: DeltaMarkerConfig): DeltaPointConfig[];
|
|
19
24
|
/** The delta `config` asks for over `allSeries`, or `undefined` while an end has no value there. */
|
|
20
25
|
static build<T extends Record<string, unknown>>(axis: 'x' | 'y', allSeries: Series<T>[], config: DeltaConfig): ResolvedDelta | undefined;
|
|
21
26
|
}
|
|
@@ -5,6 +5,21 @@ import { resolveAccessor } from '../../utils/accessors';
|
|
|
5
5
|
* it crosses that position, interpolated linearly between its two nearest points.
|
|
6
6
|
*/
|
|
7
7
|
export class Delta {
|
|
8
|
+
/**
|
|
9
|
+
* The deltas a marker draws: one for its `at`, or one for each position of a list, each with the
|
|
10
|
+
* `line`, `text` and `dot` its style functions give for that position.
|
|
11
|
+
*/
|
|
12
|
+
static points(marker) {
|
|
13
|
+
const positions = Array.isArray(marker.at) ? marker.at : [marker.at];
|
|
14
|
+
const own = (style, at) => typeof style === 'function' ? style(at) : style;
|
|
15
|
+
return positions.map((at) => ({
|
|
16
|
+
...marker,
|
|
17
|
+
at,
|
|
18
|
+
line: own(marker.line, at),
|
|
19
|
+
text: own(marker.text, at),
|
|
20
|
+
dot: own(marker.dot, at)
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
8
23
|
/** The delta `config` asks for over `allSeries`, or `undefined` while an end has no value there. */
|
|
9
24
|
static build(axis, allSeries, config) {
|
|
10
25
|
const at = Number(config.at);
|
|
@@ -1,96 +1,18 @@
|
|
|
1
1
|
<script lang="ts" generics="TData extends Record<string, unknown>">
|
|
2
|
-
import
|
|
3
|
-
import DotMarker from '../dot/DotMarker.svelte';
|
|
4
|
-
import TextMarker from '../text/TextMarker.svelte';
|
|
2
|
+
import DeltaPoint from './DeltaPoint.svelte';
|
|
5
3
|
import { Delta } from './Delta';
|
|
6
|
-
import { getConfiguration } from '../../configuration/config.svelte';
|
|
7
4
|
import type { Series } from '../../types/plots/data/common';
|
|
8
5
|
import type { DeltaMarkerConfig } from '../../types/markers/axis/delta';
|
|
9
6
|
|
|
7
|
+
/**
|
|
8
|
+
* A delta annotation: one at `marker.at`, or, when it is a list, one at each position - all with the
|
|
9
|
+
* same ends. `line`, `text` and `dot` can be functions of the position.
|
|
10
|
+
*/
|
|
10
11
|
let { marker, data = [] }: { marker: DeltaMarkerConfig; data?: Series<TData>[] } = $props();
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
const label = $derived(getConfiguration().delta.text[marker.axis]);
|
|
14
|
-
const lineAnchor = $derived(marker.text?.lineAnchor ?? label.lineAnchor);
|
|
15
|
-
|
|
16
|
-
const clampRange = $derived.by((): [number, number] | undefined => {
|
|
17
|
-
const domain = marker.axis === 'x' ? marker.yDomain : marker.xDomain;
|
|
18
|
-
if (!domain || domain.length < 2) return undefined;
|
|
19
|
-
const lo = Number(domain[0]);
|
|
20
|
-
const hi = Number(domain[domain.length - 1]);
|
|
21
|
-
return isNaN(lo) || isNaN(hi) ? undefined : [Math.min(lo, hi), Math.max(lo, hi)];
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
function clamp(v: number): number {
|
|
25
|
-
return clampRange ? Math.max(clampRange[0], Math.min(clampRange[1], v)) : v;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function pt(fixed: number, varying: number) {
|
|
29
|
-
return marker.axis === 'x' ? { x: fixed, y: varying } : { x: varying, y: fixed };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function textVaryCoord(fromVis: number, toVis: number) {
|
|
33
|
-
return lineAnchor === 'top'
|
|
34
|
-
? Math.max(fromVis, toVis)
|
|
35
|
-
: lineAnchor === 'bottom'
|
|
36
|
-
? Math.min(fromVis, toVis)
|
|
37
|
-
: (fromVis + toVis) / 2;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Pre-derive all coordinates as script-level signals so prop getters read safe
|
|
41
|
-
// values during Svelte 5 teardown, before the {#if} guard finishes unmounting.
|
|
42
|
-
const coords = $derived.by(() => {
|
|
43
|
-
if (!resolved) return null;
|
|
44
|
-
const fromVis = clamp(resolved.from ?? 0);
|
|
45
|
-
const toVis = clamp(resolved.to ?? 0);
|
|
46
|
-
const at = resolved.at ?? 0;
|
|
47
|
-
return {
|
|
48
|
-
fromPt: pt(at, fromVis),
|
|
49
|
-
toPt: pt(at, toVis),
|
|
50
|
-
textPt: pt(at, textVaryCoord(fromVis, toVis)),
|
|
51
|
-
lineData: [pt(at, fromVis), pt(at, toVis)],
|
|
52
|
-
diff: resolved.diff ?? 0
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const dotStyles = $derived({
|
|
57
|
-
dotRadius: 4,
|
|
58
|
-
dotFill: marker.line?.stroke,
|
|
59
|
-
dotStroke: marker.line?.stroke,
|
|
60
|
-
dotStrokeWidth: 2,
|
|
61
|
-
...marker.dot
|
|
62
|
-
});
|
|
13
|
+
const points = $derived(Delta.points(marker));
|
|
63
14
|
</script>
|
|
64
15
|
|
|
65
|
-
{#
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
x={(p) => p.x}
|
|
69
|
-
y={(p) => p.y}
|
|
70
|
-
stroke={marker.line?.stroke ?? 'currentColor'}
|
|
71
|
-
strokeWidth={marker.line?.strokeWidth ?? 2}
|
|
72
|
-
strokeOpacity={marker.line?.strokeOpacity ?? 0.8}
|
|
73
|
-
strokeDasharray={marker.line?.strokeDasharray ?? '6 6'}
|
|
74
|
-
strokeLinecap={marker.line?.strokeLinecap ?? 'round'}
|
|
75
|
-
/>
|
|
76
|
-
<DotMarker marker={{ type: 'dot', x: coords.fromPt.x, y: coords.fromPt.y, styles: dotStyles }} />
|
|
77
|
-
<DotMarker marker={{ type: 'dot', x: coords.toPt.x, y: coords.toPt.y, styles: dotStyles }} />
|
|
78
|
-
<TextMarker
|
|
79
|
-
marker={{
|
|
80
|
-
type: 'text',
|
|
81
|
-
x: coords.textPt.x,
|
|
82
|
-
y: coords.textPt.y,
|
|
83
|
-
text: (marker.formatDiff ?? ((v: number) => v.toFixed(1)))(coords.diff),
|
|
84
|
-
styles: {
|
|
85
|
-
fontWeight: 'bold',
|
|
86
|
-
fontSize: 14,
|
|
87
|
-
textAnchor: label.textAnchor,
|
|
88
|
-
fill: marker.line?.stroke ?? 'currentColor',
|
|
89
|
-
...marker.text,
|
|
90
|
-
dx: marker.text?.dx ?? label.dx,
|
|
91
|
-
dy: marker.text?.dy ?? label.dy,
|
|
92
|
-
lineAnchor
|
|
93
|
-
}
|
|
94
|
-
}}
|
|
95
|
-
/>
|
|
96
|
-
{/if}
|
|
16
|
+
{#each points as point (String(point.at))}
|
|
17
|
+
<DeltaPoint marker={point} {data} />
|
|
18
|
+
{/each}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script lang="ts" generics="TData extends Record<string, unknown>">
|
|
2
|
+
import { Line } from 'svelteplot';
|
|
3
|
+
import DotMarker from '../dot/DotMarker.svelte';
|
|
4
|
+
import TextMarker from '../text/TextMarker.svelte';
|
|
5
|
+
import { Delta } from './Delta';
|
|
6
|
+
import { getConfiguration } from '../../configuration/config.svelte';
|
|
7
|
+
import type { Series } from '../../types/plots/data/common';
|
|
8
|
+
import type { DeltaPointConfig } from '../../types/markers/axis/delta';
|
|
9
|
+
|
|
10
|
+
/** One delta, at the one position `marker.at` names. See {@link DeltaMarker} for a list of them. */
|
|
11
|
+
let { marker, data = [] }: { marker: DeltaPointConfig; data?: Series<TData>[] } = $props();
|
|
12
|
+
|
|
13
|
+
const resolved = $derived(Delta.build(marker.axis, data, marker));
|
|
14
|
+
const label = $derived(getConfiguration().delta.text[marker.axis]);
|
|
15
|
+
const lineAnchor = $derived(marker.text?.lineAnchor ?? label.lineAnchor);
|
|
16
|
+
|
|
17
|
+
const clampRange = $derived.by((): [number, number] | undefined => {
|
|
18
|
+
const domain = marker.axis === 'x' ? marker.yDomain : marker.xDomain;
|
|
19
|
+
if (!domain || domain.length < 2) return undefined;
|
|
20
|
+
const lo = Number(domain[0]);
|
|
21
|
+
const hi = Number(domain[domain.length - 1]);
|
|
22
|
+
return isNaN(lo) || isNaN(hi) ? undefined : [Math.min(lo, hi), Math.max(lo, hi)];
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function clamp(v: number): number {
|
|
26
|
+
return clampRange ? Math.max(clampRange[0], Math.min(clampRange[1], v)) : v;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function pt(fixed: number, varying: number) {
|
|
30
|
+
return marker.axis === 'x' ? { x: fixed, y: varying } : { x: varying, y: fixed };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function textVaryCoord(fromVis: number, toVis: number) {
|
|
34
|
+
return lineAnchor === 'top'
|
|
35
|
+
? Math.max(fromVis, toVis)
|
|
36
|
+
: lineAnchor === 'bottom'
|
|
37
|
+
? Math.min(fromVis, toVis)
|
|
38
|
+
: (fromVis + toVis) / 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Pre-derive all coordinates as script-level signals so prop getters read safe
|
|
42
|
+
// values during Svelte 5 teardown, before the {#if} guard finishes unmounting.
|
|
43
|
+
const coords = $derived.by(() => {
|
|
44
|
+
if (!resolved) return null;
|
|
45
|
+
const fromVis = clamp(resolved.from ?? 0);
|
|
46
|
+
const toVis = clamp(resolved.to ?? 0);
|
|
47
|
+
const at = resolved.at ?? 0;
|
|
48
|
+
return {
|
|
49
|
+
fromPt: pt(at, fromVis),
|
|
50
|
+
toPt: pt(at, toVis),
|
|
51
|
+
textPt: pt(at, textVaryCoord(fromVis, toVis)),
|
|
52
|
+
lineData: [pt(at, fromVis), pt(at, toVis)],
|
|
53
|
+
diff: resolved.diff ?? 0
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const dotStyles = $derived({
|
|
58
|
+
dotRadius: 4,
|
|
59
|
+
dotFill: marker.line?.stroke,
|
|
60
|
+
dotStroke: marker.line?.stroke,
|
|
61
|
+
dotStrokeWidth: 2,
|
|
62
|
+
...marker.dot
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
{#if coords}
|
|
67
|
+
<Line
|
|
68
|
+
data={coords.lineData}
|
|
69
|
+
x={(p) => p.x}
|
|
70
|
+
y={(p) => p.y}
|
|
71
|
+
stroke={marker.line?.stroke ?? 'currentColor'}
|
|
72
|
+
strokeWidth={marker.line?.strokeWidth ?? 2}
|
|
73
|
+
strokeOpacity={marker.line?.strokeOpacity ?? 0.8}
|
|
74
|
+
strokeDasharray={marker.line?.strokeDasharray ?? '6 6'}
|
|
75
|
+
strokeLinecap={marker.line?.strokeLinecap ?? 'round'}
|
|
76
|
+
/>
|
|
77
|
+
<DotMarker marker={{ type: 'dot', x: coords.fromPt.x, y: coords.fromPt.y, styles: dotStyles }} />
|
|
78
|
+
<DotMarker marker={{ type: 'dot', x: coords.toPt.x, y: coords.toPt.y, styles: dotStyles }} />
|
|
79
|
+
<TextMarker
|
|
80
|
+
marker={{
|
|
81
|
+
type: 'text',
|
|
82
|
+
x: coords.textPt.x,
|
|
83
|
+
y: coords.textPt.y,
|
|
84
|
+
text: (marker.formatDiff ?? ((v: number) => v.toFixed(1)))(coords.diff),
|
|
85
|
+
styles: {
|
|
86
|
+
fontWeight: 'bold',
|
|
87
|
+
fontSize: 14,
|
|
88
|
+
textAnchor: label.textAnchor,
|
|
89
|
+
fill: marker.line?.stroke ?? 'currentColor',
|
|
90
|
+
...marker.text,
|
|
91
|
+
dx: marker.text?.dx ?? label.dx,
|
|
92
|
+
dy: marker.text?.dy ?? label.dy,
|
|
93
|
+
lineAnchor
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
{/if}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Series } from '../../types/plots/data/common';
|
|
2
|
+
import type { DeltaPointConfig } from '../../types/markers/axis/delta';
|
|
3
|
+
declare function $$render<TData extends Record<string, unknown>>(): {
|
|
4
|
+
props: {
|
|
5
|
+
marker: DeltaPointConfig;
|
|
6
|
+
data?: Series<TData>[];
|
|
7
|
+
};
|
|
8
|
+
exports: {};
|
|
9
|
+
bindings: "";
|
|
10
|
+
slots: {};
|
|
11
|
+
events: {};
|
|
12
|
+
};
|
|
13
|
+
declare class __sveltets_Render<TData extends Record<string, unknown>> {
|
|
14
|
+
props(): ReturnType<typeof $$render<TData>>['props'];
|
|
15
|
+
events(): ReturnType<typeof $$render<TData>>['events'];
|
|
16
|
+
slots(): ReturnType<typeof $$render<TData>>['slots'];
|
|
17
|
+
bindings(): "";
|
|
18
|
+
exports(): {};
|
|
19
|
+
}
|
|
20
|
+
interface $$IsomorphicComponent {
|
|
21
|
+
new <TData extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TData>['props']>, ReturnType<__sveltets_Render<TData>['events']>, ReturnType<__sveltets_Render<TData>['slots']>> & {
|
|
22
|
+
$$bindings?: ReturnType<__sveltets_Render<TData>['bindings']>;
|
|
23
|
+
} & ReturnType<__sveltets_Render<TData>['exports']>;
|
|
24
|
+
<TData extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<TData>['props']> & {}): ReturnType<__sveltets_Render<TData>['exports']>;
|
|
25
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
26
|
+
}
|
|
27
|
+
declare const DeltaPoint: $$IsomorphicComponent;
|
|
28
|
+
type DeltaPoint<TData extends Record<string, unknown>> = InstanceType<typeof DeltaPoint<TData>>;
|
|
29
|
+
export default DeltaPoint;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A single vertical (`axis: 'x'`) or horizontal (`axis: 'y'`) reference
|
|
9
|
-
* line - or, with `data`, one line per row. Shared by author-configured
|
|
9
|
+
* line - or, with `data`, one line per row, or, with a list as `value`, one per value. Shared by author-configured
|
|
10
10
|
* `'rule'` markers and `HoverMarker`'s crosshair (which always calls it
|
|
11
11
|
* with a literal `value` and no `data`). Falls back to `'currentColor'`/`1`
|
|
12
12
|
* when the caller supplies no style. See `RuleMarkerConfig`'s own doc
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
const styles = $derived(marker.styles);
|
|
22
22
|
|
|
23
23
|
const RuleMark = $derived(axis === 'x' ? RuleX : RuleY);
|
|
24
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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>,
|
|
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,
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
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
|
|
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
|
|
209
|
+
const segs = resolveSegmentsForSeries(segments, group.series);
|
|
197
210
|
return {
|
|
198
211
|
name: group.series.name,
|
|
199
212
|
...(group.series.id !== undefined ? { id: group.series.id } : {}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { StrokeStyle, DotStyle, FontStyle, AreaStyle } from '../plots/styling';
|
|
2
2
|
import type { LineAnchor, SymbolType, TextAnchor } from '../plots/constants';
|
|
3
3
|
import type { PlotChromeEntry } from '../plots/props';
|
|
4
|
+
import type { LegendMark } from '../layout/legend';
|
|
4
5
|
import type { EdgeTicksOptions } from '../layout/scales';
|
|
5
6
|
import type { CurveName } from '../layout/styles/curve';
|
|
6
7
|
export type TextStyle = {
|
|
@@ -133,6 +134,13 @@ export type PlotStylesConfig = {
|
|
|
133
134
|
gap: string;
|
|
134
135
|
/** swatch square size */
|
|
135
136
|
swatchSize: string;
|
|
137
|
+
/** The icon a discrete legend item draws for its series. */
|
|
138
|
+
icon: {
|
|
139
|
+
/** Draws the icons with rounded corners. */
|
|
140
|
+
rounded: boolean;
|
|
141
|
+
/** The shape a series icon takes by how the series draws, unless the series sets its own `symbol`. */
|
|
142
|
+
marks: Record<LegendMark, SymbolType>;
|
|
143
|
+
};
|
|
136
144
|
/** Fallback opacity for a legend-disabled series/value when a section doesn't set its own style. */
|
|
137
145
|
disabledOpacity: number;
|
|
138
146
|
/**
|
|
@@ -40,6 +40,24 @@ export type FacetCellContext<TRow extends Record<string, unknown>> = {
|
|
|
40
40
|
export type FacetCellSnippet<TRow extends Record<string, unknown>> = Snippet<[
|
|
41
41
|
FacetCellContext<TRow>
|
|
42
42
|
]>;
|
|
43
|
+
/** A facet value as the panel's label reads it: a number or a boolean matches by the text it prints as. */
|
|
44
|
+
export type FacetValue = string | number | boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The facet panels a segment or a marker applies to. A value, or a list of them, names panels by their
|
|
47
|
+
* label - the `by` value; in a `rows`/`cols` grid, `{ row, col }` name the panels by the grid's own
|
|
48
|
+
* values, and a side left out matches every value of it.
|
|
49
|
+
*/
|
|
50
|
+
export type FacetTarget = FacetValue | FacetValue[] | {
|
|
51
|
+
row?: FacetValue | FacetValue[];
|
|
52
|
+
col?: FacetValue | FacetValue[];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* A segment or a marker that can be limited to some facet panels. Without `facet` it applies to every
|
|
56
|
+
* panel, as before; with it, only to the panels it names - and to none of a plot that has no facet.
|
|
57
|
+
*/
|
|
58
|
+
export type FacetScoped = {
|
|
59
|
+
facet?: FacetTarget;
|
|
60
|
+
};
|
|
43
61
|
export type FacetIndicatorContext = {
|
|
44
62
|
current: number;
|
|
45
63
|
total: number;
|
|
@@ -33,6 +33,8 @@ export type LegendItem = {
|
|
|
33
33
|
name: string;
|
|
34
34
|
label?: string;
|
|
35
35
|
color?: string;
|
|
36
|
+
/** How the series draws; with no `symbol`, `legend.icon.marks` picks the icon from it. */
|
|
37
|
+
mark?: LegendMark;
|
|
36
38
|
symbol?: SymbolType;
|
|
37
39
|
/** Whether the symbol is filled, or drawn as an outline. Defaults to `true`. */
|
|
38
40
|
filled?: boolean;
|
|
@@ -290,6 +292,20 @@ export type ColorOverride = Omit<ContinuousLegendSection, 'type' | 'min' | 'max'
|
|
|
290
292
|
* ramp `interactive`, and only its first ramp is.
|
|
291
293
|
*/
|
|
292
294
|
export type LegendExtension = {
|
|
295
|
+
/**
|
|
296
|
+
* Series to turn off from code, as if the reader had clicked them: a list
|
|
297
|
+
* of names (each dimmed, to `legend.disabledOpacity`), or a name → what
|
|
298
|
+
* turning it off does map - an opacity to dim to, or a full
|
|
299
|
+
* {@link LegendDisabledAction} (`dim`, `hide` or `omit`). It follows the
|
|
300
|
+
* value it is given: a series left out is turned back on. One the reader
|
|
301
|
+
* turned off themselves is left as it is.
|
|
302
|
+
*/
|
|
303
|
+
off?: string[] | Record<string, number | LegendDisabledAction>;
|
|
304
|
+
/**
|
|
305
|
+
* The series to keep on: every other series is dimmed, the inverse of
|
|
306
|
+
* listing the rest in `off`. An entry of `off` for a series wins.
|
|
307
|
+
*/
|
|
308
|
+
highlight?: string | string[];
|
|
293
309
|
items?: LegendItem[];
|
|
294
310
|
sections?: LegendSection[];
|
|
295
311
|
series?: Omit<DiscreteLegendSection, 'type' | 'items'>;
|
|
@@ -29,6 +29,8 @@ export type PlotCellContext<TRow extends Record<string, unknown>, TScales extend
|
|
|
29
29
|
scales?: TScales;
|
|
30
30
|
/** This slice's facet panel, as a {@link Pipeline.runData} over every panel resolved it. */
|
|
31
31
|
panel?: object;
|
|
32
|
+
/** Where this slice sits among the facet panels - `undefined` when the plot has no facet. */
|
|
33
|
+
facet?: NonNullable<SlicePlace['facet']>;
|
|
32
34
|
/** Whether any row - the plot's own or a marker's - belongs in this slice; rows without a slicing field belong in all. */
|
|
33
35
|
keep: (row: TRow) => boolean;
|
|
34
36
|
};
|
|
@@ -19,9 +19,22 @@ export type DeltaConfig = {
|
|
|
19
19
|
text?: FontStyle;
|
|
20
20
|
dot?: DotStyle;
|
|
21
21
|
};
|
|
22
|
-
/**
|
|
23
|
-
export type
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
18
|
-
|
|
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`.
|
|
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;
|