@fundar/data-chart-telling 1.19.0 → 1.20.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 +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/layout/facet/FacetLayout.svelte +54 -41
- package/dist/layout/legend/ContinuousSection.svelte +10 -0
- package/dist/layout/legend/LegendState.svelte.d.ts +13 -1
- package/dist/layout/legend/LegendState.svelte.js +32 -0
- package/dist/layout/plot/AbstractPlot.svelte +83 -6
- package/dist/layout/plot/AbstractPlot.svelte.d.ts +1 -1
- package/dist/layout/plot/PublicPlotContext.d.ts +5 -0
- package/dist/layout/plot/PublicPlotContext.js +7 -0
- package/dist/markers/band/BandMarker.svelte +2 -1
- package/dist/markers/geo/GeoHoverMarker.svelte +14 -11
- package/dist/phases/context/RowContext.js +2 -2
- package/dist/phases/context/rowContextKey.d.ts +2 -0
- package/dist/phases/context/rowContextKey.js +2 -0
- package/dist/phases/sharedScales/SharedStepScales.d.ts +10 -0
- package/dist/phases/sharedScales/SharedStepScales.js +43 -0
- package/dist/plots/map/geo/Core.svelte +29 -6
- package/dist/plots/map/geo/Plot.svelte +38 -10
- package/dist/plots/map/geo/phases/data.d.ts +2 -1
- package/dist/plots/map/geo/phases/data.js +8 -2
- package/dist/plots/map/geo/phases/scales.js +6 -2
- package/dist/plots/map/geo/slices.d.ts +27 -3
- package/dist/plots/map/geo/slices.js +58 -5
- package/dist/plots/ranking/bar/markers.js +2 -2
- package/dist/types/layout/plot.d.ts +2 -0
- package/dist/types/layout/styles/geo.d.ts +3 -3
- package/dist/types/plots/data/geo.d.ts +19 -5
- package/dist/types/plots/map/geo/props.d.ts +9 -3
- package/dist/types/plots/map/geo/state.d.ts +3 -1
- package/dist/utils/ConfigEquality.d.ts +11 -0
- package/dist/utils/ConfigEquality.js +37 -0
- package/docs/facets.md +12 -0
- package/docs/geo.md +35 -7
- package/docs/legend.md +6 -0
- package/docs/plots/ranking/bar.md +1 -1
- package/docs/state.md +1 -1
- package/docs/timeline.md +1 -1
- package/docs/transforms-and-statistics.md +1 -1
- package/llms-full.txt +3 -3
- package/package.json +1 -1
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import AbstractPlot from '../../../layout/plot/AbstractPlot.svelte';
|
|
4
4
|
import { merge } from '../../../utils/merge';
|
|
5
5
|
import { buildGeoDefaults } from './defaults';
|
|
6
|
-
import { buildGeoSlicing } from './slices';
|
|
6
|
+
import { buildGeoSlicing, GeoSlices } from './slices';
|
|
7
|
+
import { Slice } from '../../../layout/plot/Slice';
|
|
7
8
|
import { GeoFeatures } from './GeoFeatures.svelte';
|
|
8
9
|
import type { GeoPlotProps, GeoPlotCoreProps } from '../../../types/plots/map/geo/props';
|
|
9
10
|
import type { GeoFeature } from '../../../types/plots/data/geo';
|
|
@@ -19,9 +20,11 @@
|
|
|
19
20
|
* Optional chrome (`title`, `subtitle`, `caption`, `header`, `footer`,
|
|
20
21
|
* `legend`) frames it. `timeline` and `facet` slice its features and its
|
|
21
22
|
* markers' own rows by a field (a feature's from its `properties`); what
|
|
22
|
-
* lacks the field draws in every slice,
|
|
23
|
-
* slice
|
|
24
|
-
*
|
|
23
|
+
* lacks the field draws in every slice, the color ramp spans every
|
|
24
|
+
* slice, and the panels may come from `facet.values` alone (or `rowValues`, `colValues`,
|
|
25
|
+
* `timeline.values`), one layer of features serving them all. The callbacks that read a
|
|
26
|
+
* feature get the slice as their last argument (`GeoSliceContext`). With
|
|
27
|
+
* `coordinates.projection.domain: 'data'`, each facet zooms to its own features.
|
|
25
28
|
*/
|
|
26
29
|
let {
|
|
27
30
|
title,
|
|
@@ -54,13 +57,29 @@
|
|
|
54
57
|
buildGeoSlicing({ rows: [...features, ...markerRows], timeline, facet })
|
|
55
58
|
);
|
|
56
59
|
|
|
57
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* The color ramp's domain over every feature as each slice reads it - a facet panel, a timeline step, each
|
|
62
|
+
* with its own context - so every slice colors on the same scale. Not under `scales: 'free'` (the
|
|
63
|
+
* facet's, or the timeline's when there is no facet): each slice then fits its own.
|
|
64
|
+
*/
|
|
58
65
|
const sharedColor = $derived.by((): GeoScalesConfig | undefined => {
|
|
59
66
|
const value = core.color;
|
|
60
67
|
if (!value) return undefined;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
if ((facet ? facet.scales : timeline?.scales) === 'free') return undefined;
|
|
69
|
+
const all = resolved.features as GeoFeature<TProps>[];
|
|
70
|
+
const whole = new Slice({
|
|
71
|
+
rows: features,
|
|
72
|
+
timeline: slicing.timeline,
|
|
73
|
+
facet: slicing.facet,
|
|
74
|
+
filterMode: undefined
|
|
75
|
+
});
|
|
76
|
+
const reads = slicing.slices.length
|
|
77
|
+
? slicing.slices.flatMap(({ place, context }) => {
|
|
78
|
+
const keep = whole.keep(place) as unknown as (f: GeoFeature<TProps>) => boolean;
|
|
79
|
+
return all.filter(keep).map((f) => value(f, context));
|
|
80
|
+
})
|
|
81
|
+
: all.map((f) => value(f, {}));
|
|
82
|
+
const values = reads.filter((v): v is number => typeof v === 'number' && Number.isFinite(v));
|
|
64
83
|
return values.length
|
|
65
84
|
? { color: { domain: [Math.min(...values), Math.max(...values)] } }
|
|
66
85
|
: undefined;
|
|
@@ -69,8 +88,16 @@
|
|
|
69
88
|
/** The core's props for one slice: the features and marker rows it keeps. */
|
|
70
89
|
function sliceProps(slice: PlotCellContext<Row>): GeoPlotCoreProps<TProps> {
|
|
71
90
|
const tip = slice.tooltip as TooltipConfig<GeoFeature<TProps>> | undefined;
|
|
91
|
+
const sliceContext = GeoSlices.contextOf(slice);
|
|
72
92
|
if (!sliced)
|
|
73
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
...core,
|
|
95
|
+
width: slice.width,
|
|
96
|
+
height: slice.height,
|
|
97
|
+
margins,
|
|
98
|
+
tooltip: tip,
|
|
99
|
+
sliceContext
|
|
100
|
+
};
|
|
74
101
|
return {
|
|
75
102
|
...core,
|
|
76
103
|
data: features.filter(slice.keep) as unknown as GeoFeature<TProps>[],
|
|
@@ -82,7 +109,8 @@
|
|
|
82
109
|
height: slice.height,
|
|
83
110
|
margins: slice.margins ?? margins,
|
|
84
111
|
scales: merge(sharedColor, core.scales),
|
|
85
|
-
tooltip: tip
|
|
112
|
+
tooltip: tip,
|
|
113
|
+
sliceContext
|
|
86
114
|
} as GeoPlotCoreProps<TProps>;
|
|
87
115
|
}
|
|
88
116
|
</script>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GeoPlotState } from '../../../../types/plots/map/geo/state';
|
|
2
2
|
/**
|
|
3
3
|
* GeoPlot's "data" phase: every feature tagged with its resolved id/centroid,
|
|
4
|
-
* then reduced by `excludeParts`.
|
|
4
|
+
* then reduced by `excludeParts`. Each carries the slice it is drawn in as its
|
|
5
|
+
* row context. Reads no env.
|
|
5
6
|
*/
|
|
6
7
|
export declare function buildData<TProps extends Record<string, unknown>>(state: GeoPlotState<TProps>): GeoPlotState<TProps>;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { resolveAccessor } from '../../../../utils/accessors';
|
|
2
2
|
import { GeoGeometry } from '../GeoGeometry';
|
|
3
3
|
import { GeoFeatures } from '../GeoFeatures.svelte';
|
|
4
|
+
import { ROW_CONTEXT_KEY } from '../../../../phases/context/rowContextKey';
|
|
4
5
|
/**
|
|
5
6
|
* GeoPlot's "data" phase: every feature tagged with its resolved id/centroid,
|
|
6
|
-
* then reduced by `excludeParts`.
|
|
7
|
+
* then reduced by `excludeParts`. Each carries the slice it is drawn in as its
|
|
8
|
+
* row context. Reads no env.
|
|
7
9
|
*/
|
|
8
10
|
export function buildData(state) {
|
|
9
|
-
const { rawFeatures, featureId, excludeParts } = state;
|
|
11
|
+
const { rawFeatures, featureId, excludeParts, slice = {} } = state;
|
|
10
12
|
const getFeatureId = featureId
|
|
11
13
|
? resolveAccessor(featureId)
|
|
12
14
|
: GeoFeatures.defaultId;
|
|
@@ -23,5 +25,9 @@ export function buildData(state) {
|
|
|
23
25
|
const [cx, cy] = GeoGeometry.centroid(geometry);
|
|
24
26
|
return [{ ...f, geometry, __cx: cx, __cy: cy }];
|
|
25
27
|
});
|
|
28
|
+
// Kept as the row's context, which a copy of the feature carries along: a tooltip's `format` reads it back.
|
|
29
|
+
for (const f of new Set([...allFeatures, ...features])) {
|
|
30
|
+
f[ROW_CONTEXT_KEY] = slice;
|
|
31
|
+
}
|
|
26
32
|
return { ...state, allFeatures, features };
|
|
27
33
|
}
|
|
@@ -61,7 +61,9 @@ export function buildScales(state, env) {
|
|
|
61
61
|
return colorScale ? colorScale(v) : undefined;
|
|
62
62
|
}
|
|
63
63
|
: undefined;
|
|
64
|
-
// Every segment ramp is built up front, so the legend can draw each one.
|
|
64
|
+
// Every segment ramp is built up front, so the legend can draw each one. The plot's own ramp is left out of
|
|
65
|
+
// the legend when every colored feature has one of its own: nothing is drawn with it.
|
|
66
|
+
let usesPlotRamp = false;
|
|
65
67
|
if (colorScale) {
|
|
66
68
|
for (const f of features) {
|
|
67
69
|
const featureId = f.__id ?? '';
|
|
@@ -69,6 +71,8 @@ export function buildScales(state, env) {
|
|
|
69
71
|
const segScales = GeoSegments.scales(segments, featureId, groupingValue);
|
|
70
72
|
if (segScales?.color)
|
|
71
73
|
segmentColorScaleFor(groupingValue ?? featureId, segScales);
|
|
74
|
+
else if (color?.(f) != null)
|
|
75
|
+
usesPlotRamp = true;
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
// Plot-level only - no segment-scoped opacity ramp (unlike `resolveColor`
|
|
@@ -90,7 +94,7 @@ export function buildScales(state, env) {
|
|
|
90
94
|
: undefined;
|
|
91
95
|
return {
|
|
92
96
|
...state,
|
|
93
|
-
colorDomain: valueDomain ?? undefined,
|
|
97
|
+
colorDomain: (usesPlotRamp || !colorRamps.length ? valueDomain : undefined) ?? undefined,
|
|
94
98
|
colorRamps: colorRamps.length ? colorRamps : undefined,
|
|
95
99
|
opacityDomain: opacityDomain && typeof opacityDomain[0] === 'number'
|
|
96
100
|
? opacityDomain
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import type { FacetConfig } from '../../../types/layout/facet';
|
|
2
|
-
import type { TimelineConfig } from '../../../types/layout/timeline';
|
|
2
|
+
import type { TimelineConfig, TimeValue } from '../../../types/layout/timeline';
|
|
3
|
+
import type { SlicePlace } from '../../../types/layout/plot';
|
|
4
|
+
import type { GeoSliceContext } from '../../../types/plots/data/geo';
|
|
3
5
|
type Row = Record<string, unknown>;
|
|
6
|
+
/** One slice of a sliced map: where it sits, and the context its features are read in. */
|
|
7
|
+
export type GeoSlice = {
|
|
8
|
+
place: SlicePlace;
|
|
9
|
+
context: GeoSliceContext;
|
|
10
|
+
};
|
|
4
11
|
/**
|
|
5
12
|
* What a GeoPlot hands `Plot` to slice by: one stand-in row per value
|
|
6
|
-
* combination
|
|
7
|
-
*
|
|
13
|
+
* combination, and timeline/facet accessors that read a stand-in or a real
|
|
14
|
+
* row. A dimension's values are the ones the timeline or facet names
|
|
15
|
+
* (`timeline.values`, `facet.values`, `rowValues`, `colValues`), in
|
|
16
|
+
* their order, then any more found in the features and markers' rows - so
|
|
17
|
+
* the slices exist even when no feature carries the field. Each combination
|
|
18
|
+
* is also returned as a `slices` entry: its place and the context its
|
|
19
|
+
* features are read in.
|
|
8
20
|
*/
|
|
9
21
|
export declare function buildGeoSlicing(args: {
|
|
10
22
|
rows: Row[];
|
|
@@ -12,7 +24,19 @@ export declare function buildGeoSlicing(args: {
|
|
|
12
24
|
facet?: FacetConfig<Row>;
|
|
13
25
|
}): {
|
|
14
26
|
rows: Row[];
|
|
27
|
+
slices: GeoSlice[];
|
|
15
28
|
timeline?: TimelineConfig<Row>;
|
|
16
29
|
facet?: FacetConfig<Row>;
|
|
17
30
|
};
|
|
31
|
+
export declare const GeoSlices: {
|
|
32
|
+
/** The context a callback reads a feature in, for the slice at `place`: empty when there is none. */
|
|
33
|
+
contextOf(place: {
|
|
34
|
+
time?: TimeValue;
|
|
35
|
+
facet?: {
|
|
36
|
+
facetValue: string;
|
|
37
|
+
row?: string;
|
|
38
|
+
col?: string;
|
|
39
|
+
};
|
|
40
|
+
}): GeoSliceContext;
|
|
41
|
+
};
|
|
18
42
|
export {};
|
|
@@ -12,8 +12,13 @@ function readField(field, row) {
|
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* What a GeoPlot hands `Plot` to slice by: one stand-in row per value
|
|
15
|
-
* combination
|
|
16
|
-
*
|
|
15
|
+
* combination, and timeline/facet accessors that read a stand-in or a real
|
|
16
|
+
* row. A dimension's values are the ones the timeline or facet names
|
|
17
|
+
* (`timeline.values`, `facet.values`, `rowValues`, `colValues`), in
|
|
18
|
+
* their order, then any more found in the features and markers' rows - so
|
|
19
|
+
* the slices exist even when no feature carries the field. Each combination
|
|
20
|
+
* is also returned as a `slices` entry: its place and the context its
|
|
21
|
+
* features are read in.
|
|
17
22
|
*/
|
|
18
23
|
export function buildGeoSlicing(args) {
|
|
19
24
|
const { rows, timeline, facet } = args;
|
|
@@ -31,17 +36,47 @@ export function buildGeoSlicing(args) {
|
|
|
31
36
|
const reader = (dimension) => (row) => SLICE in row
|
|
32
37
|
? row[SLICE][dimension]
|
|
33
38
|
: readField(fields[dimension], row);
|
|
39
|
+
/** The values the timeline or facet names for a dimension, ahead of the ones found. */
|
|
40
|
+
const named = {
|
|
41
|
+
time: timeline?.values,
|
|
42
|
+
by: facet && isFacetWrap(facet) ? facet.values : undefined,
|
|
43
|
+
rows: facet && !isFacetWrap(facet) ? facet.rowValues : undefined,
|
|
44
|
+
cols: facet && !isFacetWrap(facet) ? facet.colValues : undefined
|
|
45
|
+
};
|
|
46
|
+
const time = new TimelineTime(timeline?.dateFormat);
|
|
34
47
|
let combos = [{}];
|
|
35
48
|
for (const dimension of Object.keys(fields)) {
|
|
36
49
|
const read = rows.map((row) => readField(fields[dimension], row)).filter((v) => v != null);
|
|
37
50
|
// Dates on the same step are one value.
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
51
|
+
const keyOf = (value) => dimension === 'time' ? String(time.toTime(value) ?? value) : String(value);
|
|
52
|
+
const values = [
|
|
53
|
+
...new Map([...(named[dimension] ?? []), ...read].map((value) => [keyOf(value), value])).values()
|
|
54
|
+
];
|
|
41
55
|
combos = combos.flatMap((combo) => values.map((value) => ({ ...combo, [dimension]: value })));
|
|
42
56
|
}
|
|
57
|
+
const slices = combos.map((combo) => {
|
|
58
|
+
const tick = combo.time === undefined ? undefined : time.toTime(combo.time);
|
|
59
|
+
const by = combo.by === undefined ? undefined : String(combo.by);
|
|
60
|
+
const row = combo.rows === undefined ? undefined : String(combo.rows);
|
|
61
|
+
const col = combo.cols === undefined ? undefined : String(combo.cols);
|
|
62
|
+
const value = by ?? (row !== undefined && col !== undefined ? `${row} · ${col}` : (row ?? col));
|
|
63
|
+
const place = {
|
|
64
|
+
...(tick === undefined ? {} : { time: tick }),
|
|
65
|
+
...(value === undefined
|
|
66
|
+
? {}
|
|
67
|
+
: {
|
|
68
|
+
facet: {
|
|
69
|
+
facetValue: value,
|
|
70
|
+
...(row === undefined ? {} : { row }),
|
|
71
|
+
...(col === undefined ? {} : { col })
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
return { place, context: GeoSlices.contextOf(place) };
|
|
76
|
+
});
|
|
43
77
|
return {
|
|
44
78
|
rows: combos.map((combo) => ({ [SLICE]: combo })),
|
|
79
|
+
slices,
|
|
45
80
|
timeline: timeline && {
|
|
46
81
|
...timeline,
|
|
47
82
|
accessor: reader('time'),
|
|
@@ -57,3 +92,21 @@ export function buildGeoSlicing(args) {
|
|
|
57
92
|
})
|
|
58
93
|
};
|
|
59
94
|
}
|
|
95
|
+
export const GeoSlices = {
|
|
96
|
+
/** The context a callback reads a feature in, for the slice at `place`: empty when there is none. */
|
|
97
|
+
contextOf(place) {
|
|
98
|
+
const { time, facet } = place;
|
|
99
|
+
return {
|
|
100
|
+
...(facet
|
|
101
|
+
? {
|
|
102
|
+
facet: {
|
|
103
|
+
value: facet.facetValue,
|
|
104
|
+
...(facet.row === undefined ? {} : { row: facet.row }),
|
|
105
|
+
...(facet.col === undefined ? {} : { col: facet.col })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
109
|
+
...(time === undefined ? {} : { time })
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
@@ -57,11 +57,11 @@ hover, env) {
|
|
|
57
57
|
data: [row],
|
|
58
58
|
category: categoryOf(row),
|
|
59
59
|
hover: hover.active && 'trigger',
|
|
60
|
-
|
|
60
|
+
styles: (slice
|
|
61
61
|
? isHorizontal
|
|
62
62
|
? { insetTop: slice.start, insetBottom: slice.end }
|
|
63
63
|
: { insetLeft: slice.start, insetRight: slice.end }
|
|
64
|
-
:
|
|
64
|
+
: undefined)
|
|
65
65
|
};
|
|
66
66
|
});
|
|
67
67
|
}
|
|
@@ -31,6 +31,8 @@ export type PlotCellContext<TRow extends Record<string, unknown>, TScales extend
|
|
|
31
31
|
panel?: object;
|
|
32
32
|
/** Where this slice sits among the facet panels - `undefined` when the plot has no facet. */
|
|
33
33
|
facet?: NonNullable<SlicePlace['facet']>;
|
|
34
|
+
/** The timeline step this slice shows - `undefined` when the plot has no timeline. */
|
|
35
|
+
time?: TimeValue;
|
|
34
36
|
/** Whether any row - the plot's own or a marker's - belongs in this slice; rows without a slicing field belong in all. */
|
|
35
37
|
keep: (row: TRow) => boolean;
|
|
36
38
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GeoFeature } from '../../plots/data/geo';
|
|
1
|
+
import type { GeoFeature, GeoSliceContext } from '../../plots/data/geo';
|
|
2
2
|
import type { FrameProp } from './frame';
|
|
3
3
|
import type { BasePlotStylesConfig } from './base';
|
|
4
4
|
export type GeoTileLayerConfig = {
|
|
@@ -26,8 +26,8 @@ export type GeoStylesConfig<TProps extends Record<string, unknown> = Record<stri
|
|
|
26
26
|
strokeWidth?: number;
|
|
27
27
|
/** Raster XYZ tile basemap under the vector geometry. Mercator-family projections only. */
|
|
28
28
|
tileLayer?: GeoTileLayerConfig;
|
|
29
|
-
/** Fired when a base-map feature is clicked. */
|
|
30
|
-
onFeatureClick?: (feature: GeoFeature<TProps>, event: Event) => void;
|
|
29
|
+
/** Fired when a base-map feature is clicked, with the slice it was drawn in. */
|
|
30
|
+
onFeatureClick?: (feature: GeoFeature<TProps>, event: Event, slice: GeoSliceContext) => void;
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
33
|
* `GeoTileLayerConfig`, plus `features` - never author-facing (the public
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TimeValue } from '../../layout/timeline';
|
|
2
2
|
/**
|
|
3
3
|
* One GeoJSON feature, generically typed over its `properties`. Intersected
|
|
4
4
|
* with `Record<string, unknown>` so it satisfies the same generic bound
|
|
@@ -7,6 +7,20 @@ import type { Accessor } from './common';
|
|
|
7
7
|
* index signature on its own.
|
|
8
8
|
*/
|
|
9
9
|
export type GeoFeature<TProps extends Record<string, unknown> = Record<string, unknown>> = GeoJSON.Feature<GeoJSON.Geometry, TProps> & Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Which slice of a `timeline` and `facet` a feature is being read for: the callbacks of a sliced map get it as
|
|
12
|
+
* their last argument. Empty on a map without either.
|
|
13
|
+
*/
|
|
14
|
+
export type GeoSliceContext = {
|
|
15
|
+
/** The facet panel: its label (a grid's `row · col`), and the `rows` and `cols` values it sits at. */
|
|
16
|
+
facet?: {
|
|
17
|
+
value: string;
|
|
18
|
+
row?: string;
|
|
19
|
+
col?: string;
|
|
20
|
+
};
|
|
21
|
+
/** The timeline step. */
|
|
22
|
+
time?: TimeValue;
|
|
23
|
+
};
|
|
10
24
|
export type GeoJsonInput<TProps extends Record<string, unknown>> = GeoJSON.FeatureCollection<GeoJSON.Geometry, TProps> | GeoFeature<TProps>[];
|
|
11
25
|
/**
|
|
12
26
|
* Minimal structural shape of a TopoJSON `Topology` object. Deliberately not
|
|
@@ -46,14 +60,14 @@ export type GeoDataProps<TProps extends Record<string, unknown> = Record<string,
|
|
|
46
60
|
* (e.g. `segments.argentina`) and for hover/click identification. Defaults
|
|
47
61
|
* to `feature.id ?? feature.properties?.id ?? feature.properties?.name`.
|
|
48
62
|
*/
|
|
49
|
-
featureId?:
|
|
63
|
+
featureId?: keyof GeoFeature<TProps> | ((feature: GeoFeature<TProps>, slice: GeoSliceContext) => string);
|
|
50
64
|
/**
|
|
51
65
|
* Optional continuous color-value accessor for choropleth coloring - the
|
|
52
66
|
* channel driving the ramp (mirrors heatmap's `color`), configured via
|
|
53
67
|
* `scales.color`. A feature with no `color` (or when omitted entirely)
|
|
54
68
|
* falls back to `styles.fill`/`segments`.
|
|
55
69
|
*/
|
|
56
|
-
color?: (feature: GeoFeature<TProps
|
|
70
|
+
color?: (feature: GeoFeature<TProps>, slice: GeoSliceContext) => number | undefined;
|
|
57
71
|
/**
|
|
58
72
|
* Optional structural grouping key - splits features into named buckets
|
|
59
73
|
* for `segments` to address by name (e.g. `segments.global_south`),
|
|
@@ -64,9 +78,9 @@ export type GeoDataProps<TProps extends Record<string, unknown> = Record<string,
|
|
|
64
78
|
* ungrouped. A feature whose `grouping` value has no matching `segments`
|
|
65
79
|
* key simply falls through to `segments.default`/the plot-level ramp.
|
|
66
80
|
*/
|
|
67
|
-
grouping?: (feature: GeoFeature<TProps
|
|
81
|
+
grouping?: (feature: GeoFeature<TProps>, slice: GeoSliceContext) => string | undefined;
|
|
68
82
|
/** Continuous or categorical opacity-value accessor - configure the ramp via `scales.opacity`. */
|
|
69
|
-
opacity?: (feature: GeoFeature<TProps
|
|
83
|
+
opacity?: (feature: GeoFeature<TProps>, slice: GeoSliceContext) => number | undefined;
|
|
70
84
|
/**
|
|
71
85
|
* Excludes matching parts of a feature's geometry from the map's own
|
|
72
86
|
* rendering and hover/click matching - e.g. so a separate `inset` marker
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BasePlotProps, PlotCoreProps } from '../../props';
|
|
2
2
|
import type { GeoScalesConfig, CoordinatesConfig } from '../../../layout/coordinates';
|
|
3
|
-
import type { GeoFeature, GeoDataProps } from '../../data/geo';
|
|
3
|
+
import type { GeoFeature, GeoDataProps, GeoSliceContext } from '../../data/geo';
|
|
4
|
+
import type { PlotRowContext } from '../../../layout/context';
|
|
4
5
|
import type { GeoStylesConfig } from '../../../layout/styles';
|
|
5
6
|
import type { GeoSegmentStyle } from '../../segments/common';
|
|
6
7
|
import type { GeoMarkersConfig } from '../../../markers/geo';
|
|
@@ -14,10 +15,15 @@ type OmitEach<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never
|
|
|
14
15
|
* marker's own `data` rows; a feature or row without the field is context,
|
|
15
16
|
* drawn in every slice.
|
|
16
17
|
*/
|
|
17
|
-
export type GeoPlotProps<TProps extends Record<string, unknown>> = OmitEach<BasePlotProps<GeoDataProps<TProps>, GeoFeature<TProps>, GeoStylesConfig<TProps>, GeoSegmentStyle, GeoMarkersConfig, GeoScalesConfig, CoordinatesConfig>, 'timeline' | 'facet'> & {
|
|
18
|
+
export type GeoPlotProps<TProps extends Record<string, unknown>> = OmitEach<BasePlotProps<GeoDataProps<TProps>, GeoFeature<TProps>, GeoStylesConfig<TProps>, GeoSegmentStyle, GeoMarkersConfig, GeoScalesConfig, CoordinatesConfig, GeoTooltipContext>, 'timeline' | 'facet'> & {
|
|
18
19
|
timeline?: TimelineConfig<Record<string, unknown>>;
|
|
19
20
|
facet?: FacetConfig<Record<string, unknown>>;
|
|
20
21
|
};
|
|
22
|
+
/** What a tooltip's `format` knows of a hovered feature: the shared row context, and the slice it was drawn in. */
|
|
23
|
+
export type GeoTooltipContext = PlotRowContext & GeoSliceContext;
|
|
21
24
|
/** What `GeoPlot`'s core draws from. */
|
|
22
|
-
export type GeoPlotCoreProps<TProps extends Record<string, unknown>> = PlotCoreProps<GeoDataProps<TProps>, GeoFeature<TProps>, GeoStylesConfig<TProps>, GeoSegmentStyle, GeoMarkersConfig, GeoScalesConfig, CoordinatesConfig
|
|
25
|
+
export type GeoPlotCoreProps<TProps extends Record<string, unknown>> = PlotCoreProps<GeoDataProps<TProps>, GeoFeature<TProps>, GeoStylesConfig<TProps>, GeoSegmentStyle, GeoMarkersConfig, GeoScalesConfig, CoordinatesConfig> & {
|
|
26
|
+
/** The slice of the `timeline` and `facet` this core draws - what its callbacks read its features in. */
|
|
27
|
+
sliceContext?: GeoSliceContext;
|
|
28
|
+
};
|
|
23
29
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Accessor } from '../../data/common';
|
|
2
|
-
import type { GeoFeature } from '../../data/geo';
|
|
2
|
+
import type { GeoFeature, GeoSliceContext } from '../../data/geo';
|
|
3
3
|
import type { ColorScaleConfig } from '../../../layout/scales';
|
|
4
4
|
import type { GeoScalesConfig } from '../../../layout/coordinates';
|
|
5
5
|
import type { SegmentsConfig, MarginConfig, HoverConfig } from '../../props';
|
|
@@ -46,6 +46,8 @@ export type GeoPlotState<TProps extends Record<string, unknown>> = {
|
|
|
46
46
|
}, feature: TaggedGeoFeature<TProps>) => boolean;
|
|
47
47
|
segments: SegmentsConfig<GeoSegmentStyle>;
|
|
48
48
|
scales: GeoScalesConfig;
|
|
49
|
+
/** The slice of the `timeline` and `facet` this core draws, kept on each of its features for a tooltip's `format` to read. */
|
|
50
|
+
slice?: GeoSliceContext;
|
|
49
51
|
/** The plot's `tooltip`, which the "hover" phase reads. */
|
|
50
52
|
tooltip?: TooltipProp<TaggedGeoFeature<TProps>>;
|
|
51
53
|
/** The raw `markers` prop - GeoPlot interleaves it with the base map by draw order, not a plain append. */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Whether two configuration values hold the same, for a plot to tell a new configuration from one built again. */
|
|
2
|
+
export declare class ConfigEquality {
|
|
3
|
+
#private;
|
|
4
|
+
/** How many levels of objects and arrays it looks into; a value nested deeper only matches as the same object. */
|
|
5
|
+
static readonly MAX_DEPTH = 6;
|
|
6
|
+
/**
|
|
7
|
+
* Whether `a` and `b` are the same value, or plain objects and arrays alike all the way down, dates of one
|
|
8
|
+
* time; anything else - a function, a class instance - only if it is the same object.
|
|
9
|
+
*/
|
|
10
|
+
static same(a: unknown, b: unknown, depth?: number): boolean;
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Whether two configuration values hold the same, for a plot to tell a new configuration from one built again. */
|
|
2
|
+
export class ConfigEquality {
|
|
3
|
+
/** How many levels of objects and arrays it looks into; a value nested deeper only matches as the same object. */
|
|
4
|
+
static MAX_DEPTH = 6;
|
|
5
|
+
/**
|
|
6
|
+
* Whether `a` and `b` are the same value, or plain objects and arrays alike all the way down, dates of one
|
|
7
|
+
* time; anything else - a function, a class instance - only if it is the same object.
|
|
8
|
+
*/
|
|
9
|
+
static same(a, b, depth = 0) {
|
|
10
|
+
if (Object.is(a, b))
|
|
11
|
+
return true;
|
|
12
|
+
if (depth >= ConfigEquality.MAX_DEPTH)
|
|
13
|
+
return false;
|
|
14
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
15
|
+
return false;
|
|
16
|
+
if (a instanceof Date || b instanceof Date) {
|
|
17
|
+
return a instanceof Date && b instanceof Date && +a === +b;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
20
|
+
return (Array.isArray(a) &&
|
|
21
|
+
Array.isArray(b) &&
|
|
22
|
+
a.length === b.length &&
|
|
23
|
+
a.every((value, i) => ConfigEquality.same(value, b[i], depth + 1)));
|
|
24
|
+
}
|
|
25
|
+
if (!ConfigEquality.#isPlain(a) || !ConfigEquality.#isPlain(b))
|
|
26
|
+
return false;
|
|
27
|
+
const keys = Reflect.ownKeys(a);
|
|
28
|
+
if (keys.length !== Reflect.ownKeys(b).length)
|
|
29
|
+
return false;
|
|
30
|
+
return keys.every((key) => Object.prototype.hasOwnProperty.call(b, key) &&
|
|
31
|
+
ConfigEquality.same(a[key], b[key], depth + 1));
|
|
32
|
+
}
|
|
33
|
+
static #isPlain(value) {
|
|
34
|
+
const proto = Object.getPrototypeOf(value);
|
|
35
|
+
return proto === Object.prototype || proto === null;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/docs/facets.md
CHANGED
|
@@ -168,3 +168,15 @@ Panels line up: each plot reports the margins it settled on, and once all have,
|
|
|
168
168
|
## Maps
|
|
169
169
|
|
|
170
170
|
`GeoPlot` takes `facet` and reads the field from each feature's `properties` (and from each marker row); features without the field are drawn in every panel.
|
|
171
|
+
|
|
172
|
+
The panels need not come from the features. `values` (or a grid's `rowValues` and `colValues`) name them, and they exist even when no feature carries the field: one layer of features serves every panel, with no copy of it per value. Each callback that reads a feature - `color`, `opacity`, `grouping`, `featureId`, `styles.onFeatureClick` - gets the slice it is drawn in as its last argument, `{ facet?: { value, row?, col? }, time? }`, and a tooltip's `format` finds the same `facet` and `time` in its context:
|
|
173
|
+
|
|
174
|
+
```svelte
|
|
175
|
+
<GeoPlot
|
|
176
|
+
data={provinces}
|
|
177
|
+
facet={{ by: 'sex', values: ['Women', 'Men'] }}
|
|
178
|
+
color={(f, { facet }) => valueFor(f.properties.name, facet?.value)}
|
|
179
|
+
/>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The ramp spans every panel, each read in its own slice. See [Facets and timeline](geo.md#facets-and-timeline) on maps.
|
package/docs/geo.md
CHANGED
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
|
|
20
20
|
## Data
|
|
21
21
|
|
|
22
|
-
| Prop | Meaning
|
|
23
|
-
| -------------- |
|
|
24
|
-
| `data` | A GeoJSON `FeatureCollection`, an array of `Feature`s, or a TopoJSON `Topology`
|
|
25
|
-
| `object` | Required for a `Topology`: the key under `topology.objects` to draw. Needs the optional `topojson-client` package
|
|
26
|
-
| `featureId` | Accessor for each feature's stable id. Default: `feature.id ?? properties.id ?? properties.name`. It is the key of `segments`, and identifies features in hover and click
|
|
27
|
-
| `color` | `(feature) => number \| undefined` - the value of a choropleth. Features without one use `styles.fill` or their `segments`
|
|
28
|
-
| `excludeParts` | `(part, feature) => boolean`, where `part` is `{ centroid, geometry }` - drops geometry parts from drawing and hover (a distant territory drawn in an inset instead). `segments` still apply to what remains
|
|
22
|
+
| Prop | Meaning |
|
|
23
|
+
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `data` | A GeoJSON `FeatureCollection`, an array of `Feature`s, or a TopoJSON `Topology` |
|
|
25
|
+
| `object` | Required for a `Topology`: the key under `topology.objects` to draw. Needs the optional `topojson-client` package |
|
|
26
|
+
| `featureId` | Accessor for each feature's stable id. Default: `feature.id ?? properties.id ?? properties.name`. It is the key of `segments`, and identifies features in hover and click |
|
|
27
|
+
| `color` | `(feature, slice) => number \| undefined` - the value of a choropleth. Features without one use `styles.fill` or their `segments`. `slice` is the [facet panel and timeline step](#facets-and-timeline) it is drawn in |
|
|
28
|
+
| `excludeParts` | `(part, feature) => boolean`, where `part` is `{ centroid, geometry }` - drops geometry parts from drawing and hover (a distant territory drawn in an inset instead). `segments` still apply to what remains |
|
|
29
29
|
|
|
30
30
|
## Styles
|
|
31
31
|
|
|
@@ -146,6 +146,34 @@ An `inset` draws part of the map (or other data) in its own small panel, such as
|
|
|
146
146
|
|
|
147
147
|
`location` is one of the 17 anchors (`'top-left'`, `'bottom-right'`, `'middle-left'`, ...); `features` filters by id from the plot's `data` (or give the inset its own `data`); `featurePartsFilter` narrows to geometry parts; `width` and `height` are fractions of the plot; an inset always fits its own content.
|
|
148
148
|
|
|
149
|
+
## Facets and timeline
|
|
150
|
+
|
|
151
|
+
`facet` and `timeline` slice the map by a field each feature carries in its `properties` (and each marker's own rows by the same field); what lacks the field is drawn in every slice. To draw one layer of features in every panel, with no field on them, name the panels instead: `facet.values` for `by`, `rowValues` and `colValues` for `rows` and `cols`, or `timeline.values` for the steps. A slice's values are the ones named, in that order, then any more found in the features.
|
|
152
|
+
|
|
153
|
+
Every callback that reads a feature gets the slice it is drawn in as its last argument - `color`, `opacity`, `grouping`, `featureId` and `styles.onFeatureClick` - and a tooltip's `format` has it in its context:
|
|
154
|
+
|
|
155
|
+
| Field | Meaning |
|
|
156
|
+
| ------------- | ------------------------------------------------------------------ |
|
|
157
|
+
| `facet.value` | The panel: its `by` value, or a grid's `row · col` |
|
|
158
|
+
| `facet.row` | The panel's `rows` value, on a grid |
|
|
159
|
+
| `facet.col` | The panel's `cols` value, on a grid |
|
|
160
|
+
| `time` | The timeline step; the context is empty without `facet`/`timeline` |
|
|
161
|
+
|
|
162
|
+
```svelte
|
|
163
|
+
<GeoPlot
|
|
164
|
+
data={provinces}
|
|
165
|
+
facet={{ by: 'sex', values: ['Women', 'Men'] }}
|
|
166
|
+
color={(f, { facet }) => valueFor(f.properties.name, facet?.value)}
|
|
167
|
+
tooltip={{
|
|
168
|
+
sync: true,
|
|
169
|
+
format: (f, number, { facet }) => `${f.properties.name} · ${facet?.value}`
|
|
170
|
+
}}
|
|
171
|
+
coordinates={{ projection: { type: 'mercator', domain: 'data' } }}
|
|
172
|
+
/>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The color ramp spans every panel and step, each read in its own slice, so colors compare across them; `facet.scales: 'free'` (or `timeline.scales`, with no facet) fits each slice's own instead, for panels that show different variables. To give each panel its own scheme too, return the panel from `grouping` and set `scales.color` in a `segments` entry per panel. With `projection.domain: 'data'`, each panel fits its own features. See also [Facets](facets.md#maps).
|
|
176
|
+
|
|
149
177
|
## Tooltip and hover
|
|
150
178
|
|
|
151
179
|
`tooltip` works as on other plots with the hovered feature as the row (`format: (feature) => string`, or a `content` snippet). Add `{ type: 'geo-hover' }` for an in-map highlight. Hover matches the browser's own SVG hit-testing, so a feature is hovered where it is drawn.
|
package/docs/legend.md
CHANGED
|
@@ -86,6 +86,12 @@ A continuous section narrows with `interactive: { enabled: true, mode: 'filter'
|
|
|
86
86
|
|
|
87
87
|
Series can be toggled from code too: `useFigure().legend` has `off`, `isOff(name)`, `toggle`, `turnOff`, `turnOn` (see [Reading plot state](state.md)).
|
|
88
88
|
|
|
89
|
+
The range of a continuous section can be set from code as well: `useFigure().legend.setRange([min, max])` does what dragging the handles does - they move, and the plots filter or rescale to it, in the mode the section was set up with (a second argument, `'filter'` or `'rescale'`, overrides it). `setRange(null)`, or a span that covers the whole ramp, resets it; `legend.range` reads it back. A mini legend per panel can use it to apply its own panel's range to the figure's legend on a click.
|
|
90
|
+
|
|
91
|
+
```svelte
|
|
92
|
+
<button onclick={() => figure.legend?.setRange([panelMin, panelMax])}>Use this range</button>
|
|
93
|
+
```
|
|
94
|
+
|
|
89
95
|
## Adjusting the automatic legend
|
|
90
96
|
|
|
91
97
|
An object adjusts the automatic legend while keeping what the plots read:
|
|
@@ -53,7 +53,7 @@ Bars where one axis holds the categories and the other the values. Categories on
|
|
|
53
53
|

|
|
54
54
|
|
|
55
55
|
- Several series sharing a category sit side by side, each in its slice of the category band. The gap between them is `bar.seriesPadding` in the configuration.
|
|
56
|
-
- `transforms={[{ type: 'stackY' }]}` stacks the series instead.
|
|
56
|
+
- `transforms={[{ type: 'stackY' }]}` stacks the series instead of putting them side by side. The stack runs along the value axis: `stackY` for vertical bars (values on `y`), `stackX` for horizontal ones (values on `x`, categories on `y`). The other one turns the category axis into a numeric one and the bars come out empty.
|
|
57
57
|
- `scales.x.paddingInner` / `paddingOuter` set the spacing between category bands.
|
|
58
58
|
- `styles.values.show` labels each bar; `styles.values.anchor` places the label (`'outside'`, `'middle'`, `'top'`, `'bottom'`, `'left'`, `'right'`, a corner such as `'top-left'`, or `'tip'` and `'base'`, the ends of the bar away from zero and on it).
|
|
59
59
|
- The tooltip triggers anywhere over a bar's whole band, not just over the visible bar.
|
package/docs/state.md
CHANGED
|
@@ -7,7 +7,7 @@ A plot's state - its hover, legend, timeline, margins, scales, rows and size - c
|
|
|
7
7
|
Works anywhere inside a figure, including the `header`, `footer` and `content` snippets of a plot or composer. It returns:
|
|
8
8
|
|
|
9
9
|
- `hover` - `activeX`, `activeY`, `activeSeries`, `strategy`, `sync`, `source` (the id of the plot owning the hover).
|
|
10
|
-
- `legend` (or `null`) - `entries`, `range`, `off` (names of hidden series), `isOff(name)`, `turnOff(name, action)`, `turnOn(name)`, `toggle(name, action)`. `action` is `{ mode: 'dim' | 'hide' | 'omit' | 'rescale' }`; `'omit'` removes the series and refits the axes, and `'rescale'` also makes the parts left of a whole (a stack, a treemap) the new 100%.
|
|
10
|
+
- `legend` (or `null`) - `entries`, `range`, `setRange([min, max] | null, mode?)` (narrows the continuous section as dragging it would, or resets it), `off` (names of hidden series), `isOff(name)`, `turnOff(name, action)`, `turnOn(name)`, `toggle(name, action)`. `action` is `{ mode: 'dim' | 'hide' | 'omit' | 'rescale' }`; `'omit'` removes the series and refits the axes, and `'rescale'` also makes the parts left of a whole (a stack, a treemap) the new 100%.
|
|
11
11
|
- `timeline` (or `null`) - `enabled`, `steps`, `value`, `index`, `progress`, `playing`, `loop`, `speed`, `speeds`, `controls`, and `seek`, `seekIndex`, `seekValue`, `next`, `previous`, `play`, `pause`, `toggle`, `setLoop`, `setSpeed`, `cycleSpeed`. On a timeline of dates, `steps` and `value` are `Date`s and `seekValue` takes a `Date` or a date string.
|
|
12
12
|
|
|
13
13
|
A footer that toggles series through the legend:
|
package/docs/timeline.md
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
| `values` | The steps, when not every distinct value of `accessor` - read like `accessor`'s values |
|
|
44
44
|
| `filterMode` | `'exact'` shows the rows of the current step; `'upTo'` shows every row up to it (a cumulative reveal). `LinePlot` defaults to `'upTo'` |
|
|
45
45
|
| `interpolate` | Tween values between steps so a scrub is smooth: `true`, or `{ key, value, create, progressEasing, segmentEasing }`. Line and bar plots default to interpolating their value channel |
|
|
46
|
-
| `scales` | `'shared'` (default): axes span every step, so they hold still while scrubbing; `'free'` refits both to each step; `'free-x'` / `'free-y'` refit one and keep the other still
|
|
46
|
+
| `scales` | `'shared'` (default): axes span every step (the widest any step reaches on its own rows), so they hold still while scrubbing; `'free'` refits both to each step; `'free-x'` / `'free-y'` refit one and keep the other still |
|
|
47
47
|
| `onprogress`, `onvalue` | Called with the new position (`0`-`1`) and with the step it lands on - a `Date` on a timeline of dates, else the number |
|
|
48
48
|
| `orientation`, `show`, `thickness` | A rule's direction (`'vertical'` default, beside the plot; `'horizontal'`), visibility and size |
|
|
49
49
|
|
|
@@ -72,7 +72,7 @@ Both derive rows from the data before drawing. A **transform** reshapes the rows
|
|
|
72
72
|
| Filter | `filter` | Keep rows a test holds for |
|
|
73
73
|
| Select | `select`, `selectFirst`, `selectLast`, `selectMinX`, `selectMaxX`, `selectMinY`, `selectMaxY` | Keep one row per group, such as each series' last point or peak |
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
A bar plot stacks along its value axis: `stackY` for vertical bars, `stackX` for horizontal ones. A transform on a `HistogramPlot` is merged over the plot's default binning, so `{ type: 'binX', thresholds: 20 }` changes the bins and the rest stays.
|
|
76
76
|
|
|
77
77
|
## Statistics
|
|
78
78
|
|