@ruckc/truss-chart 1.9.0 → 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/dist/BarChart.svelte +27 -2
- package/dist/BarChart.svelte.d.ts +6 -2
- package/dist/ColorScale.svelte +20 -1
- package/dist/ColorScale.svelte.d.ts +11 -0
- package/dist/DensityChart.svelte +2 -10
- package/dist/DonutChart.svelte +37 -5
- package/dist/DonutChart.svelte.d.ts +8 -0
- package/dist/Histogram.svelte +27 -1
- package/dist/Histogram.svelte.d.ts +6 -1
- package/dist/LineChart.svelte +2 -10
- package/dist/ScatterChart.svelte +27 -1
- package/dist/ScatterChart.svelte.d.ts +6 -1
- package/dist/fill.d.ts +7 -0
- package/dist/fill.js +18 -0
- package/package.json +1 -1
package/dist/BarChart.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ChartFrame from './ChartFrame.svelte';
|
|
3
|
+
import { watchFill } from './fill.js';
|
|
3
4
|
import Tooltip from './Tooltip.svelte';
|
|
4
5
|
import type { Series } from './types.js';
|
|
5
6
|
import { fmtAxis, fmtValue, linear, str, niceTicks, seriesColor, MAX_SERIES } from './util.js';
|
|
@@ -22,8 +23,12 @@
|
|
|
22
23
|
series: Series[];
|
|
23
24
|
title?: string;
|
|
24
25
|
subtitle?: string;
|
|
25
|
-
/**
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Plot height in px (default 240), or `'fill'` to size to the container's height. `'fill'` measures the
|
|
28
|
+
* parent with a ResizeObserver, so the parent needs a definite height (e.g. `height: 360px`, or a grid/flex
|
|
29
|
+
* track with a fixed size). A parent with `height: auto` collapses to the 120px minimum. Horizontal charts still grow to fit their categories.
|
|
30
|
+
*/
|
|
31
|
+
height?: number | 'fill';
|
|
27
32
|
/** `true` stacks values (positives up/right of zero, negatives down/left); `'percent'` normalises each category to 100%. */
|
|
28
33
|
stacked?: boolean | 'percent';
|
|
29
34
|
/** Bars run left→right with categories down the side — better for long labels and many categories. */
|
|
@@ -110,10 +115,22 @@
|
|
|
110
115
|
const GAP = 1; // each side of a shared edge yields a 2px surface gap
|
|
111
116
|
const labelWidth = $derived(Math.min(168, Math.max(48, Math.max(0, ...categories.map((c) => c.length)) * 6.4 + 14)));
|
|
112
117
|
const trunc = (s: string, px: number) => (s.length * 6.4 > px ? `${s.slice(0, Math.max(1, Math.floor(px / 6.4) - 1))}…` : s);
|
|
118
|
+
|
|
119
|
+
// height='fill': plot = container height minus the frame's own chrome (title, legend, key, padding)
|
|
120
|
+
let wrap = $state<HTMLElement>();
|
|
121
|
+
let fillH = $state(240);
|
|
122
|
+
const fill = $derived(height === 'fill');
|
|
123
|
+
const H = $derived(height === 'fill' ? fillH : height);
|
|
124
|
+
$effect(() => {
|
|
125
|
+
if (!fill || !wrap) return;
|
|
126
|
+
return watchFill(wrap, 'svg[role=img]', (h) => (fillH = h));
|
|
127
|
+
});
|
|
113
128
|
</script>
|
|
114
129
|
|
|
130
|
+
{#snippet chart()}
|
|
115
131
|
<ChartFrame {title} {subtitle} {items} ontoggle={toggle}>
|
|
116
132
|
{#snippet plot(w)}
|
|
133
|
+
{@const height = H}
|
|
117
134
|
{@const m = horizontal ? { top: refs.length ? 24 : 10, right: showValues ? 56 : 14, bottom: 26, left: labelWidth } : { top: showValues ? 22 : 12, right: 12, bottom: 28, left: 46 }}
|
|
118
135
|
{@const bandN = Math.max(categories.length, 1)}
|
|
119
136
|
{@const plotH = horizontal ? Math.max(height, bandN * (isStack ? 30 : Math.max(1, vis.length) * 16 + 14) + m.top + m.bottom) : height}
|
|
@@ -225,8 +242,16 @@
|
|
|
225
242
|
</table>
|
|
226
243
|
{/snippet}
|
|
227
244
|
</ChartFrame>
|
|
245
|
+
{/snippet}
|
|
246
|
+
|
|
247
|
+
{#if fill}
|
|
248
|
+
<div class="fillwrap" bind:this={wrap}>{@render chart()}</div>
|
|
249
|
+
{:else}
|
|
250
|
+
{@render chart()}
|
|
251
|
+
{/if}
|
|
228
252
|
|
|
229
253
|
<style>
|
|
254
|
+
.fillwrap { height: 100%; min-height: 0; overflow: hidden; }
|
|
230
255
|
svg { display: block; overflow: visible; }
|
|
231
256
|
.grid { stroke: var(--tr-outline-variant); stroke-opacity: 0.6; }
|
|
232
257
|
.grid[data-zero='true'] { stroke-opacity: 1; }
|
|
@@ -9,8 +9,12 @@ type $$ComponentProps = {
|
|
|
9
9
|
series: Series[];
|
|
10
10
|
title?: string;
|
|
11
11
|
subtitle?: string;
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Plot height in px (default 240), or `'fill'` to size to the container's height. `'fill'` measures the
|
|
14
|
+
* parent with a ResizeObserver, so the parent needs a definite height (e.g. `height: 360px`, or a grid/flex
|
|
15
|
+
* track with a fixed size). A parent with `height: auto` collapses to the 120px minimum. Horizontal charts still grow to fit their categories.
|
|
16
|
+
*/
|
|
17
|
+
height?: number | 'fill';
|
|
14
18
|
/** `true` stacks values (positives up/right of zero, negatives down/left); `'percent'` normalises each category to 100%. */
|
|
15
19
|
stacked?: boolean | 'percent';
|
|
16
20
|
/** Bars run left→right with categories down the side — better for long labels and many categories. */
|
package/dist/ColorScale.svelte
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
caption,
|
|
9
9
|
label,
|
|
10
10
|
palette = 'sequential',
|
|
11
|
+
gradient,
|
|
12
|
+
stops,
|
|
11
13
|
vertical = false,
|
|
12
14
|
size = 200
|
|
13
15
|
}: {
|
|
@@ -23,6 +25,17 @@
|
|
|
23
25
|
label?: string;
|
|
24
26
|
/** Which palette ramp to draw; match the ramp your marks use. */
|
|
25
27
|
palette?: ScaleKind;
|
|
28
|
+
/**
|
|
29
|
+
* Any CSS gradient (e.g. `linear-gradient(to right, #fff, #06c)`), drawn exactly as given: write the
|
|
30
|
+
* direction yourself (`to top` for a `vertical` bar). Takes precedence over `stops` and `palette`.
|
|
31
|
+
*/
|
|
32
|
+
gradient?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Colours from the low end to the high end, evenly spaced, e.g. `['#fff', '#06c', '#003']`. Built into a
|
|
35
|
+
* linear gradient that runs in the bar's direction (`vertical` runs bottom to top). Colours may be any CSS
|
|
36
|
+
* colour or `var(--token)`. Takes precedence over `palette`; ignored when `gradient` is set, or with fewer than two stops.
|
|
37
|
+
*/
|
|
38
|
+
stops?: string[];
|
|
26
39
|
/** Bar runs bottom (low) to top (high) instead of left to right. */
|
|
27
40
|
vertical?: boolean;
|
|
28
41
|
/** Bar length in px. */
|
|
@@ -30,7 +43,13 @@
|
|
|
30
43
|
} = $props();
|
|
31
44
|
|
|
32
45
|
const name = $derived(label ?? `${caption ? `${caption}: ` : ''}${low} to ${high}`);
|
|
33
|
-
|
|
46
|
+
// precedence: gradient > stops (2 or more) > palette
|
|
47
|
+
const bg = $derived.by(() => {
|
|
48
|
+
const dir = vertical ? 'to top' : 'to right';
|
|
49
|
+
if (gradient) return gradient;
|
|
50
|
+
if (stops && stops.length > 1) return `linear-gradient(${dir}, ${stops.join(', ')})`;
|
|
51
|
+
return gradientFrom(palette, dir);
|
|
52
|
+
});
|
|
34
53
|
const ends = $derived(vertical ? [high, low] : [low, high]);
|
|
35
54
|
</script>
|
|
36
55
|
|
|
@@ -12,6 +12,17 @@ type $$ComponentProps = {
|
|
|
12
12
|
label?: string;
|
|
13
13
|
/** Which palette ramp to draw; match the ramp your marks use. */
|
|
14
14
|
palette?: ScaleKind;
|
|
15
|
+
/**
|
|
16
|
+
* Any CSS gradient (e.g. `linear-gradient(to right, #fff, #06c)`), drawn exactly as given: write the
|
|
17
|
+
* direction yourself (`to top` for a `vertical` bar). Takes precedence over `stops` and `palette`.
|
|
18
|
+
*/
|
|
19
|
+
gradient?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Colours from the low end to the high end, evenly spaced, e.g. `['#fff', '#06c', '#003']`. Built into a
|
|
22
|
+
* linear gradient that runs in the bar's direction (`vertical` runs bottom to top). Colours may be any CSS
|
|
23
|
+
* colour or `var(--token)`. Takes precedence over `palette`; ignored when `gradient` is set, or with fewer than two stops.
|
|
24
|
+
*/
|
|
25
|
+
stops?: string[];
|
|
15
26
|
/** Bar runs bottom (low) to top (high) instead of left to right. */
|
|
16
27
|
vertical?: boolean;
|
|
17
28
|
/** Bar length in px. */
|
package/dist/DensityChart.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import ChartFrame from './ChartFrame.svelte';
|
|
4
|
+
import { watchFill } from './fill.js';
|
|
4
5
|
import ColorScale from './ColorScale.svelte';
|
|
5
6
|
import Tooltip from './Tooltip.svelte';
|
|
6
7
|
import { bin, DEFAULT_RAMP, extentOf, isHome, mergeExtent, nearest, panView, parseColor, prepare, rampColor, zoomView, type DensityHighlight, type DensityHover, type DensityPoint, type Prepared, type View } from './density.js';
|
|
@@ -121,16 +122,7 @@
|
|
|
121
122
|
$effect(() => {
|
|
122
123
|
void [mode, colorKey, items.length];
|
|
123
124
|
if (!fill || !wrap) return;
|
|
124
|
-
|
|
125
|
-
const measure = () => {
|
|
126
|
-
const fig = el.querySelector('figure'), cv = el.querySelector('canvas');
|
|
127
|
-
if (!fig || !cv) return;
|
|
128
|
-
fillH = Math.max(120, Math.floor(el.clientHeight - (fig.getBoundingClientRect().height - cv.getBoundingClientRect().height)));
|
|
129
|
-
};
|
|
130
|
-
const ro = new ResizeObserver(measure);
|
|
131
|
-
ro.observe(el);
|
|
132
|
-
measure();
|
|
133
|
-
return () => ro.disconnect();
|
|
125
|
+
return watchFill(wrap, 'canvas', (h) => (fillH = h));
|
|
134
126
|
});
|
|
135
127
|
|
|
136
128
|
const m = $derived({ top: 32, right: 16, bottom: xLabel ? 44 : 28, left: yLabel ? 62 : 48 });
|
package/dist/DonutChart.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ChartFrame from './ChartFrame.svelte';
|
|
3
|
+
import { watchFill } from './fill.js';
|
|
3
4
|
import { fmtValue, seriesColor, str, MAX_SERIES } from './util.js';
|
|
4
5
|
|
|
5
6
|
let {
|
|
@@ -7,13 +8,22 @@
|
|
|
7
8
|
title,
|
|
8
9
|
subtitle,
|
|
9
10
|
size = 200,
|
|
11
|
+
height,
|
|
10
12
|
format = fmtValue,
|
|
11
13
|
centerLabel = 'Total'
|
|
12
14
|
}: {
|
|
13
15
|
slices: { name: string; value: number }[];
|
|
14
16
|
title?: string;
|
|
15
17
|
subtitle?: string;
|
|
18
|
+
/** Donut diameter in px (default 200). Capped to the available width when `height` is `'fill'`. */
|
|
16
19
|
size?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Overrides `size`: a number sets the diameter in px, or `'fill'` sizes the donut to the container's height
|
|
22
|
+
* (and never wider than the container). `'fill'` measures the parent with a ResizeObserver, so the parent
|
|
23
|
+
* needs a definite height (e.g. `height: 360px`, or a grid/flex track with a fixed size). A parent with
|
|
24
|
+
* `height: auto` collapses to the 120px minimum.
|
|
25
|
+
*/
|
|
26
|
+
height?: number | 'fill';
|
|
17
27
|
format?: (n: number) => string;
|
|
18
28
|
centerLabel?: string;
|
|
19
29
|
} = $props();
|
|
@@ -34,9 +44,20 @@
|
|
|
34
44
|
const items = $derived(base.map((s) => ({ name: s.name, color: s.color, hidden: hidden.includes(s.name) })));
|
|
35
45
|
const toggle = (n: string) => (hidden = hidden.includes(n) ? hidden.filter((h) => h !== n) : [...hidden, n]);
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
// height='fill': the svg = container height minus the frame's own chrome (title, legend, padding)
|
|
48
|
+
let wrap = $state<HTMLElement>();
|
|
49
|
+
let fillH = $state(200);
|
|
50
|
+
const fill = $derived(height === 'fill');
|
|
51
|
+
$effect(() => {
|
|
52
|
+
if (!fill || !wrap) return;
|
|
53
|
+
return watchFill(wrap, 'svg', (h) => (fillH = h));
|
|
54
|
+
});
|
|
55
|
+
const diameter = (w: number) => (height === 'fill' ? Math.max(120, Math.min(fillH, w)) : (height ?? size));
|
|
56
|
+
|
|
57
|
+
// geometry for a diameter of `size` px; built per render because 'fill' also depends on the available width
|
|
58
|
+
const arcsFor = (size: number) => {
|
|
59
|
+
const r = size / 2 - 6;
|
|
60
|
+
const th = Math.max(14, size * 0.14);
|
|
40
61
|
let a0 = -Math.PI / 2;
|
|
41
62
|
return vis.map((s) => {
|
|
42
63
|
const sweep = total ? (s.value / total) * Math.PI * 2 : 0;
|
|
@@ -50,7 +71,7 @@
|
|
|
50
71
|
a0 = a1;
|
|
51
72
|
return { ...s, d };
|
|
52
73
|
});
|
|
53
|
-
|
|
74
|
+
};
|
|
54
75
|
const focus = $derived(vis.find((s) => s.name === active));
|
|
55
76
|
/** Share of the visible total; 0 (not NaN) when everything visible is 0. */
|
|
56
77
|
const pct = (v: number) => (total ? Math.round((v / total) * 100) : 0);
|
|
@@ -58,8 +79,11 @@
|
|
|
58
79
|
const sliceLabel = (a: { name: string; value: number }) => `${a.name}: ${str(format(a.value))} (${pct(a.value)}%)`;
|
|
59
80
|
</script>
|
|
60
81
|
|
|
82
|
+
{#snippet chart()}
|
|
61
83
|
<ChartFrame {title} {subtitle} {items} ontoggle={toggle}>
|
|
62
|
-
{#snippet plot()}
|
|
84
|
+
{#snippet plot(w)}
|
|
85
|
+
{@const size = diameter(w)}
|
|
86
|
+
{@const arcs = arcsFor(size)}
|
|
63
87
|
<div class="wrap">
|
|
64
88
|
<svg width={size} height={size} role="img" aria-label="{title ?? 'Donut chart'}. A table view is available.">
|
|
65
89
|
{#each arcs as a (a.name)}
|
|
@@ -88,8 +112,16 @@
|
|
|
88
112
|
</table>
|
|
89
113
|
{/snippet}
|
|
90
114
|
</ChartFrame>
|
|
115
|
+
{/snippet}
|
|
116
|
+
|
|
117
|
+
{#if fill}
|
|
118
|
+
<div class="fillwrap" bind:this={wrap}>{@render chart()}</div>
|
|
119
|
+
{:else}
|
|
120
|
+
{@render chart()}
|
|
121
|
+
{/if}
|
|
91
122
|
|
|
92
123
|
<style>
|
|
124
|
+
.fillwrap { height: 100%; min-height: 0; overflow: hidden; }
|
|
93
125
|
.wrap { display: flex; justify-content: center; padding: 8px 0; }
|
|
94
126
|
/* 2px surface gap between adjacent fills */
|
|
95
127
|
.slice { stroke: var(--tr-surface-container-low); stroke-width: 2; outline: none; transition: opacity var(--tr-dur-fast) var(--tr-ease); }
|
|
@@ -5,7 +5,15 @@ type $$ComponentProps = {
|
|
|
5
5
|
}[];
|
|
6
6
|
title?: string;
|
|
7
7
|
subtitle?: string;
|
|
8
|
+
/** Donut diameter in px (default 200). Capped to the available width when `height` is `'fill'`. */
|
|
8
9
|
size?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Overrides `size`: a number sets the diameter in px, or `'fill'` sizes the donut to the container's height
|
|
12
|
+
* (and never wider than the container). `'fill'` measures the parent with a ResizeObserver, so the parent
|
|
13
|
+
* needs a definite height (e.g. `height: 360px`, or a grid/flex track with a fixed size). A parent with
|
|
14
|
+
* `height: auto` collapses to the 120px minimum.
|
|
15
|
+
*/
|
|
16
|
+
height?: number | 'fill';
|
|
9
17
|
format?: (n: number) => string;
|
|
10
18
|
centerLabel?: string;
|
|
11
19
|
};
|
package/dist/Histogram.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ChartFrame from './ChartFrame.svelte';
|
|
3
|
+
import { watchFill } from './fill.js';
|
|
3
4
|
import Tooltip from './Tooltip.svelte';
|
|
4
5
|
import { barPath, fmtAxis, fmtValue, linear, niceTicks, seriesColor, str } from './util.js';
|
|
5
6
|
|
|
@@ -21,7 +22,12 @@
|
|
|
21
22
|
domain?: [number, number];
|
|
22
23
|
title?: string;
|
|
23
24
|
subtitle?: string;
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Plot height in px (default 240), or `'fill'` to size to the container's height. `'fill'` measures the
|
|
27
|
+
* parent with a ResizeObserver, so the parent needs a definite height (e.g. `height: 360px`, or a grid/flex
|
|
28
|
+
* track with a fixed size). A parent with `height: auto` collapses to the 120px minimum.
|
|
29
|
+
*/
|
|
30
|
+
height?: number | 'fill';
|
|
25
31
|
color?: string;
|
|
26
32
|
xFormat?: (n: number) => string;
|
|
27
33
|
/** Brushed selection [from, to] on the value axis (snaps to bin edges). Drag to select, click to clear. */
|
|
@@ -61,10 +67,22 @@
|
|
|
61
67
|
const span = (i: number) => `${str(xFormat(edges[i]))} – ${str(xFormat(edges[i + 1]))}`;
|
|
62
68
|
const snapLo = (v: number) => grid.min + Math.floor((v - grid.min) / step + 1e-9) * step;
|
|
63
69
|
const snapHi = (v: number) => grid.min + Math.ceil((v - grid.min) / step - 1e-9) * step;
|
|
70
|
+
|
|
71
|
+
// height='fill': plot = container height minus the frame's own chrome (title, legend, key, padding)
|
|
72
|
+
let wrap = $state<HTMLElement>();
|
|
73
|
+
let fillH = $state(240);
|
|
74
|
+
const fill = $derived(height === 'fill');
|
|
75
|
+
const H = $derived(height === 'fill' ? fillH : height);
|
|
76
|
+
$effect(() => {
|
|
77
|
+
if (!fill || !wrap) return;
|
|
78
|
+
return watchFill(wrap, 'svg', (h) => (fillH = h));
|
|
79
|
+
});
|
|
64
80
|
</script>
|
|
65
81
|
|
|
82
|
+
{#snippet chart()}
|
|
66
83
|
<ChartFrame {title} {subtitle}>
|
|
67
84
|
{#snippet plot(w)}
|
|
85
|
+
{@const height = H}
|
|
68
86
|
{@const m = { top: 12, right: 12, bottom: 28, left: 46 }}
|
|
69
87
|
{@const X = linear(grid.min, grid.max, m.left, w - m.right)}
|
|
70
88
|
{@const Y = linear(yScale.min, yScale.max, height - m.bottom, m.top)}
|
|
@@ -147,8 +165,16 @@
|
|
|
147
165
|
</table>
|
|
148
166
|
{/snippet}
|
|
149
167
|
</ChartFrame>
|
|
168
|
+
{/snippet}
|
|
169
|
+
|
|
170
|
+
{#if fill}
|
|
171
|
+
<div class="fillwrap" bind:this={wrap}>{@render chart()}</div>
|
|
172
|
+
{:else}
|
|
173
|
+
{@render chart()}
|
|
174
|
+
{/if}
|
|
150
175
|
|
|
151
176
|
<style>
|
|
177
|
+
.fillwrap { height: 100%; min-height: 0; overflow: hidden; }
|
|
152
178
|
svg { display: block; outline: none; overflow: visible; }
|
|
153
179
|
svg:focus-visible { border-radius: var(--tr-radius-sm); box-shadow: var(--tr-focus-ring); }
|
|
154
180
|
.grid { stroke: var(--tr-outline-variant); stroke-opacity: 0.6; }
|
|
@@ -5,7 +5,12 @@ type $$ComponentProps = {
|
|
|
5
5
|
domain?: [number, number];
|
|
6
6
|
title?: string;
|
|
7
7
|
subtitle?: string;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Plot height in px (default 240), or `'fill'` to size to the container's height. `'fill'` measures the
|
|
10
|
+
* parent with a ResizeObserver, so the parent needs a definite height (e.g. `height: 360px`, or a grid/flex
|
|
11
|
+
* track with a fixed size). A parent with `height: auto` collapses to the 120px minimum.
|
|
12
|
+
*/
|
|
13
|
+
height?: number | 'fill';
|
|
9
14
|
color?: string;
|
|
10
15
|
xFormat?: (n: number) => string;
|
|
11
16
|
/** Brushed selection [from, to] on the value axis (snaps to bin edges). Drag to select, click to clear. */
|
package/dist/LineChart.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ChartFrame from './ChartFrame.svelte';
|
|
3
3
|
import Tooltip from './Tooltip.svelte';
|
|
4
|
+
import { watchFill } from './fill.js';
|
|
4
5
|
import type { Series, Band, ReferenceLine } from './types.js';
|
|
5
6
|
import { fmtAxis, fmtValue, fmtX, isPos, linear, logLinear, logTicks, niceTicks, seriesColor, str, spreadLabels, MAX_SERIES } from './util.js';
|
|
6
7
|
|
|
@@ -96,16 +97,7 @@
|
|
|
96
97
|
const H = $derived(height === 'fill' ? fillH : height);
|
|
97
98
|
$effect(() => {
|
|
98
99
|
if (!fill || !wrap) return;
|
|
99
|
-
|
|
100
|
-
const measure = () => {
|
|
101
|
-
const fig = el.querySelector('figure'), svg = el.querySelector('svg[role=group]');
|
|
102
|
-
if (!fig || !svg) return;
|
|
103
|
-
fillH = Math.max(120, Math.floor(el.clientHeight - (fig.getBoundingClientRect().height - svg.getBoundingClientRect().height)));
|
|
104
|
-
};
|
|
105
|
-
const ro = new ResizeObserver(measure);
|
|
106
|
-
ro.observe(el);
|
|
107
|
-
measure();
|
|
108
|
-
return () => ro.disconnect();
|
|
100
|
+
return watchFill(wrap, 'svg[role=group]', (h) => (fillH = h));
|
|
109
101
|
});
|
|
110
102
|
/** Per visible series: [lo, hi] band at each x (stacked), else lo = 0, hi = value. */
|
|
111
103
|
const bands = $derived.by(() => {
|
package/dist/ScatterChart.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ChartFrame from './ChartFrame.svelte';
|
|
3
|
+
import { watchFill } from './fill.js';
|
|
3
4
|
import Tooltip from './Tooltip.svelte';
|
|
4
5
|
import type { ScatterPoint } from './types.js';
|
|
5
6
|
import { fmtAxis, fmtValue, linear, niceTicks, seriesColor, MAX_SERIES } from './util.js';
|
|
@@ -21,7 +22,12 @@
|
|
|
21
22
|
points: ScatterPoint[];
|
|
22
23
|
title?: string;
|
|
23
24
|
subtitle?: string;
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Plot height in px (default 280), or `'fill'` to size to the container's height. `'fill'` measures the
|
|
27
|
+
* parent with a ResizeObserver, so the parent needs a definite height (e.g. `height: 360px`, or a grid/flex
|
|
28
|
+
* track with a fixed size). A parent with `height: auto` collapses to the 120px minimum.
|
|
29
|
+
*/
|
|
30
|
+
height?: number | 'fill';
|
|
25
31
|
xLabel?: string;
|
|
26
32
|
yLabel?: string;
|
|
27
33
|
/** Name of the size dimension in tooltips and the table. */
|
|
@@ -63,10 +69,22 @@
|
|
|
63
69
|
const cur = $derived(active != null ? (vis[active] ?? null) : null);
|
|
64
70
|
const readout = (p: (typeof vis)[number]) =>
|
|
65
71
|
`${p.label ?? 'Point'}${p.group ? `, ${p.group}` : ''}: ${xLabel ?? 'x'} ${xFormat(p.x)}, ${yLabel ?? 'y'} ${yFormat(p.y)}${p.size != null ? `, ${sizeLabel.toLowerCase()} ${sizeFormat(p.size)}` : ''}`;
|
|
72
|
+
|
|
73
|
+
// height='fill': plot = container height minus the frame's own chrome (title, legend, key, padding)
|
|
74
|
+
let wrap = $state<HTMLElement>();
|
|
75
|
+
let fillH = $state(280);
|
|
76
|
+
const fill = $derived(height === 'fill');
|
|
77
|
+
const H = $derived(height === 'fill' ? fillH : height);
|
|
78
|
+
$effect(() => {
|
|
79
|
+
if (!fill || !wrap) return;
|
|
80
|
+
return watchFill(wrap, 'svg[role=group]', (h) => (fillH = h));
|
|
81
|
+
});
|
|
66
82
|
</script>
|
|
67
83
|
|
|
84
|
+
{#snippet chart()}
|
|
68
85
|
<ChartFrame {title} {subtitle} {items} ontoggle={toggle}>
|
|
69
86
|
{#snippet plot(w)}
|
|
87
|
+
{@const height = H}
|
|
70
88
|
{@const m = { top: 14, right: 16, bottom: xLabel ? 44 : 28, left: yLabel ? 62 : 48 }}
|
|
71
89
|
{@const X = linear(xScale.min, xScale.max, m.left, w - m.right)}
|
|
72
90
|
{@const Y = linear(yScale.min, yScale.max, height - m.bottom, m.top)}
|
|
@@ -136,8 +154,16 @@
|
|
|
136
154
|
</table>
|
|
137
155
|
{/snippet}
|
|
138
156
|
</ChartFrame>
|
|
157
|
+
{/snippet}
|
|
158
|
+
|
|
159
|
+
{#if fill}
|
|
160
|
+
<div class="fillwrap" bind:this={wrap}>{@render chart()}</div>
|
|
161
|
+
{:else}
|
|
162
|
+
{@render chart()}
|
|
163
|
+
{/if}
|
|
139
164
|
|
|
140
165
|
<style>
|
|
166
|
+
.fillwrap { height: 100%; min-height: 0; overflow: hidden; }
|
|
141
167
|
svg { display: block; outline: none; overflow: visible; }
|
|
142
168
|
svg:focus-visible { border-radius: var(--tr-radius-sm); box-shadow: var(--tr-focus-ring); }
|
|
143
169
|
.grid { stroke: var(--tr-outline-variant); stroke-opacity: 0.6; stroke-width: 1; }
|
|
@@ -4,7 +4,12 @@ type $$ComponentProps = {
|
|
|
4
4
|
points: ScatterPoint[];
|
|
5
5
|
title?: string;
|
|
6
6
|
subtitle?: string;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Plot height in px (default 280), or `'fill'` to size to the container's height. `'fill'` measures the
|
|
9
|
+
* parent with a ResizeObserver, so the parent needs a definite height (e.g. `height: 360px`, or a grid/flex
|
|
10
|
+
* track with a fixed size). A parent with `height: auto` collapses to the 120px minimum.
|
|
11
|
+
*/
|
|
12
|
+
height?: number | 'fill';
|
|
8
13
|
xLabel?: string;
|
|
9
14
|
yLabel?: string;
|
|
10
15
|
/** Name of the size dimension in tooltips and the table. */
|
package/dist/fill.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `height="fill"` measuring for the charts. Observes `el` (the fill wrapper) and reports the plot height:
|
|
3
|
+
* the wrapper's height minus the frame's own chrome (title, legend, key, padding), found by comparing the
|
|
4
|
+
* `<figure>` with the plot element matched by `probe`. The result is floored at 120px.
|
|
5
|
+
* Returns a disposer that disconnects the observer.
|
|
6
|
+
*/
|
|
7
|
+
export declare function watchFill(el: HTMLElement, probe: string, onHeight: (h: number) => void): () => void;
|
package/dist/fill.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `height="fill"` measuring for the charts. Observes `el` (the fill wrapper) and reports the plot height:
|
|
3
|
+
* the wrapper's height minus the frame's own chrome (title, legend, key, padding), found by comparing the
|
|
4
|
+
* `<figure>` with the plot element matched by `probe`. The result is floored at 120px.
|
|
5
|
+
* Returns a disposer that disconnects the observer.
|
|
6
|
+
*/
|
|
7
|
+
export function watchFill(el, probe, onHeight) {
|
|
8
|
+
const measure = () => {
|
|
9
|
+
const fig = el.querySelector('figure'), plot = el.querySelector(probe);
|
|
10
|
+
if (!fig || !plot)
|
|
11
|
+
return;
|
|
12
|
+
onHeight(Math.max(120, Math.floor(el.clientHeight - (fig.getBoundingClientRect().height - plot.getBoundingClientRect().height))));
|
|
13
|
+
};
|
|
14
|
+
const ro = new ResizeObserver(measure);
|
|
15
|
+
ro.observe(el);
|
|
16
|
+
measure();
|
|
17
|
+
return () => ro.disconnect();
|
|
18
|
+
}
|