@juspay/svelte-ui-components 2.85.0 → 2.87.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/AreaChart/AreaChart.svelte +229 -131
- package/dist/AreaChart/properties.d.ts +6 -0
- package/dist/BarChart/BarChart.svelte +421 -288
- package/dist/BarChart/properties.d.ts +6 -0
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +339 -222
- package/dist/DualAxisBarChart/properties.d.ts +4 -0
- package/dist/FunnelChart/FunnelChart.svelte +182 -83
- package/dist/FunnelChart/properties.d.ts +2 -0
- package/dist/LineChart/LineChart.svelte +396 -210
- package/dist/LineChart/properties.d.ts +9 -0
- package/dist/Sheet/Sheet.svelte +52 -20
- package/dist/Sheet/properties.d.ts +8 -0
- package/dist/_chart/Axis.svelte +32 -15
- package/dist/_chart/ChartTooltip.svelte +127 -49
- package/dist/_chart/Legend.svelte +36 -6
- package/dist/_chart/geometry.d.ts +28 -18
- package/dist/_chart/geometry.js +47 -42
- package/dist/_chart/interactions.d.ts +20 -0
- package/dist/_chart/interactions.js +34 -0
- package/dist/_chart/labels.d.ts +76 -0
- package/dist/_chart/labels.js +213 -0
- package/dist/_chart/measure.d.ts +12 -0
- package/dist/_chart/measure.js +46 -0
- package/dist/_chart/scales.d.ts +1 -1
- package/dist/_chart/scales.js +7 -2
- package/dist/_chart/tooltipPosition.d.ts +22 -0
- package/dist/_chart/tooltipPosition.js +49 -0
- package/dist/_chart/types.d.ts +29 -1
- package/package.json +1 -1
|
@@ -103,6 +103,15 @@ export type OptionalLineChartProperties = {
|
|
|
103
103
|
tooltipSnippet?: Snippet<[LineChartTooltipContext]>;
|
|
104
104
|
/** Content rendered when all series are empty. */
|
|
105
105
|
empty?: Snippet;
|
|
106
|
+
/** One tooltip listing every series at the hovered x (Highcharts shared tooltip).
|
|
107
|
+
* Defaults to true for multi-series charts; single-series behavior is unchanged. */
|
|
108
|
+
sharedTooltip?: boolean;
|
|
109
|
+
/** Legend items become click/keyboard toggles for series visibility. */
|
|
110
|
+
interactiveLegend?: boolean;
|
|
111
|
+
/** Hide the legend when the measured chart width is below this px value; 0 disables. */
|
|
112
|
+
hideLegendBelow?: number;
|
|
113
|
+
/** Render the tooltip into document.body so scroll/overflow ancestors never clip it. */
|
|
114
|
+
tooltipPortal?: boolean;
|
|
106
115
|
/** Value for the data-pw attribute on the chart container. */
|
|
107
116
|
testId?: string;
|
|
108
117
|
/** CSS class string applied to the top-level element. */
|
package/dist/Sheet/Sheet.svelte
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// Reference-counted so two Sheets open at once (or a Sheet nested inside
|
|
3
|
+
// another scroll-locking surface) don't have the second one's close clobber
|
|
4
|
+
// the first one's still-needed lock.
|
|
5
|
+
let scrollLockCount = 0;
|
|
6
|
+
|
|
7
|
+
function lockScroll() {
|
|
8
|
+
if (scrollLockCount === 0) {
|
|
9
|
+
document.body.style.overflow = 'hidden';
|
|
10
|
+
}
|
|
11
|
+
scrollLockCount += 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function unlockScroll() {
|
|
15
|
+
scrollLockCount = Math.max(0, scrollLockCount - 1);
|
|
16
|
+
if (scrollLockCount === 0) {
|
|
17
|
+
document.body.style.overflow = '';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
1
22
|
<script lang="ts">
|
|
2
23
|
import type { SheetProperties } from './properties';
|
|
3
24
|
import { fly, fade } from 'svelte/transition';
|
|
@@ -9,6 +30,7 @@
|
|
|
9
30
|
side = 'right',
|
|
10
31
|
title,
|
|
11
32
|
showOverlay = true,
|
|
33
|
+
dismissOnOutsideClick = showOverlay,
|
|
12
34
|
showCloseButton = true,
|
|
13
35
|
testId,
|
|
14
36
|
content,
|
|
@@ -41,7 +63,10 @@
|
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
function handleOverlayClick(event: MouseEvent) {
|
|
44
|
-
|
|
66
|
+
// Gate dismissal on dismissOnOutsideClick explicitly, not on the overlay's
|
|
67
|
+
// pointer-events alone: a visible-but-non-dismissible overlay still needs to
|
|
68
|
+
// catch (and swallow) the click to block the page underneath, without closing.
|
|
69
|
+
if (event.target === overlayDiv && dismissOnOutsideClick) {
|
|
45
70
|
close();
|
|
46
71
|
}
|
|
47
72
|
}
|
|
@@ -71,14 +96,6 @@
|
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
|
|
74
|
-
function lockScroll() {
|
|
75
|
-
document.body.style.overflow = 'hidden';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function unlockScroll() {
|
|
79
|
-
document.body.style.overflow = '';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
99
|
function scrollLockAction(_node: HTMLElement) {
|
|
83
100
|
lockScroll();
|
|
84
101
|
tick().then(() => {
|
|
@@ -110,7 +127,10 @@
|
|
|
110
127
|
<div
|
|
111
128
|
bind:this={overlayDiv}
|
|
112
129
|
use:scrollLockAction
|
|
113
|
-
class="sheet-overlay {showOverlay ? 'overlay-
|
|
130
|
+
class="sheet-overlay {showOverlay ? 'overlay-visible' : 'overlay-invisible'} {showOverlay ||
|
|
131
|
+
dismissOnOutsideClick
|
|
132
|
+
? 'overlay-interactive'
|
|
133
|
+
: 'overlay-noninteractive'} {classes ?? ''}"
|
|
114
134
|
onclick={handleOverlayClick}
|
|
115
135
|
onkeydown={handleKeyDown}
|
|
116
136
|
role="button"
|
|
@@ -170,12 +190,19 @@
|
|
|
170
190
|
-webkit-tap-highlight-color: transparent;
|
|
171
191
|
}
|
|
172
192
|
|
|
173
|
-
.overlay-
|
|
193
|
+
.overlay-visible {
|
|
174
194
|
background-color: var(--sheet-overlay-background, #00000066);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.overlay-invisible {
|
|
198
|
+
background-color: transparent;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.overlay-interactive {
|
|
175
202
|
pointer-events: auto;
|
|
176
203
|
}
|
|
177
204
|
|
|
178
|
-
.overlay-
|
|
205
|
+
.overlay-noninteractive {
|
|
179
206
|
pointer-events: none;
|
|
180
207
|
}
|
|
181
208
|
|
|
@@ -190,39 +217,44 @@
|
|
|
190
217
|
outline: none;
|
|
191
218
|
}
|
|
192
219
|
|
|
220
|
+
/* --sheet-top/-right/-bottom/-left all default to the previous hardcoded
|
|
221
|
+
edge-to-edge values below, so an unconfigured Sheet renders unchanged.
|
|
222
|
+
Overriding one (e.g. --sheet-top to clear a fixed header, or --sheet-bottom:
|
|
223
|
+
auto to size to content) turns the panel from an edge-to-edge slide-in into
|
|
224
|
+
an anchored floating panel, without needing a different component. */
|
|
193
225
|
.sheet-panel.left,
|
|
194
226
|
.sheet-panel.right {
|
|
195
|
-
top: 0;
|
|
196
|
-
bottom: 0;
|
|
227
|
+
top: var(--sheet-top, 0);
|
|
228
|
+
bottom: var(--sheet-bottom, 0);
|
|
197
229
|
width: var(--sheet-width, 400px);
|
|
198
230
|
max-width: var(--sheet-max-width, 100vw);
|
|
199
231
|
}
|
|
200
232
|
|
|
201
233
|
.sheet-panel.left {
|
|
202
|
-
left: 0;
|
|
234
|
+
left: var(--sheet-left, 0);
|
|
203
235
|
border-right: var(--sheet-border, none);
|
|
204
236
|
}
|
|
205
237
|
|
|
206
238
|
.sheet-panel.right {
|
|
207
|
-
right: 0;
|
|
239
|
+
right: var(--sheet-right, 0);
|
|
208
240
|
border-left: var(--sheet-border, none);
|
|
209
241
|
}
|
|
210
242
|
|
|
211
243
|
.sheet-panel.top,
|
|
212
244
|
.sheet-panel.bottom {
|
|
213
|
-
left: 0;
|
|
214
|
-
right: 0;
|
|
245
|
+
left: var(--sheet-left, 0);
|
|
246
|
+
right: var(--sheet-right, 0);
|
|
215
247
|
height: var(--sheet-height, 300px);
|
|
216
248
|
max-height: var(--sheet-max-height, 100vh);
|
|
217
249
|
}
|
|
218
250
|
|
|
219
251
|
.sheet-panel.top {
|
|
220
|
-
top: 0;
|
|
252
|
+
top: var(--sheet-top, 0);
|
|
221
253
|
border-bottom: var(--sheet-border, none);
|
|
222
254
|
}
|
|
223
255
|
|
|
224
256
|
.sheet-panel.bottom {
|
|
225
|
-
bottom: 0;
|
|
257
|
+
bottom: var(--sheet-bottom, 0);
|
|
226
258
|
border-top: var(--sheet-border, none);
|
|
227
259
|
}
|
|
228
260
|
|
|
@@ -9,6 +9,14 @@ export type OptionalSheetProperties = {
|
|
|
9
9
|
side?: SheetSide;
|
|
10
10
|
title?: string;
|
|
11
11
|
showOverlay?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether clicking outside the panel dismisses the sheet, independent of
|
|
14
|
+
* `showOverlay`'s visual tint. Defaults to mirroring `showOverlay`, so
|
|
15
|
+
* existing behavior is unchanged unless this is set explicitly. Set this to
|
|
16
|
+
* `true` alongside `showOverlay={false}` for a dismissible sheet with no
|
|
17
|
+
* dimming backdrop — e.g. an anchored dropdown-style panel.
|
|
18
|
+
*/
|
|
19
|
+
dismissOnOutsideClick?: boolean;
|
|
12
20
|
showCloseButton?: boolean;
|
|
13
21
|
testId?: string;
|
|
14
22
|
footer?: Snippet;
|
package/dist/_chart/Axis.svelte
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
showGridlines = false,
|
|
11
11
|
gridlineLength = 0,
|
|
12
12
|
label,
|
|
13
|
+
rotateTicks = false,
|
|
14
|
+
tickEvery = 1,
|
|
15
|
+
labelOffset = 36,
|
|
16
|
+
integerTicks = false,
|
|
13
17
|
classes
|
|
14
18
|
}: AxisProperties = $props();
|
|
15
19
|
|
|
@@ -19,7 +23,7 @@
|
|
|
19
23
|
|
|
20
24
|
let tickValues = $derived.by(() => {
|
|
21
25
|
if ('ticks' in scale && typeof scale.ticks === 'function') {
|
|
22
|
-
return scale.ticks(tickCount);
|
|
26
|
+
return scale.ticks(tickCount, integerTicks);
|
|
23
27
|
}
|
|
24
28
|
if ('domain' in scale && Array.isArray(scale.domain)) {
|
|
25
29
|
return scale.domain;
|
|
@@ -47,14 +51,27 @@
|
|
|
47
51
|
{@const x = positionTick(tick)}
|
|
48
52
|
<g class="tick" transform="translate({x}, 0)">
|
|
49
53
|
<line class="tick-mark" y2={orientation === 'bottom' ? TICK_SIZE : -TICK_SIZE} />
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
{#if i % tickEvery === 0}
|
|
55
|
+
{#if rotateTicks && orientation === 'bottom'}
|
|
56
|
+
<text
|
|
57
|
+
class="tick-label"
|
|
58
|
+
transform="translate(0, {TICK_SIZE + 4}) rotate(-45)"
|
|
59
|
+
text-anchor="end"
|
|
60
|
+
dominant-baseline="auto"
|
|
61
|
+
>
|
|
62
|
+
{format(tick)}
|
|
63
|
+
</text>
|
|
64
|
+
{:else}
|
|
65
|
+
<text
|
|
66
|
+
class="tick-label"
|
|
67
|
+
y={orientation === 'bottom' ? TICK_SIZE + 4 : -(TICK_SIZE + 4)}
|
|
68
|
+
text-anchor="middle"
|
|
69
|
+
dominant-baseline={orientation === 'bottom' ? 'hanging' : 'auto'}
|
|
70
|
+
>
|
|
71
|
+
{format(tick)}
|
|
72
|
+
</text>
|
|
73
|
+
{/if}
|
|
74
|
+
{/if}
|
|
58
75
|
{#if showGridlines && gridlineLength > 0}
|
|
59
76
|
<line
|
|
60
77
|
class="gridline"
|
|
@@ -68,7 +85,7 @@
|
|
|
68
85
|
<text
|
|
69
86
|
class="axis-label"
|
|
70
87
|
x={(scale.range[0] + scale.range[1]) / 2}
|
|
71
|
-
y={orientation === 'bottom' ?
|
|
88
|
+
y={orientation === 'bottom' ? labelOffset : -30}
|
|
72
89
|
text-anchor="middle"
|
|
73
90
|
>
|
|
74
91
|
{label}
|
|
@@ -113,30 +130,30 @@
|
|
|
113
130
|
|
|
114
131
|
<style>
|
|
115
132
|
.axis-line {
|
|
116
|
-
stroke: var(--chart-axis-color, #666);
|
|
133
|
+
stroke: var(--chart-axis-color, light-dark(#666, #9ca3af));
|
|
117
134
|
stroke-width: var(--chart-axis-stroke-width, 1);
|
|
118
135
|
}
|
|
119
136
|
|
|
120
137
|
.tick-mark {
|
|
121
|
-
stroke: var(--chart-axis-color, #666);
|
|
138
|
+
stroke: var(--chart-axis-color, light-dark(#666, #9ca3af));
|
|
122
139
|
stroke-width: var(--chart-axis-stroke-width, 1);
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
.tick-label {
|
|
126
|
-
fill: var(--chart-axis-color, #666);
|
|
143
|
+
fill: var(--chart-axis-color, light-dark(#666, #9ca3af));
|
|
127
144
|
font-size: var(--chart-axis-font-size, 11px);
|
|
128
145
|
font-family: var(--chart-axis-font-family, inherit);
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
.axis-label {
|
|
132
|
-
fill: var(--chart-axis-label-color, #333);
|
|
149
|
+
fill: var(--chart-axis-label-color, light-dark(#333, #e5e7eb));
|
|
133
150
|
font-size: var(--chart-axis-label-font-size, 12px);
|
|
134
151
|
font-family: var(--chart-axis-font-family, inherit);
|
|
135
152
|
font-weight: 500;
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
.gridline {
|
|
139
|
-
stroke: var(--chart-gridline-color, #e0e0e0);
|
|
156
|
+
stroke: var(--chart-gridline-color, light-dark(#e0e0e0, #374151));
|
|
140
157
|
stroke-opacity: var(--chart-gridline-opacity, 0.5);
|
|
141
158
|
stroke-dasharray: var(--chart-gridline-dash, 4 4);
|
|
142
159
|
}
|
|
@@ -1,83 +1,161 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { ChartTooltipProperties } from './types';
|
|
2
|
+
import type { ChartTooltipProperties, TooltipData } from './types';
|
|
3
|
+
import { computeTooltipPosition } from './tooltipPosition';
|
|
3
4
|
|
|
4
|
-
let {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
let {
|
|
6
|
+
data,
|
|
7
|
+
mouseX = 0,
|
|
8
|
+
mouseY = 0,
|
|
9
|
+
anchor = null,
|
|
10
|
+
portal = false,
|
|
11
|
+
originEl = null,
|
|
12
|
+
unstyled = false,
|
|
13
|
+
content,
|
|
14
|
+
customSnippet,
|
|
15
|
+
classes
|
|
16
|
+
}: ChartTooltipProperties = $props();
|
|
7
17
|
|
|
8
18
|
let tooltipEl = $state<HTMLDivElement | null>(null);
|
|
9
19
|
let tooltipWidth = $state(0);
|
|
10
20
|
let tooltipHeight = $state(0);
|
|
21
|
+
// Portal position depends on untracked DOM reads (originEl rect, viewport
|
|
22
|
+
// size); bump a tick on scroll/resize so the $derived re-runs while open.
|
|
23
|
+
let portalTick = $state(0);
|
|
11
24
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
tooltipEl?.offsetParent?.clientHeight ?? Number.POSITIVE_INFINITY
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
// Horizontal: flip to the left of the cursor when it would overflow the right edge.
|
|
20
|
-
const left = $derived.by(() => {
|
|
21
|
-
let value = mouseX + OFFSET;
|
|
22
|
-
if (value + tooltipWidth > containerWidth) {
|
|
23
|
-
value = mouseX - tooltipWidth - OFFSET;
|
|
25
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (!portal || data === null || typeof window === 'undefined') {
|
|
28
|
+
return;
|
|
24
29
|
}
|
|
25
|
-
|
|
30
|
+
const bump = () => {
|
|
31
|
+
portalTick += 1;
|
|
32
|
+
};
|
|
33
|
+
window.addEventListener('scroll', bump, { capture: true, passive: true });
|
|
34
|
+
window.addEventListener('resize', bump);
|
|
35
|
+
return () => {
|
|
36
|
+
window.removeEventListener('scroll', bump, { capture: true });
|
|
37
|
+
window.removeEventListener('resize', bump);
|
|
38
|
+
};
|
|
26
39
|
});
|
|
27
40
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Svelte action: relocates the tooltip to document.body so a position:fixed
|
|
43
|
+
* tooltip is never clipped by an overflow/scroll ancestor. `use:` actions
|
|
44
|
+
* never run during SSR.
|
|
45
|
+
*/
|
|
46
|
+
const portalToBody = (node: HTMLElement) => {
|
|
47
|
+
document.body.appendChild(node);
|
|
48
|
+
return { destroy: () => node.remove() };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const pos = $derived.by(() => {
|
|
52
|
+
const tooltip = { width: tooltipWidth, height: tooltipHeight };
|
|
53
|
+
if (portal) {
|
|
54
|
+
void portalTick;
|
|
55
|
+
// Convert container coords to viewport coords and clamp to the viewport.
|
|
56
|
+
const rect = originEl?.getBoundingClientRect();
|
|
57
|
+
const dx = rect?.left ?? 0;
|
|
58
|
+
const dy = rect?.top ?? 0;
|
|
59
|
+
const container =
|
|
60
|
+
typeof window === 'undefined'
|
|
61
|
+
? { width: Number.POSITIVE_INFINITY, height: Number.POSITIVE_INFINITY }
|
|
62
|
+
: { width: window.innerWidth, height: window.innerHeight };
|
|
63
|
+
return computeTooltipPosition({
|
|
64
|
+
mouseX: mouseX + dx,
|
|
65
|
+
mouseY: mouseY + dy,
|
|
66
|
+
anchor: anchor === null ? null : { ...anchor, x: anchor.x + dx, y: anchor.y + dy },
|
|
67
|
+
tooltip,
|
|
68
|
+
container
|
|
69
|
+
});
|
|
33
70
|
}
|
|
34
|
-
|
|
71
|
+
const container = {
|
|
72
|
+
width: tooltipEl?.offsetParent?.clientWidth ?? Number.POSITIVE_INFINITY,
|
|
73
|
+
height: tooltipEl?.offsetParent?.clientHeight ?? Number.POSITIVE_INFINITY
|
|
74
|
+
};
|
|
75
|
+
return computeTooltipPosition({ mouseX, mouseY, anchor, tooltip, container });
|
|
35
76
|
});
|
|
36
77
|
</script>
|
|
37
78
|
|
|
38
|
-
{#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{#if typeof customSnippet === 'function'}
|
|
47
|
-
{@render customSnippet(data)}
|
|
48
|
-
{:else}
|
|
49
|
-
{#if data.title}
|
|
50
|
-
<div class="tooltip-title">{data.title}</div>
|
|
51
|
-
{/if}
|
|
52
|
-
{#each data.items as item, i (i)}
|
|
53
|
-
<div class="tooltip-item">
|
|
54
|
-
{#if item.color}
|
|
55
|
-
<span class="tooltip-swatch" style="background: {item.color}"></span>
|
|
56
|
-
{/if}
|
|
57
|
-
<span class="tooltip-label">{item.label}</span>
|
|
58
|
-
<span class="tooltip-value">{item.value}</span>
|
|
59
|
-
</div>
|
|
60
|
-
{/each}
|
|
79
|
+
{#snippet inner(tooltipData: TooltipData)}
|
|
80
|
+
{#if content}
|
|
81
|
+
{@render content()}
|
|
82
|
+
{:else if typeof customSnippet === 'function'}
|
|
83
|
+
{@render customSnippet(tooltipData)}
|
|
84
|
+
{:else}
|
|
85
|
+
{#if tooltipData.title}
|
|
86
|
+
<div class="tooltip-title">{tooltipData.title}</div>
|
|
61
87
|
{/if}
|
|
62
|
-
|
|
88
|
+
{#each tooltipData.items as item, i (i)}
|
|
89
|
+
<div class="tooltip-item">
|
|
90
|
+
{#if item.color}
|
|
91
|
+
<span class="tooltip-swatch" style="background: {item.color}"></span>
|
|
92
|
+
{/if}
|
|
93
|
+
<span class="tooltip-label">{item.label}</span>
|
|
94
|
+
<span class="tooltip-value">{item.value}</span>
|
|
95
|
+
</div>
|
|
96
|
+
{/each}
|
|
97
|
+
{/if}
|
|
98
|
+
{/snippet}
|
|
99
|
+
|
|
100
|
+
{#if data !== null}
|
|
101
|
+
{#if portal}
|
|
102
|
+
<div
|
|
103
|
+
bind:this={tooltipEl}
|
|
104
|
+
bind:clientWidth={tooltipWidth}
|
|
105
|
+
bind:clientHeight={tooltipHeight}
|
|
106
|
+
class="chart-tooltip portal {unstyled ? 'unstyled' : ''} {classes ?? ''}"
|
|
107
|
+
style="left: {pos.left}px; top: {pos.top}px;"
|
|
108
|
+
use:portalToBody
|
|
109
|
+
>
|
|
110
|
+
{@render inner(data)}
|
|
111
|
+
</div>
|
|
112
|
+
{:else}
|
|
113
|
+
<div
|
|
114
|
+
bind:this={tooltipEl}
|
|
115
|
+
bind:clientWidth={tooltipWidth}
|
|
116
|
+
bind:clientHeight={tooltipHeight}
|
|
117
|
+
class="chart-tooltip {unstyled ? 'unstyled' : ''} {classes ?? ''}"
|
|
118
|
+
style="left: {pos.left}px; top: {pos.top}px;"
|
|
119
|
+
>
|
|
120
|
+
{@render inner(data)}
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
63
123
|
{/if}
|
|
64
124
|
|
|
65
125
|
<style>
|
|
66
126
|
.chart-tooltip {
|
|
67
127
|
position: absolute;
|
|
68
128
|
z-index: var(--chart-tooltip-z-index, 10);
|
|
69
|
-
background: var(--chart-tooltip-background, rgba(0, 0, 0, 0.85));
|
|
70
|
-
color: var(--chart-tooltip-color, #fff);
|
|
129
|
+
background: var(--chart-tooltip-background, light-dark(rgba(0, 0, 0, 0.85), #1f2937));
|
|
130
|
+
color: var(--chart-tooltip-color, light-dark(#fff, #f3f4f6));
|
|
71
131
|
font-size: var(--chart-tooltip-font-size, 12px);
|
|
72
132
|
font-family: var(--chart-font-family, inherit);
|
|
73
133
|
padding: var(--chart-tooltip-padding, 8px 12px);
|
|
74
134
|
border-radius: var(--chart-tooltip-border-radius, var(--radius, 4px));
|
|
75
135
|
box-shadow: var(--chart-tooltip-shadow, 0 2px 8px rgba(0, 0, 0, 0.2));
|
|
136
|
+
border: 1px solid
|
|
137
|
+
var(
|
|
138
|
+
--chart-tooltip-border-color,
|
|
139
|
+
light-dark(rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.08))
|
|
140
|
+
);
|
|
76
141
|
pointer-events: none;
|
|
77
142
|
max-width: var(--chart-tooltip-max-width, 280px);
|
|
78
143
|
width: fit-content;
|
|
79
144
|
}
|
|
80
145
|
|
|
146
|
+
.chart-tooltip.portal {
|
|
147
|
+
position: fixed;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.chart-tooltip.unstyled {
|
|
151
|
+
background: none;
|
|
152
|
+
padding: 0;
|
|
153
|
+
border: none;
|
|
154
|
+
box-shadow: none;
|
|
155
|
+
border-radius: 0;
|
|
156
|
+
max-width: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
81
159
|
.tooltip-title {
|
|
82
160
|
font-weight: 600;
|
|
83
161
|
margin-bottom: 4px;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { LegendProperties } from './types';
|
|
3
3
|
|
|
4
|
-
let { items, position = 'bottom', customSnippet, classes }: LegendProperties = $props();
|
|
4
|
+
let { items, position = 'bottom', onToggle, customSnippet, classes }: LegendProperties = $props();
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
{#if items.length > 0}
|
|
@@ -10,10 +10,23 @@
|
|
|
10
10
|
{@render customSnippet(items)}
|
|
11
11
|
{:else}
|
|
12
12
|
{#each items as item, i (i)}
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
{#if typeof onToggle === 'function'}
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
class="legend-item legend-toggle"
|
|
17
|
+
class:legend-hidden={item.hidden}
|
|
18
|
+
aria-pressed={!item.hidden}
|
|
19
|
+
onclick={() => onToggle(i)}
|
|
20
|
+
>
|
|
21
|
+
<span class="legend-swatch" style="background: {item.color}"></span>
|
|
22
|
+
<span class="legend-label">{item.label}</span>
|
|
23
|
+
</button>
|
|
24
|
+
{:else}
|
|
25
|
+
<div class="legend-item">
|
|
26
|
+
<span class="legend-swatch" style="background: {item.color}"></span>
|
|
27
|
+
<span class="legend-label">{item.label}</span>
|
|
28
|
+
</div>
|
|
29
|
+
{/if}
|
|
17
30
|
{/each}
|
|
18
31
|
{/if}
|
|
19
32
|
</div>
|
|
@@ -54,6 +67,23 @@
|
|
|
54
67
|
|
|
55
68
|
.legend-label {
|
|
56
69
|
font-size: var(--chart-legend-font-size, 12px);
|
|
57
|
-
color: var(--chart-legend-color, #333);
|
|
70
|
+
color: var(--chart-legend-color, light-dark(#333, #e5e7eb));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.legend-toggle {
|
|
74
|
+
background: none;
|
|
75
|
+
border: none;
|
|
76
|
+
padding: 0;
|
|
77
|
+
margin: 0;
|
|
78
|
+
font: inherit;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.legend-hidden .legend-swatch {
|
|
83
|
+
opacity: 0.25;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.legend-hidden .legend-label {
|
|
87
|
+
color: var(--chart-legend-hidden-color, light-dark(#bbb, #555));
|
|
58
88
|
}
|
|
59
89
|
</style>
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
import type { Margin, ChartDimensions, PieSliceLayout, ComputedSankeyNode, ComputedSankeyLink, StackedPoint } from './types';
|
|
2
|
+
import { type FontSpec } from './measure';
|
|
2
3
|
export declare function computeChartDimensions(width: number, height: number, margin?: Partial<Margin>): ChartDimensions;
|
|
4
|
+
export type AutoLayoutInput = {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
yTickLabels: string[];
|
|
8
|
+
xTickLabels: string[];
|
|
9
|
+
y2TickLabels?: string[];
|
|
10
|
+
font?: FontSpec;
|
|
11
|
+
hasXAxisLabel?: boolean;
|
|
12
|
+
hasYAxisLabel?: boolean;
|
|
13
|
+
hasY2AxisLabel?: boolean;
|
|
14
|
+
base?: Partial<Margin>;
|
|
15
|
+
};
|
|
16
|
+
export type AutoLayout = ChartDimensions & {
|
|
17
|
+
xRotate: boolean;
|
|
18
|
+
xEvery: number;
|
|
19
|
+
/**
|
|
20
|
+
* Baseline y for the bottom-axis title, already inside the reserved title
|
|
21
|
+
* band — pass straight to Axis.labelOffset, no extra padding needed.
|
|
22
|
+
*/
|
|
23
|
+
xLabelOffset: number;
|
|
24
|
+
};
|
|
3
25
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
export declare function measureTextWidth(text: string, font: string): number | null;
|
|
10
|
-
/**
|
|
11
|
-
* Left margin for a horizontal bar chart's category axis, sized to fit the
|
|
12
|
-
* widest category label. Category tick labels render right-aligned 10px left
|
|
13
|
-
* of the axis line (tick mark 6px + 4px gap), so any label wider than
|
|
14
|
-
* `margin.left - 10` bleeds out of the SVG and gets clipped by the page.
|
|
15
|
-
*
|
|
16
|
-
* - Never shrinks below `fallback` (the legacy fixed gutter), so charts whose
|
|
17
|
-
* labels already fit keep their exact current layout.
|
|
18
|
-
* - Caps at 45% of the chart width so one pathological label cannot crush the
|
|
19
|
-
* plot area; past the cap the label bleeds as before, but the plot survives.
|
|
20
|
-
* - `widestLabelWidth === null` (SSR / unmeasurable) keeps the legacy gutter.
|
|
26
|
+
* Measured, Highcharts-style margins: gutters grow to fit formatted tick labels
|
|
27
|
+
* (instead of clipping) and the bottom axis rotates/thins its labels when the
|
|
28
|
+
* per-category step is too narrow. Order matters to avoid feedback loops:
|
|
29
|
+
* left/right derive from label text only, then innerWidth decides x rotation,
|
|
30
|
+
* then bottom derives from the rotation outcome.
|
|
21
31
|
*/
|
|
22
|
-
export declare function
|
|
32
|
+
export declare function computeAutoLayout(input: AutoLayoutInput): AutoLayout;
|
|
23
33
|
export declare function computePieLayout(data: Array<{
|
|
24
34
|
label: string;
|
|
25
35
|
value: number;
|