| /* | |
| * 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. | |
| */ | |
| /** | |
| * Grid is a region which contains at most 4 cartesian systems | |
| * | |
| * TODO Default cartesian | |
| */ | |
| import {isObject, each, indexOf, retrieve3, keys} from 'zrender/src/core/util'; | |
| import {getLayoutRect, LayoutRect} from '../../util/layout'; | |
| import { | |
| createScaleByModel, | |
| ifAxisCrossZero, | |
| niceScaleExtent, | |
| estimateLabelUnionRect, | |
| getDataDimensionsOnAxis | |
| } from '../../coord/axisHelper'; | |
| import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D'; | |
| import Axis2D from './Axis2D'; | |
| import {ParsedModelFinder, ParsedModelFinderKnown, SINGLE_REFERRING} from '../../util/model'; | |
| // Depends on GridModel, AxisModel, which performs preprocess. | |
| import GridModel from './GridModel'; | |
| import CartesianAxisModel from './AxisModel'; | |
| import GlobalModel from '../../model/Global'; | |
| import ExtensionAPI from '../../core/ExtensionAPI'; | |
| import { Dictionary } from 'zrender/src/core/types'; | |
| import {CoordinateSystemMaster} from '../CoordinateSystem'; | |
| import { ScaleDataValue } from '../../util/types'; | |
| import SeriesData from '../../data/SeriesData'; | |
| import OrdinalScale from '../../scale/Ordinal'; | |
| import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper'; | |
| import { CategoryAxisBaseOption, NumericAxisBaseOptionCommon } from '../axisCommonTypes'; | |
| import { AxisBaseModel } from '../AxisBaseModel'; | |
| import { isIntervalOrLogScale } from '../../scale/helper'; | |
| import { alignScaleTicks } from '../axisAlignTicks'; | |
| import IntervalScale from '../../scale/Interval'; | |
| import LogScale from '../../scale/Log'; | |
| type Cartesian2DDimensionName = 'x' | 'y'; | |
| type FinderAxisIndex = {xAxisIndex?: number, yAxisIndex?: number}; | |
| type AxesMap = { | |
| x: Axis2D[], | |
| y: Axis2D[] | |
| }; | |
| class Grid implements CoordinateSystemMaster { | |
| // FIXME:TS where used (different from registered type 'cartesian2d')? | |
| readonly type: string = 'grid'; | |
| private _coordsMap: Dictionary<Cartesian2D> = {}; | |
| private _coordsList: Cartesian2D[] = []; | |
| private _axesMap: AxesMap = {} as AxesMap; | |
| private _axesList: Axis2D[] = []; | |
| private _rect: LayoutRect; | |
| readonly model: GridModel; | |
| readonly axisPointerEnabled = true; | |
| // Injected: | |
| name: string; | |
| // For deciding which dimensions to use when creating list data | |
| static dimensions = cartesian2DDimensions; | |
| readonly dimensions = cartesian2DDimensions; | |
| constructor(gridModel: GridModel, ecModel: GlobalModel, api: ExtensionAPI) { | |
| this._initCartesian(gridModel, ecModel, api); | |
| this.model = gridModel; | |
| } | |
| getRect(): LayoutRect { | |
| return this._rect; | |
| } | |
| update(ecModel: GlobalModel, api: ExtensionAPI): void { | |
| const axesMap = this._axesMap; | |
| this._updateScale(ecModel, this.model); | |
| function updateAxisTicks(axes: Record<number, Axis2D>) { | |
| let alignTo: Axis2D; | |
| // Axis is added in order of axisIndex. | |
| const axesIndices = keys(axes); | |
| const len = axesIndices.length; | |
| if (!len) { | |
| return; | |
| } | |
| const axisNeedsAlign: Axis2D[] = []; | |
| // Process once and calculate the ticks for those don't use alignTicks. | |
| for (let i = len - 1; i >= 0; i--) { | |
| const idx = +axesIndices[i]; // Convert to number. | |
| const axis = axes[idx]; | |
| const model = axis.model as AxisBaseModel<NumericAxisBaseOptionCommon>; | |
| const scale = axis.scale; | |
| if (// Only value and log axis without interval support alignTicks. | |
| isIntervalOrLogScale(scale) | |
| && model.get('alignTicks') | |
| && model.get('interval') == null | |
| ) { | |
| axisNeedsAlign.push(axis); | |
| } | |
| else { | |
| niceScaleExtent(scale, model); | |
| if (isIntervalOrLogScale(scale)) { // Can only align to interval or log axis. | |
| alignTo = axis; | |
| } | |
| } | |
| }; | |
| // All axes has set alignTicks. Pick the first one. | |
| // PENDING. Should we find the axis that both set interval, min, max and align to this one? | |
| if (axisNeedsAlign.length) { | |
| if (!alignTo) { | |
| alignTo = axisNeedsAlign.pop(); | |
| niceScaleExtent(alignTo.scale, alignTo.model); | |
| } | |
| each(axisNeedsAlign, axis => { | |
| alignScaleTicks( | |
| axis.scale as IntervalScale | LogScale, | |
| axis.model, | |
| alignTo.scale as IntervalScale | LogScale | |
| ); | |
| }); | |
| } | |
| } | |
| updateAxisTicks(axesMap.x); | |
| updateAxisTicks(axesMap.y); | |
| // Key: axisDim_axisIndex, value: boolean, whether onZero target. | |
| const onZeroRecords = {} as Dictionary<boolean>; | |
| each(axesMap.x, function (xAxis) { | |
| fixAxisOnZero(axesMap, 'y', xAxis, onZeroRecords); | |
| }); | |
| each(axesMap.y, function (yAxis) { | |
| fixAxisOnZero(axesMap, 'x', yAxis, onZeroRecords); | |
| }); | |
| // Resize again if containLabel is enabled | |
| // FIXME It may cause getting wrong grid size in data processing stage | |
| this.resize(this.model, api); | |
| } | |
| /** | |
| * Resize the grid | |
| */ | |
| resize(gridModel: GridModel, api: ExtensionAPI, ignoreContainLabel?: boolean): void { | |
| const boxLayoutParams = gridModel.getBoxLayoutParams(); | |
| const isContainLabel = !ignoreContainLabel && gridModel.get('containLabel'); | |
| const gridRect = getLayoutRect( | |
| boxLayoutParams, { | |
| width: api.getWidth(), | |
| height: api.getHeight() | |
| }); | |
| this._rect = gridRect; | |
| const axesList = this._axesList; | |
| adjustAxes(); | |
| // Minus label size | |
| if (isContainLabel) { | |
| each(axesList, function (axis) { | |
| if (!axis.model.get(['axisLabel', 'inside'])) { | |
| const labelUnionRect = estimateLabelUnionRect(axis); | |
| if (labelUnionRect) { | |
| const dim: 'height' | 'width' = axis.isHorizontal() ? 'height' : 'width'; | |
| const margin = axis.model.get(['axisLabel', 'margin']); | |
| gridRect[dim] -= labelUnionRect[dim] + margin; | |
| if (axis.position === 'top') { | |
| gridRect.y += labelUnionRect.height + margin; | |
| } | |
| else if (axis.position === 'left') { | |
| gridRect.x += labelUnionRect.width + margin; | |
| } | |
| } | |
| } | |
| }); | |
| adjustAxes(); | |
| } | |
| each(this._coordsList, function (coord) { | |
| // Calculate affine matrix to accelerate the data to point transform. | |
| // If all the axes scales are time or value. | |
| coord.calcAffineTransform(); | |
| }); | |
| function adjustAxes() { | |
| each(axesList, function (axis) { | |
| const isHorizontal = axis.isHorizontal(); | |
| const extent = isHorizontal ? [0, gridRect.width] : [0, gridRect.height]; | |
| const idx = axis.inverse ? 1 : 0; | |
| axis.setExtent(extent[idx], extent[1 - idx]); | |
| updateAxisTransform(axis, isHorizontal ? gridRect.x : gridRect.y); | |
| }); | |
| } | |
| } | |
| getAxis(dim: Cartesian2DDimensionName, axisIndex?: number): Axis2D { | |
| const axesMapOnDim = this._axesMap[dim]; | |
| if (axesMapOnDim != null) { | |
| return axesMapOnDim[axisIndex || 0]; | |
| } | |
| } | |
| getAxes(): Axis2D[] { | |
| return this._axesList.slice(); | |
| } | |
| /** | |
| * Usage: | |
| * grid.getCartesian(xAxisIndex, yAxisIndex); | |
| * grid.getCartesian(xAxisIndex); | |
| * grid.getCartesian(null, yAxisIndex); | |
| * grid.getCartesian({xAxisIndex: ..., yAxisIndex: ...}); | |
| * | |
| * When only xAxisIndex or yAxisIndex given, find its first cartesian. | |
| */ | |
| getCartesian(finder: FinderAxisIndex): Cartesian2D; | |
| getCartesian(xAxisIndex?: number, yAxisIndex?: number): Cartesian2D; | |
| getCartesian(xAxisIndex?: number | FinderAxisIndex, yAxisIndex?: number) { | |
| if (xAxisIndex != null && yAxisIndex != null) { | |
| const key = 'x' + xAxisIndex + 'y' + yAxisIndex; | |
| return this._coordsMap[key]; | |
| } | |
| if (isObject(xAxisIndex)) { | |
| yAxisIndex = (xAxisIndex as FinderAxisIndex).yAxisIndex; | |
| xAxisIndex = (xAxisIndex as FinderAxisIndex).xAxisIndex; | |
| } | |
| for (let i = 0, coordList = this._coordsList; i < coordList.length; i++) { | |
| if (coordList[i].getAxis('x').index === xAxisIndex | |
| || coordList[i].getAxis('y').index === yAxisIndex | |
| ) { | |
| return coordList[i]; | |
| } | |
| } | |
| } | |
| getCartesians(): Cartesian2D[] { | |
| return this._coordsList.slice(); | |
| } | |
| /** | |
| * @implements | |
| */ | |
| convertToPixel( | |
| ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue | ScaleDataValue[] | |
| ): number | number[] { | |
| const target = this._findConvertTarget(finder); | |
| return target.cartesian | |
| ? target.cartesian.dataToPoint(value as ScaleDataValue[]) | |
| : target.axis | |
| ? target.axis.toGlobalCoord(target.axis.dataToCoord(value as ScaleDataValue)) | |
| : null; | |
| } | |
| /** | |
| * @implements | |
| */ | |
| convertFromPixel( | |
| ecModel: GlobalModel, finder: ParsedModelFinder, value: number | number[] | |
| ): number | number[] { | |
| const target = this._findConvertTarget(finder); | |
| return target.cartesian | |
| ? target.cartesian.pointToData(value as number[]) | |
| : target.axis | |
| ? target.axis.coordToData(target.axis.toLocalCoord(value as number)) | |
| : null; | |
| } | |
| private _findConvertTarget(finder: ParsedModelFinderKnown): { | |
| cartesian: Cartesian2D, | |
| axis: Axis2D | |
| } { | |
| const seriesModel = finder.seriesModel; | |
| const xAxisModel = finder.xAxisModel | |
| || (seriesModel && seriesModel.getReferringComponents('xAxis', SINGLE_REFERRING).models[0]); | |
| const yAxisModel = finder.yAxisModel | |
| || (seriesModel && seriesModel.getReferringComponents('yAxis', SINGLE_REFERRING).models[0]); | |
| const gridModel = finder.gridModel; | |
| const coordsList = this._coordsList; | |
| let cartesian: Cartesian2D; | |
| let axis; | |
| if (seriesModel) { | |
| cartesian = seriesModel.coordinateSystem as Cartesian2D; | |
| indexOf(coordsList, cartesian) < 0 && (cartesian = null); | |
| } | |
| else if (xAxisModel && yAxisModel) { | |
| cartesian = this.getCartesian(xAxisModel.componentIndex, yAxisModel.componentIndex); | |
| } | |
| else if (xAxisModel) { | |
| axis = this.getAxis('x', xAxisModel.componentIndex); | |
| } | |
| else if (yAxisModel) { | |
| axis = this.getAxis('y', yAxisModel.componentIndex); | |
| } | |
| // Lowest priority. | |
| else if (gridModel) { | |
| const grid = gridModel.coordinateSystem; | |
| if (grid === this) { | |
| cartesian = this._coordsList[0]; | |
| } | |
| } | |
| return {cartesian: cartesian, axis: axis}; | |
| } | |
| /** | |
| * @implements | |
| */ | |
| containPoint(point: number[]): boolean { | |
| const coord = this._coordsList[0]; | |
| if (coord) { | |
| return coord.containPoint(point); | |
| } | |
| } | |
| /** | |
| * Initialize cartesian coordinate systems | |
| */ | |
| private _initCartesian( | |
| gridModel: GridModel, ecModel: GlobalModel, api: ExtensionAPI | |
| ): void { | |
| const grid = this; | |
| const axisPositionUsed = { | |
| left: false, | |
| right: false, | |
| top: false, | |
| bottom: false | |
| }; | |
| const axesMap = { | |
| x: {}, | |
| y: {} | |
| } as AxesMap; | |
| const axesCount = { | |
| x: 0, | |
| y: 0 | |
| }; | |
| // Create axis | |
| ecModel.eachComponent('xAxis', createAxisCreator('x'), this); | |
| ecModel.eachComponent('yAxis', createAxisCreator('y'), this); | |
| if (!axesCount.x || !axesCount.y) { | |
| // Roll back when there no either x or y axis | |
| this._axesMap = {} as AxesMap; | |
| this._axesList = []; | |
| return; | |
| } | |
| this._axesMap = axesMap; | |
| // Create cartesian2d | |
| each(axesMap.x, (xAxis, xAxisIndex) => { | |
| each(axesMap.y, (yAxis, yAxisIndex) => { | |
| const key = 'x' + xAxisIndex + 'y' + yAxisIndex; | |
| const cartesian = new Cartesian2D(key); | |
| cartesian.master = this; | |
| cartesian.model = gridModel; | |
| this._coordsMap[key] = cartesian; | |
| this._coordsList.push(cartesian); | |
| cartesian.addAxis(xAxis); | |
| cartesian.addAxis(yAxis); | |
| }); | |
| }); | |
| function createAxisCreator(dimName: Cartesian2DDimensionName) { | |
| return function (axisModel: CartesianAxisModel, idx: number): void { | |
| if (!isAxisUsedInTheGrid(axisModel, gridModel)) { | |
| return; | |
| } | |
| let axisPosition = axisModel.get('position'); | |
| if (dimName === 'x') { | |
| // Fix position | |
| if (axisPosition !== 'top' && axisPosition !== 'bottom') { | |
| // Default bottom of X | |
| axisPosition = axisPositionUsed.bottom ? 'top' : 'bottom'; | |
| } | |
| } | |
| else { | |
| // Fix position | |
| if (axisPosition !== 'left' && axisPosition !== 'right') { | |
| // Default left of Y | |
| axisPosition = axisPositionUsed.left ? 'right' : 'left'; | |
| } | |
| } | |
| axisPositionUsed[axisPosition] = true; | |
| const axis = new Axis2D( | |
| dimName, | |
| createScaleByModel(axisModel), | |
| [0, 0], | |
| axisModel.get('type'), | |
| axisPosition | |
| ); | |
| const isCategory = axis.type === 'category'; | |
| axis.onBand = isCategory && (axisModel as AxisBaseModel<CategoryAxisBaseOption>).get('boundaryGap'); | |
| axis.inverse = axisModel.get('inverse'); | |
| // Inject axis into axisModel | |
| axisModel.axis = axis; | |
| // Inject axisModel into axis | |
| axis.model = axisModel; | |
| // Inject grid info axis | |
| axis.grid = grid; | |
| // Index of axis, can be used as key | |
| axis.index = idx; | |
| grid._axesList.push(axis); | |
| axesMap[dimName][idx] = axis; | |
| axesCount[dimName]++; | |
| }; | |
| } | |
| } | |
| /** | |
| * Update cartesian properties from series. | |
| */ | |
| private _updateScale(ecModel: GlobalModel, gridModel: GridModel): void { | |
| // Reset scale | |
| each(this._axesList, function (axis) { | |
| axis.scale.setExtent(Infinity, -Infinity); | |
| if (axis.type === 'category') { | |
| const categorySortInfo = axis.model.get('categorySortInfo'); | |
| (axis.scale as OrdinalScale).setSortInfo(categorySortInfo); | |
| } | |
| }); | |
| ecModel.eachSeries(function (seriesModel) { | |
| if (isCartesian2DSeries(seriesModel)) { | |
| const axesModelMap = findAxisModels(seriesModel); | |
| const xAxisModel = axesModelMap.xAxisModel; | |
| const yAxisModel = axesModelMap.yAxisModel; | |
| if (!isAxisUsedInTheGrid(xAxisModel, gridModel) | |
| || !isAxisUsedInTheGrid(yAxisModel, gridModel) | |
| ) { | |
| return; | |
| } | |
| const cartesian = this.getCartesian( | |
| xAxisModel.componentIndex, yAxisModel.componentIndex | |
| ); | |
| const data = seriesModel.getData(); | |
| const xAxis = cartesian.getAxis('x'); | |
| const yAxis = cartesian.getAxis('y'); | |
| unionExtent(data, xAxis); | |
| unionExtent(data, yAxis); | |
| } | |
| }, this); | |
| function unionExtent(data: SeriesData, axis: Axis2D): void { | |
| each(getDataDimensionsOnAxis(data, axis.dim), function (dim) { | |
| axis.scale.unionExtentFromData(data, dim); | |
| }); | |
| } | |
| } | |
| /** | |
| * @param dim 'x' or 'y' or 'auto' or null/undefined | |
| */ | |
| getTooltipAxes(dim: Cartesian2DDimensionName | 'auto'): { | |
| baseAxes: Axis2D[], otherAxes: Axis2D[] | |
| } { | |
| const baseAxes = [] as Axis2D[]; | |
| const otherAxes = [] as Axis2D[]; | |
| each(this.getCartesians(), function (cartesian) { | |
| const baseAxis = (dim != null && dim !== 'auto') | |
| ? cartesian.getAxis(dim) : cartesian.getBaseAxis(); | |
| const otherAxis = cartesian.getOtherAxis(baseAxis); | |
| indexOf(baseAxes, baseAxis) < 0 && baseAxes.push(baseAxis); | |
| indexOf(otherAxes, otherAxis) < 0 && otherAxes.push(otherAxis); | |
| }); | |
| return {baseAxes: baseAxes, otherAxes: otherAxes}; | |
| } | |
| static create(ecModel: GlobalModel, api: ExtensionAPI): Grid[] { | |
| const grids = [] as Grid[]; | |
| ecModel.eachComponent('grid', function (gridModel: GridModel, idx) { | |
| const grid = new Grid(gridModel, ecModel, api); | |
| grid.name = 'grid_' + idx; | |
| // dataSampling requires axis extent, so resize | |
| // should be performed in create stage. | |
| grid.resize(gridModel, api, true); | |
| gridModel.coordinateSystem = grid; | |
| grids.push(grid); | |
| }); | |
| // Inject the coordinateSystems into seriesModel | |
| ecModel.eachSeries(function (seriesModel) { | |
| if (!isCartesian2DSeries(seriesModel)) { | |
| return; | |
| } | |
| const axesModelMap = findAxisModels(seriesModel); | |
| const xAxisModel = axesModelMap.xAxisModel; | |
| const yAxisModel = axesModelMap.yAxisModel; | |
| const gridModel = xAxisModel.getCoordSysModel(); | |
| if (__DEV__) { | |
| if (!gridModel) { | |
| throw new Error( | |
| 'Grid "' + retrieve3( | |
| xAxisModel.get('gridIndex'), | |
| xAxisModel.get('gridId'), | |
| 0 | |
| ) + '" not found' | |
| ); | |
| } | |
| if (xAxisModel.getCoordSysModel() !== yAxisModel.getCoordSysModel()) { | |
| throw new Error('xAxis and yAxis must use the same grid'); | |
| } | |
| } | |
| const grid = gridModel.coordinateSystem as Grid; | |
| seriesModel.coordinateSystem = grid.getCartesian( | |
| xAxisModel.componentIndex, yAxisModel.componentIndex | |
| ); | |
| }); | |
| return grids; | |
| } | |
| } | |
| /** | |
| * Check if the axis is used in the specified grid. | |
| */ | |
| function isAxisUsedInTheGrid(axisModel: CartesianAxisModel, gridModel: GridModel): boolean { | |
| return axisModel.getCoordSysModel() === gridModel; | |
| } | |
| function fixAxisOnZero( | |
| axesMap: AxesMap, | |
| otherAxisDim: Cartesian2DDimensionName, | |
| axis: Axis2D, | |
| // Key: see `getOnZeroRecordKey` | |
| onZeroRecords: Dictionary<boolean> | |
| ): void { | |
| axis.getAxesOnZeroOf = function () { | |
| // TODO: onZero of multiple axes. | |
| return otherAxisOnZeroOf ? [otherAxisOnZeroOf] : []; | |
| }; | |
| // onZero can not be enabled in these two situations: | |
| // 1. When any other axis is a category axis. | |
| // 2. When no axis is cross 0 point. | |
| const otherAxes = axesMap[otherAxisDim]; | |
| let otherAxisOnZeroOf: Axis2D; | |
| const axisModel = axis.model; | |
| const onZero = axisModel.get(['axisLine', 'onZero']); | |
| const onZeroAxisIndex = axisModel.get(['axisLine', 'onZeroAxisIndex']); | |
| if (!onZero) { | |
| return; | |
| } | |
| // If target axis is specified. | |
| if (onZeroAxisIndex != null) { | |
| if (canOnZeroToAxis(otherAxes[onZeroAxisIndex])) { | |
| otherAxisOnZeroOf = otherAxes[onZeroAxisIndex]; | |
| } | |
| } | |
| else { | |
| // Find the first available other axis. | |
| for (const idx in otherAxes) { | |
| if (otherAxes.hasOwnProperty(idx) | |
| && canOnZeroToAxis(otherAxes[idx]) | |
| // Consider that two Y axes on one value axis, | |
| // if both onZero, the two Y axes overlap. | |
| && !onZeroRecords[getOnZeroRecordKey(otherAxes[idx])] | |
| ) { | |
| otherAxisOnZeroOf = otherAxes[idx]; | |
| break; | |
| } | |
| } | |
| } | |
| if (otherAxisOnZeroOf) { | |
| onZeroRecords[getOnZeroRecordKey(otherAxisOnZeroOf)] = true; | |
| } | |
| function getOnZeroRecordKey(axis: Axis2D) { | |
| return axis.dim + '_' + axis.index; | |
| } | |
| } | |
| function canOnZeroToAxis(axis: Axis2D): boolean { | |
| return axis && axis.type !== 'category' && axis.type !== 'time' && ifAxisCrossZero(axis); | |
| } | |
| function updateAxisTransform(axis: Axis2D, coordBase: number) { | |
| const axisExtent = axis.getExtent(); | |
| const axisExtentSum = axisExtent[0] + axisExtent[1]; | |
| // Fast transform | |
| axis.toGlobalCoord = axis.dim === 'x' | |
| ? function (coord) { | |
| return coord + coordBase; | |
| } | |
| : function (coord) { | |
| return axisExtentSum - coord + coordBase; | |
| }; | |
| axis.toLocalCoord = axis.dim === 'x' | |
| ? function (coord) { | |
| return coord - coordBase; | |
| } | |
| : function (coord) { | |
| return axisExtentSum - coord + coordBase; | |
| }; | |
| } | |
| export default Grid; |