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 warn from 'warning'; 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 BasePlotOptions { /** * 获取g2Plot实例的勾子函数 */ onGetG2Instance?: (chart: Plot) => void; errorContent?: React.ReactNode; /** * 图表事件 */ events?: Record; /** * 图表标题。如需绑定事件请直接使用ReactNode。 */ readonly title?: React.ReactNode; /** * 图表副标题。如需绑定事件请直接使用ReactNode。 */ readonly description?: React.ReactNode; /** * 请使用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 = 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() { if (!this.g2Instance) { this.initInstance(); this.g2Instance.render(); } else if (this.shouldReCreate()) { // 只有数据更新就不重绘,其他全部直接重新创建实例。 this.g2Instance.destroy(); this.initInstance(); this.g2Instance.render(); } else if (this.diffConfig()) { const options = pickWithout(this.props, [ 'container', 'PlotClass', 'onGetG2Instance', 'children', ]); this.g2Instance.update(options); } else if (this.diffData()) { this.g2Instance.changeData(this.props.data); } // 缓存配置 const currentConfig = pickWithout(this.props, [ ...REACT_PIVATE_PROPS, 'container', 'PlotClass', 'onGetG2Instance', '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 !shallowEqual(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;