@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,4 +1,4 @@
|
|
|
1
|
-
import type { FacetColumns, FacetCellSnippet } from '../../types/
|
|
1
|
+
import type { FacetColumns, FacetMode, Responsive, FacetCellSnippet, FacetIndicatorSnippet, FacetNavSnippet } from '../../types/layout/facet';
|
|
2
2
|
import type { Accessor } from '../../types/plots/data/common';
|
|
3
3
|
declare function $$render<TRow extends Record<string, unknown>>(): {
|
|
4
4
|
props: {
|
|
@@ -7,6 +7,12 @@ declare function $$render<TRow extends Record<string, unknown>>(): {
|
|
|
7
7
|
columns?: FacetColumns;
|
|
8
8
|
values?: string[];
|
|
9
9
|
gap?: number;
|
|
10
|
+
mode?: Responsive<FacetMode>;
|
|
11
|
+
visible?: number;
|
|
12
|
+
pageSize?: number;
|
|
13
|
+
formatIndicator?: (current: number, total: number) => string;
|
|
14
|
+
indicator?: FacetIndicatorSnippet;
|
|
15
|
+
navButton?: FacetNavSnippet;
|
|
10
16
|
width: number;
|
|
11
17
|
height: number;
|
|
12
18
|
cell: FacetCellSnippet<TRow>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
|
-
import type { ContinuousLegendSection } from '../../types/
|
|
3
|
+
import type { ContinuousLegendSection } from '../../types/layout/legend';
|
|
4
4
|
import { makeColorScale } from '../../utils/color';
|
|
5
5
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
6
6
|
import { getLegendInteraction, createLegendInteraction } from './interaction.svelte';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
4
4
|
import { paletteColor } from '../../utils/color';
|
|
5
5
|
import { getLegendInteraction, createLegendInteraction } from './interaction.svelte';
|
|
6
|
-
import type { DiscreteLegendSection, LegendItem } from '../../types/
|
|
6
|
+
import type { DiscreteLegendSection, LegendItem } from '../../types/layout/legend';
|
|
7
7
|
import type { LineStyle } from '../../types/plots/constants';
|
|
8
8
|
|
|
9
9
|
let { section }: { section: DiscreteLegendSection } = $props();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { LegendSection } from '../../types/
|
|
2
|
+
import type { LegendSection } from '../../types/layout/legend';
|
|
3
3
|
import DiscreteSection from './DiscreteSection.svelte';
|
|
4
4
|
import ContinuousSection from './ContinuousSection.svelte';
|
|
5
5
|
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { LegendInteractionStore } from '../../types/layout/legend';
|
|
2
2
|
/** Create + register the shared legend-interaction store. Call during component init. */
|
|
3
3
|
export declare function provideLegendInteraction(): LegendInteractionStore;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Read the shared legend-interaction store, or `null` when there is no provider.
|
|
6
|
+
*
|
|
7
|
+
* Only resolves correctly when called during a real descendant component's
|
|
8
|
+
* own `<script>` instantiation, not from inside a `{@render}`ed snippet body
|
|
9
|
+
* — snippets run in their author's scope rather than as a fresh component
|
|
10
|
+
* instance. `BaseChart`'s `plot` snippet gets the same data via its `legend`
|
|
11
|
+
* context field instead, which works from either scope.
|
|
12
|
+
*/
|
|
5
13
|
export declare function getLegendInteraction(): LegendInteractionStore | null;
|
|
6
14
|
/** A private legend-interaction store for a legend/plot used outside of a {@link BaseChart}. */
|
|
7
15
|
export declare function createLegendInteraction(): LegendInteractionStore;
|
|
@@ -13,7 +13,15 @@ export function provideLegendInteraction() {
|
|
|
13
13
|
setContext(LEGEND_INTERACTION_KEY, store);
|
|
14
14
|
return store;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Read the shared legend-interaction store, or `null` when there is no provider.
|
|
18
|
+
*
|
|
19
|
+
* Only resolves correctly when called during a real descendant component's
|
|
20
|
+
* own `<script>` instantiation, not from inside a `{@render}`ed snippet body
|
|
21
|
+
* — snippets run in their author's scope rather than as a fresh component
|
|
22
|
+
* instance. `BaseChart`'s `plot` snippet gets the same data via its `legend`
|
|
23
|
+
* context field instead, which works from either scope.
|
|
24
|
+
*/
|
|
17
25
|
export function getLegendInteraction() {
|
|
18
26
|
return getContext(LEGEND_INTERACTION_KEY) ?? null;
|
|
19
27
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Plot, RuleX, RuleY, Text } from 'svelteplot';
|
|
3
|
-
import type { Orientation, TimeValue } from '../../types/
|
|
3
|
+
import type { Orientation, TimeValue } from '../../types/layout/timeline';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Purely visual time axis: a baseline rule plus the ordered step labels with
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Orientation, TimeValue, TimelineChildSnippet, TimelineInterpolation } from '../../types/
|
|
1
|
+
import type { Orientation, TimeValue, TimelineChildSnippet, TimelineInterpolation } from '../../types/layout/timeline';
|
|
2
2
|
import type { Accessor } from '../../types/plots/data/common';
|
|
3
3
|
declare function $$render<TRow extends Record<string, unknown>>(): {
|
|
4
4
|
props: {
|
|
@@ -47,12 +47,26 @@ export function createHoverController(args) {
|
|
|
47
47
|
* point at the row under the cursor rather than an arbitrary matched
|
|
48
48
|
* sibling. `BasePlotLayout` gates actual display with
|
|
49
49
|
* `impliesCrosshairX`/`Y` + the marker's own `ruleX`/`ruleY` props.
|
|
50
|
+
*
|
|
51
|
+
* `activeSeries` alone can't disambiguate two matched rows that share the
|
|
52
|
+
* same (or absent) series tag — e.g. an ungrouped bar/line chart where two
|
|
53
|
+
* categories tie on the matched axis value. `onPointer` sets `activeX`/
|
|
54
|
+
* `activeY` straight from the hit row, so when more than one series-tied
|
|
55
|
+
* candidate remains, narrowing to the one whose own x/y equals the hit
|
|
56
|
+
* row's picks the actual row under the cursor instead of an arbitrary tie.
|
|
50
57
|
*/
|
|
51
58
|
const activePoint = $derived.by(() => {
|
|
52
59
|
if (matched.length === 0)
|
|
53
60
|
return null;
|
|
54
|
-
const
|
|
55
|
-
|
|
61
|
+
const h = args.hover();
|
|
62
|
+
const bySeries = matched.filter((p) => p.series === h.activeSeries);
|
|
63
|
+
const candidates = bySeries.length > 0 ? bySeries : matched;
|
|
64
|
+
if (candidates.length === 1)
|
|
65
|
+
return candidates[0];
|
|
66
|
+
const { activeX, activeY } = h;
|
|
67
|
+
if (activeX == null || activeY == null)
|
|
68
|
+
return candidates[0];
|
|
69
|
+
return candidates.find((p) => eq(p.x, activeX) && eq(p.y, activeY)) ?? candidates[0];
|
|
56
70
|
});
|
|
57
71
|
// Whether the current strategy implies a crosshair rule on each axis by default.
|
|
58
72
|
const _impliesCrosshairX = $derived(impliesCrosshairX(args.hover().strategy));
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
{/if}
|
|
103
103
|
|
|
104
104
|
{#if data.length > 0}
|
|
105
|
-
{#each data as point (point.series ??
|
|
105
|
+
{#each data as point (`${point.series ?? ''}_${point.x}_${point.y}`)}
|
|
106
106
|
<!-- Pixel nudge for plot kinds whose series position isn't fully captured
|
|
107
107
|
by x/y alone (e.g. BarPlot's 'grouped' layout) — see HoverDisplayPoint.dx/dy. -->
|
|
108
108
|
<g transform="translate({point.dx ?? 0}, {point.dy ?? 0})">
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
</g>
|
|
121
121
|
{/each}
|
|
122
122
|
{#if showLabels}
|
|
123
|
-
{#each data as point (point.series ??
|
|
123
|
+
{#each data as point (`${point.series ?? ''}_${point.x}_${point.y}`)}
|
|
124
124
|
<g transform="translate({point.dx ?? 0}, {point.dy ?? 0})">
|
|
125
125
|
<TextMarker
|
|
126
126
|
data={[point]}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
11
11
|
import { buildGroupedSeries, validateSegments, buildScopedMarkerGroups } from '../utils/segments';
|
|
12
12
|
import { createBarLayout } from './layout.svelte';
|
|
13
|
+
import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
|
|
13
14
|
import { buildHoverPoints } from './hoverPoints';
|
|
14
15
|
import { buildBarMarkers } from './buildBarMarkers';
|
|
15
16
|
import ValueLabels from './ValueLabels.svelte';
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
import type { BasePlotStylesConfig } from '../../types/layout/styles';
|
|
23
24
|
import type { ValueAnchor } from '../../types/plots/constants';
|
|
24
25
|
import type { BarSegmentStyle } from '../../types/plots/segments/common';
|
|
26
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* A bar chart, vertical by default. Accepts either a pre-built `series`
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
buildGroupedSeries<Series<TData>, BarSegmentStyle>(resolvedSeries, segments, false),
|
|
64
66
|
);
|
|
65
67
|
const showVals = $derived(styles.values?.show ?? false);
|
|
66
|
-
const
|
|
68
|
+
const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
|
|
67
69
|
const valAnchor = $derived<ValueAnchor>(styles.values?.anchor ?? 'outside');
|
|
68
70
|
|
|
69
71
|
// `xAcc`/`yAcc` are always data-semantic (category/value) — independent of
|
|
@@ -153,6 +155,23 @@
|
|
|
153
155
|
// `row` (see BasePlotLayout/HoverPoint) is what lets the template recover the
|
|
154
156
|
// semantic category regardless of which axis it landed on.
|
|
155
157
|
const legendInteraction = $derived(getLegendInteraction());
|
|
158
|
+
const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
|
|
159
|
+
|
|
160
|
+
// A value label hovered while `styles.values.hoverIsolate` is on dims
|
|
161
|
+
// every other series exactly like a legend toggle would — local to this
|
|
162
|
+
// one `<Plot>` instance (not the shared HoverStore) so the gesture can't
|
|
163
|
+
// leak into unrelated tooltip/crosshair behavior.
|
|
164
|
+
let hoverIsolatedSeries = $state<string | null>(null);
|
|
165
|
+
|
|
166
|
+
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
167
|
+
return (
|
|
168
|
+
legendInteraction?.disabledSeries.get(name) ??
|
|
169
|
+
(hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
|
|
170
|
+
? { opacity: cfg.legend.disabledOpacity }
|
|
171
|
+
: undefined)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
156
175
|
// A legend-disabled series is dimmed on the bar itself, but never surfaces
|
|
157
176
|
// in hover/tooltip — its points are simply absent from the candidate list.
|
|
158
177
|
const hoverPoints = $derived(
|
|
@@ -178,6 +197,13 @@
|
|
|
178
197
|
// comment.
|
|
179
198
|
pointMatching: 'contains' as const,
|
|
180
199
|
});
|
|
200
|
+
|
|
201
|
+
// A bar's value label can grow past the plot's own content box (e.g. a
|
|
202
|
+
// tall bar's `anchor: 'outside'` label pushing past the top margin) —
|
|
203
|
+
// reserving real space there is what each label's own overflow
|
|
204
|
+
// measurement feeds into here, via a tracker local to this one `<Plot>`
|
|
205
|
+
// instance. See `labelOverflow.svelte.ts`.
|
|
206
|
+
const labelMargin = createLabelMarginTracker(() => margins);
|
|
181
207
|
</script>
|
|
182
208
|
|
|
183
209
|
<BasePlotLayout
|
|
@@ -189,7 +215,7 @@
|
|
|
189
215
|
getY={plotGetY}
|
|
190
216
|
{hover}
|
|
191
217
|
scales={resolvedScales}
|
|
192
|
-
{
|
|
218
|
+
margins={labelMargin.resolvedMargins}
|
|
193
219
|
xTickRotate={resolvedScales?.x?.tickRotate}
|
|
194
220
|
markers={({ setHoverMatch, clearHoverMatch }) => [
|
|
195
221
|
...buildBarMarkers({
|
|
@@ -198,7 +224,7 @@
|
|
|
198
224
|
isHorizontal,
|
|
199
225
|
hoverPoints,
|
|
200
226
|
palette: cfg.palette,
|
|
201
|
-
disabledFillFor: (name) => disabledFillOverride(
|
|
227
|
+
disabledFillFor: (name) => disabledFillOverride(effectiveDisabledFor(name)),
|
|
202
228
|
active: hover.active,
|
|
203
229
|
setHoverMatch,
|
|
204
230
|
clearHoverMatch,
|
|
@@ -210,7 +236,20 @@
|
|
|
210
236
|
>
|
|
211
237
|
{#if showVals}
|
|
212
238
|
{#each groupedSeries as group, seriesIndex (group.series.name)}
|
|
213
|
-
<ValueLabels
|
|
239
|
+
<ValueLabels
|
|
240
|
+
{group}
|
|
241
|
+
{seriesIndex}
|
|
242
|
+
{barLayout}
|
|
243
|
+
{isHorizontal}
|
|
244
|
+
{valAnchor}
|
|
245
|
+
{formatValue}
|
|
246
|
+
values={styles.values}
|
|
247
|
+
axisColor={cfg.axis.color}
|
|
248
|
+
disabled={effectiveDisabledFor(group.series.name)}
|
|
249
|
+
onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
|
|
250
|
+
onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
|
|
251
|
+
onOverflow={labelMargin.report}
|
|
252
|
+
/>
|
|
214
253
|
{/each}
|
|
215
254
|
{/if}
|
|
216
255
|
</BasePlotLayout>
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<script lang="ts" generics="TData extends Record<string, unknown>">
|
|
2
|
-
import { Text } from 'svelteplot';
|
|
2
|
+
import { Text, usePlot } from 'svelteplot';
|
|
3
3
|
import { resolveAccessor } from '../utils/accessors';
|
|
4
|
+
import { disabledFillOverride } from '../utils/legendDisabled';
|
|
5
|
+
import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
|
|
4
6
|
import type { Series } from '../../types/plots/data/common';
|
|
5
7
|
import type { ValuesStyle } from '../../types/layout/styles';
|
|
6
8
|
import type { ValueAnchor } from '../../types/plots/constants';
|
|
7
9
|
import type { BarSegmentStyle } from '../../types/plots/segments/common';
|
|
8
10
|
import type { VisualGroup } from '../../types/plots/segments/config';
|
|
11
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
12
|
+
import type { MarginOverflow } from '../utils/labelOverflow.svelte';
|
|
9
13
|
import type { BarLayout } from './layout.svelte';
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -21,20 +25,56 @@
|
|
|
21
25
|
barLayout,
|
|
22
26
|
isHorizontal,
|
|
23
27
|
valAnchor,
|
|
24
|
-
|
|
28
|
+
formatValue,
|
|
25
29
|
values,
|
|
26
30
|
axisColor,
|
|
31
|
+
disabled,
|
|
32
|
+
onHoverSeries,
|
|
33
|
+
onHoverEnd,
|
|
34
|
+
onOverflow,
|
|
27
35
|
}: {
|
|
28
36
|
group: VisualGroup<Series<TData>, BarSegmentStyle>;
|
|
29
37
|
seriesIndex: number;
|
|
30
38
|
barLayout: BarLayout<TData>;
|
|
31
39
|
isHorizontal: boolean;
|
|
32
40
|
valAnchor: ValueAnchor;
|
|
33
|
-
|
|
41
|
+
formatValue: (value: number, seriesName: string) => string;
|
|
34
42
|
values: ValuesStyle | undefined;
|
|
35
43
|
axisColor: string;
|
|
44
|
+
/** Resolved legend/hover-isolate dimming style for this group's series, if any. */
|
|
45
|
+
disabled?: LegendDisabledStyle;
|
|
46
|
+
/** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
|
|
47
|
+
onHoverSeries?: () => void;
|
|
48
|
+
onHoverEnd?: () => void;
|
|
49
|
+
/** Reports how far this label overflows the plot's own content box, in pixels, on each side (0 = no overflow). */
|
|
50
|
+
onOverflow?: (overflow: MarginOverflow) => void;
|
|
36
51
|
} = $props();
|
|
37
52
|
|
|
53
|
+
const fillOverride = $derived(disabledFillOverride(disabled));
|
|
54
|
+
const plot = usePlot();
|
|
55
|
+
let groupEl: SVGGElement | undefined = $state();
|
|
56
|
+
|
|
57
|
+
watchLabelOverflow({
|
|
58
|
+
el: () => groupEl,
|
|
59
|
+
bounds: () => {
|
|
60
|
+
// getBBox() inside isn't itself tracked — depend on whatever actually
|
|
61
|
+
// changes the rendered label layout so this re-measures.
|
|
62
|
+
void group;
|
|
63
|
+
void values;
|
|
64
|
+
void valAnchor;
|
|
65
|
+
const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
|
|
66
|
+
const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
|
|
67
|
+
if (!xRange || !yRange) return undefined;
|
|
68
|
+
return {
|
|
69
|
+
left: Math.min(xRange[0], xRange[1]),
|
|
70
|
+
right: Math.max(xRange[0], xRange[1]),
|
|
71
|
+
top: Math.min(yRange[0], yRange[1]),
|
|
72
|
+
bottom: Math.max(yRange[0], yRange[1]),
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
onOverflow: (overflow) => onOverflow?.(overflow),
|
|
76
|
+
});
|
|
77
|
+
|
|
38
78
|
const xFn = $derived(resolveAccessor(group.series.x));
|
|
39
79
|
const yFn = $derived(resolveAccessor(group.series.y));
|
|
40
80
|
|
|
@@ -68,23 +108,29 @@
|
|
|
68
108
|
const resolvedTextAnchor = $derived(font?.textAnchor === 'outside' ? undefined : font?.textAnchor);
|
|
69
109
|
</script>
|
|
70
110
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
<!-- svelteplot's Text mark never wires pointer handlers to its DOM node,
|
|
112
|
+
despite declaring them in its types — a wrapping <g> catches the
|
|
113
|
+
boundary crossing instead (pointerenter/leave fire on every ancestor). -->
|
|
114
|
+
<g bind:this={groupEl} role="presentation" onpointerenter={onHoverSeries} onpointerleave={onHoverEnd}>
|
|
115
|
+
<Text
|
|
116
|
+
data={group.series.data}
|
|
117
|
+
{...isHorizontal ? { y: xFn, x: valueTextFn } : { x: xFn, y: valueTextFn }}
|
|
118
|
+
text={(d: TData) => formatValue(Number(yFn(d)), group.series.name)}
|
|
119
|
+
dx={font?.dx ?? (isHorizontal ? defaultOffset : groupOffset.dx)}
|
|
120
|
+
dy={font?.dy ?? (isHorizontal ? groupOffset.dy : defaultOffset)}
|
|
121
|
+
textAnchor={resolvedTextAnchor ?? (isHorizontal ? (valAnchor === 'outside' || valAnchor === 'end' ? 'start' : valAnchor === 'start' ? 'end' : 'middle') : 'middle')}
|
|
122
|
+
lineAnchor={font?.lineAnchor ?? (isHorizontal ? 'middle' : defaultLA)}
|
|
123
|
+
lineHeight={font?.lineHeight}
|
|
124
|
+
rotate={font?.rotate}
|
|
125
|
+
class={font?.class}
|
|
126
|
+
textClass={font?.textClass}
|
|
127
|
+
fill={font?.fill ?? axisColor}
|
|
128
|
+
fillOpacity={fillOverride?.fillOpacity}
|
|
129
|
+
fontSize={font?.fontSize ?? 12}
|
|
130
|
+
fontWeight={font?.fontWeight}
|
|
131
|
+
fontStyle={font?.fontStyle}
|
|
132
|
+
stroke={font?.stroke}
|
|
133
|
+
strokeWidth={font?.strokeWidth}
|
|
134
|
+
paintOrder={font?.paintOrder}
|
|
135
|
+
/>
|
|
136
|
+
</g>
|
|
@@ -3,6 +3,8 @@ import type { ValuesStyle } from '../../types/layout/styles';
|
|
|
3
3
|
import type { ValueAnchor } from '../../types/plots/constants';
|
|
4
4
|
import type { BarSegmentStyle } from '../../types/plots/segments/common';
|
|
5
5
|
import type { VisualGroup } from '../../types/plots/segments/config';
|
|
6
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
7
|
+
import type { MarginOverflow } from '../utils/labelOverflow.svelte';
|
|
6
8
|
import type { BarLayout } from './layout.svelte';
|
|
7
9
|
declare function $$render<TData extends Record<string, unknown>>(): {
|
|
8
10
|
props: {
|
|
@@ -11,9 +13,16 @@ declare function $$render<TData extends Record<string, unknown>>(): {
|
|
|
11
13
|
barLayout: BarLayout<TData>;
|
|
12
14
|
isHorizontal: boolean;
|
|
13
15
|
valAnchor: ValueAnchor;
|
|
14
|
-
|
|
16
|
+
formatValue: (value: number, seriesName: string) => string;
|
|
15
17
|
values: ValuesStyle | undefined;
|
|
16
18
|
axisColor: string;
|
|
19
|
+
/** Resolved legend/hover-isolate dimming style for this group's series, if any. */
|
|
20
|
+
disabled?: LegendDisabledStyle;
|
|
21
|
+
/** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
|
|
22
|
+
onHoverSeries?: () => void;
|
|
23
|
+
onHoverEnd?: () => void;
|
|
24
|
+
/** Reports how far this label overflows the plot's own content box, in pixels, on each side (0 = no overflow). */
|
|
25
|
+
onOverflow?: (overflow: MarginOverflow) => void;
|
|
17
26
|
};
|
|
18
27
|
exports: {};
|
|
19
28
|
bindings: "";
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
const minColor = $derived(styles.colors?.min ?? cfg.continuous.min);
|
|
74
74
|
const maxColor = $derived(styles.colors?.max ?? cfg.continuous.max);
|
|
75
75
|
const showVals = $derived(styles.values?.show ?? true);
|
|
76
|
-
const
|
|
76
|
+
const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
|
|
77
77
|
|
|
78
78
|
const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, undefined));
|
|
79
79
|
const flat = $derived(resolvedSeries.flatMap((s) => s.data));
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
x: getX(d) as AxisValue,
|
|
128
128
|
y: getY(d) as AxisValue,
|
|
129
129
|
row: d,
|
|
130
|
-
label:
|
|
130
|
+
label: formatValue(Number(getZ(d)), ''),
|
|
131
131
|
})),
|
|
132
132
|
);
|
|
133
133
|
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
const hover = $derived({
|
|
217
217
|
active: tooltip != null || hasHoverMarker(markers),
|
|
218
218
|
points: hoverPoints,
|
|
219
|
-
label: (d: TData) =>
|
|
219
|
+
label: (d: TData) => formatValue(Number(getZ(d)), ''),
|
|
220
220
|
strategy: resolveHoverStrategy(tooltip?.strategy, markers, 'punctual'),
|
|
221
221
|
sync: resolveHoverSync(tooltip?.sync, markers),
|
|
222
222
|
tooltip,
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
{getX}
|
|
245
245
|
{getY}
|
|
246
246
|
{getZ}
|
|
247
|
-
{
|
|
247
|
+
{formatValue}
|
|
248
248
|
values={styles.values}
|
|
249
249
|
defaultColor={cfg.continuous.labelColor}
|
|
250
250
|
/>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
getX,
|
|
15
15
|
getY,
|
|
16
16
|
getZ,
|
|
17
|
-
|
|
17
|
+
formatValue,
|
|
18
18
|
values,
|
|
19
19
|
defaultColor,
|
|
20
20
|
}: {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
getX: (d: TData) => AxisValue;
|
|
23
23
|
getY: (d: TData) => AxisValue;
|
|
24
24
|
getZ: (d: TData) => unknown;
|
|
25
|
-
|
|
25
|
+
formatValue: (value: number, seriesName: string) => string;
|
|
26
26
|
values: ValuesStyle | undefined;
|
|
27
27
|
defaultColor: string;
|
|
28
28
|
} = $props();
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
{data}
|
|
38
38
|
x={getX}
|
|
39
39
|
y={getY}
|
|
40
|
-
text={(d: TData) =>
|
|
40
|
+
text={(d: TData) => formatValue(Number(getZ(d)), '')}
|
|
41
41
|
fill={font?.fill ?? defaultColor}
|
|
42
42
|
fontSize={font?.fontSize ?? 12}
|
|
43
43
|
fontWeight={font?.fontWeight ?? 600}
|
|
@@ -6,7 +6,7 @@ declare function $$render<TData extends Record<string, unknown>>(): {
|
|
|
6
6
|
getX: (d: TData) => AxisValue;
|
|
7
7
|
getY: (d: TData) => AxisValue;
|
|
8
8
|
getZ: (d: TData) => unknown;
|
|
9
|
-
|
|
9
|
+
formatValue: (value: number, seriesName: string) => string;
|
|
10
10
|
values: ValuesStyle | undefined;
|
|
11
11
|
defaultColor: string;
|
|
12
12
|
};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
|
|
10
10
|
import { disabledStrokeOverride, disabledDotsOverride } from '../utils/legendDisabled';
|
|
11
11
|
import { buildLineMarkers } from './buildLineMarkers';
|
|
12
|
+
import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
|
|
12
13
|
import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
|
|
13
14
|
import ValueLabels from './ValueLabels.svelte';
|
|
14
15
|
import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
import type { BasePlotStylesConfig } from '../../types/layout/styles';
|
|
20
21
|
import type { LineSegmentStyle } from '../../types/plots/segments/common';
|
|
21
22
|
import type { LineMarkersConfig } from '../../types/markers/line';
|
|
23
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
22
24
|
|
|
23
25
|
type Props = BasePlotProps<
|
|
24
26
|
SeriesProps<TData> | DataProps<TData>,
|
|
@@ -56,6 +58,22 @@
|
|
|
56
58
|
|
|
57
59
|
const cfg = $derived(getConfiguration());
|
|
58
60
|
const showVals = $derived(styles.values?.show ?? false);
|
|
61
|
+
const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
|
|
62
|
+
|
|
63
|
+
// A value label hovered while `styles.values.hoverIsolate` is on dims
|
|
64
|
+
// every other series exactly like a legend toggle would — local to this
|
|
65
|
+
// one `<Plot>` instance (not the shared HoverStore) so the gesture can't
|
|
66
|
+
// leak into unrelated tooltip/crosshair behavior.
|
|
67
|
+
let hoverIsolatedSeries = $state<string | null>(null);
|
|
68
|
+
|
|
69
|
+
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
70
|
+
return (
|
|
71
|
+
legendInteraction?.disabledSeries.get(name) ??
|
|
72
|
+
(hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
|
|
73
|
+
? { opacity: cfg.legend.disabledOpacity }
|
|
74
|
+
: undefined)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
59
77
|
|
|
60
78
|
const xAcc = $derived(resolveAccessor(resolvedSeries[0]?.x ?? ((d: TData) => d)));
|
|
61
79
|
const yAcc = $derived(resolveAccessor(resolvedSeries[0]?.y ?? ((d: TData) => d)));
|
|
@@ -97,8 +115,8 @@
|
|
|
97
115
|
const lineMarkers = $derived(
|
|
98
116
|
buildLineMarkers({
|
|
99
117
|
groupedSeries,
|
|
100
|
-
disabledStrokeFor: (name) => disabledStrokeOverride(
|
|
101
|
-
disabledDotsFor: (name) => disabledDotsOverride(
|
|
118
|
+
disabledStrokeFor: (name) => disabledStrokeOverride(effectiveDisabledFor(name)),
|
|
119
|
+
disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name)),
|
|
102
120
|
}),
|
|
103
121
|
);
|
|
104
122
|
|
|
@@ -114,31 +132,11 @@
|
|
|
114
132
|
// End-of-line labels can grow past the plot's own content box, into its
|
|
115
133
|
// margin — reserving real space there (rather than padding the domain,
|
|
116
134
|
// which would falsely imply data exists past the last real point) is
|
|
117
|
-
// what ValueLabels's own overflow measurement feeds into here
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
// "shrink margin → overflow reappears → grow margin → …". Local to this
|
|
123
|
-
// one `<Plot>` instance — see ValueLabels.svelte's own doc comment for
|
|
124
|
-
// why that locality (vs. svelteplot's shared margin context) matters.
|
|
125
|
-
let measuredLabelMargin = $state({ left: 0, right: 0 });
|
|
126
|
-
|
|
127
|
-
function handleLabelOverflow(overflow: { left: number; right: number }) {
|
|
128
|
-
const left = Math.max(measuredLabelMargin.left, overflow.left);
|
|
129
|
-
const right = Math.max(measuredLabelMargin.right, overflow.right);
|
|
130
|
-
if (left !== measuredLabelMargin.left || right !== measuredLabelMargin.right) {
|
|
131
|
-
measuredLabelMargin = { left, right };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const resolvedMargins = $derived.by(() => {
|
|
136
|
-
if (measuredLabelMargin.left <= 0 && measuredLabelMargin.right <= 0) return margins;
|
|
137
|
-
const out = { ...margins };
|
|
138
|
-
if (measuredLabelMargin.right > 0 && margins?.right === undefined) out.right = measuredLabelMargin.right;
|
|
139
|
-
if (measuredLabelMargin.left > 0 && margins?.left === undefined) out.left = measuredLabelMargin.left;
|
|
140
|
-
return out;
|
|
141
|
-
});
|
|
135
|
+
// what ValueLabels's own overflow measurement feeds into here, via a
|
|
136
|
+
// tracker local to this one `<Plot>` instance — see ValueLabels.svelte's
|
|
137
|
+
// own doc comment for why that locality (vs. svelteplot's shared margin
|
|
138
|
+
// context) matters.
|
|
139
|
+
const labelMargin = createLabelMarginTracker(() => margins);
|
|
142
140
|
</script>
|
|
143
141
|
|
|
144
142
|
<BasePlotLayout
|
|
@@ -150,13 +148,20 @@
|
|
|
150
148
|
getY={(d) => Number(yAcc(d))}
|
|
151
149
|
{hover}
|
|
152
150
|
{scales}
|
|
153
|
-
margins={resolvedMargins}
|
|
151
|
+
margins={labelMargin.resolvedMargins}
|
|
154
152
|
xTickRotate={scales?.x?.tickRotate}
|
|
155
153
|
markers={[...lineMarkers, ...markers]}
|
|
156
154
|
seriesData={resolvedSeries}
|
|
157
155
|
{scopedGroups}
|
|
158
156
|
>
|
|
159
157
|
{#if showVals}
|
|
160
|
-
<ValueLabels
|
|
158
|
+
<ValueLabels
|
|
159
|
+
{lastPoints}
|
|
160
|
+
values={styles.values}
|
|
161
|
+
onOverflow={labelMargin.report}
|
|
162
|
+
disabledFor={effectiveDisabledFor}
|
|
163
|
+
onHoverSeries={hoverIsolate ? (name) => { hoverIsolatedSeries = name; } : undefined}
|
|
164
|
+
onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
|
|
165
|
+
/>
|
|
161
166
|
{/if}
|
|
162
167
|
</BasePlotLayout>
|