custom.ts 70.3 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.
*/

P
pissang 已提交
20
// @ts-nocheck
S
sushuang 已提交
21
import {__DEV__} from '../config';
22
import {
23
    hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, keys, isArrayLike, bind
24
} from 'zrender/src/core/util';
S
sushuang 已提交
25
import * as graphicUtil from '../util/graphic';
26 27
import { enableElementHoverEmphasis, setAsHighDownDispatcher } from '../util/states';
import * as labelStyleHelper from '../label/labelStyle';
28
import {getDefaultLabel} from './helper/labelHelper';
S
sushuang 已提交
29
import createListFromArray from './helper/createListFromArray';
30
import {getLayoutOnAxis} from '../layout/barGrid';
S
sushuang 已提交
31
import DataDiffer from '../data/DataDiffer';
32
import SeriesModel from '../model/Series';
33
import Model from '../model/Model';
34
import ChartView from '../view/Chart';
35
import {createClipPath} from './helper/createClipPathFromCoordSys';
1
100pah 已提交
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
import {
    EventQueryItem, ECEvent, SeriesOption, SeriesOnCartesianOptionMixin,
    SeriesOnPolarOptionMixin, SeriesOnSingleOptionMixin, SeriesOnGeoOptionMixin,
    SeriesOnCalendarOptionMixin, ItemStyleOption, SeriesEncodeOptionMixin,
    SeriesTooltipOption,
    DimensionLoose,
    ParsedValue,
    Dictionary,
    CallbackDataParams,
    Payload,
    StageHandlerProgressParams,
    LabelOption,
    ViewRootGroup,
    OptionDataValue,
    ZRStyleProps,
    DisplayState,
    ECElement,
    DisplayStateNonNormal
} from '../util/types';
import Element, { ElementProps, ElementTextConfig } from 'zrender/src/Element';
S
sushuang 已提交
56 57 58 59 60
import prepareCartesian2d from '../coord/cartesian/prepareCustom';
import prepareGeo from '../coord/geo/prepareCustom';
import prepareSingleAxis from '../coord/single/prepareCustom';
import preparePolar from '../coord/polar/prepareCustom';
import prepareCalendar from '../coord/calendar/prepareCustom';
1
100pah 已提交
61 62 63
import ComponentModel from '../model/Component';
import List, { DefaultDataVisual } from '../data/List';
import GlobalModel from '../model/Global';
64
import { makeInner, normalizeToArray } from '../util/model';
1
100pah 已提交
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
import ExtensionAPI from '../ExtensionAPI';
import Displayable from 'zrender/src/graphic/Displayable';
import Axis2D from '../coord/cartesian/Axis2D';
import { RectLike } from 'zrender/src/core/BoundingRect';
import { PathProps } from 'zrender/src/graphic/Path';
import { ImageStyleProps } from 'zrender/src/graphic/Image';
import { CoordinateSystem } from '../coord/CoordinateSystem';
import { TextStyleProps } from 'zrender/src/graphic/Text';
import {
    convertToEC4StyleForCustomSerise,
    isEC4CompatibleStyle,
    convertFromEC4CompatibleStyle,
    LegacyStyleProps,
    warnDeprecated
} from '../util/styleCompat';
import Transformable from 'zrender/src/core/Transformable';
import { ItemStyleProps } from '../model/mixin/itemStyle';
82
import { cloneValue } from 'zrender/src/animation/Animator';
1
100pah 已提交
83 84 85 86 87 88 89


const inner = makeInner<{
    info: CustomExtraElementInfo;
    customPathData: string;
    customGraphicType: string;
    customImagePath: CustomImageOption['style']['image'];
90
    // customText: string;
1
100pah 已提交
91
    txConZ2Set: number;
92 93
    leaveToProps: ElementProps;
    userDuring: CustomBaseElementOption['during'];
1
100pah 已提交
94 95 96
}, Element>();

type CustomExtraElementInfo = Dictionary<unknown>;
97 98 99 100 101 102 103 104 105 106
const TRANSFORM_PROPS = {
    x: 1,
    y: 1,
    scaleX: 1,
    scaleY: 1,
    originX: 1,
    originY: 1,
    rotation: 1
} as const;
type TransformProps = keyof typeof TRANSFORM_PROPS;
107
const transformPropNamesStr = keys(TRANSFORM_PROPS).join(', ');
108 109 110 111 112

type TransitionAnyProps = string | string[];
type TransitionTransformProps = TransformProps | TransformProps[];
// Do not declare "Dictionary" in TransitionAnyOption to restrict the type check.
type TransitionAnyOption = {
113 114 115
    transition?: TransitionAnyProps;
    enterFrom?: Dictionary<unknown>;
    leaveTo?: Dictionary<unknown>;
116 117
};
type TransitionTransformOption = {
118 119 120
    transition?: TransitionTransformProps;
    enterFrom?: Dictionary<unknown>;
    leaveTo?: Dictionary<unknown>;
121
};
1
100pah 已提交
122 123 124

interface CustomBaseElementOption extends Partial<Pick<
    Element, TransformProps | 'silent' | 'ignore' | 'textConfig'
125
>>, TransitionTransformOption {
1
100pah 已提交
126 127 128 129 130 131 132 133
    // element type, mandatory.
    type: string;
    id?: string;
    // For animation diff.
    name?: string;
    info?: CustomExtraElementInfo;
    // `false` means remove the textContent.
    textContent?: CustomTextOption | false;
134 135
    // `false` means remove the clipPath
    clipPath?: CustomZRPathOption | false;
136 137
    // `extra` can be set in any el option for custom prop for annimation duration.
    extra?: TransitionAnyOption;
138
    // updateDuringAnimation
139
    during?(params: typeof customDuringAPI): void;
1
100pah 已提交
140 141 142 143
};
interface CustomDisplayableOption extends CustomBaseElementOption, Partial<Pick<
    Displayable, 'zlevel' | 'z' | 'z2' | 'invisible'
>> {
144
    style?: ZRStyleProps & TransitionAnyOption;
1
100pah 已提交
145 146 147 148 149 150 151 152
    // `false` means remove emphasis trigger.
    styleEmphasis?: ZRStyleProps | false;
    emphasis?: CustomDisplayableOptionOnState;
}
interface CustomDisplayableOptionOnState extends Partial<Pick<
    Displayable, TransformProps | 'textConfig' | 'z2'
>> {
    // `false` means remove emphasis trigger.
153
    style?: (ZRStyleProps & TransitionAnyOption) | false;
1
100pah 已提交
154 155 156 157 158
}
interface CustomGroupOption extends CustomBaseElementOption {
    type: 'group';
    width?: number;
    height?: number;
159
    // @deprecated
1
100pah 已提交
160 161 162 163
    diffChildrenByName?: boolean;
    children: CustomElementOption[];
    $mergeChildren: false | 'byName' | 'byIndex';
}
164 165
interface CustomZRPathOption extends CustomDisplayableOption {
    shape?: PathProps['shape'] & TransitionAnyOption;
1
100pah 已提交
166 167 168 169 170 171 172 173 174 175 176 177 178
}
interface CustomSVGPathOption extends CustomDisplayableOption {
    type: 'path';
    shape?: {
        // SVG Path, like 'M0,0 L0,-20 L70,-1 L70,0 Z'
        pathData?: string;
        // "d" is the alias of `pathData` follows the SVG convention.
        d?: string;
        layout?: 'center' | 'cover';
        x?: number;
        y?: number;
        width?: number;
        height?: number;
179
    } & TransitionAnyOption;
1
100pah 已提交
180 181 182
}
interface CustomImageOption extends CustomDisplayableOption {
    type: 'image';
183
    style?: ImageStyleProps & TransitionAnyOption;
1
100pah 已提交
184 185 186
    emphasis?: CustomImageOptionOnState;
}
interface CustomImageOptionOnState extends CustomDisplayableOptionOnState {
187
    style?: ImageStyleProps & TransitionAnyOption;
1
100pah 已提交
188 189 190 191 192 193 194 195 196 197 198 199
}
interface CustomTextOption extends CustomDisplayableOption {
    type: 'text';
}
type CustomElementOption = CustomZRPathOption | CustomSVGPathOption | CustomImageOption | CustomTextOption;
type CustomElementOptionOnState = CustomDisplayableOptionOnState | CustomImageOptionOnState;


interface CustomSeriesRenderItemAPI extends
        CustomSeriesRenderItemCoordinateSystemAPI,
        Pick<ExtensionAPI, 'getWidth' | 'getHeight' | 'getZr' | 'getDevicePixelRatio'> {
    value(dim: DimensionLoose, dataIndexInside?: number): ParsedValue;
200 201
    style(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps;
    styleEmphasis(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps;
1
100pah 已提交
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
    visual(visualType: string, dataIndexInside?: number): ReturnType<List['getItemVisual']>;
    barLayout(opt: Omit<Parameters<typeof getLayoutOnAxis>[0], 'axis'>): ReturnType<typeof getLayoutOnAxis>;
    currentSeriesIndices(): ReturnType<GlobalModel['getCurrentSeriesIndices']>;
    font(opt: Parameters<typeof graphicUtil.getFont>[0]): ReturnType<typeof graphicUtil.getFont>;
}
interface CustomSeriesRenderItemParamsCoordSys {
    type: string;
    // And extra params for each coordinate systems.
}
interface CustomSeriesRenderItemCoordinateSystemAPI {
    coord(
        data: OptionDataValue | OptionDataValue[],
        clamp?: boolean
    ): number[];
    size?(
        dataSize: OptionDataValue | OptionDataValue[],
        dataItem: OptionDataValue | OptionDataValue[]
    ): number | number[];
}
interface CustomSeriesRenderItemParams {
1
100pah 已提交
222
    context: Dictionary<unknown>;
1
100pah 已提交
223 224 225 226 227
    seriesId: string;
    seriesName: string;
    seriesIndex: number;
    coordSys: CustomSeriesRenderItemParamsCoordSys;
    dataInsideLength: number;
1
100pah 已提交
228
    encode: ReturnType<typeof wrapEncodeDef>;
1
100pah 已提交
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
}
type CustomSeriesRenderItem = (
    params: CustomSeriesRenderItemParams,
    api: CustomSeriesRenderItemAPI
) => CustomElementOption;


interface CustomSeriesOption extends
    SeriesOption,
    SeriesEncodeOptionMixin,
    SeriesOnCartesianOptionMixin,
    SeriesOnPolarOptionMixin,
    SeriesOnSingleOptionMixin,
    SeriesOnGeoOptionMixin,
    SeriesOnCalendarOptionMixin {

    // If set as 'none', do not depends on coord sys.
    coordinateSystem?: string | 'none';

    renderItem?: CustomSeriesRenderItem;
S
sushuang 已提交
249

1
100pah 已提交
250 251
    // Only works on polar and cartesian2d coordinate system.
    clip?: boolean;
S
sushuang 已提交
252

1
100pah 已提交
253 254 255 256 257 258 259 260 261 262 263
    // FIXME needed?
    tooltip?: SeriesTooltipOption;

    itemStyle?: ItemStyleOption;
    label?: LabelOption;
    emphasis?: {
        itemStyle?: ItemStyleOption;
        label?: LabelOption;
    };
}

264 265 266 267 268
interface LooseElementProps extends ElementProps {
    style?: ZRStyleProps;
    shape?: Dictionary<unknown>;
}

1
100pah 已提交
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
// Also compat with ec4, where
// `visual('color') visual('borderColor')` is supported.
const STYLE_VISUAL_TYPE = {
    color: 'fill',
    borderColor: 'stroke'
} as const;

const VISUAL_PROPS = {
    symbol: 1,
    symbolSize: 1,
    symbolKeepAspect: 1,
    legendSymbol: 1,
    visualMeta: 1,
    liftZ: 1
} as const;

const EMPHASIS = 'emphasis' as const;
const NORMAL = 'normal' as const;
const PATH_ITEM_STYLE = {
    normal: ['itemStyle'],
    emphasis: [EMPHASIS, 'itemStyle']
} as const;
const PATH_LABEL = {
    normal: ['label'],
    emphasis: [EMPHASIS, 'label']
} as const;
S
sushuang 已提交
295
// Use prefix to avoid index to be the same as el.name,
1
100pah 已提交
296
// which will cause weird update animation.
297
const GROUP_DIFF_PREFIX = 'e\0\0';
S
sushuang 已提交
298

1
100pah 已提交
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
type AttachedTxInfo = {
    isLegacy: boolean;
    normal: {
        cfg: ElementTextConfig;
        conOpt: CustomElementOption | false;
    };
    emphasis: {
        cfg: ElementTextConfig;
        conOpt: CustomElementOptionOnState;
    };
};
const attachedTxInfoTmp = {
    normal: {},
    emphasis: {}
} as AttachedTxInfo;

const Z2_SPECIFIED_BIT = {
    normal: 0,
    emphasis: 1
} as const;

320 321 322 323 324 325 326
const LEGACY_TRANSFORM_PROPS = {
    position: ['x', 'y'],
    scale: ['scaleX', 'scaleY'],
    origin: ['originX', 'originY']
} as const;
type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS;

1
100pah 已提交
327 328 329 330
export type PrepareCustomInfo = (coordSys: CoordinateSystem) => {
    coordSys: CustomSeriesRenderItemParamsCoordSys;
    api: CustomSeriesRenderItemCoordinateSystemAPI
};
331

S
sushuang 已提交
332 333 334 335 336 337 338 339 340 341 342
/**
 * To reduce total package size of each coordinate systems, the modules `prepareCustom`
 * of each coordinate systems are not required by each coordinate systems directly, but
 * required by the module `custom`.
 *
 * prepareInfoForCustomSeries {Function}: optional
 *     @return {Object} {coordSys: {...}, api: {
 *         coord: function (data, clamp) {}, // return point in global.
 *         size: function (dataSize, dataItem) {} // return size of each axis in coordSys.
 *     }}
 */
1
100pah 已提交
343
const prepareCustoms: Dictionary<PrepareCustomInfo> = {
S
sushuang 已提交
344 345 346 347 348 349 350
    cartesian2d: prepareCartesian2d,
    geo: prepareGeo,
    singleAxis: prepareSingleAxis,
    polar: preparePolar,
    calendar: prepareCalendar
};

1
100pah 已提交
351
class CustomSeriesModel extends SeriesModel<CustomSeriesOption> {
352

1
100pah 已提交
353 354
    static type = 'series.custom';
    readonly type = CustomSeriesModel.type;
S
sushuang 已提交
355

1
100pah 已提交
356
    static dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar'];
S
sushuang 已提交
357

1
100pah 已提交
358
    preventAutoZ = true;
S
sushuang 已提交
359

1
100pah 已提交
360 361
    currentZLevel: number;
    currentZ: number;
S
sushuang 已提交
362

1
100pah 已提交
363
    static defaultOption: CustomSeriesOption = {
S
sushuang 已提交
364 365 366
        coordinateSystem: 'cartesian2d', // Can be set as 'none'
        zlevel: 0,
        z: 2,
367 368
        legendHoverLink: true,

369 370 371 372
        // Custom series will not clip by default.
        // Some case will use custom series to draw label
        // For example https://echarts.apache.org/examples/en/editor.html?c=custom-gantt-flight
        clip: false
S
sushuang 已提交
373 374 375 376 377 378 379 380 381 382 383 384 385

        // Cartesian coordinate system
        // xAxisIndex: 0,
        // yAxisIndex: 0,

        // Polar coordinate system
        // polarIndex: 0,

        // Geo coordinate system
        // geoIndex: 0,

        // label: {}
        // itemStyle: {}
1
100pah 已提交
386
    };
S
sushuang 已提交
387

1
100pah 已提交
388 389 390 391 392 393
    optionUpdated() {
        this.currentZLevel = this.get('zlevel', true);
        this.currentZ = this.get('z', true);
    }

    getInitialData(option: CustomSeriesOption, ecModel: GlobalModel): List {
394
        return createListFromArray(this.getSource(), this);
1
100pah 已提交
395
    }
396

1
100pah 已提交
397 398 399 400 401
    getDataParams(dataIndex: number, dataType: string, el: Element): CallbackDataParams & {
        info: CustomExtraElementInfo
    } {
        const params = super.getDataParams(dataIndex, dataType, el) as ReturnType<CustomSeriesModel['getDataParams']>;
        el && (params.info = inner(el).info);
402
        return params;
S
sushuang 已提交
403
    }
1
100pah 已提交
404
}
P
pah100 已提交
405

1
100pah 已提交
406
ComponentModel.registerClass(CustomSeriesModel);
P
pah100 已提交
407 408 409



1
100pah 已提交
410
class CustomSeriesView extends ChartView {
P
pah100 已提交
411

1
100pah 已提交
412 413 414 415 416 417 418 419 420 421 422 423
    static type = 'custom';
    readonly type = CustomSeriesView.type;

    private _data: List;


    render(
        customSeries: CustomSeriesModel,
        ecModel: GlobalModel,
        api: ExtensionAPI,
        payload: Payload
    ): void {
424 425 426 427
        const oldData = this._data;
        const data = customSeries.getData();
        const group = this.group;
        const renderItem = makeRenderItem(customSeries, data, ecModel, api);
S
sushuang 已提交
428

429 430 431 432 433
        // By default, merge mode is applied. In most cases, custom series is
        // used in the scenario that data amount is not large but graphic elements
        // is complicated, where merge mode is probably necessary for optimization.
        // For example, reuse graphic elements and only update the transform when
        // roam or data zoom according to `actionType`.
S
sushuang 已提交
434 435
        data.diff(oldData)
            .add(function (newIdx) {
436
                createOrUpdateItem(
437
                    null, newIdx, renderItem(newIdx, payload), customSeries, group, data
S
sushuang 已提交
438 439 440
                );
            })
            .update(function (newIdx, oldIdx) {
441
                createOrUpdateItem(
442 443
                    oldData.getItemGraphicEl(oldIdx),
                    newIdx, renderItem(newIdx, payload), customSeries, group, data
444
                );
S
sushuang 已提交
445 446
            })
            .remove(function (oldIdx) {
447
                doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group);
S
sushuang 已提交
448 449
            })
            .execute();
P
pah100 已提交
450

451
        // Do clipping
452
        const clipPath = customSeries.get('clip', true)
453 454 455 456 457 458 459 460 461
            ? createClipPath(customSeries.coordinateSystem, false, customSeries)
            : null;
        if (clipPath) {
            group.setClipPath(clipPath);
        }
        else {
            group.removeClipPath();
        }

S
sushuang 已提交
462
        this._data = data;
1
100pah 已提交
463
    }
P
pah100 已提交
464

1
100pah 已提交
465 466 467 468 469
    incrementalPrepareRender(
        customSeries: CustomSeriesModel,
        ecModel: GlobalModel,
        api: ExtensionAPI
    ): void {
P
pissang 已提交
470 471
        this.group.removeAll();
        this._data = null;
1
100pah 已提交
472
    }
P
pissang 已提交
473

1
100pah 已提交
474 475 476 477 478 479 480
    incrementalRender(
        params: StageHandlerProgressParams,
        customSeries: CustomSeriesModel,
        ecModel: GlobalModel,
        api: ExtensionAPI,
        payload: Payload
    ): void {
481 482
        const data = customSeries.getData();
        const renderItem = makeRenderItem(customSeries, data, ecModel, api);
1
100pah 已提交
483
        function setIncrementalAndHoverLayer(el: Displayable) {
P
pissang 已提交
484 485 486 487 488
            if (!el.isGroup) {
                el.incremental = true;
                el.useHoverLayer = true;
            }
        }
489
        for (let idx = params.start; idx < params.end; idx++) {
490
            const el = createOrUpdateItem(null, idx, renderItem(idx, payload), customSeries, this.group, data);
P
pissang 已提交
491 492
            el.traverse(setIncrementalAndHoverLayer);
        }
1
100pah 已提交
493
    }
494

1
100pah 已提交
495
    filterForExposedEvent(
496 497
        eventType: string, query: EventQueryItem, targetEl: Element, packedEvent: ECEvent
    ): boolean {
498
        const elementName = query.element;
499
        if (elementName == null || targetEl.name === elementName) {
500 501 502 503 504 505
            return true;
        }

        // Enable to give a name on a group made by `renderItem`, and listen
        // events that triggerd by its descendents.
        while ((targetEl = targetEl.parent) && targetEl !== this.group) {
506
            if (targetEl.name === elementName) {
507 508 509 510 511 512
                return true;
            }
        }

        return false;
    }
1
100pah 已提交
513
}
P
pah100 已提交
514

1
100pah 已提交
515
ChartView.registerClass(CustomSeriesView);
P
pah100 已提交
516 517


1
100pah 已提交
518
function createEl(elOption: CustomElementOption): Element {
519
    const graphicType = elOption.type;
520
    let el;
S
sushuang 已提交
521

522 523
    // Those graphic elements are not shapes. They should not be
    // overwritten by users, so do them first.
S
sushuang 已提交
524
    if (graphicType === 'path') {
1
100pah 已提交
525
        const shape = (elOption as CustomSVGPathOption).shape;
S
sushuang 已提交
526
        // Using pathRect brings convenience to users sacle svg path.
527
        const pathRect = (shape.width != null && shape.height != null)
S
sushuang 已提交
528
            ? {
S
sushuang 已提交
529 530
                x: shape.x || 0,
                y: shape.y || 0,
S
sushuang 已提交
531 532
                width: shape.width,
                height: shape.height
1
100pah 已提交
533
            } as RectLike
S
sushuang 已提交
534
            : null;
535
        const pathData = getPathData(shape);
S
sushuang 已提交
536 537
        // Path is also used for icon, so layout 'center' by default.
        el = graphicUtil.makePath(pathData, null, pathRect, shape.layout || 'center');
1
100pah 已提交
538
        inner(el).customPathData = pathData;
S
sushuang 已提交
539 540
    }
    else if (graphicType === 'image') {
541
        el = new graphicUtil.Image({});
1
100pah 已提交
542
        inner(el).customImagePath = (elOption as CustomImageOption).style.image;
S
sushuang 已提交
543 544
    }
    else if (graphicType === 'text') {
545
        el = new graphicUtil.Text({});
546
        // inner(el).customText = (elOption.style as TextStyleProps).text;
S
sushuang 已提交
547
    }
548 549 550 551 552 553
    else if (graphicType === 'group') {
        el = new graphicUtil.Group();
    }
    else if (graphicType === 'compoundPath') {
        throw new Error('"compoundPath" is not supported yet.');
    }
S
sushuang 已提交
554
    else {
555
        const Clz = graphicUtil.getShapeClass(graphicType);
P
pah100 已提交
556

S
sushuang 已提交
557
        if (__DEV__) {
558
            assert(Clz, 'graphic type "' + graphicType + '" can not be found.');
P
pah100 已提交
559 560
        }

S
sushuang 已提交
561 562
        el = new Clz();
    }
P
pah100 已提交
563

1
100pah 已提交
564
    inner(el).customGraphicType = graphicType;
S
sushuang 已提交
565
    el.name = elOption.name;
P
pah100 已提交
566

1
100pah 已提交
567 568 569 570 571
    // Compat ec4: the default z2 lift is 1. If changing the number,
    // some cases probably be broken: hierarchy layout along z, like circle packing,
    // where emphasis only intending to modify color/border rather than lift z2.
    (el as ECElement).z2EmphasisLift = 1;

S
sushuang 已提交
572 573
    return el;
}
P
pah100 已提交
574

1
100pah 已提交
575
/**
576 577
 * ----------------------------------------------------------
 * [STRATEGY_MERGE] Merge properties or erase all properties:
1
100pah 已提交
578
 *
579
 * Based on the fact that the existing zr element probably be reused, we now consider whether
1
100pah 已提交
580
 * merge or erase all properties to the exsiting elements.
581 582 583
 * That is, if a certain props is not specified in the lastest return of `renderItem`:
 * + "Merge" means that do not modify the value on the existing element.
 * + "Erase all" means that use a default value to the existing element.
1
100pah 已提交
584 585
 *
 * "Merge" might bring some unexpected state retaining for users and "erase all" seams to be
586 587 588 589
 * more safe. "erase all" force users to specify all of the props each time, which is recommanded
 * in most cases.
 * But "erase all" theoretically disables the chance of performance optimization (e.g., just
 * generete shape and style at the first time rather than always do that).
1
100pah 已提交
590 591 592 593 594
 * So we still use "merge" rather than "erase all". If users need "erase all", they can
 * simple always set all of the props each time.
 * Some "object-like" config like `textConfig`, `textContent`, `style` which are not needed for
 * every elment, so we replace them only when user specify them. And the that is a total replace.
 *
595 596 597 598 599 600 601
 * TODO: there is no hint of 'isFirst' to users. So the performance enhancement can not be
 * performed yet. Consider the case:
 * (1) setOption to "mergeChildren" with a smaller children count
 * (2) Use dataZoom to make an item disappear.
 * (3) User dataZoom to make the item display again. At that time, renderItem need to return the
 * full option rather than partial option to recreate the element.
 *
602 603
 * ----------------------------------------------
 * [STRATEGY_NULL] `hasOwnProperty` or `== null`:
1
100pah 已提交
604 605 606 607 608 609 610
 *
 * Ditinguishing "own property" probably bring little trouble to user when make el options.
 * So we  trade a {xx: null} or {xx: undefined} as "not specified" if possible rather than
 * "set them to null/undefined". In most cases, props can not be cleared. Some typicall
 * clearable props like `style`/`textConfig`/`textContent` we enable `false` to means
 * "clear". In some othere special cases that the prop is able to set as null/undefined,
 * but not suitable to use `false`, `hasOwnProperty` is checked.
611 612 613 614
 *
 * ---------------------------------------------
 * [STRATEGY_TRANSITION] The rule of transition:
 * + For props on the root level of a element:
615
 *      If there is no `transition` specified, tansform props will be transitioned by default,
616 617
 *      which is the same as the previous setting in echarts4 and suitable for the scenario
 *      of dataZoom change.
618
 *      If `transition` specified, only the specified props will be transitioned.
619
 * + For props in `shape` and `style`:
620
 *      Only props specified in `transition` will be transitioned.
621 622 623
 * + Break:
 *      Since ec5, do not make transition to shape by default, because it might result in
 *      performance issue (especially `points` of polygon) and do not necessary in most cases.
1
100pah 已提交
624 625 626 627 628
 */
function updateElNormal(
    el: Element,
    dataIndex: number,
    elOption: CustomElementOption,
629
    styleOpt: CustomElementOption['style'],
1
100pah 已提交
630 631 632 633 634
    attachedTxInfo: AttachedTxInfo,
    seriesModel: CustomSeriesModel,
    isInit: boolean,
    isTextContent: boolean
): void {
635 636
    const transFromProps = {} as ElementProps;
    const allProps = {} as ElementProps;
1
100pah 已提交
637 638
    const elDisplayable = el.isGroup ? null : el as Displayable;

639 640
    prepareShapeOrExtraUpdate('shape', el, elOption, allProps, transFromProps, isInit);
    prepareShapeOrExtraUpdate('extra', el, elOption, allProps, transFromProps, isInit);
641
    prepareTransformUpdate(el, elOption, allProps, transFromProps, isInit);
1
100pah 已提交
642 643 644 645 646 647

    const txCfgOpt = attachedTxInfo && attachedTxInfo.normal.cfg;
    if (txCfgOpt) {
        // PENDING: whether use user object directly rather than clone?
        // TODO:5.0 textConfig transition animation?
        el.setTextConfig(txCfgOpt);
P
pah100 已提交
648 649
    }

1
100pah 已提交
650 651 652
    if (el.type === 'text' && styleOpt) {
        const textOptionStyle = styleOpt as TextStyleProps;
        // Compatible with ec4: if `textFill` or `textStroke` exists use them.
653
        hasOwn(textOptionStyle, 'textFill') && (
1
100pah 已提交
654
            textOptionStyle.fill = (textOptionStyle as any).textFill
S
sushuang 已提交
655
        );
656
        hasOwn(textOptionStyle, 'textStroke') && (
1
100pah 已提交
657
            textOptionStyle.stroke = (textOptionStyle as any).textStroke
S
sushuang 已提交
658 659
        );
    }
P
pah100 已提交
660

661 662
    prepareStyleUpdate(el, styleOpt, transFromProps, isInit);

1
100pah 已提交
663 664 665 666
    if (elDisplayable) {
        // PENDING: here the input style object is used directly.
        // Good for performance but bad for compatibility control.
        styleOpt && elDisplayable.useStyle(styleOpt);
P
pah100 已提交
667

668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688
        // When style object changed, how to trade the existing animation?
        // It is probably conplicated and not needed to cover all the cases.
        // But still need consider the case:
        // (1) When using init animation on `style.opacity`, and before the animation
        //     ended users triggers an update by mousewhell. At that time the init
        //     animation should better be continued rather than terminated.
        //     So after `useStyle` called, we should change the animation target manually
        //     to continue the effect of the init animation.
        // (2) PENDING: If the previous animation targeted at a `val1`, and currently we need
        //     to update the value to `val2` and no animation declared, should be terminate
        //     the previous animation or just modify the target of the animation?
        //     Therotically That will happen not only on `style` but also on `shape` and
        //     `transfrom` props. But we haven't handle this case at present yet.
        // (3) PENDING: Is it proper to visit `animators` and `targetName`?
        const animators = elDisplayable.animators;
        for (let i = 0; i < animators.length; i++) {
            const animator = animators[i];
            // targetName is the "topKey".
            if (animator.targetName === 'style') {
                animator.changeTarget(elDisplayable.style);
            }
P
pah100 已提交
689
        }
1
100pah 已提交
690

691
        hasOwn(elOption, 'invisible') && (elDisplayable.invisible = elOption.invisible);
S
sushuang 已提交
692
    }
P
pah100 已提交
693

694 695 696 697 698 699 700 701 702 703
    // Do not use `el.updateDuringAnimation` here becuase `el.updateDuringAnimation` will
    // be called mutiple time in each animation frame. For example, if both "transform" props
    // and shape props and style props changed, it will generate three animator and called
    // one-by-one in each animation frame.
    // We use the during in `animateTo/From` params.
    const userDuring = elOption.during;
    // For simplicity, if during not specified, the previous during will not work any more.
    inner(el).userDuring = userDuring;
    const cfgDuringCall = userDuring ? bind(duringCall, { el: el, userDuring: userDuring }) : null;

704
    el.attr(allProps);
705 706 707 708 709
    const cfg = {
        dataIndex: dataIndex,
        isFrom: true,
        during: cfgDuringCall
    };
710
    isInit
711 712
        ? graphicUtil.initProps(el, transFromProps, seriesModel, cfg)
        : graphicUtil.updateProps(el, transFromProps, seriesModel, cfg);
P
pah100 已提交
713

714
    // Merge by default.
715 716
    hasOwn(elOption, 'silent') && (el.silent = elOption.silent);
    hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore);
1
100pah 已提交
717 718 719 720 721

    if (!isTextContent) {
        // `elOption.info` enables user to mount some info on
        // elements and use them in event handlers.
        // Update them only when user specified, otherwise, remain.
722
        hasOwn(elOption, 'info') && (inner(el).info = elOption.info);
S
sushuang 已提交
723
    }
1
100pah 已提交
724

725
    styleOpt ? el.dirty() : el.markRedraw();
1
100pah 已提交
726 727
}

728

729
// See [STRATEGY_TRANSITION]
730 731
function prepareShapeOrExtraUpdate(
    mainAttr: 'shape' | 'extra',
732 733 734 735 736 737 738
    el: Element,
    elOption: CustomElementOption,
    allProps: LooseElementProps,
    transFromProps: LooseElementProps,
    isInit: boolean
): void {

739 740
    const attrOpt: Dictionary<unknown> & TransitionAnyOption = (elOption as any)[mainAttr];
    if (!attrOpt) {
741 742 743
        return;
    }

744 745
    const elPropsInAttr = (el as LooseElementProps)[mainAttr];
    let transFromPropsInAttr: Dictionary<unknown>;
746

747
    const enterFrom = attrOpt.enterFrom;
748
    if (isInit && enterFrom) {
749
        !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {});
750 751
        const enterFromKeys = keys(enterFrom);
        for (let i = 0; i < enterFromKeys.length; i++) {
752 753
            // `enterFrom` props are not necessarily also declared in `shape`/`style`/...,
            // for example, `opacity` can only declared in `enterFrom` but not in `style`.
754 755
            const key = enterFromKeys[i];
            // Do not clone, animator will perform that clone.
756
            transFromPropsInAttr[key] = enterFrom[key];
757 758 759
        }
    }

760 761 762
    if (!isInit && elPropsInAttr && attrOpt.transition) {
        !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {});
        const transitionKeys = normalizeToArray(attrOpt.transition);
763 764
        for (let i = 0; i < transitionKeys.length; i++) {
            const key = transitionKeys[i];
765
            const elVal = elPropsInAttr[key];
766
            if (__DEV__) {
767
                checkTansitionRefer(key, (attrOpt as any)[key], elVal);
768 769
            }
            // Do not clone, see `checkTansitionRefer`.
770
            transFromPropsInAttr[key] = elVal;
771 772 773
        }
    }

774 775 776 777
    const allPropsInAttr = allProps[mainAttr] = {} as Dictionary<unknown>;
    const keysInAttr = keys(attrOpt);
    for (let i = 0; i < keysInAttr.length; i++) {
        const key = keysInAttr[i];
778 779
        // To avoid share one object with different element, and
        // to avoid user modify the object inexpectedly, have to clone.
780
        allPropsInAttr[key] = cloneValue((attrOpt as any)[key]);
781 782
    }

783
    const leaveTo = attrOpt.leaveTo;
784 785
    if (leaveTo) {
        const leaveToProps = getOrCreateLeaveToPropsFromEl(el);
786
        const leaveToPropsInAttr: Dictionary<unknown> = leaveToProps[mainAttr] || (leaveToProps[mainAttr] = {});
787 788 789
        const leaveToKeys = keys(leaveTo);
        for (let i = 0; i < leaveToKeys.length; i++) {
            const key = leaveToKeys[i];
790
            leaveToPropsInAttr[key] = leaveTo[key];
791 792 793 794 795 796 797 798 799 800 801 802
        }
    }
}

// See [STRATEGY_TRANSITION].
function prepareTransformUpdate(
    el: Element,
    elOption: CustomElementOption,
    allProps: ElementProps,
    transFromProps: ElementProps,
    isInit: boolean
): void {
803
    const enterFrom = elOption.enterFrom;
804 805 806 807 808
    if (isInit && enterFrom) {
        const enterFromKeys = keys(enterFrom);
        for (let i = 0; i < enterFromKeys.length; i++) {
            const key = enterFromKeys[i] as TransformProps;
            if (__DEV__) {
809
                checkTransformPropRefer(key, 'el.enterFrom');
810 811 812 813 814 815 816
            }
            // Do not clone, animator will perform that clone.
            transFromProps[key] = enterFrom[key] as number;
        }
    }

    if (!isInit) {
817 818
        if (elOption.transition) {
            const transitionKeys = normalizeToArray(elOption.transition);
819 820 821 822
            for (let i = 0; i < transitionKeys.length; i++) {
                const key = transitionKeys[i];
                const elVal = el[key];
                if (__DEV__) {
823
                    checkTransformPropRefer(key, 'el.transition');
824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848
                    checkTansitionRefer(key, elOption[key], elVal);
                }
                // Do not clone, see `checkTansitionRefer`.
                transFromProps[key] = elVal;
            }
        }
        // This default transition see [STRATEGY_TRANSITION]
        else {
            setLagecyProp(elOption, transFromProps, 'position', el);
            setTransProp(elOption, transFromProps, 'x', el);
            setTransProp(elOption, transFromProps, 'y', el);
        }
    }

    setLagecyProp(elOption, allProps, 'position');
    setLagecyProp(elOption, allProps, 'scale');
    setLagecyProp(elOption, allProps, 'origin');
    setTransProp(elOption, allProps, 'x');
    setTransProp(elOption, allProps, 'y');
    setTransProp(elOption, allProps, 'scaleX');
    setTransProp(elOption, allProps, 'scaleY');
    setTransProp(elOption, allProps, 'originX');
    setTransProp(elOption, allProps, 'originY');
    setTransProp(elOption, allProps, 'rotation');

849
    const leaveTo = elOption.leaveTo;
850 851 852 853 854 855
    if (leaveTo) {
        const leaveToProps = getOrCreateLeaveToPropsFromEl(el);
        const leaveToKeys = keys(leaveTo);
        for (let i = 0; i < leaveToKeys.length; i++) {
            const key = leaveToKeys[i] as TransformProps;
            if (__DEV__) {
856
                checkTransformPropRefer(key, 'el.leaveTo');
857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876
            }
            leaveToProps[key] = leaveTo[key] as number;
        }
    }
}

// See [STRATEGY_TRANSITION].
function prepareStyleUpdate(
    el: Element,
    styleOpt: CustomElementOption['style'],
    transFromProps: LooseElementProps,
    isInit: boolean
): void {
    if (!styleOpt) {
        return;
    }

    const elStyle = (el as LooseElementProps).style as LooseElementProps['style'];
    let transFromStyleProps: LooseElementProps['style'];

877
    const enterFrom = styleOpt.enterFrom;
878 879 880 881 882 883 884 885 886 887
    if (isInit && enterFrom) {
        const enterFromKeys = keys(enterFrom);
        !transFromStyleProps && (transFromStyleProps = transFromProps.style = {});
        for (let i = 0; i < enterFromKeys.length; i++) {
            const key = enterFromKeys[i];
            // Do not clone, animator will perform that clone.
            (transFromStyleProps as any)[key] = enterFrom[key];
        }
    }

888 889
    if (!isInit && elStyle && styleOpt.transition) {
        const transitionKeys = normalizeToArray(styleOpt.transition);
890 891 892 893 894 895 896 897 898 899 900 901
        !transFromStyleProps && (transFromStyleProps = transFromProps.style = {});
        for (let i = 0; i < transitionKeys.length; i++) {
            const key = transitionKeys[i];
            const elVal = (elStyle as any)[key];
            if (__DEV__) {
                checkTansitionRefer(key, (styleOpt as any)[key], elVal);
            }
            // Do not clone, see `checkTansitionRefer`.
            (transFromStyleProps as any)[key] = elVal;
        }
    }

902
    const leaveTo = styleOpt.leaveTo;
903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940
    if (leaveTo) {
        const leaveToKeys = keys(leaveTo);
        const leaveToProps = getOrCreateLeaveToPropsFromEl(el);
        const leaveToStyleProps = leaveToProps.style || (leaveToProps.style = {});
        for (let i = 0; i < leaveToKeys.length; i++) {
            const key = leaveToKeys[i];
            (leaveToStyleProps as any)[key] = leaveTo[key];
        }
    }
}

function checkTansitionRefer(propName: string, optVal: unknown, elVal: unknown): void {
    const isArrLike = isArrayLike(optVal);
    assert(
        isArrLike || (optVal != null && isFinite(optVal as number)),
        'Prop `' + propName + '` must refer to a finite number or ArrayLike for transition.'
    );
    // Try not to copy array for performance, but if user use the same object in different
    // call of `renderItem`, it will casue animation transition fail.
    assert(
        !isArrLike || optVal !== elVal,
        'Prop `' + propName + '` must use different Array object each time for transition.'
    );
}

function checkTransformPropRefer(key: string, usedIn: string): void {
    assert(
        hasOwn(TRANSFORM_PROPS, key),
        'Prop `' + key + '` is not a permitted in `' + usedIn + '`. '
            + 'Only `' + keys(TRANSFORM_PROPS).join('`, `') + '` are permitted.'
    );
}

function getOrCreateLeaveToPropsFromEl(el: Element): LooseElementProps {
    const innerEl = inner(el);
    return innerEl.leaveToProps || (innerEl.leaveToProps = {});
}

941 942 943 944 945 946 947 948
// Use it to avoid it be exposed to user.
const tmpDuringScope = {} as {
    el: Element;
    isShapeDirty: boolean;
    isStyleDirty: boolean;
};
const customDuringAPI = {
    // Usually other props do not need to be changed in animation during.
949
    setTransform(key: TransformProps, val: unknown) {
950 951 952
        if (__DEV__) {
            assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `setTransform`.');
        }
953
        tmpDuringScope.el[key] = val as number;
954
        return this;
955
    },
956
    getTransform(key: TransformProps): unknown {
957 958 959
        if (__DEV__) {
            assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.');
        }
960 961
        return tmpDuringScope.el[key];
    },
962
    setShape(key: string, val: unknown) {
963 964 965 966 967
        if (__DEV__) {
            assertNotReserved(key);
        }
        const shape = (tmpDuringScope.el as graphicUtil.Path).shape
            || ((tmpDuringScope.el as graphicUtil.Path).shape = {});
968 969
        shape[key] = val;
        tmpDuringScope.isShapeDirty = true;
970
        return this;
971 972
    },
    getShape(key: string): unknown {
973 974 975 976
        if (__DEV__) {
            assertNotReserved(key);
        }
        const shape = (tmpDuringScope.el as graphicUtil.Path).shape;
977 978 979 980
        if (shape) {
            return shape[key];
        }
    },
981
    setStyle(key: string, val: unknown) {
982 983 984
        if (__DEV__) {
            assertNotReserved(key);
        }
985 986 987 988 989
        const style = (tmpDuringScope.el as Displayable).style;
        if (style) {
            style[key] = val;
            tmpDuringScope.isStyleDirty = true;
        }
990
        return this;
991 992
    },
    getStyle(key: string): unknown {
993 994 995
        if (__DEV__) {
            assertNotReserved(key);
        }
996 997 998 999
        const style = (tmpDuringScope.el as Displayable).style;
        if (style) {
            return style[key];
        }
1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017
    },
    setExtra(key: string, val: unknown) {
        if (__DEV__) {
            assertNotReserved(key);
        }
        const extra = (tmpDuringScope.el as LooseElementProps).extra
            || ((tmpDuringScope.el as LooseElementProps).extra = {});
        extra[key] = val;
        return this;
    },
    getExtra(key: string): unknown {
        if (__DEV__) {
            assertNotReserved(key);
        }
        const extra = (tmpDuringScope.el as LooseElementProps).extra;
        if (extra) {
            return extra[key];
        }
1018 1019 1020
    }
};

1021 1022 1023 1024 1025 1026 1027 1028
function assertNotReserved(key: string) {
    if (__DEV__) {
        if (key === 'transition' || key === 'enterFrom' || key === 'leaveTo') {
            throw new Error('key must not be "' + key + '"');
        }
    }
}

1029 1030 1031 1032 1033 1034
function duringCall(
    this: {
        el: Element;
        userDuring: CustomBaseElementOption['during']
    }
): void {
1035 1036 1037 1038 1039
    // Do not provide "percent" until some requirements come.
    // Because consider thies case:
    // enterFrom: {x: 100, y: 30}, transition: 'x'.
    // And enter duration is different from update duration.
    // Thus it might be confused about the meaning of "percent" in during callback.
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060
    const scope = this;
    const el = scope.el;
    if (!el) {
        return;
    }
    // If el is remove from zr by reason like legend, during still need to called,
    // becuase el will be added back to zr and the prop value should not be incorrect.

    const newstUserDuring = inner(el).userDuring;
    const scopeUserDuring = scope.userDuring;
    // Ensured a during is only called once in each animation frame.
    // If a during is called multiple times in one frame, maybe some users' calulation logic
    // might be wrong (not sure whether this usage exists).
    // The case of a during might be called twice can be: by default there is a animator for
    // 'x', 'y' when init. Before the init animation finished, call `setOption` to start
    // another animators for 'style'/'shape'/'extra'.
    if (newstUserDuring !== scopeUserDuring) {
        // release
        scope.el = scope.userDuring = null;
        return;
    }
1061

1062
    tmpDuringScope.el = el;
1063 1064 1065
    tmpDuringScope.isShapeDirty = false;
    tmpDuringScope.isStyleDirty = false;

1066 1067
    // Give no `this` to user in "during" calling.
    scopeUserDuring(customDuringAPI);
1068

1069 1070
    if (tmpDuringScope.isShapeDirty && (el as graphicUtil.Path).dirtyShape) {
        (el as graphicUtil.Path).dirtyShape();
1071
    }
1072 1073
    if (tmpDuringScope.isStyleDirty && (el as Displayable).dirtyStyle) {
        (el as Displayable).dirtyStyle();
1074 1075
    }
    // markRedraw() will be called by default in during.
1076
    // FIXME `this.markRedraw();` directly ?
1077 1078 1079

    // FIXME: if in future meet the case that some prop will be both modified in `during` and `state`,
    // consider the issue that the prop might be incorrect when return to "normal" state.
1080 1081
}

1
100pah 已提交
1082 1083 1084 1085
function updateElOnState(
    state: DisplayStateNonNormal,
    el: Element,
    elStateOpt: CustomElementOptionOnState,
1086
    styleOpt: CustomElementOptionOnState['style'],
1
100pah 已提交
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116
    attachedTxInfo: AttachedTxInfo,
    isRoot: boolean,
    isTextContent: boolean
): void {
    const elDisplayable = el.isGroup ? null : el as Displayable;
    const txCfgOpt = attachedTxInfo && attachedTxInfo[state].cfg;

    // PENDING:5.0 support customize scale change and transition animation?

    if (elDisplayable) {
        // By default support auto lift color when hover whether `emphasis` specified.
        const stateObj = elDisplayable.ensureState(state);
        if (styleOpt === false) {
            const existingEmphasisState = elDisplayable.getState(state);
            if (existingEmphasisState) {
                existingEmphasisState.style = null;
            }
        }
        else {
            // style is needed to enable defaut emphasis.
            stateObj.style = styleOpt || {};
        }
        // If `elOption.styleEmphasis` or `elOption.emphasis.style` is `false`,
        // remove hover style.
        // If `elOption.textConfig` or `elOption.emphasis.textConfig` is null/undefined, it does not
        // make sense. So for simplicity, we do not ditinguish `hasOwnProperty` and null/undefined.
        if (txCfgOpt) {
            stateObj.textConfig = txCfgOpt;
        }

1117
        enableElementHoverEmphasis(elDisplayable);
S
sushuang 已提交
1118
    }
P
pah100 已提交
1119

1120
    if (isRoot) {
1121
        setAsHighDownDispatcher(el, styleOpt !== false);
1
100pah 已提交
1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191
    }
}

function updateZ(
    el: Element,
    elOption: CustomElementOption,
    seriesModel: CustomSeriesModel,
    attachedTxInfo: AttachedTxInfo
): void {
    // Group not support textContent and not support z yet.
    if (el.isGroup) {
        return;
    }

    const elDisplayable = el as Displayable;
    const currentZ = seriesModel.currentZ;
    const currentZLevel = seriesModel.currentZLevel;
    // Always erase.
    elDisplayable.z = currentZ;
    elDisplayable.zlevel = currentZLevel;
    // z2 must not be null/undefined, otherwise sort error may occur.
    const optZ2 = elOption.z2;
    optZ2 != null && (elDisplayable.z2 = optZ2 || 0);

    const textContent = elDisplayable.getTextContent();
    if (textContent) {
        textContent.z = currentZ;
        textContent.zlevel = currentZLevel;
    }

    updateZForEachState(elDisplayable, textContent, elOption, attachedTxInfo, NORMAL);
    updateZForEachState(elDisplayable, textContent, elOption, attachedTxInfo, EMPHASIS);
}

function updateZForEachState(
    elDisplayable: Displayable,
    textContent: Displayable,
    elOption: CustomDisplayableOption,
    attachedTxInfo: AttachedTxInfo,
    state: DisplayState
): void {
    const isNormal = state === NORMAL;
    const elStateOpt = isNormal ? elOption : retrieveStateOption(elOption, state as DisplayStateNonNormal);
    const optZ2 = elStateOpt ? elStateOpt.z2 : null;
    let stateObj;
    if (optZ2 != null) {
        // Do not `ensureState` until required.
        stateObj = isNormal ? elDisplayable : elDisplayable.ensureState(state);
        stateObj.z2 = optZ2 || 0;
    }

    const txConOpt = attachedTxInfo[state].conOpt;
    if (textContent) {
        const innerEl = inner(elDisplayable);
        const txConZ2Set = innerEl.txConZ2Set || 0;
        const txOptZ2 = txConOpt ? txConOpt.z2 : null;
        const z2SetMask = 1 << Z2_SPECIFIED_BIT[state];

        // Set textContent z2 as hostEl.z2 + 1 only if
        // textContent z2 is not specified.
        if (txOptZ2 != null) {
            // Do not `ensureState` until required.
            (isNormal ? textContent : textContent.ensureState(state)).z2 = txOptZ2;
            innerEl.txConZ2Set = txConZ2Set | z2SetMask;
        }
        // If stateObj exists, that means stateObj.z2 has been updated, where the textContent z2
        // should be followed, no matter textContent or textContent.emphasis is specified in elOption.
        else if (stateObj && (txConZ2Set & z2SetMask) === 0) {
            (isNormal ? textContent : textContent.ensureState(state)).z2 = stateObj.z2 + 1;
        }
1192
    }
S
sushuang 已提交
1193
}
P
pah100 已提交
1194

1
100pah 已提交
1195 1196
function setLagecyProp(
    elOption: CustomElementOption,
1197 1198 1199
    targetProps: Partial<Pick<Transformable, TransformProps>>,
    legacyName: LegacyTransformProp,
    fromEl?: Element // If provided, retrieve from the element.
1
100pah 已提交
1200 1201
): void {
    const legacyArr = (elOption as any)[legacyName];
1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212
    const xyName = LEGACY_TRANSFORM_PROPS[legacyName];
    if (legacyArr) {
        if (fromEl) {
            targetProps[xyName[0]] = fromEl[xyName[0]];
            targetProps[xyName[1]] = fromEl[xyName[1]];
        }
        else {
            targetProps[xyName[0]] = legacyArr[0];
            targetProps[xyName[1]] = legacyArr[1];
        }
    }
1
100pah 已提交
1213
}
1214

1
100pah 已提交
1215 1216
function setTransProp(
    elOption: CustomElementOption,
1217 1218 1219
    targetProps: Partial<Pick<Transformable, TransformProps>>,
    name: TransformProps,
    fromEl?: Element // If provided, retrieve from the element.
1
100pah 已提交
1220
): void {
1221 1222
    if (elOption[name] != null) {
        targetProps[name] = fromEl ? fromEl[name] : elOption[name];
P
pah100 已提交
1223
    }
S
sushuang 已提交
1224 1225
}

1
100pah 已提交
1226 1227 1228 1229 1230 1231
function makeRenderItem(
    customSeries: CustomSeriesModel,
    data: List<CustomSeriesModel>,
    ecModel: GlobalModel,
    api: ExtensionAPI
) {
1232 1233
    const renderItem = customSeries.get('renderItem');
    const coordSys = customSeries.coordinateSystem;
1
100pah 已提交
1234
    let prepareResult = {} as ReturnType<PrepareCustomInfo>;
S
sushuang 已提交
1235 1236 1237

    if (coordSys) {
        if (__DEV__) {
1238 1239
            assert(renderItem, 'series.render is required.');
            assert(
S
sushuang 已提交
1240 1241 1242
                coordSys.prepareCustoms || prepareCustoms[coordSys.type],
                'This coordSys does not support custom series.'
            );
P
pah100 已提交
1243 1244
        }

1
100pah 已提交
1245
        // `coordSys.prepareCustoms` is used for external coord sys like bmap.
S
sushuang 已提交
1246
        prepareResult = coordSys.prepareCustoms
1
100pah 已提交
1247
            ? coordSys.prepareCustoms(coordSys)
S
sushuang 已提交
1248 1249
            : prepareCustoms[coordSys.type](coordSys);
    }
P
pah100 已提交
1250

1251
    const userAPI = defaults({
S
sushuang 已提交
1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262
        getWidth: api.getWidth,
        getHeight: api.getHeight,
        getZr: api.getZr,
        getDevicePixelRatio: api.getDevicePixelRatio,
        value: value,
        style: style,
        styleEmphasis: styleEmphasis,
        visual: visual,
        barLayout: barLayout,
        currentSeriesIndices: currentSeriesIndices,
        font: font
1
100pah 已提交
1263
    }, prepareResult.api || {}) as CustomSeriesRenderItemAPI;
S
sushuang 已提交
1264

1
100pah 已提交
1265
    const userParams: CustomSeriesRenderItemParams = {
S
tweak.  
sushuang 已提交
1266 1267 1268
        // The life cycle of context: current round of rendering.
        // The global life cycle is probably not necessary, because
        // user can store global status by themselves.
S
sushuang 已提交
1269 1270 1271 1272 1273 1274 1275 1276
        context: {},
        seriesId: customSeries.id,
        seriesName: customSeries.name,
        seriesIndex: customSeries.seriesIndex,
        coordSys: prepareResult.coordSys,
        dataInsideLength: data.count(),
        encode: wrapEncodeDef(customSeries.getData())
    };
P
pah100 已提交
1277

1
100pah 已提交
1278 1279 1280 1281
    // If someday intending to refactor them to a class, should consider do not
    // break change: currently these attribute member are encapsulated in a closure
    // so that do not need to force user to call these method with a scope.

S
sushuang 已提交
1282
    // Do not support call `api` asynchronously without dataIndexInside input.
1
100pah 已提交
1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321
    let currDataIndexInside: number;
    let currItemModel: Model<CustomSeriesOption>;
    let currItemStyleModels: Partial<Record<DisplayState, Model<CustomSeriesOption['itemStyle']>>> = {};
    let currLabelModels: Partial<Record<DisplayState, Model<CustomSeriesOption['label']>>> = {};

    const seriesItemStyleModels = {
        normal: customSeries.getModel(PATH_ITEM_STYLE.normal),
        emphasis: customSeries.getModel(PATH_ITEM_STYLE.emphasis)
    } as Record<DisplayState, Model<CustomSeriesOption['label']>>;
    const seriesLabelModels = {
        normal: customSeries.getModel(PATH_LABEL.normal),
        emphasis: customSeries.getModel(PATH_LABEL.emphasis)
    } as Record<DisplayState, Model<CustomSeriesOption['label']>>;

    function getItemModel(dataIndexInside: number): Model<CustomSeriesOption> {
        return dataIndexInside === currDataIndexInside
            ? (currItemModel || (currItemModel = data.getItemModel(dataIndexInside)))
            : data.getItemModel(dataIndexInside);
    }
    function getItemStyleModel(dataIndexInside: number, state: DisplayState) {
        return !data.hasItemOption
            ? seriesItemStyleModels[state]
            : dataIndexInside === currDataIndexInside
            ? (currItemStyleModels[state] || (
                currItemStyleModels[state] = getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state])
            ))
            : getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]);
    }
    function getLabelModel(dataIndexInside: number, state: DisplayState) {
        return !data.hasItemOption
            ? seriesLabelModels[state]
            : dataIndexInside === currDataIndexInside
            ? (currLabelModels[state] || (
                currLabelModels[state] = getItemModel(dataIndexInside).getModel(PATH_LABEL[state])
            ))
            : getItemModel(dataIndexInside).getModel(PATH_LABEL[state]);
    }

    return function (dataIndexInside: number, payload: Payload): CustomElementOption {
S
sushuang 已提交
1322
        currDataIndexInside = dataIndexInside;
1
100pah 已提交
1323 1324 1325
        currItemModel = null;
        currItemStyleModels = {};
        currLabelModels = {};
1326

S
sushuang 已提交
1327
        return renderItem && renderItem(
1328
            defaults({
S
sushuang 已提交
1329
                dataIndexInside: dataIndexInside,
1330 1331 1332
                dataIndex: data.getRawIndex(dataIndexInside),
                // Can be used for optimization when zoom or roam.
                actionType: payload ? payload.type : null
S
sushuang 已提交
1333 1334
            }, userParams),
            userAPI
S
sushuang 已提交
1335
        );
S
sushuang 已提交
1336
    };
P
pah100 已提交
1337

S
sushuang 已提交
1338 1339
    /**
     * @public
1
100pah 已提交
1340 1341
     * @param dim by default 0.
     * @param dataIndexInside by default `currDataIndexInside`.
S
sushuang 已提交
1342
     */
1
100pah 已提交
1343
    function value(dim?: DimensionLoose, dataIndexInside?: number): ParsedValue {
S
sushuang 已提交
1344 1345 1346
        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
        return data.get(data.getDimension(dim || 0), dataIndexInside);
    }
P
pah100 已提交
1347

S
sushuang 已提交
1348
    /**
1
100pah 已提交
1349 1350 1351 1352 1353
     * @deprecated The orgininal intention of `api.style` is enable to set itemStyle
     * like other series. But it not necessary and not easy to give a strict definition
     * of what it return. And since echarts5 it needs to be make compat work. So
     * deprecates it since echarts5.
     *
S
sushuang 已提交
1354 1355 1356 1357
     * By default, `visual` is applied to style (to support visualMap).
     * `visual.color` is applied at `fill`. If user want apply visual.color on `stroke`,
     * it can be implemented as:
     * `api.style({stroke: api.visual('color'), fill: null})`;
1
100pah 已提交
1358 1359 1360 1361 1362 1363
     *
     * [Compat]: since ec5, RectText has been separated from its hosts el.
     * so `api.style()` will only return the style from `itemStyle` but not handle `label`
     * any more. But `series.label` config is never published in doc.
     * We still compat it in `api.style()`. But not encourage to use it and will still not
     * to pulish it to doc.
S
sushuang 已提交
1364
     * @public
1
100pah 已提交
1365
     * @param dataIndexInside by default `currDataIndexInside`.
S
sushuang 已提交
1366
     */
1367
    function style(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps {
1
100pah 已提交
1368 1369 1370 1371
        if (__DEV__) {
            warnDeprecated('api.style', 'Please write literal style directly instead.');
        }

S
sushuang 已提交
1372
        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
P
pah100 已提交
1373

1
100pah 已提交
1374 1375 1376
        const style = data.getItemVisual(dataIndexInside, 'style');
        const visualColor = style && style.fill;
        const opacity = style && style.opacity;
P
pah100 已提交
1377

1
100pah 已提交
1378 1379
        let itemStyle = getItemStyleModel(dataIndexInside, NORMAL).getItemStyle();
        visualColor != null && (itemStyle.fill = visualColor);
S
sushuang 已提交
1380
        opacity != null && (itemStyle.opacity = opacity);
P
pah100 已提交
1381

1382
        const opt = {inheritColor: isString(visualColor) ? visualColor : '#000'};
1
100pah 已提交
1383 1384 1385 1386
        const labelModel = getLabelModel(dataIndexInside, NORMAL);
        // Now that the feture of "auto adjust text fill/stroke" has been migrated to zrender
        // since ec5, we should set `isAttached` as `false` here and make compat in
        // `convertToEC4StyleForCustomSerise`.
1387
        const textStyle = labelStyleHelper.createTextStyle(labelModel, null, opt, false, true);
1
100pah 已提交
1388
        textStyle.text = labelModel.getShallow('show')
1389
            ? retrieve2(
1
100pah 已提交
1390
                customSeries.getFormattedLabel(dataIndexInside, NORMAL),
S
sushuang 已提交
1391 1392 1393
                getDefaultLabel(data, dataIndexInside)
            )
            : null;
1394
        const textConfig = labelStyleHelper.createTextConfig(textStyle, labelModel, opt, false);
1
100pah 已提交
1395

1396
        preFetchFromExtra(userProps, itemStyle);
1
100pah 已提交
1397
        itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig);
P
pah100 已提交
1398

1399
        userProps && applyUserPropsAfter(itemStyle, userProps);
1
100pah 已提交
1400
        (itemStyle as LegacyStyleProps).legacy = true;
1401

S
sushuang 已提交
1402 1403
        return itemStyle;
    }
P
pah100 已提交
1404

S
sushuang 已提交
1405
    /**
1
100pah 已提交
1406
     * @deprecated The reason see `api.style()`
S
sushuang 已提交
1407
     * @public
1
100pah 已提交
1408
     * @param dataIndexInside by default `currDataIndexInside`.
S
sushuang 已提交
1409
     */
1410
    function styleEmphasis(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps {
1
100pah 已提交
1411 1412 1413
        if (__DEV__) {
            warnDeprecated('api.styleEmphasis', 'Please write literal style directly instead.');
        }
1414

1
100pah 已提交
1415
        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
S
sushuang 已提交
1416

1
100pah 已提交
1417 1418
        let itemStyle = getItemStyleModel(dataIndexInside, EMPHASIS).getItemStyle();
        const labelModel = getLabelModel(dataIndexInside, EMPHASIS);
1419
        const textStyle = labelStyleHelper.createTextStyle(labelModel, null, null, true, true);
1
100pah 已提交
1420
        textStyle.text = labelModel.getShallow('show')
1421
            ? retrieve3(
1
100pah 已提交
1422 1423
                customSeries.getFormattedLabel(dataIndexInside, EMPHASIS),
                customSeries.getFormattedLabel(dataIndexInside, NORMAL),
S
sushuang 已提交
1424 1425 1426
                getDefaultLabel(data, dataIndexInside)
            )
            : null;
1427
        const textConfig = labelStyleHelper.createTextConfig(textStyle, labelModel, null, true);
1
100pah 已提交
1428

1429
        preFetchFromExtra(userProps, itemStyle);
1
100pah 已提交
1430
        itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig);
P
pah100 已提交
1431

1432
        userProps && applyUserPropsAfter(itemStyle, userProps);
1
100pah 已提交
1433
        (itemStyle as LegacyStyleProps).legacy = true;
1434

S
sushuang 已提交
1435
        return itemStyle;
P
pah100 已提交
1436 1437
    }

1438
    function applyUserPropsAfter(itemStyle: ZRStyleProps, extra: ZRStyleProps): void {
1439 1440 1441 1442 1443 1444 1445
        for (const key in extra) {
            if (hasOwn(extra, key)) {
                (itemStyle as any)[key] = (extra as any)[key];
            }
        }
    }

1
100pah 已提交
1446 1447 1448 1449 1450 1451 1452 1453 1454 1455
    function preFetchFromExtra(extra: ZRStyleProps, itemStyle: ItemStyleProps): void {
        // A trick to retrieve those props firstly, which are used to
        // apply auto inside fill/stroke in `convertToEC4StyleForCustomSerise`.
        // (It's not reasonable but only for a degree of compat)
        if (extra) {
            (extra as any).textFill && ((itemStyle as any).textFill = (extra as any).textFill);
            (extra as any).textPosition && ((itemStyle as any).textPosition = (extra as any).textPosition);
        }
    }

S
sushuang 已提交
1456 1457
    /**
     * @public
1
100pah 已提交
1458
     * @param dataIndexInside by default `currDataIndexInside`.
S
sushuang 已提交
1459
     */
1
100pah 已提交
1460 1461 1462 1463
    function visual(
        visualType: keyof DefaultDataVisual,
        dataIndexInside?: number
    ): ReturnType<List['getItemVisual']> {
S
sushuang 已提交
1464
        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
1
100pah 已提交
1465

1466
        if (hasOwn(STYLE_VISUAL_TYPE, visualType)) {
1
100pah 已提交
1467 1468 1469 1470 1471 1472 1473
            const style = data.getItemVisual(dataIndexInside, 'style');
            return style
                ? style[STYLE_VISUAL_TYPE[visualType as keyof typeof STYLE_VISUAL_TYPE]] as any
                : null;
        }
        // Only support these visuals. Other visual might be inner tricky
        // for performance (like `style`), do not expose to users.
1474
        if (hasOwn(VISUAL_PROPS, visualType)) {
1
100pah 已提交
1475 1476
            return data.getItemVisual(dataIndexInside, visualType);
        }
P
pah100 已提交
1477 1478
    }

S
sushuang 已提交
1479 1480
    /**
     * @public
1
100pah 已提交
1481
     * @return If not support, return undefined.
S
sushuang 已提交
1482
     */
1
100pah 已提交
1483 1484 1485 1486 1487
    function barLayout(
        opt: Omit<Parameters<typeof getLayoutOnAxis>[0], 'axis'>
    ): ReturnType<typeof getLayoutOnAxis> {
        if (coordSys.type === 'cartesian2d') {
            const baseAxis = coordSys.getBaseAxis() as Axis2D;
1488
            return getLayoutOnAxis(defaults({axis: baseAxis}, opt));
1489
        }
P
pah100 已提交
1490 1491
    }

S
sushuang 已提交
1492 1493 1494
    /**
     * @public
     */
1
100pah 已提交
1495
    function currentSeriesIndices(): ReturnType<GlobalModel['getCurrentSeriesIndices']> {
S
sushuang 已提交
1496
        return ecModel.getCurrentSeriesIndices();
1497 1498
    }

S
sushuang 已提交
1499 1500
    /**
     * @public
1
100pah 已提交
1501
     * @return font string
S
sushuang 已提交
1502
     */
1
100pah 已提交
1503 1504 1505
    function font(
        opt: Parameters<typeof graphicUtil.getFont>[0]
    ): ReturnType<typeof graphicUtil.getFont> {
1506
        return labelStyleHelper.getFont(opt, ecModel);
S
sushuang 已提交
1507 1508 1509
    }
}

1
100pah 已提交
1510 1511
function wrapEncodeDef(data: List<CustomSeriesModel>): Dictionary<number[]> {
    const encodeDef = {} as Dictionary<number[]>;
1512
    each(data.dimensions, function (dimName, dataDimIndex) {
1513
        const dimInfo = data.getDimensionInfo(dimName);
S
sushuang 已提交
1514
        if (!dimInfo.isExtraCoord) {
1515 1516
            const coordDim = dimInfo.coordDim;
            const dataDims = encodeDef[coordDim] = encodeDef[coordDim] || [];
S
sushuang 已提交
1517 1518 1519 1520 1521 1522
            dataDims[dimInfo.coordDimIndex] = dataDimIndex;
        }
    });
    return encodeDef;
}

1523
function createOrUpdateItem(
1
100pah 已提交
1524 1525 1526 1527 1528 1529 1530
    el: Element,
    dataIndex: number,
    elOption: CustomElementOption,
    seriesModel: CustomSeriesModel,
    group: ViewRootGroup,
    data: List<CustomSeriesModel>
): Element {
1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543
    // [Rule]
    // If `renderItem` returns `null`/`undefined`/`false`, remove the previous el if existing.
    //     (It seems that violate the "merge" principle, but most of users probably intuitively
    //     regard "return;" as "show nothing element whatever", so make a exception to meet the
    //     most cases.)
    // The rule or "merge" see [STRATEGY_MERGE].

    // If `elOption` is `null`/`undefined`/`false` (when `renderItem` returns nothing).
    if (!elOption) {
        el && group.remove(el);
        return;
    }
    el = doCreateOrUpdateEl(el, dataIndex, elOption, seriesModel, group, true);
S
sushuang 已提交
1544
    el && data.setItemGraphicEl(dataIndex, el);
P
pissang 已提交
1545 1546

    return el;
S
sushuang 已提交
1547 1548
}

1549
function doCreateOrUpdateEl(
1
100pah 已提交
1550 1551 1552 1553 1554 1555 1556
    el: Element,
    dataIndex: number,
    elOption: CustomElementOption,
    seriesModel: CustomSeriesModel,
    group: ViewRootGroup,
    isRoot: boolean
): Element {
1557

1558 1559
    if (__DEV__) {
        assert(elOption, 'should not have an null/undefined element setting');
1
100pah 已提交
1560
    }
S
sushuang 已提交
1561

1562
    let toBeReplacedIdx = -1;
S
sushuang 已提交
1563

1564 1565 1566 1567
    if (el && doesElNeedRecreate(el, elOption)) {
        // Should keep at the original index, otherwise "merge by index" will be incorrect.
        toBeReplacedIdx = group.childrenRef().indexOf(el);
        el = null;
1568 1569
    }

1
100pah 已提交
1570 1571 1572 1573 1574 1575
    const isInit = !el;

    if (!el) {
        el = createEl(elOption);
    }
    else {
1576
        // FIMXE:NEXT unified clearState?
1
100pah 已提交
1577 1578 1579
        // If in some case the performance issue arised, consider
        // do not clearState but update cached normal state directly.
        el.clearStates();
1580 1581
    }

1
100pah 已提交
1582 1583 1584 1585 1586 1587 1588 1589
    attachedTxInfoTmp.normal.cfg = attachedTxInfoTmp.normal.conOpt =
        attachedTxInfoTmp.emphasis.cfg = attachedTxInfoTmp.emphasis.conOpt = null;
    attachedTxInfoTmp.isLegacy = false;

    doCreateOrUpdateAttachedTx(
        el, dataIndex, elOption, seriesModel, isInit, attachedTxInfoTmp
    );

1590 1591 1592 1593
    doCreateOrUpdateClipPath(
        el, dataIndex, elOption, seriesModel, isInit
    );

1
100pah 已提交
1594 1595 1596 1597 1598 1599 1600
    const stateOptEmphasis = retrieveStateOption(elOption, EMPHASIS);
    const styleOptEmphasis = retrieveStyleOptionOnState(elOption, stateOptEmphasis, EMPHASIS);

    updateElNormal(el, dataIndex, elOption, elOption.style, attachedTxInfoTmp, seriesModel, isInit, false);
    updateElOnState(EMPHASIS, el, stateOptEmphasis, styleOptEmphasis, attachedTxInfoTmp, isRoot, false);

    updateZ(el, elOption, seriesModel, attachedTxInfoTmp);
S
sushuang 已提交
1601

1602
    if (elOption.type === 'group') {
1
100pah 已提交
1603
        mergeChildren(
1604
            el as graphicUtil.Group, dataIndex, elOption as CustomGroupOption, seriesModel
1
100pah 已提交
1605
        );
1606 1607
    }

1608 1609 1610 1611 1612 1613
    if (toBeReplacedIdx >= 0) {
        group.replaceAt(el, toBeReplacedIdx);
    }
    else {
        group.add(el);
    }
S
sushuang 已提交
1614 1615 1616 1617

    return el;
}

1618 1619 1620
// `el` must not be null/undefined.
function doesElNeedRecreate(el: Element, elOption: CustomElementOption): boolean {
    const elInner = inner(el);
1621
    const elOptionType = elOption.type;
1622
    const elOptionShape = (elOption as CustomZRPathOption).shape;
1623
    const elOptionStyle = elOption.style;
1624
    return (
S
sushuang 已提交
1625
        // If `elOptionType` is `null`, follow the merge principle.
1626 1627
        (elOptionType != null
            && elOptionType !== elInner.customGraphicType
S
sushuang 已提交
1628 1629
        )
        || (elOptionType === 'path'
1630 1631
            && hasOwnPathData(elOptionShape)
            && getPathData(elOptionShape) !== elInner.customPathData
S
sushuang 已提交
1632 1633
        )
        || (elOptionType === 'image'
1634
            && hasOwn(elOptionStyle, 'image')
1635
            && (elOptionStyle as CustomImageOption['style']).image !== elInner.customImagePath
S
sushuang 已提交
1636
        )
1637 1638
        // // FIXME test and remove this restriction?
        // || (elOptionType === 'text'
1639
        //     && hasOwn(elOptionStyle, 'text')
1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659
        //     && (elOptionStyle as TextStyleProps).text !== elInner.customText
        // )
    );
}

function doCreateOrUpdateClipPath(
    el: Element,
    dataIndex: number,
    elOption: CustomElementOption,
    seriesModel: CustomSeriesModel,
    isInit: boolean
): void {
    // Based on the "merge" principle, if no clipPath provided,
    // do nothing. The exists clip will be totally removed only if
    // `el.clipPath` is `false`. Otherwise it will be merged/replaced.
    const clipPathOpt = elOption.clipPath;
    if (clipPathOpt === false) {
        if (el && el.getClipPath()) {
            el.removeClipPath();
        }
1660
    }
1661 1662 1663 1664 1665 1666 1667 1668
    else if (clipPathOpt) {
        let clipPath = el.getClipPath();
        if (clipPath && doesElNeedRecreate(clipPath, clipPathOpt)) {
            clipPath = null;
        }
        if (!clipPath) {
            clipPath = createEl(clipPathOpt) as graphicUtil.Path;
            if (__DEV__) {
1669
                assert(
1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681
                    clipPath instanceof graphicUtil.Path,
                    'Only any type of `path` can be used in `clipPath`, rather than ' + clipPath.type + '.'
                );
            }
            el.setClipPath(clipPath);
        }
        updateElNormal(
            clipPath, dataIndex, clipPathOpt, null, null, seriesModel, isInit, false
        );
    }
    // If not define `clipPath` in option, do nothing unnecessary.
}
1682

1
100pah 已提交
1683 1684 1685 1686 1687 1688 1689 1690 1691 1692
function doCreateOrUpdateAttachedTx(
    el: Element,
    dataIndex: number,
    elOption: CustomElementOption,
    seriesModel: CustomSeriesModel,
    isInit: boolean,
    attachedTxInfo: AttachedTxInfo
): void {
    // group do not support textContent temporarily untill necessary.
    if (el.isGroup) {
S
sushuang 已提交
1693
        return;
1694 1695
    }

1
100pah 已提交
1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737
    // Normal must be called before emphasis, for `isLegacy` detection.
    processTxInfo(elOption, null, attachedTxInfo);
    processTxInfo(elOption, EMPHASIS, attachedTxInfo);

    // If `elOption.textConfig` or `elOption.textContent` is null/undefined, it does not make sence.
    // So for simplicity, if "elOption hasOwnProperty of them but be null/undefined", we do not
    // trade them as set to null to el.
    // Especially:
    // `elOption.textContent: false` means remove textContent.
    // `elOption.textContent.emphasis.style: false` means remove the style from emphasis state.
    let txConOptNormal = attachedTxInfo.normal.conOpt as CustomElementOption | false;
    const txConOptEmphasis = attachedTxInfo.emphasis.conOpt as CustomElementOptionOnState;

    if (txConOptEmphasis != null) {
        // If textContent has emphasis state, el should auto has emphasis
        // state, otherwise it can not be triggered.
        el.ensureState(EMPHASIS);
    }

    if (txConOptNormal != null || txConOptEmphasis != null) {
        let textContent = el.getTextContent();
        if (txConOptNormal === false) {
            textContent && el.removeTextContent();
        }
        else {
            txConOptNormal = attachedTxInfo.normal.conOpt = txConOptNormal || {type: 'text'};
            if (!textContent) {
                textContent = createEl(txConOptNormal) as graphicUtil.Text;
                el.setTextContent(textContent);
            }
            else {
                // If in some case the performance issue arised, consider
                // do not clearState but update cached normal state directly.
                textContent.clearStates();
            }
            const txConStlOptNormal = txConOptNormal && txConOptNormal.style;

            updateElNormal(
                textContent, dataIndex, txConOptNormal, txConStlOptNormal, null, seriesModel, isInit, true
            );
            const txConStlOptEmphasis = retrieveStyleOptionOnState(txConOptNormal, txConOptEmphasis, EMPHASIS);
            updateElOnState(EMPHASIS, textContent, txConOptEmphasis, txConStlOptEmphasis, null, false, true);
S
sushuang 已提交
1738

1739
            txConStlOptNormal ? textContent.dirty() : textContent.markRedraw();
1
100pah 已提交
1740
        }
1741
    }
1
100pah 已提交
1742
}
1743

1
100pah 已提交
1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775
function processTxInfo(
    elOption: CustomElementOption,
    state: DisplayStateNonNormal,
    attachedTxInfo: AttachedTxInfo
): void {
    const stateOpt = !state ? elOption : retrieveStateOption(elOption, state);
    const styleOpt = !state ? elOption.style : retrieveStyleOptionOnState(elOption, stateOpt, EMPHASIS);

    const elType = elOption.type;
    let txCfg = stateOpt ? stateOpt.textConfig : null;
    const txConOptNormal = elOption.textContent;
    let txConOpt: CustomElementOption | CustomElementOptionOnState =
        !txConOptNormal ? null : !state ? txConOptNormal : retrieveStateOption(txConOptNormal, state);

    if (styleOpt && (
        // Because emphasis style has little info to detect legacy,
        // if normal is legacy, emphasis is trade as legacy.
        attachedTxInfo.isLegacy
        || isEC4CompatibleStyle(styleOpt, elType, !!txCfg, !!txConOpt)
    )) {
        attachedTxInfo.isLegacy = true;
        const convertResult = convertFromEC4CompatibleStyle(styleOpt, elType, !state);
        // Explicitly specified `textConfig` and `textContent` has higher priority than
        // the ones generated by legacy style. Otherwise if users use them and `api.style`
        // at the same time, they not both work and hardly to known why.
        if (!txCfg && convertResult.textConfig) {
            txCfg = convertResult.textConfig;
        }
        if (!txConOpt && convertResult.textContent) {
            txConOpt = convertResult.textContent;
        }
    }
S
sushuang 已提交
1776

1
100pah 已提交
1777 1778 1779 1780 1781 1782
    if (!state && txConOpt) {
        const txConOptNormal = txConOpt as CustomElementOption;
        // `textContent: {type: 'text'}`, the "type" is easy to be missing. So we tolerate it.
        !txConOptNormal.type && (txConOptNormal.type = 'text');
        if (__DEV__) {
            // Do not tolerate incorret type for forward compat.
1783
            txConOptNormal.type !== 'text' && assert(
1
100pah 已提交
1784 1785 1786 1787 1788 1789 1790 1791 1792
                txConOptNormal.type === 'text',
                'textContent.type must be "text"'
            );
        }
    }

    const info = !state ? attachedTxInfo.normal : attachedTxInfo[state];
    info.cfg = txCfg;
    info.conOpt = txConOpt;
S
sushuang 已提交
1793 1794
}

1
100pah 已提交
1795 1796 1797 1798 1799 1800 1801 1802 1803 1804
function retrieveStateOption(
    elOption: CustomElementOption, state: DisplayStateNonNormal
): CustomElementOptionOnState {
    return !state ? elOption : elOption ? elOption[state] : null;
}

function retrieveStyleOptionOnState(
    stateOptionNormal: CustomElementOption,
    stateOption: CustomElementOptionOnState,
    state: DisplayStateNonNormal
1805
): CustomElementOptionOnState['style'] {
1
100pah 已提交
1806 1807 1808 1809 1810 1811 1812 1813
    let style = stateOption && stateOption.style;
    if (style == null && state === EMPHASIS && stateOptionNormal) {
        style = stateOptionNormal.styleEmphasis;
    }
    return style;
}


1814 1815 1816 1817 1818 1819 1820 1821 1822 1823
// Usage:
// (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates that
//     the existing children will not be removed, and enables the feature that
//     update some of the props of some of the children simply by construct
//     the returned children of `renderItem` like:
//     `var children = group.children = []; children[3] = {opacity: 0.5};`
// (2) If `elOption.$mergeChildren` is `'byName'`, add/update/remove children
//     by child.name. But that might be lower performance.
// (3) If `elOption.$mergeChildren` is `false`, the existing children will be
//     replaced totally.
S
sushuang 已提交
1824 1825
// (4) If `!elOption.children`, following the "merge" principle, nothing will happen.
//
1826 1827
// For implementation simpleness, do not provide a direct way to remove sinlge
// child (otherwise the total indicies of the children array have to be modified).
1828
// User can remove a single child by set its `ignore` as `true`.
1
100pah 已提交
1829 1830 1831 1832
function mergeChildren(
    el: graphicUtil.Group,
    dataIndex: number,
    elOption: CustomGroupOption,
1833
    seriesModel: CustomSeriesModel
1
100pah 已提交
1834 1835
): void {

1836 1837 1838
    const newChildren = elOption.children;
    const newLen = newChildren ? newChildren.length : 0;
    const mergeChildren = elOption.$mergeChildren;
1839
    // `diffChildrenByName` has been deprecated.
1840 1841
    const byName = mergeChildren === 'byName' || elOption.diffChildrenByName;
    const notMerge = mergeChildren === false;
1842 1843

    // For better performance on roam update, only enter if necessary.
S
tweak.  
sushuang 已提交
1844
    if (!newLen && !byName && !notMerge) {
1845 1846 1847 1848 1849 1850 1851 1852
        return;
    }

    if (byName) {
        diffGroupChildren({
            oldChildren: el.children() || [],
            newChildren: newChildren || [],
            dataIndex: dataIndex,
1
100pah 已提交
1853
            seriesModel: seriesModel,
1854
            group: el
1855 1856 1857 1858 1859 1860 1861 1862
        });
        return;
    }

    notMerge && el.removeAll();

    // Mapping children of a group simply by index, which
    // might be better performance.
1863
    let index = 0;
1864
    for (; index < newLen; index++) {
1865
        newChildren[index] && doCreateOrUpdateEl(
1866 1867 1868
            el.childAt(index),
            dataIndex,
            newChildren[index],
1
100pah 已提交
1869
            seriesModel,
1870
            el,
1
100pah 已提交
1871
            false
1872 1873
        );
    }
1874
    for (let i = el.childCount() - 1; i >= index; i--) {
1875 1876 1877
        // Do not supprot leave elements that are not mentioned in the latest
        // `renderItem` return. Otherwise users may not have a clear and simple
        // concept that how to contorl all of the elements.
1878
        doRemoveEl(el.childAt(i), seriesModel, el);
1879 1880 1881
    }
}

1
100pah 已提交
1882 1883 1884 1885 1886
type DiffGroupContext = {
    oldChildren: Element[],
    newChildren: CustomElementOption[],
    dataIndex: number,
    seriesModel: CustomSeriesModel,
1887
    group: graphicUtil.Group
1
100pah 已提交
1888 1889
};
function diffGroupChildren(context: DiffGroupContext) {
S
sushuang 已提交
1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902
    (new DataDiffer(
        context.oldChildren,
        context.newChildren,
        getKey,
        getKey,
        context
    ))
        .add(processAddUpdate)
        .update(processAddUpdate)
        .remove(processRemove)
        .execute();
}

1
100pah 已提交
1903
function getKey(item: Element, idx: number): string {
1904
    const name = item && item.name;
S
sushuang 已提交
1905 1906 1907
    return name != null ? name : GROUP_DIFF_PREFIX + idx;
}

1
100pah 已提交
1908 1909 1910 1911 1912
function processAddUpdate(
    this: DataDiffer<DiffGroupContext>,
    newIndex: number,
    oldIndex?: number
): void {
1913 1914 1915
    const context = this.context;
    const childOption = newIndex != null ? context.newChildren[newIndex] : null;
    const child = oldIndex != null ? context.oldChildren[oldIndex] : null;
S
sushuang 已提交
1916

1917
    doCreateOrUpdateEl(
S
sushuang 已提交
1918 1919 1920
        child,
        context.dataIndex,
        childOption,
1
100pah 已提交
1921
        context.seriesModel,
S
sushuang 已提交
1922
        context.group,
1
100pah 已提交
1923
        false
S
sushuang 已提交
1924 1925 1926
    );
}

1
100pah 已提交
1927
function processRemove(this: DataDiffer<DiffGroupContext>, oldIndex: number): void {
1928 1929
    const context = this.context;
    const child = context.oldChildren[oldIndex];
1930
    doRemoveEl(child, context.seriesModel, context.group);
1931 1932
}

1933
function doRemoveEl(
1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946
    el: Element,
    seriesModel: CustomSeriesModel,
    group: ViewRootGroup
): void {
    if (el) {
        const leaveToProps = inner(el).leaveToProps;
        leaveToProps
            ? graphicUtil.updateProps(el, leaveToProps, seriesModel, {
                cb: function () {
                    group.remove(el);
                }
            })
            : group.remove(el);
1947 1948 1949
    }
}

1
100pah 已提交
1950 1951 1952 1953
/**
 * @return SVG Path data.
 */
function getPathData(shape: CustomSVGPathOption['shape']): string {
S
sushuang 已提交
1954 1955 1956 1957
    // "d" follows the SVG convention.
    return shape && (shape.pathData || shape.d);
}

1
100pah 已提交
1958
function hasOwnPathData(shape: CustomSVGPathOption['shape']): boolean {
1959
    return shape && (hasOwn(shape, 'pathData') || hasOwn(shape, 'd'));
S
sushuang 已提交
1960 1961
}