@juspay/svelte-ui-components 2.70.0 → 2.72.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.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { AreaChartProperties, AreaChartTooltipContext } from './properties';
3
+ import { onMount } from 'svelte';
3
4
  import ChartContainer from '../_chart/ChartContainer.svelte';
4
5
  import Axis from '../_chart/Axis.svelte';
5
6
  import ChartTooltip from '../_chart/ChartTooltip.svelte';
@@ -8,17 +9,24 @@
8
9
  import { computeChartDimensions, computeStackedValues } from '../_chart/geometry';
9
10
  import { linePath, areaPath } from '../_chart/paths';
10
11
  import { getColor } from '../_chart/colors';
11
- import { formatNumber } from '../_chart/format';
12
+ import { formatNumber, formatPercent } from '../_chart/format';
12
13
  import type { LegendItem, Point } from '../_chart/types';
13
14
 
14
15
  // ── Props ──────────────────────────────────────────────────────
15
16
 
17
+ // Per-instance ID for SVG gradient <linearGradient id> references. Initialised
18
+ // inside onMount so the value is only ever generated on the client — avoids an
19
+ // SSR hydration mismatch that would occur if Math.random() ran on both server
20
+ // and client and produced different strings.
21
+ let uid = $state('');
22
+
16
23
  let {
17
24
  series,
18
25
  curve = 'monotone',
19
26
  stacked = false,
20
27
  stackNormalize = false,
21
28
  fillOpacity = 0.3,
29
+ gradientFill = false,
22
30
  showDots = false,
23
31
  showLine = true,
24
32
  showValues = false,
@@ -51,6 +59,10 @@
51
59
  let mouseX = $state(0);
52
60
  let mouseY = $state(0);
53
61
 
62
+ onMount(() => {
63
+ uid = Math.random().toString(36).slice(2, 9);
64
+ });
65
+
54
66
  // ── Layout ─────────────────────────────────────────────────────
55
67
 
56
68
  let dims = $derived(computeChartDimensions(chartWidth, chartHeight));
@@ -169,8 +181,23 @@
169
181
  if (tooltipContext === null) {
170
182
  return null;
171
183
  }
184
+ const title = xTickFormat
185
+ ? xTickFormat(tooltipContext.x)
186
+ : `x: ${formatNumber(tooltipContext.x)}`;
187
+ if (stackNormalize) {
188
+ const pi = hovered?.pi ?? 0;
189
+ const columnTotal = series.reduce((sum, s) => sum + Math.max(0, s.data[pi]?.y ?? 0), 0);
190
+ return {
191
+ title,
192
+ items: tooltipContext.points.map((p) => ({
193
+ label: p.name,
194
+ value: formatPercent(Math.max(0, p.y), columnTotal),
195
+ color: p.color
196
+ }))
197
+ };
198
+ }
172
199
  return {
173
- title: `x: ${formatNumber(tooltipContext.x)}`,
200
+ title,
174
201
  items: tooltipContext.points.map((p) => ({
175
202
  label: p.name,
176
203
  value: formatNumber(p.y),
@@ -273,6 +300,37 @@
273
300
  {/if}
274
301
 
275
302
  <ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio}>
303
+ {#if gradientFill}
304
+ <!-- <defs> must be a direct child of <svg> (SVG root), not inside a transformed <g>.
305
+ gradientUnits="userSpaceOnUse" with y1/y2 in the inner coordinate space (0..innerHeight)
306
+ correctly spans the full chart height regardless of how thin each band is. -->
307
+ <defs>
308
+ {#each areas as area, si (si)}
309
+ <linearGradient
310
+ id="area-grad-{uid}-{si}"
311
+ x1="0"
312
+ y1="0"
313
+ x2="0"
314
+ y2={dims.innerHeight}
315
+ gradientUnits="userSpaceOnUse"
316
+ >
317
+ <!-- The gradient top stop is fillOpacity + 0.3 (clamped to 1), giving a richer
318
+ anchor at the top that fades to transparent at the bottom. This intentionally
319
+ exceeds the base fillOpacity so that gradient-fill areas appear more vivid
320
+ than their solid-fill counterparts (where fill-opacity equals fillOpacity). -->
321
+ <stop
322
+ offset="0%"
323
+ stop-color={area.color}
324
+ stop-opacity={Math.min(
325
+ (hovered?.si === si ? fillOpacity + 0.2 : fillOpacity) + 0.3,
326
+ 1
327
+ )}
328
+ />
329
+ <stop offset="100%" stop-color={area.color} stop-opacity={0} />
330
+ </linearGradient>
331
+ {/each}
332
+ </defs>
333
+ {/if}
276
334
  <g transform="translate({dims.margin.left}, {dims.margin.top})">
277
335
  {#if showYAxis}
278
336
  <Axis
@@ -295,8 +353,8 @@
295
353
  class="area-fill"
296
354
  class:dimmed={hovered !== null && hovered.si !== si}
297
355
  d={area.areaD}
298
- fill={area.color}
299
- fill-opacity={hovered?.si === si ? fillOpacity + 0.2 : fillOpacity}
356
+ fill={gradientFill ? `url(#area-grad-${uid}-${si})` : area.color}
357
+ fill-opacity={gradientFill ? 1 : hovered?.si === si ? fillOpacity + 0.2 : fillOpacity}
300
358
  />
301
359
  {#if showLine}
302
360
  <path
@@ -308,6 +366,16 @@
308
366
  fill="none"
309
367
  />
310
368
  {/if}
369
+ {#if area.points.length === 1 && !showDots}
370
+ <circle
371
+ class="single-point"
372
+ class:dimmed={hovered !== null && hovered.si !== si}
373
+ cx={area.points[0].x}
374
+ cy={area.points[0].y}
375
+ r={6}
376
+ fill={area.color}
377
+ />
378
+ {/if}
311
379
  {#if showDots}
312
380
  {#each area.points as point, pi (pi)}
313
381
  <circle
@@ -385,6 +453,12 @@
385
453
  .area-line.dimmed {
386
454
  opacity: var(--areachart-dimmed-opacity, 0.1);
387
455
  }
456
+ .single-point {
457
+ pointer-events: none;
458
+ }
459
+ .single-point.dimmed {
460
+ opacity: var(--areachart-dimmed-opacity, 0.1);
461
+ }
388
462
  .dot {
389
463
  transition:
390
464
  r var(--chart-transition-duration, 0.2s) ease,
@@ -400,9 +474,9 @@
400
474
  pointer-events: none;
401
475
  }
402
476
  .hover-line {
403
- stroke: var(--linechart-hover-line-color, #ccc);
477
+ stroke: var(--areachart-hover-line-color, var(--linechart-hover-line-color, #ccc));
404
478
  stroke-width: 1;
405
- stroke-dasharray: 4 4;
479
+ stroke-dasharray: var(--areachart-hover-line-dash, var(--linechart-hover-line-dash, 4 4));
406
480
  pointer-events: none;
407
481
  }
408
482
  .hover-overlay {
@@ -28,6 +28,7 @@ export type OptionalAreaChartProperties = {
28
28
  stacked?: boolean;
29
29
  stackNormalize?: boolean;
30
30
  fillOpacity?: number;
31
+ gradientFill?: boolean;
31
32
  showDots?: boolean;
32
33
  showLine?: boolean;
33
34
  showValues?: boolean;
@@ -30,6 +30,7 @@
30
30
  presetCheckmark = false,
31
31
  showDateInputs = false,
32
32
  showTimeSelection = false,
33
+ presetToggle = false,
33
34
  placeholder = 'Select date',
34
35
  dualMonth,
35
36
  timePicker,
@@ -388,6 +389,21 @@
388
389
  }
389
390
 
390
391
  function handlePreset(preset: DateRangePreset): void {
392
+ // presetToggle: clicking the already-selected preset deselects it and reverts the
393
+ // draft to the committed selection (mirrors openPicker). Lets a toggle-style preset
394
+ // such as "No Comparison" be switched back off without having to pick a calendar
395
+ // date, instead of being a one-way radio choice.
396
+ if (presetToggle && selectedPresetLabel === preset.label) {
397
+ draftStart = rangeStart;
398
+ draftEnd = rangeEnd;
399
+ draftValue = value;
400
+ selectedPresetLabel = committedPresetLabel;
401
+ const anchor = rangeStart !== null ? rangeStart : value !== null ? value : now;
402
+ leftYear = anchor.getFullYear();
403
+ leftMonth = anchor.getMonth();
404
+ calendarKey++;
405
+ return;
406
+ }
391
407
  // User explicitly picked a preset — clear the initial-preset display label
392
408
  activePresetLabel = null;
393
409
  const { start, end } = preset.getValue();
@@ -52,6 +52,13 @@ export type OptionalDateRangePickerProperties = {
52
52
  * Opt-in. Default: false.
53
53
  */
54
54
  showTimeSelection?: boolean;
55
+ /**
56
+ * Make presets toggle instead of being one-way: clicking the already-selected
57
+ * preset deselects it and reverts the draft to the committed selection. Lets a
58
+ * toggle-style preset such as "No Comparison" be switched back off without having
59
+ * to pick a calendar date. Opt-in. Default: false.
60
+ */
61
+ presetToggle?: boolean;
55
62
  /** Placeholder shown when no range is selected. */
56
63
  placeholder?: string;
57
64
  /** Show two months side by side. Defaults to true for range mode, false for single. */
@@ -1,21 +1,30 @@
1
1
  <script lang="ts">
2
2
  import type { LineChartProperties, LineChartTooltipContext } from './properties';
3
+ import { onMount } from 'svelte';
3
4
  import ChartContainer from '../_chart/ChartContainer.svelte';
4
5
  import Axis from '../_chart/Axis.svelte';
5
6
  import ChartTooltip from '../_chart/ChartTooltip.svelte';
6
7
  import Legend from '../_chart/Legend.svelte';
7
8
  import { createLinearScale, niceLinearDomain } from '../_chart/scales';
8
9
  import { computeChartDimensions } from '../_chart/geometry';
9
- import { linePath } from '../_chart/paths';
10
+ import { linePath, areaPath } from '../_chart/paths';
10
11
  import { getColor } from '../_chart/colors';
11
12
  import { formatNumber } from '../_chart/format';
12
13
  import type { LegendItem, Point } from '../_chart/types';
13
14
 
15
+ // Per-instance ID for SVG gradient <linearGradient id> references. Initialised
16
+ // inside onMount so the value is only ever generated on the client — avoids an
17
+ // SSR hydration mismatch that would occur if Math.random() ran on both server
18
+ // and client and produced different strings.
19
+ let uid = $state('');
20
+
14
21
  // ── Props ──────────────────────────────────────────────────────
15
22
 
16
23
  let {
17
24
  series,
18
25
  curve = 'monotone',
26
+ gradientFill = false,
27
+ fillOpacity = 0.3,
19
28
  showDots = true,
20
29
  showValues = false,
21
30
  dotRadius = 4,
@@ -48,6 +57,10 @@
48
57
  let mouseX = $state(0);
49
58
  let mouseY = $state(0);
50
59
 
60
+ onMount(() => {
61
+ uid = Math.random().toString(36).slice(2, 9);
62
+ });
63
+
51
64
  // ── Layout ─────────────────────────────────────────────────────
52
65
 
53
66
  let dims = $derived(computeChartDimensions(chartWidth, chartHeight));
@@ -82,7 +95,12 @@
82
95
  series.map((s, si) => {
83
96
  const color = s.color ?? getColor(si);
84
97
  const points: Point[] = s.data.map((d) => ({ x: xScale(d.x), y: yScale(d.y) }));
85
- return { color, points, path: linePath(points, curve) };
98
+ return {
99
+ color,
100
+ points,
101
+ path: linePath(points, curve),
102
+ areaD: areaPath(points, dims.innerHeight, curve)
103
+ };
86
104
  })
87
105
  );
88
106
 
@@ -123,7 +141,7 @@
123
141
  return null;
124
142
  }
125
143
  return {
126
- title: `x: ${formatNumber(tooltipContext.x)}`,
144
+ title: xTickFormat ? xTickFormat(tooltipContext.x) : `x: ${formatNumber(tooltipContext.x)}`,
127
145
  items: tooltipContext.points.map((p) => ({
128
146
  label: p.name,
129
147
  value: formatNumber(p.y),
@@ -227,6 +245,34 @@
227
245
  {/if}
228
246
 
229
247
  <ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio}>
248
+ {#if gradientFill}
249
+ <defs>
250
+ {#each lines as line, si (si)}
251
+ <linearGradient
252
+ id="line-grad-{uid}-{si}"
253
+ x1="0"
254
+ y1="0"
255
+ x2="0"
256
+ y2={dims.innerHeight}
257
+ gradientUnits="userSpaceOnUse"
258
+ >
259
+ <!-- The gradient top stop is fillOpacity + 0.3 (clamped to 1), giving a richer
260
+ anchor at the top that fades to transparent at the bottom. This intentionally
261
+ exceeds the base fillOpacity so that gradient-fill areas appear more vivid
262
+ than a flat solid-fill at fillOpacity alone. -->
263
+ <stop
264
+ offset="0%"
265
+ stop-color={line.color}
266
+ stop-opacity={Math.min(
267
+ (hovered?.si === si ? fillOpacity + 0.2 : fillOpacity) + 0.3,
268
+ 1
269
+ )}
270
+ />
271
+ <stop offset="100%" stop-color={line.color} stop-opacity={0} />
272
+ </linearGradient>
273
+ {/each}
274
+ </defs>
275
+ {/if}
230
276
  <g transform="translate({dims.margin.left}, {dims.margin.top})">
231
277
  {#if showYAxis}
232
278
  <Axis
@@ -245,6 +291,14 @@
245
291
  {/if}
246
292
 
247
293
  {#each lines as line, si (si)}
294
+ {#if gradientFill}
295
+ <path
296
+ class="line-area-fill"
297
+ class:dimmed={hovered !== null && hovered.si !== si}
298
+ d={line.areaD}
299
+ fill="url(#line-grad-{uid}-{si})"
300
+ />
301
+ {/if}
248
302
  <path
249
303
  class="line-path"
250
304
  class:dimmed={hovered !== null && hovered.si !== si}
@@ -253,6 +307,16 @@
253
307
  stroke-width={strokeWidth}
254
308
  fill="none"
255
309
  />
310
+ {#if line.points.length === 1 && !showDots}
311
+ <circle
312
+ class="single-point"
313
+ class:dimmed={hovered !== null && hovered.si !== si}
314
+ cx={line.points[0].x}
315
+ cy={line.points[0].y}
316
+ r={dotRadius * 1.5}
317
+ fill={line.color}
318
+ />
319
+ {/if}
256
320
  {#if showDots}
257
321
  {#each line.points as point, pi (pi)}
258
322
  <circle
@@ -313,6 +377,15 @@
313
377
  width: 100%;
314
378
  position: relative;
315
379
  }
380
+ .line-area-fill {
381
+ transition:
382
+ fill-opacity var(--chart-transition-duration, 0.2s) ease,
383
+ opacity var(--chart-transition-duration, 0.2s) ease;
384
+ pointer-events: none;
385
+ }
386
+ .line-area-fill.dimmed {
387
+ opacity: var(--linechart-dimmed-opacity, 0.2);
388
+ }
316
389
  .line-path {
317
390
  transition: opacity var(--chart-transition-duration, 0.2s) ease;
318
391
  stroke-linecap: round;
@@ -322,6 +395,12 @@
322
395
  .line-path.dimmed {
323
396
  opacity: var(--linechart-dimmed-opacity, 0.2);
324
397
  }
398
+ .single-point {
399
+ pointer-events: none;
400
+ }
401
+ .single-point.dimmed {
402
+ opacity: var(--linechart-dimmed-opacity, 0.2);
403
+ }
325
404
  .dot {
326
405
  transition:
327
406
  r var(--chart-transition-duration, 0.2s) ease,
@@ -25,6 +25,8 @@ export type MandatoryLineChartProperties = {
25
25
  };
26
26
  export type OptionalLineChartProperties = {
27
27
  curve?: CurveType;
28
+ gradientFill?: boolean;
29
+ fillOpacity?: number;
28
30
  showDots?: boolean;
29
31
  showValues?: boolean;
30
32
  dotRadius?: number;
@@ -118,6 +118,7 @@ export function linePath(points, curve = 'linear') {
118
118
  return points.length === 1 ? `M ${points[0].x} ${points[0].y}` : '';
119
119
  }
120
120
  switch (curve) {
121
+ case 'spline':
121
122
  case 'monotone':
122
123
  case 'natural':
123
124
  return monotonePath(points);
@@ -31,7 +31,7 @@ export type BandScale = {
31
31
  bandwidth: number;
32
32
  step: number;
33
33
  };
34
- export type CurveType = 'linear' | 'monotone' | 'step' | 'natural';
34
+ export type CurveType = 'linear' | 'monotone' | 'spline' | 'step' | 'natural';
35
35
  export type AxisOrientation = 'top' | 'right' | 'bottom' | 'left';
36
36
  export type PieSliceLayout = {
37
37
  index: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.70.0",
3
+ "version": "2.72.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",