@fundar/data-chart-telling 0.0.26 → 0.0.28
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/dist/charts/BaseChart.svelte +18 -10
- package/dist/charts/BaseChart.svelte.d.ts +4 -2
- package/dist/charts/heatmap/Chart.svelte +2 -2
- package/dist/charts/pyramid/Chart.svelte +2 -2
- package/dist/configuration/config.svelte.js +26 -0
- package/dist/configuration/themes/dark.js +4 -0
- package/dist/configuration/themes/index.d.ts +64 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -0
- package/dist/layout/coordinates/CoordinatesLayout.svelte +1 -1
- package/dist/layout/facet/FacetIndicator.svelte +31 -0
- package/dist/layout/facet/FacetIndicator.svelte.d.ts +8 -0
- package/dist/layout/facet/FacetLayout.svelte +249 -24
- package/dist/layout/facet/FacetLayout.svelte.d.ts +7 -1
- package/dist/layout/legend/ContinuousSection.svelte +1 -1
- package/dist/layout/legend/ContinuousSection.svelte.d.ts +1 -1
- package/dist/layout/legend/DiscreteSection.svelte +1 -1
- package/dist/layout/legend/DiscreteSection.svelte.d.ts +1 -1
- package/dist/layout/legend/LegendLayout.svelte +1 -1
- package/dist/layout/legend/LegendLayout.svelte.d.ts +1 -1
- package/dist/layout/legend/interaction.svelte.d.ts +9 -1
- package/dist/layout/legend/interaction.svelte.js +9 -1
- package/dist/layout/timeline/Timeline.svelte +1 -1
- package/dist/layout/timeline/Timeline.svelte.d.ts +1 -1
- package/dist/layout/timeline/TimelineLayout.svelte +1 -1
- package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
- package/dist/layout/tooltip/controller.svelte.js +16 -2
- package/dist/markers/HoverMarker.svelte +2 -2
- package/dist/plots/bar/Plot.svelte +43 -4
- package/dist/plots/bar/ValueLabels.svelte +69 -23
- package/dist/plots/bar/ValueLabels.svelte.d.ts +10 -1
- package/dist/plots/heatmap/Plot.svelte +4 -4
- package/dist/plots/heatmap/ValueLabels.svelte +3 -3
- package/dist/plots/heatmap/ValueLabels.svelte.d.ts +1 -1
- package/dist/plots/line/Plot.svelte +34 -29
- package/dist/plots/line/ValueLabels.svelte +73 -51
- package/dist/plots/line/ValueLabels.svelte.d.ts +8 -4
- package/dist/plots/pyramid/Plot.svelte +41 -6
- package/dist/plots/pyramid/ValueLabels.svelte +70 -24
- package/dist/plots/pyramid/ValueLabels.svelte.d.ts +10 -1
- package/dist/plots/scatter/Plot.svelte +40 -4
- package/dist/plots/scatter/ValueLabels.svelte +90 -47
- package/dist/plots/scatter/ValueLabels.svelte.d.ts +14 -5
- package/dist/plots/utils/labelOverflow.svelte.d.ts +51 -0
- package/dist/plots/utils/labelOverflow.svelte.js +116 -0
- package/dist/plots/utils/legendDisabled.d.ts +1 -1
- package/dist/types/charts/common.d.ts +3 -50
- package/dist/types/charts/props.d.ts +3 -2
- package/dist/types/configuration/styling.d.ts +19 -0
- package/dist/types/layout/facet.d.ts +41 -0
- package/dist/types/layout/legend.d.ts +65 -1
- package/dist/types/layout/styles.d.ts +1 -12
- package/dist/types/layout/timeline.d.ts +32 -0
- package/dist/types/layout/timeline.js +1 -0
- package/dist/utils/interpolate.d.ts +1 -1
- package/package.json +1 -1
- package/dist/types/charts/legend.d.ts +0 -60
- /package/dist/types/{charts/legend.js → layout/facet.js} +0 -0
|
@@ -1,39 +1,77 @@
|
|
|
1
1
|
<script lang="ts" generics="TData extends Record<string, unknown>">
|
|
2
2
|
import { Text, usePlot } from 'svelteplot';
|
|
3
3
|
import { SvelteMap } from 'svelte/reactivity';
|
|
4
|
+
import { resolveAccessor } from '../utils/accessors';
|
|
5
|
+
import { disabledFillOverride } from '../utils/legendDisabled';
|
|
6
|
+
import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
|
|
4
7
|
import { resolveEdgeAwareAnchor, estimateTextWidth } from '../../utils/edgeAwareAnchor';
|
|
5
|
-
import type {
|
|
8
|
+
import type { Series } from '../../types/plots/data/common';
|
|
6
9
|
import type { ValuesStyle } from '../../types/layout/styles';
|
|
10
|
+
import type { ScatterSegmentStyle } from '../../types/plots/segments/common';
|
|
11
|
+
import type { VisualGroup } from '../../types/plots/segments/config';
|
|
12
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
13
|
+
import type { MarginOverflow } from '../utils/labelOverflow.svelte';
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
|
-
* Renders
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* Renders one series' point value labels — one `<Text>` mark per series
|
|
17
|
+
* (mirrors line/bar/pyramid's own `ValueLabels`), so a legend-disabled or
|
|
18
|
+
* hover-isolated series can carry its own constant `fillOpacity`. Labels
|
|
19
|
+
* the Y value, matching the "Y is the value axis" convention every other
|
|
20
|
+
* plot kind uses. Placement is edge-aware: a label whose default
|
|
14
21
|
* anchor/offset would push it past the plot's own content box flips to
|
|
15
|
-
* the opposite side instead of clipping
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* unresponsiveness when many `<Plot>` instances mount/unmount together
|
|
19
|
-
* (e.g. switching from a multi-story docs page to a single story).
|
|
22
|
+
* the opposite side instead of clipping — `onOverflow` below is a second,
|
|
23
|
+
* coarser line of defense for whatever the flip still can't fit (e.g. a
|
|
24
|
+
* corner point with no room in any direction).
|
|
20
25
|
*/
|
|
21
26
|
let {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
getY,
|
|
25
|
-
fmtVal,
|
|
27
|
+
group,
|
|
28
|
+
formatValue,
|
|
26
29
|
values,
|
|
27
30
|
defaultColor,
|
|
31
|
+
disabled,
|
|
32
|
+
onHoverSeries,
|
|
33
|
+
onHoverEnd,
|
|
34
|
+
onOverflow,
|
|
28
35
|
}: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
getY: (d: TData) => AxisValue;
|
|
32
|
-
fmtVal: (value: number, seriesName: string) => string;
|
|
36
|
+
group: VisualGroup<Series<TData>, ScatterSegmentStyle>;
|
|
37
|
+
formatValue: (value: number, seriesName: string) => string;
|
|
33
38
|
values: ValuesStyle | undefined;
|
|
34
39
|
defaultColor: string;
|
|
40
|
+
/** Resolved legend/hover-isolate dimming style for this group's series, if any. */
|
|
41
|
+
disabled?: LegendDisabledStyle;
|
|
42
|
+
/** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
|
|
43
|
+
onHoverSeries?: () => void;
|
|
44
|
+
onHoverEnd?: () => void;
|
|
45
|
+
/** Reports how far this series' labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
|
|
46
|
+
onOverflow?: (overflow: MarginOverflow) => void;
|
|
35
47
|
} = $props();
|
|
36
48
|
|
|
49
|
+
const data = $derived(group.series.data);
|
|
50
|
+
const getX = $derived(resolveAccessor(group.series.x));
|
|
51
|
+
const getY = $derived(resolveAccessor(group.series.y));
|
|
52
|
+
const plot = usePlot();
|
|
53
|
+
let groupEl: SVGGElement | undefined = $state();
|
|
54
|
+
|
|
55
|
+
watchLabelOverflow({
|
|
56
|
+
el: () => groupEl,
|
|
57
|
+
bounds: () => {
|
|
58
|
+
// getBBox() inside isn't itself tracked — depend on whatever actually
|
|
59
|
+
// changes the rendered label layout so this re-measures.
|
|
60
|
+
void data;
|
|
61
|
+
void values;
|
|
62
|
+
const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
|
|
63
|
+
const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
|
|
64
|
+
if (!xRange || !yRange) return undefined;
|
|
65
|
+
return {
|
|
66
|
+
left: Math.min(xRange[0], xRange[1]),
|
|
67
|
+
right: Math.max(xRange[0], xRange[1]),
|
|
68
|
+
top: Math.min(yRange[0], yRange[1]),
|
|
69
|
+
bottom: Math.max(yRange[0], yRange[1]),
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
onOverflow: (overflow) => onOverflow?.(overflow),
|
|
73
|
+
});
|
|
74
|
+
|
|
37
75
|
const font = $derived(values?.fontStyle);
|
|
38
76
|
const preferredTextAnchor = $derived(
|
|
39
77
|
font?.textAnchor === 'outside' || font?.textAnchor === undefined ? 'start' : font.textAnchor,
|
|
@@ -42,11 +80,10 @@
|
|
|
42
80
|
const gapX = $derived(Math.abs(font?.dx ?? 8));
|
|
43
81
|
const gapY = $derived(Math.abs(font?.dy ?? 0));
|
|
44
82
|
const fontSize = $derived(typeof font?.fontSize === 'number' ? font.fontSize : 11);
|
|
45
|
-
|
|
46
|
-
const plot = usePlot();
|
|
83
|
+
const fillOverride = $derived(disabledFillOverride(disabled));
|
|
47
84
|
|
|
48
85
|
function textOf(d: TData): string {
|
|
49
|
-
return
|
|
86
|
+
return formatValue(Number(getY(d)), group.series.name);
|
|
50
87
|
}
|
|
51
88
|
|
|
52
89
|
/** One placement per row, keyed by row reference — avoids recomputing the same scale/edge lookup for each of the four accessor channels below. */
|
|
@@ -79,28 +116,34 @@
|
|
|
79
116
|
});
|
|
80
117
|
</script>
|
|
81
118
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
119
|
+
<!-- svelteplot's Text mark never wires pointer handlers to its DOM node,
|
|
120
|
+
despite declaring them in its types — a wrapping <g> catches the
|
|
121
|
+
boundary crossing instead (pointerenter/leave fire on every ancestor). -->
|
|
122
|
+
<g bind:this={groupEl} role="presentation" onpointerenter={onHoverSeries} onpointerleave={onHoverEnd}>
|
|
123
|
+
<Text
|
|
124
|
+
{data}
|
|
125
|
+
x={getX}
|
|
126
|
+
y={getY}
|
|
127
|
+
text={(d: TData) => textOf(d)}
|
|
128
|
+
fill={font?.fill ?? defaultColor}
|
|
129
|
+
fillOpacity={fillOverride?.fillOpacity}
|
|
130
|
+
{fontSize}
|
|
131
|
+
fontWeight={font?.fontWeight ?? 500}
|
|
132
|
+
fontStyle={font?.fontStyle}
|
|
133
|
+
textAnchor={(d: TData) => placements.get(d)?.textAnchor ?? preferredTextAnchor}
|
|
134
|
+
lineAnchor={
|
|
135
|
+
// svelteplot's own `lineAnchor` accessor type isn't parameterized over the mark's row type like its siblings are.
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
+
(d: any) => placements.get(d)?.lineAnchor ?? preferredLineAnchor
|
|
138
|
+
}
|
|
139
|
+
dx={(d: TData) => placements.get(d)?.dx ?? gapX}
|
|
140
|
+
dy={(d: TData) => placements.get(d)?.dy ?? gapY}
|
|
141
|
+
lineHeight={font?.lineHeight}
|
|
142
|
+
rotate={font?.rotate}
|
|
143
|
+
stroke={font?.stroke}
|
|
144
|
+
strokeWidth={font?.strokeWidth}
|
|
145
|
+
paintOrder={font?.paintOrder}
|
|
146
|
+
class={font?.class}
|
|
147
|
+
textClass={font?.textClass}
|
|
148
|
+
/>
|
|
149
|
+
</g>
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Series } from '../../types/plots/data/common';
|
|
2
2
|
import type { ValuesStyle } from '../../types/layout/styles';
|
|
3
|
+
import type { ScatterSegmentStyle } from '../../types/plots/segments/common';
|
|
4
|
+
import type { VisualGroup } from '../../types/plots/segments/config';
|
|
5
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
6
|
+
import type { MarginOverflow } from '../utils/labelOverflow.svelte';
|
|
3
7
|
declare function $$render<TData extends Record<string, unknown>>(): {
|
|
4
8
|
props: {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
getY: (d: TData) => AxisValue;
|
|
8
|
-
fmtVal: (value: number, seriesName: string) => string;
|
|
9
|
+
group: VisualGroup<Series<TData>, ScatterSegmentStyle>;
|
|
10
|
+
formatValue: (value: number, seriesName: string) => string;
|
|
9
11
|
values: ValuesStyle | undefined;
|
|
10
12
|
defaultColor: string;
|
|
13
|
+
/** Resolved legend/hover-isolate dimming style for this group's series, if any. */
|
|
14
|
+
disabled?: LegendDisabledStyle;
|
|
15
|
+
/** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
|
|
16
|
+
onHoverSeries?: () => void;
|
|
17
|
+
onHoverEnd?: () => void;
|
|
18
|
+
/** Reports how far this series' labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
|
|
19
|
+
onOverflow?: (overflow: MarginOverflow) => void;
|
|
11
20
|
};
|
|
12
21
|
exports: {};
|
|
13
22
|
bindings: "";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MarginConfig } from '../../types/plots/props';
|
|
2
|
+
/** How far a value label's rendered box extends past the plot's own content box, in pixels, on each side (0 = no overflow). */
|
|
3
|
+
export type MarginOverflow = {
|
|
4
|
+
left: number;
|
|
5
|
+
right: number;
|
|
6
|
+
top: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Watches an SVG group's rendered bounding box against the plot's own
|
|
11
|
+
* content box (`bounds`) and reports how far it overflows on each side.
|
|
12
|
+
* Polled across successive paints until two consecutive readings agree,
|
|
13
|
+
* rather than trusted after a single frame: the group's own resolved
|
|
14
|
+
* geometry (driven by svelteplot's separate reactive graph) and svelteplot's
|
|
15
|
+
* own axis auto-margins can take a variable number of frames to settle —
|
|
16
|
+
* more on a page with many other components competing for layout (e.g. a
|
|
17
|
+
* docs page) than in an isolated story. Measuring too early can catch
|
|
18
|
+
* content still at its pre-layout default position; capped so a layout that
|
|
19
|
+
* genuinely never stabilizes still reports its last reading instead of
|
|
20
|
+
* polling forever. Also waits for web fonts to finish loading, since glyph
|
|
21
|
+
* metrics (and therefore `getBBox()`) can still reflect a fallback font's
|
|
22
|
+
* narrower widths until the real font swaps in.
|
|
23
|
+
*/
|
|
24
|
+
export declare function watchLabelOverflow(args: {
|
|
25
|
+
el: () => SVGGElement | undefined;
|
|
26
|
+
bounds: () => {
|
|
27
|
+
left: number;
|
|
28
|
+
right: number;
|
|
29
|
+
top: number;
|
|
30
|
+
bottom: number;
|
|
31
|
+
} | undefined;
|
|
32
|
+
onOverflow: (overflow: MarginOverflow) => void;
|
|
33
|
+
}): void;
|
|
34
|
+
/**
|
|
35
|
+
* Tracks value-label overflow reported by (possibly several, e.g. one per
|
|
36
|
+
* series) {@link watchLabelOverflow} calls and grows a local margin override
|
|
37
|
+
* to fit it — monotonically, since shrinking it back would re-expose the
|
|
38
|
+
* overflow that caused the growth in the first place, oscillating between
|
|
39
|
+
* "shrink margin → overflow reappears → grow margin → …". Each side is only
|
|
40
|
+
* ever set when the caller's own `margins` prop hasn't already pinned it, so
|
|
41
|
+
* an explicit caller override always wins over this auto-grown one.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createLabelMarginTracker(margins: () => MarginConfig | undefined): {
|
|
44
|
+
report: (overflow: MarginOverflow) => void;
|
|
45
|
+
readonly resolvedMargins: Partial<{
|
|
46
|
+
top: number | "auto";
|
|
47
|
+
right: number | "auto";
|
|
48
|
+
bottom: number | "auto";
|
|
49
|
+
left: number | "auto";
|
|
50
|
+
}> | undefined;
|
|
51
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const NO_OVERFLOW = { left: 0, right: 0, top: 0, bottom: 0 };
|
|
2
|
+
/**
|
|
3
|
+
* Watches an SVG group's rendered bounding box against the plot's own
|
|
4
|
+
* content box (`bounds`) and reports how far it overflows on each side.
|
|
5
|
+
* Polled across successive paints until two consecutive readings agree,
|
|
6
|
+
* rather than trusted after a single frame: the group's own resolved
|
|
7
|
+
* geometry (driven by svelteplot's separate reactive graph) and svelteplot's
|
|
8
|
+
* own axis auto-margins can take a variable number of frames to settle —
|
|
9
|
+
* more on a page with many other components competing for layout (e.g. a
|
|
10
|
+
* docs page) than in an isolated story. Measuring too early can catch
|
|
11
|
+
* content still at its pre-layout default position; capped so a layout that
|
|
12
|
+
* genuinely never stabilizes still reports its last reading instead of
|
|
13
|
+
* polling forever. Also waits for web fonts to finish loading, since glyph
|
|
14
|
+
* metrics (and therefore `getBBox()`) can still reflect a fallback font's
|
|
15
|
+
* narrower widths until the real font swaps in.
|
|
16
|
+
*/
|
|
17
|
+
export function watchLabelOverflow(args) {
|
|
18
|
+
$effect(() => {
|
|
19
|
+
const el = args.el();
|
|
20
|
+
const bounds = args.bounds();
|
|
21
|
+
const reportOverflow = args.onOverflow;
|
|
22
|
+
if (!el || !bounds)
|
|
23
|
+
return;
|
|
24
|
+
const MAX_ATTEMPTS = 30;
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
let frame;
|
|
27
|
+
let attempt = 0;
|
|
28
|
+
let lastReading;
|
|
29
|
+
function measure() {
|
|
30
|
+
if (cancelled)
|
|
31
|
+
return;
|
|
32
|
+
let box;
|
|
33
|
+
try {
|
|
34
|
+
box = el.getBBox();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const overflow = {
|
|
40
|
+
left: Math.ceil(Math.max(0, bounds.left - box.x)),
|
|
41
|
+
right: Math.ceil(Math.max(0, box.x + box.width - bounds.right)),
|
|
42
|
+
top: Math.ceil(Math.max(0, bounds.top - box.y)),
|
|
43
|
+
bottom: Math.ceil(Math.max(0, box.y + box.height - bounds.bottom)),
|
|
44
|
+
};
|
|
45
|
+
attempt += 1;
|
|
46
|
+
const stable = lastReading != null &&
|
|
47
|
+
lastReading.left === overflow.left &&
|
|
48
|
+
lastReading.right === overflow.right &&
|
|
49
|
+
lastReading.top === overflow.top &&
|
|
50
|
+
lastReading.bottom === overflow.bottom;
|
|
51
|
+
lastReading = overflow;
|
|
52
|
+
if (stable || attempt >= MAX_ATTEMPTS) {
|
|
53
|
+
reportOverflow(overflow);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
frame = requestAnimationFrame(measure);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const fontsReady = document.fonts?.ready ?? Promise.resolve();
|
|
60
|
+
fontsReady.then(() => {
|
|
61
|
+
if (cancelled)
|
|
62
|
+
return;
|
|
63
|
+
frame = requestAnimationFrame(measure);
|
|
64
|
+
});
|
|
65
|
+
return () => {
|
|
66
|
+
cancelled = true;
|
|
67
|
+
if (frame !== undefined)
|
|
68
|
+
cancelAnimationFrame(frame);
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Tracks value-label overflow reported by (possibly several, e.g. one per
|
|
74
|
+
* series) {@link watchLabelOverflow} calls and grows a local margin override
|
|
75
|
+
* to fit it — monotonically, since shrinking it back would re-expose the
|
|
76
|
+
* overflow that caused the growth in the first place, oscillating between
|
|
77
|
+
* "shrink margin → overflow reappears → grow margin → …". Each side is only
|
|
78
|
+
* ever set when the caller's own `margins` prop hasn't already pinned it, so
|
|
79
|
+
* an explicit caller override always wins over this auto-grown one.
|
|
80
|
+
*/
|
|
81
|
+
export function createLabelMarginTracker(margins) {
|
|
82
|
+
let measured = $state(NO_OVERFLOW);
|
|
83
|
+
function report(overflow) {
|
|
84
|
+
const next = {
|
|
85
|
+
left: Math.max(measured.left, overflow.left),
|
|
86
|
+
right: Math.max(measured.right, overflow.right),
|
|
87
|
+
top: Math.max(measured.top, overflow.top),
|
|
88
|
+
bottom: Math.max(measured.bottom, overflow.bottom),
|
|
89
|
+
};
|
|
90
|
+
if (next.left !== measured.left ||
|
|
91
|
+
next.right !== measured.right ||
|
|
92
|
+
next.top !== measured.top ||
|
|
93
|
+
next.bottom !== measured.bottom) {
|
|
94
|
+
measured = next;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const resolvedMargins = $derived.by(() => {
|
|
98
|
+
const base = margins();
|
|
99
|
+
if (measured.left <= 0 && measured.right <= 0 && measured.top <= 0 && measured.bottom <= 0)
|
|
100
|
+
return base;
|
|
101
|
+
const out = { ...base };
|
|
102
|
+
if (measured.right > 0 && base?.right === undefined)
|
|
103
|
+
out.right = measured.right;
|
|
104
|
+
if (measured.left > 0 && base?.left === undefined)
|
|
105
|
+
out.left = measured.left;
|
|
106
|
+
if (measured.top > 0 && base?.top === undefined)
|
|
107
|
+
out.top = measured.top;
|
|
108
|
+
if (measured.bottom > 0 && base?.bottom === undefined)
|
|
109
|
+
out.bottom = measured.bottom;
|
|
110
|
+
return out;
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
report,
|
|
114
|
+
get resolvedMargins() { return resolvedMargins; },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LegendDisabledStyle } from '../../types/
|
|
1
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
2
2
|
import type { StrokeStyle, DotStyle } from '../../types/plots/styling';
|
|
3
3
|
/**
|
|
4
4
|
* The three per-mark override layers a legend "disabled" state can apply,
|
|
@@ -1,66 +1,19 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import type { EasingName, EasingFn } from './interpolate';
|
|
5
|
-
export type TimeValue = number;
|
|
6
|
-
export type Orientation = 'horizontal' | 'vertical';
|
|
7
|
-
export type FacetColumns = number | {
|
|
8
|
-
desktop: number;
|
|
9
|
-
mobile: number;
|
|
10
|
-
};
|
|
2
|
+
import type { TimeValue } from '../layout/timeline';
|
|
3
|
+
import type { LegendPlotContext } from '../layout/legend';
|
|
11
4
|
export type ChartBoxContext = {
|
|
12
5
|
width: number;
|
|
13
6
|
height: number;
|
|
14
7
|
};
|
|
15
8
|
export type ChartBoxSnippet = Snippet<[ChartBoxContext]>;
|
|
16
|
-
export type FacetCellContext<TRow extends Record<string, unknown>> = {
|
|
17
|
-
facetValue: string;
|
|
18
|
-
data: TRow[];
|
|
19
|
-
width: number;
|
|
20
|
-
height: number;
|
|
21
|
-
};
|
|
22
|
-
export type FacetCellSnippet<TRow extends Record<string, unknown>> = Snippet<[
|
|
23
|
-
FacetCellContext<TRow>
|
|
24
|
-
]>;
|
|
25
|
-
export type TimelineChildContext<TRow extends Record<string, unknown>> = {
|
|
26
|
-
selectedValue: TimeValue;
|
|
27
|
-
data: TRow[];
|
|
28
|
-
width: number;
|
|
29
|
-
height: number;
|
|
30
|
-
};
|
|
31
|
-
export type TimelineChildSnippet<TRow extends Record<string, unknown>> = Snippet<[
|
|
32
|
-
TimelineChildContext<TRow>
|
|
33
|
-
]>;
|
|
34
9
|
export type ChartPlotContext<TRow extends Record<string, unknown>> = {
|
|
35
10
|
data: TRow[];
|
|
36
11
|
width: number;
|
|
37
12
|
height: number;
|
|
38
13
|
facetValue?: string;
|
|
39
14
|
selectedValue?: TimeValue;
|
|
15
|
+
legend?: LegendPlotContext;
|
|
40
16
|
};
|
|
41
17
|
export type ChartPlotSnippet<TRow extends Record<string, unknown>> = Snippet<[
|
|
42
18
|
ChartPlotContext<TRow>
|
|
43
19
|
]>;
|
|
44
|
-
export type TimelineInterpolation<TRow extends Record<string, unknown>> = {
|
|
45
|
-
key?: Accessor<TRow, unknown>;
|
|
46
|
-
value?: Accessor<TRow, AxisValue>;
|
|
47
|
-
create?: (lower: TRow, upper: TRow, x: TimeValue, value: number) => TRow;
|
|
48
|
-
progressEasing?: EasingName | EasingFn;
|
|
49
|
-
segmentEasing?: EasingName | EasingFn;
|
|
50
|
-
};
|
|
51
|
-
export type TimelineConfig<TRow extends Record<string, unknown>> = {
|
|
52
|
-
accessor: Accessor<TRow, TimeValue>;
|
|
53
|
-
progress: number;
|
|
54
|
-
values?: TimeValue[];
|
|
55
|
-
orientation?: Orientation;
|
|
56
|
-
show?: boolean;
|
|
57
|
-
thickness?: number;
|
|
58
|
-
filterMode?: 'exact' | 'upTo';
|
|
59
|
-
interpolate?: boolean | TimelineInterpolation<TRow>;
|
|
60
|
-
};
|
|
61
|
-
export type FacetConfig<TRow extends Record<string, unknown>> = {
|
|
62
|
-
by: Accessor<TRow, unknown>;
|
|
63
|
-
columns?: FacetColumns;
|
|
64
|
-
values?: string[];
|
|
65
|
-
gap?: number;
|
|
66
|
-
};
|
|
@@ -5,8 +5,9 @@ import type { BasePlotStylesConfig } from '../layout/styles';
|
|
|
5
5
|
import type { SegmentsConfig, MarginConfig } from '../plots/props';
|
|
6
6
|
import type { AxisBasedMarkersConfig } from '../markers/common';
|
|
7
7
|
import type { TooltipProp } from '../layout/tooltip';
|
|
8
|
-
import type { LegendSection } from '
|
|
9
|
-
import type { FacetConfig
|
|
8
|
+
import type { LegendSection } from '../layout/legend';
|
|
9
|
+
import type { FacetConfig } from '../layout/facet';
|
|
10
|
+
import type { TimelineConfig } from '../layout/timeline';
|
|
10
11
|
/**
|
|
11
12
|
* Common props accepted by every chart component (bar, line, heatmap, pyramid,
|
|
12
13
|
* …). The `TSegmentStyle` parameter lets each chart narrow `segments` to its
|
|
@@ -118,6 +118,25 @@ export type ChartConfig = {
|
|
|
118
118
|
font: FontStyle;
|
|
119
119
|
area: AreaStyle;
|
|
120
120
|
};
|
|
121
|
+
facet: {
|
|
122
|
+
/** Position badge shown in `carousel`/`paginated` facet modes. */
|
|
123
|
+
indicator: {
|
|
124
|
+
background: string;
|
|
125
|
+
color: string;
|
|
126
|
+
fontSize: string;
|
|
127
|
+
padding: string;
|
|
128
|
+
radius: string;
|
|
129
|
+
};
|
|
130
|
+
/** Prev/next buttons shown in `paginated` facet mode. */
|
|
131
|
+
navButton: {
|
|
132
|
+
background: string;
|
|
133
|
+
color: string;
|
|
134
|
+
/** Button diameter (width and height). */
|
|
135
|
+
size: string;
|
|
136
|
+
fontSize: string;
|
|
137
|
+
disabledOpacity: number;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
121
140
|
};
|
|
122
141
|
/** Deep-partial input accepted by `setConfiguration`. */
|
|
123
142
|
export type ChartConfigInput = DeepPartial<ChartConfig>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Accessor } from '../plots/data/common';
|
|
3
|
+
export type Responsive<T> = T | {
|
|
4
|
+
desktop: T;
|
|
5
|
+
mobile: T;
|
|
6
|
+
};
|
|
7
|
+
export type FacetColumns = Responsive<number>;
|
|
8
|
+
export type FacetMode = 'grid' | 'carousel' | 'paginated';
|
|
9
|
+
export type FacetCellContext<TRow extends Record<string, unknown>> = {
|
|
10
|
+
facetValue: string;
|
|
11
|
+
data: TRow[];
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
export type FacetCellSnippet<TRow extends Record<string, unknown>> = Snippet<[
|
|
16
|
+
FacetCellContext<TRow>
|
|
17
|
+
]>;
|
|
18
|
+
export type FacetIndicatorContext = {
|
|
19
|
+
current: number;
|
|
20
|
+
total: number;
|
|
21
|
+
};
|
|
22
|
+
export type FacetIndicatorSnippet = Snippet<[FacetIndicatorContext]>;
|
|
23
|
+
export type FacetNavDirection = 'prev' | 'next';
|
|
24
|
+
export type FacetNavContext = {
|
|
25
|
+
direction: FacetNavDirection;
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
onclick: () => void;
|
|
28
|
+
};
|
|
29
|
+
export type FacetNavSnippet = Snippet<[FacetNavContext]>;
|
|
30
|
+
export type FacetConfig<TRow extends Record<string, unknown>> = {
|
|
31
|
+
by: Accessor<TRow, unknown>;
|
|
32
|
+
columns?: FacetColumns;
|
|
33
|
+
values?: string[];
|
|
34
|
+
gap?: number;
|
|
35
|
+
mode?: Responsive<FacetMode>;
|
|
36
|
+
visible?: number;
|
|
37
|
+
pageSize?: number;
|
|
38
|
+
formatIndicator?: (current: number, total: number) => string;
|
|
39
|
+
indicator?: FacetIndicatorSnippet;
|
|
40
|
+
navButton?: FacetNavSnippet;
|
|
41
|
+
};
|
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
import type { SvelteMap } from 'svelte/reactivity';
|
|
2
|
-
import type {
|
|
2
|
+
import type { SymbolType, LineStyle } from '../plots/constants';
|
|
3
|
+
import type { StrokeStyle, DotStyle } from '../plots/styling';
|
|
4
|
+
export type LegendSectionType = 'discrete' | 'continuous';
|
|
5
|
+
/**
|
|
6
|
+
* Style applied to whatever a legend interaction has turned "off" — a
|
|
7
|
+
* toggled-off discrete series, or a datapoint/cell/feature outside an active
|
|
8
|
+
* continuous range filter. `opacity` is a blanket convenience: any
|
|
9
|
+
* `*Opacity` field a consumer doesn't set falls back to it, so
|
|
10
|
+
* `{ opacity: 0.1 }` alone is enough to read as "disabled" everywhere.
|
|
11
|
+
*/
|
|
12
|
+
export type LegendDisabledStyle = {
|
|
13
|
+
opacity?: number;
|
|
14
|
+
stroke?: StrokeStyle;
|
|
15
|
+
dots?: DotStyle;
|
|
16
|
+
fill?: string;
|
|
17
|
+
fillOpacity?: number;
|
|
18
|
+
};
|
|
19
|
+
export type LegendItem = {
|
|
20
|
+
name: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
color?: string;
|
|
23
|
+
symbol?: SymbolType;
|
|
24
|
+
opacity?: number;
|
|
25
|
+
lineStyle?: LineStyle;
|
|
26
|
+
/** Initial toggled-off state (uncontrolled — the user can still click it back on). */
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type DiscreteLegendSection = {
|
|
30
|
+
type: 'discrete';
|
|
31
|
+
title?: string;
|
|
32
|
+
items: LegendItem[];
|
|
33
|
+
columns?: number;
|
|
34
|
+
/** Enables click-to-toggle on each item. */
|
|
35
|
+
interactive?: boolean;
|
|
36
|
+
/** Style applied to a toggled-off item's series. Falls back to `cfg.legend.disabledOpacity`. */
|
|
37
|
+
disabledStyle?: LegendDisabledStyle;
|
|
38
|
+
};
|
|
39
|
+
export type ContinuousLegendSection = {
|
|
40
|
+
type: 'continuous';
|
|
41
|
+
title?: string;
|
|
42
|
+
columns?: number;
|
|
43
|
+
min: number;
|
|
44
|
+
max: number;
|
|
45
|
+
colorMin?: string;
|
|
46
|
+
colorMax?: string;
|
|
47
|
+
ticks?: number[];
|
|
48
|
+
width?: number;
|
|
49
|
+
format?: (v: number) => string;
|
|
50
|
+
/** Enables drag handles on the bar's edges to narrow the visible range. */
|
|
51
|
+
interactive?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* How the color ramp responds to a narrowed range: `'rescale'` reprojects
|
|
54
|
+
* it onto the new bounds, `'filter'` keeps the original domain and only
|
|
55
|
+
* excludes out-of-range values. Defaults to `'filter'`.
|
|
56
|
+
*/
|
|
57
|
+
mode?: 'rescale' | 'filter';
|
|
58
|
+
/** Style applied to values outside the active range. Falls back to `cfg.legend.disabledOpacity`. */
|
|
59
|
+
disabledStyle?: LegendDisabledStyle;
|
|
60
|
+
};
|
|
61
|
+
export type LegendSection = DiscreteLegendSection | ContinuousLegendSection;
|
|
3
62
|
/**
|
|
4
63
|
* Shared legend-interaction state for cross-cutting legend → plot wiring.
|
|
5
64
|
*
|
|
@@ -24,3 +83,8 @@ export type LegendInteractionStore = {
|
|
|
24
83
|
disabledStyle: LegendDisabledStyle;
|
|
25
84
|
} | null;
|
|
26
85
|
};
|
|
86
|
+
/** Read-only view of which series a legend has toggled off, resolved from {@link BaseChart}'s legend-interaction store. */
|
|
87
|
+
export type LegendPlotContext = {
|
|
88
|
+
disabledSeries: ReadonlySet<string>;
|
|
89
|
+
isDisabled: (name: string) => boolean;
|
|
90
|
+
};
|
|
@@ -12,20 +12,9 @@ import type { GeoFeature } from '../plots/data/geo';
|
|
|
12
12
|
export type ValuesStyle = {
|
|
13
13
|
show?: boolean;
|
|
14
14
|
anchor?: ValueAnchor;
|
|
15
|
-
/** Called with the numeric value and the series name. Return a string to render. */
|
|
16
15
|
format?: (value: number, seriesName: string) => string;
|
|
17
|
-
|
|
16
|
+
hoverIsolate?: boolean;
|
|
18
17
|
fontStyle?: FontStyle;
|
|
19
|
-
/**
|
|
20
|
-
* Style for the leader line drawn from a label displaced to clear an
|
|
21
|
-
* overlap with another series' label, back to its real data point.
|
|
22
|
-
* Labels only ever move when they'd otherwise collide — this is a no-op
|
|
23
|
-
* style until that happens. Falls back to a neutral grey, thin, solid
|
|
24
|
-
* stroke — deliberately not the series' own colour or stroke width, since
|
|
25
|
-
* matching the data line reads as more data rather than as a leader,
|
|
26
|
-
* right where several lines are already converging. Unset fields keep
|
|
27
|
-
* that fallback.
|
|
28
|
-
*/
|
|
29
18
|
strokeStyle?: StrokeStyle;
|
|
30
19
|
};
|
|
31
20
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { AxisValue } from './scales';
|
|
3
|
+
import type { Accessor } from '../plots/data/common';
|
|
4
|
+
import type { EasingName, EasingFn } from '../charts/interpolate';
|
|
5
|
+
export type TimeValue = number;
|
|
6
|
+
export type Orientation = 'horizontal' | 'vertical';
|
|
7
|
+
export type TimelineChildContext<TRow extends Record<string, unknown>> = {
|
|
8
|
+
selectedValue: TimeValue;
|
|
9
|
+
data: TRow[];
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
export type TimelineChildSnippet<TRow extends Record<string, unknown>> = Snippet<[
|
|
14
|
+
TimelineChildContext<TRow>
|
|
15
|
+
]>;
|
|
16
|
+
export type TimelineInterpolation<TRow extends Record<string, unknown>> = {
|
|
17
|
+
key?: Accessor<TRow, unknown>;
|
|
18
|
+
value?: Accessor<TRow, AxisValue>;
|
|
19
|
+
create?: (lower: TRow, upper: TRow, x: TimeValue, value: number) => TRow;
|
|
20
|
+
progressEasing?: EasingName | EasingFn;
|
|
21
|
+
segmentEasing?: EasingName | EasingFn;
|
|
22
|
+
};
|
|
23
|
+
export type TimelineConfig<TRow extends Record<string, unknown>> = {
|
|
24
|
+
accessor: Accessor<TRow, TimeValue>;
|
|
25
|
+
progress: number;
|
|
26
|
+
values?: TimeValue[];
|
|
27
|
+
orientation?: Orientation;
|
|
28
|
+
show?: boolean;
|
|
29
|
+
thickness?: number;
|
|
30
|
+
filterMode?: 'exact' | 'upTo';
|
|
31
|
+
interpolate?: boolean | TimelineInterpolation<TRow>;
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|