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 @@ 150 + 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