import { isEqual, GroupStyleProps, IContext, LayoutProps, Component, Children, jsx, Ref, createRef, } from '@antv/f-engine'; import { ScaleConfig } from '../deps/f2-scale/src'; import { each, findIndex, isArray, deepMix, isEmpty } from '@antv/util'; import CoordController, { Coord } from '../controller/coord'; import ScaleController from '../controller/scale'; import Theme from '../theme'; import { Data, DataRecord, DataRecordScale } from './Data'; import { CoordType, CoordProps } from './Coord'; export { Point } from './types'; export interface ChartProps { data: Data; scale?: DataRecordScale; coord?: CoordType | CoordProps; style?: GroupStyleProps; theme?: Record; children?: any; } export interface ChartState { filters?: any; highlights?: any; } export interface ChartChildProps { data?: Data; chart?: Chart; coord?: Coord; layout?: LayoutProps; } export interface PositionLayout { position: 'top' | 'right' | 'bottom' | 'left'; width: number; height: number; } export interface ComponentPosition { component: Component; layout: PositionLayout | PositionLayout[]; } // 统计图表 class Chart< TRecord extends DataRecord = DataRecord, IProps extends ChartProps = ChartProps > extends Component { // 坐标系 private componentsPosition: ComponentPosition[] = []; // controller public coord: CoordController; public scale: ScaleController; public adjust: any; public coordRef: Ref; constructor(props: IProps, context?: IContext) { super(props); const { theme, px2hd } = context; // hack 处理,设置默认的主题样式 // 目前没想到其他更合适的方式,只能先这样处理 context.theme = deepMix(px2hd(Theme), theme); const { data } = props; this.scale = new ScaleController(data); this.coord = new CoordController(); this.coordRef = createRef(); // state this.state = { filters: {}, highlights: {}, }; } private getStyle(props: IProps) { const { context, layout } = this; const { theme, px2hd } = context; const { left, top, width, height } = layout; const { style: customStyle } = props; return px2hd({ left, top, width, height, ...theme.chart, ...customStyle, }); } willMount() { const { props, coord, scale } = this; const { scale: scaleOptions, coord: coordOption } = props; this.resetCoordLayout(); // 初始化 scale scale.create(scaleOptions); // 初始化 coord coord.create(coordOption); } // props 更新 willReceiveProps(nextProps: IProps, context) { const { scale, coord, props: lastProps } = this; const { style: nextStyle, data: nextData, scale: nextScale } = nextProps; const { style: lastStyle, data: lastData, scale: lastScale } = lastProps; // style 更新 if (!isEqual(nextStyle, lastStyle) || context !== this.context) { const style = this.getStyle(nextProps); coord.updateLayout(style); } if (nextData !== lastData) { scale.changeData(nextData); } // scale if (!isEqual(nextScale, lastScale)) { scale.update(nextScale); } } willUpdate(): void { this.coord.create(this.props.coord); } on(eventName: string, listener: (...args: any[]) => void) { const roolEl = this.coordRef.current; if (!roolEl || !roolEl.gesture) return; const gesture = roolEl.gesture; gesture.on(eventName, listener); } off(eventName: string, listener: (...args: any[]) => void) { const roolEl = this.coordRef.current; if (!roolEl || !roolEl.gesture) return; const gesture = roolEl.gesture; gesture.off(eventName, listener); } // 给需要显示的组件留空 layoutCoord(layout: PositionLayout) { this.coord.useLayout(layout); } resetCoordLayout() { const { coord, props } = this; const style = this.getStyle(props); coord.updateLayout(style); } updateAdjust(adjust: any) { this.adjust = adjust; } updateCoordLayout(layout: PositionLayout | PositionLayout[]) { if (isArray(layout)) { layout.forEach((item) => { this.layoutCoord(item); }); return; } this.layoutCoord(layout); } updateCoordFor(component: Component, layout: PositionLayout | PositionLayout[]) { if (!layout) return; const { componentsPosition } = this; const componentPosition = { component, layout }; const existIndex = findIndex(componentsPosition, (item) => { return item.component === component; }); // 说明是已经存在的组件 if (existIndex > -1) { componentsPosition.splice(existIndex, 1, componentPosition); // 先重置,然后整体重新算一次 this.resetCoordLayout(); // 再整体计算前,需要去掉已经销毁的组件 this.removeComponentsPositionCache(); componentsPosition.forEach((componentPosition) => { const { layout } = componentPosition; this.updateCoordLayout(layout); }); return; } // 是新组件,直接添加 componentsPosition.push(componentPosition); this.updateCoordLayout(layout); } removeComponentsPositionCache() { if (!this.componentsPosition?.length) return; for (let i = this.componentsPosition.length; i > -1; i--) { const item = this.componentsPosition[i]; if (item && item.component && item.component.destroyed) { this.componentsPosition.splice(i, 1); } } } getGeometrys() { // @ts-ignore const { children } = this.children; const geometrys: Component[] = []; Children.toArray(children).forEach((element) => { if (!element) return false; const { component } = element; // @ts-ignore if (component && component.isGeometry) { geometrys.push(component); } }); return geometrys; } /** * calculate dataset's position on canvas * @param {Object} record the dataset * @return {Object} return the position */ getPosition(record) { const coord = this.getCoord(); const xScale = this.getXScales()[0]; const xField = xScale.field; const yScales = this.getYScales(); // default first let yScale = yScales[0]; let yField = yScale.field; for (let i = 0, len = yScales.length; i < len; i++) { const scale = yScales[i]; const field = scale.field; if (record[field]) { yScale = scale; yField = field; break; } } const x = xScale.scale(record[xField]); const y = yScale.scale(record[yField]); return coord.convertPoint({ x, y }); } getSnapRecords(point, inCoordRange?) { const geometrys = this.getGeometrys(); if (!geometrys.length) return; // @ts-ignore return geometrys[0].getSnapRecords(point, inCoordRange); } getRecords(data, field) { const geometrys = this.getGeometrys(); if (!geometrys.length) return; // @ts-ignore return geometrys[0].getRecords(data, field); } getLegendItems(point?) { const geometrys = this.getGeometrys(); if (!geometrys.length) return; // @ts-ignore return geometrys[0].getLegendItems(point); } setScale(field: string, option: ScaleConfig) { this.scale.setScale(field, option); } getScale(field: string) { return this.scale.getScale(field); } getScales() { return this.scale.getScales(); } getXScales() { const geometrys = this.getGeometrys(); return geometrys.map((component) => { // @ts-ignore return component.getXScale(); }); } getYScales() { const geometrys = this.getGeometrys(); return geometrys.map((component) => { // @ts-ignore return component.getYScale(); }); } getColorScales() { const geometrys = this.getGeometrys(); return geometrys.map((component) => { // @ts-ignore return component.getColorScale(); }); } getLayout() { return this.coord.layout; } getCoord() { return this.coord.coord; } filter(field: string, condition) { const { filters } = this.state; this.setState({ filters: { ...filters, [field]: condition, }, }); } highlight(field: string, condition) { if (condition === null) { this.setState({ highlights: {}, }); return; } this.setState({ highlights: { [field]: condition, }, }); } _getRenderData() { const { props, state } = this; const { data } = props; const { filters } = state; if (!filters || !Object.keys(filters).length) { return data; } let filteredData = data; each(filters, (condition, field) => { if (!condition) return; filteredData = filteredData.filter((record) => { return condition(record[field], record); }); }); return filteredData; } getHighlightStyle(record) { const { highlights } = this.state; let isHighLight = false; if (isEmpty(highlights)) return; each(highlights, (condition, field) => { if (condition(record[field], record)) { isHighLight = true; return false; } }); return isHighLight ? {} : { opacity: 0.5 }; } render() { const { props, scale, layout: chartLayout } = this; const { children, data: originData } = props; if (!originData) return null; const data = this._getRenderData(); const layout = this.getLayout(); const coord = this.getCoord(); const scaleOptions = scale.getOptions(); const { width, height } = chartLayout; return ( {Children.map(children, (child) => { return Children.cloneElement(child, { data, chart: this, layout, coord, // 传 scaleOptions 是为了让 child 感知到 props 的的变化,合理的做法的应该是传递 scale,但是现在无法感知到 scale 的变化, 所以暂时只能先这么处理,scaleOptions 子组件目前是使用不到的。 scaleOptions, }); })} ); } } export default Chart;