graphic.ts 46.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/*
* 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.
*/

S
sushuang 已提交
20 21 22 23
import * as pathTool from 'zrender/src/tool/path';
import * as colorTool from 'zrender/src/tool/color';
import * as matrix from 'zrender/src/core/matrix';
import * as vector from 'zrender/src/core/vector';
P
pissang 已提交
24
import Path, { PathProps } from 'zrender/src/graphic/Path';
25
import Transformable from 'zrender/src/core/Transformable';
1
100pah 已提交
26
import ZRImage, { ImageStyleProps } from 'zrender/src/graphic/Image';
P
pissang 已提交
27
import Group from 'zrender/src/graphic/Group';
1
100pah 已提交
28
import ZRText, { TextStyleProps } from 'zrender/src/graphic/Text';
S
sushuang 已提交
29 30 31 32 33 34 35 36 37 38 39 40
import Circle from 'zrender/src/graphic/shape/Circle';
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';
41
import BoundingRect from 'zrender/src/core/BoundingRect';
P
pissang 已提交
42
import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable';
43
import * as subPixelOptimizeUtil from 'zrender/src/graphic/helper/subPixelOptimize';
P
pissang 已提交
44
import { Dictionary } from 'zrender/src/core/types';
P
pissang 已提交
45 46 47 48
import LRU from 'zrender/src/core/LRU';
import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable';
import { PatternObject } from 'zrender/src/graphic/Pattern';
import { GradientObject } from 'zrender/src/graphic/Gradient';
49
import Element, { ElementEvent, ElementTextConfig } from 'zrender/src/Element';
P
pissang 已提交
50
import Model from '../model/Model';
51 52 53 54 55 56
import {
    AnimationOptionMixin,
    LabelOption,
    AnimationDelayCallbackParam,
    DisplayState,
    ECElement,
57
    ZRRectLike,
58 59
    ColorString,
    DataModel,
P
pissang 已提交
60 61
    ECEventData,
    ZRStyleProps
62
} from './types';
P
pissang 已提交
63
import GlobalModel from '../model/Global';
64
import { makeInner } from './model';
65 66 67 68 69 70 71 72 73 74
import {
    isFunction,
    retrieve2,
    extend,
    keys,
    trim,
    isArrayLike,
    map,
    defaults
} from 'zrender/src/core/util';
S
sushuang 已提交
75

L
lang 已提交
76

P
pissang 已提交
77 78
const mathMax = Math.max;
const mathMin = Math.min;
79

P
pissang 已提交
80
const EMPTY_OBJ = {};
81

82
export const Z2_EMPHASIS_LIFT = 10;
83

S
sushuang 已提交
84
// key: label model property nane, value: style property name.
P
pissang 已提交
85
export const CACHED_LABEL_STYLE_PROPERTIES = {
S
sushuang 已提交
86 87 88 89 90
    color: 'textFill',
    textBorderColor: 'textStroke',
    textBorderWidth: 'textStrokeWidth'
};

P
pissang 已提交
91 92
const EMPHASIS = 'emphasis';
const NORMAL = 'normal';
93

S
sushuang 已提交
94
// Reserve 0 as default.
P
pissang 已提交
95 96 97 98 99 100 101 102 103
let _highlightNextDigit = 1;
const _highlightKeyMap: Dictionary<number> = {};

const _customShapeMap: Dictionary<{ new(): Path }> = {};

type ExtendShapeOpt = Parameters<typeof Path.extend>[0];
type ExtendShapeReturn = ReturnType<typeof Path.extend>;


104
type ExtendedProps = {
P
pissang 已提交
105 106
    __highlighted?: boolean | 'layer' | 'plain'
    __highByOuter: number
S
sushuang 已提交
107

P
pissang 已提交
108
    __highDownSilentOnTouch: boolean
109
    __onStateChange: (fromState: DisplayState, toState: DisplayState) => void
110

P
pissang 已提交
111
    __highDownDispatcher: boolean
1
100pah 已提交
112 113 114
};
type ExtendedElement = Element & ExtendedProps;
type ExtendedDisplayable = Displayable & ExtendedProps;
P
pissang 已提交
115 116 117 118 119 120 121 122 123 124

type TextCommonParams = {
    /**
     * Whether diable drawing box of block (outer most).
     */
    disableBox?: boolean
    /**
     * Specify a color when color is 'auto',
     * for textFill, textStroke, textBackgroundColor, and textBorderColor. If autoColor specified, it is used as default textFill.
     */
125
    autoColor?: ColorString
P
pissang 已提交
126 127 128 129 130

    forceRich?: boolean

    getTextPosition?: (textStyleModel: Model, isEmphasis?: boolean) => string | string[] | number[]

P
pissang 已提交
131 132 133
    defaultOutsidePosition?: LabelOption['position']

    textStyle?: ZRStyleProps
1
100pah 已提交
134
};
P
pissang 已提交
135

S
sushuang 已提交
136 137 138
/**
 * Extend shape with parameters
 */
P
pissang 已提交
139
export function extendShape(opts: ExtendShapeOpt): ExtendShapeReturn {
S
sushuang 已提交
140
    return Path.extend(opts);
S
sushuang 已提交
141
}
L
lang 已提交
142

P
pissang 已提交
143 144 145
const extendPathFromString = pathTool.extendFromString;
type SVGPathOption = Parameters<typeof extendPathFromString>[1];
type SVGPathCtor = ReturnType<typeof extendPathFromString>;
1
100pah 已提交
146
type SVGPath = InstanceType<SVGPathCtor>;
S
sushuang 已提交
147 148 149
/**
 * Extend path
 */
P
pissang 已提交
150 151
export function extendPath(pathData: string, opts: SVGPathOption): SVGPathCtor {
    return extendPathFromString(pathData, opts);
S
sushuang 已提交
152
}
153

154 155 156
/**
 * Register a user defined shape.
 * The shape class can be fetched by `getShapeClass`
S
SHUANG SU 已提交
157 158
 * This method will overwrite the registered shapes, including
 * the registered built-in shapes, if using the same `name`.
159 160 161
 * The shape can be used in `custom series` and
 * `graphic component` by declaring `{type: name}`.
 *
P
pissang 已提交
162 163
 * @param name
 * @param ShapeClass Can be generated by `extendShape`.
164
 */
P
pissang 已提交
165
export function registerShape(name: string, ShapeClass: {new(): Path}) {
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
    _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 register 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 rearly happen. So we dont make more mechanism
 * to resolve this issue here.
 *
P
pissang 已提交
193 194
 * @param name
 * @return The shape class. If not found, return nothing.
195
 */
P
pissang 已提交
196
export function getShapeClass(name: string): {new(): Path} {
197 198 199 200 201
    if (_customShapeMap.hasOwnProperty(name)) {
        return _customShapeMap[name];
    }
}

S
sushuang 已提交
202 203
/**
 * Create a path element from path data string
P
pissang 已提交
204 205 206 207
 * @param pathData
 * @param opts
 * @param rect
 * @param layout 'center' or 'cover' default to be cover
S
sushuang 已提交
208
 */
P
pissang 已提交
209 210 211
export function makePath(
    pathData: string,
    opts: SVGPathOption,
212
    rect: ZRRectLike,
P
pissang 已提交
213 214
    layout?: 'center' | 'cover'
): SVGPath {
215
    const path = pathTool.createFromString(pathData, opts);
S
sushuang 已提交
216 217
    if (rect) {
        if (layout === 'center') {
S
sushuang 已提交
218
            rect = centerGraphic(rect, path.getBoundingRect());
219
        }
S
sushuang 已提交
220
        resizePath(path, rect);
S
sushuang 已提交
221 222
    }
    return path;
S
sushuang 已提交
223
}
S
sushuang 已提交
224 225 226

/**
 * Create a image element from image url
P
pissang 已提交
227 228 229 230
 * @param imageUrl image url
 * @param opts options
 * @param rect constrain rect
 * @param layout 'center' or 'cover'. Default to be 'cover'
S
sushuang 已提交
231
 */
P
pissang 已提交
232
export function makeImage(
P
pissang 已提交
233
    imageUrl: string,
234
    rect: ZRRectLike,
P
pissang 已提交
235 236
    layout?: 'center' | 'cover'
) {
1
100pah 已提交
237
    const path = new ZRImage({
S
sushuang 已提交
238 239 240 241 242 243 244
        style: {
            image: imageUrl,
            x: rect.x,
            y: rect.y,
            width: rect.width,
            height: rect.height
        },
P
pissang 已提交
245
        onload(img) {
S
sushuang 已提交
246
            if (layout === 'center') {
247
                const boundingRect = {
S
sushuang 已提交
248 249 250 251
                    width: img.width,
                    height: img.height
                };
                path.setStyle(centerGraphic(rect, boundingRect));
O
Ovilia 已提交
252 253
            }
        }
S
sushuang 已提交
254 255
    });
    return path;
S
sushuang 已提交
256
}
S
sushuang 已提交
257 258 259 260

/**
 * Get position of centered element in bounding box.
 *
P
pissang 已提交
261 262 263
 * @param  rect         element local bounding box
 * @param  boundingRect constraint bounding box
 * @return element position containing x, y, width, and height
S
sushuang 已提交
264
 */
265
function centerGraphic(rect: ZRRectLike, boundingRect: {
P
pissang 已提交
266 267
    width: number
    height: number
268
}): ZRRectLike {
S
sushuang 已提交
269
    // Set rect to center, keep width / height ratio.
270
    const aspect = boundingRect.width / boundingRect.height;
271 272
    let width = rect.height * aspect;
    let height;
S
sushuang 已提交
273 274
    if (width <= rect.width) {
        height = rect.height;
O
Ovilia 已提交
275
    }
S
sushuang 已提交
276 277 278 279
    else {
        width = rect.width;
        height = width / aspect;
    }
280 281
    const cx = rect.x + rect.width / 2;
    const cy = rect.y + rect.height / 2;
S
sushuang 已提交
282 283 284 285 286 287

    return {
        x: cx - width / 2,
        y: cy - height / 2,
        width: width,
        height: height
288
    };
S
sushuang 已提交
289 290
}

291
export const mergePath = pathTool.mergePath;
S
sushuang 已提交
292 293 294

/**
 * Resize a path to fit the rect
P
pissang 已提交
295 296
 * @param path
 * @param rect
S
sushuang 已提交
297
 */
298
export function resizePath(path: SVGPath, rect: ZRRectLike): void {
S
sushuang 已提交
299 300 301
    if (!path.applyTransform) {
        return;
    }
302

303
    const pathRect = path.getBoundingRect();
S
sushuang 已提交
304

305
    const m = pathRect.calculateTransform(rect);
S
sushuang 已提交
306 307

    path.applyTransform(m);
S
sushuang 已提交
308
}
S
sushuang 已提交
309 310 311 312

/**
 * Sub pixel optimize line for canvas
 */
P
pissang 已提交
313 314 315 316 317 318 319 320
export function subPixelOptimizeLine(param: {
    shape: {
        x1: number, y1: number, x2: number, y2: number
    },
    style: {
        lineWidth: number
    }
}) {
321
    subPixelOptimizeUtil.subPixelOptimizeLine(param.shape, param.shape, param.style);
S
sushuang 已提交
322
    return param;
S
sushuang 已提交
323
}
S
sushuang 已提交
324 325 326 327

/**
 * Sub pixel optimize rect for canvas
 */
P
pissang 已提交
328 329 330 331 332 333 334 335
export function subPixelOptimizeRect(param: {
    shape: {
        x: number, y: number, width: number, height: number
    },
    style: {
        lineWidth: number
    }
}) {
336
    subPixelOptimizeUtil.subPixelOptimizeRect(param.shape, param.shape, param.style);
S
sushuang 已提交
337
    return param;
S
sushuang 已提交
338
}
S
sushuang 已提交
339 340 341 342

/**
 * Sub pixel optimize for canvas
 *
P
pissang 已提交
343 344 345 346
 * @param position Coordinate, such as x, y
 * @param lineWidth Should be nonnegative integer.
 * @param positiveOrNegative Default false (negative).
 * @return Optimized position.
S
sushuang 已提交
347
 */
348
export const subPixelOptimize = subPixelOptimizeUtil.subPixelOptimize;
349

S
sushuang 已提交
350

P
pissang 已提交
351
function hasFillOrStroke(fillOrStroke: string | PatternObject | GradientObject) {
S
sushuang 已提交
352
    return fillOrStroke != null && fillOrStroke !== 'none';
S
sushuang 已提交
353 354
}

S
sushuang 已提交
355
// Most lifted color are duplicated.
356
const liftedColorCache = new LRU<string>(100);
S
sushuang 已提交
357

P
pissang 已提交
358
function liftColor(color: string): string {
S
sushuang 已提交
359 360 361
    if (typeof color !== 'string') {
        return color;
    }
362
    let liftedColor = liftedColorCache.get(color);
S
sushuang 已提交
363 364
    if (!liftedColor) {
        liftedColor = colorTool.lift(color, -0.1);
P
pissang 已提交
365
        liftedColorCache.put(color, liftedColor);
S
sushuang 已提交
366 367
    }
    return liftedColor;
S
sushuang 已提交
368 369
}

370 371 372 373 374
function singleEnterEmphasis(el: Element) {

    (el as ExtendedElement).__highlighted = true;

    // el may be an array.
P
pissang 已提交
375 376 377
    if (!el.states.emphasis) {
        return;
    }
378
    const disp = el as Displayable;
379

380 381 382
    const emphasisStyle = disp.states.emphasis.style;
    const currentFill = disp.style && disp.style.fill;
    const currentStroke = disp.style && disp.style.stroke;
P
pissang 已提交
383

P
pissang 已提交
384
    el.useState('emphasis');
S
sushuang 已提交
385

386 387
    if (emphasisStyle && (currentFill || currentStroke)) {
        if (!hasFillOrStroke(emphasisStyle.fill)) {
388 389
            disp.style.fill = liftColor(currentFill);
        }
390
        if (!hasFillOrStroke(emphasisStyle.stroke)) {
391 392 393
            disp.style.stroke = liftColor(currentStroke);
        }
        disp.z2 += Z2_EMPHASIS_LIFT;
P
pissang 已提交
394 395
    }

396 397 398 399
    const textContent = el.getTextContent();
    if (textContent) {
        textContent.z2 += Z2_EMPHASIS_LIFT;
    }
400
    // TODO hover layer
S
sushuang 已提交
401 402
}

S
sushuang 已提交
403

404
function singleEnterNormal(el: Element) {
P
pissang 已提交
405
    el.clearStates();
406
    (el as ExtendedElement).__highlighted = false;
S
sushuang 已提交
407 408
}

409
function updateElementState<T>(
410 411
    el: ExtendedElement,
    updater: (this: void, el: Element, commonParam?: T) => void,
P
pissang 已提交
412 413
    commonParam?: T
) {
414
    // If root is group, also enter updater for `onStateChange`.
415 416 417
    let fromState: DisplayState = NORMAL;
    let toState: DisplayState = NORMAL;
    let trigger;
418
    // See the rule of `onStateChange` on `graphic.setAsHighDownDispatcher`.
419 420 421
    el.__highlighted && (fromState = EMPHASIS, trigger = true);
    updater(el, commonParam);
    el.__highlighted && (toState = EMPHASIS, trigger = true);
422 423
    trigger && el.__onStateChange && el.__onStateChange(fromState, toState);
}
424

425 426 427 428 429 430 431 432
function traverseUpdateState<T>(
    el: ExtendedElement,
    updater: (this: void, el: Element, commonParam?: T) => void,
    commonParam?: T
) {
    updateElementState(el, updater, commonParam);
    el.isGroup && el.traverse(function (child: ExtendedElement) {
        updateElementState(child, updater, commonParam);
433 434
    });

435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
}

/**
 * If we reuse elements when rerender.
 * DONT forget to clearStates before we update the style and shape.
 * Or we may update on the wrong state instead of normal state.
 */
export function clearStates(el: Element) {
    if (el.isGroup) {
        el.traverse(function (child) {
            child.clearStates();
        });
    }
    else {
        el.clearStates();
    }
S
sushuang 已提交
451 452 453
}

/**
P
pissang 已提交
454 455
 * Set hover style (namely "emphasis style") of element.
 * @param el Should not be `zrender/graphic/Group`.
S
sushuang 已提交
456
 */
P
pissang 已提交
457 458 459 460 461
export function enableElementHoverEmphasis(el: Displayable, hoverStl?: ZRStyleProps) {
    if (hoverStl) {
        const emphasisState = el.ensureState('emphasis');
        emphasisState.style = hoverStl;
    }
S
sushuang 已提交
462

S
sushuang 已提交
463 464 465 466 467 468
    // FIXME
    // It is not completely right to save "normal"/"emphasis" flag on elements.
    // It probably should be saved on `data` of series. Consider the cases:
    // (1) A highlighted elements are moved out of the view port and re-enter
    // again by dataZoom.
    // (2) call `setOption` and replace elements totally when they are highlighted.
P
pissang 已提交
469 470 471
    if ((el as ExtendedDisplayable).__highlighted) {
        singleEnterNormal(el);
        singleEnterEmphasis(el);
L
lang 已提交
472
    }
S
sushuang 已提交
473 474
}

475 476
export function enterEmphasisWhenMouseOver(el: Element, e: ElementEvent) {
    !shouldSilent(el, e)
S
sushuang 已提交
477
        // "emphasis" event highlight has higher priority than mouse highlight.
478
        && !(el as ExtendedElement).__highByOuter
479
        && traverseUpdateState((el as ExtendedElement), singleEnterEmphasis);
S
sushuang 已提交
480
}
481

482 483
export function leaveEmphasisWhenMouseOut(el: Element, e: ElementEvent) {
    !shouldSilent(el, e)
S
sushuang 已提交
484
        // "emphasis" event highlight has higher priority than mouse highlight.
485
        && !(el as ExtendedElement).__highByOuter
486
        && traverseUpdateState((el as ExtendedElement), singleEnterNormal);
S
sushuang 已提交
487 488
}

489 490
export function enterEmphasis(el: Element, highlightDigit?: number) {
    (el as ExtendedElement).__highByOuter |= 1 << (highlightDigit || 0);
491
    traverseUpdateState((el as ExtendedElement), singleEnterEmphasis);
S
sushuang 已提交
492 493
}

494 495
export function leaveEmphasis(el: Element, highlightDigit?: number) {
    !((el as ExtendedElement).__highByOuter &= ~(1 << (highlightDigit || 0)))
496
        && traverseUpdateState((el as ExtendedElement), singleEnterNormal);
497 498
}

499 500
function shouldSilent(el: Element, e: ElementEvent) {
    return (el as ExtendedElement).__highDownSilentOnTouch && e.zrByTouch;
S
sushuang 已提交
501 502 503
}

/**
P
pissang 已提交
504
 * Enable the function that mouseover will trigger the emphasis state.
S
sushuang 已提交
505
 *
P
pissang 已提交
506 507
 * It will set hoverStyle to 'emphasis' state of each children displayables.
 * If hoverStyle is not given, it will just ignore it and use the preset 'emphasis' state.
S
sushuang 已提交
508
 *
P
pissang 已提交
509 510
 * NOTICE
 * (1)
S
sushuang 已提交
511 512
 * Call the method for a "root" element once. Do not call it for each descendants.
 * If the descendants elemenets of a group has itself hover style different from the
513
 * root group, we can simply mount the style on `el.states.emphasis` for them, but should
S
sushuang 已提交
514 515
 * not call this method for them.
 *
516
 * (2) The given hover style will replace the style in emphasis state already exists.
S
sushuang 已提交
517
 */
P
pissang 已提交
518
export function enableHoverEmphasis(el: Element, hoverStyle?: ZRStyleProps) {
519
    setAsHighDownDispatcher(el, true);
520
    traverseUpdateState(el as ExtendedElement, enableElementHoverEmphasis, hoverStyle);
S
sushuang 已提交
521 522 523
}

/**
524
 * @param {module:zrender/Element} el
525
 * @param {Function} [el.onStateChange] Called when state updated.
526
 *        Since `setHoverStyle` has the constraint that it must be called after
527
 *        all of the normal style updated, `onStateChange` is not needed to
528 529 530 531 532 533 534 535 536
 *        trigger if both `fromState` and `toState` is 'normal', and needed to
 *        trigger if both `fromState` and `toState` is 'emphasis', which enables
 *        to sync outside style settings to "emphasis" state.
 *        @this {string} This dispatcher `el`.
 *        @param {string} fromState Can be "normal" or "emphasis".
 *               `fromState` might equal to `toState`,
 *               for example, when this method is called when `el` is
 *               on "emphasis" state.
 *        @param {string} toState Can be "normal" or "emphasis".
S
Tweak  
sushuang 已提交
537 538
 *
 *        FIXME
539
 *        CAUTION: Do not expose `onStateChange` outside echarts.
S
Tweak  
sushuang 已提交
540 541 542 543 544
 *        Because it is not a complete solution. The update
 *        listener should not have been mount in element,
 *        and the normal/emphasis state should not have
 *        mantained on elements.
 *
545
 * @param {boolean} [el.highDownSilentOnTouch=false]
546 547 548 549 550 551 552 553 554
 *        In touch device, mouseover event will be trigger on touchstart event
 *        (see module:zrender/dom/HandlerProxy). By this mechanism, we can
 *        conveniently use hoverStyle when tap on touch screen without additional
 *        code for compatibility.
 *        But if the chart/component has select feature, which usually also use
 *        hoverStyle, there might be conflict between 'select-highlight' and
 *        'hover-highlight' especially when roam is enabled (see geo for example).
 *        In this case, `highDownSilentOnTouch` should be used to disable
 *        hover-highlight on touch device.
555
 * @param {boolean} [asDispatcher=true] If `false`, do not set as "highDownDispatcher".
S
sushuang 已提交
556
 */
557 558
export function setAsHighDownDispatcher(el: Element, asDispatcher: boolean) {
    const disable = asDispatcher === false;
559
    const extendedEl = el as ExtendedElement;
560
    // Make `highDownSilentOnTouch` and `onStateChange` only work after
561
    // `setAsHighDownDispatcher` called. Avoid it is modified by user unexpectedly.
562 563 564
    if ((el as ECElement).highDownSilentOnTouch) {
        extendedEl.__highDownSilentOnTouch = (el as ECElement).highDownSilentOnTouch;
    }
565 566
    if ((el as ECElement).onStateChange) {
        extendedEl.__onStateChange = (el as ECElement).onStateChange;
567
    }
568 569 570 571 572 573

    // Simple optimize, since this method might be
    // called for each elements of a group in some cases.
    if (!disable || extendedEl.__highDownDispatcher) {

        // Emphasis, normal can be triggered manually by API or other components like hover link.
574
        // el[method]('emphasis', onElementEmphasisEvent)[method]('normal', onElementNormalEvent);
575 576 577 578
        // Also keep previous record.
        extendedEl.__highByOuter = extendedEl.__highByOuter || 0;

        extendedEl.__highDownDispatcher = !disable;
S
sushuang 已提交
579
    }
S
sushuang 已提交
580
}
S
sushuang 已提交
581

582
export function isHighDownDispatcher(el: Element): boolean {
P
pissang 已提交
583
    return !!(el && (el as ExtendedDisplayable).__highDownDispatcher);
584 585
}

S
sushuang 已提交
586 587 588 589 590 591 592 593
/**
 * Support hightlight/downplay record on each elements.
 * For the case: hover highlight/downplay (legend, visualMap, ...) and
 * user triggerred hightlight/downplay should not conflict.
 * Only all of the highlightDigit cleared, return to normal.
 * @param {string} highlightKey
 * @return {number} highlightDigit
 */
P
pissang 已提交
594
export function getHighlightDigit(highlightKey: number) {
595
    let highlightDigit = _highlightKeyMap[highlightKey];
S
sushuang 已提交
596 597 598 599 600 601
    if (highlightDigit == null && _highlightNextDigit <= 32) {
        highlightDigit = _highlightKeyMap[highlightKey] = _highlightNextDigit++;
    }
    return highlightDigit;
}

602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
interface SetLabelStyleOpt<LDI> extends TextCommonParams {
    defaultText?: string | (
        (labelDataIndex: LDI, opt: SetLabelStyleOpt<LDI>) => string
    ),
    // Fetch text by `opt.labelFetcher.getFormattedLabel(opt.labelDataIndex, 'normal'/'emphasis', null, opt.labelDimIndex)`
    labelFetcher?: {
        getFormattedLabel?: (
            // In MapDraw case it can be string (region name)
            labelDataIndex: LDI,
            state: DisplayState,
            dataType: string,
            labelDimIndex: number
        ) => string
    },
    labelDataIndex?: LDI,
    labelDimIndex?: number
}
619 620


S
sushuang 已提交
621
/**
622
 * Set normal styles and emphasis styles about text on target element
1
100pah 已提交
623 624
 * If target is a ZRText. It will create a new style object.
 * If target is other Element. It will create or reuse ZRText which is attached on the target.
625 626
 * And create a new style object.
 *
1
100pah 已提交
627 628
 * NOTICE: Because the style on ZRText will be replaced with new(only x, y are keeped).
 * So please use the style on ZRText after use this method.
S
sushuang 已提交
629
 */
630
export function setLabelStyle<LDI>(
631
    targetEl: Element,
P
pissang 已提交
632 633
    normalModel: Model,
    emphasisModel: Model,
634
    opt?: SetLabelStyleOpt<LDI>,
1
100pah 已提交
635 636
    normalSpecified?: TextStyleProps,
    emphasisSpecified?: TextStyleProps
P
pissang 已提交
637
    // TODO specified position?
S
sushuang 已提交
638 639
) {
    opt = opt || EMPTY_OBJ;
1
100pah 已提交
640
    const isSetOnText = targetEl instanceof ZRText;
S
sushuang 已提交
641

P
pissang 已提交
642 643
    const showNormal = normalModel.getShallow('show');
    const showEmphasis = emphasisModel.getShallow('show');
S
sushuang 已提交
644 645 646 647

    // Consider performance, only fetch label when necessary.
    // If `normal.show` is `false` and `emphasis.show` is `true` and `emphasis.formatter` is not set,
    // label should be displayed, where text is fetched by `normal.formatter` or `opt.defaultText`.
648
    let richText = isSetOnText ? targetEl as ZRText : null;
S
sushuang 已提交
649
    if (showNormal || showEmphasis) {
650 651 652 653 654
        const labelFetcher = opt.labelFetcher;
        const labelDataIndex = opt.labelDataIndex;
        const labelDimIndex = opt.labelDimIndex;

        let baseText;
S
sushuang 已提交
655 656 657 658
        if (labelFetcher) {
            baseText = labelFetcher.getFormattedLabel(labelDataIndex, 'normal', null, labelDimIndex);
        }
        if (baseText == null) {
659
            baseText = isFunction(opt.defaultText) ? opt.defaultText(labelDataIndex, opt) : opt.defaultText;
S
sushuang 已提交
660
        }
661 662 663 664 665 666 667
        const normalStyleText = baseText;
        const emphasisStyleText = retrieve2(
            labelFetcher
                ? labelFetcher.getFormattedLabel(labelDataIndex, 'emphasis', null, labelDimIndex)
                : null,
            baseText
        );
S
sushuang 已提交
668

1
100pah 已提交
669
        if (!isSetOnText) {
P
pissang 已提交
670
            // Reuse the previous
671
            richText = targetEl.getTextContent();
P
pissang 已提交
672
            if (!richText) {
1
100pah 已提交
673
                richText = new ZRText();
674
                targetEl.setTextContent(richText);
P
pissang 已提交
675 676
            }
        }
677
        richText.ignore = !showNormal;
P
pissang 已提交
678 679

        const emphasisState = richText.ensureState('emphasis');
680
        emphasisState.ignore = !showEmphasis;
P
pissang 已提交
681

S
sushuang 已提交
682 683 684 685 686 687
        // Always set `textStyle` even if `normalStyle.text` is null, because default
        // values have to be set on `normalStyle`.
        // If we set default values on `emphasisStyle`, consider case:
        // Firstly, `setOption(... label: {normal: {text: null}, emphasis: {show: true}} ...);`
        // Secondly, `setOption(... label: {noraml: {show: true, text: 'abc', color: 'red'} ...);`
        // Then the 'red' will not work on emphasis.
688
        const normalStyle = createTextStyle(
P
pissang 已提交
689 690
            normalModel,
            normalSpecified,
691 692
            opt,
            false,
1
100pah 已提交
693
            !isSetOnText
P
pissang 已提交
694
        );
695
        emphasisState.style = createTextStyle(
P
pissang 已提交
696 697 698
            emphasisModel,
            emphasisSpecified,
            opt,
699
            true,
1
100pah 已提交
700
            !isSetOnText
P
pissang 已提交
701
        );
702

1
100pah 已提交
703
        if (!isSetOnText) {
704 705 706 707 708 709
            // Always create new
            targetEl.setTextConfig(createTextConfig(
                normalStyle,
                normalModel,
                opt
            ));
710 711
            const targetElEmphasisState = targetEl.ensureState('emphasis');
            targetElEmphasisState.textConfig = createTextConfig(
712 713 714 715
                emphasisState.style,
                emphasisModel,
                opt
            );
P
pissang 已提交
716
        }
S
sushuang 已提交
717

718 719 720
        normalStyle.text = normalStyleText;
        emphasisState.style.text = emphasisStyleText;

721 722 723 724 725 726 727 728
        // Keep x and y
        if (richText.style.x != null) {
            normalStyle.x = richText.style.x;
        }
        if (richText.style.y != null) {
            normalStyle.y = richText.style.y;
        }

729 730
        // Always create new style.
        richText.useStyle(normalStyle);
P
pissang 已提交
731 732

        richText.dirty();
733
    }
P
pissang 已提交
734 735 736
    else if (richText) {
        // Not display rich text.
        richText.ignore = true;
737
    }
P
pissang 已提交
738

739
    targetEl.dirty();
740 741
}

S
sushuang 已提交
742 743 744
/**
 * Set basic textStyle properties.
 */
745
export function createTextStyle(
P
pissang 已提交
746
    textStyleModel: Model,
1
100pah 已提交
747
    specifiedTextStyle?: TextStyleProps,    // Can be overrided by settings in model.
P
pissang 已提交
748
    opt?: TextCommonParams,
749 750
    isEmphasis?: boolean,
    isAttached?: boolean // If text is attached on an element. If so, auto color will handling in zrender.
S
sushuang 已提交
751
) {
1
100pah 已提交
752
    const textStyle: TextStyleProps = {};
753
    setTextStyleCommon(textStyle, textStyleModel, opt, isEmphasis, isAttached);
754
    specifiedTextStyle && extend(textStyle, specifiedTextStyle);
755
    // textStyle.host && textStyle.host.dirty && textStyle.host.dirty(false);
S
sushuang 已提交
756 757

    return textStyle;
S
sushuang 已提交
758
}
S
sushuang 已提交
759

760
export function createTextConfig(
1
100pah 已提交
761
    textStyle: TextStyleProps,
762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799
    textStyleModel: Model,
    opt?: TextCommonParams,
    isEmphasis?: boolean
) {
    const textConfig: ElementTextConfig = {};
    let labelPosition;
    let labelRotate = textStyleModel.getShallow('rotate');
    const labelDistance = retrieve2(
        textStyleModel.getShallow('distance'), isEmphasis ? null : 5
    );
    const labelOffset = textStyleModel.getShallow('offset');

    if (opt.getTextPosition) {
        labelPosition = opt.getTextPosition(textStyleModel, isEmphasis);
    }
    else {
        labelPosition = textStyleModel.getShallow('position')
            || (isEmphasis ? null : 'inside');
        // 'outside' is not a valid zr textPostion value, but used
        // in bar series, and magric type should be considered.
        labelPosition === 'outside' && (labelPosition = opt.defaultOutsidePosition || 'top');
    }

    if (labelPosition != null) {
        textConfig.position = labelPosition;
    }
    if (labelOffset != null) {
        textConfig.offset = labelOffset;
    }
    if (labelRotate != null) {
        labelRotate *= Math.PI / 180;
        textConfig.rotation = labelRotate;
    }
    if (labelDistance != null) {
        textConfig.distance = labelDistance;
    }

    // fill and auto is determined by the color of path fill if it's not specified by developers.
1
100pah 已提交
800 801
    textConfig.outsideFill = opt.autoColor || null;
    textConfig.insideStroke = opt.autoColor || null;
1
100pah 已提交
802 803
    // Set default stroke, which is useful when label is over other
    // messy graphics (like lines) in background.
1
tweak  
100pah 已提交
804
    textConfig.outsideStroke = 'rgba(255, 255, 255, 0.9)';
1
100pah 已提交
805 806 807 808 809 810 811 812 813 814 815
    // if (!textStyle.fill) {
    //     textConfig.insideFill = 'auto';
    //     textConfig.outsideFill = opt.autoColor || null;
    // }
    // if (!textStyle.stroke) {
    //     textConfig.insideStroke = 'auto';
    // }
    // else if (opt.autoColor) {
    //     // TODO: stroke set to autoColor. if label is inside?
    //     textConfig.insideStroke = opt.autoColor;
    // }
816 817 818 819

    return textConfig;
}

S
sushuang 已提交
820 821

/**
S
sushuang 已提交
822 823 824 825 826 827 828
 * The uniform entry of set text style, that is, retrieve style definitions
 * from `model` and set to `textStyle` object.
 *
 * Never in merge mode, but in overwrite mode, that is, all of the text style
 * properties will be set. (Consider the states of normal and emphasis and
 * default value can be adopted, merge would make the logic too complicated
 * to manage.)
S
sushuang 已提交
829
 */
P
pissang 已提交
830
function setTextStyleCommon(
1
100pah 已提交
831
    textStyle: TextStyleProps,
P
pissang 已提交
832 833
    textStyleModel: Model,
    opt?: TextCommonParams,
834 835
    isEmphasis?: boolean,
    isAttached?: boolean
P
pissang 已提交
836
) {
S
sushuang 已提交
837 838 839
    // Consider there will be abnormal when merge hover style to normal style if given default value.
    opt = opt || EMPTY_OBJ;

840 841
    const ecModel = textStyleModel.ecModel;
    const globalTextStyle = ecModel && ecModel.option.textStyle;
S
sushuang 已提交
842

S
sushuang 已提交
843 844 845 846 847
    // Consider case:
    // {
    //     data: [{
    //         value: 12,
    //         label: {
848 849
    //             rich: {
    //                 // no 'a' here but using parent 'a'.
S
sushuang 已提交
850 851 852 853 854 855 856
    //             }
    //         }
    //     }],
    //     rich: {
    //         a: { ... }
    //     }
    // }
857
    const richItemNames = getRichItemNames(textStyleModel);
1
100pah 已提交
858
    let richResult: TextStyleProps['rich'];
S
sushuang 已提交
859 860
    if (richItemNames) {
        richResult = {};
861
        for (const name in richItemNames) {
S
sushuang 已提交
862 863
            if (richItemNames.hasOwnProperty(name)) {
                // Cascade is supported in rich.
864
                const richTextStyle = textStyleModel.getModel(['rich', name]);
S
sushuang 已提交
865
                // In rich, never `disableBox`.
S
Tweak  
sushuang 已提交
866 867 868 869
                // FIXME: consider `label: {formatter: '{a|xx}', color: 'blue', rich: {a: {}}}`,
                // the default color `'blue'` will not be adopted if no color declared in `rich`.
                // That might confuses users. So probably we should put `textStyleModel` as the
                // root ancestor of the `richTextStyle`. But that would be a break change.
870
                setTokenTextStyle(richResult[name] = {}, richTextStyle, globalTextStyle, opt, isEmphasis, isAttached);
S
sushuang 已提交
871 872 873 874
            }
        }
    }

875 876 877 878 879 880
    if (richResult) {
        textStyle.rich = richResult;
    }
    const overflow = textStyleModel.get('overflow');
    if (overflow) {
        textStyle.overflow = overflow;
P
pissang 已提交
881 882
    }

883
    setTokenTextStyle(textStyle, textStyleModel, globalTextStyle, opt, isEmphasis, isAttached, true);
P
pissang 已提交
884 885

    // TODO
S
sushuang 已提交
886 887 888 889 890 891 892 893 894 895
    if (opt.forceRich && !opt.textStyle) {
        opt.textStyle = {};
    }
}

// Consider case:
// {
//     data: [{
//         value: 12,
//         label: {
896 897
//             rich: {
//                 // no 'a' here but using parent 'a'.
S
sushuang 已提交
898 899 900 901 902 903 904
//             }
//         }
//     }],
//     rich: {
//         a: { ... }
//     }
// }
P
pissang 已提交
905 906
// TODO TextStyleModel
function getRichItemNames(textStyleModel: Model<LabelOption>) {
S
sushuang 已提交
907
    // Use object to remove duplicated names.
908
    let richItemNameMap: Dictionary<number>;
S
sushuang 已提交
909
    while (textStyleModel && textStyleModel !== textStyleModel.ecModel) {
910
        const rich = (textStyleModel.option || EMPTY_OBJ as LabelOption).rich;
S
sushuang 已提交
911 912
        if (rich) {
            richItemNameMap = richItemNameMap || {};
913
            const richKeys = keys(rich);
P
pissang 已提交
914 915 916
            for (let i = 0; i < richKeys.length; i++) {
                const richKey = richKeys[i];
                richItemNameMap[richKey] = 1;
S
sushuang 已提交
917
            }
918
        }
S
sushuang 已提交
919 920 921 922 923
        textStyleModel = textStyleModel.parentModel;
    }
    return richItemNameMap;
}

924 925 926 927 928 929 930 931 932 933 934 935 936 937 938
const TEXT_PROPS_WITH_GLOBAL = [
    'fontStyle', 'fontWeight', 'fontSize', 'fontFamily',
    'textShadowColor', 'textShadowBlur', 'textShadowOffsetX', 'textShadowOffsetY'
] as const;

const TEXT_PROPS_SELF = [
    'align', 'lineHeight', 'width', 'height', 'tag', 'verticalAlign'
] as const;

const TEXT_PROPS_BOX = [
    'padding', 'borderWidth', 'borderRadius',
    'backgroundColor', 'borderColor',
    'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY'
] as const;

P
pissang 已提交
939
function setTokenTextStyle(
1
100pah 已提交
940
    textStyle: TextStyleProps['rich'][string],
P
pissang 已提交
941 942 943 944
    textStyleModel: Model<LabelOption>,
    globalTextStyle: LabelOption,
    opt?: TextCommonParams,
    isEmphasis?: boolean,
945
    isAttached?: boolean,
P
pissang 已提交
946 947
    isBlock?: boolean
) {
S
sushuang 已提交
948 949 950
    // In merge mode, default value should not be given.
    globalTextStyle = !isEmphasis && globalTextStyle || EMPTY_OBJ;

951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969
    const autoColor = opt && opt.autoColor;
    let fillColor = textStyleModel.getShallow('color');
    let strokeColor = textStyleModel.getShallow('textBorderColor');
    if (fillColor === 'auto' && autoColor) {
        fillColor = autoColor;
    }
    if (strokeColor === 'auto' && autoColor) {
        strokeColor = autoColor;
    }
    fillColor = fillColor || globalTextStyle.color;
    strokeColor = strokeColor || globalTextStyle.textBorderColor;
    if (fillColor != null) {
        textStyle.fill = fillColor;
    }
    if (strokeColor != null) {
        textStyle.stroke = strokeColor;
    }

    const lineWidth = retrieve2(
S
sushuang 已提交
970 971 972
        textStyleModel.getShallow('textBorderWidth'),
        globalTextStyle.textBorderWidth
    );
973 974 975 976
    if (lineWidth != null) {
        textStyle.lineWidth = lineWidth;
    }

977 978
    // TODO
    if (!isEmphasis && !isAttached) {
S
sushuang 已提交
979
        // Set default finally.
980
        if (textStyle.fill == null && opt.autoColor) {
P
pissang 已提交
981
            textStyle.fill = opt.autoColor;
S
sushuang 已提交
982
        }
P
pah100 已提交
983 984
    }

S
sushuang 已提交
985 986 987
    // Do not use `getFont` here, because merge should be supported, where
    // part of these properties may be changed in emphasis style, and the
    // others should remain their original value got from normal style.
988 989 990 991 992 993 994
    for (let i = 0; i < TEXT_PROPS_WITH_GLOBAL.length; i++) {
        const key = TEXT_PROPS_WITH_GLOBAL[i];
        const val = retrieve2(textStyleModel.getShallow(key), globalTextStyle[key]);
        if (val != null) {
            (textStyle as any)[key] = val;
        }
    }
S
sushuang 已提交
995

996 997 998 999 1000 1001 1002
    for (let i = 0; i < TEXT_PROPS_SELF.length; i++) {
        const key = TEXT_PROPS_SELF[i];
        const val = textStyleModel.getShallow(key);
        if (val != null) {
            (textStyle as any)[key] = val;
        }
    }
S
sushuang 已提交
1003

1004 1005 1006 1007 1008
    if (textStyle.verticalAlign == null) {
        const baseline = textStyleModel.getShallow('baseline');
        if (baseline != null) {
            textStyle.verticalAlign = baseline;
        }
P
pah100 已提交
1009 1010
    }

S
sushuang 已提交
1011

1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027
    if (!isBlock || !opt.disableBox) {
        if (textStyle.backgroundColor === 'auto' && autoColor) {
            textStyle.backgroundColor = autoColor;
        }
        if (textStyle.borderColor === 'auto' && autoColor) {
            textStyle.borderColor = autoColor;
        }

        for (let i = 0; i < TEXT_PROPS_BOX.length; i++) {
            const key = TEXT_PROPS_BOX[i];
            const val = textStyleModel.getShallow(key);
            if (val != null) {
                (textStyle as any)[key] = val;
            }
        }
    }
S
sushuang 已提交
1028 1029
}

P
pissang 已提交
1030
export function getFont(opt: LabelOption, ecModel: GlobalModel) {
1031
    const gTextStyleModel = ecModel && ecModel.getModel('textStyle');
1032
    return trim([
S
sushuang 已提交
1033 1034 1035 1036 1037
        // FIXME in node-canvas fontWeight is before fontStyle
        opt.fontStyle || gTextStyleModel && gTextStyleModel.getShallow('fontStyle') || '',
        opt.fontWeight || gTextStyleModel && gTextStyleModel.getShallow('fontWeight') || '',
        (opt.fontSize || gTextStyleModel && gTextStyleModel.getShallow('fontSize') || 12) + 'px',
        opt.fontFamily || gTextStyleModel && gTextStyleModel.getShallow('fontFamily') || 'sans-serif'
O
Ovilia 已提交
1038
    ].join(' '));
S
sushuang 已提交
1039
}
S
sushuang 已提交
1040

1041
function animateOrSetProps<Props>(
P
pissang 已提交
1042
    isUpdate: boolean,
P
pissang 已提交
1043
    el: Element<Props>,
1044
    props: Props,
P
pissang 已提交
1045
    animatableModel?: Model<AnimationOptionMixin> & {
P
pissang 已提交
1046
        getAnimationDelayParams?: (el: Element<Props>, dataIndex: number) => AnimationDelayCallbackParam
P
pissang 已提交
1047
    },
1048
    dataIndex?: number | (() => void),
P
pissang 已提交
1049 1050
    cb?: () => void
) {
S
sushuang 已提交
1051 1052 1053 1054 1055 1056 1057
    if (typeof dataIndex === 'function') {
        cb = dataIndex;
        dataIndex = null;
    }
    // Do not check 'animation' property directly here. Consider this case:
    // animation model is an `itemModel`, whose does not have `isAnimationEnabled`
    // but its parent model (`seriesModel`) does.
1058
    const animationEnabled = animatableModel && animatableModel.isAnimationEnabled();
S
sushuang 已提交
1059 1060

    if (animationEnabled) {
1061
        let duration = animatableModel.getShallow(
P
pissang 已提交
1062 1063
            isUpdate ? 'animationDurationUpdate' : 'animationDuration'
        );
1064
        const animationEasing = animatableModel.getShallow(
P
pissang 已提交
1065 1066
            isUpdate ? 'animationEasingUpdate' : 'animationEasing'
        );
1067
        let animationDelay = animatableModel.getShallow(
P
pissang 已提交
1068 1069
            isUpdate ? 'animationDelayUpdate' : 'animationDelay'
        );
S
sushuang 已提交
1070 1071
        if (typeof animationDelay === 'function') {
            animationDelay = animationDelay(
1072
                dataIndex as number,
S
sushuang 已提交
1073
                animatableModel.getAnimationDelayParams
1074
                    ? animatableModel.getAnimationDelayParams(el, dataIndex as number)
S
sushuang 已提交
1075 1076
                    : null
            );
P
pah100 已提交
1077
        }
S
sushuang 已提交
1078
        if (typeof duration === 'function') {
1079
            duration = duration(dataIndex as number);
1080 1081
        }

S
sushuang 已提交
1082
        duration > 0
1083 1084 1085 1086 1087 1088 1089
            ? el.animateTo(props, {
                duration,
                delay: animationDelay || 0,
                easing: animationEasing,
                done: cb,
                force: !!cb
            })
S
sushuang 已提交
1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103
            : (el.stopAnimation(), el.attr(props), cb && cb());
    }
    else {
        el.stopAnimation();
        el.attr(props);
        cb && cb();
    }
}

/**
 * Update graphic element properties with or without animation according to the
 * configuration in series.
 *
 * Caution: this method will stop previous animation.
1104
 * So do not use this method to one element twice before
S
sushuang 已提交
1105 1106 1107 1108 1109 1110 1111 1112 1113 1114
 * animation starts, unless you know what you are doing.
 * @example
 *     graphic.updateProps(el, {
 *         position: [100, 100]
 *     }, seriesModel, dataIndex, function () { console.log('Animation done!'); });
 *     // Or
 *     graphic.updateProps(el, {
 *         position: [100, 100]
 *     }, seriesModel, function () { console.log('Animation done!'); });
 */
1115
function updateProps<Props>(
P
pissang 已提交
1116
    el: Element<Props>,
1117
    props: Props,
P
pissang 已提交
1118 1119
    // TODO: TYPE AnimatableModel
    animatableModel?: Model<AnimationOptionMixin>,
1120
    dataIndex?: number | (() => void),
P
pissang 已提交
1121 1122
    cb?: () => void
) {
S
sushuang 已提交
1123
    animateOrSetProps(true, el, props, animatableModel, dataIndex, cb);
S
sushuang 已提交
1124
}
S
sushuang 已提交
1125

1126 1127
export {updateProps};

S
sushuang 已提交
1128 1129 1130 1131 1132
/**
 * Init graphic element properties with or without animation according to the
 * configuration in series.
 *
 * Caution: this method will stop previous animation.
1133
 * So do not use this method to one element twice before
S
sushuang 已提交
1134 1135
 * animation starts, unless you know what you are doing.
 */
1136
export function initProps<Props>(
P
pissang 已提交
1137
    el: Element<Props>,
1138
    props: Props,
P
pissang 已提交
1139
    animatableModel?: Model<AnimationOptionMixin>,
1140
    dataIndex?: number | (() => void),
P
pissang 已提交
1141 1142
    cb?: () => void
) {
S
sushuang 已提交
1143
    animateOrSetProps(false, el, props, animatableModel, dataIndex, cb);
S
sushuang 已提交
1144
}
S
sushuang 已提交
1145 1146 1147 1148 1149

/**
 * Get transform matrix of target (param target),
 * in coordinate of its ancestor (param ancestor)
 *
P
pissang 已提交
1150 1151
 * @param target
 * @param [ancestor]
S
sushuang 已提交
1152
 */
1
100pah 已提交
1153
export function getTransform(target: Transformable, ancestor?: Transformable): matrix.MatrixArray {
1154
    const mat = matrix.identity([]);
S
sushuang 已提交
1155 1156 1157 1158 1159

    while (target && target !== ancestor) {
        matrix.mul(mat, target.getLocalTransform(), mat);
        target = target.parent;
    }
1160

S
sushuang 已提交
1161
    return mat;
S
sushuang 已提交
1162
}
S
sushuang 已提交
1163 1164 1165

/**
 * Apply transform to an vertex.
P
pissang 已提交
1166 1167
 * @param target [x, y]
 * @param transform Can be:
S
sushuang 已提交
1168 1169
 *      + Transform matrix: like [1, 0, 0, 1, 0, 0]
 *      + {position, rotation, scale}, the same as `zrender/Transformable`.
P
pissang 已提交
1170 1171
 * @param invert Whether use invert matrix.
 * @return [x, y]
S
sushuang 已提交
1172
 */
P
pissang 已提交
1173 1174 1175 1176
export function applyTransform(
    target: vector.VectorArray,
    transform: Transformable | matrix.MatrixArray,
    invert?: boolean
P
pissang 已提交
1177
): number[] {
1178
    if (transform && !isArrayLike(transform)) {
S
sushuang 已提交
1179 1180
        transform = Transformable.getLocalTransform(transform);
    }
L
lang 已提交
1181

S
sushuang 已提交
1182
    if (invert) {
P
pissang 已提交
1183
        transform = matrix.invert([], transform as matrix.MatrixArray);
S
sushuang 已提交
1184
    }
P
pissang 已提交
1185
    return vector.applyTransform([], target, transform as matrix.MatrixArray);
S
sushuang 已提交
1186
}
S
sushuang 已提交
1187 1188

/**
P
pissang 已提交
1189 1190 1191 1192
 * @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'
S
sushuang 已提交
1193
 */
P
pissang 已提交
1194 1195 1196 1197 1198
export function transformDirection(
    direction: 'left' | 'right' | 'top' | 'bottom',
    transform: matrix.MatrixArray,
    invert?: boolean
): 'left' | 'right' | 'top' | 'bottom' {
S
sushuang 已提交
1199 1200

    // Pick a base, ensure that transform result will not be (0, 0).
1201
    const hBase = (transform[4] === 0 || transform[5] === 0 || transform[0] === 0)
S
sushuang 已提交
1202
        ? 1 : Math.abs(2 * transform[4] / transform[0]);
1203
    const vBase = (transform[4] === 0 || transform[5] === 0 || transform[2] === 0)
S
sushuang 已提交
1204 1205
        ? 1 : Math.abs(2 * transform[4] / transform[2]);

1206
    let vertex: vector.VectorArray = [
S
sushuang 已提交
1207 1208 1209 1210
        direction === 'left' ? -hBase : direction === 'right' ? hBase : 0,
        direction === 'top' ? -vBase : direction === 'bottom' ? vBase : 0
    ];

S
sushuang 已提交
1211
    vertex = applyTransform(vertex, transform, invert);
S
sushuang 已提交
1212 1213 1214 1215

    return Math.abs(vertex[0]) > Math.abs(vertex[1])
        ? (vertex[0] > 0 ? 'right' : 'left')
        : (vertex[1] > 0 ? 'bottom' : 'top');
S
sushuang 已提交
1216
}
S
sushuang 已提交
1217

P
pissang 已提交
1218 1219 1220 1221 1222 1223
function isNotGroup(el: Element): el is Displayable {
    return !el.isGroup;
}
function isPath(el: Displayable): el is Path {
    return (el as Path).shape != null;
}
S
sushuang 已提交
1224 1225 1226 1227
/**
 * Apply group transition animation from g1 to g2.
 * If no animatableModel, no animation.
 */
P
pissang 已提交
1228 1229 1230 1231 1232
export function groupTransition(
    g1: Group,
    g2: Group,
    animatableModel: Model<AnimationOptionMixin>
) {
S
sushuang 已提交
1233 1234 1235
    if (!g1 || !g2) {
        return;
    }
L
lang 已提交
1236

P
pissang 已提交
1237
    function getElMap(g: Group) {
1238
        const elMap: Dictionary<Displayable> = {};
P
pissang 已提交
1239 1240
        g.traverse(function (el: Element) {
            if (isNotGroup(el) && el.anid) {
P
pissang 已提交
1241
                elMap[el.anid] = el;
L
lang 已提交
1242 1243
            }
        });
S
sushuang 已提交
1244 1245
        return elMap;
    }
P
pissang 已提交
1246
    function getAnimatableProps(el: Displayable) {
1247
        const obj: PathProps = {
1248 1249
            x: el.x,
            y: el.y,
S
sushuang 已提交
1250 1251
            rotation: el.rotation
        };
P
pissang 已提交
1252
        if (isPath(el)) {
1253
            obj.shape = extend({}, el.shape);
1254
        }
S
sushuang 已提交
1255 1256
        return obj;
    }
1257
    const elMap1 = getElMap(g1);
S
sushuang 已提交
1258 1259

    g2.traverse(function (el) {
P
pissang 已提交
1260
        if (isNotGroup(el) && el.anid) {
1261
            const oldEl = elMap1[el.anid];
S
sushuang 已提交
1262
            if (oldEl) {
1263
                const newProp = getAnimatableProps(el);
S
sushuang 已提交
1264
                el.attr(getAnimatableProps(oldEl));
1265
                updateProps(el, newProp, animatableModel, getECData(el).dataIndex);
S
sushuang 已提交
1266
            }
S
sushuang 已提交
1267
        }
S
sushuang 已提交
1268
    });
S
sushuang 已提交
1269
}
S
sushuang 已提交
1270

1
100pah 已提交
1271
export function clipPointsByRect(points: vector.VectorArray[], rect: ZRRectLike): number[][] {
S
sushuang 已提交
1272 1273
    // FIXME: this way migth be incorrect when grpahic clipped by a corner.
    // and when element have border.
1274
    return map(points, function (point) {
1275
        let x = point[0];
S
sushuang 已提交
1276 1277
        x = mathMax(x, rect.x);
        x = mathMin(x, rect.x + rect.width);
1278
        let y = point[1];
S
sushuang 已提交
1279 1280 1281 1282
        y = mathMax(y, rect.y);
        y = mathMin(y, rect.y + rect.height);
        return [x, y];
    });
S
sushuang 已提交
1283
}
S
sushuang 已提交
1284 1285

/**
P
pissang 已提交
1286
 * Return a new clipped rect. If rect size are negative, return undefined.
S
sushuang 已提交
1287
 */
1288
export function clipRectByRect(targetRect: ZRRectLike, rect: ZRRectLike): ZRRectLike {
1289 1290 1291 1292
    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);
S
sushuang 已提交
1293

S
sushuang 已提交
1294 1295
    // If the total rect is cliped, nothing, including the border,
    // should be painted. So return undefined.
S
sushuang 已提交
1296 1297 1298 1299 1300 1301 1302 1303
    if (x2 >= x && y2 >= y) {
        return {
            x: x,
            y: y,
            width: x2 - x,
            height: y2 - y
        };
    }
S
sushuang 已提交
1304
}
S
sushuang 已提交
1305

P
pissang 已提交
1306 1307 1308
export function createIcon(
    iconStr: string,    // Support 'image://' or 'path://' or direct svg path.
    opt?: Omit<DisplayableProps, 'style'>,
1309
    rect?: ZRRectLike
1
100pah 已提交
1310
): SVGPath | ZRImage {
1311
    const innerOpts: DisplayableProps = extend({rectHover: true}, opt);
P
pissang 已提交
1312
    const style: ZRStyleProps = innerOpts.style = {strokeNoScale: true};
S
sushuang 已提交
1313 1314 1315 1316 1317
    rect = rect || {x: -1, y: -1, width: 2, height: 2};

    if (iconStr) {
        return iconStr.indexOf('image://') === 0
            ? (
P
pissang 已提交
1318
                (style as ImageStyleProps).image = iconStr.slice(8),
1319
                defaults(style, rect),
1
100pah 已提交
1320
                new ZRImage(innerOpts)
S
sushuang 已提交
1321 1322
            )
            : (
S
sushuang 已提交
1323
                makePath(
S
sushuang 已提交
1324
                    iconStr.replace('path://', ''),
P
pissang 已提交
1325
                    innerOpts,
S
sushuang 已提交
1326 1327 1328 1329 1330
                    rect,
                    'center'
                )
            );
    }
S
sushuang 已提交
1331
}
S
sushuang 已提交
1332

1333 1334 1335 1336 1337 1338
/**
 * 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.
 */
P
pissang 已提交
1339 1340 1341 1342
export function linePolygonIntersect(
    a1x: number, a1y: number, a2x: number, a2y: number,
    points: vector.VectorArray[]
): boolean {
1343
    for (let i = 0, p2 = points[points.length - 1]; i < points.length; i++) {
1344
        const p = points[i];
1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357
        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.
 */
P
pissang 已提交
1358 1359 1360 1361
export function lineLineIntersect(
    a1x: number, a1y: number, a2x: number, a2y: number,
    b1x: number, b1y: number, b2x: number, b2y: number
): boolean {
1362
    // let `vec_m` to be `vec_a2 - vec_a1` and `vec_n` to be `vec_b2 - vec_b1`.
1363 1364 1365 1366
    const mx = a2x - a1x;
    const my = a2y - a1y;
    const nx = b2x - b1x;
    const ny = b2y - b1y;
1367 1368 1369

    // `vec_m` and `vec_n` are parallel iff
    //     exising `k` such that `vec_m = k · vec_n`, equivalent to `vec_m X vec_n = 0`.
1370
    const nmCrossProduct = crossProduct2d(nx, ny, mx, my);
1371 1372 1373 1374 1375 1376 1377 1378
    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)`.
1379 1380 1381
    const b1a1x = a1x - b1x;
    const b1a1y = a1y - b1y;
    const q = crossProduct2d(b1a1x, b1a1y, mx, my) / nmCrossProduct;
1382 1383 1384
    if (q < 0 || q > 1) {
        return false;
    }
1385
    const p = crossProduct2d(b1a1x, b1a1y, nx, ny) / nmCrossProduct;
1386 1387 1388 1389 1390 1391 1392 1393 1394 1395
    if (p < 0 || p > 1) {
        return false;
    }

    return true;
}

/**
 * Cross product of 2-dimension vector.
 */
P
pissang 已提交
1396
function crossProduct2d(x1: number, y1: number, x2: number, y2: number) {
1397 1398 1399
    return x1 * y2 - x2 * y1;
}

P
pissang 已提交
1400
function nearZero(val: number) {
1401 1402 1403
    return val <= (1e-6) && val >= -(1e-6);
}

1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415

/**
 * ECData stored on graphic element
 */
export interface ECData {
    dataIndex?: number;
    dataModel?: DataModel;
    eventData?: ECEventData;
    seriesIndex?: number;
    dataType?: string;
}

1416
export const getECData = makeInner<ECData, Element>();
1417

1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429
// Register built-in shapes. These shapes might be overwirtten
// by users, although we do not recommend that.
registerShape('circle', Circle);
registerShape('sector', Sector);
registerShape('ring', Ring);
registerShape('polygon', Polygon);
registerShape('polyline', Polyline);
registerShape('rect', Rect);
registerShape('line', Line);
registerShape('bezierCurve', BezierCurve);
registerShape('arc', Arc);

S
sushuang 已提交
1430 1431
export {
    Group,
1
100pah 已提交
1432 1433
    ZRImage as Image,
    ZRText as Text,
S
sushuang 已提交
1434 1435 1436 1437 1438 1439 1440 1441 1442
    Circle,
    Sector,
    Ring,
    Polygon,
    Polyline,
    Rect,
    Line,
    BezierCurve,
    Arc,
P
pissang 已提交
1443
    IncrementalDisplayable,
S
sushuang 已提交
1444 1445 1446
    CompoundPath,
    LinearGradient,
    RadialGradient,
P
pissang 已提交
1447 1448
    BoundingRect,
    Path
1449
};