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

Merged
merged 5 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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.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;
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;
}
}
}
Comment on lines +527 to +548
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the basic idea of these lines?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important to understand lines 527 to 548 are also lines 489-493 where the case for nameLocation: center is handled. That means, lines 527 to 548 are only valid for nameLocation: start | end.
The main reason for these conditions is the different anchoring of the axis names based on the rotation quadrant.

On a horizontal axis, all multiples of 90 degrees are anchored at their mid point, why it is necessary to reserve half of the length of the name. Additionally we need to remove the space needed by the labels since the grid will later be reduced by the sum of axisLabel.margin, axis label size, nameGap, and the axis name size. But, e.g., for nameLocation: start and axis position: left, the space needed for the labels to the left and the name to the left overlaps, since the labels are to the left of the axis and half of the name is left of the axis. (E.g. the name needs 50px left of the axis and the labels 30px. Since we sum up the reserved space, it would be 80px in total. However, the farthest point left of the axis is still the start of the name 50px to the left. So the name only needs an additional space of 50px-30px.)

The else if contains all conditions for having the name rotation in different quadrants (excluding multiples of 90°). The different quadrants on different axes have different anchors resulting in a different reserved space. E.g. for axis with position: left

  • 45°: Anchor is at end of the name and name is to the left of the axis => need to reserve space to the left
  • 135°: Anchor is at end of the name and name is to the right of the axis => no need to reserve space to the right since there will be the plot
  • 225°: Anchor is at start of the name and name is to the left of the axis => need to reserve space to the left
  • 315°: Anchor is at start of the name and name is to the right of the axis => no need to reserve space to the right since there will be the plot

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you further explain what is the gap between the axis and the name gap?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 4 potential properties for each axis reducing the space of the grid. These are in order from the axis to the grid boundary: axisLabel.margin, axis label size (determined by estimateLabelUnionRect, axisHelper), nameGap, and the axis name size (determined by computeNameBoundingRect).
The axisToNameGapStart gap is the sum of the axisLabel.margin and the axis label size, so from the axis to the start of the name gap.


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, 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
Loading
Loading