Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix 9265: Axis names overlap with labels #19534

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/component/axis/AxisBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ 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';
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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -601,10 +603,6 @@ function isTwoLabelOverlapped(
return firstRect.intersect(nextRect);
}

function isNameLocationCenter(nameLocation: string) {
return nameLocation === 'middle' || nameLocation === 'center';
}


function createTicks(
ticksCoords: TickCoord[],
Expand Down
151 changes: 150 additions & 1 deletion src/coord/axisHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof makeColumnLayout>;
Expand Down Expand Up @@ -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.getModel('name').option);
const defaultRotation = axis.isHorizontal() || !isNameLocationCenter(axisModel.get('nameLocation')) ? 0 : -90;
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;
Expand Down Expand Up @@ -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;
}
15 changes: 14 additions & 1 deletion src/coord/cartesian/AxisModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -53,6 +60,12 @@ export class CartesianAxisModel extends ComponentModel<CartesianAxisOption>

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;
}
Expand Down
55 changes: 39 additions & 16 deletions src/coord/cartesian/Grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,26 @@
* 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,
computeReservedSpace,
ReservedSpace,
CartesianAxisPositionMargins
} from '../../coord/axisHelper';
import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D';
import Axis2D from './Axis2D';
import {ParsedModelFinder, ParsedModelFinderKnown, SINGLE_REFERRING} from '../../util/model';

// 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';
Expand All @@ -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';
Expand Down Expand Up @@ -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();
}

Expand Down