【问题标题】:Lines on eclipse map not drawing to scale in d3.js map日食地图上的线条未在 d3.js 地图中按比例绘制
【发布时间】:2026-01-05 16:00:01
【问题描述】:

我有以下美国地图,显示了日食发生时日食带内或附近的每个县的当前预测天气状况。我希望能够显示指示全食区的北部、南部和中心线的线条,但我无法让它们正确缩放。

显示的红线应该是北线,但它没有按比例绘制。

这是代码,有什么想法吗?最底部的那一行

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

是我试图画线的地方。

谢谢

<!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>

【问题讨论】:

    标签: d3.js topojson map-projections


    【解决方案1】:

    您的地图按预期运行,但您可以根据需要使其工作。

    问题

    此文件:https://d3js.org/us-10m.v1.json已经投影(它由二维平面上的 x,y 坐标和任意距离单位组成)。

    您的线没有投影(它由代表 3d 地球上的点的经度/纬度对组成)。

    最终,您的要素位于两个不同的坐标系中,这会导致问题,因为您以相同的方式从两个不同的坐标系中绘制这些点。

    确定地理数据是否已在 d3 中投影

    要确定您的数据是否已经投影,您可以查看:

    • geoPath 是否使用空投影?
    • 坐标对是有效的经纬度对吗?

    查看此文件,您可以看到没有为地理路径分配投影(这将像:d3.geoPath().projection(projection))。

    此外,您可以看到 topojson 坐标大致在[0,0][960,600] 的范围内(转换为 geojson 以查看普通坐标)。这些不是有效的经度/纬度对([+/-180,+/-90])。

    但是,您的线要素是使用经度/纬度对绘制的。这个特征不是投影的(如果用一个特定的球体来表示地球,在WGS84中可能会说是“投影”,但实际上,这个上下文中的WGS84只是表示一个基准面,而不是一个投影。供参考,WGS84是d3 在从 long/lats 转换为平面上的点时使用的基准/参考椭球体。

    发生了什么?

    空投影将采用坐标 [x,y] 并返回相同的坐标。 因此,快速放弃已经投影的特征是在查看器 (mapshaper.org) 中查看特征,如果特征是倒置的,那么您就有了投影数据。发生这种情况是因为 svg 坐标空间在顶部放置零,而经度/纬度对在赤道放置零,-90 再往下。

    在绘制地图时,您要绘制一条线:

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

    当您使用空投影时,这条线只是从距离顶部向下 45 像素、距离左侧 137 像素的点到向下 13 像素、距离左侧 37 像素的点。 此外,美国大陆的经度是负数,但当然,它们根本不会出现在您的地图上

    解决方案

    您需要对数据使用一致的预测。为此,您可以执行以下操作之一:

    1. 找出用于 topojson 的投影并将您的坐标转换为该投影,这样您就可以在线的坐标数组中使用转换后的坐标,并用空投影“投影”线。

    2. 找出用于 topojson 的投影并使用 d3.geoProjection 对其进行模拟,此解决方案将使用带有空投影的 geoPath 用于 topojson(目前),以及带有非常具体的投影的 geoPath将直线转换到同一个坐标空间

    3. 取消投影 topojson 并对两个特征(线和 topojson)使用相同的投影。

    4. 找到一个未投影的美国topojson/geojson/etc,并对两个特征(线和topojson)使用相同的投影。

    鉴于您拥有的源文件可能是由 Mike Bostock 制作的,并且他可能使用与他在 d3 中实现的相同的投影公式来创建文件,我们可能会很幸运并弄清楚我们可以使用什么 d3.geoProjection将我们的坐标投影到与 topojson 相同的坐标空间(上面的选项 2)。

    默认 d3.geoAlbers 投影以美国为中心,与大多数其他 d3 投影相比是一个例外。默认情况下,这可能包含用于创建 topojson 的关键投影参数,即标准平行线、中心点和旋转。

    但是,d3 投影的默认比例预计投影平面为 960 x 500。 d3 投影的默认平移假定平移为 [960/2,500/2]。如果我们更改这些以适应 600 像素高的特征,我们很幸运,并且可以将坐标投影到非常相似的坐标空间,即使不是相同的坐标空间:

    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.
    

    这给了我们一个成功的重叠。 Here 是覆盖在投影 topojson 上的未投影数据 topojson。这使我们能够通过将线投影到未投影的特征之上来实现您的目标,请参阅here(我没有您的数据 tsv,因此等值线只是噪声)。

    您还可以使用 geoTransform 而不是 geoProjection 来缩放已经投影的数据,请参阅answer。这将允许您选择不需要 960 像素 x 600 像素来显示所有功能的 svg 尺寸。

    解决方案警告

    遗憾的是,选项 1,2 和 3 要求您了解 topojson 中的地理数据是如何投影的,以便您可以模拟或反转它。 如果您没有此信息,则无法使用这些选项。每当处理使用不同空间参考系统(SRS,或坐标参考系统,CRS)的地理数据时,都是如此。

    在这些情况下留下选项 4。可以在block 中找到美国的未投影 topojson(以及良好的投影参数)。这是原始的topojsonHere 是您的线路的演示(此 topojson 数据没有县 ID,因此会破坏您的 chloropleth)。

    【讨论】:

    • 谢谢安德鲁。现在正在消化这个。
    • 安德鲁,如果我知道例如 topojson 是使用 d3.geoAlbersUsa 投影的,我也可以这样翻译我的坐标,不是吗?另外,它有多少种不同的投影方式?难道我不能尝试直到找到合适的吗?顺便说一句,这对整个事情来说非常新。
    • 如果使用与 d3.geoAlbersUsa 相同的参数进行投影,则使用该投影投影点可能会导致对齐(我相信我尝试过一次不成功,但我可能记错了) .如果您无法以这种方式对齐它们,那么考虑到 albers 是使用居中和旋转坐标以及两个标准纬线以及一个比例构建的,可能会产生很多猜测。
    • 我想我已经使用 d3 albers 非常紧密地对齐了投影(albersUsa 可能会起作用,但如果终点比美国大陆更靠近阿拉斯加,它的复合投影方面可能会产生意想不到的后果)。
    • 还在消化...非常感谢您的努力