diff --git a/example/radar/cars.csv b/example/radar/cars.csv new file mode 100644 index 0000000..5f22f5c --- /dev/null +++ b/example/radar/cars.csv @@ -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 \ No newline at end of file diff --git a/example/radar/radar.html b/example/radar/radar.html new file mode 100644 index 0000000..a6c2f1c --- /dev/null +++ b/example/radar/radar.html @@ -0,0 +1,54 @@ + + + + + Radar Chart + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/lib/charts/radar.js b/lib/charts/radar.js new file mode 100644 index 0000000..f6dbb67 --- /dev/null +++ b/lib/charts/radar.js @@ -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; +}); \ No newline at end of file