You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

D3.js中俄罗斯联邦区GeoJSON多边形反转仅西伯利亚区异常求助

解决俄罗斯联邦区地图的异常线条问题

问题分析

你遇到的西伯利亚联邦区左上角异常线条,主要是因为全局多边形反转变换未适配该区域跨日界线的特殊几何结构,导致路径绘制时出现了多余的连接线段。

解决方案

放弃全局统一的多边形反转逻辑,改为针对每个GeoJSON要素的几何形状单独处理缠绕顺序,确保D3能正确识别填充区域。同时优化投影参数,避免坐标偏移引发的绘制异常。

步骤1:修正多边形缠绕顺序

D3遵循右手定则判断多边形内外区域,若GeoJSON要素的坐标顺序错误,会导致填充区域反向。可通过d3.geoArea判断面积正负,反转异常多边形的坐标:

const fixWindingOrder = (feature) => {
  const clonedFeature = JSON.parse(JSON.stringify(feature)); // 深拷贝避免修改原数据
  if (clonedFeature.geometry.type === 'Polygon') {
    if (d3.geoArea(clonedFeature) < 0) {
      clonedFeature.geometry.coordinates[0] = clonedFeature.geometry.coordinates[0].reverse();
    }
  } else if (clonedFeature.geometry.type === 'MultiPolygon') {
    clonedFeature.geometry.coordinates.forEach(polygon => {
      if (d3.geoArea({ type: 'Polygon', coordinates: polygon }) < 0) {
        polygon[0] = polygon[0].reverse();
      }
    });
  }
  return clonedFeature;
};

步骤2:优化投影参数

微调投影的旋转角度并添加裁剪限制,避免跨日界线的路径异常:

const projection = d3geo
  .geoConicConformal()
  .scale(300)
  .center([54, 44])
  .rotate([-100, 0]) // 调整旋转角度适配西伯利亚区域
  .clipAngle(180); // 限制绘制范围,避免无效坐标

修改后的完整组件代码

function Map({
  statistics,
  style = {},
  colorsForScale = ["#F4F3EE", "#969AFF", "#000"]
}) {
  useEffect(() => {}, [statistics]);

  const [regionDescription, setRegionDescription] = useState("");
  const [regionValue, setRegionValue] = useState("");
  const { language } = useLanguage();
  const [translations, setTranslations] = useState([]);

  useEffect(() => {
    getTranslations()
      .then((data) => setTranslations(data))
      .catch(() => {})
      .finally(() => {});
  }, []);

  // 优化后的投影配置
  const projection = d3geo
    .geoConicConformal()
    .scale(300)
    .center([54, 44])
    .rotate([-100, 0])
    .clipAngle(180);

  // 修正多边形缠绕顺序工具函数
  const fixWindingOrder = (feature) => {
    const clonedFeature = JSON.parse(JSON.stringify(feature));
    if (clonedFeature.geometry.type === 'Polygon') {
      if (d3.geoArea(clonedFeature) < 0) {
        clonedFeature.geometry.coordinates[0] = clonedFeature.geometry.coordinates[0].reverse();
      }
    } else if (clonedFeature.geometry.type === 'MultiPolygon') {
      clonedFeature.geometry.coordinates.forEach(polygon => {
        if (d3.geoArea({ type: 'Polygon', coordinates: polygon }) < 0) {
          polygon[0] = polygon[0].reverse();
        }
      });
    }
    return clonedFeature;
  };

  const path = d3geo.geoPath().projection(projection);
  const min = 0;
  const max = 100;

  const getScale = () => [min, (min + max) / 2, max];
  const colorScale = d3.scaleLinear(getScale(), colorsForScale);

  const mapElements = useMemo(() => {
    if (statistics.length > 0) {
      return geoData.features.map((d) => {
        const fixedFeature = fixWindingOrder(d);
        const pathD = path(fixedFeature);
        if (!pathD) return null;

        const relevantStatistics = statistics.find(
          (item) => item.name === d.properties.name
        );
        const color = relevantStatistics ? colorScale(relevantStatistics.value) : "lightgrey";

        return (
          <path
            key={`map-element-${d.properties.name}`}
            name={d.properties.name}
            d={pathD}
            fill={color}
            stroke="#0e1724"
            strokeWidth="0.5"
            strokeOpacity="0.5"
            opacity="0.9"
            onMouseEnter={(e) => {
              d3.select(e.target).attr("opacity", 1);
              setRegionDescription(
                language === "ru" 
                  ? relevantStatistics.name 
                  : translations.find(t => t.name_ru === relevantStatistics.name)?.name_en || relevantStatistics.name
              );
              setRegionValue(Math.round(relevantStatistics.value));
            }}
            onMouseOut={(e) => {
              d3.select(e.target).attr("opacity", 0.9);
              setRegionDescription("");
              setRegionValue("");
            }}
          />
        );
      });
    } else {
      return <p>No map data.</p>;
    }
  }, [geoData, statistics, language, translations]);

  const mapTooltip = useRef(null);
  const setTooltipPosition = (x, y) => {
    if (!mapTooltip.current) return;
    let newX = x - mapTooltip.current.offsetWidth / 2;
    newX = Math.max(newX, 0);
    mapTooltip.current.style.transform = `translate(${newX}px, ${y + 12}px)`;
  };

  if (statistics) {
    return (
      <div
        onPointerMove={(ev) => setTooltipPosition(ev.clientX, ev.clientY)}
        style={{ position: "relative", display: "inline-block", ...style }}
      >
        <svg className="map">
          <g className="map">{mapElements}</g>
        </svg>
        <div
          className={`map-tooltip ${!regionDescription && "hidden"}`}
          ref={mapTooltip}
        >
          {regionDescription && (
            <>
              <h3>{regionDescription}</h3>
              <h1>{regionValue}%</h1>
            </>
          )}
        </div>
      </div>
    );
  }
  return <> </>;
}

export default Map;

额外提示

  • 用GeoJSON在线工具检查西伯利亚联邦区的几何结构,确认无无效坐标点
  • 若线条问题仍存在,可尝试调整投影的scale参数放大地图,观察细节排查坐标异常

内容的提问来源于stack exchange,提问作者neuroL

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 17:57:12