/** RadarChart * * This is the main reuseable function to draw radar charts. * * The original d3 project is found on: https://github.com/alangrafu/radar-chart-d3 * This version is based on the cleaned version found on: http://bl.ocks.org/nbremer/6506614 * with some reorganization of code and added commenting, as well as further function abstractions * to allow for addition/removal of visualization components via tweaking configuration parameters. * **/ var RadarChart = { draw: function(id, data, options) { // add touch to mouseover and mouseout var over = "ontouchstart" in window ? "touchstart" : "mouseover"; var out = "ontouchstart" in window ? "touchend" : "mouseout"; /** Initiate default configuration parameters and vis object * **/ // initiate default config var w = 300; var h = 300; var config = { w: w, h: h, facet: false, levels: 5, levelScale: 0.85, labelScale: 1.0, facetPaddingScale: 2.5, maxValue: 0, radians: 2 * Math.PI, polygonAreaOpacity: 0.3, polygonStrokeOpacity: 1, polygonPointSize: 4, legendBoxSize: 10, translateX: w / 4, translateY: h / 4, paddingX: w, paddingY: h, colors: d3.scale.category10(), showLevels: true, showLevelsLabels: true, showAxesLabels: true, showAxes: true, showLegend: true, showVertices: true, showPolygons: true }; // initiate main vis component var vis = { svg: null, tooltip: null, levels: null, axis: null, vertices: null, legend: null, allAxis: null, total: null, radius: null }; // feed user configuration options if ("undefined" !== typeof options) { for (var i in options) { if ("undefined" !== typeof options[i]) { config[i] = options[i]; } } } render(data); // render the visualization /** helper functions * * @function: render: render the visualization * @function: updateConfig: update configuration parameters * @function: buildVis: build visualization using the other build helper functions * @function: buildVisComponents: build main vis components * @function: buildLevels: build "spiderweb" levels * @function: buildLevelsLabels: build out the levels labels * @function: buildAxes: builds out the axes * @function: buildAxesLabels: builds out the axes labels * @function: buildCoordinates: builds [x, y] coordinates of polygon vertices. * @function: buildPolygons: builds out the polygon areas of the dataset * @function: buildVertices: builds out the polygon vertices of the dataset * @function: buildLegend: builds out the legend **/ // render the visualization function render(data) { // remove existing svg if exists d3.select(id).selectAll("svg").remove(); updateConfig(); if (config.facet) { data.forEach(function(d, i) { buildVis([d]); // build svg for each data group // override colors vis.svg.selectAll(".polygon-areas") .attr("stroke", config.colors(i)) .attr("fill", config.colors(i)); vis.svg.selectAll(".polygon-vertices") .attr("fill", config.colors(i)); vis.svg.selectAll(".legend-tiles") .attr("fill", config.colors(i)); }); } else { buildVis(data); // build svg } } // update configuration parameters function updateConfig() { // adjust config parameters config.maxValue = Math.max(config.maxValue, d3.max(data, function(d) { return d3.max(d.axes, function(o) { return o.value; }); })); config.w *= config.levelScale; config.h *= config.levelScale; config.paddingX = config.w * config.levelScale; config.paddingY = config.h * config.levelScale; // if facet required: if (config.facet) { config.w /= data.length; config.h /= data.length; config.paddingX /= (data.length / config.facetPaddingScale); config.paddingY /= (data.length / config.facetPaddingScale); config.polygonPointSize *= Math.pow(0.9, data.length); } } //build visualization using the other build helper functions function buildVis(data) { buildVisComponents(); buildCoordinates(data); if (config.showLevels) buildLevels(); if (config.showLevelsLabels) buildLevelsLabels(); if (config.showAxes) buildAxes(); if (config.showAxesLabels) buildAxesLabels(); if (config.showLegend) buildLegend(data); if (config.showVertices) buildVertices(data); if (config.showPolygons) buildPolygons(data); } // build main vis components function buildVisComponents() { // update vis parameters vis.allAxis = data[0].axes.map(function(i, j) { return i.axis; }); vis.totalAxes = vis.allAxis.length; vis.radius = Math.min(config.w / 2, config.h / 2); // create main vis svg vis.svg = d3.select(id) .append("svg").classed("svg-vis", true) .attr("width", config.w + config.paddingX) .attr("height", config.h + config.paddingY) .append("svg:g") .attr("transform", "translate(" + config.translateX + "," + config.translateY + ")");; // create verticesTooltip vis.verticesTooltip = d3.select("body") .append("div").classed("verticesTooltip", true) .attr("opacity", 0) .style({ "position": "absolute", "color": "black", "font-size": "10px", "width": "100px", "height": "auto", "padding": "5px", "border": "2px solid gray", "border-radius": "5px", "pointer-events": "none", "opacity": "0", "background": "#f4f4f4" }); // create levels vis.levels = vis.svg.selectAll(".levels") .append("svg:g").classed("levels", true); // create axes vis.axes = vis.svg.selectAll(".axes") .append("svg:g").classed("axes", true); // create vertices vis.vertices = vis.svg.selectAll(".vertices"); //Initiate Legend vis.legend = vis.svg.append("svg:g").classed("legend", true) .attr("height", config.h / 2) .attr("width", config.w / 2) .attr("transform", "translate(" + 0 + ", " + 1.1 * config.h + ")"); } // builds out the levels of the spiderweb function buildLevels() { for (var level = 0; level < config.levels; level++) { var levelFactor = vis.radius * ((level + 1) / config.levels); // build level-lines vis.levels .data(vis.allAxis).enter() .append("svg:line").classed("level-lines", true) .attr("x1", function(d, i) { return levelFactor * (1 - Math.sin(i * config.radians / vis.totalAxes)); }) .attr("y1", function(d, i) { return levelFactor * (1 - Math.cos(i * config.radians / vis.totalAxes)); }) .attr("x2", function(d, i) { return levelFactor * (1 - Math.sin((i + 1) * config.radians / vis.totalAxes)); }) .attr("y2", function(d, i) { return levelFactor * (1 - Math.cos((i + 1) * config.radians / vis.totalAxes)); }) .attr("transform", "translate(" + (config.w / 2 - levelFactor) + ", " + (config.h / 2 - levelFactor) + ")") .attr("stroke", "gray") .attr("stroke-width", "0.5px"); } } // builds out the levels labels function buildLevelsLabels() { for (var level = 0; level < config.levels; level++) { var levelFactor = vis.radius * ((level + 1) / config.levels); // build level-labels vis.levels .data([1]).enter() .append("svg:text").classed("level-labels", true) .text((config.maxValue * (level + 1) / config.levels).toFixed(2)) .attr("x", function(d) { return levelFactor * (1 - Math.sin(0)); }) .attr("y", function(d) { return levelFactor * (1 - Math.cos(0)); }) .attr("transform", "translate(" + (config.w / 2 - levelFactor + 5) + ", " + (config.h / 2 - levelFactor) + ")") .attr("fill", "gray") .attr("font-family", "sans-serif") .attr("font-size", 10 * config.labelScale + "px"); } } // builds out the axes function buildAxes() { vis.axes .data(vis.allAxis).enter() .append("svg:line").classed("axis-lines", true) .attr("x1", config.w / 2) .attr("y1", config.h / 2) .attr("x2", function(d, i) { return config.w / 2 * (1 - Math.sin(i * config.radians / vis.totalAxes)); }) .attr("y2", function(d, i) { return config.h / 2 * (1 - Math.cos(i * config.radians / vis.totalAxes)); }) .attr("stroke", "grey") .attr("stroke-width", "1px"); } // builds out the axes labels function buildAxesLabels() { vis.axes .data(vis.allAxis).enter() .append("svg:text").classed("axis-labels", true) .text(function(d) { return d; }) .attr("text-anchor", "middle") .attr("x", function(d, i) { return config.w / 2 * (1 - 1.3 * Math.sin(i * config.radians / vis.totalAxes)); }) .attr("y", function(d, i) { return config.h / 2 * (1 - 1.1 * Math.cos(i * config.radians / vis.totalAxes)); }) .attr("font-family", "sans-serif") .attr("font-size", 11 * config.labelScale + "px"); } // builds [x, y] coordinates of polygon vertices. function buildCoordinates(data) { data.forEach(function(group) { group.axes.forEach(function(d, i) { d.coordinates = { // [x, y] coordinates x: config.w / 2 * (1 - (parseFloat(Math.max(d.value, 0)) / config.maxValue) * Math.sin(i * config.radians / vis.totalAxes)), y: config.h / 2 * (1 - (parseFloat(Math.max(d.value, 0)) / config.maxValue) * Math.cos(i * config.radians / vis.totalAxes)) }; }); }); } // builds out the polygon vertices of the dataset function buildVertices(data) { data.forEach(function(group, g) { vis.vertices .data(group.axes).enter() .append("svg:circle").classed("polygon-vertices", true) .attr("r", config.polygonPointSize) .attr("cx", function(d, i) { return d.coordinates.x; }) .attr("cy", function(d, i) { return d.coordinates.y; }) .attr("fill", config.colors(g)) .on(over, verticesTooltipShow) .on(out, verticesTooltipHide); }); } // builds out the polygon areas of the dataset function buildPolygons(data) { vis.vertices .data(data).enter() .append("svg:polygon").classed("polygon-areas", true) .attr("points", function(group) { // build verticesString for each group var verticesString = ""; group.axes.forEach(function(d) { verticesString += d.coordinates.x + "," + d.coordinates.y + " "; }); return verticesString; }) .attr("stroke-width", "2px") .attr("stroke", function(d, i) { return config.colors(i); }) .attr("fill", function(d, i) { return config.colors(i); }) .attr("fill-opacity", config.polygonAreaOpacity) .attr("stroke-opacity", config.polygonStrokeOpacity) .on(over, function(d) { vis.svg.selectAll(".polygon-areas") // fade all other polygons out .transition(250) .attr("fill-opacity", 0.1) .attr("stroke-opacity", 0.1); d3.select(this) // focus on active polygon .transition(250) .attr("fill-opacity", 0.7) .attr("stroke-opacity", config.polygonStrokeOpacity); }) .on(out, function() { d3.selectAll(".polygon-areas") .transition(250) .attr("fill-opacity", config.polygonAreaOpacity) .attr("stroke-opacity", 1); }); } // builds out the legend function buildLegend(data) { //Create legend squares vis.legend.selectAll(".legend-tiles") .data(data).enter() .append("svg:rect").classed("legend-tiles", true) .attr("x", config.w - config.paddingX / 2) .attr("y", function(d, i) { return i * 2 * config.legendBoxSize; }) .attr("width", config.legendBoxSize) .attr("height", config.legendBoxSize) .attr("fill", function(d, g) { return config.colors(g); }); //Create text next to squares vis.legend.selectAll(".legend-labels") .data(data).enter() .append("svg:text").classed("legend-labels", true) .attr("x", config.w - config.paddingX / 2 + (1.5 * config.legendBoxSize)) .attr("y", function(d, i) { return i * 2 * config.legendBoxSize; }) .attr("dy", 0.07 * config.legendBoxSize + "em") .attr("font-size", 11 * config.labelScale + "px") .attr("fill", "gray") .text(function(d) { return d.group; }); } // show tooltip of vertices function verticesTooltipShow(d) { vis.verticesTooltip.style("opacity", 0.9) .html("Value: " + d.value + "
" + "Description: " + d.description + "
") .style("left", (d3.event.pageX) + "px") .style("top", (d3.event.pageY) + "px"); } // hide tooltip of vertices function verticesTooltipHide() { vis.verticesTooltip.style("opacity", 0); } } };