@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.
Files changed (118) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +31 -46
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/vendors.js +1 -1
  5. package/dist/cjs/vendors~echarts.js +1 -1
  6. package/dist/esm/index.js +1 -14270
  7. package/dist/esm/vendors.js +1 -16770
  8. package/dist/esm/vendors~echarts.js +1 -59417
  9. package/package.json +10 -15
  10. package/src/adapters/h5/index.ts +38 -38
  11. package/src/adapters/index.ts +32 -34
  12. package/src/adapters/types.ts +23 -55
  13. package/src/charts/boxplot/types.ts +2 -2
  14. package/src/charts/common/BaseChartWrapper.tsx +9 -7
  15. package/src/charts/createChartComponent.tsx +9 -21
  16. package/src/charts/createOptionChartComponent.tsx +32 -0
  17. package/src/charts/funnel/__tests__/index.test.tsx +99 -0
  18. package/src/charts/funnel/index.tsx +64 -0
  19. package/src/charts/funnel/types.ts +6 -0
  20. package/src/charts/graph/__tests__/index.test.tsx +116 -0
  21. package/src/charts/graph/index.tsx +70 -0
  22. package/src/charts/graph/types.ts +6 -0
  23. package/src/charts/heatmap/__tests__/index.test.tsx +139 -0
  24. package/src/charts/heatmap/index.tsx +107 -0
  25. package/src/charts/heatmap/types.ts +6 -0
  26. package/src/charts/index.ts +47 -57
  27. package/src/charts/liquid/__tests__/index.test.tsx +52 -0
  28. package/src/charts/liquid/index.tsx +7 -133
  29. package/src/charts/liquid/types.ts +6 -6
  30. package/src/charts/parallel/types.ts +3 -3
  31. package/src/charts/radar/__tests__/index.test.tsx +210 -0
  32. package/src/charts/radar/index.tsx +147 -0
  33. package/src/charts/radar/types.ts +13 -0
  34. package/src/charts/sankey/__tests__/index.test.tsx +124 -0
  35. package/src/charts/sankey/index.tsx +70 -0
  36. package/src/charts/sankey/types.ts +6 -0
  37. package/src/charts/tree/__tests__/index.test.tsx +71 -0
  38. package/src/charts/tree/index.tsx +1 -1
  39. package/src/charts/tree/types.ts +8 -8
  40. package/src/charts/types.ts +208 -106
  41. package/src/charts/wordcloud/__tests__/index.test.tsx +106 -0
  42. package/src/charts/wordcloud/index.tsx +79 -0
  43. package/src/charts/wordcloud/types.ts +6 -0
  44. package/src/components/DataFilter/index.tsx +7 -6
  45. package/src/core/animation/types.ts +6 -6
  46. package/src/core/components/Annotation.tsx +6 -6
  47. package/src/core/components/BaseChart.tsx +97 -133
  48. package/src/core/components/LazyChart.tsx +3 -8
  49. package/src/core/components/hooks/index.ts +6 -2
  50. package/src/core/components/hooks/usePerformance.ts +8 -2
  51. package/src/core/components/hooks/useVirtualScroll.ts +2 -1
  52. package/src/core/types/common.ts +2 -1
  53. package/src/core/types/platform.ts +1 -0
  54. package/src/core/utils/__tests__/deepClone.test.ts +317 -0
  55. package/src/core/utils/__tests__/index.test.ts +2 -1
  56. package/src/core/utils/chartInstances.ts +13 -0
  57. package/src/core/utils/common.ts +20 -36
  58. package/src/core/utils/deepClone.ts +114 -0
  59. package/src/core/utils/download.ts +22 -28
  60. package/src/core/utils/drillDown.ts +1 -0
  61. package/src/core/utils/events.ts +12 -0
  62. package/src/core/utils/export/ExportUtils.ts +2 -1
  63. package/src/core/utils/format.ts +44 -0
  64. package/src/core/utils/index.ts +18 -159
  65. package/src/core/utils/merge.ts +25 -0
  66. package/src/core/utils/performance/PerformanceAnalyzer.ts +3 -1
  67. package/src/core/utils/performance/hooks.ts +7 -0
  68. package/src/core/utils/performance/index.ts +2 -0
  69. package/src/{hooks → core/utils/performance}/useAnimation.ts +6 -5
  70. package/src/{hooks → core/utils/performance}/useDataZoom.ts +7 -2
  71. package/src/{hooks → core/utils/performance}/usePerformance.ts +39 -39
  72. package/src/{hooks → core/utils/performance}/usePerformanceHooks.ts +39 -39
  73. package/src/core/utils/runtime.ts +190 -0
  74. package/src/editor/components/ThemeSelector.tsx +3 -3
  75. package/src/hooks/chartConnectHelpers.ts +6 -0
  76. package/src/hooks/index.ts +54 -626
  77. package/src/hooks/types.ts +27 -0
  78. package/src/hooks/useChartAutoResize.ts +73 -0
  79. package/src/hooks/useChartConnect.ts +5 -1
  80. package/src/hooks/useChartDownload.ts +1 -1
  81. package/src/hooks/useChartHistory.ts +1 -3
  82. package/src/hooks/useChartInit.ts +59 -0
  83. package/src/hooks/useChartOptions.ts +259 -0
  84. package/src/hooks/useChartPerformance.ts +109 -0
  85. package/src/hooks/useChartSelection.ts +23 -12
  86. package/src/hooks/useChartTheme.ts +51 -0
  87. package/src/hooks/useDataTransform.ts +19 -4
  88. package/src/index.ts +5 -10
  89. package/src/react-dom.d.ts +3 -3
  90. package/src/themes/index.ts +30 -855
  91. package/src/themes/palettes/blue-green.ts +13 -0
  92. package/src/themes/palettes/chalk.ts +13 -0
  93. package/src/themes/palettes/cyber.ts +44 -0
  94. package/src/themes/palettes/dark.ts +52 -0
  95. package/src/themes/palettes/default.ts +52 -0
  96. package/src/themes/palettes/elegant.ts +34 -0
  97. package/src/themes/palettes/forest.ts +13 -0
  98. package/src/themes/palettes/glass.ts +49 -0
  99. package/src/themes/palettes/golden.ts +13 -0
  100. package/src/themes/palettes/neon.ts +43 -0
  101. package/src/themes/palettes/ocean.ts +39 -0
  102. package/src/themes/palettes/pastel.ts +37 -0
  103. package/src/themes/palettes/purple-passion.ts +13 -0
  104. package/src/themes/palettes/retro.ts +33 -0
  105. package/src/themes/palettes/sunset.ts +40 -0
  106. package/src/themes/palettes/walden.ts +13 -0
  107. package/src/themes/registry.ts +184 -0
  108. package/src/themes/types.ts +213 -0
  109. package/src/core/utils/codeGenerator/CodeGenerator.ts +0 -669
  110. package/src/core/utils/codeGenerator/index.ts +0 -13
  111. package/src/core/utils/codeGenerator/types.ts +0 -198
  112. package/src/core/utils/configGenerator/ConfigGenerator.ts +0 -583
  113. package/src/core/utils/configGenerator/index.ts +0 -13
  114. package/src/core/utils/configGenerator/types.ts +0 -449
  115. package/src/core/utils/debug/DebugPanel.tsx +0 -640
  116. package/src/core/utils/debug/debugger.ts +0 -322
  117. package/src/core/utils/debug/index.ts +0 -21
  118. 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
- const throttledFn = throttle(mockFn, 100);
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
+ }
@@ -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
- * 下载 Blob 对象
41
- * @param blob Blob 数据
40
+ * 触发浏览器下载的通用函数
41
+ * @param href 下载链接
42
42
  * @param filename 文件名
43
+ * @param cleanup 可选的清理回调(如 revokeObjectURL)
43
44
  */
44
- export function downloadBlob(blob: Blob, filename: string): void {
45
- const url = URL.createObjectURL(blob);
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 = url;
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
- document.body.removeChild(link);
58
- }
59
- URL.revokeObjectURL(url);
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
- const link = document.createElement('a');
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
  * DrillDown - 数据下钻工具
3
4
  * 支持点击图表数据项时,自动下钻到更细粒度的数据视图
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 事件名称常量定义
3
+ */
4
+ export const events = {
5
+ click: 'click',
6
+ mousemove: 'mousemove',
7
+ mouseup: 'mouseup',
8
+ mousedown: 'mousedown',
9
+ mouseover: 'mouseover',
10
+ mouseout: 'mouseout',
11
+ globalout: 'globalout',
12
+ };
@@ -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 EChartsDataURLOptions {
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
+ }