TreemapView.js 19.9 KB
Newer Older
P
pah100 已提交
1
 define(function(require) {
P
pah100 已提交
2 3 4 5

    var zrUtil = require('zrender/core/util');
    var graphic = require('../../util/graphic');
    var layout = require('../../util/layout');
P
pah100 已提交
6
    var DataDiffer = require('../../data/DataDiffer');
P
pah100 已提交
7 8
    var modelUtil = require('../../util/model');
    var helper = require('./helper');
P
pah100 已提交
9
    var Breadcrumb = require('./Breadcrumb');
P
pah100 已提交
10 11 12 13
    var RoamController = require('../../component/helper/RoamController');
    var BoundingRect = require('zrender/core/BoundingRect');
    var matrix = require('zrender/core/matrix');
    var bind = zrUtil.bind;
P
pah100 已提交
14 15 16
    var Group = graphic.Group;
    var Rect = graphic.Rect;

P
pah100 已提交
17 18 19 20
    var ANIMATION_DURATION = 700;
    var EASING = 'cubicOut';
    var DRAG_THRESHOLD = 3;

P
pah100 已提交
21 22 23 24 25 26 27
    return require('../../echarts').extendChartView({

        type: 'treemap',

        /**
         * @override
         */
P
pah100 已提交
28
        init: function (o, api) {
P
pah100 已提交
29

P
pah100 已提交
30 31 32 33
            /**
             * @private
             * @type {module:zrender/container/Group}
             */
P
pah100 已提交
34
            this._containerGroup;
P
pah100 已提交
35 36 37 38 39 40 41

            /**
             * @private
             * @type {Object.<string, Array.<module:zrender/container/Group>>}
             */
            this._storage;

P
pah100 已提交
42 43 44 45 46 47
            /**
             * @private
             * @type {Object.<string, Array.<module:zrender/container/Group>>}
             */
            this._lastShapes;

P
pah100 已提交
48 49 50 51 52
            /**
             * @private
             * @type {module:echarts/data/Tree}
             */
            this._oldTree;
P
pah100 已提交
53 54 55 56 57 58

            /**
             * @private
             * @type {module:echarts/chart/treemap/Breadcrumb}
             */
            this._breadcrumb;
P
pah100 已提交
59 60 61 62 63 64 65 66 67 68 69 70

            /**
             * @private
             * @type {module:echarts/component/helper/RoamController}
             */
            this._controller;

            /**
             * 'ready', 'dragging', 'animating'
             * @private
             */
            this._state = 'ready';
P
pah100 已提交
71 72 73 74 75
        },

        /**
         * @override
         */
P
pah100 已提交
76 77 78 79 80
        render: function (seriesModel, ecModel, api, payload) {
            if (helper.irrelevant(payload, seriesModel)) {
                return;
            }

P
pah100 已提交
81
            this.seriesModel = seriesModel;
P
pah100 已提交
82
            this.api = api;
P
pah100 已提交
83
            this.ecModel = ecModel;
P
pah100 已提交
84

P
pah100 已提交
85 86
            var thisTree = seriesModel.getData().tree;
            var containerGroup = this._containerGroup;
P
pah100 已提交
87 88
            var lastContainerPosition;
            var containerSize = seriesModel.containerSize;
P
pah100 已提交
89

P
pah100 已提交
90
            if (!containerGroup) {
P
pah100 已提交
91 92
                containerGroup = this._containerGroup = new Group();
                this._initEvents(containerGroup);
P
pah100 已提交
93
                this.group.add(containerGroup);
P
pah100 已提交
94
            }
P
pah100 已提交
95 96 97 98
            else {
                // First rendering do not animate.
                lastContainerPosition = containerGroup.position.slice();
            }
P
pah100 已提交
99

P
pah100 已提交
100
            var positionInfo = layout.parsePositionInfo(
P
pah100 已提交
101 102 103 104
                {
                    x: seriesModel.get('x'),
                    y: seriesModel.get('y'),
                    x2: seriesModel.get('x2'),
P
pah100 已提交
105 106 107
                    y2: seriesModel.get('y2'),
                    width: containerSize[0],
                    height: containerSize[1]
P
pah100 已提交
108 109 110 111 112 113
                },
                {
                    width: api.getWidth(),
                    height: api.getHeight()
                }
            );
P
pah100 已提交
114
            containerGroup.position = [positionInfo.x, positionInfo.y];
P
pah100 已提交
115

P
pah100 已提交
116 117 118 119 120 121
            this._doRender(thisTree, containerGroup, seriesModel);

            var targetInfo = helper.retrieveTargetNodeInfo(payload, seriesModel);
            var viewRect = payload && payload.type === 'treemapRender' && payload.viewRect;

            this._positionRoot(containerGroup, positionInfo, thisTree.root, viewRect, targetInfo);
P
pah100 已提交
122

P
pah100 已提交
123 124 125
            this._doAnimation(payload, containerGroup, lastContainerPosition);

            this._initController(positionInfo, thisTree.root, seriesModel, api);
P
pah100 已提交
126 127

            this._renderBreadcrumb(seriesModel, api, targetInfo);
P
pah100 已提交
128 129 130 131 132 133 134 135
        },

        /**
         * @private
         */
        _doRender: function (thisTree, containerGroup, seriesModel) {
            var oldTree = this._oldTree;

P
pah100 已提交
136 137
            // Clear last shape records.
            this._lastShapes = {nodeGroup: [], background: [], content: []};
P
pah100 已提交
138 139
            var thisStorage = {nodeGroup: [], background: [], content: []};
            var oldStorage = this._storage;
P
pah100 已提交
140
            var renderNode = bind(this._renderNode, this);
P
pah100 已提交
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
            var viewRoot = seriesModel.getViewRoot();

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

            // Process all removing.
            clearStorage(oldStorage);

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

P
pah100 已提交
157

P
pah100 已提交
158 159 160 161 162 163
            function dualTravel(thisViewChildren, oldViewChildren, parentGroup, sameTree, inView) {
                // When 'render' is triggered by action,
                // 'this' and 'old' may be the same tree,
                // we use rawIndex in that case.
                if (sameTree) {
                    oldViewChildren = thisViewChildren;
P
pah100 已提交
164
                    zrUtil.each(thisViewChildren, function (child, index) {
P
pah100 已提交
165
                        !child.isRemoved() && processNode(index, index);
P
pah100 已提交
166 167
                    });
                }
P
pah100 已提交
168 169
                // Diff hierarchically (diff only in each subtree, but not whole).
                // because, consistency of view is important.
P
pah100 已提交
170
                else {
P
pah100 已提交
171 172 173 174 175 176 177 178 179
                    (new DataDiffer(oldViewChildren, thisViewChildren, getKey))
                        .add(processNode)
                        .update(processNode)
                        .remove(zrUtil.curry(processNode, null));
                }

                function getKey(node) {
                    // Identify by name or raw index.
                    return node.name != null ? node.name : node.getRawIndex();
P
pah100 已提交
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
                }

                function processNode(newIndex, oldIndex) {
                    var thisNode = newIndex != null ? thisViewChildren[newIndex] : null;
                    var oldNode = oldIndex != null ? oldViewChildren[oldIndex] : null;

                    // Whether under viewRoot.
                    var subInView = inView || thisNode === viewRoot;
                    // If not under viewRoot, only remove.
                    if (!subInView) {
                        thisNode = null;
                    }

                    var group = renderNode(
                        thisNode, oldNode, parentGroup, thisStorage, oldStorage
                    );
                    dualTravel(
                        thisNode && thisNode.viewChildren || [],
                        oldNode && oldNode.viewChildren || [],
                        group,
P
pah100 已提交
200
                        sameTree,
P
pah100 已提交
201 202 203 204
                        subInView
                    );
                }
            }
P
pah100 已提交
205 206 207 208 209 210 211 212

            function clearStorage(storage) {
                storage && zrUtil.each(storage, function (store) {
                    zrUtil.each(store, function (shape) {
                        shape && shape.parent && shape.parent.remove(shape);
                    });
                });
            }
P
pah100 已提交
213 214 215 216 217
        },

        /**
         * @private
         */
P
pah100 已提交
218 219 220
        _renderNode: function (thisNode, oldNode, parentGroup, thisStorage, oldStorage) {
            var thisRawIndex = thisNode && thisNode.getRawIndex();
            var oldRawIndex = oldNode && oldNode.getRawIndex();
P
pah100 已提交
221
            var lastShapes = this._lastShapes;
P
pah100 已提交
222 223 224 225 226 227

            if (!thisNode) {
                return;
            }

            var layout = thisNode.getLayout();
P
pah100 已提交
228 229 230
            var thisWidth = layout.width;
            var thisHeight = layout.height;

P
pah100 已提交
231 232
            // Node group
            var group = giveGraphic('nodeGroup', Group, 'position');
P
pah100 已提交
233
            parentGroup.add(group);
P
pah100 已提交
234
            group.position = [layout.x, layout.y];
P
pah100 已提交
235 236 237

            var itemStyleModel = thisNode.getModel('itemStyle.normal');
            var borderColor = itemStyleModel.get('borderColor') || itemStyleModel.get('gapColor');
P
pah100 已提交
238 239

            // Background
P
pah100 已提交
240 241 242 243
            var bg = giveGraphic('background', Rect, 'shape');
            bg.setShape({x: 0, y: 0, width: thisWidth, height: thisHeight});
            bg.setStyle({fill: borderColor});
            group.add(bg);
P
pah100 已提交
244 245

            var thisViewChildren = thisNode.viewChildren;
P
pah100 已提交
246 247

            // No children, render content.
P
pah100 已提交
248
            if (!thisViewChildren || !thisViewChildren.length) {
P
pah100 已提交
249
                var borderWidth = layout.borderWidth;
P
pah100 已提交
250

P
pah100 已提交
251 252 253 254 255 256 257 258 259 260 261
                var content = giveGraphic('content', Rect, 'shape');
                var contentWidth = Math.max(thisWidth - 2 * borderWidth, 0);
                var contentHeight = Math.max(thisHeight - 2 * borderWidth, 0);
                var textStyleModel = thisNode.getModel('label.normal.textStyle');
                var text = thisNode.getModel().get('name');
                var textRect = textStyleModel.getTextRect(text);

                if (textRect.width > contentWidth || textRect.height > contentHeight) {
                    text = '';
                }

L
lang 已提交
262
                content.culling = true;
P
pah100 已提交
263 264 265
                content.setShape({
                    x: borderWidth,
                    y: borderWidth,
P
pah100 已提交
266 267
                    width: contentWidth,
                    height: contentHeight
P
pah100 已提交
268 269
                });
                content.setStyle({
P
pah100 已提交
270 271 272 273 274
                    fill: thisNode.getVisual('color', true),
                    text: text,
                    textFill: textStyleModel.get('color'),
                    textAlign: textStyleModel.get('align'),
                    textFont: textStyleModel.getFont()
P
pah100 已提交
275 276
                });
                group.add(content);
P
pah100 已提交
277 278
            }

P
pah100 已提交
279 280
            return group;

P
pah100 已提交
281
            function giveGraphic(storage, Ctor, type) {
P
pah100 已提交
282 283 284 285 286
                var shape = oldRawIndex != null && oldStorage && oldStorage[storage][oldRawIndex];

                if (shape) {
                    // Remove from oldStorage
                    oldStorage && (oldStorage[storage][oldRawIndex] = null);
P
pah100 已提交
287 288 289
                    var lastCfg = lastShapes[storage][thisRawIndex] = {};
                    lastCfg[type] = type === 'position'
                         ? shape.position.slice() : zrUtil.extend({}, shape.shape);
P
pah100 已提交
290 291
                }
                else {
P
pah100 已提交
292 293
                    // FIXME
                    // 太多函数?
P
pah100 已提交
294 295 296 297 298
                    shape = new Ctor();
                }

                // Set to thisStorage
                return thisStorage[storage][thisRawIndex] = shape;
P
pah100 已提交
299
            }
P
pah100 已提交
300 301
        },

P
pah100 已提交
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
        /**
         * @private
         */
        _doAnimation: function (payload, containerGroup, lastContainerPosition) {
            if (!this.ecModel.get('animation')
                || (payload && payload.type === 'treemapRender')
            ) {
                return;
            }

            var lastShapes = this._lastShapes;
            var animationCount = 0;
            var that = this;

            zrUtil.each(this._storage, function (shapes, key) {
                zrUtil.each(shapes, function (shape, index) {
                    var last = lastShapes[key][index];
                    if (!last) {
                        return;
                    }
                    if (last.position) {
                        var target = shape.position.slice();
                        shape.position = last.position;
                        shape.animateTo({position: target}, ANIMATION_DURATION, EASING, done);
                        animationCount++;
                    }
                    else if (last.shape) {
                        var target = zrUtil.extend({}, shape.shape);
                        shape.setShape(last.shape);
                        shape.animateTo({shape: target}, ANIMATION_DURATION, EASING, done);
                        animationCount++;
                    }
                });
            });

            if (lastContainerPosition) {
                var target = containerGroup.position.slice();
                containerGroup.position = lastContainerPosition;
                containerGroup.animateTo({position: target}, ANIMATION_DURATION, EASING, done);
                animationCount++;
            }

            if (animationCount) {
                this._state = 'animating';
            }
            function done() {
                animationCount--;
                if (!animationCount) {
                    that._state = 'ready';
                }
            }
        },

        /**
         * @private
         */
        _initController: function (positionInfo, root, seriesModel, api) {
            var controller = this._controller;
            var nodeGroups = this._storage.nodeGroup;
            var rootGroup = nodeGroups[root.getRawIndex()];
            var containerGroup = this._containerGroup;

            // Init controller.
            if (!controller) {
                controller = this._controller = new RoamController(api.getZr());
                controller.on('pan', bind(onPan, this));
                controller.on('zoom', bind(onZoom, this));
                controller.rect = new BoundingRect(0, 0, api.getWidth(), api.getHeight());
            }

            if (!seriesModel.get('roam')) {
                controller.off('pan').off('zoom');
                this._controller = null;
                return;
            }

            function onPan(dx, dy) {
                if (this._state !== 'animating'
                    && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)
                ) {
                    // FIXME
                    // 找个好点的方法?
                    this._state = 'dragging';
                    var pos = rootGroup.position;
                    pos[0] += dx;
                    pos[1] += dy;
                    rootGroup.dirty();

                    // Update breadcrumb when drag move.
                    this._renderBreadcrumb(seriesModel, api);
                }
            }

            function onZoom(scale, mouseX, mouseY) {
                if (this._state !== 'animating' && this._state !== 'dragging') {
                    // var rect = this.group.getBoundingRect([containerGroup]);
                    var rect = containerGroup.getBoundingRect();

                    mouseX -= positionInfo.x;
                    mouseY -= positionInfo.y;

                    // Recalculate bounding rect.
                    var m = matrix.create();
                    matrix.translate(m, m, [-mouseX, -mouseY]);
                    matrix.scale(m, m, [scale, scale]);
                    matrix.translate(m, m, [mouseX, mouseY]);

                    rect.applyTransform(m);

                    this.api.dispatch({
                        type: 'treemapRender',
                        from: this.uid,
                        seriesId: this.seriesModel.uid,
                        viewRect: {
                            x: rect.x, y: rect.y, width: rect.width, height: rect.height
                        }
                    });
                }
            }
        },

        /**
         * @private
         */
        _initEvents: function (containerGroup, clickCallback) {
            // FIXME
            // 不用click以及silent的原因是,animate时视图设置silent true来避免click生效,
            // 但是animate中,按下鼠标,animate结束后(silent设回为false)松开鼠标,
            // 还是会触发click,期望是不触发。
            var maybeClick = false;
            var that = this;

            containerGroup.on('mousedown', function (e) {
                if (that._state === 'ready') {
                    maybeClick = true;
                }
            });
            containerGroup.on('mouseup', function (e) {
                if (that._state !== 'ready') {
                    that._state === 'dragging' && (that._state = 'ready');
                    return;
                }
                if (maybeClick) {
                    maybeClick = false;
                    onClick(e);
                }
            });

            function onClick(e) {
                var targetInfo = that.findTarget(e.offsetX, e.offsetY);
                if (targetInfo) {
                    that._zoomToNode(targetInfo);
                }
            }
        },

P
pah100 已提交
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
        /**
         * @private
         */
        _renderBreadcrumb: function (seriesModel, api, targetInfo) {
            if (!targetInfo) {
                // Find breadcrumb tail on center of containerGroup.
                targetInfo = this.findTarget(api.getWidth() / 2, api.getHeight() / 2);

                if (!targetInfo) {
                    targetInfo = {node: seriesModel.getData().tree.root};
                }
            }

            (this._breadcrumb || (this._breadcrumb = new Breadcrumb(this.group, onSelect)))
                .render(seriesModel, api, targetInfo.node);

            var that = this;
            function onSelect(node) {
                that._zoomToNode({node: node});
            }
        },

P
pah100 已提交
480 481 482 483
        /**
         * @override
         */
        remove: function () {
P
pah100 已提交
484
            this.group.removeAll();
P
pah100 已提交
485
            this._containerGroup = null;
P
pah100 已提交
486 487
            this._storage = null;
            this._oldTree = null;
P
pah100 已提交
488 489

            this._breadcrumb && this._breadcrumb.remove();
P
pah100 已提交
490 491
        },

P
pah100 已提交
492 493 494 495
        /**
         * @private
         */
        _zoomToNode: function (targetInfo) {
P
pah100 已提交
496
            this.api.dispatch({
P
pah100 已提交
497
                type: 'treemapZoomToNode',
P
pah100 已提交
498
                from: this.uid,
P
pah100 已提交
499 500
                seriesId: this.seriesModel.uid,
                targetInfo: targetInfo
P
pah100 已提交
501 502 503 504 505
            });
        },

        /**
         * @public
P
pah100 已提交
506 507
         * @param {number} x Global coord x.
         * @param {number} y Global coord y.
P
pah100 已提交
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
         * @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.
         */
        findTarget: function (x, y) {
            var targetInfo;
            var viewRoot = this.seriesModel.getViewRoot();

            viewRoot.eachNode({attr: 'viewChildren', order: 'preorder'}, function (node) {
                var nodeGroup = this._storage.nodeGroup[node.getRawIndex()];
                var point = nodeGroup.transformCoordToLocal(x, y);
                if (nodeGroup.getBoundingRect().contain(point[0], point[1])) {
                    targetInfo = {node: node, offsetX: point[0], offsetY: point[1]};
                }
                else {
                    return false; // Suppress visit subtree.
                }
            }, this);

            return targetInfo;
P
pah100 已提交
529 530 531 532 533
        },

        /**
         * @private
         */
P
pah100 已提交
534
        _positionRoot: function(containerGroup, positionInfo, root, viewRect, targetInfo) {
P
pah100 已提交
535 536 537
            var nodeGroups = this._storage.nodeGroup;
            var rootGroup = nodeGroups[root.getRawIndex()];

P
pah100 已提交
538 539 540 541 542
            if (viewRect) {
                rootGroup.position = [viewRect.x, viewRect.y];
                return;
            }

P
pah100 已提交
543 544 545 546 547
            if (!targetInfo) {
                rootGroup.position = [0, 0];
                return;
            }

P
pah100 已提交
548
            // If targetInfo is fetched by 'retrieveTargetNodeInfo',
P
pah100 已提交
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
            // old tree and new tree are the same tree,
            // so we can use raw index of targetInfo.node to find shape from storage.

            var targetNode = targetInfo.node;

            var targetGroup = nodeGroups[targetNode.getRawIndex()];

            if (!targetGroup) {
                rootGroup.position = [0, 0];
                return;
            }

            var targetRect = targetGroup.getBoundingRect();
            var targetCenter = modelUtil.transformCoordToAncestor(
                [targetRect.width / 2, targetRect.height / 2], targetGroup, containerGroup
            );
            rootGroup.position = [
P
pah100 已提交
566 567
                positionInfo.width / 2 - targetCenter[0],
                positionInfo.height / 2 - targetCenter[1]
P
pah100 已提交
568
            ];
P
pah100 已提交
569 570 571
        }

    });
P
pah100 已提交
572

P
pah100 已提交
573
});