import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import _uniqId from '@antv/util/lib/unique-id'; import _isFunction from '@antv/util/lib/is-function'; import withContainer from './boundary/withContainer'; import ErrorBoundary, { ErrorFallback } from './boundary/ErrorBoundary'; import RootChartContext from './context/root'; import ChartViewContext from './context/view'; import { visibleHelper } from './utils/plotTools'; import shallowEqual from './utils/shallowEqual'; import pickWithout from './utils/pickWithout'; import cloneDeep from './utils/cloneDeep'; import { REACT_PIVATE_PROPS } from './utils/constant'; import { Plot } from '@antv/g2plot/lib/core/plot'; import { ResizeObserver } from '@juggle/resize-observer'; import getElementSize from './utils/getElementSize'; import { polyfillEvents, polyfillTitleEvent, polyfillDescriptionEvent, } from './plots/core/polyfill'; import { debounce, isArray, isFunction, isNil } from '@antv/util'; import isEqual from './utils/isEqual'; import warn from 'warning'; // 国际化处理 import { registerLocale } from '@antv/g2plot/lib/core/locale'; import { EN_US_LOCALE } from '@antv/g2plot/lib/locales/en_US'; import { ZH_CN_LOCALE } from '@antv/g2plot/lib/locales/zh_CN'; /** default locale register */ registerLocale('en-US', EN_US_LOCALE); registerLocale('zh-CN', ZH_CN_LOCALE); const DEFAULT_PLACEHOLDER = (
暂无数据
); const DESCRIPTION_STYLE: CSSProperties = { padding: '8px 24px 10px 10px', fontFamily: 'PingFang SC', fontSize: 12, color: 'grey', textAlign: 'left', lineHeight: '16px', }; const TITLE_STYLE: CSSProperties = { padding: '10px 0 0 10px', fontFamily: 'PingFang SC', fontSize: 18, color: 'black', textAlign: 'left', lineHeight: '20px', }; interface VisibleText { visible?: boolean; text: string; } interface BasePlotOptions { /** * 获取g2Plot实例的勾子函数 */ onGetG2Instance?: (chart: Plot) => void; errorContent?: React.ReactNode; /** * 图表事件 */ events?: Record; /** * 图表标题。如需绑定事件请直接使用ReactNode。 */ readonly title?: React.ReactNode | VisibleText; /** * 图表副标题。如需绑定事件请直接使用ReactNode。 */ readonly description?: React.ReactNode | VisibleText; /** * 请使用autoFit替代forceFit */ forceFit?: boolean; /** * 是否是物料组件,因搭建引擎消费ref和原来的组件吐的react实例不兼容。 * 该属性会影响ref的消费,为ali-lowcode-engine消费而生。 */ isMaterial?: boolean; } export { BasePlotOptions }; class BasePlot extends React.Component { [x: string]: any; g2Instance: any; preConfig: any; public _context: { chart: any } = { chart: null }; componentDidMount() { if (this.props.children && this.g2Instance.chart) { this.g2Instance.chart.render(); } polyfillEvents(this.g2Instance, {}, this.props); this.g2Instance.data = this.props.data; this.preConfig = cloneDeep( pickWithout(this.props, [ ...REACT_PIVATE_PROPS, 'container', 'PlotClass', 'onGetG2Instance', 'data', ]), ); } componentDidUpdate(prevProps) { if (this.props.children && this.g2Instance.chart) { this.g2Instance.chart.render(); } // 兼容1.0 的events写法 polyfillEvents(this.g2Instance, prevProps, this.props); } componentWillUnmount() { if (this.g2Instance) { setTimeout(() => { this.g2Instance.destroy(); this.g2Instance = null; this._context.chart = null; }, 0); } } public getG2Instance() { return this.g2Instance; } getChartView() { return this.g2Instance.chart; } protected checkInstanceReady() { // 缓存配置 const currentConfig = pickWithout(this.props, [ ...REACT_PIVATE_PROPS, 'container', 'PlotClass', 'onGetG2Instance', 'data', ]); if (!this.g2Instance) { this.initInstance(); this.g2Instance.render(); } else if (this.shouldReCreate()) { // forceupdate this.g2Instance.destroy(); this.initInstance(); this.g2Instance.render(); } else if (this.diffConfig()) { // options更新 this.g2Instance.update({ ...currentConfig, data: this.props.data, }); } else if (this.diffData()) { this.g2Instance.changeData(this.props.data); } this.preConfig = cloneDeep(currentConfig); this.g2Instance.data = this.props.data; } initInstance() { const { container, PlotClass, onGetG2Instance, children, ...options } = this.props; this.g2Instance = new PlotClass(container, options); this._context.chart = this.g2Instance; if (_isFunction(onGetG2Instance)) { onGetG2Instance(this.g2Instance); } } diffConfig() { // 只有数据更新就不重绘,其他全部直接重新创建实例。 const preConfig = this.preConfig || {}; const currentConfig = pickWithout(this.props, [ ...REACT_PIVATE_PROPS, 'container', 'PlotClass', 'onGetG2Instance', 'data', ]); return !isEqual(preConfig, currentConfig); } diffData() { // 只有数据更新就不重绘,其他全部直接重新创建实例。 const preData = this.g2Instance.data; const data = this.props.data; if (!isArray(preData) || !isArray(data)) { // 非数组直接对比 return !preData === data; } if (preData.length !== data.length) { return true; } let isEqual = true; preData.forEach((element, index) => { if (!shallowEqual(element, data[index])) { isEqual = false; } }); return !isEqual; } shouldReCreate() { const { forceUpdate } = this.props; if (forceUpdate) { return true; } return false; } render() { this.checkInstanceReady(); const chartView = this.getChartView(); return ( {/* 每次更新都直接刷新子组件 */}
{this.props.children}
); } } const BxPlot = withContainer(BasePlot) as any; function createPlot>( PlotClass, name: string, transCfg: Function = cfg => cfg, ) { const Com = React.forwardRef((props: IPlotConfig, ref) => { // containerStyle 应该删掉,可以通过containerProps.style 配置不影响用户暂时保留 const { title, description, autoFit = true, forceFit, errorContent = ErrorFallback, containerStyle, containerProps, placeholder, ErrorBoundaryProps, isMaterial, ...cfg } = props; const realCfg = transCfg(cfg); const container = useRef(); const titleDom = useRef(); const descDom = useRef(); const [chartHeight, setChartHeight] = useState(0); const resizeObserver = useRef(); const resizeFn = useCallback(() => { if (!container.current) { return; } const containerSize = getElementSize(container.current, props); const titleSize = titleDom.current ? getElementSize(titleDom.current) : { width: 0, height: 0 }; const descSize = descDom.current ? getElementSize(descDom.current) : { width: 0, height: 0 }; let ch = containerSize.height - titleSize.height - descSize.height; if (ch === 0) { // 高度为0 是因为用户没有设置高度 ch = 350; } if (ch < 20) { // 设置了高度,但是太小了 ch = 20; } // 误差达到1像素后再重置,防止精度问题 if (Math.abs(chartHeight - ch) > 1) { setChartHeight(ch); } }, [container.current, titleDom.current, chartHeight, descDom.current]); const resize = useCallback(debounce(resizeFn, 500), [resizeFn]); const FallbackComponent = React.isValidElement(errorContent) ? () => errorContent : errorContent; // 每个图表的showPlaceholder 逻辑不一样,有的是判断value,该方法为静态方法 if (placeholder && !realCfg.data) { const pl = placeholder === true ? DEFAULT_PLACEHOLDER : placeholder; // plot 默认是400px高度 return (
{pl}
); } const titleCfg = visibleHelper(title, false) as any; const descriptionCfg = visibleHelper(description, false) as any; const titleStyle = { ...TITLE_STYLE, ...titleCfg.style }; const descStyle = { ...DESCRIPTION_STYLE, ...descriptionCfg.style, top: titleStyle.height }; const isAutoFit = forceFit !== undefined ? forceFit : autoFit; if (!isNil(forceFit)) { warn(false, '请使用autoFit替代forceFit'); } useEffect(() => { if (!isAutoFit) { if (container.current) { resizeFn(); resizeObserver.current && resizeObserver.current.unobserve(container.current); } } else { if (container.current) { resizeFn(); resizeObserver.current = new ResizeObserver(resize); resizeObserver.current.observe(container.current); } else { setChartHeight(0); } } return () => { resizeObserver.current && container.current && resizeObserver.current.unobserve(container.current); }; }, [container.current, isAutoFit]); return (
{ container.current = el; // null or div // 合并ref,供搭建引擎消费。原来的ref已使用,搭建引擎需要最外层div。 if (isMaterial) { if (isFunction(ref)) { ref(el); } else if (ref) { ref.current = el; } } }} className="bizcharts-plot" {...containerProps} style={{ position: 'relative', height: props.height || '100%', width: props.width || '100%', }} > {/* title 不一定有 */} {titleCfg.visible && (
{titleCfg.text}
)} {/* description 不一定有 */} {descriptionCfg.visible && (
{descriptionCfg.text}
)} {!!chartHeight && ( )}
); }); Com.displayName = name || PlotClass.name; return Com; } export default createPlot;