diff --git a/src/component/axis/AxisBuilder.ts b/src/component/axis/AxisBuilder.ts index 1644d50246..4fdfcdca2c 100644 --- a/src/component/axis/AxisBuilder.ts +++ b/src/component/axis/AxisBuilder.ts @@ -28,7 +28,7 @@ import {isRadianAroundZero, remRadian} from '../../util/number'; import {createSymbol, normalizeSymbolOffset} from '../../util/symbol'; import * as matrixUtil from 'zrender/src/core/matrix'; import {applyTransform as v2ApplyTransform} from 'zrender/src/core/vector'; -import {shouldShowAllLabels} from '../../coord/axisHelper'; +import {isNameLocationCenter, shouldShowAllLabels} from '../../coord/axisHelper'; import { AxisBaseModel } from '../../coord/AxisBaseModel'; import { ZRTextVerticalAlign, ZRTextAlign, ECElement, ColorString } from '../../util/types'; import { AxisBaseOption } from '../../coord/axisCommonTypes'; @@ -36,6 +36,7 @@ import Element from 'zrender/src/Element'; import { PathStyleProps } from 'zrender/src/graphic/Path'; import OrdinalScale from '../../scale/Ordinal'; import { prepareLayoutList, hideOverlap } from '../../label/labelLayoutHelper'; +import CartesianAxisModel from '../../coord/cartesian/AxisModel'; const PI = Math.PI; @@ -376,7 +377,8 @@ const builders: Record<'axisLine' | 'axisTickLabel' | 'axisName', AxisElementsBu const nameLocation = axisModel.get('nameLocation'); const nameDirection = opt.nameDirection; const textStyleModel = axisModel.getModel('nameTextStyle'); - const gap = axisModel.get('nameGap') || 0; + const axisToNameGapStartGap = axisModel instanceof CartesianAxisModel ? axisModel.axisToNameGapStartGap : 0; + const gap = (axisModel.get('nameGap') || 0) + axisToNameGapStartGap; const extent = axisModel.axis.getExtent(); const gapSignal = extent[0] > extent[1] ? -1 : 1; @@ -601,10 +603,6 @@ function isTwoLabelOverlapped( return firstRect.intersect(nextRect); } -function isNameLocationCenter(nameLocation: string) { - return nameLocation === 'middle' || nameLocation === 'center'; -} - function createTicks( ticksCoords: TickCoord[], diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 53485fb31e..6d6b3ac7f8 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -40,11 +40,12 @@ import { TimeAxisLabelFormatterOption, ValueAxisBaseOption } from './axisCommonTypes'; -import type CartesianAxisModel from './cartesian/AxisModel'; +import CartesianAxisModel, { CartesianAxisPosition, inverseCartesianAxisPositionMap } from './cartesian/AxisModel'; import SeriesData from '../data/SeriesData'; import { getStackedDimension } from '../data/helper/dataStackHelper'; import { Dictionary, DimensionName, ScaleTick, TimeScaleTick } from '../util/types'; import { ensureScaleRawExtentInfo } from './scaleRawExtentInfo'; +import Axis2D from './cartesian/Axis2D'; type BarWidthAndOffset = ReturnType; @@ -340,6 +341,24 @@ export function estimateLabelUnionRect(axis: Axis) { return rect; } +/** + * @param axis + * @return Be null/undefined if no name. + */ +export function computeNameBoundingRect(axis: Axis2D): BoundingRect { + const axisModel = axis.model; + if (!axisModel.get('name')) { + return; + } + const axisLabelModel = axisModel.getModel('nameTextStyle'); + const unRotatedNameBoundingRect = axisLabelModel.getTextRect(axisModel.get('name')); + const defaultRotation = axis.isHorizontal() || !isNameLocationCenter(axisModel.get('nameLocation')) ? 0 : -90; + const rotatedNameBoundingRect = rotateTextRect( + unRotatedNameBoundingRect, axisModel.get('nameRotate') ?? defaultRotation + ); + return rotatedNameBoundingRect; +} + function rotateTextRect(textRect: RectLike, rotate: number) { const rotateRadians = rotate * Math.PI / 180; const beforeWidth = textRect.width; @@ -399,3 +418,133 @@ export function unionAxisExtentFromData(dataExtent: number[], data: SeriesData, }); } } + +export function isNameLocationCenter(nameLocation: string) { + return nameLocation === 'middle' || nameLocation === 'center'; +} + +function isNameLocationStart(nameLocation: string) { + return nameLocation === 'start'; +} + +function isNameLocationEnd(nameLocation: string) { + return nameLocation === 'end'; +} + + +export type CartesianAxisPositionMargins = {[K in CartesianAxisPosition]: number}; + +export type ReservedSpace = { + labels: CartesianAxisPositionMargins, + name: CartesianAxisPositionMargins, + nameGap: CartesianAxisPositionMargins, + namePositionCurrAxis: CartesianAxisPosition +}; + +/* + * Compute the reserved space (determined by axis labels and axis names) in each direction + */ +export function computeReservedSpace( + axis: Axis2D, labelUnionRect: BoundingRect, nameBoundingRect: BoundingRect +): ReservedSpace { + const reservedSpace: ReservedSpace = { + labels: {left: 0, top: 0, right: 0, bottom: 0}, + nameGap: {left: 0, top: 0, right: 0, bottom: 0}, + name: {left: 0, top: 0, right: 0, bottom: 0}, + namePositionCurrAxis: null + }; + + const boundingRectDim = axis.isHorizontal() ? 'height' : 'width'; + + if (labelUnionRect) { + const margin = axis.model.get(['axisLabel', 'margin']); + reservedSpace.labels[axis.position] = labelUnionRect[boundingRectDim] + margin; + } + + if (nameBoundingRect) { + let nameLocation = axis.model.get('nameLocation'); + const onZeroOfAxis = axis.getAxesOnZeroOf()?.[0]; + let namePositionOrthogonalAxis: CartesianAxisPosition = axis.position; + if (onZeroOfAxis && ['start', 'end'].includes(nameLocation)) { + const defaultZero = onZeroOfAxis.isHorizontal() ? 'left' : 'bottom'; + namePositionOrthogonalAxis = onZeroOfAxis.inverse + ? inverseCartesianAxisPositionMap[defaultZero] + : defaultZero; + } + + const nameGap = axis.model.get('nameGap'); + const nameRotate = axis.model.get('nameRotate'); + + if (axis.inverse) { + if (nameLocation === 'start') { + nameLocation = 'end'; + } + else if (nameLocation === 'end') { + nameLocation = 'start'; + } + } + + const nameBoundingRectSize = nameBoundingRect[boundingRectDim]; + + if (isNameLocationCenter(nameLocation)) { + reservedSpace.namePositionCurrAxis = axis.position; + reservedSpace.nameGap[axis.position] = nameGap; + reservedSpace.name[axis.position] = nameBoundingRectSize; + } + else { + const inverseBoundingRectDim = boundingRectDim === 'height' ? 'width' : 'height'; + const nameBoundingRectSizeInverseDim = nameBoundingRect?.[inverseBoundingRectDim] || 0; + + const rotationInRadians = nameRotate * (Math.PI / 180); + const sin = Math.sin(rotationInRadians); + const cos = Math.cos(rotationInRadians); + + const nameRotationIsFirstOrThirdQuadrant = sin > 0 && cos > 0 || sin < 0 && cos < 0; + const nameRotationIsSecondOrFourthQuadrant = sin > 0 && cos < 0 || sin < 0 && cos > 0; + const nameRotationIsMultipleOf180degrees = sin === 0 || cos === 1 || cos === -1; + const nameRotationIsMultipleOf90degrees = + nameRotationIsMultipleOf180degrees || sin === 1 || sin === -1 || cos === 0; + + const nameLocationIsStart = isNameLocationStart(nameLocation); + const nameLocationIsEnd = isNameLocationEnd(nameLocation); + + const reservedSpacePosition = axis.isHorizontal() + ? (nameLocationIsStart ? 'left' : 'right') + : (nameLocationIsStart ? 'bottom' : 'top'); + + reservedSpace.namePositionCurrAxis = reservedSpacePosition; + reservedSpace.nameGap[reservedSpacePosition] = nameGap; + reservedSpace.name[reservedSpacePosition] = nameBoundingRectSizeInverseDim; + + const reservedLabelSpace = reservedSpace.labels[namePositionOrthogonalAxis]; + const reservedNameSpace = nameBoundingRectSize - reservedLabelSpace; + + const orthogonalAxisPositionIsTop = namePositionOrthogonalAxis === 'top'; + const orthogonalAxisPositionIsBottom = namePositionOrthogonalAxis === 'bottom'; + const orthogonalAxisPositionIsLeft = namePositionOrthogonalAxis === 'left'; + const orthogonalAxisPositionIsRight = namePositionOrthogonalAxis === 'right'; + + if (axis.isHorizontal() && nameRotationIsMultipleOf90degrees + || !axis.isHorizontal() && nameRotationIsMultipleOf180degrees) { + reservedSpace.name[namePositionOrthogonalAxis] = nameBoundingRectSize / 2 - reservedLabelSpace; + } + else if ( + axis.isHorizontal() && ( + nameLocationIsStart && orthogonalAxisPositionIsTop && nameRotationIsSecondOrFourthQuadrant + || nameLocationIsStart && orthogonalAxisPositionIsBottom && nameRotationIsFirstOrThirdQuadrant + || nameLocationIsEnd && orthogonalAxisPositionIsTop && nameRotationIsFirstOrThirdQuadrant + || nameLocationIsEnd && orthogonalAxisPositionIsBottom && nameRotationIsSecondOrFourthQuadrant + ) + || !axis.isHorizontal() && ( + nameLocationIsStart && orthogonalAxisPositionIsLeft && nameRotationIsFirstOrThirdQuadrant + || nameLocationIsStart && orthogonalAxisPositionIsRight && nameRotationIsSecondOrFourthQuadrant + || nameLocationIsEnd && orthogonalAxisPositionIsLeft && nameRotationIsSecondOrFourthQuadrant + || nameLocationIsEnd && orthogonalAxisPositionIsRight && nameRotationIsFirstOrThirdQuadrant + ) + ) { + reservedSpace.name[namePositionOrthogonalAxis] = reservedNameSpace; + } + } + } + return reservedSpace; +} \ No newline at end of file diff --git a/src/coord/cartesian/AxisModel.ts b/src/coord/cartesian/AxisModel.ts index 6b39adafe1..d501e4fed7 100644 --- a/src/coord/cartesian/AxisModel.ts +++ b/src/coord/cartesian/AxisModel.ts @@ -25,11 +25,18 @@ import Axis2D from './Axis2D'; import { AxisBaseOption } from '../axisCommonTypes'; import GridModel from './GridModel'; import { AxisBaseModel } from '../AxisBaseModel'; -import {OrdinalSortInfo} from '../../util/types'; +import { OrdinalSortInfo } from '../../util/types'; import { SINGLE_REFERRING } from '../../util/model'; export type CartesianAxisPosition = 'top' | 'bottom' | 'left' | 'right'; +export const inverseCartesianAxisPositionMap = { + left: 'right', + right: 'left', + top: 'bottom', + bottom: 'top' +} as const; + export type CartesianAxisOption = AxisBaseOption & { gridIndex?: number; gridId?: string; @@ -53,6 +60,12 @@ export class CartesianAxisModel extends ComponentModel axis: Axis2D; + /** + * The gap between the axis and the name gap. + * Injected outside. + */ + axisToNameGapStartGap: number = 0; + getCoordSysModel(): GridModel { return this.getReferringComponents('grid', SINGLE_REFERRING).models[0] as GridModel; } diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts index d22ddef556..2c10825aa4 100644 --- a/src/coord/cartesian/Grid.ts +++ b/src/coord/cartesian/Grid.ts @@ -23,14 +23,18 @@ * TODO Default cartesian */ -import {isObject, each, indexOf, retrieve3, keys} from 'zrender/src/core/util'; +import {isObject, each, indexOf, retrieve3, keys, map} from 'zrender/src/core/util'; import {getLayoutRect, LayoutRect} from '../../util/layout'; import { createScaleByModel, ifAxisCrossZero, niceScaleExtent, estimateLabelUnionRect, - getDataDimensionsOnAxis + getDataDimensionsOnAxis, + computeNameBoundingRect, + computeReservedSpace, + ReservedSpace, + CartesianAxisPositionMargins } from '../../coord/axisHelper'; import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D'; import Axis2D from './Axis2D'; @@ -38,7 +42,7 @@ import {ParsedModelFinder, ParsedModelFinderKnown, SINGLE_REFERRING} from '../.. // Depends on GridModel, AxisModel, which performs preprocess. import GridModel from './GridModel'; -import CartesianAxisModel from './AxisModel'; +import CartesianAxisModel, { CartesianAxisPosition } from './AxisModel'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { Dictionary } from 'zrender/src/core/types'; @@ -53,6 +57,7 @@ import { isIntervalOrLogScale } from '../../scale/helper'; import { alignScaleTicks } from '../axisAlignTicks'; import IntervalScale from '../../scale/Interval'; import LogScale from '../../scale/Log'; +import { BoundingRect } from 'zrender'; type Cartesian2DDimensionName = 'x' | 'y'; @@ -184,25 +189,43 @@ class Grid implements CoordinateSystemMaster { adjustAxes(); - // Minus label size + // Minus label, name, and nameGap size if (isContainLabel) { + const reservedSpacePerAxis: ReservedSpace[] = []; each(axesList, function (axis) { + const nameBoundingRect = computeNameBoundingRect(axis); + + let labelUnionRect: BoundingRect; if (!axis.model.get(['axisLabel', 'inside'])) { - const labelUnionRect = estimateLabelUnionRect(axis); - if (labelUnionRect) { - const dim: 'height' | 'width' = axis.isHorizontal() ? 'height' : 'width'; - const margin = axis.model.get(['axisLabel', 'margin']); - gridRect[dim] -= labelUnionRect[dim] + margin; - if (axis.position === 'top') { - gridRect.y += labelUnionRect.height + margin; - } - else if (axis.position === 'left') { - gridRect.x += labelUnionRect.width + margin; - } - } + labelUnionRect = estimateLabelUnionRect(axis); } + + reservedSpacePerAxis.push(computeReservedSpace(axis, labelUnionRect, nameBoundingRect)); + }); + + const maxLabelSpace: CartesianAxisPositionMargins = { left: 0, top: 0, right: 0, bottom: 0}; + const maxNameAndNameGapSpace: CartesianAxisPositionMargins = { left: 0, top: 0, right: 0, bottom: 0}; + const cartesianAxisPositions: CartesianAxisPosition[] = ['left', 'top', 'right', 'bottom']; + + each(cartesianAxisPositions, (position) => { + maxLabelSpace[position] = Math.max(...map(reservedSpacePerAxis, ({ labels }) => labels[position])); + maxNameAndNameGapSpace[position] = + Math.max(...map(reservedSpacePerAxis, ({ name, nameGap }) => name[position] + nameGap[position])); + }); + + axesList.forEach((axis, axisIndex) => { + axis.model.axisToNameGapStartGap = + maxLabelSpace[reservedSpacePerAxis[axisIndex].namePositionCurrAxis]; }); + const maxReservedSpaceLeft = maxLabelSpace.left + maxNameAndNameGapSpace.left; + const maxReservedSpaceTop = maxLabelSpace.top + maxNameAndNameGapSpace.top; + + gridRect.x += maxReservedSpaceLeft; + gridRect.y += maxReservedSpaceTop; + gridRect.width -= maxReservedSpaceLeft + maxLabelSpace.right + maxNameAndNameGapSpace.right; + gridRect.height -= maxReservedSpaceTop + maxLabelSpace.bottom + maxNameAndNameGapSpace.bottom; + adjustAxes(); } diff --git a/test/axis-containLabelAndName.html b/test/axis-containLabelAndName.html new file mode 100755 index 0000000000..ea455a913d --- /dev/null +++ b/test/axis-containLabelAndName.html @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + +
+ + + + + + + + \ No newline at end of file