diff --git a/src/component/dataZoom/AxisProxy.ts b/src/component/dataZoom/AxisProxy.ts index 8e9a7aa23bf36d9c8b88d3234fa094649518622a..b322d91bc3a6bce89020bba56de4a12f1ab3d898 100644 --- a/src/component/dataZoom/AxisProxy.ts +++ b/src/component/dataZoom/AxisProxy.ts @@ -26,9 +26,10 @@ import SeriesModel from '../../model/Series'; import ExtensionAPI from '../../ExtensionAPI'; import { Dictionary } from '../../util/types'; // TODO Polar? -import CartesianAxisModel from '../../coord/cartesian/AxisModel'; import DataZoomModel from './DataZoomModel'; import { AxisBaseModel } from '../../coord/AxisBaseModel'; +import { unionAxisExtentFromData } from '../../coord/axisHelper'; +import { ensureScaleRawExtentInfo } from '../../coord/scaleRawExtentInfo'; const each = zrUtil.each; const asc = numberUtil.asc; @@ -127,32 +128,6 @@ class AxisProxy { return this.ecModel.getComponent(this._dimName + 'Axis', this._axisIndex) as AxisBaseModel; } - getOtherAxisModel() { - const axisDim = this._dimName; - const ecModel = this.ecModel; - const axisModel = this.getAxisModel(); - const isCartesian = axisDim === 'x' || axisDim === 'y'; - let otherAxisDim: 'x' | 'y' | 'radius' | 'angle'; - let coordSysIndexName: 'gridIndex' | 'polarIndex'; - if (isCartesian) { - coordSysIndexName = 'gridIndex'; - otherAxisDim = axisDim === 'x' ? 'y' : 'x'; - } - else { - coordSysIndexName = 'polarIndex'; - otherAxisDim = axisDim === 'angle' ? 'radius' : 'angle'; - } - let foundOtherAxisModel; - ecModel.eachComponent(otherAxisDim + 'Axis', function (otherAxisModel) { - if (((otherAxisModel as CartesianAxisModel).get(coordSysIndexName as 'gridIndex') || 0) - === ((axisModel as CartesianAxisModel).get(coordSysIndexName as 'gridIndex') || 0) - ) { - foundOtherAxisModel = otherAxisModel; - } - }); - return foundOtherAxisModel; - } - getMinMaxSpan() { return zrUtil.clone(this._minMaxSpan); } @@ -291,15 +266,6 @@ class AxisProxy { this._setAxisModel(); } - restore(dataZoomModel: DataZoomModel) { - if (dataZoomModel !== this._dataZoomModel) { - return; - } - - this._valueWindow = this._percentWindow = null; - this._setAxisModel(true); - } - filterData(dataZoomModel: DataZoomModel, api: ExtensionAPI) { if (dataZoomModel !== this._dataZoomModel) { return; @@ -421,7 +387,7 @@ class AxisProxy { }, this); } - private _setAxisModel(isRestore?: boolean) { + private _setAxisModel() { const axisModel = this.getAxisModel(); @@ -435,34 +401,29 @@ class AxisProxy { // [0, 500]: arbitrary value, guess axis extent. let precision = numberUtil.getPixelPrecision(valueWindow, [0, 500]); precision = Math.min(precision, 20); - // isRestore or isFull - const useOrigin = isRestore || (percentWindow[0] === 0 && percentWindow[1] === 100); - axisModel.setRange( - useOrigin ? null : +valueWindow[0].toFixed(precision), - useOrigin ? null : +valueWindow[1].toFixed(precision) - ); + // For value axis, if min/max/scale are not set, we just use the extent obtained + // by series data, which may be a little different from the extent calculated by + // `axisHelper.getScaleExtent`. But the different just affects the experience a + // little when zooming. So it will not be fixed until some users require it strongly. + const rawExtentInfo = axisModel.axis.scale.rawExtentInfo; + if (percentWindow[0] !== 0) { + rawExtentInfo.setDeterminedMinMax('min', +valueWindow[0].toFixed(precision)); + } + if (percentWindow[1] !== 100) { + rawExtentInfo.setDeterminedMinMax('max', +valueWindow[1].toFixed(precision)); + } + rawExtentInfo.freeze(); } } function calculateDataExtent(axisProxy: AxisProxy, axisDim: string, seriesModels: SeriesModel[]) { - let dataExtent = [Infinity, -Infinity]; + const dataExtent = [Infinity, -Infinity]; each(seriesModels, function (seriesModel) { - const seriesData = seriesModel.getData(); - if (seriesData) { - each(seriesData.mapDimensionsAll(axisDim), function (dim) { - const seriesExtent = seriesData.getApproximateExtent(dim); - seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]); - seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]); - }); - } + unionAxisExtentFromData(dataExtent, seriesModel.getData(), axisDim); }); - if (dataExtent[1] < dataExtent[0]) { - dataExtent = [NaN, NaN]; - } - // It is important to get "consistent" extent when more then one axes is // controlled by a `dataZoom`, otherwise those axes will not be synchronized // when zooming. But it is difficult to know what is "consistent", considering @@ -472,46 +433,10 @@ function calculateDataExtent(axisProxy: AxisProxy, axisDim: string, seriesModels // extent can be obtained from axis.data). // Nevertheless, user can set min/max/scale on axes to make extent of axes // consistent. - fixExtentByAxis(axisProxy, dataExtent); - - return dataExtent as [number, number]; -} - -function fixExtentByAxis(axisProxy: AxisProxy, dataExtent: number[]) { - const axisModel = axisProxy.getAxisModel() as CartesianAxisModel; - const min = axisModel.getMin(true); - - // For category axis, if min/max/scale are not set, extent is determined - // by axis.data by default. - const isCategoryAxis = axisModel.get('type') === 'category'; - const axisDataLen = isCategoryAxis && axisModel.getCategories().length; - - if (min != null && min !== 'dataMin' && typeof min !== 'function') { - dataExtent[0] = min; - } - else if (isCategoryAxis) { - dataExtent[0] = axisDataLen > 0 ? 0 : NaN; - } - - const max = axisModel.getMax(true); - if (max != null && max !== 'dataMax' && typeof max !== 'function') { - dataExtent[1] = max; - } - else if (isCategoryAxis) { - dataExtent[1] = axisDataLen > 0 ? axisDataLen - 1 : NaN; - } - - if (!axisModel.get('scale', true)) { - dataExtent[0] > 0 && (dataExtent[0] = 0); - dataExtent[1] < 0 && (dataExtent[1] = 0); - } - - // For value axis, if min/max/scale are not set, we just use the extent obtained - // by series data, which may be a little different from the extent calculated by - // `axisHelper.getScaleExtent`. But the different just affects the experience a - // little when zooming. So it will not be fixed until some users require it strongly. + const axisModel = axisProxy.getAxisModel(); + const rawExtentResult = ensureScaleRawExtentInfo(axisModel.axis.scale, axisModel, dataExtent).calculate(); - return dataExtent; + return [rawExtentResult.min, rawExtentResult.max] as [number, number]; } export default AxisProxy; diff --git a/src/component/dataZoom/DataZoomModel.ts b/src/component/dataZoom/DataZoomModel.ts index fad975c3461e214315d482e8f06389bf991a90ef..f711444dbfd44f1f3cf07a625d1702af75cdd2be 100644 --- a/src/component/dataZoom/DataZoomModel.ts +++ b/src/component/dataZoom/DataZoomModel.ts @@ -268,10 +268,10 @@ class DataZoomModel extends Compon this._resetTarget(); - this._giveAxisProxies(); + this._prepareAxisProxies(); } - private _giveAxisProxies() { + private _prepareAxisProxies() { const axisProxies = this._axisProxies; this.eachTargetAxis(function (dimNames, axisIndex, dataZoomModel, ecModel) { diff --git a/src/component/dataZoom/dataZoomProcessor.ts b/src/component/dataZoom/dataZoomProcessor.ts index 3a255f516e72773f8a75ffd6a46fcf182633f3e7..960d62c9569c3f7bded92789ea090b07f3deefbd 100644 --- a/src/component/dataZoom/dataZoomProcessor.ts +++ b/src/component/dataZoom/dataZoomProcessor.ts @@ -22,7 +22,7 @@ import {createHashMap, each} from 'zrender/src/core/util'; import SeriesModel from '../../model/Series'; import DataZoomModel from './DataZoomModel'; -echarts.registerProcessor({ +echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER, { // `dataZoomProcessor` will only be performed in needed series. Consider if // there is a line series and a pie series, it is better not to update the diff --git a/src/component/dataZoom/helper.ts b/src/component/dataZoom/helper.ts index 4860f2712100f763cff8bfdd45da19101f493664..ef364b11360e8991a77ae8b8313bde849d681d77 100644 --- a/src/component/dataZoom/helper.ts +++ b/src/component/dataZoom/helper.ts @@ -32,6 +32,7 @@ export interface DataZoomPayloadBatchItem { const AXIS_DIMS = ['x', 'y', 'z', 'radius', 'angle', 'single'] as const; // Supported coords. +// FIXME: polar has been broken (but rarely used). const COORDS = ['cartesian2d', 'polar', 'singleAxis'] as const; export function isCoordSupported(coordType: string) { diff --git a/src/component/gridSimple.ts b/src/component/gridSimple.ts index a46b0503242ca4eb0433554492851b7a76b0fc23..f1b6c9c63a51d9f7ba104764ed4fd32e2281f559 100644 --- a/src/component/gridSimple.ts +++ b/src/component/gridSimple.ts @@ -22,6 +22,7 @@ import * as zrUtil from 'zrender/src/core/util'; import * as graphic from '../util/graphic'; import './axis'; +import '../coord/cartesian/defaultAxisExtentFromData'; import ComponentView from '../view/Component'; import GlobalModel from '../model/Global'; import GridModel from '../coord/cartesian/GridModel'; diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index 4dac22504d657e53bd76464b0721707fe7612c6e..fb00c4b72bd20f951996207c443d10a6647db7b9 100644 --- a/src/coord/axisCommonTypes.ts +++ b/src/coord/axisCommonTypes.ts @@ -20,7 +20,7 @@ import { TextCommonOption, LineStyleOption, OrdinalRawValue, ZRColor, AreaStyleOption, ComponentOption, ColorString, - AnimationOptionMixin, Dictionary + AnimationOptionMixin, Dictionary, ScaleDataValue } from '../util/types'; @@ -71,21 +71,15 @@ export interface AxisBaseOption extends ComponentOption, boundaryGap?: boolean | [number | string, number | string]; // Min value of the axis. can be: - // + a number + // + ScaleDataValue // + 'dataMin': use the min value in data. // + null/undefined: auto decide min value (consider pretty look and boundaryGap). - min?: number | 'dataMin' | ((extent: {min: number, max: number}) => number); + min?: ScaleDataValue | 'dataMin' | ((extent: {min: number, max: number}) => ScaleDataValue); // Max value of the axis. can be: - // + a number + // + ScaleDataValue // + 'dataMax': use the max value in data. // + null/undefined: auto decide max value (consider pretty look and boundaryGap). - max?: number | 'dataMax' | ((extent: {min: number, max: number}) => number); - // Readonly prop, specifies start value of the range when using data zoom. - // Only for internal usage. - rangeStart?: number; - // Readonly prop, specifies end value of the range when using data zoom. - // Only for internal usage. - rangeEnd?: number; + max?: ScaleDataValue | 'dataMax' | ((extent: {min: number, max: number}) => ScaleDataValue); // Optional value can be: // + `false`: always include value 0. // + `true`: the extent do not consider value 0. diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 9eeac8fec23f778ea7911b7f12cfe03bc7d0e6cb..2393f7c6f530e44d52524d5635ecfb31485ab784 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -17,12 +17,10 @@ * under the License. */ -import {__DEV__} from '../config'; import * as zrUtil from 'zrender/src/core/util'; import OrdinalScale from '../scale/Ordinal'; import IntervalScale from '../scale/Interval'; import Scale from '../scale/Scale'; -import * as numberUtil from '../util/number'; import { prepareLayoutBarSeries, makeColumnLayout, @@ -37,119 +35,31 @@ import LogScale from '../scale/Log'; import Axis from './Axis'; import { AxisBaseOption } from './axisCommonTypes'; import type CartesianAxisModel from './cartesian/AxisModel'; +import List from '../data/List'; +import { getStackedDimension } from '../data/helper/dataStackHelper'; +import { Dictionary, ScaleDataValue } from '../util/types'; +import { ensureScaleRawExtentInfo } from './scaleRawExtentInfo'; + type BarWidthAndOffset = ReturnType; /** * Get axis scale extent before niced. * Item of returned array can only be number (including Infinity and NaN). + * + * Caution: + * Precondition of calling this method: + * The scale extent has been initailized using series data extent via + * `scale.setExtent` or `scale.unionExtentFromData`; */ export function getScaleExtent(scale: Scale, model: AxisBaseModel) { const scaleType = scale.type; + const rawExtentResult = ensureScaleRawExtentInfo(scale, model, scale.getExtent()).calculate(); - let min = model.getMin(); - let max = model.getMax(); - const originalExtent = scale.getExtent(); - - let axisDataLen; - let boundaryGapInner: number[]; - let span; - if (scaleType === 'ordinal') { - axisDataLen = model.getCategories().length; - } - else { - const boundaryGap = model.get('boundaryGap'); - const boundaryGapArr = zrUtil.isArray(boundaryGap) - ? boundaryGap : [boundaryGap || 0, boundaryGap || 0]; - - if (typeof boundaryGapArr[0] === 'boolean' || typeof boundaryGapArr[1] === 'boolean') { - if (__DEV__) { - console.warn('Boolean type for boundaryGap is only ' - + 'allowed for ordinal axis. Please use string in ' - + 'percentage instead, e.g., "20%". Currently, ' - + 'boundaryGap is set to be 0.'); - } - boundaryGapInner = [0, 0]; - } - else { - boundaryGapInner = [ - numberUtil.parsePercent(boundaryGapArr[0], 1), - numberUtil.parsePercent(boundaryGapArr[1], 1) - ]; - } - span = (originalExtent[1] - originalExtent[0]) - || Math.abs(originalExtent[0]); - } - - // Notice: When min/max is not set (that is, when there are null/undefined, - // which is the most common case), these cases should be ensured: - // (1) For 'ordinal', show all axis.data. - // (2) For others: - // + `boundaryGap` is applied (if min/max set, boundaryGap is - // disabled). - // + If `needCrossZero`, min/max should be zero, otherwise, min/max should - // be the result that originalExtent enlarged by boundaryGap. - // (3) If no data, it should be ensured that `scale.setBlank` is set. - - // FIXME - // (1) When min/max is 'dataMin' or 'dataMax', should boundaryGap be able to used? - // (2) When `needCrossZero` and all data is positive/negative, should it be ensured - // that the results processed by boundaryGap are positive/negative? - - if (min === 'dataMin') { - min = originalExtent[0]; - } - else if (typeof min === 'function') { - min = min({ - min: originalExtent[0], - max: originalExtent[1] - }); - } - - if (max === 'dataMax') { - max = originalExtent[1]; - } - else if (typeof max === 'function') { - max = max({ - min: originalExtent[0], - max: originalExtent[1] - }); - } + scale.setBlank(rawExtentResult.isBlank); - const fixMin = min != null; - const fixMax = max != null; - - if (min == null) { - min = scaleType === 'ordinal' - ? (axisDataLen ? 0 : NaN) - : originalExtent[0] - boundaryGapInner[0] * span; - } - if (max == null) { - max = scaleType === 'ordinal' - ? (axisDataLen ? axisDataLen - 1 : NaN) - : originalExtent[1] + boundaryGapInner[1] * span; - } - - (min == null || !isFinite(min)) && (min = NaN); - (max == null || !isFinite(max)) && (max = NaN); - - scale.setBlank( - zrUtil.eqNaN(min) - || zrUtil.eqNaN(max) - || ((scale instanceof OrdinalScale) && !scale.getOrdinalMeta().categories.length) - ); - - // Evaluate if axis needs cross zero - if (model.getNeedCrossZero()) { - // Axis is over zero and min is not set - if (min > 0 && max > 0 && !fixMin) { - min = 0; - } - // Axis is under zero and max is not set - if (min < 0 && max < 0 && !fixMax) { - max = 0; - } - } + let min = rawExtentResult.min; + let max = rawExtentResult.max; // If bars are placed on a base axis of type time or interval account for axis boundary overflow and current axis // is base axis @@ -185,8 +95,8 @@ export function getScaleExtent(scale: Scale, model: AxisBaseModel) { extent: [min, max], // "fix" means "fixed", the value should not be // changed in the subsequent steps. - fixMin: fixMin, - fixMax: fixMax + fixMin: rawExtentResult.minFixed, + fixMax: rawExtentResult.maxFixed }; } @@ -230,6 +140,9 @@ function adjustScaleForOverflow( return {min: min, max: max}; } +// Precondition of calling this method: +// The scale extent has been initailized using series data extent via +// `scale.setExtent` or `scale.unionExtentFromData`; export function niceScaleExtent(scale: Scale, model: AxisBaseModel) { const extentInfo = getScaleExtent(scale, model); const extent = extentInfo.extent; @@ -427,3 +340,36 @@ export function shouldShowAllLabels(axis: Axis) { && getOptionCategoryInterval(axis.getLabelModel()) === 0; } +export function getDataDimensionsOnAxis(data: List, axisDim: string) { + // Remove duplicated dat dimensions caused by `getStackedDimension`. + const dataDimMap = {} as Dictionary; + // Currently `mapDimensionsAll` will contian stack result dimension ('__\0ecstackresult'). + // PENDING: is it reasonable? Do we need to remove the original dim from "coord dim" since + // there has been stacked result dim? + zrUtil.each(data.mapDimensionsAll(axisDim), function (dataDim) { + // For example, the extent of the orginal dimension + // is [0.1, 0.5], the extent of the `stackResultDimension` + // is [7, 9], the final extent should NOT include [0.1, 0.5], + // because there is no graphic corresponding to [0.1, 0.5]. + // See the case in `test/area-stack.html` `main1`, where area line + // stack needs `yAxis` not start from 0. + dataDimMap[getStackedDimension(data, dataDim)] = true; + }); + return zrUtil.keys(dataDimMap); +} + +export function unionAxisExtentFromData(dataExtent: number[], data: List, axisDim: string): void { + if (data) { + zrUtil.each(getDataDimensionsOnAxis(data, axisDim), function (dim) { + const seriesExtent = data.getApproximateExtent(dim); + seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]); + seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]); + }); + } +} + +export function parseAxisModelMinMax(scale: Scale, minMax: ScaleDataValue): number { + return minMax == null ? null + : zrUtil.eqNaN(minMax) ? NaN + : scale.parse(minMax); +} diff --git a/src/coord/axisModelCommonMixin.ts b/src/coord/axisModelCommonMixin.ts index 1e45c5c0d83324d7bb5fb6c4997d4c5fc918cfb3..c1c458f548d48d7be4315075120c0cca90e60ff3 100644 --- a/src/coord/axisModelCommonMixin.ts +++ b/src/coord/axisModelCommonMixin.ts @@ -30,48 +30,9 @@ interface AxisModelCommonMixin extends Pick { - /** - * @return min value or 'dataMin' or null/undefined (means auto) or NaN - */ - getMin(origin?: boolean): AxisBaseOption['min'] | number { - const option = this.option; - let min = (!origin && option.rangeStart != null) - ? option.rangeStart : option.min; - - if (this.axis - && min != null - && min !== 'dataMin' - && typeof min !== 'function' - && !zrUtil.eqNaN(min) - ) { - min = this.axis.scale.parse(min); - } - return min; - } - - /** - * @return max value or 'dataMax' or null/undefined (means auto) or NaN - */ - getMax(origin?: boolean): AxisBaseOption['max'] | number { - const option = this.option; - let max = (!origin && option.rangeEnd != null) - ? option.rangeEnd : option.max; - - if (this.axis - && max != null - && max !== 'dataMax' - && typeof max !== 'function' - && !zrUtil.eqNaN(max) - ) { - max = this.axis.scale.parse(max); - } - return max; - } - getNeedCrossZero(): boolean { const option = this.option; - return (option.rangeStart != null || option.rangeEnd != null) - ? false : !option.scale; + return !option.scale; } /** @@ -82,22 +43,6 @@ class AxisModelCommonMixin { return; } - /** - * @param rangeStart Can only be finite number or null/undefined or NaN. - * @param rangeEnd Can only be finite number or null/undefined or NaN. - */ - setRange(rangeStart: number, rangeEnd: number): void { - this.option.rangeStart = rangeStart; - this.option.rangeEnd = rangeEnd; - } - - /** - * Reset range - */ - resetRange(): void { - // rangeStart and rangeEnd is readonly. - this.option.rangeStart = this.option.rangeEnd = null; - } } export {AxisModelCommonMixin}; diff --git a/src/coord/cartesian/AxisModel.ts b/src/coord/cartesian/AxisModel.ts index d8e4aee87eb8673e5a6b09adca4aaf54f8880504..3b6db87b8f41fe211bddd9d0bd201fec0a44f480 100644 --- a/src/coord/cartesian/AxisModel.ts +++ b/src/coord/cartesian/AxisModel.ts @@ -44,21 +44,6 @@ class CartesianAxisModel extends ComponentModel axis: Axis2D; - init(...args: any) { - super.init.apply(this, args); - this.resetRange(); - } - - mergeOption(...args: any) { - super.mergeOption.apply(this, args); - this.resetRange(); - } - - restoreData(...args: any) { - super.restoreData.apply(this, args); - this.resetRange(); - } - getCoordSysModel(): GridModel { return this.ecModel.queryComponents({ mainType: 'grid', diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts index 0b0f2dd3595db395bcc3375d376c5ef7b4cd872a..4a0b54bb1d96c24073b1a94ab178f22e355c4339 100644 --- a/src/coord/cartesian/Grid.ts +++ b/src/coord/cartesian/Grid.ts @@ -24,18 +24,18 @@ */ import {__DEV__} from '../../config'; -import {isObject, each, map, indexOf, retrieve, retrieve3} from 'zrender/src/core/util'; +import {isObject, each, indexOf, retrieve3} from 'zrender/src/core/util'; import {getLayoutRect, LayoutRect} from '../../util/layout'; import { createScaleByModel, ifAxisCrossZero, niceScaleExtent, - estimateLabelUnionRect + estimateLabelUnionRect, + getDataDimensionsOnAxis } from '../../coord/axisHelper'; import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D'; import Axis2D from './Axis2D'; import CoordinateSystemManager from '../../CoordinateSystem'; -import {getStackedDimension} from '../../data/helper/dataStackHelper'; import {ParsedModelFinder} from '../../util/model'; // Depends on GridModel, AxisModel, which performs preprocess. @@ -47,7 +47,7 @@ import { Dictionary } from 'zrender/src/core/types'; import {CoordinateSystemMaster} from '../CoordinateSystem'; import { ScaleDataValue } from '../../util/types'; import List from '../../data/List'; -import SeriesModel from '../../model/Series'; +import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper'; type Cartesian2DDimensionName = 'x' | 'y'; @@ -412,10 +412,10 @@ class Grid implements CoordinateSystemMaster { axis.scale.setExtent(Infinity, -Infinity); }); ecModel.eachSeries(function (seriesModel) { - if (isCartesian2D(seriesModel)) { - const axesModels = findAxesModels(seriesModel); - const xAxisModel = axesModels[0]; - const yAxisModel = axesModels[1]; + if (isCartesian2DSeries(seriesModel)) { + const axesModelMap = findAxisModels(seriesModel); + const xAxisModel = axesModelMap.xAxisModel; + const yAxisModel = axesModelMap.yAxisModel; if (!isAxisUsedInTheGrid(xAxisModel, gridModel) || !isAxisUsedInTheGrid(yAxisModel, gridModel) @@ -438,13 +438,8 @@ class Grid implements CoordinateSystemMaster { }, this); function unionExtent(data: List, axis: Axis2D): void { - each(data.mapDimensionsAll(axis.dim), function (dim) { - axis.scale.unionExtentFromData( - // For example, the extent of the orginal dimension - // is [0.1, 0.5], the extent of the `stackResultDimension` - // is [7, 9], the final extent should not include [0.1, 0.5]. - data, getStackedDimension(data, dim) - ); + each(getDataDimensionsOnAxis(data, axis.dim), function (dim) { + axis.scale.unionExtentFromData(data, dim); }); } } @@ -486,13 +481,13 @@ class Grid implements CoordinateSystemMaster { // Inject the coordinateSystems into seriesModel ecModel.eachSeries(function (seriesModel) { - if (!isCartesian2D(seriesModel)) { + if (!isCartesian2DSeries(seriesModel)) { return; } - const axesModels = findAxesModels(seriesModel); - const xAxisModel = axesModels[0]; - const yAxisModel = axesModels[1]; + const axesModelMap = findAxisModels(seriesModel); + const xAxisModel = axesModelMap.xAxisModel; + const yAxisModel = axesModelMap.yAxisModel; const gridModel = xAxisModel.getCoordSysModel(); @@ -612,28 +607,6 @@ function updateAxisTransform(axis: Axis2D, coordBase: number) { }; } -const axesTypes = ['xAxis', 'yAxis']; -function findAxesModels(seriesModel: SeriesModel): CartesianAxisModel[] { - return map(axesTypes, function (axisType) { - const axisModel = seriesModel.getReferringComponents(axisType)[0] as CartesianAxisModel; - - if (__DEV__) { - if (!axisModel) { - throw new Error(axisType + ' "' + retrieve( - seriesModel.get(axisType + 'Index' as any), - seriesModel.get(axisType + 'Id' as any), - 0 - ) + '" not found'); - } - } - return axisModel; - }); -} - -function isCartesian2D(seriesModel: SeriesModel): boolean { - return seriesModel.get('coordinateSystem') === 'cartesian2d'; -} - CoordinateSystemManager.register('cartesian2d', Grid); export default Grid; diff --git a/src/coord/cartesian/cartesianAxisHelper.ts b/src/coord/cartesian/cartesianAxisHelper.ts index db82726d36cbb487a43809ece5ada8a5ae1edac3..f732f36ba04c2de45c8cf1c3387994cba77129f1 100644 --- a/src/coord/cartesian/cartesianAxisHelper.ts +++ b/src/coord/cartesian/cartesianAxisHelper.ts @@ -21,6 +21,8 @@ import * as zrUtil from 'zrender/src/core/util'; import GridModel from './GridModel'; import CartesianAxisModel from './AxisModel'; +import SeriesModel from '../../model/Series'; +import { __DEV__ } from '../../config'; interface CartesianAxisLayout { position: [number, number]; @@ -95,3 +97,36 @@ export function layout( return layout; } + +export function isCartesian2DSeries(seriesModel: SeriesModel): boolean { + return seriesModel.get('coordinateSystem') === 'cartesian2d'; +} + +export function findAxisModels(seriesModel: SeriesModel): { + xAxisModel: CartesianAxisModel; + yAxisModel: CartesianAxisModel; +} { + const axisModelMap = { + xAxisModel: null, + yAxisModel: null + } as ReturnType; + zrUtil.each(axisModelMap, function (v, key) { + const axisType = key.replace(/Model$/, ''); + const axisModel = seriesModel.getReferringComponents(axisType)[0] as CartesianAxisModel; + + if (__DEV__) { + if (!axisModel) { + throw new Error(axisType + ' "' + zrUtil.retrieve3( + seriesModel.get(axisType + 'Index' as any), + seriesModel.get(axisType + 'Id' as any), + 0 + ) + '" not found'); + } + } + + axisModelMap[key] = axisModel; + }); + + return axisModelMap; +} + diff --git a/src/coord/cartesian/defaultAxisExtentFromData.ts b/src/coord/cartesian/defaultAxisExtentFromData.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b0e58eecffc00e6fac5f86c0137ac6f34c3477a --- /dev/null +++ b/src/coord/cartesian/defaultAxisExtentFromData.ts @@ -0,0 +1,262 @@ +/* +* 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 * as echarts from '../../echarts'; +import { createHashMap, each, HashMap, hasOwn, keys, map } from 'zrender/src/core/util'; +import SeriesModel from '../../model/Series'; +import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper'; +import { getDataDimensionsOnAxis, unionAxisExtentFromData } from '../axisHelper'; +import { AxisBaseModel } from '../AxisBaseModel'; +import Axis from '../Axis'; +import GlobalModel from '../../model/Global'; +import { Dictionary } from '../../util/types'; +import { ScaleRawExtentInfo, ScaleRawExtentResult, ensureScaleRawExtentInfo } from '../scaleRawExtentInfo'; + + +type AxisRecord = { + condExtent: number[]; + rawExtentInfo?: ScaleRawExtentInfo; + rawExtentResult?: ScaleRawExtentResult + tarExtent?: number[]; +}; + +type SeriesRecord = { + seriesModel: SeriesModel; + xAxisModel: AxisBaseModel; + yAxisModel: AxisBaseModel; +}; + +// A tricky: the priority is just after dataZoom processor. +// If dataZoom has fixed the min/max, this processor do not need to work. +echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER + 10, { + + getTargetSeries: function (ecModel) { + const seriesModelMap = createHashMap(); + ecModel.eachSeries(function (seriesModel: SeriesModel) { + isCartesian2DSeries(seriesModel) && seriesModelMap.set(seriesModel.uid, seriesModel); + }); + return seriesModelMap; + }, + + overallReset: function (ecModel, api) { + const seriesRecords = [] as SeriesRecord[]; + const axisRecordMap = createHashMap(); + + prepareDataExtentOnAxis(ecModel, axisRecordMap, seriesRecords); + calculateFilteredExtent(axisRecordMap, seriesRecords); + shrinkAxisExtent(axisRecordMap); + } +}); + +function prepareDataExtentOnAxis( + ecModel: GlobalModel, + axisRecordMap: HashMap, + seriesRecords: SeriesRecord[] +): void { + ecModel.eachSeries(function (seriesModel: SeriesModel) { + if (!isCartesian2DSeries(seriesModel)) { + return; + } + + const axesModelMap = findAxisModels(seriesModel); + const xAxisModel = axesModelMap.xAxisModel; + const yAxisModel = axesModelMap.yAxisModel; + const xAxis = xAxisModel.axis; + const yAxis = yAxisModel.axis; + const xRawExtentInfo = xAxis.scale.rawExtentInfo; + const yRawExtentInfo = yAxis.scale.rawExtentInfo; + const data = seriesModel.getData(); + + // If either axis controlled by other filter like "dataZoom", + // use the rule of dataZoom rather than adopting the rules here. + if ( + (xRawExtentInfo && xRawExtentInfo.frozen) + || (yRawExtentInfo && yRawExtentInfo.frozen) + ) { + return; + } + + seriesRecords.push({ + seriesModel: seriesModel, + xAxisModel: xAxisModel, + yAxisModel: yAxisModel + }); + + // FIXME: this logic needs to be consistent with + // `coord/cartesian/Grid.ts#_updateScale`. + // It's not good to implement one logic in multiple places. + unionAxisExtentFromData(prepareAxisRecord(axisRecordMap, xAxisModel).condExtent, data, xAxis.dim); + unionAxisExtentFromData(prepareAxisRecord(axisRecordMap, yAxisModel).condExtent, data, yAxis.dim); + }); +} + +function calculateFilteredExtent( + axisRecordMap: HashMap, + seriesRecords: SeriesRecord[] +) { + each(seriesRecords, function (seriesRecord) { + const xAxisModel = seriesRecord.xAxisModel; + const yAxisModel = seriesRecord.yAxisModel; + const xAxis = xAxisModel.axis; + const yAxis = yAxisModel.axis; + const xAxisRecord = prepareAxisRecord(axisRecordMap, xAxisModel); + const yAxisRecord = prepareAxisRecord(axisRecordMap, yAxisModel); + xAxisRecord.rawExtentInfo = ensureScaleRawExtentInfo( + xAxis.scale, xAxisModel, xAxisRecord.condExtent + ); + yAxisRecord.rawExtentInfo = ensureScaleRawExtentInfo( + yAxis.scale, yAxisModel, yAxisRecord.condExtent + ); + xAxisRecord.rawExtentResult = xAxisRecord.rawExtentInfo.calculate(); + yAxisRecord.rawExtentResult = yAxisRecord.rawExtentInfo.calculate(); + + // If the "xAxis" is set `min`/`max`, some data items might be out of the cartesian. + // then the "yAxis" may needs to calculate extent only based on the data items inside + // the cartesian (similar to what "dataZoom" did). + // A typical case is bar-racing, where bars ara sort dynamically and may only need to + // displayed part of the whole bars. + + const data = seriesRecord.seriesModel.getData(); + // For duplication removal. + const condDimMap: Dictionary = {}; + const tarDimMap: Dictionary = {}; + let condAxisExtent: number[]; + let tarAxisRecord: AxisRecord; + + function addCondition(axis: Axis, axisRecord: AxisRecord) { + // But for simplicity and safty and performance, we only adopt this + // feature on category axis at present. + const condExtent = axisRecord.condExtent; + const rawExtentResult = axisRecord.rawExtentResult; + if (axis.type === 'category' + && (condExtent[0] < rawExtentResult.min || rawExtentResult.max < condExtent[1]) + ) { + each(getDataDimensionsOnAxis(data, axis.dim), function (dataDim) { + if (!hasOwn(condDimMap, dataDim)) { + condDimMap[dataDim] = true; + condAxisExtent = [rawExtentResult.min, rawExtentResult.max]; + } + }); + } + } + function addTarget(axis: Axis, axisRecord: AxisRecord) { + const rawExtentResult = axisRecord.rawExtentResult; + if (axis.type !== 'category' + && (!rawExtentResult.minFixed || !rawExtentResult.maxFixed) + ) { + each(getDataDimensionsOnAxis(data, axis.dim), function (dataDim) { + if (!hasOwn(condDimMap, dataDim) && !hasOwn(tarDimMap, dataDim)) { + tarDimMap[dataDim] = true; + tarAxisRecord = axisRecord; + } + }); + } + } + + addCondition(xAxis, xAxisRecord); + addCondition(yAxis, yAxisRecord); + addTarget(xAxis, xAxisRecord); + addTarget(yAxis, yAxisRecord); + + const condDims = keys(condDimMap); + const tarDims = keys(tarDimMap); + const tarDimExtents = map(tarDims, function () { + return initExtent(); + }); + + const condDimsLen = condDims.length; + const tarDimsLen = tarDims.length; + + if (!condDimsLen || !tarDimsLen) { + return; + } + + const singleCondDim = condDimsLen === 1 ? condDims[0] : null; + const singleTarDim = tarDimsLen === 1 ? tarDims[0] : null; + const dataLen = data.count(); + + // Time consuming, because this is a "block task". + // Simple optimization for the vast majority of cases. + if (singleCondDim && singleTarDim) { + for (let dataIdx = 0; dataIdx < dataLen; dataIdx++) { + const condVal = data.get(singleCondDim, dataIdx) as number; + if (condVal >= condAxisExtent[0] && condVal <= condAxisExtent[1]) { + unionExtent(tarDimExtents[0], data.get(singleTarDim, dataIdx) as number); + } + } + } + else { + for (let dataIdx = 0; dataIdx < dataLen; dataIdx++) { + for (let j = 0; j < condDimsLen; j++) { + const condVal = data.get(condDims[j], dataIdx) as number; + if (condVal >= condAxisExtent[0] && condVal <= condAxisExtent[1]) { + for (let k = 0; k < tarDimsLen; k++) { + unionExtent(tarDimExtents[k], data.get(tarDims[k], dataIdx) as number); + } + // Any one dim is in range means satisfied. + break; + } + } + } + } + + each(tarDimExtents, function (tarDimExtent, i) { + const dim = tarDims[i]; + // FIXME: if there has been approximateExtent set? + data.setApproximateExtent(tarDimExtent as [number, number], dim); + const tarAxisExtent = tarAxisRecord.tarExtent = tarAxisRecord.tarExtent || initExtent(); + unionExtent(tarAxisExtent, tarDimExtent[0]); + unionExtent(tarAxisExtent, tarDimExtent[1]); + }); + }); +} + +function shrinkAxisExtent(axisRecordMap: HashMap) { + axisRecordMap.each(function (axisRecord) { + const tarAxisExtent = axisRecord.tarExtent; + if (tarAxisExtent) { + const rawExtentResult = axisRecord.rawExtentResult; + const rawExtentInfo = axisRecord.rawExtentInfo; + // Shink the original extent. + if (!rawExtentResult.minFixed && tarAxisExtent[0] > rawExtentResult.min) { + rawExtentInfo.modifyDataMinMax('min', tarAxisExtent[0]); + } + if (!rawExtentResult.maxFixed && tarAxisExtent[1] < rawExtentResult.max) { + rawExtentInfo.modifyDataMinMax('max', tarAxisExtent[1]); + } + } + }); +} + +function prepareAxisRecord( + axisRecordMap: HashMap, + axisModel: AxisBaseModel +): AxisRecord { + return axisRecordMap.get(axisModel.uid) + || axisRecordMap.set(axisModel.uid, { condExtent: initExtent() }); +} + +function initExtent() { + return [Infinity, -Infinity]; +} + +function unionExtent(extent: number[], val: number) { + val < extent[0] && (extent[0] = val); + val > extent[1] && (extent[1] = val); +} diff --git a/src/coord/polar/polarCreator.ts b/src/coord/polar/polarCreator.ts index b214779b765af89fb597fcd4fe2641572e4a247d..04789ddf9c3f9d013597d3f102820580a179591c 100644 --- a/src/coord/polar/polarCreator.ts +++ b/src/coord/polar/polarCreator.ts @@ -25,10 +25,10 @@ import Polar from './Polar'; import {parsePercent} from '../../util/number'; import { createScaleByModel, - niceScaleExtent + niceScaleExtent, + getDataDimensionsOnAxis } from '../../coord/axisHelper'; import CoordinateSystem from '../../CoordinateSystem'; -import {getStackedDimension} from '../../data/helper/dataStackHelper'; import PolarModel from './PolarModel'; import ExtensionAPI from '../../ExtensionAPI'; @@ -86,15 +86,11 @@ function updatePolarScale(this: Polar, ecModel: GlobalModel, api: ExtensionAPI) ecModel.eachSeries(function (seriesModel) { if (seriesModel.coordinateSystem === polar) { const data = seriesModel.getData(); - zrUtil.each(data.mapDimensionsAll('radius'), function (dim) { - radiusAxis.scale.unionExtentFromData( - data, getStackedDimension(data, dim) - ); + zrUtil.each(getDataDimensionsOnAxis(data, 'radius'), function (dim) { + radiusAxis.scale.unionExtentFromData(data, dim); }); - zrUtil.each(data.mapDimensionsAll('angle'), function (dim) { - angleAxis.scale.unionExtentFromData( - data, getStackedDimension(data, dim) - ); + zrUtil.each(getDataDimensionsOnAxis(data, 'angle'), function (dim) { + angleAxis.scale.unionExtentFromData(data, dim); }); } }); diff --git a/src/coord/radar/Radar.ts b/src/coord/radar/Radar.ts index 1a02f0ee00289f9016c49772c46a4dbc1518f9b4..c825d1e2e61a62fdbd40c8500752fc7ddcce3d73 100644 --- a/src/coord/radar/Radar.ts +++ b/src/coord/radar/Radar.ts @@ -25,7 +25,8 @@ import IntervalScale from '../../scale/Interval'; import * as numberUtil from '../../util/number'; import { getScaleExtent, - niceScaleExtent + niceScaleExtent, + parseAxisModelMinMax } from '../axisHelper'; import CoordinateSystemManager from '../../CoordinateSystem'; import { CoordinateSystemMaster, CoordinateSystem } from '../CoordinateSystem'; @@ -192,8 +193,8 @@ class Radar implements CoordinateSystem, CoordinateSystemMaster { const axisModel = indicatorAxis.model; const scale = indicatorAxis.scale as IntervalScale; - const fixedMin = axisModel.getMin() as number; - const fixedMax = axisModel.getMax() as number; + const fixedMin = parseAxisModelMinMax(scale, axisModel.get('min', true) as ScaleDataValue); + const fixedMax = parseAxisModelMinMax(scale, axisModel.get('max', true) as ScaleDataValue); let interval = scale.getInterval(); if (fixedMin != null && fixedMax != null) { diff --git a/src/coord/scaleRawExtentInfo.ts b/src/coord/scaleRawExtentInfo.ts new file mode 100644 index 0000000000000000000000000000000000000000..7eb59cbd5dacd3e388fe0afd34fa4a2fcfb9c502 --- /dev/null +++ b/src/coord/scaleRawExtentInfo.ts @@ -0,0 +1,316 @@ +/* +* 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 { assert, isArray, eqNaN, isFunction } from 'zrender/src/core/util'; +import { __DEV__ } from '../config'; +import Scale from '../scale/Scale'; +import { AxisBaseModel } from './AxisBaseModel'; +import { parsePercent } from 'zrender/src/contain/text'; +import { parseAxisModelMinMax } from './axisHelper'; +import { AxisBaseOption } from './axisCommonTypes'; + + +export interface ScaleRawExtentResult { + // `min`/`max` defines data available range, determined by + // `dataMin`/`dataMax` and explicit specified min max related option. + // The final extent will be based on the `min`/`max` and may be enlarge + // a little (say, "nice strategy", e.g., niceScale, boundaryGap). + // Ensure `min`/`max` be finite number or NaN here. + // (not to be null/undefined) `NaN` means min/max axis is blank. + readonly min: number; + readonly max: number; + // `minFixed`/`maxFixed` marks that `min`/`max` should be used + // in the final extent without other "nice strategy". + readonly minFixed: boolean; + readonly maxFixed: boolean; + // Mark that the axis should be blank. + readonly isBlank: boolean; +} + +export class ScaleRawExtentInfo { + + private _needCrossZero: boolean; + private _isOrdinal: boolean; + private _axisDataLen: number; + private _boundaryGapInner: number[]; + + // Accurate raw value get from model. + private _modelMinRaw: AxisBaseOption['min']; + private _modelMaxRaw: AxisBaseOption['max']; + + // Can be `finite number`/`null`/`undefined`/`NaN` + private _modelMinNum: number; + private _modelMaxNum: number; + + // Range union by series data on this axis. + // May be modified if data is filtered. + private _dataMin: number; + private _dataMax: number; + + // Highest priority if specified. + private _determinedMin: number; + private _determinedMax: number; + + // Make that the `rawExtentInfo` can not be modified any more. + readonly frozen: boolean; + + + constructor( + scale: Scale, + model: AxisBaseModel, + // Usually: data extent from all series on this axis. + originalExtent: number[] + ) { + this._prepareParams(scale, model, originalExtent); + } + + /** + * Parameters depending on ouside (like model, user callback) + * are prepared and fixed here. + */ + private _prepareParams( + scale: Scale, + model: AxisBaseModel, + // Usually: data extent from all series on this axis. + dataExtent: number[] + ) { + if (dataExtent[1] < dataExtent[0]) { + dataExtent = [NaN, NaN]; + } + this._dataMin = dataExtent[0]; + this._dataMax = dataExtent[1]; + + const isOrdinal = this._isOrdinal = scale.type === 'ordinal'; + this._needCrossZero = model.getNeedCrossZero(); + + const modelMinRaw = this._modelMinRaw = model.get('min', true); + if (isFunction(modelMinRaw)) { + // This callback alway provide users the full data extent (before data filtered). + this._modelMinNum = parseAxisModelMinMax(scale, modelMinRaw({ + min: dataExtent[0], + max: dataExtent[1] + })); + } + else if (modelMinRaw !== 'dataMin') { + this._modelMinNum = parseAxisModelMinMax(scale, modelMinRaw); + } + + const modelMaxRaw = this._modelMaxRaw = model.get('max', true); + if (isFunction(modelMaxRaw)) { + // This callback alway provide users the full data extent (before data filtered). + this._modelMaxNum = parseAxisModelMinMax(scale, modelMaxRaw({ + min: dataExtent[0], + max: dataExtent[1] + })); + } + else if (modelMaxRaw !== 'dataMax') { + this._modelMaxNum = parseAxisModelMinMax(scale, modelMaxRaw); + } + + if (isOrdinal) { + // FIXME: there is a flaw here: if there is no "block" data processor like `dataZoom`, + // and progressive rendering is using, here the category result might just only contain + // the processed chunk rather than the entire result. + this._axisDataLen = model.getCategories().length; + } + else { + const boundaryGap = model.get('boundaryGap'); + const boundaryGapArr = isArray(boundaryGap) + ? boundaryGap : [boundaryGap || 0, boundaryGap || 0]; + + if (typeof boundaryGapArr[0] === 'boolean' || typeof boundaryGapArr[1] === 'boolean') { + if (__DEV__) { + console.warn('Boolean type for boundaryGap is only ' + + 'allowed for ordinal axis. Please use string in ' + + 'percentage instead, e.g., "20%". Currently, ' + + 'boundaryGap is set to be 0.'); + } + this._boundaryGapInner = [0, 0]; + } + else { + this._boundaryGapInner = [ + parsePercent(boundaryGapArr[0], 1), + parsePercent(boundaryGapArr[1], 1) + ]; + } + } + } + + /** + * Calculate extent by prepared parameters. + * This method has no external dependency and can be called duplicatedly, + * getting the same result. + * If parameters changed, should call this method to recalcuate. + */ + calculate(): ScaleRawExtentResult { + // Notice: When min/max is not set (that is, when there are null/undefined, + // which is the most common case), these cases should be ensured: + // (1) For 'ordinal', show all axis.data. + // (2) For others: + // + `boundaryGap` is applied (if min/max set, boundaryGap is + // disabled). + // + If `needCrossZero`, min/max should be zero, otherwise, min/max should + // be the result that originalExtent enlarged by boundaryGap. + // (3) If no data, it should be ensured that `scale.setBlank` is set. + + const isOrdinal = this._isOrdinal; + const dataMin = this._dataMin; + const dataMax = this._dataMax; + const axisDataLen = this._axisDataLen; + const boundaryGapInner = this._boundaryGapInner; + + const span = !isOrdinal + ? ((dataMax - dataMin) || Math.abs(dataMin)) + : null; + + // Currently if a `'value'` axis model min is specified as 'dataMin'/'dataMax', + // `boundaryGap` will not be used. It's the different from specifying as `null`/`undefined`. + let min = this._modelMinRaw === 'dataMin' ? dataMin : this._modelMinNum; + let max = this._modelMaxRaw === 'dataMax' ? dataMax : this._modelMaxNum; + + // If `_modelMinNum`/`_modelMaxNum` is `null`/`undefined`, should not be fixed. + let minFixed = min != null; + let maxFixed = max != null; + + if (min == null) { + min = isOrdinal + ? (axisDataLen ? 0 : NaN) + : dataMin - boundaryGapInner[0] * span; + } + if (max == null) { + max = isOrdinal + ? (axisDataLen ? axisDataLen - 1 : NaN) + : dataMax + boundaryGapInner[1] * span; + } + + (min == null || !isFinite(min)) && (min = NaN); + (max == null || !isFinite(max)) && (max = NaN); + + if (min > max) { + min = NaN; + max = NaN; + } + + const isBlank = eqNaN(min) + || eqNaN(max) + || (isOrdinal && !axisDataLen); + + // If data extent modified, need to recalculated to ensure cross zero. + if (this._needCrossZero) { + // Axis is over zero and min is not set + if (min > 0 && max > 0 && !minFixed) { + min = 0; + // minFixed = true; + } + // Axis is under zero and max is not set + if (min < 0 && max < 0 && !maxFixed) { + max = 0; + // maxFixed = true; + } + // PENDING: + // When `needCrossZero` and all data is positive/negative, should it be ensured + // that the results processed by boundaryGap are positive/negative? + // If so, here `minFixed`/`maxFixed` need to be set. + } + + const determinedMin = this._determinedMin; + const determinedMax = this._determinedMax; + if (determinedMin != null) { + min = determinedMin; + minFixed = true; + } + if (determinedMax != null) { + max = determinedMax; + maxFixed = true; + } + + // Ensure min/max be finite number or NaN here. (not to be null/undefined) + // `NaN` means min/max axis is blank. + return { + min: min, + max: max, + minFixed: minFixed, + maxFixed: maxFixed, + isBlank: isBlank + }; + } + + modifyDataMinMax(minMaxName: 'min' | 'max', val: number): void { + if (__DEV__) { + assert(!this.frozen); + } + this[DATA_MIN_MAX_ATTR[minMaxName]] = val; + } + + setDeterminedMinMax(minMaxName: 'min' | 'max', val: number): void { + const attr = DETERMINED_MIN_MAX_ATTR[minMaxName]; + if (__DEV__) { + assert( + !this.frozen + // Earse them usually means logic flaw. + && (this[attr] == null) + ); + } + this[attr] = val; + } + + freeze() { + // @ts-ignore + this.frozen = true; + } +} + +const DETERMINED_MIN_MAX_ATTR = { min: '_determinedMin', max: '_determinedMax' } as const; +const DATA_MIN_MAX_ATTR = { min: '_dataMin', max: '_dataMax' } as const; + +/** + * Get scale min max and related info only depends on model settings. + * This method can be called after coordinate system created. + * For example, in data processing stage. + * + * Scale extent info probably be required multiple times during a workflow. + * For example: + * (1) `dataZoom` depends it to get the axis extent in "100%" state. + * (2) `processor/extentCalculator` depends it to make sure whethe axis extent is specified. + * (3) `coordSys.update` use it to finally decide the scale extent. + * But the callback of `min`/`max` should not be called multiple time. + * The code should not be duplicated either. + * So we cache the result in the scale instance, which will be recreated in the begining + * of the workflow. + */ +export function ensureScaleRawExtentInfo( + scale: Scale, + model: AxisBaseModel, + // Usually: data extent from all series on this axis. + originalExtent: number[] +): ScaleRawExtentInfo { + + // Do not permit to recreate. + let rawExtentInfo = scale.rawExtentInfo; + if (rawExtentInfo) { + return rawExtentInfo; + } + + rawExtentInfo = new ScaleRawExtentInfo(scale, model, originalExtent); + // @ts-ignore + scale.rawExtentInfo = rawExtentInfo; + + return rawExtentInfo; +} + diff --git a/src/data/List.ts b/src/data/List.ts index 1b17cfa611afa702745a92798787675b185a6c2b..1630820510e2833460e4472f94a7ec4f437e90d3 100644 --- a/src/data/List.ts +++ b/src/data/List.ts @@ -876,6 +876,14 @@ class List< } /** + * PENDING: In fact currently this function is only used to short-circuit + * the calling of `scale.unionExtentFromData` when data have been filtered by modules + * like "dataZoom". `scale.unionExtentFromData` is used to calculate data extent for series on + * an axis, but if a "axis related data filter module" is used, the extent of the axis have + * been fixed and no need to calling `scale.unionExtentFromData` actually. + * But if we add "custom data filter" in future, which is not "axis related", this method may + * be still needed. + * * Optimize for the scenario that data is filtered by a given extent. * Consider that if data amount is more than hundreds of thousand, * extent calculation will cost more than 10ms and the cache will @@ -883,9 +891,13 @@ class List< */ getApproximateExtent(dim: DimensionLoose): [number, number] { dim = this.getDimension(dim); - return this._approximateExtent[dim] || this.getDataExtent(dim /*, stack */); + return this._approximateExtent[dim] || this.getDataExtent(dim); } + /** + * Calculate extent on a filtered data might be time consuming. + * Approximate extent is only used for: calculte extent of filtered data outside. + */ setApproximateExtent(extent: [number, number], dim: DimensionLoose): void { dim = this.getDimension(dim); this._approximateExtent[dim] = extent.slice() as [number, number]; diff --git a/src/echarts.ts b/src/echarts.ts index 57d475d230aeb568000263a4f2b0d6495131e94d..753aba3a08f12f41c6254e30d54a29451fa86379 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -88,9 +88,14 @@ export const dependencies = { const TEST_FRAME_REMAIN_TIME = 1; -const PRIORITY_PROCESSOR_FILTER = 1000; const PRIORITY_PROCESSOR_SERIES_FILTER = 800; +// Some data processors depends on the stack result dimension (to calculate data extent). +// So data stack stage should be in front of data processing stage. const PRIORITY_PROCESSOR_DATASTACK = 900; +// "Data filter" will block the stream, so it should be +// put at the begining of data processing. +const PRIORITY_PROCESSOR_FILTER = 1000; +const PRIORITY_PROCESSOR_DEFAULT = 2000; const PRIORITY_PROCESSOR_STATISTIC = 5000; const PRIORITY_VISUAL_LAYOUT = 1000; @@ -2131,7 +2136,7 @@ export function registerProcessor( priority: number | StageHandler | StageHandlerOverallReset, processor?: StageHandler | StageHandlerOverallReset ): void { - normalizeRegister(dataProcessorFuncs, priority, processor, PRIORITY_PROCESSOR_FILTER); + normalizeRegister(dataProcessorFuncs, priority, processor, PRIORITY_PROCESSOR_DEFAULT); } /** diff --git a/src/helper.ts b/src/helper.ts index 85d4825672330f9bd3f2ab7a10e9dcb6f5877bed..92622b4896070ad56e1fb5b3981a3681eccad6e3 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -102,8 +102,6 @@ export function createScale(dataExtent: number[], option: object | AxisBaseModel * `getMin(origin: boolean) => number` * `getMax(origin: boolean) => number` * `getNeedCrossZero() => boolean` - * `setRange(start: number, end: number)` - * `resetRange()` */ export function mixinAxisModelCommonMethods(Model: Model) { zrUtil.mixin(Model, AxisModelCommonMixin); diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts index a5bf397ef5bf634ec351c6255fd63811eafc8023..7cedc8ac54c493fec704d261a8743b3f9000ab11 100644 --- a/src/scale/Scale.ts +++ b/src/scale/Scale.ts @@ -22,6 +22,8 @@ import * as clazzUtil from '../util/clazz'; import { Dictionary } from 'zrender/src/core/types'; import List from '../data/List'; import { DimensionName, ScaleDataValue, OptionDataValue } from '../util/types'; +import { ScaleRawExtentInfo } from '../coord/scaleRawExtentInfo'; + abstract class Scale { @@ -33,6 +35,9 @@ abstract class Scale { private _isBlank: boolean; + // Inject + readonly rawExtentInfo: ScaleRawExtentInfo; + constructor(setting?: Dictionary) { this._setting = setting || {}; this._extent = [Infinity, -Infinity]; diff --git a/test/axis-filter-extent.html b/test/axis-filter-extent.html new file mode 100644 index 0000000000000000000000000000000000000000..a6ef027a5ef9ac6d561970419a004dbb602db114 --- /dev/null +++ b/test/axis-filter-extent.html @@ -0,0 +1,422 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +