UNPKG

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