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

feat(label): support labels auto-rotation #19348

Open
wants to merge 1 commit 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions src/component/axis/AxisBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {shouldShowAllLabels, isNameLocationCenter} from '../../coord/axisHelper';
import { AxisBaseModel } from '../../coord/AxisBaseModel';
import { ZRTextVerticalAlign, ZRTextAlign, ECElement, ColorString } from '../../util/types';
import { AxisBaseOption } from '../../coord/axisCommonTypes';
Expand Down Expand Up @@ -376,8 +376,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 gap = axisModel.axis.getNameGap();
const extent = axisModel.axis.getExtent();
const gapSignal = extent[0] > extent[1] ? -1 : 1;
const pos = [
Expand Down Expand Up @@ -601,11 +601,6 @@ function isTwoLabelOverlapped(
return firstRect.intersect(nextRect);
}

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

Choose a reason for hiding this comment

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

Moved to coord/axisHelper so it can be reused elsewhere.



function createTicks(
ticksCoords: TickCoord[],
tickTransform: matrixUtil.MatrixArray,
Expand Down Expand Up @@ -742,13 +737,10 @@ function buildAxisLabel(

const labelModel = axisModel.getModel('axisLabel');
const labelMargin = labelModel.get('margin');
const labels = axis.getViewLabels();
const { labels, rotation } = axis.getViewLabelsAndRotation();
Copy link
Author

Choose a reason for hiding this comment

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

Labels + calculated rotation.


// Special label rotate.
const labelRotation = (
retrieve(opt.labelRotate, labelModel.get('rotate')) || 0
) * PI / 180;

const labelRotation = (opt.labelRotate || labelModel.get('rotate') || rotation || 0) * PI / 180;
const labelLayout = AxisBuilder.innerTextLayout(opt.rotation, labelRotation, opt.labelDirection);
const rawCategoryData = axisModel.getCategories && axisModel.getCategories(true);

Expand Down
20 changes: 19 additions & 1 deletion src/coord/Axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import {linearMap, getPixelPrecision, round} from '../util/number';
import {
createAxisTicks,
createAxisLabels,
calculateCategoryInterval
calculateCategoryInterval,
calculateCategoryAutoLayout,
getAxisNameGap
} from './axisTickLabelBuilder';
import Scale from '../scale/Scale';
import { DimensionName, ScaleDataValue, ScaleTick } from '../util/types';
Expand Down Expand Up @@ -224,6 +226,14 @@ class Axis {
return createAxisLabels(this).labels;
}

getViewLabelsAndRotation(): ReturnType<typeof createAxisLabels> {
return createAxisLabels(this);
}

getNameGap(): ReturnType<typeof getAxisNameGap> {
return getAxisNameGap(this);
Copy link
Author

Choose a reason for hiding this comment

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

As correctly pointed out in #14996 (comment), the utility of the auto-rotate feature would be severely diminished unless we also gave users a way to automatically manage the location of the centered axis names. This is accomplished through the introduction of the nameLayout: 'auto' option -- please see below.

}

getLabelModel(): Model<AxisBaseOption['axisLabel']> {
return this.model.getModel('axisLabel');
}
Expand Down Expand Up @@ -269,6 +279,14 @@ class Axis {
return calculateCategoryInterval(this);
}

/**
* Only be called in category axis.
* Can be overridden, consider other axes like in 3D.
* @return Auto layout properties (interval, rotation) for cateogry axis tick and label
*/
calculateCategoryAutoLayout(interval?: number): ReturnType<typeof calculateCategoryAutoLayout> {
return calculateCategoryAutoLayout(this, interval);
}
}

function fixExtentWithBands(extent: [number, number], nTick: number): void {
Expand Down
9 changes: 8 additions & 1 deletion src/coord/axisCommonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ export interface AxisBaseOptionCommon extends ComponentOption,
placeholder?: string;
};
nameTextStyle?: AxisNameTextStyleOption;
// The gap between axisName and axisLine.
// The gap between axisName and axisLine/labels
nameGap?: number;
nameLayout?: 'auto';
Copy link
Author

Choose a reason for hiding this comment

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

An alternative was adding 'auto' as one of the nameGap's allowed values, but that would take away the ability to control the automatically managed distance between the name and the labels.


silent?: boolean;
triggerEvent?: boolean;
Expand Down Expand Up @@ -220,6 +221,8 @@ interface AxisLabelBaseOption extends Omit<TextCommonOption, 'color'> {
// Whether axisLabel is inside the grid or outside the grid.
inside?: boolean,
rotate?: number,
autoRotate?: boolean | [number, ...number[]],
Copy link
Author

Choose a reason for hiding this comment

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

An attempted alternative was allowing the auto and number[] options for axisLabel.rotate, but that created too many rippling changes through the codebase. The implemented behavior is that specifying "hard-coded" label rotation option through rotate disables autorotation.

minDistance?: number,
Copy link
Author

Choose a reason for hiding this comment

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

minDistance is the minimal-allowed distance between the labels before they are rotated/collapsed.

// true | false | null/undefined (auto)
showMinLabel?: boolean,
// true | false | null/undefined (auto)
Expand All @@ -241,7 +244,11 @@ interface AxisLabelBaseOption extends Omit<TextCommonOption, 'color'> {
// Color can be callback
color?: ColorString | ((value?: string | number, index?: number) => ColorString)
overflow?: TextStyleProps['overflow']
// approximate auto-layout computations (autoRotate, hideOverlap) if the total number of axis labels is over
// the trheshold; defaults to 40
layoutApproximationThreshold?: number
Copy link
Author

Choose a reason for hiding this comment

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

This was previously hard-coded in the code. Elevated it into an option so that users can choose when/whether they want to trigger this optimization, since it could lead to less-than-precise layout.

}

interface AxisLabelOption<TType extends OptionAxisType> extends AxisLabelBaseOption {
formatter?: LabelFormatters[TType]
}
Expand Down
1 change: 1 addition & 0 deletions src/coord/axisDefault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const defaultOption: AxisBaseOption = {
// Whether axisLabel is inside the grid or outside the grid.
inside: false,
rotate: 0,
minDistance: 10,
Copy link
Author

Choose a reason for hiding this comment

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

Default labels' minDistance to 10px to match the current observable auto-interval behavior.

// true | false | null/undefined (auto)
showMinLabel: null,
// true | false | null/undefined (auto)
Expand Down
68 changes: 4 additions & 64 deletions src/coord/axisHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
makeColumnLayout,
retrieveColumnLayout
} from '../layout/barGrid';
import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';

import TimeScale from '../scale/Time';
import Model from '../model/Model';
Expand Down Expand Up @@ -290,69 +289,6 @@ export function getAxisRawValue(axis: Axis, tick: ScaleTick): number | string {
return axis.type === 'category' ? axis.scale.getLabel(tick) : tick.value;
}

/**
* @param axis
* @return Be null/undefined if no labels.
*/
export function estimateLabelUnionRect(axis: Axis) {
Copy link
Author

@agurtovoy agurtovoy Nov 30, 2023

Choose a reason for hiding this comment

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

Moved to axisTickLabelBuilder.ts (the only place this was called from) so we can reuse label size computations we do there.

const axisModel = axis.model;
const scale = axis.scale;

if (!axisModel.get(['axisLabel', 'show']) || scale.isBlank()) {
return;
}

let realNumberScaleTicks: ScaleTick[];
let tickCount;
const categoryScaleExtent = scale.getExtent();

// Optimize for large category data, avoid call `getTicks()`.
if (scale instanceof OrdinalScale) {
tickCount = scale.count();
}
else {
realNumberScaleTicks = scale.getTicks();
tickCount = realNumberScaleTicks.length;
}

const axisLabelModel = axis.getLabelModel();
const labelFormatter = makeLabelFormatter(axis);

let rect;
let step = 1;
// Simple optimization for large amount of labels
if (tickCount > 40) {
step = Math.ceil(tickCount / 40);
}
for (let i = 0; i < tickCount; i += step) {
const tick = realNumberScaleTicks
? realNumberScaleTicks[i]
: {
value: categoryScaleExtent[0] + i
};
const label = labelFormatter(tick, i);
const unrotatedSingleRect = axisLabelModel.getTextRect(label);
const singleRect = rotateTextRect(unrotatedSingleRect, axisLabelModel.get('rotate') || 0);

rect ? rect.union(singleRect) : (rect = singleRect);
}

return rect;
}

function rotateTextRect(textRect: RectLike, rotate: number) {
const rotateRadians = rotate * Math.PI / 180;
const beforeWidth = textRect.width;
const beforeHeight = textRect.height;
const afterWidth = beforeWidth * Math.abs(Math.cos(rotateRadians))
+ Math.abs(beforeHeight * Math.sin(rotateRadians));
const afterHeight = beforeWidth * Math.abs(Math.sin(rotateRadians))
+ Math.abs(beforeHeight * Math.cos(rotateRadians));
const rotatedRect = new BoundingRect(textRect.x, textRect.y, afterWidth, afterHeight);

return rotatedRect;
}

/**
* @param model axisLabelModel or axisTickModel
* @return {number|String} Can be null|'auto'|number|function
Expand Down Expand Up @@ -399,3 +335,7 @@ export function unionAxisExtentFromData(dataExtent: number[], data: SeriesData,
});
}
}

export function isNameLocationCenter(nameLocation: string) {
Copy link
Author

Choose a reason for hiding this comment

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

Moved here from AxisBuilder.ts for cleaner reuse.

return nameLocation === 'middle' || nameLocation === 'center';
}
Loading
Loading