- Planetary grid by William Becker and Beth Hagens
- Places mostly based on Simon E. Davies design
- Location data based on this map
- Controls: Left/Right and Esc
Canvas version: Planetary Grid Browser II
Canvas version: Planetary Grid Browser II
function Config() { | |
'use strict'; | |
return { | |
shift: 31.2, | |
lineWidth: 1, | |
symbolLine: 4, | |
dash: [2, 3], | |
fontSize: 16, | |
lineMid: 11, | |
titleSize: 18, | |
lineHeight: 19, | |
durationScale: 1.2, | |
durationSpeed: 1.5, | |
durationMin: 750, | |
durationMax: 2000, | |
frameLineWidth: 3, | |
mraf: !window.chrome, | |
filter: { | |
map: {}, | |
graticule: {off: true}, | |
'cool-line': {}, | |
'hot-line': {}, | |
'balanced-line': {}, | |
'cool-point': {off: true}, | |
'hot-point': {off: true}, | |
'balanced-point': {off: true}, | |
megalith: {}, | |
mound: {}, | |
pyramid: {}, | |
temple: {}, | |
volcano: {}, | |
place: {} | |
}, | |
colors: { | |
water: '#def4ff', | |
graticule: '#999', | |
land: '#ffffff', | |
border: 'rgba(0,0,0,0.5)', | |
cool: '#1f78b4', | |
hot: '#e31a1c', | |
balanced: '#333', | |
frame: '#333', | |
focus: 'rgba(0,0,0,0.87)', | |
selection: 'rgba(255,255,255,0.58)', | |
shape: '#333', | |
bg: '#fff' | |
}, | |
shapes: { | |
megalith: '#299ae6', | |
mound: '#90de43', | |
pyramid: '#ffff4d', | |
temple: '#ff7f00', | |
volcano: '#e31a1c', | |
place: '#ccc' | |
}, | |
sizes: { | |
megalith: 80, | |
mound: 66, | |
pyramid: 66, | |
temple: 80, | |
volcano: 48, | |
place: 48, | |
'hot-line': 80, | |
'cool-line': 80, | |
'balanced-line': 80, | |
'cool-point': 80, | |
'hot-point': 80, | |
'balanced-point': 80, | |
'map': 100 | |
}, | |
symbols: { | |
megalith: 'square', | |
mound: 'triangle-up', | |
pyramid: 'triangle-up', | |
temple: 'cross', | |
volcano: 'circle', | |
place: 'circle' | |
} | |
}; | |
} |
function SvgGlobe(root, width, height, cfg) { | |
'use strict'; | |
var maxScale = 3; | |
var self; | |
var round = d3.geo.transform({ | |
point: function (x, y) { | |
this.stream.point(~~x, ~~y); | |
} | |
}); | |
var projectionGlobe = d3.geo | |
.orthographic() | |
.clipAngle(90) | |
.precision(0) | |
.translate([height / 2, height / 2]) | |
.scale(height / 2); | |
var projectionRaw = d3.geo | |
.orthographic() | |
.precision(2) | |
.clipAngle(90) | |
.translate([height / 2, height / 2]) | |
.scale(height / 2); | |
var projectionRawZero = d3.geo | |
.orthographic() | |
.precision(0) | |
.clipAngle(90) | |
.translate([height / 2, height / 2]) | |
.scale(height / 2); | |
var projectionRawZeroRound = { | |
stream: function (s) { | |
return projectionRawZero.stream(round.stream(s)); | |
} | |
}; | |
var projectionGlobeCalc = d3.geo | |
.orthographic() | |
.clipAngle(90) | |
.translate([height / 2, height / 2]) | |
.scale(height / 2); | |
var projectionLinesGlobe = d3.geo | |
.orthographic() | |
.rotate([cfg.shift, 0, 0]) | |
.precision(10) | |
.clipAngle(90) | |
.translate([height / 2, height / 2]) | |
.scale(height / 2); | |
var pathRaw = d3.geo.path() | |
.projection(projectionRaw); | |
var pathRawZero = d3.geo.path() | |
.projection(projectionRawZero); | |
var pathRawZeroRound = d3.geo.path() | |
.projection(projectionRawZeroRound); | |
var pathLinesGlobe = d3.geo.path() | |
.projection(projectionLinesGlobe); | |
var zoom = d3.behavior.zoom().scaleExtent([1, maxScale]) | |
.on('zoomstart', zoomed) | |
.on('zoom', zoomed) | |
.on('zoomend', zoomed); | |
root.append('circle') | |
.attr('class', 'overlay-white') | |
.attr('cx', width / 2) | |
.attr('cy', height / 2) | |
.attr('r', height / 2); | |
root.append('circle') | |
.attr('class', 'map water') | |
.attr('cx', width / 2) | |
.attr('cy', height / 2) | |
.attr('r', height / 2); | |
var graticuleGlobe = root.append('path').datum(d3.geo.graticule()()) | |
.attr('class', 'graticule') | |
.style('display', 'none') | |
.attr('d', pathRaw); | |
var landGlobe = root.append('path').attr('class', 'map land country'); | |
var lineGlobeG = root.append('g').attr('stroke-width', 1); | |
root.call(zoom); | |
root.append('circle') | |
.attr('class', 'border') | |
.style('stroke-width', cfg.frameLineWidth) | |
.attr('cx', width / 2) | |
.attr('cy', height / 2) | |
.attr('r', height / 2); | |
var g = root.append('g'); | |
var h = d3.geo.hexakisIcosahedron; | |
Utils.svgLines(lineGlobeG, pathLinesGlobe, h.icosahedronEdges(), 'cool-line', 'Cool line'); | |
Utils.svgLines(lineGlobeG, pathLinesGlobe, h.hexakisCenterEdges(), 'hot-line', 'Hot line'); | |
Utils.svgLines(lineGlobeG, pathLinesGlobe, h.hexakisSideEdges(), 'balanced-line', 'Balanced line'); | |
var linesSelection = root.selectAll('.line'); | |
var highlight = root.append('circle') | |
.style('display', 'none') | |
.attr('class', 'border land') | |
.attr('cx', width / 2) | |
.attr('cy', height / 2) | |
.attr('r', 5); | |
var a, pG, pGL; | |
function zoomed() { | |
var m = d3.mouse(this); | |
if (d3.event && d3.event.sourceEvent) { | |
d3.event.sourceEvent.stopPropagation(); | |
d3.event.sourceEvent.preventDefault(); | |
} | |
({ | |
zoomstart: function () { | |
pG = projectionGlobe.rotate(); | |
pGL = projectionLinesGlobe.rotate(); | |
projectionGlobeCalc.rotate(pG); | |
a = projectionGlobeCalc.invert(m); | |
}, | |
zoom: function () { | |
var b = projectionGlobeCalc.invert(m); | |
var pgR = [pG[0] + b[0] - a[0], pG[1] + b[1] - a[1]]; | |
var plgR = [pGL[0] + b[0] - a[0], pGL[1] + b[1] - a[1]]; | |
if (self.canZoom && !self.canZoom(pgR)) { | |
return; | |
} | |
if (!isNaN(b[0]) && !isNaN(b[1])) { | |
projectionRaw.rotate(pgR); | |
projectionRawZero.rotate(pgR); | |
projectionGlobe.rotate(pgR); | |
projectionLinesGlobe.rotate(plgR); | |
update(); | |
if (self.onZoomed) { | |
var s = zoom.scale(); | |
self.onZoomed(null, s, pgR); | |
} | |
} | |
}, | |
zoomend: function () { | |
} | |
})[d3.event.type](); | |
} | |
function updateSelection() { | |
if (self.selection) { | |
var coo = self.selection.geometry ? self.selection : { | |
'type': 'Feature', | |
'geometry': {'type': 'Point', 'coordinates': [self.selection[0] + cfg.shift, self.selection[1], 0]} | |
}; | |
var p = pathRaw.centroid(coo); | |
if (!isNaN(p[0]) && !isNaN(p[1])) { | |
p[0] -= width / 2; | |
p[1] -= height / 2; | |
highlight.style('display', null); | |
highlight.attr('transform', 'translate(' + p + ')'); | |
} else { | |
highlight.style('display', 'none'); | |
} | |
} else { | |
highlight.style('display', 'none'); | |
} | |
} | |
function update(running) { | |
projectionLinesGlobe.precision(running ? 2 : 1); | |
if (!cfg.filter.map.off) { | |
landGlobe.attr('d', pathRawZeroRound); | |
} | |
if (!cfg.filter.graticule.off) { | |
graticuleGlobe.attr('d', pathRaw); | |
} | |
linesSelection.attr('d', pathLinesGlobe); | |
updateSelection(); | |
} | |
function setZoom(t, s, i, running) { | |
zoom.scale(s); | |
projectionLinesGlobe.rotate([i[0] + cfg.shift, i[1]]); | |
projectionGlobe.rotate(i); | |
projectionRaw.rotate(i); | |
projectionRawZero.rotate(i); | |
update(running) | |
} | |
self = { | |
root: root, | |
land: landGlobe, | |
pathRaw: pathRaw, | |
setZoom: setZoom, | |
updateSelection: updateSelection, | |
update: update | |
}; | |
return self; | |
} |
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<title>Planetary Grid I</title> | |
<link rel="stylesheet" href="style.css"> | |
<body> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script> | |
<script src="/darosh/raw/2fe464efd794bde5ed68/hexakis-icosahedron.js"></script> | |
<script src="/darosh/raw/2d12a584a14910032ab8/togeojson.js"></script> | |
<script src="config.js"></script> | |
<script src="utils.js"></script> | |
<script src="globe.js"></script> | |
<script src="map.js"></script> | |
<script src="legend.js"></script> | |
<script src="list.js"></script> | |
<script src="info.js"></script> | |
<script> | |
(function () { | |
'use strict'; | |
var widthList = 220; | |
var margin = 12; | |
var widthScrollBar = self.frameElement ? 0 : 20; | |
var widthScreen = Math.max(document.body.clientWidth || 0, 960) - widthScrollBar; | |
widthScreen = Math.min(1480, widthScreen); | |
var heightScreen = Math.max(500, widthScreen / (960 / 480)); | |
margin = widthScreen > 960 ? margin * 2 : margin; | |
var widthGlobe = 200; | |
var heightGlobe = 200; | |
if (heightScreen > 660) { | |
widthGlobe = 280; | |
heightGlobe = 280; | |
} | |
var widthLegend = 140; | |
var widthMap = widthScreen - Math.max(widthLegend, widthGlobe / 2) - 2 * margin; | |
var heightMap = heightScreen - heightGlobe / 2 - 2 * margin; | |
var selected; | |
var cfg = new Config(); | |
cfg.url = 'http://bl.ocks.org/darosh/14e2e4e14898f13e13c7'; | |
var svg = d3.select('body').append('svg') | |
.attr('width', widthMap + Math.max(widthLegend, widthGlobe / 2) + 2 * margin) | |
.attr('height', heightMap + heightGlobe / 2 + 2 * margin); | |
var map = new SvgMap(svg, widthMap, heightMap, margin, cfg); | |
var legend = new SvgLegend(svg.append('g') | |
.attr('transform', 'translate(' + [widthMap + margin + cfg.lineMid, margin] + ')'), | |
cfg, clickedLegend | |
); | |
cfg.filter = legend.lookFilter; | |
var globe = new SvgGlobe(svg.append('g') | |
.attr('transform', 'translate(' + [widthMap - widthGlobe / 2 + margin, heightMap - heightGlobe / 2 + margin] + ')'), | |
widthGlobe, heightGlobe, cfg | |
); | |
var info = new HtmlInfo(d3.select('body').append('div') | |
.style('position', 'absolute') | |
.style('left', (2 * margin) + 'px') | |
.style('top', (2 * margin) + 'px') | |
.style('border', cfg.frameLineWidth + 'px solid ' + cfg.colors.frame) | |
.style('padding', (cfg.titleSize * 0.75) + 'px') | |
.style('min-width', (cfg.titleSize * 8) + 'px') | |
.style('background-color', cfg.colors.bg), | |
cfg | |
); | |
var controls = svg.append('g') | |
.attr('transform', 'translate(' + [margin * 1.5, heightMap + margin * .5 - 22] + ')'); | |
Utils.svgControls(controls, setIndex, map.reset); | |
var list = new SvgList(svg.append('g') | |
.attr('transform', 'translate(' + [margin, (heightMap + margin + cfg.lineMid)] + ')'), | |
cfg, widthMap - widthGlobe / 2, widthList, | |
function (h) { | |
h = heightMap + h + 3 * margin; | |
svg.attr('height', h); | |
d3.select(self.frameElement).style('height', h + 'px'); | |
}, | |
selectedPlace | |
); | |
map.clickedPoint = function (p) { | |
selectedPlace(p); | |
}; | |
map.onZoomed = globe.setZoom; | |
globe.onZoomed = map.setZoom; | |
globe.canZoom = function (i) { | |
var p = map.projection(i); | |
return !isNaN(p[0]) && !isNaN(p[1]); | |
}; | |
if (self.frameElement) { | |
self.frameElement.focus(); | |
} | |
d3.select('body').on('keydown', function () { | |
var i = null; | |
if (d3.event.keyCode === 37) { | |
i = -1; | |
} else if (d3.event.keyCode === 39) { | |
i = +1; | |
} else if (d3.event.keyCode === 27 && selected) { | |
selectedPlace(selected); | |
} | |
if (i !== null) { | |
setIndex(i); | |
} | |
}); | |
d3.json('mercator-countries.json', function (topo) { | |
topojson.presimplify(topo); | |
map.countries.datum(topojson.mesh(topo, topo.objects.countries)).attr('d', map.path); | |
}); | |
d3.json('mercator-land.json', function (topo) { | |
topojson.presimplify(topo); | |
map.land.datum(topojson.feature(topo, topo.objects.land)).attr('d', map.path); | |
}); | |
d3.json('land.json', function (topo) { | |
globe.land.datum(topojson.feature(topo, topo.objects.land)).attr('d', globe.pathRaw); | |
}); | |
d3.xml('/darosh/raw/2d12a584a14910032ab8/places.kml', function (xml) { | |
var geo = toGeoJSON.kml(xml); | |
Utils.parsePlaces(geo); | |
if (list) { | |
list.update(geo); | |
} | |
cfg.filtered = geo.features; | |
map.placesSelection = Utils.svgPlaces(map.g, geo, map.pathRaw, 1.25, cfg, selectedPlace); | |
}); | |
function clickedLegend(d) { | |
map.root.selectAll('.' + d.key).style('display', d.off ? 'none' : null); | |
globe.root.selectAll('.' + d.key).style('display', d.off ? 'none' : null); | |
list.filter(legend.lookShapes); | |
if (!d.off) { | |
globe.update(); | |
map.update(); | |
} | |
} | |
function selectedPlace(d) { | |
list.selection(selected, d); | |
if (d === selected) { | |
d = null; | |
} | |
globe.selection = d; | |
if (d === null) { | |
globe.updateSelection(); | |
} | |
info.update(d); | |
selected = d; | |
map.zoomTo(d, info.size); | |
} | |
function setIndex(i) { | |
var f = cfg.filtered; | |
var c = f.indexOf(selected); | |
c = c === -1 ? 0 : (c + i); | |
c = (c + f.length) % f.length; | |
if (list) { | |
list.selection(selected, f[c]); | |
} | |
info.update(f[c]); | |
globe.selection = f[c]; | |
setTimeout(function () { | |
map.zoomTo(f[c], info.size); | |
}, 50); | |
selected = f[c]; | |
} | |
})(); | |
</script> | |
</body> |
function HtmlInfo(root, cfg) { | |
'use strict'; | |
var self; | |
var previous; | |
var a = root.append('a') | |
.style('font-size', (cfg.titleSize * 1.25) + 'px') | |
.style('line-height', 1) | |
.style('font-weight', 'bold') | |
.style('color', '#333') | |
.attr('href', 'http://bl.ocks.org/darosh/7b816a50e66bb62208a7') | |
.attr('target', '_blank') | |
.text('Planetary Grid'); | |
root = root.append('div') | |
.style('display', 'none'); | |
var b = root.append('b') | |
.style('display', 'block') | |
.style('font-size', cfg.titleSize + 'px'); | |
var s = b.append('svg') | |
.attr('width', cfg.titleSize * 1.6) | |
.attr('height', cfg.titleSize * 1.6) | |
.style('float', 'left') | |
.style('margin-top', (-cfg.titleSize * 0.25) + 'px') | |
.style('margin-left', (-cfg.titleSize * 0.25) + 'px') | |
.style('margin-right', (cfg.titleSize * 0.25) + 'px') | |
.append('path') | |
.style('stroke-width', 2) | |
.attr('transform', 'translate(' + [cfg.titleSize * 1.6 / 2, cfg.titleSize * 1.6 / 2] + ')'); | |
var t = b.append('span'); | |
var c = root.append('small') | |
.style('display', 'block') | |
.style('text-align', 'right') | |
.style('margin-top', (cfg.titleSize / 4) + 'px') | |
.style('margin-bottom', (cfg.titleSize / 2) + 'px'); | |
var d = root.append('a') | |
.style('display', 'block') | |
.attr('target', '_blank') | |
.text('Wikipedia'); | |
var e = root.append('a') | |
.style('display', 'block') | |
.attr('target', '_blank') | |
.text('Google'); | |
var f = root.append('a') | |
.style('display', 'block') | |
.attr('target', '_blank') | |
.text('Google Maps'); | |
var g = root.append('a') | |
.style('display', 'block') | |
.attr('target', '_blank') | |
.text('Google Earth'); | |
function update(feature) { | |
if (previous === feature) { | |
return; | |
} | |
previous = feature; | |
if (feature) { | |
t.text(feature.properties.name); | |
s | |
.attr('class', feature.coordinates ? 'legend-symbol ' + feature.type : 'symbol ' + feature.properties.description) | |
.attr('d', function () { | |
var s = cfg.sizes[feature.properties.description || 'place'] * cfg.titleSize / 6; | |
return d3.svg.symbol().size(s).type(cfg.symbols[feature.properties ? feature.properties.description : 'circle'])(); | |
}); | |
if (feature.type !== 'Feature') { | |
d.style('display', 'none'); | |
e.style('display', 'none'); | |
} else { | |
d.style('display', 'block'); | |
e.style('display', 'block'); | |
} | |
var coo = [feature.geometry.coordinates[1], feature.geometry.coordinates[0]]; | |
coo[0] = d3.round(coo[0], 3); | |
coo[1] = d3.round(coo[1], 3); | |
c.text(d3.round(coo[0], 2) + ', ' + d3.round(coo[1], 2)); | |
d.attr('href', 'https://wikipedia.org/wiki/Special:Search/' + feature.properties.name); | |
e.attr('href', 'https://www.google.com/search?q=' + feature.properties.name); | |
f.attr('href', 'https://www.google.com/maps/@' + coo[0] + ',' + coo[1] + ',12z'); | |
g.attr('href', 'https://www.google.com/maps/@' + coo[0] + ',' + coo[1] + ',512m/data=!3m1!1e3'); | |
a.style('display', 'none'); | |
root.style('display', null); | |
self.size = root.node().getBoundingClientRect(); | |
} else { | |
a.style('display', null); | |
root.style('display', 'none'); | |
} | |
} | |
return self = { | |
update: update | |
}; | |
} |
function SvgLegend(root, cfg, clicked) { | |
'use strict'; | |
var shapes = cfg.symbols; | |
var data = d3.map(shapes).entries().filter(function (d) { | |
return d.key !== 'undefined' | |
}); | |
data.forEach(function (v) { | |
v.classed = 'symbol ' + v.key; | |
v.symbol = true; | |
}); | |
data = data.concat([ | |
{key: 'cool-line', value: 'circle', path: 'M-10,0 L10,0'}, | |
{key: 'hot-line', value: 'circle', path: 'M-10,0 L10,0'}, | |
{key: 'balanced-line', value: 'circle', path: 'M-10,0 L10,0'}, | |
{key: 'cool-point', value: 'circle', classed: 'cool-point', off: true}, | |
{key: 'hot-point', value: 'circle', classed: 'hot-point', off: true}, | |
{key: 'balanced-point', value: 'circle', classed: 'balanced-point', off: true}, | |
{key: 'graticule', value: 'circle', path: 'M-8.25,0 L8.25,0', off: true}, | |
{key: 'map', value: 'square', classed: 'map'} | |
]); | |
var l = root.selectAll('legend').data(data); | |
var g = l.enter().append('g') | |
.attr('class', 'legend') | |
.attr('transform', function (d, i) { | |
return 'translate(' + [0, i * cfg.lineHeight] + ')'; | |
}) | |
.style('opacity', function (d) { | |
return d.off ? 0.25 : null; | |
}) | |
.on('click', function (d) { | |
d.off = !d.off; | |
d3.select(this).style('opacity', d.off ? 0.25 : null); | |
clicked(d); | |
}); | |
g.append('path') | |
.attr('class', function (d) { | |
return d.classed || d.key; | |
}) | |
.style('stroke-width', function (d) { | |
return (d.key === 'map') ? 1.5 : ((d.path || d.classed) && !d.symbol) ? 2 : 1; | |
}) | |
.attr('transform', 'translate(' + [cfg.fontSize / 2, cfg.lineMid] + ')') | |
.attr('d', function (d) { | |
return d.path || d3.svg.symbol().size(cfg.sizes[d.key]).type(d.value)(); | |
}); | |
g.append('text') | |
.attr('dy', cfg.fontSize) | |
.attr('dx', cfg.fontSize + cfg.lineMid) | |
.text(function (d) { | |
return d.key.replace('-', ' '); | |
}); | |
var lookShapes = {}; | |
var lookFilter = {}; | |
data.forEach(function (d) { | |
lookFilter[d.key] = d; | |
if (shapes[d.key]) { | |
lookShapes[d.key] = d; | |
} | |
}); | |
return { | |
data: data, | |
lookShapes: lookShapes, | |
lookFilter: lookFilter | |
}; | |
} |
function SvgList(root, cfg, width, minItemWidth, updated, clicked) { | |
'use strict'; | |
var self; | |
var shapes = cfg.symbols; | |
var cols = Math.floor(width / minItemWidth); | |
var itemWidth = Math.floor((width + cfg.fontSize) / cols); | |
var data; | |
var previousData = []; | |
function selection(o, n) { | |
root.selectAll('g').data(o ? [o, n] : [n], function (d) { | |
return d && d.properties ? d.properties.id : null; | |
}) | |
.style('font-weight', function (d) { | |
return ((d === n) && (n !== o)) ? 'bold' : null; | |
}) | |
.selectAll('text') | |
.each(wrap); | |
} | |
function update(topo) { | |
data = topo.features; | |
self.filtered = data; | |
var lastRow = Math.ceil(data.length / cols); | |
var height = lastRow * cfg.lineHeight; | |
updated(height); | |
enter(data, previousData); | |
previousData = data; | |
} | |
function filter(l) { | |
var filtered = data.filter(function (d) { | |
var t = d.properties.description; | |
return !l[t].off; | |
}); | |
enter(filtered, previousData); | |
previousData = filtered; | |
cfg.filtered = filtered; | |
} | |
function enter(filtered, previousData) { | |
var lastRow = Math.ceil(filtered.length / cols); | |
var g = root.selectAll('g').data(filtered, function (d) { | |
return d.properties.id; | |
}); | |
g.exit().remove(); | |
g.transition().attr('transform', function (d, i) { | |
var row = i % lastRow; | |
var col = (i - row) / lastRow; | |
return 'translate(' + [col * itemWidth, row * cfg.lineHeight] + ')'; | |
}); | |
var a = g.enter().append('g') | |
.attr('class', 'item') | |
.style('opacity', previousData.length ? 0 : 1) | |
.attr('transform', function (d, i) { | |
var row = i % lastRow; | |
var col = (i - row) / lastRow; | |
return 'translate(' + [col * itemWidth, row * cfg.lineHeight] + ')'; | |
}) | |
.on('click', clicked); | |
a.transition().delay(previousData.length ? 250 : 0) | |
.style('opacity', 1); | |
a.append('path') | |
.attr('class', function (d) { | |
return 'symbol ' + d.properties.description; | |
}) | |
.attr('transform', 'translate(' + [cfg.fontSize / 2, cfg.lineMid] + ')') | |
.attr('d', function (d) { | |
var s = cfg.sizes[d.properties.description]; | |
return d3.svg.symbol().size(s).type(shapes[d.properties.description])(); | |
}); | |
a.append('text') | |
.attr('dy', cfg.fontSize) | |
.attr('dx', cfg.fontSize + cfg.lineMid) | |
.text(function (d) { | |
return d.properties.name; | |
}) | |
.each(wrap); | |
} | |
function wrap() { | |
var self = d3.select(this); | |
var text = self.data()[0].properties.name; | |
self.text(text); | |
var textLength = self.node().getComputedTextLength(); | |
while (textLength > (itemWidth - 50) && text.length > 0) { | |
text = text.slice(0, -1); | |
self.text(text + '…'); | |
textLength = self.node().getComputedTextLength(); | |
} | |
self.text(self.text().replace(' …', '…')); | |
} | |
self = { | |
update: update, | |
filter: filter, | |
selection: selection | |
}; | |
return self; | |
} |
function SvgMap(svg, width, height, margin, cfg) { | |
'use strict'; | |
var maxScale = 3; | |
var self; | |
var focused; | |
var arrowStart; | |
var prevScale; | |
var referenceSize = Math.sqrt(height * height + width * width) * cfg.durationScale; | |
var clip = d3.geo.clipExtent().extent([[-width / 2, -height / 2], [width / 2, height / 2]]); | |
var projection = d3.geo.mercator() | |
.translate([0, 0]) | |
.precision(0) | |
.scale(width / 2 / Math.PI); | |
var simplify = d3.geo.transform({ | |
point: function (x, y, z) { | |
if (z >= projectionSimplified.area) { | |
this.stream.point(~~(x * width), ~~(y * width)); | |
} | |
} | |
}); | |
var round = d3.geo.transform({ | |
point: function (x, y) { | |
this.stream.point(~~(x * 10) / 10, ~~(y * 10) / 10); | |
} | |
}); | |
var projectionRounded = { | |
stream: function (s) { | |
return projection.stream(round.stream(s)); | |
}, | |
baseArea: 4e-3 / width | |
}; | |
var projectionLinesRounded = { | |
stream: function (s) { | |
return projectionLines.stream(round.stream(s)); | |
}, | |
baseArea: 4e-3 / width | |
}; | |
var projectionSimplified = { | |
stream: function (s) { | |
return simplify.stream(clip.stream(s)); | |
}, | |
baseArea: 4e-3 / width | |
}; | |
projectionSimplified.area = projectionSimplified.baseArea; | |
var projectionLines = d3.geo.mercator() | |
.rotate([cfg.shift, 0, 0]) | |
.translate([0, 0]) | |
.precision(1) | |
.scale(width / 2 / Math.PI); | |
var path = d3.geo.path() | |
.projection(projectionSimplified); | |
var pathRaw = d3.geo.path() | |
.projection(projection); | |
var pathRawRounded = d3.geo.path() | |
.projection(projectionRounded); | |
var pathLines = d3.geo.path() | |
.projection(projectionLines); | |
var pathLinesRounded = d3.geo.path() | |
.projection(projectionLinesRounded); | |
var zoom = d3.behavior.zoom() | |
.scaleExtent([1, maxScale]) | |
.on('zoom', zoomed); | |
var defs = svg.append('defs'); | |
defs.append('clipPath') | |
.attr('id', 'clip-map') | |
.append('rect') | |
.attr("x", -width / 2) | |
.attr("y", -height / 2) | |
.attr("width", width) | |
.attr("height", height); | |
defs.append('clipPath') | |
.attr('id', 'clip-arrow') | |
.append('rect') | |
.attr("x", margin) | |
.attr("y", margin) | |
.attr("width", width) | |
.attr("height", height); | |
var clipGroup = svg.append("g") | |
.attr("transform", "translate(" + [width / 2 + margin, height / 2 + margin] + ")") | |
.style('clip-path', 'url(#clip-map)') | |
.call(zoom); | |
clipGroup.append("rect") | |
.attr("class", "map water") | |
.attr("x", -width / 2) | |
.attr("y", -height / 2) | |
.attr("width", width) | |
.attr("height", height); | |
clipGroup.append("rect") | |
.attr("class", "overlay") | |
.attr("x", -width / 2) | |
.attr("y", -height / 2) | |
.attr("width", width) | |
.attr("height", height); | |
var g = clipGroup.append('g'); | |
var graticulePath = g.append('path') | |
.datum(d3.geo.graticule()()) | |
.attr('class', 'graticule') | |
.style('display', 'none') | |
.attr('d', pathRaw); | |
var land = g.append('path').attr('class', 'map land'); | |
var countries = g.append('path').attr('class', 'map country'); | |
var h = d3.geo.hexakisIcosahedron; | |
var coolLines = Utils.svgLines(g, pathLines, h.icosahedronEdges(), 'cool-line'); | |
var hotLines = Utils.svgLines(g, pathLines, h.hexakisCenterEdges(), 'hot-line'); | |
var balancedLines = Utils.svgLines(g, pathLines, h.hexakisSideEdges(), 'balanced-line'); | |
var coolPointsData = Utils.pointsToFeatures(h.icosahedronPoints(), 'cool-point', 'Cool point', cfg.shift); | |
var hotPointsData = Utils.pointsToFeatures(h.hexakisCenterPoints(), 'hot-point', 'Hot point', cfg.shift); | |
var balancedPointsData = Utils.pointsToFeatures(h.hexakisCrossPoints(), 'balanced-point', 'Balanced point', cfg.shift); | |
Utils.svgPoints(g, coolPointsData, 'cool-point', projectionLines, clickedPoint, 'none'); | |
Utils.svgPoints(g, hotPointsData, 'hot-point', projectionLines, clickedPoint, 'none'); | |
Utils.svgPoints(g, balancedPointsData, 'balanced-point', projectionLines, clickedPoint, 'none'); | |
function clickedPoint(d) { | |
self.clickedPoint(d); | |
} | |
var placesGroup = g.append('g'); | |
var selectedCircleGroup = svg.append('g') | |
.style('display', 'none') | |
.style('clip-path', 'url(#clip-arrow)'); | |
var selectedCircle = selectedCircleGroup.append('circle') | |
.attr('cx', margin) | |
.attr('cy', margin) | |
.attr('r', 20) | |
.attr('class', 'border'); | |
var selectedArrow = selectedCircleGroup.append('line') | |
.attr('x1', margin * 4) | |
.attr('y1', margin * 4) | |
.attr('class', 'border'); | |
svg.append("rect") | |
.attr("class", "border") | |
.style('stroke-width', cfg.frameLineWidth) | |
.attr("transform", "translate(" + [width / 2 + margin, height / 2 + margin] + ")") | |
.attr("x", -width / 2 + cfg.frameLineWidth / 2) | |
.attr("y", -height / 2 + cfg.frameLineWidth / 2) | |
.attr("width", width - cfg.frameLineWidth) | |
.attr("height", height - cfg.frameLineWidth); | |
function getFixedZoom(t, s) { | |
var S = (width - height) / 2; | |
t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0])); | |
t[1] = Math.min(height / 2 * (s - 1) + S * s, Math.max(height / 2 * (1 - s) - S * s, t[1])); | |
} | |
function fixZoom() { | |
var t = zoom.translate(); | |
var s = zoom.scale(); | |
getFixedZoom(t, s); | |
zoom.translate(t); | |
} | |
function roundTranslate(t) { | |
t[0] = Math.round(t[0]); | |
t[1] = Math.round(t[1]); | |
} | |
function zoomed() { | |
fixZoom(); | |
var t = zoom.translate(); | |
var s = zoom.scale(); | |
var i = projection.invert([t[0] / s, t[1] / s]); | |
update(); | |
if (self.onZoomed) { | |
self.onZoomed(t, s, i); | |
} | |
} | |
function update(running) { | |
var t = zoom.translate(); | |
var s = Math.min(maxScale, d3.round(zoom.scale(), 6)); | |
roundTranslate(t); | |
g.attr('transform', 'translate(' + t + ')scale(' + s + ')'); | |
var ps = (running === true) ? 1 : zoom.scale(); | |
var c = translateToCenter(t, s); | |
var ce = [[ | |
-width / 2 + c[0] - width / 2 / s, | |
-height / 2 + c[1] - height / 2 / s], [ | |
-width / 2 + c[0] + width / 2 / s, | |
-height / 2 + c[1] + height / 2 / s]]; | |
clip.extent(ce); | |
projectionLines.precision(running ? 1 : Math.sqrt(1 / 2) / s / s); | |
projectionSimplified.area = projectionSimplified.baseArea / ps / ps; | |
projection.clipExtent(ce); | |
projectionLines.clipExtent(ce); | |
var localPathRaw, localPathLines; | |
if (running) { | |
localPathRaw = pathRawRounded; | |
localPathLines = pathLinesRounded; | |
} else { | |
localPathRaw = pathRaw; | |
localPathLines = pathLines; | |
} | |
if (!cfg.filter['graticule'].off) { | |
graticulePath.attr('d', localPathRaw); | |
} | |
if (!cfg.filter['cool-line'].off) { | |
coolLines.attr('d', localPathLines); | |
} | |
if (!cfg.filter['hot-line'].off) { | |
hotLines.attr('d', localPathLines); | |
} | |
if (!cfg.filter['balanced-line'].off) { | |
balancedLines.attr('d', localPathLines); | |
} | |
if (!cfg.filter.map.off) { | |
land.attr('d', path); | |
countries.attr('d', path); | |
} | |
if (prevScale !== s) { | |
g.style('stroke-width', 1 / s); | |
if (!cfg.filter['graticule'].off) { | |
graticulePath.style('stroke-dasharray', (2 / s ) + ',' + (3 / s)); | |
} | |
//self.placesSelection.attr('transform', function (d) { | |
// return d.translate + 'scale(' + 1 / s + ')'; | |
//}); | |
self.placesSelection.defs.attr('transform', function () { | |
return 'scale(' + 1 / s + ')'; | |
}); | |
prevScale = s; | |
} | |
if (focused) { | |
var f = [width / 2 + focused[0] * s + t[0], height / 2 + focused[1] * s + t[1]]; | |
var l = [[arrowStart.width / 2 + 2 * margin, arrowStart.height / 2 + 2 * margin], [f[0] + margin, f[1] + margin]]; | |
var d = Math.sqrt(Math.pow(l[1][0] - l[0][0], 2) + Math.pow(l[1][1] - l[0][1], 2)); | |
var r = (d - 20) / d; | |
l[1][0] = l[0][0] + r * (l[1][0] - l[0][0]); | |
l[1][1] = l[0][1] + r * (l[1][1] - l[0][1]); | |
selectedCircle | |
.attr('transform', 'translate(' + [d3.round(f[0]), d3.round(f[1])] + ')'); | |
selectedArrow | |
.attr('x1', l[0][0]) | |
.attr('y1', l[0][1]) | |
.attr('x2', l[1][0]) | |
.attr('y2', l[1][1]); | |
selectedCircleGroup.style('display', null); | |
} else { | |
selectedCircleGroup.style('display', 'none'); | |
} | |
} | |
function setZoom(t, s, i) { | |
var p = projection(i); | |
zoom.scale(s); | |
zoom.translate([p[0] * s, p[1] * s]); | |
fixZoom(); | |
update(); | |
} | |
function translateToCenter(t, s) { | |
return [-t[0] / s + width / 2, -t[1] / s + height / 2]; | |
} | |
function centerToTranslate(c, s) { | |
return [s * (-c[0] + width / 2), s * (-c[1] + height / 2)]; | |
} | |
function zoomTransition(p, toScale) { | |
var fromScale = zoom.scale(); | |
var fromTranslate = zoom.translate(); | |
var toTranslate = [-p[0] * toScale, -p[1] * toScale]; | |
getFixedZoom(toTranslate, toScale); | |
var from = translateToCenter(fromTranslate, fromScale); | |
from[2] = referenceSize / fromScale; | |
var to = translateToCenter(toTranslate, toScale); | |
to[2] = referenceSize / toScale; | |
var zi = d3.interpolateZoom(from, to); | |
var dur = Math.min(cfg.durationMax, Math.max(cfg.durationMin, zi.duration * cfg.durationSpeed)); | |
d3.transition().duration(dur).tween('tween', tween); | |
function tween() { | |
return function (t) { | |
var z = zi(t); | |
var s = referenceSize / z[2]; | |
var tr = centerToTranslate(z, s); | |
getFixedZoom(tr, s); | |
zoom.translate(tr); | |
zoom.scale(s); | |
update(t < 1); | |
if (self.onZoomed) { | |
var i = projection.invert([tr[0] / s, tr[1] / s]); | |
self.onZoomed(z, s, i, t < 1); | |
} | |
}; | |
} | |
} | |
function zoomTo(d, ast) { | |
if (!d) { | |
focused = null; | |
selectedCircleGroup.style('display', 'none'); | |
return; | |
} | |
var c = d.geometry ? [d.geometry.coordinates[0], d.geometry.coordinates[1]] : [d[0] + cfg.shift, d[1]]; | |
var p = projection(c); | |
arrowStart = ast; | |
focused = p; | |
zoomTransition(p, maxScale) | |
} | |
function reset() { | |
zoomTransition(projection([0, 0]), 1); | |
} | |
self = { | |
root: svg, | |
land: land, | |
countries: countries, | |
path: path, | |
pathRaw: pathRaw, | |
g: placesGroup, | |
projection: projection, | |
setZoom: setZoom, | |
zoomTo: zoomTo, | |
reset: reset, | |
update: update | |
}; | |
return self; | |
} |
body { | |
margin: 0; | |
padding: 0; | |
font-family: Arial, Helvetica, sans-serif; | |
font-size: 16px; | |
line-height: 19px; | |
text-rendering: optimizeLegibility; | |
color: rgba(0,0,0,0.87); | |
} | |
svg, canvas { | |
display: block; | |
} | |
a { | |
text-decoration: none; | |
color: #1f78b4; | |
} | |
a:hover { | |
text-decoration: underline; | |
color: #e31a1c; | |
} | |
a:visited { | |
color: #333; | |
} | |
small { | |
color: rgba(0,0,0,0.64); | |
} | |
.no-select { | |
user-select: none; | |
} | |
.point { | |
pointer-events: all; | |
cursor: pointer; | |
} | |
.water { | |
fill: #def4ff; | |
} | |
.border { | |
fill: none; | |
stroke-width: 2.5; | |
stroke: #444; | |
stroke-opacity: 1; | |
} | |
.overlay { | |
fill: none; | |
pointer-events: all; | |
} | |
.overlay-white { | |
fill: #fff; | |
pointer-events: all; | |
} | |
.country { | |
stroke: #000; | |
stroke-opacity: .33; | |
fill: none; | |
} | |
.land { | |
fill: rgba(255, 255, 255, 0.87); | |
stroke: 0; | |
} | |
.graticule { | |
fill: none; | |
stroke: #bbb; | |
stroke-opacity: 0.66; | |
} | |
.cool-point { | |
fill: none; | |
stroke: #1f78b4; | |
stroke-opacity: 0.85; | |
} | |
.hot-point { | |
fill: none; | |
stroke: #e31a1c; | |
stroke-opacity: 0.86; | |
} | |
.balanced-point { | |
fill: none; | |
stroke: #333; | |
stroke-opacity: 0.85; | |
} | |
.cool-line { | |
fill: none; | |
stroke: #1f78b4; | |
stroke-opacity: .85; | |
} | |
.hot-line { | |
fill: none; | |
stroke: #e31a1c; | |
stroke-opacity: .85; | |
} | |
.balanced-line { | |
fill: none; | |
stroke: #333; | |
stroke-opacity: .85; | |
} | |
.symbol { | |
stroke-width: 1; | |
stroke: #000; | |
cursor: pointer; | |
} | |
.place { | |
fill: #888; | |
fill-opacity: 0.33; | |
} | |
.pyramid { | |
fill: #ffff4d; | |
} | |
.megalith { | |
fill: #299ae6; | |
} | |
.temple { | |
fill: #ff7f00; | |
} | |
.mound { | |
fill: #90de43; | |
} | |
.volcano { | |
fill: #e31a1c; | |
} | |
.graticule { | |
stroke-dasharray: 2, 3; | |
} | |
.legend { | |
cursor: pointer; | |
pointer-events: all; | |
} | |
.item { | |
cursor: pointer; | |
} | |
.control { | |
cursor: pointer; | |
} | |
.legend-symbol { | |
stroke-width: 2; | |
} | |
.legend .map { | |
fill: #def4ff; | |
stroke: #333; | |
} |
var Utils = (function () { | |
'use strict'; | |
function parsePlaces(geo) { | |
geo.features.sort(function (a, b) { | |
return a.properties.name.localeCompare(b.properties.name); | |
}); | |
geo.features.forEach(function (v, k) { | |
v.properties.id = k; | |
v.properties.description = v.properties.description || 'place'; | |
}); | |
} | |
function pointsToFeatures(points, type, name, shift) { | |
return points.coordinates.map(function (p) { | |
var sh = p[0] + shift; | |
sh = (sh > 180) ? (sh - 360) : sh; | |
return { | |
coordinates: p, | |
geometry: { | |
coordinates: [sh, p[1]], | |
type: 'Point' | |
}, | |
type: type, | |
properties: { | |
name: name | |
} | |
} | |
}); | |
} | |
function trans(path) { | |
return function (d) { | |
var p = path.centroid(d); | |
if (isNaN(p[0]) || isNaN(p[1])) { | |
p[0] = -1e10; | |
p[1] = -1e10; | |
} | |
return d.translate = 'translate(' + p + ')'; | |
}; | |
} | |
function addSymbols(root, cfg, scale) { | |
var g = root.append('defs'); | |
d3.keys(cfg.symbols).forEach(function (d) { | |
g.append('path') | |
.attr('id', d) | |
.attr('class', 'symbol ' + d) | |
.attr('d', d3.svg.symbol() | |
.size(scale * scale * cfg.sizes[d]) | |
.type(cfg.symbols[d])()); | |
}); | |
return g.selectAll('path'); | |
} | |
function svgPlaces(root, geo, t, scale, cfg, clicked) { | |
t = trans(t); | |
var defs = addSymbols(root, cfg, scale); | |
var places = root.selectAll() | |
.data(geo.features) | |
.enter() | |
.append('use') | |
.attr('transform', t) | |
.attr('xlink:href', function (d) { | |
return '#' + d.properties.description; | |
}) | |
.on('click', clicked); | |
return { | |
defs: defs, | |
places: places | |
} | |
} | |
function _svgPlaces(root, geo, t, scale, cfg, clicked) { | |
t = trans(t); | |
return root.selectAll() | |
.data(geo.features) | |
.enter() | |
.append('path') | |
.attr('transform', t) | |
.attr('class', function (d) { | |
return 'symbol ' + d.properties.description; | |
}) | |
.attr('d', function (d) { | |
return d3.svg.symbol() | |
.size(scale * cfg.sizes[d.properties.description]) | |
.type(cfg.symbols[d.properties.description])(); | |
}) | |
.on('click', clicked); | |
} | |
function svgLines(root, path, data, type) { | |
return root.append('path') | |
.datum(data) | |
.attr('class', 'line ' + type) | |
.attr('d', path); | |
} | |
function svgPoints(root, points, type, projection, clicked, display) { | |
points = points.filter(function (d) { | |
var p = projection(d.coordinates); | |
if (Math.abs(p[0]) !== Infinity && Math.abs(p[1]) !== Infinity) { | |
d.projected = p; | |
return true; | |
} | |
}); | |
var p = root.selectAll('.point.' + type).data(points); | |
p.enter().append('circle') | |
.attr('class', 'point ' + type) | |
.style('display', display) | |
.attr('r', 5) | |
.attr('transform', function (d) { | |
return 'translate(' + d.projected + ')'; | |
}) | |
.on('click', clicked); | |
} | |
function svgControls(root, setIndex, reset) { | |
root.append('path') | |
.attr('class', 'control') | |
.attr('d', d3.svg.symbol().size(120).type('triangle-up')) | |
.attr('transform', 'translate(60,10)rotate(90)') | |
.on('click', function () { | |
setIndex(+1); | |
}); | |
root.append('path') | |
.attr('class', 'control') | |
.attr('d', d3.svg.symbol().size(120).type('circle')) | |
.attr('transform', 'translate(35,10)') | |
.on('click', reset); | |
root.append('path') | |
.attr('class', 'control') | |
.attr('d', d3.svg.symbol().size(120).type('triangle-up')) | |
.attr('transform', 'translate(10,10)rotate(-90)') | |
.on('click', function () { | |
setIndex(-1); | |
}); | |
} | |
return { | |
svgLines: svgLines, | |
svgPoints: svgPoints, | |
svgPlaces: svgPlaces, | |
svgControls: svgControls, | |
parsePlaces: parsePlaces, | |
pointsToFeatures: pointsToFeatures | |
}; | |
})(); |