/*
|
* 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
|
} from '../../util/types';
|
import GlobalModel from '../../model/Global';
|
import { LayoutRect } from '../../util/layout';
|
import List from '../../data/List';
|
import { normalizeToArray } from '../../util/model';
|
import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
|
import enableAriaDecalForTree from '../helper/enableAriaDecalForTree';
|
|
// Only support numberic value.
|
type TreemapSeriesDataValue = number | number[];
|
|
interface BreadcrumbItemStyleOption extends ItemStyleOption {
|
// TODO: textStyle should be in breadcrumb.label
|
textStyle?: LabelOption
|
}
|
|
interface TreemapSeriesLabelOption extends SeriesLabelOption {
|
ellipsis?: boolean
|
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 {
|
treePathInfo?: 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
|
|
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 suuport 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, 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 behavious defined above.
|
*/
|
nodeClick?: 'zoomToNode' | 'link'
|
|
breadcrumb?: BoxLayoutOptionMixin & {
|
show?: boolean
|
height?: number
|
|
emptyItemWidth: number // With of empty width
|
itemStyle?: BreadcrumbItemStyleOption
|
|
emphasis?: {
|
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'
|
}
|
}
|
},
|
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',
|
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%'],
|
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(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: List) {
|
nodeData.wrapMethod('getItemModel', function (model, idx) {
|
const node = tree.getNodeByDataIndex(idx);
|
const levelModel = levelModels[node.depth];
|
// 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.treePathInfo = wrapTreePathInfo(node, this);
|
|
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;
|