blob: efc0f06aa1a61db805932c9f163eabeccf20f003 [file] [log] [blame]
/*
* 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,
ColorBy
} 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 {
/**
* @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 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 = 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;