/* * 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['labels'] { return createAxisLabels(this).labels; } getLabelModel(): Model { 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 { 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;