Please, open in a new window to see the full map.
This is the first of a serie of experiments with crossfilter.js and the power of d3.js for building massive data interactive maps. I called it crossmap.
This map is fed with this dataset from infochimps' datasets. This dataset contains a list of wikipedia articles, tagged and geolocated. My particular ETL process has extracted 5456 articles.
The map provides a brush component for select a region, and a list of grouped tags related to selection. This list is updated interactively as the brush selection changes. If you click in any tag from list, the related points in the map are remarked. If you moves the mouse over selecteds points you will see a link to related wikipedia article.
In a future experiment I will add zoom capability. I will experiment too with the performance charging the whole dataset (over 200K entries) and the possible workarounds to have a fluent user experience.
xxxxxxxxxx
<html>
<head>
<title>Wikipedia crossfilter</title>
<style type="text/css">
.row {
display: table-row;
}
.cell {
display: table-cell;
}
.map {
width: 960px;
}
.map svg {
background-color: white;
}
svg, g, rect {
pointer-events: all;
}
.tags {
width: 150px;
padding: 25px;
overflow: auto;
}
.keyword {
cursor: pointer;
}
.keyword:hover {
text-decoration: underline;
}
.land {
opacity: .7;
stroke: #888;
}
.point {
fill: yellow;
opacity: .3;
}
.point:hover {
fill: blue;
stroke: #000;
}
.selected {
fill: #f00;
opacity: .9;
cursor: pointer;
}
.brush, .brush .resize {
fill: green;
stroke: green;
pointer-events: all;
}
.brush .extent {
fill-opacity: .125;
shape-rendering: crispEdges;
}
.background {
fill: none;
pointer-events: all;
}
.tooltip {
position: absolute;
min-width: 100px;
padding: 11px 10px;
background-color: #ECEEDD;
opacity: 0;
text-align: center;
border: 1px solid gray;
}
</style>
</head>
<body>
<script src="crossfilter.v1.min.js"></script>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script type="text/javascript">
var wiki, keyword, path, lat, lon, projection, svg, tags, x, brush, brushSvg, points, extent,
tooltip, handleHeight = 50;
// load data
d3.csv('wikipedia.csv', function(error, wikipedia) {
if (error) throw error;
wikipedia.forEach(function(d, i) {
d.lat = parseFloat(d.lat);
d.lon = parseFloat(d.lon);
d.keywords = d.keywords.split(";");
d.url = d.url.replace("'","").replace("'","");
});
wikipedia = wikipedia.filter(function(el) {
return el.hasOwnProperty("lat") && el.hasOwnProperty("lat") &&
el.lat !== undefined && el.lon !== undefined &&
el.lat !== null && el.lon !== null &&
!isNaN(el.lat) && !isNaN(el.lon);
});
wiki = crossfilter(wikipedia);
keyword = wiki.dimension(function(d) { return d.keywords; });
var reduceAdd = function(p, v) {
v.keywords.forEach(function(val, idx) {
p[val] = (p[val] || 0 ) + 1;
});
return p;
}
var reduceRemove = function(p ,v) {
v.keywords.forEach(function(val, idx) {
p[val] = (p[val] || 0 ) - 1;
});
return p;
}
var reduceInitial = function() {
return {};
}
var keywords = function() {
return keyword.groupAll().reduce(reduceAdd, reduceRemove, reduceInitial).value();
}
lat = wiki.dimension(function(d) { return d.lat; });
lon = wiki.dimension(function(d) { return d.lon; });
// build world
var width = 960,
height = 600,
tagsWidth = 200,
color = d3.scale.category20b();
projection = d3.geo.mercator()
.scale((width + 1) / 2 / Math.PI)
.translate([width / 2, height / 2])
.precision(.1);
path = d3.geo.path()
.projection(projection);
svg = d3.select('.map').append("svg")
.attr("width", width)
.attr("height", height);
tags = d3.select(".tags").append("svg")
.attr("width", tagsWidth)
.attr("height", height)
.append("g")
.attr("transform", "translate(8,6)");
x = d3.scale.linear().range([0, width]);
tooltip = d3.select("body")
.append("div")
.attr("class","tooltip");
// filter keywords by latitude
var updateKeywords = function() {
var extent = brush.extent(),
coordStart = x(extent[0]),
coordEnd = x(extent[1]),
start = projection.invert([coordStart, null]),
end = projection.invert([coordEnd, null]),
coords = [start[0], end[0]];
lon.filterRange(coords);
drawKeywords();
}
var buildBrush = function() {
brush = d3.svg.brush()
.x(x)
.on("brushstart", startbrush)
.on("brush", updateKeywords)
.on("brushend", brushend);
brushSvg = svg.append("g")
.attr("class", "brush")
.call(brush)
.selectAll('rect')
.attr("height", height);
// set initial parameters for brush handlers
d3.selectAll(".resize")
.select("rect")
.attr("visibility", "visible")
.attr("height", handleHeight)
.attr("fill", "#888")
.attr("transform","translate(0," + (height/2 - handleHeight/2) + ")")
.style("visibility", "visible");
// vertical animation of brush handlers
d3.select('.brush').on("mousemove", function() {
var _y = d3.mouse(this)[1];
d3.selectAll(".resize")
.select("rect")
.attr("transform","translate(0," + (_y - handleHeight/2) + ")")
});
}
var startbrush = function() {
svg.selectAll('.point')
.classed("selected", false)
.style("opacity", .3);
}
var brushend = function() {
if (brush.empty()) {
setBrushDefaults();
}
}
var setBrushDefaults = function() {
brush.extent([.465625,.52]);
d3.selectAll('.brush')
.call(brush);
updateKeywords();
}
// load world data
d3.json('/../../data/world-50m.json', function(error, world) {
if (error) throw error;
var countries = topojson.feature(world, world.objects.countries);
svg.selectAll(".land")
.data(countries.features)
.enter()
.append("path")
.attr("class", "land")
.attr("d", path)
.style("fill", function(d, i) { return color(i % 10); });
buildBrush();
drawMarkers();
setBrushDefaults();
});
// draw all points from dataset
var drawMarkers = function() {
d3.selectAll('.point').remove();
var data = lat.filterAll().top(Infinity);
points = svg.selectAll('.point')
.data(data)
.enter()
.append("svg:circle")
.attr("cx", function(d){ return projection([d.lon,d.lat])[0]; })
.attr("cy", function(d) { return projection([d.lon,d.lat])[1]; })
.attr("r", 3)
.attr("class", "point");
points.data(data).exit().remove();
}
// draw keywords filtered by longitude range selected by brush
var drawKeywords = function() {
var f = [], _keywords = keywords();
// transform data
for (var i in _keywords) {
f.push({ key: i, value: _keywords[i] });
}
var sort = crossfilter.quicksort.by(function(d) {
return d.value;
});
sort(f, 0, f.length);
f.reverse();
// refresh keywords list
d3.selectAll(".keyword").remove();
var h = 20;
tags.selectAll(".keyword")
.data(f)
.enter()
.append("text")
.attr("class", "keyword")
.attr("transform", function(d,i) { return "translate(0," + (h * i) + ")" })
.attr("dy", ".35em")
.text(function(d) { return d.key + " (" + d.value + ")"; })
.on('click', function(item, ev) {
//console.log(item, ev);
keyword.filterExact(item.key);
selectMarkers();
});
}
// mark as selected the filtered markers on selecting a keyword
var selectMarkers = function() {
svg.selectAll('.point')
.classed("selected", false)
.style("opacity", .3)
.attr("r", 3);
var markers = keyword.top(Infinity);
points
.data(markers, function(d){ return [d.lon, d.lat]; })
.classed("selected", true)
.attr("r", 7)
.on("mouseover", function(d) {
tooltip
.style("left", d3.event.pageX)
.style("top", d3.event.pageY)
.html('<a href="' + d.url + '" target="_blank">' + d.url + '</a>');
tooltip.transition()
.duration(200)
.style("opacity", 1);
})
.on("mouseout", function() {
tooltip.transition()
.duration(2000)
.style("opacity", 0);
});
d3.selectAll(".point:not(.selected)")
.style("opacity", .03);
}
});
</script>
<div class="row">
<div class="cell map"></div>
<div class="cell tags"></div>
</div>
</body>
</html>
Modified http://d3js.org/d3.v3.min.js to a secure url
Modified http://d3js.org/topojson.v1.min.js to a secure url
Changed /mbostock/raw/4090846/world-50m.json to a local referenece
https://d3js.org/d3.v3.min.js
https://d3js.org/topojson.v1.min.js