react-simple-maps icon indicating copy to clipboard operation
react-simple-maps copied to clipboard

Rotating an orthogonal map

Open mikesol opened this issue 7 years ago • 4 comments

Hey! First off, thanks for your GREAT library! I would like to create an orthogonal map that rotates with a frame rate of 2-3 fps. I have read up on how to create custom projections and have succeeded in making one with geoOrthogonal, however, I cannot figure out a way to access the projection's rotation so that I can create a setInterval that makes it turn. Where in the react-simple-maps ecosystem is this possible? It'd be great if there were a simple tutorial that showed how to do this. For example, the color tutorial helped a great deal and I now have colors automatically changing as new API data pours in. Something like that for map rotation would be great!

This is a bit like https://github.com/zcreativelabs/react-simple-maps/issues/23 in that it is looking to change a parameter dynamically, albeit a different parameter with a custom projection. Reading over that issue didn't help me advance with this one, which is why I'm opening this as a separate issue. Any help would be appreciated! And thanks again for this awesome work!

mikesol avatar Nov 25 '17 20:11 mikesol

Hi @mikesol

Thank you for giving react-simple-maps a try and I am glad you like it.

The globe thing is actually an interesting issue. The main problem with react-simple-maps and globes in specific is that the solution for panning as it is currently implemented uses a transform on the group of geographies (so it doesn't affect the projection). This boosts the overall performance, and allows for caching the individual paths so that they don’t have to be recalculated every time.

Globes however require the projection to be recalculated on rotation, which would mean that ZoomableGroup would have to work differently. I am working on a solution for this with an additional component (working title: ZoomableGlobe), but it is still a work in progress. I have done some experiments with autorotation, but so far the performance is still a bit of an issue (with pure d3 this is probably better).

Interestingly enough, since react-simple-maps is very modular, you can use parts of it to create a custom globe with rotation behaviour, like this:


import React, { Component } from "react"
import { geoPath, geoOrthographic } from "d3-geo"
import { timer } from "d3-timer" 

import {
  Geographies,
  Geography,
} from "react-simple-maps"

class Globe extends Component {
  constructor() {
    super()
    this.state = {
      isPressed: false,
      mouseX: 0,
      mouseY: 0,
      rotation: [0,0,0],
    }
    this.projection = this.projection.bind(this)
    this.handleMouseDown = this.handleMouseDown.bind(this)
    this.handleMouseMove = this.handleMouseMove.bind(this)
    this.handleMouseUp = this.handleMouseUp.bind(this)
    this.startAnimation = this.startAnimation.bind(this)
  }
  projection() {
    return geoOrthographic()
      .translate([ 800 / 2, 800 / 2 ])
      .rotate(this.state.rotation)
      .clipAngle(90)
      .scale(200)
  }
  handleMouseDown({ pageX, pageY }) {
    this.autorotation.stop()
    this.setState({
      isPressed: true,
      mouseX: pageX,
      mouseY: pageY,
    })
  }
  handleMouseMove({ pageX, pageY }) {
    if (!this.state.isPressed) return
    const differenceX = this.state.mouseX - pageX
    const differenceY = this.state.mouseY - pageY
    this.setState({
      rotation: [
        this.state.rotation[0] - differenceX / 2,
        this.state.rotation[1] + differenceY / 2,
        0,
      ],
      mouseX: pageX,
      mouseY: pageY,
    })
  }
  handleMouseUp({ pageX, pageY }) {
    this.setState({
      isPressed: false,
    })
    this.autorotation.restart(this.startAnimation, 500)
  }
  startAnimation() {
    const rotation = [this.state.rotation[0] + 0.18, this.state.rotation[1] - 0.06, 0]
    this.setState({ rotation })
  }
  componentDidMount() {
    this.autorotation = timer(this.startAnimation)
  }
  render() {
    return (
      <svg width={ 800 } height={ 800 } viewBox="0 0 800 800" preserveAspectRatio="xMidYMid">
        <g
          onMouseDown={this.handleMouseDown}
          onMouseMove={this.handleMouseMove}
          onMouseUp={this.handleMouseUp}
          onMouseLeave={this.handleMouseUp}
          >
          <path
            fill="#F6F6F6"
            d={geoPath().projection(this.projection())({ type: "Sphere" })}
          />
          <Geographies
            projection={this.projection}
            geography={"/path/to/topojson-file.json"}
            disableOptimization
            >
            {(geos, proj) =>
              geos.map(geo =>
                <Geography
                  key={geo.id}
                  geography={geo}
                  projection={proj}
                  round
                />
              )
            }
          </Geographies>
        </g>
      </svg>
    )
  }
}

Note, this is a very trivial implementation of drag rotation, there are smarter ones out there, but it shows a gist of how to go about this. I also used disableOptimization without assigning cacheId to make sure that the projection update works.

Anyway, thanks for bringing this up. It is on the roadmap and I would like to see it in react-simple-maps soon, but I want to spend a bit more time looking at various quirks and autorotation.

zimrick avatar Nov 27 '17 12:11 zimrick

Ok, so I just ran a test for kicks whether it would be possible to rotate a globe inside react-simple-maps, and the autorotation actually works. What does not work is the dragging. So I correct what I said above in terms of autorotation, that part is possible with the current version of react-simple-maps. The new component that I am working on would fix the dragging part.

import React, { Component } from "react"
import { geoOrthographic } from "d3-geo"
import { timer } from "d3"

import {
  ComposableMap,
  ZoomableGroup,
  Geographies,
  Geography,
} from "react-simple-maps"

class Globe extends Component {
  constructor() {
    super()
    this.state = {
      rotation: [0,0,0],
    }
    this.projection = this.projection.bind(this)
    this.startAnimation = this.startAnimation.bind(this)
  }
  projection() {
    return geoOrthographic()
      .translate([ 800 / 2, 800 / 2 ])
      .rotate(this.state.rotation)
      .scale(200)
      .clipAngle(90)
      .precision(.1)
  }
  startAnimation() {
    const rotation = [this.state.rotation[0] + 0.18, this.state.rotation[1] - 0.06, 0]
    this.setState({ rotation })
  }
  componentDidMount() {
    this.autorotation = timer(this.startAnimation)
  }
  render() {
    return (
      <ComposableMap width={800} height={800} projection={this.projection}>
        <ZoomableGroup>
          <Geographies
            geography={"/static/world-110m.json"}
            disableOptimization
            >
            {(geos, proj) =>
              geos.map((geo, i) =>
                <Geography
                  key={`${geo.properties.ISO_A3}-${i}`}
                  geography={geo}
                  projection={proj}
                  round
                />
              )
            }
          </Geographies>
        </ZoomableGroup>
      </ComposableMap>
    )
  }
}

export default Globe

zimrick avatar Nov 27 '17 16:11 zimrick

did you finish that new component by any chance? @zimrick

Jehutty avatar Apr 02 '19 14:04 Jehutty

Unfortunately no, I moved onto another project.

mikesol avatar Apr 02 '19 16:04 mikesol