581 lines
26 KiB
JavaScript
581 lines
26 KiB
JavaScript
|
|
/*
|
|
* 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.
|
|
*/
|
|
|
|
|
|
/**
|
|
* AUTO-GENERATED FILE. DO NOT MODIFY.
|
|
*/
|
|
|
|
/*
|
|
* 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, each, createHashMap } from 'zrender/lib/core/util.js';
|
|
import { parsePercent } from 'zrender/lib/contain/text.js';
|
|
import { isIntervalScale, isLogScale, isOrdinalScale, isTimeScale } from '../scale/helper.js';
|
|
import { makeInner, initExtentForUnion, unionExtentFromNumber, isValidNumberForExtent, extentHasValue, unionExtentFromExtent, unionExtentStartFromNumber, unionExtentEndFromNumber, ensureExtentAscSimply } from '../util/model.js';
|
|
import { discourageOnAxisZero, getDataDimensionsOnAxis, isAxisOnBand } from './axisHelper.js';
|
|
import { getCoordForCoordSysUsageKindBox } from '../core/CoordinateSystem.js';
|
|
import { error } from '../util/log.js';
|
|
import { isNullableNumberFinite, mathMax, mathMin } from '../util/number.js';
|
|
import { SCALE_EXTENT_KIND_MAPPING } from '../scale/scaleMapper.js';
|
|
import { eachKeyOnAxis, eachSeriesOnAxis } from './axisStatistics.js';
|
|
/**
|
|
* NOTICE: Can be only used in `ensureScaleStore(axisLike)`.
|
|
*
|
|
* In most cases the instances of `Axis` and `Scale` are one-to-one mapping and share the same lifecycle.
|
|
* But in some external usage (such as echarts-gl), axis instance does not necessarily exist, and only
|
|
* scale instance and axisModel are used. Therefore we store the internal info on scale instance directly.
|
|
*/
|
|
var scaleInner = makeInner();
|
|
export var AXIS_EXTENT_INFO_BUILD_FROM_COORD_SYS_UPDATE = 1;
|
|
export var AXIS_EXTENT_INFO_BUILD_FROM_DATA_ZOOM = 2;
|
|
var AXIS_EXTENT_INFO_BUILD_FROM_EMPTY = 3;
|
|
var ScaleRawExtentInfo = /** @class */function () {
|
|
function ScaleRawExtentInfo(scale, model,
|
|
// Typically: data extent from all series on this axis.
|
|
dataExtent, requireStartValue, requireContainShape) {
|
|
var isOrdinal = isOrdinalScale(scale);
|
|
var axisDataLen = 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.
|
|
? model.getCategories().length : null;
|
|
// [CATEGORY_AXIS_MODEL_DATA_IS_EMPTY_ARRAY]:
|
|
// This is only for backward compatibility - `xxxAxis: {data: []}` can declare this axis as
|
|
// a "category" axis and use `series.data` to determine its extent but the axis is blank - only
|
|
// axis line is displayed. This is a conincidence, but it is used in some cases.
|
|
var categoryAxisModelDataIsEmptyArray;
|
|
if (isOrdinal) {
|
|
var axisModelDataArray = model.getCategories(true);
|
|
categoryAxisModelDataIsEmptyArray = axisModelDataArray && !axisModelDataArray.length;
|
|
}
|
|
// NOTE: also considered the input dataExtent may be still in the initialized state `[Infinity, -Infinity]`.
|
|
var dataMM = dataExtent.slice();
|
|
// custom dataMin/dataMax.
|
|
// Also considered `modelDataMinMax[0] > modelDataMinMax[1]` may occur.
|
|
if (isIntervalScale(scale) || isLogScale(scale) || isTimeScale(scale)) {
|
|
unionExtentStartFromNumber(dataMM, parseAxisModelMinMax(scale, model.get('dataMin', true)));
|
|
unionExtentEndFromNumber(dataMM, parseAxisModelMinMax(scale, model.get('dataMax', true)));
|
|
}
|
|
if (!extentHasValue(dataMM)) {
|
|
// dataMM may be still `[Infinity, -Infinity]`, we use `NaN` on the subsequent calculations
|
|
// to force the `noZoomEffMM` to be `[NaN, NaN]` if needed.
|
|
dataMM[0] = dataMM[1] = NaN;
|
|
}
|
|
var noZoomEffMM = [];
|
|
var fixMM = [false, false];
|
|
// 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 `needIncludeZero`, 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.
|
|
var modelMinRaw = model.get('min', true);
|
|
if (modelMinRaw === 'dataMin') {
|
|
noZoomEffMM[0] = dataMM[0];
|
|
fixMM[0] = true;
|
|
} else {
|
|
noZoomEffMM[0] = parseAxisModelMinMax(scale, isFunction(modelMinRaw)
|
|
// This callback always provides users the full data extent (before data is filtered).
|
|
? modelMinRaw({
|
|
min: dataMM[0],
|
|
max: dataMM[1]
|
|
}) : modelMinRaw);
|
|
// If `xxxAxis.min: null/undefined`, min should not be fixed.
|
|
fixMM[0] = noZoomEffMM[0] != null;
|
|
}
|
|
var modelMaxRaw = model.get('max', true);
|
|
if (modelMaxRaw === 'dataMax') {
|
|
noZoomEffMM[1] = dataMM[1];
|
|
fixMM[1] = true;
|
|
} else {
|
|
noZoomEffMM[1] = parseAxisModelMinMax(scale, isFunction(modelMaxRaw)
|
|
// This callback always provides users the full data extent (before data is filtered).
|
|
? modelMaxRaw({
|
|
min: dataMM[0],
|
|
max: dataMM[1]
|
|
}) : modelMaxRaw);
|
|
// If `xxxAxis.max: null/undefined`, max should not be fixed.
|
|
fixMM[1] = noZoomEffMM[1] != null;
|
|
}
|
|
var boundaryGap = parseBoundaryGapOption(scale, model);
|
|
var span = !isOrdinal
|
|
// PENDING: Historicall behavior but may not reasonable enough.
|
|
? dataMM[1] - dataMM[0] || Math.abs(dataMM[0]) : null;
|
|
// NOTE: If a numeric axis min/max is specified as 'dataMin'/'dataMax',
|
|
// `boundaryGap` will not be used.
|
|
if (noZoomEffMM[0] == null) {
|
|
noZoomEffMM[0] = isOrdinal ? categoryAxisModelDataIsEmptyArray ? dataMM[0] : axisDataLen ? 0 : NaN : dataMM[0] - boundaryGap[0] * span;
|
|
}
|
|
if (noZoomEffMM[1] == null) {
|
|
noZoomEffMM[1] = isOrdinal ? categoryAxisModelDataIsEmptyArray ? dataMM[1] : axisDataLen ? axisDataLen - 1 : NaN : dataMM[1] + boundaryGap[1] * span;
|
|
}
|
|
// Normalize to `NaN` if invalid; e.g., this may occur when `dataMM` has Infinity.
|
|
!isValidNumberForExtent(noZoomEffMM[0]) && (noZoomEffMM[0] = NaN);
|
|
!isValidNumberForExtent(noZoomEffMM[1]) && (noZoomEffMM[1] = NaN);
|
|
var isBlank = categoryAxisModelDataIsEmptyArray || eqNaN(noZoomEffMM[0]) || eqNaN(noZoomEffMM[1]) || isOrdinal && !axisDataLen;
|
|
// NOTE: `needIncludeZero` is not applicable to LogScale, TimeScale, OrdinalScale.
|
|
var needIncludeZeroApplicable = isIntervalScale(scale);
|
|
var needIncludeZero = needIncludeZeroApplicable && model.needIncludeZero && model.needIncludeZero();
|
|
if (needIncludeZero) {
|
|
if (noZoomEffMM[0] > 0 && noZoomEffMM[1] > 0 && !fixMM[0]) {
|
|
noZoomEffMM[0] = 0;
|
|
// fixMM[0] = true;
|
|
}
|
|
if (noZoomEffMM[0] < 0 && noZoomEffMM[1] < 0 && !fixMM[1]) {
|
|
noZoomEffMM[1] = 0;
|
|
// fixMM[1] = true;
|
|
}
|
|
}
|
|
var needToggleAxisInverse = false;
|
|
if (noZoomEffMM[0] > noZoomEffMM[1]) {
|
|
// Historically, if users set `xxxAxis.min > xxxAxis.max`, or `xxxAxis.max < dataExtent[0]`,
|
|
// or `xxxAxis.min > dataExtent[1]` the behavior is sometimes like `xxxAxis.inverse = true`,
|
|
// sometimes abnormal. We remain backward compatible with the former one, though this feature
|
|
// may not be reasonable.
|
|
// And handle it after "needIncludeZero" is also for backward compatibility.
|
|
noZoomEffMM.reverse();
|
|
needToggleAxisInverse = true;
|
|
}
|
|
var startValue = parseAxisModelMinMax(scale, model.get('startValue', true));
|
|
var startValueSpecified = startValue != null;
|
|
if (!isNullableNumberFinite(startValue) && requireStartValue) {
|
|
startValue = scale.getDefaultStartValue ? scale.getDefaultStartValue() : 0;
|
|
}
|
|
if (isNullableNumberFinite(startValue)
|
|
// Keep backward compatibility and enable `xxxAxis.scale: true` enabled on bar series:
|
|
// if `xxxAxis.scale: true` and `startValue` is not specified, do not union the default `startValue`,
|
|
&& (startValueSpecified || !needIncludeZeroApplicable || needIncludeZero)) {
|
|
if (startValue < noZoomEffMM[0] && !fixMM[0]) {
|
|
noZoomEffMM[0] = startValue;
|
|
fixMM[0] = true;
|
|
} else if (startValue > noZoomEffMM[1] && !fixMM[1]) {
|
|
noZoomEffMM[1] = startValue;
|
|
fixMM[1] = true;
|
|
}
|
|
}
|
|
var internal = this._i = {
|
|
scale: scale,
|
|
dataMM: dataMM,
|
|
noZoomEffMM: noZoomEffMM,
|
|
zoomMM: [],
|
|
fixMM: fixMM,
|
|
zoomFixMM: [false, false],
|
|
startValue: startValue,
|
|
isBlank: isBlank,
|
|
incl0: needIncludeZero,
|
|
tggAxInv: needToggleAxisInverse,
|
|
ctnShp: requireContainShape
|
|
};
|
|
sanitizeExtent(internal, noZoomEffMM);
|
|
}
|
|
ScaleRawExtentInfo.prototype.makeNoZoom = function () {
|
|
return this._i.noZoomEffMM.slice();
|
|
};
|
|
ScaleRawExtentInfo.prototype.makeFinal = function () {
|
|
var internal = this._i;
|
|
var zoomMM = internal.zoomMM;
|
|
var noZoomEffMM = internal.noZoomEffMM;
|
|
var zoomFixMM = internal.zoomFixMM;
|
|
var fixMM = internal.fixMM;
|
|
var result = {
|
|
fixMM: fixMM,
|
|
zoomFixMM: zoomFixMM,
|
|
isBlank: internal.isBlank,
|
|
incl0: internal.incl0,
|
|
tggAxInv: internal.tggAxInv,
|
|
ctnShp: internal.ctnShp,
|
|
effMM: noZoomEffMM.slice()
|
|
};
|
|
var effMM = result.effMM;
|
|
// NOTE: Switching `fixMM` probably leads to abrupt extent changes when draging a `dataZoom`
|
|
// handle, since `fixMM` impact the "nice extent" and "nice ticks" calculation.
|
|
// Consider a case:
|
|
// dataZoom `start` is 2% but its `end` is 100%, (or vice versa), we currently only set `fixMM[0]`
|
|
// as `true` but remain `fixMM[1]` as `false` for this case to avoid unnecessary abrupt change.
|
|
// Incidentally, the effect is not unacceptable if we set both `fixMM[0]/[1]` as `true`.
|
|
if (zoomMM[0] != null) {
|
|
effMM[0] = zoomMM[0];
|
|
fixMM[0] = zoomFixMM[0] = true;
|
|
}
|
|
if (zoomMM[1] != null) {
|
|
effMM[1] = zoomMM[1];
|
|
fixMM[1] = zoomFixMM[1] = true;
|
|
}
|
|
sanitizeExtent(internal, effMM);
|
|
return result;
|
|
};
|
|
ScaleRawExtentInfo.prototype.makeRenderInfo = function () {
|
|
return {
|
|
startValue: this._i.startValue
|
|
};
|
|
};
|
|
/**
|
|
* NOTICE:
|
|
* - Do not set them if the percent are 0% or 100%. (See `AxisProxy['reset']`.)
|
|
* - The caller must ensure `start <= end` and the range is equal or less then `noZoomEffMM`.
|
|
* (See `AxisProxy['calculateDataWindow']`.)
|
|
* - The outcome `_zoomMM` may have both `NullUndefined` and a finite value, like `[undefined, 123]`.
|
|
*/
|
|
ScaleRawExtentInfo.prototype.setZoomMM = function (idxMinMax, val) {
|
|
this._i.zoomMM[idxMinMax] = val;
|
|
};
|
|
return ScaleRawExtentInfo;
|
|
}();
|
|
export { ScaleRawExtentInfo };
|
|
/**
|
|
* Should be called when a new extent is created or modified.
|
|
*/
|
|
function sanitizeExtent(internal, mm) {
|
|
var scale = internal.scale;
|
|
var dataMM = internal.dataMM;
|
|
if (scale.sanitize) {
|
|
mm[0] = scale.sanitize(mm[0], dataMM);
|
|
mm[1] = scale.sanitize(mm[1], dataMM);
|
|
ensureExtentAscSimply(mm);
|
|
}
|
|
}
|
|
function parseAxisModelMinMax(scale, minMax) {
|
|
return minMax == null ? null // null/undefined means not specified and other default values can be applied.
|
|
: eqNaN(minMax) ? NaN // NaN means a deliberate invalid number.
|
|
: scale.parse(minMax);
|
|
}
|
|
function parseBoundaryGapOption(scale, model) {
|
|
var boundaryGapOptionArr;
|
|
if (isOrdinalScale(scale)) {
|
|
boundaryGapOptionArr = [0, 0];
|
|
} else {
|
|
var boundaryGap = model.get('boundaryGap');
|
|
if (typeof boundaryGap === 'boolean') {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
if (boundaryGap === true) {
|
|
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 0.');
|
|
}
|
|
}
|
|
boundaryGap = null;
|
|
}
|
|
boundaryGapOptionArr = isArray(boundaryGap) ? boundaryGap : [boundaryGap, boundaryGap];
|
|
}
|
|
return [parseBoundaryGapOptionItem(boundaryGapOptionArr[0]), parseBoundaryGapOptionItem(boundaryGapOptionArr[1])];
|
|
}
|
|
function parseBoundaryGapOptionItem(opt) {
|
|
return parsePercent(typeof opt === 'boolean' ? 0 : opt, 1) || 0;
|
|
}
|
|
/**
|
|
* NOTE: `associateSeriesWithAxis` is not necessarily called, e.g., when
|
|
* an axis is not used by any series.
|
|
*/
|
|
function ensureScaleStore(axisLike) {
|
|
var store = scaleInner(axisLike.scale);
|
|
if (!store.extent) {
|
|
store.extent = initExtentForUnion();
|
|
}
|
|
return store;
|
|
}
|
|
/**
|
|
* This supports union extent on case like: pie (or other similar series)
|
|
* lays out on cartesian2d.
|
|
* @see scaleRawExtentInfoCreate
|
|
*/
|
|
export function scaleRawExtentInfoEnableBoxCoordSysUsage(axisLike, coordSysDimIdxMap) {
|
|
ensureScaleStore(axisLike).dimIdxInCoord = coordSysDimIdxMap.get(axisLike.dim);
|
|
}
|
|
/**
|
|
* @usage
|
|
* class SomeCoordSys {
|
|
* static create() {
|
|
* ecModel.eachSeries(function (seriesModel) {
|
|
* associateSeriesWithAxis(axis1, seriesModel, ...);
|
|
* associateSeriesWithAxis(axis2, seriesModel, ...);
|
|
* // ...
|
|
* });
|
|
* }
|
|
* update() {
|
|
* scaleRawExtentInfoCreate(axis1);
|
|
* scaleRawExtentInfoCreate(axis2);
|
|
* }
|
|
* }
|
|
* class AxisProxy {
|
|
* reset() {
|
|
* scaleRawExtentInfoCreate(axis1);
|
|
* }
|
|
* }
|
|
*
|
|
* NOTICE:
|
|
* - `associateSeriesWithAxis`(in `axisStatistics.ts`) should be called in:
|
|
* - Coord sys create method.
|
|
* - `scaleRawExtentInfoCreate` should be typically called in:
|
|
* - `dataZoom` processor. It requires processing like:
|
|
* 1. Filter series data by dataZoom1;
|
|
* 2. Union the filtered data and init the extent of the orthogonal axes, which is the 100% of dataZoom2;
|
|
* 3. Filter series data by dataZoom2;
|
|
* 4. ...
|
|
* - Coord sys update method, for other axes that not covered by `dataZoom`.
|
|
* NOTE: If a `dataZoom` covers this series, this data and its extent has been dataZoom-filtered.
|
|
* Therefore this handling should not before `dataZoom`.
|
|
* - The callback of `min`/`max` in ec option should NOT be called multiple times,
|
|
* therefore, we initialize `ScaleRawExtentInfo` uniformly in `scaleRawExtentInfoCreate`.
|
|
*
|
|
* @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
|
|
*/
|
|
export function scaleRawExtentInfoCreate(axis, from) {
|
|
var scale = axis.scale;
|
|
var model = axis.model;
|
|
var axisDim = axis.dim;
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
assert(scale && model && axisDim);
|
|
}
|
|
if (scale.rawExtentInfo) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
// Check for incorrect impl - the duplicated calling of this method is only allowed in
|
|
// these cases:
|
|
// - First in `AxisProxy['reset']` (for dataZoom)
|
|
// - Then in `CoordinateSystem['update']`.
|
|
// - Then after `chart.appendData()` due to `dirtyOnOverallProgress: true`
|
|
assert(scale.rawExtentInfo.from !== from || from === AXIS_EXTENT_INFO_BUILD_FROM_DATA_ZOOM);
|
|
}
|
|
return;
|
|
}
|
|
scaleRawExtentInfoCreateDeal(scale, axis, axisDim, model, from);
|
|
}
|
|
function scaleRawExtentInfoCreateDeal(scale, axis, axisDim, model, from) {
|
|
var scaleStore = ensureScaleStore(axis);
|
|
var extent = scaleStore.extent;
|
|
var requireStartValue = false;
|
|
eachSeriesOnAxis(axis, function (seriesModel) {
|
|
if (seriesModel.boxCoordinateSystem) {
|
|
// This supports union extent on case like: pie (or other similar series)
|
|
// lays out on cartesian2d.
|
|
var coord = getCoordForCoordSysUsageKindBox(seriesModel).coord;
|
|
var dimIdx = scaleStore.dimIdxInCoord;
|
|
if (!(dimIdx >= 0)) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
// Require `scaleRawExtentInfoEnableBoxCoordSysUsage` have been called to support it.
|
|
// But if users set it, give a error log but no exceptions.
|
|
error("Property \"series.coord\" is not supported on axis " + seriesModel.boxCoordinateSystem.type + ".");
|
|
}
|
|
}
|
|
// Only `[val1, val2]` case needs to be supported currently.
|
|
else if (isArray(coord)) {
|
|
var coordItem = coord[dimIdx];
|
|
if (coordItem != null && !isArray(coordItem)) {
|
|
unionExtentFromNumber(extent, scale.parse(coordItem));
|
|
}
|
|
}
|
|
} else if (seriesModel.coordinateSystem) {
|
|
// NOTE: This data may have been filtered by dataZoom on orthogonal axes.
|
|
var data_1 = seriesModel.getData();
|
|
if (data_1) {
|
|
var filter_1 = scale.getFilter ? scale.getFilter() : null;
|
|
each(getDataDimensionsOnAxis(data_1, axisDim), function (dim) {
|
|
unionExtentFromExtent(extent, data_1.getApproximateExtent(dim, filter_1));
|
|
});
|
|
}
|
|
if (seriesModel.__requireStartValue && seriesModel.__requireStartValue(axis)) {
|
|
requireStartValue = true;
|
|
}
|
|
}
|
|
});
|
|
var requireContainShape = determineRequireContainShape(scale, axis, model);
|
|
var rawExtentInfo = new ScaleRawExtentInfo(scale, model, extent, requireStartValue, requireContainShape);
|
|
injectScaleRawExtentInfo(scale, rawExtentInfo, from);
|
|
scaleStore.extent = null; // Clean up
|
|
}
|
|
/**
|
|
* `rawExtentInfo` may not be created in some cases, such as no series declared or extra useless
|
|
* axes declared in ec option. In this case we still create a default one for that empty axis.
|
|
*/
|
|
function scaleRawExtentInfoBuildDefault(axisLike, dataExtent) {
|
|
var scale = axisLike.scale;
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
assert(!scale.rawExtentInfo);
|
|
}
|
|
injectScaleRawExtentInfo(scale, new ScaleRawExtentInfo(scale, axisLike.model, dataExtent, false, false), AXIS_EXTENT_INFO_BUILD_FROM_EMPTY);
|
|
}
|
|
function injectScaleRawExtentInfo(scale, scaleRawExtentInfo, from) {
|
|
// @ts-ignore
|
|
scale.rawExtentInfo = scaleRawExtentInfo;
|
|
// @ts-ignore
|
|
scaleRawExtentInfo.from = from;
|
|
}
|
|
/**
|
|
* See `axisSnippets.ts` for some commonly used handlers.
|
|
*
|
|
* FIXME:
|
|
* `boundaryGap: true` (i.e., `onBand: true` in code) has long been supported on category axis.
|
|
* And it is implemented in different code and not merged to this implementation yet.
|
|
*/
|
|
export function registerAxisContainShapeHandler(
|
|
// `axisStatKey` is used to quickly omit irrelevant handlers,
|
|
// since handlers need to be iterated per axis.
|
|
axisStatKey, handler) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
assert(!axisContainShapeHandlerMap.get(axisStatKey));
|
|
}
|
|
axisContainShapeHandlerMap.set(axisStatKey, handler);
|
|
}
|
|
var axisContainShapeHandlerMap = createHashMap();
|
|
/**
|
|
* Prepare axis scale extent before "nice".
|
|
* Item of returned array can only be number (including Infinity and NaN).
|
|
*/
|
|
export function adoptScaleRawExtentInfoAndPrepare(scale, model, ecModel, axis, externalDataExtent) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
assert(!externalDataExtent || !scale.rawExtentInfo);
|
|
}
|
|
if (!scale.rawExtentInfo) {
|
|
scaleRawExtentInfoBuildDefault({
|
|
scale: scale,
|
|
model: model
|
|
}, externalDataExtent || initExtentForUnion());
|
|
}
|
|
var rawExtentResult = scale.rawExtentInfo.makeFinal();
|
|
// NOTE: This `scale.setExtent()` is required by:
|
|
// - `axisNiceTicks.ts` and `axisAlignTicks.ts`, where the internal `scaleMapper` may be required.
|
|
var effectiveMinMax = rawExtentResult.effMM;
|
|
scale.setExtent(effectiveMinMax[0], effectiveMinMax[1]);
|
|
scale.setBlank(rawExtentResult.isBlank);
|
|
if (axis && rawExtentResult.tggAxInv && ecModel && !ecModel.get('legacyMinMaxDontInverseAxis')) {
|
|
axis.inverse = !axis.inverse;
|
|
}
|
|
return rawExtentResult;
|
|
}
|
|
function determineRequireContainShape(scale, axis, model) {
|
|
var onBand = isAxisOnBand(scale, model);
|
|
var modelContainShape = model.get('containShape', true);
|
|
if (modelContainShape == null && !onBand) {
|
|
modelContainShape = true;
|
|
}
|
|
if (!modelContainShape) {
|
|
return false;
|
|
}
|
|
var requireContainShape = false;
|
|
eachKeyOnAxis(axis, function (axisStatKey) {
|
|
requireContainShape = !!axisContainShapeHandlerMap.get(axisStatKey) || requireContainShape;
|
|
});
|
|
return requireContainShape;
|
|
}
|
|
/**
|
|
* This implements ec option `someAxis.containShape`. That is, expand scale extent slightly to
|
|
* ensure shapes of specific series are fully contained in the axis extent without overflow.
|
|
*
|
|
* NOTICE:
|
|
* Scale extent (data extent) and axis pixel extent (pixel extent) and are required as inputs.
|
|
* - See BAND_WIDTH_USED_SCALE_LINEAR_SPAN.
|
|
* - Axis pixel extent has been set outside, though it may be modified later (e.g., via `outerBounds`).
|
|
*
|
|
* @tutorial [AXIS_CONTAIN_SHAPE_PROCESSING_ORDER]
|
|
* This is a trade-off between the following 2 approaches:
|
|
* - Steps: (the current implementation)
|
|
* 1. Process `dataZoom` based on a full window `noZoomEffMM`.
|
|
* 2. Perform "nice" or "align" scale, where `intervalScaleEnsureValidExtent`-ish may be performed to
|
|
* expand extent to avoid `extent[0] === extent[1]`.
|
|
* 3. Calculate linear supplement of containShape based on the final result.
|
|
* Cons:
|
|
* - Abrupt changes occur when zooming away from 0% or 100%.
|
|
* - Edge shapes are clipped in "dataZoom shadow".
|
|
* - Steps: (discarded)
|
|
* 1. Calculate linear supplement of containShape based on `noZoomEffMM` and
|
|
* `intervalScaleEnsureValidExtent`-ish.
|
|
* 2. Process `dataZoom` based on a full window `noZoomEffMM + linearSupplement`.
|
|
* 3. Perform "nice"/"align" scale.
|
|
* Cons:
|
|
* - Input `startValue: 0` (in ec option or action) does not corresponds to `0%`, which is unacceptable.
|
|
* - Not easy to perform `intervalScaleEnsureValidExtent`-ish before "nice"/"align" processing.
|
|
*
|
|
* @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
|
|
*/
|
|
export function adoptScaleExtentKindMapping(axis, scale, rawExtentResult, ecModel) {
|
|
if (!rawExtentResult.ctnShp) {
|
|
return;
|
|
}
|
|
var linearSupplement;
|
|
eachKeyOnAxis(axis, function (axisStatKey) {
|
|
var handler = axisContainShapeHandlerMap.get(axisStatKey);
|
|
if (handler) {
|
|
// This feature can be implemented by either expanding axis extent or scale extent. The choice depends
|
|
// on whether series shape sizes are defined in pixels or data space. For example, scatter series glyph
|
|
// sizes is mainly defined in pixel, while bar series `bandWidth` is mainly determined by given percents
|
|
// of data scale. Since currently scatter does not require this feature, we implement it only on the
|
|
// data scale.
|
|
var singleLinearSupplement = handler(axis, ecModel);
|
|
if (singleLinearSupplement) {
|
|
linearSupplement = linearSupplement || [0, 0];
|
|
unionExtentStartFromNumber(linearSupplement, singleLinearSupplement[0]);
|
|
unionExtentEndFromNumber(linearSupplement, singleLinearSupplement[1]);
|
|
// Consider the consistency of `onZero` behavior (not varying by series data; otherwise error-prone
|
|
// to users), if any `containShape` really performed on this axis, discourage `onZero`.
|
|
discourageOnAxisZero(axis);
|
|
}
|
|
}
|
|
});
|
|
if (!linearSupplement) {
|
|
return;
|
|
}
|
|
var scaleExtent = scale.getExtent();
|
|
if (isOrdinalScale(scale)) {
|
|
if (!axis.onBand) {
|
|
// - Zooming on `OrdinalScale` auto "snaps" to integer ticks, which causes edge shapes to
|
|
// always overlap and be clipped at the boundaries. Therefore we always supplement it with
|
|
// half bandWith to avoid that overlapping.
|
|
// - `linearSupplement` is typically [-0.5, 0.5] in this case. `linearSupplement` exists only
|
|
// if any series call `registerAxisContainShapeHandler`.
|
|
// - PENDING: For historical reason, `onBand: true` has another implementation to handle
|
|
// this case. Merge them to this?
|
|
scale.setExtent2(SCALE_EXTENT_KIND_MAPPING, mathMin(scaleExtent[0], scaleExtent[0] + linearSupplement[0]), mathMax(scaleExtent[1], scaleExtent[1] + linearSupplement[1]));
|
|
}
|
|
} else {
|
|
// For other cases, `SCALE_EXTENT_KIND_MAPPING` is only used on the full window of `dataZoom`,
|
|
// where the visual result is more intuitive when zooming: when dataZoom is applied and its ends
|
|
// (i.e., `zoomMM`) do not reach 0% or 100%, the axis ends should exactly respect to the dataZoom
|
|
// ends, and shapes are clipped if overflowing.
|
|
var scaleExtentExpanded = scaleExtent.slice();
|
|
if (!rawExtentResult.zoomFixMM[0]) {
|
|
scaleExtentExpanded[0] = mathMin(scaleExtentExpanded[0], scale.transformOut(scale.transformIn(scaleExtentExpanded[0], null) + linearSupplement[0], null));
|
|
}
|
|
if (!rawExtentResult.zoomFixMM[1]) {
|
|
scaleExtentExpanded[1] = mathMax(scaleExtentExpanded[1], scale.transformOut(scale.transformIn(scaleExtentExpanded[1], null) + linearSupplement[1], null));
|
|
}
|
|
if (scaleExtentExpanded[0] < scaleExtent[0] || scaleExtentExpanded[1] > scaleExtent[1]) {
|
|
scale.setExtent2(SCALE_EXTENT_KIND_MAPPING, scaleExtentExpanded[0], scaleExtentExpanded[1]);
|
|
}
|
|
}
|
|
// NOTE: since currently `SCALE_EXTENT_KIND_MAPPING` is never required to be displayed, we
|
|
// do not need to find a proper precision for that. But if it is required in the future, We
|
|
// can use `getAcceptableTickPrecision` to find a proper precision.
|
|
} |