fix(图表): 统一坐标轴标签的旋转锚点与轴线间距
This commit is contained in:
@@ -792,7 +792,7 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
|
||||
...label,
|
||||
labelAutoHide: true,
|
||||
labelAutoRotate: false,
|
||||
labelTransform: `rotate(${axis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(axis)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
@@ -487,7 +487,7 @@ export class BidirectionalHorizontalBar extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle({ ...xAxis, position }),
|
||||
transform: [
|
||||
{
|
||||
type: 'hide',
|
||||
@@ -583,6 +583,11 @@ export class BidirectionalHorizontalBar extends G2ChartView {
|
||||
}
|
||||
merge(yAxisOption, { position: POSITION_MAP[yAxis.position] })
|
||||
merge(yAxisExtOption, { position: POSITION_MAP[yAxisExt.position] })
|
||||
merge(yAxisOption, this.getAxisLabelStyle({ ...yAxis, position: yAxisOption.position }))
|
||||
merge(
|
||||
yAxisExtOption,
|
||||
this.getAxisLabelStyle({ ...yAxisExt, position: yAxisExtOption.position })
|
||||
)
|
||||
// 横向布局下数值轴在上下方,G2 默认轴高度偏保守,会挤出较大的底部/顶部空白
|
||||
const axisTickLength = 4
|
||||
const axisLabelSpacing = 4
|
||||
|
||||
@@ -320,14 +320,16 @@ export class BulletGraph extends G2ChartView<RuntimeOptions, G2Bullet> {
|
||||
}
|
||||
|
||||
protected configXAxis(chart: Chart): any {
|
||||
return this.getAxisConfig(chart, 'xAxis')
|
||||
const horizontal = parseJson(chart.customAttr).basicStyle.layout === 'horizontal'
|
||||
return this.getAxisConfig(chart, 'xAxis', horizontal ? 'left' : 'bottom')
|
||||
}
|
||||
|
||||
protected configYAxis(chart: Chart): any {
|
||||
return this.getAxisConfig(chart, 'yAxis')
|
||||
const horizontal = parseJson(chart.customAttr).basicStyle.layout === 'horizontal'
|
||||
return this.getAxisConfig(chart, 'yAxis', horizontal ? 'bottom' : 'left')
|
||||
}
|
||||
|
||||
protected getAxisConfig(chart: Chart, axisType: string) {
|
||||
protected getAxisConfig(chart: Chart, axisType: string, position: string) {
|
||||
const customStyle = parseJson(chart.customStyle)
|
||||
const axis = JSON.parse(JSON.stringify(customStyle[axisType]))
|
||||
if (customStyle[axisType] && axis.show) {
|
||||
@@ -383,7 +385,7 @@ export class BulletGraph extends G2ChartView<RuntimeOptions, G2Bullet> {
|
||||
...grid,
|
||||
// 刻度值
|
||||
...label,
|
||||
labelTransform: `rotate(${axis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle({ ...axis, position })
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ export class HorizontalBar extends Bar {
|
||||
...label,
|
||||
labelAutoHide: true,
|
||||
labelAutoRotate: false,
|
||||
labelTransform: `rotate(${Math.abs(axis.axisLabel.rotate || 0)})`
|
||||
...this.getAxisLabelStyle(axis)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
@@ -156,39 +156,6 @@ export class RangeBar extends HorizontalBar {
|
||||
return newChart
|
||||
}
|
||||
|
||||
protected configXAxis(chart: PanelChart, options: ViewSpec): ViewSpec {
|
||||
const tmpOptions = super.configXAxis(chart, options)
|
||||
const { yAxis } = parseJson(chart.customStyle)
|
||||
if (tmpOptions.children[0].axis.x) {
|
||||
const rotate = Math.abs(yAxis.axisLabel.rotate || 0)
|
||||
const rotateRatio = Math.sin((rotate * Math.PI) / 180)
|
||||
// 按旋转后的横向投影补偿半个字号,保持标签与轴线的视觉间距
|
||||
tmpOptions.children[0].axis.x.labelSpacing = 4 + (yAxis.axisLabel.fontSize / 2) * rotateRatio
|
||||
if (rotate) {
|
||||
const { data, encode } = tmpOptions.children[0]
|
||||
const edgeData = yAxis.position === 'right' ? data[data.length - 1] : data[0]
|
||||
const label = `${edgeData[encode.x] ?? ''}`
|
||||
const lengthLimit = yAxis.axisLabel.lengthLimit
|
||||
const displayLabel =
|
||||
lengthLimit && label.length > lengthLimit
|
||||
? `${label.substring(0, lengthLimit)}...`
|
||||
: label
|
||||
const labelWidth = Array.from(displayLabel).reduce(
|
||||
(width, char) => width + yAxis.axisLabel.fontSize * (/[^\x00-\xff]/.test(char) ? 1 : 0.6),
|
||||
0
|
||||
)
|
||||
// 为边缘标签旋转后的纵向投影预留空间,避免超出画布被裁剪
|
||||
const edgeInset = Math.ceil((labelWidth * rotateRatio) / 2)
|
||||
if (yAxis.position === 'right') {
|
||||
tmpOptions.insetBottom = Math.max(tmpOptions.insetBottom || 0, edgeInset)
|
||||
} else {
|
||||
tmpOptions.insetTop = Math.max(tmpOptions.insetTop || 0, edgeInset)
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmpOptions
|
||||
}
|
||||
|
||||
protected configYAxis(chart: PanelChart, options: ViewSpec): ViewSpec {
|
||||
const tmpOptions = super.configYAxis(chart, options)
|
||||
if (tmpOptions.children[0].axis?.y === false) {
|
||||
@@ -200,14 +167,22 @@ export class RangeBar extends HorizontalBar {
|
||||
const rangeChart = asRangeDateChart(chart)
|
||||
const isDate = !!rangeChart.data.isDate
|
||||
const dateFormat = options.children[0].scale.y.mask
|
||||
const formatAxisValue = val => {
|
||||
if (isDate) {
|
||||
return formatRangeDate(new Date(val), dateFormat)
|
||||
}
|
||||
return valueFormatter(val, axis.axisLabelFormatter)
|
||||
}
|
||||
const formatAxisLabel = (val, index, ticks) => {
|
||||
if (ticks?.length > 1 && index === ticks.length - 1) {
|
||||
return ''
|
||||
}
|
||||
return formatAxisValue(val)
|
||||
}
|
||||
tmpOptions.children[0].axis.y = {
|
||||
...tmpOptions.children[0].axis.y,
|
||||
labelFormatter: val => {
|
||||
if (isDate) {
|
||||
return formatRangeDate(new Date(val), dateFormat)
|
||||
}
|
||||
return valueFormatter(val, axis.axisLabelFormatter)
|
||||
}
|
||||
labelAutoHide: { keepHeader: true },
|
||||
labelFormatter: formatAxisLabel
|
||||
}
|
||||
return tmpOptions
|
||||
}
|
||||
|
||||
@@ -584,7 +584,7 @@ export class StockLine extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle(xAxis),
|
||||
transform: [
|
||||
{
|
||||
type: 'hide',
|
||||
@@ -646,7 +646,7 @@ export class StockLine extends G2ChartView {
|
||||
gridLineDash,
|
||||
// 隐藏最低分割线,避免与底部 X 轴线重叠
|
||||
gridFilter: (_, index) => index !== 0,
|
||||
labelTransform: `rotate(${yAxis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle(yAxis),
|
||||
labelFormatter: d => {
|
||||
return valueFormatter(d, yAxis.axisLabelFormatter)
|
||||
}
|
||||
|
||||
@@ -479,7 +479,7 @@ export class Area extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(xAxis)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,7 +533,7 @@ export class Area extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: yAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${yAxis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle(yAxis),
|
||||
labelFormatter: d => {
|
||||
return valueFormatter(d, yAxis.axisLabelFormatter)
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ export class Line extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(xAxis)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,7 +497,7 @@ export class Line extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: yAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${yAxis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle(yAxis),
|
||||
labelFormatter: d => {
|
||||
return valueFormatter(d, yAxis.axisLabelFormatter)
|
||||
}
|
||||
|
||||
@@ -711,7 +711,7 @@ export class GroupLineMix extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(xAxis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,7 +652,7 @@ export class GroupLineMix extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(xAxis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,7 +646,7 @@ export class StackLineMix extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(xAxis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,7 +560,7 @@ export class ColumnLineMix extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(xAxis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ export class Quadrant extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle(xAxis),
|
||||
// 应用横轴标签格式配置
|
||||
labelFormatter: d => {
|
||||
return valueFormatter(d, xAxis.axisLabelFormatter)
|
||||
@@ -437,7 +437,7 @@ export class Quadrant extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: yAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${yAxis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle(yAxis),
|
||||
labelFormatter: d => {
|
||||
return valueFormatter(d, yAxis.axisLabelFormatter)
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@ export class MultiScatter extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: axisStyle.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${axisStyle.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(axisStyle)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -413,7 +413,7 @@ export class Scatter extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: xAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(xAxis)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,7 +465,7 @@ export class Scatter extends G2ChartView {
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: yAxis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${yAxis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle(yAxis),
|
||||
labelFormatter: d => {
|
||||
return valueFormatter(d, yAxis.axisLabelFormatter)
|
||||
}
|
||||
|
||||
@@ -509,7 +509,7 @@ export class TableG2Chart extends G2ChartView {
|
||||
label: xAxis.axisLabel.show,
|
||||
labelFill: xAxis.axisLabel.color,
|
||||
labelFontSize: xAxis.axisLabel.fontSize,
|
||||
labelTransform: `rotate(${xAxis.axisLabel.rotate || 0})`
|
||||
...this.getAxisLabelStyle(xAxis)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,7 +547,7 @@ export class TableG2Chart extends G2ChartView {
|
||||
label: yAxis.axisLabel.show,
|
||||
labelFill: yAxis.axisLabel.color,
|
||||
labelFontSize: yAxis.axisLabel.fontSize,
|
||||
labelTransform: `rotate(${yAxis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle(yAxis),
|
||||
labelFormatter: d => {
|
||||
const str = toString(d)
|
||||
if (!str) {
|
||||
|
||||
@@ -18,6 +18,24 @@ export const getLegendNavButtonPath = (size: number) => [
|
||||
['Z']
|
||||
]
|
||||
|
||||
let axisLabelMeasureContext: CanvasRenderingContext2D | null | undefined
|
||||
|
||||
const measureAxisLabelWidth = (value: unknown, fontSize: number) => {
|
||||
const label =
|
||||
typeof value === 'object' && value !== null
|
||||
? `${value['label'] ?? value['value'] ?? ''}`
|
||||
: `${value ?? ''}`
|
||||
axisLabelMeasureContext ??= document.createElement('canvas').getContext('2d')
|
||||
if (axisLabelMeasureContext) {
|
||||
axisLabelMeasureContext.font = `${fontSize}px sans-serif`
|
||||
return axisLabelMeasureContext.measureText(label).width
|
||||
}
|
||||
return Array.from(label).reduce(
|
||||
(width, char) => width + fontSize * (/[^\x00-\xff]/.test(char) ? 1 : 0.6),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
export interface G2DrawOptions<O> extends AntVDrawOptions<O> {
|
||||
/**
|
||||
* 缩放比例
|
||||
@@ -41,10 +59,98 @@ export abstract class G2ChartView<
|
||||
*
|
||||
* 某些图表必须读取首次渲染生成的布局信息,修正配置后再执行一次 render
|
||||
* 公共渲染组件会等待该钩子结束,再恢复联动等依赖最终图形元素的事件状态
|
||||
* 未实现该钩子的图表不会增加额外处理或渲染,原有渲染流程保持不变
|
||||
* 未实现该钩子的图表不会增加专属处理,公共轴标签边界校正独立执行
|
||||
*/
|
||||
public afterRender?(chart: P): void | Promise<void>
|
||||
|
||||
/**
|
||||
* 轴标签超出画布时改用显式内边距,优先保留标签并压缩绘图区
|
||||
*/
|
||||
public async adjustAxisLabelOverflow(chart: P): Promise<void> {
|
||||
const context = chart?.getContext()
|
||||
const canvas = context?.canvas
|
||||
const axisViews = (context?.views ?? []).filter(view =>
|
||||
view.components?.some(
|
||||
component =>
|
||||
`${component.type}`.startsWith('axis') &&
|
||||
['top', 'right', 'bottom', 'left'].includes(`${component.position}`)
|
||||
)
|
||||
)
|
||||
if (!axisViews.length) {
|
||||
return
|
||||
}
|
||||
const labels = canvas?.getRoot().querySelectorAll('.g2-axis-label-item') ?? []
|
||||
if (!labels.length) {
|
||||
return
|
||||
}
|
||||
const { width = chart.getContainer().clientWidth, height = chart.getContainer().clientHeight } =
|
||||
canvas.getConfig()
|
||||
const visibleBounds = labels
|
||||
.filter(label => label.style.visibility !== 'hidden')
|
||||
.map(label => label.getBBox())
|
||||
.filter(({ width, height }) => width > 0 && height > 0)
|
||||
if (!visibleBounds.length) {
|
||||
return
|
||||
}
|
||||
const safeSpacing = 4
|
||||
const minX = Math.min(...visibleBounds.map(({ x }) => x))
|
||||
const minY = Math.min(...visibleBounds.map(({ y }) => y))
|
||||
const maxX = Math.max(...visibleBounds.map(({ x, width }) => x + width))
|
||||
const maxY = Math.max(...visibleBounds.map(({ y, height }) => y + height))
|
||||
const overflow = {
|
||||
paddingLeft: Math.max(0, safeSpacing - minX),
|
||||
paddingRight: Math.max(0, maxX - width + safeSpacing),
|
||||
paddingTop: Math.max(0, safeSpacing - minY),
|
||||
paddingBottom: Math.max(0, maxY - height + safeSpacing)
|
||||
}
|
||||
if (Object.values(overflow).every(value => value < 1)) {
|
||||
return
|
||||
}
|
||||
const options = chart.options()
|
||||
Object.entries(overflow).forEach(([key, value]) => {
|
||||
if (value < 1) {
|
||||
return
|
||||
}
|
||||
const layoutValue = Math.max(0, ...axisViews.map(view => Number(view.layout?.[key]) || 0))
|
||||
const optionValue = typeof options[key] === 'number' ? options[key] : 0
|
||||
options[key] = Math.ceil(Math.max(layoutValue, optionValue) + value)
|
||||
})
|
||||
chart.options(options)
|
||||
await chart.render()
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一坐标轴标签的旋转锚点与轴线间距
|
||||
*/
|
||||
protected getAxisLabelStyle(axis: DeepPartial<ChartAxisStyle>): Partial<AxisComponent> {
|
||||
const position = axis.position
|
||||
const rotate = Number(axis.axisLabel.rotate) || 0
|
||||
const rotateRadian = (rotate * Math.PI) / 180
|
||||
const rotateRatio = Math.sin(Math.abs(rotateRadian))
|
||||
const fontSize = axis.axisLabel.fontSize || 12
|
||||
if (position === 'top' || position === 'bottom') {
|
||||
const direction = position === 'top' ? -1 : 1
|
||||
return {
|
||||
labelSpacing: 4,
|
||||
labelTextAlign: 'center',
|
||||
labelTextBaseline: position === 'top' ? 'bottom' : 'top',
|
||||
labelTransform: value => {
|
||||
const offset = (measureAxisLabelWidth(value, fontSize) * rotateRatio) / 2
|
||||
return `translate(0, ${(direction * offset).toFixed(2)}px) rotate(${rotate})`
|
||||
}
|
||||
}
|
||||
}
|
||||
if (position === 'left' || position === 'right') {
|
||||
return {
|
||||
labelSpacing: 4 + (fontSize * rotateRatio) / 2,
|
||||
labelTextAlign: position === 'left' ? 'right' : 'left',
|
||||
labelTextBaseline: 'middle',
|
||||
labelTransform: `rotate(${rotate})`
|
||||
}
|
||||
}
|
||||
return { labelTransform: `rotate(${rotate})` }
|
||||
}
|
||||
|
||||
protected getLegend = (chart: Chart) => {
|
||||
let legend = {}
|
||||
let customStyle: CustomStyle
|
||||
@@ -145,7 +251,7 @@ export abstract class G2ChartView<
|
||||
gridStrokeOpacity: 1,
|
||||
gridLineWidth: axis.splitLine.lineStyle.width,
|
||||
gridLineDash,
|
||||
labelTransform: `rotate(${axis.axisLabel.rotate || 0})`,
|
||||
...this.getAxisLabelStyle(axis),
|
||||
transform: [
|
||||
{
|
||||
type: 'hide',
|
||||
|
||||
@@ -602,9 +602,10 @@ const renderG2 = async (chart, chartView: G2ChartView<any, any>) => {
|
||||
})
|
||||
// 固定本轮创建的实例,避免等待异步渲染期间新一轮 renderG2 替换全局 myChart 后误操作新实例
|
||||
const chartInstance = myChart
|
||||
// 先等待 G2 首次渲染,图表视图的 afterRender 才能安全读取 view.layout 等首次布局结果
|
||||
// 先等待 G2 首次渲染,后续布局校正才能安全读取实际标签边界与 view.layout
|
||||
await chartInstance?.render()
|
||||
// 仅实现钩子的特殊图表会执行后处理;普通 G2 图表没有额外逻辑,也不会产生额外 render
|
||||
await chartView.adjustAxisLabelOverflow(chartInstance)
|
||||
// 仅实现钩子的特殊图表会继续执行专属后处理
|
||||
await chartView.afterRender?.(chartInstance)
|
||||
// 异步等待期间若图表已被新实例替换,本轮旧实例不再回放联动状态,避免污染当前画布
|
||||
if (chartInstance && chartInstance === myChart) {
|
||||
|
||||
Reference in New Issue
Block a user