TreemapSeries.js 12.8 KB
/*
* 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.
*/

import * as zrUtil from 'zrender/src/core/util';
import SeriesModel from '../../model/Series';
import Tree from '../../data/Tree';
import Model from '../../model/Model';
import {encodeHTML, addCommas} from '../../util/format';
import {wrapTreePathInfo} from '../helper/treeHelper';

export default SeriesModel.extend({

    type: 'series.treemap',

    layoutMode: 'box',

    dependencies: ['grid', 'polar'],

    /**
     * @type {module:echarts/data/Tree~Node}
     */
    _viewRoot: null,

    defaultOption: {
        // Disable progressive rendering
        progressive: 0,
        hoverLayerThreshold: Infinity,
        // center: ['50%', '50%'],          // not supported in ec3.
        // size: ['80%', '80%'],            // deprecated, compatible with ec2.
        left: 'center',
        top: 'middle',
        right: null,
        bottom: null,
        width: '80%',
        height: '80%',
        sort: true,                         // Can be null or false or true
                                            // (order by desc default, asc not supported yet (strange effect))
        clipWindow: 'origin',               // Size of clipped window when zooming. 'origin' or 'fullscreen'
        squareRatio: 0.5 * (1 + Math.sqrt(5)), // golden ratio
        leafDepth: null,                    // Nodes on depth from root are regarded as leaves.
                                            // Count from zero (zero represents only view root).
        drillDownIcon: '▶',                 // Use html character temporarily because it is complicated
                                            // to align specialized icon. ▷▶❒❐▼✚

        zoomToNodeRatio: 0.32 * 0.32,       // Be effective when using zoomToNode. Specify the proportion of the
                                            // target node area in the view area.
        roam: true,                         // true, false, 'scale' or 'zoom', 'move'.
        nodeClick: 'zoomToNode',            // Leaf node click behaviour: 'zoomToNode', 'link', false.
                                            // If leafDepth is set and clicking a node which has children but
                                            // be on left depth, the behaviour would be changing root. Otherwise
                                            // use behavious defined above.
        animation: true,
        animationDurationUpdate: 900,
        animationEasing: 'quinticInOut',
        breadcrumb: {
            show: true,
            height: 22,
            left: 'center',
            top: 'bottom',
            // right
            // bottom
            emptyItemWidth: 25,             // Width of empty node.
            itemStyle: {
                color: 'rgba(0,0,0,0.7)', //'#5793f3',
                borderColor: 'rgba(255,255,255,0.7)',
                borderWidth: 1,
                shadowColor: 'rgba(150,150,150,1)',
                shadowBlur: 3,
                shadowOffsetX: 0,
                shadowOffsetY: 0,
                textStyle: {
                    color: '#fff'
                }
            },
            emphasis: {
                textStyle: {}
            }
        },
        label: {
            show: true,
            // Do not use textDistance, for ellipsis rect just the same as treemap node rect.
            distance: 0,
            padding: 5,
            position: 'inside', // Can be [5, '5%'] or position stirng like 'insideTopLeft', ...
            // formatter: null,
            color: '#fff',
            ellipsis: true
            // align
            // verticalAlign
        },
        upperLabel: {                   // Label when node is parent.
            show: false,
            position: [0, '50%'],
            height: 20,
            // formatter: null,
            color: '#fff',
            ellipsis: true,
            // align: null,
            verticalAlign: 'middle'
        },
        itemStyle: {
            color: null,            // Can be 'none' if not necessary.
            colorAlpha: null,       // Can be 'none' if not necessary.
            colorSaturation: null,  // Can be 'none' if not necessary.
            borderWidth: 0,
            gapWidth: 0,
            borderColor: '#fff',
            borderColorSaturation: null // If specified, borderColor will be ineffective, and the
                                        // border color is evaluated by color of current node and
                                        // borderColorSaturation.
        },
        emphasis: {
            upperLabel: {
                show: true,
                position: [0, '50%'],
                color: '#fff',
                ellipsis: true,
                verticalAlign: 'middle'
            }
        },

        visualDimension: 0,                 // Can be 0, 1, 2, 3.
        visualMin: null,
        visualMax: null,

        color: [],                  // + treemapSeries.color should not be modified. Please only modified
                                    // level[n].color (if necessary).
                                    // + Specify color list of each level. level[0].color would be global
                                    // color list if not specified. (see method `setDefault`).
                                    // + But set as a empty array to forbid fetch color from global palette
                                    // when using nodeModel.get('color'), otherwise nodes on deep level
                                    // will always has color palette set and are not able to inherit color
                                    // from parent node.
                                    // + TreemapSeries.color can not be set as 'none', otherwise effect
                                    // legend color fetching (see seriesColor.js).
        colorAlpha: null,           // Array. Specify color alpha range of each level, like [0.2, 0.8]
        colorSaturation: null,      // Array. Specify color saturation of each level, like [0.2, 0.5]
        colorMappingBy: 'index',    // 'value' or 'index' or 'id'.
        visibleMin: 10,             // If area less than this threshold (unit: pixel^2), node will not
                                    // be rendered. Only works when sort is 'asc' or 'desc'.
        childrenVisibleMin: null,   // If area of a node less than this threshold (unit: pixel^2),
                                    // grandchildren will not show.
                                    // Why grandchildren? If not grandchildren but children,
                                    // some siblings show children and some not,
                                    // the appearance may be mess and not consistent,
        levels: []                  // Each item: {
                                    //     visibleMin, itemStyle, visualDimension, label
                                    // }
        // data: {
        //      value: [],
        //      children: [],
        //      link: 'http://xxx.xxx.xxx',
        //      target: 'blank' or 'self'
        // }
    },

    /**
     * @override
     */
    getInitialData: function (option, ecModel) {
        // Create a virtual root.
        var root = {name: option.name, children: option.data};

        completeTreeValue(root);

        var levels = option.levels || [];

        levels = option.levels = setDefault(levels, ecModel);

        var treeOption = {};

        treeOption.levels = levels;

        // Make sure always a new tree is created when setOption,
        // in TreemapView, we check whether oldTree === newTree
        // to choose mappings approach among old shapes and new shapes.
        return Tree.createTree(root, this, treeOption).data;
    },

    optionUpdated: function () {
        this.resetViewRoot();
    },

    /**
     * @override
     * @param {number} dataIndex
     * @param {boolean} [mutipleSeries=false]
     */
    formatTooltip: function (dataIndex) {
        var data = this.getData();
        var value = this.getRawValue(dataIndex);
        var formattedValue = zrUtil.isArray(value)
            ? addCommas(value[0]) : addCommas(value);
        var name = data.getName(dataIndex);

        return encodeHTML(name + ': ' + formattedValue);
    },

    /**
     * Add tree path to tooltip param
     *
     * @override
     * @param {number} dataIndex
     * @return {Object}
     */
    getDataParams: function (dataIndex) {
        var params = SeriesModel.prototype.getDataParams.apply(this, arguments);

        var node = this.getData().tree.getNodeByDataIndex(dataIndex);
        params.treePathInfo = wrapTreePathInfo(node, this);

        return params;
    },

    /**
     * @public
     * @param {Object} layoutInfo {
     *                                x: containerGroup x
     *                                y: containerGroup y
     *                                width: containerGroup width
     *                                height: containerGroup height
     *                            }
     */
    setLayoutInfo: function (layoutInfo) {
        /**
         * @readOnly
         * @type {Object}
         */
        this.layoutInfo = this.layoutInfo || {};
        zrUtil.extend(this.layoutInfo, layoutInfo);
    },

    /**
     * @param  {string} id
     * @return {number} index
     */
    mapIdToIndex: function (id) {
        // A feature is implemented:
        // index is monotone increasing with the sequence of
        // input id at the first time.
        // This feature can make sure that each data item and its
        // mapped color have the same index between data list and
        // color list at the beginning, which is useful for user
        // to adjust data-color mapping.

        /**
         * @private
         * @type {Object}
         */
        var idIndexMap = this._idIndexMap;

        if (!idIndexMap) {
            idIndexMap = this._idIndexMap = zrUtil.createHashMap();
            /**
             * @private
             * @type {number}
             */
            this._idIndexMapCount = 0;
        }

        var index = idIndexMap.get(id);
        if (index == null) {
            idIndexMap.set(id, index = this._idIndexMapCount++);
        }

        return index;
    },

    getViewRoot: function () {
        return this._viewRoot;
    },

    /**
     * @param {module:echarts/data/Tree~Node} [viewRoot]
     */
    resetViewRoot: function (viewRoot) {
        viewRoot
            ? (this._viewRoot = viewRoot)
            : (viewRoot = this._viewRoot);

        var root = this.getRawData().tree.root;

        if (!viewRoot
            || (viewRoot !== root && !root.contains(viewRoot))
        ) {
            this._viewRoot = root;
        }
    }
});

/**
 * @param {Object} dataNode
 */
function completeTreeValue(dataNode) {
    // Postorder travel tree.
    // If value of none-leaf node is not set,
    // calculate it by suming up the value of all children.
    var sum = 0;

    zrUtil.each(dataNode.children, function (child) {

        completeTreeValue(child);

        var childValue = child.value;
        zrUtil.isArray(childValue) && (childValue = childValue[0]);

        sum += childValue;
    });

    var thisValue = dataNode.value;
    if (zrUtil.isArray(thisValue)) {
        thisValue = thisValue[0];
    }

    if (thisValue == null || isNaN(thisValue)) {
        thisValue = sum;
    }
    // Value should not less than 0.
    if (thisValue < 0) {
        thisValue = 0;
    }

    zrUtil.isArray(dataNode.value)
        ? (dataNode.value[0] = thisValue)
        : (dataNode.value = thisValue);
}

/**
 * set default to level configuration
 */
function setDefault(levels, ecModel) {
    var globalColorList = ecModel.get('color');

    if (!globalColorList) {
        return;
    }

    levels = levels || [];
    var hasColorDefine;
    zrUtil.each(levels, function (levelDefine) {
        var model = new Model(levelDefine);
        var modelColor = model.get('color');

        if (model.get('itemStyle.color')
            || (modelColor && modelColor !== 'none')
        ) {
            hasColorDefine = true;
        }
    });

    if (!hasColorDefine) {
        var level0 = levels[0] || (levels[0] = {});
        level0.color = globalColorList.slice();
    }

    return levels;
}