-
Notifications
You must be signed in to change notification settings - Fork 19.7k
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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 = [ | ||
|
@@ -601,11 +601,6 @@ function isTwoLabelOverlapped( | |
return firstRect.intersect(nextRect); | ||
} | ||
|
||
function isNameLocationCenter(nameLocation: string) { | ||
return nameLocation === 'middle' || nameLocation === 'center'; | ||
} | ||
|
||
|
||
function createTicks( | ||
ticksCoords: TickCoord[], | ||
tickTransform: matrixUtil.MatrixArray, | ||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -224,6 +226,14 @@ class Axis { | |
return createAxisLabels(this).labels; | ||
} | ||
|
||
getViewLabelsAndRotation(): ReturnType<typeof createAxisLabels> { | ||
return createAxisLabels(this); | ||
} | ||
|
||
getNameGap(): ReturnType<typeof getAxisNameGap> { | ||
return getAxisNameGap(this); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
|
||
getLabelModel(): Model<AxisBaseOption['axisLabel']> { | ||
return this.model.getModel('axisLabel'); | ||
} | ||
|
@@ -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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative was adding |
||
|
||
silent?: boolean; | ||
triggerEvent?: boolean; | ||
|
@@ -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[]], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An attempted alternative was allowing the |
||
minDistance?: number, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
// true | false | null/undefined (auto) | ||
showMinLabel?: boolean, | ||
// true | false | null/undefined (auto) | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -83,6 +83,7 @@ const defaultOption: AxisBaseOption = { | |
// Whether axisLabel is inside the grid or outside the grid. | ||
inside: false, | ||
rotate: 0, | ||
minDistance: 10, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Default labels' |
||
// true | false | null/undefined (auto) | ||
showMinLabel: null, | ||
// true | false | null/undefined (auto) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to |
||
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 | ||
|
@@ -399,3 +335,7 @@ export function unionAxisExtentFromData(dataExtent: number[], data: SeriesData, | |
}); | ||
} | ||
} | ||
|
||
export function isNameLocationCenter(nameLocation: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved here from |
||
return nameLocation === 'middle' || nameLocation === 'center'; | ||
} |
There was a problem hiding this comment.
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.