blob: 6d7ae72abe27907ffe8111850427534814ac86b8 [file] [log] [blame]
/**
* echarts图表类:chord diagram
*
* @author pissang (https://github.com/pissang/)
*
*/
define(function(require) {
require('../util/shape/chord');
'use strict';
var _devicePixelRatio = window.devicePixelRatio || 1;
function Chord(ecConfig, messageCenter, zr, option, component) {
var self = this;
var ComponentBase = require('../component/base');
ComponentBase.call(this, ecConfig, zr);
var CalculableBase = require('./calculableBase');
CalculableBase.call(this, zr, option);
var ecData = require('../util/ecData');
var zrUtil = require('zrender/tool/util');
var vec2 = require('zrender/tool/vector');
var NDArray = require('../util/ndarray');
var legend;
var getColor;
var isSelected;
var series;
this.type = ecConfig.CHART_TYPE_CHORD;
var _zlevelBase = self.getZlevelBase();
var chordSerieSample;
// Config
var chordSeries = [];
var groups;
var startAngle;
var clockWise;
var innerRadius;
var outerRadius;
var padding;
var sortGroups;
var sortSubGroups;
var center;
var showScale;
var showScaleText;
var strokeFix = 0;
// Adjacency matrix
var dataMat;
var sectorShapes = [];
var chordShapes = [];
var scaleLineLength = 4;
var scaleUnitAngle = 4;
function _buildShape() {
self.selectedMap = {};
chordSeries = [];
chordSerieSample = null;
var matrix = [];
var serieNumber = 0;
for (var i = 0, l = series.length; i < l; i++) {
if (series[i].type === self.type) {
// Use the config of first chord serie
if (!chordSerieSample) {
chordSerieSample = series[i];
self.reformOption(chordSerieSample);
}
var _isSelected = isSelected(series[i].name);
// Filter by selected serie
self.selectedMap[series[i].name] = _isSelected;
if (!_isSelected) {
continue;
}
chordSeries.push(series[i]);
self.buildMark(
series[i],
i,
component
);
matrix.push(series[i].matrix);
serieNumber++;
}
}
if (!chordSerieSample) {
return;
}
if (!chordSeries.length) {
return;
}
var zrWidth = zr.getWidth();
var zrHeight = zr.getHeight();
var zrSize = Math.min(zrWidth, zrHeight);
groups = chordSerieSample.data;
startAngle = chordSerieSample.startAngle;
// Constrain to [0, 360]
startAngle = startAngle % 360;
if (startAngle < 0) {
startAngle = startAngle + 360;
}
clockWise = chordSerieSample.clockWise;
innerRadius = self.parsePercent(
chordSerieSample.radius[0],
zrSize / 2
);
outerRadius = self.parsePercent(
chordSerieSample.radius[1],
zrSize / 2
);
padding = chordSerieSample.padding;
sortGroups = chordSerieSample.sort;
sortSubGroups = chordSerieSample.sortSub;
showScale = chordSerieSample.showScale;
showScaleText = chordSerieSample.showScaleText;
center = [
self.parsePercent(chordSerieSample.center[0], zrWidth),
self.parsePercent(chordSerieSample.center[1], zrHeight)
];
var fixSize =
chordSerieSample.itemStyle.normal.chordStyle.lineStyle.width -
chordSerieSample.itemStyle.normal.lineStyle.width;
strokeFix =
(fixSize / _devicePixelRatio) / innerRadius / Math.PI * 180;
dataMat = new NDArray(matrix);
dataMat = dataMat._transposelike([1, 2, 0]);
// Filter the data by selected legend
var res = _filterData(dataMat, groups);
dataMat = res[0];
groups = res[1];
// Check if data is valid
var shape = dataMat.shape();
if (shape[0] !== shape[1] || shape[0] !== groups.length) {
throw new Error('Data not valid');
}
if (shape[0] === 0 || shape[2] === 0) {
return;
}
// Down to 2 dimension
// More convenient for angle calculating and sort
dataMat.reshape(shape[0], shape[1] * shape[2]);
// Processing data
var sumOut = dataMat.sum(1);
var percents = sumOut.mul(1 / sumOut.sum());
var groupNumber = shape[0];
var subGroupNumber = shape[1] * shape[2];
var groupAngles = percents.mul(360 - padding * groupNumber);
var subGroupAngles = dataMat.div(
dataMat.sum(1).reshape(groupNumber, 1)
);
subGroupAngles = subGroupAngles.mul(
groupAngles.sub(strokeFix * 2).reshape(groupNumber, 1)
);
switch (sortGroups) {
case 'ascending':
case 'descending':
var groupIndices = groupAngles
.argsort(0, sortGroups);
groupAngles['sort'](0, sortGroups);
sumOut['sort'](0, sortGroups);
break;
default:
var groupIndices = NDArray.range(shape[0]);
}
switch (sortSubGroups) {
case 'ascending':
case 'descending':
var subGroupIndices = subGroupAngles
.argsort(1, sortSubGroups);
subGroupAngles['sort'](1, sortSubGroups);
break;
default:
var subGroupIndices = NDArray
.range(subGroupNumber)
.reshape(1, subGroupNumber)
.repeat(groupNumber, 0);
}
var groupIndicesArr = groupIndices.toArray();
var groupAnglesArr = groupAngles.toArray();
var subGroupIndicesArr = subGroupIndices.toArray();
var subGroupAnglesArr = subGroupAngles.toArray();
var sumOutArray = sumOut.toArray();
var sectorAngles = [];
var chordAngles = new NDArray(
groupNumber, subGroupNumber
).toArray();
var values = [];
var start = 0;
var end = 0;
for (var i = 0; i < groupNumber; i++) {
var sortedIdx = groupIndicesArr[i];
values[sortedIdx] = sumOutArray[i];
end = start + groupAnglesArr[i];
sectorAngles[sortedIdx] = [start, end];
// Sub Group
var subStart = start + strokeFix;
var subEnd = subStart;
for (var j = 0; j < subGroupNumber; j++) {
subEnd = subStart + subGroupAnglesArr[sortedIdx][j];
var subSortedIndex = subGroupIndicesArr[sortedIdx][j];
/*jshint maxlen : 200*/
chordAngles[sortedIdx][subSortedIndex]
= [subStart, subEnd];
subStart = subEnd;
}
start = end + padding;
}
// reset data
chordShapes = new NDArray(groupNumber, groupNumber, serieNumber)
.toArray();
sectorShapes = [];
_buildSectors(sectorAngles, values);
chordAngles = new NDArray(chordAngles).reshape(
groupNumber, groupNumber, serieNumber, 2
).toArray();
_buildChords(chordAngles, dataMat.reshape(shape).toArray());
var res = normalizeValue(values);
if (showScale) {
_buildScales(
res[0],
res[1],
sectorAngles,
new NDArray(res[0]).sum() / (360 - padding * groupNumber)
);
}
}
function _filterData (dataMat, groups) {
var indices = [];
var groupsFilted = [];
// Filter by selected group
for (var i = 0; i < groups.length; i++) {
var name = groups[i].name;
self.selectedMap[name] = isSelected(name);
if (!self.selectedMap[name]) {
indices.push(i);
} else {
groupsFilted.push(groups[i]);
}
}
if (indices.length) {
dataMat = dataMat['delete'](indices, 0);
dataMat = dataMat['delete'](indices, 1);
}
if (!dataMat.size()) {
return [dataMat, groupsFilted];
}
// Empty data also need to be removed
indices = [];
var groupsFilted2 = [];
var shape = dataMat.shape();
dataMat.reshape(shape[0], shape[1] * shape[2]);
var sumOutArray = dataMat.sum(1).toArray();
dataMat.reshape(shape);
for (var i = 0; i < groupsFilted.length; i++) {
if (sumOutArray[i] === 0) {
indices.push(i);
} else {
groupsFilted2.push(groupsFilted[i]);
}
}
if (indices.length) {
dataMat = dataMat['delete'](indices, 0);
dataMat = dataMat['delete'](indices, 1);
}
return [dataMat, groupsFilted2];
}
function _buildSectors(angles, data) {
var len = groups.length;
var len2 = chordSeries.length;
var timeout;
var showLabel = self.query(
chordSerieSample, 'itemStyle.normal.label.show'
);
var labelColor = self.query(
chordSerieSample, 'itemStyle.normal.label.color'
);
function createMouseOver(idx) {
return function() {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function(){
for (var i = 0; i < len; i++) {
sectorShapes[i].style.opacity
= i === idx ? 1 : 0.1;
zr.modShape(
sectorShapes[i].id,
sectorShapes[i]
);
for (var j = 0; j < len; j++) {
for (var k = 0; k < len2; k++) {
var chordShape = chordShapes[i][j][k];
if (chordShape) {
chordShape.style.opacity
= (i === idx || j === idx)
? 0.5 : 0.03;
zr.modShape(chordShape.id, chordShape);
}
}
}
}
zr.refresh();
}, 50);
};
}
function createMouseOut() {
return function() {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function(){
for (var i = 0; i < len; i++) {
sectorShapes[i].style.opacity = 1.0;
zr.modShape(sectorShapes[i].id, sectorShapes[i]);
for (var j = 0; j < len; j++) {
for (var k = 0; k < len2; k++) {
var chordShape = chordShapes[i][j][k];
if (chordShape) {
chordShape.style.opacity = 0.5;
zr.modShape(chordShape.id, chordShape);
}
}
}
}
zr.refresh();
}, 50);
};
}
for (var i = 0; i < len; i++) {
var group = groups[i];
var angle = angles[i];
var _start = (clockWise ? (360 - angle[1]) : angle[0])
+ startAngle;
var _end = (clockWise ? (360 - angle[0]) : angle[1])
+ startAngle;
var sector = {
id : zr.newShapeId(self.type),
shape : 'sector',
zlevel : _zlevelBase,
style : {
x : center[0],
y : center[1],
r0 : innerRadius,
r : outerRadius,
startAngle : _start,
endAngle : _end,
brushType : 'fill',
opacity: 1,
color : getColor(group.name)
},
highlightStyle : {
brushType : 'fill'
}
};
sector.style.lineWidth = self.deepQuery(
[group, chordSerieSample],
'itemStyle.normal.lineStyle.width'
);
sector.highlightStyle.lineWidth = self.deepQuery(
[group, chordSerieSample],
'itemStyle.emphasis.lineStyle.width'
);
sector.style.strokeColor = self.deepQuery(
[group, chordSerieSample],
'itemStyle.normal.lineStyle.color'
);
sector.highlightStyle.strokeColor = self.deepQuery(
[group, chordSerieSample],
'itemStyle.emphasis.lineStyle.color'
);
if (sector.style.lineWidth > 0) {
sector.style.brushType = 'both';
}
if (sector.highlightStyle.lineWidth > 0) {
sector.highlightStyle.brushType = 'both';
}
ecData.pack(
sector,
chordSeries[0],
0,
data[i], 0,
group.name
);
if (showLabel) {
var halfAngle = [_start + _end] / 2;
halfAngle %= 360; // Constrain to [0,360]
var isRightSide = halfAngle <= 90
|| halfAngle >= 270;
halfAngle = halfAngle * Math.PI / 180;
var v = [Math.cos(halfAngle), -Math.sin(halfAngle)];
var distance = showScaleText ? 45 : 20;
var start = vec2.scale([], v, outerRadius + distance);
vec2.add(start, start, center);
var labelShape = {
shape : 'text',
id : zr.newShapeId(self.type),
zlevel : _zlevelBase - 1,
hoverable : false,
style : {
x : start[0],
y : start[1],
text : group.name,
textAlign : isRightSide ? 'left' : 'right',
color : labelColor
}
};
labelShape.style.textColor = self.deepQuery(
[group, chordSerieSample],
'itemStyle.normal.label.textStyle.color'
) || '#fff';
labelShape.style.textFont = self.getFont(self.deepQuery(
[group, chordSerieSample],
'itemStyle.normal.label.textStyle'
));
zr.addShape(labelShape);
self.shapeList.push(labelShape);
}
sector.onmouseover = createMouseOver(i);
sector.onmouseout = createMouseOut();
self.shapeList.push(sector);
sectorShapes.push(sector);
zr.addShape(sector);
}
}
function _buildChords(angles, dataArr) {
var len = angles.length;
if (!len) {
return;
}
var len2 = angles[0][0].length;
var chordLineStyle
= chordSerieSample.itemStyle.normal.chordStyle.lineStyle;
var chordLineStyleEmphsis
= chordSerieSample.itemStyle.emphasis.chordStyle.lineStyle;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len; j++) {
for (var k = 0; k < len2; k++) {
if (chordShapes[j][i][k]) {
continue;
}
var angleIJ0 = angles[i][j][k][0];
var angleJI0 = angles[j][i][k][0];
var angleIJ1 = angles[i][j][k][1];
var angleJI1 = angles[j][i][k][1];
if (angleIJ0 - angleJI1 === 0 ||
angleJI0 - angleJI1 === 0) {
chordShapes[i][j][k] = null;
continue;
}
var color;
if (len2 === 1) {
if (angleIJ1 - angleIJ0 <= angleJI1 - angleJI0) {
color = getColor(groups[i].name);
} else {
color = getColor(groups[j].name);
}
} else {
color = getColor(chordSeries[k].name);
}
var s0 = !clockWise ? (360 - angleIJ1) : angleIJ0;
var s1 = !clockWise ? (360 - angleIJ0) : angleIJ1;
var t0 = !clockWise ? (360 - angleJI1) : angleJI0;
var t1 = !clockWise ? (360 - angleJI0) : angleJI1;
var chord = {
id : zr.newShapeId(self.type),
shape : 'chord',
zlevel : _zlevelBase,
style : {
center : center,
r : innerRadius,
source0 : s0 - startAngle,
source1 : s1 - startAngle,
target0 : t0 - startAngle,
target1 : t1 - startAngle,
brushType : 'both',
opacity : 0.5,
color : color,
lineWidth : chordLineStyle.width,
strokeColor : chordLineStyle.color
},
highlightStyle : {
brushType : 'both',
lineWidth : chordLineStyleEmphsis.width,
strokeColor : chordLineStyleEmphsis.color
}
};
ecData.pack(
chord,
chordSeries[k],
k,
dataArr[i][j][k], 0,
groups[i].name,
groups[j].name,
dataArr[j][i][k]
);
chordShapes[i][j][k] = chord;
self.shapeList.push(chord);
zr.addShape(chord);
}
}
}
}
function _buildScales(
values,
unitPostfix,
angles,
unitValue
) {
for (var i = 0; i < angles.length; i++) {
var subStartAngle = angles[i][0];
var subEndAngle = angles[i][1];
var scaleAngle = subStartAngle;
while (scaleAngle < subEndAngle) {
var thelta = ((clockWise ? (360 - scaleAngle) : scaleAngle)
+ startAngle) / 180 * Math.PI;
var v = [
Math.cos(thelta),
-Math.sin(thelta)
];
var start = vec2.scale([], v, outerRadius + 1);
vec2.add(start, start, center);
var end = vec2.scale([], v, outerRadius + scaleLineLength);
vec2.add(end, end, center);
var scaleShape = {
shape : 'line',
id : zr.newShapeId(self.type),
zlevel : _zlevelBase - 1,
hoverable : false,
style : {
xStart : start[0],
yStart : start[1],
xEnd : end[0],
yEnd : end[1],
lineCap : 'round',
brushType : 'stroke',
strokeColor : '#666'
}
};
self.shapeList.push(scaleShape);
zr.addShape(scaleShape);
scaleAngle += scaleUnitAngle;
}
if (!showScaleText) {
continue;
}
var scaleTextAngle = subStartAngle;
var step = unitValue * 5 * scaleUnitAngle;
var scaleValues = NDArray.range(0, values[i], step).toArray();
while (scaleTextAngle < subEndAngle) {
var thelta = clockWise
? (360 - scaleTextAngle) : scaleTextAngle;
thelta = (thelta + startAngle) % 360;
var isRightSide = thelta <= 90
|| thelta >= 270;
var textShape = {
shape : 'text',
id : zr.newShapeId(self.type),
zlevel : _zlevelBase - 1,
hoverable : false,
style : {
x : isRightSide
? outerRadius + scaleLineLength + 4
: -outerRadius - scaleLineLength - 4,
y : 0,
text : Math.round(scaleValues.shift()*10)/10
+ unitPostfix,
textAlign : isRightSide ? 'left' : 'right'
},
position : center.slice(),
rotation : isRightSide
? [thelta / 180 * Math.PI, 0, 0]
: [
(thelta + 180) / 180 * Math.PI,
0, 0
]
};
self.shapeList.push(textShape);
zr.addShape(textShape);
scaleTextAngle += scaleUnitAngle * 5;
}
}
}
function normalizeValue(values) {
var result = [];
var max = new NDArray(values).max();
var unitPostfix, unitScale;
if (max > 10000) {
unitPostfix = 'k';
unitScale = 1 / 1000;
} else if (max > 10000000) {
unitPostfix = 'm';
unitScale = 1 / 1000000;
} else if (max > 10000000000) {
unitPostfix = 'b';
unitScale = 1 / 1000000000;
} else {
unitPostfix = '';
unitScale = 1;
}
for (var i = 0; i < values.length; i++) {
result[i] = values[i] * unitScale;
}
return [result, unitPostfix];
}
function init(newOption, newComponent) {
component = newComponent;
refresh(newOption);
}
function refresh(newOption) {
if (newOption) {
option = newOption;
series = option.series;
}
self.clear();
legend = component.legend;
if (legend) {
getColor = legend.getColor;
isSelected = legend.isSelected;
} else {
var colorIndices = {};
var colorMap = {};
var count = 0;
getColor = function(key) {
if (colorMap[key]) {
return colorMap[key];
}
if (colorIndices[key] === undefined) {
colorIndices[key] = count++;
}
// key is serie name
for (var i = 0; i < chordSeries.length; i++) {
if (chordSeries[i].name === key) {
colorMap[key] = self.query(
chordSeries[i],
'itemStyle.normal.color'
);
break;
}
}
if (!colorMap[key]) {
var len = groups.length;
// key is group name
for (var i = 0; i < len; i++) {
if (groups[i].name === key) {
colorMap[key] = self.query(
groups[i],
'itemStyle.normal.color'
);
break;
}
}
}
if (!colorMap[key]) {
colorMap[key] = zr.getColor(colorIndices[key]);
}
return colorMap[key];
};
isSelected = function() {
return true;
};
}
_buildShape();
}
function reformOption(opt) {
var _merge = zrUtil.merge;
opt = _merge(
opt || {},
ecConfig.chord,
{
'overwrite' : false,
'recursive' : true
}
);
opt.itemStyle.normal.label.textStyle = _merge(
opt.itemStyle.normal.label.textStyle || {},
ecConfig.textStyle,
{
'overwrite' : false,
'recursive' : true
}
);
}
self.init = init;
self.refresh = refresh;
self.reformOption = reformOption;
init(option, component);
}
require('../chart').define('chord', Chord);
return Chord;
});