/**
|
* echarts图表类:矩形树图
|
*
|
* @desc echarts基于Canvas,纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据统计图表。
|
* @author Loutongbing (娄同兵, loutongbing@126.com)
|
*/
|
|
define(function (require) {
|
var ChartBase = require('./base');
|
// 图形依赖
|
var toolArea = require('zrender/tool/area');
|
// 图形依赖
|
var RectangleShape = require('zrender/shape/Rectangle');
|
var TextShape = require('zrender/shape/Text');
|
var LineShape = require('zrender/shape/Line');
|
// 布局依赖
|
var TreeMapLayout = require('../layout/TreeMap');
|
// 数据依赖
|
var Tree = require('../data/Tree');
|
|
var ecConfig = require('../config');
|
// 维恩图默认参数
|
ecConfig.treemap = {
|
zlevel: 0, // 一级层叠
|
z: 1, // 二级层叠
|
calculable: false,
|
clickable: true,
|
center: ['50%', '50%'],
|
size: ['80%', '80%'],
|
root: '',
|
itemStyle: {
|
normal: {
|
// color: 各异,
|
label: {
|
show: true,
|
x: 5,
|
y: 12,
|
textStyle: {
|
align: 'left',
|
color: '#000',
|
fontFamily: 'Arial',
|
fontSize: 13,
|
fontStyle: 'normal',
|
fontWeight: 'normal'
|
}
|
},
|
breadcrumb: {
|
show: true,
|
textStyle: {}
|
},
|
borderWidth: 1,
|
borderColor: '#ccc',
|
childBorderWidth: 1,
|
childBorderColor: '#ccc'
|
},
|
emphasis: {}
|
}
|
};
|
|
var ecData = require('../util/ecData');
|
var zrConfig = require('zrender/config');
|
var zrEvent = require('zrender/tool/event');
|
var zrUtil = require('zrender/tool/util');
|
var zrColor = require('zrender/tool/color');
|
|
/**
|
* 构造函数
|
* @param {Object} messageCenter echart消息中心
|
* @param {ZRender} zr zrender实例
|
* @param {Object} series 数据
|
* @param {Object} component 组件
|
* @constructor
|
* @exports Treemap
|
*/
|
function Treemap(ecTheme, messageCenter, zr, option, myChart) {
|
// 图表基类
|
ChartBase.call(this, ecTheme, messageCenter, zr, option, myChart);
|
this.refresh(option);
|
var self = this;
|
self._onclick = function (params) {
|
return self.__onclick(params);
|
};
|
self.zr.on(zrConfig.EVENT.CLICK, self._onclick);
|
}
|
Treemap.prototype = {
|
type : ecConfig.CHART_TYPE_TREEMAP,
|
/**
|
* 刷新
|
*/
|
refresh: function (newOption) {
|
this.clear();
|
|
if (newOption) {
|
this.option = newOption;
|
this.series = this.option.series;
|
}
|
|
// Map storing all trees of series
|
this._treesMap = {};
|
|
var series = this.series;
|
var legend = this.component.legend;
|
|
for (var i = 0; i < series.length; i++) {
|
if (series[i].type === ecConfig.CHART_TYPE_TREEMAP) {
|
series[i] = this.reformOption(series[i]);
|
|
var seriesName = series[i].name || '';
|
this.selectedMap[seriesName] =
|
legend ? legend.isSelected(seriesName) : true;
|
if (!this.selectedMap[seriesName]) {
|
continue;
|
}
|
|
this._buildSeries(series[i], i);
|
}
|
}
|
},
|
|
_buildSeries: function(series, seriesIndex) {
|
var tree = Tree.fromOptionData(series.name, series.data);
|
|
this._treesMap[seriesIndex] = tree;
|
|
var treeRoot = series.root && tree.getNodeById(series.root) || tree.root;
|
this._buildTreemap(treeRoot, seriesIndex);
|
},
|
|
/**
|
* 构建单个
|
*
|
* @param {Object} data 数据
|
*/
|
_buildTreemap : function (treeRoot, seriesIndex) {
|
|
// 清除之前的 shapes
|
var shapeList = this.shapeList;
|
for (var i = 0; i < shapeList.length;) {
|
var shape = shapeList[i];
|
if (ecData.get(shape, 'seriesIndex') === seriesIndex) {
|
this.zr.delShape(shapeList[i]);
|
shapeList.splice(i, 1);
|
}
|
else {
|
i++;
|
}
|
}
|
|
var currentShapeLen = shapeList.length;
|
var series = this.series[seriesIndex];
|
|
var itemStyle = series.itemStyle;
|
var treemapWidth = this.parsePercent(series.size[0], this.zr.getWidth()) || 400;
|
var treemapHeight = this.parsePercent(series.size[1], this.zr.getHeight()) || 500;
|
var center = this.parseCenter(this.zr, series.center);
|
var treemapX = center[0] - treemapWidth * 0.5;
|
var treemapY = center[1] - treemapHeight * 0.5;
|
|
var treemapArea = treemapWidth * treemapHeight; // 计算总面积
|
// 遍历数组,通过value与area0计算实际面积area
|
var sum = 0;
|
var areaArr = [];
|
|
var children = treeRoot.children;
|
for (var i = 0; i < children.length; i++) {
|
sum += children[i].data.value;
|
}
|
for (var j = 0; j < children.length; j++) {
|
areaArr.push(children[j].data.value * treemapArea / sum);
|
}
|
var treeMapLayout = new TreeMapLayout({
|
x: treemapX,
|
y: treemapY,
|
width: treemapWidth,
|
height: treemapHeight
|
});
|
var locationArr = treeMapLayout.run(areaArr);
|
|
for (var k = 0; k < locationArr.length; k++) {
|
var dataItem = children[k].data;
|
var rect = locationArr[k];
|
var queryTarget = [dataItem.itemStyle, itemStyle];
|
var itemStyleMerged = this.deepMerge(queryTarget);
|
if (!itemStyleMerged.normal.color) {
|
itemStyleMerged.normal.color = this.zr.getColor(k);
|
}
|
if (!itemStyleMerged.emphasis.color) {
|
itemStyleMerged.emphasis.color = itemStyleMerged.normal.color;
|
}
|
this._buildItem(
|
dataItem,
|
itemStyleMerged,
|
rect,
|
seriesIndex,
|
k
|
);
|
// 绘制二级节点
|
if (dataItem.children) {
|
this._buildChildrenTreemap(
|
dataItem.children,
|
itemStyleMerged,
|
rect,
|
seriesIndex
|
);
|
}
|
}
|
|
if (this.query(series, 'itemStyle.normal.breadcrumb.show')) {
|
this._buildBreadcrumb(
|
treeRoot, seriesIndex, treemapX, treemapY + treemapHeight
|
);
|
}
|
|
for (var i = currentShapeLen; i < shapeList.length; i++) {
|
this.zr.addShape(shapeList[i]);
|
}
|
},
|
|
/**
|
* 构建单个item
|
*/
|
_buildItem : function (
|
dataItem,
|
itemStyle,
|
rect,
|
seriesIndex,
|
dataIndex
|
) {
|
var series = this.series;
|
var rectangle = this.getRectangle(
|
dataItem,
|
itemStyle,
|
rect
|
);
|
// todo
|
ecData.pack(
|
rectangle,
|
series[seriesIndex], seriesIndex,
|
dataItem, dataIndex,
|
dataItem.name
|
);
|
this.shapeList.push(rectangle);
|
},
|
|
/**
|
* 构建矩形
|
* @param {Object} data 数据
|
* @param {Object} itemStyle 合并后的样式
|
* @param {number} x 矩形横坐标
|
* @param {number} y 矩形纵坐标
|
* @param {number} width 矩形宽
|
* @param {number} height 矩形高
|
* @return {Object} 返回一个矩形
|
*/
|
getRectangle: function (
|
dataItem,
|
itemStyle,
|
rect
|
) {
|
var emphasis = itemStyle.emphasis;
|
var normal = itemStyle.normal;
|
var textShape = this.getLabel(
|
itemStyle,
|
rect,
|
dataItem.name,
|
dataItem.value
|
);
|
var hoverable = this.option.hoverable;
|
var rectangleShape = {
|
zlevel: this.getZlevelBase(),
|
z: this.getZBase(),
|
hoverable: hoverable,
|
clickable: true,
|
style: zrUtil.merge(
|
{
|
x: rect.x,
|
y: rect.y,
|
width: rect.width,
|
height: rect.height,
|
brushType: 'both',
|
color: normal.color,
|
lineWidth: normal.borderWidth,
|
strokeColor: normal.borderColor
|
},
|
textShape.style,
|
true
|
),
|
highlightStyle: zrUtil.merge(
|
{
|
color: emphasis.color,
|
lineWidth: emphasis.borderWidth,
|
strokeColor: emphasis.borderColor
|
},
|
textShape.highlightStyle,
|
true
|
)
|
};
|
return new RectangleShape(rectangleShape);
|
},
|
/**
|
* 获取label样式
|
* @param {Object} itemStyle 合并后的样式
|
* @param {Object} rect 矩形信息
|
* @param {string} name 数据名称
|
* @param {number} value 数据值
|
* @return {Object} 返回label的样式
|
*/
|
getLabel: function (
|
itemStyle,
|
rect,
|
name,
|
value
|
) {
|
var normalTextStyle = itemStyle.normal.label.textStyle;
|
var queryTarget = [itemStyle.emphasis.label.textStyle, normalTextStyle];
|
var emphasisTextStyle = this.deepMerge(queryTarget);
|
|
var formatter = itemStyle.normal.label.formatter;
|
var text = this.getLabelText(name, value, formatter);
|
var textFont = this.getFont(normalTextStyle);
|
var textWidth = toolArea.getTextWidth(text, textFont);
|
var textHeight = toolArea.getTextHeight(text, textFont);
|
|
var emphasisFormatter = this.deepQuery(
|
[itemStyle.emphasis, itemStyle.normal],
|
'label.formatter'
|
);
|
var emphasisText = this.getLabelText(name, value, emphasisFormatter);
|
var emphasisTextFont = this.getFont(emphasisTextStyle);
|
var emphasisTextWidth = toolArea.getTextWidth(text, emphasisTextFont);
|
var emphasisTextHeight = toolArea.getTextHeight(text, emphasisTextFont);
|
if (!itemStyle.normal.label.show) {
|
text = '';
|
}
|
else if (itemStyle.normal.label.x + textWidth > rect.width
|
|| itemStyle.normal.label.y + textHeight > rect.height) {
|
text = '';
|
}
|
|
if (!itemStyle.emphasis.label.show) {
|
emphasisText = '';
|
}
|
else if (emphasisTextStyle.x + emphasisTextWidth > rect.width
|
|| emphasisTextStyle.y + emphasisTextHeight > rect.height) {
|
emphasisText = '';
|
}
|
|
// 用label方法写title
|
var textShape = {
|
style: {
|
textX: rect.x + itemStyle.normal.label.x,
|
textY: rect.y + itemStyle.normal.label.y,
|
text: text,
|
textPosition: 'specific',
|
textColor: normalTextStyle.color,
|
textFont: textFont
|
},
|
highlightStyle: {
|
textX: rect.x + itemStyle.emphasis.label.x,
|
textY: rect.y + itemStyle.emphasis.label.y,
|
text: emphasisText,
|
textColor: emphasisTextStyle.color,
|
textPosition: 'specific'
|
}
|
};
|
return textShape;
|
},
|
/**
|
* 支持formatter
|
* @param {string} name 数据名称
|
* @param {number} value 数据值
|
* @param {string|Object} formatter 构造器
|
* @return {Object} 返回label的样式
|
*/
|
getLabelText: function (name, value, formatter) {
|
if (formatter) {
|
if (typeof formatter === 'function') {
|
return formatter.call(
|
this.myChart,
|
name,
|
value
|
);
|
}
|
else if (typeof formatter === 'string') {
|
formatter = formatter.replace('{b}', '{b0}')
|
.replace('{c}', '{c0}');
|
formatter = formatter.replace('{b0}', name)
|
.replace('{c0}', value);
|
return formatter;
|
}
|
}
|
else {
|
return name;
|
}
|
},
|
/*
|
* 构建子级矩形图,这里用线表示不再画矩形
|
*
|
* @param {Object} data 数据
|
* @param {Object} rect
|
* @return
|
*/
|
_buildChildrenTreemap: function (
|
data,
|
itemStyle,
|
rect,
|
seriesIndex
|
) {
|
var treemapArea = rect.width * rect.height; // 计算总面积
|
// 遍历数组,通过value与area0计算实际面积area
|
var sum = 0;
|
var areaArr = [];
|
for (var i = 0; i < data.length; i++) {
|
sum += data[i].value;
|
}
|
for (var j = 0; j < data.length; j++) {
|
areaArr.push(data[j].value * treemapArea / sum);
|
}
|
var treeMapLayout = new TreeMapLayout({
|
x: rect.x,
|
y: rect.y,
|
width: rect.width,
|
height: rect.height
|
});
|
var locationArr = treeMapLayout.run(areaArr);
|
var lineWidth = itemStyle.normal.childBorderWidth;
|
var lineColor = itemStyle.normal.childBorderColor;
|
for (var k = 0; k < locationArr.length; k++) {
|
var item = locationArr[k];
|
var lines = [];
|
// 容器边框不能重复画
|
// 上边
|
if (rect.y.toFixed(2) !== item.y.toFixed(2)) {
|
lines.push(this._getLine(
|
item.x,
|
item.y,
|
item.x + item.width,
|
item.y,
|
lineWidth,
|
lineColor
|
));
|
}
|
// 左边
|
if (rect.x.toFixed(2) !== item.x.toFixed(2)) {
|
lines.push(this._getLine(
|
item.x,
|
item.y,
|
item.x,
|
item.y + item.height,
|
lineWidth,
|
lineColor
|
));
|
}
|
// 下边
|
if ((rect.y + rect.height).toFixed(2) !== (item.y + item.height).toFixed(2)) {
|
lines.push(this._getLine(
|
item.x,
|
item.y + item.height,
|
item.x + item.width,
|
item.y + item.height,
|
lineWidth,
|
lineColor
|
));
|
}
|
// 右边
|
if ((rect.x + rect.width).toFixed(2) !== (item.x + item.width).toFixed(2)) {
|
lines.push(this._getLine(
|
item.x + item.width,
|
item.y,
|
item.x + item.width,
|
item.y + item.height,
|
lineWidth,
|
lineColor
|
));
|
}
|
for (var l = 0; l < lines.length; l++) {
|
ecData.set(lines[l], 'seriesIndex', seriesIndex);
|
this.shapeList.push(lines[l]);
|
}
|
}
|
},
|
/*
|
* 构建线段
|
* @param {number} xStart 开始坐标
|
* @param {number} yStart 开始坐标
|
* @param {number} xEnd 结束坐标
|
* @param {number} yEnd 结束坐标
|
* @param {number} lineWidth 线宽
|
* @param {number} lineColor 颜色
|
* @return {Object} 返回一个线段
|
*/
|
_getLine : function (
|
xStart,
|
yStart,
|
xEnd,
|
yEnd,
|
lineWidth,
|
lineColor
|
) {
|
var lineShape = {
|
zlevel: this.getZlevelBase(),
|
z: this.getZBase(),
|
hoverable: false,
|
style: {
|
xStart: xStart,
|
yStart: yStart,
|
xEnd: xEnd,
|
yEnd: yEnd,
|
lineWidth: lineWidth,
|
strokeColor: lineColor
|
}
|
};
|
return new LineShape(lineShape);
|
|
},
|
|
_buildBreadcrumb: function (treeRoot, seriesIndex, x, y) {
|
var stack = [];
|
|
var current = treeRoot;
|
while (current) {
|
stack.unshift(current.data.name);
|
current = current.parent;
|
}
|
|
var series = this.series[seriesIndex];
|
var textStyle = this.query(series, 'itemStyle.normal.breadcrumb.textStyle') || {};
|
var textEmphasisStyle =
|
this.query(series, 'itemStyle.emphasis.breadcrumb.textStyle') || {};
|
|
var commonStyle = {
|
y: y + 10,
|
textBaseline: 'top',
|
textAlign: 'left',
|
color: textStyle.color,
|
textFont: this.getFont(textStyle)
|
};
|
var commonHighlightStyle = {
|
brushType: 'fill',
|
color: textEmphasisStyle.color || zrColor.lift(textStyle.color, -0.3),
|
textFont: this.getFont(textEmphasisStyle)
|
};
|
|
for (var i = 0; i < stack.length; i++) {
|
var textShape = new TextShape({
|
zlevel: this.getZlevelBase(),
|
z: this.getZBase(),
|
style: zrUtil.merge({
|
x: x,
|
text: stack[i] + (stack.length - 1 - i ? ' > ' : '')
|
}, commonStyle),
|
clickable: true,
|
highlightStyle: commonHighlightStyle
|
});
|
|
ecData.set(textShape, 'seriesIndex', seriesIndex);
|
ecData.set(textShape, 'name', stack[i]);
|
|
x += textShape.getRect(textShape.style).width;
|
|
this.shapeList.push(textShape);
|
}
|
},
|
|
__onclick : function (params) {
|
var target = params.target;
|
if (target) {
|
var seriesIndex = ecData.get(target, 'seriesIndex');
|
var name = ecData.get(target, 'name');
|
var tree = this._treesMap[seriesIndex];
|
var root = tree.getNodeById(name);
|
|
if (root && root.children.length) {
|
this._buildTreemap(root, seriesIndex);
|
}
|
}
|
}
|
};
|
|
zrUtil.inherits(Treemap, ChartBase);
|
|
// 图表注册
|
require('../chart').define('treemap', Treemap);
|
|
return Treemap;
|
});
|