A redesign of the radar chart function that was created by alangrafu, used in my blog on Making the D3 Radar Chart look a bit better
An even newer version created 2 years later can be found here
| height: 650 | |
| license: mit |
A redesign of the radar chart function that was created by alangrafu, used in my blog on Making the D3 Radar Chart look a bit better
An even newer version created 2 years later can be found here
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> | |
| <title>Radar chart</title> | |
| <script src="http://d3js.org/d3.v3.min.js"></script> | |
| <script src="RadarChart.js"></script> | |
| <style> | |
| body { | |
| overflow: hidden; | |
| margin: 0; | |
| font-size: 14px; | |
| font-family: "Helvetica Neue", Helvetica; | |
| } | |
| #chart { | |
| position: absolute; | |
| top: 50px; | |
| left: 100px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="body"> | |
| <div id="chart"></div> | |
| </div> | |
| <script type="text/javascript" src="script.js"></script> | |
| </body> | |
| </html> |
| //Practically all this code comes from https://github.com/alangrafu/radar-chart-d3 | |
| //I only made some additions and aesthetic adjustments to make the chart look better | |
| //(of course, that is only my point of view) | |
| //Such as a better placement of the titles at each line end, | |
| //adding numbers that reflect what each circular level stands for | |
| //Not placing the last level and slight differences in color | |
| // | |
| //For a bit of extra information check the blog about it: | |
| //http://nbremer.blogspot.nl/2013/09/making-d3-radar-chart-look-bit-better.html | |
| var RadarChart = { | |
| draw: function(id, d, options){ | |
| var cfg = { | |
| radius: 5, | |
| w: 600, | |
| h: 600, | |
| factor: 1, | |
| factorLegend: .85, | |
| levels: 3, | |
| maxValue: 0, | |
| radians: 2 * Math.PI, | |
| opacityArea: 0.5, | |
| ToRight: 5, | |
| TranslateX: 80, | |
| TranslateY: 30, | |
| ExtraWidthX: 100, | |
| ExtraWidthY: 100, | |
| color: d3.scale.category10() | |
| }; | |
| if('undefined' !== typeof options){ | |
| for(var i in options){ | |
| if('undefined' !== typeof options[i]){ | |
| cfg[i] = options[i]; | |
| } | |
| } | |
| } | |
| cfg.maxValue = Math.max(cfg.maxValue, d3.max(d, function(i){return d3.max(i.map(function(o){return o.value;}))})); | |
| var allAxis = (d[0].map(function(i, j){return i.axis})); | |
| var total = allAxis.length; | |
| var radius = cfg.factor*Math.min(cfg.w/2, cfg.h/2); | |
| var Format = d3.format('%'); | |
| d3.select(id).select("svg").remove(); | |
| var g = d3.select(id) | |
| .append("svg") | |
| .attr("width", cfg.w+cfg.ExtraWidthX) | |
| .attr("height", cfg.h+cfg.ExtraWidthY) | |
| .append("g") | |
| .attr("transform", "translate(" + cfg.TranslateX + "," + cfg.TranslateY + ")"); | |
| ; | |
| var tooltip; | |
| //Circular segments | |
| for(var j=0; j<cfg.levels-1; j++){ | |
| var levelFactor = cfg.factor*radius*((j+1)/cfg.levels); | |
| 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", "line") | |
| .style("stroke", "grey") | |
| .style("stroke-opacity", "0.75") | |
| .style("stroke-width", "0.3px") | |
| .attr("transform", "translate(" + (cfg.w/2-levelFactor) + ", " + (cfg.h/2-levelFactor) + ")"); | |
| } | |
| //Text indicating at what % each level is | |
| for(var j=0; j<cfg.levels; j++){ | |
| var levelFactor = cfg.factor*radius*((j+1)/cfg.levels); | |
| 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", "legend") | |
| .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(Format((j+1)*cfg.maxValue/cfg.levels)); | |
| } | |
| series = 0; | |
| var axis = g.selectAll(".axis") | |
| .data(allAxis) | |
| .enter() | |
| .append("g") | |
| .attr("class", "axis"); | |
| axis.append("line") | |
| .attr("x1", cfg.w/2) | |
| .attr("y1", cfg.h/2) | |
| .attr("x2", function(d, i){return cfg.w/2*(1-cfg.factor*Math.sin(i*cfg.radians/total));}) | |
| .attr("y2", function(d, i){return cfg.h/2*(1-cfg.factor*Math.cos(i*cfg.radians/total));}) | |
| .attr("class", "line") | |
| .style("stroke", "grey") | |
| .style("stroke-width", "1px"); | |
| axis.append("text") | |
| .attr("class", "legend") | |
| .text(function(d){return d}) | |
| .style("font-family", "sans-serif") | |
| .style("font-size", "11px") | |
| .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))-60*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);}); | |
| d.forEach(function(y, x){ | |
| dataValues = []; | |
| g.selectAll(".nodes") | |
| .data(y, function(j, i){ | |
| dataValues.push([ | |
| cfg.w/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)), | |
| cfg.h/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total)) | |
| ]); | |
| }); | |
| dataValues.push(dataValues[0]); | |
| g.selectAll(".area") | |
| .data([dataValues]) | |
| .enter() | |
| .append("polygon") | |
| .attr("class", "radar-chart-serie"+series) | |
| .style("stroke-width", "2px") | |
| .style("stroke", cfg.color(series)) | |
| .attr("points",function(d) { | |
| var str=""; | |
| for(var pti=0;pti<d.length;pti++){ | |
| str=str+d[pti][0]+","+d[pti][1]+" "; | |
| } | |
| return str; | |
| }) | |
| .style("fill", function(j, i){return cfg.color(series)}) | |
| .style("fill-opacity", cfg.opacityArea) | |
| .on('mouseover', function (d){ | |
| z = "polygon."+d3.select(this).attr("class"); | |
| g.selectAll("polygon") | |
| .transition(200) | |
| .style("fill-opacity", 0.1); | |
| g.selectAll(z) | |
| .transition(200) | |
| .style("fill-opacity", .7); | |
| }) | |
| .on('mouseout', function(){ | |
| g.selectAll("polygon") | |
| .transition(200) | |
| .style("fill-opacity", cfg.opacityArea); | |
| }); | |
| series++; | |
| }); | |
| series=0; | |
| d.forEach(function(y, x){ | |
| g.selectAll(".nodes") | |
| .data(y).enter() | |
| .append("svg:circle") | |
| .attr("class", "radar-chart-serie"+series) | |
| .attr('r', cfg.radius) | |
| .attr("alt", function(j){return Math.max(j.value, 0)}) | |
| .attr("cx", function(j, i){ | |
| dataValues.push([ | |
| cfg.w/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)), | |
| cfg.h/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total)) | |
| ]); | |
| return cfg.w/2*(1-(Math.max(j.value, 0)/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)); | |
| }) | |
| .attr("cy", function(j, i){ | |
| return cfg.h/2*(1-(Math.max(j.value, 0)/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total)); | |
| }) | |
| .attr("data-id", function(j){return j.axis}) | |
| .style("fill", cfg.color(series)).style("fill-opacity", .9) | |
| .on('mouseover', function (d){ | |
| newX = parseFloat(d3.select(this).attr('cx')) - 10; | |
| newY = parseFloat(d3.select(this).attr('cy')) - 5; | |
| tooltip | |
| .attr('x', newX) | |
| .attr('y', newY) | |
| .text(Format(d.value)) | |
| .transition(200) | |
| .style('opacity', 1); | |
| z = "polygon."+d3.select(this).attr("class"); | |
| g.selectAll("polygon") | |
| .transition(200) | |
| .style("fill-opacity", 0.1); | |
| g.selectAll(z) | |
| .transition(200) | |
| .style("fill-opacity", .7); | |
| }) | |
| .on('mouseout', function(){ | |
| tooltip | |
| .transition(200) | |
| .style('opacity', 0); | |
| g.selectAll("polygon") | |
| .transition(200) | |
| .style("fill-opacity", cfg.opacityArea); | |
| }) | |
| .append("svg:title") | |
| .text(function(j){return Math.max(j.value, 0)}); | |
| series++; | |
| }); | |
| //Tooltip | |
| tooltip = g.append('text') | |
| .style('opacity', 0) | |
| .style('font-family', 'sans-serif') | |
| .style('font-size', '13px'); | |
| } | |
| }; |
| var w = 500, | |
| h = 500; | |
| var colorscale = d3.scale.category10(); | |
| //Legend titles | |
| var LegendOptions = ['Smartphone','Tablet']; | |
| //Data | |
| var d = [ | |
| [ | |
| {axis:"Email",value:0.59}, | |
| {axis:"Social Networks",value:0.56}, | |
| {axis:"Internet Banking",value:0.42}, | |
| {axis:"News Sportsites",value:0.34}, | |
| {axis:"Search Engine",value:0.48}, | |
| {axis:"View Shopping sites",value:0.14}, | |
| {axis:"Paying Online",value:0.11}, | |
| {axis:"Buy Online",value:0.05}, | |
| {axis:"Stream Music",value:0.07}, | |
| {axis:"Online Gaming",value:0.12}, | |
| {axis:"Navigation",value:0.27}, | |
| {axis:"App connected to TV program",value:0.03}, | |
| {axis:"Offline Gaming",value:0.12}, | |
| {axis:"Photo Video",value:0.4}, | |
| {axis:"Reading",value:0.03}, | |
| {axis:"Listen Music",value:0.22}, | |
| {axis:"Watch TV",value:0.03}, | |
| {axis:"TV Movies Streaming",value:0.03}, | |
| {axis:"Listen Radio",value:0.07}, | |
| {axis:"Sending Money",value:0.18}, | |
| {axis:"Other",value:0.07}, | |
| {axis:"Use less Once week",value:0.08} | |
| ],[ | |
| {axis:"Email",value:0.48}, | |
| {axis:"Social Networks",value:0.41}, | |
| {axis:"Internet Banking",value:0.27}, | |
| {axis:"News Sportsites",value:0.28}, | |
| {axis:"Search Engine",value:0.46}, | |
| {axis:"View Shopping sites",value:0.29}, | |
| {axis:"Paying Online",value:0.11}, | |
| {axis:"Buy Online",value:0.14}, | |
| {axis:"Stream Music",value:0.05}, | |
| {axis:"Online Gaming",value:0.19}, | |
| {axis:"Navigation",value:0.14}, | |
| {axis:"App connected to TV program",value:0.06}, | |
| {axis:"Offline Gaming",value:0.24}, | |
| {axis:"Photo Video",value:0.17}, | |
| {axis:"Reading",value:0.15}, | |
| {axis:"Listen Music",value:0.12}, | |
| {axis:"Watch TV",value:0.1}, | |
| {axis:"TV Movies Streaming",value:0.14}, | |
| {axis:"Listen Radio",value:0.06}, | |
| {axis:"Sending Money",value:0.16}, | |
| {axis:"Other",value:0.07}, | |
| {axis:"Use less Once week",value:0.17} | |
| ] | |
| ]; | |
| //Options for the Radar chart, other than default | |
| var mycfg = { | |
| w: w, | |
| h: h, | |
| maxValue: 0.6, | |
| levels: 6, | |
| ExtraWidthX: 300 | |
| } | |
| //Call function to draw the Radar chart | |
| //Will expect that data is in %'s | |
| RadarChart.draw("#chart", d, mycfg); | |
| //////////////////////////////////////////// | |
| /////////// Initiate legend //////////////// | |
| //////////////////////////////////////////// | |
| var svg = d3.select('#body') | |
| .selectAll('svg') | |
| .append('svg') | |
| .attr("width", w+300) | |
| .attr("height", h) | |
| //Create the title for the legend | |
| var text = svg.append("text") | |
| .attr("class", "title") | |
| .attr('transform', 'translate(90,0)') | |
| .attr("x", w - 70) | |
| .attr("y", 10) | |
| .attr("font-size", "12px") | |
| .attr("fill", "#404040") | |
| .text("What % of owners use a specific service in a week"); | |
| //Initiate Legend | |
| var legend = svg.append("g") | |
| .attr("class", "legend") | |
| .attr("height", 100) | |
| .attr("width", 200) | |
| .attr('transform', 'translate(90,20)') | |
| ; | |
| //Create colour squares | |
| legend.selectAll('rect') | |
| .data(LegendOptions) | |
| .enter() | |
| .append("rect") | |
| .attr("x", w - 65) | |
| .attr("y", function(d, i){ return i * 20;}) | |
| .attr("width", 10) | |
| .attr("height", 10) | |
| .style("fill", function(d, i){ return colorscale(i);}) | |
| ; | |
| //Create text next to squares | |
| legend.selectAll('text') | |
| .data(LegendOptions) | |
| .enter() | |
| .append("text") | |
| .attr("x", w - 52) | |
| .attr("y", function(d, i){ return i * 20 + 9;}) | |
| .attr("font-size", "11px") | |
| .attr("fill", "#737373") | |
| .text(function(d) { return d; }) | |
| ; |
Thanks @nbremer for the work that went into making a radar chart look nicer - your post on it was very helpful! Just following up that one: Like you said, it would be a nice idea to highlight the polygons on hover over the legend-key/text; did you get to implement that? It would be a great addtion!
What is the type of licence?
Im pretty new to D3 and just had a look at your chart extension. Looks pretty good!
I was trying to add custom labels (e.g. Title - line break - subtitle) but could not find an easy way.
Do you know a way to extend your code for allowing that?
Thanks again.
Good job,
I like all this, but I dont know how to do to save the chart as image
I'm trying use this tutorial http://bl.ocks.org/Rokotyan/0556f8facbaf344507cdc45dc3622177 but dont work :(
Can you help me?
Great work on this one! Nice looking radar chart are hard to generate. Quick question (please note I am new to D3...) : is there an easy way to rotate the entire radar graph by some degree? I am trying to generate a radar graph that will not have a 'vertex' at the top, but rather have an 'edge'... Any idea? Thanks a lot :)