UNPKG

11.2 kBTypeScriptView Raw
1import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
2
3import _uniqId from '@antv/util/lib/unique-id';
4
5import _isFunction from '@antv/util/lib/is-function';
6import withContainer from './boundary/withContainer';
7import ErrorBoundary, { ErrorFallback } from './boundary/ErrorBoundary';
8import RootChartContext from './context/root';
9import ChartViewContext from './context/view';
10import { visibleHelper } from './utils/plotTools';
11import shallowEqual from './utils/shallowEqual';
12import pickWithout from './utils/pickWithout';
13import cloneDeep from './utils/cloneDeep';
14import { REACT_PIVATE_PROPS } from './utils/constant';
15import { Plot } from '@antv/g2plot/lib/core/plot';
16import { ResizeObserver } from '@juggle/resize-observer';
17import getElementSize from './utils/getElementSize';
18import {
19 polyfillEvents,
20 polyfillTitleEvent,
21 polyfillDescriptionEvent,
22} from './plots/core/polyfill';
23import { debounce, isArray, isFunction, isNil } from '@antv/util';
24import warn from 'warning';
25
26const DEFAULT_PLACEHOLDER = (
27 <div
28 style={{ position: 'absolute', top: '48%', left: '50%', color: '#aaa', textAlign: 'center' }}
29 >
30 暂无数据
31 </div>
32);
33
34const DESCRIPTION_STYLE: CSSProperties = {
35 padding: '8px 24px 10px 10px',
36 fontFamily: 'PingFang SC',
37 fontSize: 12,
38 color: 'grey',
39 textAlign: 'left',
40 lineHeight: '16px',
41};
42
43const TITLE_STYLE: CSSProperties = {
44 padding: '10px 0 0 10px',
45 fontFamily: 'PingFang SC',
46 fontSize: 18,
47 color: 'black',
48 textAlign: 'left',
49 lineHeight: '20px',
50};
51
52interface BasePlotOptions {
53 /**
54 * 获取g2Plot实例的勾子函数
55 */
56 onGetG2Instance?: (chart: Plot<any>) => void;
57 errorContent?: React.ReactNode;
58 /**
59 * 图表事件
60 */
61 events?: Record<string, Function>;
62 /**
63 * 图表标题。如需绑定事件请直接使用ReactNode。
64 */
65 readonly title?: React.ReactNode;
66 /**
67 * 图表副标题。如需绑定事件请直接使用ReactNode。
68 */
69 readonly description?: React.ReactNode;
70 /**
71 * 请使用autoFit替代forceFit
72 */
73 forceFit?: boolean;
74 /**
75 * 是否是物料组件,因搭建引擎消费ref和原来的组件吐的react实例不兼容。
76 * 该属性会影响ref的消费,为ali-lowcode-engine消费而生。
77 */
78 isMaterial?: boolean;
79}
80
81export { BasePlotOptions };
82
83class BasePlot extends React.Component<any> {
84 [x: string]: any;
85 g2Instance: any;
86 preConfig: any;
87 public _context: { chart: any } = { chart: null };
88
89 componentDidMount() {
90 if (this.props.children && this.g2Instance.chart) {
91 this.g2Instance.chart.render();
92 }
93 polyfillEvents(this.g2Instance, {}, this.props);
94 this.g2Instance.data = this.props.data;
95 this.preConfig = pickWithout(this.props, [
96 ...REACT_PIVATE_PROPS,
97 'container',
98 'PlotClass',
99 'onGetG2Instance',
100 'data',
101 ]);
102 }
103 componentDidUpdate(prevProps) {
104 if (this.props.children && this.g2Instance.chart) {
105 this.g2Instance.chart.render();
106 }
107 // 兼容1.0 的events写法
108 polyfillEvents(this.g2Instance, prevProps, this.props);
109 }
110 componentWillUnmount() {
111 if (this.g2Instance) {
112 setTimeout(() => {
113 this.g2Instance.destroy();
114 this.g2Instance = null;
115 this._context.chart = null;
116 }, 0);
117 }
118 }
119 public getG2Instance() {
120 return this.g2Instance;
121 }
122
123 getChartView() {
124 return this.g2Instance.chart;
125 }
126
127 protected checkInstanceReady() {
128 if (!this.g2Instance) {
129 this.initInstance();
130 this.g2Instance.render();
131 } else if (this.shouldReCreate()) {
132 // 只有数据更新就不重绘,其他全部直接重新创建实例。
133 this.g2Instance.destroy();
134 this.initInstance();
135 this.g2Instance.render();
136 } else if (this.diffConfig()) {
137 const options = pickWithout(this.props, [
138 'container',
139 'PlotClass',
140 'onGetG2Instance',
141 'children',
142 ]);
143 this.g2Instance.update(options);
144 } else if (this.diffData()) {
145 this.g2Instance.changeData(this.props.data);
146 }
147 // 缓存配置
148 const currentConfig = pickWithout(this.props, [
149 ...REACT_PIVATE_PROPS,
150 'container',
151 'PlotClass',
152 'onGetG2Instance',
153 'data',
154 ]);
155 this.preConfig = cloneDeep(currentConfig);
156 this.g2Instance.data = this.props.data;
157 }
158
159 initInstance() {
160 const { container, PlotClass, onGetG2Instance, children, ...options } = this.props;
161 this.g2Instance = new PlotClass(container, options);
162 this._context.chart = this.g2Instance;
163 if (_isFunction(onGetG2Instance)) {
164 onGetG2Instance(this.g2Instance);
165 }
166 }
167 diffConfig() {
168 // 只有数据更新就不重绘,其他全部直接重新创建实例。
169 const preConfig = this.preConfig || {};
170 const currentConfig = pickWithout(this.props, [
171 ...REACT_PIVATE_PROPS,
172 'container',
173 'PlotClass',
174 'onGetG2Instance',
175 'data',
176 ]);
177 return !shallowEqual(preConfig, currentConfig);
178 }
179 diffData() {
180 // 只有数据更新就不重绘,其他全部直接重新创建实例。
181 const preData = this.g2Instance.data;
182 const data = this.props.data;
183
184 if (!isArray(preData) || !isArray(data)) {
185 // 非数组直接对比
186 return !preData === data;
187 }
188 if (preData.length !== data.length) {
189 return true;
190 }
191 let isEqual = true;
192 preData.forEach((element, index) => {
193 if (!shallowEqual(element, data[index])) {
194 isEqual = false;
195 }
196 });
197 return !isEqual;
198 }
199
200 shouldReCreate() {
201 const { forceUpdate } = this.props;
202 if (forceUpdate) {
203 return true;
204 }
205 return false;
206 }
207
208 render() {
209 this.checkInstanceReady();
210 const chartView = this.getChartView();
211 return (
212 <RootChartContext.Provider value={this._context}>
213 {/* 每次更新都直接刷新子组件 */}
214 <ChartViewContext.Provider value={chartView}>
215 <div key={_uniqId('plot-chart')}>{this.props.children}</div>
216 </ChartViewContext.Provider>
217 </RootChartContext.Provider>
218 );
219 }
220}
221
222const BxPlot = withContainer(BasePlot) as any;
223
224function createPlot<IPlotConfig extends Record<string, any>>(
225 PlotClass,
226 name: string,
227 transCfg: Function = cfg => cfg,
228) {
229 const Com = React.forwardRef<any, IPlotConfig>((props: IPlotConfig, ref) => {
230 // containerStyle 应该删掉,可以通过containerProps.style 配置不影响用户暂时保留
231 const { title, description, autoFit = true, forceFit, errorContent = ErrorFallback, containerStyle, containerProps, placeholder, ErrorBoundaryProps, isMaterial, ...cfg } = props;
232
233 const realCfg = transCfg(cfg);
234 const container = useRef<HTMLDivElement>();
235 const titleDom = useRef();
236 const descDom = useRef();
237
238 const [chartHeight, setChartHeight] = useState(0);
239 const resizeObserver = useRef<ResizeObserver>();
240 const resizeFn = useCallback(() => {
241 if (!container.current) {
242 return
243 }
244 const containerSize = getElementSize(container.current, props)
245 const titleSize = titleDom.current ? getElementSize(titleDom.current) : { width: 0, height: 0 };
246 const descSize = descDom.current ? getElementSize(descDom.current) : { width: 0, height: 0 };
247 let ch = (containerSize.height - titleSize.height - descSize.height);
248 if (ch === 0) {
249 // 高度为0 是因为用户没有设置高度
250 ch = 350;
251 }
252 if (ch < 20) {
253 // 设置了高度,但是太小了
254 ch = 20;
255 }
256 // 误差达到1像素后再重置,防止精度问题
257 if (Math.abs(chartHeight - ch) > 1) {
258 setChartHeight(ch);
259 }
260 }, [container.current, titleDom.current, chartHeight, descDom.current])
261 const resize = useCallback(debounce(resizeFn, 500),[resizeFn])
262
263 const FallbackComponent = React.isValidElement(errorContent) ? () => errorContent : errorContent;
264 // 每个图表的showPlaceholder 逻辑不一样,有的是判断value,该方法为静态方法
265 if (placeholder && !realCfg.data) {
266 const pl = placeholder === true ? DEFAULT_PLACEHOLDER : placeholder;
267 // plot 默认是400px高度
268 return <ErrorBoundary FallbackComponent={FallbackComponent} {...ErrorBoundaryProps}>
269 <div style={{ width: props.width || '100%', height: props.height || 400, textAlign: 'center', position: 'relative' }}>
270 {pl}
271 </div>
272 </ErrorBoundary>;
273 }
274 const titleCfg = visibleHelper(title, false) as any;
275 const descriptionCfg = visibleHelper(description, false) as any;
276 const titleStyle = {...TITLE_STYLE, ...titleCfg.style};
277 const descStyle = { ...DESCRIPTION_STYLE, ...descriptionCfg.style, top: titleStyle.height };
278 const isAutoFit = (forceFit !== undefined) ? forceFit : autoFit;
279
280 if (!isNil(forceFit)) {
281 warn(false, '请使用autoFit替代forceFit');
282 };
283
284 useEffect(() => {
285 if (!isAutoFit) {
286 if (container.current) {
287 resizeFn();
288 resizeObserver.current && resizeObserver.current.unobserve(container.current);
289 }
290 } else {
291 if (container.current) {
292 resizeFn();
293 resizeObserver.current = new ResizeObserver(resize);
294 resizeObserver.current.observe(container.current);
295 } else {
296 setChartHeight(0);
297 }
298 }
299 return () => {
300 resizeObserver.current && container.current && resizeObserver.current.unobserve(container.current)
301 };
302 }, [container.current, isAutoFit])
303
304
305 return <ErrorBoundary FallbackComponent={FallbackComponent} {...ErrorBoundaryProps}>
306 <div ref={(el) => {
307 container.current = el; // null or div
308 // 合并ref,供搭建引擎消费。原来的ref已使用,搭建引擎需要最外层div。
309 if (isMaterial) {
310 if (isFunction(ref)) {
311 ref(el);
312 } else if(ref) {
313 ref.current = el;
314 }
315 }
316 }} className="bizcharts-plot" {...containerProps} style={{ position:'relative', height: props.height || '100%', width: props.width || '100%' }}>
317 {/* title 不一定有 */}
318 { titleCfg.visible && <div ref={titleDom} {...polyfillTitleEvent(realCfg)} className="bizcharts-plot-title" style={titleStyle}>{titleCfg.text}</div> }
319 {/* description 不一定有 */}
320 { descriptionCfg.visible && <div ref={descDom} {...polyfillDescriptionEvent(realCfg)} className="bizcharts-plot-description" style={descStyle}>{descriptionCfg.text}</div> }
321 {!!chartHeight && <BxPlot
322 // API 统一
323 appendPadding={[10 , 5, 10, 10]}
324 autoFit={isAutoFit}
325 // 注意:isMaterial ref 吐的是最外层div,供ali-lowcode-engine消费。原先的消费方式不能breack。
326 ref={isMaterial ? undefined : ref}
327 {...realCfg}
328 PlotClass={PlotClass}
329 containerStyle={{
330 // 精度有误差
331 ...containerStyle,
332 height: chartHeight,
333 }}
334 />}
335 </div>
336 </ErrorBoundary>
337 });
338 Com.displayName = name || PlotClass.name;
339 return Com;
340}
341
342export default createPlot;
343
\No newline at end of file