/*
|
* Licensed to the Apache Software Foundation (ASF) under one
|
* or more contributor license agreements. See the NOTICE file
|
* distributed with this work for additional information
|
* regarding copyright ownership. The ASF licenses this file
|
* to you under the Apache License, Version 2.0 (the
|
* "License"); you may not use this file except in compliance
|
* with the License. You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing,
|
* software distributed under the License is distributed on an
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
* KIND, either express or implied. See the License for the
|
* specific language governing permissions and limitations
|
* under the License.
|
*/
|
|
import { each, createHashMap, merge, HashMap, assert } from 'zrender/src/core/util';
|
import AxisProxy from './AxisProxy';
|
import ComponentModel from '../../model/Component';
|
import {
|
LayoutOrient,
|
ComponentOption,
|
LabelOption
|
} from '../../util/types';
|
import Model from '../../model/Model';
|
import GlobalModel from '../../model/Global';
|
import { AxisBaseModel } from '../../coord/AxisBaseModel';
|
import {
|
getAxisMainType, DATA_ZOOM_AXIS_DIMENSIONS, DataZoomAxisDimension
|
} from './helper';
|
import SingleAxisModel from '../../coord/single/AxisModel';
|
import { MULTIPLE_REFERRING, SINGLE_REFERRING } from '../../util/model';
|
|
|
export interface DataZoomOption extends ComponentOption {
|
|
mainType?: 'dataZoom'
|
|
/**
|
* Default auto by axisIndex
|
*/
|
orient?: LayoutOrient
|
|
/**
|
* Default the first horizontal category axis.
|
*/
|
xAxisIndex?: number | number[]
|
xAxisId?: string | string[]
|
|
/**
|
* Default the first vertical category axis.
|
*/
|
yAxisIndex?: number | number[]
|
yAxisId?: string | string[]
|
|
radiusAxisIndex?: number | number[]
|
radiusAxisId?: string | string[]
|
angleAxisIndex?: number | number[]
|
angleAxisId?: string | string[]
|
|
singleAxisIndex?: number | number[]
|
singleAxisId?: string | string[]
|
|
/**
|
* Possible values: 'filter' or 'empty' or 'weakFilter'.
|
* 'filter': data items which are out of window will be removed. This option is
|
* applicable when filtering outliers. For each data item, it will be
|
* filtered if one of the relevant dimensions is out of the window.
|
* 'weakFilter': data items which are out of window will be removed. This option
|
* is applicable when filtering outliers. For each data item, it will be
|
* filtered only if all of the relevant dimensions are out of the same
|
* side of the window.
|
* 'empty': data items which are out of window will be set to empty.
|
* This option is applicable when user should not neglect
|
* that there are some data items out of window.
|
* 'none': Do not filter.
|
* Taking line chart as an example, line will be broken in
|
* the filtered points when filterModel is set to 'empty', but
|
* be connected when set to 'filter'.
|
*/
|
filterMode?: 'filter' | 'weakFilter' | 'empty' | 'none'
|
|
/**
|
* Dispatch action by the fixed rate, avoid frequency.
|
* default 100. Do not throttle when use null/undefined.
|
* If animation === true and animationDurationUpdate > 0,
|
* default value is 100, otherwise 20.
|
*/
|
throttle?: number | null | undefined
|
/**
|
* Start percent. 0 ~ 100
|
*/
|
start?: number
|
/**
|
* End percent. 0 ~ 100
|
*/
|
end?: number
|
/**
|
* Start value. If startValue specified, start is ignored
|
*/
|
startValue?: number
|
/**
|
* End value. If endValue specified, end is ignored.
|
*/
|
endValue?: number
|
/**
|
* Min span percent, 0 - 100
|
* The range of dataZoom can not be smaller than that.
|
*/
|
minSpan?: number
|
/**
|
* Max span percent, 0 - 100
|
* The range of dataZoom can not be larger than that.
|
*/
|
maxSpan?: number
|
|
minValueSpan?: number
|
|
maxValueSpan?: number
|
|
rangeMode?: ['value' | 'percent', 'value' | 'percent']
|
|
realtime?: boolean
|
|
// Available when type is slider
|
textStyle?: LabelOption
|
}
|
|
type RangeOption = Pick<DataZoomOption, 'start' | 'end' | 'startValue' | 'endValue'>;
|
|
export type DataZoomExtendedAxisBaseModel = AxisBaseModel & {
|
__dzAxisProxy: AxisProxy
|
};
|
|
class DataZoomAxisInfo {
|
indexList: number[] = [];
|
indexMap: boolean[] = [];
|
|
add(axisCmptIdx: number) {
|
// Remove duplication.
|
if (!this.indexMap[axisCmptIdx]) {
|
this.indexList.push(axisCmptIdx);
|
this.indexMap[axisCmptIdx] = true;
|
}
|
}
|
}
|
export type DataZoomTargetAxisInfoMap = HashMap<DataZoomAxisInfo, DataZoomAxisDimension>;
|
|
class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends ComponentModel<Opts> {
|
static type = 'dataZoom';
|
type = DataZoomModel.type;
|
|
static dependencies = [
|
'xAxis', 'yAxis', 'radiusAxis', 'angleAxis', 'singleAxis', 'series', 'toolbox'
|
];
|
|
|
static defaultOption: DataZoomOption = {
|
zlevel: 0,
|
z: 4, // Higher than normal component (z: 2).
|
|
filterMode: 'filter',
|
|
start: 0,
|
end: 100
|
};
|
|
private _autoThrottle = true;
|
|
private _orient: LayoutOrient;
|
|
private _targetAxisInfoMap: DataZoomTargetAxisInfoMap;
|
|
private _noTarget: boolean = true;
|
|
/**
|
* It is `[rangeModeForMin, rangeModeForMax]`.
|
* The optional values for `rangeMode`:
|
* + `'value'` mode: the axis extent will always be determined by
|
* `dataZoom.startValue` and `dataZoom.endValue`, despite
|
* how data like and how `axis.min` and `axis.max` are.
|
* + `'percent'` mode: `100` represents 100% of the `[dMin, dMax]`,
|
* where `dMin` is `axis.min` if `axis.min` specified, otherwise `data.extent[0]`,
|
* and `dMax` is `axis.max` if `axis.max` specified, otherwise `data.extent[1]`.
|
* Axis extent will be determined by the result of the percent of `[dMin, dMax]`.
|
*
|
* For example, when users are using dynamic data (update data periodically via `setOption`),
|
* if in `'value`' mode, the window will be kept in a fixed value range despite how
|
* data are appended, while if in `'percent'` mode, whe window range will be changed alone with
|
* the appended data (suppose `axis.min` and `axis.max` are not specified).
|
*/
|
private _rangePropMode: DataZoomOption['rangeMode'] = ['percent', 'percent'];
|
|
/**
|
* @readonly
|
*/
|
settledOption: Opts;
|
|
init(option: Opts, parentModel: Model, ecModel: GlobalModel): void {
|
|
const inputRawOption = retrieveRawOption(option);
|
|
/**
|
* Suppose a "main process" start at the point that model prepared (that is,
|
* model initialized or merged or method called in `action`).
|
* We should keep the `main process` idempotent, that is, given a set of values
|
* on `option`, we get the same result.
|
*
|
* But sometimes, values on `option` will be updated for providing users
|
* a "final calculated value" (`dataZoomProcessor` will do that). Those value
|
* should not be the base/input of the `main process`.
|
*
|
* So in that case we should save and keep the input of the `main process`
|
* separately, called `settledOption`.
|
*
|
* For example, consider the case:
|
* (Step_1) brush zoom the grid by `toolbox.dataZoom`,
|
* where the original input `option.startValue`, `option.endValue` are earsed by
|
* calculated value.
|
* (Step)2) click the legend to hide and show a series,
|
* where the new range is calculated by the earsed `startValue` and `endValue`,
|
* which brings incorrect result.
|
*/
|
this.settledOption = inputRawOption;
|
|
this.mergeDefaultAndTheme(option, ecModel);
|
|
this._doInit(inputRawOption);
|
}
|
|
mergeOption(newOption: Opts): void {
|
const inputRawOption = retrieveRawOption(newOption);
|
|
//FIX #2591
|
merge(this.option, newOption, true);
|
merge(this.settledOption, inputRawOption, true);
|
|
this._doInit(inputRawOption);
|
}
|
|
private _doInit(inputRawOption: Opts): void {
|
const thisOption = this.option;
|
|
// if (!env.canvasSupported) {
|
// thisOption.realtime = false;
|
// }
|
|
this._setDefaultThrottle(inputRawOption);
|
|
this._updateRangeUse(inputRawOption);
|
|
const settledOption = this.settledOption;
|
each([['start', 'startValue'], ['end', 'endValue']] as const, function (names, index) {
|
// start/end has higher priority over startValue/endValue if they
|
// both set, but we should make chart.setOption({endValue: 1000})
|
// effective, rather than chart.setOption({endValue: 1000, end: null}).
|
if (this._rangePropMode[index] === 'value') {
|
thisOption[names[0]] = settledOption[names[0]] = null;
|
}
|
// Otherwise do nothing and use the merge result.
|
}, this);
|
|
this._resetTarget();
|
}
|
|
private _resetTarget() {
|
const optionOrient = this.get('orient', true);
|
const targetAxisIndexMap = this._targetAxisInfoMap = createHashMap<DataZoomAxisInfo, DataZoomAxisDimension>();
|
|
const hasAxisSpecified = this._fillSpecifiedTargetAxis(targetAxisIndexMap);
|
|
if (hasAxisSpecified) {
|
this._orient = optionOrient || this._makeAutoOrientByTargetAxis();
|
}
|
else {
|
this._orient = optionOrient || 'horizontal';
|
this._fillAutoTargetAxisByOrient(targetAxisIndexMap, this._orient);
|
}
|
|
this._noTarget = true;
|
targetAxisIndexMap.each(function (axisInfo) {
|
if (axisInfo.indexList.length) {
|
this._noTarget = false;
|
}
|
}, this);
|
}
|
|
private _fillSpecifiedTargetAxis(targetAxisIndexMap: DataZoomTargetAxisInfoMap): boolean {
|
let hasAxisSpecified = false;
|
|
each(DATA_ZOOM_AXIS_DIMENSIONS, function (axisDim) {
|
const refering = this.getReferringComponents(getAxisMainType(axisDim), MULTIPLE_REFERRING);
|
// When user set axisIndex as a empty array, we think that user specify axisIndex
|
// but do not want use auto mode. Because empty array may be encountered when
|
// some error occured.
|
if (!refering.specified) {
|
return;
|
}
|
hasAxisSpecified = true;
|
const axisInfo = new DataZoomAxisInfo();
|
each(refering.models, function (axisModel) {
|
axisInfo.add(axisModel.componentIndex);
|
});
|
targetAxisIndexMap.set(axisDim, axisInfo);
|
}, this);
|
|
return hasAxisSpecified;
|
}
|
|
private _fillAutoTargetAxisByOrient(targetAxisIndexMap: DataZoomTargetAxisInfoMap, orient: LayoutOrient): void {
|
const ecModel = this.ecModel;
|
let needAuto = true;
|
|
// Find axis that parallel to dataZoom as default.
|
if (needAuto) {
|
const axisDim = orient === 'vertical' ? 'y' : 'x';
|
const axisModels = ecModel.findComponents({ mainType: axisDim + 'Axis' });
|
setParallelAxis(axisModels, axisDim);
|
}
|
// Find axis that parallel to dataZoom as default.
|
if (needAuto) {
|
const axisModels = ecModel.findComponents({
|
mainType: 'singleAxis',
|
filter: (axisModel: SingleAxisModel) => axisModel.get('orient', true) === orient
|
});
|
setParallelAxis(axisModels, 'single');
|
}
|
|
function setParallelAxis(axisModels: ComponentModel[], axisDim: DataZoomAxisDimension): void {
|
// At least use the first parallel axis as the target axis.
|
const axisModel = axisModels[0];
|
if (!axisModel) {
|
return;
|
}
|
|
const axisInfo = new DataZoomAxisInfo();
|
axisInfo.add(axisModel.componentIndex);
|
targetAxisIndexMap.set(axisDim, axisInfo);
|
needAuto = false;
|
|
// Find parallel axes in the same grid.
|
if (axisDim === 'x' || axisDim === 'y') {
|
const gridModel = axisModel.getReferringComponents('grid', SINGLE_REFERRING).models[0];
|
gridModel && each(axisModels, function (axModel) {
|
if (axisModel.componentIndex !== axModel.componentIndex
|
&& gridModel === axModel.getReferringComponents('grid', SINGLE_REFERRING).models[0]
|
) {
|
axisInfo.add(axModel.componentIndex);
|
}
|
});
|
}
|
}
|
|
if (needAuto) {
|
// If no parallel axis, find the first category axis as default. (Also consider polar).
|
each(DATA_ZOOM_AXIS_DIMENSIONS, function (axisDim) {
|
if (!needAuto) {
|
return;
|
}
|
const axisModels = ecModel.findComponents({
|
mainType: getAxisMainType(axisDim),
|
filter: (axisModel: SingleAxisModel) => axisModel.get('type', true) === 'category'
|
});
|
if (axisModels[0]) {
|
const axisInfo = new DataZoomAxisInfo();
|
axisInfo.add(axisModels[0].componentIndex);
|
targetAxisIndexMap.set(axisDim, axisInfo);
|
needAuto = false;
|
}
|
}, this);
|
}
|
}
|
|
private _makeAutoOrientByTargetAxis(): LayoutOrient {
|
let dim: string;
|
|
// Find the first axis
|
this.eachTargetAxis(function (axisDim) {
|
!dim && (dim = axisDim);
|
}, this);
|
|
return dim === 'y' ? 'vertical' : 'horizontal';
|
}
|
|
private _setDefaultThrottle(inputRawOption: DataZoomOption): void {
|
// When first time user set throttle, auto throttle ends.
|
if (inputRawOption.hasOwnProperty('throttle')) {
|
this._autoThrottle = false;
|
}
|
if (this._autoThrottle) {
|
const globalOption = this.ecModel.option;
|
this.option.throttle = (
|
globalOption.animation && globalOption.animationDurationUpdate > 0
|
) ? 100 : 20;
|
}
|
}
|
|
private _updateRangeUse(inputRawOption: RangeOption): void {
|
const rangePropMode = this._rangePropMode;
|
const rangeModeInOption = this.get('rangeMode');
|
|
each([['start', 'startValue'], ['end', 'endValue']] as const, function (names, index) {
|
const percentSpecified = inputRawOption[names[0]] != null;
|
const valueSpecified = inputRawOption[names[1]] != null;
|
if (percentSpecified && !valueSpecified) {
|
rangePropMode[index] = 'percent';
|
}
|
else if (!percentSpecified && valueSpecified) {
|
rangePropMode[index] = 'value';
|
}
|
else if (rangeModeInOption) {
|
rangePropMode[index] = rangeModeInOption[index];
|
}
|
else if (percentSpecified) { // percentSpecified && valueSpecified
|
rangePropMode[index] = 'percent';
|
}
|
// else remain its original setting.
|
});
|
}
|
|
noTarget(): boolean {
|
return this._noTarget;
|
}
|
|
getFirstTargetAxisModel(): AxisBaseModel {
|
let firstAxisModel: AxisBaseModel;
|
this.eachTargetAxis(function (axisDim, axisIndex) {
|
if (firstAxisModel == null) {
|
firstAxisModel = this.ecModel.getComponent(
|
getAxisMainType(axisDim), axisIndex
|
) as AxisBaseModel;
|
}
|
}, this);
|
|
return firstAxisModel;
|
}
|
|
/**
|
* @param {Function} callback param: axisModel, dimNames, axisIndex, dataZoomModel, ecModel
|
*/
|
eachTargetAxis<Ctx>(
|
callback: (
|
this: Ctx,
|
axisDim: DataZoomAxisDimension,
|
axisIndex: number
|
) => void,
|
context?: Ctx
|
): void {
|
this._targetAxisInfoMap.each(function (axisInfo, axisDim) {
|
each(axisInfo.indexList, function (axisIndex) {
|
callback.call(context, axisDim, axisIndex);
|
});
|
});
|
}
|
|
/**
|
* @return If not found, return null/undefined.
|
*/
|
getAxisProxy(axisDim: DataZoomAxisDimension, axisIndex: number): AxisProxy {
|
const axisModel = this.getAxisModel(axisDim, axisIndex);
|
if (axisModel) {
|
return (axisModel as DataZoomExtendedAxisBaseModel).__dzAxisProxy;
|
}
|
}
|
|
/**
|
* @return If not found, return null/undefined.
|
*/
|
getAxisModel(axisDim: DataZoomAxisDimension, axisIndex: number): AxisBaseModel {
|
if (__DEV__) {
|
assert(axisDim && axisIndex != null);
|
}
|
const axisInfo = this._targetAxisInfoMap.get(axisDim);
|
if (axisInfo && axisInfo.indexMap[axisIndex]) {
|
return this.ecModel.getComponent(getAxisMainType(axisDim), axisIndex) as AxisBaseModel;
|
}
|
}
|
|
/**
|
* If not specified, set to undefined.
|
*/
|
setRawRange(opt: RangeOption): void {
|
const thisOption = this.option;
|
const settledOption = this.settledOption;
|
each([['start', 'startValue'], ['end', 'endValue']] as const, function (names) {
|
// Consider the pair <start, startValue>:
|
// If one has value and the other one is `null/undefined`, we both set them
|
// to `settledOption`. This strategy enables the feature to clear the original
|
// value in `settledOption` to `null/undefined`.
|
// But if both of them are `null/undefined`, we do not set them to `settledOption`
|
// and keep `settledOption` with the original value. This strategy enables users to
|
// only set <end or endValue> but not set <start or startValue> when calling
|
// `dispatchAction`.
|
// The pair <end, endValue> is treated in the same way.
|
if (opt[names[0]] != null || opt[names[1]] != null) {
|
thisOption[names[0]] = settledOption[names[0]] = opt[names[0]];
|
thisOption[names[1]] = settledOption[names[1]] = opt[names[1]];
|
}
|
}, this);
|
|
this._updateRangeUse(opt);
|
}
|
|
setCalculatedRange(opt: RangeOption): void {
|
const option = this.option;
|
each(['start', 'startValue', 'end', 'endValue'] as const, function (name) {
|
option[name] = opt[name];
|
});
|
}
|
|
getPercentRange(): number[] {
|
const axisProxy = this.findRepresentativeAxisProxy();
|
if (axisProxy) {
|
return axisProxy.getDataPercentWindow();
|
}
|
}
|
|
/**
|
* For example, chart.getModel().getComponent('dataZoom').getValueRange('y', 0);
|
*
|
* @return [startValue, endValue] value can only be '-' or finite number.
|
*/
|
getValueRange(axisDim: DataZoomAxisDimension, axisIndex: number): number[] {
|
if (axisDim == null && axisIndex == null) {
|
const axisProxy = this.findRepresentativeAxisProxy();
|
if (axisProxy) {
|
return axisProxy.getDataValueWindow();
|
}
|
}
|
else {
|
return this.getAxisProxy(axisDim, axisIndex).getDataValueWindow();
|
}
|
}
|
|
/**
|
* @param axisModel If axisModel given, find axisProxy
|
* corresponding to the axisModel
|
*/
|
findRepresentativeAxisProxy(axisModel?: AxisBaseModel): AxisProxy {
|
if (axisModel) {
|
return (axisModel as DataZoomExtendedAxisBaseModel).__dzAxisProxy;
|
}
|
|
// Find the first hosted axisProxy
|
let firstProxy;
|
const axisDimList = this._targetAxisInfoMap.keys();
|
for (let i = 0; i < axisDimList.length; i++) {
|
const axisDim = axisDimList[i];
|
const axisInfo = this._targetAxisInfoMap.get(axisDim);
|
for (let j = 0; j < axisInfo.indexList.length; j++) {
|
const proxy = this.getAxisProxy(axisDim, axisInfo.indexList[j]);
|
if (proxy.hostedBy(this)) {
|
return proxy;
|
}
|
if (!firstProxy) {
|
firstProxy = proxy;
|
}
|
}
|
}
|
|
// If no hosted proxy found, still need to return a proxy.
|
// This case always happens in toolbox dataZoom, where axes are all hosted by
|
// other dataZooms.
|
return firstProxy;
|
}
|
|
getRangePropMode(): DataZoomModel['_rangePropMode'] {
|
return this._rangePropMode.slice() as DataZoomModel['_rangePropMode'];
|
}
|
|
getOrient(): LayoutOrient {
|
if (__DEV__) {
|
// Should not be called before initialized.
|
assert(this._orient);
|
}
|
return this._orient;
|
}
|
|
}
|
/**
|
* Retrieve the those raw params from option, which will be cached separately.
|
* becasue they will be overwritten by normalized/calculated values in the main
|
* process.
|
*/
|
function retrieveRawOption<T extends DataZoomOption>(option: T) {
|
const ret = {} as T;
|
each(
|
['start', 'end', 'startValue', 'endValue', 'throttle'] as const,
|
function (name) {
|
option.hasOwnProperty(name) && (ret[name] = option[name]);
|
}
|
);
|
return ret;
|
}
|
|
export default DataZoomModel;
|