|  | /* | 
|  | * 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, { TreeNode } from '../../data/Tree'; | 
|  | import Model from '../../model/Model'; | 
|  | import {wrapTreePathInfo} from '../helper/treeHelper'; | 
|  | import { | 
|  | SeriesOption, | 
|  | BoxLayoutOptionMixin, | 
|  | ItemStyleOption, | 
|  | LabelOption, | 
|  | RoamOptionMixin, | 
|  | CallbackDataParams, | 
|  | ColorString, | 
|  | StatesOptionMixin, | 
|  | OptionId, | 
|  | OptionName, | 
|  | DecalObject, | 
|  | SeriesLabelOption, | 
|  | DefaultEmphasisFocus, | 
|  | AriaOptionMixin, | 
|  | BlurScope | 
|  | } from '../../util/types'; | 
|  | import GlobalModel from '../../model/Global'; | 
|  | import { LayoutRect } from '../../util/layout'; | 
|  | import SeriesData from '../../data/SeriesData'; | 
|  | import { normalizeToArray } from '../../util/model'; | 
|  | import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup'; | 
|  | import enableAriaDecalForTree from '../helper/enableAriaDecalForTree'; | 
|  |  | 
|  | // Only support numeric value. | 
|  | type TreemapSeriesDataValue = number | number[]; | 
|  |  | 
|  | interface BreadcrumbItemStyleOption extends ItemStyleOption { | 
|  | // TODO: textStyle should be in breadcrumb.label | 
|  | textStyle?: LabelOption | 
|  | } | 
|  |  | 
|  | interface TreemapSeriesLabelOption extends SeriesLabelOption { | 
|  | formatter?: string | ((params: CallbackDataParams) => string) | 
|  | } | 
|  |  | 
|  | interface TreemapSeriesItemStyleOption<TCbParams = never> extends ItemStyleOption<TCbParams> { | 
|  | borderRadius?: number | number[] | 
|  |  | 
|  | colorAlpha?: number | 
|  | colorSaturation?: number | 
|  |  | 
|  | borderColorSaturation?: number | 
|  |  | 
|  | gapWidth?: number | 
|  | } | 
|  |  | 
|  | interface TreePathInfo { | 
|  | name: string | 
|  | dataIndex: number | 
|  | value: TreemapSeriesDataValue | 
|  | } | 
|  |  | 
|  | interface TreemapSeriesCallbackDataParams extends CallbackDataParams { | 
|  | /** | 
|  | * @deprecated | 
|  | */ | 
|  | treePathInfo?: TreePathInfo[] | 
|  |  | 
|  | treeAncestors?: TreePathInfo[] | 
|  | } | 
|  |  | 
|  | interface ExtraStateOption { | 
|  | emphasis?: { | 
|  | focus?: DefaultEmphasisFocus | 'descendant' | 'ancestor' | 
|  | } | 
|  | } | 
|  |  | 
|  | export interface TreemapStateOption<TCbParams = never> { | 
|  | itemStyle?: TreemapSeriesItemStyleOption<TCbParams> | 
|  | label?: TreemapSeriesLabelOption | 
|  | upperLabel?: TreemapSeriesLabelOption | 
|  | } | 
|  |  | 
|  | export interface TreemapSeriesVisualOption { | 
|  | /** | 
|  | * Which dimension will be applied with the visual properties. | 
|  | */ | 
|  | visualDimension?: number | string | 
|  |  | 
|  | /** | 
|  | * @deprecated Use colorBy instead | 
|  | */ | 
|  | colorMappingBy?: 'value' | 'index' | 'id' | 
|  |  | 
|  | visualMin?: number | 
|  | visualMax?: number | 
|  |  | 
|  | colorAlpha?: number[] | 'none' | 
|  | colorSaturation?: number[] | 'none' | 
|  | // A color list for a level. Each node in the level will obtain a color from the color list. | 
|  | // Only support ColorString for interpolation. | 
|  | // color?: ColorString[] | 
|  |  | 
|  | /** | 
|  | * A node will not be shown when its area size is smaller than this value (unit: px square). | 
|  | */ | 
|  | visibleMin?: number | 
|  | /** | 
|  | * Children will not be shown when area size of a node is smaller than this value (unit: px square). | 
|  | */ | 
|  | childrenVisibleMin?: number | 
|  | } | 
|  |  | 
|  | export interface TreemapSeriesLevelOption extends TreemapSeriesVisualOption, | 
|  | TreemapStateOption, StatesOptionMixin<TreemapStateOption, ExtraStateOption> { | 
|  |  | 
|  | color?: ColorString[] | 'none', | 
|  | decal?: DecalObject[] | 'none' | 
|  | } | 
|  |  | 
|  | export interface TreemapSeriesNodeItemOption extends TreemapSeriesVisualOption, | 
|  | TreemapStateOption, StatesOptionMixin<TreemapStateOption, ExtraStateOption> { | 
|  | id?: OptionId | 
|  | name?: OptionName | 
|  |  | 
|  | value?: TreemapSeriesDataValue | 
|  |  | 
|  | children?: TreemapSeriesNodeItemOption[] | 
|  |  | 
|  | color?: ColorString[] | 'none' | 
|  |  | 
|  | decal?: DecalObject[] | 'none' | 
|  | } | 
|  |  | 
|  | export interface TreemapSeriesOption | 
|  | extends SeriesOption<TreemapStateOption<TreemapSeriesCallbackDataParams>, ExtraStateOption>, | 
|  | TreemapStateOption<TreemapSeriesCallbackDataParams>, | 
|  | BoxLayoutOptionMixin, | 
|  | RoamOptionMixin, | 
|  | TreemapSeriesVisualOption { | 
|  |  | 
|  | type?: 'treemap' | 
|  |  | 
|  | /** | 
|  | * configuration in echarts2 | 
|  | * @deprecated | 
|  | */ | 
|  | size?: (number | string)[] | 
|  |  | 
|  | /** | 
|  | * If sort in desc order. | 
|  | * Default to be desc. asc has strange effect | 
|  | */ | 
|  | sort?: boolean | 'asc' | 'desc' | 
|  |  | 
|  | /** | 
|  | * Size of clipped window when zooming. 'origin' or 'fullscreen' | 
|  | */ | 
|  | clipWindow?: 'origin' | 'fullscreen' | 
|  |  | 
|  | squareRatio?: number | 
|  | /** | 
|  | * Nodes on depth from root are regarded as leaves. | 
|  | * Count from zero (zero represents only view root). | 
|  | */ | 
|  | leafDepth?: number | 
|  |  | 
|  | drillDownIcon?: string | 
|  |  | 
|  | /** | 
|  | * Be effective when using zoomToNode. Specify the proportion of the | 
|  | * target node area in the view area. | 
|  | */ | 
|  | zoomToNodeRatio?: number | 
|  | /** | 
|  | * 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 behaviour defined above. | 
|  | */ | 
|  | nodeClick?: 'zoomToNode' | 'link' | false | 
|  |  | 
|  | breadcrumb?: BoxLayoutOptionMixin & { | 
|  | show?: boolean | 
|  | height?: number | 
|  |  | 
|  | emptyItemWidth?: number  // With of empty width | 
|  | itemStyle?: BreadcrumbItemStyleOption | 
|  |  | 
|  | emphasis?: { | 
|  | disabled?: boolean | 
|  | focus?: DefaultEmphasisFocus | 
|  | blurScope?: BlurScope | 
|  | itemStyle?: BreadcrumbItemStyleOption | 
|  | } | 
|  | } | 
|  |  | 
|  | levels?: TreemapSeriesLevelOption[] | 
|  |  | 
|  | data?: TreemapSeriesNodeItemOption[] | 
|  | } | 
|  |  | 
|  | class TreemapSeriesModel extends SeriesModel<TreemapSeriesOption> { | 
|  |  | 
|  | static type = 'series.treemap'; | 
|  | type = TreemapSeriesModel.type; | 
|  |  | 
|  | static layoutMode = 'box' as const; | 
|  |  | 
|  | preventUsingHoverLayer = true; | 
|  |  | 
|  | layoutInfo: LayoutRect; | 
|  |  | 
|  | designatedVisualItemStyle: TreemapSeriesItemStyleOption; | 
|  |  | 
|  | private _viewRoot: TreeNode; | 
|  | private _idIndexMap: zrUtil.HashMap<number>; | 
|  | private _idIndexMapCount: number; | 
|  |  | 
|  | static defaultOption: TreemapSeriesOption = { | 
|  | // Disable progressive rendering | 
|  | progressive: 0, | 
|  | // size: ['80%', '80%'],            // deprecated, compatible with ec2. | 
|  | left: 'center', | 
|  | top: 'middle', | 
|  | width: '80%', | 
|  | height: '80%', | 
|  | sort: true, | 
|  |  | 
|  | clipWindow: 'origin', | 
|  | squareRatio: 0.5 * (1 + Math.sqrt(5)), // golden ratio | 
|  | leafDepth: null, | 
|  |  | 
|  | drillDownIcon: '▶',                 // Use html character temporarily because it is complicated | 
|  | // to align specialized icon. ▷▶❒❐▼✚ | 
|  |  | 
|  | zoomToNodeRatio: 0.32 * 0.32, | 
|  |  | 
|  | roam: true, | 
|  | nodeClick: 'zoomToNode', | 
|  | 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', | 
|  | textStyle: { | 
|  | color: '#fff' | 
|  | } | 
|  | }, | 
|  | emphasis: { | 
|  | itemStyle: { | 
|  | color: 'rgba(0,0,0,0.9)' // '#5793f3', | 
|  | } | 
|  | } | 
|  | }, | 
|  | 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 string like 'insideTopLeft', ... | 
|  | // formatter: null, | 
|  | color: '#fff', | 
|  | overflow: 'truncate' | 
|  | // align | 
|  | // verticalAlign | 
|  | }, | 
|  | upperLabel: {                   // Label when node is parent. | 
|  | show: false, | 
|  | position: [0, '50%'], | 
|  | height: 20, | 
|  | // formatter: null, | 
|  | // color: '#fff', | 
|  | overflow: 'truncate', | 
|  | // 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%'], | 
|  | overflow: 'truncate', | 
|  | 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 | 
|  | // } | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * @override | 
|  | */ | 
|  | getInitialData(option: TreemapSeriesOption, ecModel: GlobalModel) { | 
|  | // Create a virtual root. | 
|  | const root: TreemapSeriesNodeItemOption = { | 
|  | name: option.name, | 
|  | children: option.data | 
|  | }; | 
|  |  | 
|  | completeTreeValue(root); | 
|  |  | 
|  | let levels = option.levels || []; | 
|  |  | 
|  | // Used in "visual priority" in `treemapVisual.js`. | 
|  | // This way is a little tricky, must satisfy the precondition: | 
|  | //   1. There is no `treeNode.getModel('itemStyle.xxx')` used. | 
|  | //   2. The `Model.prototype.getModel()` will not use any clone-like way. | 
|  | const designatedVisualItemStyle = this.designatedVisualItemStyle = {}; | 
|  | const designatedVisualModel = new Model({itemStyle: designatedVisualItemStyle}, this, ecModel); | 
|  |  | 
|  | levels = option.levels = setDefault(levels, ecModel); | 
|  | const levelModels = zrUtil.map(levels || [], function (levelDefine) { | 
|  | return new Model(levelDefine, designatedVisualModel, ecModel); | 
|  | }, this); | 
|  |  | 
|  | // 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. | 
|  | const tree = Tree.createTree(root, this, beforeLink); | 
|  |  | 
|  | function beforeLink(nodeData: SeriesData) { | 
|  | nodeData.wrapMethod('getItemModel', function (model, idx) { | 
|  | const node = tree.getNodeByDataIndex(idx); | 
|  | const levelModel = node ? levelModels[node.depth] : null; | 
|  | // If no levelModel, we also need `designatedVisualModel`. | 
|  | model.parentModel = levelModel || designatedVisualModel; | 
|  | return model; | 
|  | }); | 
|  | } | 
|  |  | 
|  | return tree.data; | 
|  | } | 
|  |  | 
|  | optionUpdated() { | 
|  | this.resetViewRoot(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @override | 
|  | * @param {number} dataIndex | 
|  | * @param {boolean} [mutipleSeries=false] | 
|  | */ | 
|  | formatTooltip( | 
|  | dataIndex: number, | 
|  | multipleSeries: boolean, | 
|  | dataType: string | 
|  | ) { | 
|  | const data = this.getData(); | 
|  | const value = this.getRawValue(dataIndex) as TreemapSeriesDataValue; | 
|  | const name = data.getName(dataIndex); | 
|  |  | 
|  | return createTooltipMarkup('nameValue', { name: name, value: value }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Add tree path to tooltip param | 
|  | * | 
|  | * @override | 
|  | * @param {number} dataIndex | 
|  | * @return {Object} | 
|  | */ | 
|  | getDataParams(dataIndex: number) { | 
|  | const params = super.getDataParams.apply(this, arguments as any) as TreemapSeriesCallbackDataParams; | 
|  |  | 
|  | const node = this.getData().tree.getNodeByDataIndex(dataIndex); | 
|  | params.treeAncestors = wrapTreePathInfo(node, this); | 
|  | // compatitable the previous code. | 
|  | params.treePathInfo = params.treeAncestors; | 
|  |  | 
|  | return params; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @public | 
|  | * @param {Object} layoutInfo { | 
|  | *                                x: containerGroup x | 
|  | *                                y: containerGroup y | 
|  | *                                width: containerGroup width | 
|  | *                                height: containerGroup height | 
|  | *                            } | 
|  | */ | 
|  | setLayoutInfo(layoutInfo: LayoutRect) { | 
|  | /** | 
|  | * @readOnly | 
|  | * @type {Object} | 
|  | */ | 
|  | this.layoutInfo = this.layoutInfo || {} as LayoutRect; | 
|  | zrUtil.extend(this.layoutInfo, layoutInfo); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param  {string} id | 
|  | * @return {number} index | 
|  | */ | 
|  | mapIdToIndex(id: string): number { | 
|  | // 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} | 
|  | */ | 
|  | let idIndexMap = this._idIndexMap; | 
|  |  | 
|  | if (!idIndexMap) { | 
|  | idIndexMap = this._idIndexMap = zrUtil.createHashMap(); | 
|  | /** | 
|  | * @private | 
|  | * @type {number} | 
|  | */ | 
|  | this._idIndexMapCount = 0; | 
|  | } | 
|  |  | 
|  | let index = idIndexMap.get(id); | 
|  | if (index == null) { | 
|  | idIndexMap.set(id, index = this._idIndexMapCount++); | 
|  | } | 
|  |  | 
|  | return index; | 
|  | } | 
|  |  | 
|  | getViewRoot() { | 
|  | return this._viewRoot; | 
|  | } | 
|  |  | 
|  | resetViewRoot(viewRoot?: TreeNode) { | 
|  | viewRoot | 
|  | ? (this._viewRoot = viewRoot) | 
|  | : (viewRoot = this._viewRoot); | 
|  |  | 
|  | const root = this.getRawData().tree.root; | 
|  |  | 
|  | if (!viewRoot | 
|  | || (viewRoot !== root && !root.contains(viewRoot)) | 
|  | ) { | 
|  | this._viewRoot = root; | 
|  | } | 
|  | } | 
|  |  | 
|  | enableAriaDecal() { | 
|  | enableAriaDecalForTree(this); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {Object} dataNode | 
|  | */ | 
|  | function completeTreeValue(dataNode: TreemapSeriesNodeItemOption) { | 
|  | // Postorder travel tree. | 
|  | // If value of none-leaf node is not set, | 
|  | // calculate it by suming up the value of all children. | 
|  | let sum = 0; | 
|  |  | 
|  | zrUtil.each(dataNode.children, function (child) { | 
|  |  | 
|  | completeTreeValue(child); | 
|  |  | 
|  | let childValue = child.value; | 
|  | zrUtil.isArray(childValue) && (childValue = childValue[0]); | 
|  |  | 
|  | sum += childValue; | 
|  | }); | 
|  |  | 
|  | let 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: TreemapSeriesLevelOption[], ecModel: GlobalModel) { | 
|  | const globalColorList = normalizeToArray(ecModel.get('color')) as ColorString[]; | 
|  | const globalDecalList = normalizeToArray( | 
|  | (ecModel as Model<AriaOptionMixin>).get(['aria', 'decal', 'decals']) | 
|  | ) as DecalObject[]; | 
|  |  | 
|  | if (!globalColorList) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | levels = levels || []; | 
|  | let hasColorDefine; | 
|  | let hasDecalDefine; | 
|  | zrUtil.each(levels, function (levelDefine) { | 
|  | const model = new Model(levelDefine); | 
|  | const modelColor = model.get('color'); | 
|  | const modelDecal = model.get('decal'); | 
|  |  | 
|  | if (model.get(['itemStyle', 'color']) | 
|  | || (modelColor && modelColor !== 'none') | 
|  | ) { | 
|  | hasColorDefine = true; | 
|  | } | 
|  | if (model.get(['itemStyle', 'decal']) | 
|  | || (modelDecal && modelDecal !== 'none') | 
|  | ) { | 
|  | hasDecalDefine = true; | 
|  | } | 
|  | }); | 
|  |  | 
|  | const level0 = levels[0] || (levels[0] = {}); | 
|  | if (!hasColorDefine) { | 
|  | level0.color = globalColorList.slice(); | 
|  | } | 
|  | if (!hasDecalDefine && globalDecalList) { | 
|  | level0.decal = globalDecalList.slice(); | 
|  | } | 
|  |  | 
|  | return levels; | 
|  | } | 
|  |  | 
|  | export default TreemapSeriesModel; |