///////////////////////////////////////////////////////// /////////////// The Radar Chart Function //////////////// /////////////// Written by Nadieh Bremer //////////////// ////////////////// VisualCinnamon.com /////////////////// /////////// Inspired by the code of alangrafu /////////// ///////////////////////////////////////////////////////// function RadarChart(id, data, options) { var cfg = { w: 800, //Width of the circle h: 800, //Height of the circle margin: {top: 20, right: 20, bottom: 20, left: 20}, //The margins around the circle levels: 4, //How many levels or inner circles should there be drawn maxValue: 0, //What is the value that the biggest circle will represent labelFactor: 1.25, //How much farther than the radius of the outer circle should the labels be placed wrapWidth: 60, //The number of pixels after which a label needs to be given a new line opacityArea: 0.35, //The opacity of the area of the blob dotRadius: 4, //The size of the colored circles of each blog opacityCircles: 0.05, //The opacity of the circles of each blob strokeWidth: 2, //The width of the stroke around each blob roundStrokes: false, //If true the area and stroke will follow a round path (cardinal-closed) color: d3.scaleOrdinal(d3.schemeCategory10), //Color function axisName: "axis", value: "value", sortAreas: true, }; //Put all of the options into a variable called cfg if('undefined' !== typeof options){ for(var i in options){ if('undefined' !== typeof options[i]){ cfg[i] = options[i]; } } } //If the supplied maxValue is smaller than the actual one, replace by the max in the data var maxValue = Math.max(cfg.maxValue, d3.max(data.nodes.map(function(o) { return o.score; })) ); var radius = Math.min(cfg.w/2, cfg.h/2), //Radius of the outermost circle Format = d3.format('%'); //Percentage formatting //Scale for the radius var rScale = d3.scaleLinear() .range([0, radius]) .domain([0, maxValue]); ///////////////////////////////////////////////////////// //////////// Create the container SVG and g ///////////// ///////////////////////////////////////////////////////// //Remove whatever chart with the same id/class was present before d3.select(id).select("svg").remove(); //Initiate the radar chart SVG var svg = d3.select(id).append("svg") .attr("width", cfg.w + cfg.margin.left + cfg.margin.right) .attr("height", cfg.h + cfg.margin.top + cfg.margin.bottom) .attr("class", "radar"+id); //Append a g element var g = svg.append("g") .attr("transform", "translate(" + (cfg.w/2 + cfg.margin.left) + "," + (cfg.h/2 + cfg.margin.top) + ")"); ///////////////////////////////////////////////////////// ////////// Glow filter for some extra pizzazz /////////// ///////////////////////////////////////////////////////// //Filter for the outside glow var filter = g.append('defs').append('filter').attr('id','glow'), feGaussianBlur = filter.append('feGaussianBlur').attr('stdDeviation','2.5').attr('result','coloredBlur'), feMerge = filter.append('feMerge'), feMergeNode_1 = feMerge.append('feMergeNode').attr('in','coloredBlur'), feMergeNode_2 = feMerge.append('feMergeNode').attr('in','SourceGraphic'); ///////////////////////////////////////////////////////// /////////////// Draw the Circular grid ////////////////// ///////////////////////////////////////////////////////// //Wrapper for the grid & axes var axisGrid = g.append("g").attr("class", "axisWrapper"); //Draw the background circles axisGrid.selectAll(".levels") .data(d3.range(1,(cfg.levels+1)).reverse()) .enter() .append("circle") .attr("class", "gridCircle") .attr("r", function(d, i){return radius/cfg.levels*d;}) .style("fill", "#CDCDCD") .style("stroke", "#CDCDCD") .style("fill-opacity", cfg.opacityCircles) .style("filter" , "url(#glow)"); //Text indicating at what % each level is axisGrid.selectAll(".axisLabel") .data(d3.range(1,(cfg.levels+1)).reverse()) .enter().append("text") .attr("class", "axisLabel") .attr("x", 4) .attr("y", function(d){return -d*radius/cfg.levels;}) .attr("dy", "0.4em") .style("font-size", "14px") .attr("fill", "#737373") .text(function(d,i) { //console.log('d: '+ d + ' levels : ' + cfg.levels); return Format(maxValue * d/cfg.levels); }); ///////////////////////////////////////////////////////// /////////////// Draw the Circles //////////////////////// ///////////////////////////////////////////////////////// var simulation = d3.forceSimulation() .force("link", d3.forceLink() .distance(function(d){ if(Number.isInteger(d.source.score)) { dist = d.value; Math.random; } else { dist = 30; } //console.log(d); return dist; }) .strength(0.5)) .force("charge", d3.forceManyBody()) .force("center", d3.forceCenter(cfg.w/2 + cfg.margin.left, cfg.h/2 + cfg.margin.top)); var nodes = data.nodes, nodeById = d3.map(nodes, function(d) { return d.id; }), links = [], bilinks = []; nodes.forEach(function(nx) { var s = nodeById.get(nx.id), t = nodeById.get(cfg.sourceNode), i = {}; // intermediate node nodes.push(i); links.push({source: s, target: i, score: 0}, {source: i, target: t}); bilinks.push([s, i, t]); }); console.log('links: ' ,links); console.log('nodes: ' ,nodes); var link = svg.selectAll(".link") .data(bilinks) .enter().append("path") .attr("class", "link"); var node = svg.selectAll(".node") .data(nodes.filter(function(d) { if(d.id) { return d;} })) .enter().append("circle") .attr("class", "node") .attr("r", 15) .attr("fill", function(d) { return color(d.group); }); /*.call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended));*/ simulation .nodes(nodes) .on("tick", ticked); simulation.force("link") .links(links); function ticked() { link.attr("d", positionLink); node.attr("transform", positionNode); } function positionLink(d) { return "M" + d[0].x + "," + d[0].y + "S" + d[1].x + "," + d[1].y + " " + d[2].x + "," + d[2].y; } function positionNode(d) { //console.log(d); return "translate(" + d.x + "," + d.y + ")"; } function dragstarted(d) { if (!d3.event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x, d.fy = d.y; } function dragged(d) { d.fx = d3.event.x, d.fy = d3.event.y; } function dragended(d) { if (!d3.event.active) simulation.alphaTarget(0); d.fx = null, d.fy = null; } ///////////////////////////////////////////////////////// /////////////////// Helper Function ///////////////////// ///////////////////////////////////////////////////////// //Taken from http://bl.ocks.org/mbostock/7555321 //Wraps SVG text function wrap(text, width) { text.each(function() { var text = d3.select(this), words = text.text().split(/\s+/).reverse(), word, line = [], lineNumber = 0, lineHeight = 1.4, // ems y = text.attr("y"), x = text.attr("x"), dy = parseFloat(text.attr("dy")), tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em"); while (word = words.pop()) { line.push(word); tspan.text(line.join(" ")); if (tspan.node().getComputedTextLength() > width) { line.pop(); tspan.text(line.join(" ")); line = [word]; tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word); } } }); }//wrap }//RadarChart