From 945abd8571e4e12a07dbb2d29ab3ea542a71103d Mon Sep 17 00:00:00 2001 From: Robin Gerling Date: Tue, 9 Jan 2024 17:08:56 +0100 Subject: [PATCH 1/4] fix(grid): Initial version to resize grid by axis name size. close #9265 --- src/component/axis/AxisBuilder.ts | 4 +- src/coord/axisHelper.ts | 21 ++ src/coord/cartesian/AxisModel.ts | 15 +- src/coord/cartesian/Grid.ts | 326 +++++++++++++++++++++++++++++- 4 files changed, 353 insertions(+), 13 deletions(-) diff --git a/src/component/axis/AxisBuilder.ts b/src/component/axis/AxisBuilder.ts index 1644d50246..50b362960e 100644 --- a/src/component/axis/AxisBuilder.ts +++ b/src/component/axis/AxisBuilder.ts @@ -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; diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 53485fb31e..61832a5ebf 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -45,6 +45,7 @@ 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,26 @@ 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.getModel('name').option); + const defaultRotationYAxis = (axisModel.get('nameLocation') === 'end' || axisModel.get('nameLocation') === 'start') + && !axis.isHorizontal() ? 0 : -90; + const defaultRotation = axis.dim === 'x' ? 0 : defaultRotationYAxis; + const rotatedNameBoundingRect = rotateTextRect( + unRotatedNameBoundingRect, axisModel.getModel('nameRotate').option ?? defaultRotation + ); + return rotatedNameBoundingRect; +} + function rotateTextRect(textRect: RectLike, rotate: number) { const rotateRadians = rotate * Math.PI / 180; const beforeWidth = textRect.width; 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..d4f6ded009 100644 --- a/src/coord/cartesian/Grid.ts +++ b/src/coord/cartesian/Grid.ts @@ -23,14 +23,15 @@ * TODO Default cartesian */ -import {isObject, each, indexOf, retrieve3, keys} from 'zrender/src/core/util'; +import {isObject, each, indexOf, retrieve3, keys, reduce, map} from 'zrender/src/core/util'; import {getLayoutRect, LayoutRect} from '../../util/layout'; import { createScaleByModel, ifAxisCrossZero, niceScaleExtent, estimateLabelUnionRect, - getDataDimensionsOnAxis + getDataDimensionsOnAxis, + computeNameBoundingRect } from '../../coord/axisHelper'; import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D'; import Axis2D from './Axis2D'; @@ -38,7 +39,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, inverseCartesianAxisPositionMap } from './AxisModel'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { Dictionary } from 'zrender/src/core/types'; @@ -63,6 +64,15 @@ type AxesMap = { y: Axis2D[] }; +type CartesianAxisPositionMargins = {[K in CartesianAxisPosition]: number}; + +type ReservedSpace = { + labels: CartesianAxisPositionMargins, + name: CartesianAxisPositionMargins, + nameGap: CartesianAxisPositionMargins, + namePositionCurrAxis: CartesianAxisPosition +}; + class Grid implements CoordinateSystemMaster { // FIXME:TS where used (different from registered type 'cartesian2d')? @@ -186,21 +196,315 @@ class Grid implements CoordinateSystemMaster { // Minus label size if (isContainLabel) { + const reservedSpacePerAxis: ReservedSpace[] = []; each(axesList, function (axis) { + let labelUnionRect = { width: 0, height: 0}; 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; + labelUnionRect = estimateLabelUnionRect(axis); + } + + 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 nameBoundingRect = computeNameBoundingRect(axis) || { width: 0, height: 0}; + if (labelUnionRect.width !== 0 || labelUnionRect.height !== 0 + || nameBoundingRect.width !== 0 || nameBoundingRect.height !== 0) { + + 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'); + const margin = axis.model.get(['axisLabel', 'margin']); + + if (axis.inverse) { + if (nameLocation === 'start') { + nameLocation = 'end'; + } + else if (nameLocation === 'end') { + nameLocation = 'start'; + } + } + + if (axis.isHorizontal()) { if (axis.position === 'top') { - gridRect.y += labelUnionRect.height + margin; + reservedSpace.labels.top = labelUnionRect.height + margin; + } + else { + reservedSpace.labels.bottom = labelUnionRect.height + margin; + } + + if (nameLocation === 'start') { + reservedSpace.namePositionCurrAxis = 'left'; + reservedSpace.nameGap.left = nameGap; + reservedSpace.name.left = nameBoundingRect.width; + const sin = Math.sin(nameRotate * (Math.PI / 180)); + const cos = Math.cos(nameRotate * (Math.PI / 180)); + if (namePositionOrthogonalAxis === 'top') { + // 0: half height + // 90: half height + // 180: half height + // 270: half height + // 1. 0 + // 3. 0 + // 2. complete height + // 4. complete height + if (sin === 0 || sin === 1 || sin === -1 || cos === 0 || sin === 1 || cos === -1) { + reservedSpace.name.top = + nameBoundingRect.height / 2 - reservedSpace.labels.top; + } + else if (sin > 0 && cos < 0 || sin < 0 && cos > 0) { + reservedSpace.name.top = + nameBoundingRect.height - reservedSpace.labels.top; + } + else { + reservedSpace.name.top = 0; + } + } + else { + // 0: half height + // 90: half height + // 180: half height + // 270: half height + // 1. complete height + // 3. complete height + // 2. 0 + // 4. 0 + if (sin === 0 || sin === 1 || sin === -1 || cos === 0 || sin === 1 || cos === -1) { + reservedSpace.name.bottom = + nameBoundingRect.height / 2 - reservedSpace.labels.bottom; + } + else if (sin > 0 && cos > 0 || sin < 0 && cos < 0) { + reservedSpace.name.bottom = + nameBoundingRect.height - reservedSpace.labels.bottom; + } + else { + reservedSpace.name.bottom = 0; + } + } } - else if (axis.position === 'left') { - gridRect.x += labelUnionRect.width + margin; + else if (nameLocation === 'end') { + reservedSpace.namePositionCurrAxis = 'right'; + reservedSpace.nameGap.right = nameGap; + reservedSpace.name.right = nameBoundingRect.width; + const sin = Math.sin(nameRotate * (Math.PI / 180)); + const cos = Math.cos(nameRotate * (Math.PI / 180)); + if (namePositionOrthogonalAxis === 'top') { + // 0: half height + // 90: half height + // 180: half height + // 270: half height + // 1. complete height + // 3. complete height + // 2. 0 + // 4. 0 + if (sin === 0 || sin === 1 || sin === -1 || cos === 0 || sin === 1 || cos === -1) { + reservedSpace.name.top = + nameBoundingRect.height / 2 - reservedSpace.labels.top; + } + else if (sin < 0 && cos > 0 || sin > 0 && cos < 0) { + reservedSpace.name.top = + nameBoundingRect.height - reservedSpace.labels.top; + } + else { + reservedSpace.name.top = 0; + } + } + else { + // 0: half height + // 90: half height + // 180: half height + // 270: half height + // 1. 0 + // 3. 0 + // 2. complete height + // 4. complete height + if (sin === 0 || sin === 1 || sin === -1 || cos === 0 || sin === 1 || cos === -1) { + reservedSpace.name.bottom = + nameBoundingRect.height / 2 - reservedSpace.labels.bottom; + } + else if (sin > 0 && cos < 0 || sin < 0 && cos > 0) { + reservedSpace.name.bottom = + nameBoundingRect.height - reservedSpace.labels.bottom; + } + else { + reservedSpace.name.bottom = 0; + } + } + } + else { + if (axis.position === 'top') { + reservedSpace.namePositionCurrAxis = 'top'; + reservedSpace.nameGap.top = nameGap; + reservedSpace.name.top = nameBoundingRect.height; + } + else { + reservedSpace.namePositionCurrAxis = 'bottom'; + reservedSpace.nameGap.bottom = nameGap; + reservedSpace.name.bottom = nameBoundingRect.height; + } + } + } + else { + if (axis.position === 'left') { + reservedSpace.labels.left = labelUnionRect.width + margin; + } + else { + reservedSpace.labels.right = labelUnionRect.width + margin; + } + + if (nameLocation === 'start') { + reservedSpace.namePositionCurrAxis = 'bottom'; + reservedSpace.nameGap.bottom = nameGap; + reservedSpace.name.bottom = nameBoundingRect.height; + if (namePositionOrthogonalAxis === 'left') { + // 90: 0 + // 270: 0 + // 2. 0 + // 4. 0 + // 0: half width + // 180: half width + // 1. complete width + // 3. complete width + const sin = Math.sin(nameRotate * (Math.PI / 180)); + const cos = Math.cos(nameRotate * (Math.PI / 180)); + if (sin === 0 || cos === 1 || cos === -1) { + reservedSpace.name.left = + nameBoundingRect.width / 2 - reservedSpace.labels.left; + } + else if (sin > 0 && cos > 0 || sin < 0 && cos < 0) { + reservedSpace.name.left = + nameBoundingRect.width - reservedSpace.labels.left; + } + else { + reservedSpace.name.left = 0; + } + } + else { + // 90: 0 + // 270: 0 + // 1. 0 + // 3. 0 + // 0: half width + // 180: half width + // 2. complete width + // 4. complete width + const sin = Math.sin(nameRotate * (Math.PI / 180)); + const cos = Math.cos(nameRotate * (Math.PI / 180)); + if (sin === 0 || cos === 1 || cos === -1) { + reservedSpace.name.right = + nameBoundingRect.width / 2 - reservedSpace.labels.right; + } + else if (sin > 0 && cos < 0 || sin < 0 && cos > 0) { + reservedSpace.name.right = + nameBoundingRect.width - reservedSpace.labels.right; + } + else { + reservedSpace.name.right = 0; + } + } + } + else if (nameLocation === 'end') { + reservedSpace.namePositionCurrAxis = 'top'; + reservedSpace.nameGap.top = nameGap; + reservedSpace.name.top = nameBoundingRect.height; + if (namePositionOrthogonalAxis === 'left') { + // 90: 0 + // 270: 0 + // 1. 0 + // 3. 0 + // 0: half width + // 180: half width + // 2. complete width + // 4. complete width + const sin = Math.sin(nameRotate * (Math.PI / 180)); + const cos = Math.cos(nameRotate * (Math.PI / 180)); + if (sin === 0 || cos === 1 || cos === -1) { + reservedSpace.name.left = + nameBoundingRect.width / 2 - reservedSpace.labels.left; + } + else if (sin > 0 && cos < 0 || sin < 0 && cos > 0) { + reservedSpace.name.left = nameBoundingRect.width - reservedSpace.labels.left; + } + else { + reservedSpace.name.left = 0; + } + } + else { + // 90: 0 + // 270: 0 + // 2. 0 + // 4. 0 + // 0: half width + // 180: half width + // 1. complete width + // 3. complete width + const sin = Math.sin(nameRotate * (Math.PI / 180)); + const cos = Math.cos(nameRotate * (Math.PI / 180)); + if (sin === 0 || cos === 1 || cos === -1) { + reservedSpace.name.right = + nameBoundingRect.width / 2 - reservedSpace.labels.right; + } + else if (sin > 0 && cos > 0 || sin < 0 && cos < 0) { + reservedSpace.name.right = + nameBoundingRect.width - reservedSpace.labels.right; + } + else { + reservedSpace.name.right = 0; + } + } + } + else { + if (axis.position === 'left') { + reservedSpace.namePositionCurrAxis = 'left'; + reservedSpace.nameGap.left = nameGap; + reservedSpace.name.left = nameBoundingRect.width; + } + else { + reservedSpace.namePositionCurrAxis = 'right'; + reservedSpace.nameGap.right = nameGap; + reservedSpace.name.right = nameBoundingRect.width; + } } } } + reservedSpacePerAxis.push(reservedSpace); + }); + + const maxLabelSpace: CartesianAxisPositionMargins = { left: 0, top: 0, right: 0, bottom: 0}; + const maxNameAndNameGapSpace: CartesianAxisPositionMargins = { left: 0, top: 0, right: 0, bottom: 0}; + + each(['left', 'top', 'right', 'bottom'] as CartesianAxisPosition[], (direction) => { + maxLabelSpace[direction] = Math.max(...map(reservedSpacePerAxis, ({ labels }) => labels[direction])); + maxNameAndNameGapSpace[direction] = + Math.max(...map(reservedSpacePerAxis, ({ name, nameGap }) => name[direction] + nameGap[direction])); + }); + + gridRect.x += maxLabelSpace.left + maxNameAndNameGapSpace.left; + gridRect.y += maxLabelSpace.top + maxNameAndNameGapSpace.top; + gridRect.width -= gridRect.x + maxLabelSpace.right + maxNameAndNameGapSpace.right; + gridRect.height -= gridRect.y + maxLabelSpace.bottom + maxNameAndNameGapSpace.bottom; + + const allLabelMargins = reduce(['left', 'top', 'right', 'bottom'] as CartesianAxisPosition[], + (acc, key) => ({ + ...acc, + [key]: reduce(reservedSpacePerAxis, (sum, { labels }) => sum += labels[key], 0) + }), {} as CartesianAxisPositionMargins); + + axesList.forEach((axis, axisIndex) => { + axis.model.axisToNameGapStartGap = + allLabelMargins[reservedSpacePerAxis[axisIndex].namePositionCurrAxis]; }); adjustAxes(); From a50b1c1c834be74846d7b2153a8c8c5a4e04f1a8 Mon Sep 17 00:00:00 2001 From: Robin Gerling Date: Thu, 18 Jan 2024 10:20:34 +0100 Subject: [PATCH 2/4] fix(grid): Deduplicate conditions to resize grid by axis name size. close #9265 --- src/component/axis/AxisBuilder.ts | 6 +- src/coord/axisHelper.ts | 132 +++++++++++- src/coord/cartesian/Grid.ts | 331 +++-------------------------- test/axis-containLabelAndName.html | 158 ++++++++++++++ 4 files changed, 315 insertions(+), 312 deletions(-) create mode 100755 test/axis-containLabelAndName.html diff --git a/src/component/axis/AxisBuilder.ts b/src/component/axis/AxisBuilder.ts index 50b362960e..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'; @@ -603,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 61832a5ebf..2e40674021 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -40,7 +40,7 @@ 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'; @@ -420,3 +420,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/Grid.ts b/src/coord/cartesian/Grid.ts index d4f6ded009..eaecb800b1 100644 --- a/src/coord/cartesian/Grid.ts +++ b/src/coord/cartesian/Grid.ts @@ -31,7 +31,10 @@ import { niceScaleExtent, estimateLabelUnionRect, getDataDimensionsOnAxis, - computeNameBoundingRect + computeNameBoundingRect, + computeReservedSpace, + ReservedSpace, + CartesianAxisPositionMargins } from '../../coord/axisHelper'; import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D'; import Axis2D from './Axis2D'; @@ -39,7 +42,7 @@ import {ParsedModelFinder, ParsedModelFinderKnown, SINGLE_REFERRING} from '../.. // Depends on GridModel, AxisModel, which performs preprocess. import GridModel from './GridModel'; -import CartesianAxisModel, { CartesianAxisPosition, inverseCartesianAxisPositionMap } from './AxisModel'; +import CartesianAxisModel, { CartesianAxisPosition } from './AxisModel'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { Dictionary } from 'zrender/src/core/types'; @@ -54,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'; @@ -64,15 +68,6 @@ type AxesMap = { y: Axis2D[] }; -type CartesianAxisPositionMargins = {[K in CartesianAxisPosition]: number}; - -type ReservedSpace = { - labels: CartesianAxisPositionMargins, - name: CartesianAxisPositionMargins, - nameGap: CartesianAxisPositionMargins, - namePositionCurrAxis: CartesianAxisPosition -}; - class Grid implements CoordinateSystemMaster { // FIXME:TS where used (different from registered type 'cartesian2d')? @@ -194,319 +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) { - let labelUnionRect = { width: 0, height: 0}; + const nameBoundingRect = computeNameBoundingRect(axis); + + let labelUnionRect: BoundingRect; if (!axis.model.get(['axisLabel', 'inside'])) { labelUnionRect = estimateLabelUnionRect(axis); } - 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 nameBoundingRect = computeNameBoundingRect(axis) || { width: 0, height: 0}; - if (labelUnionRect.width !== 0 || labelUnionRect.height !== 0 - || nameBoundingRect.width !== 0 || nameBoundingRect.height !== 0) { - - 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'); - const margin = axis.model.get(['axisLabel', 'margin']); - - if (axis.inverse) { - if (nameLocation === 'start') { - nameLocation = 'end'; - } - else if (nameLocation === 'end') { - nameLocation = 'start'; - } - } - - if (axis.isHorizontal()) { - if (axis.position === 'top') { - reservedSpace.labels.top = labelUnionRect.height + margin; - } - else { - reservedSpace.labels.bottom = labelUnionRect.height + margin; - } - - if (nameLocation === 'start') { - reservedSpace.namePositionCurrAxis = 'left'; - reservedSpace.nameGap.left = nameGap; - reservedSpace.name.left = nameBoundingRect.width; - const sin = Math.sin(nameRotate * (Math.PI / 180)); - const cos = Math.cos(nameRotate * (Math.PI / 180)); - if (namePositionOrthogonalAxis === 'top') { - // 0: half height - // 90: half height - // 180: half height - // 270: half height - // 1. 0 - // 3. 0 - // 2. complete height - // 4. complete height - if (sin === 0 || sin === 1 || sin === -1 || cos === 0 || sin === 1 || cos === -1) { - reservedSpace.name.top = - nameBoundingRect.height / 2 - reservedSpace.labels.top; - } - else if (sin > 0 && cos < 0 || sin < 0 && cos > 0) { - reservedSpace.name.top = - nameBoundingRect.height - reservedSpace.labels.top; - } - else { - reservedSpace.name.top = 0; - } - } - else { - // 0: half height - // 90: half height - // 180: half height - // 270: half height - // 1. complete height - // 3. complete height - // 2. 0 - // 4. 0 - if (sin === 0 || sin === 1 || sin === -1 || cos === 0 || sin === 1 || cos === -1) { - reservedSpace.name.bottom = - nameBoundingRect.height / 2 - reservedSpace.labels.bottom; - } - else if (sin > 0 && cos > 0 || sin < 0 && cos < 0) { - reservedSpace.name.bottom = - nameBoundingRect.height - reservedSpace.labels.bottom; - } - else { - reservedSpace.name.bottom = 0; - } - } - } - else if (nameLocation === 'end') { - reservedSpace.namePositionCurrAxis = 'right'; - reservedSpace.nameGap.right = nameGap; - reservedSpace.name.right = nameBoundingRect.width; - const sin = Math.sin(nameRotate * (Math.PI / 180)); - const cos = Math.cos(nameRotate * (Math.PI / 180)); - if (namePositionOrthogonalAxis === 'top') { - // 0: half height - // 90: half height - // 180: half height - // 270: half height - // 1. complete height - // 3. complete height - // 2. 0 - // 4. 0 - if (sin === 0 || sin === 1 || sin === -1 || cos === 0 || sin === 1 || cos === -1) { - reservedSpace.name.top = - nameBoundingRect.height / 2 - reservedSpace.labels.top; - } - else if (sin < 0 && cos > 0 || sin > 0 && cos < 0) { - reservedSpace.name.top = - nameBoundingRect.height - reservedSpace.labels.top; - } - else { - reservedSpace.name.top = 0; - } - } - else { - // 0: half height - // 90: half height - // 180: half height - // 270: half height - // 1. 0 - // 3. 0 - // 2. complete height - // 4. complete height - if (sin === 0 || sin === 1 || sin === -1 || cos === 0 || sin === 1 || cos === -1) { - reservedSpace.name.bottom = - nameBoundingRect.height / 2 - reservedSpace.labels.bottom; - } - else if (sin > 0 && cos < 0 || sin < 0 && cos > 0) { - reservedSpace.name.bottom = - nameBoundingRect.height - reservedSpace.labels.bottom; - } - else { - reservedSpace.name.bottom = 0; - } - } - } - else { - if (axis.position === 'top') { - reservedSpace.namePositionCurrAxis = 'top'; - reservedSpace.nameGap.top = nameGap; - reservedSpace.name.top = nameBoundingRect.height; - } - else { - reservedSpace.namePositionCurrAxis = 'bottom'; - reservedSpace.nameGap.bottom = nameGap; - reservedSpace.name.bottom = nameBoundingRect.height; - } - } - } - else { - if (axis.position === 'left') { - reservedSpace.labels.left = labelUnionRect.width + margin; - } - else { - reservedSpace.labels.right = labelUnionRect.width + margin; - } - - if (nameLocation === 'start') { - reservedSpace.namePositionCurrAxis = 'bottom'; - reservedSpace.nameGap.bottom = nameGap; - reservedSpace.name.bottom = nameBoundingRect.height; - if (namePositionOrthogonalAxis === 'left') { - // 90: 0 - // 270: 0 - // 2. 0 - // 4. 0 - // 0: half width - // 180: half width - // 1. complete width - // 3. complete width - const sin = Math.sin(nameRotate * (Math.PI / 180)); - const cos = Math.cos(nameRotate * (Math.PI / 180)); - if (sin === 0 || cos === 1 || cos === -1) { - reservedSpace.name.left = - nameBoundingRect.width / 2 - reservedSpace.labels.left; - } - else if (sin > 0 && cos > 0 || sin < 0 && cos < 0) { - reservedSpace.name.left = - nameBoundingRect.width - reservedSpace.labels.left; - } - else { - reservedSpace.name.left = 0; - } - } - else { - // 90: 0 - // 270: 0 - // 1. 0 - // 3. 0 - // 0: half width - // 180: half width - // 2. complete width - // 4. complete width - const sin = Math.sin(nameRotate * (Math.PI / 180)); - const cos = Math.cos(nameRotate * (Math.PI / 180)); - if (sin === 0 || cos === 1 || cos === -1) { - reservedSpace.name.right = - nameBoundingRect.width / 2 - reservedSpace.labels.right; - } - else if (sin > 0 && cos < 0 || sin < 0 && cos > 0) { - reservedSpace.name.right = - nameBoundingRect.width - reservedSpace.labels.right; - } - else { - reservedSpace.name.right = 0; - } - } - } - else if (nameLocation === 'end') { - reservedSpace.namePositionCurrAxis = 'top'; - reservedSpace.nameGap.top = nameGap; - reservedSpace.name.top = nameBoundingRect.height; - if (namePositionOrthogonalAxis === 'left') { - // 90: 0 - // 270: 0 - // 1. 0 - // 3. 0 - // 0: half width - // 180: half width - // 2. complete width - // 4. complete width - const sin = Math.sin(nameRotate * (Math.PI / 180)); - const cos = Math.cos(nameRotate * (Math.PI / 180)); - if (sin === 0 || cos === 1 || cos === -1) { - reservedSpace.name.left = - nameBoundingRect.width / 2 - reservedSpace.labels.left; - } - else if (sin > 0 && cos < 0 || sin < 0 && cos > 0) { - reservedSpace.name.left = nameBoundingRect.width - reservedSpace.labels.left; - } - else { - reservedSpace.name.left = 0; - } - } - else { - // 90: 0 - // 270: 0 - // 2. 0 - // 4. 0 - // 0: half width - // 180: half width - // 1. complete width - // 3. complete width - const sin = Math.sin(nameRotate * (Math.PI / 180)); - const cos = Math.cos(nameRotate * (Math.PI / 180)); - if (sin === 0 || cos === 1 || cos === -1) { - reservedSpace.name.right = - nameBoundingRect.width / 2 - reservedSpace.labels.right; - } - else if (sin > 0 && cos > 0 || sin < 0 && cos < 0) { - reservedSpace.name.right = - nameBoundingRect.width - reservedSpace.labels.right; - } - else { - reservedSpace.name.right = 0; - } - } - } - else { - if (axis.position === 'left') { - reservedSpace.namePositionCurrAxis = 'left'; - reservedSpace.nameGap.left = nameGap; - reservedSpace.name.left = nameBoundingRect.width; - } - else { - reservedSpace.namePositionCurrAxis = 'right'; - reservedSpace.nameGap.right = nameGap; - reservedSpace.name.right = nameBoundingRect.width; - } - } - } - } - reservedSpacePerAxis.push(reservedSpace); + 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(['left', 'top', 'right', 'bottom'] as CartesianAxisPosition[], (direction) => { - maxLabelSpace[direction] = Math.max(...map(reservedSpacePerAxis, ({ labels }) => labels[direction])); - maxNameAndNameGapSpace[direction] = - Math.max(...map(reservedSpacePerAxis, ({ name, nameGap }) => name[direction] + nameGap[direction])); + each(cartesianAxisPositions, (position) => { + maxLabelSpace[position] = Math.max(...map(reservedSpacePerAxis, ({ labels }) => labels[position])); + maxNameAndNameGapSpace[position] = + Math.max(...map(reservedSpacePerAxis, ({ name, nameGap }) => name[position] + nameGap[position])); }); - gridRect.x += maxLabelSpace.left + maxNameAndNameGapSpace.left; - gridRect.y += maxLabelSpace.top + maxNameAndNameGapSpace.top; - gridRect.width -= gridRect.x + maxLabelSpace.right + maxNameAndNameGapSpace.right; - gridRect.height -= gridRect.y + maxLabelSpace.bottom + maxNameAndNameGapSpace.bottom; - - const allLabelMargins = reduce(['left', 'top', 'right', 'bottom'] as CartesianAxisPosition[], - (acc, key) => ({ - ...acc, - [key]: reduce(reservedSpacePerAxis, (sum, { labels }) => sum += labels[key], 0) - }), {} as CartesianAxisPositionMargins); - axesList.forEach((axis, axisIndex) => { axis.model.axisToNameGapStartGap = - allLabelMargins[reservedSpacePerAxis[axisIndex].namePositionCurrAxis]; + 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 From 1422365e104b630b4efebb919e27f4b7321a1449 Mon Sep 17 00:00:00 2001 From: Robin Gerling Date: Mon, 29 Jan 2024 12:18:22 +0100 Subject: [PATCH 3/4] fix(grid) Simplify default rotation computation of y-axis. close #9265 --- src/coord/axisHelper.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 2e40674021..b1707c96ef 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -352,9 +352,7 @@ export function computeNameBoundingRect(axis: Axis2D): BoundingRect { } const axisLabelModel = axisModel.getModel('nameTextStyle'); const unRotatedNameBoundingRect = axisLabelModel.getTextRect(axisModel.getModel('name').option); - const defaultRotationYAxis = (axisModel.get('nameLocation') === 'end' || axisModel.get('nameLocation') === 'start') - && !axis.isHorizontal() ? 0 : -90; - const defaultRotation = axis.dim === 'x' ? 0 : defaultRotationYAxis; + const defaultRotation = axis.isHorizontal() || !isNameLocationCenter(axisModel.get('nameLocation')) ? 0 : -90; const rotatedNameBoundingRect = rotateTextRect( unRotatedNameBoundingRect, axisModel.getModel('nameRotate').option ?? defaultRotation ); From ad4d68a68b08c8c97f678b1634fc50d9d18e5e9c Mon Sep 17 00:00:00 2001 From: Robin Gerling Date: Fri, 2 Aug 2024 10:46:31 +0200 Subject: [PATCH 4/4] fix(grid) Remove .option access and an unused import. close #9265 --- src/coord/axisHelper.ts | 4 ++-- src/coord/cartesian/Grid.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index b1707c96ef..6d6b3ac7f8 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -351,10 +351,10 @@ export function computeNameBoundingRect(axis: Axis2D): BoundingRect { return; } const axisLabelModel = axisModel.getModel('nameTextStyle'); - const unRotatedNameBoundingRect = axisLabelModel.getTextRect(axisModel.getModel('name').option); + const unRotatedNameBoundingRect = axisLabelModel.getTextRect(axisModel.get('name')); const defaultRotation = axis.isHorizontal() || !isNameLocationCenter(axisModel.get('nameLocation')) ? 0 : -90; const rotatedNameBoundingRect = rotateTextRect( - unRotatedNameBoundingRect, axisModel.getModel('nameRotate').option ?? defaultRotation + unRotatedNameBoundingRect, axisModel.get('nameRotate') ?? defaultRotation ); return rotatedNameBoundingRect; } diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts index eaecb800b1..2c10825aa4 100644 --- a/src/coord/cartesian/Grid.ts +++ b/src/coord/cartesian/Grid.ts @@ -23,7 +23,7 @@ * TODO Default cartesian */ -import {isObject, each, indexOf, retrieve3, keys, reduce, map} 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,