Playground made to explore d3-geo‘s map fit and clip capabilities.
Start playground with:
npm start
Made with control-panel
.
var d3 = Object.assign({},
require('d3-selection'),
require('d3-request'),
require('d3-geo'),
require('d3-geo-projection')
)
var topojson = require('topojson-client')
var control = require('control-panel')
var sphere = {type: 'Sphere'}
var baseMapUrl = 'https://d3js.org/world-110m.v1.json'
var blk = '#000000'
var red = '#990000'
var panelInputs = [{
type: 'range',
label: 'Width',
min: 0,
max: 0.8 * window.innerWidth - 20,
initial: 0.8 * window.innerWidth - 20
}, {
type: 'range',
label: 'Height',
min: 0,
max: window.innerHeight - 20,
initial: 0.75 * window.innerHeight
}, {
type: 'range',
label: 'Pad X',
min: 0,
max: 0.5 * window.innerWidth,
initial: 10
}, {
type: 'range',
label: 'Pad Y',
min: 0,
max: 0.5 * window.innerHeight,
initial: 30
}, {
type: 'select',
label: 'Projection',
options: Object.keys(d3).filter(k => {
try {
var proj = d3[k]()
return proj.invert && proj.stream
} catch (e) {
return false
}
})
.map(k => k.substr(3)),
initial: 'Equirectangular'
}, {
type: 'range',
label: 'Yaw',
min: -360,
max: 360,
initial: -180
}, {
type: 'range',
label: 'Pitch',
min: -180,
max: 180,
initial: 0
}, {
type: 'range',
label: 'Latitude 0',
min: -90,
max: 90,
initial: -60
}, {
type: 'range',
label: 'Latitude 1',
min: -90,
max: 90,
initial: 60
}, {
type: 'range',
label: 'Longitude 0',
min: -180,
max: 180,
initial: 140
}, {
type: 'range',
label: 'Longitude 1',
min: -180,
max: 180,
initial: -140
}, {
type: 'checkbox',
label: 'Fit',
initial: false
}, {
type: 'checkbox',
label: 'Clip',
initial: false
}, {
type: 'color',
label: 'Canvas color',
initial: blk
}, {
type: 'color',
label: 'Frame color',
initial: blk
}, {
type: 'color',
label: 'Sphere color',
initial: blk
}, {
type: 'color',
label: 'Range box color',
initial: red
}]
var opts0 = {}
panelInputs.forEach(o => { opts0[o.label] = o.initial })
var panel = control(panelInputs, {
root: d3.select('body').append('div').node(),
theme: 'light',
width: '20%'
})
var map = {}
map.canvas = setCanvas(map, opts0)
map.projection = setProjection(map, opts0)
map.rangeBox = makeRangeBox(map, opts0)
d3.json(baseMapUrl, (err, topology) => {
if (err) throw err
var baseMap = topojson.mesh(topology)
var update = (opts) => {
map.canvas = setCanvas(map, opts)
map.projection = setProjection(map, opts)
map.rangeBox = makeRangeBox(map, opts)
map.context.clearRect(0, 0, opts.Width, opts.Height)
drawPath(map, baseMap, {
lineWidth: 1,
strokeStyle: blk
})
drawPath(map, sphere, {
lineWidth: 2.5,
strokeStyle: opts['Sphere color']
})
drawPath(map, map.rangeBox, {
lineWidth: 3,
strokeStyle: opts['Range box color'],
setLineDash: [5]
})
drawPadRect(map, opts)
}
panel.on('input', update)
update(opts0)
})
function _draw (context, fn, style) {
context.save()
context.beginPath()
Object.keys(style).forEach(k => {
if (typeof context[k] === 'function') {
context[k](style[k])
} else {
context[k] = style[k]
}
})
fn()
context.closePath()
context.stroke()
context.restore()
}
function drawPath (map, d, style) {
var projection = map.projection
var context = map.context
var path = d3.geoPath(projection, context)
var fn = () => path(d)
_draw(context, fn, style)
}
function drawPadRect (map, opts) {
var context = map.context
var w = opts.Width
var h = opts.Height
var padX = opts['Pad X']
var padY = opts['Pad Y']
var fn = () => context.rect(padX, padY, w - 2 * padX, h - 2 * padY)
_draw(context, fn, {
lineWidth: 3,
strokeStyle: opts['Frame color'],
setLineDash: [5]
})
}
function setCanvas (map, opts) {
if (!map.canvas) {
map.canvas = d3.select('body').append('canvas')
.style('position', 'absolute')
.style('top', '0px')
.style('left', '20%')
}
if (!map.context) {
map.context = map.canvas.node().getContext('2d')
}
map.canvas
.attr('width', opts0.Width)
.attr('height', opts0.Height)
.style('border', `5px solid ${opts['Canvas color']}`)
return map.canvas
}
function setProjection (map, opts) {
var w = opts.Width
var h = opts.Height
var padX = opts['Pad X']
var padY = opts['Pad Y']
var extent = [[padX, padY], [w - padX, h - padY]]
var proj = d3['geo' + opts.Projection]()
var apply = (fname, args) => {
if (typeof proj[fname] === 'function') {
proj = proj[fname].apply(null, args)
}
return proj
}
apply('rotate', [[opts.Yaw, opts.Pitch]])
apply('fitExtent', [extent, opts.Fit ? map.rangeBox : sphere])
apply('clipExtent', [opts.Clip ? d3.geoPath(proj).bounds(map.rangeBox) : null])
return proj
}
function makeRangeBox (map, opts) {
var lon0 = opts['Longitude 0']
var lon1 = opts['Longitude 1']
var lat0 = opts['Latitude 0']
var lat1 = opts['Latitude 1']
// to cross antimeridian w/o ambiguity
if (lon0 > 0 && lon1 < 0) {
lon1 += 360
}
// to make lat span unambiguous
if (lat0 > lat1) {
var tmp = lat0
lat0 = lat1
lat1 = tmp
}
var dlon4 = (lon1 - lon0) / 4
return {
type: 'Polygon',
coordinates: [[
[lon0, lat0],
[lon0, lat1],
[lon0 + dlon4, lat1],
[lon0 + 2 * dlon4, lat1],
[lon0 + 3 * dlon4, lat1],
[lon1, lat1],
[lon1, lat0],
[lon1 - dlon4, lat0],
[lon1 - 2 * dlon4, lat0],
[lon1 - 3 * dlon4, lat0],
[lon0, lat0]
]]
}
}
"Playground to explore d3-geo's map fit and clip capabilities"
{
"name": "d3-geo-clip-fit-playground",
"version": "1.0.1",
"description": "Playground made to explore d3-geo's map fit and clip capabilities",
"main": "index.js",
"scripts": {
"start": "budo index.js --open --live",
"build": "browserify -t es2020 index.js | uglifyjs | indexhtmlify > index.html"
},
"keywords": [],
"author": "Étienne Tétreault-Pinard <etienne.t.pinard@gmail.com>",
"license": "MIT",
"dependencies": {
"control-panel": "^1.2.0",
"d3-geo": "^1.7.1",
"d3-geo-projection": "^2.3.1",
"d3-request": "^1.0.6",
"d3-selection": "^1.1.0",
"topojson-client": "^3.0.0"
},
"devDependencies": {
"budo": "^10.0.4",
"es2020": "^1.1.9",
"indexhtmlify": "^1.3.1"
}
}