@juspay/svelte-ui-components 2.30.0 → 2.32.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 (45) hide show
  1. package/dist/AreaChart/AreaChart.svelte +421 -0
  2. package/dist/AreaChart/AreaChart.svelte.d.ts +4 -0
  3. package/dist/AreaChart/properties.d.ts +62 -0
  4. package/dist/AreaChart/properties.js +1 -0
  5. package/dist/BarChart/BarChart.svelte +372 -0
  6. package/dist/BarChart/BarChart.svelte.d.ts +4 -0
  7. package/dist/BarChart/properties.d.ts +44 -0
  8. package/dist/BarChart/properties.js +1 -0
  9. package/dist/LineChart/LineChart.svelte +361 -0
  10. package/dist/LineChart/LineChart.svelte.d.ts +4 -0
  11. package/dist/LineChart/properties.d.ts +59 -0
  12. package/dist/LineChart/properties.js +1 -0
  13. package/dist/PieChart/PieChart.svelte +236 -0
  14. package/dist/PieChart/PieChart.svelte.d.ts +4 -0
  15. package/dist/PieChart/properties.d.ts +36 -0
  16. package/dist/PieChart/properties.js +1 -0
  17. package/dist/SankeyChart/SankeyChart.svelte +312 -0
  18. package/dist/SankeyChart/SankeyChart.svelte.d.ts +4 -0
  19. package/dist/SankeyChart/properties.d.ts +52 -0
  20. package/dist/SankeyChart/properties.js +1 -0
  21. package/dist/Select/Select.svelte +43 -24
  22. package/dist/Select/properties.d.ts +5 -0
  23. package/dist/_chart/Axis.svelte +143 -0
  24. package/dist/_chart/Axis.svelte.d.ts +4 -0
  25. package/dist/_chart/ChartContainer.svelte +81 -0
  26. package/dist/_chart/ChartContainer.svelte.d.ts +4 -0
  27. package/dist/_chart/ChartTooltip.svelte +81 -0
  28. package/dist/_chart/ChartTooltip.svelte.d.ts +4 -0
  29. package/dist/_chart/Legend.svelte +59 -0
  30. package/dist/_chart/Legend.svelte.d.ts +4 -0
  31. package/dist/_chart/colors.d.ts +4 -0
  32. package/dist/_chart/colors.js +36 -0
  33. package/dist/_chart/format.d.ts +3 -0
  34. package/dist/_chart/format.js +28 -0
  35. package/dist/_chart/geometry.d.ts +24 -0
  36. package/dist/_chart/geometry.js +204 -0
  37. package/dist/_chart/paths.d.ts +4 -0
  38. package/dist/_chart/paths.js +138 -0
  39. package/dist/_chart/scales.d.ts +5 -0
  40. package/dist/_chart/scales.js +77 -0
  41. package/dist/_chart/types.d.ts +128 -0
  42. package/dist/_chart/types.js +1 -0
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +5 -0
  45. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,312 @@
1
+ <script lang="ts">
2
+ import type { SankeyChartProperties, SankeyTooltipContext } from './properties';
3
+ import ChartContainer from '../_chart/ChartContainer.svelte';
4
+ import ChartTooltip from '../_chart/ChartTooltip.svelte';
5
+ import { computeSankeyLayout } from '../_chart/geometry';
6
+ import { getColor } from '../_chart/colors';
7
+ import { formatNumber } from '../_chart/format';
8
+ import { SvelteSet } from 'svelte/reactivity';
9
+
10
+ // ── Props ──────────────────────────────────────────────────────
11
+
12
+ let {
13
+ nodes,
14
+ links,
15
+ nodeWidth = 16,
16
+ nodePadding = 8,
17
+ iterations = 6,
18
+ showValues = false,
19
+ showLabels = true,
20
+ aspectRatio = 16 / 9,
21
+ valueFormat,
22
+ tooltipSnippet,
23
+ empty,
24
+ onnodeclick,
25
+ onlinkclick,
26
+ onnodehover,
27
+ onlinkhover,
28
+ testId,
29
+ classes
30
+ }: SankeyChartProperties = $props();
31
+
32
+ // ── State ──────────────────────────────────────────────────────
33
+
34
+ let containerEl: HTMLDivElement | null = $state(null);
35
+ let chartWidth = $state(0);
36
+ let chartHeight = $state(0);
37
+ let hoveredNode = $state<string | null>(null);
38
+ let hoveredLink = $state<{ source: string; target: string } | null>(null);
39
+ let mouseX = $state(0);
40
+ let mouseY = $state(0);
41
+
42
+ // ── Layout ─────────────────────────────────────────────────────
43
+
44
+ let format = $derived(valueFormat ?? formatNumber);
45
+ let isEmpty = $derived(nodes.length === 0);
46
+ const MARGIN = 40;
47
+ let layout = $derived(
48
+ computeSankeyLayout(
49
+ nodes,
50
+ links,
51
+ Math.max(0, chartWidth - MARGIN * 2),
52
+ Math.max(0, chartHeight - MARGIN * 2),
53
+ nodeWidth,
54
+ nodePadding,
55
+ iterations
56
+ )
57
+ );
58
+
59
+ let connectedNodes = $derived.by(() => {
60
+ if (hoveredNode !== null) {
61
+ const connected = new SvelteSet<string>([hoveredNode]);
62
+ for (const l of links) {
63
+ if (l.source === hoveredNode || l.target === hoveredNode) {
64
+ connected.add(l.source);
65
+ connected.add(l.target);
66
+ }
67
+ }
68
+ return connected;
69
+ }
70
+ if (hoveredLink !== null) {
71
+ return new SvelteSet([hoveredLink.source, hoveredLink.target]);
72
+ }
73
+ return null;
74
+ });
75
+
76
+ // ── Tooltip ────────────────────────────────────────────────────
77
+
78
+ let tooltipData = $derived.by(() => {
79
+ if (hoveredNode !== null) {
80
+ const n = layout.nodes.find((nd) => nd.id === hoveredNode);
81
+ if (!n) {
82
+ return null;
83
+ }
84
+ return {
85
+ title: n.label,
86
+ items: [
87
+ {
88
+ label: 'Total flow',
89
+ value: format(n.value),
90
+ color: n.color ?? getColor(layout.nodes.indexOf(n))
91
+ }
92
+ ]
93
+ };
94
+ }
95
+ if (hoveredLink !== null) {
96
+ const l = links.find(
97
+ (lk) => lk.source === hoveredLink!.source && lk.target === hoveredLink!.target
98
+ );
99
+ if (!l) {
100
+ return null;
101
+ }
102
+ return {
103
+ title: `${nodes.find((n) => n.id === l.source)?.label ?? l.source} → ${nodes.find((n) => n.id === l.target)?.label ?? l.target}`,
104
+ items: [{ label: 'Flow', value: format(l.value) }]
105
+ };
106
+ }
107
+ return null;
108
+ });
109
+
110
+ let tooltipContext = $derived.by<SankeyTooltipContext | null>(() => {
111
+ if (hoveredNode !== null) {
112
+ const n = findNode(hoveredNode);
113
+ const computed = layout.nodes.find((nd) => nd.id === hoveredNode);
114
+ if (!n || !computed) {
115
+ return null;
116
+ }
117
+ return { type: 'node', node: n, value: computed.value };
118
+ }
119
+ if (hoveredLink !== null) {
120
+ const l = findLink(hoveredLink.source, hoveredLink.target);
121
+ if (!l) {
122
+ return null;
123
+ }
124
+ return { type: 'link', link: l };
125
+ }
126
+ return null;
127
+ });
128
+
129
+ // ── Interactions ───────────────────────────────────────────────
130
+
131
+ function trackMouse(e: MouseEvent) {
132
+ if (containerEl === null) {
133
+ return;
134
+ }
135
+ const rect = containerEl.getBoundingClientRect();
136
+ mouseX = e.clientX - rect.left;
137
+ mouseY = e.clientY - rect.top;
138
+ }
139
+
140
+ function isLinkHighlighted(source: string, target: string): boolean {
141
+ if (hoveredLink !== null) {
142
+ return source === hoveredLink.source && target === hoveredLink.target;
143
+ }
144
+ if (hoveredNode !== null) {
145
+ return source === hoveredNode || target === hoveredNode;
146
+ }
147
+ return false;
148
+ }
149
+
150
+ function findNode(id: string) {
151
+ return nodes.find((n) => n.id === id);
152
+ }
153
+
154
+ function findLink(source: string, target: string) {
155
+ return links.find((l) => l.source === source && l.target === target);
156
+ }
157
+
158
+ function handleNodeEnter(e: MouseEvent, id: string) {
159
+ hoveredNode = id;
160
+ trackMouse(e);
161
+ const orig = findNode(id);
162
+ if (orig) {
163
+ onnodehover?.({ node: orig });
164
+ }
165
+ }
166
+
167
+ function handleNodeLeave() {
168
+ hoveredNode = null;
169
+ onnodehover?.(null);
170
+ }
171
+
172
+ function handleNodeClick(id: string) {
173
+ const orig = findNode(id);
174
+ if (orig) {
175
+ onnodeclick?.({ node: orig });
176
+ }
177
+ }
178
+
179
+ function handleLinkEnter(e: MouseEvent, source: string, target: string) {
180
+ hoveredLink = { source, target };
181
+ trackMouse(e);
182
+ const orig = findLink(source, target);
183
+ if (orig) {
184
+ onlinkhover?.({ link: orig });
185
+ }
186
+ }
187
+
188
+ function handleLinkLeave() {
189
+ hoveredLink = null;
190
+ onlinkhover?.(null);
191
+ }
192
+
193
+ function handleLinkClick(source: string, target: string) {
194
+ const orig = findLink(source, target);
195
+ if (orig) {
196
+ onlinkclick?.({ link: orig });
197
+ }
198
+ }
199
+ </script>
200
+
201
+ <div
202
+ class="sankey-chart {classes ?? ''}"
203
+ bind:this={containerEl}
204
+ data-pw={typeof testId === 'string' ? testId : null}
205
+ >
206
+ {#if isEmpty && typeof empty === 'function'}
207
+ <div class="chart-empty">{@render empty()}</div>
208
+ {:else}
209
+ <ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio}>
210
+ <g transform="translate({MARGIN}, {MARGIN})">
211
+ {#each layout.links as link, i (i)}
212
+ {@const highlighted = isLinkHighlighted(link.source, link.target)}
213
+ {@const dimmed = (hoveredNode !== null || hoveredLink !== null) && !highlighted}
214
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
215
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
216
+ <path
217
+ class="sankey-link"
218
+ d={link.path}
219
+ fill="none"
220
+ stroke={link.color ?? getColor(layout.nodes.findIndex((n) => n.id === link.source))}
221
+ stroke-width={Math.max(1, link.width)}
222
+ stroke-opacity={highlighted ? 0.7 : dimmed ? 0.08 : 0.4}
223
+ onmouseenter={(e) => handleLinkEnter(e, link.source, link.target)}
224
+ onmousemove={trackMouse}
225
+ onmouseleave={handleLinkLeave}
226
+ onclick={() => handleLinkClick(link.source, link.target)}
227
+ />
228
+ {/each}
229
+
230
+ {#each layout.nodes as node, ni (ni)}
231
+ {@const color = node.color ?? getColor(ni)}
232
+ {@const dimmed = connectedNodes !== null && !connectedNodes.has(node.id)}
233
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
234
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
235
+ <rect
236
+ class="sankey-node"
237
+ class:node-dimmed={dimmed}
238
+ x={node.x}
239
+ y={node.y}
240
+ width={node.width}
241
+ height={node.height}
242
+ rx={2}
243
+ ry={2}
244
+ fill={color}
245
+ onmouseenter={(e) => handleNodeEnter(e, node.id)}
246
+ onmousemove={trackMouse}
247
+ onmouseleave={handleNodeLeave}
248
+ onclick={() => handleNodeClick(node.id)}
249
+ />
250
+ {#if showLabels}
251
+ <text
252
+ class="sankey-label"
253
+ class:node-dimmed={dimmed}
254
+ x={node.column === 0 ? node.x - 6 : node.x + node.width + 6}
255
+ y={node.y + node.height / 2}
256
+ text-anchor={node.column === 0 ? 'end' : 'start'}
257
+ dominant-baseline="middle"
258
+ >{node.label}{#if showValues}
259
+ ({format(node.value)}){/if}</text
260
+ >
261
+ {/if}
262
+ {/each}
263
+ </g>
264
+ </ChartContainer>
265
+
266
+ {#if typeof tooltipSnippet === 'function' && tooltipContext !== null}
267
+ <div class="chart-tooltip-slot" style="left: {mouseX + 12}px; top: {mouseY - 12}px;">
268
+ {@render tooltipSnippet(tooltipContext)}
269
+ </div>
270
+ {:else}
271
+ <ChartTooltip data={tooltipData} {mouseX} {mouseY} />
272
+ {/if}
273
+ {/if}
274
+ </div>
275
+
276
+ <style>
277
+ .sankey-chart {
278
+ width: 100%;
279
+ position: relative;
280
+ }
281
+ .sankey-link {
282
+ transition: stroke-opacity var(--chart-transition-duration, 0.2s) ease;
283
+ cursor: pointer;
284
+ }
285
+ .sankey-node {
286
+ transition: opacity var(--chart-transition-duration, 0.2s) ease;
287
+ cursor: pointer;
288
+ }
289
+ .sankey-node.node-dimmed {
290
+ opacity: var(--sankey-dimmed-opacity, 0.15);
291
+ }
292
+ .sankey-label {
293
+ fill: var(--sankey-label-color, #333);
294
+ font-size: var(--sankey-label-font-size, 12px);
295
+ font-family: var(--chart-font-family, inherit);
296
+ pointer-events: none;
297
+ transition: opacity var(--chart-transition-duration, 0.2s) ease;
298
+ }
299
+ .sankey-label.node-dimmed {
300
+ opacity: var(--sankey-dimmed-opacity, 0.15);
301
+ }
302
+ .chart-tooltip-slot {
303
+ position: absolute;
304
+ z-index: 10;
305
+ pointer-events: none;
306
+ }
307
+ .chart-empty {
308
+ padding: var(--chart-empty-padding, 32px 24px);
309
+ color: var(--chart-empty-color, #9ca3af);
310
+ text-align: center;
311
+ }
312
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { SankeyChartProperties } from './properties';
2
+ declare const SankeyChart: import("svelte").Component<SankeyChartProperties, {}, "">;
3
+ type SankeyChart = ReturnType<typeof SankeyChart>;
4
+ export default SankeyChart;
@@ -0,0 +1,52 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type SankeyNode = {
3
+ id: string;
4
+ label?: string;
5
+ color?: string;
6
+ };
7
+ export type SankeyLink = {
8
+ source: string;
9
+ target: string;
10
+ value: number;
11
+ color?: string;
12
+ };
13
+ export type SankeyTooltipContext = {
14
+ type: 'node';
15
+ node: SankeyNode;
16
+ value: number;
17
+ } | {
18
+ type: 'link';
19
+ link: SankeyLink;
20
+ };
21
+ export type SankeyChartProperties = MandatorySankeyChartProperties & OptionalSankeyChartProperties & SankeyChartEventProperties;
22
+ export type MandatorySankeyChartProperties = {
23
+ nodes: SankeyNode[];
24
+ links: SankeyLink[];
25
+ };
26
+ export type OptionalSankeyChartProperties = {
27
+ nodeWidth?: number;
28
+ nodePadding?: number;
29
+ iterations?: number;
30
+ showValues?: boolean;
31
+ showLabels?: boolean;
32
+ aspectRatio?: number;
33
+ valueFormat?: (value: number) => string;
34
+ tooltipSnippet?: Snippet<[SankeyTooltipContext]>;
35
+ empty?: Snippet;
36
+ testId?: string;
37
+ classes?: string;
38
+ };
39
+ export type SankeyChartEventProperties = {
40
+ onnodeclick?: (event: {
41
+ node: SankeyNode;
42
+ }) => void;
43
+ onlinkclick?: (event: {
44
+ link: SankeyLink;
45
+ }) => void;
46
+ onnodehover?: (event: {
47
+ node: SankeyNode;
48
+ } | null) => void;
49
+ onlinkhover?: (event: {
50
+ link: SankeyLink;
51
+ } | null) => void;
52
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -13,6 +13,7 @@
13
13
  disabled = false,
14
14
  bottomContent,
15
15
  optionIndicator,
16
+ triggerSummary,
16
17
  testId,
17
18
  itemTestId,
18
19
  onchange,
@@ -256,30 +257,48 @@
256
257
  tabindex={disabled ? -1 : searchable ? -1 : 0}
257
258
  >
258
259
  {#if multiple}
259
- {#each value as id (id)}
260
- <Pill
261
- text={getLabel(id)}
262
- dismissible
263
- {disabled}
264
- ondismiss={() => removeItem(id)}
265
- {...typeof testId === 'string' ? { testId: `${testId}-pill-${id}` } : {}}
266
- />
267
- {/each}
268
- {#if searchable}
269
- <input
270
- class="select-search"
271
- type="text"
272
- value={query}
273
- oninput={handleSearchInput}
274
- onfocus={handleSearchFocus}
275
- bind:this={searchInputEl}
276
- placeholder={value.length === 0 ? placeholder : ''}
277
- {disabled}
278
- autocomplete="off"
279
- tabindex={disabled ? -1 : 0}
280
- />
281
- {:else if value.length === 0}
282
- <span class="select-placeholder">{placeholder}</span>
260
+ {#if typeof triggerSummary === 'function'}
261
+ {@render triggerSummary({ value, items })}
262
+ {#if searchable}
263
+ <input
264
+ class="select-search"
265
+ type="text"
266
+ value={query}
267
+ oninput={handleSearchInput}
268
+ onfocus={handleSearchFocus}
269
+ bind:this={searchInputEl}
270
+ placeholder={value.length === 0 ? placeholder : ''}
271
+ {disabled}
272
+ autocomplete="off"
273
+ tabindex={disabled ? -1 : 0}
274
+ />
275
+ {/if}
276
+ {:else}
277
+ {#each value as id (id)}
278
+ <Pill
279
+ text={getLabel(id)}
280
+ dismissible
281
+ {disabled}
282
+ ondismiss={() => removeItem(id)}
283
+ {...typeof testId === 'string' ? { testId: `${testId}-pill-${id}` } : {}}
284
+ />
285
+ {/each}
286
+ {#if searchable}
287
+ <input
288
+ class="select-search"
289
+ type="text"
290
+ value={query}
291
+ oninput={handleSearchInput}
292
+ onfocus={handleSearchFocus}
293
+ bind:this={searchInputEl}
294
+ placeholder={value.length === 0 ? placeholder : ''}
295
+ {disabled}
296
+ autocomplete="off"
297
+ tabindex={disabled ? -1 : 0}
298
+ />
299
+ {:else if value.length === 0}
300
+ <span class="select-placeholder">{placeholder}</span>
301
+ {/if}
283
302
  {/if}
284
303
  {:else if searchable}
285
304
  <input
@@ -27,6 +27,11 @@ export type OptionalSelectProperties = {
27
27
  open?: boolean;
28
28
  /** Horizontal anchor of the dropdown panel. `'left'` (default) anchors to the trigger's left edge; `'right'` anchors to the right edge so a content-wider panel hangs leftward instead of overflowing. */
29
29
  dropdownAlign?: 'left' | 'right';
30
+ /** Snippet for rendering a compact trigger summary in multiple mode instead of one Pill per selected value. Receives `{ value, items }` so the consumer can compute e.g. "All" / "3 selected". When not provided the default Pill-per-value behaviour is used. */
31
+ triggerSummary?: import('svelte').Snippet<[{
32
+ value: string[];
33
+ items: SelectItem[];
34
+ }]>;
30
35
  };
31
36
  export type SelectEventProperties = {
32
37
  onchange?: (value: string[]) => void;
@@ -0,0 +1,143 @@
1
+ <script lang="ts">
2
+ import type { AxisProperties } from './types';
3
+ import { defaultTickFormat } from './format';
4
+
5
+ let {
6
+ orientation,
7
+ scale,
8
+ tickCount = 5,
9
+ tickFormat,
10
+ showGridlines = false,
11
+ gridlineLength = 0,
12
+ label,
13
+ classes
14
+ }: AxisProperties = $props();
15
+
16
+ let format = $derived(tickFormat ?? defaultTickFormat);
17
+
18
+ let isHorizontal = $derived(orientation === 'top' || orientation === 'bottom');
19
+
20
+ let tickValues = $derived.by(() => {
21
+ if ('ticks' in scale && typeof scale.ticks === 'function') {
22
+ return scale.ticks(tickCount);
23
+ }
24
+ if ('domain' in scale && Array.isArray(scale.domain)) {
25
+ return scale.domain;
26
+ }
27
+ return [];
28
+ });
29
+
30
+ let isBand = $derived('bandwidth' in scale);
31
+ let bandOffset = $derived(isBand && 'bandwidth' in scale ? scale.bandwidth / 2 : 0);
32
+
33
+ function positionTick(tick: number | string): number {
34
+ if ('bandwidth' in scale) {
35
+ return scale(String(tick)) + bandOffset;
36
+ }
37
+ return scale(Number(tick)) + bandOffset;
38
+ }
39
+
40
+ const TICK_SIZE = 6;
41
+ </script>
42
+
43
+ <g class="axis axis-{orientation} {classes ?? ''}">
44
+ {#if isHorizontal}
45
+ <line class="axis-line" x1={scale.range[0]} x2={scale.range[1]} y1={0} y2={0} />
46
+ {#each tickValues as tick, i (i)}
47
+ {@const x = positionTick(tick)}
48
+ <g class="tick" transform="translate({x}, 0)">
49
+ <line class="tick-mark" y2={orientation === 'bottom' ? TICK_SIZE : -TICK_SIZE} />
50
+ <text
51
+ class="tick-label"
52
+ y={orientation === 'bottom' ? TICK_SIZE + 4 : -(TICK_SIZE + 4)}
53
+ text-anchor="middle"
54
+ dominant-baseline={orientation === 'bottom' ? 'hanging' : 'auto'}
55
+ >
56
+ {format(tick)}
57
+ </text>
58
+ {#if showGridlines && gridlineLength > 0}
59
+ <line
60
+ class="gridline"
61
+ y1={0}
62
+ y2={orientation === 'bottom' ? -gridlineLength : gridlineLength}
63
+ />
64
+ {/if}
65
+ </g>
66
+ {/each}
67
+ {#if label}
68
+ <text
69
+ class="axis-label"
70
+ x={(scale.range[0] + scale.range[1]) / 2}
71
+ y={orientation === 'bottom' ? 36 : -30}
72
+ text-anchor="middle"
73
+ >
74
+ {label}
75
+ </text>
76
+ {/if}
77
+ {:else}
78
+ <line class="axis-line" y1={scale.range[0]} y2={scale.range[1]} x1={0} x2={0} />
79
+ {#each tickValues as tick, i (i)}
80
+ {@const y = positionTick(tick)}
81
+ <g class="tick" transform="translate(0, {y})">
82
+ <line class="tick-mark" x2={orientation === 'left' ? -TICK_SIZE : TICK_SIZE} />
83
+ <text
84
+ class="tick-label"
85
+ x={orientation === 'left' ? -(TICK_SIZE + 4) : TICK_SIZE + 4}
86
+ text-anchor={orientation === 'left' ? 'end' : 'start'}
87
+ dominant-baseline="middle"
88
+ >
89
+ {format(tick)}
90
+ </text>
91
+ {#if showGridlines && gridlineLength > 0}
92
+ <line
93
+ class="gridline"
94
+ x1={0}
95
+ x2={orientation === 'left' ? gridlineLength : -gridlineLength}
96
+ />
97
+ {/if}
98
+ </g>
99
+ {/each}
100
+ {#if label}
101
+ <text
102
+ class="axis-label"
103
+ transform="rotate(-90)"
104
+ x={-(scale.range[0] + scale.range[1]) / 2}
105
+ y={orientation === 'left' ? -40 : 40}
106
+ text-anchor="middle"
107
+ >
108
+ {label}
109
+ </text>
110
+ {/if}
111
+ {/if}
112
+ </g>
113
+
114
+ <style>
115
+ .axis-line {
116
+ stroke: var(--chart-axis-color, #666);
117
+ stroke-width: var(--chart-axis-stroke-width, 1);
118
+ }
119
+
120
+ .tick-mark {
121
+ stroke: var(--chart-axis-color, #666);
122
+ stroke-width: var(--chart-axis-stroke-width, 1);
123
+ }
124
+
125
+ .tick-label {
126
+ fill: var(--chart-axis-color, #666);
127
+ font-size: var(--chart-axis-font-size, 11px);
128
+ font-family: var(--chart-axis-font-family, inherit);
129
+ }
130
+
131
+ .axis-label {
132
+ fill: var(--chart-axis-label-color, #333);
133
+ font-size: var(--chart-axis-label-font-size, 12px);
134
+ font-family: var(--chart-axis-font-family, inherit);
135
+ font-weight: 500;
136
+ }
137
+
138
+ .gridline {
139
+ stroke: var(--chart-gridline-color, #e0e0e0);
140
+ stroke-opacity: var(--chart-gridline-opacity, 0.5);
141
+ stroke-dasharray: var(--chart-gridline-dash, 4 4);
142
+ }
143
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { AxisProperties } from './types';
2
+ declare const Axis: import("svelte").Component<AxisProperties, {}, "">;
3
+ type Axis = ReturnType<typeof Axis>;
4
+ export default Axis;
@@ -0,0 +1,81 @@
1
+ <script lang="ts">
2
+ import type { ChartContainerProperties } from './types';
3
+ import { onMount } from 'svelte';
4
+
5
+ let {
6
+ width = $bindable(0),
7
+ height = $bindable(0),
8
+ aspectRatio = 16 / 9,
9
+ minHeight = 0,
10
+ testId,
11
+ classes,
12
+ children
13
+ }: ChartContainerProperties = $props();
14
+
15
+ let containerEl: HTMLDivElement | null = $state(null);
16
+
17
+ onMount(() => {
18
+ if (containerEl === null) {
19
+ return;
20
+ }
21
+
22
+ function measure() {
23
+ if (containerEl === null) {
24
+ return;
25
+ }
26
+ const rect = containerEl.getBoundingClientRect();
27
+ const w = Math.round(rect.width);
28
+ width = w;
29
+ height = Math.max(minHeight, Math.round(w / aspectRatio));
30
+ }
31
+
32
+ measure();
33
+
34
+ // Coalesce bursts of resize events into a single measure per frame, always
35
+ // using the latest size (a leading-edge debounce would drop the final size).
36
+ let frame = 0;
37
+ const observer = new ResizeObserver(() => {
38
+ cancelAnimationFrame(frame);
39
+ frame = requestAnimationFrame(measure);
40
+ });
41
+ observer.observe(containerEl);
42
+
43
+ return () => {
44
+ cancelAnimationFrame(frame);
45
+ observer.disconnect();
46
+ };
47
+ });
48
+ </script>
49
+
50
+ <div
51
+ class="chart-container {classes ?? ''}"
52
+ bind:this={containerEl}
53
+ data-pw={typeof testId === 'string' ? testId : null}
54
+ >
55
+ {#if width > 0 && height > 0}
56
+ <svg
57
+ viewBox="0 0 {width} {height}"
58
+ preserveAspectRatio="xMidYMid meet"
59
+ role="img"
60
+ {width}
61
+ {height}
62
+ >
63
+ {@render children()}
64
+ </svg>
65
+ {/if}
66
+ </div>
67
+
68
+ <style>
69
+ .chart-container {
70
+ width: 100%;
71
+ background: var(--chart-background, transparent);
72
+ font-family: var(--chart-font-family, inherit);
73
+ }
74
+
75
+ svg {
76
+ display: block;
77
+ width: 100%;
78
+ height: auto;
79
+ overflow: visible;
80
+ }
81
+ </style>