Spaces:
Running
Running
| /* | |
| * 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 repeatedly 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 | |
| ) { | |
| // Should 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) { | |
| // For mobile device, 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; | |