@fundar/data-chart-telling 1.8.1 → 1.10.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 +27 -0
- package/dist/configuration/config.svelte.js +3 -5
- package/dist/layout/interaction/PlotInteraction.svelte.js +1 -1
- package/dist/layout/legend/ContinuousSection.svelte +90 -21
- package/dist/layout/legend/DisabledStyle.d.ts +1 -1
- package/dist/layout/legend/DisabledStyle.js +9 -9
- package/dist/layout/styles/BasicStyles.d.ts +13 -0
- package/dist/layout/styles/BasicStyles.js +31 -0
- package/dist/markers/arrow/ArrowMarker.svelte +2 -0
- package/dist/markers/rule/RuleMarker.svelte +2 -0
- package/dist/phases/data/DataPhase.js +5 -1
- package/dist/phases/transforms/TransformsPhase.js +2 -2
- package/dist/plots/correlation/heatmap/defaults.js +1 -1
- package/dist/plots/correlation/heatmap/markers.js +5 -1
- package/dist/plots/correlation/scatter/defaults.js +2 -1
- package/dist/plots/distribution/box/defaults.js +5 -1
- package/dist/plots/distribution/box/markers.js +9 -2
- package/dist/plots/distribution/density/defaults.js +7 -1
- package/dist/plots/distribution/density/markers.js +4 -0
- package/dist/plots/distribution/histogram/defaults.js +7 -1
- package/dist/plots/distribution/histogram/markers.js +10 -2
- package/dist/plots/distribution/pyramid/defaults.js +5 -1
- package/dist/plots/distribution/pyramid/markers.js +10 -1
- package/dist/plots/distribution/violin/defaults.js +5 -1
- package/dist/plots/distribution/violin/markers.js +4 -0
- package/dist/plots/evolution/area/defaults.js +3 -1
- package/dist/plots/evolution/area/markers.js +18 -9
- package/dist/plots/evolution/line/defaults.js +6 -1
- package/dist/plots/evolution/line/markers.d.ts +2 -2
- package/dist/plots/evolution/line/markers.js +5 -5
- package/dist/plots/evolution/slope/markers.js +1 -1
- package/dist/plots/ranking/bar/defaults.js +5 -1
- package/dist/plots/ranking/bar/markers.js +10 -2
- package/dist/plots/ranking/cleveland/ClevelandMarkers.js +37 -12
- package/dist/plots/ranking/cleveland/defaults.js +4 -2
- package/dist/plots/utils/segments.d.ts +3 -1
- package/dist/plots/utils/segments.js +40 -19
- package/dist/types/configuration/styling.d.ts +5 -7
- package/dist/types/layout/legend.d.ts +1 -1
- package/dist/types/layout/styles/base.d.ts +27 -1
- package/dist/types/phases/data/phase.d.ts +3 -0
- package/dist/types/phases/state.d.ts +2 -0
- package/dist/types/phases/transforms/phase.d.ts +2 -0
- package/dist/types/plots/segments/common.d.ts +2 -2
- package/docs/plots/ranking/dumbbell.md +1 -1
- package/docs/plots/ranking/lollipop.md +1 -1
- package/docs/styles-and-segments.md +32 -2
- package/docs/theming.md +28 -29
- package/examples/SegmentsBySeries.svelte +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [1.10.0] - 2026-11-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `styles.stroke`, `styles.dot` and `styles.area` on every axis-based plot: the style every series is drawn with, each as it is or as a function of the series' name. A plot uses the ones its own marks are made of and ignores the rest: `stroke` and `dot` on `LinePlot`, `SlopePlot`, `SparklinePlot` and the plots with connectors (`LollipopPlot`, `DumbbellPlot`, `ClevelandPlot`, `ForestPlot`); `area` on `AreaPlot`, `BarPlot`, `BulletPlot`, `PyramidPlot`, `HistogramPlot`, `BoxPlot`, `DensityPlot`, `ViolinPlot` and `HeatmapPlot`; `dot` on `ScatterPlot`. A segment's own style wins over them field by field, a connector styled with `styles.connector` wins over `styles.stroke`, and the connectors of those plots - and their arrows - take its dashes and line caps and joins (`strokeDasharray`, `strokeLinecap`, `strokeLinejoin`) from it or from a segment, and a series a legend turned off dims over all of them. Before, restyling every series - or one of them - took a `segments` entry per series.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- The outline an area style sets - `stroke`, `strokeWidth`, `strokeOpacity`, `strokeDasharray` and `borderRadius` - reaches the bars of `BarPlot`, `BulletPlot` and `PyramidPlot` and the bins of `HistogramPlot`, whether it comes from a segment or from `styles.area`. These plots read only the fill of a segment's style, so the rest was ignored; a series a legend dims fades its outline with its fill.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Breaking:** the dots of a line's style are `dot`, not `dots`, so they read like `styles.dot`: `LineSegmentStyle.dots` (in `segments` and in the `styles.gaps` zones), the legend's `LegendDisabledStyle.dots` and `line.gap.dots` in the configuration. Rename them where they are set.
|
|
18
|
+
|
|
19
|
+
## [1.9.0] - 2026-11-03
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- A continuous legend no longer lets its labels touch. Its edge labels (the two ends of the domain) are always written; a tick label is left out, with its mark, when it comes within `legend.continuous.label.overlap.margin` px of an edge label or of a tick before it (weighed from the low end up). Before, a tick near an end was written over it.
|
|
24
|
+
- In the interactive continuous legend, handle labels that meet no longer turn vertical: the label of the handle that rests on the domain's end is hidden, and the moving one stays horizontal and writes both values (`18% – 20%`) until they part. That label follows its handle but is kept inside the legend's width. If both or neither rest on an end, the one moved last stays.
|
|
25
|
+
|
|
26
|
+
- `legend.continuous.label.weight` (`--dct-continuous-label-weight`) styles every label of a continuous legend - the edges, the ticks and the interactive handles - and defaults to `400`. It styled only the handle labels, at `600`, so an interactive legend wrote its values in bold and a plain one did not. Set it to `600` to keep the old look of the handles.
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- `legend.continuous.label.overlap.rotate` and its CSS variable `--dct-continuous-label-overlap-rotate`: there is no vertical label to rotate any more. `overlap.enabled` and `overlap.margin` stay, and now govern both behaviours above.
|
|
31
|
+
|
|
5
32
|
## [1.8.1] - 2026-11-02
|
|
6
33
|
|
|
7
34
|
### Fixed
|
|
@@ -140,11 +140,10 @@ export const DEFAULT_CONFIG = {
|
|
|
140
140
|
label: {
|
|
141
141
|
rotate: '0deg',
|
|
142
142
|
size: '0.875rem',
|
|
143
|
-
weight:
|
|
143
|
+
weight: 400,
|
|
144
144
|
overlap: {
|
|
145
145
|
enabled: true,
|
|
146
|
-
margin: 4
|
|
147
|
-
rotate: '90deg'
|
|
146
|
+
margin: 4
|
|
148
147
|
}
|
|
149
148
|
}
|
|
150
149
|
}
|
|
@@ -304,7 +303,7 @@ export const DEFAULT_CONFIG = {
|
|
|
304
303
|
dotRadius: 4,
|
|
305
304
|
gap: {
|
|
306
305
|
stroke: { strokeOpacity: 0.6, strokeDasharray: '4 4' },
|
|
307
|
-
|
|
306
|
+
dot: {}
|
|
308
307
|
}
|
|
309
308
|
},
|
|
310
309
|
lollipop: {
|
|
@@ -392,7 +391,6 @@ export function configToCssVars(c) {
|
|
|
392
391
|
'--dct-continuous-label-rotate': c.legend.continuous.label.rotate,
|
|
393
392
|
'--dct-continuous-label-size': c.legend.continuous.label.size,
|
|
394
393
|
'--dct-continuous-label-weight': String(c.legend.continuous.label.weight),
|
|
395
|
-
'--dct-continuous-label-overlap-rotate': c.legend.continuous.label.overlap.rotate,
|
|
396
394
|
'--dct-tooltip-bg': c.tooltip.background,
|
|
397
395
|
'--dct-tooltip-color': c.tooltip.color,
|
|
398
396
|
'--dct-tooltip-size': c.tooltip.fontSize,
|
|
@@ -47,7 +47,7 @@ export class PlotInteraction {
|
|
|
47
47
|
}
|
|
48
48
|
/** `series`' dots while dimmed. */
|
|
49
49
|
dotsFor(series) {
|
|
50
|
-
return DisabledStyle.
|
|
50
|
+
return DisabledStyle.dot(this.dimmed(series));
|
|
51
51
|
}
|
|
52
52
|
/** Isolates `series` - every other one dims - or ends it with `null`. */
|
|
53
53
|
isolate(series) {
|
|
@@ -51,6 +51,32 @@
|
|
|
51
51
|
}))
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
+
// ── Labels that would touch ───────────────────────────────────────────────
|
|
55
|
+
// The edge labels (the domain's two ends) are always written. A tick label stays only if it clears them and
|
|
56
|
+
// every tick kept before it, by `overlap.margin` px; ticks are weighed from the low end up.
|
|
57
|
+
const overlapLook = $derived(cfg.legend.continuous.label.overlap);
|
|
58
|
+
let edgeWidths = $state({ start: 0, end: 0 });
|
|
59
|
+
let tickWidths = $state<Record<number, number>>({});
|
|
60
|
+
|
|
61
|
+
const hiddenTicks = $derived.by((): number[] => {
|
|
62
|
+
const hidden: number[] = [];
|
|
63
|
+
if (isInteractive || !overlapLook.enabled) return hidden;
|
|
64
|
+
const margin = overlapLook.margin;
|
|
65
|
+
const taken: [number, number][] = [
|
|
66
|
+
[0, edgeWidths.start],
|
|
67
|
+
[barWidth - edgeWidths.end, barWidth]
|
|
68
|
+
];
|
|
69
|
+
for (const tick of [...tickPositions].sort((a, b) => a.t - b.t)) {
|
|
70
|
+
const half = (tickWidths[tick.v] ?? 0) / 2;
|
|
71
|
+
const from = trackX(tick.t) - half;
|
|
72
|
+
const to = trackX(tick.t) + half;
|
|
73
|
+
if (taken.some(([start, end]) => from < end + margin && to > start - margin))
|
|
74
|
+
hidden.push(tick.v);
|
|
75
|
+
else taken.push([from, to]);
|
|
76
|
+
}
|
|
77
|
+
return hidden;
|
|
78
|
+
});
|
|
79
|
+
|
|
54
80
|
// ── Drag-to-narrow-margins ────────────────────────────────────────────────
|
|
55
81
|
const interaction = LegendState.ensure();
|
|
56
82
|
|
|
@@ -58,6 +84,9 @@
|
|
|
58
84
|
let lo = $state(untrack(() => section.min));
|
|
59
85
|
let hi = $state(untrack(() => section.max));
|
|
60
86
|
|
|
87
|
+
// The handle moved last, which keeps its label when the two labels meet and one has to go.
|
|
88
|
+
let lastDragged = $state<'lo' | 'hi' | null>(null);
|
|
89
|
+
|
|
61
90
|
// Whether each handle was dragged; an untouched one follows the section's bounds.
|
|
62
91
|
let loAdjusted = $state(false);
|
|
63
92
|
let hiAdjusted = $state(false);
|
|
@@ -150,6 +179,7 @@
|
|
|
150
179
|
event.preventDefault();
|
|
151
180
|
if (which === 'lo') loAdjusted = true;
|
|
152
181
|
else hiAdjusted = true;
|
|
182
|
+
lastDragged = which;
|
|
153
183
|
const target = event.currentTarget as SVGElement;
|
|
154
184
|
target.setPointerCapture(event.pointerId);
|
|
155
185
|
const startX = event.clientX;
|
|
@@ -190,12 +220,40 @@
|
|
|
190
220
|
|
|
191
221
|
let loLabelWidth = $state(0);
|
|
192
222
|
let hiLabelWidth = $state(0);
|
|
223
|
+
let mergedLabelWidth = $state(0);
|
|
193
224
|
|
|
194
225
|
const labelsOverlap = $derived(
|
|
195
226
|
isInteractive &&
|
|
196
|
-
|
|
197
|
-
(hiT - loT) * trackWidth - loLabelWidth - hiLabelWidth <
|
|
198
|
-
|
|
227
|
+
overlapLook.enabled &&
|
|
228
|
+
(hiT - loT) * trackWidth - loLabelWidth - hiLabelWidth < overlapLook.margin
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// When the handle labels meet, the one whose handle still sits on the domain's end gives way and the moving
|
|
232
|
+
// one stays, horizontal, and writes both values ("18% – 20%"). If both or neither sit on an end, the one moved
|
|
233
|
+
// last stays.
|
|
234
|
+
const hiddenHandle = $derived.by((): 'lo' | 'hi' | null => {
|
|
235
|
+
if (!labelsOverlap) return null;
|
|
236
|
+
const loOnEnd = lo <= section.min;
|
|
237
|
+
const hiOnEnd = hi >= section.max;
|
|
238
|
+
if (loOnEnd !== hiOnEnd) return loOnEnd ? 'lo' : 'hi';
|
|
239
|
+
return lastDragged === 'lo' ? 'hi' : 'lo';
|
|
240
|
+
});
|
|
241
|
+
const survivingHandle = $derived(
|
|
242
|
+
hiddenHandle === null ? null : hiddenHandle === 'lo' ? 'hi' : 'lo'
|
|
243
|
+
);
|
|
244
|
+
const mergedLabel = $derived(`${fmt(lo)} – ${fmt(hi)}`);
|
|
245
|
+
|
|
246
|
+
// Where each handle label is anchored: at its handle, except the merged one, which follows its handle but is
|
|
247
|
+
// kept inside the legend's width, growing inward from the end it sits at.
|
|
248
|
+
const loLabelLeft = $derived(
|
|
249
|
+
survivingHandle === 'lo'
|
|
250
|
+
? Math.max(0, Math.min(trackX(loT), barWidth - mergedLabelWidth))
|
|
251
|
+
: trackX(loT)
|
|
252
|
+
);
|
|
253
|
+
const hiLabelLeft = $derived(
|
|
254
|
+
survivingHandle === 'hi'
|
|
255
|
+
? Math.min(barWidth, Math.max(trackX(hiT), mergedLabelWidth))
|
|
256
|
+
: trackX(hiT)
|
|
199
257
|
);
|
|
200
258
|
</script>
|
|
201
259
|
|
|
@@ -247,7 +305,9 @@
|
|
|
247
305
|
fill="url(#{gradientId})"
|
|
248
306
|
/>
|
|
249
307
|
{#each tickPositions as tick (tick.v)}
|
|
250
|
-
|
|
308
|
+
{#if !hiddenTicks.includes(tick.v)}
|
|
309
|
+
<rect class="dct-continuous__tick-mark" x={trackX(tick.t)} y={barHeight} />
|
|
310
|
+
{/if}
|
|
251
311
|
{/each}
|
|
252
312
|
|
|
253
313
|
{#if isInteractive}
|
|
@@ -328,22 +388,37 @@
|
|
|
328
388
|
<span class="dct-continuous__tick-measure" aria-hidden="true" bind:clientWidth={hiLabelWidth}
|
|
329
389
|
>{fmt(hi)}</span
|
|
330
390
|
>
|
|
391
|
+
<span
|
|
392
|
+
class="dct-continuous__tick-measure"
|
|
393
|
+
aria-hidden="true"
|
|
394
|
+
bind:clientWidth={mergedLabelWidth}>{mergedLabel}</span
|
|
395
|
+
>
|
|
331
396
|
<span
|
|
332
397
|
class="dct-continuous__tick dct-continuous__tick--handle-lo"
|
|
333
|
-
class:dct-continuous__tick--
|
|
334
|
-
style:left="{
|
|
398
|
+
class:dct-continuous__tick--hidden={hiddenHandle === 'lo'}
|
|
399
|
+
style:left="{loLabelLeft}px">{survivingHandle === 'lo' ? mergedLabel : fmt(lo)}</span
|
|
335
400
|
>
|
|
336
401
|
<span
|
|
337
402
|
class="dct-continuous__tick dct-continuous__tick--handle-hi"
|
|
338
|
-
class:dct-continuous__tick--
|
|
339
|
-
style:left="{
|
|
403
|
+
class:dct-continuous__tick--hidden={hiddenHandle === 'hi'}
|
|
404
|
+
style:left="{hiLabelLeft}px">{survivingHandle === 'hi' ? mergedLabel : fmt(hi)}</span
|
|
340
405
|
>
|
|
341
406
|
{:else}
|
|
342
|
-
<span
|
|
407
|
+
<span
|
|
408
|
+
class="dct-continuous__edge dct-continuous__edge--start"
|
|
409
|
+
bind:clientWidth={edgeWidths.start}>{fmt(section.min)}</span
|
|
410
|
+
>
|
|
343
411
|
{#each tickPositions as tick (tick.v)}
|
|
344
|
-
<span
|
|
412
|
+
<span
|
|
413
|
+
class="dct-continuous__tick"
|
|
414
|
+
class:dct-continuous__tick--hidden={hiddenTicks.includes(tick.v)}
|
|
415
|
+
style:left="{trackX(tick.t)}px"
|
|
416
|
+
bind:clientWidth={tickWidths[tick.v]}>{tick.label}</span
|
|
417
|
+
>
|
|
345
418
|
{/each}
|
|
346
|
-
<span class="dct-continuous__edge dct-continuous__edge--end"
|
|
419
|
+
<span class="dct-continuous__edge dct-continuous__edge--end" bind:clientWidth={edgeWidths.end}
|
|
420
|
+
>{fmt(section.max)}</span
|
|
421
|
+
>
|
|
347
422
|
{/if}
|
|
348
423
|
</div>
|
|
349
424
|
</div>
|
|
@@ -358,6 +433,7 @@
|
|
|
358
433
|
position: relative;
|
|
359
434
|
height: 1rem;
|
|
360
435
|
font-size: var(--dct-continuous-label-size, 0.875rem);
|
|
436
|
+
font-weight: var(--dct-continuous-label-weight, 400);
|
|
361
437
|
user-select: none;
|
|
362
438
|
}
|
|
363
439
|
.dct-continuous__edge {
|
|
@@ -376,10 +452,6 @@
|
|
|
376
452
|
transform: translateX(-50%) rotate(var(--dct-continuous-label-rotate, 0deg));
|
|
377
453
|
}
|
|
378
454
|
/* Anchored outward so the label grows into the bar. */
|
|
379
|
-
.dct-continuous__tick--handle-lo,
|
|
380
|
-
.dct-continuous__tick--handle-hi {
|
|
381
|
-
font-weight: var(--dct-continuous-label-weight, 600);
|
|
382
|
-
}
|
|
383
455
|
.dct-continuous__tick--handle-lo {
|
|
384
456
|
transform: rotate(var(--dct-continuous-label-rotate, 0deg));
|
|
385
457
|
}
|
|
@@ -392,13 +464,10 @@
|
|
|
392
464
|
position: absolute;
|
|
393
465
|
visibility: hidden;
|
|
394
466
|
white-space: nowrap;
|
|
395
|
-
font-weight: var(--dct-continuous-label-weight, 600);
|
|
396
467
|
}
|
|
397
|
-
/*
|
|
398
|
-
.dct-continuous__tick--
|
|
399
|
-
|
|
400
|
-
transform-origin: left bottom;
|
|
401
|
-
transform: rotate(var(--dct-continuous-label-overlap-rotate, 90deg));
|
|
468
|
+
/* A label that would touch another keeps its place, so its width stays measured, and is not drawn. */
|
|
469
|
+
.dct-continuous__tick--hidden {
|
|
470
|
+
visibility: hidden;
|
|
402
471
|
}
|
|
403
472
|
/* Tick mark below the bar at each non-interactive tick's position. */
|
|
404
473
|
.dct-continuous__tick-mark {
|
|
@@ -11,7 +11,7 @@ export declare class DisabledStyle {
|
|
|
11
11
|
/** The style a legend item draws with while off - it stays visible, so it can be turned back on. */
|
|
12
12
|
static ofItem(action: LegendDisabledAction | undefined, fallbackOpacity: number): LegendDisabledStyle | undefined;
|
|
13
13
|
static stroke(disabled: LegendDisabledStyle | undefined): StrokeStyle | undefined;
|
|
14
|
-
static
|
|
14
|
+
static dot(disabled: LegendDisabledStyle | undefined): DotStyle | undefined;
|
|
15
15
|
static fill(disabled: LegendDisabledStyle | undefined): {
|
|
16
16
|
fill?: string;
|
|
17
17
|
fillOpacity?: number;
|
|
@@ -32,18 +32,18 @@ export class DisabledStyle {
|
|
|
32
32
|
strokeLinejoin: disabled.stroke?.strokeLinejoin
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
static
|
|
35
|
+
static dot(disabled) {
|
|
36
36
|
if (!disabled)
|
|
37
37
|
return undefined;
|
|
38
38
|
return {
|
|
39
|
-
dotRadius: disabled.
|
|
40
|
-
dotFill: disabled.
|
|
41
|
-
dotFillOpacity: disabled.
|
|
42
|
-
dotSymbol: disabled.
|
|
43
|
-
dotStroke: disabled.
|
|
44
|
-
dotStrokeWidth: disabled.
|
|
45
|
-
dotStrokeOpacity: disabled.
|
|
46
|
-
dotStrokeDasharray: disabled.
|
|
39
|
+
dotRadius: disabled.dot?.dotRadius,
|
|
40
|
+
dotFill: disabled.dot?.dotFill,
|
|
41
|
+
dotFillOpacity: disabled.dot?.dotFillOpacity ?? disabled.opacity,
|
|
42
|
+
dotSymbol: disabled.dot?.dotSymbol,
|
|
43
|
+
dotStroke: disabled.dot?.dotStroke,
|
|
44
|
+
dotStrokeWidth: disabled.dot?.dotStrokeWidth,
|
|
45
|
+
dotStrokeOpacity: disabled.dot?.dotStrokeOpacity ?? disabled.opacity,
|
|
46
|
+
dotStrokeDasharray: disabled.dot?.dotStrokeDasharray
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
static fill(disabled) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BasicStylesConfig, BasicStylesShape, PerSeries } from '../../types/layout/styles';
|
|
2
|
+
/**
|
|
3
|
+
* The style a plot draws every series with, out of its `styles.stroke`,
|
|
4
|
+
* `styles.dot` and `styles.area`: the ones its kind's segment style is made of
|
|
5
|
+
* (`shape`), each as it is or as a function of the series' name. The segments
|
|
6
|
+
* of a series restyle a stretch of it over this.
|
|
7
|
+
*/
|
|
8
|
+
export declare class BasicStyles {
|
|
9
|
+
/** The style of the series named `name`, or `undefined` when there is none to give. */
|
|
10
|
+
static of<TStyle>(styles: BasicStylesConfig | undefined, shape: BasicStylesShape | undefined): ((seriesName: string) => TStyle | undefined) | undefined;
|
|
11
|
+
/** A style as it is, or what its function says of the series named `name`; a function has nothing to say without a name. */
|
|
12
|
+
static read<TStyle extends object>(style: PerSeries<TStyle> | undefined, name: string | undefined): TStyle | undefined;
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The style a plot draws every series with, out of its `styles.stroke`,
|
|
3
|
+
* `styles.dot` and `styles.area`: the ones its kind's segment style is made of
|
|
4
|
+
* (`shape`), each as it is or as a function of the series' name. The segments
|
|
5
|
+
* of a series restyle a stretch of it over this.
|
|
6
|
+
*/
|
|
7
|
+
export class BasicStyles {
|
|
8
|
+
/** The style of the series named `name`, or `undefined` when there is none to give. */
|
|
9
|
+
static of(styles, shape) {
|
|
10
|
+
if (!styles || !shape)
|
|
11
|
+
return undefined;
|
|
12
|
+
const keys = 'flat' in shape ? [shape.flat] : shape.nested;
|
|
13
|
+
if (keys.every((key) => styles[key] === undefined))
|
|
14
|
+
return undefined;
|
|
15
|
+
return (name) => {
|
|
16
|
+
const picked = Object.fromEntries(keys.flatMap((key) => {
|
|
17
|
+
const style = BasicStyles.read(styles[key], name);
|
|
18
|
+
return style ? [[key, style]] : [];
|
|
19
|
+
}));
|
|
20
|
+
if (Object.keys(picked).length === 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
return ('flat' in shape ? picked[shape.flat] : picked);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** A style as it is, or what its function says of the series named `name`; a function has nothing to say without a name. */
|
|
26
|
+
static read(style, name) {
|
|
27
|
+
if (typeof style !== 'function')
|
|
28
|
+
return style;
|
|
29
|
+
return name === undefined ? undefined : style(name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -3,6 +3,7 @@ import { ColorRamp } from '../../utils/color';
|
|
|
3
3
|
import { resolveAccessor } from '../../utils/accessors';
|
|
4
4
|
import { buildGroupedSeries, buildScopedMarkerGroups } from '../../plots/utils/segments';
|
|
5
5
|
import { buildGapFillMaps, isGapValue } from '../../plots/utils/gaps';
|
|
6
|
+
import { BasicStyles } from '../../layout/styles/BasicStyles';
|
|
6
7
|
import { FacetPanels } from '../facet/FacetPanels';
|
|
7
8
|
import { PlotAxes } from './PlotAxes';
|
|
8
9
|
/**
|
|
@@ -39,10 +40,12 @@ export class DataPhase {
|
|
|
39
40
|
? FacetPanels.splitSeries(fullSeries, state.panels, state.panelOf)
|
|
40
41
|
: fullSeries;
|
|
41
42
|
const resolvedSeries = ctx.interaction?.legend?.withoutOmitted(panelSeries) ?? panelSeries;
|
|
42
|
-
const
|
|
43
|
+
const baseFor = BasicStyles.of(state.styles, options.segments?.base);
|
|
44
|
+
const groupedSeries = buildGroupedSeries(resolvedSeries, segments, connect, baseFor);
|
|
43
45
|
const scopedGroups = buildScopedMarkerGroups({
|
|
44
46
|
groupedSeries,
|
|
45
47
|
segments,
|
|
48
|
+
baseFor,
|
|
46
49
|
colorFor: (s) => colorForSeries(s.name),
|
|
47
50
|
segmentColorFor: (style, defaultColor) => segmentColor?.(style) ?? defaultColor
|
|
48
51
|
});
|
|
@@ -57,6 +60,7 @@ export class DataPhase {
|
|
|
57
60
|
seriesGrouping,
|
|
58
61
|
allSeriesNames: fullSeries.map((s) => s.name),
|
|
59
62
|
groupedSeries,
|
|
63
|
+
segmentBase: baseFor,
|
|
60
64
|
flat: resolvedSeries.flatMap((s) => s.data),
|
|
61
65
|
scopedGroups,
|
|
62
66
|
colorForSeries,
|
|
@@ -159,7 +159,7 @@ export class TransformsPhase {
|
|
|
159
159
|
static #buildGroups(state, connect) {
|
|
160
160
|
const { data } = state.processedData;
|
|
161
161
|
const series = (state.resolvedSeries ?? []).map((s) => TransformsPhase.#processedSeries(state, s, data.filter((row) => row[SERIES_ID_KEY] === seriesIdOf(s))));
|
|
162
|
-
return buildGroupedSeries(series, state.segments, connect);
|
|
162
|
+
return buildGroupedSeries(series, state.segments, connect, state.segmentBase);
|
|
163
163
|
}
|
|
164
164
|
/**
|
|
165
165
|
* The processed rows in their own order, split into runs of consecutive rows
|
|
@@ -181,7 +181,7 @@ export class TransformsPhase {
|
|
|
181
181
|
else
|
|
182
182
|
runs.push(TransformsPhase.#processedSeries(state, source, [row]));
|
|
183
183
|
}
|
|
184
|
-
return buildGroupedSeries(runs, state.segments, false);
|
|
184
|
+
return buildGroupedSeries(runs, state.segments, false, state.segmentBase);
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
187
187
|
* One series accessor shared by every series as-is, so a field key keeps
|
|
@@ -57,7 +57,7 @@ function extendWithColorScales(state, ctx) {
|
|
|
57
57
|
*/
|
|
58
58
|
export function buildHeatmapDefaults() {
|
|
59
59
|
return {
|
|
60
|
-
data: { discreteAxes: 2 },
|
|
60
|
+
data: { discreteAxes: 2, segments: { base: { flat: 'area' } } },
|
|
61
61
|
mapping: {
|
|
62
62
|
// A heatmap with no `color` still reads one per cell, as nothing.
|
|
63
63
|
extend: (state) => ({
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { LegendState } from '../../../layout/legend/LegendState.svelte';
|
|
2
2
|
import { DisabledStyle } from '../../../layout/legend/DisabledStyle';
|
|
3
3
|
import { resolveAccessor } from '../../../utils/accessors';
|
|
4
|
+
import { merge } from '../../../utils/merge';
|
|
5
|
+
import { SERIES_KEY } from '../../../phases/transforms/TransformsPhase';
|
|
4
6
|
/** A processed channel as a per-row function. */
|
|
5
7
|
function readChannel(channel) {
|
|
6
8
|
return resolveAccessor(channel);
|
|
@@ -103,8 +105,10 @@ export function buildMarks(state, ctx) {
|
|
|
103
105
|
return base;
|
|
104
106
|
return { ...base, ...DisabledStyle.fill(range.disabledStyle) };
|
|
105
107
|
}
|
|
108
|
+
// `styles.area`, as the cell's series sets it: what every cell is drawn with, under its segments.
|
|
106
109
|
function getCellStyle(d) {
|
|
107
|
-
|
|
110
|
+
const base = state.segmentBase?.(String(d[SERIES_KEY]));
|
|
111
|
+
return rangeFilteredStyle(merge(base ?? {}, segStyleMap.get(`${String(getX(d))}::${String(getY(d))}`)), Number(getColor(d)));
|
|
108
112
|
}
|
|
109
113
|
const styleGroups = (() => {
|
|
110
114
|
const groups = new Map();
|
|
@@ -14,7 +14,11 @@ export function buildBoxDefaults() {
|
|
|
14
14
|
return {
|
|
15
15
|
data: {
|
|
16
16
|
discreteAxes: 1,
|
|
17
|
-
segments: {
|
|
17
|
+
segments: {
|
|
18
|
+
color: (style) => style?.fill,
|
|
19
|
+
connect: false,
|
|
20
|
+
base: { flat: 'area' }
|
|
21
|
+
}
|
|
18
22
|
},
|
|
19
23
|
scales: {
|
|
20
24
|
bandOrder: 'data',
|
|
@@ -15,7 +15,14 @@ export function buildMarks(state, ctx) {
|
|
|
15
15
|
const disabled = ctx.interaction?.fillFor(name);
|
|
16
16
|
return group.visualSegments.map((seg) => {
|
|
17
17
|
const { stroke, strokeWidth, strokeOpacity, borderRadius } = seg.styles;
|
|
18
|
-
const
|
|
18
|
+
const fill = disabled?.fill ?? seg.styles.fill ?? state.colorForSeries(name);
|
|
19
|
+
// An outline asked for without a color is the fill's.
|
|
20
|
+
const outline = {
|
|
21
|
+
stroke: stroke ?? (strokeWidth !== undefined ? fill : undefined),
|
|
22
|
+
strokeWidth,
|
|
23
|
+
strokeOpacity,
|
|
24
|
+
borderRadius
|
|
25
|
+
};
|
|
19
26
|
return {
|
|
20
27
|
type: 'box',
|
|
21
28
|
series: name,
|
|
@@ -25,7 +32,7 @@ export function buildMarks(state, ctx) {
|
|
|
25
32
|
bar: {
|
|
26
33
|
// The box's own outline, from its segment; unset fields stay svelteplot's.
|
|
27
34
|
...Object.fromEntries(Object.entries(outline).filter(([, v]) => v !== undefined)),
|
|
28
|
-
fill
|
|
35
|
+
fill,
|
|
29
36
|
fillOpacity: disabled?.fillOpacity ?? seg.styles.fillOpacity ?? cfg.box.fillOpacity
|
|
30
37
|
}
|
|
31
38
|
};
|
|
@@ -19,7 +19,13 @@ function mergeDensity(own, horizontal) {
|
|
|
19
19
|
*/
|
|
20
20
|
export function buildDensityDefaults() {
|
|
21
21
|
return {
|
|
22
|
-
data: {
|
|
22
|
+
data: {
|
|
23
|
+
segments: {
|
|
24
|
+
color: (style) => style?.fill,
|
|
25
|
+
connect: false,
|
|
26
|
+
base: { flat: 'area' }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
23
29
|
transforms: {
|
|
24
30
|
transforms: (state) => mergeDensity(state.transforms ?? [], state.axes?.isHorizontal ?? false)
|
|
25
31
|
},
|
|
@@ -24,6 +24,10 @@ export function buildMarks(state, ctx) {
|
|
|
24
24
|
strokeDasharray: disabledStroke?.strokeDasharray ?? seg.styles.strokeDasharray
|
|
25
25
|
}
|
|
26
26
|
: {
|
|
27
|
+
// An outline asked for without a color is the fill's.
|
|
28
|
+
...(seg.styles.strokeWidth !== undefined
|
|
29
|
+
? { stroke: disabledFill?.fill ?? color }
|
|
30
|
+
: {}),
|
|
27
31
|
...seg.styles,
|
|
28
32
|
fill: disabledFill?.fill ?? color,
|
|
29
33
|
fillOpacity: disabledFill?.fillOpacity ?? seg.styles.fillOpacity ?? cfg.density.fillOpacity
|
|
@@ -32,7 +32,13 @@ function mergeBin(own, cfg, horizontal) {
|
|
|
32
32
|
*/
|
|
33
33
|
export function buildHistogramDefaults() {
|
|
34
34
|
return {
|
|
35
|
-
data: {
|
|
35
|
+
data: {
|
|
36
|
+
segments: {
|
|
37
|
+
color: (style) => style?.fill,
|
|
38
|
+
connect: false,
|
|
39
|
+
base: { flat: 'area' }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
36
42
|
transforms: {
|
|
37
43
|
transforms: (state, ctx) => mergeBin(state.transforms ?? [], ctx.cfg, state.axes?.isHorizontal ?? false)
|
|
38
44
|
},
|
|
@@ -72,6 +72,8 @@ export function buildMarks(state, ctx) {
|
|
|
72
72
|
const fillOpacity = ctx.cfg.histogram.fillOpacity;
|
|
73
73
|
const rects = (state.processedGroups ?? []).flatMap((group) => {
|
|
74
74
|
const disabled = ctx.interaction?.fillFor(group.series.name);
|
|
75
|
+
// A stroke the segment sets is not a reason to lose the series' fill, as the marker would take it for the color.
|
|
76
|
+
const color = state.colorForSeries(group.series.name);
|
|
75
77
|
return group.visualSegments.map((seg) => Transforms.finish({
|
|
76
78
|
type: 'rect',
|
|
77
79
|
series: group.series.name,
|
|
@@ -79,9 +81,15 @@ export function buildMarks(state, ctx) {
|
|
|
79
81
|
...channels,
|
|
80
82
|
...options,
|
|
81
83
|
styles: {
|
|
84
|
+
// An outline asked for without a color is the fill's.
|
|
85
|
+
...(seg.styles.strokeWidth !== undefined
|
|
86
|
+
? { stroke: disabled?.fill ?? seg.styles.fill ?? color }
|
|
87
|
+
: {}),
|
|
88
|
+
...seg.styles,
|
|
89
|
+
fill: disabled?.fill ?? seg.styles.fill ?? color,
|
|
82
90
|
fillOpacity: disabled?.fillOpacity ?? seg.styles.fillOpacity ?? fillOpacity,
|
|
83
|
-
|
|
84
|
-
...(disabled?.
|
|
91
|
+
// A dimmed series fades its outline with its fill.
|
|
92
|
+
...(disabled?.fillOpacity !== undefined ? { strokeOpacity: disabled.fillOpacity } : {})
|
|
85
93
|
}
|
|
86
94
|
}, transforms));
|
|
87
95
|
});
|
|
@@ -20,7 +20,11 @@ export function buildPyramidDefaults() {
|
|
|
20
20
|
return {
|
|
21
21
|
data: {
|
|
22
22
|
discreteAxes: 1,
|
|
23
|
-
segments: {
|
|
23
|
+
segments: {
|
|
24
|
+
color: (style) => style?.fill,
|
|
25
|
+
connect: false,
|
|
26
|
+
base: { flat: 'area' }
|
|
27
|
+
},
|
|
24
28
|
orderSeries: (series, state) => {
|
|
25
29
|
const sorted = [...series].sort(byName).map((s) => s.name);
|
|
26
30
|
const reversed = state.scales.grouping?.reverse ?? false;
|
|
@@ -119,8 +119,17 @@ export function buildMarks(state, ctx) {
|
|
|
119
119
|
data: realData,
|
|
120
120
|
...span(processedCategory, signed),
|
|
121
121
|
styles: {
|
|
122
|
+
// An outline asked for without a color is the fill's.
|
|
123
|
+
...(seg.styles.strokeWidth !== undefined
|
|
124
|
+
? { stroke: disabledFill?.fill ?? seg.styles.fill ?? defaultColor }
|
|
125
|
+
: {}),
|
|
126
|
+
...seg.styles,
|
|
122
127
|
fill: disabledFill?.fill ?? seg.styles.fill ?? defaultColor,
|
|
123
|
-
fillOpacity: disabledFill?.fillOpacity ?? seg.styles.fillOpacity ?? 1
|
|
128
|
+
fillOpacity: disabledFill?.fillOpacity ?? seg.styles.fillOpacity ?? 1,
|
|
129
|
+
// A dimmed series fades its outline with its fill.
|
|
130
|
+
...(disabledFill?.fillOpacity !== undefined
|
|
131
|
+
? { strokeOpacity: disabledFill.fillOpacity }
|
|
132
|
+
: {})
|
|
124
133
|
}
|
|
125
134
|
});
|
|
126
135
|
}
|
|
@@ -34,7 +34,11 @@ export function buildViolinDefaults() {
|
|
|
34
34
|
return {
|
|
35
35
|
data: {
|
|
36
36
|
discreteAxes: 1,
|
|
37
|
-
segments: {
|
|
37
|
+
segments: {
|
|
38
|
+
color: (style) => style?.fill,
|
|
39
|
+
connect: false,
|
|
40
|
+
base: { flat: 'area' }
|
|
41
|
+
}
|
|
38
42
|
},
|
|
39
43
|
scales: {
|
|
40
44
|
bandOrder: 'data',
|
|
@@ -45,6 +45,10 @@ export function buildMarks(state, ctx) {
|
|
|
45
45
|
peak,
|
|
46
46
|
slot: { index, count: groups.length },
|
|
47
47
|
styles: {
|
|
48
|
+
// An outline asked for without a color is the fill's.
|
|
49
|
+
...(seg.styles.strokeWidth !== undefined
|
|
50
|
+
? { stroke: disabled?.fill ?? seg.styles.fill ?? state.colorForSeries(name) }
|
|
51
|
+
: {}),
|
|
48
52
|
...seg.styles,
|
|
49
53
|
fill: disabled?.fill ?? seg.styles.fill ?? state.colorForSeries(name),
|
|
50
54
|
fillOpacity: disabled?.fillOpacity ?? seg.styles.fillOpacity ?? cfg.violin.fillOpacity
|
|
@@ -3,7 +3,9 @@ import { ValueLabels } from '../../../layout/styles/ValueLabels';
|
|
|
3
3
|
/** AreaPlot on the shared pipeline: areas from a zero baseline, author markers taking the plot's `x`/`y`, and its own markers. Its plot defaults to: hover along x, cumulative timeline tweening `y` per series. */
|
|
4
4
|
export function buildAreaDefaults() {
|
|
5
5
|
return {
|
|
6
|
-
data: {
|
|
6
|
+
data: {
|
|
7
|
+
segments: { color: (style) => style?.fill, base: { flat: 'area' } }
|
|
8
|
+
},
|
|
7
9
|
statistics: { inheritPositions: true },
|
|
8
10
|
scales: { zero: ['y'] },
|
|
9
11
|
transforms: { labels: { select: 'last', skip: ValueLabels.skipGaps } },
|