@juspay/svelte-ui-components 2.31.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.
- package/dist/AreaChart/AreaChart.svelte +421 -0
- package/dist/AreaChart/AreaChart.svelte.d.ts +4 -0
- package/dist/AreaChart/properties.d.ts +62 -0
- package/dist/AreaChart/properties.js +1 -0
- package/dist/BarChart/BarChart.svelte +372 -0
- package/dist/BarChart/BarChart.svelte.d.ts +4 -0
- package/dist/BarChart/properties.d.ts +44 -0
- package/dist/BarChart/properties.js +1 -0
- package/dist/LineChart/LineChart.svelte +361 -0
- package/dist/LineChart/LineChart.svelte.d.ts +4 -0
- package/dist/LineChart/properties.d.ts +59 -0
- package/dist/LineChart/properties.js +1 -0
- package/dist/PieChart/PieChart.svelte +236 -0
- package/dist/PieChart/PieChart.svelte.d.ts +4 -0
- package/dist/PieChart/properties.d.ts +36 -0
- package/dist/PieChart/properties.js +1 -0
- package/dist/SankeyChart/SankeyChart.svelte +312 -0
- package/dist/SankeyChart/SankeyChart.svelte.d.ts +4 -0
- package/dist/SankeyChart/properties.d.ts +52 -0
- package/dist/SankeyChart/properties.js +1 -0
- package/dist/_chart/Axis.svelte +143 -0
- package/dist/_chart/Axis.svelte.d.ts +4 -0
- package/dist/_chart/ChartContainer.svelte +81 -0
- package/dist/_chart/ChartContainer.svelte.d.ts +4 -0
- package/dist/_chart/ChartTooltip.svelte +81 -0
- package/dist/_chart/ChartTooltip.svelte.d.ts +4 -0
- package/dist/_chart/Legend.svelte +59 -0
- package/dist/_chart/Legend.svelte.d.ts +4 -0
- package/dist/_chart/colors.d.ts +4 -0
- package/dist/_chart/colors.js +36 -0
- package/dist/_chart/format.d.ts +3 -0
- package/dist/_chart/format.js +28 -0
- package/dist/_chart/geometry.d.ts +24 -0
- package/dist/_chart/geometry.js +204 -0
- package/dist/_chart/paths.d.ts +4 -0
- package/dist/_chart/paths.js +138 -0
- package/dist/_chart/scales.d.ts +5 -0
- package/dist/_chart/scales.js +77 -0
- package/dist/_chart/types.d.ts +128 -0
- package/dist/_chart/types.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { LegendProperties } from './types';
|
|
3
|
+
|
|
4
|
+
let { items, position = 'bottom', customSnippet, classes }: LegendProperties = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
{#if items.length > 0}
|
|
8
|
+
<div class="chart-legend position-{position} {classes ?? ''}">
|
|
9
|
+
{#if typeof customSnippet === 'function'}
|
|
10
|
+
{@render customSnippet(items)}
|
|
11
|
+
{:else}
|
|
12
|
+
{#each items as item, i (i)}
|
|
13
|
+
<div class="legend-item">
|
|
14
|
+
<span class="legend-swatch" style="background: {item.color}"></span>
|
|
15
|
+
<span class="legend-label">{item.label}</span>
|
|
16
|
+
</div>
|
|
17
|
+
{/each}
|
|
18
|
+
{/if}
|
|
19
|
+
</div>
|
|
20
|
+
{/if}
|
|
21
|
+
|
|
22
|
+
<style>
|
|
23
|
+
.chart-legend {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-wrap: wrap;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
gap: var(--chart-legend-gap, 16px);
|
|
29
|
+
font-family: var(--chart-font-family, inherit);
|
|
30
|
+
padding: 8px 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.position-top {
|
|
34
|
+
padding-bottom: 12px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.position-bottom {
|
|
38
|
+
padding-top: 12px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.legend-item {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 6px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.legend-swatch {
|
|
48
|
+
display: inline-block;
|
|
49
|
+
width: var(--chart-legend-swatch-size, 12px);
|
|
50
|
+
height: var(--chart-legend-swatch-size, 12px);
|
|
51
|
+
border-radius: 2px;
|
|
52
|
+
flex-shrink: 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.legend-label {
|
|
56
|
+
font-size: var(--chart-legend-font-size, 12px);
|
|
57
|
+
color: var(--chart-legend-color, #333);
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const DEFAULT_PALETTE: string[];
|
|
2
|
+
export declare function getColor(index: number, palette?: string[]): string;
|
|
3
|
+
export declare function generatePalette(baseColor: string, count: number): string[];
|
|
4
|
+
export declare function getContrastColor(hex: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { hexToRgb, hexToHsv, hsvToHex } from '../utils';
|
|
2
|
+
export const DEFAULT_PALETTE = [
|
|
3
|
+
'#4e79a7',
|
|
4
|
+
'#f28e2b',
|
|
5
|
+
'#e15759',
|
|
6
|
+
'#76b7b2',
|
|
7
|
+
'#59a14f',
|
|
8
|
+
'#edc948',
|
|
9
|
+
'#b07aa1',
|
|
10
|
+
'#ff9da7',
|
|
11
|
+
'#9c755f',
|
|
12
|
+
'#bab0ac'
|
|
13
|
+
];
|
|
14
|
+
export function getColor(index, palette = DEFAULT_PALETTE) {
|
|
15
|
+
return palette[index % palette.length];
|
|
16
|
+
}
|
|
17
|
+
export function generatePalette(baseColor, count) {
|
|
18
|
+
const hsv = hexToHsv(baseColor);
|
|
19
|
+
if (hsv === null) {
|
|
20
|
+
return DEFAULT_PALETTE.slice(0, count);
|
|
21
|
+
}
|
|
22
|
+
const colors = [];
|
|
23
|
+
for (let i = 0; i < count; i++) {
|
|
24
|
+
const h = (hsv.h + i / count) % 1;
|
|
25
|
+
colors.push(hsvToHex(h, hsv.s, hsv.v));
|
|
26
|
+
}
|
|
27
|
+
return colors;
|
|
28
|
+
}
|
|
29
|
+
export function getContrastColor(hex) {
|
|
30
|
+
const rgb = hexToRgb(hex);
|
|
31
|
+
if (rgb === null) {
|
|
32
|
+
return '#000000';
|
|
33
|
+
}
|
|
34
|
+
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
35
|
+
return luminance > 0.5 ? '#000000' : '#ffffff';
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function formatNumber(value) {
|
|
2
|
+
const abs = Math.abs(value);
|
|
3
|
+
if (abs >= 1e9) {
|
|
4
|
+
return (value / 1e9).toFixed(1).replace(/\.0$/, '') + 'B';
|
|
5
|
+
}
|
|
6
|
+
if (abs >= 1e6) {
|
|
7
|
+
return (value / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
|
|
8
|
+
}
|
|
9
|
+
if (abs >= 1e3) {
|
|
10
|
+
return (value / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
|
|
11
|
+
}
|
|
12
|
+
if (Number.isInteger(value)) {
|
|
13
|
+
return value.toString();
|
|
14
|
+
}
|
|
15
|
+
return value.toFixed(1);
|
|
16
|
+
}
|
|
17
|
+
export function formatPercent(value, total) {
|
|
18
|
+
if (total === 0) {
|
|
19
|
+
return '0%';
|
|
20
|
+
}
|
|
21
|
+
return Math.round((value / total) * 100) + '%';
|
|
22
|
+
}
|
|
23
|
+
export function defaultTickFormat(value) {
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
return formatNumber(value);
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Margin, ChartDimensions, PieSliceLayout, ComputedSankeyNode, ComputedSankeyLink, StackedPoint } from './types';
|
|
2
|
+
export declare function computeChartDimensions(width: number, height: number, margin?: Partial<Margin>): ChartDimensions;
|
|
3
|
+
export declare function computePieLayout(data: Array<{
|
|
4
|
+
label: string;
|
|
5
|
+
value: number;
|
|
6
|
+
color?: string;
|
|
7
|
+
}>, startAngle?: number, padAngle?: number): PieSliceLayout[];
|
|
8
|
+
export declare function computeSankeyLayout(nodes: Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
color?: string;
|
|
12
|
+
}>, links: Array<{
|
|
13
|
+
source: string;
|
|
14
|
+
target: string;
|
|
15
|
+
value: number;
|
|
16
|
+
color?: string;
|
|
17
|
+
}>, width: number, height: number, nodeWidth?: number, nodePadding?: number, iterations?: number): {
|
|
18
|
+
nodes: ComputedSankeyNode[];
|
|
19
|
+
links: ComputedSankeyLink[];
|
|
20
|
+
};
|
|
21
|
+
export declare function computeStackedValues(seriesData: Array<Array<{
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
}>>): StackedPoint[][];
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
export function computeChartDimensions(width, height, margin = {}) {
|
|
2
|
+
const m = {
|
|
3
|
+
top: margin.top ?? 20,
|
|
4
|
+
right: margin.right ?? 20,
|
|
5
|
+
bottom: margin.bottom ?? 40,
|
|
6
|
+
left: margin.left ?? 50
|
|
7
|
+
};
|
|
8
|
+
return {
|
|
9
|
+
width,
|
|
10
|
+
height,
|
|
11
|
+
margin: m,
|
|
12
|
+
innerWidth: Math.max(0, width - m.left - m.right),
|
|
13
|
+
innerHeight: Math.max(0, height - m.top - m.bottom)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
// ── Pie layout ──────────────────────────────────────────────────
|
|
17
|
+
export function computePieLayout(data, startAngle = -Math.PI / 2, padAngle = 0) {
|
|
18
|
+
const total = data.reduce((sum, d) => sum + Math.max(0, d.value), 0);
|
|
19
|
+
if (total === 0) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const slices = [];
|
|
23
|
+
let angle = startAngle;
|
|
24
|
+
for (let i = 0; i < data.length; i++) {
|
|
25
|
+
const d = data[i];
|
|
26
|
+
const val = Math.max(0, d.value);
|
|
27
|
+
const sliceAngle = (val / total) * Math.PI * 2;
|
|
28
|
+
const start = angle + padAngle / 2;
|
|
29
|
+
const end = angle + sliceAngle - padAngle / 2;
|
|
30
|
+
slices.push({
|
|
31
|
+
index: i,
|
|
32
|
+
startAngle: start,
|
|
33
|
+
endAngle: end,
|
|
34
|
+
midAngle: (start + end) / 2,
|
|
35
|
+
value: val,
|
|
36
|
+
percentage: (val / total) * 100,
|
|
37
|
+
label: d.label,
|
|
38
|
+
color: d.color
|
|
39
|
+
});
|
|
40
|
+
angle += sliceAngle;
|
|
41
|
+
}
|
|
42
|
+
return slices;
|
|
43
|
+
}
|
|
44
|
+
// ── Sankey layout ───────────────────────────────────────────────
|
|
45
|
+
export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16, nodePadding = 8, iterations = 6) {
|
|
46
|
+
if (nodes.length === 0) {
|
|
47
|
+
return { nodes: [], links: [] };
|
|
48
|
+
}
|
|
49
|
+
// Build adjacency
|
|
50
|
+
const outgoing = new Map();
|
|
51
|
+
const incoming = new Map();
|
|
52
|
+
for (const n of nodes) {
|
|
53
|
+
outgoing.set(n.id, []);
|
|
54
|
+
incoming.set(n.id, []);
|
|
55
|
+
}
|
|
56
|
+
for (const l of links) {
|
|
57
|
+
outgoing.get(l.source)?.push({ target: l.target, value: l.value });
|
|
58
|
+
incoming.get(l.target)?.push({ source: l.source, value: l.value });
|
|
59
|
+
}
|
|
60
|
+
// Assign columns via topological ordering
|
|
61
|
+
const columns = new Map();
|
|
62
|
+
const visited = new Set();
|
|
63
|
+
function assignColumn(id) {
|
|
64
|
+
if (columns.has(id)) {
|
|
65
|
+
return columns.get(id);
|
|
66
|
+
}
|
|
67
|
+
if (visited.has(id)) {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
visited.add(id);
|
|
71
|
+
const deps = incoming.get(id) ?? [];
|
|
72
|
+
const col = deps.length === 0 ? 0 : Math.max(...deps.map((d) => assignColumn(d.source) + 1));
|
|
73
|
+
columns.set(id, col);
|
|
74
|
+
return col;
|
|
75
|
+
}
|
|
76
|
+
for (const n of nodes) {
|
|
77
|
+
assignColumn(n.id);
|
|
78
|
+
}
|
|
79
|
+
const maxCol = Math.max(0, ...columns.values());
|
|
80
|
+
// Compute node values (max of incoming/outgoing)
|
|
81
|
+
const nodeValues = new Map();
|
|
82
|
+
for (const n of nodes) {
|
|
83
|
+
const outVal = (outgoing.get(n.id) ?? []).reduce((s, l) => s + l.value, 0);
|
|
84
|
+
const inVal = (incoming.get(n.id) ?? []).reduce((s, l) => s + l.value, 0);
|
|
85
|
+
nodeValues.set(n.id, Math.max(outVal, inVal));
|
|
86
|
+
}
|
|
87
|
+
// Group nodes by column
|
|
88
|
+
const columnGroups = new Map();
|
|
89
|
+
for (const n of nodes) {
|
|
90
|
+
const col = columns.get(n.id) ?? 0;
|
|
91
|
+
if (!columnGroups.has(col)) {
|
|
92
|
+
columnGroups.set(col, []);
|
|
93
|
+
}
|
|
94
|
+
columnGroups.get(col).push(n.id);
|
|
95
|
+
}
|
|
96
|
+
const colWidth = maxCol === 0 ? 0 : (width - nodeWidth) / maxCol;
|
|
97
|
+
// Initialize y positions
|
|
98
|
+
const nodeY = new Map();
|
|
99
|
+
const nodeH = new Map();
|
|
100
|
+
for (const [, ids] of columnGroups) {
|
|
101
|
+
const totalValue = ids.reduce((s, id) => s + (nodeValues.get(id) ?? 0), 0);
|
|
102
|
+
const availableHeight = height - (ids.length - 1) * nodePadding;
|
|
103
|
+
let y = 0;
|
|
104
|
+
for (const id of ids) {
|
|
105
|
+
const val = nodeValues.get(id) ?? 0;
|
|
106
|
+
const h = totalValue > 0 ? (val / totalValue) * availableHeight : availableHeight / ids.length;
|
|
107
|
+
nodeY.set(id, y);
|
|
108
|
+
nodeH.set(id, Math.max(1, h));
|
|
109
|
+
y += h + nodePadding;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Iterative relaxation (upstream pass)
|
|
113
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
114
|
+
for (const [, ids] of columnGroups) {
|
|
115
|
+
for (const id of ids) {
|
|
116
|
+
const deps = incoming.get(id) ?? [];
|
|
117
|
+
if (deps.length > 0) {
|
|
118
|
+
const weightedY = deps.reduce((s, d) => {
|
|
119
|
+
const sy = nodeY.get(d.source) ?? 0;
|
|
120
|
+
const sh = nodeH.get(d.source) ?? 0;
|
|
121
|
+
return s + (sy + sh / 2) * d.value;
|
|
122
|
+
}, 0) / deps.reduce((s, d) => s + d.value, 0);
|
|
123
|
+
nodeY.set(id, Math.max(0, weightedY - (nodeH.get(id) ?? 0) / 2));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Resolve overlaps
|
|
127
|
+
ids.sort((a, b) => (nodeY.get(a) ?? 0) - (nodeY.get(b) ?? 0));
|
|
128
|
+
let y = 0;
|
|
129
|
+
for (const id of ids) {
|
|
130
|
+
const cy = nodeY.get(id) ?? 0;
|
|
131
|
+
if (cy < y) {
|
|
132
|
+
nodeY.set(id, y);
|
|
133
|
+
}
|
|
134
|
+
y = (nodeY.get(id) ?? 0) + (nodeH.get(id) ?? 0) + nodePadding;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Build computed nodes
|
|
139
|
+
const computedNodes = nodes.map((n) => ({
|
|
140
|
+
id: n.id,
|
|
141
|
+
label: n.label ?? n.id,
|
|
142
|
+
x: (columns.get(n.id) ?? 0) * colWidth,
|
|
143
|
+
y: nodeY.get(n.id) ?? 0,
|
|
144
|
+
width: nodeWidth,
|
|
145
|
+
height: nodeH.get(n.id) ?? 0,
|
|
146
|
+
value: nodeValues.get(n.id) ?? 0,
|
|
147
|
+
color: n.color,
|
|
148
|
+
column: columns.get(n.id) ?? 0
|
|
149
|
+
}));
|
|
150
|
+
const nodeById = new Map(computedNodes.map((n) => [n.id, n]));
|
|
151
|
+
// Build computed links with Bezier paths
|
|
152
|
+
const sourceOffsets = new Map();
|
|
153
|
+
const targetOffsets = new Map();
|
|
154
|
+
for (const n of nodes) {
|
|
155
|
+
sourceOffsets.set(n.id, nodeY.get(n.id) ?? 0);
|
|
156
|
+
targetOffsets.set(n.id, nodeY.get(n.id) ?? 0);
|
|
157
|
+
}
|
|
158
|
+
const computedLinks = links.map((l) => {
|
|
159
|
+
const sourceNode = nodeById.get(l.source);
|
|
160
|
+
const targetNode = nodeById.get(l.target);
|
|
161
|
+
const sVal = nodeValues.get(l.source) ?? 1;
|
|
162
|
+
const sourceH = nodeH.get(l.source) ?? 0;
|
|
163
|
+
const linkWidth = Math.max(1, (l.value / Math.max(sVal, 1)) * sourceH);
|
|
164
|
+
const sx = (sourceNode?.x ?? 0) + nodeWidth;
|
|
165
|
+
const sy = (sourceOffsets.get(l.source) ?? 0) + linkWidth / 2;
|
|
166
|
+
const tx = targetNode?.x ?? 0;
|
|
167
|
+
const ty = (targetOffsets.get(l.target) ?? 0) + linkWidth / 2;
|
|
168
|
+
sourceOffsets.set(l.source, (sourceOffsets.get(l.source) ?? 0) + linkWidth);
|
|
169
|
+
targetOffsets.set(l.target, (targetOffsets.get(l.target) ?? 0) + linkWidth);
|
|
170
|
+
const midX = (sx + tx) / 2;
|
|
171
|
+
const path = `M ${sx} ${sy} C ${midX} ${sy}, ${midX} ${ty}, ${tx} ${ty}`;
|
|
172
|
+
return {
|
|
173
|
+
source: l.source,
|
|
174
|
+
target: l.target,
|
|
175
|
+
value: l.value,
|
|
176
|
+
color: l.color,
|
|
177
|
+
sourceX: sx,
|
|
178
|
+
sourceY: sy,
|
|
179
|
+
targetX: tx,
|
|
180
|
+
targetY: ty,
|
|
181
|
+
width: linkWidth,
|
|
182
|
+
path
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
return { nodes: computedNodes, links: computedLinks };
|
|
186
|
+
}
|
|
187
|
+
// ── Stacked values ──────────────────────────────────────────────
|
|
188
|
+
export function computeStackedValues(seriesData) {
|
|
189
|
+
if (seriesData.length === 0) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const stacked = [];
|
|
193
|
+
const baselines = new Map();
|
|
194
|
+
for (const series of seriesData) {
|
|
195
|
+
const stackedSeries = [];
|
|
196
|
+
for (const point of series) {
|
|
197
|
+
const base = baselines.get(point.x) ?? 0;
|
|
198
|
+
stackedSeries.push({ x: point.x, y0: base, y1: base + point.y });
|
|
199
|
+
baselines.set(point.x, base + point.y);
|
|
200
|
+
}
|
|
201
|
+
stacked.push(stackedSeries);
|
|
202
|
+
}
|
|
203
|
+
return stacked;
|
|
204
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Point, CurveType } from './types';
|
|
2
|
+
export declare function arcPath(cx: number, cy: number, innerR: number, outerR: number, startAngle: number, endAngle: number): string;
|
|
3
|
+
export declare function linePath(points: Point[], curve?: CurveType): string;
|
|
4
|
+
export declare function areaPath(points: Point[], baseline: number, curve?: CurveType): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export function arcPath(cx, cy, innerR, outerR, startAngle, endAngle) {
|
|
2
|
+
const fullCircle = Math.abs(endAngle - startAngle) >= Math.PI * 2 - 0.001;
|
|
3
|
+
if (fullCircle) {
|
|
4
|
+
if (innerR > 0) {
|
|
5
|
+
return (`M ${cx + outerR} ${cy}` +
|
|
6
|
+
`A ${outerR} ${outerR} 0 1 1 ${cx - outerR} ${cy}` +
|
|
7
|
+
`A ${outerR} ${outerR} 0 1 1 ${cx + outerR} ${cy}` +
|
|
8
|
+
`M ${cx + innerR} ${cy}` +
|
|
9
|
+
`A ${innerR} ${innerR} 0 1 0 ${cx - innerR} ${cy}` +
|
|
10
|
+
`A ${innerR} ${innerR} 0 1 0 ${cx + innerR} ${cy}Z`);
|
|
11
|
+
}
|
|
12
|
+
return (`M ${cx + outerR} ${cy}` +
|
|
13
|
+
`A ${outerR} ${outerR} 0 1 1 ${cx - outerR} ${cy}` +
|
|
14
|
+
`A ${outerR} ${outerR} 0 1 1 ${cx + outerR} ${cy}Z`);
|
|
15
|
+
}
|
|
16
|
+
const x1 = cx + outerR * Math.cos(startAngle);
|
|
17
|
+
const y1 = cy + outerR * Math.sin(startAngle);
|
|
18
|
+
const x2 = cx + outerR * Math.cos(endAngle);
|
|
19
|
+
const y2 = cy + outerR * Math.sin(endAngle);
|
|
20
|
+
const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
|
|
21
|
+
if (innerR > 0) {
|
|
22
|
+
const x3 = cx + innerR * Math.cos(endAngle);
|
|
23
|
+
const y3 = cy + innerR * Math.sin(endAngle);
|
|
24
|
+
const x4 = cx + innerR * Math.cos(startAngle);
|
|
25
|
+
const y4 = cy + innerR * Math.sin(startAngle);
|
|
26
|
+
return (`M ${x1} ${y1}` +
|
|
27
|
+
`A ${outerR} ${outerR} 0 ${largeArc} 1 ${x2} ${y2}` +
|
|
28
|
+
`L ${x3} ${y3}` +
|
|
29
|
+
`A ${innerR} ${innerR} 0 ${largeArc} 0 ${x4} ${y4}Z`);
|
|
30
|
+
}
|
|
31
|
+
return `M ${cx} ${cy}` + `L ${x1} ${y1}` + `A ${outerR} ${outerR} 0 ${largeArc} 1 ${x2} ${y2}Z`;
|
|
32
|
+
}
|
|
33
|
+
function linearPath(points) {
|
|
34
|
+
if (points.length === 0) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
let d = `M ${points[0].x} ${points[0].y}`;
|
|
38
|
+
for (let i = 1; i < points.length; i++) {
|
|
39
|
+
d += ` L ${points[i].x} ${points[i].y}`;
|
|
40
|
+
}
|
|
41
|
+
return d;
|
|
42
|
+
}
|
|
43
|
+
function stepPath(points) {
|
|
44
|
+
if (points.length === 0) {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
let d = `M ${points[0].x} ${points[0].y}`;
|
|
48
|
+
for (let i = 1; i < points.length; i++) {
|
|
49
|
+
const midX = (points[i - 1].x + points[i].x) / 2;
|
|
50
|
+
d += ` L ${midX} ${points[i - 1].y} L ${midX} ${points[i].y}`;
|
|
51
|
+
}
|
|
52
|
+
if (points.length > 1) {
|
|
53
|
+
d += ` L ${points[points.length - 1].x} ${points[points.length - 1].y}`;
|
|
54
|
+
}
|
|
55
|
+
return d;
|
|
56
|
+
}
|
|
57
|
+
function monotoneTangents(points) {
|
|
58
|
+
const n = points.length;
|
|
59
|
+
const slopes = [];
|
|
60
|
+
const tangents = new Array(n);
|
|
61
|
+
for (let i = 0; i < n - 1; i++) {
|
|
62
|
+
const dx = points[i + 1].x - points[i].x;
|
|
63
|
+
slopes.push(dx === 0 ? 0 : (points[i + 1].y - points[i].y) / dx);
|
|
64
|
+
}
|
|
65
|
+
tangents[0] = slopes[0] ?? 0;
|
|
66
|
+
tangents[n - 1] = slopes[n - 2] ?? 0;
|
|
67
|
+
for (let i = 1; i < n - 1; i++) {
|
|
68
|
+
if (slopes[i - 1] * slopes[i] <= 0) {
|
|
69
|
+
tangents[i] = 0;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
tangents[i] = (slopes[i - 1] + slopes[i]) / 2;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Fritsch-Carlson: ensure monotonicity
|
|
76
|
+
for (let i = 0; i < n - 1; i++) {
|
|
77
|
+
if (Math.abs(slopes[i]) < 1e-10) {
|
|
78
|
+
tangents[i] = 0;
|
|
79
|
+
tangents[i + 1] = 0;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const alpha = tangents[i] / slopes[i];
|
|
83
|
+
const beta = tangents[i + 1] / slopes[i];
|
|
84
|
+
const s = alpha * alpha + beta * beta;
|
|
85
|
+
if (s > 9) {
|
|
86
|
+
const t = 3 / Math.sqrt(s);
|
|
87
|
+
tangents[i] = t * alpha * slopes[i];
|
|
88
|
+
tangents[i + 1] = t * beta * slopes[i];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return tangents;
|
|
93
|
+
}
|
|
94
|
+
function monotonePath(points) {
|
|
95
|
+
if (points.length === 0) {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
if (points.length === 1) {
|
|
99
|
+
return `M ${points[0].x} ${points[0].y}`;
|
|
100
|
+
}
|
|
101
|
+
if (points.length === 2) {
|
|
102
|
+
return linearPath(points);
|
|
103
|
+
}
|
|
104
|
+
const tangents = monotoneTangents(points);
|
|
105
|
+
let d = `M ${points[0].x} ${points[0].y}`;
|
|
106
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
107
|
+
const dx = (points[i + 1].x - points[i].x) / 3;
|
|
108
|
+
const cp1x = points[i].x + dx;
|
|
109
|
+
const cp1y = points[i].y + dx * tangents[i];
|
|
110
|
+
const cp2x = points[i + 1].x - dx;
|
|
111
|
+
const cp2y = points[i + 1].y - dx * tangents[i + 1];
|
|
112
|
+
d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${points[i + 1].x} ${points[i + 1].y}`;
|
|
113
|
+
}
|
|
114
|
+
return d;
|
|
115
|
+
}
|
|
116
|
+
export function linePath(points, curve = 'linear') {
|
|
117
|
+
if (points.length < 2) {
|
|
118
|
+
return points.length === 1 ? `M ${points[0].x} ${points[0].y}` : '';
|
|
119
|
+
}
|
|
120
|
+
switch (curve) {
|
|
121
|
+
case 'monotone':
|
|
122
|
+
case 'natural':
|
|
123
|
+
return monotonePath(points);
|
|
124
|
+
case 'step':
|
|
125
|
+
return stepPath(points);
|
|
126
|
+
default:
|
|
127
|
+
return linearPath(points);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function areaPath(points, baseline, curve = 'linear') {
|
|
131
|
+
if (points.length === 0) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
const topPath = linePath(points, curve);
|
|
135
|
+
const lastPoint = points[points.length - 1];
|
|
136
|
+
const firstPoint = points[0];
|
|
137
|
+
return topPath + ` L ${lastPoint.x} ${baseline} L ${firstPoint.x} ${baseline} Z`;
|
|
138
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LinearScale, BandScale } from './types';
|
|
2
|
+
export declare function niceLinearDomain(min: number, max: number): [number, number];
|
|
3
|
+
export declare function computeLinearTicks(domain: [number, number], count?: number): number[];
|
|
4
|
+
export declare function createLinearScale(domain: [number, number], range: [number, number]): LinearScale;
|
|
5
|
+
export declare function createBandScale(domain: string[], range: [number, number], padding?: number): BandScale;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function niceLinearDomain(min, max) {
|
|
2
|
+
if (min === max) {
|
|
3
|
+
return min === 0 ? [0, 1] : [min > 0 ? 0 : min * 2, max > 0 ? max * 2 : 0];
|
|
4
|
+
}
|
|
5
|
+
const range = max - min;
|
|
6
|
+
const exp = Math.floor(Math.log10(range));
|
|
7
|
+
const step = Math.pow(10, exp);
|
|
8
|
+
const niceMin = Math.floor(min / step) * step;
|
|
9
|
+
const niceMax = Math.ceil(max / step) * step;
|
|
10
|
+
return [niceMin, niceMax];
|
|
11
|
+
}
|
|
12
|
+
export function computeLinearTicks(domain, count = 5) {
|
|
13
|
+
const [min, max] = domain;
|
|
14
|
+
if (min === max) {
|
|
15
|
+
return [min];
|
|
16
|
+
}
|
|
17
|
+
const rawStep = (max - min) / count;
|
|
18
|
+
const exp = Math.floor(Math.log10(rawStep));
|
|
19
|
+
const base = Math.pow(10, exp);
|
|
20
|
+
let step;
|
|
21
|
+
const ratio = rawStep / base;
|
|
22
|
+
if (ratio <= 1.5) {
|
|
23
|
+
step = base;
|
|
24
|
+
}
|
|
25
|
+
else if (ratio <= 3) {
|
|
26
|
+
step = base * 2;
|
|
27
|
+
}
|
|
28
|
+
else if (ratio <= 7) {
|
|
29
|
+
step = base * 5;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
step = base * 10;
|
|
33
|
+
}
|
|
34
|
+
const ticks = [];
|
|
35
|
+
let tick = Math.ceil(min / step) * step;
|
|
36
|
+
while (tick <= max + step * 0.001) {
|
|
37
|
+
ticks.push(Math.round(tick * 1e10) / 1e10);
|
|
38
|
+
tick += step;
|
|
39
|
+
}
|
|
40
|
+
return ticks;
|
|
41
|
+
}
|
|
42
|
+
export function createLinearScale(domain, range) {
|
|
43
|
+
const [d0, d1] = domain;
|
|
44
|
+
const [r0, r1] = range;
|
|
45
|
+
const dSpan = d1 - d0 || 1;
|
|
46
|
+
const rSpan = r1 - r0;
|
|
47
|
+
const fn = (value) => r0 + ((value - d0) / dSpan) * rSpan;
|
|
48
|
+
return Object.assign(fn, {
|
|
49
|
+
domain,
|
|
50
|
+
range,
|
|
51
|
+
ticks: (count) => computeLinearTicks(domain, count),
|
|
52
|
+
invert: (pixel) => d0 + ((pixel - r0) / rSpan) * dSpan
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function createBandScale(domain, range, padding = 0.1) {
|
|
56
|
+
const [r0, r1] = range;
|
|
57
|
+
const n = domain.length || 1;
|
|
58
|
+
const totalRange = r1 - r0;
|
|
59
|
+
// d3-scale band formula (paddingOuter = 0): step = range / (n - padding)
|
|
60
|
+
// Derivation: n*bandwidth + (n-1)*gap = totalRange,
|
|
61
|
+
// with bandwidth = step*(1-padding) and gap = step*padding
|
|
62
|
+
// → step*(n - padding) = totalRange
|
|
63
|
+
const step = totalRange / Math.max(1, n - padding);
|
|
64
|
+
const bandwidth = step * (1 - padding);
|
|
65
|
+
const indexMap = new Map();
|
|
66
|
+
domain.forEach((label, i) => indexMap.set(label, i));
|
|
67
|
+
const fn = (label) => {
|
|
68
|
+
const idx = indexMap.get(label) ?? 0;
|
|
69
|
+
return r0 + idx * step;
|
|
70
|
+
};
|
|
71
|
+
return Object.assign(fn, {
|
|
72
|
+
domain,
|
|
73
|
+
range,
|
|
74
|
+
bandwidth,
|
|
75
|
+
step
|
|
76
|
+
});
|
|
77
|
+
}
|