Skip to content

Instantly share code, notes, and snippets.

@mph006
Last active March 7, 2016 15:53
Show Gist options
  • Save mph006/336bbae1ffdbe132fee5 to your computer and use it in GitHub Desktop.
Save mph006/336bbae1ffdbe132fee5 to your computer and use it in GitHub Desktop.
Radar Chart - Axis Addition/Removal

Basic Radar chart layout code can be found here : http://bl.ocks.org/nbremer/6506614

The purpose of this block is to explore the ability to update radar chart axes. This block is not complete, as removing an axis out of order (ie, not at 0 deg) throws off the animation. If anyone knows how to fix this bug, please let me know.

var d = [
{
name:"John Doe",
color:"#d62728",
skills:[
{axis:"Node",value:0},
{axis:"D3",value:0},
{axis:"Javascript",value:0},
{axis:"HTML",value:0},
{axis:"CSS",value:0},
{axis:"Python",value:0},
{axis:"Haskell",value:0},
{axis:"SmallTalk",value:0},
{axis:"ADA",value:0},
{axis:"R",value:0},
{axis:"Julia",value:0},
{axis:"C++",value:0},
{axis:"Java",value:0},
{axis:"Lisp",value:0},
{axis:"PHP",value:0}
]
}
];
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Axis Addition/Removal</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js" charset="utf-8"></script>
<style type="text/css">
body {
overflow: hidden;
margin: 0;
font-size: 14px;
font-family: "proxima_nova_reg", sans-serif;
}
body .add-skill {
position: absolute;
left: 100px;
top: 50px;
height: 20px;
background-color: #a8a8a8;
color: white;
border-radius: 40px;
padding: 8px 16px 7px 13px;
cursor: default;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#chart {
position: absolute;
left: 100px;
}
.yaxis, .xaxis {
font: 10px sans-serif;
width: 100px;
}
.yaxis path,
.yaxis line,
.yaxis tick {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.xaxis path,
.xaxis line {
fill: none;
stroke: black;
stroke-dasharray: 10,10;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<div id="body">
<div id="chart"></div>
<div class="add-skill">Add New Axis</div>
</div>
</body>
<script type="text/javascript" src="data.js"></script>
<script type="text/javascript" src="script.js"></script>
</html>
//cfg for the radar chart formatting
var cfg = {
w: 675,
h: 675,
maxValue: 10,
levels: 10,
opacityArea: 0.20,
radius:5,
radians: 2 * Math.PI,
factor:1,
factorLegend: 0.85,
ToRight: 5,
duration:200,
ExtraWidthX: 700,
ExtraWidthY: 200,
TranslateX: 100,
TranslateY:60,
};
function init(){
cfg.maxValue = Math.max(cfg.maxValue, d3.max(d, function(i){return d3.max(i.skills.map(function(o){return o.value;}));}));
var allAxis = (d[0].skills.map(function(i, j){return i.axis;}));
var total = allAxis.length;
var radius = cfg.factor*Math.min(cfg.w/2, cfg.h/2);
d3.select("#chart").select("svg").remove();
var maxAxisValues = [];
var g = d3.select("#chart")
.append("svg")
.attr("width", cfg.w+cfg.ExtraWidthX)
.attr("height", cfg.h+cfg.ExtraWidthY)
.append("g")
.attr("transform", "translate(" + cfg.TranslateX + "," + cfg.TranslateY + ")")
.attr("id","chartArea");
var tooltip;
//Web and text labels
for(var j=0; j<cfg.levels; j++){
var levelFactor = cfg.factor*radius*((j+1)/cfg.levels);
//Drawing web
g.selectAll(".levels")
.data(allAxis)
.enter()
.append("svg:line")
.attr("x1", function(d, i){
return levelFactor*(1-cfg.factor*Math.sin(i*cfg.radians/total));})
.attr("y1", function(d, i){
return levelFactor*(1-cfg.factor*Math.cos(i*cfg.radians/total));})
.attr("x2", function(d, i){
return levelFactor*(1-cfg.factor*Math.sin((i+1)*cfg.radians/total));})
.attr("y2", function(d, i){
return levelFactor*(1-cfg.factor*Math.cos((i+1)*cfg.radians/total));})
.attr("class", "web-"+j)
.style("stroke", "grey")
.style("stroke-opacity", "0.75")
.style("stroke-width", "0.3px")
.attr("transform", "translate(" + (cfg.w/2-levelFactor) + ", " + (cfg.h/2-levelFactor) + ")");
//Drawing text labels
g.selectAll(".levels")
.data([1]) //dummy data
.enter()
.append("svg:text")
.attr("x", function(d){return levelFactor*(1-cfg.factor*Math.sin(0));})
.attr("y", function(d){return levelFactor*(1-cfg.factor*Math.cos(0));})
.attr("class", "skill-value")
.style("font-family", "sans-serif")
.style("font-size", "10px")
.attr("transform", "translate(" + (cfg.w/2-levelFactor + cfg.ToRight) + ", " + (cfg.h/2-levelFactor) + ")")
.attr("fill", "#737373")
.text((j+1)*cfg.maxValue/cfg.levels);
}
function drawAxis(){
var axis = g.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis")
.attr("id",function(d){return d;});
axis.append("line")
.attr("x1", cfg.w/2)
.attr("y1", cfg.h/2)
.attr("x2", function(j, i){
maxAxisValues[i] = {x:cfg.w/2*(1-cfg.factor*Math.sin(i*cfg.radians/total)), y:0};
return maxAxisValues[i].x;
})
.attr("y2", function(j, i){
maxAxisValues[i].y = cfg.h/2*(1-cfg.factor*Math.cos(i*cfg.radians/total));
return maxAxisValues[i].y;
})
.attr("class", "skill-axis").style("stroke", "grey").style("stroke-width", "1px");
axis.append("text")
.attr("class", "skill-legend")
.text(function(d){return d;})
.style("font-family", "sans-serif")
.style("font-size", "11px")
.style("cursor","pointer")
.attr("text-anchor", "middle")
.attr("dy", "1.5em")
.attr("transform", function(d, i){return "translate(0, -10)";})
.attr("x", function(d, i){
return cfg.w/2*(1-cfg.factorLegend*Math.sin(i*cfg.radians/total))-80*Math.sin(i*cfg.radians/total);
})
.attr("y", function(d, i){
return cfg.h/2*(1-Math.cos(i*cfg.radians/total))-20*Math.cos(i*cfg.radians/total);
})
.on("click",removeAttribute);
}
drawAxis();
} //end init();
init();
function removeSkillFromGlobalData(skill){
for(var i=0; i < d.length; i++){
for(var j=0; j<d[i].skills.length; j++){
if(d[i].skills[j].axis===skill){
d[i].skills.splice(j,1);
}
}
}
}
function adSkillToGlobalData(skill){
for(var i=0; i < d.length; i++){
d[i].skills.push({
axis: skill,
value: 1
});
}
}
function updateRadarChart(){
cfg.maxValue = Math.max(cfg.maxValue, d3.max(d, function(i){return d3.max(i.skills.map(function(o){return o.value;}));}));
var allAxis = (d[0].skills.map(function(i, j){return i.axis;}));
var total = allAxis.length;
var radius = cfg.factor*Math.min(cfg.w/2, cfg.h/2);
var maxAxisValues = [];
var chart = d3.select('#chartArea');
var groups = chart.selectAll(".axis")
.data(allAxis);
// groups.attr('class', 'axis update');
var newGroup = groups.enter()
.append("g")
.attr('class','axis enter')
.attr('id', function(d){ return d;});
newGroup.append("line")
.attr("x1", cfg.w/2)
.attr("y1", cfg.h/2)
.attr("x2", function(j, i){
maxAxisValues[i] = {x:cfg.w/2*(1-cfg.factor*Math.sin(i*cfg.radians/total)), y:0};
return maxAxisValues[i].x;
})
.attr("y2", function(j, i){
maxAxisValues[i].y = cfg.h/2*(1-cfg.factor*Math.cos(i*cfg.radians/total));
return maxAxisValues[i].y;
})
.attr("class", "skill-axis")
.style("stroke", "grey")
.style("stroke-width", "1px")
.style("opacity",0);
newGroup.append("text")
.attr("class", "skill-legend")
.text(function(d){return d;})
.style("font-family", "sans-serif")
.style("font-size", "11px")
.style("cursor","pointer")
.style("opacity",0)
.attr("text-anchor", "middle")
.attr("dy", "1.5em")
.attr("transform", function(d, i){return "translate(0, -10)";})
.attr("x", function(d, i){
return cfg.w/2*(1-cfg.factorLegend*Math.sin(i*cfg.radians/total))-80*Math.sin(i*cfg.radians/total);
})
.attr("y", function(d, i){
return cfg.h/2*(1-Math.cos(i*cfg.radians/total))-20*Math.cos(i*cfg.radians/total);
})
.on("click",removeAttribute);
var axis = d3.selectAll(".skill-axis")
.data(allAxis,function(d){return d;});
axis.transition().duration(1000)
.attr("x1", cfg.w/2)
.attr("y1", cfg.h/2)
.attr("x2", function(j, i){
maxAxisValues[i] = {x:cfg.w/2*(1-cfg.factor*Math.sin(i*cfg.radians/total)), y:0};
return maxAxisValues[i].x;
})
.attr("y2", function(j, i){
maxAxisValues[i].y = cfg.h/2*(1-cfg.factor*Math.cos(i*cfg.radians/total));
return maxAxisValues[i].y;
})
.style("opacity",1);
var labels = d3.selectAll(".skill-legend")
.data(allAxis).transition().duration(1000)
.attr("transform", function(d, i){return "translate(0, -10)";})
.attr("x", function(d, i){
return cfg.w/2*(1-cfg.factorLegend*Math.sin(i*cfg.radians/total))-80*Math.sin(i*cfg.radians/total);
})
.attr("y", function(d, i){
return cfg.h/2*(1-Math.cos(i*cfg.radians/total))-20*Math.cos(i*cfg.radians/total);
})
.style("opacity",1);
// axis.text(function(d){return d;})
// .attr("transform", function(d, i){return "translate(0, -10)";})
// .attr("x", function(d, i){
// return cfg.w/2*(1-cfg.factorLegend*Math.sin(i*cfg.radians/total))-80*Math.sin(i*cfg.radians/total);
// })
// .attr("y", function(d, i){
// return cfg.h/2*(1-Math.cos(i*cfg.radians/total))-20*Math.cos(i*cfg.radians/total);
// })
// .on("click",removeAttribute);
for(var j=0; j<cfg.levels; j++){
var levelFactor = cfg.factor*radius*((j+1)/cfg.levels);
var web = d3.selectAll(".web-" + j)
.data(allAxis);
var newLines = web.enter()
.append("line")
.attr("class", "web-"+j)
.style("stroke", "grey")
.style("stroke-width", "0.3px")
.attr("transform", "translate(" + (cfg.w/2-levelFactor) + ", " + (cfg.h/2-levelFactor) + ")")
.attr("x1", function(d, i){
return levelFactor*(1-cfg.factor*Math.sin(i*cfg.radians/total));})
.attr("y1", function(d, i){
return levelFactor*(1-cfg.factor*Math.cos(i*cfg.radians/total));})
.attr("x2", function(d, i){
return levelFactor*(1-cfg.factor*Math.sin((i+1)*cfg.radians/total));})
.attr("y2", function(d, i){
return levelFactor*(1-cfg.factor*Math.cos((i+1)*cfg.radians/total));});
web.enter()
.append("svg:line")
.attr("x1", function(d, i){
return levelFactor*(1-cfg.factor*Math.sin(i*cfg.radians/total));})
.attr("y1", function(d, i){
return levelFactor*(1-cfg.factor*Math.cos(i*cfg.radians/total));})
.attr("x2", function(d, i){
return levelFactor*(1-cfg.factor*Math.sin((i+1)*cfg.radians/total));})
.attr("y2", function(d, i){
return levelFactor*(1-cfg.factor*Math.cos((i+1)*cfg.radians/total));})
.attr("transform", "translate(" + (cfg.w/2-levelFactor) + ", " + (cfg.h/2-levelFactor) + ")");
web.exit().remove();
}
// debugger;
groups.exit().transition().duration(500).style("opacity",0).remove();
}
// //ToDo:: Instead of fading out, redrawing everything, and fading in here, it would be nice to transition in the new axis & web
// function fadeOut(){
// d3.selectAll(".web").transition().duration(500).style("opacity",0);
// d3.selectAll(".axis").transition().duration(500).style("opacity",0);
// d3.selectAll(".skill-value").transition().duration(500).style("opacity",0);
// }
// function fadeIn(){
// setTimeout(function(){
// d3.selectAll(".axis").remove();
// d3.selectAll(".web").remove();
// d3.selectAll(".skill-value").remove();
// init();
// d3.selectAll(".web").transition().duration(500).style("opacity",1);
// d3.selectAll(".axis").transition().duration(500).style("opacity",1);
// d3.selectAll(".skill-value").transition().duration(500).style("opacity",1);
// },500);
// }
function removeAttribute(){
if(confirm("Remove Skill "+d3.select(this)[0][0].__data__+"?")){
removeSkillFromGlobalData(d3.select(this)[0][0].__data__);
updateRadarChart();
// fadeOut();
// fadeIn();
}
}
function addSkill(){
var skill = prompt("Enter A Skill");
if(skill.length <1){
alert("Please enter a valid skill!");
}
else{
adSkillToGlobalData(skill);
updateRadarChart();
// fadeOut();
// fadeIn();
}
}
d3.select(".add-skill").on("click",addSkill);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment