@fundar/data-chart-telling 1.15.0 → 1.17.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 +38 -0
- package/dist/configuration/ConfigVars.svelte +14 -0
- package/dist/configuration/ConfigVars.svelte.d.ts +3 -0
- package/dist/configuration/config.svelte.d.ts +5 -0
- package/dist/configuration/config.svelte.js +18 -4
- package/dist/configuration/themes/dark.js +1 -1
- package/dist/configuration/themes/editorial.js +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +2 -1
- package/dist/layout/base/BasePlotLayout.svelte +1 -1
- package/dist/layout/figure/Figure.svelte +5 -12
- package/dist/layout/figure/FigureChrome.svelte +2 -10
- package/dist/layout/geometry/GeometryLayout.svelte +42 -1
- package/dist/layout/hover/HoverSettings.d.ts +3 -1
- package/dist/layout/hover/HoverSettings.js +4 -2
- package/dist/layout/legend/DisabledStyle.d.ts +1 -1
- package/dist/layout/legend/DisabledStyle.js +1 -1
- package/dist/layout/legend/DiscreteBadgeItem.svelte +1 -1
- package/dist/layout/legend/DiscreteSection.svelte +1 -1
- package/dist/layout/legend/LegendState.svelte.d.ts +12 -2
- package/dist/layout/legend/LegendState.svelte.js +18 -4
- package/dist/layout/legend/LegendText.d.ts +4 -1
- package/dist/layout/legend/LegendText.js +7 -1
- package/dist/layout/margins/PlotMargins.svelte.d.ts +2 -2
- package/dist/layout/margins/PlotMargins.svelte.js +2 -2
- package/dist/layout/plot/Plot.svelte +4 -2
- package/dist/layout/plot/Plot.svelte.d.ts +2 -1
- package/dist/layout/scales/ScalesLayout.svelte +1 -1
- package/dist/layout/styles/ValueLabels.d.ts +33 -2
- package/dist/layout/styles/ValueLabels.js +67 -8
- package/dist/layout/timeline/Timeline.svelte +5 -1
- package/dist/layout/timeline/TimelineLayout.svelte +4 -1
- package/dist/layout/timeline/TimelineSteps.d.ts +6 -2
- package/dist/layout/timeline/TimelineSteps.js +9 -3
- package/dist/markers/area/AreaMarker.svelte +3 -0
- package/dist/markers/arrow/ArrowMarker.svelte +9 -5
- package/dist/markers/bar/BarMarker.svelte +45 -48
- package/dist/markers/bollinger/BollingerMarker.svelte +3 -0
- package/dist/markers/box/BoxMarker.svelte +3 -1
- package/dist/markers/cell/CellMarker.svelte +11 -7
- package/dist/markers/contour/ContourMarker.svelte +3 -0
- package/dist/markers/delaunay/DelaunayLinkMarker.svelte +9 -5
- package/dist/markers/delaunay/DelaunayMeshMarker.svelte +3 -0
- package/dist/markers/density/DensityMarker.svelte +3 -0
- package/dist/markers/difference/DifferenceMarker.svelte +3 -0
- package/dist/markers/dot/DotMarker.svelte +11 -6
- package/dist/markers/geo/GraticuleMarker.svelte +3 -0
- package/dist/markers/geo/SphereMarker.svelte +3 -0
- package/dist/markers/hover/HoverMarker.svelte +22 -6
- package/dist/markers/hull/HullMarker.svelte +3 -0
- package/dist/markers/image/ImageMarker.svelte +3 -0
- package/dist/markers/line/LineMarker.svelte +3 -0
- package/dist/markers/link/LinkMarker.svelte +3 -0
- package/dist/markers/raster/RasterMarker.svelte +3 -0
- package/dist/markers/rect/RectMarker.svelte +25 -17
- package/dist/markers/regression/RegressionMarker.svelte +3 -0
- package/dist/markers/rule/RuleMarker.svelte +15 -5
- package/dist/markers/text/TextFit.d.ts +44 -0
- package/dist/markers/text/TextFit.js +86 -0
- package/dist/markers/text/TextMarker.svelte +186 -90
- package/dist/markers/text/TextMarker.svelte.d.ts +2 -0
- package/dist/markers/tick/TickMarker.svelte +9 -3
- package/dist/markers/trail/TrailMarker.svelte +3 -0
- package/dist/markers/utils/BoxLabel.d.ts +58 -0
- package/dist/markers/utils/BoxLabel.js +175 -0
- package/dist/markers/utils/MarkerEvents.d.ts +23 -0
- package/dist/markers/utils/MarkerEvents.js +44 -0
- package/dist/markers/utils/MarkerLabel.svelte +33 -0
- package/dist/markers/utils/MarkerLabel.svelte.d.ts +10 -0
- package/dist/markers/utils/RowStroke.d.ts +9 -21
- package/dist/markers/utils/RowStroke.js +10 -21
- package/dist/markers/utils/RowStyle.d.ts +26 -0
- package/dist/markers/utils/RowStyle.js +55 -0
- package/dist/markers/vector/VectorMarker.svelte +3 -0
- package/dist/markers/voronoi/VoronoiMarker.svelte +9 -5
- package/dist/markers/voronoi/VoronoiMeshMarker.svelte +3 -0
- package/dist/markers/waffle/WaffleMarker.svelte +9 -5
- package/dist/phases/Pipeline.d.ts +3 -1
- package/dist/phases/Pipeline.js +1 -0
- package/dist/phases/data/DataPhase.js +51 -10
- package/dist/phases/hover/HoverPhase.js +9 -1
- package/dist/phases/mapping/MappingPhase.js +2 -1
- package/dist/phases/markers/MarkerStyles.d.ts +18 -0
- package/dist/phases/markers/MarkerStyles.js +111 -0
- package/dist/phases/markers/MarkersPhase.d.ts +1 -1
- package/dist/phases/markers/MarkersPhase.js +7 -5
- package/dist/phases/statistics/StatisticsPhase.js +1 -1
- package/dist/phases/transforms/Transforms.js +3 -1
- package/dist/phases/transforms/TransformsPhase.js +58 -4
- package/dist/phases/transforms/methods/TreemapTransform.d.ts +28 -0
- package/dist/phases/transforms/methods/TreemapTransform.js +119 -0
- package/dist/plots/correlation/heatmap/defaults.js +2 -2
- package/dist/plots/correlation/heatmap/markers.d.ts +0 -8
- package/dist/plots/correlation/heatmap/markers.js +8 -35
- package/dist/plots/correlation/scatter/markers.d.ts +1 -1
- package/dist/plots/correlation/scatter/markers.js +2 -2
- package/dist/plots/distribution/histogram/defaults.js +2 -2
- package/dist/plots/distribution/histogram/markers.d.ts +1 -10
- package/dist/plots/distribution/histogram/markers.js +17 -59
- package/dist/plots/distribution/pyramid/defaults.js +3 -3
- package/dist/plots/distribution/pyramid/markers.d.ts +0 -10
- package/dist/plots/distribution/pyramid/markers.js +13 -64
- package/dist/plots/distribution/violin/markers.js +1 -1
- package/dist/plots/map/geo/phases/markers.js +3 -2
- package/dist/plots/part-of-a-whole/treemap/Plot.svelte +47 -0
- package/dist/plots/part-of-a-whole/treemap/Plot.svelte.d.ts +26 -0
- package/dist/plots/part-of-a-whole/treemap/defaults.d.ts +10 -0
- package/dist/plots/part-of-a-whole/treemap/defaults.js +167 -0
- package/dist/plots/ranking/bar/defaults.js +2 -2
- package/dist/plots/ranking/bar/markers.d.ts +1 -11
- package/dist/plots/ranking/bar/markers.js +38 -80
- package/dist/plots/ranking/bullet/markers.js +2 -3
- package/dist/plots/ranking/cleveland/ClevelandMarkers.js +17 -13
- package/dist/types/configuration/styling.d.ts +33 -2
- package/dist/types/layout/legend.d.ts +8 -1
- package/dist/types/layout/mapping.d.ts +12 -3
- package/dist/types/layout/scales.d.ts +6 -0
- package/dist/types/layout/tooltip.d.ts +16 -4
- package/dist/types/markers/axis/arrow.d.ts +2 -8
- package/dist/types/markers/axis/bar.d.ts +4 -8
- package/dist/types/markers/axis/cell.d.ts +5 -5
- package/dist/types/markers/axis/delaunayLink.d.ts +2 -2
- package/dist/types/markers/axis/rect.d.ts +4 -8
- package/dist/types/markers/axis/rule.d.ts +2 -16
- package/dist/types/markers/axis/tick.d.ts +2 -2
- package/dist/types/markers/axis/voronoi.d.ts +2 -2
- package/dist/types/markers/axis/waffle.d.ts +4 -3
- package/dist/types/markers/basic/dot.d.ts +2 -2
- package/dist/types/markers/basic/text.d.ts +56 -14
- package/dist/types/markers/channels/commonMarkerConfig.d.ts +4 -1
- package/dist/types/markers/channels/commonMarkerConfigWithArea.d.ts +1 -2
- package/dist/types/markers/channels/commonMarkerConfigWithLabel.d.ts +10 -0
- package/dist/types/markers/channels/commonMarkerConfigWithLabel.js +1 -0
- package/dist/types/markers/channels/commonMarkerEvents.d.ts +17 -0
- package/dist/types/markers/channels/commonMarkerEvents.js +13 -0
- package/dist/types/markers/geo/contour.d.ts +2 -1
- package/dist/types/markers/geo/graticule.d.ts +2 -1
- package/dist/types/markers/geo/link.d.ts +2 -1
- package/dist/types/markers/geo/sphere.d.ts +2 -1
- package/dist/types/markers/geo/vector.d.ts +2 -1
- package/dist/types/phases/data/phase.d.ts +5 -0
- package/dist/types/phases/hover/phase.d.ts +2 -0
- package/dist/types/phases/state.d.ts +7 -0
- package/dist/types/phases/statistics/phase.d.ts +2 -1
- package/dist/types/phases/transforms/phase.d.ts +11 -0
- package/dist/types/plots/constants.d.ts +14 -14
- package/dist/types/plots/correlation/heatmap/props.d.ts +3 -3
- package/dist/types/plots/correlation/scatter/props.d.ts +3 -3
- package/dist/types/plots/data/common.d.ts +19 -6
- package/dist/types/plots/data/geo.d.ts +1 -1
- package/dist/types/plots/distribution/box/props.d.ts +3 -3
- package/dist/types/plots/distribution/density/props.d.ts +3 -11
- package/dist/types/plots/distribution/histogram/props.d.ts +3 -11
- package/dist/types/plots/distribution/pyramid/props.d.ts +3 -3
- package/dist/types/plots/distribution/violin/props.d.ts +3 -3
- package/dist/types/plots/evolution/area/props.d.ts +3 -3
- package/dist/types/plots/evolution/line/props.d.ts +3 -3
- package/dist/types/plots/part-of-a-whole/treemap/props.d.ts +19 -0
- package/dist/types/plots/part-of-a-whole/treemap/props.js +1 -0
- package/dist/types/plots/part-of-a-whole/treemap/state.d.ts +6 -0
- package/dist/types/plots/part-of-a-whole/treemap/state.js +1 -0
- package/dist/types/plots/props.d.ts +7 -4
- package/dist/types/plots/ranking/bar/props.d.ts +3 -3
- package/dist/types/plots/ranking/cleveland/props.d.ts +3 -3
- package/dist/types/plots/ranking/dumbbell/props.d.ts +3 -3
- package/dist/types/plots/ranking/forest/props.d.ts +3 -3
- package/dist/types/plots/ranking/lollipop/props.d.ts +3 -3
- package/dist/types/plots/styling.d.ts +24 -0
- package/dist/types/transforms/index.d.ts +3 -1
- package/dist/types/transforms/treemap.d.ts +32 -0
- package/dist/types/transforms/treemap.js +5 -0
- package/dist/utils/color/ColorTones.d.ts +19 -0
- package/dist/utils/color/ColorTones.js +64 -0
- package/dist/utils/color/CssColor.d.ts +14 -5
- package/dist/utils/color/CssColor.js +69 -27
- package/dist/utils/color/index.d.ts +4 -0
- package/dist/utils/color/index.js +4 -0
- package/dist/utils/grouping.d.ts +18 -1
- package/dist/utils/grouping.js +31 -0
- package/docs/concepts.md +3 -2
- package/docs/legend.md +8 -5
- package/docs/markers.md +76 -26
- package/docs/plots/part-of-a-whole/treemap.md +69 -0
- package/docs/plots/ranking/bar.md +1 -1
- package/docs/plots.md +21 -20
- package/docs/scales.md +3 -0
- package/docs/state.md +1 -1
- package/docs/styles-and-segments.md +19 -18
- package/docs/theming.md +5 -12
- package/docs/tooltip-and-hover.md +1 -1
- package/docs/transforms-and-statistics.md +17 -16
- package/examples/Treemap.svelte +13 -0
- package/examples/datasets/economic-activity.ts +40 -0
- package/examples/datasets/index.ts +1 -0
- package/llms-full.txt +5 -5
- package/llms.txt +2 -0
- package/package.json +3 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts" generics="TData = Record<string, unknown>">
|
|
2
|
+
import { RowStyle } from '../utils/RowStyle';
|
|
3
|
+
import { MarkerEvents } from '../utils/MarkerEvents';
|
|
2
4
|
import { Text } from 'svelteplot';
|
|
3
5
|
import { usePlot } from '../../layout/plot/usePlot';
|
|
4
6
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
@@ -6,10 +8,13 @@
|
|
|
6
8
|
import { RowOffset } from '../utils/RowOffset';
|
|
7
9
|
import RowOffsetMarks from '../utils/RowOffsetMarks.svelte';
|
|
8
10
|
import { Declutter } from './Declutter';
|
|
11
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
12
|
+
import { TextFit, type FittedText } from './TextFit';
|
|
9
13
|
import { NumberFormat } from '../../utils/NumberFormat';
|
|
10
14
|
import { PlotMargins } from '../../layout/margins/PlotMargins.svelte';
|
|
11
15
|
import { Box } from '../../layout/margins/Box';
|
|
12
16
|
import type { TextMarkerConfig } from '../../types/markers/common';
|
|
17
|
+
import type { BoxChannels, BoxSizer } from '../utils/BoxLabel';
|
|
13
18
|
import type { TextAnchor } from '../../types/plots/constants';
|
|
14
19
|
|
|
15
20
|
/**
|
|
@@ -20,7 +25,8 @@
|
|
|
20
25
|
* draws one label per row, inside the plot's content box. With
|
|
21
26
|
* `fitMargins`, the plot's margins grow until the labels fit.
|
|
22
27
|
*/
|
|
23
|
-
const { marker }: { marker: TextMarkerConfig } = $props();
|
|
28
|
+
const { marker, box }: { marker: TextMarkerConfig; box?: BoxChannels | BoxSizer } = $props();
|
|
29
|
+
const events = MarkerEvents.use(() => marker);
|
|
24
30
|
|
|
25
31
|
// A text's anchors as the side of its point it sits on, and back.
|
|
26
32
|
const TEXT_SIDES = { start: 'after', middle: 'middle', end: 'before' } as const;
|
|
@@ -47,15 +53,16 @@
|
|
|
47
53
|
data.length > 0 ? data : placedByRows(x) || placedByRows(y) ? [] : [{} as TData]
|
|
48
54
|
);
|
|
49
55
|
// Per-row accessors give literal values; an empty one takes the flat default.
|
|
50
|
-
const flatFill = $derived(styles?.fill ?? 'currentColor');
|
|
51
56
|
const fill = $derived.by(() => {
|
|
52
|
-
const own =
|
|
53
|
-
if (typeof own !== 'function') return own ??
|
|
54
|
-
return { value: (d: TData) => own(d) ??
|
|
57
|
+
const own = styles?.fill;
|
|
58
|
+
if (typeof own !== 'function') return RowStyle.of(own, displayData) ?? 'currentColor';
|
|
59
|
+
return { value: (d: TData) => own(d) ?? 'currentColor', scale: null };
|
|
55
60
|
});
|
|
56
61
|
const fillOpacity = $derived.by(() => {
|
|
57
|
-
const own =
|
|
58
|
-
return typeof own === 'function'
|
|
62
|
+
const own = styles?.fillOpacity;
|
|
63
|
+
return typeof own === 'function'
|
|
64
|
+
? { value: (d: TData) => own(d) ?? 1, scale: null }
|
|
65
|
+
: RowStyle.opacity(own);
|
|
59
66
|
});
|
|
60
67
|
const fontSize = $derived(styles?.fontSize ?? 12);
|
|
61
68
|
const fontWeight = $derived(styles?.fontWeight ?? 'normal');
|
|
@@ -97,11 +104,7 @@
|
|
|
97
104
|
};
|
|
98
105
|
});
|
|
99
106
|
const perRow = $derived(
|
|
100
|
-
!!declutter ||
|
|
101
|
-
!!marker.edgeAware ||
|
|
102
|
-
!!marker.onPointerEnter ||
|
|
103
|
-
!!marker.onPointerLeave ||
|
|
104
|
-
typeof marker.textAnchor === 'function'
|
|
107
|
+
!!declutter || !!marker.edgeAware || events.any || typeof marker.textAnchor === 'function'
|
|
105
108
|
);
|
|
106
109
|
const dir = $derived(Math.sign(dx) || 1);
|
|
107
110
|
|
|
@@ -128,9 +131,101 @@
|
|
|
128
131
|
return typeof value === 'number' ? numberText(value) : String(value ?? '');
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
/** The pixel size of the box a row's label is fitted to - from the edges it names, or measured by the shape it labels. */
|
|
135
|
+
const boxSizeOf = $derived.by(() => {
|
|
136
|
+
const fit = marker.fit === true ? {} : marker.fit;
|
|
137
|
+
if (!fit) return undefined;
|
|
138
|
+
const own = typeof box === 'function' ? undefined : box;
|
|
139
|
+
const [x1, x2, y1, y2] = [
|
|
140
|
+
fit.x1 ?? own?.x1,
|
|
141
|
+
fit.x2 ?? own?.x2,
|
|
142
|
+
fit.y1 ?? own?.y1,
|
|
143
|
+
fit.y2 ?? own?.y2
|
|
144
|
+
];
|
|
145
|
+
const xScale = plot.scales.x?.fn;
|
|
146
|
+
const yScale = plot.scales.y?.fn;
|
|
147
|
+
if (![x1, x2, y1, y2].includes(undefined)) {
|
|
148
|
+
if (!xScale || !yScale) return undefined;
|
|
149
|
+
const [left, right, top, bottom] = [x1, x2, y1, y2].map(channel);
|
|
150
|
+
const span = (
|
|
151
|
+
scale: typeof xScale,
|
|
152
|
+
from: (d: TData) => unknown,
|
|
153
|
+
to: (d: TData) => unknown,
|
|
154
|
+
d: TData
|
|
155
|
+
) => Math.abs(Number(scale(to(d) as never)) - Number(scale(from(d) as never)));
|
|
156
|
+
return (d: TData) => ({
|
|
157
|
+
width: span(xScale, left, right, d),
|
|
158
|
+
height: span(yScale, top, bottom, d)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return typeof box === 'function' ? (d: TData) => box(d, plot.scales as never) : undefined;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* What each label is written as and at, fitted to its box (`fit`), less the padding. A label with no room is left
|
|
166
|
+
* out, with `undefined`; one with no box to measure is written as it is.
|
|
167
|
+
*/
|
|
168
|
+
const fitted = $derived.by(() => {
|
|
169
|
+
const fit = marker.fit === true ? {} : marker.fit;
|
|
170
|
+
if (!fit || !boxSizeOf) return undefined;
|
|
171
|
+
const padding = fit.padding ?? cfg.text.label.padding;
|
|
172
|
+
const measure = (text: string, size: number) => Box.ofText(text, size, cfg).width;
|
|
173
|
+
const fallback = fit.fallback;
|
|
174
|
+
const fallbackOf = (d: TData) => {
|
|
175
|
+
// As `text` is: a function's result, a field of the row, else the text itself.
|
|
176
|
+
const value =
|
|
177
|
+
typeof fallback === 'function'
|
|
178
|
+
? fallback(d)
|
|
179
|
+
: typeof fallback === 'string' && typeof d === 'object' && d !== null && fallback in d
|
|
180
|
+
? (d as Record<string, unknown>)[fallback]
|
|
181
|
+
: fallback;
|
|
182
|
+
return typeof value === 'number' ? numberText(value) : String(value);
|
|
183
|
+
};
|
|
184
|
+
// Built whole inside this derivation and never changed after it, so it has nothing to react to.
|
|
185
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
186
|
+
const out = new Map<TData, FittedText | undefined>();
|
|
187
|
+
for (const d of displayData) {
|
|
188
|
+
const size = boxSizeOf(d);
|
|
189
|
+
out.set(
|
|
190
|
+
d,
|
|
191
|
+
size
|
|
192
|
+
? TextFit.fit({
|
|
193
|
+
text: textOf(d),
|
|
194
|
+
fallback: fallback === undefined ? undefined : fallbackOf(d),
|
|
195
|
+
width: size.width - 2 * padding,
|
|
196
|
+
height: size.height - 2 * padding,
|
|
197
|
+
fontSize,
|
|
198
|
+
minFontSize: fit.minFontSize ?? cfg.text.label.minFontSize,
|
|
199
|
+
lineHeight: styles?.lineHeight ?? 1.2,
|
|
200
|
+
minLetters: fit.minLetters ?? cfg.text.label.minLetters,
|
|
201
|
+
measure
|
|
202
|
+
})
|
|
203
|
+
: { text: textOf(d), fontSize }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
});
|
|
208
|
+
/** `rows` by the size their labels are written at: svelteplot takes one size for a mark, and a function of the row sets its scales off in a loop. */
|
|
209
|
+
function sizeGroups(rows: TData[]): { size: number; rows: TData[]; texts?: string[] }[] {
|
|
210
|
+
if (!fitted) return [{ size: fontSize, rows }];
|
|
211
|
+
const bySize = new SvelteMap<number, { rows: TData[]; texts: string[] }>();
|
|
212
|
+
for (const d of rows) {
|
|
213
|
+
const own = fitted.get(d);
|
|
214
|
+
const group = bySize.get(own?.fontSize ?? fontSize) ?? { rows: [], texts: [] };
|
|
215
|
+
group.rows.push(d);
|
|
216
|
+
group.texts.push(own?.text ?? '');
|
|
217
|
+
bySize.set(own?.fontSize ?? fontSize, group);
|
|
218
|
+
}
|
|
219
|
+
return [...bySize].map(([size, group]) => ({ size, ...group }));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The rows with room for a label. */
|
|
223
|
+
const shown = $derived(fitted ? displayData.filter((d) => fitted.get(d)) : displayData);
|
|
224
|
+
|
|
131
225
|
function opacityOf(d: TData): number | undefined {
|
|
132
|
-
const value =
|
|
133
|
-
|
|
226
|
+
const value = styles?.fillOpacity;
|
|
227
|
+
if (typeof value === 'function') return value(d);
|
|
228
|
+
return typeof value === 'string' ? (d as Record<string, number>)[value] : value;
|
|
134
229
|
}
|
|
135
230
|
|
|
136
231
|
/** Each row's pixel position, once both scales resolve. */
|
|
@@ -188,10 +283,10 @@
|
|
|
188
283
|
let groupEl: SVGGElement | undefined = $state();
|
|
189
284
|
|
|
190
285
|
// The plot measures what it draws: how far it reaches out, and with `fitMargins` its margins grow to fit it.
|
|
191
|
-
const measured = $derived(!!plotMargins && (!!marker.
|
|
286
|
+
const measured = $derived(!!plotMargins && (!!marker.onoverflow || !!marker.fitMargins));
|
|
192
287
|
plotMargins?.watch(() => (measured ? groupEl : undefined), {
|
|
193
288
|
grow: () => !!marker.fitMargins,
|
|
194
|
-
|
|
289
|
+
onoverflow: (overflow) => marker.onoverflow?.(overflow),
|
|
195
290
|
// The box isn't tracked reactively: a new layout measures again.
|
|
196
291
|
deps: () => [marker, points]
|
|
197
292
|
});
|
|
@@ -205,38 +300,41 @@
|
|
|
205
300
|
rowLineAnchor: typeof lineAnchor
|
|
206
301
|
)}
|
|
207
302
|
<g transform={projected ? `translate(${rowDx},${rowDy})` : undefined}>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
303
|
+
{#each sizeGroups(rows) as group (group.size)}
|
|
304
|
+
<Text
|
|
305
|
+
data={group.rows}
|
|
306
|
+
x={x ?? (marker.frameAnchor ? undefined : 0)}
|
|
307
|
+
y={y ?? (marker.frameAnchor ? undefined : 0)}
|
|
308
|
+
frameAnchor={marker.frameAnchor}
|
|
309
|
+
text={((d: TData, index: number) => {
|
|
310
|
+
if (group.texts) return group.texts[index];
|
|
311
|
+
const value = valueOf(d);
|
|
312
|
+
return typeof value === 'number' ? numberText(value) : value;
|
|
313
|
+
}) as (d: TData) => string}
|
|
314
|
+
dx={projected ? 0 : rowDx}
|
|
315
|
+
dy={projected ? 0 : rowDy}
|
|
316
|
+
fill={fill as never}
|
|
317
|
+
fillOpacity={fillOpacity as never}
|
|
318
|
+
fontSize={group.size}
|
|
319
|
+
{fontWeight}
|
|
320
|
+
textAnchor={rowAnchor}
|
|
321
|
+
lineAnchor={rowLineAnchor}
|
|
322
|
+
fontStyle={fontStyleProp}
|
|
323
|
+
lineHeight={styles?.lineHeight}
|
|
324
|
+
rotate={styles?.rotate}
|
|
325
|
+
stroke={RowStyle.of(styles?.stroke, rows)}
|
|
326
|
+
strokeWidth={RowStyle.field(styles?.strokeWidth)}
|
|
327
|
+
paintOrder={styles?.paintOrder}
|
|
328
|
+
class={styles?.class}
|
|
329
|
+
textClass={styles?.textClass}
|
|
330
|
+
/>
|
|
331
|
+
{/each}
|
|
234
332
|
</g>
|
|
235
333
|
{/snippet}
|
|
236
334
|
|
|
237
335
|
{#snippet labels()}
|
|
238
336
|
{#if !perRow}
|
|
239
|
-
<RowOffsetMarks {marker} rows={
|
|
337
|
+
<RowOffsetMarks {marker} rows={shown}>
|
|
240
338
|
{#snippet mark(rows)}
|
|
241
339
|
{@render label(rows, dx, dy, textAnchor, lineAnchor)}
|
|
242
340
|
{/snippet}
|
|
@@ -244,54 +342,52 @@
|
|
|
244
342
|
{:else if (!declutter && !marker.edgeAware) || points}
|
|
245
343
|
<!-- A decluttered or edge-aware label waits for both scales. -->
|
|
246
344
|
{#each displayData as d, i (i)}
|
|
247
|
-
{
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
{
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
345
|
+
{#if !fitted || fitted.get(d)}
|
|
346
|
+
{@const lane = lanes?.get(i)}
|
|
347
|
+
{@const placement = placements?.[i]}
|
|
348
|
+
{@const extraDy = lane?.extraDy ?? 0}
|
|
349
|
+
{@const laneDir = lane?.aligned ? lane.dir : dir}
|
|
350
|
+
{@const labelDx = lane?.aligned ? laneDir * lane.reach : dx}
|
|
351
|
+
{#if declutter && points && (lane?.aligned || extraDy !== 0)}
|
|
352
|
+
{@const { px, py } = points[i]}
|
|
353
|
+
{@const laneX = lane?.aligned ? px + laneDir * lane.laneReach : px + labelDx}
|
|
354
|
+
{@const connector = declutter.connector}
|
|
355
|
+
<path
|
|
356
|
+
d={`M ${px} ${py} H ${laneX} V ${py + extraDy} H ${px + labelDx}`}
|
|
357
|
+
fill="none"
|
|
358
|
+
stroke={connector.stroke}
|
|
359
|
+
stroke-width={connector.strokeWidth}
|
|
360
|
+
stroke-opacity={connector.strokeOpacity ?? opacityOf(d) ?? 1}
|
|
361
|
+
stroke-dasharray={connector.strokeDasharray}
|
|
362
|
+
stroke-linecap={connector.strokeLinecap ?? 'square'}
|
|
363
|
+
stroke-linejoin={connector.strokeLinejoin ?? 'miter'}
|
|
364
|
+
/>
|
|
365
|
+
{/if}
|
|
366
|
+
<!-- svelteplot's Text never wires pointer handlers to its DOM node; a wrapping <g> catches them. -->
|
|
367
|
+
<g role="presentation" {...events.forRow(d, i)}>
|
|
368
|
+
<RowOffsetMarks {marker} rows={[d]}>
|
|
369
|
+
{#snippet mark(rows)}
|
|
370
|
+
{#if placement}
|
|
371
|
+
{@render label(
|
|
372
|
+
rows,
|
|
373
|
+
placement.dx,
|
|
374
|
+
placement.dy + extraDy,
|
|
375
|
+
placement.textAnchor,
|
|
376
|
+
placement.lineAnchor
|
|
377
|
+
)}
|
|
378
|
+
{:else}
|
|
379
|
+
{@render label(
|
|
380
|
+
rows,
|
|
381
|
+
labelDx,
|
|
382
|
+
dy + extraDy,
|
|
383
|
+
lane?.aligned && laneDir !== dir ? flipAnchor(anchorAt(d)) : anchorAt(d),
|
|
384
|
+
lineAnchor
|
|
385
|
+
)}
|
|
386
|
+
{/if}
|
|
387
|
+
{/snippet}
|
|
388
|
+
</RowOffsetMarks>
|
|
389
|
+
</g>
|
|
266
390
|
{/if}
|
|
267
|
-
<!-- svelteplot's Text never wires pointer handlers to its DOM node; a wrapping <g> catches them. -->
|
|
268
|
-
<g
|
|
269
|
-
role="presentation"
|
|
270
|
-
onpointerenter={marker.onPointerEnter && (() => marker.onPointerEnter?.(d))}
|
|
271
|
-
onpointerleave={marker.onPointerLeave && (() => marker.onPointerLeave?.(d))}
|
|
272
|
-
>
|
|
273
|
-
<RowOffsetMarks {marker} rows={[d]}>
|
|
274
|
-
{#snippet mark(rows)}
|
|
275
|
-
{#if placement}
|
|
276
|
-
{@render label(
|
|
277
|
-
rows,
|
|
278
|
-
placement.dx,
|
|
279
|
-
placement.dy + extraDy,
|
|
280
|
-
placement.textAnchor,
|
|
281
|
-
placement.lineAnchor
|
|
282
|
-
)}
|
|
283
|
-
{:else}
|
|
284
|
-
{@render label(
|
|
285
|
-
rows,
|
|
286
|
-
labelDx,
|
|
287
|
-
dy + extraDy,
|
|
288
|
-
lane?.aligned && laneDir !== dir ? flipAnchor(anchorAt(d)) : anchorAt(d),
|
|
289
|
-
lineAnchor
|
|
290
|
-
)}
|
|
291
|
-
{/if}
|
|
292
|
-
{/snippet}
|
|
293
|
-
</RowOffsetMarks>
|
|
294
|
-
</g>
|
|
295
391
|
{/each}
|
|
296
392
|
{/if}
|
|
297
393
|
{/snippet}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TextMarkerConfig } from '../../types/markers/common';
|
|
2
|
+
import type { BoxChannels, BoxSizer } from '../utils/BoxLabel';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
marker: TextMarkerConfig;
|
|
5
|
+
box?: BoxChannels | BoxSizer;
|
|
4
6
|
};
|
|
5
7
|
declare function $$render<TData = Record<string, unknown>>(): {
|
|
6
8
|
props: $$ComponentProps;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { RowStyle } from '../utils/RowStyle';
|
|
3
|
+
import { MarkerEvents } from '../utils/MarkerEvents';
|
|
2
4
|
import { TickX, TickY } from 'svelteplot';
|
|
3
5
|
import { usePlot } from '../../layout/plot/usePlot';
|
|
4
6
|
import { MarkerChannels } from '../utils/MarkerChannels';
|
|
@@ -7,6 +9,7 @@
|
|
|
7
9
|
|
|
8
10
|
/** Renders one `'tick'` marker as `<TickX>` or `<TickY>`, picked from the way the plot runs (`usePlot().axes`). */
|
|
9
11
|
let { marker }: { marker: TickMarkerConfig } = $props();
|
|
12
|
+
const events = MarkerEvents.use(() => marker);
|
|
10
13
|
|
|
11
14
|
const plot = usePlot();
|
|
12
15
|
const isHorizontal = $derived(plot.axes?.isHorizontal ?? false);
|
|
@@ -15,7 +18,9 @@
|
|
|
15
18
|
const cfg = $derived(getConfiguration());
|
|
16
19
|
const data = $derived(marker.data ?? []);
|
|
17
20
|
const channelStroke = $derived(MarkerChannels.buildFill(data, marker.color, marker.scales, cfg));
|
|
18
|
-
const stroke = $derived(
|
|
21
|
+
const stroke = $derived(
|
|
22
|
+
channelStroke ?? RowStyle.of(marker.styles?.stroke, data) ?? 'currentColor'
|
|
23
|
+
);
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* A tick across a band axis spans the whole band, whatever `tickLength` says -
|
|
@@ -33,14 +38,15 @@
|
|
|
33
38
|
</script>
|
|
34
39
|
|
|
35
40
|
<TickMark
|
|
41
|
+
{...events.props}
|
|
36
42
|
{data}
|
|
37
43
|
x={marker.x}
|
|
38
44
|
y={marker.y}
|
|
39
45
|
tickLength={marker.tickLength}
|
|
40
46
|
{...insetProps}
|
|
41
47
|
{stroke}
|
|
42
|
-
strokeWidth={marker.styles?.strokeWidth}
|
|
43
|
-
strokeOpacity={marker.styles?.strokeOpacity}
|
|
48
|
+
strokeWidth={RowStyle.field(marker.styles?.strokeWidth)}
|
|
49
|
+
strokeOpacity={RowStyle.opacity(marker.styles?.strokeOpacity)}
|
|
44
50
|
strokeDasharray={marker.styles?.strokeDasharray}
|
|
45
51
|
strokeLinecap={marker.styles?.strokeLinecap}
|
|
46
52
|
/>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { MarkerEvents } from '../utils/MarkerEvents';
|
|
2
3
|
import { Trail } from 'svelteplot';
|
|
3
4
|
import { MarkerChannels } from '../utils/MarkerChannels';
|
|
4
5
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
@@ -6,6 +7,7 @@
|
|
|
6
7
|
|
|
7
8
|
/** Renders one `'trail'` marker as `<Trail>` - a ribbon whose width follows `r`. */
|
|
8
9
|
let { marker }: { marker: TrailMarkerConfig } = $props();
|
|
10
|
+
const events = MarkerEvents.use(() => marker);
|
|
9
11
|
|
|
10
12
|
const cfg = $derived(getConfiguration());
|
|
11
13
|
const data = $derived(marker.data ?? []);
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
</script>
|
|
14
16
|
|
|
15
17
|
<Trail
|
|
18
|
+
{...events.props}
|
|
16
19
|
{data}
|
|
17
20
|
x={marker.x}
|
|
18
21
|
y={marker.y}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { TextMarkerConfig } from '../../types/markers/common';
|
|
2
|
+
import type { TextLabelConfig } from '../../types/markers/basic/text';
|
|
3
|
+
/** Where a row's edge along an axis is: a field of the rows, the symbol a transform keyed it by, a function of the row, or a literal. */
|
|
4
|
+
type Edge = unknown;
|
|
5
|
+
/** The edges of the box each row spans, in the data's own terms. */
|
|
6
|
+
export type BoxChannels = {
|
|
7
|
+
x1: Edge;
|
|
8
|
+
x2: Edge;
|
|
9
|
+
y1: Edge;
|
|
10
|
+
y2: Edge;
|
|
11
|
+
};
|
|
12
|
+
/** The scales a box is measured on. */
|
|
13
|
+
export type BoxScales = Record<string, {
|
|
14
|
+
type?: string;
|
|
15
|
+
fn: any;
|
|
16
|
+
} | undefined>;
|
|
17
|
+
/** The pixel size of the box of a row, for the markers whose boxes are bands of a category axis rather than edges. */
|
|
18
|
+
export type BoxSizer = (row: unknown, scales: BoxScales) => {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
} | undefined;
|
|
22
|
+
/** The `text` marker that labels the boxes a marker draws, and where each label's box is. */
|
|
23
|
+
export type BoxLabelMarker = {
|
|
24
|
+
marker: TextMarkerConfig;
|
|
25
|
+
box: BoxChannels | BoxSizer;
|
|
26
|
+
};
|
|
27
|
+
/** What a marker has to say for its boxes to be labelled. */
|
|
28
|
+
export type Labelled = {
|
|
29
|
+
type?: string;
|
|
30
|
+
data?: unknown[];
|
|
31
|
+
label?: TextLabelConfig;
|
|
32
|
+
x?: Edge;
|
|
33
|
+
y?: Edge;
|
|
34
|
+
x1?: Edge;
|
|
35
|
+
x2?: Edge;
|
|
36
|
+
y1?: Edge;
|
|
37
|
+
y2?: Edge;
|
|
38
|
+
isHorizontal?: boolean;
|
|
39
|
+
role?: string;
|
|
40
|
+
thickness?: number;
|
|
41
|
+
/** The marker's styles: the insets of a cell or a bar are read off them. */
|
|
42
|
+
styles?: object;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* How a marker that draws a box per row writes a label in each: the `text` marker for them, centred on the box,
|
|
46
|
+
* and where the box is for it to fit to. A `rect` says it with its edges, a `cell` is a band of each axis, and a bar
|
|
47
|
+
* or a waffle is a band of its category axis and the stretch of its value.
|
|
48
|
+
*/
|
|
49
|
+
export declare class BoxLabel {
|
|
50
|
+
#private;
|
|
51
|
+
/** `undefined` when the marker has no label, or does not say where its boxes are. */
|
|
52
|
+
static of(marker: Labelled, plot?: {
|
|
53
|
+
isHorizontal?: boolean;
|
|
54
|
+
scales?: () => BoxScales | undefined;
|
|
55
|
+
gap?: number;
|
|
56
|
+
}): BoxLabelMarker | undefined;
|
|
57
|
+
}
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const read = (edge) => (row) => typeof edge === 'function'
|
|
2
|
+
? edge(row)
|
|
3
|
+
: typeof edge === 'string' || typeof edge === 'symbol'
|
|
4
|
+
? row?.[edge]
|
|
5
|
+
: edge;
|
|
6
|
+
/** The middle of two positions along an axis, whether numbers or dates. */
|
|
7
|
+
const middle = (from, to) => from instanceof Date || to instanceof Date
|
|
8
|
+
? new Date((Number(from) + Number(to)) / 2)
|
|
9
|
+
: (Number(from) + Number(to)) / 2;
|
|
10
|
+
const bandwidth = (scale) => scale?.type === 'band' ? Number(scale.fn.bandwidth()) : undefined;
|
|
11
|
+
const distance = (scale, from, to) => scale ? Math.abs(Number(scale.fn(to)) - Number(scale.fn(from))) : undefined;
|
|
12
|
+
/**
|
|
13
|
+
* How a marker that draws a box per row writes a label in each: the `text` marker for them, centred on the box,
|
|
14
|
+
* and where the box is for it to fit to. A `rect` says it with its edges, a `cell` is a band of each axis, and a bar
|
|
15
|
+
* or a waffle is a band of its category axis and the stretch of its value.
|
|
16
|
+
*/
|
|
17
|
+
export class BoxLabel {
|
|
18
|
+
/** `undefined` when the marker has no label, or does not say where its boxes are. */
|
|
19
|
+
static of(marker, plot = {}) {
|
|
20
|
+
if (!marker.label)
|
|
21
|
+
return undefined;
|
|
22
|
+
const own = BoxLabel.#shape(marker, plot.isHorizontal ?? marker.isHorizontal ?? false);
|
|
23
|
+
if (!own)
|
|
24
|
+
return undefined;
|
|
25
|
+
const { anchor, outside, gap, ...label } = marker.label;
|
|
26
|
+
const size = typeof own.box === 'function' ? own.box : own.size;
|
|
27
|
+
const placed = BoxLabel.#anchored({
|
|
28
|
+
anchor,
|
|
29
|
+
outside,
|
|
30
|
+
gap: gap ?? plot.gap
|
|
31
|
+
}, label, size, plot.scales);
|
|
32
|
+
return {
|
|
33
|
+
box: own.box,
|
|
34
|
+
marker: {
|
|
35
|
+
// A label outside its shape has nothing to fit to.
|
|
36
|
+
fit: !outside,
|
|
37
|
+
...label,
|
|
38
|
+
...placed,
|
|
39
|
+
type: 'text',
|
|
40
|
+
data: marker.data,
|
|
41
|
+
x: own.x,
|
|
42
|
+
y: own.y
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* What puts a label at `anchor` of its shape: the pixels it is moved from the middle - a function of the row, for
|
|
48
|
+
* the size of its shape is the row's own - and the sides of its point it is written on. `{}` for the middle.
|
|
49
|
+
*/
|
|
50
|
+
static #anchored(where, label, size, scales) {
|
|
51
|
+
const { anchor = 'middle', outside = false } = where;
|
|
52
|
+
if (anchor === 'middle' || !size)
|
|
53
|
+
return {};
|
|
54
|
+
const gap = where.gap ?? 4;
|
|
55
|
+
const horizontal = anchor.endsWith('left') ? -1 : anchor.endsWith('right') ? 1 : 0;
|
|
56
|
+
const vertical = anchor.startsWith('top') ? -1 : anchor.startsWith('bottom') ? 1 : 0;
|
|
57
|
+
// Beyond an edge only means something for an edge: a corner is inside its shape.
|
|
58
|
+
const beyond = outside && horizontal * vertical === 0;
|
|
59
|
+
const away = beyond ? 1 : -1;
|
|
60
|
+
const move = (axis, side, row) => {
|
|
61
|
+
// Read when a label is drawn, not when its marker is made: the scales move as the labels register.
|
|
62
|
+
const current = scales?.();
|
|
63
|
+
const box = current ? size(row, current) : undefined;
|
|
64
|
+
return box ? side * (box[axis] / 2 + away * gap) : 0;
|
|
65
|
+
};
|
|
66
|
+
// A number of the label's own (or its styles') is added to the move: it is the push from the anchor.
|
|
67
|
+
const own = (value, fallback) => Number(typeof value === 'number' ? value : (fallback ?? 0));
|
|
68
|
+
const baseX = own(label.dx, label.styles?.dx);
|
|
69
|
+
const baseY = own(label.dy, label.styles?.dy);
|
|
70
|
+
return {
|
|
71
|
+
dx: horizontal === 0 ? label.dx : (row) => baseX + move('width', horizontal, row),
|
|
72
|
+
dy: vertical === 0 ? label.dy : (row) => baseY + move('height', vertical, row),
|
|
73
|
+
styles: {
|
|
74
|
+
...label.styles,
|
|
75
|
+
textAnchor: label.styles?.textAnchor ??
|
|
76
|
+
(horizontal === 0 ? 'middle' : (horizontal === 1) === beyond ? 'start' : 'end'),
|
|
77
|
+
lineAnchor: label.styles?.lineAnchor ??
|
|
78
|
+
(vertical === 0 ? 'middle' : (vertical === 1) === beyond ? 'top' : 'bottom')
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** A style of the marker, when it is a number: a function of the row says nothing of a box in general. */
|
|
83
|
+
static #style(marker, name) {
|
|
84
|
+
const value = marker.styles?.[name];
|
|
85
|
+
return typeof value === 'number' ? value : undefined;
|
|
86
|
+
}
|
|
87
|
+
static #shape(marker, isHorizontal) {
|
|
88
|
+
if (marker.type === 'cell')
|
|
89
|
+
return BoxLabel.#cell(marker);
|
|
90
|
+
if (marker.type === 'bar') {
|
|
91
|
+
return marker.role === 'segment'
|
|
92
|
+
? BoxLabel.#bar(marker, marker.isHorizontal ?? false)
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
|
95
|
+
if (marker.type === 'waffle')
|
|
96
|
+
return BoxLabel.#bar(marker, isHorizontal);
|
|
97
|
+
return BoxLabel.#edges(marker);
|
|
98
|
+
}
|
|
99
|
+
/** A rectangle: the box is between its edges. */
|
|
100
|
+
static #edges({ x, y, x1, x2, y1, y2 }) {
|
|
101
|
+
// A side with a value and no start, next to one with both its edges, runs from zero to it, as a bar does.
|
|
102
|
+
if (y1 === undefined &&
|
|
103
|
+
y2 === undefined &&
|
|
104
|
+
y !== undefined &&
|
|
105
|
+
x1 !== undefined &&
|
|
106
|
+
x2 !== undefined) {
|
|
107
|
+
[y1, y2] = [0, y];
|
|
108
|
+
}
|
|
109
|
+
if (x1 === undefined &&
|
|
110
|
+
x2 === undefined &&
|
|
111
|
+
x !== undefined &&
|
|
112
|
+
y1 !== undefined &&
|
|
113
|
+
y2 !== undefined) {
|
|
114
|
+
[x1, x2] = [0, x];
|
|
115
|
+
}
|
|
116
|
+
if ([x1, x2, y1, y2].some((edge) => edge === undefined))
|
|
117
|
+
return undefined;
|
|
118
|
+
const [left, right, top, bottom] = [x1, x2, y1, y2].map(read);
|
|
119
|
+
const size = (row, scales) => {
|
|
120
|
+
const [width, height] = [
|
|
121
|
+
distance(scales.x, left(row), right(row)),
|
|
122
|
+
distance(scales.y, top(row), bottom(row))
|
|
123
|
+
];
|
|
124
|
+
return width === undefined || height === undefined ? undefined : { width, height };
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
box: { x1, x2, y1, y2 },
|
|
128
|
+
size,
|
|
129
|
+
x: (row) => middle(left(row), right(row)),
|
|
130
|
+
y: (row) => middle(top(row), bottom(row))
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** A cell: a band of each axis, less its inset. */
|
|
134
|
+
static #cell(marker) {
|
|
135
|
+
if (marker.x === undefined || marker.y === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
const inset = Number(BoxLabel.#style(marker, 'inset') ?? 0.5);
|
|
138
|
+
const box = (_, scales) => {
|
|
139
|
+
const [width, height] = [bandwidth(scales.x), bandwidth(scales.y)];
|
|
140
|
+
return width === undefined || height === undefined
|
|
141
|
+
? undefined
|
|
142
|
+
: { width: width - 2 * inset, height: height - 2 * inset };
|
|
143
|
+
};
|
|
144
|
+
return { box: box, x: marker.x, y: marker.y };
|
|
145
|
+
}
|
|
146
|
+
/** A bar or a waffle: a band of the category axis, as wide as its insets leave it, and as long as its value. */
|
|
147
|
+
static #bar(marker, isHorizontal) {
|
|
148
|
+
const [category, from, to] = isHorizontal
|
|
149
|
+
? [marker.y, marker.x1, marker.x2]
|
|
150
|
+
: [marker.x, marker.y1, marker.y2];
|
|
151
|
+
const value = isHorizontal ? marker.x : marker.y;
|
|
152
|
+
// Without both ends, the bar runs from zero to its value.
|
|
153
|
+
const [start, end] = from !== undefined && to !== undefined ? [from, to] : [0, value];
|
|
154
|
+
if (category === undefined || end === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
const [first, last] = [start, end].map(read);
|
|
157
|
+
const [before, after] = isHorizontal
|
|
158
|
+
? [BoxLabel.#style(marker, 'insetTop'), BoxLabel.#style(marker, 'insetBottom')]
|
|
159
|
+
: [BoxLabel.#style(marker, 'insetLeft'), BoxLabel.#style(marker, 'insetRight')];
|
|
160
|
+
const middleOf = (row) => middle(first(row), last(row));
|
|
161
|
+
const box = (row, scales) => {
|
|
162
|
+
const band = bandwidth(isHorizontal ? scales.y : scales.x);
|
|
163
|
+
const length = distance(isHorizontal ? scales.x : scales.y, first(row), last(row));
|
|
164
|
+
if (band === undefined || length === undefined)
|
|
165
|
+
return undefined;
|
|
166
|
+
const thickness = marker.thickness ?? band * (1 - Number(before ?? 0) - Number(after ?? 0));
|
|
167
|
+
return isHorizontal
|
|
168
|
+
? { width: length, height: thickness }
|
|
169
|
+
: { width: thickness, height: length };
|
|
170
|
+
};
|
|
171
|
+
return isHorizontal
|
|
172
|
+
? { box: box, x: middleOf, y: category }
|
|
173
|
+
: { box: box, x: category, y: middleOf };
|
|
174
|
+
}
|
|
175
|
+
}
|