/* * 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 extends ItemStyleOption { 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 { itemStyle?: TreemapSeriesItemStyleOption 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 { color?: ColorString[] | 'none', decal?: DecalObject[] | 'none' } export interface TreemapSeriesNodeItemOption extends TreemapSeriesVisualOption, TreemapStateOption, StatesOptionMixin { id?: OptionId name?: OptionName value?: TreemapSeriesDataValue children?: TreemapSeriesNodeItemOption[] color?: ColorString[] | 'none' decal?: DecalObject[] | 'none' } export interface TreemapSeriesOption extends SeriesOption, ExtraStateOption>, TreemapStateOption, 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 { 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; 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).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;