| /* |
| * 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 pathTool from 'zrender/src/tool/path'; |
| import * as matrix from 'zrender/src/core/matrix'; |
| import * as vector from 'zrender/src/core/vector'; |
| import Path, { PathProps } from 'zrender/src/graphic/Path'; |
| import Transformable, { copyTransform } from 'zrender/src/core/Transformable'; |
| import ZRImage, { ImageStyleProps } from 'zrender/src/graphic/Image'; |
| import Group from 'zrender/src/graphic/Group'; |
| import ZRText from 'zrender/src/graphic/Text'; |
| import Circle from 'zrender/src/graphic/shape/Circle'; |
| import Ellipse from 'zrender/src/graphic/shape/Ellipse'; |
| import Sector from 'zrender/src/graphic/shape/Sector'; |
| import Ring from 'zrender/src/graphic/shape/Ring'; |
| import Polygon from 'zrender/src/graphic/shape/Polygon'; |
| import Polyline from 'zrender/src/graphic/shape/Polyline'; |
| import Rect from 'zrender/src/graphic/shape/Rect'; |
| import Line from 'zrender/src/graphic/shape/Line'; |
| import BezierCurve from 'zrender/src/graphic/shape/BezierCurve'; |
| import Arc from 'zrender/src/graphic/shape/Arc'; |
| import CompoundPath from 'zrender/src/graphic/CompoundPath'; |
| import LinearGradient from 'zrender/src/graphic/LinearGradient'; |
| import RadialGradient from 'zrender/src/graphic/RadialGradient'; |
| import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect'; |
| import OrientedBoundingRect from 'zrender/src/core/OrientedBoundingRect'; |
| import Point from 'zrender/src/core/Point'; |
| import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable'; |
| import * as subPixelOptimizeUtil from 'zrender/src/graphic/helper/subPixelOptimize'; |
| import { Dictionary } from 'zrender/src/core/types'; |
| import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable'; |
| import Element from 'zrender/src/Element'; |
| import Model from '../model/Model'; |
| import { |
| AnimationOptionMixin, |
| ZRRectLike, |
| ZRStyleProps, |
| CommonTooltipOption, |
| ComponentItemTooltipLabelFormatterParams, |
| NullUndefined, |
| ComponentOption, |
| Payload |
| } from './types'; |
| import { |
| extend, |
| isArrayLike, |
| map, |
| defaults, |
| isString, |
| keys, |
| each, |
| hasOwn, |
| isArray, |
| isNumber, |
| clone, |
| assert, |
| } from 'zrender/src/core/util'; |
| import { getECData } from './innerStore'; |
| import ComponentModel from '../model/Component'; |
| |
| import { |
| updateProps, |
| initProps, |
| removeElement, |
| removeElementWithFadeOut, |
| isElementRemoved |
| } from '../animation/basicTransition'; |
| import { ExtendedElement } from '../core/ExtendedElement'; |
| import { mathMin, mathMax, mathAbs } from './number'; |
| import type ExtensionAPI from '../core/ExtensionAPI'; |
| import type CanvasPainter from 'zrender/src/canvas/Painter'; |
| |
| /** |
| * @deprecated export for compatitable reason |
| */ |
| export {updateProps, initProps, removeElement, removeElementWithFadeOut, isElementRemoved}; |
| |
| |
| const _customShapeMap: Dictionary<{ new(): Path }> = {}; |
| |
| type ExtendShapeOpt = Parameters<typeof Path.extend>[0]; |
| type ExtendShapeReturn = ReturnType<typeof Path.extend>; |
| |
| export const XY = ['x', 'y'] as const; |
| export const WH = ['width', 'height'] as const; |
| |
| /** |
| * NOTICE: Only canvas renderer can set these hoverLayer flags. |
| * @see ElementCommonState['hoverLayer'] |
| */ |
| export const HOVER_LAYER_NO = 0; |
| export const HOVER_LAYER_FROM_THRESHOLD = 1; |
| export const HOVER_LAYER_FOR_INCREMENTAL = 2; |
| |
| /** |
| * Extend shape with parameters |
| */ |
| export function extendShape(opts: ExtendShapeOpt): ExtendShapeReturn { |
| return Path.extend(opts); |
| } |
| |
| const extendPathFromString = pathTool.extendFromString; |
| type SVGPathOption = Parameters<typeof extendPathFromString>[1]; |
| type SVGPathCtor = ReturnType<typeof extendPathFromString>; |
| type SVGPath = InstanceType<SVGPathCtor>; |
| /** |
| * Extend path |
| */ |
| export function extendPath(pathData: string, opts: SVGPathOption): SVGPathCtor { |
| return extendPathFromString(pathData, opts); |
| } |
| |
| /** |
| * Register a user defined shape. |
| * The shape class can be fetched by `getShapeClass` |
| * This method will overwrite the registered shapes, including |
| * the registered built-in shapes, if using the same `name`. |
| * The shape can be used in `custom series` and |
| * `graphic component` by declaring `{type: name}`. |
| * |
| * @param name |
| * @param ShapeClass Can be generated by `extendShape`. |
| */ |
| export function registerShape(name: string, ShapeClass: {new(): Path}) { |
| _customShapeMap[name] = ShapeClass; |
| } |
| |
| /** |
| * Find shape class registered by `registerShape`. Usually used in |
| * fetching user defined shape. |
| * |
| * [Caution]: |
| * (1) This method **MUST NOT be used inside echarts !!!**, unless it is prepared |
| * to use user registered shapes. |
| * Because the built-in shape (see `getBuiltInShape`) will be registered by |
| * `registerShape` by default. That enables users to get both built-in |
| * shapes as well as the shapes belonging to themsleves. But users can overwrite |
| * the built-in shapes by using names like 'circle', 'rect' via calling |
| * `registerShape`. So the echarts inner featrues should not fetch shapes from here |
| * in case that it is overwritten by users, except that some features, like |
| * `custom series`, `graphic component`, do it deliberately. |
| * |
| * (2) In the features like `custom series`, `graphic component`, the user input |
| * `{tpye: 'xxx'}` does not only specify shapes but also specify other graphic |
| * elements like `'group'`, `'text'`, `'image'` or event `'path'`. Those names |
| * are reserved names, that is, if some user registers a shape named `'image'`, |
| * the shape will not be used. If we intending to add some more reserved names |
| * in feature, that might bring break changes (disable some existing user shape |
| * names). But that case probably rarely happens. So we don't make more mechanism |
| * to resolve this issue here. |
| * |
| * @param name |
| * @return The shape class. If not found, return nothing. |
| */ |
| export function getShapeClass(name: string): {new(): Path} { |
| if (_customShapeMap.hasOwnProperty(name)) { |
| return _customShapeMap[name]; |
| } |
| } |
| |
| /** |
| * Create a path element from path data string |
| * @param pathData |
| * @param opts |
| * @param rect |
| * @param layout 'center' or 'cover' default to be cover |
| */ |
| export function makePath( |
| pathData: string, |
| opts: SVGPathOption, |
| rect: ZRRectLike, |
| layout?: 'center' | 'cover' |
| ): SVGPath { |
| const path = pathTool.createFromString(pathData, opts); |
| if (rect) { |
| if (layout === 'center') { |
| rect = centerGraphic(rect, path.getBoundingRect()); |
| } |
| resizePath(path, rect); |
| } |
| return path; |
| } |
| |
| /** |
| * Create a image element from image url |
| * @param imageUrl image url |
| * @param opts options |
| * @param rect constrain rect |
| * @param layout 'center' or 'cover'. Default to be 'cover' |
| */ |
| export function makeImage( |
| imageUrl: string, |
| rect: ZRRectLike, |
| layout?: 'center' | 'cover' |
| ) { |
| const zrImg = new ZRImage({ |
| style: { |
| image: imageUrl, |
| x: rect.x, |
| y: rect.y, |
| width: rect.width, |
| height: rect.height |
| }, |
| onload(img) { |
| if (layout === 'center') { |
| const boundingRect = { |
| width: img.width, |
| height: img.height |
| }; |
| zrImg.setStyle(centerGraphic(rect, boundingRect)); |
| } |
| } |
| }); |
| return zrImg; |
| } |
| |
| /** |
| * Get position of centered element in bounding box. |
| * |
| * @param rect element local bounding box |
| * @param boundingRect constraint bounding box |
| * @return element position containing x, y, width, and height |
| */ |
| function centerGraphic(rect: ZRRectLike, boundingRect: { |
| width: number |
| height: number |
| }): ZRRectLike { |
| // Set rect to center, keep width / height ratio. |
| const aspect = boundingRect.width / boundingRect.height; |
| let width = rect.height * aspect; |
| let height; |
| if (width <= rect.width) { |
| height = rect.height; |
| } |
| else { |
| width = rect.width; |
| height = width / aspect; |
| } |
| const cx = rect.x + rect.width / 2; |
| const cy = rect.y + rect.height / 2; |
| |
| return { |
| x: cx - width / 2, |
| y: cy - height / 2, |
| width: width, |
| height: height |
| }; |
| } |
| |
| export const mergePath = pathTool.mergePath; |
| |
| /** |
| * Resize a path to fit the rect |
| * @param path |
| * @param rect |
| */ |
| export function resizePath(path: SVGPath, rect: ZRRectLike): void { |
| if (!path.applyTransform) { |
| return; |
| } |
| |
| const pathRect = path.getBoundingRect(); |
| |
| const m = pathRect.calculateTransform(rect); |
| |
| path.applyTransform(m); |
| } |
| |
| /** |
| * Sub pixel optimize line for canvas |
| */ |
| export function subPixelOptimizeLine( |
| shape: { |
| x1: number, y1: number, x2: number, y2: number |
| }, |
| lineWidth: number |
| ) { |
| subPixelOptimizeUtil.subPixelOptimizeLine(shape, shape, {lineWidth}); |
| return shape; |
| } |
| |
| /** |
| * Sub pixel optimize rect for canvas |
| */ |
| export function subPixelOptimizeRect( |
| shape: { |
| x: number, y: number, width: number, height: number |
| }, |
| style: { |
| lineWidth?: number |
| } |
| ) { |
| subPixelOptimizeUtil.subPixelOptimizeRect(shape, shape, style); |
| return shape; |
| } |
| |
| /** |
| * Sub pixel optimize for canvas |
| * |
| * @param position Coordinate, such as x, y |
| * @param lineWidth Should be nonnegative integer. |
| * @param positiveOrNegative Default false (negative). |
| * @return Optimized position. |
| */ |
| export const subPixelOptimize = subPixelOptimizeUtil.subPixelOptimize; |
| |
| |
| /** |
| * Get transform matrix of target (param target), |
| * in coordinate of its ancestor (param ancestor) |
| * |
| * @param target |
| * @param [ancestor] |
| */ |
| export function getTransform(target: Transformable, ancestor?: Transformable): matrix.MatrixArray { |
| const mat = matrix.identity([]); |
| |
| while (target && target !== ancestor) { |
| matrix.mul(mat, target.getLocalTransform(), mat); |
| target = target.parent; |
| } |
| |
| return mat; |
| } |
| |
| /** |
| * Apply transform to an vertex. |
| * @param target [x, y] |
| * @param transform Can be: |
| * + Transform matrix: like [1, 0, 0, 1, 0, 0] |
| * + {position, rotation, scale}, the same as `zrender/Transformable`. |
| * @param invert Whether use invert matrix. |
| * @return [x, y] |
| */ |
| export function applyTransform( |
| target: vector.VectorArray, |
| transform: Transformable | matrix.MatrixArray, |
| invert?: boolean |
| ): number[] { |
| if (transform && !isArrayLike(transform)) { |
| transform = Transformable.getLocalTransform(transform); |
| } |
| |
| if (invert) { |
| transform = matrix.invert([], transform as matrix.MatrixArray); |
| } |
| return vector.applyTransform([], target, transform as matrix.MatrixArray); |
| } |
| |
| /** |
| * @param direction 'left' 'right' 'top' 'bottom' |
| * @param transform Transform matrix: like [1, 0, 0, 1, 0, 0] |
| * @param invert Whether use invert matrix. |
| * @return Transformed direction. 'left' 'right' 'top' 'bottom' |
| */ |
| export function transformDirection( |
| direction: 'left' | 'right' | 'top' | 'bottom', |
| transform: matrix.MatrixArray, |
| invert?: boolean |
| ): 'left' | 'right' | 'top' | 'bottom' { |
| |
| // Pick a base, ensure that transform result will not be (0, 0). |
| const hBase = (transform[4] === 0 || transform[5] === 0 || transform[0] === 0) |
| ? 1 : mathAbs(2 * transform[4] / transform[0]); |
| const vBase = (transform[4] === 0 || transform[5] === 0 || transform[2] === 0) |
| ? 1 : mathAbs(2 * transform[4] / transform[2]); |
| |
| let vertex: vector.VectorArray = [ |
| direction === 'left' ? -hBase : direction === 'right' ? hBase : 0, |
| direction === 'top' ? -vBase : direction === 'bottom' ? vBase : 0 |
| ]; |
| |
| vertex = applyTransform(vertex, transform, invert); |
| |
| return mathAbs(vertex[0]) > mathAbs(vertex[1]) |
| ? (vertex[0] > 0 ? 'right' : 'left') |
| : (vertex[1] > 0 ? 'bottom' : 'top'); |
| } |
| |
| function isNotGroup(el: Element): el is Displayable { |
| return !el.isGroup; |
| } |
| function isPath(el: Displayable): el is Path { |
| return (el as Path).shape != null; |
| } |
| /** |
| * Apply group transition animation from g1 to g2. |
| * If no animatableModel, no animation. |
| */ |
| export function groupTransition( |
| g1: Group, |
| g2: Group, |
| animatableModel: Model<AnimationOptionMixin> |
| ) { |
| if (!g1 || !g2) { |
| return; |
| } |
| |
| function getElMap(g: Group) { |
| const elMap: Dictionary<Displayable> = {}; |
| g.traverse(function (el: Element) { |
| if (isNotGroup(el) && el.anid) { |
| elMap[el.anid] = el; |
| } |
| }); |
| return elMap; |
| } |
| function getAnimatableProps(el: Displayable) { |
| const obj: PathProps = { |
| x: el.x, |
| y: el.y, |
| rotation: el.rotation |
| }; |
| if (isPath(el)) { |
| obj.shape = clone(el.shape); |
| } |
| return obj; |
| } |
| const elMap1 = getElMap(g1); |
| |
| g2.traverse(function (el) { |
| if (isNotGroup(el) && el.anid) { |
| const oldEl = elMap1[el.anid]; |
| if (oldEl) { |
| const newProp = getAnimatableProps(el); |
| el.attr(getAnimatableProps(oldEl)); |
| updateProps(el, newProp, animatableModel, getECData(el).dataIndex); |
| } |
| } |
| }); |
| } |
| |
| export function clipPointsByRect(points: vector.VectorArray[], rect: ZRRectLike): number[][] { |
| // FIXME: This way might be incorrect when graphic clipped by a corner |
| // and when element has a border. |
| return map(points, function (point) { |
| let x = point[0]; |
| x = mathMax(x, rect.x); |
| x = mathMin(x, rect.x + rect.width); |
| let y = point[1]; |
| y = mathMax(y, rect.y); |
| y = mathMin(y, rect.y + rect.height); |
| return [x, y]; |
| }); |
| } |
| |
| /** |
| * Return a new clipped rect. If rect size are negative, return undefined. |
| */ |
| export function clipRectByRect(targetRect: ZRRectLike, rect: ZRRectLike): ZRRectLike | undefined { |
| const x = mathMax(targetRect.x, rect.x); |
| const x2 = mathMin(targetRect.x + targetRect.width, rect.x + rect.width); |
| const y = mathMax(targetRect.y, rect.y); |
| const y2 = mathMin(targetRect.y + targetRect.height, rect.y + rect.height); |
| |
| // If the total rect is cliped, nothing, including the border, |
| // should be painted. So return undefined. |
| if (x2 >= x && y2 >= y) { |
| return { |
| x: x, |
| y: y, |
| width: x2 - x, |
| height: y2 - y |
| }; |
| } |
| } |
| |
| export function createIcon( |
| iconStr: string, // Support 'image://' or 'path://' or direct svg path. |
| opt?: Omit<DisplayableProps, 'style'>, |
| rect?: ZRRectLike |
| ): SVGPath | ZRImage { |
| const innerOpts: DisplayableProps = extend({rectHover: true}, opt); |
| const style: ZRStyleProps = innerOpts.style = {strokeNoScale: true}; |
| rect = rect || {x: -1, y: -1, width: 2, height: 2}; |
| |
| if (iconStr) { |
| return iconStr.indexOf('image://') === 0 |
| ? ( |
| (style as ImageStyleProps).image = iconStr.slice(8), |
| defaults(style, rect), |
| new ZRImage(innerOpts) |
| ) |
| : ( |
| makePath( |
| iconStr.replace('path://', ''), |
| innerOpts, |
| rect, |
| 'center' |
| ) |
| ); |
| } |
| } |
| |
| /** |
| * Return `true` if the given line (line `a`) and the given polygon |
| * are intersect. |
| * Note that we do not count colinear as intersect here because no |
| * requirement for that. We could do that if required in future. |
| */ |
| export function linePolygonIntersect( |
| a1x: number, a1y: number, a2x: number, a2y: number, |
| points: vector.VectorArray[] |
| ): boolean { |
| for (let i = 0, p2 = points[points.length - 1]; i < points.length; i++) { |
| const p = points[i]; |
| if (lineLineIntersect(a1x, a1y, a2x, a2y, p[0], p[1], p2[0], p2[1])) { |
| return true; |
| } |
| p2 = p; |
| } |
| } |
| |
| /** |
| * Return `true` if the given two lines (line `a` and line `b`) |
| * are intersect. |
| * Note that we do not count colinear as intersect here because no |
| * requirement for that. We could do that if required in future. |
| */ |
| export function lineLineIntersect( |
| a1x: number, a1y: number, a2x: number, a2y: number, |
| b1x: number, b1y: number, b2x: number, b2y: number |
| ): boolean { |
| // let `vec_m` to be `vec_a2 - vec_a1` and `vec_n` to be `vec_b2 - vec_b1`. |
| const mx = a2x - a1x; |
| const my = a2y - a1y; |
| const nx = b2x - b1x; |
| const ny = b2y - b1y; |
| |
| // `vec_m` and `vec_n` are parallel iff |
| // existing `k` such that `vec_m = k · vec_n`, equivalent to `vec_m X vec_n = 0`. |
| const nmCrossProduct = crossProduct2d(nx, ny, mx, my); |
| if (nearZero(nmCrossProduct)) { |
| return false; |
| } |
| |
| // `vec_m` and `vec_n` are intersect iff |
| // existing `p` and `q` in [0, 1] such that `vec_a1 + p * vec_m = vec_b1 + q * vec_n`, |
| // such that `q = ((vec_a1 - vec_b1) X vec_m) / (vec_n X vec_m)` |
| // and `p = ((vec_a1 - vec_b1) X vec_n) / (vec_n X vec_m)`. |
| const b1a1x = a1x - b1x; |
| const b1a1y = a1y - b1y; |
| const q = crossProduct2d(b1a1x, b1a1y, mx, my) / nmCrossProduct; |
| if (q < 0 || q > 1) { |
| return false; |
| } |
| const p = crossProduct2d(b1a1x, b1a1y, nx, ny) / nmCrossProduct; |
| if (p < 0 || p > 1) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Cross product of 2-dimension vector. |
| */ |
| function crossProduct2d(x1: number, y1: number, x2: number, y2: number) { |
| return x1 * y2 - x2 * y1; |
| } |
| |
| function nearZero(val: number) { |
| return val <= (1e-6) && val >= -(1e-6); |
| } |
| |
| /** |
| * NOTE: |
| * A negative-width/height rect (due to negative margins) is not supported; |
| * it will be clampped to zero width/height. |
| * Although negative-width/height rects can be defined reasonably following the |
| * similar sense in CSS, but they are rarely used, hard to understand and complicated. |
| * |
| * @param rect Assume its width/height >= 0 if existing. |
| * x/y/width/height is allowed to be NaN, |
| * for the case that only x/width or y/height is intended to be computed. |
| * @param delta |
| * If be `number[]`, should be `[top, right, bottom, left]`, |
| * which can be used in padding or margin case. |
| * @see `normalizeCssArray` in `util/format.ts` |
| * If be `number`, it means [delta, delta, delta, delta], |
| * which can be used in lineWidth (borderWith) case, |
| * [NOTICE]: commonly pass lineWidth / 2, following the convention that border is |
| * half inside half outside of the rect. |
| * @param shrinkOrExpand |
| * `true` - shrink if `delta[i]` is positive, commmonly used in `padding` case. |
| * `false` - expand if `delta[i]` is positive, commmonly used in `margin` case. (default) |
| * @param noNegative |
| * `true` - negative `delta[i]` will be clampped to 0. |
| * `false` - No clamp to `delta`. (default). |
| * @return The input `rect`. |
| */ |
| export function expandOrShrinkRect<TRect extends RectLike>( |
| rect: TRect, |
| delta: number[] | number | NullUndefined, |
| shrinkOrExpand: boolean, |
| noNegative: boolean, |
| minSize?: number[] // by default [0, 0]. |
| ): TRect { |
| if (delta == null) { |
| return rect; |
| } |
| else if (isNumber(delta)) { |
| _tmpExpandRectDelta[0] = _tmpExpandRectDelta[1] = _tmpExpandRectDelta[2] = _tmpExpandRectDelta[3] = delta; |
| } |
| else { |
| if (__DEV__) { |
| assert(delta.length === 4); |
| } |
| _tmpExpandRectDelta[0] = delta[0]; |
| _tmpExpandRectDelta[1] = delta[1]; |
| _tmpExpandRectDelta[2] = delta[2]; |
| _tmpExpandRectDelta[3] = delta[3]; |
| } |
| if (noNegative) { |
| _tmpExpandRectDelta[0] = mathMax(0, _tmpExpandRectDelta[0]); |
| _tmpExpandRectDelta[1] = mathMax(0, _tmpExpandRectDelta[1]); |
| _tmpExpandRectDelta[2] = mathMax(0, _tmpExpandRectDelta[2]); |
| _tmpExpandRectDelta[3] = mathMax(0, _tmpExpandRectDelta[3]); |
| } |
| if (shrinkOrExpand) { |
| _tmpExpandRectDelta[0] = -_tmpExpandRectDelta[0]; |
| _tmpExpandRectDelta[1] = -_tmpExpandRectDelta[1]; |
| _tmpExpandRectDelta[2] = -_tmpExpandRectDelta[2]; |
| _tmpExpandRectDelta[3] = -_tmpExpandRectDelta[3]; |
| } |
| expandRectOnOneDimension(rect, _tmpExpandRectDelta, 'x', 'width', 3, 1, minSize && minSize[0] || 0); |
| expandRectOnOneDimension(rect, _tmpExpandRectDelta, 'y', 'height', 0, 2, minSize && minSize[1] || 0); |
| |
| return rect; |
| } |
| const _tmpExpandRectDelta = [0, 0, 0, 0]; |
| function expandRectOnOneDimension( |
| rect: RectLike, |
| delta: number[], |
| xy: 'x' | 'y', |
| wh: 'width' | 'height', |
| ltIdx: 3 | 0, rbIdx: 1 | 2, |
| minSize: number |
| ): void { |
| const deltaSum = delta[rbIdx] + delta[ltIdx]; |
| const oldSize = rect[wh]; |
| rect[wh] += deltaSum; |
| minSize = mathMax(0, mathMin(minSize, oldSize)); |
| if (rect[wh] < minSize) { |
| rect[wh] = minSize; |
| // Try to make the position of the zero rect reasonable in most visual cases. |
| rect[xy] += ( |
| delta[ltIdx] >= 0 ? -delta[ltIdx] |
| : delta[rbIdx] >= 0 ? oldSize + delta[rbIdx] |
| : mathAbs(deltaSum) > 1e-8 ? (oldSize - minSize) * delta[ltIdx] / deltaSum |
| : 0 |
| ); |
| } |
| else { |
| rect[xy] -= delta[ltIdx]; |
| } |
| } |
| |
| export function setTooltipConfig(opt: { |
| el: Element, |
| componentModel: ComponentModel, |
| itemName: string, |
| itemTooltipOption?: string | CommonTooltipOption<unknown> |
| formatterParamsExtra?: Dictionary<unknown> |
| }): void { |
| const itemTooltipOption = opt.itemTooltipOption; |
| const componentModel = opt.componentModel; |
| const itemName = opt.itemName; |
| |
| const itemTooltipOptionObj = isString(itemTooltipOption) |
| ? { formatter: itemTooltipOption } |
| : itemTooltipOption; |
| const mainType = componentModel.mainType; |
| const componentIndex = componentModel.componentIndex; |
| |
| const formatterParams = { |
| componentType: mainType, |
| name: itemName, |
| $vars: ['name'] |
| } as ComponentItemTooltipLabelFormatterParams; |
| (formatterParams as any)[mainType + 'Index'] = componentIndex; |
| |
| const formatterParamsExtra = opt.formatterParamsExtra; |
| if (formatterParamsExtra) { |
| each(keys(formatterParamsExtra), key => { |
| if (!hasOwn(formatterParams, key)) { |
| formatterParams[key] = formatterParamsExtra[key]; |
| formatterParams.$vars.push(key); |
| } |
| }); |
| } |
| |
| const ecData = getECData(opt.el); |
| ecData.componentMainType = mainType; |
| ecData.componentIndex = componentIndex; |
| ecData.tooltipConfig = { |
| name: itemName, |
| option: defaults({ |
| content: itemName, |
| encodeHTMLContent: true, |
| formatterParams: formatterParams |
| }, itemTooltipOptionObj) |
| }; |
| } |
| |
| function traverseElement(el: Element, cb: (el: Element) => boolean | void) { |
| let stopped; |
| // TODO |
| // Polyfill for fixing zrender group traverse don't visit it's root issue. |
| if (el.isGroup) { |
| stopped = cb(el); |
| } |
| if (!stopped) { |
| el.traverse(cb); |
| } |
| } |
| |
| export function traverseElements(els: Element | Element[] | undefined | null, cb: (el: Element) => boolean | void) { |
| if (els) { |
| if (isArray(els)) { |
| for (let i = 0; i < els.length; i++) { |
| traverseElement(els[i], cb); |
| } |
| } |
| else { |
| traverseElement(els, cb); |
| } |
| } |
| } |
| |
| /** |
| * After a boundingRect applying a `transform`, whether to be still parallel screen X and Y. |
| */ |
| export function isBoundingRectAxisAligned(transform: matrix.MatrixArray | NullUndefined): boolean { |
| return !transform |
| || (mathAbs(transform[1]) < AXIS_ALIGN_EPSILON && mathAbs(transform[2]) < AXIS_ALIGN_EPSILON) |
| || (mathAbs(transform[0]) < AXIS_ALIGN_EPSILON && mathAbs(transform[3]) < AXIS_ALIGN_EPSILON); |
| } |
| const AXIS_ALIGN_EPSILON = 1e-5; |
| |
| /** |
| * Create or copy to the existing bounding rect to avoid modifying `source`. |
| * |
| * @usage |
| * out.rect = ensureCopyRect(out.rect, sourceRect); |
| */ |
| export function ensureCopyRect( |
| target: BoundingRect | NullUndefined, |
| source: BoundingRect |
| ): BoundingRect { |
| return target ? BoundingRect.copy(target, source) : source.clone(); |
| } |
| |
| /** |
| * Create or copy to the existing transform to avoid modifying `source`. |
| * |
| * [CAUTION]: transform is `NullUndefined` if no transform, following convention of zrender, |
| * and enable to bypass some unnecessary calculation, since in most cases there is no transform. |
| * |
| * @usage |
| * out.transform = ensureCopyTransform(out.transform, sourceTransform); |
| */ |
| export function ensureCopyTransform( |
| target: matrix.MatrixArray | NullUndefined, |
| source: matrix.MatrixArray | NullUndefined |
| ): matrix.MatrixArray | NullUndefined { |
| return source ? matrix.copy(target || matrix.create(), source) : undefined; |
| } |
| |
| export function retrieveZInfo( |
| model: Model<Partial<Pick<ComponentOption, 'z' | 'zlevel'>>>, |
| ): { |
| z: ComponentOption['z'] |
| zlevel: ComponentOption['zlevel'] |
| } { |
| return { |
| z: model.get('z') || 0, |
| zlevel: model.get('zlevel') || 0, |
| }; |
| } |
| |
| /** |
| * Assume all of the elements has the same `z` and `zlevel`. |
| */ |
| export function calcZ2Range(el: Element): { |
| min: number |
| max: number |
| } { |
| let max = -Infinity; |
| let min = Infinity; |
| traverseElement(el, el => { |
| visitEl(el); |
| visitEl(el.getTextContent()); |
| visitEl(el.getTextGuideLine()); |
| }); |
| function visitEl(el: Element): void { |
| if (!el || el.isGroup) { |
| return; |
| } |
| const currentStates = el.currentStates; |
| if (currentStates.length) { |
| for (let idx = 0; idx < currentStates.length; idx++) { |
| calcZ2(el.states[currentStates[idx]] as Displayable); |
| } |
| } |
| calcZ2(el as Displayable); |
| } |
| function calcZ2(entity: Pick<Displayable, 'z2'>): void { |
| if (entity) { |
| const z2 = entity.z2; |
| // Consider z2 may be NullUndefined |
| if (z2 > max) { |
| max = z2; |
| } |
| if (z2 < min) { |
| min = z2; |
| } |
| } |
| } |
| if (min > max) { |
| min = max = 0; |
| } |
| return {min, max}; |
| } |
| |
| export function traverseUpdateZ( |
| el: Element, |
| z: number, |
| zlevel: number, |
| ): void { |
| doUpdateZ(el, z, zlevel, -Infinity); |
| } |
| |
| function doUpdateZ( |
| el: Element, |
| z: number, |
| zlevel: number, |
| // FIXME: Ideally all the labels should be above all the glyphs by default, |
| // e.g. in graph, edge labels should be above node elements. |
| // Currently impl does not guarantee that. |
| maxZ2: number, |
| ): number { |
| // `ignoreModelZ` is used to intentionally lift elements to cover other elements, |
| // where maxZ2 (for label.z2) should also not be counted for its parents. |
| if ((el as ExtendedElement).ignoreModelZ) { |
| return maxZ2; |
| } |
| |
| // Group may also have textContent |
| const label = el.getTextContent(); |
| const labelLine = el.getTextGuideLine(); |
| const isGroup = el.isGroup; |
| |
| if (isGroup) { |
| // set z & zlevel of children elements of Group |
| const children = (el as Group).childrenRef(); |
| for (let i = 0; i < children.length; i++) { |
| maxZ2 = mathMax( |
| doUpdateZ( |
| children[i], |
| z, |
| zlevel, |
| maxZ2 |
| ), |
| maxZ2 |
| ); |
| } |
| } |
| else { |
| // not Group |
| (el as Displayable).z = z; |
| (el as Displayable).zlevel = zlevel; |
| |
| maxZ2 = mathMax((el as Displayable).z2 || 0, maxZ2); |
| } |
| |
| // NOTICE: Do not call any method that can set REDRAW_BIT, |
| // otherwise progressive rendering is broken. |
| |
| // always set z and zlevel if label/labelLine exists |
| if (label) { |
| label.z = z; |
| label.zlevel = zlevel; |
| // lift z2 of text content |
| // TODO if el.emphasis.z2 is spcefied, what about textContent. |
| isFinite(maxZ2) && (label.z2 = maxZ2 + 2); |
| } |
| if (labelLine) { |
| const textGuideLineConfig = el.textGuideLineConfig; |
| labelLine.z = z; |
| labelLine.zlevel = zlevel; |
| isFinite(maxZ2) |
| && (labelLine.z2 = maxZ2 + (textGuideLineConfig && textGuideLineConfig.showAbove ? 1 : -1)); |
| } |
| return maxZ2; |
| } |
| |
| export function payloadDisableAnimation<TPayload extends Payload>( |
| payload: TPayload |
| ): TPayload { |
| // Disable animation in `updateProps` of `graphic.ts`. |
| payload.animation = {duration: 0}; |
| return payload; |
| } |
| |
| /** |
| * Decompose an affine matrix to |
| * x/y/scaleX/scaleY/rotation/skewX/skewY |
| */ |
| export function decomposeTransform( |
| out: Transformable, |
| mt: matrix.MatrixArray | NullUndefined |
| ): Transformable { |
| mt |
| ? matrix.copy(tmpDTR.transform, mt) |
| : matrix.identity(tmpDTR.transform); |
| // Use a tmp transformable to avoid effects from parent. |
| tmpDTR.decomposeTransform(); |
| copyTransform(out, tmpDTR); |
| return out; |
| } |
| const tmpDTR = new Transformable(); |
| tmpDTR.transform = matrix.create(); |
| |
| /** |
| * If not canvas painter, return null/undefined. |
| */ |
| export function getCurrentCanvasPainter(api: ExtensionAPI): CanvasPainter | NullUndefined { |
| const painter = api.getZr().painter; |
| return painter.getType() === 'canvas' ? (painter as CanvasPainter) : null; |
| } |
| |
| |
| // Register built-in shapes. These shapes might be overwritten |
| // by users, although we do not recommend that. |
| registerShape('circle', Circle); |
| registerShape('ellipse', Ellipse); |
| registerShape('sector', Sector); |
| registerShape('ring', Ring); |
| registerShape('polygon', Polygon); |
| registerShape('polyline', Polyline); |
| registerShape('rect', Rect); |
| registerShape('line', Line); |
| registerShape('bezierCurve', BezierCurve); |
| registerShape('arc', Arc); |
| |
| export { |
| Group, |
| ZRImage as Image, |
| ZRText as Text, |
| Circle, |
| Ellipse, |
| Sector, |
| Ring, |
| Polygon, |
| Polyline, |
| Rect, |
| Line, |
| BezierCurve, |
| Arc, |
| IncrementalDisplayable, |
| CompoundPath, |
| LinearGradient, |
| RadialGradient, |
| BoundingRect, |
| OrientedBoundingRect, |
| Point, |
| Path |
| }; |