TreemapView.ts 38.0 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
import {bind, each, indexOf, curry, extend, retrieve, normalizeCssArray} from 'zrender/src/core/util';
S
sushuang 已提交
21
import * as graphic from '../../util/graphic';
P
pissang 已提交
22 23 24 25 26 27
import {
    isHighDownDispatcher,
    setAsHighDownDispatcher,
    setDefaultStateProxy,
    enableHoverFocus
} from '../../util/states';
S
sushuang 已提交
28
import DataDiffer from '../../data/DataDiffer';
29
import * as helper from '../helper/treeHelper';
S
sushuang 已提交
30
import Breadcrumb from './Breadcrumb';
P
pissang 已提交
31 32
import RoamController, { RoamEventParams } from '../../component/helper/RoamController';
import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
S
sushuang 已提交
33
import * as matrix from 'zrender/src/core/matrix';
S
sushuang 已提交
34
import * as animationUtil from '../../util/animation';
S
sushuang 已提交
35
import makeStyleMapper from '../../model/mixin/makeStyleMapper';
P
pissang 已提交
36 37 38 39 40 41 42 43 44 45 46
import ChartView from '../../view/Chart';
import Tree, { TreeNode } from '../../data/Tree';
import TreemapSeriesModel, { TreemapSeriesNodeItemOption } from './TreemapSeries';
import GlobalModel from '../../model/Global';
import ExtensionAPI from '../../ExtensionAPI';
import Model from '../../model/Model';
import { LayoutRect } from '../../util/layout';
import { TreemapLayoutNode } from './treemapLayout';
import Element from 'zrender/src/Element';
import Displayable from 'zrender/src/graphic/Displayable';
import { makeInner } from '../../util/model';
P
pissang 已提交
47
import { PathStyleProps, PathProps } from 'zrender/src/graphic/Path';
P
pissang 已提交
48 49 50 51 52 53 54
import { TreeSeriesNodeItemOption } from '../tree/TreeSeries';
import {
    TreemapRootToNodePayload,
    TreemapMovePayload,
    TreemapRenderPayload,
    TreemapZoomToNodePayload
} from './treemapAction';
55
import { ColorString, ECElement } from '../../util/types';
56 57
import { windowOpen } from '../../util/format';
import { TextStyleProps } from 'zrender/src/graphic/Text';
P
pissang 已提交
58
import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle';
P
pissang 已提交
59 60 61 62 63

const Group = graphic.Group;
const Rect = graphic.Rect;

const DRAG_THRESHOLD = 3;
64 65
const PATH_LABEL_NOAMAL = 'label';
const PATH_UPPERLABEL_NORMAL = 'upperLabel';
P
pissang 已提交
66 67 68 69
const Z_BASE = 10; // Should bigger than every z.
const Z_BG = 1;
const Z_CONTENT = 2;

70
const getStateItemStyle = makeStyleMapper([
S
sushuang 已提交
71 72 73 74 75 76 77 78 79 80
    ['fill', 'color'],
    // `borderColor` and `borderWidth` has been occupied,
    // so use `stroke` to indicate the stroke of the rect.
    ['stroke', 'strokeColor'],
    ['lineWidth', 'strokeWidth'],
    ['shadowBlur'],
    ['shadowOffsetX'],
    ['shadowOffsetY'],
    ['shadowColor']
]);
P
pissang 已提交
81
const getItemStyleNormal = function (model: Model<TreemapSeriesNodeItemOption['itemStyle']>): PathStyleProps {
S
sushuang 已提交
82
    // Normal style props should include emphasis style props.
83
    const itemStyle = getStateItemStyle(model) as PathStyleProps;
S
sushuang 已提交
84 85 86 87 88
    // Clear styles set by emphasis.
    itemStyle.stroke = itemStyle.fill = itemStyle.lineWidth = null;
    return itemStyle;
};

P
pissang 已提交
89 90 91 92 93
interface RenderElementStorage {
    nodeGroup: graphic.Group[]
    background: graphic.Rect[]
    content: graphic.Rect[]
}
S
sushuang 已提交
94

P
pissang 已提交
95 96 97 98 99 100 101 102 103 104 105 106 107 108
type LastCfgStorage = {
    [key in keyof RenderElementStorage]: LastCfg[]
    // nodeGroup: {
    //     old: Pick<graphic.Group, 'position'>[]
    //     fadein: boolean
    // }[]
    // background: {
    //     old: Pick<graphic.Rect, 'shape'>
    //     fadein: boolean
    // }[]
    // content: {
    //     old: Pick<graphic.Rect, 'shape'>
    //     fadein: boolean
    // }[]
1
100pah 已提交
109
};
S
sushuang 已提交
110

P
pissang 已提交
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
interface FoundTargetInfo {
    node: TreeNode

    offsetX?: number
    offsetY?: number
}

interface RenderResult {
    lastsForAnimation: LastCfgStorage
    willInvisibleEls?: graphic.Rect[]
    willDeleteEls: RenderElementStorage
    renderFinally: () => void
}

interface ReRoot {
    rootNodeGroup: graphic.Group
    direction: 'drillDown' | 'rollUp'
}

interface LastCfg {
131 132
    oldX?: number
    oldY?: number
P
pissang 已提交
133 134 135 136 137 138 139 140 141 142 143 144
    oldShape?: graphic.Rect['shape']
    fadein: boolean
}

const inner = makeInner<{
    nodeWidth: number
    nodeHeight: number
    willDelete: boolean
}, Element>();

class TreemapView extends ChartView {

1
100pah 已提交
145 146
    static type = 'treemap';
    type = TreemapView.type;
P
pissang 已提交
147

1
100pah 已提交
148 149 150
    private _containerGroup: graphic.Group;
    private _breadcrumb: Breadcrumb;
    private _controller: RoamController;
P
pissang 已提交
151

1
100pah 已提交
152
    private _oldTree: Tree;
P
pissang 已提交
153

1
100pah 已提交
154
    private _state: 'ready' | 'animating' = 'ready';
P
pissang 已提交
155 156 157

    private _storage = createStorage() as RenderElementStorage;

1
100pah 已提交
158 159 160
    seriesModel: TreemapSeriesModel;
    api: ExtensionAPI;
    ecModel: GlobalModel;
P
pah100 已提交
161

S
sushuang 已提交
162 163 164
    /**
     * @override
     */
P
pissang 已提交
165 166 167 168 169 170
    render(
        seriesModel: TreemapSeriesModel,
        ecModel: GlobalModel,
        api: ExtensionAPI,
        payload: TreemapZoomToNodePayload | TreemapRenderPayload | TreemapMovePayload | TreemapRootToNodePayload
    ) {
P
pah100 已提交
171

172
        const models = ecModel.findComponents({
S
sushuang 已提交
173 174
            mainType: 'series', subType: 'treemap', query: payload
        });
P
pissang 已提交
175
        if (indexOf(models, seriesModel) < 0) {
S
sushuang 已提交
176 177
            return;
        }
P
pah100 已提交
178

S
sushuang 已提交
179 180 181 182
        this.seriesModel = seriesModel;
        this.api = api;
        this.ecModel = ecModel;

183 184
        const types = ['treemapZoomToNode', 'treemapRootToNode'];
        const targetInfo = helper
185
            .retrieveTargetInfo(payload, types, seriesModel);
186 187 188 189
        const payloadType = payload && payload.type;
        const layoutInfo = seriesModel.layoutInfo;
        const isInit = !this._oldTree;
        const thisStorage = this._storage;
S
sushuang 已提交
190 191

        // Mark new root when action is treemapRootToNode.
192
        const reRoot = (payloadType === 'treemapRootToNode' && targetInfo && thisStorage)
S
sushuang 已提交
193 194
            ? {
                rootNodeGroup: thisStorage.nodeGroup[targetInfo.node.getRawIndex()],
P
pissang 已提交
195
                direction: (payload as TreemapRootToNodePayload).direction
S
sushuang 已提交
196 197 198
            }
            : null;

199
        const containerGroup = this._giveContainerGroup(layoutInfo);
P
pah100 已提交
200

201
        const renderResult = this._doRender(containerGroup, seriesModel, reRoot);
S
sushuang 已提交
202 203 204 205 206 207 208 209 210
        (
            !isInit && (
                !payloadType
                || payloadType === 'treemapZoomToNode'
                || payloadType === 'treemapRootToNode'
            )
        )
            ? this._doAnimation(containerGroup, renderResult, seriesModel, reRoot)
            : renderResult.renderFinally();
P
pah100 已提交
211

S
sushuang 已提交
212
        this._resetController(api);
213

S
sushuang 已提交
214
        this._renderBreadcrumb(seriesModel, api, targetInfo);
P
pissang 已提交
215
    }
P
pah100 已提交
216

1
100pah 已提交
217
    private _giveContainerGroup(layoutInfo: LayoutRect) {
218
        let containerGroup = this._containerGroup;
S
sushuang 已提交
219 220 221 222 223 224 225
        if (!containerGroup) {
            // FIXME
            // 加一层containerGroup是为了clip,但是现在clip功能并没有实现。
            containerGroup = this._containerGroup = new Group();
            this._initEvents(containerGroup);
            this.group.add(containerGroup);
        }
226 227
        containerGroup.x = layoutInfo.x;
        containerGroup.y = layoutInfo.y;
228

S
sushuang 已提交
229
        return containerGroup;
P
pissang 已提交
230
    }
S
sushuang 已提交
231

1
100pah 已提交
232
    private _doRender(containerGroup: graphic.Group, seriesModel: TreemapSeriesModel, reRoot: ReRoot): RenderResult {
233 234
        const thisTree = seriesModel.getData().tree;
        const oldTree = this._oldTree;
S
sushuang 已提交
235 236

        // Clear last shape records.
237 238 239 240
        const lastsForAnimation = createStorage() as LastCfgStorage;
        const thisStorage = createStorage() as RenderElementStorage;
        const oldStorage = this._storage;
        const willInvisibleEls: RenderResult['willInvisibleEls'] = [];
P
pissang 已提交
241 242 243 244 245 246 247 248 249

        function doRenderNode(thisNode: TreeNode, oldNode: TreeNode, parentGroup: graphic.Group, depth: number) {
            return renderNode(
                seriesModel,
                thisStorage, oldStorage, reRoot,
                lastsForAnimation, willInvisibleEls,
                thisNode, oldNode, parentGroup, depth
            );
        }
S
sushuang 已提交
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264

        // Notice: when thisTree and oldTree are the same tree (see list.cloneShallow),
        // the oldTree is actually losted, so we can not find all of the old graphic
        // elements from tree. So we use this stragegy: make element storage, move
        // from old storage to new storage, clear old storage.

        dualTravel(
            thisTree.root ? [thisTree.root] : [],
            (oldTree && oldTree.root) ? [oldTree.root] : [],
            containerGroup,
            thisTree === oldTree || !oldTree,
            0
        );

        // Process all removing.
265
        const willDeleteEls = clearStorage(oldStorage) as RenderElementStorage;
S
sushuang 已提交
266 267 268 269 270

        this._oldTree = thisTree;
        this._storage = thisStorage;

        return {
P
pissang 已提交
271 272 273
            lastsForAnimation,
            willDeleteEls,
            renderFinally
S
sushuang 已提交
274 275
        };

P
pissang 已提交
276 277 278 279 280 281 282
        function dualTravel(
            thisViewChildren: TreemapLayoutNode[],
            oldViewChildren: TreemapLayoutNode[],
            parentGroup: graphic.Group,
            sameTree: boolean,
            depth: number
        ) {
S
sushuang 已提交
283 284 285 286 287 288 289
            // When 'render' is triggered by action,
            // 'this' and 'old' may be the same tree,
            // we use rawIndex in that case.
            if (sameTree) {
                oldViewChildren = thisViewChildren;
                each(thisViewChildren, function (child, index) {
                    !child.isRemoved() && processNode(index, index);
290
                });
291
            }
S
sushuang 已提交
292 293 294 295 296 297
            // Diff hierarchically (diff only in each subtree, but not whole).
            // because, consistency of view is important.
            else {
                (new DataDiffer(oldViewChildren, thisViewChildren, getKey, getKey))
                    .add(processNode)
                    .update(processNode)
P
pissang 已提交
298
                    .remove(curry(processNode, null))
S
sushuang 已提交
299 300
                    .execute();
            }
P
pah100 已提交
301

P
pissang 已提交
302
            function getKey(node: TreeNode) {
S
sushuang 已提交
303 304
                // Identify by name or raw index.
                return node.getId();
P
pah100 已提交
305 306
            }

P
pissang 已提交
307
            function processNode(newIndex: number, oldIndex?: number) {
308 309
                const thisNode = newIndex != null ? thisViewChildren[newIndex] : null;
                const oldNode = oldIndex != null ? oldViewChildren[oldIndex] : null;
310

311
                const group = doRenderNode(thisNode, oldNode, parentGroup, depth);
P
pah100 已提交
312

S
sushuang 已提交
313 314 315 316 317 318 319 320 321
                group && dualTravel(
                    thisNode && thisNode.viewChildren || [],
                    oldNode && oldNode.viewChildren || [],
                    group,
                    sameTree,
                    depth + 1
                );
            }
        }
322

P
pissang 已提交
323
        function clearStorage(storage: RenderElementStorage) {
324
            const willDeleteEls = createStorage() as RenderElementStorage;
S
sushuang 已提交
325
            storage && each(storage, function (store, storageName) {
326
                const delEls = willDeleteEls[storageName];
S
sushuang 已提交
327
                each(store, function (el) {
P
pissang 已提交
328
                    el && (delEls.push(el as any), inner(el).willDelete = true);
S
sushuang 已提交
329 330 331 332
                });
            });
            return willDeleteEls;
        }
P
pah100 已提交
333

S
sushuang 已提交
334 335 336 337
        function renderFinally() {
            each(willDeleteEls, function (els) {
                each(els, function (el) {
                    el.parent && el.parent.remove(el);
P
pah100 已提交
338
                });
339
            });
S
sushuang 已提交
340 341 342 343 344 345 346
            each(willInvisibleEls, function (el) {
                el.invisible = true;
                // Setting invisible is for optimizing, so no need to set dirty,
                // just mark as invisible.
                el.dirty();
            });
        }
P
pissang 已提交
347
    }
348

1
100pah 已提交
349
    private _doAnimation(
P
pissang 已提交
350 351 352 353 354
        containerGroup: graphic.Group,
        renderResult: RenderResult,
        seriesModel: TreemapSeriesModel,
        reRoot: ReRoot
    ) {
S
sushuang 已提交
355 356 357
        if (!seriesModel.get('animation')) {
            return;
        }
P
pah100 已提交
358

359 360 361
        const duration = seriesModel.get('animationDurationUpdate');
        const easing = seriesModel.get('animationEasing');
        const animationWrap = animationUtil.createWrap();
362

S
sushuang 已提交
363 364 365
        // Make delete animations.
        each(renderResult.willDeleteEls, function (store, storageName) {
            each(store, function (el, rawIndex) {
P
pissang 已提交
366
                if ((el as Displayable).invisible) {
S
sushuang 已提交
367 368
                    return;
                }
369

370
                const parent = el.parent; // Always has parent, and parent is nodeGroup.
371
                let target: PathProps;
372
                const innerStore = inner(parent);
S
sushuang 已提交
373 374 375 376 377 378 379 380 381 382

                if (reRoot && reRoot.direction === 'drillDown') {
                    target = parent === reRoot.rootNodeGroup
                        // This is the content element of view root.
                        // Only `content` will enter this branch, because
                        // `background` and `nodeGroup` will not be deleted.
                        ? {
                            shape: {
                                x: 0,
                                y: 0,
P
pissang 已提交
383 384
                                width: innerStore.nodeWidth,
                                height: innerStore.nodeHeight
S
sushuang 已提交
385 386 387 388
                            },
                            style: {
                                opacity: 0
                            }
P
pah100 已提交
389
                        }
S
sushuang 已提交
390 391 392 393
                        // Others.
                        : {style: {opacity: 0}};
                }
                else {
394 395
                    let targetX = 0;
                    let targetY = 0;
S
sushuang 已提交
396

P
pissang 已提交
397
                    if (!innerStore.willDelete) {
S
sushuang 已提交
398 399 400
                        // Let node animate to right-bottom corner, cooperating with fadeout,
                        // which is appropriate for user understanding.
                        // Divided by 2 for reRoot rolling up effect.
P
pissang 已提交
401 402
                        targetX = innerStore.nodeWidth / 2;
                        targetY = innerStore.nodeHeight / 2;
P
pah100 已提交
403
                    }
404

S
sushuang 已提交
405
                    target = storageName === 'nodeGroup'
406
                        ? {x: targetX, y: targetY, style: {opacity: 0}}
S
sushuang 已提交
407 408 409 410 411
                        : {
                            shape: {x: targetX, y: targetY, width: 0, height: 0},
                            style: {opacity: 0}
                        };
                }
P
pissang 已提交
412
                // @ts-ignore
S
sushuang 已提交
413
                target && animationWrap.add(el, target, duration, easing);
414
            });
S
sushuang 已提交
415
        });
P
pah100 已提交
416

S
sushuang 已提交
417 418 419
        // Make other animations
        each(this._storage, function (store, storageName) {
            each(store, function (el, rawIndex) {
420 421
                const last = renderResult.lastsForAnimation[storageName][rawIndex];
                const target: PathProps = {};
P
pah100 已提交
422

S
sushuang 已提交
423
                if (!last) {
P
pah100 已提交
424
                    return;
P
pah100 已提交
425
                }
426

P
pissang 已提交
427
                if (el instanceof graphic.Group) {
428 429 430 431 432
                    if (last.oldX != null) {
                        target.x = el.x;
                        target.y = el.y;
                        el.x = last.oldX;
                        el.y = last.oldY;
S
sushuang 已提交
433
                    }
P
pah100 已提交
434
                }
S
sushuang 已提交
435
                else {
P
pissang 已提交
436 437 438
                    if (last.oldShape) {
                        target.shape = extend({}, el.shape);
                        el.setShape(last.oldShape);
439 440
                    }

S
sushuang 已提交
441 442 443 444 445 446 447 448 449
                    if (last.fadein) {
                        el.setStyle('opacity', 0);
                        target.style = {opacity: 1};
                    }
                    // When animation is stopped for succedent animation starting,
                    // el.style.opacity might not be 1
                    else if (el.style.opacity !== 1) {
                        target.style = {opacity: 1};
                    }
450 451
                }

P
pissang 已提交
452
                // @ts-ignore
S
sushuang 已提交
453 454 455
                animationWrap.add(el, target, duration, easing);
            });
        }, this);
P
pah100 已提交
456

S
sushuang 已提交
457
        this._state = 'animating';
P
pah100 已提交
458

S
sushuang 已提交
459 460 461 462 463 464
        animationWrap
            .done(bind(function () {
                this._state = 'ready';
                renderResult.renderFinally();
            }, this))
            .start();
P
pissang 已提交
465
    }
P
pah100 已提交
466

1
100pah 已提交
467
    private _resetController(api: ExtensionAPI) {
468
        let controller = this._controller;
S
sushuang 已提交
469 470 471 472 473 474 475 476

        // Init controller.
        if (!controller) {
            controller = this._controller = new RoamController(api.getZr());
            controller.enable(this.seriesModel.get('roam'));
            controller.on('pan', bind(this._onPan, this));
            controller.on('zoom', bind(this._onZoom, this));
        }
P
pah100 已提交
477

478
        const rect = new BoundingRect(0, 0, api.getWidth(), api.getHeight());
S
sushuang 已提交
479 480 481
        controller.setPointerChecker(function (e, x, y) {
            return rect.contain(x, y);
        });
P
pissang 已提交
482
    }
483

1
100pah 已提交
484
    private _clearController() {
485
        let controller = this._controller;
S
sushuang 已提交
486 487 488 489
        if (controller) {
            controller.dispose();
            controller = null;
        }
P
pissang 已提交
490
    }
491

1
100pah 已提交
492
    private _onPan(e: RoamEventParams['pan']) {
S
sushuang 已提交
493
        if (this._state !== 'animating'
494
            && (Math.abs(e.dx) > DRAG_THRESHOLD || Math.abs(e.dy) > DRAG_THRESHOLD)
S
sushuang 已提交
495 496
        ) {
            // These param must not be cached.
497
            const root = this.seriesModel.getData().tree.root;
S
sushuang 已提交
498 499 500 501

            if (!root) {
                return;
            }
502

503
            const rootLayout = root.getLayout();
P
pah100 已提交
504

S
sushuang 已提交
505 506 507 508 509 510 511 512 513
            if (!rootLayout) {
                return;
            }

            this.api.dispatchAction({
                type: 'treemapMove',
                from: this.uid,
                seriesId: this.seriesModel.id,
                rootRect: {
514
                    x: rootLayout.x + e.dx, y: rootLayout.y + e.dy,
S
sushuang 已提交
515
                    width: rootLayout.width, height: rootLayout.height
P
pah100 已提交
516
                }
517
            } as TreemapMovePayload);
S
sushuang 已提交
518
        }
P
pissang 已提交
519
    }
S
sushuang 已提交
520

1
100pah 已提交
521
    private _onZoom(e: RoamEventParams['zoom']) {
522 523
        let mouseX = e.originX;
        let mouseY = e.originY;
524

S
sushuang 已提交
525 526
        if (this._state !== 'animating') {
            // These param must not be cached.
527
            const root = this.seriesModel.getData().tree.root;
P
pah100 已提交
528

S
sushuang 已提交
529 530
            if (!root) {
                return;
P
pah100 已提交
531 532
            }

533
            const rootLayout = root.getLayout();
P
pah100 已提交
534

S
sushuang 已提交
535 536
            if (!rootLayout) {
                return;
P
pah100 已提交
537 538
            }

539
            const rect = new BoundingRect(
S
sushuang 已提交
540 541
                rootLayout.x, rootLayout.y, rootLayout.width, rootLayout.height
            );
542
            const layoutInfo = this.seriesModel.layoutInfo;
543

S
sushuang 已提交
544 545 546 547 548
            // Transform mouse coord from global to containerGroup.
            mouseX -= layoutInfo.x;
            mouseY -= layoutInfo.y;

            // Scale root bounding rect.
549
            const m = matrix.create();
S
sushuang 已提交
550
            matrix.translate(m, m, [-mouseX, -mouseY]);
551
            matrix.scale(m, m, [e.scale, e.scale]);
S
sushuang 已提交
552 553 554
            matrix.translate(m, m, [mouseX, mouseY]);

            rect.applyTransform(m);
P
pah100 已提交
555

P
pah100 已提交
556
            this.api.dispatchAction({
S
sushuang 已提交
557
                type: 'treemapRender',
P
pah100 已提交
558 559
                from: this.uid,
                seriesId: this.seriesModel.id,
S
sushuang 已提交
560 561 562
                rootRect: {
                    x: rect.x, y: rect.y,
                    width: rect.width, height: rect.height
P
pah100 已提交
563
                }
564
            } as TreemapRenderPayload);
P
pah100 已提交
565
        }
P
pissang 已提交
566
    }
P
pah100 已提交
567

1
100pah 已提交
568
    private _initEvents(containerGroup: graphic.Group) {
P
pissang 已提交
569
        containerGroup.on('click', (e) => {
S
sushuang 已提交
570 571 572
            if (this._state !== 'ready') {
                return;
            }
P
pah100 已提交
573

574
            const nodeClick = this.seriesModel.get('nodeClick', true);
575

S
sushuang 已提交
576 577 578
            if (!nodeClick) {
                return;
            }
579

580
            const targetInfo = this.findTarget(e.offsetX, e.offsetY);
581

S
sushuang 已提交
582 583 584
            if (!targetInfo) {
                return;
            }
585

586
            const node = targetInfo.node;
S
sushuang 已提交
587 588 589 590 591 592 593 594
            if (node.getLayout().isLeafRoot) {
                this._rootToNode(targetInfo);
            }
            else {
                if (nodeClick === 'zoomToNode') {
                    this._zoomToNode(targetInfo);
                }
                else if (nodeClick === 'link') {
595 596 597
                    const itemModel = node.hostTree.data.getItemModel<TreeSeriesNodeItemOption>(node.dataIndex);
                    const link = itemModel.get('link', true);
                    const linkTarget = itemModel.get('target', true) || 'blank';
S
susiwen8 已提交
598
                    link && windowOpen(link, linkTarget);
S
sushuang 已提交
599 600
                }
            }
601

S
sushuang 已提交
602
        }, this);
P
pissang 已提交
603
    }
604

1
100pah 已提交
605
    private _renderBreadcrumb(seriesModel: TreemapSeriesModel, api: ExtensionAPI, targetInfo: FoundTargetInfo) {
S
sushuang 已提交
606 607 608 609 610 611 612
        if (!targetInfo) {
            targetInfo = seriesModel.get('leafDepth', true) != null
                ? {node: seriesModel.getViewRoot()}
                // FIXME
                // better way?
                // Find breadcrumb tail on center of containerGroup.
                : this.findTarget(api.getWidth() / 2, api.getHeight() / 2);
613

S
sushuang 已提交
614 615 616 617
            if (!targetInfo) {
                targetInfo = {node: seriesModel.getData().tree.root};
            }
        }
618

S
sushuang 已提交
619
        (this._breadcrumb || (this._breadcrumb = new Breadcrumb(this.group)))
P
pissang 已提交
620 621 622 623 624 625 626 627
            .render(seriesModel, api, targetInfo.node, (node) => {
                if (this._state !== 'animating') {
                    helper.aboveViewRoot(seriesModel.getViewRoot(), node)
                        ? this._rootToNode({node: node})
                        : this._zoomToNode({node: node});
                }
            });
    }
628

S
sushuang 已提交
629 630 631
    /**
     * @override
     */
P
pissang 已提交
632
    remove() {
S
sushuang 已提交
633 634
        this._clearController();
        this._containerGroup && this._containerGroup.removeAll();
P
pissang 已提交
635
        this._storage = createStorage() as RenderElementStorage;
S
sushuang 已提交
636 637
        this._state = 'ready';
        this._breadcrumb && this._breadcrumb.remove();
P
pissang 已提交
638
    }
S
sushuang 已提交
639

P
pissang 已提交
640
    dispose() {
S
sushuang 已提交
641
        this._clearController();
P
pissang 已提交
642
    }
643

1
100pah 已提交
644
    private _zoomToNode(targetInfo: FoundTargetInfo) {
S
sushuang 已提交
645 646 647 648 649 650
        this.api.dispatchAction({
            type: 'treemapZoomToNode',
            from: this.uid,
            seriesId: this.seriesModel.id,
            targetNode: targetInfo.node
        });
P
pissang 已提交
651
    }
652

1
100pah 已提交
653
    private _rootToNode(targetInfo: FoundTargetInfo) {
S
sushuang 已提交
654 655 656 657 658 659
        this.api.dispatchAction({
            type: 'treemapRootToNode',
            from: this.uid,
            seriesId: this.seriesModel.id,
            targetNode: targetInfo.node
        });
P
pissang 已提交
660
    }
661

S
sushuang 已提交
662 663 664 665 666 667 668 669 670
    /**
     * @public
     * @param {number} x Global coord x.
     * @param {number} y Global coord y.
     * @return {Object} info If not found, return undefined;
     * @return {number} info.node Target node.
     * @return {number} info.offsetX x refer to target node.
     * @return {number} info.offsetY y refer to target node.
     */
P
pissang 已提交
671
    findTarget(x: number, y: number): FoundTargetInfo {
672
        let targetInfo;
673
        const viewRoot = this.seriesModel.getViewRoot();
S
sushuang 已提交
674 675

        viewRoot.eachNode({attr: 'viewChildren', order: 'preorder'}, function (node) {
676
            const bgEl = this._storage.background[node.getRawIndex()];
S
sushuang 已提交
677 678
            // If invisible, there might be no element.
            if (bgEl) {
679 680
                const point = bgEl.transformCoordToLocal(x, y);
                const shape = bgEl.shape;
S
sushuang 已提交
681 682 683 684 685 686 687

                // For performance consideration, dont use 'getBoundingRect'.
                if (shape.x <= point[0]
                    && point[0] <= shape.x + shape.width
                    && shape.y <= point[1]
                    && point[1] <= shape.y + shape.height
                ) {
P
pissang 已提交
688 689 690 691 692
                    targetInfo = {
                        node: node,
                        offsetX: point[0],
                        offsetY: point[1]
                    };
S
sushuang 已提交
693 694 695 696 697 698
                }
                else {
                    return false; // Suppress visit subtree.
                }
            }
        }, this);
699

S
sushuang 已提交
700 701
        return targetInfo;
    }
P
pissang 已提交
702
}
S
sushuang 已提交
703 704 705 706

/**
 * @inner
 */
P
pissang 已提交
707 708 709 710 711 712
function createStorage(): RenderElementStorage | LastCfgStorage {
    return {
        nodeGroup: [],
        background: [],
        content: []
    };
S
sushuang 已提交
713 714 715 716 717 718 719
}

/**
 * @inner
 * @return Return undefined means do not travel further.
 */
function renderNode(
P
pissang 已提交
720 721 722 723 724 725 726 727 728 729
    seriesModel: TreemapSeriesModel,
    thisStorage: RenderElementStorage,
    oldStorage: RenderElementStorage,
    reRoot: ReRoot,
    lastsForAnimation: RenderResult['lastsForAnimation'],
    willInvisibleEls: RenderResult['willInvisibleEls'],
    thisNode: TreeNode,
    oldNode: TreeNode,
    parentGroup: graphic.Group,
    depth: number
S
sushuang 已提交
730 731 732 733 734 735 736 737
) {
    // Whether under viewRoot.
    if (!thisNode) {
        // Deleting nodes will be performed finally. This method just find
        // element from old storage, or create new element, set them to new
        // storage, and set styles.
        return;
    }
738

S
sushuang 已提交
739 740
    // -------------------------------------------------------------------
    // Start of closure variables available in "Procedures in renderNode".
741

742 743 744
    const thisLayout = thisNode.getLayout();
    const data = seriesModel.getData();
    const nodeModel = thisNode.getModel<TreemapSeriesNodeItemOption>();
745 746 747 748

    // Only for enabling highlight/downplay. Clear firstly.
    // Because some node will not be rendered.
    data.setItemGraphicEl(thisNode.dataIndex, null);
749

S
sushuang 已提交
750 751 752
    if (!thisLayout || !thisLayout.isInView) {
        return;
    }
753

754 755 756 757
    const thisWidth = thisLayout.width;
    const thisHeight = thisLayout.height;
    const borderWidth = thisLayout.borderWidth;
    const thisInvisible = thisLayout.invisible;
758

759 760
    const thisRawIndex = thisNode.getRawIndex();
    const oldRawIndex = oldNode && oldNode.getRawIndex();
761

762 763 764 765 766
    const thisViewChildren = thisNode.viewChildren;
    const upperHeight = thisLayout.upperHeight;
    const isParent = thisViewChildren && thisViewChildren.length;
    const itemStyleNormalModel = nodeModel.getModel('itemStyle');
    const itemStyleEmphasisModel = nodeModel.getModel(['emphasis', 'itemStyle']);
767 768
    const itemStyleBlurModel = nodeModel.getModel(['blur', 'itemStyle']);
    const itemStyleSelectModel = nodeModel.getModel(['select', 'itemStyle']);
P
pissang 已提交
769
    const borderRadius = itemStyleNormalModel.get('borderRadius') || 0;
770

S
sushuang 已提交
771 772
    // End of closure ariables available in "Procedures in renderNode".
    // -----------------------------------------------------------------
773

S
sushuang 已提交
774
    // Node group
775
    const group = giveGraphic('nodeGroup', Group);
776

S
sushuang 已提交
777 778 779
    if (!group) {
        return;
    }
780

S
sushuang 已提交
781 782
    parentGroup.add(group);
    // x,y are not set when el is above view root.
783 784
    group.x = thisLayout.x || 0;
    group.y = thisLayout.y || 0;
785
    group.markRedraw();
P
pissang 已提交
786 787
    inner(group).nodeWidth = thisWidth;
    inner(group).nodeHeight = thisHeight;
788

S
sushuang 已提交
789 790 791
    if (thisLayout.isAboveViewRoot) {
        return group;
    }
792

S
sushuang 已提交
793
    // Background
794
    const bg = giveGraphic('background', Rect, depth, Z_BG);
795
    bg && renderBackground(group, bg, isParent && thisLayout.upperLabelHeight);
796

P
pissang 已提交
797 798 799
    const focus = nodeModel.get(['emphasis', 'focus']);
    const blurScope = nodeModel.get(['emphasis', 'blurScope']);

800 801 802
    const focusDataIndices: number[] = focus === 'ancestor'
        ? thisNode.getAncestorsIndices()
        : focus === 'descendant' ? thisNode.getDescendantIndices() : null;
803

S
sushuang 已提交
804
    // No children, render content.
805 806 807 808
    if (isParent) {
        // Because of the implementation about "traverse" in graphic hover style, we
        // can not set hover listener on the "group" of non-leaf node. Otherwise the
        // hover event from the descendents will be listenered.
809 810
        if (isHighDownDispatcher(group)) {
            setAsHighDownDispatcher(group, false);
811 812
        }
        if (bg) {
813
            setAsHighDownDispatcher(bg, true);
814 815
            // Only for enabling highlight/downplay.
            data.setItemGraphicEl(thisNode.dataIndex, bg);
P
pissang 已提交
816

817
            enableHoverFocus(bg, focusDataIndices || focus, blurScope);
818 819 820
        }
    }
    else {
821
        const content = giveGraphic('content', Rect, depth, Z_CONTENT);
S
sushuang 已提交
822
        content && renderContent(group, content);
823

824 825
        if (bg && isHighDownDispatcher(bg)) {
            setAsHighDownDispatcher(bg, false);
826
        }
827
        setAsHighDownDispatcher(group, true);
828 829
        // Only for enabling highlight/downplay.
        data.setItemGraphicEl(thisNode.dataIndex, group);
P
pissang 已提交
830

831
        enableHoverFocus(group, focusDataIndices || focus, blurScope);
S
sushuang 已提交
832
    }
833

S
sushuang 已提交
834
    return group;
835

S
sushuang 已提交
836 837 838
    // ----------------------------
    // | Procedures in renderNode |
    // ----------------------------
839

P
pissang 已提交
840
    function renderBackground(group: graphic.Group, bg: graphic.Rect, useUpperLabel: boolean) {
841
        const ecData = graphic.getECData(bg);
S
sushuang 已提交
842
        // For tooltip.
P
pissang 已提交
843 844
        ecData.dataIndex = thisNode.dataIndex;
        ecData.seriesIndex = seriesModel.seriesIndex;
S
sushuang 已提交
845

P
pissang 已提交
846
        bg.setShape({x: 0, y: 0, width: thisWidth, height: thisHeight, r: borderRadius});
S
sushuang 已提交
847

848 849 850 851
        if (thisInvisible) {
            // If invisible, do not set visual, otherwise the element will
            // change immediately before animation. We think it is OK to
            // remain its origin color when moving out of the view window.
852
            processInvisible(bg);
853 854
        }
        else {
855
            bg.invisible = false;
856
            const visualBorderColor = thisNode.getVisual('style').stroke;
857
            const normalStyle = getItemStyleNormal(itemStyleNormalModel);
S
sushuang 已提交
858
            normalStyle.fill = visualBorderColor;
859 860 861 862 863 864
            const emphasisStyle = getStateItemStyle(itemStyleEmphasisModel);
            emphasisStyle.fill = itemStyleEmphasisModel.get('borderColor');
            const blurStyle = getStateItemStyle(itemStyleBlurModel);
            blurStyle.fill = itemStyleBlurModel.get('borderColor');
            const selectStyle = getStateItemStyle(itemStyleSelectModel);
            selectStyle.fill = itemStyleSelectModel.get('borderColor');
S
sushuang 已提交
865 866

            if (useUpperLabel) {
867
                const upperLabelWidth = thisWidth - 2 * borderWidth;
S
sushuang 已提交
868 869

                prepareText(
P
pissang 已提交
870
                    bg, visualBorderColor, upperLabelWidth, upperHeight,
S
sushuang 已提交
871 872
                    {x: borderWidth, y: 0, width: upperLabelWidth, height: upperHeight}
                );
873
            }
S
sushuang 已提交
874
            // For old bg.
875
            else {
P
pissang 已提交
876
                bg.removeTextContent();
877 878
            }

S
sushuang 已提交
879
            bg.setStyle(normalStyle);
880 881 882 883

            bg.ensureState('emphasis').style = emphasisStyle;
            bg.ensureState('blur').style = blurStyle;
            bg.ensureState('select').style = selectStyle;
884
            setDefaultStateProxy(bg);
885
        }
886

S
sushuang 已提交
887 888
        group.add(bg);
    }
889

P
pissang 已提交
890
    function renderContent(group: graphic.Group, content: graphic.Rect) {
891
        const ecData = graphic.getECData(content);
S
sushuang 已提交
892
        // For tooltip.
P
pissang 已提交
893 894
        ecData.dataIndex = thisNode.dataIndex;
        ecData.seriesIndex = seriesModel.seriesIndex;
895

896 897
        const contentWidth = Math.max(thisWidth - 2 * borderWidth, 0);
        const contentHeight = Math.max(thisHeight - 2 * borderWidth, 0);
898

S
sushuang 已提交
899 900 901 902 903
        content.culling = true;
        content.setShape({
            x: borderWidth,
            y: borderWidth,
            width: contentWidth,
P
pissang 已提交
904 905
            height: contentHeight,
            r: borderRadius
S
sushuang 已提交
906
        });
907

908 909 910 911 912 913 914
        if (thisInvisible) {
            // If invisible, do not set visual, otherwise the element will
            // change immediately before animation. We think it is OK to
            // remain its origin color when moving out of the view window.
            processInvisible(content);
        }
        else {
915
            content.invisible = false;
916
            const visualColor = thisNode.getVisual('style').fill;
917
            const normalStyle = getItemStyleNormal(itemStyleNormalModel);
S
sushuang 已提交
918
            normalStyle.fill = visualColor;
919 920 921
            const emphasisStyle = getStateItemStyle(itemStyleEmphasisModel);
            const blurStyle = getStateItemStyle(itemStyleBlurModel);
            const selectStyle = getStateItemStyle(itemStyleSelectModel);
922

P
pissang 已提交
923
            prepareText(content, visualColor, contentWidth, contentHeight);
924

S
sushuang 已提交
925
            content.setStyle(normalStyle);
926 927 928
            content.ensureState('emphasis').style = emphasisStyle;
            content.ensureState('blur').style = blurStyle;
            content.ensureState('select').style = selectStyle;
929
            setDefaultStateProxy(content);
930
        }
S
sushuang 已提交
931 932 933 934

        group.add(content);
    }

P
pissang 已提交
935
    function processInvisible(element: graphic.Rect) {
936 937 938
        // Delay invisible setting utill animation finished,
        // avoid element vanish suddenly before animation.
        !element.invisible && willInvisibleEls.push(element);
S
sushuang 已提交
939
    }
940

P
pissang 已提交
941
    function prepareText(
P
pissang 已提交
942
        rectEl: graphic.Rect,
P
pissang 已提交
943 944 945 946 947
        visualColor: ColorString,
        width: number,
        height: number,
        upperLabelRect?: RectLike
    ) {
948 949 950 951
        const normalLabelModel = nodeModel.getModel(
            upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL
        );

952
        let text = retrieve(
S
sushuang 已提交
953
            seriesModel.getFormattedLabel(
954
                thisNode.dataIndex, 'normal', null, null, normalLabelModel.get('formatter')
S
sushuang 已提交
955 956 957 958
            ),
            nodeModel.get('name')
        );
        if (!upperLabelRect && thisLayout.isLeafRoot) {
959
            const iconChar = seriesModel.get('drillDownIcon', true);
S
sushuang 已提交
960
            text = iconChar ? iconChar + ' ' + text : text;
961 962
        }

963
        const isShow = normalLabelModel.getShallow('show');
S
sushuang 已提交
964

965
        setLabelStyle(
P
pissang 已提交
966
            rectEl, getLabelStatesModels(nodeModel, upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL),
S
sushuang 已提交
967 968
            {
                defaultText: isShow ? text : null,
969
                inheritColor: visualColor,
970
                labelFetcher: seriesModel,
971
                labelDataIndex: thisNode.dataIndex
S
sushuang 已提交
972 973 974
            }
        );

975 976 977 978
        const textEl = rectEl.getTextContent();
        const textStyle = textEl.style;
        const textPadding = normalizeCssArray(textStyle.padding || 0);

979 980 981 982 983 984
        if (upperLabelRect) {
            rectEl.setTextConfig({
                layoutRect: upperLabelRect
            });
            (textEl as ECElement).disableLabelLayout = true;
        }
985 986 987 988 989 990 991 992 993 994 995 996 997 998
        textEl.beforeUpdate = function () {
            const width = Math.max(
                (upperLabelRect ? upperLabelRect.width : rectEl.shape.width) - textPadding[1] - textPadding[3], 0
            );
            const height = Math.max(
                (upperLabelRect ? upperLabelRect.height : rectEl.shape.height) - textPadding[0] - textPadding[2], 0
            );
            if (textStyle.width !== width || textStyle.height !== height) {
                textEl.setStyle({
                    width,
                    height
                });
            }
        };
999

1000
        textStyle.truncateMinChar = 2;
1001
        textStyle.lineOverflow = 'truncate';
1002 1003 1004 1005

        addDrillDownIcon(textStyle, upperLabelRect, thisLayout);
        const textEmphasisState = textEl.getState('emphasis');
        addDrillDownIcon(textEmphasisState ? textEmphasisState.style : null, upperLabelRect, thisLayout);
S
sushuang 已提交
1006 1007
    }

1008 1009
    function addDrillDownIcon(style: TextStyleProps, upperLabelRect: RectLike, thisLayout: any) {
        const text = style ? style.text : null;
1010
        if (!upperLabelRect && thisLayout.isLeafRoot && text != null) {
1011
            const iconChar = seriesModel.get('drillDownIcon', true);
1012 1013 1014 1015
            style.text = iconChar ? iconChar + ' ' + text : text;
        }
    }

P
pissang 已提交
1016 1017 1018 1019 1020 1021
    function giveGraphic<T extends graphic.Group | graphic.Rect>(
        storageName: keyof RenderElementStorage,
        Ctor: {new(): T},
        depth?: number,
        z?: number
    ): T {
1022
        let element = oldRawIndex != null && oldStorage[storageName][oldRawIndex];
1023
        const lasts = lastsForAnimation[storageName];
1024

S
sushuang 已提交
1025 1026 1027
        if (element) {
            // Remove from oldStorage
            oldStorage[storageName][oldRawIndex] = null;
P
pissang 已提交
1028
            prepareAnimationWhenHasOld(lasts, element);
1029
        }
S
sushuang 已提交
1030 1031
        // If invisible and no old element, do not create new element (for optimizing).
        else if (!thisInvisible) {
P
pissang 已提交
1032 1033 1034 1035 1036
            element = new Ctor();
            if (element instanceof Displayable) {
                element.z = calculateZ(depth, z);
            }
            prepareAnimationWhenNoOld(lasts, element);
S
sushuang 已提交
1037 1038 1039
        }

        // Set to thisStorage
P
pissang 已提交
1040
        return (thisStorage[storageName][thisRawIndex] = element) as T;
1041 1042
    }

P
pissang 已提交
1043
    function prepareAnimationWhenHasOld(lasts: LastCfg[], element: graphic.Group | graphic.Rect) {
1044
        const lastCfg = lasts[thisRawIndex] = {} as LastCfg;
P
pissang 已提交
1045
        if (element instanceof Group) {
1046 1047
            lastCfg.oldX = element.x;
            lastCfg.oldY = element.y;
P
pissang 已提交
1048 1049 1050 1051
        }
        else {
            lastCfg.oldShape = extend({}, element.shape);
        }
1052 1053
    }

S
sushuang 已提交
1054 1055
    // If a element is new, we need to find the animation start point carefully,
    // otherwise it will looks strange when 'zoomToNode'.
P
pissang 已提交
1056
    function prepareAnimationWhenNoOld(lasts: LastCfg[], element: graphic.Group | graphic.Rect) {
1057 1058 1059
        const lastCfg = lasts[thisRawIndex] = {} as LastCfg;
        const parentNode = thisNode.parentNode;
        const isGroup = element instanceof graphic.Group;
S
sushuang 已提交
1060 1061

        if (parentNode && (!reRoot || reRoot.direction === 'drillDown')) {
1062 1063
            let parentOldX = 0;
            let parentOldY = 0;
S
sushuang 已提交
1064 1065 1066

            // New nodes appear from right-bottom corner in 'zoomToNode' animation.
            // For convenience, get old bounding rect from background.
1067
            const parentOldBg = lastsForAnimation.background[parentNode.getRawIndex()];
P
pissang 已提交
1068 1069 1070
            if (!reRoot && parentOldBg && parentOldBg.oldShape) {
                parentOldX = parentOldBg.oldShape.width;
                parentOldY = parentOldBg.oldShape.height;
S
sushuang 已提交
1071 1072 1073 1074
            }

            // When no parent old shape found, its parent is new too,
            // so we can just use {x:0, y:0}.
P
pissang 已提交
1075
            if (isGroup) {
1076 1077
                lastCfg.oldX = 0;
                lastCfg.oldY = parentOldY;
P
pissang 已提交
1078 1079 1080 1081
            }
            else {
                lastCfg.oldShape = {x: parentOldX, y: parentOldY, width: 0, height: 0};
            }
S
sushuang 已提交
1082 1083 1084
        }

        // Fade in, user can be aware that these nodes are new.
P
pissang 已提交
1085
        lastCfg.fadein = !isGroup;
S
sushuang 已提交
1086
    }
1087

S
sushuang 已提交
1088 1089 1090 1091 1092 1093 1094 1095
}

// We can not set all backgroud with the same z, Because the behaviour of
// drill down and roll up differ background creation sequence from tree
// hierarchy sequence, which cause that lowser background element overlap
// upper ones. So we calculate z based on depth.
// Moreover, we try to shrink down z interval to [0, 1] to avoid that
// treemap with large z overlaps other components.
P
pissang 已提交
1096
function calculateZ(depth: number, zInLevel: number) {
1097
    const zb = depth * Z_BASE + zInLevel;
S
sushuang 已提交
1098 1099
    return (zb - 1) / zb;
}
P
pissang 已提交
1100 1101

ChartView.registerClass(TreemapView);