blob: 0d8603c55cac7f8f7c26d35236225bd547b5a2be [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// TODO depends on DataZoom and Brush
import * as zrUtil from 'zrender/src/core/util';
import BrushController, { BrushControllerEvents, BrushDimensionMinMax } from '../../helper/BrushController';
import BrushTargetManager, { BrushTargetInfoCartesian2D } from '../../helper/BrushTargetManager';
import * as history from '../../dataZoom/history';
import sliderMove from '../../helper/sliderMove';
import {
ToolboxFeature,
ToolboxFeatureModel,
ToolboxFeatureOption
} from '../featureManager';
import GlobalModel from '../../../model/Global';
import ExtensionAPI from '../../../core/ExtensionAPI';
import { Payload, Dictionary, ComponentOption, ItemStyleOption } from '../../../util/types';
import Cartesian2D from '../../../coord/cartesian/Cartesian2D';
import CartesianAxisModel from '../../../coord/cartesian/AxisModel';
import DataZoomModel from '../../dataZoom/DataZoomModel';
import {
DataZoomPayloadBatchItem, DataZoomAxisDimension
} from '../../dataZoom/helper';
import {
ModelFinderObject, ModelFinderIndexQuery, makeInternalComponentId,
ModelFinderIdQuery, parseFinder, ParsedModelFinderKnown
} from '../../../util/model';
import ToolboxModel from '../ToolboxModel';
import { registerInternalOptionCreator } from '../../../model/internalComponentCreator';
import ComponentModel from '../../../model/Component';
const each = zrUtil.each;
const DATA_ZOOM_ID_BASE = makeInternalComponentId('toolbox-dataZoom_');
const ICON_TYPES = ['zoom', 'back'] as const;
type IconType = typeof ICON_TYPES[number];
export interface ToolboxDataZoomFeatureOption extends ToolboxFeatureOption {
type?: IconType[]
icon?: {[key in IconType]?: string}
title?: {[key in IconType]?: string}
// TODO: TYPE Use type in dataZoom
filterMode?: 'filter' | 'weakFilter' | 'empty' | 'none'
// Backward compat: false means 'none'
xAxisIndex?: ModelFinderIndexQuery
yAxisIndex?: ModelFinderIndexQuery
xAxisId?: ModelFinderIdQuery
yAxisId?: ModelFinderIdQuery,
brushStyle?: ItemStyleOption
}
type ToolboxDataZoomFeatureModel = ToolboxFeatureModel<ToolboxDataZoomFeatureOption>;
class DataZoomFeature extends ToolboxFeature<ToolboxDataZoomFeatureOption> {
brushController: BrushController;
isZoomActive: boolean;
render(
featureModel: ToolboxDataZoomFeatureModel,
ecModel: GlobalModel,
api: ExtensionAPI,
payload: Payload
) {
if (!this.brushController) {
this.brushController = new BrushController(api.getZr());
this.brushController.on('brush', zrUtil.bind(this._onBrush, this))
.mount();
}
updateZoomBtnStatus(featureModel, ecModel, this, payload, api);
updateBackBtnStatus(featureModel, ecModel);
}
onclick(
ecModel: GlobalModel,
api: ExtensionAPI,
type: IconType
) {
handlers[type].call(this);
}
remove(
ecModel: GlobalModel,
api: ExtensionAPI
) {
this.brushController.unmount();
}
dispose(
ecModel: GlobalModel,
api: ExtensionAPI
) {
this.brushController.dispose();
}
private _onBrush(eventParam: BrushControllerEvents['brush']): void {
const areas = eventParam.areas;
if (!eventParam.isEnd || !areas.length) {
return;
}
const snapshot: history.DataZoomStoreSnapshot = {};
const ecModel = this.ecModel;
this.brushController.updateCovers([]); // remove cover
const brushTargetManager = new BrushTargetManager(
makeAxisFinder(this.model),
ecModel,
{include: ['grid']}
);
brushTargetManager.matchOutputRanges(areas, ecModel, function (area, coordRange, coordSys: Cartesian2D) {
if (coordSys.type !== 'cartesian2d') {
return;
}
const brushType = area.brushType;
if (brushType === 'rect') {
setBatch('x', coordSys, (coordRange as BrushDimensionMinMax[])[0]);
setBatch('y', coordSys, (coordRange as BrushDimensionMinMax[])[1]);
}
else {
setBatch(
({lineX: 'x', lineY: 'y'} as const)[brushType as 'lineX' | 'lineY'],
coordSys,
coordRange as BrushDimensionMinMax
);
}
});
history.push(ecModel, snapshot);
this._dispatchZoomAction(snapshot);
function setBatch(dimName: DataZoomAxisDimension, coordSys: Cartesian2D, minMax: number[]) {
const axis = coordSys.getAxis(dimName);
const axisModel = axis.model;
const dataZoomModel = findDataZoom(dimName, axisModel, ecModel);
// Restrict range.
const minMaxSpan = dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan();
if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
minMax = sliderMove(
0, minMax.slice(), axis.scale.getExtent(), 0,
minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan
);
}
dataZoomModel && (snapshot[dataZoomModel.id] = {
dataZoomId: dataZoomModel.id,
startValue: minMax[0],
endValue: minMax[1]
});
}
function findDataZoom(
dimName: DataZoomAxisDimension, axisModel: CartesianAxisModel, ecModel: GlobalModel
): DataZoomModel {
let found;
ecModel.eachComponent({mainType: 'dataZoom', subType: 'select'}, function (dzModel: DataZoomModel) {
const has = dzModel.getAxisModel(dimName, axisModel.componentIndex);
has && (found = dzModel);
});
return found;
}
};
/**
* @internal
*/
_dispatchZoomAction(snapshot: history.DataZoomStoreSnapshot): void {
const batch: DataZoomPayloadBatchItem[] = [];
// Convert from hash map to array.
each(snapshot, function (batchItem, dataZoomId) {
batch.push(zrUtil.clone(batchItem));
});
batch.length && this.api.dispatchAction({
type: 'dataZoom',
from: this.uid,
batch: batch
});
}
static getDefaultOption(ecModel: GlobalModel) {
const defaultOption: ToolboxDataZoomFeatureOption = {
show: true,
filterMode: 'filter',
// Icon group
icon: {
zoom: 'M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1',
back: 'M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26'
},
// `zoom`, `back`
title: ecModel.getLocale(['toolbox', 'dataZoom', 'title']),
brushStyle: {
borderWidth: 0,
color: 'rgba(210,219,238,0.2)'
}
};
return defaultOption;
}
}
const handlers: { [key in IconType]: (this: DataZoomFeature) => void } = {
zoom: function () {
const nextActive = !this.isZoomActive;
this.api.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: nextActive
});
},
back: function () {
this._dispatchZoomAction(history.pop(this.ecModel));
}
};
function makeAxisFinder(dzFeatureModel: ToolboxDataZoomFeatureModel): ModelFinderObject {
const setting = {
xAxisIndex: dzFeatureModel.get('xAxisIndex', true),
yAxisIndex: dzFeatureModel.get('yAxisIndex', true),
xAxisId: dzFeatureModel.get('xAxisId', true),
yAxisId: dzFeatureModel.get('yAxisId', true)
} as ModelFinderObject;
// If both `xAxisIndex` `xAxisId` not set, it means 'all'.
// If both `yAxisIndex` `yAxisId` not set, it means 'all'.
// Some old cases set like this below to close yAxis control but leave xAxis control:
// `{ feature: { dataZoom: { yAxisIndex: false } }`.
if (setting.xAxisIndex == null && setting.xAxisId == null) {
setting.xAxisIndex = 'all';
}
if (setting.yAxisIndex == null && setting.yAxisId == null) {
setting.yAxisIndex = 'all';
}
return setting;
}
function updateBackBtnStatus(
featureModel: ToolboxDataZoomFeatureModel,
ecModel: GlobalModel
) {
featureModel.setIconStatus(
'back',
history.count(ecModel) > 1 ? 'emphasis' : 'normal'
);
}
function updateZoomBtnStatus(
featureModel: ToolboxDataZoomFeatureModel,
ecModel: GlobalModel,
view: DataZoomFeature,
payload: Payload,
api: ExtensionAPI
) {
let zoomActive = view.isZoomActive;
if (payload && payload.type === 'takeGlobalCursor') {
zoomActive = payload.key === 'dataZoomSelect'
? payload.dataZoomSelectActive : false;
}
view.isZoomActive = zoomActive;
featureModel.setIconStatus('zoom', zoomActive ? 'emphasis' : 'normal');
const brushTargetManager = new BrushTargetManager(
makeAxisFinder(featureModel),
ecModel,
{include: ['grid']}
);
const panels = brushTargetManager.makePanelOpts(api, function (targetInfo: BrushTargetInfoCartesian2D) {
return (targetInfo.xAxisDeclared && !targetInfo.yAxisDeclared)
? 'lineX'
: (!targetInfo.xAxisDeclared && targetInfo.yAxisDeclared)
? 'lineY'
: 'rect';
});
view.brushController
.setPanels(panels)
.enableBrush(
(zoomActive && panels.length)
? {
brushType: 'auto',
brushStyle: featureModel.getModel('brushStyle').getItemStyle()
}
: false
);
}
registerInternalOptionCreator('dataZoom', function (ecModel: GlobalModel): ComponentOption[] {
const toolboxModel = ecModel.getComponent('toolbox', 0) as ToolboxModel;
if (!toolboxModel) {
return;
}
const dzFeatureModel = toolboxModel.getModel(['feature', 'dataZoom'] as any) as ToolboxDataZoomFeatureModel;
const dzOptions = [] as ComponentOption[];
const finder = makeAxisFinder(dzFeatureModel);
const finderResult = parseFinder(ecModel, finder) as ParsedModelFinderKnown;
each(finderResult.xAxisModels, axisModel => buildInternalOptions(axisModel, 'xAxis', 'xAxisIndex'));
each(finderResult.yAxisModels, axisModel => buildInternalOptions(axisModel, 'yAxis', 'yAxisIndex'));
function buildInternalOptions(
axisModel: ComponentModel,
axisMainType: 'xAxis' | 'yAxis',
axisIndexPropName: 'xAxisIndex' | 'yAxisIndex'
) {
const axisIndex = axisModel.componentIndex;
const newOpt = {
type: 'select',
$fromToolbox: true,
// Default to be filter
filterMode: dzFeatureModel.get('filterMode', true) || 'filter',
// Id for merge mapping.
id: DATA_ZOOM_ID_BASE + axisMainType + axisIndex
} as Dictionary<unknown>;
newOpt[axisIndexPropName] = axisIndex;
dzOptions.push(newOpt);
}
return dzOptions;
});
export default DataZoomFeature;