@fundar/data-chart-telling 0.0.43 → 0.0.45
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.
|
@@ -20,9 +20,12 @@
|
|
|
20
20
|
*
|
|
21
21
|
* container ▸ timeline (one shared scrubber) ▸ facet (split per small-multiple) ▸ plot
|
|
22
22
|
*
|
|
23
|
-
* The leaf snippet receives `{ data, width, height, facetValue?, selectedValue
|
|
24
|
-
* and knows nothing about how that box or that data slice was
|
|
25
|
-
* is what lets any plot (line, bar, heatmap, …) drop in
|
|
23
|
+
* The leaf snippet receives `{ data, width, height, facetValue?, selectedValue?,
|
|
24
|
+
* margins? }` and knows nothing about how that box or that data slice was
|
|
25
|
+
* produced, which is what lets any plot (line, bar, heatmap, …) drop in
|
|
26
|
+
* unchanged. `margins` is only present when `facet.labelsForMargin` is
|
|
27
|
+
* given — forward it to the plot's own `margins` prop so every facet
|
|
28
|
+
* shares the same content box.
|
|
26
29
|
*/
|
|
27
30
|
let {
|
|
28
31
|
width,
|
|
@@ -83,17 +86,19 @@
|
|
|
83
86
|
formatIndicator={facet.formatIndicator}
|
|
84
87
|
indicator={facet.indicator}
|
|
85
88
|
navButton={facet.navButton}
|
|
89
|
+
labelsForMargin={facet.labelsForMargin}
|
|
86
90
|
width={w}
|
|
87
91
|
height={h}
|
|
88
92
|
>
|
|
89
|
-
{#snippet cell({ facetValue, data: cellData, width: cw, height: ch })}
|
|
93
|
+
{#snippet cell({ facetValue, data: cellData, width: cw, height: ch, margins })}
|
|
90
94
|
{@render plot({
|
|
91
95
|
data: cellData,
|
|
92
96
|
width: cw,
|
|
93
97
|
height: ch,
|
|
94
98
|
facetValue,
|
|
95
99
|
selectedValue,
|
|
96
|
-
legend: legendPlotContext
|
|
100
|
+
legend: legendPlotContext,
|
|
101
|
+
margins
|
|
97
102
|
})}
|
|
98
103
|
{/snippet}
|
|
99
104
|
</FacetLayout>
|
|
@@ -32,20 +32,53 @@
|
|
|
32
32
|
const xValues = $derived(data.map(getX));
|
|
33
33
|
const yValues = $derived(data.map(getY));
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
function naturalTickCount(span: number, spacing: number, min: number): number {
|
|
36
|
+
return Math.max(min, Math.round(span / spacing));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `[min, max]` of a pinned `scale.domain` (if numeric), else the extent of `values`. */
|
|
40
|
+
function axisExtent(scale: AxisBasedScalesConfig<TData>['x'], values: AxisValue[]): [number, number] | undefined {
|
|
41
|
+
const domainNumbers = scale?.domain?.filter((d): d is number => typeof d === 'number');
|
|
42
|
+
const source = domainNumbers && domainNumbers.length >= 2 ? domainNumbers : values.filter((v): v is number => typeof v === 'number');
|
|
43
|
+
if (source.length < 2) return undefined;
|
|
44
|
+
const lo = Math.min(...source);
|
|
45
|
+
const hi = Math.max(...source);
|
|
46
|
+
return hi > lo ? [lo, hi] : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Caps tick count to the axis's distinct value count, never above what fits the pixel span. Skipped once the caller sets tick density directly, or pins the domain. */
|
|
36
50
|
function autoTickCount(scale: AxisBasedScalesConfig<TData>['x'], values: AxisValue[], span: number, spacing: number, min: number): number | undefined {
|
|
37
|
-
if (scale?.ticks != null || scale?.tickSpacing != null || scale?.interval != null) return undefined;
|
|
51
|
+
if (scale?.ticks != null || scale?.tickSpacing != null || scale?.interval != null || scale?.domain != null) return undefined;
|
|
38
52
|
const uniqueCount = new Set(values.map(String)).size;
|
|
39
|
-
const natural =
|
|
53
|
+
const natural = naturalTickCount(span, spacing, min);
|
|
40
54
|
return uniqueCount >= 2 && uniqueCount < natural ? uniqueCount : undefined;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
/** Forces a whole-unit tick interval on an integer domain that spans fewer integers than the pixel span would naturally want ticks for. */
|
|
58
|
+
function integerTickInterval(
|
|
59
|
+
scale: AxisBasedScalesConfig<TData>['x'],
|
|
60
|
+
values: AxisValue[],
|
|
61
|
+
span: number,
|
|
62
|
+
spacing: number,
|
|
63
|
+
min: number
|
|
64
|
+
): number | undefined {
|
|
65
|
+
if (scale?.ticks != null || scale?.tickSpacing != null || scale?.interval != null) return undefined;
|
|
66
|
+
if (scale?.type != null && scale.type !== 'linear' && scale.type !== 'auto') return undefined;
|
|
67
|
+
const extent = axisExtent(scale, values);
|
|
68
|
+
if (!extent) return undefined;
|
|
69
|
+
const [lo, hi] = extent;
|
|
70
|
+
if (!Number.isInteger(lo) || !Number.isInteger(hi)) return undefined;
|
|
71
|
+
return hi - lo < naturalTickCount(span, spacing, min) ? 1 : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const xSpan = $derived(Math.max(0, width - numericMargins.left - numericMargins.right));
|
|
75
|
+
const ySpan = $derived(Math.max(0, height - numericMargins.top - numericMargins.bottom));
|
|
76
|
+
|
|
77
|
+
const xTickCount = $derived(autoTickCount(scales?.x, xValues, xSpan, 80, 3));
|
|
78
|
+
const yTickCount = $derived(autoTickCount(scales?.y, yValues, ySpan, 50, 2));
|
|
79
|
+
|
|
80
|
+
const xTickInterval = $derived(integerTickInterval(scales?.x, xValues, xSpan, 80, 3));
|
|
81
|
+
const yTickInterval = $derived(integerTickInterval(scales?.y, yValues, ySpan, 50, 2));
|
|
49
82
|
|
|
50
83
|
/** Vertical Y-axis title placement, when `scales.y.labelOrientation === 'vertical'`. */
|
|
51
84
|
const verticalYLabel = $derived.by(() => {
|
|
@@ -61,6 +94,7 @@
|
|
|
61
94
|
|
|
62
95
|
<AxisX
|
|
63
96
|
tickCount={xTickCount}
|
|
97
|
+
interval={xTickInterval}
|
|
64
98
|
tickSize={scales?.x?.tickSize ?? cfg.axis.xTickSize}
|
|
65
99
|
tickPadding={scales?.x?.tickPadding ?? cfg.axis.xTickPadding}
|
|
66
100
|
tickFontSize={cfg.axis.fontSize}
|
|
@@ -68,6 +102,7 @@
|
|
|
68
102
|
/>
|
|
69
103
|
<AxisY
|
|
70
104
|
tickCount={yTickCount}
|
|
105
|
+
interval={yTickInterval}
|
|
71
106
|
tickSize={scales?.y?.tickSize ?? cfg.axis.yTickSize}
|
|
72
107
|
tickPadding={scales?.y?.tickPadding ?? cfg.axis.yTickPadding}
|
|
73
108
|
tickFontSize={cfg.axis.fontSize}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { TimeValue } from '../layout/timeline';
|
|
3
3
|
import type { LegendPlotContext } from '../layout/legend';
|
|
4
|
+
import type { MarginConfig } from '../plots/props';
|
|
4
5
|
export type ChartBoxContext = {
|
|
5
6
|
width: number;
|
|
6
7
|
height: number;
|
|
@@ -13,6 +14,8 @@ export type ChartPlotContext<TRow extends Record<string, unknown>> = {
|
|
|
13
14
|
facetValue?: string;
|
|
14
15
|
selectedValue?: TimeValue;
|
|
15
16
|
legend?: LegendPlotContext;
|
|
17
|
+
/** Present only when faceted with `facet.labelsForMargin` — forward to the plot's own `margins` prop so every facet shares the same content box. */
|
|
18
|
+
margins?: MarginConfig;
|
|
16
19
|
};
|
|
17
20
|
export type ChartPlotSnippet<TRow extends Record<string, unknown>> = Snippet<[
|
|
18
21
|
ChartPlotContext<TRow>
|