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

20 21
// @ts-nocheck

S
sushuang 已提交
22
import {__DEV__} from '../config';
S
sushuang 已提交
23
import * as zrUtil from 'zrender/src/core/util';
S
sushuang 已提交
24
import * as graphicUtil from '../util/graphic';
25
import {getDefaultLabel} from './helper/labelHelper';
S
sushuang 已提交
26
import createListFromArray from './helper/createListFromArray';
27
import {getLayoutOnAxis} from '../layout/barGrid';
S
sushuang 已提交
28
import DataDiffer from '../data/DataDiffer';
29
import SeriesModel from '../model/Series';
30
import Model from '../model/Model';
31
import ChartView from '../view/Chart';
32
import {createClipPath} from './helper/createClipPathFromCoordSys';
33 34
import {EventQueryItem, ECEvent} from '../util/types';
import Element from 'zrender/src/Element';
S
sushuang 已提交
35 36 37 38 39 40 41

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';

42 43 44 45 46
const CACHED_LABEL_STYLE_PROPERTIES = graphicUtil.CACHED_LABEL_STYLE_PROPERTIES;
const ITEM_STYLE_NORMAL_PATH = ['itemStyle'];
const ITEM_STYLE_EMPHASIS_PATH = ['emphasis', 'itemStyle'];
const LABEL_NORMAL = ['label'];
const LABEL_EMPHASIS = ['emphasis', 'label'];
S
sushuang 已提交
47 48
// Use prefix to avoid index to be the same as el.name,
// which will cause weird udpate animation.
49
const GROUP_DIFF_PREFIX = 'e\0\0';
S
sushuang 已提交
50

51

S
sushuang 已提交
52 53 54 55 56 57 58 59 60 61 62
/**
 * 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.
 *     }}
 */
63
const prepareCustoms = {
S
sushuang 已提交
64 65 66 67 68 69 70
    cartesian2d: prepareCartesian2d,
    geo: prepareGeo,
    singleAxis: prepareSingleAxis,
    polar: preparePolar,
    calendar: prepareCalendar
};

71

S
sushuang 已提交
72 73 74 75
// ------
// Model
// ------

76
SeriesModel.extend({
S
sushuang 已提交
77 78 79 80 81 82 83 84 85

    type: 'series.custom',

    dependencies: ['grid', 'polar', 'geo', 'singleAxis', 'calendar'],

    defaultOption: {
        coordinateSystem: 'cartesian2d', // Can be set as 'none'
        zlevel: 0,
        z: 2,
86 87
        legendHoverLink: true,

88 89 90 91 92 93 94
        useTransform: true,

        // 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
        // Only works on polar and cartesian2d coordinate system.
        clip: false
S
sushuang 已提交
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109

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

        // Polar coordinate system
        // polarIndex: 0,

        // Geo coordinate system
        // geoIndex: 0,

        // label: {}
        // itemStyle: {}
    },

110 111 112
    /**
     * @override
     */
S
sushuang 已提交
113
    getInitialData: function (option, ecModel) {
114
        return createListFromArray(this.getSource(), this);
115 116 117 118 119 120
    },

    /**
     * @override
     */
    getDataParams: function (dataIndex, dataType, el) {
121
        const params = SeriesModel.prototype.getDataParams.apply(this, arguments);
122 123
        el && (params.info = el.info);
        return params;
S
sushuang 已提交
124 125
    }
});
P
pah100 已提交
126

S
sushuang 已提交
127 128 129
// -----
// View
// -----
P
pah100 已提交
130

131
ChartView.extend({
P
pah100 已提交
132

S
sushuang 已提交
133
    type: 'custom',
P
pah100 已提交
134

S
sushuang 已提交
135 136 137 138 139
    /**
     * @private
     * @type {module:echarts/data/List}
     */
    _data: null,
P
pah100 已提交
140

S
sushuang 已提交
141 142 143
    /**
     * @override
     */
144
    render: function (customSeries, ecModel, api, payload) {
145 146 147 148
        const oldData = this._data;
        const data = customSeries.getData();
        const group = this.group;
        const renderItem = makeRenderItem(customSeries, data, ecModel, api);
S
sushuang 已提交
149

150 151 152 153 154
        // 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 已提交
155 156
        data.diff(oldData)
            .add(function (newIdx) {
157
                createOrUpdate(
158
                    null, newIdx, renderItem(newIdx, payload), customSeries, group, data
S
sushuang 已提交
159 160 161
                );
            })
            .update(function (newIdx, oldIdx) {
162
                const el = oldData.getItemGraphicEl(oldIdx);
163
                createOrUpdate(
164
                    el, newIdx, renderItem(newIdx, payload), customSeries, group, data
165
                );
S
sushuang 已提交
166 167
            })
            .remove(function (oldIdx) {
168
                const el = oldData.getItemGraphicEl(oldIdx);
S
sushuang 已提交
169 170 171
                el && group.remove(el);
            })
            .execute();
P
pah100 已提交
172

173
        // Do clipping
174
        const clipPath = customSeries.get('clip', true)
175 176 177 178 179 180 181 182 183
            ? createClipPath(customSeries.coordinateSystem, false, customSeries)
            : null;
        if (clipPath) {
            group.setClipPath(clipPath);
        }
        else {
            group.removeClipPath();
        }

S
sushuang 已提交
184 185
        this._data = data;
    },
P
pah100 已提交
186

P
pissang 已提交
187 188 189 190 191
    incrementalPrepareRender: function (customSeries, ecModel, api) {
        this.group.removeAll();
        this._data = null;
    },

192
    incrementalRender: function (params, customSeries, ecModel, api, payload) {
193 194
        const data = customSeries.getData();
        const renderItem = makeRenderItem(customSeries, data, ecModel, api);
P
pissang 已提交
195 196 197 198 199 200
        function setIncrementalAndHoverLayer(el) {
            if (!el.isGroup) {
                el.incremental = true;
                el.useHoverLayer = true;
            }
        }
201
        for (let idx = params.start; idx < params.end; idx++) {
202
            const el = createOrUpdate(null, idx, renderItem(idx, payload), customSeries, this.group, data);
P
pissang 已提交
203 204 205 206
            el.traverse(setIncrementalAndHoverLayer);
        }
    },

S
sushuang 已提交
207 208 209
    /**
     * @override
     */
210 211 212 213 214
    dispose: zrUtil.noop,

    /**
     * @override
     */
215 216 217
    filterForExposedEvent: function (
        eventType: string, query: EventQueryItem, targetEl: Element, packedEvent: ECEvent
    ): boolean {
218
        const elementName = query.element;
219
        if (elementName == null || targetEl.name === elementName) {
220 221 222 223 224 225
            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) {
226
            if (targetEl.name === elementName) {
227 228 229 230 231 232
                return true;
            }
        }

        return false;
    }
S
sushuang 已提交
233
});
P
pah100 已提交
234 235


S
sushuang 已提交
236
function createEl(elOption) {
237
    const graphicType = elOption.type;
238
    let el;
S
sushuang 已提交
239

240 241
    // Those graphic elements are not shapes. They should not be
    // overwritten by users, so do them first.
S
sushuang 已提交
242
    if (graphicType === 'path') {
243
        const shape = elOption.shape;
S
sushuang 已提交
244
        // Using pathRect brings convenience to users sacle svg path.
245
        const pathRect = (shape.width != null && shape.height != null)
S
sushuang 已提交
246
            ? {
S
sushuang 已提交
247 248
                x: shape.x || 0,
                y: shape.y || 0,
S
sushuang 已提交
249 250 251 252
                width: shape.width,
                height: shape.height
            }
            : null;
253
        const pathData = getPathData(shape);
S
sushuang 已提交
254 255 256
        // Path is also used for icon, so layout 'center' by default.
        el = graphicUtil.makePath(pathData, null, pathRect, shape.layout || 'center');
        el.__customPathData = pathData;
S
sushuang 已提交
257 258
    }
    else if (graphicType === 'image') {
259
        el = new graphicUtil.Image({});
S
sushuang 已提交
260 261 262
        el.__customImagePath = elOption.style.image;
    }
    else if (graphicType === 'text') {
263
        el = new graphicUtil.Text({});
S
sushuang 已提交
264 265
        el.__customText = elOption.style.text;
    }
266 267 268 269 270 271
    else if (graphicType === 'group') {
        el = new graphicUtil.Group();
    }
    else if (graphicType === 'compoundPath') {
        throw new Error('"compoundPath" is not supported yet.');
    }
S
sushuang 已提交
272
    else {
273
        const Clz = graphicUtil.getShapeClass(graphicType);
P
pah100 已提交
274

S
sushuang 已提交
275 276
        if (__DEV__) {
            zrUtil.assert(Clz, 'graphic type "' + graphicType + '" can not be found.');
P
pah100 已提交
277 278
        }

S
sushuang 已提交
279 280
        el = new Clz();
    }
P
pah100 已提交
281

S
sushuang 已提交
282 283
    el.__customGraphicType = graphicType;
    el.name = elOption.name;
P
pah100 已提交
284

S
sushuang 已提交
285 286
    return el;
}
P
pah100 已提交
287

S
sushuang 已提交
288
function updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot) {
289 290
    const transitionProps = {};
    const elOptionStyle = elOption.style || {};
P
pah100 已提交
291

292 293 294 295 296
    elOption.shape && (transitionProps.shape = zrUtil.clone(elOption.shape));
    elOption.position && (transitionProps.position = elOption.position.slice());
    elOption.scale && (transitionProps.scale = elOption.scale.slice());
    elOption.origin && (transitionProps.origin = elOption.origin.slice());
    elOption.rotation && (transitionProps.rotation = elOption.rotation);
P
pah100 已提交
297

S
sushuang 已提交
298
    if (el.type === 'image' && elOption.style) {
299
        const targetStyle = transitionProps.style = {};
S
sushuang 已提交
300 301 302
        zrUtil.each(['x', 'y', 'width', 'height'], function (prop) {
            prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit);
        });
P
pah100 已提交
303 304
    }

S
sushuang 已提交
305
    if (el.type === 'text' && elOption.style) {
306
        const targetStyle = transitionProps.style = {};
S
sushuang 已提交
307 308 309 310 311 312 313 314 315 316 317 318
        zrUtil.each(['x', 'y'], function (prop) {
            prepareStyleTransition(prop, targetStyle, elOptionStyle, el.style, isInit);
        });
        // Compatible with previous: both support
        // textFill and fill, textStroke and stroke in 'text' element.
        !elOptionStyle.hasOwnProperty('textFill') && elOptionStyle.fill && (
            elOptionStyle.textFill = elOptionStyle.fill
        );
        !elOptionStyle.hasOwnProperty('textStroke') && elOptionStyle.stroke && (
            elOptionStyle.textStroke = elOptionStyle.stroke
        );
    }
P
pah100 已提交
319

S
sushuang 已提交
320 321
    if (el.type !== 'group') {
        el.useStyle(elOptionStyle);
P
pah100 已提交
322

S
sushuang 已提交
323 324 325
        // Init animation.
        if (isInit) {
            el.style.opacity = 0;
326
            let targetOpacity = elOptionStyle.opacity;
S
sushuang 已提交
327 328
            targetOpacity == null && (targetOpacity = 1);
            graphicUtil.initProps(el, {style: {opacity: targetOpacity}}, animatableModel, dataIndex);
P
pah100 已提交
329
        }
S
sushuang 已提交
330
    }
P
pah100 已提交
331

S
sushuang 已提交
332
    if (isInit) {
333
        el.attr(transitionProps);
S
sushuang 已提交
334 335
    }
    else {
336
        graphicUtil.updateProps(el, transitionProps, animatableModel, dataIndex);
S
sushuang 已提交
337
    }
P
pah100 已提交
338

339
    // Merge by default.
S
sushuang 已提交
340
    // z2 must not be null/undefined, otherwise sort error may occur.
341 342 343 344 345 346 347 348
    elOption.hasOwnProperty('z2') && el.attr('z2', elOption.z2 || 0);
    elOption.hasOwnProperty('silent') && el.attr('silent', elOption.silent);
    elOption.hasOwnProperty('invisible') && el.attr('invisible', elOption.invisible);
    elOption.hasOwnProperty('ignore') && el.attr('ignore', elOption.ignore);
    // `elOption.info` enables user to mount some info on
    // elements and use them in event handlers.
    // Update them only when user specified, otherwise, remain.
    elOption.hasOwnProperty('info') && el.attr('info', elOption.info);
P
pah100 已提交
349

S
sushuang 已提交
350 351
    // If `elOption.styleEmphasis` is `false`, remove hover style. The
    // logic is ensured by `graphicUtil.setElementHoverStyle`.
352
    const styleEmphasis = elOption.styleEmphasis;
353 354
    // hoverStyle should always be set here, because if the hover style
    // may already be changed, where the inner cache should be reset.
P
pissang 已提交
355
    graphicUtil.enableElementHoverEmphasis(el, styleEmphasis);
356
    if (isRoot) {
357
        graphicUtil.setAsHighDownDispatcher(el, styleEmphasis !== false);
358
    }
S
sushuang 已提交
359
}
P
pah100 已提交
360

S
sushuang 已提交
361 362 363 364
function prepareStyleTransition(prop, targetStyle, elOptionStyle, oldElStyle, isInit) {
    if (elOptionStyle[prop] != null && !isInit) {
        targetStyle[prop] = elOptionStyle[prop];
        elOptionStyle[prop] = oldElStyle[prop];
P
pah100 已提交
365
    }
S
sushuang 已提交
366 367 368
}

function makeRenderItem(customSeries, data, ecModel, api) {
369 370
    const renderItem = customSeries.get('renderItem');
    const coordSys = customSeries.coordinateSystem;
371
    let prepareResult = {};
S
sushuang 已提交
372 373 374 375 376 377 378 379

    if (coordSys) {
        if (__DEV__) {
            zrUtil.assert(renderItem, 'series.render is required.');
            zrUtil.assert(
                coordSys.prepareCustoms || prepareCustoms[coordSys.type],
                'This coordSys does not support custom series.'
            );
P
pah100 已提交
380 381
        }

S
sushuang 已提交
382 383 384 385
        prepareResult = coordSys.prepareCustoms
            ? coordSys.prepareCustoms()
            : prepareCustoms[coordSys.type](coordSys);
    }
P
pah100 已提交
386

387
    const userAPI = zrUtil.defaults({
S
sushuang 已提交
388 389 390 391 392 393 394 395 396 397 398 399 400
        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
    }, prepareResult.api || {});

401
    const userParams = {
S
tweak.  
sushuang 已提交
402 403 404
        // 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 已提交
405 406 407 408 409 410 411 412
        context: {},
        seriesId: customSeries.id,
        seriesName: customSeries.name,
        seriesIndex: customSeries.seriesIndex,
        coordSys: prepareResult.coordSys,
        dataInsideLength: data.count(),
        encode: wrapEncodeDef(customSeries.getData())
    };
P
pah100 已提交
413

S
sushuang 已提交
414
    // Do not support call `api` asynchronously without dataIndexInside input.
415 416 417 418 419 420
    let currDataIndexInside;
    let currDirty = true;
    let currItemModel;
    let currLabelNormalModel;
    let currLabelEmphasisModel;
    let currVisualColor;
S
sushuang 已提交
421

422
    return function (dataIndexInside, payload) {
S
sushuang 已提交
423 424
        currDataIndexInside = dataIndexInside;
        currDirty = true;
425

S
sushuang 已提交
426 427 428
        return renderItem && renderItem(
            zrUtil.defaults({
                dataIndexInside: dataIndexInside,
429 430 431
                dataIndex: data.getRawIndex(dataIndexInside),
                // Can be used for optimization when zoom or roam.
                actionType: payload ? payload.type : null
S
sushuang 已提交
432 433
            }, userParams),
            userAPI
S
sushuang 已提交
434
        );
S
sushuang 已提交
435
    };
P
pah100 已提交
436

S
sushuang 已提交
437 438 439 440 441 442 443 444 445 446
    // Do not update cache until api called.
    function updateCache(dataIndexInside) {
        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
        if (currDirty) {
            currItemModel = data.getItemModel(dataIndexInside);
            currLabelNormalModel = currItemModel.getModel(LABEL_NORMAL);
            currLabelEmphasisModel = currItemModel.getModel(LABEL_EMPHASIS);
            currVisualColor = data.getItemVisual(dataIndexInside, 'color');

            currDirty = false;
P
pah100 已提交
447
        }
S
sushuang 已提交
448
    }
P
pah100 已提交
449

S
sushuang 已提交
450 451 452 453 454 455 456 457 458 459
    /**
     * @public
     * @param {number|string} dim
     * @param {number} [dataIndexInside=currDataIndexInside]
     * @return {number|string} value
     */
    function value(dim, dataIndexInside) {
        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
        return data.get(data.getDimension(dim || 0), dataIndexInside);
    }
P
pah100 已提交
460

S
sushuang 已提交
461 462 463 464 465 466 467 468 469 470 471 472
    /**
     * 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})`;
     * @public
     * @param {Object} [extra]
     * @param {number} [dataIndexInside=currDataIndexInside]
     */
    function style(extra, dataIndexInside) {
        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
        updateCache(dataIndexInside);
P
pah100 已提交
473

474
        const itemStyle = currItemModel.getModel(ITEM_STYLE_NORMAL_PATH).getItemStyle();
P
pah100 已提交
475

S
sushuang 已提交
476
        currVisualColor != null && (itemStyle.fill = currVisualColor);
477
        const opacity = data.getItemVisual(dataIndexInside, 'opacity');
S
sushuang 已提交
478
        opacity != null && (itemStyle.opacity = opacity);
P
pah100 已提交
479

480
        const labelModel = extra
481 482 483
            ? applyExtraBefore(extra, currLabelNormalModel)
            : currLabelNormalModel;

484
        const textStyle = graphicUtil.createTextStyle(labelModel, null, {
485
            inheritColor: currVisualColor,
S
sushuang 已提交
486 487
            isRectText: true
        });
P
pah100 已提交
488

489 490 491
        // TODO
        zrUtil.extend(itemStyle, textStyle);

492
        itemStyle.text = labelModel.getShallow('show')
S
sushuang 已提交
493 494 495 496 497
            ? zrUtil.retrieve2(
                customSeries.getFormattedLabel(dataIndexInside, 'normal'),
                getDefaultLabel(data, dataIndexInside)
            )
            : null;
P
pah100 已提交
498

499 500
        extra && applyExtraAfter(itemStyle, extra);

S
sushuang 已提交
501 502
        return itemStyle;
    }
P
pah100 已提交
503

S
sushuang 已提交
504 505 506 507 508 509 510 511 512
    /**
     * @public
     * @param {Object} [extra]
     * @param {number} [dataIndexInside=currDataIndexInside]
     */
    function styleEmphasis(extra, dataIndexInside) {
        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
        updateCache(dataIndexInside);

513
        const itemStyle = currItemModel.getModel(ITEM_STYLE_EMPHASIS_PATH).getItemStyle();
S
sushuang 已提交
514

515
        const labelModel = extra
516 517 518
            ? applyExtraBefore(extra, currLabelEmphasisModel)
            : currLabelEmphasisModel;

519
        const textStyle = graphicUtil.createTextStyle(labelModel, null, {
S
sushuang 已提交
520 521
            isRectText: true
        }, true);
522 523
        zrUtil.extend(itemStyle, textStyle);

S
sushuang 已提交
524

525
        itemStyle.text = labelModel.getShallow('show')
S
sushuang 已提交
526 527 528 529 530 531
            ? zrUtil.retrieve3(
                customSeries.getFormattedLabel(dataIndexInside, 'emphasis'),
                customSeries.getFormattedLabel(dataIndexInside, 'normal'),
                getDefaultLabel(data, dataIndexInside)
            )
            : null;
P
pah100 已提交
532

533 534
        extra && applyExtraAfter(itemStyle, extra);

S
sushuang 已提交
535
        return itemStyle;
P
pah100 已提交
536 537
    }

S
sushuang 已提交
538 539 540 541 542 543 544 545
    /**
     * @public
     * @param {string} visualType
     * @param {number} [dataIndexInside=currDataIndexInside]
     */
    function visual(visualType, dataIndexInside) {
        dataIndexInside == null && (dataIndexInside = currDataIndexInside);
        return data.getItemVisual(dataIndexInside, visualType);
P
pah100 已提交
546 547
    }

S
sushuang 已提交
548 549 550 551 552
    /**
     * @public
     * @param {number} opt.count Positive interger.
     * @param {number} [opt.barWidth]
     * @param {number} [opt.barMaxWidth]
S
SHUANG SU 已提交
553
     * @param {number} [opt.barMinWidth]
S
sushuang 已提交
554 555 556 557 558 559
     * @param {number} [opt.barGap]
     * @param {number} [opt.barCategoryGap]
     * @return {Object} {width, offset, offsetCenter} is not support, return undefined.
     */
    function barLayout(opt) {
        if (coordSys.getBaseAxis) {
560
            const baseAxis = coordSys.getBaseAxis();
561
            return getLayoutOnAxis(zrUtil.defaults({axis: baseAxis}, opt), api);
562
        }
P
pah100 已提交
563 564
    }

S
sushuang 已提交
565 566 567 568 569 570
    /**
     * @public
     * @return {Array.<number>}
     */
    function currentSeriesIndices() {
        return ecModel.getCurrentSeriesIndices();
571 572
    }

S
sushuang 已提交
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
    /**
     * @public
     * @param {Object} opt
     * @param {string} [opt.fontStyle]
     * @param {number} [opt.fontWeight]
     * @param {number} [opt.fontSize]
     * @param {string} [opt.fontFamily]
     * @return {string} font string
     */
    function font(opt) {
        return graphicUtil.getFont(opt, ecModel);
    }
}

function wrapEncodeDef(data) {
588
    const encodeDef = {};
S
sushuang 已提交
589
    zrUtil.each(data.dimensions, function (dimName, dataDimIndex) {
590
        const dimInfo = data.getDimensionInfo(dimName);
S
sushuang 已提交
591
        if (!dimInfo.isExtraCoord) {
592 593
            const coordDim = dimInfo.coordDim;
            const dataDims = encodeDef[coordDim] = encodeDef[coordDim] || [];
S
sushuang 已提交
594 595 596 597 598 599 600
            dataDims[dimInfo.coordDimIndex] = dataDimIndex;
        }
    });
    return encodeDef;
}

function createOrUpdate(el, dataIndex, elOption, animatableModel, group, data) {
S
sushuang 已提交
601
    el = doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, true);
S
sushuang 已提交
602
    el && data.setItemGraphicEl(dataIndex, el);
P
pissang 已提交
603 604

    return el;
S
sushuang 已提交
605 606
}

S
sushuang 已提交
607
function doCreateOrUpdate(el, dataIndex, elOption, animatableModel, group, data, isRoot) {
608

S
sushuang 已提交
609 610 611 612 613 614 615 616 617 618
    // [Rule]
    // By default, follow merge mode.
    //     (It probably brings benifit for performance in some cases of large data, where
    //     user program can be optimized to that only updated props needed to be re-calculated,
    //     or according to `actionType` some calculation can be skipped.)
    // 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.)

619
    const simplyRemove = !elOption; // `null`/`undefined`/`false`
S
sushuang 已提交
620
    elOption = elOption || {};
621 622 623
    const elOptionType = elOption.type;
    const elOptionShape = elOption.shape;
    const elOptionStyle = elOption.style;
S
sushuang 已提交
624

625
    if (el && (
S
sushuang 已提交
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
        simplyRemove
        // || elOption.$merge === false
        // If `elOptionType` is `null`, follow the merge principle.
        || (elOptionType != null
            && elOptionType !== el.__customGraphicType
        )
        || (elOptionType === 'path'
            && hasOwnPathData(elOptionShape) && getPathData(elOptionShape) !== el.__customPathData
        )
        || (elOptionType === 'image'
            && hasOwn(elOptionStyle, 'image') && elOptionStyle.image !== el.__customImagePath
        )
        // FIXME test and remove this restriction?
        || (elOptionType === 'text'
            && hasOwn(elOptionShape, 'text') && elOptionStyle.text !== el.__customText
641 642
        )
    )) {
S
sushuang 已提交
643 644
        group.remove(el);
        el = null;
645 646
    }

S
sushuang 已提交
647
    // `elOption.type` is undefined when `renderItem` returns nothing.
S
sushuang 已提交
648
    if (simplyRemove) {
S
sushuang 已提交
649
        return;
650 651
    }

652
    const isInit = !el;
S
sushuang 已提交
653
    !el && (el = createEl(elOption));
S
sushuang 已提交
654
    updateEl(el, dataIndex, elOption, animatableModel, data, isInit, isRoot);
S
sushuang 已提交
655

656 657
    if (elOptionType === 'group') {
        mergeChildren(el, dataIndex, elOption, animatableModel, data);
658 659
    }

660
    // Always add whatever already added to ensure sequence.
S
sushuang 已提交
661 662 663 664 665
    group.add(el);

    return el;
}

666 667 668 669 670 671 672 673 674 675
// 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 已提交
676 677
// (4) If `!elOption.children`, following the "merge" principle, nothing will happen.
//
678 679 680 681 682
// 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).
// User can remove a single child by set its `ignore` as `true` or replace
// it by another element, where its `$merge` can be set as `true` if necessary.
function mergeChildren(el, dataIndex, elOption, animatableModel, data) {
683 684 685
    const newChildren = elOption.children;
    const newLen = newChildren ? newChildren.length : 0;
    const mergeChildren = elOption.$mergeChildren;
686
    // `diffChildrenByName` has been deprecated.
687 688
    const byName = mergeChildren === 'byName' || elOption.diffChildrenByName;
    const notMerge = mergeChildren === false;
689 690

    // For better performance on roam update, only enter if necessary.
S
tweak.  
sushuang 已提交
691
    if (!newLen && !byName && !notMerge) {
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710
        return;
    }

    if (byName) {
        diffGroupChildren({
            oldChildren: el.children() || [],
            newChildren: newChildren || [],
            dataIndex: dataIndex,
            animatableModel: animatableModel,
            group: el,
            data: data
        });
        return;
    }

    notMerge && el.removeAll();

    // Mapping children of a group simply by index, which
    // might be better performance.
711
    let index = 0;
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
    for (; index < newLen; index++) {
        newChildren[index] && doCreateOrUpdate(
            el.childAt(index),
            dataIndex,
            newChildren[index],
            animatableModel,
            el,
            data
        );
    }
    if (__DEV__) {
        zrUtil.assert(
            !notMerge || el.childCount() === index,
            'MUST NOT contain empty item in children array when `group.$mergeChildren` is `false`.'
        );
    }
}

S
sushuang 已提交
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
function diffGroupChildren(context) {
    (new DataDiffer(
        context.oldChildren,
        context.newChildren,
        getKey,
        getKey,
        context
    ))
        .add(processAddUpdate)
        .update(processAddUpdate)
        .remove(processRemove)
        .execute();
}

function getKey(item, idx) {
745
    const name = item && item.name;
S
sushuang 已提交
746 747 748 749
    return name != null ? name : GROUP_DIFF_PREFIX + idx;
}

function processAddUpdate(newIndex, oldIndex) {
750 751 752
    const context = this.context;
    const childOption = newIndex != null ? context.newChildren[newIndex] : null;
    const child = oldIndex != null ? context.oldChildren[oldIndex] : null;
S
sushuang 已提交
753 754 755 756 757 758 759 760 761 762 763

    doCreateOrUpdate(
        child,
        context.dataIndex,
        childOption,
        context.animatableModel,
        context.group,
        context.data
    );
}

764 765 766 767
// `graphic#applyDefaultTextStyle` will cache
// textFill, textStroke, textStrokeWidth.
// We have to do this trick.
function applyExtraBefore(extra, model) {
768
    const dummyModel = new Model({}, model);
769 770 771 772 773 774 775 776 777
    zrUtil.each(CACHED_LABEL_STYLE_PROPERTIES, function (stylePropName, modelPropName) {
        if (extra.hasOwnProperty(stylePropName)) {
            dummyModel.option[modelPropName] = extra[stylePropName];
        }
    });
    return dummyModel;
}

function applyExtraAfter(itemStyle, extra) {
778
    for (const key in extra) {
779 780 781 782 783 784 785 786
        if (extra.hasOwnProperty(key)
            || !CACHED_LABEL_STYLE_PROPERTIES.hasOwnProperty(key)
        ) {
            itemStyle[key] = extra[key];
        }
    }
}

S
sushuang 已提交
787
function processRemove(oldIndex) {
788 789
    const context = this.context;
    const child = context.oldChildren[oldIndex];
S
sushuang 已提交
790 791
    child && context.group.remove(child);
}
S
sushuang 已提交
792 793 794 795 796 797 798 799 800 801 802 803 804

function getPathData(shape) {
    // "d" follows the SVG convention.
    return shape && (shape.pathData || shape.d);
}

function hasOwnPathData(shape) {
    return shape && (shape.hasOwnProperty('pathData') || shape.hasOwnProperty('d'));
}

function hasOwn(host, prop) {
    return host && host.hasOwnProperty(prop);
}