block by etpinard f5d9afd6de6f53f49eecb6fa01352b7e

Playground made to explore d3-geo's map fit and clip capabilities

Full Screen

Playground made to explore d3-geo‘s map fit and clip capabilities.

Usage

Start playground with:

npm start

Made with control-panel.

index.js

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]
    ]]
  }
}

d3-geo-clip-fit-playground

"Playground to explore d3-geo's map fit and clip capabilities"

package.json

{
  "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"
  }
}