Skip to content

Instantly share code, notes, and snippets.

@dahtah
Last active October 9, 2022 15:16
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save dahtah/4482115 to your computer and use it in GitHub Desktop.
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.

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);
});
{
"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" ]
}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script src="http://twitter.github.com/bootstrap/assets/js/bootstrap.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script src="http://d3js.org/d3.v2.js"></script>
<script src="code.js"></script>
<style>
.axis,
.frame {
shape-rendering: crispEdges;
}
.axis line {
stroke: #00F;
stroke-width: 2px;
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis path {
stroke: #00F;
stroke-width: 2px;
stroke: black;
fill: none;
}
.axis text {
font-family: sans-serif;
font-size: 10px;
}
.ghost {
fill: blue;
fill-opacity: 0;
}
.label {
font-size: 12px;
color: blue;
}
.datapoint {
r: 5;
fill: blue;
fill-opacity: .5;
}
#d3plot {
width: 500px;
height: 500px;
}
</style>
</head>
<body>
<div id="d3plot" class="d3-scatter-output"> </div>
</body>
</html>
@mskonan
Copy link

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
Copy link

mskonan commented Apr 30, 2013

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment