radar-chart
Version:
345 lines (335 loc) • 13.2 kB
JavaScript
(function(){
var Radar = {
//regexLine : /<line.*?x1="([\d\.]*?)".*?y1="([\d\.]*?)".*?x2="([\d\.]*?)".*?y2="([\d\.]*?)".*?>/gi;
//newline : '<line x1="$1" y1="$2" x2="$3" y2="$4"/>';
options : {
viewBox : '0,0,610,610',
preserveAspectRatio : "none",
chart : {width:'100%', height:'100%'},
isDonut : true, //中心點是否摟空
centerRadius : 20, //中心點空白偏移量
levels:5, //橫軸層數
verticalAxis : {show:true, css:'vertical-axis'}, //縱軸線
horizontalAxis : {show:true, css: 'horizontal-axis'}, //橫軸線
verticalZoom : 0.8, //縱軸縮放 (縱軸padding功用)
maxValue : 100, //縱軸上的最大值
minValue : 0, //縱軸上的最小值
scale : 10, //縱軸刻度數
scaleText : '',
scaleShow : true,
scaleColor : 'black',
scaleWidth : 2,
scaleFontSize : 14,
showTitle: true, //項目標題
placement:{show:true, type:'circle' , radius: 7}, //項目放置點 {type:'rect', width:10, height:10}
//panel
infoPanel : {show:true, width:70, height:24}, //資訊板
infoPanelRadius: {rx:5, ry:5}, //圓角
infoPanelGap : {h:10,v:10},
infoPadding : {left:3 , top: 4},
//
arc : 2 * Math.PI, //圓弧
areaShow : true, //是否顯示一個項目的區塊
color : d3.scale.category10(),
horizontalDashEnable : true , //是否呈現虛線
verticalDashEnable : true,
itemTitleGap : 6, //縱軸上的標題間隔
surmiseFontGap: 3, //假設的font gap
isDebug : false //除錯模式才會把 例外丟出
},
mixOptions : function(options){
var opt = Object.create(this.options);
if(options!=null){
for(var attr in options){
opt[attr] = options[attr];
}
}
return opt;
},
reset : function(id){
d3.select(id).select('svg').remove();
},
//利用三角函式計算坐標點
getPoint : function(radius, radians, offset){
return {
x : radius * -Math.sin(radians) + offset,
y : radius * -Math.cos(radians) + offset
};
},
//繪製刻度表
drawScaleLine : function(svg, verticalLength, onePiece, minLength, opt, self){
if(opt.scaleShow){
var p1 = self.getPoint(verticalLength, 0 , minLength);
var p2 = self.getPoint(opt.centerRadius, 0 , minLength);
var basicLength = (p2.y - p1.y) / opt.levels;
var basicValue = (opt.maxValue - opt.minValue) / opt.levels;
/*
svg.append('g').attr('class','scale-group')
.append('line')
.attr('x1', p1.x)
.attr('y1', p1.y)
.attr('x2', p2.x)
.attr('y2', p2.y)
.attr('stroke', opt.scaleColor )
.attr('stroke-width', opt.scaleWidth);
*/
var g = svg.append('g').attr('class','scale-group');
for(var j=0 , count = opt.levels ; j <= count ; j++){
var textY = p2.y - j * basicLength;
g.append('text')
.attr('x', p1.x)
.attr('y', textY)
.attr('fill', opt.scaleColor)
.attr('font-size', opt.scaleFontSize + 'px')
.text(j * basicValue + opt.scaleText);
}
}
},
//繪製橫軸
drawHorizontalAxis : function(svg, index, onePiece, minLength, radius, group, self){
var p1 = self.getPoint(radius, onePiece * (index) , minLength);
var p2 = self.getPoint(radius, onePiece * (index+1) , minLength);
var line = group.append('line');
var css = (self.options.horizontalDashEnable) ? 'horizontal-axis dash' : 'horizontal-axis' ;
line.attr('class', css).attr('x1', p1.x).attr('y1', p1.y).attr('x2', p2.x).attr('y2', p2.y);
},
//繪製橫軸網
drawHorizontalWeb : function(svg, show, onePiece, minLength, verticalLength, count, opt, self){
if(show){
var horizontalAxisGap = verticalLength / opt.levels;
for(var outIndex=0; outIndex <= opt.levels ; outIndex++){
var group = svg.append('g').attr('class', 'horizontal-web');
var radius = outIndex * horizontalAxisGap + opt.centerRadius;
for(var index=0; index < count ; index++){
svg.call(self.drawHorizontalAxis, index, onePiece, minLength, radius, group, self);
}
}
}
},
//繪製縱軸
drawVerticalAxis : function(svg, p1, p2, group, self){
var line = group.append('line');
var css = (self.options.verticalDashEnable) ? ' vertical-axis dash' : 'vertical-axis';
line.attr('class', css).attr('x1', p1.x).attr('y1', p1.y).attr('x2', p2.x).attr('y2', p2.y);
},
//繪製軸上的標題
drawItemTitle : function(svg, p1, p2, title, group, minLength, opt, self){
//minLength 就是矩行的中心點位置
var x = Math.ceil(p1.x);
var y = Math.ceil(p1.y);
var text = group.append('text').attr('class', 'item-title').text(title);
var fontSize = text.style("font-size").replace('pt','').replace('px','') | 0;
var gapTotal = opt.surmiseFontGap * (title.length - 1);
var offsetW = (fontSize * title.length + gapTotal) ; //
var offsetH = fontSize / 2 | 0;
//x is rigth
if(x > minLength){
text.attr('x', p1.x + opt.itemTitleGap );
}else if(x < minLength){
text.attr('x', p1.x - opt.itemTitleGap - offsetW);
}else{
//x is center
text.attr('x', p1.x - offsetW / 2 );
}
if(y > minLength){
text.attr('y', p1.y + offsetH + opt.itemTitleGap + 5);
}else if(y < minLength){
text.attr('y', p1.y - offsetH - opt.itemTitleGap);
}else{
text.attr('y', p1.y);
}
},
//繪製縱軸網
drawVerticalWeb : function(svg, show, onePiece, minLength, verticalLength, count, opt, self){
if(show){
var axixGroup = svg.append('g').attr('class', 'vertical-web');
var titleGroup = svg.append('g').attr('class', 'title-group');
var titleList = svg.datum()[0];
for(var index=0; index < count ; index++){
var currentRadians = onePiece * index; //當前縱軸的弧度
var p1 = self.getPoint(verticalLength, currentRadians , minLength);
var p2 = self.getPoint(opt.centerRadius, currentRadians , minLength);
var title = titleList[index].title;
svg.call(self.drawVerticalAxis, p1, p2, axixGroup, self);
svg.call(self.drawItemTitle, p1, p2, title, titleGroup, minLength, opt, self);
}
}
},
//計算出所有值得落點
getPoinst : function(svg, onePiece, minLength, centerRadius, opt, self){
var pointList = {
areaPoints : [],
pointPoints : []
};
var areaCount = svg.datum().length;
for(var outIndex=0; outIndex < areaCount; outIndex++ ){
pointList.pointPoints[outIndex] = [];
var areaData = svg.datum()[outIndex];
var points='';
for(var index=0, valueCount=areaData.length ; index < valueCount ; index++){
var val = areaData[index].value;
//var radius = (minLength-centerRadius )/ (opt.maxValue - opt.minValue) * val + centerRadius;
var radius = (minLength*opt.verticalZoom )/ (opt.maxValue - opt.minValue) * val + centerRadius;
var point = self.getPoint(radius, onePiece * index , minLength );
points += point.x + ' ' + point.y + ',';
pointList.pointPoints[outIndex].push(point);
}
points = points.substr(0, points.length-1);
pointList.areaPoints.push(points);
}
return pointList;
},
areaMouseOver : function(data){
var areaGroup = d3.select('.area-box').selectAll('.area-group');
var areas = areaGroup.selectAll('.area');
//var marks = areaGroup.selectAll('.mark');
var currentArea = d3.select(this).select('.area');
//var currentMarks = d3.select(this).selectAll('.mark');
areas.classed('areaFade', true);
//marks.classed('areaFade', true);
currentArea.classed('areaFade', false).classed('areaHover', true);
//currentMarks.classed('areaFade', false);//.classed('areaHover', true);
},
areaMouseOut : function(data){
d3.select('.area-box').selectAll('.area-group').selectAll('.area').classed('areaFade', false);
d3.select(this).select('.area').classed('areaHover', false);
//d3.select('.area-box').selectAll('.area-group').selectAll('.mark').classed('areaFade', false);
},
//繪製區域
drawArea : function(svg, show, areaPoints, opt, self){
var areaBox = svg.append('g').attr('class', 'area-box');
if(show){
for(var i=0, l=areaPoints.length; i < l ; i++){
var areaGrup = areaBox.append('g').attr('class', 'area-group');
var area = areaGrup.append('polygon');
area.attr('class', 'area')
.attr('points',areaPoints[i])
.style('fill', opt.color(i))
.style('stroke', opt.color(i));
areaGrup.on('mouseover', self.areaMouseOver);
areaGrup.on('mouseout' , self.areaMouseOut);
}
}
},
markMouseOver : function(data){
var self = this.self;
var target = d3.select(this);
var x = Number(target.attr('cx'));
var y = Number(target.attr('cy'));
var gap = self.options.infoPanelGap;
var padding = self.options.infoPadding;
var viewBoxList = self.options.viewBox.split(',');
var w = viewBoxList[2];
var h = viewBoxList[3];
var panelX = 0;
if(x + gap.h + self.options.infoPanel.width > w ){
panelX = x - gap.h - self.options.infoPanel.width;
panelTitleX = x - gap.h + padding.top - self.options.infoPanel.width;
}else{
panelX = x + gap.h;
panelTitleX = x + gap.h + padding.top;
}
self.panel
.classed('panel-show', true)
.attr('x', panelX)
.attr('y', y + gap.v);
self.panelTitle
.classed('panel-show', true)
.attr('x', panelTitleX)
.attr('y', y + gap.v + 14 + padding.left)
.text(data.title + ':' + data.showValue);
},
markMouseOut : function(data){
var self = this.self;
self.panel.classed('panel-show', false);
self.panelTitle.classed('panel-show', false);
self.panelTitle.text('');
},
//標記分數
drawMark : function(svg, points, opt, self){
var gulps = svg.selectAll('.area-group')[0];
var dataList = svg.datum();
for(var groupIndex=0, count=points.length ; groupIndex < count ; groupIndex++){
var gulp = d3.select(gulps[groupIndex]); //這是跟區域同一圖層
var markPoints = points[groupIndex];
var dataItem = dataList[groupIndex];
for(var markIndex=0 , l = markPoints.length ; markIndex < l ; markIndex++){
var obj = dataItem[markIndex];
var p = markPoints[markIndex];
var mark = gulp.append(opt.placement.type);
mark.attr('class', 'mark');
mark.attr('cx', p.x).attr('cy', p.y).attr('r', opt.placement.radius);
mark.style('fill', opt.color(groupIndex));
mark.datum(obj); //bind data
mark[0][0].self = self; //bind self
mark.on('mouseover', self.markMouseOver);
mark.on('mouseout' , self.markMouseOut);
}
}
},
panel : null,
panelTitle : null,
//panelText : null,
drawInfoPanel : function(svg, opt, self){
var w = opt.infoPanel.width;
var h = opt.infoPanel.height;
var panelGroup = svg.append('g').attr('class','infoPanel-group');
self.panel = panelGroup.append('rect')
.attr('class','infoPanel')
.attr('width', w)
.attr('height', h)
.attr('x', 0)
.attr('y', 0)
.attr('rx', opt.infoPanelRadius.rx)
.attr('ry', opt.infoPanelRadius.ry);
self.panelTitle = panelGroup.append('text').attr('class', 'panel-title');
// self.panelText = panelGroup.append('text').attr('class', 'panel-text');
},
renderRadar : function(svg, opt, self){
var total = svg.datum()[0].length;
var onePiece = opt.arc / total; //一條直軸所占的弧
var viewBoxList = opt.viewBox.split(',');
var w = viewBoxList[2];
var h = viewBoxList[3];
//var minLength = Math.min(svg.property('clientHeight') / 2, svg.property('clientWidth') / 2); //矩形的最小邊 / 2
var minLength = Math.min(h / 2, w / 2);
var verticalLength = opt.verticalZoom * minLength;// - opt.centerRadius; //縱軸長度
var pointsList = self.getPoinst(svg, onePiece, minLength, opt.centerRadius, opt, self); //計算出落點坐標
//reander horizontal axis
svg.call(self.drawHorizontalWeb, opt.horizontalAxis.show, onePiece, minLength, verticalLength, total, opt, self);
//render vertical axis
svg.call(self.drawVerticalWeb, opt.verticalAxis.show, onePiece, minLength, verticalLength+opt.centerRadius, total, opt, self);
//繪製刻度
svg.call(self.drawScaleLine, verticalLength + opt.centerRadius, onePiece, minLength, opt, self);
//reander area
svg.call(self.drawArea, opt.areaShow, pointsList.areaPoints, opt, self );
//rander mark
svg.call(self.drawMark, pointsList.pointPoints, opt, self);
//rander info panel
svg.call(self.drawInfoPanel, opt, self);
},
draw : function(id, data, options){
try{
this.reset(id);
var opt = (options) ? this.mixOptions(options) : this.mixOptions(null) ;
var svg = d3.select(id).append("svg");
svg.attr('class', 'radar');
svg.attr("width", opt.chart.width);
svg.attr("height", opt.chart.height);
svg.attr("viewBox",opt.viewBox);
svg.attr("preserveAspectRatio", opt.preserveAspectRatio);
svg.datum(data); //塞入數據
svg.call(this.renderRadar, opt, this); //要呼叫的函數 (會先執行)
}catch(e){
if(opt.isDebug)
throw e;
}
}
};
if(!window.Radar){
window.Radar = Radar;
}
if(typeof(module)!= "undefined"){
module.exports = Radar;
}
}).call(this);