index.html
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono" rel="stylesheet">
<style>
html, body {
font-family: 'Ubuntu Mono', monospace;
}
.county {
fill: #ebeae0;
stroke: #cdcbb1;
}
.road {
fill: none;
stroke: #b566ff;
}
.road.motorway {
stroke-width: 1px;
}
.road.trunk,
.road.motorway_link,
.road.primary {
stroke-width: 0.5px;
}
.water {
fill: #e3e3ff;
stroke: #d7d7ff;
}
.label line {
stroke: #333;
stroke-width: 1px;
}
.label text {
text-shadow: -1px -1px 1px #fff,
-1px 0px 1px #fff,
-1px 1px 1px #fff,
0px -1px 1px #fff,
0px 1px 1px #fff,
1px -1px 1px #fff,
1px 0px 1px #fff,
1px 1px 1px #fff;
}
.label.extra-large {
font-size: 18px;
}
.label.large {
font-size: 15px;
}
.label.medium {
font-size: 12px;
}
.label.small {
font-size: 10px;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/topojson@3"></script>
<script>
var width = 960,
height = 960;
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) + ',' + (-100) + ')')
var projection = d3.geoMercator();
var isoprojection = isometricProjection();
var path = isometricPath()
.projection(projection)
.isoprojection(isoprojection);
d3.json('topo.json', function(error, topo) {
if (error) throw error;
var countyData = topojson.feature(topo, topo.objects.counties),
roadData = topojson.feature(topo, topo.objects.roads),
waterData = topojson.feature(topo, topo.objects.water);
projection.fitExtent([[40, 40], [width - 40, height - 40]], countyData);
var water = svg.append('g').attr('class', 'water')
.selectAll('.water').data(waterData.features)
.enter().append('path')
.attr('class', 'water')
.attr('d', path.height(-20));
var county = svg.append('g').attr('class', 'counties')
.selectAll('.county').data(countyData.features)
.enter().append('path')
.attr('class', 'county')
.attr('d', path.height(0));
var road = svg.append('g').attr('class', 'roads')
.selectAll('.road').data(roadData.features)
.enter().append('path')
.attr('class', function(d) { return 'road ' + d.properties.fclass; })
.attr('d', path.height(20));
var label = svg.append('g').attr('class', 'labels')
.selectAll('label').data(labelData())
.enter().append('g')
.attr('class', function(d) { return 'label ' + d.className; });
label.append('line');
label.append('text')
.style('text-anchor', 'middle')
.attr('dy', '-0.33em')
.text(function(d) { return d.label; });
label
.each(function(d) {
var p = projection(d.location),
l0 = isoprojection([p[0], -p[1], 20]),
l1 = isoprojection([p[0], -p[1], 20 + d.height]);
d3.select(this).select('line')
.attr('x1', l0[0])
.attr('y1', l0[1])
.attr('x2', l1[0])
.attr('y2', l1[1]);
d3.select(this).select('text')
.attr('x', l1[0])
.attr('y', l1[1]);
});
function update() {
water.attr('d', path.height(-20));
county.attr('d', path.height(0));
road.attr('d', path.height(20));
label
.each(function(d) {
var p = projection(d.location),
l0 = isoprojection([p[0], -p[1], 20]),
l1 = isoprojection([p[0], -p[1], 20 + d.height]);
d3.select(this).select('line')
.attr('x1', l0[0])
.attr('y1', l0[1])
.attr('x2', l1[0])
.attr('y2', l1[1]);
d3.select(this).select('text')
.attr('x', l1[0])
.attr('y', l1[1]);
});
}
d3.interval(function(elapsed) {
var t = elapsed / 15000 - Math.PI / 4,
pitch = ((Math.sin(t) + 1) / 2) * (Math.PI / 12) + (Math.PI / 6);
yaw = ((Math.sin(t) + 1) / 2) * (Math.PI / 12) - (Math.PI / 4);
isoprojection
.pitch(pitch)
.yaw(yaw);
update();
}, 33);
});
function isometricProjection() {
var sin = Math.sin,
cos = Math.cos,
asin = Math.asin,
tan = Math.atan,
PI = Math.PI;
var pitch = PI / 6,
yaw = PI / 4,
alpha = asin(tan(pitch)),
beta = yaw;
function project(point) {
var ax = point[1],
ay = -point[2],
az = point[0];
var x = cos(beta) * ax - sin(beta) * az,
y = cos(alpha) * ay + sin(alpha) *
(sin(beta) * ax + cos(beta) * az);
return [x, y];
}
project.pitch = function(x) {
if (!arguments.length) return alpha;
pitch = x;
alpha = Math.asin(Math.tan(pitch));
return project;
};
project.yaw = function(x) {
if (!arguments.length) return beta;
yaw = x;
beta = yaw;
return project;
};
return project;
}
function isometricPath() {
var projection,
isoprojection,
height = 0;
function path(feature) {
if (feature.geometry) {
return {
Polygon: polygon,
MultiPolygon: multipolygon,
LineString: linestring,
}[feature.geometry.type](feature.geometry.coordinates);
} else {
return null;
}
}
path.isoprojection = function(x) {
if (!arguments.length) return isoprojection;
isoprojection = x;
return path;
};
path.projection = function(x) {
if (!arguments.length) return projection;
projection = x;
return path;
};
path.height = function(x) {
if (!arguments.length) return height;
height = x;
return path;
};
function project(point) {
var p = projection(point),
d = [p[0], -p[1], height];
return isoprojection(d);
}
function multipolygon(coordinates) {
return 'M' + coordinates.map(function(multipolygon) {
var outerPolygon = multipolygon[0],
innerPolygons = multipolygon.slice(1);
var pathString = outerPolygon.map(function(point) {
return project(point).join(',');
}).join('L');
if (innerPolygons.length > 0) {
pathString += innerPolygons.map(function(polygon) {
return 'M' + polygon.map(function(point) {
return project(point).join(',');
}).join('L');
});
};
return pathString;
}).join('M');
}
function polygon(coordinates) {
var outerPolygon = coordinates[0],
innerPolygons = coordinates.slice(1);
var pathString = 'M' + outerPolygon.map(function(point) {
return project(point).join(',');
}).join('L');
if (innerPolygons.length > 0) {
pathString += innerPolygons.map(function(polygon) {
return 'M' + polygon.map(function(point) {
return project(point).join(',');
}).join('L');
});
};
return pathString;
}
function linestring(coordinates) {
return 'M' + coordinates.map(function(point) {
return project(point).join(',');
});
}
return path;
}
function radiansToDegrees(radians) { return radians * 180 / Math.PI; }
function degreesToRadians(degrees) { return degrees * Math.PI / 180; }
function labelData() {
return [
{
label: 'Milwaukee',
location: [-87.909665, 43.041331],
height: 50,
className: 'extra-large'
},
{
label: 'Wauwautosa',
location: [-88.010879, 43.050316],
height: 20,
className: 'medium'
},
{
label: 'West Allis',
location: [-88.007216, 43.016242],
height: 20,
className: 'medium'
},
{
label: 'Waukesha',
location: [-88.233588, 43.013146],
height: 40,
className: 'large'
},
{
label: 'Brookfield',
location: [-88.106788, 43.061748],
height: 20,
className: 'small'
},
{
label: 'Oak Creek',
location: [-87.864665, 42.884469],
height: 20,
className: 'medium'
},
{
label: 'Cedarburg',
location: [-87.989352, 43.296658],
height: 20,
className: 'small'
},
{
label: 'Menomonee Falls',
location: [-88.106047, 43.184513],
height: 20,
className: 'small'
},
{
label: 'Muskego',
location: [-88.141417, 42.904828],
height: 20,
className: 'small'
}
];
}
</script>
</body>
</html>