From f29c758ac879483e8adef29ed1504af83491271b Mon Sep 17 00:00:00 2001
From: Angelos Chatzimparmpas
Date: Mon, 4 Feb 2019 14:41:15 +0100
Subject: [PATCH] added starplot, fixed barchart, added annotations
---
css/style.css | 26 +-
index.html | 20 +-
js/k-nearest.js | 270 ------------------
js/tsne_vis.js | 366 +++++++++++++++++++++----
modules/d3-annotations/.gitignore | 2 +
modules/d3-annotations/d3-annotator.js | 299 ++++++++++++++++++++
modules/d3-star/.gitignore | 2 +
modules/d3-star/d3-starPlot.js | 272 ++++++++++++++++++
8 files changed, 922 insertions(+), 335 deletions(-)
delete mode 100755 js/k-nearest.js
create mode 100755 modules/d3-annotations/.gitignore
create mode 100755 modules/d3-annotations/d3-annotator.js
create mode 100755 modules/d3-star/.gitignore
create mode 100755 modules/d3-star/d3-starPlot.js
diff --git a/css/style.css b/css/style.css
index 79dc5aa..ab0e91e 100755
--- a/css/style.css
+++ b/css/style.css
@@ -117,7 +117,7 @@ cursor: default;
#knnBarChart {
width: 50vw;
height: 4.2vw;
- margin-top: 3.6vw;
+ margin-top: 4.4vw;
border: 1px solid black;
position: absolute;
display: block;
@@ -177,7 +177,7 @@ svg#legend4 {
#ThumbNailsList{
border: 1px solid black;
width: 15.4vw;
- height: 8vw;
+ height: 13vw;
padding: 10px 10px 10px 10px;
}
@@ -298,4 +298,26 @@ rect {
.bar {
/*shape-rendering: crispEdges;*/
+}
+
+
+.annotation circle {
+ fill: none;
+ stroke: black;
+}
+
+.annotation path {
+ fill: none;
+ stroke: black;
+ shape-rendering: crispEdges;
+}
+
+/* Styling of the main SVG behind canvas */
+#SvgAnnotator {
+ position: absolute;
+ z-index: 3;
+}
+
+#toggleAnnotator {
+ color: darkgray;
}
\ No newline at end of file
diff --git a/index.html b/index.html
index f17387c..f3ca32c 100755
--- a/index.html
+++ b/index.html
@@ -6,8 +6,9 @@
-
-
+
+
+
@@ -17,7 +18,6 @@
-
@@ -62,6 +62,7 @@
+
@@ -84,7 +85,10 @@
+ Type the text of the form:
+
+
@@ -162,19 +166,23 @@
-
-
+
+
-
History Thumbnails
+ Switch Mode
diff --git a/js/k-nearest.js b/js/k-nearest.js
deleted file mode 100755
index 4833aaf..0000000
--- a/js/k-nearest.js
+++ /dev/null
@@ -1,270 +0,0 @@
-function kNearestNeighbors(k, points, points2d) {
-
- var averagekNN = 0;
- var Distances = [];
- var Distances2d = [];
- //var sortedDistances = [];
- var point = points;
- var point2d = points2d;
- /*
- * Loop through our nodes and look for unknown types.
- */
- for (var i=0; i
guess.count) {
- guess.type = type;
- guess.count = types[type];
- }
- }
-
- this.guess = guess;
-
- return types;
- };
-
- calculateRanges = function() {
- this.areas = {min: 1000000, max: 0};
- this.rooms = {min: 1000000, max: 0};
-
- for (var i in this.nodes) {
- if (this.nodes.hasOwnProperty(i)) {
-
- if (this.nodes[i].rooms < this.rooms.min) {
- this.rooms.min = this.nodes[i].rooms;
- }
-
- if (this.nodes[i].rooms > this.rooms.max) {
- this.rooms.max = this.nodes[i].rooms;
- }
-
- if (this.nodes[i].area < this.areas.min) {
- this.areas.min = this.nodes[i].area;
- }
-
- if (this.nodes[i].area > this.areas.max) {
- this.areas.max = this.nodes[i].area;
- }
- }
- }
-
- };*/
-}
-
-/*
- function findNearest(point) {
-
- // TODO: make this more efficient by not recalculating quadtree at
- // each call of findNearest()
-
- // Extract points from the data array
- //points = data.map(function(d) { return [x(d), y(d)]; });
-
- // Add quadtree info to the points
- //nodes = quadtreeify(points);
-
- // Flag k-nearest points by adding `selected` property set to `true`
- kNearest(new Array(nodes), [], point);
-
- // Return nearest points along with indices from origianl `data` array
- return points
- .map(function(d, i) {
- var datum = [d[0], d[1]];
- datum.i = i;
- return d.selected ? datum : null;
- })
- .filter(function(d) { return d !== null; });
- }
-
- findNearest.extent = function(_) {
- if (!arguments.length) return extent;
- extent = _;
- //quadtree.extent(extent);
- return findNearest;
- };
-
- findNearest.data = function(_) {
- if (!arguments.length) return data;
- data = _;
- return findNearest;
- };
-
- findNearest.k = function(_) {
- if (!arguments.length) return k;
- k = _;
- return findNearest;
- };
-
- findNearest.x = function(_) {
- if (!arguments.length) return x;
- x = _;
- return findNearest;
- };
-
- findNearest.y = function(_) {
- if (!arguments.length) return y;
- y = _;
- return findNearest;
- };
-
- return findNearest;
-
- // Add quadtree information to each point (i.e., rectangles, depth, ...)
- function quadtreeify(points) {
- var nodes = quadtree(points);
- nodes.depth = 0;
- nodes.visit(function(node, x1, y1, x2, y2) {
- node.x1 = x1;
- node.y1 = y1;
- node.x2 = x2;
- node.y2 = y2;
- for (var i = 0; i < 4; i++) {
- if (node.nodes[i]) node.nodes[i].depth = node.depth + 1;
- }
- });
- return nodes;
- }
-
- // calculate the euclidean distance of two points with coordinates a(ax, ay) and b(bx, by)
- function euclideanDistance(ax, ay, bx, by) {
- return Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
- }
-
- // calculate minimum distance between search point rectangles
- function minDistance(x, y, x1, y1, x2, y2) {
- var dx1 = x - x1,
- dx2 = x - x2,
- dy1 = y - y1,
- dy2 = y - y2;
-
- // x is between x1 and x2
- if (dx1 * dx2 < 0) {
- // (x, y) is inside the rectangle
- if (dy1 * dy2 < 0) {
- return 0; // return 0 as a point in the rectangle
- }
- return Math.min(Math.abs(dy1), Math.abs(dy2));
- }
-
- // y is between y1 and y2 (and not inside rectangle)
- if (dy1 * dy2 < 0) {
- return Math.min(Math.abs(dx1), Math.abs(dx2));
- }
- return Math.min(
- Math.min(euclideanDistance(x,y,x1,y1), euclideanDistance(x,y,x2,y2)),
- Math.min(euclideanDistance(x,y,x1,y2), euclideanDistance(x,y,x2,y1))
- );
- }
-
- // Find the nodes within the specified rectangle (used recursively)
- function kNearest(bestQueue, resultQueue, point) {
- var x = point[0],
- y = point[1];
-
- // sort children according to their minDistance/euclideanDistance to search point
- bestQueue.sort(function(a, b) {
- // add minDistance to nodes if not there already
- [a, b].forEach(function(d) {
- if (d.minDistance === undefined) {
- d.scanned = true;
- if (d.leaf) {
- d.point.scanned = true;
- d.minDistance = euclideanDistance(x, y, d.x, d.y);
- }
- else {
- d.minDistance = minDistance(x, y, d.x1, d.y1, d.x2, d.y2);
- }
- }
- });
- return b.minDistance - a.minDistance;
- });
-
- // add nearest leafs (if any)
- for (var i = bestQueue.length - 1; i >= 0; i--) {
- var elem = bestQueue[i];
- if (elem.leaf) {
- elem.point.selected = true;
- bestQueue.pop();
- resultQueue.push(elem);
- } else { break; }
- if (resultQueue.length >= k) break;
- }
-
- // check if enough points found
- if (resultQueue.length >= k || bestQueue.length == 0) { return; }
- else {
- // ...otherwise add child nodes to bestQueue and recurse
- var visitedNode = bestQueue.pop();
- visitedNode.visited = true;
- visitedNode.nodes.forEach(function(d) {
- bestQueue.push(d);
- });
- kNearest(bestQueue, resultQueue, point);
- }
- }
- */
\ No newline at end of file
diff --git a/js/tsne_vis.js b/js/tsne_vis.js
index 5b657d8..7dc74c3 100755
--- a/js/tsne_vis.js
+++ b/js/tsne_vis.js
@@ -1,6 +1,6 @@
// t-SNE.js object and other global variables
-var toggleValue = false; var k; var points = []; var all_fields; var pointsbeta = [];
+var toggleValue = false; var k; var points = []; var all_fields; var pointsbeta = []; var KNNEnabled = true;
// These are the dimensions for the square shape of the main panel\
var dimensions = document.getElementById('modtSNEcanvas').offsetWidth;
@@ -73,6 +73,57 @@ function setToggle(toggleVal){
toggleValue = toggleVal;
}
+function setContinue(){
+ d3v3.select("#SvgAnnotator").style("z-index", 2);
+}
+
+function setAnnotator(){
+
+ var viewport2 = getViewport();
+ var vw2 = viewport2[0];
+ var vh2 = viewport2[1];
+ var textarea = document.getElementById("comment").value;
+ var annotations = [
+ {
+ "cx": 232,
+ "cy": 123,
+ "r": 103,
+ "text": textarea,
+ "textOffset": [
+ 114,
+ 88
+ ]
+ }
+ ];
+
+ var ringNote = d3v3.ringNote()
+ .draggable(true);
+
+ var svgAnnotator = d3v3.select("#SvgAnnotator")
+ .attr("width", vw2 * 0.5)
+ .attr("height", vh2 * 0.872)
+ .style("z-index", 3);
+
+
+ var gAnnotations = svgAnnotator.append("g")
+ .attr("class", "annotations")
+ .call(ringNote, annotations);
+
+ // Styling individual annotations based on bound data
+ gAnnotations.selectAll(".annotation circle")
+ .classed("shaded", function(d) { return d.shaded; });
+
+ // Hide or show the controls
+ var draggable = true;
+ d3.select("input")
+ .on("change", function() {
+ ringNote.draggable(draggable = !draggable);
+ gAnnotations
+ .call(ringNote, annotations)
+ .selectAll(".annotation circle")
+ .classed("shaded", function(d) { return d.shaded; });
+ });
+}
// function that executes after data is successfully loaded
function init(data, results_all, fields) {
@@ -88,7 +139,7 @@ function init(data, results_all, fields) {
tsne = new tsnejs.tSNE(opt);
final_dataset = data;
dataFeatures = results_all;
- var temp, object;
+ var object;
for (let k = 0; k < dataFeatures.length; k++){
ArrayContainsDataFeatures.push(Object.values(dataFeatures[k]).concat(k));
object = [];
@@ -99,7 +150,7 @@ function init(data, results_all, fields) {
});
ArrayContainsDataFeaturesCleared.push(object);
}
-
+ $("#datasetDetails").html("Number of Dimensions: " + ArrayContainsDataFeaturesCleared[0].length + ", Number of Samples: " + final_dataset.length);
dists = computeDistances(data, document.getElementById("param-distance").value, document.getElementById("param-transform").value);
tsne.initDataDist(dists);
all_labels = [];
@@ -260,12 +311,11 @@ function updateEmbedding() {
}
return obj;
}
- if (step_counter == 1 || step_counter == max_counter){
+ if (step_counter == max_counter){
ShepardHeatMap();
+ OverviewtSNE(points);
+ BetatSNE(points);
}
- OverviewtSNE(points);
- BetatSNE(points);
- //CosttSNE(points);
}
function ShepardHeatMap () {
@@ -285,12 +335,14 @@ function ShepardHeatMap () {
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
dists2d = computeDistances(points2d, document.getElementById("param-distance").value, document.getElementById("param-transform").value);
+
var dist_list2d = [];
var dist_list = [];
for (var j=0; j1; k--){
+
+ findNearest = 0;
+ var indexOrderSliced = [];
+ var indexOrderSliced2d = [];
+ var count1 = new Array(selectedPoints.length).fill(0);
+ var count2 = new Array(selectedPoints.length).fill(0);
+ counter1 = 0;
+ counter2 = 0;
+
+ for (var i=0; i -1) {
+ indices[i].splice(index, 1);
+ }
+ // sorting the mapped array containing the reduced values
+ indices[i].sort(function(a, b) {
+ if (a[1] > b[1]) {
+ return 1;
+ }
+ if (a[1] < b[1]) {
+ return -1;
+ }
+ return 0;
+ });
+
+ indexOrder[i] = indices[i].map(function(value) { return value[0]; });
+
+ // temporary array holds objects with position and sort-value
+ indices2d[i] = dists2d[i].map(function(el, i) {
+ return [ i, el ];
+ })
+ var index2d = indices2d[i].indexOf(selectedPoints[i].id);
+ if (index2d > -1) {
+ indices2d[i].splice(index2d, 1);
+ }
+
+ // sorting the mapped array containing the reduced values
+ indices2d[i].sort(function(a, b) {
+ if (a[1] > b[1]) {
+ return 1;
+ }
+ if (a[1] < b[1]) {
+ return -1;
+ }
+ return 0;
+ });
+ indexOrder2d[i] = indices2d[i].map(function(value) { return value[0]; });
+ }
+ indexOrderSliced[i] = indexOrder[i].slice(0,k);
+ indexOrderSliced2d[i] = indexOrder2d[i].slice(0,k);
- var barWidth = (svgWidth / findNearestTable.length);
- var knnBarChartSVG = svg.selectAll("rect")
- .data(findNearestTable)
- .enter()
- .append("rect")
- .attr("y", function(d) {
- return Math.round(svgHeight - d)
- })
- .attr("height", function(d) {
+ for (var m=0; m < indexOrderSliced2d[i].length; m++){
+ if (indexOrderSliced[i].includes(indexOrderSliced2d[i][m])){
+ count1[i] = count1[i] + 1;
+ temp[i] = temp[i] + 1;
+ }
+ if(indexOrderSliced[i][m] == indexOrderSliced2d[i][m]){
+ count2[i] = count2[i] + 1;
+ temp2[i] = temp2[i] + 1;
+ }
+ }
+ if (count1[i] != 0){
+ counter1 = (count1[i] / temp[i]) + counter1;
+ }
+ if (count2[i] != 0){
+ counter2 = (count2[i] / temp2[i]) + counter2;
+ }
+
+ }
+
+ sumUnion = counter1 / selectedPoints.length;
+ sumIntersection = counter2 / selectedPoints.length;
+ if (sumUnion == 0){
+ findNearest = 0;
+ } else{
+ findNearest = sumIntersection / sumUnion;
+ }
+
+ if (isNaN(findNearest)){
+ findNearest = 0;
+ }
+ findNearestTable.push(findNearest * vh * 2);
+ }
+ findNearestTable.reverse();
+ var barPadding = 5;
+ d3.select("#knnBarChart").selectAll("rect").remove();
+
+ var svg2 = d3.select('#knnBarChart')
+ .attr("class", "bar-chart");
+
+
+ var barWidth = (vw / findNearestTable.length);
+
+ var knnBarChartSVG = svg2.selectAll("rect")
+ .data(findNearestTable)
+ .enter()
+ .append("rect")
+ .attr("y", function(d) {
+ return Math.round(vh*2 - d)
+ })
+ .attr("height", function(d) {
return d;
- })
- .attr("width", barWidth - barPadding)
- .attr("transform", function (d, i) {
- var translate = [barWidth * i, 0];
- return "translate("+ translate +")";
- });*/
+ })
+ .attr("width", barWidth - barPadding)
+ .attr("transform", function (d, i) {
+ var translate = [barWidth * i, 0];
+ return "translate("+ translate +")";
+ });
+ }
+
+ d3.select("#starPlot").selectAll('g').remove();
+
+ if(selectedPoints.length <= 10){
+
+ var viewport3 = getViewport();
+ var vw3 = viewport3[0] * 0.2;
+
+ var margin = {top: 50, right: 100, bottom: 80, left: 150},
+ width = Math.min(vw3, window.innerWidth - 10) - margin.left - margin.right,
+ height = Math.min(width, window.innerHeight - margin.top - margin.bottom - 20);
+
+ var FeatureWise = [];
+
+ for (var j=0; j px) {
+ // East
+ if (y > py) return "SE";
+ if (y < -py) return "NE";
+ if (x > r) return "E";
+ return null;
+ }
+ else if (x < -px) {
+ // West
+ if (y > py) return "SW";
+ if (y < -py) return "NW";
+ if (x < -r) return "W";
+ return null;
+ }
+ else {
+ // Center
+ if (y > r) return "S";
+ if (y < -r) return "N";
+ }
+ }
+ }
+
+ function dragmoveCenter(d) {
+ var gRingNote = d3v3.select(this.parentNode.parentNode);
+
+ d.cx += d3v3.event.x;
+ d.cy += d3v3.event.y;
+
+ gRingNote
+ .attr("transform", function(d) {
+ return "translate(" + d.cx + "," + d.cy + ")";
+ });
+ }
+
+ function dragmoveRadius(d) {
+ var gRingNote = d3v3.select(this.parentNode.parentNode),
+ gAnnotation = gRingNote.select(".annotation"),
+ circle = gAnnotation.select("circle"),
+ line = gAnnotation.select("path"),
+ text = gAnnotation.select("text"),
+ radius = d3v3.select(this);
+
+ d.r += d3v3.event.dx;
+
+ circle.attr("r", function(d) { return d.r; });
+ radius.attr("cx", function(d) { return d.r; });
+ line.call(updateLine);
+ text.call(updateText);
+ }
+
+ function dragmoveText(d) {
+ var gAnnotation = d3v3.select(this.parentNode),
+ line = gAnnotation.select("path"),
+ text = d3v3.select(this);
+
+ d.textOffset[0] += d3v3.event.dx;
+ d.textOffset[1] += d3v3.event.dy;
+
+ text.call(updateText);
+ line.call(updateLine);
+ }
+
+ function updateLine(selection) {
+ return selection.attr("d", function(d) {
+ var x = d.textOffset[0],
+ y = d.textOffset[1],
+ lineData = getLineData(x, y, d.r);
+ return path(lineData);
+ });
+ }
+
+ function getLineData(x, y, r) {
+ var region = getRegion(x, y, r);
+
+ if (region == null) {
+ // No line if text is inside the circle
+ return [];
+ }
+ else {
+ // Cardinal directions
+ if (region == "N") return [[0, -r], [0, y]];
+ if (region == "E") return [[r, 0], [x, 0]];
+ if (region == "S") return [[0, r], [0, y]];
+ if (region == "W") return [[-r, 0],[x, 0]];
+
+ var d0 = r * Math.cos(Math.PI/4),
+ d1 = Math.min(Math.abs(x), Math.abs(y)) - d0;
+
+ // Intermediate directions
+ if (region == "NE") return [[ d0, -d0], [ d0 + d1, -d0 - d1], [x, y]];
+ if (region == "SE") return [[ d0, d0], [ d0 + d1, d0 + d1], [x, y]];
+ if (region == "SW") return [[-d0, d0], [-d0 - d1, d0 + d1], [x, y]];
+ if (region == "NW") return [[-d0, -d0], [-d0 - d1, -d0 - d1], [x, y]];
+ }
+ }
+
+ function updateText(selection) {
+ return selection.each(function(d) {
+ var x = d.textOffset[0],
+ y = d.textOffset[1],
+ region = getRegion(x, y, d.r),
+ textCoords = getTextCoords(x, y, region);
+
+ d3v3.select(this)
+ .attr("x", textCoords.x)
+ .attr("y", textCoords.y)
+ .text(d.text)
+ .each(function(d) {
+ var x = d.textOffset[0],
+ y = d.textOffset[1],
+ textAnchor = getTextAnchor(x, y, region);
+
+ var dx = textAnchor == "start" ? "0.33em" :
+ textAnchor == "end" ? "-0.33em" : "0";
+
+ var dy = textAnchor !== "middle" ? ".33em" :
+ ["NW", "N", "NE"].indexOf(region) !== -1 ? "-.33em" : "1em";
+
+ var orientation = textAnchor !== "middle" ? undefined :
+ ["NW", "N", "NE"].indexOf(region) !== -1 ? "bottom" : "top";
+
+ d3v3.select(this)
+ .style("text-anchor", textAnchor)
+ .attr("dx", dx)
+ .attr("dy", dy)
+ .call(wrapText, d.textWidth || 960, orientation);
+ });
+ });
+ }
+
+ function getTextCoords(x, y, region) {
+ if (region == "N") return { x: 0, y: y };
+ if (region == "E") return { x: x, y: 0 };
+ if (region == "S") return { x: 0, y: y };
+ if (region == "W") return { x: x, y: 0 };
+ return { x: x, y: y };
+ }
+
+ function getTextAnchor(x, y, region) {
+ if (region == null) {
+ return "middle";
+ }
+ else {
+ // Cardinal directions
+ if (region == "N") return "middle";
+ if (region == "E") return "start";
+ if (region == "S") return "middle";
+ if (region == "W") return "end";
+
+ var xLonger = Math.abs(x) > Math.abs(y);
+
+ // Intermediate directions`
+ if (region == "NE") return xLonger ? "start" : "middle";
+ if (region == "SE") return xLonger ? "start" : "middle";
+ if (region == "SW") return xLonger ? "end" : "middle";
+ if (region == "NW") return xLonger ? "end" : "middle";
+ }
+ }
+
+ // Adapted from: https://bl.ocks.org/mbostock/7555321
+ function wrapText(text, width, orientation) {
+ text.each(function(d) {
+ var text = d3v3.select(this),
+ words = text.text().split(/\s+/).reverse(),
+ word,
+ line = [],
+ lineNumber = 1,
+ lineHeight = 1.1, // ems
+ x = text.attr("x"),
+ dx = text.attr("dx"),
+ tspan = text.text(null).append("tspan").attr("x", x).attr("dx", dx);
+ 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("dx", dx)
+ .attr("dy", lineHeight + "em")
+ .text(word);
+ lineNumber++;
+ }
+ }
+
+ var dy;
+ if (orientation == "bottom") {
+ dy = -lineHeight * (lineNumber-1) - .33;
+ }
+ else if (orientation == "top") {
+ dy = 1;
+ }
+ else {
+ dy = -lineHeight * ((lineNumber-1) / 2) + .33;
+ }
+ text.attr("dy", dy + "em");
+
+ });
+ }
+
+ function styleControl(selection) {
+ selection
+ .attr("r", controlRadius)
+ .style("fill-opacity", "0")
+ .style("stroke", "black")
+ .style("stroke-dasharray", "3, 3")
+ .style("cursor", "move");
+ }
+
+ return draw;
+};
\ No newline at end of file
diff --git a/modules/d3-star/.gitignore b/modules/d3-star/.gitignore
new file mode 100755
index 0000000..a56a7ef
--- /dev/null
+++ b/modules/d3-star/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+
diff --git a/modules/d3-star/d3-starPlot.js b/modules/d3-star/d3-starPlot.js
new file mode 100755
index 0000000..3e6a542
--- /dev/null
+++ b/modules/d3-star/d3-starPlot.js
@@ -0,0 +1,272 @@
+/////////////////////////////////////////////////////////
+/////////////// The Radar Chart Function ////////////////
+/////////////// Written by Nadieh Bremer ////////////////
+////////////////// VisualCinnamon.com ///////////////////
+/////////// Inspired by the code of alangrafu ///////////
+/////////////////////////////////////////////////////////
+
+function RadarChart(id, data, options) {
+ var cfg = {
+ w: 600, //Width of the circle
+ h: 600, //Height of the circle
+ margin: {top: 20, right: 20, bottom: 20, left: 20}, //The margins of the SVG
+ levels: 3, //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.1, //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: d3v3.scale.category10() //Color function
+ };
+
+ //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]; }
+ }//for i
+ }//if
+
+ //If the supplied maxValue is smaller than the actual one, replace by the max in the data
+ var maxValue = Math.max(cfg.maxValue, d3v3.max(data, function(i){return d3v3.max(i.map(function(o){return o.value;}))}));
+
+ var allAxis = (data[0].map(function(i, j){return i.axis})), //Names of each axis
+ total = allAxis.length, //The number of different axes
+ radius = Math.min(cfg.w/2, cfg.h/2), //Radius of the outermost circle
+ Format = d3v3.format('%'), //Percentage formatting
+ angleSlice = Math.PI * 2 / total; //The width in radians of each "slice"
+
+ //Scale for the radius
+ var rScale = d3v3.scale.linear()
+ .range([0, radius])
+ .domain([0, maxValue]);
+
+ /////////////////////////////////////////////////////////
+ //////////// Create the container SVG and g /////////////
+ /////////////////////////////////////////////////////////
+
+ //Remove whatever chart with the same id/class was present before
+ d3v3.select(id).select("svg").remove();
+
+ //Initiate the radar chart SVG
+ var svg = d3v3.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(d3v3.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(d3v3.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", "10px")
+ .attr("fill", "#737373")
+ .text(function(d,i) { return Format(maxValue * d/cfg.levels); });
+
+ /////////////////////////////////////////////////////////
+ //////////////////// Draw the axes //////////////////////
+ /////////////////////////////////////////////////////////
+
+ //Create the straight lines radiating outward from the center
+ var axis = axisGrid.selectAll(".axis")
+ .data(allAxis)
+ .enter()
+ .append("g")
+ .attr("class", "axis");
+ //Append the lines
+ axis.append("line")
+ .attr("x1", 0)
+ .attr("y1", 0)
+ .attr("x2", function(d, i){ return rScale(maxValue*1.1) * Math.cos(angleSlice*i - Math.PI/2); })
+ .attr("y2", function(d, i){ return rScale(maxValue*1.1) * Math.sin(angleSlice*i - Math.PI/2); })
+ .attr("class", "line")
+ .style("stroke", "white")
+ .style("stroke-width", "2px");
+
+ //Append the labels at each axis
+ axis.append("text")
+ .attr("class", "legend")
+ .style("font-size", "11px")
+ .attr("text-anchor", "middle")
+ .attr("dy", "0.35em")
+ .attr("x", function(d, i){ return rScale(maxValue * cfg.labelFactor) * Math.cos(angleSlice*i - Math.PI/2); })
+ .attr("y", function(d, i){ return rScale(maxValue * cfg.labelFactor) * Math.sin(angleSlice*i - Math.PI/2); })
+ .text(function(d){return d})
+ .call(wrap, cfg.wrapWidth);
+
+ /////////////////////////////////////////////////////////
+ ///////////// Draw the radar chart blobs ////////////////
+ /////////////////////////////////////////////////////////
+
+ //The radial line function
+ var radarLine = d3v3.svg.line.radial()
+ .interpolate("linear-closed")
+ .radius(function(d) { return rScale(d.value); })
+ .angle(function(d,i) { return i*angleSlice; });
+
+ if(cfg.roundStrokes) {
+ radarLine.interpolate("cardinal-closed");
+ }
+
+ //Create a wrapper for the blobs
+ var blobWrapper = g.selectAll(".radarWrapper")
+ .data(data)
+ .enter().append("g")
+ .attr("class", "radarWrapper");
+
+ //Append the backgrounds
+ blobWrapper
+ .append("path")
+ .attr("class", "radarArea")
+ .attr("d", function(d,i) { return radarLine(d); })
+ .style("fill", function(d,i) { return cfg.color(i); })
+ .style("fill-opacity", cfg.opacityArea)
+ .on('mouseover', function (d,i){
+ //Dim all blobs
+ d3v3.selectAll(".radarArea")
+ .transition().duration(200)
+ .style("fill-opacity", 0.1);
+ //Bring back the hovered over blob
+ d3v3.select(this)
+ .transition().duration(200)
+ .style("fill-opacity", 0.7);
+ })
+ .on('mouseout', function(){
+ //Bring back all blobs
+ d3v3.selectAll(".radarArea")
+ .transition().duration(200)
+ .style("fill-opacity", cfg.opacityArea);
+ });
+
+ //Create the outlines
+ blobWrapper.append("path")
+ .attr("class", "radarStroke")
+ .attr("d", function(d,i) { return radarLine(d); })
+ .style("stroke-width", cfg.strokeWidth + "px")
+ .style("stroke", function(d,i) { return cfg.color(i); })
+ .style("fill", "none")
+ .style("filter" , "url(#glow)");
+
+ //Append the circles
+ blobWrapper.selectAll(".radarCircle")
+ .data(function(d,i) { return d; })
+ .enter().append("circle")
+ .attr("class", "radarCircle")
+ .attr("r", cfg.dotRadius)
+ .attr("cx", function(d,i){ return rScale(d.value) * Math.cos(angleSlice*i - Math.PI/2); })
+ .attr("cy", function(d,i){ return rScale(d.value) * Math.sin(angleSlice*i - Math.PI/2); })
+ .style("fill", function(d,i,j) { return cfg.color(j); })
+ .style("fill-opacity", 0.8);
+
+ /////////////////////////////////////////////////////////
+ //////// Append invisible circles for tooltip ///////////
+ /////////////////////////////////////////////////////////
+
+ //Wrapper for the invisible circles on top
+ var blobCircleWrapper = g.selectAll(".radarCircleWrapper")
+ .data(data)
+ .enter().append("g")
+ .attr("class", "radarCircleWrapper");
+
+ //Append a set of invisible circles on top for the mouseover pop-up
+ blobCircleWrapper.selectAll(".radarInvisibleCircle")
+ .data(function(d,i) { return d; })
+ .enter().append("circle")
+ .attr("class", "radarInvisibleCircle")
+ .attr("r", cfg.dotRadius*1.5)
+ .attr("cx", function(d,i){ return rScale(d.value) * Math.cos(angleSlice*i - Math.PI/2); })
+ .attr("cy", function(d,i){ return rScale(d.value) * Math.sin(angleSlice*i - Math.PI/2); })
+ .style("fill", "none")
+ .style("pointer-events", "all")
+ .on("mouseover", function(d,i) {
+ newX = parseFloat(d3v3.select(this).attr('cx')) - 10;
+ newY = parseFloat(d3v3.select(this).attr('cy')) - 10;
+
+ tooltip
+ .attr('x', newX)
+ .attr('y', newY)
+ .text(Format(d.value))
+ .transition().duration(200)
+ .style('opacity', 1);
+ })
+ .on("mouseout", function(){
+ tooltip.transition().duration(200)
+ .style("opacity", 0);
+ });
+
+ //Set up the small tooltip for when you hover over a circle
+ var tooltip = g.append("text")
+ .attr("class", "tooltip")
+ .style("opacity", 0);
+
+ /////////////////////////////////////////////////////////
+ /////////////////// Helper Function /////////////////////
+ /////////////////////////////////////////////////////////
+
+ //Taken from http://bl.ocks.org/mbostock/7555321
+ //Wraps SVG text
+ function wrap(text, width) {
+ text.each(function() {
+ var text = d3v3.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
\ No newline at end of file