Merge pull request #63 from xiecong/butterfly

radar chart
This commit is contained in:
xiecong 2012-11-18 23:52:53 -08:00
commit 781528411d
3 changed files with 559 additions and 0 deletions

6
example/radar/cars.csv Normal file
View File

@ -0,0 +1,6 @@
name,economy (mpg),cylinders,displacement (cc),power (hp),weight (lb),0-60 mph (s),year
Cadillac Eldorado,23,8,350,125,3900,17.4,79
Citroen DS-21 Pallas,,4,133,115,3090,17.5,70
Ford Capri II,25,4,140,92,2572,14.9,76
Mazda 626,31.3,4,120,75,2542,17.5,80
Oldsmobile Cutlass Ciera,38,6,262,85,3015,17,82
1 name economy (mpg) cylinders displacement (cc) power (hp) weight (lb) 0-60 mph (s) year
2 Cadillac Eldorado 23 8 350 125 3900 17.4 79
3 Citroen DS-21 Pallas 4 133 115 3090 17.5 70
4 Ford Capri II 25 4 140 92 2572 14.9 76
5 Mazda 626 31.3 4 120 75 2542 17.5 80
6 Oldsmobile Cutlass Ciera 38 6 262 85 3015 17 82

54
example/radar/radar.html Normal file
View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Radar Chart</title>
<script src="../../build/deps.js"></script>
<!-- chord -->
<script src="../../deps/seajs/sea.js"></script>
<script>
seajs.config({
alias: {
'DataV': '/lib/datav.js',
'Radar': '/lib/charts/radar.js'
}
});
</script>
<style type="text/css">
#chart {
border-top: 1px dashed #F00;
border-bottom: 1px dashed #F00;
padding-left: 20px;
}
.textArea {
border: 2px solid black;
color: black;
font-family: monospace;
height: 3in;
overflow: auto;
padding: 0.5em;
width: 750px;
}
</style>
</head>
<body>
<div id="chart"></div>
<script type="text/javascript">
seajs.use(["Radar", "DataV"], function (Radar, DataV) {
// DataV.changeTheme("datav");
var radar = new Radar("chart", {
width: 800,
height: 600,
legend: true
});
DataV.csv("cars.csv", function (source) {
radar.setSource(source);
radar.render();
});
});
</script>
</script>
</body>
</html>

499
lib/charts/radar.js Normal file
View File

@ -0,0 +1,499 @@
/*global Raphael, d3 */
/*!
* Radar的兼容定义
*/;
(function (name, definition) {
if (typeof define === 'function') { // Module
define(definition);
} else { // Assign to common namespaces or simply the global object (window)
this[name] = definition(function (id) {
return this[id];
});
}
})('Radar', function (require) {
var DataV = require('DataV');
/**
* 构造函数
* Options:
*
* - `width` 数字图片宽度默认为800表示图片高800px
* - `height` 数字图片高度默认为800
* - `legend` 布尔值图例是否显示默认为 true, 显示设为false则不显示
* - `radius` 数字雷达图半径默认是画布高度的40%
*
* Examples:
* create Radar Chart in a dom node with id "chart", width is 500; height is 600px;
* ```
* var radar = new Radar("chart", {"width": 500, "height": 600});
* ```
* @param {Object} container 表示在html的哪个容器中绘制该组件
* @param {Object} options 为用户自定义的组件的属性比如画布大小
*/
var Radar = DataV.extend(DataV.Chart, {
type: "Radar",
initialize: function (container, options) {
this.node = this.checkContainer(container);
this.click = 0;
this.clickedNum = 0;
// Properties
this.allDimensions = [];
//this.dimensions = [];
this.dimensionType = {};
this.dimensionDomain = {};
this.axises = [];
//图的大小设置
this.defaults.legend = true;
this.defaults.width = 800;
this.defaults.height = 800;
//设置用户指定的属性
this.setOptions(options);
this.legendArea = [20, this.defaults.height, 200, 220];
if (this.defaults.legend) {
this.defaults.xOffset = this.legendArea[2];
} else {
this.defaults.xOffset = 0;
}
this.defaults.radius = Math.min((this.defaults.width - this.defaults.xOffset), this.defaults.height) * 0.4;
//创建画布
this.createCanvas();
this.groups = this.canvas.set();
}
});
/**
* 创建画布
*/
Radar.prototype.createCanvas = function () {
this.canvas = new Raphael(this.node, this.defaults.width, this.defaults.height);
this.node.style.position = "relative";
this.floatTag = DataV.FloatTag()(this.node);
this.floatTag.css({
"visibility": "hidden"
});
};
/**
* 获取颜色
* @param {Number} i 元素类别编号
* @return {String} 返回颜色值
*/
Radar.prototype.getColor = function (i) {
var color = DataV.getColor();
return color[i % color.length][0];
};
/**
* 绘制radar chart
*/
Radar.prototype.render = function () {
var conf = this.defaults;
var that = this;
this.canvas.clear();
var groups = this.groups;
var paper = this.canvas;
var axises = this.axises;
var lNum = this.allDimensions.length - 1;
var axisloopStr = "";
//console.log(lNum);
for (var i = 0; i < lNum; ++i) {
var cos = (conf.radius) * Math.cos(2 * Math.PI * i / lNum) * 0.9;
var sin = (conf.radius) * Math.sin(2 * Math.PI * i / lNum) * 0.9;
var axis = paper.path("M,0,0,L," + cos + "," + sin).attr({
'stroke-opacity': 0.5,
'stroke-width': 1
});
axis.data("x", cos).data("y", sin).transform("T" + (conf.radius + conf.xOffset) + "," + conf.radius);
axises.push(axis);
var axisText = paper.text().attr({
"font-family": "Verdana",
"font-size": 12,
"text": this.allDimensions[i + 1],
'stroke-opacity': 1
}).transform("T" + (conf.radius + cos + conf.xOffset) + "," + (conf.radius + sin));
axisText.translate(axisText.getBBox().width * cos / 2 / conf.radius, axisText.getBBox().height * sin / 2 / conf.radius); // + "R" + (360 * i / lNum + 90)
if (i === 0) {
axisloopStr += "M";
} else {
axisloopStr += "L";
}
axisloopStr += axises[i].data('x') + " " + axises[i].data('y');
}
axisloopStr += "Z";
paper.circle(conf.radius + conf.xOffset, conf.radius, conf.radius * 0.3).attr({
'stroke-opacity': 0.5,
'stroke-width': 1
});
paper.circle(conf.radius + conf.xOffset, conf.radius, conf.radius * 0.6).attr({
'stroke-opacity': 0.5,
'stroke-width': 1
});
paper.circle(conf.radius + conf.xOffset, conf.radius, conf.radius * 0.9).attr({
'stroke-opacity': 0.5,
'stroke-width': 1
});
var mouseOver = function () {
if (!this.data('clicked')) {
if (that.clickedNum === 0) {
groups.attr({
'stroke-opacity': 0.5
});
}
var index = this.data('index');
this.attr({
'stroke-width': 5,
'stroke-opacity': 1
}).toFront();
that.underBn[index].attr({
'opacity': 0.5
}).show();
}
}
var mouseOut = function () {
if (!this.data('clicked')) {
if (that.clickedNum === 0) {
groups.attr({
'stroke-opacity': 1
});
} else {
this.attr({
'stroke-opacity': 0.5
});
}
var index = this.data('index');
this.attr({
'stroke-width': 2
});
that.underBn[index].hide();
}
}
var mouseClick = function () {
var index = this.data('index');
if (!this.data('clicked')) {
if (that.clickedNum === 0) {
groups.attr({
'stroke-opacity': 0.5
});
}
this.attr({
'fill': that.getColor(index),
'stroke-opacity': 1,
'fill-opacity': 0.1
}).toFront();
that.underBn[index].attr({
'opacity': 1
}).show();
this.data('clicked', true);
that.clickedNum++;
} else {
that.clickedNum--;
if (that.clickedNum === 0) {
groups.attr({
'stroke-opacity': 1
});
} else {
this.attr({
'stroke-opacity': 0.5
});
}
this.attr({
'fill': "",
'fill-opacity': 0
});
that.underBn[index].hide();
this.data('clicked', false);
}
}
var source = this.source;
var allDimensions = this.allDimensions;
var dimensionDomain = this.dimensionDomain;
for (var i = 0; i < source.length; i++) {
var pathStr = "";
for (var j = 1; j < allDimensions.length; j++) {
var rate = 0.1 + 0.8 * (source[i][allDimensions[j]] - dimensionDomain[allDimensions[j]][0]) / (dimensionDomain[allDimensions[j]][1] - dimensionDomain[allDimensions[j]][0]);
//console.log(source[i][allDimensions[j]]+","+dimensionDomain[allDimensions[j]][0]+","+dimensionDomain[allDimensions[j]][1]);
if (j != 1) {
pathStr += ",L";
} else {
pathStr += "M";
}
pathStr += rate * axises[j - 1].data('x') + " " + rate * axises[j - 1].data('y');
}
pathStr += "Z";
var loop = paper.path(pathStr).transform("T" + (conf.radius + conf.xOffset) + "," + conf.radius).attr({
'stroke': that.getColor(i),
'stroke-width': 2,
'fill-opacity': 0
}).data('name', source[i][allDimensions[0]]).data('index', i).mouseover(mouseOver).mouseout(mouseOut).click(mouseClick);
groups.push(loop);
};
if (conf.legend) {
this.legend();
}
};
/**
* get dimension types
* @return {Object} {key: dimension name(column name); value: dimenType("ordinal" or "quantitativ")}
*/
Radar.prototype.getDimensionTypes = function () {
return $.extend({}, this.dimensionType);
};
/**
* get dimension domain
* @return {Object} {key: dimension name(column name); value: extent array;}
*/
Radar.prototype.getDimensionDomains = function () {
return $.extend({}, this.dimensionDomain);
};
/*!
* get default ordinal dimension domain
* @param {array} a: array of source ordinal column values
* @return {array} unique string array
*/
Radar.prototype._setOrdinalDomain = function (a) {
var uniq = [];
var index = {};
var i = -1,
n = a.length,
ai;
while (++i < n) {
if (typeof index[ai = a[i]] === 'undefined') {
index[ai] = uniq.push(ai) - 1;
}
}
uniq.itemIndex = index;
return uniq;
};
/*!
* set default dimension domain
* @param {string} dimen: dimension string
*/
Radar.prototype._setDefaultDimensionDomain = function (dimen) {
var conf = this.defaults;
if (this.dimensionType[dimen] === "quantitative") {
this.dimensionDomain[dimen] = d3.extent(this.source, function (p) {
return +p[dimen]
});
} else {
this.dimensionDomain[dimen] = this._setOrdinalDomain(this.source.map(function (p) {
return p[dimen]
}));
}
};
/**
* 对原始数据进行处理
* @param {Array} table 将要被绘制成磊达图的二维表数据
*/
Radar.prototype.setSource = function (source) {
//source is 2-dimension array
var conf = this.defaults;
this.allDimensions = source[0];
//by default all dimensions show
this.dimensions = source[0];
//this.source is array of line; key is dimension, value is line's value in that dimension
this.source = [];
for (var i = 1, l = source.length; i < l; i++) {
var line = {},
dimen = this.allDimensions;
for (var j = 0, ll = dimen.length; j < ll; j++) {
line[dimen[j]] = source[i][j];
}
this.source.push(line);
}
//judge dimesions type auto
//if all number, quantitative else ordinal
this.numeric = this.allDimensions.length;
this.dimensionType = {};
for (var i = 0, l = this.allDimensions.length; i < l; i++) {
var type = "quantitative";
for (var j = 1, ll = source.length; j < ll; j++) {
var d = source[j][i];
if (d && (!DataV.isNumeric(d))) {
type = "ordinal";
this.numeric--;
break;
}
}
this.dimensionType[this.allDimensions[i]] = type;
}
this.setDimensionDomain();
};
/**
* set dimension domain
* Examples:
* ```
* parallel.setDimensionDomain({
* "cylinders": [4, 8], //quantitative
* "year": ["75", "79", "80"] //ordinal
* });
* ```
* @param {Object} dimenDomain {key: dimension name(column name); value: domain array (quantitative domain is digit array whose length is 2, ordinal domain is string array whose length could be larger than 2;}
*/
Radar.prototype.setDimensionDomain = function (dimenDomain) {
//set default dimensionDomain, extent for quantitative type, item array for ordinal type
var conf = this.defaults;
var dimen, i, l, domain;
if (arguments.length === 0) {
for (i = 0, l = this.allDimensions.length; i < l; i++) {
dimen = this.allDimensions[i];
this._setDefaultDimensionDomain(dimen);
}
} else {
for (prop in dimenDomain) {
if (dimenDomain.hasOwnProperty(prop) && this.dimensionType[prop]) {
domain = dimenDomain[prop];
if (!(domain instanceof Array)) {
throw new Error("domain should be an array");
} else {
if (this.dimensionType[prop] === "quantitative" && domain.length !== 2) {
throw new Error("quantitative's domain should be an array with two items, for example: [num1, num2]");
}
if (this.dimensionType[prop] === "quantitative") {
this.dimensionDomain[prop] = domain;
} else if (this.dimensionType[prop] === "ordinal") {
this.dimensionDomain[prop] = this._setOrdinalDomain(domain);
}
}
}
}
}
};
/**
* 绘制图例
*/
Radar.prototype.legend = function () {
var that = this;
var conf = this.defaults;
var paper = this.canvas;
var legendArea = this.legendArea;
this.rectBn = paper.set();
var rectBn = this.rectBn;
this.underBn = [];
var underBn = this.underBn;
var groups = this.groups;
for (var i = 0, l = this.groups.length; i < l; i++) {
//底框
underBn.push(paper.rect(legendArea[0] + 10, legendArea[1] - 17 - (20 + 3) * i, 190, 20).attr({
"fill": "#ebebeb",
"stroke": "none"
}).hide());
//色框
paper.rect(legendArea[0] + 10 + 3, legendArea[1] - 6 - (20 + 3) * i - 6, 16, 8).attr({
"fill": this.getColor(i),
"stroke": "none"
});
//文字
paper.text(legendArea[0] + 10 + 3 + 16 + 8, legendArea[1] - 10 - (20 + 3) * i, this.groups[i].data('name')).attr({
"fill": "black",
"fill-opacity": 1,
"font-family": "Verdana",
"font-size": 12,
"text-anchor": "start"
});
//选框
rectBn.push(paper.rect(legendArea[0] + 10, legendArea[1] - 16 - (20 + 3) * i, 180, 20).attr({
"fill": "white",
"fill-opacity": 0,
"stroke": "none"
}));
}
rectBn.forEach(function (d, i) {
// TODO 这里的事件建议采用事件委托
d.mouseover(function () {
if (!groups[i].data("clicked")) {
if (that.clickedNum === 0) {
groups.attr({
'stroke-opacity': 0.5
});
}
groups[i].attr({
'stroke-width': 5,
'stroke-opacity': 1
});
underBn[i].attr('opacity', 0.5);
underBn[i].show();
}
}).mouseout(function () {
if (!groups[i].data("clicked")) {
if (that.clickedNum === 0) {
groups.attr({
'stroke-opacity': 1
});
} else {
groups[i].attr({
'stroke-opacity': 0.5
});
}
groups[i].attr({
'stroke-width': 2
});
underBn[i].hide();
}
});
d.click(function () {
if (groups[i].data('clicked')) {
that.clickedNum--;
if (that.clickedNum === 0) {
groups.attr({
'stroke-opacity': 1
});
} else {
groups[i].attr({
'stroke-opacity': 0.5
});
}
groups[i].data('clicked', false).attr({
'stroke-width': 2,
'fill': "",
'fill-opacity': 0
});
underBn[i].hide();
} else {
if (that.clickedNum === 0) {
groups.attr({
'stroke-opacity': 0.5
});
}
groups[i].data('clicked', true).attr({
'stroke-width': 5,
'stroke-opacity': 1,
'fill': that.getColor(i),
'fill-opacity': 0.1
}).toFront();
underBn[i].attr({
'opacity': 1
}).show();
that.clickedNum++;
}
});
});
};
return Radar;
});