d3.js. Spinning globe with bars
Asked Answered
C

4

8

I am trying to create spinning globe with bars like in this example. You can see my example here. And everything goes fine until bars go over horizon. I have no idea how to cut bars from the bottom when they on other side of planet. Anybody can suggest me how to do it?

 /*
 * Original code source
 * http://codepen.io/teetteet/pen/Dgvfw
 */

var width = 400;
var height = 400;
var scrollSpeed = 50;
var current = 180;

var longitudeScale = d3.scale.linear()
  .domain([0, width])
  .range([-180, 180]);

var planetProjection = d3.geo.orthographic()
  .scale(200)
  .rotate([longitudeScale(current), 0])
  .translate([width / 2, height / 2])
  .clipAngle(90);
var barProjection = d3.geo.orthographic()
  .scale(200)
  .rotate([longitudeScale(current), 0])
  .translate([width / 2, height / 2])
  .clipAngle(90);

var path = d3.geo.path()
  .projection(planetProjection);

var svg = d3.select("body").append("svg")
  .attr("width", width)
  .attr("height", height);

d3.json("https://dl.dropboxusercontent.com/s/4hp49mvf7pa2cg2/world-110m.json?dl=1", function(error, world) {
  if (error) throw error;

  var planet = svg.append("path")
    .datum(topojson.feature(world, world.objects.land))
    .attr("class", "land")
    .attr("d", path);

  d3.csv("https://dl.dropboxusercontent.com/s/v4kn2hrnjlgx1np/data.csv?dl=1", function(error, data) {
    if (error) throw error;

    var max = d3.max(data, function(d) {
      return parseInt(d.Value);
    })

    var lengthScale = d3.scale.linear()
      .domain([0, max])
      .range([200, 250])

      var bars = svg.selectAll(".bar")
        .data(data)
        .enter()
        .append("line")
        .attr("class", "bar")
        .attr("stroke", "red")
        .attr("stroke-width", "2");

    function bgscroll() {

      current += 1;

      planetProjection.rotate([longitudeScale(current), 0]);
      barProjection.rotate([longitudeScale(current), 0]);

      planet.attr("d", path);

      bars.attr("x1", function(d) {
         return planetProjection([d.Longitude, d.Latitude])[0];
       }).attr("y1", function(d) {
         return planetProjection([d.Longitude, d.Latitude])[1];
       }).attr("x2", function(d) {
         barProjection.scale(lengthScale(d.Value));
         return barProjection([d.Longitude, d.Latitude])[0];
       }).attr("y2", function(d) {
         barProjection.scale(lengthScale(d.Value));
         return barProjection([d.Longitude, d.Latitude])[1];
       });
    }

//    bgscroll();
     setInterval(bgscroll, scrollSpeed);  
  })
})
Claudetta answered 17/7, 2015 at 15:51 Comment(0)
A
6

To clip off the bars at the horizon, we add a mask centered at the globe 2D center and with it's radius. Then we apply this mask only if the bottom edge crosses the horizon (by tracking the longitude).

Creating the mask

// get the center of the circle
var center = planetProjection.translate();
// edge point
var edge = planetProjection([-90, 90])
// radius
var r = Math.pow(Math.pow(center[0] - edge[0], 2) + Math.pow(center[1] - edge[1], 2), 0.5);

svg.append("defs")
    .append("clipPath")
    .append("circle")
    .attr("id", "edgeCircle")
    .attr("cx", center[0])
    .attr("cy", center[1])
    .attr("r", r)

var mask = svg.append("mask").attr("id", "edge")
mask.append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", "100%")
    .attr("height", "100%")
    .attr("fill", "white");
mask.append("use")
    .attr("xlink:href", "#edgeCircle")
    .attr("fill", "black");

Applying the mask

.... bars ....
.attr("mask", function (d) {
    // make the range from 0 to 360, so that it's easier to compare
    var longitude = Number(d.Longitude) + 180;
    // +270 => -90 => the position of the left edge when the center is at 0
    // -value because a rotation to the right => left edge longitude is reducing
    // 360 because we want the range from 0 to 360
    var startLongitude = 360 - ((longitudeScale(current) + 270) % 360);
    // the right edge is start edge + 180
    var endLongitude = (startLongitude + 180) % 360;
    if ((startLongitude < endLongitude && longitude > startLongitude && longitude < endLongitude) ||
        // wrap around
        (startLongitude > endLongitude && (longitude > startLongitude || longitude < endLongitude)))
        return null;
    else
        return "url(#edge)";
});

We could also do this by measuring the distance.


Fiddle - http://jsfiddle.net/gp3wvm8o/


enter image description here

Alumroot answered 23/7, 2015 at 12:40 Comment(0)
A
1

Just keep track of the range of visible longitudes and hide the bars if they are not in that range

.attr("display", function(d) {
    // make the range from 0 to 360, so that it's easier to compare
    var longitude = Number(d.Longitude) + 180;
    // +270 => -90 => the position of the left edge when the center is at 0
    // -value because a rotation to the right => left edge longitude is reducing
    // 360 because we want the range from 0 to 360
    var startLongitude = 360 - ((longitudeScale(current) + 270) % 360);
    // the right edge is start edge + 180
    var endLongitude = (startLongitude + 180) % 360;
    if ((startLongitude < endLongitude && longitude > startLongitude && longitude < endLongitude) ||
        // wrap around
        (startLongitude > endLongitude && (longitude > startLongitude || longitude < endLongitude)))
        return "block";
    else
        return "none";
})

Fiddle - http://jsfiddle.net/b12ryhda/

Alumroot answered 18/7, 2015 at 19:21 Comment(2)
Thank you. This is good idea, but i don't want to hide bars at once, but gradually, like in example i provide on topClaudetta
@Claudetta - have posted it as a separate answer since the core part is essentially different (display vs. mask)Alumroot
R
0

A simpler method that works with canvas would be to:

  1. Draw all the bars without clipping
  2. Draw the map
  3. Draw only the foreground bars (i.e., use clipping)

This clipping does not have to be down manually but can leverage the path.centroid method which respects the clipping set on the projections by clipAngle. Pseudo code might look like:

let projection = d3.geoOrthographic()
               .clipAngle(90)
               ...
let barProjection = d3.geoOrthographic()
               .clipAngle(90)
               ...

let path = d3.geoPath()
         .projection(projection)
         .context(canvasCtx)
let barPath = d3.geoPath()
         .projection(barProjection)

let renderBar = function(isBgLayer = false) {
    let barLengthAsScale = ...
    barProjection.scale(barLengthAsScale)
    let barStart, barEnd
    if (isBgLayer) {
        barStart = projection([ lon, lat ])
        barEnd = barProjection([ lon, lat ])
    } else {
        let geoJs = { type: 'Point', coordinates: [ lon, lat ] }
        barStart = path.centroid(geoJs)
        barEnd = barPath.centroid(geoJs)
    }
    // draw line from start to end using canvasCtx
};

let renderMap = function(topology) {
    // normal map drawing to canvas
};

// then to render a frame
renderBar(true)
renderMap(topoJsonTopology)
renderBar()

Some bars will be drawn twice, but with I've found that canvas is fast enough to keep up with the drawing and keep animations smooth with at least 200+ bars.

For an example, check out this code on GitHub and the live page.

Ringo answered 18/3, 2020 at 13:3 Comment(0)
C
0

I did it so many times ago then forget and recently find working example on JSFiffle here.

/*
 * Based on http://codepen.io/teetteet/pen/Dgvfw
 */
var width = 400;
var height = 400;
var scrollSpeed = 50;
var current = 180;

var longitudeScale = d3.scale.linear()
  .domain([0, width])
  .range([-180, 180]);

var planetProjection = d3.geo.orthographic()
  .scale(200)
  .rotate([longitudeScale(current), 0])
  .translate([width / 2, height / 2])
  .clipAngle(90);
var barProjection = d3.geo.orthographic()
  .scale(200)
  .rotate([longitudeScale(current), 0])
  .translate([width / 2, height / 2])
  .clipAngle(90);

var path = d3.geo.path()
  .projection(planetProjection);

var svg = d3.select("body").append("svg")
  .attr("width", width)
  .attr("height", height);

// mask creation
var center = planetProjection.translate();   // get the center of the circle
var edge = planetProjection([-90, 90]); // edge point 
var r = Math.pow(Math.pow(center[0] - edge[0], 2) + Math.pow(center[1] - edge[1], 2), 0.5); // radius

svg.append("defs")
    .append("clipPath")
    .append("circle")
    .attr("id", "edgeCircle")
    .attr("cx", center[0])
    .attr("cy", center[1])
    .attr("r", r)
var mask = svg.append("mask").attr("id", "edge")
mask.append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", "100%")
    .attr("height", "100%")
    .attr("fill", "white");
mask.append("use")
    .attr("xlink:href", "#edgeCircle")
    .attr("fill", "black");

d3.json("https://unpkg.com/[email protected]/world/110m.json", function(error, world) {
  if (error) throw error;

  var planet = svg.append("path")
    .datum(topojson.feature(world, world.objects.land))
    .attr("class", "land")
    .attr("d", path);

  d3.csv("https://dl.dropboxusercontent.com/s/3tseu6lxyl715pt/cities.csv?dl=1", function(error, data) {
    if (error) throw error;

    var max = d3.max(data, function(d) {
      return parseInt(d.Value);
    })

    var lengthScale = d3.scale.linear()
      .domain([0, max])
      .range([200, 250])

      var bars = svg.selectAll(".bar")
        .data(data)
        .enter()
        .append("line")
        .attr("class", "bar")
        .attr("stroke", "red")
        .attr("stroke-width", "2");

    function bgscroll() {

      current += 1;

      planetProjection.rotate([longitudeScale(current), 0]);
      barProjection.rotate([longitudeScale(current), 0]);

      planet.attr("d", path);

      bars.attr("x1", function(d) {
         return planetProjection([d.Longitude, d.Latitude])[0];
       }).attr("y1", function(d) {
         return planetProjection([d.Longitude, d.Latitude])[1];
       }).attr("x2", function(d) {
         barProjection.scale(lengthScale(d.Value));
         return barProjection([d.Longitude, d.Latitude])[0];
       }).attr("y2", function(d) {
         barProjection.scale(lengthScale(d.Value));
         return barProjection([d.Longitude, d.Latitude])[1];
       }).attr("mask", function (d) {
        // make the range from 0 to 360, so that it's easier to compare
        var longitude = Number(d.Longitude) + 180;
        // +270 => -90 => the position of the left edge when the center is at 0
        // -value because a rotation to the right => left edge longitude is reducing
        // 360 because we want the range from 0 to 360
        var startLongitude = 360 - ((longitudeScale(current) + 270) % 360);
        // the right edge is start edge + 180
        var endLongitude = (startLongitude + 180) % 360;
        if ((startLongitude < endLongitude && longitude > startLongitude && longitude < endLongitude) ||
            // wrap around
            (startLongitude > endLongitude && (longitude > startLongitude || longitude < endLongitude)))
            return null;
        else
            return "url(#edge)";
    });
    }

//    bgscroll();
     setInterval(bgscroll, scrollSpeed);  
  })
})
body {
  background: #fcfcfa;
}
.stroke {
  fill: none;
  stroke: #000;
  stroke-width: 3px;
}
.fill {
  fill: #fff;
}
.graticule {
  fill: none;
  stroke: #777;
  stroke-width: .5px;
  stroke-opacity: .5;
}
.land {
  fill: #222;
}

.boundary {
  fill: none;
  stroke: #fff;
  stroke-width: .5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
Claudetta answered 12/11, 2022 at 15:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.