提交 a708a76c 编写于 作者: P pah100

treemap visual: 3 level and more

上级 9ab9ba62
......@@ -114,13 +114,11 @@ define(function (require) {
var rect = {x: layoutOffset, y: layoutOffset, width: width, height: height};
var rowFixedLength = mathMin(width, height);
var best = Infinity; // the best row score so far
var remaining = viewChildren.slice();
var remainingLen;
var row = [];
row.area = 0;
while ((remainingLen = remaining.length) > 0) {
var child = remaining[remainingLen - 1];
for (var i = 0, len = viewChildren.length; i < len;) {
var child = viewChildren[i];
row.push(child);
row.area += child.getLayout().area;
......@@ -128,7 +126,7 @@ define(function (require) {
// continue with this orientation
if (score <= best) {
remaining.pop();
i++;
best = score;
}
// abort, and try a different orientation
......@@ -154,57 +152,136 @@ define(function (require) {
};
/**
* Set area to each child.
* Set area to each child, and calculate data extent for visual coding.
*/
function initChildren(node, width, height, options) {
var viewChildren = node.children || [];
var nodeModel = node.getModel();
var orderBy = options.sort;
orderBy !== 'asc' && orderBy !== 'desc' && (orderBy = null);
// Sort children, order by desc.
viewChildren = zrUtil.filter(viewChildren, function (child) {
return !child.isRemoved();
});
if (options.sort) {
viewChildren.sort(function (a, b) {
// If 'asc', sort by desc, because layout is performed from tail to head.
return options.sort === 'asc'
? b.getValue() - a.getValue() : a.getValue() - b.getValue();
});
}
sort(viewChildren, orderBy);
var sum = 0;
for (var i = 0, len = viewChildren.length; i < len; i++) {
sum += viewChildren[i].getValue();
var info = statistic(nodeModel, viewChildren, orderBy);
if (info.sum === 0) {
return node.viewChildren = [];
}
var totalArea = width * height;
var nodeModel = node.getModel();
// Filter by thredshold.
for (var i = viewChildren.length - 1; i >= 0; i--) {
var value = viewChildren[i].getValue();
info.sum = filterByThreshold(nodeModel, totalArea, info.sum, orderBy, viewChildren);
if (value / sum * totalArea < nodeModel.get('visibleMin')) {
viewChildren.splice(i, 1);
sum -= value;
}
else {
break;
}
if (info.sum === 0) {
return node.viewChildren = [];
}
// Set area to each child.
for (var i = 0, len = viewChildren.length; i < len; i++) {
var area = viewChildren[i].getValue() / sum * totalArea;
var area = viewChildren[i].getValue() / info.sum * totalArea;
// Do not use setLayout({...}, true), because it is needed to clear last layout.
viewChildren[i].setLayout({area: area});
}
node.viewChildren = viewChildren;
node.setLayout({dataExtent: info.dataExtent}, true);
return viewChildren;
}
/**
* Consider 'visibleMin'. Modify viewChildren and get new sum.
*/
function filterByThreshold(nodeModel, totalArea, sum, orderBy, orderedChildren) {
// visibleMin is not supported yet when no option.sort.
if (!orderBy) {
return sum;
}
var visibleMin = nodeModel.get('visibleMin');
var len = orderedChildren.length;
var deletePoint = len;
// Always travel from little value to big value.
for (var i = len - 1; i >= 0; i--) {
var value = orderedChildren[
orderBy === 'asc' ? len - i - 1 : i
].getValue();
if (value / sum * totalArea < visibleMin) {
deletePoint = i;
sum -= value;
}
}
orderBy === 'asc'
? orderedChildren.splice(0, len - deletePoint)
: orderedChildren.splice(deletePoint, len - deletePoint);
return sum;
}
/**
* Sort
*/
function sort(viewChildren, orderBy) {
if (orderBy) {
viewChildren.sort(function (a, b) {
return orderBy === 'asc'
? a.getValue() - b.getValue() : b.getValue() - a.getValue();
});
}
return viewChildren;
}
/**
* Statistic
*/
function statistic(nodeModel, children, orderBy) {
// Calculate sum.
var sum = 0;
for (var i = 0, len = children.length; i < len; i++) {
sum += children[i].getValue();
}
// Statistic data extent for latter visual coding.
// Notice: data extent should be calculate based on raw children
// but not filtered view children, otherwise visual mapping will not
// be stable when zoom (where children is filtered by visibleMin).
var dimension = nodeModel.get('visualDimension');
var dataExtent;
// The same as area dimension.
if (!children || !children.length) {
dataExtent = [NaN, NaN];
}
else if (dimension === 'value' && orderBy) {
dataExtent = [
children[children.length - 1].getValue(),
children[0].getValue()
];
orderBy === 'asc' && dataExtent.reverse();
}
// Other dimension.
else {
var dataExtent = [Infinity, -Infinity];
zrUtil.each(children, function (child) {
var value = child.getValue(dimension);
value < dataExtent[0] && (dataExtent[0] = value);
value > dataExtent[1] && (dataExtent[1] = value);
});
}
return {sum: sum, dataExtent: dataExtent};
}
/**
* Computes the score for the specified row,
* as the worst aspect ratio.
......
......@@ -3,6 +3,7 @@ define(function(require) {
var SeriesModel = require('../../model/Series');
var Tree = require('../../data/Tree');
var zrUtil = require('zrender/core/util');
var Model = require('../../model/Model');
return SeriesModel.extend({
......@@ -25,7 +26,7 @@ define(function(require) {
clipWindow: 'origin', // 缩放时窗口大小。'origin' or 'fullscreen'
squareRatio: 0.5 * (1 + Math.sqrt(5)), // golden ratio
root: '',
colorDimension: 'value', // 默认第一个维度。
visualDimension: 'value', // 默认第一个维度。
zoomToNodeRatio: 0.32 * 0.32, // zoom to node时 node占可视区域的面积比例。
roam: true,
breadcrumb: {
......@@ -71,9 +72,12 @@ define(function(require) {
},
itemStyle: {
normal: {
color: null, // 各异 可以为数组,表示同一level的color 选取列表。
colorA: null, // 默认不设置 可以为数组,表示同一level的color alpha 选取范围。
colorS: null, // 默认不设置 可以为数组,表示同一level的color alpha 选取范围。
color: null, // 各异 如不需,可设为'none'
colorA: null, // 默认不设置 如不需,可设为'none'
colorS: null, // 默认不设置 如不需,可设为'none'
colorRange: null, // 为数组,表示同一level的color 选取列表。默认取系统color列表。
colorARange: null, // 为数组,表示同一level的color alpha 选取范围。
colorSRange: null, // 为数组,表示同一level的color alpha 选取范围。
colorMapping: 'byIndex', // 'byIndex' or 'byValue'
borderWidth: 0,
borderColor: 'rgba(0,0,0,0)',
......@@ -82,9 +86,10 @@ define(function(require) {
},
emphasis: {}
},
visibleMin: 5, // Less than this threshold, node will not be rendered.
visibleMin: 10, // If area less than this threshold (unit: pixel^2), node will not be rendered.
// Only works when sort is 'asc' or 'desc'.
levels: [] // Each item: {
// visibleMin, itemStyle, colorDimension
// visibleMin, itemStyle, visualDimension
// }
},
......@@ -105,6 +110,8 @@ define(function(require) {
// sereis.mergeOption 的 getInitData是否放在merge后,从而能直接获取merege后的结果而非手动判断。
var levels = option.levels || (this.option || {}).levels || [];
levels = option.levels = setDefault(levels, ecModel);
// 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.
......@@ -177,4 +184,35 @@ define(function(require) {
: (dataNode.value = thisValue);
}
/**
* set default to level configuration
*/
function setDefault(levels, ecModel) {
var globalColorList = ecModel.get('color');
if (!globalColorList) {
return;
}
levels = levels || [];
var hasColorDefine;
zrUtil.each(levels, function (levelDefine) {
var model = new Model(levelDefine);
if (model.get('itemStyle.normal.color')
|| model.get('itemStyle.normal.colorRange')
) {
hasColorDefine = true;
}
});
if (!hasColorDefine) {
var level0 = levels[0] || (levels[0] = {});
var itemStyle = level0.itemStyle || (level0.itemStyle = {});
var normal = itemStyle.normal || (itemStyle.normal = {});
normal.colorRange = globalColorList.slice();
}
return levels;
}
});
\ No newline at end of file
......@@ -374,8 +374,8 @@
// Init controller.
if (!controller) {
controller = this._controller = new RoamController(api.getZr());
controller.on('pan', bind(onPan, this));
controller.on('zoom', bind(onZoom, this));
controller.on('pan', bind(this._onPan, this));
controller.on('zoom', bind(this._onZoom, this));
}
controller.rect = new BoundingRect(0, 0, api.getWidth(), api.getHeight());
......@@ -385,58 +385,63 @@
this._controller = null;
return;
}
},
function onPan(dx, dy) {
if (this._state !== 'animating'
&& (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)
) {
// These param must not be cached.
var seriesModel = this.seriesModel;
var root = seriesModel.getData().tree.root;
if (!root) {
return;
}
var rootGroup = this._storage.nodeGroup[root.getRawIndex()];
// FIXME
// 找个好点的方法?
this._state = 'dragging';
var pos = rootGroup.position;
pos[0] += dx;
pos[1] += dy;
rootGroup.dirty();
// Update breadcrumb when drag move.
this._renderBreadcrumb(seriesModel, api);
/**
* @private
*/
_onPan: function (dx, dy) {
if (this._state !== 'animating'
&& (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)
) {
// These param must not be cached.
var seriesModel = this.seriesModel;
var root = seriesModel.getData().tree.root;
if (!root) {
return;
}
var rootGroup = this._storage.nodeGroup[root.getRawIndex()];
// FIXME
// 找个好点的方法?
this._state = 'dragging';
var pos = rootGroup.position;
pos[0] += dx;
pos[1] += dy;
rootGroup.dirty();
// Update breadcrumb when drag move.
this._renderBreadcrumb(seriesModel, this.api);
}
},
function onZoom(scale, mouseX, mouseY) {
if (this._state !== 'animating' && this._state !== 'dragging') {
// These param must not be cached.
var rect = this._containerGroup.getBoundingRect();
var positionInfo = this.positionInfo;
mouseX -= positionInfo.x;
mouseY -= positionInfo.y;
// Recalculate bounding rect.
var m = matrix.create();
matrix.translate(m, m, [-mouseX, -mouseY]);
matrix.scale(m, m, [scale, scale]);
matrix.translate(m, m, [mouseX, mouseY]);
rect.applyTransform(m);
this.api.dispatch({
type: 'treemapRender',
from: this.uid,
seriesId: this.seriesModel.uid,
viewRect: {
x: rect.x, y: rect.y, width: rect.width, height: rect.height
}
});
}
/**
* @private
*/
_onZoom: function (scale, mouseX, mouseY) {
if (this._state !== 'animating' && this._state !== 'dragging') {
// These param must not be cached.
var rect = this._containerGroup.getBoundingRect();
var positionInfo = this.positionInfo;
mouseX -= positionInfo.x;
mouseY -= positionInfo.y;
// Recalculate bounding rect.
var m = matrix.create();
matrix.translate(m, m, [-mouseX, -mouseY]);
matrix.scale(m, m, [scale, scale]);
matrix.translate(m, m, [mouseX, mouseY]);
rect.applyTransform(m);
this.api.dispatch({
type: 'treemapRender',
from: this.uid,
seriesId: this.seriesModel.uid,
viewRect: {
x: rect.x, y: rect.y, width: rect.width, height: rect.height
}
});
}
},
......
......@@ -4,44 +4,47 @@ define(function (require) {
var zrColor = require('zrender/tool/color');
var zrUtil = require('zrender/core/util');
var isArray = zrUtil.isArray;
var each = zrUtil.each;
var VISUAL_LIST = ['color', 'colorA', 'colorS'];
var ITEM_STYLE_NORMAL = 'itemStyle.normal';
return function (ecModel, payload) {
var globalColorList = ecModel.get('color');
ecModel.eachSeriesByType('treemap', function (seriesModel) {
if (payload && payload.seriesId && seriesModel.uid !== payload.seriesId) {
return;
}
var root = seriesModel.getData().tree.root;
var tree = seriesModel.getData().tree;
var root = tree.root;
var seriesItemStyleModel = seriesModel.getModel(ITEM_STYLE_NORMAL);
if (root.isRemoved()) {
return;
}
var rootVisual = {};
each(VISUAL_LIST, function (name) {
var visual = seriesModel.get('itemStyle.normal.' + name);
rootVisual[name] = isArray(visual) ? null : visual;
var levelItemStyles = zrUtil.map(tree.levelModels, function (levelModel) {
return levelModel ? levelModel.get(ITEM_STYLE_NORMAL) : null;
});
!rootVisual.color && (rootVisual.color = globalColorList);
travelTree(
root,
rootVisual,
seriesModel,
{},
levelItemStyles,
seriesItemStyleModel,
seriesModel.getViewRoot().getAncestors()
);
});
};
function travelTree(node, designatedVisual, seriesModel, viewRootAncestors) {
var visuals = buildVisuals(node, designatedVisual, seriesModel);
function travelTree(
node, designatedVisual, levelItemStyles, seriesItemStyleModel, viewRootAncestors
) {
var nodeModel = node.getModel();
var nodeItemStyleModel = node.getModel(ITEM_STYLE_NORMAL);
var levelItemStyle = levelItemStyles[node.depth];
var visuals = buildVisuals(
nodeItemStyleModel, designatedVisual, levelItemStyle, seriesItemStyleModel
);
var viewChildren = node.viewChildren;
if (!viewChildren || !viewChildren.length) {
......@@ -49,36 +52,38 @@ define(function (require) {
node.setVisual('color', calculateColor(visuals, node));
}
else {
var mappingWrap = buildVisualMapping(node, visuals, viewChildren);
var mappingWrap = buildVisualMapping(
node, nodeItemStyleModel, visuals, viewChildren
);
// Designate visual to children.
zrUtil.each(viewChildren, function (child, index) {
// If higher than viewRoot, only ancestors of viewRoot is needed to visit.
if (child.depth >= viewRootAncestors.length || child === viewRootAncestors[child.depth]) {
var childVisual = mapVisual(node, visuals, child, index, mappingWrap);
travelTree(child, childVisual, seriesModel, viewRootAncestors);
if (child.depth >= viewRootAncestors.length
|| child === viewRootAncestors[child.depth]
) {
var childVisual = mapVisual(nodeModel, visuals, child, index, mappingWrap);
travelTree(
child, childVisual, levelItemStyles, seriesItemStyleModel, viewRootAncestors
);
}
});
}
}
function buildVisuals(node, designatedVisual, seriesModel) {
function buildVisuals(
nodeItemStyleModel, designatedVisual, levelItemStyle, seriesItemStyleModel
) {
var visuals = zrUtil.extend({}, designatedVisual);
zrUtil.each(VISUAL_LIST, function (visualName) {
// Priority: thisNode > thisLevel > parentNodeDesignated
var visualValue = node.getModel('itemStyle.normal').get(visualName, true); // Ignore parent
zrUtil.each(['color', 'colorA', 'colorS'], function (visualName) {
// Priority: thisNode > thisLevel > parentNodeDesignated > seriesModel
var val = nodeItemStyleModel.get(visualName, true); // Ignore parent
if (visualValue == null) {
var levelModel = node.getLevelModel();
visualValue = levelModel ? levelModel.get(visualName, true) : null;
}
if (visualValue == null) {
visualValue = designatedVisual[visualName];
}
val == null && levelItemStyle && (val = levelItemStyle[visualName]);
val == null && (val = designatedVisual[visualName]);
val == null && (val = seriesItemStyleModel.get(visualName));
if (visualValue != null) {
visuals[visualName] = visualValue;
}
val != null && (visuals[visualName] = val);
});
return visuals;
......@@ -101,74 +106,77 @@ define(function (require) {
}
}
function buildVisualMapping(node, visuals, viewChildren) {
function getValueVisualDefine(visuals, name) {
var value = visuals[name];
if (value != null && value !== 'none') {
return value;
}
}
function buildVisualMapping(
node, nodeItemStyleModel, visuals, viewChildren
) {
if (!viewChildren || !viewChildren.length) {
return;
}
var mappingVisualName = getRangeVisualName(visuals, 'color')
var rangeVisual = getRangeVisual(nodeItemStyleModel, 'color')
|| (
visuals.color != null
&& visuals.color !== 'none'
&& (
getRangeVisualName(visuals, 'colorA')
|| getRangeVisualName(visuals, 'colorS')
getRangeVisual(nodeItemStyleModel, 'colorA')
|| getRangeVisual(nodeItemStyleModel, 'colorS')
)
);
if (!mappingVisualName) {
if (!rangeVisual) {
return;
}
var mappingType = mappingVisualName === 'color'
var mappingType = rangeVisual.name === 'color'
? (
node.getModel('itemStyle.normal').get('colorMapping') === 'byValue'
nodeItemStyleModel.get('colorMapping') === 'byValue'
? 'color' : 'colorByIndex'
)
: mappingVisualName;
: rangeVisual.name;
var dataExtent = mappingType === 'colorByIndex'
? null
: calculateDataExtent(node, viewChildren);
: node.getLayout().dataExtent;
return {
mapping: new VisualMapping({
type: mappingType,
dataExtent: dataExtent,
dataNormalizer: 'linear',
visual: visuals[mappingVisualName]
visual: rangeVisual.range
}),
visualName: mappingVisualName
visualName: rangeVisual.name
};
}
function calculateDataExtent(node, viewChildren) {
var dimension = node.getModel().get('colorDimension');
// The same as area dimension.
if (dimension === 'value') {
return [
viewChildren[viewChildren.length - 1].getValue(),
viewChildren[0].getValue()
];
}
// Other dimension.
else {
var dataExtent = [Infinity, -Infinity];
each(viewChildren, function (child) {
var value = child.getValue(dimension);
value < dataExtent[0] && (dataExtent[0] = value);
value > dataExtent[1] && (dataExtent[1] = value);
});
}
// Notice: If we dont have the attribute 'colorRange', but only use
// attribute 'color' to represent both concepts of 'colorRange' and 'color',
// (It means 'colorRange' when 'color' is Array, means 'color' when not array),
// this problem will be encountered:
// If a level-1 node dont have children, and its siblings has children,
// and colorRange is set on level-1, then the node can not be colored.
// So we separate 'colorRange' and 'color' to different attributes.
function getRangeVisual(nodeItemStyleModel, name) {
// 'colorRange', 'colorARange', 'colorSRange'.
// If not exsits on this node, fetch from levels and series.
var range = nodeItemStyleModel.get(name + 'Range');
return (isArray(range) && range.length) ? {name: name, range: range} : null;
}
function mapVisual(node, visuals, child, index, mappingWrap) {
function mapVisual(nodeModel, visuals, child, index, mappingWrap) {
var childVisuals = zrUtil.extend({}, visuals);
if (mappingWrap) {
var mapping = mappingWrap.mapping;
var value = mapping.type === 'colorByIndex'
? index : child.getValue(node.getModel().get('colorDimension'));
? index : child.getValue(nodeModel.get('visualDimension'));
childVisuals[mappingWrap.visualName] = mapping.mapValueToVisual(value);
}
......@@ -176,18 +184,4 @@ define(function (require) {
return childVisuals;
}
function getValueVisualDefine(visuals, name) {
var value = visuals[name];
if (value != null && !isArray(value)) {
return value;
}
}
function getRangeVisualName(visuals, name) {
var value = visuals[name];
if (value != null && isArray(value)) {
return name;
}
}
});
\ No newline at end of file
......@@ -11,9 +11,6 @@ define(function (require) {
});
});
// TODO
// undo redo
function processSingleDataRange(dataRangeModel, ecModel) {
dataRangeModel.eachTargetSeries(function (seriesModel) {
var visualMappings = dataRangeModel.targetVisuals;
......
此差异已折叠。
<html>
<head>
<meta charset="utf-8">
<script src="esl.js"></script>
<script src="config.js"></script>
</head>
<body>
<style>
html, body, #main {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
<div id="main"></div>
<script src="data/disk.tree.js"></script>
<script>
require([
'echarts',
'echarts/component/legend',
'echarts/chart/treemap',
], function (echarts) {
var chart = echarts.init(document.getElementById('main'), null, {
renderer: 'canvas'
});
chart.setOption({
series: [
{
name:'Disk Usage',
type:'treemap',
visibleMin: 300,
label: {
show: true,
formatter: "{b}"
// normal: {
// textStyle: {
// color: 'black'
// }
// }
},
itemStyle: {
normal: {
borderColor: '#fff'
},
emphasis: {
}
},
levels: [
{
itemStyle: {
normal: {
borderWidth: 0,
gapWidth: 5,
color: '#d14a61' // default color
}
}
},
{
itemStyle: {
normal: {
gapWidth: 1,
colorRange: [ // '#5793f3',
'#d14a61', '#fd9c35',
'#675bba', '#fec42c', '#dd4444',
'#d4df5a', '#cd4870'
]
}
}
},
{
itemStyle: {
normal: {
// gapWidth: 1,
colorSRange: [0.35, 0.5]
}
}
}
],
data: window.disk_usage_tree
}
]
});
});
</script>
</body>
</html>
\ No newline at end of file
......@@ -21,6 +21,7 @@
require([
'echarts',
'echarts/component/legend',
'echarts/chart/treemap',
], function (echarts) {
......@@ -30,12 +31,17 @@
chart.setOption({
legend: {
data: ['Obama’s 2012 Budget Proposal: How $3.7 Trillion is Spent']
},
series: [
{
name:'Obama’s 2012 Budget Proposal: How $3.7 Trillion is Spent',
type:'treemap',
// visibleMin: 20,
label: {
show: true,
show: true, //
formatter: "{b}"
},
itemStyle: {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册