Instantly share code, notes, and snippets.

Last active October 9, 2022 15:16
Star You must be signed in to star a gist
Example of interactive MDS visualisation

The position of the cities are chosen so that the distances between points approximate the road distances in the dataset ("eurodist" in the R package). The location of the points is computed by Multidimensional Scaling (cmdscale in R).

For example, the road distance between Athens and Barcelona is 3313 km. Because road distances for all possible pairs cannot be represented with complete accuracy on a 2D plane, the representation given by MDS can be misleading. In this interactive graph you can hover over a city to visualise the true road distances to its neighbours. If you hover over Gibraltar you'll see that the distances are quite accurate, whereas distances from Athens to most other cities are too large compared to actual road distances.

The idea for the example comes from the documentation for the cmdscale command. The added interactive mechanisms and D3 code are by Simon Barthelme, University of Geneva, Brain and Learning Lab.

This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
 Array.prototype.enorm = function () { return Math.sqrt(this.reduce(function(prev,cur) { return prev + cur*cur; },0)); } dist = function(a,b){ return (a.add(b.mult(-1))).enorm(); } Array.prototype.add = function (b) { var s = Array(this.length); for (var ind = 0; ind < this.length; ind++) { if (typeof(b)=="number") { s[ind] = this[ind]+ b; } else { s[ind] = this[ind]+ b[ind]; } } return s; }; Array.prototype.mult = function (b) { var s = Array(this.length); for (var ind = 0; ind < this.length; ind++) { if (typeof(b)=="number") { s[ind] = this[ind]* b; } else { s[ind] = this[ind]* b[ind]; } } return s; }; Array.prototype.norm = function () { var s = 0; for (var ind = 0; ind < this.length; ind++) { s[ind] = this[ind] + b[ind]; } return s; }; Array.prototype.max = function () { return Math.max.apply(Math, this); }; Array.prototype.min = function () { return Math.min.apply(Math, this); }; Array.prototype.range = function () { return [this.min(), this.max()]; }; function project(a,b,r) { var d = a.add(b.mult(-1)); var rat=r/d.enorm(); return b.add(d.mult(rat)); } function recompute_positions(ind,X,D) { var d = D[ind],Xc = Array(X.length),x = [X[ind].x,X[ind].y]; for (i = 0; i < X.length; i++) { if (i != ind) { var tmp = project([X[i].x,X[i].y],x,d[i]); Xc[i] = {'x': tmp[0],'y': tmp[1]}; } else { Xc[ind] = X[ind]; } } return Xc; } //Used to expand slightly the plotting window expand = function(r) { var d = r[1] - r[0],alpha=.1; return r.add([-alpha*d, alpha*d]); } d3scatterplot = function(svg,X,D,cities) { var nPix= 420,n=X.length,mar = [40,60,40,40]; var xv = X.map(function(e) { return e.x;}),xRange=expand(xv.range()); var yv = X.map(function(e) { return e.y;}),yRange=expand(yv.range()); svg.attr("width", nPix+mar[0]+mar[2]) .attr("height", nPix+mar[1]+mar[3]); var sg = svg.append("g") .attr("transform", "translate(" + mar[0] + "," + mar[1] + ")"); var xScale = d3.scale.linear() .range([0, nPix]) .domain(xRange); var yScale = d3.scale.linear() .range([nPix, 0]) .domain(yRange); var labels = sg.selectAll(".labels") .data(X).enter() .append("text") .attr("class", "label") .attr("x",function(d) {return xScale(d.x);}) .attr("y",function(d) {return yScale(d.y);}) .text(function(d,i) {return cities[i];}) .attr("font-size",10) .attr("id",function(d,i) {return "label" + i}); var dots = sg.selectAll(".datapoint") .data(X).enter() .append("circle") .attr("class", "datapoint") .attr("cx",function(d) {return xScale(d.x);}) .attr("cy",function(d) {return yScale(d.y);}) .attr("id",function(d,i) {return "point" + i}) .attr("r",2); var ghosts = sg.selectAll(".ghost") .data(X).enter() .append("circle") .attr("class", "ghost") .attr("cx",function(d) {return xScale(d.x);}) .attr("cy",function(d) {return yScale(d.y);}) .attr("r",8); console.log(labels); var xAxis = d3.svg.axis().scale(xScale).orient("bottom").ticks(4); svg.append("g").call(xAxis) .attr("class", "axis") //Assign "axis" class .attr("transform","translate(" + mar[0] + "," + (nPix+mar[1]) + ")"); var yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(4); svg.append("g").call(yAxis) .attr("class", "axis") //Assign "axis" class .attr("transform","translate(" + mar[0] + "," + (mar[3]) + ")"); ghosts.on("mouseover",function(d,i) { var Xn = recompute_positions(i,X,D); labels.data(Xn) .transition() .attr("x",function(d) {return xScale(d.x);}) .attr("y",function(d) {return yScale(d.y);}); }); reset = function(el) { labels.data(X) .attr("x",function(d) {return xScale(d.x);}) .attr("y",function(d) {return yScale(d.y);}); } ghosts.on("mouseout",reset); svg.on("mouseout",reset); //Reset also if the mouse leaves the frame, the capture of "mouseout" events being rather unreliable } d3.json("data.js", function(data) { var svg = d3.select("#d3plot").append("svg") .attr("width","100%") .attr("height","100%"); d3scatterplot(svg,data.X,data.D,data.cities); });
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
 { "X": [ { "x": 2290.3, "y": -1798.8 }, { "x": -825.38, "y": -546.81 }, { "x": 59.183, "y": 367.08 }, { "x": -82.846, "y": 429.91 }, { "x": -352.5, "y": 290.91 }, { "x": 293.69, "y": 405.31 }, { "x": 681.93, "y": 1108.6 }, { "x": -9.4234, "y": -240.41 }, { "x": -2048.4, "y": -642.46 }, { "x": 561.11, "y": 773.37 }, { "x": 164.92, "y": 549.37 }, { "x": -1935, "y": -49.125 }, { "x": -226.42, "y": -187.09 }, { "x": -1423.4, "y": -305.88 }, { "x": -299.5, "y": -388.81 }, { "x": 260.88, "y": -416.67 }, { "x": 587.68, "y": -81.182 }, { "x": -156.84, "y": 211.14 }, { "x": 709.41, "y": -1109.4 }, { "x": 839.45, "y": 1836.8 }, { "x": 911.23, "y": -205.93 } ], "D": [ [ 0, 3313, 2963, 3175, 3339, 2762, 3276, 2610, 4485, 2977, 3030, 4532, 2753, 3949, 2865, 2282, 2179, 3000, 817, 3927, 1991 ], [ 3313, 0, 1318, 1326, 1294, 1498, 2218, 803, 1172, 2018, 1490, 1305, 645, 636, 521, 1014, 1365, 1033, 1460, 2868, 1802 ], [ 2963, 1318, 0, 204, 583, 206, 966, 677, 2256, 597, 172, 2084, 690, 1558, 1011, 925, 747, 285, 1511, 1616, 1175 ], [ 3175, 1326, 204, 0, 460, 409, 1136, 747, 2224, 714, 330, 2052, 739, 1550, 1059, 1077, 977, 280, 1662, 1786, 1381 ], [ 3339, 1294, 583, 460, 0, 785, 1545, 853, 2047, 1115, 731, 1827, 789, 1347, 1101, 1209, 1160, 340, 1794, 2196, 1588 ], [ 2762, 1498, 206, 409, 785, 0, 760, 1662, 2436, 460, 269, 2290, 714, 1764, 1035, 911, 583, 465, 1497, 1403, 937 ], [ 3276, 2218, 966, 1136, 1545, 760, 0, 1418, 3196, 460, 269, 2971, 1458, 2498, 1778, 1537, 1104, 1176, 2050, 650, 1455 ], [ 2610, 803, 677, 747, 853, 1662, 1418, 0, 1975, 1118, 895, 1936, 158, 1439, 425, 328, 591, 513, 995, 2068, 1019 ], [ 4485, 1172, 2256, 2224, 2047, 2436, 3196, 1975, 0, 2897, 2428, 676, 1817, 698, 1693, 2185, 2565, 1971, 2631, 3886, 2974 ], [ 2977, 2018, 597, 714, 1115, 460, 460, 1118, 2897, 0, 550, 2671, 1159, 2198, 1479, 1238, 805, 877, 1751, 949, 1155 ], [ 3030, 1490, 172, 330, 731, 269, 269, 895, 2428, 550, 0, 2280, 863, 1730, 1183, 1098, 851, 457, 1683, 1500, 1205 ], [ 4532, 1305, 2084, 2052, 1827, 2290, 2971, 1936, 676, 2671, 2280, 0, 1178, 668, 1762, 2250, 2507, 1799, 2700, 3231, 2937 ], [ 2753, 645, 690, 739, 789, 714, 1458, 158, 1817, 1159, 863, 1178, 0, 1281, 320, 328, 724, 471, 1048, 2108, 1157 ], [ 3949, 636, 1558, 1550, 1347, 1764, 2498, 1439, 698, 2198, 1730, 668, 1281, 0, 1157, 1724, 2010, 1273, 2097, 3188, 2409 ], [ 2865, 521, 1011, 1059, 1101, 1035, 1778, 425, 1693, 1479, 1183, 1762, 320, 1157, 0, 618, 1109, 792, 1011, 2428, 1363 ], [ 2282, 1014, 925, 1077, 1209, 911, 1537, 328, 2185, 1238, 1098, 2250, 328, 1724, 618, 0, 331, 856, 586, 2187, 898 ], [ 2179, 1365, 747, 977, 1160, 583, 1104, 591, 2565, 805, 851, 2507, 724, 2010, 1109, 331, 0, 821, 946, 1754, 428 ], [ 3000, 1033, 285, 280, 340, 465, 1176, 513, 1971, 877, 457, 1799, 471, 1273, 792, 856, 821, 0, 1476, 1827, 1249 ], [ 817, 1460, 1511, 1662, 1794, 1497, 2050, 995, 2631, 1751, 1683, 2700, 1048, 2097, 1011, 586, 946, 1476, 0, 2707, 1209 ], [ 3927, 2868, 1616, 1786, 2196, 1403, 650, 2068, 3886, 949, 1500, 3231, 2108, 3188, 2428, 2187, 1754, 1827, 2707, 0, 2105 ], [ 1991, 1802, 1175, 1381, 1588, 937, 1455, 1019, 2974, 1155, 1205, 2937, 1157, 2409, 1363, 898, 428, 1249, 1209, 2105, 0 ] ], "cities": [ "Athens", "Barcelona", "Brussels", "Calais", "Cherbourg", "Cologne", "Copenhagen", "Geneva", "Gibraltar", "Hamburg", "Hook of Holland", "Lisbon", "Lyons", "Madrid", "Marseilles", "Milan", "Munich", "Paris", "Rome", "Stockholm", "Vienna" ] }
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters


### mskonan commented Apr 30, 2013

Hi, It's really nice.
I'd like to reproduce and modify it.
But when I have 3 files (index.html, code.js and data.js) in a same directory and open index.html, it'd show a blank page. I'm pretty new to html and Javascript. What am I missing?
Any help would be appreciated.
Best,

Kyle

### mskonan commented Apr 30, 2013

Never mind. It worked in Firefox, not in Chrome or IE. Thanks.