@juspay/svelte-ui-components 2.74.0 → 2.76.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.
Files changed (31) hide show
  1. package/dist/BarChart/BarChart.svelte +163 -48
  2. package/dist/BarChart/properties.d.ts +41 -0
  3. package/dist/DeltaIndicator/DeltaIndicator.svelte +87 -0
  4. package/dist/DeltaIndicator/DeltaIndicator.svelte.d.ts +4 -0
  5. package/dist/DeltaIndicator/properties.d.ts +26 -0
  6. package/dist/DeltaIndicator/properties.js +1 -0
  7. package/dist/DualAxisBarChart/DualAxisBarChart.svelte +521 -0
  8. package/dist/DualAxisBarChart/DualAxisBarChart.svelte.d.ts +4 -0
  9. package/dist/DualAxisBarChart/properties.d.ts +108 -0
  10. package/dist/DualAxisBarChart/properties.js +1 -0
  11. package/dist/FunnelChart/FunnelChart.svelte +335 -0
  12. package/dist/FunnelChart/FunnelChart.svelte.d.ts +4 -0
  13. package/dist/FunnelChart/properties.d.ts +69 -0
  14. package/dist/FunnelChart/properties.js +1 -0
  15. package/dist/IframeViewer/IframeViewer.svelte +65 -0
  16. package/dist/IframeViewer/IframeViewer.svelte.d.ts +4 -0
  17. package/dist/IframeViewer/properties.d.ts +33 -0
  18. package/dist/IframeViewer/properties.js +1 -0
  19. package/dist/LineChart/LineChart.svelte +190 -42
  20. package/dist/LineChart/properties.d.ts +69 -0
  21. package/dist/PieChart/PieChart.svelte +52 -20
  22. package/dist/PieChart/properties.d.ts +26 -0
  23. package/dist/SankeyChart/SankeyChart.svelte +15 -5
  24. package/dist/SankeyChart/properties.d.ts +19 -0
  25. package/dist/_chart/geometry.d.ts +1 -1
  26. package/dist/_chart/geometry.js +5 -4
  27. package/dist/_chart/highlight.d.ts +15 -0
  28. package/dist/_chart/highlight.js +1 -0
  29. package/dist/index.d.ts +9 -0
  30. package/dist/index.js +4 -0
  31. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { onMount } from 'svelte';
2
3
  import type {
3
4
  BarChartProperties,
4
5
  BarChartRenderContext,
@@ -6,6 +7,7 @@
6
7
  BarFillGradient,
7
8
  BarFillPattern
8
9
  } from './properties';
10
+ import type { ChartHighlightAPI } from '../_chart/highlight';
9
11
  import ChartContainer from '../_chart/ChartContainer.svelte';
10
12
  import Axis from '../_chart/Axis.svelte';
11
13
  import ChartTooltip from '../_chart/ChartTooltip.svelte';
@@ -62,6 +64,12 @@
62
64
  tooltipSnippet,
63
65
  empty,
64
66
  renderOverlay,
67
+ onChartReady,
68
+ highlightedIndex = null,
69
+ normaliseToFirstPoint = false,
70
+ topN,
71
+ overflowLabel = 'Other',
72
+ hideBarGraphics = false,
65
73
  onbarclick,
66
74
  onbarhover,
67
75
  testId,
@@ -76,6 +84,24 @@
76
84
  let hovered = $state<{ si: number; pi: number } | null>(null);
77
85
  let mouseX = $state(0);
78
86
  let mouseY = $state(0);
87
+ /** Internal tracking for the imperative highlight API (onChartReady). */
88
+ let apiHighlightedIndex = $state<number | null>(null);
89
+
90
+ // ── onMount: emit ChartHighlightAPI via onChartReady ──────────
91
+
92
+ onMount(() => {
93
+ if (typeof onChartReady !== 'function') {
94
+ return;
95
+ }
96
+ const api: ChartHighlightAPI = {
97
+ highlight: (index) => {
98
+ apiHighlightedIndex = index;
99
+ },
100
+ getCategories: () => labels,
101
+ type: 'bar-chart'
102
+ };
103
+ onChartReady(api);
104
+ });
79
105
 
80
106
  // ── Layout ─────────────────────────────────────────────────────
81
107
 
@@ -84,13 +110,91 @@
84
110
  let dims = $derived(computeChartDimensions(chartWidth, chartHeight));
85
111
 
86
112
  let isMulti = $derived(Array.isArray(series) && series.length > 0);
87
- let resolvedSeries = $derived(isMulti ? series! : [{ name: '', data: data ?? [] }]);
113
+
114
+ // ── Raw resolved series (before transformations) ──────────────
115
+
116
+ let rawSeries = $derived(isMulti ? series! : [{ name: '', data: data ?? [] }]);
117
+
118
+ // ── normaliseToFirstPoint transformation ──────────────────────
119
+
120
+ /**
121
+ * When normaliseToFirstPoint is true, each value is expressed as a
122
+ * percentage of the series' first data point's value (baseline = 100).
123
+ * Series whose first point is zero are left unchanged to avoid division
124
+ * by zero.
125
+ */
126
+ let normalisedSeries = $derived.by(() => {
127
+ if (!normaliseToFirstPoint) {
128
+ return rawSeries;
129
+ }
130
+ return rawSeries.map((s) => {
131
+ const baseline = s.data[0]?.value ?? 0;
132
+ if (baseline === 0) {
133
+ return s;
134
+ }
135
+ return {
136
+ ...s,
137
+ data: s.data.map((d) => ({
138
+ ...d,
139
+ value: (d.value / baseline) * 100
140
+ }))
141
+ };
142
+ });
143
+ });
144
+
145
+ // ── topN transformation ────────────────────────────────────────
146
+
147
+ /**
148
+ * Clips to the top N bars (by first-series value, descending) and aggregates
149
+ * the rest into a single overflow bar. Operates on each series in parallel so
150
+ * the same category ordering is preserved across series.
151
+ * Only applies in non-multi or single-series mode (groupMode-agnostic:
152
+ * works by reordering labels, not by touching multi-series stacking).
153
+ */
154
+ let resolvedSeries = $derived.by(() => {
155
+ if (topN == null || normalisedSeries.length === 0) {
156
+ return normalisedSeries;
157
+ }
158
+ const firstSeriesData = normalisedSeries[0]?.data ?? [];
159
+ if (firstSeriesData.length <= topN) {
160
+ return normalisedSeries;
161
+ }
162
+ // Sort indices by first-series value descending
163
+ const sortedIndices = firstSeriesData
164
+ .map((d, i) => ({ value: d.value, i }))
165
+ .sort((a, b) => b.value - a.value)
166
+ .map((item) => item.i);
167
+
168
+ const topIndices = new Set(sortedIndices.slice(0, topN));
169
+
170
+ return normalisedSeries.map((s) => {
171
+ const topData = s.data.filter((_, idx) => topIndices.has(idx));
172
+ const overflowValue = s.data
173
+ .filter((_, idx) => !topIndices.has(idx))
174
+ .reduce((sum, d) => sum + d.value, 0);
175
+ return {
176
+ ...s,
177
+ data: [...topData, { label: overflowLabel, value: overflowValue }]
178
+ };
179
+ });
180
+ });
88
181
 
89
182
  let labels = $derived.by(() => {
90
183
  const first = resolvedSeries[0]?.data ?? [];
91
184
  return first.map((d) => d.label);
92
185
  });
93
186
 
187
+ // ── Effective highlighted index: merge declarative + imperative ─
188
+
189
+ /**
190
+ * The declarative `highlightedIndex` prop takes precedence when explicitly
191
+ * set (non-null). The imperative API writes to `apiHighlightedIndex`.
192
+ * When both are null the chart renders normally (no dimming).
193
+ */
194
+ let effectiveHighlightedIndex = $derived(
195
+ highlightedIndex != null ? highlightedIndex : apiHighlightedIndex
196
+ );
197
+
94
198
  let isNormalized = $derived(stackNormalize && isMulti && groupMode === 'stacked');
95
199
 
96
200
  /** When stackNormalize is active and no custom valueFormat is supplied, append % so the
@@ -606,53 +710,61 @@
606
710
  </g>
607
711
  {/if}
608
712
 
609
- {#each bars as bar, i (i)}
610
- <!-- svelte-ignore a11y_no_static_element_interactions -->
611
- <!-- svelte-ignore a11y_click_events_have_key_events -->
612
- {#if isStackedMode && barRadius > 0}
613
- <path
614
- class="bar"
615
- class:hovered={hovered?.si === bar.si && hovered?.pi === bar.pi}
616
- class:dimmed={hovered !== null &&
617
- (hovered.si !== bar.si || hovered.pi !== bar.pi)}
618
- d={stackedBarPath(bar)}
619
- fill={barFillAttr(bar)}
620
- aria-label="{bar.dataPoint.label}: {getDisplayValue(bar)}"
621
- onmouseenter={(e) => handleEnter(e, bar)}
622
- onmousemove={trackMouse}
623
- onmouseleave={handleLeave}
624
- onclick={() => handleClick(bar)}
625
- />
626
- {:else}
627
- <rect
628
- class="bar"
629
- class:hovered={hovered?.si === bar.si && hovered?.pi === bar.pi}
630
- class:dimmed={hovered !== null &&
631
- (hovered.si !== bar.si || hovered.pi !== bar.pi)}
632
- x={bar.x}
633
- y={bar.y}
634
- width={bar.width}
635
- height={bar.height}
636
- rx={barRadius}
637
- ry={barRadius}
638
- fill={barFillAttr(bar)}
639
- aria-label="{bar.dataPoint.label}: {getDisplayValue(bar)}"
640
- onmouseenter={(e) => handleEnter(e, bar)}
641
- onmousemove={trackMouse}
642
- onmouseleave={handleLeave}
643
- onclick={() => handleClick(bar)}
644
- />
645
- {/if}
646
- {#if showValues && !isStackedMode}
647
- <text
648
- class="bar-value"
649
- x={isVertical ? bar.x + bar.width / 2 : bar.x + bar.width + 4}
650
- y={isVertical ? bar.y - 4 : bar.y + bar.height / 2}
651
- text-anchor={isVertical ? 'middle' : 'start'}
652
- dominant-baseline={isVertical ? 'auto' : 'middle'}>{getDisplayValue(bar)}</text
653
- >
654
- {/if}
655
- {/each}
713
+ {#if !hideBarGraphics}
714
+ {#each bars as bar, i (i)}
715
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
716
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
717
+ {#if isStackedMode && barRadius > 0}
718
+ <path
719
+ class="bar"
720
+ class:hovered={hovered?.si === bar.si && hovered?.pi === bar.pi}
721
+ class:highlighted={effectiveHighlightedIndex !== null &&
722
+ effectiveHighlightedIndex === bar.pi}
723
+ class:dimmed={(hovered !== null &&
724
+ (hovered.si !== bar.si || hovered.pi !== bar.pi)) ||
725
+ (effectiveHighlightedIndex !== null && effectiveHighlightedIndex !== bar.pi)}
726
+ d={stackedBarPath(bar)}
727
+ fill={barFillAttr(bar)}
728
+ aria-label="{bar.dataPoint.label}: {getDisplayValue(bar)}"
729
+ onmouseenter={(e) => handleEnter(e, bar)}
730
+ onmousemove={trackMouse}
731
+ onmouseleave={handleLeave}
732
+ onclick={() => handleClick(bar)}
733
+ />
734
+ {:else}
735
+ <rect
736
+ class="bar"
737
+ class:hovered={hovered?.si === bar.si && hovered?.pi === bar.pi}
738
+ class:highlighted={effectiveHighlightedIndex !== null &&
739
+ effectiveHighlightedIndex === bar.pi}
740
+ class:dimmed={(hovered !== null &&
741
+ (hovered.si !== bar.si || hovered.pi !== bar.pi)) ||
742
+ (effectiveHighlightedIndex !== null && effectiveHighlightedIndex !== bar.pi)}
743
+ x={bar.x}
744
+ y={bar.y}
745
+ width={bar.width}
746
+ height={bar.height}
747
+ rx={barRadius}
748
+ ry={barRadius}
749
+ fill={barFillAttr(bar)}
750
+ aria-label="{bar.dataPoint.label}: {getDisplayValue(bar)}"
751
+ onmouseenter={(e) => handleEnter(e, bar)}
752
+ onmousemove={trackMouse}
753
+ onmouseleave={handleLeave}
754
+ onclick={() => handleClick(bar)}
755
+ />
756
+ {/if}
757
+ {#if showValues && !isStackedMode}
758
+ <text
759
+ class="bar-value"
760
+ x={isVertical ? bar.x + bar.width / 2 : bar.x + bar.width + 4}
761
+ y={isVertical ? bar.y - 4 : bar.y + bar.height / 2}
762
+ text-anchor={isVertical ? 'middle' : 'start'}
763
+ dominant-baseline={isVertical ? 'auto' : 'middle'}>{getDisplayValue(bar)}</text
764
+ >
765
+ {/if}
766
+ {/each}
767
+ {/if}
656
768
 
657
769
  <!-- A1-4: renderOverlay escape hatch — rendered after all bars -->
658
770
  {#if typeof renderOverlay === 'function'}
@@ -691,6 +803,9 @@
691
803
  .bar.hovered {
692
804
  opacity: var(--barchart-bar-hover-opacity, 1);
693
805
  }
806
+ .bar.highlighted {
807
+ opacity: var(--barchart-bar-highlighted-opacity, 1);
808
+ }
694
809
  .bar.dimmed {
695
810
  opacity: var(--barchart-bar-dimmed-opacity, 0.3);
696
811
  }
@@ -1,4 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { ChartHighlightAPI } from '../_chart/highlight';
2
3
  export type BarFillPattern = {
3
4
  pattern: {
4
5
  /** SVG pattern element type: 'lines' | 'dots' | 'crosshatch' */
@@ -112,6 +113,46 @@ export type OptionalBarChartProperties = {
112
113
  * live in SVG coordinate space.
113
114
  */
114
115
  renderOverlay?: Snippet<[BarChartRenderContext]>;
116
+ /**
117
+ * Called once on mount with an imperative `ChartHighlightAPI` handle.
118
+ * Use `api.highlight(index)` to emphasise a specific bar (by its zero-based
119
+ * position within the category axis) and dim all others. Pass `null` to clear.
120
+ * The `type` field on the handle is always `'bar-chart'`.
121
+ */
122
+ onChartReady?: (api: ChartHighlightAPI) => void;
123
+ /**
124
+ * Declarative complement to `onChartReady`. When set to a non-null number the
125
+ * bar at that zero-based category index is highlighted (full opacity) and the
126
+ * remaining bars are dimmed. Set to `null` or omit to show all bars normally.
127
+ */
128
+ highlightedIndex?: number | null;
129
+ /**
130
+ * When `true`, each series' values are expressed as a percentage of that
131
+ * series' own first data point. A value equal to the baseline renders as 100,
132
+ * a value twice the baseline as 200, and so on. Useful for comparing relative
133
+ * growth across series that start at very different absolute levels.
134
+ * Has no effect on series that have no data or whose first point is zero.
135
+ */
136
+ normaliseToFirstPoint?: boolean;
137
+ /**
138
+ * When set, only the top `topN` bars (by descending value) are shown
139
+ * individually. The remaining bars are aggregated into a single bar whose
140
+ * value is the sum of all omitted bars and whose label is `overflowLabel`
141
+ * (default `"Other"`). Has no effect when the data has `topN` or fewer bars.
142
+ * In multi-series mode the ranking is based on the first series.
143
+ */
144
+ topN?: number;
145
+ /**
146
+ * Label for the aggregated overflow bar produced by `topN`. Defaults to
147
+ * `"Other"`. Has no effect when `topN` is not set.
148
+ */
149
+ overflowLabel?: string;
150
+ /**
151
+ * When `true`, bar rectangles are not rendered. Only the axis labels, gridlines,
152
+ * and legend remain visible. Useful for building legend-only or label-only
153
+ * thumbnails alongside a full chart.
154
+ */
155
+ hideBarGraphics?: boolean;
115
156
  testId?: string;
116
157
  classes?: string;
117
158
  };
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import type { DeltaIndicatorProperties, DeltaDirection } from './properties';
3
+
4
+ let {
5
+ value,
6
+ format,
7
+ invertColors = false,
8
+ hideArrow = false,
9
+ neutralThreshold = 0,
10
+ testId,
11
+ classes
12
+ }: DeltaIndicatorProperties = $props();
13
+
14
+ const defaultFormat = (input: number): string => `${Math.round(Math.abs(input))}%`;
15
+
16
+ // A negative threshold would invert the neutral band and misclassify direction, so
17
+ // clamp it to a non-negative value before the comparison math.
18
+ const safeNeutralThreshold = $derived(Math.max(0, neutralThreshold));
19
+
20
+ let direction = $derived<DeltaDirection>(
21
+ value > safeNeutralThreshold ? 'up' : value < -safeNeutralThreshold ? 'down' : 'neutral'
22
+ );
23
+
24
+ // Tone drives the color. 'up' reads as positive by default; invertColors flips
25
+ // the positive/negative tone for lower-is-better metrics. Neutral is always muted.
26
+ let tone = $derived(
27
+ direction === 'neutral'
28
+ ? 'neutral'
29
+ : (direction === 'up') !== invertColors
30
+ ? 'positive'
31
+ : 'negative'
32
+ );
33
+
34
+ let text = $derived((format ?? defaultFormat)(value));
35
+ </script>
36
+
37
+ <span
38
+ class="delta-indicator delta-{tone} {classes ?? ''}"
39
+ data-pw={typeof testId === 'string' ? testId : null}
40
+ >
41
+ {#if !hideArrow && direction !== 'neutral'}
42
+ <span class="delta-indicator-arrow delta-arrow-{direction}" aria-hidden="true">
43
+ <svg viewBox="0 0 10 10"><path d="M5 1 L9 8 L1 8 Z" /></svg>
44
+ </span>
45
+ {/if}
46
+ <span class="delta-indicator-text">{text}</span>
47
+ </span>
48
+
49
+ <style>
50
+ .delta-indicator {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: var(--delta-indicator-gap, 2px);
54
+ font-size: var(--delta-indicator-font-size, 13px);
55
+ font-weight: var(--delta-indicator-font-weight, 600);
56
+ line-height: 1;
57
+ }
58
+
59
+ .delta-positive {
60
+ color: var(--delta-indicator-positive-color, #1a9d6f);
61
+ }
62
+
63
+ .delta-negative {
64
+ color: var(--delta-indicator-negative-color, #e5484d);
65
+ }
66
+
67
+ .delta-neutral {
68
+ color: var(--delta-indicator-neutral-color, #8a8a8a);
69
+ }
70
+
71
+ .delta-indicator-arrow {
72
+ display: inline-flex;
73
+ width: var(--delta-indicator-arrow-size, 9px);
74
+ height: var(--delta-indicator-arrow-size, 9px);
75
+ flex-shrink: 0;
76
+ }
77
+
78
+ .delta-indicator-arrow svg {
79
+ width: 100%;
80
+ height: 100%;
81
+ fill: currentColor;
82
+ }
83
+
84
+ .delta-arrow-down {
85
+ transform: rotate(180deg);
86
+ }
87
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { DeltaIndicatorProperties } from './properties';
2
+ declare const DeltaIndicator: import("svelte").Component<DeltaIndicatorProperties, {}, "">;
3
+ type DeltaIndicator = ReturnType<typeof DeltaIndicator>;
4
+ export default DeltaIndicator;
@@ -0,0 +1,26 @@
1
+ export type DeltaIndicatorProperties = MandatoryDeltaIndicatorProperties & OptionalDeltaIndicatorProperties;
2
+ export type DeltaDirection = 'up' | 'down' | 'neutral';
3
+ export type MandatoryDeltaIndicatorProperties = {
4
+ /** The change amount; its sign selects the up / down / neutral direction. */
5
+ value: number;
6
+ };
7
+ export type OptionalDeltaIndicatorProperties = {
8
+ /**
9
+ * Formats the displayed text from the raw value. Default renders the rounded
10
+ * absolute value followed by a percent sign (e.g. `12%`).
11
+ */
12
+ format?: (value: number) => string;
13
+ /**
14
+ * Swap the up/down colors for lower-is-better metrics (e.g. bounce rate, RTO),
15
+ * where a decrease is "good" (positive tone) and an increase is "bad".
16
+ */
17
+ invertColors?: boolean;
18
+ /** Hide the directional arrow, showing only the formatted text. */
19
+ hideArrow?: boolean;
20
+ /** Absolute values at or below this are treated as `neutral` (default 0). */
21
+ neutralThreshold?: number;
22
+ /** Test selector applied as the `data-pw` attribute. */
23
+ testId?: string;
24
+ /** Additional CSS classes for theming. */
25
+ classes?: string;
26
+ };
@@ -0,0 +1 @@
1
+ export {};