1

I have the following map of the US showing the currently predicted weather conditions for each county in or near the eclipse zone at the time of the eclipse. I want to be able to show the lines indicating the northern, southern and center lines of the zone of totality but I can't get them to scale properly.

The red line thats show is supposed to be northern line but it's not drawing to scale.

Map of weather in the eclipse zone

Here's the code, any ideas? The line at the very bottom

svg.append('path').datum(feature.geometry).attr('class', 'mine').attr("d", path2);

is where I'm trying to draw the line.

Thanks

<!DOCTYPE html>
<meta charset="utf-8">
<style>

    .counties {
        fill: none;
        stroke: #ddd;
    }

    .states {
        fill: none;
        stroke: #000;
        stroke-linejoin: round;
    }

    .mine {
        fill: #f00;
        stroke: #f00;
        stroke-linejoin: round;
    }

</style>
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script>

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

    var unemployment = d3.map();

    var path = d3.geoPath();
    var path2 = d3.geoPath();

    var x = d3.scaleLinear()
        .domain([1, 10])
        .rangeRound([600, 860]);

    var color = d3.scaleThreshold()
        .domain(d3.range(2, 10))
        .range(d3.schemeBlues[9]);

    var g = svg.append("g")
        .attr("class", "key")
        .attr("transform", "translate(0,40)");

    g.selectAll("rect")
        .data(color.range().map(function (d) {
            d = color.invertExtent(d);
            if (d[0] == null) d[0] = x.domain()[0];
            if (d[1] == null) d[1] = x.domain()[1];
            return d;
        }))
        .enter().append("rect")
        .attr("height", 8)
        .attr("x", function (d) {
            return x(d[0]);
        })
        .attr("width", function (d) {
            return x(d[1]) - x(d[0]);
        })
        .attr("fill", function (d) {
            return color(d[0]);
        });

    g.append("text")
        .attr("class", "caption")
        .attr("x", x.range()[0])
        .attr("y", -6)
        .attr("fill", "#000")
        .attr("text-anchor", "start")
        .attr("font-weight", "bold")
        .text("Forecast Conditions");

    g.call(d3.axisBottom(x)
        .tickSize(13)
        .tickFormat(function (x, i) {
            //return i ? x : x;
            if (i == 0)
                return "Clear";
            else if (i == 7)
                return "Rain";
            else
                return "";
        })
        .tickValues(color.domain()))
        .select(".domain")
        .remove();

    d3.queue()
        .defer(d3.json, "https://d3js.org/us-10m.v1.json")
        .defer(d3.tsv, "unemployment.tsv", function (d) {
            var forecast = {
                forecastNum: d.forecastNum,
                name: d.name,
                forecastText: d.forecastText
            };
            unemployment.set(d.id, forecast);
        })
        .await(ready);

    function ready(error, us) {
        if (error) throw error;

        var feature = {
            type: "Feature",
            properties: {},
            geometry: {
                type: "LineString",
                coordinates: [
                    [136.9522, 45.1172],
                    [36.8017, 13.6517],
                ]
            }
        };
        svg.append("g")
            .attr("class", "counties")
            .selectAll("path")
            .data(topojson.feature(us, us.objects.counties).features)
            .enter().append("path")
            .attr("fill", function (d) {
                return color(d.forecastNum = unemployment.get(d.id).forecastNum);
            })
            .attr("d", path)
            .append("title")
            .text(function (d) {
                var fc = unemployment.get(d.id);

                var s = fc.name + " " + fc.forecastText;
                console.log('[' + s + "]");
                return s;
            });

        svg.append("path")
            .datum(topojson.mesh(us, us.objects.states, function (a, b) {
                return a !== b;
            }))
            .attr("class", "states")
            .attr("d", path)
        ;
//        svg.append("circle").attr("r",50).attr("transform", function() {return "translate(" + projection([-75,43]) + ")";});
        svg.append('path').datum(feature.geometry).attr('class', 'mine').attr("d", path2);

    }

</script>
Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
JJF
  • 2,681
  • 2
  • 18
  • 31

1 Answers1

0

Your map is performing as expected but you can make it work as desired.

The Problem

This file: https://d3js.org/us-10m.v1.json is already projected (it consists of x,y coordinates on a 2d plane with an arbitrary unit of distance).

Your line is not projected (it consists of longitude/latitude pairs which represent points on a 3d globe).

Ultimately, you have features in two different coordinate systems which causes problems because you are plotting these points from two different coordinate systems the same way.

Determining if Geographic Data is Already Projected in d3

To determine if your data is already projected, you can check to see:

  • does the geoPath use a null projection?
  • are the coordinate pairs valid latitude longitude pairs?

Looking at this file, you can see that no projection is assigned to the geopath (this would be done like: d3.geoPath().projection(projection)).

Also, you can see that the topojson coordinates are roughly within the bounds of [0,0] and [960,600] (convert to geojson to see plain coordinates). These are not valid longitude/latitude pairs (which are [+/-180,+/-90]).

Your line feature, however, is plotted with longitude/latitude pairs. This feature is not projected (if using a specific spheroid to represent the earth, it might be said to be "projected" in WGS84, but in reality, WGS84 in this context just represents a datum, not a projection. For reference, WGS84 is the datum/reference spheroid used by d3 when it converts from long/lats to points on a plane).

What is Happening?

A null projection will take in coordinates [x,y] and return those very same coordinates. As such, a quick give away of already projected features is to view the features in a viewer (mapshaper.org), if the feature is upside down, then you have projected data. This happens because svg coordinate space places zero at the top, while longitude/latitude pairs place zero at the equator, with -90 further down.

When drawing your map, you have a line you wish to draw:

       coordinates: [
            [136.9522, 45.1172],
            [36.8017, 13.6517],
        ]

As you are using a null projection, the line simply goes from a point 45 pixels down from the top and 137 pixels from the left, to a point 13 pixels down and 37 pixels from the left. Also, longitudes in the continental US are negative, but of course, then they won't appear on your map as it is at all.

Solution

You need to use consistent projections for your data. To do so you can do one of:

  1. Figure out the projection used for the topojson and convert your coordinates to that projection so you can use the converted coordinates in your line's coordinate array and "project" the line with a null projection too.

  2. Figure out the projection used for the topojson and emulate it with a d3.geoProjection, this solution will use a geoPath with a null projection for the topojson (as currently), and a geoPath with a pretty specific projection to convert the line to the same coordinate space

  3. Unproject the topojson and use the same projection for both features (line and topojson).

  4. Find an unprojected topojson/geojson/etc of the US and use the same projection for both features (line and topojson).

Given that the source file you have is likely made by Mike Bostock, and he likely used the same projection formula to create the file as he implemented in d3, we might get lucky and figure out what d3.geoProjection we could use to project our coordinates to the same coordinate space as the topojson (option 2 above).

The default d3.geoAlbers projection is centered on the US, an exception compared with most other d3 projections. This might contain by default the key projection parameters used to create the topojson, namely standard parallels, centering point, and rotation.

However, the default scale of a d3 projection anticipates a 960 x 500 projection plane. The default translate of a d3 projection assumes a translate of [960/2,500/2]. If we change these to accommodate a 600 pixel tall feature, we get lucky and can project coordinates to a very similar coordinate space, if not the same coordinate space:

var projection = d3.geoAlbers();
var scale = d3.geoAlbers().scale(); // get the default scale

projection.scale(scale * 1.2) // adjust for 600 pixels tall
  .translate([960/2,600/2]    // adjust for 600 pixels tall here too.

var geoPath = d3.geoPath.projection(projection)  // use the projection to draw features.

This gives us a successful overlap. Here is a unprojected data topojson overlaid on the projected topojson. This allows us to achieve your goal by projecting the line on top of the unprojected features, see here (I don't have your data tsv, so the choropleth is just noise).

You can also scale already projected data using a geoTransform rather than a geoProjection, see this answer. This will allow you to choose svg sizes that don't require 960 px by 600 px to show all features.

Solution Caveat

Sadly, options 1,2 and 3 require you to know how the geographic data in the topojson was projected so that you can emulate it or reverse it. If you do not have this information, these options are not possible. This is true whenever dealing with geographic data that use different spatial reference systems (SRS, or coordinate reference systems, CRS).

That leaves option 4 in these cases. A unprojected topojson for the States can be found here in this block (along with good projection parameters). Here is the raw topojson. Here is a demonstration of your line (This topojson data does not have county ids however and would therefore break your chloropleth).

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • Thanks Andrew. Digesting this now. – JJF Aug 16 '17 at 16:59
  • Andrew, if I knew for example the topojson was projected using d3.geoAlbersUsa I could translate my coordinates that way too no? Also, how many different ways could it be projected? Couldn't I just try until I found the right one? Very Very new to this whole thing BTW. – JJF Aug 16 '17 at 17:03
  • If it was projected using the same parameters as a d3.geoAlbersUsa, then projecting the point with that projection might lead to alignment (I believe I tried this once unsuccessfuly, but I could be mis-remembering). If you can't align them this way, then it could be a lot of guessing given that an albers is built with centering and rotation coordinates and two standard parallels, as well as a scale. – Andrew Reid Aug 16 '17 at 17:15
  • I think I've aligned the projections very closely using a d3 albers (albersUsa might work, but the composite projection aspect of it might have unintended consequences if the end point is closer to Alaska than the continental US). – Andrew Reid Aug 16 '17 at 18:05
  • Still digesting... thank you so much for your effort – JJF Aug 16 '17 at 19:37
  • Looking at the result and looking at other eclipse maps, I thought I would quickly add - the two points you have selected don't create a very representative line as eclipses don't move along great circles like a d3 geoPath. The curvature of the Albers seems to compound this. Instead you could use a more detailed geojson for the trajectory. Take a look at this [bl.ock](https://bl.ocks.org/andrew-reid/f37c408ac047c1d735b2040b7453406f). Raw geojson [here](https://gist.githubusercontent.com/Andrew-Reid/f37c408ac047c1d735b2040b7453406f/raw/Central-Line.geojson). – Andrew Reid Aug 16 '17 at 21:54
  • Also, d3.geoAlbersUsa will work fine too with that data, it won't change the appearance of what is there, but will allow you to project points to Alaska or Hawaii. – Andrew Reid Aug 16 '17 at 22:07
  • Thanks Andrew. I have a lot more points from Nasa, about 90. I think that should fill it in don't you? – JJF Aug 17 '17 at 00:03
  • Should work, if that fails, the geojson in my comment [above](https://stackoverflow.com/questions/45711745/lines-on-eclipse-map-not-drawing-to-scale-in-d3-js-map/45718633?noredirect=1#comment78405022_45718633) has a ridiculous number of points for what is needed. – Andrew Reid Aug 17 '17 at 01:42