@agions/taroviz 1.6.0 → 1.7.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.
@@ -0,0 +1,323 @@
1
+ /**
2
+ * useDataZoom - 数据缩放 Hook
3
+ * 控制图表的数据缩放区域(dataZoom),支持拖拽和滚轮缩放
4
+ */
5
+ import { useRef, useCallback, useEffect } from 'react';
6
+ import type { RefObject } from 'react';
7
+ import type { ChartInstance } from './index';
8
+
9
+ // ============================================================================
10
+ // 类型定义
11
+ // ============================================================================
12
+
13
+ /** dataZoom 类型 */
14
+ export type DataZoomType = 'inside' | 'slider';
15
+
16
+ /** dataZoom 配置选项 */
17
+ export interface UseDataZoomOptions {
18
+ /** dataZoom 类型: inside=内置型(滚轮/双指), slider=滑块型 */
19
+ type?: DataZoomType;
20
+ /** 起始位置 (0-100) */
21
+ start?: number;
22
+ /** 结束位置 (0-100) */
23
+ end?: number;
24
+ /** 最小跨度 (0-100) */
25
+ minSpan?: number;
26
+ /** 最大跨度 (0-100) */
27
+ maxSpan?: number;
28
+ /** 是否锁定缩放 */
29
+ zoomLock?: boolean;
30
+ /** 节流时间 (ms) */
31
+ throttle?: number;
32
+ /** 是否禁用 */
33
+ disabled?: boolean;
34
+ /** 是否显示缩放痕迹 */
35
+ brushSelect?: boolean;
36
+ /** 缩放模式: 'scale' | 'mix' */
37
+ zoomMode?: 'scale' | 'mix';
38
+ /** 是否在组件卸载时重置缩放 */
39
+ resetOnUnmount?: boolean;
40
+ /** 缩放变化回调 */
41
+ onZoomChange?: (range: { start: number; end: number }) => void;
42
+ }
43
+
44
+ /** 缩放范围 */
45
+ export interface ZoomRange {
46
+ start: number;
47
+ end: number;
48
+ }
49
+
50
+ /** dataZoom 返回值 */
51
+ export interface UseDataZoomReturn {
52
+ /** 绑定 dataZoom 到图表实例 */
53
+ bindDataZoom: (chartInstance: ChartInstance) => void;
54
+ /** 设置缩放范围 */
55
+ setZoomRange: (start: number, end: number) => void;
56
+ /** 重置缩放到初始状态 */
57
+ resetZoom: () => void;
58
+ /** 获取当前缩放范围 */
59
+ getZoomRange: () => ZoomRange;
60
+ /** 起始位置的原始值 (Ref) */
61
+ startValue: RefObject<number | string | Date | undefined>;
62
+ /** 结束位置的原始值 (Ref) */
63
+ endValue: RefObject<number | string | Date | undefined>;
64
+ /** 绑定事件处理 */
65
+ bindEvents: (chartInstance: ChartInstance) => void;
66
+ /** 缩放变化回调 */
67
+ onZoomChange?: (range: ZoomRange) => void;
68
+ }
69
+
70
+ // ============================================================================
71
+ // Hook 实现
72
+ // ============================================================================
73
+
74
+ /**
75
+ * 使用图表数据缩放
76
+ * @param options 配置选项
77
+ * @returns dataZoom 操作接口
78
+ */
79
+ export function useDataZoom(options: UseDataZoomOptions = {}): UseDataZoomReturn {
80
+ const {
81
+ type = 'inside',
82
+ start = 0,
83
+ end = 100,
84
+ minSpan,
85
+ maxSpan,
86
+ zoomLock = false,
87
+ throttle = 16,
88
+ disabled = false,
89
+ brushSelect = false,
90
+ zoomMode = 'scale',
91
+ resetOnUnmount = false,
92
+ onZoomChange,
93
+ } = options;
94
+
95
+ // Refs
96
+ const chartRef = useRef<ChartInstance | null>(null);
97
+ const startValueRef = useRef<number | string | Date | undefined>(undefined);
98
+ const endValueRef = useRef<number | string | Date | undefined>(undefined);
99
+ const isBindingRef = useRef(false);
100
+ const throttledHandlerRef = useRef<((...args: any[]) => void) | null>(null);
101
+
102
+ // 节流处理
103
+ const throttledCallback = useCallback(
104
+ <T extends (...args: any[]) => void>(fn: T): T => {
105
+ let lastCall = 0;
106
+ return ((...args: any[]) => {
107
+ const now = Date.now();
108
+ if (now - lastCall >= throttle) {
109
+ lastCall = now;
110
+ fn(...args);
111
+ }
112
+ }) as T;
113
+ },
114
+ [throttle]
115
+ );
116
+
117
+ // 构建 dataZoom 配置
118
+ const buildDataZoomConfig = useCallback(() => {
119
+ const config: any[] = [];
120
+
121
+ // 内置型 dataZoom
122
+ if (type === 'inside' || zoomMode === 'mix') {
123
+ config.push({
124
+ type: 'inside',
125
+ start,
126
+ end,
127
+ zoomLock,
128
+ disabled,
129
+ zoomOnMouseWheel: true,
130
+ moveOnMouseMove: false,
131
+ moveOnMouseWheel: false,
132
+ preventDefaultMouseMove: true,
133
+ });
134
+ }
135
+
136
+ // 滑块型 dataZoom
137
+ if (type === 'slider' || zoomMode === 'mix') {
138
+ config.push({
139
+ type: 'slider',
140
+ start,
141
+ end,
142
+ minSpan,
143
+ maxSpan,
144
+ zoomLock,
145
+ disabled,
146
+ show: true,
147
+ brushSelect,
148
+ throttle,
149
+ });
150
+ }
151
+
152
+ return config;
153
+ }, [type, start, end, minSpan, maxSpan, zoomLock, disabled, brushSelect, throttle]);
154
+
155
+ // 绑定 dataZoom 到图表
156
+ const bindDataZoom = useCallback(
157
+ (chartInstance: ChartInstance) => {
158
+ if (!chartInstance || isBindingRef.current) return;
159
+
160
+ isBindingRef.current = true;
161
+ chartRef.current = chartInstance;
162
+
163
+ try {
164
+ // 设置 dataZoom 配置
165
+ const dataZoomConfig = buildDataZoomConfig();
166
+ chartInstance.setOption(
167
+ {
168
+ dataZoom: dataZoomConfig,
169
+ },
170
+ false,
171
+ true
172
+ );
173
+ } catch (e) {
174
+ console.warn('[useDataZoom] Failed to bind dataZoom:', e);
175
+ }
176
+
177
+ isBindingRef.current = false;
178
+ },
179
+ [buildDataZoomConfig]
180
+ );
181
+
182
+ // 解绑 dataZoom
183
+ const unbindDataZoom = useCallback(
184
+ (chartInstance: ChartInstance) => {
185
+ if (!chartInstance) return;
186
+
187
+ try {
188
+ // 通过 dispatchAction 关闭 dataZoom
189
+ chartInstance.dispatchAction?.({
190
+ type: 'dataZoom',
191
+ start: 0,
192
+ end: 100,
193
+ });
194
+ } catch (e) {
195
+ console.warn('[useDataZoom] Failed to unbind dataZoom:', e);
196
+ }
197
+ },
198
+ []
199
+ );
200
+
201
+ // 设置缩放范围
202
+ const setZoomRange = useCallback((newStart: number, newEnd: number) => {
203
+ const chart = chartRef.current;
204
+ if (!chart) return;
205
+
206
+ const clampedStart = Math.max(0, Math.min(100, newStart));
207
+ const clampedEnd = Math.max(0, Math.min(100, newEnd));
208
+
209
+ try {
210
+ chart.dispatchAction?.({
211
+ type: 'dataZoom',
212
+ start: clampedStart,
213
+ end: clampedEnd,
214
+ });
215
+ } catch (e) {
216
+ console.warn('[useDataZoom] Failed to set zoom range:', e);
217
+ }
218
+ }, []);
219
+
220
+ // 重置缩放
221
+ const resetZoom = useCallback(() => {
222
+ setZoomRange(start, end);
223
+ }, [setZoomRange, start, end]);
224
+
225
+ // 获取当前缩放范围
226
+ const getZoomRange = useCallback((): ZoomRange => {
227
+ const chart = chartRef.current;
228
+ if (!chart) {
229
+ return { start, end };
230
+ }
231
+
232
+ try {
233
+ const option = chart.getOption?.();
234
+ const dataZoom = option?.dataZoom as any[];
235
+ if (Array.isArray(dataZoom) && dataZoom.length > 0) {
236
+ // 优先获取 inside 类型的范围
237
+ const insideZoom = dataZoom.find((dz: any) => dz.type === 'inside');
238
+ const sliderZoom = dataZoom.find((dz: any) => dz.type === 'slider');
239
+ const zoom = insideZoom || sliderZoom || dataZoom[0];
240
+ return {
241
+ start: zoom.start ?? start,
242
+ end: zoom.end ?? end,
243
+ };
244
+ }
245
+ } catch (e) {
246
+ console.warn('[useDataZoom] Failed to get zoom range:', e);
247
+ }
248
+
249
+ return { start, end };
250
+ }, [start, end]);
251
+
252
+ // 绑定事件
253
+ const bindEvents = useCallback(
254
+ (chartInstance: ChartInstance) => {
255
+ if (!chartInstance) return;
256
+
257
+ // 创建缩放变化处理器
258
+ const handleZoomChange = (params: any) => {
259
+ if (disabled) return;
260
+
261
+ const { start: newStart, end: newEnd } = params || {};
262
+ if (newStart !== undefined) startValueRef.current = newStart;
263
+ if (newEnd !== undefined) endValueRef.current = newEnd;
264
+
265
+ onZoomChange?.({ start: newStart ?? start, end: newEnd ?? end });
266
+ };
267
+
268
+ // 使用节流包装
269
+ throttledHandlerRef.current = throttledCallback(handleZoomChange);
270
+
271
+ try {
272
+ // 监听 dataZoom 事件
273
+ chartInstance.on('datazoom', throttledHandlerRef.current);
274
+ } catch (e) {
275
+ console.warn('[useDataZoom] Failed to bind events:', e);
276
+ }
277
+ },
278
+ [disabled, onZoomChange, throttledCallback]
279
+ );
280
+
281
+ // 解绑事件
282
+ const unbindEvents = useCallback(
283
+ (chartInstance: ChartInstance) => {
284
+ if (!chartInstance || !throttledHandlerRef.current) return;
285
+
286
+ try {
287
+ chartInstance.off('datazoom', throttledHandlerRef.current);
288
+ } catch (e) {
289
+ console.warn('[useDataZoom] Failed to unbind events:', e);
290
+ }
291
+ },
292
+ []
293
+ );
294
+
295
+ // 清理:组件卸载时
296
+ useEffect(() => {
297
+ return () => {
298
+ const chart = chartRef.current;
299
+ if (chart) {
300
+ if (resetOnUnmount) {
301
+ unbindDataZoom(chart);
302
+ }
303
+ unbindEvents(chart);
304
+ }
305
+ };
306
+ }, [resetOnUnmount, unbindDataZoom, unbindEvents]);
307
+
308
+ return {
309
+ bindDataZoom,
310
+ setZoomRange,
311
+ resetZoom,
312
+ getZoomRange,
313
+ startValue: startValueRef,
314
+ endValue: endValueRef,
315
+ bindEvents,
316
+ };
317
+ }
318
+
319
+ // ============================================================================
320
+ // 导出
321
+ // ============================================================================
322
+
323
+ export default useDataZoom;
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * TaroViz - 基于 Taro 和 ECharts 的多端图表组件库
3
- * @version 1.6.0
3
+ * @version 1.7.0
4
4
  */
5
5
 
6
6
  // 核心组件
@@ -59,6 +59,25 @@ export { default as WordCloudChart } from './charts/wordcloud';
59
59
  export { default as BoxplotChart } from './charts/boxplot';
60
60
  export { default as ParallelChart } from './charts/parallel';
61
61
 
62
+ // v1.7.0 新增组件
63
+ export { DataFilter, type DataFilterProps, type FilterField, type FilterValues } from './components/DataFilter';
64
+ export {
65
+ createDrillDown,
66
+ canDrillDown,
67
+ buildHierarchy,
68
+ createRegionDrillDown,
69
+ createCategoryDrillDown,
70
+ type DrillDownConfig,
71
+ type DrillDownSource,
72
+ type DrillDownReturn,
73
+ type DrillDownEventParams,
74
+ type DrillUpEventParams,
75
+ } from './core/utils/drillDown';
76
+
77
+ // v1.7.0 新增图表组件
78
+ export { default as LiquidChart } from './charts/liquid';
79
+ export { default as TreeChart } from './charts/tree';
80
+
62
81
  // 适配器
63
82
  export { getAdapter, detectPlatform, getEnv } from './adapters';
64
83
  export { default as H5Adapter } from './adapters/h5';
@@ -152,10 +171,14 @@ export {
152
171
  useDataTransform,
153
172
  useTableTransform,
154
173
  useTimeSeriesTransform,
174
+ // v1.7.0 新增 Hooks
175
+ useDataZoom,
176
+ useChartConnect,
177
+ useChartDownload,
155
178
  } from './hooks';
156
179
 
157
180
  /**
158
181
  * 库信息
159
182
  */
160
183
  export const name = 'taroviz';
161
- export const version = '1.6.0';
184
+ export const version = '1.7.0';
@@ -871,3 +871,6 @@ export default {
871
871
  getLightThemes,
872
872
  getDarkThemes,
873
873
  };
874
+
875
+ export { useAutoTheme } from './useAutoTheme';
876
+ export type { UseAutoThemeOptions } from './useAutoTheme';
@@ -0,0 +1,66 @@
1
+ /**
2
+ * useAutoTheme - 自动跟随系统暗色模式 Hook
3
+ * 当用户操作系统级别的暗色模式时,图表自动切换到对应的主题
4
+ */
5
+ import { useEffect, useRef } from 'react';
6
+ import { switchTheme } from './index';
7
+
8
+ /** 自动主题配置选项 */
9
+ export interface UseAutoThemeOptions {
10
+ /** 是否启用自动跟随 */
11
+ enabled?: boolean;
12
+ /** 暗色主题名称 */
13
+ darkThemeName?: string;
14
+ /** 亮色主题名称 */
15
+ lightThemeName?: string;
16
+ /** 延迟切换的防抖时间 ms */
17
+ debounceMs?: number;
18
+ }
19
+
20
+ /**
21
+ * 自动跟随系统暗色模式
22
+ * @param options 配置选项
23
+ */
24
+ export function useAutoTheme(options?: UseAutoThemeOptions): void {
25
+ const {
26
+ enabled = true,
27
+ darkThemeName = 'dark',
28
+ lightThemeName = 'light',
29
+ debounceMs = 0,
30
+ } = options || {};
31
+
32
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
33
+
34
+ useEffect(() => {
35
+ if (!enabled) return;
36
+
37
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
38
+
39
+ const handleChange = (e: MediaQueryListEvent) => {
40
+ // 清除之前的定时器
41
+ if (timeoutRef.current) {
42
+ clearTimeout(timeoutRef.current);
43
+ }
44
+
45
+ // 防抖处理
46
+ timeoutRef.current = setTimeout(() => {
47
+ const isDark = e.matches;
48
+ switchTheme(isDark ? darkThemeName : lightThemeName);
49
+ }, debounceMs);
50
+ };
51
+
52
+ // 初始设置
53
+ const isDark = mediaQuery.matches;
54
+ switchTheme(isDark ? darkThemeName : lightThemeName);
55
+
56
+ // 监听变化
57
+ mediaQuery.addEventListener('change', handleChange);
58
+
59
+ return () => {
60
+ mediaQuery.removeEventListener('change', handleChange);
61
+ if (timeoutRef.current) {
62
+ clearTimeout(timeoutRef.current);
63
+ }
64
+ };
65
+ }, [enabled, darkThemeName, lightThemeName, debounceMs]);
66
+ }