@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
|
@@ -8,13 +8,27 @@
|
|
|
8
8
|
import Axis from '../_chart/Axis.svelte';
|
|
9
9
|
import ChartTooltip from '../_chart/ChartTooltip.svelte';
|
|
10
10
|
import Legend from '../_chart/Legend.svelte';
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
createBandScale,
|
|
13
|
+
createLinearScale,
|
|
14
|
+
niceLinearDomain,
|
|
15
|
+
computeLinearTicks
|
|
16
|
+
} from '../_chart/scales';
|
|
17
|
+
import { computeChartDimensions, computeAutoLayout } from '../_chart/geometry';
|
|
13
18
|
import { getColor } from '../_chart/colors';
|
|
14
19
|
import { formatNumber } from '../_chart/format';
|
|
15
20
|
import { roundedRectPath, linePath } from '../_chart/paths';
|
|
16
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
LegendItem,
|
|
23
|
+
TooltipData,
|
|
24
|
+
LinearScale,
|
|
25
|
+
BandScale,
|
|
26
|
+
Point,
|
|
27
|
+
TooltipAnchor
|
|
28
|
+
} from '../_chart/types';
|
|
17
29
|
import { DEFAULT_CHART_CORNER_RADIUS, DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
|
|
30
|
+
import { pointerPositionIn, dismissOnOutsidePointerDown } from '../_chart/interactions';
|
|
31
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
18
32
|
|
|
19
33
|
// ── Per-instance uid for SVG <defs> ids ────────────────────────
|
|
20
34
|
const uid = Math.random().toString(36).slice(2, 9);
|
|
@@ -36,6 +50,8 @@
|
|
|
36
50
|
minBarHeight = 2,
|
|
37
51
|
margin,
|
|
38
52
|
tooltipPortal = false,
|
|
53
|
+
interactiveLegend = false,
|
|
54
|
+
hideLegendBelow = 360,
|
|
39
55
|
tooltipSnippet,
|
|
40
56
|
onbarclick,
|
|
41
57
|
testId,
|
|
@@ -45,43 +61,35 @@
|
|
|
45
61
|
// ── State ──────────────────────────────────────────────────────
|
|
46
62
|
|
|
47
63
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
64
|
+
let plotEl: HTMLDivElement | null = $state(null);
|
|
48
65
|
let chartWidth = $state(0);
|
|
49
66
|
let chartHeight = $state(0);
|
|
50
67
|
let hoveredCategoryIndex = $state<number | null>(null);
|
|
51
68
|
let mouseX = $state(0);
|
|
52
69
|
let mouseY = $state(0);
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
const hiddenSeries = new SvelteSet<number>();
|
|
71
|
+
|
|
72
|
+
function toggleSeries(index: number): void {
|
|
73
|
+
if (hiddenSeries.has(index)) {
|
|
74
|
+
hiddenSeries.delete(index);
|
|
75
|
+
} else {
|
|
76
|
+
hiddenSeries.add(index);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
55
79
|
|
|
56
80
|
// ── Formatters ─────────────────────────────────────────────────
|
|
57
81
|
|
|
58
82
|
const leftFormat = $derived(leftAxis.valueFormat ?? formatNumber);
|
|
59
83
|
const rightFormat = $derived(rightAxis.valueFormat ?? formatNumber);
|
|
60
84
|
|
|
61
|
-
// ── Layout — wider right margin to accommodate right-axis labels ─
|
|
62
|
-
|
|
63
|
-
const dims = $derived(
|
|
64
|
-
computeChartDimensions(chartWidth, chartHeight, {
|
|
65
|
-
top: 24,
|
|
66
|
-
right: 56,
|
|
67
|
-
bottom: 40,
|
|
68
|
-
left: 56,
|
|
69
|
-
...margin
|
|
70
|
-
})
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
// ── Scales ─────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
const catScale: BandScale = $derived(
|
|
76
|
-
createBandScale(categories, [0, dims.innerWidth], barPadding)
|
|
77
|
-
);
|
|
78
|
-
|
|
79
85
|
/**
|
|
80
86
|
* Computes the [min, max] domain for all series mapped to the given axis index,
|
|
81
87
|
* then applies nice rounding. Returns [0, 1] for empty series.
|
|
82
88
|
*/
|
|
83
89
|
const axisDomain = (axisIndex: 0 | 1): [number, number] => {
|
|
84
|
-
const axisSeries = series.filter(
|
|
90
|
+
const axisSeries = series.filter(
|
|
91
|
+
(s, si) => s.yAxisIndex === axisIndex && !hiddenSeries.has(si)
|
|
92
|
+
);
|
|
85
93
|
if (axisSeries.length === 0) {
|
|
86
94
|
return [0, 1];
|
|
87
95
|
}
|
|
@@ -95,6 +103,39 @@
|
|
|
95
103
|
const leftDomain: [number, number] = $derived(axisDomain(0));
|
|
96
104
|
const rightDomain: [number, number] = $derived(axisDomain(1));
|
|
97
105
|
|
|
106
|
+
// ── Layout — auto-sized margins from measured tick-label widths ─
|
|
107
|
+
|
|
108
|
+
const yTickCount = $derived(Math.max(2, Math.min(6, Math.floor(chartHeight / 70))));
|
|
109
|
+
|
|
110
|
+
const layout = $derived.by(() =>
|
|
111
|
+
computeAutoLayout({
|
|
112
|
+
width: chartWidth,
|
|
113
|
+
height: chartHeight,
|
|
114
|
+
yTickLabels: computeLinearTicks(leftDomain, yTickCount).map((t) => leftFormat(t)),
|
|
115
|
+
y2TickLabels: computeLinearTicks(rightDomain, yTickCount).map((t) => rightFormat(t)),
|
|
116
|
+
xTickLabels: categories,
|
|
117
|
+
hasYAxisLabel: Boolean(leftAxis.title),
|
|
118
|
+
hasY2AxisLabel: Boolean(rightAxis.title),
|
|
119
|
+
base: { top: 24, right: 28, bottom: 40, left: 28 }
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// The margin prop stays an explicit per-side override on top of auto-sizing.
|
|
124
|
+
const dims = $derived(
|
|
125
|
+
computeChartDimensions(chartWidth, chartHeight, {
|
|
126
|
+
top: margin?.top ?? layout.margin.top,
|
|
127
|
+
right: margin?.right ?? layout.margin.right,
|
|
128
|
+
bottom: margin?.bottom ?? layout.margin.bottom,
|
|
129
|
+
left: margin?.left ?? layout.margin.left
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// ── Scales ─────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
const catScale: BandScale = $derived(
|
|
136
|
+
createBandScale(categories, [0, dims.innerWidth], barPadding)
|
|
137
|
+
);
|
|
138
|
+
|
|
98
139
|
const leftScale: LinearScale = $derived(createLinearScale(leftDomain, [dims.innerHeight, 0]));
|
|
99
140
|
const rightScale: LinearScale = $derived(createLinearScale(rightDomain, [dims.innerHeight, 0]));
|
|
100
141
|
|
|
@@ -113,13 +154,23 @@
|
|
|
113
154
|
const leftAxisSeries: AxisSeriesEntry[] = $derived(
|
|
114
155
|
series
|
|
115
156
|
.map((s, si) => ({ series: s, seriesIndex: si }))
|
|
116
|
-
.filter(
|
|
157
|
+
.filter(
|
|
158
|
+
(entry) =>
|
|
159
|
+
entry.series.yAxisIndex === 0 &&
|
|
160
|
+
entry.series.type !== 'line' &&
|
|
161
|
+
!hiddenSeries.has(entry.seriesIndex)
|
|
162
|
+
)
|
|
117
163
|
);
|
|
118
164
|
|
|
119
165
|
const rightAxisSeries: AxisSeriesEntry[] = $derived(
|
|
120
166
|
series
|
|
121
167
|
.map((s, si) => ({ series: s, seriesIndex: si }))
|
|
122
|
-
.filter(
|
|
168
|
+
.filter(
|
|
169
|
+
(entry) =>
|
|
170
|
+
entry.series.yAxisIndex === 1 &&
|
|
171
|
+
entry.series.type !== 'line' &&
|
|
172
|
+
!hiddenSeries.has(entry.seriesIndex)
|
|
173
|
+
)
|
|
123
174
|
);
|
|
124
175
|
|
|
125
176
|
const columnSeriesEntries: AxisSeriesEntry[] = $derived([...leftAxisSeries, ...rightAxisSeries]);
|
|
@@ -168,7 +219,10 @@
|
|
|
168
219
|
const barY = value >= 0 ? valueY : zeroY;
|
|
169
220
|
const barHeight = Math.max(minBarHeight, Math.abs(valueY - zeroY));
|
|
170
221
|
|
|
171
|
-
const path =
|
|
222
|
+
const path =
|
|
223
|
+
value >= 0
|
|
224
|
+
? roundedRectPath(barX, barY, barW, barHeight, barRadius, barRadius, 0, 0)
|
|
225
|
+
: roundedRectPath(barX, barY, barW, barHeight, 0, 0, barRadius, barRadius);
|
|
172
226
|
|
|
173
227
|
result.push({
|
|
174
228
|
x: barX,
|
|
@@ -201,7 +255,7 @@
|
|
|
201
255
|
}
|
|
202
256
|
return series
|
|
203
257
|
.map((s, si) => ({ series: s, seriesIndex: si }))
|
|
204
|
-
.filter((entry) => entry.series.type === 'line')
|
|
258
|
+
.filter((entry) => entry.series.type === 'line' && !hiddenSeries.has(entry.seriesIndex))
|
|
205
259
|
.map((entry) => {
|
|
206
260
|
const scale = entry.series.yAxisIndex === 0 ? leftScale : rightScale;
|
|
207
261
|
const color = resolvedColor(entry.series, entry.seriesIndex);
|
|
@@ -223,7 +277,11 @@
|
|
|
223
277
|
// ── Legend items ───────────────────────────────────────────────
|
|
224
278
|
|
|
225
279
|
const legendItems: LegendItem[] = $derived(
|
|
226
|
-
series.map((s, si) => ({
|
|
280
|
+
series.map((s, si) => ({
|
|
281
|
+
label: s.name,
|
|
282
|
+
color: resolvedColor(s, si),
|
|
283
|
+
hidden: hiddenSeries.has(si)
|
|
284
|
+
}))
|
|
227
285
|
);
|
|
228
286
|
|
|
229
287
|
// ── Tooltip ────────────────────────────────────────────────────
|
|
@@ -246,17 +304,47 @@
|
|
|
246
304
|
}
|
|
247
305
|
const catIdx = hoveredCategoryIndex;
|
|
248
306
|
const category = categories[catIdx];
|
|
249
|
-
const items = series
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
307
|
+
const items = series
|
|
308
|
+
.map((s, si) => ({ s, si }))
|
|
309
|
+
.filter(({ si }) => !hiddenSeries.has(si))
|
|
310
|
+
.map(({ s, si }) => {
|
|
311
|
+
const fmt = s.yAxisIndex === 0 ? leftFormat : rightFormat;
|
|
312
|
+
return {
|
|
313
|
+
label: s.name,
|
|
314
|
+
value: fmt(s.data[catIdx] ?? 0),
|
|
315
|
+
color: resolvedColor(s, si)
|
|
316
|
+
};
|
|
317
|
+
});
|
|
257
318
|
return { title: category, items };
|
|
258
319
|
});
|
|
259
320
|
|
|
321
|
+
// Category-anchored tooltip position: the topmost bar-top or line-dot y
|
|
322
|
+
// across all visible series at the hovered category, matching the
|
|
323
|
+
// Highcharts shared-tooltip anchor convention.
|
|
324
|
+
const anchor = $derived.by<TooltipAnchor | null>(() => {
|
|
325
|
+
if (hoveredCategoryIndex === null) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const catIdx = hoveredCategoryIndex;
|
|
329
|
+
const ys: number[] = [];
|
|
330
|
+
for (const bar of bars) {
|
|
331
|
+
if (bar.categoryIndex === catIdx) {
|
|
332
|
+
ys.push(bar.y);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const ls of lineSeriesData) {
|
|
336
|
+
const p = ls.points[catIdx];
|
|
337
|
+
if (p) {
|
|
338
|
+
ys.push(p.y);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
x: dims.margin.left + catScale(categories[catIdx]) + catScale.bandwidth / 2,
|
|
343
|
+
y: dims.margin.top + (ys.length > 0 ? Math.min(...ys) : 0),
|
|
344
|
+
side: 'top'
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
|
|
260
348
|
// ── Axis tick formatters ───────────────────────────────────────
|
|
261
349
|
|
|
262
350
|
const leftTickFormat = (tick: number | string): string =>
|
|
@@ -283,36 +371,34 @@
|
|
|
283
371
|
}))
|
|
284
372
|
);
|
|
285
373
|
|
|
374
|
+
function categoryAriaLabel(catIdx: number): string {
|
|
375
|
+
const parts = series
|
|
376
|
+
.map((s, si) => ({ s, si }))
|
|
377
|
+
.filter(({ si }) => !hiddenSeries.has(si))
|
|
378
|
+
.map(
|
|
379
|
+
({ s }) =>
|
|
380
|
+
`${s.name} ${(s.yAxisIndex === 0 ? leftFormat : rightFormat)(s.data[catIdx] ?? 0)}`
|
|
381
|
+
);
|
|
382
|
+
return `${categories[catIdx]}: ${parts.join(', ')}`;
|
|
383
|
+
}
|
|
384
|
+
|
|
286
385
|
// ── Interactions ───────────────────────────────────────────────
|
|
287
386
|
|
|
288
|
-
const trackMouse = (event:
|
|
289
|
-
|
|
290
|
-
|
|
387
|
+
const trackMouse = (event: PointerEvent) => {
|
|
388
|
+
const position = pointerPositionIn(plotEl, event);
|
|
389
|
+
if (position !== null) {
|
|
390
|
+
mouseX = position.x;
|
|
391
|
+
mouseY = position.y;
|
|
291
392
|
}
|
|
292
|
-
const rect = containerEl.getBoundingClientRect();
|
|
293
|
-
mouseX = event.clientX - rect.left;
|
|
294
|
-
mouseY = event.clientY - rect.top;
|
|
295
|
-
mouseClientX = event.clientX;
|
|
296
|
-
mouseClientY = event.clientY;
|
|
297
393
|
};
|
|
298
394
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
* `tooltipPortal` is set; `use:` actions never run during SSR.
|
|
303
|
-
*/
|
|
304
|
-
const portalToBody = (node: HTMLElement) => {
|
|
305
|
-
document.body.appendChild(node);
|
|
306
|
-
return {
|
|
307
|
-
destroy: () => {
|
|
308
|
-
node.remove();
|
|
309
|
-
}
|
|
310
|
-
};
|
|
395
|
+
const handleCategoryEnter = (event: PointerEvent, catIdx: number) => {
|
|
396
|
+
hoveredCategoryIndex = catIdx;
|
|
397
|
+
trackMouse(event);
|
|
311
398
|
};
|
|
312
399
|
|
|
313
|
-
const
|
|
400
|
+
const handleFocus = (catIdx: number) => {
|
|
314
401
|
hoveredCategoryIndex = catIdx;
|
|
315
|
-
trackMouse(event);
|
|
316
402
|
};
|
|
317
403
|
|
|
318
404
|
const handleCategoryLeave = () => {
|
|
@@ -326,6 +412,24 @@
|
|
|
326
412
|
onbarclick({ categoryIndex: catIdx, context: buildTooltipContext(catIdx) });
|
|
327
413
|
};
|
|
328
414
|
|
|
415
|
+
const handleKeydown = (event: KeyboardEvent, catIdx: number) => {
|
|
416
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
handleCategoryClick(catIdx);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// Touch taps have no pointerleave: dismiss when a pointerdown lands outside.
|
|
423
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
424
|
+
$effect(() => {
|
|
425
|
+
if (hoveredCategoryIndex === null) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
return dismissOnOutsidePointerDown(containerEl, () => {
|
|
429
|
+
hoveredCategoryIndex = null;
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
329
433
|
// ── Empty state ────────────────────────────────────────────────
|
|
330
434
|
|
|
331
435
|
const isEmpty = $derived(
|
|
@@ -339,168 +443,184 @@
|
|
|
339
443
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
340
444
|
>
|
|
341
445
|
{#if !isEmpty}
|
|
342
|
-
{#if showLegend}
|
|
343
|
-
|
|
446
|
+
{#if showLegend && (chartWidth === 0 || hideLegendBelow === 0 || chartWidth >= hideLegendBelow)}
|
|
447
|
+
{#if interactiveLegend}
|
|
448
|
+
<Legend items={legendItems} position="top" onToggle={toggleSeries} />
|
|
449
|
+
{:else}
|
|
450
|
+
<Legend items={legendItems} position="top" />
|
|
451
|
+
{/if}
|
|
344
452
|
{/if}
|
|
345
453
|
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
orientation="left"
|
|
357
|
-
scale={leftScale}
|
|
358
|
-
{showGridlines}
|
|
359
|
-
gridlineLength={dims.innerWidth}
|
|
360
|
-
tickFormat={leftTickFormat}
|
|
361
|
-
classes={leftAxis.color ? `axis-left-colored` : ''}
|
|
362
|
-
/>
|
|
363
|
-
|
|
364
|
-
<!-- Right Y-axis (index 1) — positioned at innerWidth -->
|
|
365
|
-
<g transform="translate({dims.innerWidth}, 0)">
|
|
454
|
+
<div class="chart-plot" bind:this={plotEl}>
|
|
455
|
+
<ChartContainer
|
|
456
|
+
bind:width={chartWidth}
|
|
457
|
+
bind:height={chartHeight}
|
|
458
|
+
{aspectRatio}
|
|
459
|
+
{maxHeight}
|
|
460
|
+
{minHeight}
|
|
461
|
+
>
|
|
462
|
+
<g transform="translate({dims.margin.left}, {dims.margin.top})">
|
|
463
|
+
<!-- Left Y-axis (index 0) -->
|
|
366
464
|
<Axis
|
|
367
|
-
orientation="
|
|
368
|
-
scale={
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
465
|
+
orientation="left"
|
|
466
|
+
scale={leftScale}
|
|
467
|
+
tickCount={yTickCount}
|
|
468
|
+
{showGridlines}
|
|
469
|
+
gridlineLength={dims.innerWidth}
|
|
470
|
+
tickFormat={leftTickFormat}
|
|
471
|
+
classes={leftAxis.color ? `axis-left-colored` : ''}
|
|
372
472
|
/>
|
|
373
|
-
</g>
|
|
374
473
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
2}) rotate(90)"
|
|
396
|
-
text-anchor="middle"
|
|
397
|
-
style={rightAxis.color ? `fill: ${rightAxis.color}` : ''}
|
|
398
|
-
>
|
|
399
|
-
{rightAxis.title}
|
|
400
|
-
</text>
|
|
401
|
-
{/if}
|
|
402
|
-
|
|
403
|
-
<!-- Column/bar shapes -->
|
|
404
|
-
{#each bars as bar, barIdx (barIdx)}
|
|
405
|
-
<path
|
|
406
|
-
class="bar-shape"
|
|
407
|
-
class:bar-hovered={hoveredCategoryIndex === bar.categoryIndex}
|
|
408
|
-
class:bar-dimmed={hoveredCategoryIndex !== null &&
|
|
409
|
-
hoveredCategoryIndex !== bar.categoryIndex}
|
|
410
|
-
d={bar.path}
|
|
411
|
-
fill={bar.color}
|
|
412
|
-
aria-label="{categories[bar.categoryIndex]}: {bar.value}"
|
|
413
|
-
role="img"
|
|
414
|
-
/>
|
|
415
|
-
{/each}
|
|
416
|
-
|
|
417
|
-
<!-- Line series drawn above columns -->
|
|
418
|
-
{#each lineSeriesData as ls, lsi (lsi)}
|
|
419
|
-
{#if ls.points.length >= 2}
|
|
420
|
-
<path
|
|
421
|
-
class="line-series"
|
|
422
|
-
d={linePath(ls.points, 'monotone')}
|
|
423
|
-
stroke={ls.color}
|
|
424
|
-
fill="none"
|
|
474
|
+
<!-- Right Y-axis (index 1) — positioned at innerWidth -->
|
|
475
|
+
<g transform="translate({dims.innerWidth}, 0)">
|
|
476
|
+
<Axis
|
|
477
|
+
orientation="right"
|
|
478
|
+
scale={rightScale}
|
|
479
|
+
tickCount={yTickCount}
|
|
480
|
+
showGridlines={false}
|
|
481
|
+
tickFormat={rightTickFormat}
|
|
482
|
+
classes={rightAxis.color ? `axis-right-colored` : ''}
|
|
483
|
+
/>
|
|
484
|
+
</g>
|
|
485
|
+
|
|
486
|
+
<!-- X-axis at bottom -->
|
|
487
|
+
<g transform="translate(0, {dims.innerHeight})">
|
|
488
|
+
<Axis
|
|
489
|
+
orientation="bottom"
|
|
490
|
+
scale={catScale}
|
|
491
|
+
rotateTicks={layout.xRotate}
|
|
492
|
+
tickEvery={layout.xEvery}
|
|
493
|
+
showGridlines={false}
|
|
425
494
|
/>
|
|
495
|
+
</g>
|
|
496
|
+
|
|
497
|
+
<!-- Axis titles -->
|
|
498
|
+
{#if leftAxis.title}
|
|
499
|
+
<text
|
|
500
|
+
class="axis-title axis-title-left"
|
|
501
|
+
transform="translate({-dims.margin.left + 12}, {dims.innerHeight / 2}) rotate(-90)"
|
|
502
|
+
text-anchor="middle"
|
|
503
|
+
style={leftAxis.color ? `fill: ${leftAxis.color}` : ''}
|
|
504
|
+
>
|
|
505
|
+
{leftAxis.title}
|
|
506
|
+
</text>
|
|
507
|
+
{/if}
|
|
508
|
+
{#if rightAxis.title}
|
|
509
|
+
<text
|
|
510
|
+
class="axis-title axis-title-right"
|
|
511
|
+
transform="translate({dims.innerWidth + dims.margin.right - 12}, {dims.innerHeight /
|
|
512
|
+
2}) rotate(90)"
|
|
513
|
+
text-anchor="middle"
|
|
514
|
+
style={rightAxis.color ? `fill: ${rightAxis.color}` : ''}
|
|
515
|
+
>
|
|
516
|
+
{rightAxis.title}
|
|
517
|
+
</text>
|
|
426
518
|
{/if}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
class
|
|
432
|
-
class:
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
fill={
|
|
437
|
-
|
|
438
|
-
stroke-width="var(--dual-axis-dot-stroke-width, 1.5)"
|
|
439
|
-
aria-label="{categories[ptIdx]}: {series[ls.seriesIndex]?.data[ptIdx] ?? 0}"
|
|
519
|
+
|
|
520
|
+
<!-- Column/bar shapes -->
|
|
521
|
+
{#each bars as bar, barIdx (barIdx)}
|
|
522
|
+
<path
|
|
523
|
+
class="bar-shape"
|
|
524
|
+
class:bar-hovered={hoveredCategoryIndex === bar.categoryIndex}
|
|
525
|
+
class:bar-dimmed={hoveredCategoryIndex !== null &&
|
|
526
|
+
hoveredCategoryIndex !== bar.categoryIndex}
|
|
527
|
+
d={bar.path}
|
|
528
|
+
fill={bar.color}
|
|
529
|
+
aria-label="{categories[bar.categoryIndex]}: {bar.value}"
|
|
440
530
|
role="img"
|
|
441
531
|
/>
|
|
442
532
|
{/each}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
533
|
+
|
|
534
|
+
<!-- Line series drawn above columns -->
|
|
535
|
+
{#each lineSeriesData as ls, lsi (lsi)}
|
|
536
|
+
{#if ls.points.length >= 2}
|
|
537
|
+
<path
|
|
538
|
+
class="line-series"
|
|
539
|
+
d={linePath(ls.points, 'monotone')}
|
|
540
|
+
stroke={ls.color}
|
|
541
|
+
fill="none"
|
|
542
|
+
/>
|
|
543
|
+
{/if}
|
|
544
|
+
<!-- Line dots -->
|
|
545
|
+
{#each ls.points as pt, ptIdx (ptIdx)}
|
|
546
|
+
<circle
|
|
547
|
+
class="line-dot"
|
|
548
|
+
class:dot-hovered={hoveredCategoryIndex === ptIdx}
|
|
549
|
+
class:dot-dimmed={hoveredCategoryIndex !== null && hoveredCategoryIndex !== ptIdx}
|
|
550
|
+
cx={pt.x}
|
|
551
|
+
cy={pt.y}
|
|
552
|
+
r={hoveredCategoryIndex === ptIdx ? 6 : 4}
|
|
553
|
+
fill={ls.color}
|
|
554
|
+
style="stroke: var(--dual-axis-dot-stroke, light-dark(#fff, #111827)); stroke-width: var(--dual-axis-dot-stroke-width, 1.5);"
|
|
555
|
+
aria-label="{categories[ptIdx]}: {series[ls.seriesIndex]?.data[ptIdx] ?? 0}"
|
|
556
|
+
role="img"
|
|
557
|
+
/>
|
|
558
|
+
{/each}
|
|
559
|
+
{/each}
|
|
560
|
+
|
|
561
|
+
<!-- Invisible per-category hover targets (full inner height) -->
|
|
562
|
+
{#each hoverRects as hr (hr.catIdx)}
|
|
563
|
+
<rect
|
|
564
|
+
class="hover-target"
|
|
565
|
+
x={hr.x}
|
|
566
|
+
y={0}
|
|
567
|
+
width={hr.width}
|
|
568
|
+
height={dims.innerHeight}
|
|
569
|
+
fill="transparent"
|
|
570
|
+
data-category-index={hr.catIdx}
|
|
571
|
+
tabindex="0"
|
|
572
|
+
role="button"
|
|
573
|
+
aria-label={categoryAriaLabel(hr.catIdx)}
|
|
574
|
+
onpointerenter={(event) => handleCategoryEnter(event, hr.catIdx)}
|
|
575
|
+
onpointermove={trackMouse}
|
|
576
|
+
onpointerleave={handleCategoryLeave}
|
|
577
|
+
onfocus={() => handleFocus(hr.catIdx)}
|
|
578
|
+
onblur={handleCategoryLeave}
|
|
579
|
+
onkeydown={(event) => handleKeydown(event, hr.catIdx)}
|
|
580
|
+
onclick={() => handleCategoryClick(hr.catIdx)}
|
|
581
|
+
/>
|
|
582
|
+
{/each}
|
|
583
|
+
|
|
584
|
+
<!-- Hover vertical guideline -->
|
|
585
|
+
{#if hoveredCategoryIndex !== null}
|
|
586
|
+
{@const guideX = catScale(categories[hoveredCategoryIndex]) + catScale.bandwidth / 2}
|
|
587
|
+
<line class="hover-guideline" x1={guideX} x2={guideX} y1={0} y2={dims.innerHeight} />
|
|
588
|
+
{/if}
|
|
589
|
+
</g>
|
|
590
|
+
|
|
591
|
+
<!-- SVG defs id namespace anchor (keeps uid live in reactive graph) -->
|
|
592
|
+
<defs>
|
|
593
|
+
<marker id="{uid}-anchor" />
|
|
594
|
+
</defs>
|
|
595
|
+
</ChartContainer>
|
|
596
|
+
|
|
597
|
+
{#if typeof tooltipSnippet === 'function'}
|
|
598
|
+
<ChartTooltip
|
|
599
|
+
data={tooltipData}
|
|
600
|
+
{mouseX}
|
|
601
|
+
{mouseY}
|
|
602
|
+
{anchor}
|
|
603
|
+
portal={tooltipPortal}
|
|
604
|
+
originEl={plotEl}
|
|
605
|
+
unstyled
|
|
487
606
|
>
|
|
488
|
-
{#
|
|
489
|
-
|
|
607
|
+
{#snippet content()}
|
|
608
|
+
{#if hoveredCategoryIndex !== null}
|
|
490
609
|
{@render tooltipSnippet(buildTooltipContext(hoveredCategoryIndex))}
|
|
491
|
-
|
|
492
|
-
{
|
|
493
|
-
|
|
494
|
-
{/if}
|
|
495
|
-
</div>
|
|
496
|
-
{:else if typeof tooltipSnippet === 'function'}
|
|
497
|
-
<div class="chart-tooltip-slot" style="left: {mouseX + 12}px; top: {mouseY - 12}px;">
|
|
498
|
-
{@render tooltipSnippet(buildTooltipContext(hoveredCategoryIndex))}
|
|
499
|
-
</div>
|
|
610
|
+
{/if}
|
|
611
|
+
{/snippet}
|
|
612
|
+
</ChartTooltip>
|
|
500
613
|
{:else}
|
|
501
|
-
<ChartTooltip
|
|
614
|
+
<ChartTooltip
|
|
615
|
+
data={tooltipData}
|
|
616
|
+
{mouseX}
|
|
617
|
+
{mouseY}
|
|
618
|
+
{anchor}
|
|
619
|
+
portal={tooltipPortal}
|
|
620
|
+
originEl={plotEl}
|
|
621
|
+
/>
|
|
502
622
|
{/if}
|
|
503
|
-
|
|
623
|
+
</div>
|
|
504
624
|
{:else}
|
|
505
625
|
<div class="chart-empty">No data available.</div>
|
|
506
626
|
{/if}
|
|
@@ -512,6 +632,10 @@
|
|
|
512
632
|
position: relative;
|
|
513
633
|
}
|
|
514
634
|
|
|
635
|
+
.chart-plot {
|
|
636
|
+
position: relative;
|
|
637
|
+
}
|
|
638
|
+
|
|
515
639
|
.bar-shape {
|
|
516
640
|
transition: opacity var(--chart-transition-duration, 0.2s) ease;
|
|
517
641
|
cursor: pointer;
|
|
@@ -547,8 +671,13 @@
|
|
|
547
671
|
cursor: pointer;
|
|
548
672
|
}
|
|
549
673
|
|
|
674
|
+
.hover-target:focus-visible {
|
|
675
|
+
outline: 2px solid var(--chart-axis-label-color, light-dark(#333, #e5e7eb));
|
|
676
|
+
outline-offset: -2px;
|
|
677
|
+
}
|
|
678
|
+
|
|
550
679
|
.hover-guideline {
|
|
551
|
-
stroke: var(--dual-axis-guideline-color, #aaa);
|
|
680
|
+
stroke: var(--dual-axis-guideline-color, light-dark(#aaa, #4b5563));
|
|
552
681
|
stroke-width: var(--dual-axis-guideline-width, 1);
|
|
553
682
|
stroke-dasharray: var(--dual-axis-guideline-dash, 4 3);
|
|
554
683
|
pointer-events: none;
|
|
@@ -556,27 +685,15 @@
|
|
|
556
685
|
}
|
|
557
686
|
|
|
558
687
|
.axis-title {
|
|
559
|
-
fill: var(--chart-axis-label-color, #333);
|
|
688
|
+
fill: var(--chart-axis-label-color, light-dark(#333, #e5e7eb));
|
|
560
689
|
font-size: var(--chart-axis-label-font-size, 11px);
|
|
561
690
|
font-family: var(--chart-font-family, inherit);
|
|
562
691
|
font-weight: 500;
|
|
563
692
|
}
|
|
564
693
|
|
|
565
|
-
.chart-tooltip-slot {
|
|
566
|
-
position: absolute;
|
|
567
|
-
z-index: 10;
|
|
568
|
-
pointer-events: none;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
.chart-tooltip-portal {
|
|
572
|
-
position: fixed;
|
|
573
|
-
z-index: var(--chart-tooltip-z-index, 10);
|
|
574
|
-
pointer-events: none;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
694
|
.chart-empty {
|
|
578
695
|
padding: var(--chart-empty-padding, 32px 24px);
|
|
579
|
-
color: var(--chart-empty-color, #9ca3af);
|
|
696
|
+
color: var(--chart-empty-color, light-dark(#9ca3af, #6b7280));
|
|
580
697
|
text-align: center;
|
|
581
698
|
}
|
|
582
699
|
</style>
|