@juspay/svelte-ui-components 2.86.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/_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
|
@@ -12,18 +12,28 @@
|
|
|
12
12
|
import Axis from '../_chart/Axis.svelte';
|
|
13
13
|
import ChartTooltip from '../_chart/ChartTooltip.svelte';
|
|
14
14
|
import Legend from '../_chart/Legend.svelte';
|
|
15
|
-
import { createBandScale, createLinearScale, niceLinearDomain } from '../_chart/scales';
|
|
16
15
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
createBandScale,
|
|
17
|
+
createLinearScale,
|
|
18
|
+
niceLinearDomain,
|
|
19
|
+
computeLinearTicks
|
|
20
|
+
} from '../_chart/scales';
|
|
21
|
+
import { computeAutoLayout } from '../_chart/geometry';
|
|
22
|
+
import { getColor, getContrastColor } from '../_chart/colors';
|
|
22
23
|
import { formatNumber } from '../_chart/format';
|
|
23
24
|
import { roundedRectPath } from '../_chart/paths';
|
|
25
|
+
import { measureText, readCssVarPx } from '../_chart/measure';
|
|
26
|
+
import {
|
|
27
|
+
resolveEndLabel,
|
|
28
|
+
resolveInsideLabel,
|
|
29
|
+
placedLabelRect,
|
|
30
|
+
dropOverlapping
|
|
31
|
+
} from '../_chart/labels';
|
|
24
32
|
import type { LegendItem, BarRect } from '../_chart/types';
|
|
25
33
|
import { DEFAULT_CHART_CORNER_RADIUS, DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
|
|
26
|
-
import {
|
|
34
|
+
import type { TooltipAnchor } from '../_chart/types';
|
|
35
|
+
import { pointerPositionIn, dismissOnOutsidePointerDown } from '../_chart/interactions';
|
|
36
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
27
37
|
|
|
28
38
|
// ── Per-instance uid prefix for <defs> ids (A1-3) ─────────────
|
|
29
39
|
// Derived at module scope per the library uid pattern (e.g. AreaChart
|
|
@@ -77,6 +87,9 @@
|
|
|
77
87
|
topN,
|
|
78
88
|
overflowLabel = 'Other',
|
|
79
89
|
hideBarGraphics = false,
|
|
90
|
+
interactiveLegend = false,
|
|
91
|
+
hideLegendBelow = 360,
|
|
92
|
+
tooltipPortal = false,
|
|
80
93
|
onbarclick,
|
|
81
94
|
onbarhover,
|
|
82
95
|
testId,
|
|
@@ -86,17 +99,22 @@
|
|
|
86
99
|
// ── State ──────────────────────────────────────────────────────
|
|
87
100
|
|
|
88
101
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
102
|
+
let plotEl: HTMLDivElement | null = $state(null);
|
|
89
103
|
let chartWidth = $state(0);
|
|
90
104
|
let chartHeight = $state(0);
|
|
91
105
|
let hovered = $state<{ si: number; pi: number } | null>(null);
|
|
92
106
|
let mouseX = $state(0);
|
|
93
107
|
let mouseY = $state(0);
|
|
108
|
+
let anchor = $state<TooltipAnchor | null>(null);
|
|
109
|
+
let valueFontSize = $state(14);
|
|
94
110
|
/** Internal tracking for the imperative highlight API (onChartReady). */
|
|
95
111
|
let apiHighlightedIndex = $state<number | null>(null);
|
|
112
|
+
const hiddenSeries = new SvelteSet<number>();
|
|
96
113
|
|
|
97
114
|
// ── onMount: emit ChartHighlightAPI via onChartReady ──────────
|
|
98
115
|
|
|
99
116
|
onMount(() => {
|
|
117
|
+
valueFontSize = readCssVarPx(containerEl, '--barchart-value-font-size', 14);
|
|
100
118
|
if (typeof onChartReady !== 'function') {
|
|
101
119
|
return;
|
|
102
120
|
}
|
|
@@ -185,63 +203,26 @@
|
|
|
185
203
|
});
|
|
186
204
|
});
|
|
187
205
|
|
|
206
|
+
// Legend-toggled series are excluded from geometry and scales (Highcharts
|
|
207
|
+
// rescales on toggle) but stay in legendItems so they can be re-enabled.
|
|
208
|
+
let visibleEntries = $derived(
|
|
209
|
+
resolvedSeries.map((s, si) => ({ s, si })).filter((e) => !hiddenSeries.has(e.si))
|
|
210
|
+
);
|
|
211
|
+
let visibleOrder = $derived(new Map(visibleEntries.map((e, vi) => [e.si, vi])));
|
|
212
|
+
|
|
213
|
+
function toggleSeries(index: number): void {
|
|
214
|
+
if (hiddenSeries.has(index)) {
|
|
215
|
+
hiddenSeries.delete(index);
|
|
216
|
+
} else {
|
|
217
|
+
hiddenSeries.add(index);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
188
221
|
let labels = $derived.by(() => {
|
|
189
222
|
const first = resolvedSeries[0]?.data ?? [];
|
|
190
223
|
return first.map((d) => d.label);
|
|
191
224
|
});
|
|
192
225
|
|
|
193
|
-
/**
|
|
194
|
-
* Widest category label in the axis tick font, for the horizontal-orientation
|
|
195
|
-
* gutter below. Horizontal charts put category text (not short numeric ticks)
|
|
196
|
-
* on the Y axis, so the gutter must fit real words like "Submitted Address" —
|
|
197
|
-
* a fixed gutter clips them. Resolved from the mounted container so CSS-var
|
|
198
|
-
* theming (--chart-axis-font-size / --chart-axis-font-family) is honoured.
|
|
199
|
-
* Null when unmeasurable (SSR, no canvas) — the gutter then keeps its legacy
|
|
200
|
-
* fixed width.
|
|
201
|
-
*/
|
|
202
|
-
let widestCategoryLabelWidth = $derived.by(() => {
|
|
203
|
-
if (isVertical || !showYAxis || labels.length === 0 || containerEl === null) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
const containerStyle = getComputedStyle(containerEl);
|
|
207
|
-
const axisFontSize = containerStyle.getPropertyValue('--chart-axis-font-size').trim() || '11px';
|
|
208
|
-
const axisFontFamilyToken = containerStyle.getPropertyValue('--chart-axis-font-family').trim();
|
|
209
|
-
const axisFontFamily =
|
|
210
|
-
axisFontFamilyToken === '' || axisFontFamilyToken === 'inherit'
|
|
211
|
-
? containerStyle.fontFamily || 'sans-serif'
|
|
212
|
-
: axisFontFamilyToken;
|
|
213
|
-
const axisFont = `${axisFontSize} ${axisFontFamily}`;
|
|
214
|
-
let widest: number | null = null;
|
|
215
|
-
for (const label of labels) {
|
|
216
|
-
const labelWidth = measureTextWidth(label, axisFont);
|
|
217
|
-
if (labelWidth === null) {
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
if (widest === null || labelWidth > widest) {
|
|
221
|
-
widest = labelWidth;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return widest;
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// When an axis is hidden its gutter (Y = 50px for tick labels, X = 40px) is dead
|
|
228
|
-
// space that squeezes the plot into the centre. Collapse the tick-label gutter but
|
|
229
|
-
// keep a symmetric breathing-room inset so the edge bars (and their value labels)
|
|
230
|
-
// don't sit flush against the container edges. In horizontal orientation the
|
|
231
|
-
// visible Y axis carries category text, so its gutter grows to fit the widest
|
|
232
|
-
// label (never shrinking below the legacy 50px, capped at 45% of the width).
|
|
233
|
-
let dims = $derived(
|
|
234
|
-
computeChartDimensions(chartWidth, chartHeight, {
|
|
235
|
-
left: showYAxis
|
|
236
|
-
? isVertical
|
|
237
|
-
? 50
|
|
238
|
-
: computeHorizontalCategoryGutter(widestCategoryLabelWidth, chartWidth)
|
|
239
|
-
: 28,
|
|
240
|
-
right: 28,
|
|
241
|
-
bottom: showXAxis ? 40 : 8
|
|
242
|
-
})
|
|
243
|
-
);
|
|
244
|
-
|
|
245
226
|
// ── Effective highlighted index: merge declarative + imperative ─
|
|
246
227
|
|
|
247
228
|
/**
|
|
@@ -262,6 +243,18 @@
|
|
|
262
243
|
isNormalized && valueFormat == null ? (v: number) => `${formatNumber(v)}%` : format
|
|
263
244
|
);
|
|
264
245
|
|
|
246
|
+
let isStackedMode = $derived(isMulti && groupMode === 'stacked');
|
|
247
|
+
|
|
248
|
+
function getDisplayValue(bar: BarRect): string {
|
|
249
|
+
if (isNormalized && bar.normalizedValue != null) {
|
|
250
|
+
return normalizedFormat(bar.normalizedValue);
|
|
251
|
+
}
|
|
252
|
+
if (bar.isFloating && Array.isArray(bar.dataPoint.range)) {
|
|
253
|
+
return `${format(bar.dataPoint.range[0])} – ${format(bar.dataPoint.range[1])}`;
|
|
254
|
+
}
|
|
255
|
+
return format(bar.dataPoint.value);
|
|
256
|
+
}
|
|
257
|
+
|
|
265
258
|
// ── Y-extent: account for floating bars (A1-1) ────────────────
|
|
266
259
|
|
|
267
260
|
let yExtent = $derived.by<[number, number]>(() => {
|
|
@@ -273,13 +266,13 @@
|
|
|
273
266
|
return [0, 100];
|
|
274
267
|
}
|
|
275
268
|
const totalsPerLabel = labels.map((_, labelIndex) =>
|
|
276
|
-
|
|
269
|
+
visibleEntries.reduce((sum, { s }) => sum + Math.max(0, s.data[labelIndex]?.value ?? 0), 0)
|
|
277
270
|
);
|
|
278
271
|
return niceLinearDomain(0, Math.max(0, ...totalsPerLabel));
|
|
279
272
|
}
|
|
280
273
|
// Collect all individual values including floating-bar low/high endpoints
|
|
281
274
|
const all: number[] = [];
|
|
282
|
-
for (const s of
|
|
275
|
+
for (const { s } of visibleEntries) {
|
|
283
276
|
for (const d of s.data) {
|
|
284
277
|
if (Array.isArray(d.range)) {
|
|
285
278
|
all.push(d.range[0], d.range[1]);
|
|
@@ -294,6 +287,27 @@
|
|
|
294
287
|
return niceLinearDomain(Math.min(0, ...all), Math.max(0, ...all));
|
|
295
288
|
});
|
|
296
289
|
|
|
290
|
+
let valTickCount = $derived(
|
|
291
|
+
isVertical
|
|
292
|
+
? Math.max(2, Math.min(6, Math.floor(chartHeight / 70)))
|
|
293
|
+
: Math.max(2, Math.min(8, Math.floor(chartWidth / 90)))
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
let layout = $derived.by(() => {
|
|
297
|
+
const valFmt = isNormalized ? normalizedFormat : format;
|
|
298
|
+
const valTicks = computeLinearTicks(yExtent, valTickCount).map((t) => valFmt(t));
|
|
299
|
+
return computeAutoLayout({
|
|
300
|
+
width: chartWidth,
|
|
301
|
+
height: chartHeight,
|
|
302
|
+
yTickLabels: showYAxis ? (isVertical ? valTicks : labels) : [],
|
|
303
|
+
xTickLabels: showXAxis ? (isVertical ? labels : valTicks) : [],
|
|
304
|
+
hasYAxisLabel: Boolean(yAxisLabel) && showYAxis,
|
|
305
|
+
hasXAxisLabel: Boolean(xAxisLabel) && showXAxis,
|
|
306
|
+
base: { top: 20, left: showYAxis ? 50 : 28, right: 28, bottom: showXAxis ? 40 : 8 }
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
let dims = $derived(layout);
|
|
310
|
+
|
|
297
311
|
let catScale = $derived(
|
|
298
312
|
createBandScale(labels, isVertical ? [0, dims.innerWidth] : [0, dims.innerHeight], barPadding)
|
|
299
313
|
);
|
|
@@ -366,13 +380,13 @@
|
|
|
366
380
|
};
|
|
367
381
|
|
|
368
382
|
if (isMulti && groupMode === 'grouped') {
|
|
369
|
-
const subBand = catScale.bandwidth /
|
|
370
|
-
for (let
|
|
371
|
-
const s =
|
|
383
|
+
const subBand = catScale.bandwidth / Math.max(1, visibleEntries.length);
|
|
384
|
+
for (let vi = 0; vi < visibleEntries.length; vi++) {
|
|
385
|
+
const { s, si } = visibleEntries[vi];
|
|
372
386
|
const seriesFill: BarFill = s.color ?? getColor(si);
|
|
373
387
|
for (let pi = 0; pi < s.data.length; pi++) {
|
|
374
388
|
const d = s.data[pi];
|
|
375
|
-
const catPos = catScale(d.label) +
|
|
389
|
+
const catPos = catScale(d.label) + vi * subBand;
|
|
376
390
|
const barW = Math.max(1, subBand * 0.9);
|
|
377
391
|
const geom = barGeometry(d, catPos, barW);
|
|
378
392
|
const effectiveFill: BarFill = d.color ?? seriesFill;
|
|
@@ -383,11 +397,11 @@
|
|
|
383
397
|
}
|
|
384
398
|
} else if (isMulti && groupMode === 'stacked') {
|
|
385
399
|
const categoryTotals = labels.map((_, labelIndex) =>
|
|
386
|
-
|
|
400
|
+
visibleEntries.reduce((sum, { s }) => sum + Math.max(0, s.data[labelIndex]?.value ?? 0), 0)
|
|
387
401
|
);
|
|
388
402
|
const stackBase = new Array(labels.length).fill(0);
|
|
389
|
-
for (let
|
|
390
|
-
const s =
|
|
403
|
+
for (let vi = 0; vi < visibleEntries.length; vi++) {
|
|
404
|
+
const { s, si } = visibleEntries[vi];
|
|
391
405
|
const seriesFill: BarFill = s.color ?? getColor(si);
|
|
392
406
|
for (let pi = 0; pi < s.data.length; pi++) {
|
|
393
407
|
const d = s.data[pi];
|
|
@@ -461,6 +475,41 @@
|
|
|
461
475
|
return result;
|
|
462
476
|
});
|
|
463
477
|
|
|
478
|
+
let valueFont = $derived({ size: valueFontSize, weight: 600 });
|
|
479
|
+
|
|
480
|
+
// Highcharts label chain per bar (outside → justify inside → crop), then a
|
|
481
|
+
// global allowOverlap:false pass so neighbouring labels never collide.
|
|
482
|
+
let barLabels = $derived.by(() => {
|
|
483
|
+
if (!showValues) {
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
const plot = { width: dims.innerWidth, height: dims.innerHeight };
|
|
487
|
+
const placements = bars.map((bar) => {
|
|
488
|
+
const text = getDisplayValue(bar);
|
|
489
|
+
const size = measureText(text, valueFont);
|
|
490
|
+
const p = isStackedMode
|
|
491
|
+
? resolveInsideLabel({ bar, label: size })
|
|
492
|
+
: resolveEndLabel({
|
|
493
|
+
bar,
|
|
494
|
+
plot,
|
|
495
|
+
label: size,
|
|
496
|
+
orientation,
|
|
497
|
+
negative: !bar.isFloating && bar.dataPoint.value < 0
|
|
498
|
+
});
|
|
499
|
+
return { bar, text, size, p };
|
|
500
|
+
});
|
|
501
|
+
const mask = dropOverlapping(
|
|
502
|
+
placements.map(({ p, size }) => (p.placement === 'hidden' ? null : placedLabelRect(p, size)))
|
|
503
|
+
);
|
|
504
|
+
return placements.map((entry, i) => ({
|
|
505
|
+
...entry,
|
|
506
|
+
visible: mask[i] === true && entry.p.placement !== 'hidden'
|
|
507
|
+
}));
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const contrastOutline = (fill: string): string =>
|
|
511
|
+
getContrastColor(fill) === '#000000' ? '#ffffff' : '#000000';
|
|
512
|
+
|
|
464
513
|
// ── Defs: pattern and gradient fill resolution (A1-2 / A1-3) ──
|
|
465
514
|
|
|
466
515
|
/**
|
|
@@ -520,7 +569,8 @@
|
|
|
520
569
|
isMulti
|
|
521
570
|
? resolvedSeries.map((s, i) => ({
|
|
522
571
|
label: s.name,
|
|
523
|
-
color: fallbackColor(s.color ?? getColor(i), i)
|
|
572
|
+
color: fallbackColor(s.color ?? getColor(i), i),
|
|
573
|
+
hidden: hiddenSeries.has(i)
|
|
524
574
|
}))
|
|
525
575
|
: []
|
|
526
576
|
);
|
|
@@ -535,14 +585,13 @@
|
|
|
535
585
|
|
|
536
586
|
// ── Stacked bar path helper ────────────────────────────────────
|
|
537
587
|
|
|
538
|
-
let lastSeriesIndex = $derived(resolvedSeries.length - 1);
|
|
539
|
-
|
|
540
588
|
function stackedBarPath(bar: BarRect): string {
|
|
541
589
|
if (barRadius <= 0) {
|
|
542
590
|
return roundedRectPath(bar.x, bar.y, bar.width, bar.height, 0, 0, 0, 0);
|
|
543
591
|
}
|
|
544
|
-
const
|
|
545
|
-
const
|
|
592
|
+
const vi = visibleOrder.get(bar.si) ?? 0;
|
|
593
|
+
const isFirst = vi === 0;
|
|
594
|
+
const isLast = vi === visibleEntries.length - 1;
|
|
546
595
|
if (isVertical) {
|
|
547
596
|
const tl = isLast ? barRadius : 0;
|
|
548
597
|
const tr = isLast ? barRadius : 0;
|
|
@@ -597,26 +646,75 @@
|
|
|
597
646
|
|
|
598
647
|
// ── Interactions ───────────────────────────────────────────────
|
|
599
648
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
649
|
+
// Narrows an event's currentTarget to Element without an `as` cast (repo
|
|
650
|
+
// lint bans type assertions outside test files).
|
|
651
|
+
const targetElement = (e: Event): Element | null =>
|
|
652
|
+
e.currentTarget instanceof Element ? e.currentTarget : null;
|
|
653
|
+
|
|
654
|
+
function trackMouse(e: PointerEvent): void {
|
|
655
|
+
const position = pointerPositionIn(plotEl, e);
|
|
656
|
+
if (position !== null) {
|
|
657
|
+
mouseX = position.x;
|
|
658
|
+
mouseY = position.y;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Anchor from the live element rect (not SVG math) so legend offset and
|
|
663
|
+
// scrollable-mode scroll position are automatically accounted for.
|
|
664
|
+
function anchorFromElement(el: Element): TooltipAnchor | null {
|
|
665
|
+
if (plotEl === null) {
|
|
666
|
+
return null;
|
|
603
667
|
}
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
668
|
+
const r = el.getBoundingClientRect();
|
|
669
|
+
const c = plotEl.getBoundingClientRect();
|
|
670
|
+
return isVertical
|
|
671
|
+
? { x: r.left + r.width / 2 - c.left, y: r.top - c.top, side: 'top' }
|
|
672
|
+
: { x: r.right - c.left, y: r.top + r.height / 2 - c.top, side: 'right' };
|
|
607
673
|
}
|
|
608
674
|
|
|
609
|
-
function
|
|
675
|
+
function activateBar(target: Element, bar: BarRect): void {
|
|
610
676
|
hovered = { si: bar.si, pi: bar.pi };
|
|
611
|
-
|
|
677
|
+
anchor = anchorFromElement(target);
|
|
612
678
|
onbarhover?.({ index: bar.pi, dataPoint: bar.dataPoint });
|
|
613
679
|
}
|
|
614
680
|
|
|
615
|
-
function
|
|
681
|
+
function handleEnter(e: PointerEvent, bar: BarRect): void {
|
|
682
|
+
const el = targetElement(e);
|
|
683
|
+
if (el !== null) {
|
|
684
|
+
activateBar(el, bar);
|
|
685
|
+
}
|
|
686
|
+
trackMouse(e);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function handleFocus(e: FocusEvent, bar: BarRect): void {
|
|
690
|
+
const el = targetElement(e);
|
|
691
|
+
if (el !== null) {
|
|
692
|
+
activateBar(el, bar);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function handleLeave(): void {
|
|
616
697
|
hovered = null;
|
|
698
|
+
anchor = null;
|
|
617
699
|
onbarhover?.(null);
|
|
618
700
|
}
|
|
619
701
|
|
|
702
|
+
function handleKeydown(e: KeyboardEvent, bar: BarRect): void {
|
|
703
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
704
|
+
e.preventDefault();
|
|
705
|
+
handleClick(bar);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Touch taps have no pointerleave: dismiss when a pointerdown lands outside.
|
|
710
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
711
|
+
$effect(() => {
|
|
712
|
+
if (hovered === null) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
return dismissOnOutsidePointerDown(containerEl, handleLeave);
|
|
716
|
+
});
|
|
717
|
+
|
|
620
718
|
function handleClick(bar: BarRect) {
|
|
621
719
|
onbarclick?.({ index: bar.pi, dataPoint: bar.dataPoint });
|
|
622
720
|
}
|
|
@@ -626,18 +724,6 @@
|
|
|
626
724
|
? null
|
|
627
725
|
: (bars.find((b) => b.si === hovered!.si && b.pi === hovered!.pi) ?? null);
|
|
628
726
|
}
|
|
629
|
-
|
|
630
|
-
let isStackedMode = $derived(isMulti && groupMode === 'stacked');
|
|
631
|
-
|
|
632
|
-
function getDisplayValue(bar: BarRect): string {
|
|
633
|
-
if (isNormalized && bar.normalizedValue != null) {
|
|
634
|
-
return normalizedFormat(bar.normalizedValue);
|
|
635
|
-
}
|
|
636
|
-
if (bar.isFloating && Array.isArray(bar.dataPoint.range)) {
|
|
637
|
-
return `${format(bar.dataPoint.range[0])} – ${format(bar.dataPoint.range[1])}`;
|
|
638
|
-
}
|
|
639
|
-
return format(bar.dataPoint.value);
|
|
640
|
-
}
|
|
641
727
|
</script>
|
|
642
728
|
|
|
643
729
|
<div
|
|
@@ -648,80 +734,85 @@
|
|
|
648
734
|
{#if isEmpty && typeof empty === 'function'}
|
|
649
735
|
<div class="chart-empty">{@render empty()}</div>
|
|
650
736
|
{:else}
|
|
651
|
-
{#if isMulti && showLegend}
|
|
652
|
-
|
|
737
|
+
{#if isMulti && showLegend && (chartWidth === 0 || hideLegendBelow === 0 || chartWidth >= hideLegendBelow)}
|
|
738
|
+
{#if interactiveLegend}
|
|
739
|
+
<Legend items={legendItems} position="top" onToggle={toggleSeries} />
|
|
740
|
+
{:else}
|
|
741
|
+
<Legend items={legendItems} position="top" />
|
|
742
|
+
{/if}
|
|
653
743
|
{/if}
|
|
654
744
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
<
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
{#
|
|
678
|
-
{
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
{
|
|
723
|
-
|
|
724
|
-
|
|
745
|
+
<div class="chart-plot" bind:this={plotEl}>
|
|
746
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
747
|
+
<div
|
|
748
|
+
class="chart-scroll-area"
|
|
749
|
+
role="region"
|
|
750
|
+
aria-label={yAxisLabel ? `${yAxisLabel} bar chart` : 'Bar chart'}
|
|
751
|
+
tabindex={scrollable ? 0 : null}
|
|
752
|
+
style={scrollable
|
|
753
|
+
? `overflow-x: auto; -webkit-overflow-scrolling: touch; height: var(--barchart-scroll-area-height, auto);`
|
|
754
|
+
: ''}
|
|
755
|
+
>
|
|
756
|
+
<div style={scrollable ? `min-width: ${minScrollWidth}px;` : ''}>
|
|
757
|
+
<ChartContainer
|
|
758
|
+
bind:width={chartWidth}
|
|
759
|
+
bind:height={chartHeight}
|
|
760
|
+
{aspectRatio}
|
|
761
|
+
{maxHeight}
|
|
762
|
+
{minHeight}
|
|
763
|
+
>
|
|
764
|
+
<!-- A1-2 / A1-3: SVG <defs> for pattern and gradient fills -->
|
|
765
|
+
{#if defsEntries.length > 0}
|
|
766
|
+
<defs>
|
|
767
|
+
{#each defsEntries as entry (entry.id)}
|
|
768
|
+
{#if 'pattern' in entry.fill}
|
|
769
|
+
{@const pat = entry.fill.pattern}
|
|
770
|
+
{@const patSize = pat.size ?? 8}
|
|
771
|
+
{@const patColor = pat.color ?? entry.bar.color}
|
|
772
|
+
{@const patBg = pat.background ?? 'transparent'}
|
|
773
|
+
{@const patStrokeW = pat.strokeWidth ?? 1.5}
|
|
774
|
+
<pattern
|
|
775
|
+
id={entry.id}
|
|
776
|
+
patternUnits="userSpaceOnUse"
|
|
777
|
+
width={patSize}
|
|
778
|
+
height={patSize}
|
|
779
|
+
>
|
|
780
|
+
<rect width={patSize} height={patSize} fill={patBg} />
|
|
781
|
+
{#if pat.type === 'lines'}
|
|
782
|
+
<line
|
|
783
|
+
x1="0"
|
|
784
|
+
y1={patSize}
|
|
785
|
+
x2={patSize}
|
|
786
|
+
y2="0"
|
|
787
|
+
stroke={patColor}
|
|
788
|
+
stroke-width={patStrokeW}
|
|
789
|
+
/>
|
|
790
|
+
{:else if pat.type === 'crosshatch'}
|
|
791
|
+
<line
|
|
792
|
+
x1="0"
|
|
793
|
+
y1={patSize}
|
|
794
|
+
x2={patSize}
|
|
795
|
+
y2="0"
|
|
796
|
+
stroke={patColor}
|
|
797
|
+
stroke-width={patStrokeW}
|
|
798
|
+
/>
|
|
799
|
+
<line
|
|
800
|
+
x1="0"
|
|
801
|
+
y1="0"
|
|
802
|
+
x2={patSize}
|
|
803
|
+
y2={patSize}
|
|
804
|
+
stroke={patColor}
|
|
805
|
+
stroke-width={patStrokeW}
|
|
806
|
+
/>
|
|
807
|
+
{:else}
|
|
808
|
+
<!-- dots -->
|
|
809
|
+
<circle cx={patSize / 2} cy={patSize / 2} r={patStrokeW} fill={patColor} />
|
|
810
|
+
{/if}
|
|
811
|
+
</pattern>
|
|
812
|
+
{:else if 'gradient' in entry.fill}
|
|
813
|
+
{@const grad = entry.fill.gradient}
|
|
814
|
+
{@const isHoriz = grad.direction === 'horizontal'}
|
|
815
|
+
<!--
|
|
725
816
|
gradientUnits="userSpaceOnUse" is required here.
|
|
726
817
|
objectBoundingBox ratios are undefined on degenerate (zero-height)
|
|
727
818
|
path bounding boxes (stacked segments, Firefox renders black).
|
|
@@ -731,129 +822,163 @@
|
|
|
731
822
|
without any margin offset. This matches the AreaChart pattern
|
|
732
823
|
(feat/linechart-gradient ea5f794, lines 282-284).
|
|
733
824
|
-->
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
{/if}
|
|
754
|
-
|
|
755
|
-
<g transform="translate({dims.margin.left}, {dims.margin.top})">
|
|
756
|
-
{#if showYAxis}
|
|
757
|
-
<Axis
|
|
758
|
-
orientation="left"
|
|
759
|
-
scale={isVertical ? valScale : catScale}
|
|
760
|
-
{showGridlines}
|
|
761
|
-
gridlineLength={dims.innerWidth}
|
|
762
|
-
label={yAxisLabel}
|
|
763
|
-
/>
|
|
825
|
+
<linearGradient
|
|
826
|
+
id={entry.id}
|
|
827
|
+
x1={entry.bar.x}
|
|
828
|
+
y1={entry.bar.y}
|
|
829
|
+
x2={isHoriz ? entry.bar.x + entry.bar.width : entry.bar.x}
|
|
830
|
+
y2={isHoriz ? entry.bar.y : entry.bar.y + entry.bar.height}
|
|
831
|
+
gradientUnits="userSpaceOnUse"
|
|
832
|
+
>
|
|
833
|
+
{#each grad.stops as stop, stopIndex (`${stopIndex}-${stop.offset}`)}
|
|
834
|
+
<stop
|
|
835
|
+
offset="{stop.offset * 100}%"
|
|
836
|
+
stop-color={stop.color}
|
|
837
|
+
stop-opacity={stop.opacity ?? 1}
|
|
838
|
+
/>
|
|
839
|
+
{/each}
|
|
840
|
+
</linearGradient>
|
|
841
|
+
{/if}
|
|
842
|
+
{/each}
|
|
843
|
+
</defs>
|
|
764
844
|
{/if}
|
|
765
|
-
|
|
766
|
-
|
|
845
|
+
|
|
846
|
+
<g transform="translate({dims.margin.left}, {dims.margin.top})">
|
|
847
|
+
{#if showYAxis}
|
|
767
848
|
<Axis
|
|
768
|
-
orientation="
|
|
769
|
-
scale={isVertical ?
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
849
|
+
orientation="left"
|
|
850
|
+
scale={isVertical ? valScale : catScale}
|
|
851
|
+
tickCount={valTickCount}
|
|
852
|
+
{showGridlines}
|
|
853
|
+
gridlineLength={dims.innerWidth}
|
|
854
|
+
label={yAxisLabel}
|
|
773
855
|
/>
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
class:dimmed={(hovered !== null &&
|
|
788
|
-
(hovered.si !== bar.si || hovered.pi !== bar.pi)) ||
|
|
789
|
-
(effectiveHighlightedIndex !== null && effectiveHighlightedIndex !== bar.pi)}
|
|
790
|
-
d={stackedBarPath(bar)}
|
|
791
|
-
fill={barFillAttr(bar)}
|
|
792
|
-
aria-label="{bar.dataPoint.label}: {getDisplayValue(bar)}"
|
|
793
|
-
onmouseenter={(e) => handleEnter(e, bar)}
|
|
794
|
-
onmousemove={trackMouse}
|
|
795
|
-
onmouseleave={handleLeave}
|
|
796
|
-
onclick={() => handleClick(bar)}
|
|
856
|
+
{/if}
|
|
857
|
+
{#if showXAxis}
|
|
858
|
+
<g transform="translate(0, {dims.innerHeight})">
|
|
859
|
+
<Axis
|
|
860
|
+
orientation="bottom"
|
|
861
|
+
scale={isVertical ? catScale : valScale}
|
|
862
|
+
tickCount={valTickCount}
|
|
863
|
+
rotateTicks={layout.xRotate}
|
|
864
|
+
tickEvery={layout.xEvery}
|
|
865
|
+
labelOffset={layout.xLabelOffset}
|
|
866
|
+
showGridlines={!isVertical && showGridlines}
|
|
867
|
+
gridlineLength={dims.innerHeight}
|
|
868
|
+
label={xAxisLabel}
|
|
797
869
|
/>
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
870
|
+
</g>
|
|
871
|
+
{/if}
|
|
872
|
+
|
|
873
|
+
{#if !hideBarGraphics}
|
|
874
|
+
{#each bars as bar, i (i)}
|
|
875
|
+
{#if isStackedMode && barRadius > 0}
|
|
876
|
+
<path
|
|
877
|
+
class="bar"
|
|
878
|
+
class:hovered={hovered?.si === bar.si && hovered?.pi === bar.pi}
|
|
879
|
+
class:highlighted={effectiveHighlightedIndex !== null &&
|
|
880
|
+
effectiveHighlightedIndex === bar.pi}
|
|
881
|
+
class:dimmed={(hovered !== null &&
|
|
882
|
+
(hovered.si !== bar.si || hovered.pi !== bar.pi)) ||
|
|
883
|
+
(effectiveHighlightedIndex !== null &&
|
|
884
|
+
effectiveHighlightedIndex !== bar.pi)}
|
|
885
|
+
d={stackedBarPath(bar)}
|
|
886
|
+
fill={barFillAttr(bar)}
|
|
887
|
+
tabindex="0"
|
|
888
|
+
role="button"
|
|
889
|
+
aria-label="{bar.dataPoint.label}: {getDisplayValue(bar)}"
|
|
890
|
+
onpointerenter={(e) => handleEnter(e, bar)}
|
|
891
|
+
onpointermove={trackMouse}
|
|
892
|
+
onpointerleave={handleLeave}
|
|
893
|
+
onfocus={(e) => handleFocus(e, bar)}
|
|
894
|
+
onblur={handleLeave}
|
|
895
|
+
onkeydown={(e) => handleKeydown(e, bar)}
|
|
896
|
+
onclick={() => handleClick(bar)}
|
|
897
|
+
/>
|
|
898
|
+
{:else}
|
|
899
|
+
<rect
|
|
900
|
+
class="bar"
|
|
901
|
+
class:hovered={hovered?.si === bar.si && hovered?.pi === bar.pi}
|
|
902
|
+
class:highlighted={effectiveHighlightedIndex !== null &&
|
|
903
|
+
effectiveHighlightedIndex === bar.pi}
|
|
904
|
+
class:dimmed={(hovered !== null &&
|
|
905
|
+
(hovered.si !== bar.si || hovered.pi !== bar.pi)) ||
|
|
906
|
+
(effectiveHighlightedIndex !== null &&
|
|
907
|
+
effectiveHighlightedIndex !== bar.pi)}
|
|
908
|
+
x={bar.x}
|
|
909
|
+
y={bar.y}
|
|
910
|
+
width={bar.width}
|
|
911
|
+
height={bar.height}
|
|
912
|
+
rx={barRadius}
|
|
913
|
+
ry={barRadius}
|
|
914
|
+
fill={barFillAttr(bar)}
|
|
915
|
+
tabindex="0"
|
|
916
|
+
role="button"
|
|
917
|
+
aria-label="{bar.dataPoint.label}: {getDisplayValue(bar)}"
|
|
918
|
+
onpointerenter={(e) => handleEnter(e, bar)}
|
|
919
|
+
onpointermove={trackMouse}
|
|
920
|
+
onpointerleave={handleLeave}
|
|
921
|
+
onfocus={(e) => handleFocus(e, bar)}
|
|
922
|
+
onblur={handleLeave}
|
|
923
|
+
onkeydown={(e) => handleKeydown(e, bar)}
|
|
924
|
+
onclick={() => handleClick(bar)}
|
|
925
|
+
/>
|
|
926
|
+
{/if}
|
|
927
|
+
{/each}
|
|
928
|
+
{#each barLabels as bl, i (i)}
|
|
929
|
+
{#if bl.visible}
|
|
930
|
+
<text
|
|
931
|
+
class="bar-value"
|
|
932
|
+
class:bar-value-inside={bl.p.placement === 'inside'}
|
|
933
|
+
x={bl.p.x}
|
|
934
|
+
y={bl.p.y}
|
|
935
|
+
text-anchor={bl.p.textAnchor}
|
|
936
|
+
dominant-baseline={bl.p.dominantBaseline}
|
|
937
|
+
style={bl.p.placement === 'inside'
|
|
938
|
+
? `fill: ${getContrastColor(bl.bar.color)}; stroke: ${contrastOutline(bl.bar.color)};`
|
|
939
|
+
: ''}>{bl.text}</text
|
|
940
|
+
>
|
|
941
|
+
{/if}
|
|
942
|
+
{/each}
|
|
943
|
+
{/if}
|
|
944
|
+
|
|
945
|
+
<!-- A1-4: renderOverlay escape hatch — rendered after all bars -->
|
|
946
|
+
{#if typeof renderOverlay === 'function'}
|
|
947
|
+
{@render renderOverlay(overlayContext)}
|
|
948
|
+
{/if}
|
|
949
|
+
</g>
|
|
950
|
+
</ChartContainer>
|
|
951
|
+
</div>
|
|
844
952
|
</div>
|
|
845
|
-
</div>
|
|
846
953
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
{
|
|
852
|
-
|
|
954
|
+
{#if typeof tooltipSnippet === 'function'}
|
|
955
|
+
<ChartTooltip
|
|
956
|
+
data={tooltipData}
|
|
957
|
+
{mouseX}
|
|
958
|
+
{mouseY}
|
|
959
|
+
{anchor}
|
|
960
|
+
portal={tooltipPortal}
|
|
961
|
+
originEl={plotEl}
|
|
962
|
+
unstyled
|
|
963
|
+
>
|
|
964
|
+
{#snippet content()}
|
|
965
|
+
{@const hb = hoveredBar()}
|
|
966
|
+
{#if hb}
|
|
967
|
+
{@render tooltipSnippet(hb.dataPoint, hb.pi)}
|
|
968
|
+
{/if}
|
|
969
|
+
{/snippet}
|
|
970
|
+
</ChartTooltip>
|
|
971
|
+
{:else}
|
|
972
|
+
<ChartTooltip
|
|
973
|
+
data={tooltipData}
|
|
974
|
+
{mouseX}
|
|
975
|
+
{mouseY}
|
|
976
|
+
{anchor}
|
|
977
|
+
portal={tooltipPortal}
|
|
978
|
+
originEl={plotEl}
|
|
979
|
+
/>
|
|
853
980
|
{/if}
|
|
854
|
-
|
|
855
|
-
<ChartTooltip data={tooltipData} {mouseX} {mouseY} />
|
|
856
|
-
{/if}
|
|
981
|
+
</div>
|
|
857
982
|
{/if}
|
|
858
983
|
</div>
|
|
859
984
|
|
|
@@ -865,6 +990,9 @@
|
|
|
865
990
|
.chart-scroll-area {
|
|
866
991
|
width: 100%;
|
|
867
992
|
}
|
|
993
|
+
.chart-plot {
|
|
994
|
+
position: relative;
|
|
995
|
+
}
|
|
868
996
|
.bar {
|
|
869
997
|
transition: opacity var(--chart-transition-duration, 0.2s) ease;
|
|
870
998
|
cursor: pointer;
|
|
@@ -878,21 +1006,26 @@
|
|
|
878
1006
|
.bar.dimmed {
|
|
879
1007
|
opacity: var(--barchart-bar-dimmed-opacity, 0.3);
|
|
880
1008
|
}
|
|
1009
|
+
.bar:focus-visible {
|
|
1010
|
+
outline: 2px solid var(--chart-axis-label-color, light-dark(#333, #e5e7eb));
|
|
1011
|
+
outline-offset: 1px;
|
|
1012
|
+
}
|
|
881
1013
|
.bar-value {
|
|
882
|
-
fill: var(--barchart-value-color, #333);
|
|
1014
|
+
fill: var(--barchart-value-color, light-dark(#333, #e5e7eb));
|
|
883
1015
|
font-size: var(--barchart-value-font-size, 14px);
|
|
884
1016
|
font-weight: var(--barchart-value-font-weight, 600);
|
|
885
1017
|
font-family: var(--chart-font-family, inherit);
|
|
886
1018
|
pointer-events: none;
|
|
887
1019
|
}
|
|
888
|
-
.
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1020
|
+
.bar-value-inside {
|
|
1021
|
+
paint-order: stroke;
|
|
1022
|
+
stroke-width: 2px;
|
|
1023
|
+
stroke-opacity: 0.35;
|
|
1024
|
+
stroke-linejoin: round;
|
|
892
1025
|
}
|
|
893
1026
|
.chart-empty {
|
|
894
1027
|
padding: var(--chart-empty-padding, 32px 24px);
|
|
895
|
-
color: var(--chart-empty-color, #9ca3af);
|
|
1028
|
+
color: var(--chart-empty-color, light-dark(#9ca3af, #6b7280));
|
|
896
1029
|
text-align: center;
|
|
897
1030
|
}
|
|
898
1031
|
</style>
|