import { jsx, isEqual, Component } from '@antv/f-engine'; import { deepMix, isFunction, mix, each, clone, isString, isNumber, isArray, isNil, } from '@antv/util'; import { ChartChildProps, PositionLayout } from '../../chart'; import { Style, Tick, AxisProps } from './types'; import { DataRecord } from '../../chart/Data'; type BBox = { height: number; width: number; }; const SHOW_MIDDLE_LABEL_THRESHOLD = 10; export { AxisProps }; export default (View) => { return class Axis< TRecord extends DataRecord = DataRecord, IProps extends AxisProps = AxisProps > extends Component { axisStyle: Style = {}; maxLabelWidth: number = 0; ticks: Tick[]; constructor(props: IProps & ChartChildProps) { super(props); const { chart, field } = props; const scaleOption = this.getScaleOption(props); chart.setScale(field as string, scaleOption); } willReceiveProps(nextProps: IProps & ChartChildProps) { const { props: lastProps } = this; const { chart, field } = nextProps; const nextScaleOption = this.getScaleOption(nextProps); const lastScaleOption = this.getScaleOption(lastProps); if (!isEqual(nextScaleOption, lastScaleOption)) { chart.setScale(field as string, nextScaleOption); } } willMount() { this.updateCoord(); } willUpdate() { this.updateCoord(); } getScaleOption(props: IProps) { const { type, tickCount, range, mask, formatter, ticks, min, max, nice } = props; return { type, tickCount, range, mask, formatter, min, max, nice, ticks, }; } _getDimType(): 'x' | 'y' { const { props } = this; const { field, chart } = props; const xScales = chart.getXScales(); const scales = xScales.filter((scale) => { return scale.field === field; }); return scales.length > 0 ? 'x' : 'y'; } // 获取ticks最大的宽高 getMaxBBox(ticks, style: Style): BBox { const { context } = this; const { measureText } = context; const { label, labelOffset } = style; let width = 0; let height = 0; ticks.forEach((tick: Tick) => { if (!label) return; const { labelStyle = {}, text } = tick; const bbox = measureText(labelStyle.text || text, { ...label, ...labelStyle }); width = Math.max(width, bbox.width); height = Math.max(height, bbox.height); }); if (!width && !height) { return { width, height }; } const bbox = { width: width + labelOffset, height: height + labelOffset, }; return bbox; } _getPosition() { const { props } = this; const { position, coord } = props; if (position) { return position; } const dimType = this._getDimType(); if (coord.transposed) { return dimType === 'x' ? 'left' : 'bottom'; } return dimType === 'x' ? 'bottom' : 'left'; } getTicks() { const { props } = this; const { field, chart } = props; const scale = chart.getScale(field as string); let ticks = scale.getTicks(); // 设置tick的样式 ticks = this._setTicksStyle(ticks); ticks = this._generateGridPoints(ticks); return ticks; } /** * 生成极坐标下网格线的交叉点 * @param ticks * @returns */ _generateGridPoints(ticks) { const { props } = this; const { chart, coord } = props; if (!coord.isPolar) { return ticks; } const dimType = this._getDimType(); // 只需要在 y 的时候生成 if (dimType !== 'y') { return ticks; } const xScale = chart.getXScales()[0]; const xTicks = xScale.getTicks(); ticks.forEach((tick) => { const gridPoints = xTicks.map((xTick) => { return coord.convertPoint({ x: xTick.value, y: tick.value, }); }); // 添加第 1 个点,形成环状 gridPoints.push(gridPoints[0]); tick.gridPoints = gridPoints; }); return ticks; } _setTicksStyle(ticks) { const { props, context } = this; const { theme, px2hd } = context; const { style = {} } = props; const { axis: themeAxis } = theme; each(themeAxis, (value, key) => { // 关闭tick的样式 if (style[key] === null) { return; } const styleValue = isFunction(style[key]) ? undefined : style[key]; if (isString(value) || isNumber(value)) { this.axisStyle[key] = px2hd(styleValue) || value; } else if (isArray(styleValue)) { this.axisStyle[key] = styleValue.map((d) => px2hd(deepMix(clone(value), d))); } else { this.axisStyle[key] = px2hd(deepMix(clone(value), styleValue)); } }); return ticks.map((tick: Tick, index) => { const { label, grid } = style; const { label: defaultLabelStyle, grid: defaultGridStyle } = themeAxis; if (isFunction(label)) { tick.labelStyle = px2hd(mix({}, defaultLabelStyle, label(tick.text, index, ticks))); } if (isFunction(grid)) { tick.gridStyle = px2hd(mix({}, defaultGridStyle, grid(tick.text, index, ticks.length))); } return tick; }); } convertTicks(ticks) { const { props } = this; const { coord } = props; const dimType = this._getDimType(); const otherDim = dimType === 'x' ? 'y' : 'x'; return ticks.map((tick) => { const start = coord.convertPoint({ [dimType]: tick.value, [otherDim]: 0, }); const end = coord.convertPoint({ [dimType]: tick.value, [otherDim]: 1, }); return { ...tick, points: [start, end], }; }); } calculateLabelOverflow(lastTick): number { if (!lastTick) { return 0; } const { props, context, axisStyle } = this; const { measureText } = context; const { label } = axisStyle; const { coord } = props; const { labelStyle = {}, text } = lastTick; const tickBBox = measureText(labelStyle.text || text, { ...label, ...labelStyle }); const lastTickPoint = coord.convertPoint({ x: lastTick.value, y: 0, }); let labelRightEdge = lastTickPoint.x; const { align = 'center' } = { ...label, ...labelStyle }; if (align === 'center') { labelRightEdge += tickBBox.width / 2; } else if (align === 'left' || align === 'start') { labelRightEdge += tickBBox.width; } return labelRightEdge > coord.right ? labelRightEdge - coord.right : 0; } _getXTicksDistance(ticks) { const { props } = this; const { coord } = props; const firstPoint = coord.convertPoint({ x: ticks[0].value, y: 0, }); const secondPoint = coord.convertPoint({ x: ticks[1].value, y: 0, }); return Math.abs(secondPoint.x - firstPoint.x); } measureLayout(ticks): PositionLayout | PositionLayout[] { const { props, context } = this; const { visible, coord, style, labelAutoRotate = false, labelAutoHide = false } = props; if (visible === false) { return null; } const { width: customWidth, height: customHeight } = style || {}; const bbox = this.getMaxBBox(ticks, this.axisStyle); const { isPolar } = coord; const dimType = this._getDimType(); const width = isNil(customWidth) ? bbox.width : context.px2hd(customWidth); const height = isNil(customHeight) ? bbox.height : context.px2hd(customHeight); if (isPolar) { // 机坐标系的 y 不占位置 if (dimType === 'y') { return null; } // 4 个方向都需要留空 return ['top', 'right', 'bottom', 'left'].map( (position: 'top' | 'right' | 'bottom' | 'left') => { return { position, width, height, }; } ); } // 直角坐标系下 const position = this._getPosition(); if ((labelAutoRotate || labelAutoHide) && dimType === 'x') { const lastTick = ticks[ticks.length - 1]; const overflowWidth = this.calculateLabelOverflow(lastTick); return [ { position, width, height, }, { position: 'right', width: overflowWidth, height: 0, }, ]; } return { position, width, height, }; } findSuitableRotation(ticks) { const { context } = this; const { measureText } = context; const averageSpace = this._getXTicksDistance([ticks[0], ticks[1]]); const { label } = this.axisStyle; const { labelStyle = {}, text } = ticks[0]; const bbox = measureText(labelStyle.text || text, { ...label, ...labelStyle }); const labelHeight = bbox.height; // 安全距离 const safetyDistance = 2; const availableSpace = labelHeight + safetyDistance; const sinValue = availableSpace / averageSpace; const clampedSinValue = Math.max(-1, Math.min(1, sinValue)); const theoreticalAngle = (Math.asin(clampedSinValue) * 180) / Math.PI; const ceiledAngle = Math.ceil(theoreticalAngle); if (ceiledAngle > 0 && ceiledAngle <= 90) { ticks.forEach((tick) => { tick.labelStyle = tick.labelStyle || {}; tick.labelStyle.align = 'start'; tick.labelStyle.transform = `rotate(${ceiledAngle}deg)`; tick.labelStyle.transformOrigin = '0 50%'; }); } } hasOverlapAtSeq(ticks, step) { const { px2hd } = this.context; const { safetyDistance = 2 } = this.props; const XDistance = this._getXTicksDistance([ticks[0], ticks[step]]); let prevIdx = 0; for (let currIdx = step; currIdx <= ticks.length - 1; currIdx += step) { const { label } = this.axisStyle; const { labelStyle = {} } = ticks[prevIdx]; const { align = 'center' } = { ...label, ...labelStyle }; let minDistance = (ticks[prevIdx].labelWidth + ticks[currIdx].labelWidth) / 2 + px2hd(safetyDistance); if (prevIdx === 0 && align === 'between') { minDistance += ticks[prevIdx].labelWidth / 2; } if (XDistance < minDistance) { return true; } prevIdx = currIdx; } return false; } hasOverlap(ticks) { const { context } = this; const { measureText } = context; const tickCount = ticks.length; const { label } = this.axisStyle; for (let i = 0; i < tickCount; i++) { const tick = ticks[i]; const { labelStyle = {}, text } = tick; const bbox = measureText(labelStyle.text || text, { ...label, ...labelStyle }); tick.labelWidth = bbox.width; this.maxLabelWidth = Math.max(this.maxLabelWidth, bbox.width); } return this.hasOverlapAtSeq(ticks, 1); } findLabelsToHide(ticks) { const { props } = this; const { coord } = props; const tickCount = ticks.length; const initialSeq = Math.floor(this.maxLabelWidth / (coord.width / (tickCount - 1))); const range = tickCount - 1; const maxSeq = Math.floor(range / 2); let finalSeq = initialSeq; while (finalSeq <= maxSeq && range % finalSeq !== 0) { finalSeq++; } while (finalSeq <= maxSeq && this.hasOverlapAtSeq(ticks, finalSeq)) { finalSeq++; while (finalSeq <= maxSeq && range % finalSeq !== 0) { finalSeq++; } } if (finalSeq === 1) { return; } ticks.forEach((tick) => { tick.visible = false; }); // 没找到最佳步长,则保留第一个和最后一个数据,如果总range较大,保留中间的label if (finalSeq > maxSeq) { ticks[0].visible = true; if (range > SHOW_MIDDLE_LABEL_THRESHOLD && !this.hasOverlapAtSeq(ticks, maxSeq)) { ticks[maxSeq].visible = true; } ticks[range].visible = true; return; } for (let i = 0; i <= range; i += finalSeq) { ticks[i].visible = true; } } // 计算坐标轴布局并更新坐标系 updateCoord() { const { props } = this; const { chart, labelAutoRotate = false, labelAutoHide = false } = props; const ticks = this.getTicks(); if ((labelAutoRotate || labelAutoHide) && this.hasOverlap(ticks)) { if (labelAutoRotate) { this.findSuitableRotation(ticks); } if (labelAutoHide) { this.findLabelsToHide(ticks); } this.ticks = ticks; } // 测量并获取布局信息 const layout = this.measureLayout(ticks); // 更新图表的坐标系 chart.updateCoordFor(this, layout); } render() { const { props, axisStyle } = this; const { visible, coord } = props; const dimType = this._getDimType(); if (visible === false) { return null; } const ticks = this.ticks ? this.ticks : this.getTicks(); const position = this._getPosition(); return ( ); } }; };