Spaces:
Running
Running
| /* | |
| * 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 {each, map} from 'zrender/src/core/util'; | |
| import {linearMap, getPixelPrecision, round} from '../util/number'; | |
| import { | |
| createAxisTicks, | |
| createAxisLabels, | |
| calculateCategoryInterval | |
| } from './axisTickLabelBuilder'; | |
| import Scale from '../scale/Scale'; | |
| import { DimensionName, ScaleDataValue, ScaleTick } from '../util/types'; | |
| import OrdinalScale from '../scale/Ordinal'; | |
| import Model from '../model/Model'; | |
| import { AxisBaseOption, CategoryAxisBaseOption, OptionAxisType } from './axisCommonTypes'; | |
| import { AxisBaseModel } from './AxisBaseModel'; | |
| const NORMALIZED_EXTENT = [0, 1] as [number, number]; | |
| interface TickCoord { | |
| coord: number; | |
| // That is `scaleTick.value`. | |
| tickValue?: ScaleTick['value']; | |
| } | |
| /** | |
| * Base class of Axis. | |
| */ | |
| class Axis { | |
| /** | |
| * Axis type | |
| * - 'category' | |
| * - 'value' | |
| * - 'time' | |
| * - 'log' | |
| */ | |
| type: OptionAxisType; | |
| // Axis dimension. Such as 'x', 'y', 'z', 'angle', 'radius'. | |
| readonly dim: DimensionName; | |
| // Axis scale | |
| scale: Scale; | |
| private _extent: [number, number]; | |
| // Injected outside | |
| model: AxisBaseModel; | |
| onBand: CategoryAxisBaseOption['boundaryGap'] = false; | |
| inverse: AxisBaseOption['inverse'] = false; | |
| constructor(dim: DimensionName, scale: Scale, extent: [number, number]) { | |
| this.dim = dim; | |
| this.scale = scale; | |
| this._extent = extent || [0, 0]; | |
| } | |
| /** | |
| * If axis extent contain given coord | |
| */ | |
| contain(coord: number): boolean { | |
| const extent = this._extent; | |
| const min = Math.min(extent[0], extent[1]); | |
| const max = Math.max(extent[0], extent[1]); | |
| return coord >= min && coord <= max; | |
| } | |
| /** | |
| * If axis extent contain given data | |
| */ | |
| containData(data: ScaleDataValue): boolean { | |
| return this.scale.contain(data); | |
| } | |
| /** | |
| * Get coord extent. | |
| */ | |
| getExtent(): [number, number] { | |
| return this._extent.slice() as [number, number]; | |
| } | |
| /** | |
| * Get precision used for formatting | |
| */ | |
| getPixelPrecision(dataExtent?: [number, number]): number { | |
| return getPixelPrecision( | |
| dataExtent || this.scale.getExtent(), | |
| this._extent | |
| ); | |
| } | |
| /** | |
| * Set coord extent | |
| */ | |
| setExtent(start: number, end: number): void { | |
| const extent = this._extent; | |
| extent[0] = start; | |
| extent[1] = end; | |
| } | |
| /** | |
| * Convert data to coord. Data is the rank if it has an ordinal scale | |
| */ | |
| dataToCoord(data: ScaleDataValue, clamp?: boolean): number { | |
| let extent = this._extent; | |
| const scale = this.scale; | |
| data = scale.normalize(data); | |
| if (this.onBand && scale.type === 'ordinal') { | |
| extent = extent.slice() as [number, number]; | |
| fixExtentWithBands(extent, (scale as OrdinalScale).count()); | |
| } | |
| return linearMap(data, NORMALIZED_EXTENT, extent, clamp); | |
| } | |
| /** | |
| * Convert coord to data. Data is the rank if it has an ordinal scale | |
| */ | |
| coordToData(coord: number, clamp?: boolean): number { | |
| let extent = this._extent; | |
| const scale = this.scale; | |
| if (this.onBand && scale.type === 'ordinal') { | |
| extent = extent.slice() as [number, number]; | |
| fixExtentWithBands(extent, (scale as OrdinalScale).count()); | |
| } | |
| const t = linearMap(coord, extent, NORMALIZED_EXTENT, clamp); | |
| return this.scale.scale(t); | |
| } | |
| /** | |
| * Convert pixel point to data in axis | |
| */ | |
| pointToData(point: number[], clamp?: boolean): number { | |
| // Should be implemented in derived class if necessary. | |
| return; | |
| } | |
| /** | |
| * Different from `zrUtil.map(axis.getTicks(), axis.dataToCoord, axis)`, | |
| * `axis.getTicksCoords` considers `onBand`, which is used by | |
| * `boundaryGap:true` of category axis and splitLine and splitArea. | |
| * @param opt.tickModel default: axis.model.getModel('axisTick') | |
| * @param opt.clamp If `true`, the first and the last | |
| * tick must be at the axis end points. Otherwise, clip ticks | |
| * that outside the axis extent. | |
| */ | |
| getTicksCoords(opt?: { | |
| tickModel?: Model, | |
| clamp?: boolean | |
| }): TickCoord[] { | |
| opt = opt || {}; | |
| const tickModel = opt.tickModel || this.getTickModel(); | |
| const result = createAxisTicks(this, tickModel as AxisBaseModel); | |
| const ticks = result.ticks; | |
| const ticksCoords = map(ticks, function (tickVal) { | |
| return { | |
| coord: this.dataToCoord( | |
| this.scale.type === 'ordinal' | |
| ? (this.scale as OrdinalScale).getRawOrdinalNumber(tickVal) | |
| : tickVal | |
| ), | |
| tickValue: tickVal | |
| }; | |
| }, this); | |
| const alignWithLabel = tickModel.get('alignWithLabel'); | |
| fixOnBandTicksCoords( | |
| this, ticksCoords, alignWithLabel, opt.clamp | |
| ); | |
| return ticksCoords; | |
| } | |
| getMinorTicksCoords(): TickCoord[][] { | |
| if (this.scale.type === 'ordinal') { | |
| // Category axis doesn't support minor ticks | |
| return []; | |
| } | |
| const minorTickModel = this.model.getModel('minorTick'); | |
| let splitNumber = minorTickModel.get('splitNumber'); | |
| // Protection. | |
| if (!(splitNumber > 0 && splitNumber < 100)) { | |
| splitNumber = 5; | |
| } | |
| const minorTicks = this.scale.getMinorTicks(splitNumber); | |
| const minorTicksCoords = map(minorTicks, function (minorTicksGroup) { | |
| return map(minorTicksGroup, function (minorTick) { | |
| return { | |
| coord: this.dataToCoord(minorTick), | |
| tickValue: minorTick | |
| }; | |
| }, this); | |
| }, this); | |
| return minorTicksCoords; | |
| } | |
| getViewLabels(): ReturnType<typeof createAxisLabels>['labels'] { | |
| return createAxisLabels(this).labels; | |
| } | |
| getLabelModel(): Model<AxisBaseOption['axisLabel']> { | |
| return this.model.getModel('axisLabel'); | |
| } | |
| /** | |
| * Notice here we only get the default tick model. For splitLine | |
| * or splitArea, we should pass the splitLineModel or splitAreaModel | |
| * manually when calling `getTicksCoords`. | |
| * In GL, this method may be overridden to: | |
| * `axisModel.getModel('axisTick', grid3DModel.getModel('axisTick'));` | |
| */ | |
| getTickModel(): Model { | |
| return this.model.getModel('axisTick'); | |
| } | |
| /** | |
| * Get width of band | |
| */ | |
| getBandWidth(): number { | |
| const axisExtent = this._extent; | |
| const dataExtent = this.scale.getExtent(); | |
| let len = dataExtent[1] - dataExtent[0] + (this.onBand ? 1 : 0); | |
| // Fix #2728, avoid NaN when only one data. | |
| len === 0 && (len = 1); | |
| const size = Math.abs(axisExtent[1] - axisExtent[0]); | |
| return Math.abs(size) / len; | |
| } | |
| /** | |
| * Get axis rotate, by degree. | |
| */ | |
| getRotate: () => number; | |
| /** | |
| * Only be called in category axis. | |
| * Can be overridden, consider other axes like in 3D. | |
| * @return Auto interval for cateogry axis tick and label | |
| */ | |
| calculateCategoryInterval(): ReturnType<typeof calculateCategoryInterval> { | |
| return calculateCategoryInterval(this); | |
| } | |
| } | |
| function fixExtentWithBands(extent: [number, number], nTick: number): void { | |
| const size = extent[1] - extent[0]; | |
| const len = nTick; | |
| const margin = size / len / 2; | |
| extent[0] += margin; | |
| extent[1] -= margin; | |
| } | |
| // If axis has labels [1, 2, 3, 4]. Bands on the axis are | |
| // |---1---|---2---|---3---|---4---|. | |
| // So the displayed ticks and splitLine/splitArea should between | |
| // each data item, otherwise cause misleading (e.g., split tow bars | |
| // of a single data item when there are two bar series). | |
| // Also consider if tickCategoryInterval > 0 and onBand, ticks and | |
| // splitLine/spliteArea should layout appropriately corresponding | |
| // to displayed labels. (So we should not use `getBandWidth` in this | |
| // case). | |
| function fixOnBandTicksCoords( | |
| axis: Axis, ticksCoords: TickCoord[], alignWithLabel: boolean, clamp: boolean | |
| ) { | |
| const ticksLen = ticksCoords.length; | |
| if (!axis.onBand || alignWithLabel || !ticksLen) { | |
| return; | |
| } | |
| const axisExtent = axis.getExtent(); | |
| let last; | |
| let diffSize; | |
| if (ticksLen === 1) { | |
| ticksCoords[0].coord = axisExtent[0]; | |
| last = ticksCoords[1] = {coord: axisExtent[1]}; | |
| } | |
| else { | |
| const crossLen = ticksCoords[ticksLen - 1].tickValue - ticksCoords[0].tickValue; | |
| const shift = (ticksCoords[ticksLen - 1].coord - ticksCoords[0].coord) / crossLen; | |
| each(ticksCoords, function (ticksItem) { | |
| ticksItem.coord -= shift / 2; | |
| }); | |
| const dataExtent = axis.scale.getExtent(); | |
| diffSize = 1 + dataExtent[1] - ticksCoords[ticksLen - 1].tickValue; | |
| last = {coord: ticksCoords[ticksLen - 1].coord + shift * diffSize}; | |
| ticksCoords.push(last); | |
| } | |
| const inverse = axisExtent[0] > axisExtent[1]; | |
| // Handling clamp. | |
| if (littleThan(ticksCoords[0].coord, axisExtent[0])) { | |
| clamp ? (ticksCoords[0].coord = axisExtent[0]) : ticksCoords.shift(); | |
| } | |
| if (clamp && littleThan(axisExtent[0], ticksCoords[0].coord)) { | |
| ticksCoords.unshift({coord: axisExtent[0]}); | |
| } | |
| if (littleThan(axisExtent[1], last.coord)) { | |
| clamp ? (last.coord = axisExtent[1]) : ticksCoords.pop(); | |
| } | |
| if (clamp && littleThan(last.coord, axisExtent[1])) { | |
| ticksCoords.push({coord: axisExtent[1]}); | |
| } | |
| function littleThan(a: number, b: number): boolean { | |
| // Avoid rounding error cause calculated tick coord different with extent. | |
| // It may cause an extra unnecessary tick added. | |
| a = round(a); | |
| b = round(b); | |
| return inverse ? a > b : a < b; | |
| } | |
| } | |
| export default Axis; | |