-
// Generated by CoffeeScript 1.10.0
(function() {
var MARGIN, height, svg, width, zoom, zoomable_layer;
MARGIN = 100;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
zoomable_layer = svg.append('g');
zoom = d3.behavior.zoom().scaleExtent([0.1, 400]).on('zoom', function() {
zoomable_layer.attr({
transform: "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")"
});
return zoomable_layer.selectAll('.semantic_zoom').attr({
transform: "scale(" + (1 / zoom.scale()) + ")"
});
});
svg.call(zoom);
d3.json('//wafi.iit.cnr.it/webvis/tmp/dbpedia/galileo_matrix.json', function(data) {
var enter_indicator_semzooms, enter_indicators, enter_points, enter_semzooms, indicators, keys, link_indicators, links, links_data, m, max_x, max_y, min_x, min_y, points, points_data, x, y;
m = [];
keys = [];
data.forEach(function(e1) {
var row;
keys.push(e1.k1);
row = [];
m.push(row);
return e1.similarities.forEach(function(e2) {
var dist;
dist = 1 - e2.sim;
return row.push(dist);
});
});
points_data = mds_classic(m);
points_data.forEach(function(d) {
d[0] = d[0] + 0.001 * Math.random();
return d[1] = d[1] + 0.001 * Math.random();
});
min_x = d3.min(points_data, function(d) {
return d[0];
});
max_x = d3.max(points_data, function(d) {
return d[0];
});
min_y = d3.min(points_data, function(d) {
return d[1];
});
max_y = d3.max(points_data, function(d) {
return d[1];
});
x = d3.scale.linear().domain([min_x, max_x]).range([MARGIN, width - MARGIN]);
y = d3.scale.linear().domain([min_y, max_y]).range([MARGIN, height - MARGIN]);
links_data = [];
points_data.forEach(function(p1, i1) {
var array;
array = [];
points_data.forEach(function(p2, i2) {
if (i1 !== i2) {
return array.push({
source: p1,
target: p2,
dist: m[i1][i2]
});
}
});
return links_data = links_data.concat(array);
});
links_data.forEach(function(d) {
d.mul = d.dist / Math.sqrt(Math.pow(d.target[1] - d.source[1], 2) + Math.pow(d.target[0] - d.source[0], 2));
d.indicator_x = x(d.source[0]) + d.mul * (x(d.target[0]) - x(d.source[0]));
return d.indicator_y = y(d.source[1]) + d.mul * (y(d.target[1]) - y(d.source[1]));
});
links = zoomable_layer.selectAll('.link').data(links_data);
links.enter().append('line').attr({
"class": 'link',
x1: function(d) {
return x(d.source[0]);
},
y1: function(d) {
return y(d.source[1]);
},
x2: function(d) {
return x(d.target[0]);
},
y2: function(d) {
return y(d.target[1]);
}
});
link_indicators = zoomable_layer.selectAll('.link_indicator').data(links_data.filter(function(d) {
return d.mul > 1;
}));
link_indicators.enter().append('line').attr({
"class": 'link_indicator',
x1: function(d) {
return x(d.target[0]);
},
y1: function(d) {
return y(d.target[1]);
},
x2: function(d) {
return d.indicator_x;
},
y2: function(d) {
return d.indicator_y;
}
});
points = zoomable_layer.selectAll('.point').data(points_data);
enter_points = points.enter().append('g').attr({
"class": 'point',
transform: function(d) {
return "translate(" + (x(d[0])) + "," + (y(d[1])) + ")";
}
});
enter_semzooms = enter_points.append('g').attr({
"class": 'semantic_zoom'
});
enter_semzooms.append('circle').attr({
r: 6,
opacity: 0.3
});
enter_semzooms.append('circle').attr({
r: 4
});
enter_semzooms.append('text').text(function(d, i) {
return keys[i].replace('http://dbpedia.org/resource/', '').replace(/_/g, ' ');
}).attr({
y: 12,
dy: '0.35em'
});
enter_points.append('title').text(function(d, i) {
var name;
name = keys[i].replace('http://dbpedia.org/resource/', '').replace(/_/g, ' ');
return name + ("\n" + d[0] + ", " + d[1]);
});
indicators = zoomable_layer.selectAll('.indicator').data(links_data);
enter_indicators = indicators.enter().append('g').attr({
"class": 'indicator',
transform: function(d) {
return "translate(" + d.indicator_x + " " + d.indicator_y + ")";
}
});
enter_indicator_semzooms = enter_indicators.append('g').attr({
"class": 'semantic_zoom'
});
enter_indicator_semzooms.append('circle').attr({
r: 5
});
return enter_points.on('click', function(d) {
links.classed('visible', function(l) {
return l.source === d;
});
indicators.classed('visible', function(l) {
return l.source === d;
});
return link_indicators.classed('visible', function(l) {
return l.source === d;
});
});
});
}).call(this);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="numeric-1.2.6.min.js"></script>
<script src="mds.js"></script>
<link rel="stylesheet" type="text/css" href="index.css">
<title>Multidimensional Duck</title>
</head>
<body>
<svg width="960px" height="500px"></svg>
<script src="index.js"></script>
</body>
</html>
MARGIN = 100
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
# append a group for zoomable content
zoomable_layer = svg.append('g')
# define a zoom behavior
zoom = d3.behavior.zoom()
.scaleExtent([0.1,400])
.on 'zoom', () ->
# GEOMETRIC ZOOM
zoomable_layer
.attr
transform: "translate(#{zoom.translate()})scale(#{zoom.scale()})"
# SEMANTIC ZOOM
# scale back all objects that have to be semantically zoomed
zoomable_layer.selectAll('.semantic_zoom')
.attr
transform: "scale(#{1/zoom.scale()})"
svg.call(zoom)
d3.json '//wafi.iit.cnr.it/webvis/tmp/dbpedia/galileo_matrix.json', (data) ->
m = []
keys = []
data.forEach (e1) ->
keys.push e1.k1
row = []
m.push row
e1.similarities.forEach (e2) ->
dist = 1-e2.sim
row.push dist
points_data = mds_classic(m)
# jitter to avoid overplotting
# FIXME use collision detection
points_data.forEach (d) ->
d[0] = d[0] + 0.001*Math.random()
d[1] = d[1] + 0.001*Math.random()
min_x = d3.min points_data, (d) -> d[0]
max_x = d3.max points_data, (d) -> d[0]
min_y = d3.min points_data, (d) -> d[1]
max_y = d3.max points_data, (d) -> d[1]
x = d3.scale.linear()
.domain([min_x, max_x])
.range([MARGIN, width-MARGIN])
y = d3.scale.linear()
.domain([min_y, max_y])
.range([MARGIN, height-MARGIN])
links_data = []
points_data.forEach (p1,i1) ->
array = []
points_data.forEach (p2,i2) ->
if i1 isnt i2
array.push {source: p1, target: p2, dist: m[i1][i2]}
links_data = links_data.concat array
links_data.forEach (d) ->
d.mul = d.dist / Math.sqrt( Math.pow(d.target[1]-d.source[1],2) + Math.pow(d.target[0]-d.source[0],2) )
d.indicator_x = x(d.source[0]) + d.mul*(x(d.target[0])-x(d.source[0]))
d.indicator_y = y(d.source[1]) + d.mul*(y(d.target[1])-y(d.source[1]))
# links
links = zoomable_layer.selectAll('.link')
.data(links_data)
links.enter().append('line')
.attr
class: 'link'
x1: (d) -> x(d.source[0])
y1: (d) -> y(d.source[1])
x2: (d) -> x(d.target[0])
y2: (d) -> y(d.target[1])
# link indicators
link_indicators = zoomable_layer.selectAll('.link_indicator')
.data(links_data.filter (d) -> d.mul > 1)
link_indicators.enter().append('line')
.attr
class: 'link_indicator'
x1: (d) -> x(d.target[0])
y1: (d) -> y(d.target[1])
x2: (d) -> d.indicator_x
y2: (d) -> d.indicator_y
# points
points = zoomable_layer.selectAll('.point')
.data(points_data)
enter_points = points.enter().append('g')
.attr
class: 'point'
transform: (d) -> "translate(#{x(d[0])},#{y(d[1])})"
enter_semzooms = enter_points.append('g')
.attr
class: 'semantic_zoom'
enter_semzooms.append('circle')
.attr
r: 6
opacity: 0.3
enter_semzooms.append('circle')
.attr
r: 4
# FIXME use label placement
enter_semzooms.append('text')
.text (d,i)-> keys[i].replace('http://dbpedia.org/resource/', '').replace(/_/g,' ')
.attr
y: 12
dy: '0.35em'
enter_points.append('title')
.text (d,i) ->
name = keys[i].replace('http://dbpedia.org/resource/', '').replace(/_/g,' ')
return name + "\n#{d[0]}, #{d[1]}"
# distance indicators
indicators = zoomable_layer.selectAll('.indicator')
.data(links_data)
enter_indicators = indicators.enter().append('g')
.attr
class: 'indicator'
transform: (d) -> "translate(#{d.indicator_x} #{d.indicator_y})"
enter_indicator_semzooms = enter_indicators.append('g')
.attr
class: 'semantic_zoom'
enter_indicator_semzooms.append('circle')
.attr
r: 5
# interaction
enter_points
.on 'click', (d) ->
links.classed 'visible', (l) -> l.source is d
indicators.classed 'visible', (l) -> l.source is d
link_indicators.classed 'visible', (l) -> l.source is d
svg {
background: #E6DFCC;
}
.point {
fill: brown;
opacity: 0.8;
cursor: pointer;
}
.point:hover {
opacity: 1;
}
.point text {
fill: #333;
text-anchor: middle;
font-family: sans-serif;
font-size: 10px;
}
.link, .indicator, .link_indicator {
stroke: white;
fill: none;
visibility: hidden;
pointer-events: none;
vector-effect: non-scaling-stroke;
}
.link_indicator {
stroke-dasharray: 2 2;
}
.visible.link, .visible.indicator, .visible.link_indicator {
visibility: visible;
}
// given a matrix of distances between some points, returns the
/// point coordinates that best approximate the distances
mds_classic = function(distances, dimensions) {
dimensions = dimensions || 2;
// square distances
var M = numeric.mul(-.5, numeric.pow(distances, 2));
// double centre the rows/columns
function mean(A) { return numeric.div(numeric.add.apply(null, A), A.length); }
var rowMeans = mean(M),
colMeans = mean(numeric.transpose(M)),
totalMean = mean(rowMeans);
for (var i = 0; i < M.length; ++i) {
for (var j =0; j < M[0].length; ++j) {
M[i][j] += totalMean - rowMeans[i] - colMeans[j];
}
}
// take the SVD of the double centred matrix, and return the
// points from it
var ret = numeric.svd(M),
eigenValues = numeric.sqrt(ret.S);
return ret.U.map(function(row) {
return numeric.mul(row, eigenValues).splice(0, dimensions);
});
};