blob: a2f3e0ae892122f0ee51cadf7ce4dec28c777cd8 [file] [log] [blame]
/*
* 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, 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: AxisBaseOption['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 overrided 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 overrided, 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[0]};
}
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 unecessary tick added.
a = round(a);
b = round(b);
return inverse ? a > b : a < b;
}
}
export default Axis;