echarts BaseAxisPointer 源码
echarts BaseAxisPointer 代码
文件路径:/src/component/axisPointer/BaseAxisPointer.ts
/*
* 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 zrUtil from 'zrender/src/core/util';
import * as graphic from '../../util/graphic';
import * as axisPointerModelHelper from './modelHelper';
import * as eventTool from 'zrender/src/core/event';
import * as throttleUtil from '../../util/throttle';
import {makeInner} from '../../util/model';
import { AxisPointer } from './AxisPointer';
import { AxisBaseModel } from '../../coord/AxisBaseModel';
import ExtensionAPI from '../../core/ExtensionAPI';
import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable';
import Element from 'zrender/src/Element';
import { VerticalAlign, HorizontalAlign, CommonAxisPointerOption } from '../../util/types';
import { PathProps } from 'zrender/src/graphic/Path';
import Model from '../../model/Model';
import { TextProps } from 'zrender/src/graphic/Text';
const inner = makeInner<{
lastProp?: DisplayableProps
labelEl?: graphic.Text
pointerEl?: Displayable
}, Element>();
const clone = zrUtil.clone;
const bind = zrUtil.bind;
type Icon = ReturnType<typeof graphic.createIcon>;
interface Transform {
x: number,
y: number,
rotation: number
}
type AxisValue = CommonAxisPointerOption['value'];
// Not use top level axisPointer model
type AxisPointerModel = Model<CommonAxisPointerOption>;
interface BaseAxisPointer {
/**
* Should be implemenented by sub-class if support `handle`.
*/
getHandleTransform(value: AxisValue, axisModel: AxisBaseModel, axisPointerModel: AxisPointerModel): Transform
/**
* * Should be implemenented by sub-class if support `handle`.
*/
updateHandleTransform(
transform: Transform,
delta: number[],
axisModel: AxisBaseModel,
axisPointerModel: AxisPointerModel
): Transform & {
cursorPoint: number[]
tooltipOption?: {
verticalAlign?: VerticalAlign
align?: HorizontalAlign
}
}
}
export interface AxisPointerElementOptions {
graphicKey: string
pointer: PathProps & {
type: 'Line' | 'Rect' | 'Circle' | 'Sector'
}
label: TextProps
}
/**
* Base axis pointer class in 2D.
*/
class BaseAxisPointer implements AxisPointer {
private _group: graphic.Group;
private _lastGraphicKey: string;
private _handle: Icon;
private _dragging = false;
private _lastValue: AxisValue;
private _lastStatus: CommonAxisPointerOption['status'];
private _payloadInfo: ReturnType<BaseAxisPointer['updateHandleTransform']>;
/**
* If have transition animation
*/
private _moveAnimation: boolean;
private _axisModel: AxisBaseModel;
private _axisPointerModel: AxisPointerModel;
private _api: ExtensionAPI;
/**
* In px, arbitrary value. Do not set too small,
* no animation is ok for most cases.
*/
protected animationThreshold = 15;
/**
* @implement
*/
render(axisModel: AxisBaseModel, axisPointerModel: AxisPointerModel, api: ExtensionAPI, forceRender?: boolean) {
const value = axisPointerModel.get('value');
const status = axisPointerModel.get('status');
// Bind them to `this`, not in closure, otherwise they will not
// be replaced when user calling setOption in not merge mode.
this._axisModel = axisModel;
this._axisPointerModel = axisPointerModel;
this._api = api;
// Optimize: `render` will be called repeatly during mouse move.
// So it is power consuming if performing `render` each time,
// especially on mobile device.
if (!forceRender
&& this._lastValue === value
&& this._lastStatus === status
) {
return;
}
this._lastValue = value;
this._lastStatus = status;
let group = this._group;
const handle = this._handle;
if (!status || status === 'hide') {
// Do not clear here, for animation better.
group && group.hide();
handle && handle.hide();
return;
}
group && group.show();
handle && handle.show();
// Otherwise status is 'show'
const elOption = {} as AxisPointerElementOptions;
this.makeElOption(elOption, value, axisModel, axisPointerModel, api);
// Enable change axis pointer type.
const graphicKey = elOption.graphicKey;
if (graphicKey !== this._lastGraphicKey) {
this.clear(api);
}
this._lastGraphicKey = graphicKey;
const moveAnimation = this._moveAnimation =
this.determineAnimation(axisModel, axisPointerModel);
if (!group) {
group = this._group = new graphic.Group();
this.createPointerEl(group, elOption, axisModel, axisPointerModel);
this.createLabelEl(group, elOption, axisModel, axisPointerModel);
api.getZr().add(group);
}
else {
const doUpdateProps = zrUtil.curry(updateProps, axisPointerModel, moveAnimation);
this.updatePointerEl(group, elOption, doUpdateProps);
this.updateLabelEl(group, elOption, doUpdateProps, axisPointerModel);
}
updateMandatoryProps(group, axisPointerModel, true);
this._renderHandle(value);
}
/**
* @implement
*/
remove(api: ExtensionAPI) {
this.clear(api);
}
/**
* @implement
*/
dispose(api: ExtensionAPI) {
this.clear(api);
}
/**
* @protected
*/
determineAnimation(axisModel: AxisBaseModel, axisPointerModel: AxisPointerModel): boolean {
const animation = axisPointerModel.get('animation');
const axis = axisModel.axis;
const isCategoryAxis = axis.type === 'category';
const useSnap = axisPointerModel.get('snap');
// Value axis without snap always do not snap.
if (!useSnap && !isCategoryAxis) {
return false;
}
if (animation === 'auto' || animation == null) {
const animationThreshold = this.animationThreshold;
if (isCategoryAxis && axis.getBandWidth() > animationThreshold) {
return true;
}
// It is important to auto animation when snap used. Consider if there is
// a dataZoom, animation will be disabled when too many points exist, while
// it will be enabled for better visual effect when little points exist.
if (useSnap) {
const seriesDataCount = axisPointerModelHelper.getAxisInfo(axisModel).seriesDataCount;
const axisExtent = axis.getExtent();
// Approximate band width
return Math.abs(axisExtent[0] - axisExtent[1]) / seriesDataCount > animationThreshold;
}
return false;
}
return animation === true;
}
/**
* add {pointer, label, graphicKey} to elOption
* @protected
*/
makeElOption(
elOption: AxisPointerElementOptions,
value: AxisValue,
axisModel: AxisBaseModel,
axisPointerModel: AxisPointerModel,
api: ExtensionAPI
) {
// Shoule be implemenented by sub-class.
}
/**
* @protected
*/
createPointerEl(
group: graphic.Group,
elOption: AxisPointerElementOptions,
axisModel: AxisBaseModel,
axisPointerModel: AxisPointerModel
) {
const pointerOption = elOption.pointer;
if (pointerOption) {
const pointerEl = inner(group).pointerEl = new graphic[pointerOption.type](
clone(elOption.pointer)
);
group.add(pointerEl);
}
}
/**
* @protected
*/
createLabelEl(
group: graphic.Group,
elOption: AxisPointerElementOptions,
axisModel: AxisBaseModel,
axisPointerModel: AxisPointerModel
) {
if (elOption.label) {
const labelEl = inner(group).labelEl = new graphic.Text(
clone(elOption.label)
);
group.add(labelEl);
updateLabelShowHide(labelEl, axisPointerModel);
}
}
/**
* @protected
*/
updatePointerEl(
group: graphic.Group,
elOption: AxisPointerElementOptions,
updateProps: (el: Element, props: PathProps) => void
) {
const pointerEl = inner(group).pointerEl;
if (pointerEl && elOption.pointer) {
pointerEl.setStyle(elOption.pointer.style);
updateProps(pointerEl, {shape: elOption.pointer.shape});
}
}
/**
* @protected
*/
updateLabelEl(
group: graphic.Group,
elOption: AxisPointerElementOptions,
updateProps: (el: Element, props: PathProps) => void,
axisPointerModel: AxisPointerModel
) {
const labelEl = inner(group).labelEl;
if (labelEl) {
labelEl.setStyle(elOption.label.style);
updateProps(labelEl, {
// Consider text length change in vertical axis, animation should
// be used on shape, otherwise the effect will be weird.
// TODOTODO
// shape: elOption.label.shape,
x: elOption.label.x,
y: elOption.label.y
});
updateLabelShowHide(labelEl, axisPointerModel);
}
}
/**
* @private
*/
_renderHandle(value: AxisValue) {
if (this._dragging || !this.updateHandleTransform) {
return;
}
const axisPointerModel = this._axisPointerModel;
const zr = this._api.getZr();
let handle = this._handle;
const handleModel = axisPointerModel.getModel('handle');
const status = axisPointerModel.get('status');
if (!handleModel.get('show') || !status || status === 'hide') {
handle && zr.remove(handle);
this._handle = null;
return;
}
let isInit;
if (!this._handle) {
isInit = true;
handle = this._handle = graphic.createIcon(
handleModel.get('icon'),
{
cursor: 'move',
draggable: true,
onmousemove(e) {
// Fot mobile devicem, prevent screen slider on the button.
eventTool.stop(e.event);
},
onmousedown: bind(this._onHandleDragMove, this, 0, 0),
drift: bind(this._onHandleDragMove, this),
ondragend: bind(this._onHandleDragEnd, this)
}
);
zr.add(handle);
}
updateMandatoryProps(handle, axisPointerModel, false);
// update style
(handle as graphic.Path).setStyle(handleModel.getItemStyle(null, [
'color', 'borderColor', 'borderWidth', 'opacity',
'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY'
]));
// update position
let handleSize = handleModel.get('size');
if (!zrUtil.isArray(handleSize)) {
handleSize = [handleSize, handleSize];
}
handle.scaleX = handleSize[0] / 2;
handle.scaleY = handleSize[1] / 2;
throttleUtil.createOrUpdate(
this,
'_doDispatchAxisPointer',
handleModel.get('throttle') || 0,
'fixRate'
);
this._moveHandleToValue(value, isInit);
}
private _moveHandleToValue(value: AxisValue, isInit?: boolean) {
updateProps(
this._axisPointerModel,
!isInit && this._moveAnimation,
this._handle,
getHandleTransProps(this.getHandleTransform(
value, this._axisModel, this._axisPointerModel
))
);
}
private _onHandleDragMove(dx: number, dy: number) {
const handle = this._handle;
if (!handle) {
return;
}
this._dragging = true;
// Persistent for throttle.
const trans = this.updateHandleTransform(
getHandleTransProps(handle),
[dx, dy],
this._axisModel,
this._axisPointerModel
);
this._payloadInfo = trans;
handle.stopAnimation();
(handle as graphic.Path).attr(getHandleTransProps(trans));
inner(handle).lastProp = null;
this._doDispatchAxisPointer();
}
/**
* Throttled method.
*/
_doDispatchAxisPointer() {
const handle = this._handle;
if (!handle) {
return;
}
const payloadInfo = this._payloadInfo;
const axisModel = this._axisModel;
this._api.dispatchAction({
type: 'updateAxisPointer',
x: payloadInfo.cursorPoint[0],
y: payloadInfo.cursorPoint[1],
tooltipOption: payloadInfo.tooltipOption,
axesInfo: [{
axisDim: axisModel.axis.dim,
axisIndex: axisModel.componentIndex
}]
});
}
private _onHandleDragEnd() {
this._dragging = false;
const handle = this._handle;
if (!handle) {
return;
}
const value = this._axisPointerModel.get('value');
// Consider snap or categroy axis, handle may be not consistent with
// axisPointer. So move handle to align the exact value position when
// drag ended.
this._moveHandleToValue(value);
// For the effect: tooltip will be shown when finger holding on handle
// button, and will be hidden after finger left handle button.
this._api.dispatchAction({
type: 'hideTip'
});
}
/**
* @private
*/
clear(api: ExtensionAPI) {
this._lastValue = null;
this._lastStatus = null;
const zr = api.getZr();
const group = this._group;
const handle = this._handle;
if (zr && group) {
this._lastGraphicKey = null;
group && zr.remove(group);
handle && zr.remove(handle);
this._group = null;
this._handle = null;
this._payloadInfo = null;
}
throttleUtil.clear(this, '_doDispatchAxisPointer');
}
/**
* @protected
*/
doClear() {
// Implemented by sub-class if necessary.
}
buildLabel(xy: number[], wh: number[], xDimIndex: 0 | 1) {
xDimIndex = xDimIndex || 0;
return {
x: xy[xDimIndex],
y: xy[1 - xDimIndex],
width: wh[xDimIndex],
height: wh[1 - xDimIndex]
};
}
}
function updateProps(
animationModel: AxisPointerModel,
moveAnimation: boolean,
el: Element,
props: DisplayableProps
) {
// Animation optimize.
if (!propsEqual(inner(el).lastProp, props)) {
inner(el).lastProp = props;
moveAnimation
? graphic.updateProps(el, props, animationModel as Model<
// Ignore animation property
Pick<CommonAxisPointerOption, 'animationDurationUpdate' | 'animationEasingUpdate'>
>)
: (el.stopAnimation(), el.attr(props));
}
}
function propsEqual(lastProps: any, newProps: any) {
if (zrUtil.isObject(lastProps) && zrUtil.isObject(newProps)) {
let equals = true;
zrUtil.each(newProps, function (item, key) {
equals = equals && propsEqual(lastProps[key], item);
});
return !!equals;
}
else {
return lastProps === newProps;
}
}
function updateLabelShowHide(labelEl: Element, axisPointerModel: AxisPointerModel) {
labelEl[axisPointerModel.get(['label', 'show']) ? 'show' : 'hide']();
}
function getHandleTransProps(trans: Transform): Transform {
return {
x: trans.x || 0,
y: trans.y || 0,
rotation: trans.rotation || 0
};
}
function updateMandatoryProps(
group: Element,
axisPointerModel: AxisPointerModel,
silent?: boolean
) {
const z = axisPointerModel.get('z');
const zlevel = axisPointerModel.get('zlevel');
group && group.traverse(function (el: Displayable) {
if (el.type !== 'group') {
z != null && (el.z = z);
zlevel != null && (el.zlevel = zlevel);
el.silent = silent;
}
});
}
export default BaseAxisPointer;
相关信息
相关文章
echarts CartesianAxisPointer 源码
0
赞
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦