@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.
@@ -7,12 +7,16 @@
7
7
  import Axis from '../_chart/Axis.svelte';
8
8
  import ChartTooltip from '../_chart/ChartTooltip.svelte';
9
9
  import Legend from '../_chart/Legend.svelte';
10
- import { createLinearScale, niceLinearDomain } from '../_chart/scales';
11
- import { computeChartDimensions } from '../_chart/geometry';
10
+ import { createLinearScale, niceLinearDomain, computeLinearTicks } from '../_chart/scales';
11
+ import { computeAutoLayout } from '../_chart/geometry';
12
12
  import { linePath, areaPath } from '../_chart/paths';
13
13
  import { getColor } from '../_chart/colors';
14
- import { formatNumber } from '../_chart/format';
15
- import type { LegendItem, Point } from '../_chart/types';
14
+ import { formatNumber, defaultTickFormat } from '../_chart/format';
15
+ import { measureText } from '../_chart/measure';
16
+ import { resolvePointLabels } from '../_chart/labels';
17
+ import type { LegendItem, Point, TooltipAnchor } from '../_chart/types';
18
+ import { pointerPositionIn, dismissOnOutsidePointerDown } from '../_chart/interactions';
19
+ import { SvelteSet } from 'svelte/reactivity';
16
20
 
17
21
  // Per-instance ID for SVG gradient <linearGradient id> references. Initialised
18
22
  // inside onMount so the value is only ever generated on the client — avoids an
@@ -49,6 +53,10 @@
49
53
  maxHeight = DEFAULT_CHART_MAX_HEIGHT,
50
54
  tooltipSnippet,
51
55
  empty,
56
+ sharedTooltip,
57
+ interactiveLegend = false,
58
+ hideLegendBelow = 360,
59
+ tooltipPortal = false,
52
60
  highlightedIndex = null,
53
61
  onChartReady,
54
62
  onpointclick,
@@ -60,6 +68,7 @@
60
68
  // ── State ──────────────────────────────────────────────────────
61
69
 
62
70
  let containerEl: HTMLDivElement | null = $state(null);
71
+ let plotEl: HTMLDivElement | null = $state(null);
63
72
  let chartWidth = $state(0);
64
73
  let chartHeight = $state(0);
65
74
  let hovered = $state<{ si: number; pi: number } | null>(null);
@@ -68,6 +77,19 @@
68
77
  let internalHighlight = $state<number | null>(null);
69
78
  let mouseX = $state(0);
70
79
  let mouseY = $state(0);
80
+ const hiddenSeries = new SvelteSet<number>();
81
+
82
+ function toggleSeries(index: number): void {
83
+ if (hiddenSeries.has(index)) {
84
+ hiddenSeries.delete(index);
85
+ } else {
86
+ hiddenSeries.add(index);
87
+ }
88
+ }
89
+
90
+ // Shared-mode switch: one tooltip listing every visible series at the
91
+ // hovered x. Defaults to true for multi-series charts.
92
+ let shared = $derived(sharedTooltip ?? series.length > 1);
71
93
 
72
94
  // Effective highlighted index: the declarative prop takes precedence when it is a non-null
73
95
  // number; otherwise the imperative API value (internalHighlight) is used. This lets callers
@@ -103,15 +125,15 @@
103
125
 
104
126
  // ── Layout ─────────────────────────────────────────────────────
105
127
 
106
- let dims = $derived(computeChartDimensions(chartWidth, chartHeight));
107
-
108
128
  let isEmpty = $derived(series.length === 0 || series.every((s) => s.data.length === 0));
109
129
 
110
130
  let xExtent = $derived.by<[number, number]>(() => {
111
131
  if (xDomain) {
112
132
  return xDomain;
113
133
  }
114
- const allX = series.flatMap((s) => s.data.map((d) => d.x));
134
+ const allX = series
135
+ .filter((_, si) => !hiddenSeries.has(si))
136
+ .flatMap((s) => s.data.map((d) => d.x));
115
137
  if (allX.length === 0) {
116
138
  return [0, 1];
117
139
  }
@@ -121,13 +143,54 @@
121
143
  if (yDomain) {
122
144
  return yDomain;
123
145
  }
124
- const allY = series.flatMap((s) => s.data.map((d) => d.y));
146
+ const allY = series
147
+ .filter((_, si) => !hiddenSeries.has(si))
148
+ .flatMap((s) => s.data.map((d) => d.y));
125
149
  if (allY.length === 0) {
126
150
  return [0, 1];
127
151
  }
128
152
  return niceLinearDomain(Math.min(0, ...allY), Math.max(...allY));
129
153
  });
130
154
 
155
+ // ── Category tick formatter ────────────────────────────────────
156
+
157
+ // When xAxisCategories is supplied we build a formatter that maps the numeric
158
+ // x-value (1-based index used in the data) to the corresponding category label.
159
+ let resolvedXTickFormat = $derived.by(() => {
160
+ if (xAxisCategories && xAxisCategories.length > 0) {
161
+ const categories = xAxisCategories;
162
+ return (value: number | string): string => {
163
+ const numericValue = typeof value === 'string' ? parseFloat(value) : value;
164
+ // x values are 1-based indices; category array is 0-based.
165
+ const categoryIndex = Math.round(numericValue) - 1;
166
+ return categories[categoryIndex] ?? String(value);
167
+ };
168
+ }
169
+ return xTickFormat;
170
+ });
171
+
172
+ let yTickCount = $derived(Math.max(2, Math.min(6, Math.floor(chartHeight / 70))));
173
+ let xTickCount = $derived(Math.max(2, Math.min(8, Math.floor(chartWidth / 90))));
174
+ // Category positions are whole numbers — fractional tick steps would repeat labels.
175
+ let xIntegerTicks = $derived(Boolean(xAxisCategories && xAxisCategories.length > 0));
176
+
177
+ let layout = $derived.by(() => {
178
+ const yFmt = yTickFormat ?? defaultTickFormat;
179
+ const xFmt = resolvedXTickFormat ?? defaultTickFormat;
180
+ return computeAutoLayout({
181
+ width: chartWidth,
182
+ height: chartHeight,
183
+ yTickLabels: showYAxis ? computeLinearTicks(yExtent, yTickCount).map((t) => yFmt(t)) : [],
184
+ xTickLabels: showXAxis
185
+ ? computeLinearTicks(xExtent, xTickCount, xIntegerTicks).map((t) => xFmt(t))
186
+ : [],
187
+ hasYAxisLabel: Boolean(yAxisLabel) && showYAxis,
188
+ hasXAxisLabel: Boolean(xAxisLabel) && showXAxis,
189
+ base: { top: 20, right: 20, bottom: showXAxis ? 40 : 8, left: showYAxis ? 50 : 20 }
190
+ });
191
+ });
192
+ let dims = $derived(layout);
193
+
131
194
  let xScale = $derived(createLinearScale(xExtent, [0, dims.innerWidth]));
132
195
  let yScale = $derived(createLinearScale(yExtent, [dims.innerHeight, 0]));
133
196
 
@@ -139,32 +202,20 @@
139
202
  color,
140
203
  points,
141
204
  path: linePath(points, curve),
142
- areaD: areaPath(points, dims.innerHeight, curve)
205
+ areaD: areaPath(points, dims.innerHeight, curve),
206
+ hidden: hiddenSeries.has(si)
143
207
  };
144
208
  })
145
209
  );
146
210
 
147
211
  let legendItems = $derived<LegendItem[]>(
148
- series.map((s, i) => ({ label: s.name, color: s.color ?? getColor(i) }))
212
+ series.map((s, i) => ({
213
+ label: s.name,
214
+ color: s.color ?? getColor(i),
215
+ hidden: hiddenSeries.has(i)
216
+ }))
149
217
  );
150
218
 
151
- // ── Category tick formatter ────────────────────────────────────
152
-
153
- // When xAxisCategories is supplied we build a formatter that maps the numeric
154
- // x-value (1-based index used in the data) to the corresponding category label.
155
- let resolvedXTickFormat = $derived.by(() => {
156
- if (xAxisCategories && xAxisCategories.length > 0) {
157
- const categories = xAxisCategories;
158
- return (value: number | string): string => {
159
- const numericValue = typeof value === 'string' ? parseFloat(value) : value;
160
- // x values are 1-based indices; category array is 0-based.
161
- const categoryIndex = Math.round(numericValue) - 1;
162
- return categories[categoryIndex] ?? String(value);
163
- };
164
- }
165
- return xTickFormat;
166
- });
167
-
168
219
  // ── Tooltip ────────────────────────────────────────────────────
169
220
 
170
221
  let hoveredPoint = $derived(
@@ -181,8 +232,11 @@
181
232
  if (effectiveHighlight === null) {
182
233
  return null;
183
234
  }
184
- // Use the first series that has a point at this index.
235
+ // Use the first VISIBLE series that has a point at this index.
185
236
  for (const line of lines) {
237
+ if (line.hidden) {
238
+ continue;
239
+ }
186
240
  const point = line.points[effectiveHighlight];
187
241
  if (point) {
188
242
  return point.x;
@@ -220,19 +274,76 @@
220
274
  : `x: ${formatNumber(tooltipContext.x)}`;
221
275
  return {
222
276
  title: xLabel,
223
- items: tooltipContext.points.map((p) => ({
224
- label: p.name,
225
- value: formatNumber(p.y),
226
- color: p.color
227
- }))
277
+ items: tooltipContext.points
278
+ .map((p, si) => ({ p, si }))
279
+ .filter(
280
+ ({ si }) =>
281
+ !hiddenSeries.has(si) &&
282
+ (shared || si === hovered?.si) &&
283
+ series[si]?.data[hovered!.pi] != null
284
+ )
285
+ .map(({ p }) => ({ label: p.name, value: formatNumber(p.y), color: p.color }))
228
286
  };
229
287
  });
230
288
 
289
+ // Highcharts hover halo: a translucent ring behind the active marker(s).
290
+ let haloPoints = $derived.by(() => {
291
+ if (hovered === null) {
292
+ return [];
293
+ }
294
+ return lines.flatMap((line, si) => {
295
+ if (line.hidden || (!shared && si !== hovered!.si)) {
296
+ return [];
297
+ }
298
+ const p = line.points[hovered!.pi];
299
+ return p ? [{ x: p.x, y: p.y, color: line.color }] : [];
300
+ });
301
+ });
302
+
303
+ let anchor = $derived.by<TooltipAnchor | null>(() => {
304
+ if (hovered === null || haloPoints.length === 0) {
305
+ return null;
306
+ }
307
+ return {
308
+ x: haloPoints[0].x + dims.margin.left,
309
+ y: Math.min(...haloPoints.map((p) => p.y)) + dims.margin.top,
310
+ side: 'top'
311
+ };
312
+ });
313
+
314
+ // ── Point labels ───────────────────────────────────────────────
315
+
316
+ let pointLabelPlacements = $derived.by(() => {
317
+ if (!showValues) {
318
+ return [];
319
+ }
320
+ const plot = { width: dims.innerWidth, height: dims.innerHeight };
321
+ const font = { size: 11 };
322
+ return lines.map((line, si) =>
323
+ line.hidden
324
+ ? []
325
+ : resolvePointLabels({
326
+ points: line.points,
327
+ labels: series[si].data.map((d) => measureText(formatNumber(d.y), font)),
328
+ plot
329
+ })
330
+ );
331
+ });
332
+
231
333
  // ── Highlight dim logic ────────────────────────────────────────
232
334
 
233
335
  // A point index is "dimmed" when the highlight system is active (hover or
234
336
  // imperative highlight) and the point is not the active one.
235
337
  const isDotDimmed = (si: number, pi: number): boolean => {
338
+ if (shared) {
339
+ if (hovered !== null) {
340
+ return hovered.pi !== pi;
341
+ }
342
+ if (effectiveHighlight !== null) {
343
+ return pi !== effectiveHighlight;
344
+ }
345
+ return false;
346
+ }
236
347
  // Hover interaction takes precedence over imperative highlight.
237
348
  if (hovered !== null) {
238
349
  return hovered.si !== si || hovered.pi !== pi;
@@ -244,6 +355,9 @@
244
355
  };
245
356
 
246
357
  const isLineDimmed = (si: number): boolean => {
358
+ if (shared) {
359
+ return false;
360
+ }
247
361
  if (hovered !== null) {
248
362
  return hovered.si !== si;
249
363
  }
@@ -263,22 +377,31 @@
263
377
 
264
378
  // ── Interactions ───────────────────────────────────────────────
265
379
 
266
- const trackMouse = (e: MouseEvent): void => {
267
- if (containerEl === null) {
268
- return;
380
+ const trackMouse = (e: PointerEvent): void => {
381
+ const position = pointerPositionIn(plotEl, e);
382
+ if (position !== null) {
383
+ mouseX = position.x;
384
+ mouseY = position.y;
269
385
  }
270
- const rect = containerEl.getBoundingClientRect();
271
- mouseX = e.clientX - rect.left;
272
- mouseY = e.clientY - rect.top;
273
386
  };
274
387
 
275
388
  const findNearest = (plotX: number, plotY: number): { si: number; pi: number } | null => {
276
389
  if (series.length === 0) {
277
390
  return null;
278
391
  }
279
- // Use the longest series as the index reference so hover still works when
280
- // the first series is empty or shorter than the others.
281
- const reference = series.reduce((a, b) => (b.data.length > a.data.length ? b : a)).data;
392
+ // Use the longest VISIBLE series as the index reference so hover still works
393
+ // when the first series is empty or shorter than the others, and never
394
+ // anchors to a series the user has toggled off via the legend.
395
+ const visibleEntries = series
396
+ .map((s, si) => ({ s, si }))
397
+ .filter((entry) => !hiddenSeries.has(entry.si));
398
+ if (visibleEntries.length === 0) {
399
+ return null;
400
+ }
401
+ const referenceEntry = visibleEntries.reduce((a, b) =>
402
+ b.s.data.length > a.s.data.length ? b : a
403
+ );
404
+ const reference = referenceEntry.s.data;
282
405
  if (reference.length === 0) {
283
406
  return null;
284
407
  }
@@ -292,9 +415,14 @@
292
415
  nearestPi = i;
293
416
  }
294
417
  }
295
- let nearestSi = 0;
418
+ // The reference series is visible and owns nearestPi by construction, so
419
+ // it's a safe default if no other visible series' point is nearer in y.
420
+ let nearestSi = referenceEntry.si;
296
421
  let nearestYDist = Infinity;
297
422
  for (let si = 0; si < series.length; si++) {
423
+ if (hiddenSeries.has(si)) {
424
+ continue;
425
+ }
298
426
  const p = series[si].data[nearestPi];
299
427
  if (!p) {
300
428
  continue;
@@ -309,7 +437,7 @@
309
437
  return { si: nearestSi, pi: nearestPi };
310
438
  };
311
439
 
312
- const handleOverlayMove = (e: MouseEvent): void => {
440
+ const handleOverlayMove = (e: PointerEvent): void => {
313
441
  trackMouse(e);
314
442
  const plotX = mouseX - dims.margin.left;
315
443
  const plotY = mouseY - dims.margin.top;
@@ -341,6 +469,15 @@
341
469
  onpointclick?.({ seriesIndex: hovered.si, pointIndex: hovered.pi, point });
342
470
  }
343
471
  };
472
+
473
+ // Touch taps have no pointerleave: dismiss when a pointerdown lands outside.
474
+ // eslint-disable-next-line no-restricted-syntax
475
+ $effect(() => {
476
+ if (hovered === null) {
477
+ return;
478
+ }
479
+ return dismissOnOutsidePointerDown(containerEl, handleLeave);
480
+ });
344
481
  </script>
345
482
 
346
483
  <div
@@ -351,177 +488,224 @@
351
488
  {#if isEmpty && typeof empty === 'function'}
352
489
  <div class="chart-empty">{@render empty()}</div>
353
490
  {:else}
354
- {#if showLegend && series.length > 1}
355
- <Legend items={legendItems} position="top" />
491
+ {#if showLegend && series.length > 1 && (chartWidth === 0 || hideLegendBelow === 0 || chartWidth >= hideLegendBelow)}
492
+ {#if interactiveLegend}
493
+ <Legend items={legendItems} position="top" onToggle={toggleSeries} />
494
+ {:else}
495
+ <Legend items={legendItems} position="top" />
496
+ {/if}
356
497
  {/if}
357
498
 
358
- <ChartContainer
359
- bind:width={chartWidth}
360
- bind:height={chartHeight}
361
- {aspectRatio}
362
- {minHeight}
363
- {maxHeight}
364
- >
365
- {#if gradientFill || showArea}
366
- <defs>
367
- {#each lines as line, si (si)}
368
- {#if gradientFill}
369
- <linearGradient
370
- id="line-grad-{uid}-{si}"
371
- x1="0"
372
- y1="0"
373
- x2="0"
374
- y2={dims.innerHeight}
375
- gradientUnits="userSpaceOnUse"
376
- >
377
- <!-- The gradient top stop is fillOpacity + 0.3 (clamped to 1), giving a richer
499
+ <div class="chart-plot" bind:this={plotEl}>
500
+ <ChartContainer
501
+ bind:width={chartWidth}
502
+ bind:height={chartHeight}
503
+ {aspectRatio}
504
+ {minHeight}
505
+ {maxHeight}
506
+ >
507
+ {#if gradientFill || showArea}
508
+ <defs>
509
+ {#each lines as line, si (si)}
510
+ {#if gradientFill}
511
+ <linearGradient
512
+ id="line-grad-{uid}-{si}"
513
+ x1="0"
514
+ y1="0"
515
+ x2="0"
516
+ y2={dims.innerHeight}
517
+ gradientUnits="userSpaceOnUse"
518
+ >
519
+ <!-- The gradient top stop is fillOpacity + 0.3 (clamped to 1), giving a richer
378
520
  anchor at the top that fades to transparent at the bottom. This intentionally
379
521
  exceeds the base fillOpacity so that gradient-fill areas appear more vivid
380
522
  than a flat solid-fill at fillOpacity alone. -->
381
- <stop
382
- offset="0%"
383
- stop-color={line.color}
384
- stop-opacity={Math.min(
385
- (hovered?.si === si ? fillOpacity + 0.2 : fillOpacity) + 0.3,
386
- 1
387
- )}
523
+ <stop
524
+ offset="0%"
525
+ stop-color={line.color}
526
+ stop-opacity={Math.min(
527
+ (hovered?.si === si ? fillOpacity + 0.2 : fillOpacity) + 0.3,
528
+ 1
529
+ )}
530
+ />
531
+ <stop offset="100%" stop-color={line.color} stop-opacity={0} />
532
+ </linearGradient>
533
+ {/if}
534
+ {#if showArea}
535
+ <linearGradient
536
+ id="line-area-{uid}-{si}"
537
+ x1="0"
538
+ y1="0"
539
+ x2="0"
540
+ y2={dims.innerHeight}
541
+ gradientUnits="userSpaceOnUse"
542
+ >
543
+ <stop
544
+ offset="0%"
545
+ stop-color={areaGradient ? areaGradient.from : line.color}
546
+ stop-opacity={areaGradient ? 1 : 0.35}
547
+ />
548
+ <stop
549
+ offset="100%"
550
+ stop-color={areaGradient ? areaGradient.to : line.color}
551
+ stop-opacity={areaGradient ? 1 : 0}
552
+ />
553
+ </linearGradient>
554
+ {/if}
555
+ {/each}
556
+ </defs>
557
+ {/if}
558
+ <g transform="translate({dims.margin.left}, {dims.margin.top})">
559
+ {#if showYAxis}
560
+ <Axis
561
+ orientation="left"
562
+ scale={yScale}
563
+ tickCount={yTickCount}
564
+ {showGridlines}
565
+ gridlineLength={dims.innerWidth}
566
+ label={yAxisLabel}
567
+ tickFormat={yTickFormat}
568
+ />
569
+ {/if}
570
+ {#if showXAxis}
571
+ <g transform="translate(0, {dims.innerHeight})">
572
+ <Axis
573
+ orientation="bottom"
574
+ scale={xScale}
575
+ tickCount={xTickCount}
576
+ integerTicks={xIntegerTicks}
577
+ rotateTicks={layout.xRotate}
578
+ tickEvery={layout.xEvery}
579
+ labelOffset={layout.xLabelOffset}
580
+ label={xAxisLabel}
581
+ tickFormat={resolvedXTickFormat}
582
+ />
583
+ </g>
584
+ {/if}
585
+
586
+ {#each haloPoints as hp, i (i)}
587
+ <circle class="dot-halo" cx={hp.x} cy={hp.y} r={dotRadius + 6} fill={hp.color} />
588
+ {/each}
589
+
590
+ {#each lines as line, si (si)}
591
+ {#if !line.hidden}
592
+ {#if showArea}
593
+ <path
594
+ class="line-area-fill"
595
+ class:dimmed={isLineDimmed(si)}
596
+ d={line.areaD}
597
+ fill="url(#line-area-{uid}-{si})"
388
598
  />
389
- <stop offset="100%" stop-color={line.color} stop-opacity={0} />
390
- </linearGradient>
391
- {/if}
392
- {#if showArea}
393
- <linearGradient
394
- id="line-area-{uid}-{si}"
395
- x1="0"
396
- y1="0"
397
- x2="0"
398
- y2={dims.innerHeight}
399
- gradientUnits="userSpaceOnUse"
400
- >
401
- <stop
402
- offset="0%"
403
- stop-color={areaGradient ? areaGradient.from : line.color}
404
- stop-opacity={areaGradient ? 1 : 0.35}
599
+ {:else if gradientFill}
600
+ <path
601
+ class="line-area-fill"
602
+ class:dimmed={hovered !== null && hovered.si !== si}
603
+ d={line.areaD}
604
+ fill="url(#line-grad-{uid}-{si})"
405
605
  />
406
- <stop
407
- offset="100%"
408
- stop-color={areaGradient ? areaGradient.to : line.color}
409
- stop-opacity={areaGradient ? 1 : 0}
606
+ {/if}
607
+ <path
608
+ class="line-path"
609
+ class:dimmed={isLineDimmed(si)}
610
+ d={line.path}
611
+ stroke={line.color}
612
+ stroke-width={strokeWidth}
613
+ fill="none"
614
+ />
615
+ {#if line.points.length === 1 && !showDots}
616
+ <circle
617
+ class="single-point"
618
+ class:dimmed={isLineDimmed(si)}
619
+ cx={line.points[0].x}
620
+ cy={line.points[0].y}
621
+ r={dotRadius * 1.5}
622
+ fill={line.color}
410
623
  />
411
- </linearGradient>
624
+ {/if}
625
+ {#if showDots}
626
+ {#each line.points as point, pi (pi)}
627
+ <circle
628
+ class="dot"
629
+ class:dimmed={isDotDimmed(si, pi)}
630
+ class:highlighted={isHighlightedDot(si, pi)}
631
+ cx={point.x}
632
+ cy={point.y}
633
+ r={isHighlightedDot(si, pi) ? dotRadius * 1.5 : dotRadius}
634
+ fill={line.color}
635
+ />
636
+ {/each}
637
+ {/if}
638
+ {#if showValues && pointLabelPlacements[si]}
639
+ {#each line.points as _point, pi (pi)}
640
+ {@const pl = pointLabelPlacements[si][pi]}
641
+ {#if pl?.visible}
642
+ <text
643
+ class="point-value"
644
+ x={pl.x}
645
+ y={pl.y}
646
+ text-anchor="middle"
647
+ dominant-baseline={pl.dominantBaseline}
648
+ >{formatNumber(series[si].data[pi].y)}</text
649
+ >
650
+ {/if}
651
+ {/each}
652
+ {/if}
412
653
  {/if}
413
654
  {/each}
414
- </defs>
415
- {/if}
416
- <g transform="translate({dims.margin.left}, {dims.margin.top})">
417
- {#if showYAxis}
418
- <Axis
419
- orientation="left"
420
- scale={yScale}
421
- {showGridlines}
422
- gridlineLength={dims.innerWidth}
423
- label={yAxisLabel}
424
- tickFormat={yTickFormat}
425
- />
426
- {/if}
427
- {#if showXAxis}
428
- <g transform="translate(0, {dims.innerHeight})">
429
- <Axis
430
- orientation="bottom"
431
- scale={xScale}
432
- label={xAxisLabel}
433
- tickFormat={resolvedXTickFormat}
434
- />
435
- </g>
436
- {/if}
437
655
 
438
- {#each lines as line, si (si)}
439
- {#if showArea}
440
- <path
441
- class="line-area-fill"
442
- class:dimmed={isLineDimmed(si)}
443
- d={line.areaD}
444
- fill="url(#line-area-{uid}-{si})"
656
+ {#if activeLineX !== null}
657
+ <line
658
+ class="hover-line"
659
+ x1={activeLineX}
660
+ x2={activeLineX}
661
+ y1={0}
662
+ y2={dims.innerHeight}
445
663
  />
446
- {:else if gradientFill}
447
- <path
448
- class="line-area-fill"
449
- class:dimmed={hovered !== null && hovered.si !== si}
450
- d={line.areaD}
451
- fill="url(#line-grad-{uid}-{si})"
452
- />
453
- {/if}
454
- <path
455
- class="line-path"
456
- class:dimmed={isLineDimmed(si)}
457
- d={line.path}
458
- stroke={line.color}
459
- stroke-width={strokeWidth}
460
- fill="none"
461
- />
462
- {#if line.points.length === 1 && !showDots}
463
- <circle
464
- class="single-point"
465
- class:dimmed={isLineDimmed(si)}
466
- cx={line.points[0].x}
467
- cy={line.points[0].y}
468
- r={dotRadius * 1.5}
469
- fill={line.color}
470
- />
471
- {/if}
472
- {#if showDots}
473
- {#each line.points as point, pi (pi)}
474
- <circle
475
- class="dot"
476
- class:dimmed={isDotDimmed(si, pi)}
477
- class:highlighted={isHighlightedDot(si, pi)}
478
- cx={point.x}
479
- cy={point.y}
480
- r={isHighlightedDot(si, pi) ? dotRadius * 1.5 : dotRadius}
481
- fill={line.color}
482
- />
483
- {/each}
484
664
  {/if}
485
- {#if showValues}
486
- {#each line.points as point, pi (pi)}
487
- <text
488
- class="point-value"
489
- x={point.x}
490
- y={point.y - 8}
491
- text-anchor="middle"
492
- dominant-baseline="auto">{formatNumber(series[si].data[pi].y)}</text
493
- >
494
- {/each}
495
- {/if}
496
- {/each}
497
-
498
- {#if activeLineX !== null}
499
- <line class="hover-line" x1={activeLineX} x2={activeLineX} y1={0} y2={dims.innerHeight} />
500
- {/if}
501
665
 
502
- <!-- svelte-ignore a11y_no_static_element_interactions -->
503
- <!-- svelte-ignore a11y_click_events_have_key_events -->
504
- <rect
505
- class="hover-overlay"
506
- x={0}
507
- y={0}
508
- width={dims.innerWidth}
509
- height={dims.innerHeight}
510
- fill="transparent"
511
- onmousemove={handleOverlayMove}
512
- onmouseleave={handleLeave}
513
- onclick={handleClick}
666
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
667
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
668
+ <rect
669
+ class="hover-overlay"
670
+ x={0}
671
+ y={0}
672
+ width={dims.innerWidth}
673
+ height={dims.innerHeight}
674
+ fill="transparent"
675
+ onpointermove={handleOverlayMove}
676
+ onpointerleave={handleLeave}
677
+ onclick={handleClick}
678
+ />
679
+ </g>
680
+ </ChartContainer>
681
+
682
+ {#if typeof tooltipSnippet === 'function'}
683
+ <ChartTooltip
684
+ data={tooltipData}
685
+ {mouseX}
686
+ {mouseY}
687
+ {anchor}
688
+ portal={tooltipPortal}
689
+ originEl={plotEl}
690
+ unstyled
691
+ >
692
+ {#snippet content()}
693
+ {#if tooltipContext !== null}
694
+ {@render tooltipSnippet(tooltipContext)}
695
+ {/if}
696
+ {/snippet}
697
+ </ChartTooltip>
698
+ {:else}
699
+ <ChartTooltip
700
+ data={tooltipData}
701
+ {mouseX}
702
+ {mouseY}
703
+ {anchor}
704
+ portal={tooltipPortal}
705
+ originEl={plotEl}
514
706
  />
515
- </g>
516
- </ChartContainer>
517
-
518
- {#if typeof tooltipSnippet === 'function' && tooltipContext !== null}
519
- <div class="chart-tooltip-slot" style="left: {mouseX + 12}px; top: {mouseY - 12}px;">
520
- {@render tooltipSnippet(tooltipContext)}
521
- </div>
522
- {:else}
523
- <ChartTooltip data={tooltipData} {mouseX} {mouseY} />
524
- {/if}
707
+ {/if}
708
+ </div>
525
709
  {/if}
526
710
  </div>
527
711
 
@@ -530,6 +714,9 @@
530
714
  width: 100%;
531
715
  position: relative;
532
716
  }
717
+ .chart-plot {
718
+ position: relative;
719
+ }
533
720
  .line-area-fill {
534
721
  transition:
535
722
  fill-opacity var(--chart-transition-duration, 0.2s) ease,
@@ -558,7 +745,7 @@
558
745
  transition:
559
746
  r var(--chart-transition-duration, 0.2s) ease,
560
747
  opacity var(--chart-transition-duration, 0.2s) ease;
561
- stroke: var(--chart-background, #fff);
748
+ stroke: var(--chart-dot-stroke, light-dark(#fff, #111827));
562
749
  stroke-width: 2;
563
750
  pointer-events: none;
564
751
  }
@@ -566,17 +753,21 @@
566
753
  opacity: var(--linechart-dimmed-opacity, 0.2);
567
754
  }
568
755
  .dot.highlighted {
569
- stroke: var(--linechart-highlight-ring-color, #fff);
756
+ stroke: var(--linechart-highlight-ring-color, light-dark(#fff, #111827));
570
757
  stroke-width: var(--linechart-highlight-ring-width, 2.5);
571
758
  }
759
+ .dot-halo {
760
+ opacity: 0.25;
761
+ pointer-events: none;
762
+ }
572
763
  .point-value {
573
- fill: var(--linechart-value-color, #333);
764
+ fill: var(--linechart-value-color, light-dark(#333, #e5e7eb));
574
765
  font-size: var(--linechart-value-font-size, 11px);
575
766
  font-family: var(--chart-font-family, inherit);
576
767
  pointer-events: none;
577
768
  }
578
769
  .hover-line {
579
- stroke: var(--linechart-hover-line-color, #ccc);
770
+ stroke: var(--linechart-hover-line-color, light-dark(#ccc, #4b5563));
580
771
  stroke-width: 1;
581
772
  stroke-dasharray: var(--linechart-hover-line-dash, 4 4);
582
773
  pointer-events: none;
@@ -584,14 +775,9 @@
584
775
  .hover-overlay {
585
776
  cursor: crosshair;
586
777
  }
587
- .chart-tooltip-slot {
588
- position: absolute;
589
- z-index: 10;
590
- pointer-events: none;
591
- }
592
778
  .chart-empty {
593
779
  padding: var(--chart-empty-padding, 32px 24px);
594
- color: var(--chart-empty-color, #9ca3af);
780
+ color: var(--chart-empty-color, light-dark(#9ca3af, #6b7280));
595
781
  text-align: center;
596
782
  }
597
783
  </style>