| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import {retrieve, defaults, extend, each, isObject, map, isString, isNumber, isFunction} from 'zrender/src/core/util'; | |
| import * as graphic from '../../util/graphic'; | |
| import {getECData} from '../../util/innerStore'; | |
| import {createTextStyle} from '../../label/labelStyle'; | |
| import Model from '../../model/Model'; | |
| 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 { 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'; | |
| const PI = Math.PI; | |
| type AxisIndexKey = 'xAxisIndex' | 'yAxisIndex' | 'radiusAxisIndex' | |
| | 'angleAxisIndex' | 'singleAxisIndex'; | |
| type AxisEventData = { | |
| componentType: string | |
| componentIndex: number | |
| targetType: 'axisName' | 'axisLabel' | |
| name?: string | |
| value?: string | number | |
| dataIndex?: number | |
| tickIndex?: number | |
| } & { | |
| [key in AxisIndexKey]?: number | |
| }; | |
| type AxisLabelText = graphic.Text & { | |
| __fullText: string | |
| __truncatedText: string | |
| } & ECElement; | |
| export interface AxisBuilderCfg { | |
| position?: number[] | |
| rotation?: number | |
| /** | |
| * Used when nameLocation is 'middle' or 'center'. | |
| * 1 | -1 | |
| */ | |
| nameDirection?: number | |
| tickDirection?: number | |
| labelDirection?: number | |
| /** | |
| * Usefull when onZero. | |
| */ | |
| labelOffset?: number | |
| /** | |
| * default get from axisModel. | |
| */ | |
| axisLabelShow?: boolean | |
| /** | |
| * default get from axisModel. | |
| */ | |
| axisName?: string | |
| axisNameAvailableWidth?: number | |
| /** | |
| * by degree, default get from axisModel. | |
| */ | |
| labelRotate?: number | |
| strokeContainThreshold?: number | |
| nameTruncateMaxWidth?: number | |
| silent?: boolean | |
| handleAutoShown?(elementType: 'axisLine' | 'axisTick'): boolean | |
| } | |
| interface TickCoord { | |
| coord: number | |
| tickValue?: number | |
| } | |
| /** | |
| * A final axis is translated and rotated from a "standard axis". | |
| * So opt.position and opt.rotation is required. | |
| * | |
| * A standard axis is and axis from [0, 0] to [0, axisExtent[1]], | |
| * for example: (0, 0) ------------> (0, 50) | |
| * | |
| * nameDirection or tickDirection or labelDirection is 1 means tick | |
| * or label is below the standard axis, whereas is -1 means above | |
| * the standard axis. labelOffset means offset between label and axis, | |
| * which is useful when 'onZero', where axisLabel is in the grid and | |
| * label in outside grid. | |
| * | |
| * Tips: like always, | |
| * positive rotation represents anticlockwise, and negative rotation | |
| * represents clockwise. | |
| * The direction of position coordinate is the same as the direction | |
| * of screen coordinate. | |
| * | |
| * Do not need to consider axis 'inverse', which is auto processed by | |
| * axis extent. | |
| */ | |
| class AxisBuilder { | |
| axisModel: AxisBaseModel; | |
| opt: AxisBuilderCfg; | |
| readonly group = new graphic.Group(); | |
| private _transformGroup: graphic.Group; | |
| constructor(axisModel: AxisBaseModel, opt?: AxisBuilderCfg) { | |
| this.opt = opt; | |
| this.axisModel = axisModel; | |
| // Default value | |
| defaults( | |
| opt, | |
| { | |
| labelOffset: 0, | |
| nameDirection: 1, | |
| tickDirection: 1, | |
| labelDirection: 1, | |
| silent: true, | |
| handleAutoShown: () => true | |
| } as AxisBuilderCfg | |
| ); | |
| // FIXME Not use a seperate text group? | |
| const transformGroup = new graphic.Group({ | |
| x: opt.position[0], | |
| y: opt.position[1], | |
| rotation: opt.rotation | |
| }); | |
| // this.group.add(transformGroup); | |
| // this._transformGroup = transformGroup; | |
| transformGroup.updateTransform(); | |
| this._transformGroup = transformGroup; | |
| } | |
| hasBuilder(name: keyof typeof builders) { | |
| return !!builders[name]; | |
| } | |
| add(name: keyof typeof builders) { | |
| builders[name](this.opt, this.axisModel, this.group, this._transformGroup); | |
| } | |
| getGroup() { | |
| return this.group; | |
| } | |
| static innerTextLayout(axisRotation: number, textRotation: number, direction: number) { | |
| const rotationDiff = remRadian(textRotation - axisRotation); | |
| let textAlign; | |
| let textVerticalAlign; | |
| if (isRadianAroundZero(rotationDiff)) { // Label is parallel with axis line. | |
| textVerticalAlign = direction > 0 ? 'top' : 'bottom'; | |
| textAlign = 'center'; | |
| } | |
| else if (isRadianAroundZero(rotationDiff - PI)) { // Label is inverse parallel with axis line. | |
| textVerticalAlign = direction > 0 ? 'bottom' : 'top'; | |
| textAlign = 'center'; | |
| } | |
| else { | |
| textVerticalAlign = 'middle'; | |
| if (rotationDiff > 0 && rotationDiff < PI) { | |
| textAlign = direction > 0 ? 'right' : 'left'; | |
| } | |
| else { | |
| textAlign = direction > 0 ? 'left' : 'right'; | |
| } | |
| } | |
| return { | |
| rotation: rotationDiff, | |
| textAlign: textAlign as ZRTextAlign, | |
| textVerticalAlign: textVerticalAlign as ZRTextVerticalAlign | |
| }; | |
| } | |
| static makeAxisEventDataBase(axisModel: AxisBaseModel) { | |
| const eventData = { | |
| componentType: axisModel.mainType, | |
| componentIndex: axisModel.componentIndex | |
| } as AxisEventData; | |
| eventData[axisModel.mainType + 'Index' as AxisIndexKey] = axisModel.componentIndex; | |
| return eventData; | |
| } | |
| static isLabelSilent(axisModel: AxisBaseModel): boolean { | |
| const tooltipOpt = axisModel.get('tooltip'); | |
| return axisModel.get('silent') | |
| // Consider mouse cursor, add these restrictions. | |
| || !( | |
| axisModel.get('triggerEvent') || (tooltipOpt && tooltipOpt.show) | |
| ); | |
| } | |
| }; | |
| interface AxisElementsBuilder { | |
| ( | |
| opt: AxisBuilderCfg, | |
| axisModel: AxisBaseModel, | |
| group: graphic.Group, | |
| transformGroup: graphic.Group | |
| ):void | |
| } | |
| const builders: Record<'axisLine' | 'axisTickLabel' | 'axisName', AxisElementsBuilder> = { | |
| axisLine(opt, axisModel, group, transformGroup) { | |
| let shown = axisModel.get(['axisLine', 'show']); | |
| if (shown === 'auto' && opt.handleAutoShown) { | |
| shown = opt.handleAutoShown('axisLine'); | |
| } | |
| if (!shown) { | |
| return; | |
| } | |
| const extent = axisModel.axis.getExtent(); | |
| const matrix = transformGroup.transform; | |
| const pt1 = [extent[0], 0]; | |
| const pt2 = [extent[1], 0]; | |
| const inverse = pt1[0] > pt2[0]; | |
| if (matrix) { | |
| v2ApplyTransform(pt1, pt1, matrix); | |
| v2ApplyTransform(pt2, pt2, matrix); | |
| } | |
| const lineStyle = extend( | |
| { | |
| lineCap: 'round' | |
| }, | |
| axisModel.getModel(['axisLine', 'lineStyle']).getLineStyle() | |
| ); | |
| const line = new graphic.Line({ | |
| shape: { | |
| x1: pt1[0], | |
| y1: pt1[1], | |
| x2: pt2[0], | |
| y2: pt2[1] | |
| }, | |
| style: lineStyle, | |
| strokeContainThreshold: opt.strokeContainThreshold || 5, | |
| silent: true, | |
| z2: 1 | |
| }); | |
| graphic.subPixelOptimizeLine(line.shape, line.style.lineWidth); | |
| line.anid = 'line'; | |
| group.add(line); | |
| let arrows = axisModel.get(['axisLine', 'symbol']); | |
| if (arrows != null) { | |
| let arrowSize = axisModel.get(['axisLine', 'symbolSize']); | |
| if (isString(arrows)) { | |
| // Use the same arrow for start and end point | |
| arrows = [arrows, arrows]; | |
| } | |
| if (isString(arrowSize) || isNumber(arrowSize)) { | |
| // Use the same size for width and height | |
| arrowSize = [arrowSize as number, arrowSize as number]; | |
| } | |
| const arrowOffset = normalizeSymbolOffset(axisModel.get(['axisLine', 'symbolOffset']) || 0, arrowSize); | |
| const symbolWidth = arrowSize[0]; | |
| const symbolHeight = arrowSize[1]; | |
| each([{ | |
| rotate: opt.rotation + Math.PI / 2, | |
| offset: arrowOffset[0], | |
| r: 0 | |
| }, { | |
| rotate: opt.rotation - Math.PI / 2, | |
| offset: arrowOffset[1], | |
| r: Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) | |
| + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) | |
| }], function (point, index) { | |
| if (arrows[index] !== 'none' && arrows[index] != null) { | |
| const symbol = createSymbol( | |
| arrows[index], | |
| -symbolWidth / 2, | |
| -symbolHeight / 2, | |
| symbolWidth, | |
| symbolHeight, | |
| lineStyle.stroke, | |
| true | |
| ); | |
| // Calculate arrow position with offset | |
| const r = point.r + point.offset; | |
| const pt = inverse ? pt2 : pt1; | |
| symbol.attr({ | |
| rotation: point.rotate, | |
| x: pt[0] + r * Math.cos(opt.rotation), | |
| y: pt[1] - r * Math.sin(opt.rotation), | |
| silent: true, | |
| z2: 11 | |
| }); | |
| group.add(symbol); | |
| } | |
| }); | |
| } | |
| }, | |
| axisTickLabel(opt, axisModel, group, transformGroup) { | |
| const ticksEls = buildAxisMajorTicks(group, transformGroup, axisModel, opt); | |
| const labelEls = buildAxisLabel(group, transformGroup, axisModel, opt); | |
| fixMinMaxLabelShow(axisModel, labelEls, ticksEls); | |
| buildAxisMinorTicks(group, transformGroup, axisModel, opt.tickDirection); | |
| // This bit fixes the label overlap issue for the time chart. | |
| // See https://github.com/apache/echarts/issues/14266 for more. | |
| if (axisModel.get(['axisLabel', 'hideOverlap'])) { | |
| const labelList = prepareLayoutList(map(labelEls, label => ({ | |
| label, | |
| priority: label.z2, | |
| defaultAttr: { | |
| ignore: label.ignore | |
| } | |
| }))); | |
| hideOverlap(labelList); | |
| } | |
| }, | |
| axisName(opt, axisModel, group, transformGroup) { | |
| const name = retrieve(opt.axisName, axisModel.get('name')); | |
| if (!name) { | |
| return; | |
| } | |
| const nameLocation = axisModel.get('nameLocation'); | |
| const nameDirection = opt.nameDirection; | |
| const textStyleModel = axisModel.getModel('nameTextStyle'); | |
| const gap = axisModel.get('nameGap') || 0; | |
| const extent = axisModel.axis.getExtent(); | |
| const gapSignal = extent[0] > extent[1] ? -1 : 1; | |
| const pos = [ | |
| nameLocation === 'start' | |
| ? extent[0] - gapSignal * gap | |
| : nameLocation === 'end' | |
| ? extent[1] + gapSignal * gap | |
| : (extent[0] + extent[1]) / 2, // 'middle' | |
| // Reuse labelOffset. | |
| isNameLocationCenter(nameLocation) ? opt.labelOffset + nameDirection * gap : 0 | |
| ]; | |
| let labelLayout; | |
| let nameRotation = axisModel.get('nameRotate'); | |
| if (nameRotation != null) { | |
| nameRotation = nameRotation * PI / 180; // To radian. | |
| } | |
| let axisNameAvailableWidth; | |
| if (isNameLocationCenter(nameLocation)) { | |
| labelLayout = AxisBuilder.innerTextLayout( | |
| opt.rotation, | |
| nameRotation != null ? nameRotation : opt.rotation, // Adapt to axis. | |
| nameDirection | |
| ); | |
| } | |
| else { | |
| labelLayout = endTextLayout( | |
| opt.rotation, nameLocation, nameRotation || 0, extent | |
| ); | |
| axisNameAvailableWidth = opt.axisNameAvailableWidth; | |
| if (axisNameAvailableWidth != null) { | |
| axisNameAvailableWidth = Math.abs( | |
| axisNameAvailableWidth / Math.sin(labelLayout.rotation) | |
| ); | |
| !isFinite(axisNameAvailableWidth) && (axisNameAvailableWidth = null); | |
| } | |
| } | |
| const textFont = textStyleModel.getFont(); | |
| const truncateOpt = axisModel.get('nameTruncate', true) || {}; | |
| const ellipsis = truncateOpt.ellipsis; | |
| const maxWidth = retrieve( | |
| opt.nameTruncateMaxWidth, truncateOpt.maxWidth, axisNameAvailableWidth | |
| ); | |
| const textEl = new graphic.Text({ | |
| x: pos[0], | |
| y: pos[1], | |
| rotation: labelLayout.rotation, | |
| silent: AxisBuilder.isLabelSilent(axisModel), | |
| style: createTextStyle(textStyleModel, { | |
| text: name, | |
| font: textFont, | |
| overflow: 'truncate', | |
| width: maxWidth, | |
| ellipsis, | |
| fill: textStyleModel.getTextColor() | |
| || axisModel.get(['axisLine', 'lineStyle', 'color']) as ColorString, | |
| align: textStyleModel.get('align') | |
| || labelLayout.textAlign, | |
| verticalAlign: textStyleModel.get('verticalAlign') | |
| || labelLayout.textVerticalAlign | |
| }), | |
| z2: 1 | |
| }) as AxisLabelText; | |
| graphic.setTooltipConfig({ | |
| el: textEl, | |
| componentModel: axisModel, | |
| itemName: name | |
| }); | |
| textEl.__fullText = name; | |
| // Id for animation | |
| textEl.anid = 'name'; | |
| if (axisModel.get('triggerEvent')) { | |
| const eventData = AxisBuilder.makeAxisEventDataBase(axisModel); | |
| eventData.targetType = 'axisName'; | |
| eventData.name = name; | |
| getECData(textEl).eventData = eventData; | |
| } | |
| // FIXME | |
| transformGroup.add(textEl); | |
| textEl.updateTransform(); | |
| group.add(textEl); | |
| textEl.decomposeTransform(); | |
| } | |
| }; | |
| function endTextLayout( | |
| rotation: number, textPosition: 'start' | 'middle' | 'end', textRotate: number, extent: number[] | |
| ) { | |
| const rotationDiff = remRadian(textRotate - rotation); | |
| let textAlign: ZRTextAlign; | |
| let textVerticalAlign: ZRTextVerticalAlign; | |
| const inverse = extent[0] > extent[1]; | |
| const onLeft = (textPosition === 'start' && !inverse) | |
| || (textPosition !== 'start' && inverse); | |
| if (isRadianAroundZero(rotationDiff - PI / 2)) { | |
| textVerticalAlign = onLeft ? 'bottom' : 'top'; | |
| textAlign = 'center'; | |
| } | |
| else if (isRadianAroundZero(rotationDiff - PI * 1.5)) { | |
| textVerticalAlign = onLeft ? 'top' : 'bottom'; | |
| textAlign = 'center'; | |
| } | |
| else { | |
| textVerticalAlign = 'middle'; | |
| if (rotationDiff < PI * 1.5 && rotationDiff > PI / 2) { | |
| textAlign = onLeft ? 'left' : 'right'; | |
| } | |
| else { | |
| textAlign = onLeft ? 'right' : 'left'; | |
| } | |
| } | |
| return { | |
| rotation: rotationDiff, | |
| textAlign: textAlign, | |
| textVerticalAlign: textVerticalAlign | |
| }; | |
| } | |
| function fixMinMaxLabelShow( | |
| axisModel: AxisBaseModel, | |
| labelEls: graphic.Text[], | |
| tickEls: graphic.Line[] | |
| ) { | |
| if (shouldShowAllLabels(axisModel.axis)) { | |
| return; | |
| } | |
| // If min or max are user set, we need to check | |
| // If the tick on min(max) are overlap on their neighbour tick | |
| // If they are overlapped, we need to hide the min(max) tick label | |
| const showMinLabel = axisModel.get(['axisLabel', 'showMinLabel']); | |
| const showMaxLabel = axisModel.get(['axisLabel', 'showMaxLabel']); | |
| // FIXME | |
| // Have not consider onBand yet, where tick els is more than label els. | |
| labelEls = labelEls || []; | |
| tickEls = tickEls || []; | |
| const firstLabel = labelEls[0]; | |
| const nextLabel = labelEls[1]; | |
| const lastLabel = labelEls[labelEls.length - 1]; | |
| const prevLabel = labelEls[labelEls.length - 2]; | |
| const firstTick = tickEls[0]; | |
| const nextTick = tickEls[1]; | |
| const lastTick = tickEls[tickEls.length - 1]; | |
| const prevTick = tickEls[tickEls.length - 2]; | |
| if (showMinLabel === false) { | |
| ignoreEl(firstLabel); | |
| ignoreEl(firstTick); | |
| } | |
| else if (isTwoLabelOverlapped(firstLabel, nextLabel)) { | |
| if (showMinLabel) { | |
| ignoreEl(nextLabel); | |
| ignoreEl(nextTick); | |
| } | |
| else { | |
| ignoreEl(firstLabel); | |
| ignoreEl(firstTick); | |
| } | |
| } | |
| if (showMaxLabel === false) { | |
| ignoreEl(lastLabel); | |
| ignoreEl(lastTick); | |
| } | |
| else if (isTwoLabelOverlapped(prevLabel, lastLabel)) { | |
| if (showMaxLabel) { | |
| ignoreEl(prevLabel); | |
| ignoreEl(prevTick); | |
| } | |
| else { | |
| ignoreEl(lastLabel); | |
| ignoreEl(lastTick); | |
| } | |
| } | |
| } | |
| function ignoreEl(el: Element) { | |
| el && (el.ignore = true); | |
| } | |
| function isTwoLabelOverlapped( | |
| current: graphic.Text, | |
| next: graphic.Text | |
| ) { | |
| // current and next has the same rotation. | |
| const firstRect = current && current.getBoundingRect().clone(); | |
| const nextRect = next && next.getBoundingRect().clone(); | |
| if (!firstRect || !nextRect) { | |
| return; | |
| } | |
| // When checking intersect of two rotated labels, we use mRotationBack | |
| // to avoid that boundingRect is enlarge when using `boundingRect.applyTransform`. | |
| const mRotationBack = matrixUtil.identity([]); | |
| matrixUtil.rotate(mRotationBack, mRotationBack, -current.rotation); | |
| firstRect.applyTransform(matrixUtil.mul([], mRotationBack, current.getLocalTransform())); | |
| nextRect.applyTransform(matrixUtil.mul([], mRotationBack, next.getLocalTransform())); | |
| return firstRect.intersect(nextRect); | |
| } | |
| function isNameLocationCenter(nameLocation: string) { | |
| return nameLocation === 'middle' || nameLocation === 'center'; | |
| } | |
| function createTicks( | |
| ticksCoords: TickCoord[], | |
| tickTransform: matrixUtil.MatrixArray, | |
| tickEndCoord: number, | |
| tickLineStyle: PathStyleProps, | |
| anidPrefix: string | |
| ) { | |
| const tickEls = []; | |
| const pt1: number[] = []; | |
| const pt2: number[] = []; | |
| for (let i = 0; i < ticksCoords.length; i++) { | |
| const tickCoord = ticksCoords[i].coord; | |
| pt1[0] = tickCoord; | |
| pt1[1] = 0; | |
| pt2[0] = tickCoord; | |
| pt2[1] = tickEndCoord; | |
| if (tickTransform) { | |
| v2ApplyTransform(pt1, pt1, tickTransform); | |
| v2ApplyTransform(pt2, pt2, tickTransform); | |
| } | |
| // Tick line, Not use group transform to have better line draw | |
| const tickEl = new graphic.Line({ | |
| shape: { | |
| x1: pt1[0], | |
| y1: pt1[1], | |
| x2: pt2[0], | |
| y2: pt2[1] | |
| }, | |
| style: tickLineStyle, | |
| z2: 2, | |
| autoBatch: true, | |
| silent: true | |
| }); | |
| graphic.subPixelOptimizeLine(tickEl.shape, tickEl.style.lineWidth); | |
| tickEl.anid = anidPrefix + '_' + ticksCoords[i].tickValue; | |
| tickEls.push(tickEl); | |
| } | |
| return tickEls; | |
| } | |
| function buildAxisMajorTicks( | |
| group: graphic.Group, | |
| transformGroup: graphic.Group, | |
| axisModel: AxisBaseModel, | |
| opt: AxisBuilderCfg | |
| ) { | |
| const axis = axisModel.axis; | |
| const tickModel = axisModel.getModel('axisTick'); | |
| let shown = tickModel.get('show'); | |
| if (shown === 'auto' && opt.handleAutoShown) { | |
| shown = opt.handleAutoShown('axisTick'); | |
| } | |
| if (!shown || axis.scale.isBlank()) { | |
| return; | |
| } | |
| const lineStyleModel = tickModel.getModel('lineStyle'); | |
| const tickEndCoord = opt.tickDirection * tickModel.get('length'); | |
| const ticksCoords = axis.getTicksCoords(); | |
| const ticksEls = createTicks(ticksCoords, transformGroup.transform, tickEndCoord, defaults( | |
| lineStyleModel.getLineStyle(), | |
| { | |
| stroke: axisModel.get(['axisLine', 'lineStyle', 'color']) | |
| } | |
| ), 'ticks'); | |
| for (let i = 0; i < ticksEls.length; i++) { | |
| group.add(ticksEls[i]); | |
| } | |
| return ticksEls; | |
| } | |
| function buildAxisMinorTicks( | |
| group: graphic.Group, | |
| transformGroup: graphic.Group, | |
| axisModel: AxisBaseModel, | |
| tickDirection: number | |
| ) { | |
| const axis = axisModel.axis; | |
| const minorTickModel = axisModel.getModel('minorTick'); | |
| if (!minorTickModel.get('show') || axis.scale.isBlank()) { | |
| return; | |
| } | |
| const minorTicksCoords = axis.getMinorTicksCoords(); | |
| if (!minorTicksCoords.length) { | |
| return; | |
| } | |
| const lineStyleModel = minorTickModel.getModel('lineStyle'); | |
| const tickEndCoord = tickDirection * minorTickModel.get('length'); | |
| const minorTickLineStyle = defaults( | |
| lineStyleModel.getLineStyle(), | |
| defaults( | |
| axisModel.getModel('axisTick').getLineStyle(), | |
| { | |
| stroke: axisModel.get(['axisLine', 'lineStyle', 'color']) | |
| } | |
| ) | |
| ); | |
| for (let i = 0; i < minorTicksCoords.length; i++) { | |
| const minorTicksEls = createTicks( | |
| minorTicksCoords[i], transformGroup.transform, tickEndCoord, minorTickLineStyle, 'minorticks_' + i | |
| ); | |
| for (let k = 0; k < minorTicksEls.length; k++) { | |
| group.add(minorTicksEls[k]); | |
| } | |
| } | |
| } | |
| function buildAxisLabel( | |
| group: graphic.Group, | |
| transformGroup: graphic.Group, | |
| axisModel: AxisBaseModel, | |
| opt: AxisBuilderCfg | |
| ) { | |
| const axis = axisModel.axis; | |
| const show = retrieve(opt.axisLabelShow, axisModel.get(['axisLabel', 'show'])); | |
| if (!show || axis.scale.isBlank()) { | |
| return; | |
| } | |
| const labelModel = axisModel.getModel('axisLabel'); | |
| const labelMargin = labelModel.get('margin'); | |
| const labels = axis.getViewLabels(); | |
| // Special label rotate. | |
| const labelRotation = ( | |
| retrieve(opt.labelRotate, labelModel.get('rotate')) || 0 | |
| ) * PI / 180; | |
| const labelLayout = AxisBuilder.innerTextLayout(opt.rotation, labelRotation, opt.labelDirection); | |
| const rawCategoryData = axisModel.getCategories && axisModel.getCategories(true); | |
| const labelEls: graphic.Text[] = []; | |
| const silent = AxisBuilder.isLabelSilent(axisModel); | |
| const triggerEvent = axisModel.get('triggerEvent'); | |
| each(labels, function (labelItem, index) { | |
| const tickValue = axis.scale.type === 'ordinal' | |
| ? (axis.scale as OrdinalScale).getRawOrdinalNumber(labelItem.tickValue) | |
| : labelItem.tickValue; | |
| const formattedLabel = labelItem.formattedLabel; | |
| const rawLabel = labelItem.rawLabel; | |
| let itemLabelModel = labelModel; | |
| if (rawCategoryData && rawCategoryData[tickValue]) { | |
| const rawCategoryItem = rawCategoryData[tickValue]; | |
| if (isObject(rawCategoryItem) && rawCategoryItem.textStyle) { | |
| itemLabelModel = new Model( | |
| rawCategoryItem.textStyle, labelModel, axisModel.ecModel | |
| ); | |
| } | |
| } | |
| const textColor = itemLabelModel.getTextColor() as AxisBaseOption['axisLabel']['color'] | |
| || axisModel.get(['axisLine', 'lineStyle', 'color']); | |
| const tickCoord = axis.dataToCoord(tickValue); | |
| const textEl = new graphic.Text({ | |
| x: tickCoord, | |
| y: opt.labelOffset + opt.labelDirection * labelMargin, | |
| rotation: labelLayout.rotation, | |
| silent: silent, | |
| z2: 10 + (labelItem.level || 0), | |
| style: createTextStyle(itemLabelModel, { | |
| text: formattedLabel, | |
| align: itemLabelModel.getShallow('align', true) | |
| || labelLayout.textAlign, | |
| verticalAlign: itemLabelModel.getShallow('verticalAlign', true) | |
| || itemLabelModel.getShallow('baseline', true) | |
| || labelLayout.textVerticalAlign, | |
| fill: isFunction(textColor) | |
| ? textColor( | |
| // (1) In category axis with data zoom, tick is not the original | |
| // index of axis.data. So tick should not be exposed to user | |
| // in category axis. | |
| // (2) Compatible with previous version, which always use formatted label as | |
| // input. But in interval scale the formatted label is like '223,445', which | |
| // maked user repalce ','. So we modify it to return original val but remain | |
| // it as 'string' to avoid error in replacing. | |
| axis.type === 'category' | |
| ? rawLabel | |
| : axis.type === 'value' | |
| ? tickValue + '' | |
| : tickValue, | |
| index | |
| ) | |
| : textColor as string | |
| }) | |
| }); | |
| textEl.anid = 'label_' + tickValue; | |
| // Pack data for mouse event | |
| if (triggerEvent) { | |
| const eventData = AxisBuilder.makeAxisEventDataBase(axisModel); | |
| eventData.targetType = 'axisLabel'; | |
| eventData.value = rawLabel; | |
| eventData.tickIndex = index; | |
| if (axis.type === 'category') { | |
| eventData.dataIndex = tickValue; | |
| } | |
| getECData(textEl).eventData = eventData; | |
| } | |
| // FIXME | |
| transformGroup.add(textEl); | |
| textEl.updateTransform(); | |
| labelEls.push(textEl); | |
| group.add(textEl); | |
| textEl.decomposeTransform(); | |
| }); | |
| return labelEls; | |
| } | |
| export default AxisBuilder; |