@agions/taroviz 1.11.5 → 2.0.3
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/CHANGELOG.md +245 -0
- package/README.md +31 -46
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/vendors.js +1 -1
- package/dist/cjs/vendors~echarts.js +1 -1
- package/dist/esm/index.js +1 -14270
- package/dist/esm/vendors.js +1 -16770
- package/dist/esm/vendors~echarts.js +1 -59417
- package/package.json +10 -15
- package/src/adapters/h5/index.ts +38 -38
- package/src/adapters/index.ts +32 -34
- package/src/adapters/types.ts +23 -55
- package/src/charts/boxplot/types.ts +2 -2
- package/src/charts/common/BaseChartWrapper.tsx +9 -7
- package/src/charts/createChartComponent.tsx +9 -21
- package/src/charts/createOptionChartComponent.tsx +32 -0
- package/src/charts/funnel/__tests__/index.test.tsx +99 -0
- package/src/charts/funnel/index.tsx +64 -0
- package/src/charts/funnel/types.ts +6 -0
- package/src/charts/graph/__tests__/index.test.tsx +116 -0
- package/src/charts/graph/index.tsx +70 -0
- package/src/charts/graph/types.ts +6 -0
- package/src/charts/heatmap/__tests__/index.test.tsx +139 -0
- package/src/charts/heatmap/index.tsx +107 -0
- package/src/charts/heatmap/types.ts +6 -0
- package/src/charts/index.ts +47 -57
- package/src/charts/liquid/__tests__/index.test.tsx +52 -0
- package/src/charts/liquid/index.tsx +7 -133
- package/src/charts/liquid/types.ts +6 -6
- package/src/charts/parallel/types.ts +3 -3
- package/src/charts/radar/__tests__/index.test.tsx +210 -0
- package/src/charts/radar/index.tsx +147 -0
- package/src/charts/radar/types.ts +13 -0
- package/src/charts/sankey/__tests__/index.test.tsx +124 -0
- package/src/charts/sankey/index.tsx +70 -0
- package/src/charts/sankey/types.ts +6 -0
- package/src/charts/tree/__tests__/index.test.tsx +71 -0
- package/src/charts/tree/index.tsx +1 -1
- package/src/charts/tree/types.ts +8 -8
- package/src/charts/types.ts +208 -106
- package/src/charts/wordcloud/__tests__/index.test.tsx +106 -0
- package/src/charts/wordcloud/index.tsx +79 -0
- package/src/charts/wordcloud/types.ts +6 -0
- package/src/components/DataFilter/index.tsx +7 -6
- package/src/core/animation/types.ts +6 -6
- package/src/core/components/Annotation.tsx +6 -6
- package/src/core/components/BaseChart.tsx +97 -133
- package/src/core/components/LazyChart.tsx +3 -8
- package/src/core/components/hooks/index.ts +6 -2
- package/src/core/components/hooks/usePerformance.ts +8 -2
- package/src/core/components/hooks/useVirtualScroll.ts +2 -1
- package/src/core/types/common.ts +2 -1
- package/src/core/types/platform.ts +1 -0
- package/src/core/utils/__tests__/deepClone.test.ts +317 -0
- package/src/core/utils/__tests__/index.test.ts +2 -1
- package/src/core/utils/chartInstances.ts +13 -0
- package/src/core/utils/common.ts +20 -36
- package/src/core/utils/deepClone.ts +114 -0
- package/src/core/utils/download.ts +22 -28
- package/src/core/utils/drillDown.ts +1 -0
- package/src/core/utils/events.ts +12 -0
- package/src/core/utils/export/ExportUtils.ts +2 -1
- package/src/core/utils/format.ts +44 -0
- package/src/core/utils/index.ts +18 -159
- package/src/core/utils/merge.ts +25 -0
- package/src/core/utils/performance/PerformanceAnalyzer.ts +3 -1
- package/src/core/utils/performance/hooks.ts +7 -0
- package/src/core/utils/performance/index.ts +2 -0
- package/src/{hooks → core/utils/performance}/useAnimation.ts +6 -5
- package/src/{hooks → core/utils/performance}/useDataZoom.ts +7 -2
- package/src/{hooks → core/utils/performance}/usePerformance.ts +39 -39
- package/src/{hooks → core/utils/performance}/usePerformanceHooks.ts +39 -39
- package/src/core/utils/runtime.ts +190 -0
- package/src/editor/components/ThemeSelector.tsx +3 -3
- package/src/hooks/chartConnectHelpers.ts +6 -0
- package/src/hooks/index.ts +54 -626
- package/src/hooks/types.ts +27 -0
- package/src/hooks/useChartAutoResize.ts +73 -0
- package/src/hooks/useChartConnect.ts +5 -1
- package/src/hooks/useChartDownload.ts +1 -1
- package/src/hooks/useChartHistory.ts +1 -3
- package/src/hooks/useChartInit.ts +59 -0
- package/src/hooks/useChartOptions.ts +259 -0
- package/src/hooks/useChartPerformance.ts +109 -0
- package/src/hooks/useChartSelection.ts +23 -12
- package/src/hooks/useChartTheme.ts +51 -0
- package/src/hooks/useDataTransform.ts +19 -4
- package/src/index.ts +5 -10
- package/src/react-dom.d.ts +3 -3
- package/src/themes/index.ts +30 -855
- package/src/themes/palettes/blue-green.ts +13 -0
- package/src/themes/palettes/chalk.ts +13 -0
- package/src/themes/palettes/cyber.ts +44 -0
- package/src/themes/palettes/dark.ts +52 -0
- package/src/themes/palettes/default.ts +52 -0
- package/src/themes/palettes/elegant.ts +34 -0
- package/src/themes/palettes/forest.ts +13 -0
- package/src/themes/palettes/glass.ts +49 -0
- package/src/themes/palettes/golden.ts +13 -0
- package/src/themes/palettes/neon.ts +43 -0
- package/src/themes/palettes/ocean.ts +39 -0
- package/src/themes/palettes/pastel.ts +37 -0
- package/src/themes/palettes/purple-passion.ts +13 -0
- package/src/themes/palettes/retro.ts +33 -0
- package/src/themes/palettes/sunset.ts +40 -0
- package/src/themes/palettes/walden.ts +13 -0
- package/src/themes/registry.ts +184 -0
- package/src/themes/types.ts +213 -0
- package/src/core/utils/codeGenerator/CodeGenerator.ts +0 -669
- package/src/core/utils/codeGenerator/index.ts +0 -13
- package/src/core/utils/codeGenerator/types.ts +0 -198
- package/src/core/utils/configGenerator/ConfigGenerator.ts +0 -583
- package/src/core/utils/configGenerator/index.ts +0 -13
- package/src/core/utils/configGenerator/types.ts +0 -449
- package/src/core/utils/debug/DebugPanel.tsx +0 -640
- package/src/core/utils/debug/debugger.ts +0 -322
- package/src/core/utils/debug/index.ts +0 -21
- package/src/core/utils/debug/types.ts +0 -142
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { deepClone, deepMerge } from '../deepClone';
|
|
2
|
+
|
|
3
|
+
describe('deepClone', () => {
|
|
4
|
+
// Primitives
|
|
5
|
+
it('clones primitives', () => {
|
|
6
|
+
expect(deepClone(42)).toBe(42);
|
|
7
|
+
expect(deepClone('hello')).toBe('hello');
|
|
8
|
+
expect(deepClone(true)).toBe(true);
|
|
9
|
+
expect(deepClone(false)).toBe(false);
|
|
10
|
+
expect(deepClone(0)).toBe(0);
|
|
11
|
+
expect(deepClone(-1)).toBe(-1);
|
|
12
|
+
expect(deepClone(3.14)).toBe(3.14);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('handles null and undefined', () => {
|
|
16
|
+
expect(deepClone(null)).toBe(null);
|
|
17
|
+
expect(deepClone(undefined)).toBe(undefined);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Objects
|
|
21
|
+
it('clones plain objects', () => {
|
|
22
|
+
const obj = { a: 1, b: 'two', c: true };
|
|
23
|
+
const clone = deepClone(obj);
|
|
24
|
+
expect(clone).toEqual(obj);
|
|
25
|
+
expect(clone).not.toBe(obj);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('clones nested objects', () => {
|
|
29
|
+
const obj = { a: { b: { c: { d: 42 } } } };
|
|
30
|
+
const clone = deepClone(obj);
|
|
31
|
+
expect(clone).toEqual(obj);
|
|
32
|
+
expect(clone).not.toBe(obj);
|
|
33
|
+
expect(clone.a).not.toBe(obj.a);
|
|
34
|
+
expect(clone.a.b).not.toBe(obj.a.b);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Arrays
|
|
38
|
+
it('clones arrays', () => {
|
|
39
|
+
const arr = [1, 2, 3, [4, 5]];
|
|
40
|
+
const clone = deepClone(arr);
|
|
41
|
+
expect(clone).toEqual(arr);
|
|
42
|
+
expect(clone).not.toBe(arr);
|
|
43
|
+
expect(clone[3]).not.toBe(arr[3]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('clones arrays of objects', () => {
|
|
47
|
+
const arr = [{ a: 1 }, { b: 2 }];
|
|
48
|
+
const clone = deepClone(arr);
|
|
49
|
+
expect(clone).toEqual(arr);
|
|
50
|
+
expect(clone[0]).not.toBe(arr[0]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Date
|
|
54
|
+
it('clones Date objects', () => {
|
|
55
|
+
const date = new Date('2024-01-15');
|
|
56
|
+
const clone = deepClone(date);
|
|
57
|
+
expect(clone).toEqual(date);
|
|
58
|
+
expect(clone).not.toBe(date);
|
|
59
|
+
expect(clone.getTime()).toBe(date.getTime());
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// RegExp
|
|
63
|
+
it('clones RegExp objects', () => {
|
|
64
|
+
const re = /foo/gi;
|
|
65
|
+
const clone = deepClone(re);
|
|
66
|
+
expect(clone.source).toBe('foo');
|
|
67
|
+
expect(clone.flags).toBe('gi');
|
|
68
|
+
expect(clone).not.toBe(re);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Functions
|
|
72
|
+
it('preserves function references', () => {
|
|
73
|
+
const fn = (x: number) => x * 2;
|
|
74
|
+
expect(deepClone(fn)).toBe(fn);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('preserves nested function references', () => {
|
|
78
|
+
const fn = () => 'hello';
|
|
79
|
+
const obj = { formatter: fn, nested: { callback: fn } };
|
|
80
|
+
const clone = deepClone(obj);
|
|
81
|
+
expect(clone.formatter).toBe(fn);
|
|
82
|
+
expect(clone.nested.callback).toBe(fn);
|
|
83
|
+
expect(clone).not.toBe(obj);
|
|
84
|
+
expect(clone.nested).not.toBe(obj.nested);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Map
|
|
88
|
+
it('clones Map objects', () => {
|
|
89
|
+
const map = new Map<string, number>([
|
|
90
|
+
['a', 1],
|
|
91
|
+
['b', 2],
|
|
92
|
+
]);
|
|
93
|
+
const clone = deepClone(map);
|
|
94
|
+
expect(clone).not.toBe(map);
|
|
95
|
+
expect(clone.get('a')).toBe(1);
|
|
96
|
+
expect(clone.get('b')).toBe(2);
|
|
97
|
+
expect(clone.size).toBe(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('clones Map with object keys and values', () => {
|
|
101
|
+
const key = { id: 1 };
|
|
102
|
+
const val = { name: 'test' };
|
|
103
|
+
const map = new Map([[key, val]]);
|
|
104
|
+
const clone = deepClone(map);
|
|
105
|
+
expect(clone).not.toBe(map);
|
|
106
|
+
// Keys are cloned, so we can't look up with the original key
|
|
107
|
+
const clonedEntries = Array.from(clone.entries());
|
|
108
|
+
expect(clonedEntries).toHaveLength(1);
|
|
109
|
+
expect(clonedEntries[0][0]).toEqual(key);
|
|
110
|
+
expect(clonedEntries[0][0]).not.toBe(key);
|
|
111
|
+
expect(clonedEntries[0][1]).toEqual(val);
|
|
112
|
+
expect(clonedEntries[0][1]).not.toBe(val);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Set
|
|
116
|
+
it('clones Set objects', () => {
|
|
117
|
+
const set = new Set([1, 2, 3]);
|
|
118
|
+
const clone = deepClone(set);
|
|
119
|
+
expect(clone).not.toBe(set);
|
|
120
|
+
expect(clone.size).toBe(3);
|
|
121
|
+
expect(clone.has(1)).toBe(true);
|
|
122
|
+
expect(clone.has(2)).toBe(true);
|
|
123
|
+
expect(clone.has(3)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('clones Set with object values', () => {
|
|
127
|
+
const obj = { a: 1 };
|
|
128
|
+
const set = new Set([obj]);
|
|
129
|
+
const clone = deepClone(set);
|
|
130
|
+
const clonedValues = Array.from(clone.values());
|
|
131
|
+
expect(clonedValues[0]).toEqual(obj);
|
|
132
|
+
expect(clonedValues[0]).not.toBe(obj);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Circular references
|
|
136
|
+
it('handles circular references in objects', () => {
|
|
137
|
+
const obj: Record<string, unknown> = { a: 1 };
|
|
138
|
+
obj.self = obj;
|
|
139
|
+
const clone = deepClone(obj);
|
|
140
|
+
expect(clone.a).toBe(1);
|
|
141
|
+
expect(clone.self).toBe(clone);
|
|
142
|
+
expect(clone).not.toBe(obj);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('handles circular references in arrays', () => {
|
|
146
|
+
const arr: unknown[] = [1, 2];
|
|
147
|
+
arr.push(arr);
|
|
148
|
+
const clone = deepClone(arr);
|
|
149
|
+
expect(clone[0]).toBe(1);
|
|
150
|
+
expect(clone[1]).toBe(2);
|
|
151
|
+
expect(clone[2]).toBe(clone);
|
|
152
|
+
expect(clone).not.toBe(arr);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('handles cross-reference between objects', () => {
|
|
156
|
+
const shared = { value: 42 };
|
|
157
|
+
const obj = { a: shared, b: shared };
|
|
158
|
+
const clone = deepClone(obj);
|
|
159
|
+
expect(clone.a).toEqual(shared);
|
|
160
|
+
expect(clone.b).toEqual(shared);
|
|
161
|
+
// Both references should point to the same clone
|
|
162
|
+
expect(clone.a).toBe(clone.b);
|
|
163
|
+
expect(clone.a).not.toBe(shared);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ECharts-like option objects
|
|
167
|
+
it('clones ECharts-like option objects', () => {
|
|
168
|
+
const formatter = (params: { value: number }) => `${params.value}%`;
|
|
169
|
+
const options = {
|
|
170
|
+
title: {
|
|
171
|
+
text: 'Sales',
|
|
172
|
+
subtext: '2024',
|
|
173
|
+
},
|
|
174
|
+
tooltip: {
|
|
175
|
+
trigger: 'axis' as const,
|
|
176
|
+
formatter,
|
|
177
|
+
},
|
|
178
|
+
xAxis: {
|
|
179
|
+
type: 'category' as const,
|
|
180
|
+
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
|
|
181
|
+
},
|
|
182
|
+
yAxis: {
|
|
183
|
+
type: 'value' as const,
|
|
184
|
+
},
|
|
185
|
+
series: [
|
|
186
|
+
{
|
|
187
|
+
name: 'Sales',
|
|
188
|
+
type: 'line' as const,
|
|
189
|
+
data: [150, 230, 224, 218, 135],
|
|
190
|
+
label: {
|
|
191
|
+
show: true,
|
|
192
|
+
formatter: (params: { value: number }) => `${params.value}`,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const clone = deepClone(options);
|
|
199
|
+
|
|
200
|
+
// Structure equality
|
|
201
|
+
expect(clone).toEqual(options);
|
|
202
|
+
// Deep copy
|
|
203
|
+
expect(clone).not.toBe(options);
|
|
204
|
+
expect(clone.title).not.toBe(options.title);
|
|
205
|
+
expect(clone.series).not.toBe(options.series);
|
|
206
|
+
expect(clone.series[0]).not.toBe(options.series[0]);
|
|
207
|
+
// Function preserved
|
|
208
|
+
expect(clone.tooltip.formatter).toBe(formatter);
|
|
209
|
+
expect(clone.series[0].label.formatter).toBe(options.series[0].label.formatter);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Mixed types
|
|
213
|
+
it('clones complex mixed structures', () => {
|
|
214
|
+
const obj = {
|
|
215
|
+
str: 'hello',
|
|
216
|
+
num: 42,
|
|
217
|
+
bool: true,
|
|
218
|
+
nil: null,
|
|
219
|
+
undef: undefined,
|
|
220
|
+
date: new Date('2024-06-01'),
|
|
221
|
+
regex: /test/gi,
|
|
222
|
+
arr: [1, [2, 3], { a: 4 }],
|
|
223
|
+
fn: () => 'test',
|
|
224
|
+
map: new Map([['key', { nested: true }]]),
|
|
225
|
+
set: new Set([{ x: 1 }, { x: 2 }]),
|
|
226
|
+
nested: {
|
|
227
|
+
deep: {
|
|
228
|
+
deeper: {
|
|
229
|
+
value: 'bottom',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const clone = deepClone(obj);
|
|
236
|
+
|
|
237
|
+
expect(clone.str).toBe('hello');
|
|
238
|
+
expect(clone.num).toBe(42);
|
|
239
|
+
expect(clone.bool).toBe(true);
|
|
240
|
+
expect(clone.nil).toBe(null);
|
|
241
|
+
expect(clone.undef).toBe(undefined);
|
|
242
|
+
expect(clone.date).toEqual(obj.date);
|
|
243
|
+
expect(clone.date).not.toBe(obj.date);
|
|
244
|
+
expect(clone.regex.source).toBe('test');
|
|
245
|
+
expect(clone.regex).not.toBe(obj.regex);
|
|
246
|
+
expect(clone.arr).toEqual([1, [2, 3], { a: 4 }]);
|
|
247
|
+
expect(clone.arr).not.toBe(obj.arr);
|
|
248
|
+
expect(clone.fn).toBe(obj.fn);
|
|
249
|
+
expect(clone.map).not.toBe(obj.map);
|
|
250
|
+
expect(clone.set).not.toBe(obj.set);
|
|
251
|
+
expect(clone.nested.deep.deeper.value).toBe('bottom');
|
|
252
|
+
expect(clone.nested).not.toBe(obj.nested);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('deepMerge', () => {
|
|
257
|
+
it('merges flat objects', () => {
|
|
258
|
+
const target = { a: 1, b: 2 };
|
|
259
|
+
const source = { b: 3, c: 4 };
|
|
260
|
+
const result = deepMerge(target, source);
|
|
261
|
+
expect(result).toEqual({ a: 1, b: 3, c: 4 });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('merges nested objects recursively', () => {
|
|
265
|
+
const target = { a: { x: 1, y: 2 }, b: 1 };
|
|
266
|
+
const source = { a: { y: 3, z: 4 } };
|
|
267
|
+
const result = deepMerge(target, source);
|
|
268
|
+
expect(result).toEqual({ a: { x: 1, y: 3, z: 4 }, b: 1 });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('replaces arrays instead of merging them', () => {
|
|
272
|
+
const target = { arr: [1, 2, 3] };
|
|
273
|
+
const source = { arr: [4, 5] };
|
|
274
|
+
const result = deepMerge(target, source);
|
|
275
|
+
expect(result.arr).toEqual([4, 5]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('replaces primitives with source values', () => {
|
|
279
|
+
const target = { a: 1, b: 'old' };
|
|
280
|
+
const source = { a: 2, b: 'new' };
|
|
281
|
+
const result = deepMerge(target, source);
|
|
282
|
+
expect(result).toEqual({ a: 2, b: 'new' });
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('preserves function references from source', () => {
|
|
286
|
+
const fn = () => 'hello';
|
|
287
|
+
const target = { a: 1 };
|
|
288
|
+
const source = { formatter: fn };
|
|
289
|
+
const result = deepMerge(target, source);
|
|
290
|
+
expect(result.formatter).toBe(fn);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('does not mutate target or source', () => {
|
|
294
|
+
const target = { a: { x: 1 } };
|
|
295
|
+
const source = { a: { y: 2 } };
|
|
296
|
+
deepMerge(target, source);
|
|
297
|
+
expect(target).toEqual({ a: { x: 1 } });
|
|
298
|
+
expect(source).toEqual({ a: { y: 2 } });
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('merges ECharts-like option objects', () => {
|
|
302
|
+
const target = {
|
|
303
|
+
title: { text: 'Chart' },
|
|
304
|
+
tooltip: { trigger: 'axis' as const },
|
|
305
|
+
series: [{ type: 'line' as const, data: [1, 2, 3] }],
|
|
306
|
+
};
|
|
307
|
+
const source = {
|
|
308
|
+
title: { subtext: 'Sub' },
|
|
309
|
+
tooltip: { formatter: (p: { value: number }) => `${p.value}%` },
|
|
310
|
+
};
|
|
311
|
+
const result = deepMerge(target, source);
|
|
312
|
+
expect(result.title).toEqual({ text: 'Chart', subtext: 'Sub' });
|
|
313
|
+
expect(result.tooltip.trigger).toBe('axis');
|
|
314
|
+
expect(typeof result.tooltip.formatter).toBe('function');
|
|
315
|
+
expect(result.series).toEqual([{ type: 'line', data: [1, 2, 3] }]);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -92,7 +92,8 @@ describe('Data Processing Utilities', () => {
|
|
|
92
92
|
|
|
93
93
|
it('should limit execution to once per interval', () => {
|
|
94
94
|
const mockFn = jest.fn();
|
|
95
|
-
|
|
95
|
+
// 使用 leading=true, trailing=false 来匹配简单 throttle 行为
|
|
96
|
+
const throttledFn = throttle(mockFn, 100, { leading: true, trailing: false });
|
|
96
97
|
|
|
97
98
|
throttledFn();
|
|
98
99
|
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
@@ -83,3 +83,16 @@ export function resizeAllCharts(): void {
|
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Dispose and remove stale chart instances (those whose DOM container is gone)
|
|
89
|
+
* Call periodically or from ErrorBoundary componentDidCatch
|
|
90
|
+
*/
|
|
91
|
+
export function disposeStaleCharts(): void {
|
|
92
|
+
Object.keys(CHART_INSTANCES).forEach((id) => {
|
|
93
|
+
const instance = CHART_INSTANCES[id];
|
|
94
|
+
if (!instance || instance.isDisposed()) {
|
|
95
|
+
delete CHART_INSTANCES[id];
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
package/src/core/utils/common.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment detection utilities
|
|
3
|
+
*
|
|
4
|
+
* Backward-compatible re-exports that delegate to the unified detectRuntime()
|
|
5
|
+
* in ./runtime.ts. New code should import detectRuntime() directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { detectRuntime as _detectRuntime } from './runtime';
|
|
9
|
+
|
|
10
|
+
export { detectRuntime, resetRuntimeCache } from './runtime';
|
|
11
|
+
export type { RuntimeInfo, MiniAppType } from './runtime';
|
|
12
|
+
|
|
1
13
|
/**
|
|
2
14
|
* 获取DOM元素
|
|
3
15
|
* @param selector 选择器或DOM元素
|
|
@@ -21,14 +33,12 @@ export const isBrowser = typeof window !== 'undefined' && typeof document !== 'u
|
|
|
21
33
|
* @returns 是否为NodeJS环境
|
|
22
34
|
*/
|
|
23
35
|
export const isNode = (() => {
|
|
24
|
-
// 更可靠的环境检测:检查是否是真正的 Node.js 环境
|
|
25
|
-
// 而不是打包后的代码(如 webpack 定义的 process.env)
|
|
26
36
|
try {
|
|
27
37
|
return (
|
|
28
38
|
typeof process !== 'undefined' &&
|
|
29
|
-
process.versions &&
|
|
30
|
-
process.versions.node &&
|
|
31
|
-
Object.prototype.toString.call(globalThis.process) === '[object process]'
|
|
39
|
+
(process as any).versions &&
|
|
40
|
+
(process as any).versions.node &&
|
|
41
|
+
Object.prototype.toString.call((globalThis as any).process) === '[object process]'
|
|
32
42
|
);
|
|
33
43
|
} catch {
|
|
34
44
|
return false;
|
|
@@ -40,39 +50,13 @@ export const isNode = (() => {
|
|
|
40
50
|
* @returns 是否为React Native环境
|
|
41
51
|
*/
|
|
42
52
|
export const isReactNative =
|
|
43
|
-
typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
53
|
+
typeof navigator !== 'undefined' && (navigator as any).product === 'ReactNative';
|
|
44
54
|
|
|
45
55
|
/**
|
|
46
56
|
* 是否为小程序环境
|
|
57
|
+
*
|
|
58
|
+
* Delegates to detectRuntime() for unified detection.
|
|
59
|
+
* Cached internally — repeated calls are cheap.
|
|
47
60
|
* @returns 是否为小程序环境
|
|
48
61
|
*/
|
|
49
|
-
export const isMiniApp = (): boolean =>
|
|
50
|
-
// 使用类型断言来安全地检查全局变量
|
|
51
|
-
const globalObj =
|
|
52
|
-
typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : {};
|
|
53
|
-
const win = globalObj as Window & {
|
|
54
|
-
wx?: unknown;
|
|
55
|
-
my?: unknown;
|
|
56
|
-
swan?: unknown;
|
|
57
|
-
tt?: unknown;
|
|
58
|
-
jd?: unknown;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
if (typeof win.wx !== 'undefined') {
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
if (typeof win.my !== 'undefined') {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
if (typeof win.swan !== 'undefined') {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
if (typeof win.tt !== 'undefined') {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
if (typeof win.jd !== 'undefined') {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return false;
|
|
78
|
-
};
|
|
62
|
+
export const isMiniApp = (): boolean => _detectRuntime().isMiniApp;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written deep clone utility for ECharts option objects.
|
|
3
|
+
* Handles all JS types, circular references, and preserves function references.
|
|
4
|
+
*/
|
|
5
|
+
export function deepClone<T>(value: T, seen?: WeakMap<object, unknown>): T {
|
|
6
|
+
// primitives, null, undefined
|
|
7
|
+
if (value === null || value === undefined) return value;
|
|
8
|
+
if (typeof value !== 'object' && typeof value !== 'function') return value;
|
|
9
|
+
// preserve function references
|
|
10
|
+
if (typeof value === 'function') return value;
|
|
11
|
+
|
|
12
|
+
// circular reference handling
|
|
13
|
+
if (!seen) seen = new WeakMap();
|
|
14
|
+
if (seen.has(value as object)) return seen.get(value as object) as T;
|
|
15
|
+
|
|
16
|
+
// Date
|
|
17
|
+
if (value instanceof Date) {
|
|
18
|
+
const d = new Date(value.getTime());
|
|
19
|
+
seen.set(value as object, d);
|
|
20
|
+
return d as unknown as T;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// RegExp
|
|
24
|
+
if (value instanceof RegExp) {
|
|
25
|
+
const r = new RegExp(value.source, value.flags);
|
|
26
|
+
seen.set(value as object, r);
|
|
27
|
+
return r as unknown as T;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Map
|
|
31
|
+
if (value instanceof Map) {
|
|
32
|
+
const m = new Map();
|
|
33
|
+
seen.set(value as object, m);
|
|
34
|
+
value.forEach((v, k) => {
|
|
35
|
+
m.set(deepClone(k, seen), deepClone(v, seen));
|
|
36
|
+
});
|
|
37
|
+
return m as unknown as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set
|
|
41
|
+
if (value instanceof Set) {
|
|
42
|
+
const s = new Set();
|
|
43
|
+
seen.set(value as object, s);
|
|
44
|
+
value.forEach((v) => {
|
|
45
|
+
s.add(deepClone(v, seen));
|
|
46
|
+
});
|
|
47
|
+
return s as unknown as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Array
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
const arr: unknown[] = [];
|
|
53
|
+
seen.set(value as object, arr);
|
|
54
|
+
for (let i = 0; i < value.length; i++) {
|
|
55
|
+
arr[i] = deepClone(value[i], seen);
|
|
56
|
+
}
|
|
57
|
+
return arr as unknown as T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Plain object
|
|
61
|
+
const clone = {} as Record<string, unknown>;
|
|
62
|
+
seen.set(value as object, clone);
|
|
63
|
+
for (const key of Object.keys(value as object)) {
|
|
64
|
+
clone[key] = deepClone((value as Record<string, unknown>)[key], seen);
|
|
65
|
+
}
|
|
66
|
+
return clone as T;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Deep merge source into target. Returns a new object.
|
|
71
|
+
* Arrays are replaced, not merged. Objects are merged recursively.
|
|
72
|
+
*/
|
|
73
|
+
export function deepMerge<T extends Record<string, unknown>, U extends Record<string, unknown>>(
|
|
74
|
+
target: T,
|
|
75
|
+
source: U
|
|
76
|
+
): T & U {
|
|
77
|
+
const result: Record<string, unknown> = {};
|
|
78
|
+
|
|
79
|
+
// Copy all target keys
|
|
80
|
+
for (const key of Object.keys(target)) {
|
|
81
|
+
result[key] = deepClone(target[key]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Merge source keys
|
|
85
|
+
for (const key of Object.keys(source)) {
|
|
86
|
+
const tVal = result[key];
|
|
87
|
+
const sVal = source[key];
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
tVal !== null &&
|
|
91
|
+
tVal !== undefined &&
|
|
92
|
+
sVal !== null &&
|
|
93
|
+
sVal !== undefined &&
|
|
94
|
+
typeof tVal === 'object' &&
|
|
95
|
+
typeof sVal === 'object' &&
|
|
96
|
+
!Array.isArray(tVal) &&
|
|
97
|
+
!Array.isArray(sVal) &&
|
|
98
|
+
!(tVal instanceof Date) &&
|
|
99
|
+
!(tVal instanceof RegExp) &&
|
|
100
|
+
!(tVal instanceof Map) &&
|
|
101
|
+
!(tVal instanceof Set) &&
|
|
102
|
+
!(sVal instanceof Date) &&
|
|
103
|
+
!(sVal instanceof RegExp) &&
|
|
104
|
+
!(sVal instanceof Map) &&
|
|
105
|
+
!(sVal instanceof Set)
|
|
106
|
+
) {
|
|
107
|
+
result[key] = deepMerge(tVal as Record<string, unknown>, sVal as Record<string, unknown>);
|
|
108
|
+
} else {
|
|
109
|
+
result[key] = deepClone(sVal);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result as T & U;
|
|
114
|
+
}
|
|
@@ -37,55 +37,49 @@ export function generateFormattedFilename(name: string, format: string): string
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* @param
|
|
40
|
+
* 触发浏览器下载的通用函数
|
|
41
|
+
* @param href 下载链接
|
|
42
42
|
* @param filename 文件名
|
|
43
|
+
* @param cleanup 可选的清理回调(如 revokeObjectURL)
|
|
43
44
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
function triggerDownload(href: string, filename: string, cleanup?: () => void): void {
|
|
46
|
+
if (typeof document === 'undefined') return;
|
|
46
47
|
const link = document.createElement('a');
|
|
47
|
-
link.href =
|
|
48
|
+
link.href = href;
|
|
48
49
|
link.download = filename;
|
|
49
50
|
link.style.display = 'none';
|
|
50
|
-
|
|
51
51
|
document.body.appendChild(link);
|
|
52
52
|
link.click();
|
|
53
|
-
|
|
54
|
-
// 延迟清理,确保下载对话框已打开
|
|
55
53
|
const timerId = setTimeout(() => {
|
|
56
|
-
if (link.parentNode)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
if (link.parentNode) document.body.removeChild(link);
|
|
55
|
+
cleanup?.();
|
|
56
|
+
// Auto-remove from timers array
|
|
57
|
+
const idx = timers.indexOf(timerId);
|
|
58
|
+
if (idx !== -1) timers.splice(idx, 1);
|
|
60
59
|
}, 100);
|
|
61
60
|
timers.push(timerId);
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
/**
|
|
64
|
+
* 下载 Blob 对象
|
|
65
|
+
* @param blob Blob 数据
|
|
66
|
+
* @param filename 文件名
|
|
67
|
+
*/
|
|
68
|
+
export function downloadBlob(blob: Blob, filename: string): void {
|
|
69
|
+
const url = URL.createObjectURL(blob);
|
|
70
|
+
triggerDownload(url, filename, () => URL.revokeObjectURL(url));
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
/**
|
|
65
74
|
* 下载数据 URL
|
|
66
75
|
* @param dataUrl 数据 URL
|
|
67
76
|
* @param filename 文件名
|
|
68
77
|
*/
|
|
69
78
|
export function downloadDataUrl(dataUrl: string, filename: string): void {
|
|
70
|
-
|
|
71
|
-
link.href = dataUrl;
|
|
72
|
-
link.download = filename;
|
|
73
|
-
link.style.display = 'none';
|
|
74
|
-
|
|
75
|
-
document.body.appendChild(link);
|
|
76
|
-
link.click();
|
|
77
|
-
|
|
78
|
-
// 延迟清理
|
|
79
|
-
const timerId = setTimeout(() => {
|
|
80
|
-
if (link.parentNode) {
|
|
81
|
-
document.body.removeChild(link);
|
|
82
|
-
}
|
|
83
|
-
}, 100);
|
|
84
|
-
timers.push(timerId);
|
|
79
|
+
triggerDownload(dataUrl, filename);
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
/**
|
|
88
|
-
* 下载文件(支持 string | Blob)
|
|
89
83
|
* 下载文件(支持 string | Blob)
|
|
90
84
|
* @param data 数据(string 或 Blob)
|
|
91
85
|
* @param filename 文件名
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
1
2
|
/**
|
|
2
3
|
* TaroViz 图表导出工具
|
|
3
4
|
* 支持导出为 PNG、JPEG、SVG、PDF 等格式
|
|
@@ -18,7 +19,7 @@ type ExportImageType = 'png' | 'jpeg' | 'svg' | 'webp' | 'gif';
|
|
|
18
19
|
/**
|
|
19
20
|
* getDataURL 方法参数(基于 ECharts 官方类型,扩展 webp/gif 支持)
|
|
20
21
|
*/
|
|
21
|
-
interface
|
|
22
|
+
interface __EChartsDataURLOptions {
|
|
22
23
|
type?: ExportImageType;
|
|
23
24
|
pixelRatio?: number;
|
|
24
25
|
backgroundColor?: string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 格式化数值
|
|
3
|
+
* @param value 要格式化的数值
|
|
4
|
+
* @param digits 小数位数
|
|
5
|
+
* @param options 配置选项
|
|
6
|
+
* @returns 格式化后的字符串
|
|
7
|
+
*/
|
|
8
|
+
export function formatNumber(
|
|
9
|
+
value: number,
|
|
10
|
+
digits: number = 2,
|
|
11
|
+
options: {
|
|
12
|
+
useGrouping?: boolean;
|
|
13
|
+
locale?: string;
|
|
14
|
+
} = {}
|
|
15
|
+
): string {
|
|
16
|
+
const { useGrouping = true, locale = 'zh-CN' } = options;
|
|
17
|
+
|
|
18
|
+
return new Intl.NumberFormat(locale, {
|
|
19
|
+
minimumFractionDigits: digits,
|
|
20
|
+
maximumFractionDigits: digits,
|
|
21
|
+
useGrouping,
|
|
22
|
+
}).format(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 获取颜色的对比色
|
|
27
|
+
* @param color 十六进制颜色值
|
|
28
|
+
* @returns 对比色
|
|
29
|
+
*/
|
|
30
|
+
export function getContrastColor(color: string): string {
|
|
31
|
+
// 移除#前缀
|
|
32
|
+
const hex = color.replace('#', '');
|
|
33
|
+
|
|
34
|
+
// 转换为RGB
|
|
35
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
36
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
37
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
38
|
+
|
|
39
|
+
// 计算亮度
|
|
40
|
+
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
|
41
|
+
|
|
42
|
+
// 根据亮度返回黑色或白色
|
|
43
|
+
return brightness > 128 ? '#000000' : '#FFFFFF';
|
|
44
|
+
}
|