3

When having a marker on a globe, the marker lays flat on the surface.

Although there might be trouble the moment the marker rotates out of sight; is there a way to give this marker height?

Instead of a dot on the surface of the globe, I'm trying to get a dot on a needle, sticking out a little bit above the surface of the globe.

Not this:

-----o-----

But this:

     o
_____|_____

Mimicking one of those:

Push-pin needle

Currently the marker is drawn as follows:

const width = 220;
const height = 220;
const config = {
  speed: 0.025,
  verticalTilt: 10,
  horizontalTilt: -10
}
let locations = [];
const svg = d3.select('svg')
  .attr('width', width).attr('height', height);
const markerGroup = svg.append('g');
const projection = d3.geoOrthographic();
const initialScale = projection.scale(99.5).translate([100, 100]);
const path = d3.geoPath().projection(projection);
const center = [width / 2, height / 2];

drawGlobe();
drawGraticule();
enableRotation();

const locationData = [
    {"latitude": -33.8688, "longitude": 151.2093}
];

function drawGlobe() {
  d3.queue()
    .defer(d3.json, 'https://raw.githubusercontent.com/cszang/dendrobox/master/data/world-110m2.json')
    .await((error, worldData) => {
      svg.selectAll(".segment")
        .data(topojson.feature(worldData, worldData.objects.countries).features)
        .enter().append("path")
        .attr("class", "segment")
        .attr("d", path)
        .style("stroke", "silver")
        .style("stroke-width", "1px")
        .style("fill", (d, i) => 'silver')
        .style("opacity", ".5");
      locations = locationData;
      drawMarkers();
    });
}

function drawGraticule() {
  const graticule = d3.geoGraticule()
    .step([10, 10]);

  svg.append("path")
    .datum(graticule)
    .attr("class", "graticule")
    .attr("d", path)
    .style("fill", "#fff")
    .style("stroke", "#ececec");
}

function enableRotation() {
  d3.timer(function(elapsed) {
    projection.rotate([config.speed * elapsed - 120, config.verticalTilt, config.horizontalTilt]);
    svg.selectAll("path").attr("d", path);
    drawMarkers();
  });
}

function drawMarkers() {
  const markers = markerGroup.selectAll('circle')
    .data(locations);
  markers
    .enter()
    .append('circle')
    .merge(markers)
    .attr('cx', d => projection([d.longitude, d.latitude])[0])
    .attr('cy', d => projection([d.longitude, d.latitude])[1])
    .attr('fill', d => {
      const coordinate = [d.longitude, d.latitude];
      gdistance = d3.geoDistance(coordinate, projection.invert(center));
      return gdistance > 1.55 ? 'none' : 'tomato';
    })

    // 1.57

    .attr('r', 3);

  markerGroup.each(function() {
    this.parentNode.appendChild(this);
  });
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<svg></svg>
Jordy
  • 347
  • 2
  • 11

1 Answers1

2

Using this answer as inspiration, you can create a second projection, equivalent to the first one, but with a larger scale value. That will project a point directly above the actual point on the globe, as if it was hanging above it. This allows you to draw a line from the ground up, and look at it from all angles. It even works with your hide marker logic.

const width = 220;
const height = 220;
const config = {
  speed: 0.025,
  verticalTilt: 10,
  horizontalTilt: -10
}
let locations = [];
const svg = d3.select('svg')
  .attr('width', width).attr('height', height);
const markerGroup = svg.append('g');
const projection = d3.geoOrthographic()
  .scale(99.5)
  .translate([100, 100]);
const markerProjection = d3.geoOrthographic()
  .scale(108)
  .translate(projection.translate());

const path = d3.geoPath().projection(projection);
const center = [width / 2, height / 2];

drawGlobe();
drawGraticule();
enableRotation();

const locationData = [
    {"latitude": -33.8688, "longitude": 151.2093}
];

function drawGlobe() {
  d3.queue()
    .defer(d3.json, 'https://raw.githubusercontent.com/cszang/dendrobox/master/data/world-110m2.json')
    .await((error, worldData) => {
      svg.selectAll(".segment")
        .data(topojson.feature(worldData, worldData.objects.countries).features)
        .enter().append("path")
        .attr("class", "segment")
        .attr("d", path)
        .style("stroke", "silver")
        .style("stroke-width", "1px")
        .style("fill", (d, i) => 'silver')
        .style("opacity", ".5");
      locations = locationData;
      drawMarkers();
    });
}

function drawGraticule() {
  const graticule = d3.geoGraticule()
    .step([10, 10]);

  svg.append("path")
    .datum(graticule)
    .attr("class", "graticule")
    .attr("d", path)
    .style("fill", "#fff")
    .style("stroke", "#ececec");
}

function enableRotation() {
  d3.timer(function(elapsed) {
    projection.rotate([config.speed * elapsed - 120, config.verticalTilt, config.horizontalTilt]);
    markerProjection.rotate(projection.rotate());
    svg.selectAll("path").attr("d", path);
    drawMarkers();
  });
}

function drawMarkers() {
  const markers = markerGroup.selectAll('.marker')
    .data(locations);
  const newMarkers = markers
    .enter()
    .append('g')
    .attr('class', 'marker')
  
  newMarkers.append("line");
  
  newMarkers.append("circle")
    .attr("r", 3);
  
  newMarkers.merge(markers)
    .selectAll("line")
    .attr("x1", d => projection([d.longitude, d.latitude])[0])
    .attr("y1", d => projection([d.longitude, d.latitude])[1])
    .attr("x2", d => markerProjection([d.longitude, d.latitude])[0])
    .attr("y2", d => markerProjection([d.longitude, d.latitude])[1])
    .attr('stroke', d => {
      const coordinate = [d.longitude, d.latitude];
      gdistance = d3.geoDistance(coordinate, markerProjection.invert(center));
      return gdistance > (Math.PI / 2) ? 'none' : 'black';
    })
  
  newMarkers
    .merge(markers)
    .selectAll("circle")
    .attr('cx', d => markerProjection([d.longitude, d.latitude])[0])
    .attr('cy', d => markerProjection([d.longitude, d.latitude])[1])
    .attr('fill', d => {
      const coordinate = [d.longitude, d.latitude];
      gdistance = d3.geoDistance(coordinate, markerProjection.invert(center));
      return gdistance > (Math.PI / 2) ? 'none' : 'tomato';
    })

  markerGroup.each(function() {
    this.parentNode.appendChild(this);
  });
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<svg></svg>
Ruben Helsloot
  • 12,582
  • 6
  • 26
  • 49
  • Beautifully done. Also, it seems I no longer need a separate json file holding the locationData, when using the code in your answer, do I? – Jordy Nov 04 '20 at 21:28
  • Not if the values are simply hardcoded, no. But if you have many markers (15+), I'd put it in a JSON file because it can clutter your code. But that's just preference – Ruben Helsloot Nov 04 '20 at 21:30