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

Highcharts正射投影地球仪地图相关技术问题咨询

现有代码

在线示例链接

查看完整代码

HTML

<script src="https://code.highcharts.com/maps/highmaps.js"></script>
<script src="https://code.highcharts.com/maps/modules/data.js"></script>
<script src="https://code.highcharts.com/maps/modules/exporting.js"></script>
<script src="https://code.highcharts.com/maps/modules/accessibility.js"></script>

<div class="globe-container">
  <div class="circle"></div>
  <div id="globe"></div>
</div>

CSS

.globe-container {
  position: absolute;
  z-index: 0;
  overflow: hidden;
  width: 913px;
  height: 506px;
}

.circle {
  width: 873px;
  height: 873px;
  border-radius: 50%;
  position: absolute;
  top: 0;
  right: 0;
  background: linear-gradient(
    118.16deg,
    #be84cf 6.43%,
    #56c0f5 61.63%,
    #52fbff 100%
  );
  opacity: 0.2;
  z-index: 0;
}

#globe {
  border-radius: 50%;
  height: 750px;
  width: 750px;
  margin-top: 60px;
  margin-left: 100px;
}

JavaScript

(完整数据见在线示例,以下为核心逻辑代码)

function initializeGlobe() {
  const self = this;
  const getGraticule = () => {
    const data = [];

    // Meridians
    for (let x = -180; x <= 180; x += 15) {
      data.push({
        geometry: {
          type: "LineString",
          coordinates:
            x % 90 === 0
              ? [
                  [x, -90],
                  [x, 0],
                  [x, 90]
                ]
              : [
                  [x, -80],
                  [x, 80]
                ]
        }
      });
    }

    // Latitudes
    for (let y = -90; y <= 90; y += 10) {
      const coordinates = [];
      for (let x = -180; x <= 180; x += 5) {
        coordinates.push([x, y]);
      }
      data.push({
        geometry: {
          type: "LineString",
          coordinates
        },
        lineWidth: y === 0 ? 2 : undefined
      });
    }

    return data;
  };

  this.chart = Highcharts.mapChart("globe", {
    chart: {
      map: topology,
      backgroundColor: "#dbf4fe",
      margin: 0,
      padding: 0,
      spacing: [0, 0, 0, 0]
    },
    accessibility: {
      enabled: false
    },
    credits: {
      enabled: false
    },

    title: {
      text: null
    },

    legend: {
      enabled: false
    },

    mapNavigation: {
      enabled: true,
      enableDoubleClickZoomTo: false
    },

    mapView: {
      maxZoom: 4,
      projection: {
        name: "Orthographic",
        rotation: [50, -20]
      },
      animation: true,
      zoom: 3
    },

    tooltip: {
      enabled: false
    },

    series: [
      {
        name: "Graticule",
        id: "graticule",
        type: "mapline",
        data: getGraticule(),
        enableMouseTracking: false
      },
      {
        data: countries,
        joinBy: "name",
        name: "",
        color: "#549ee2",
        borderColor: "#75aee1",
        cursor: "pointer",
        events: {
          click: (event) => {
            event.srcElement.point.select();
            event.srcElement.point.zoomTo();
          }
        },

        states: {
          normal: {
            animation: false
          },
          inactive: {
            color: "#75aee1",
            borderColor: "#75aee1",
            animation: {
              duration: 200
            }
          },
          select: {
            color: "#131660",
            borderColor: "#75aee1",
            enabled: true
          },
          hover: {
            color: "#131660",
            borderColor: "#75aee1",
            animation: {
              duration: 200
            }
          }
        },
        dataLabels: {
          enabled: false
        }
      }
    ]
  });

  const renderSea = () => {
    let verb = "animate";
    if (!this.chart.sea) {
      this.chart.sea = this.chart.renderer
        .circle()
        .attr({
          fill: {
            radialGradient: {
              cx: 0.4,
              cy: 0.4,
              r: 1
            },
            stops: [[0, "#dbf4fe"]]
          },
          zIndex: 0
        })
        .add(this.chart.get("graticule").group);
      verb = "attr";
    }

    const bounds = this.chart.get("graticule").bounds,
      p1 = this.chart.mapView.projectedUnitsToPixels({
        x: bounds.x1,
        y: bounds.y1
      }),
      p2 = this.chart.mapView.projectedUnitsToPixels({
        x: bounds.x2,
        y: bounds.y2
      });
    this.chart.sea[verb]({
      cx: (p1.x + p2.x) / 2,
      cy: (p1.y + p2.y) / 2,
      r: Math.min(p2.x - p1.x, p1.y - p2.y) / 2
    });
  };

  renderSea();
  Highcharts.addEvent(this.chart, "redraw", renderSea);
  this.rotateGlobe(this.chart);
}

function rotateGlobe(chart) {
  const interval = setInterval(() => {
    const rotation = chart.mapView.projection.options.rotation;
    rotation[0] += 5;
    if (rotation[0] === 355) {
      clearInterval(interval);
    }
    chart.update(
      {
        mapView: {
          projection: {
            rotation
          }
        }
      },
      void 0,
      void 0,
      false
    );
  }, 1);

  setTimeout(() => {
    clearInterval(interval);
  }, 10000);
}

initializeGlobe();

技术问题解答

1. 点击单个国家时旋转地球仪使其居中显示

修改国家系列的点击事件逻辑,通过计算国家中心经纬度调整投影旋转参数,实现平滑居中:

click: (event) => {
  const point = event.srcElement.point;
  point.select();
  // 获取国家几何中心的经纬度
  const center = point.geometry.getCenter();
  // Orthographic投影的旋转参数是视角旋转,需对经纬度取反
  const newRotation = [-center.lon, -center.lat, 0];
  // 更新地图视图,开启动画实现平滑旋转
  point.series.chart.update({
    mapView: {
      projection: {
        rotation: newRotation
      }
    }
  }, true);
}

无需调用zoomTo(),直接通过旋转视角让目标国家居中,更符合地球仪的交互逻辑。

2. 地图数据的简化方案

  • 不是必须使用TopoJSON,Highcharts支持多种格式:
    • GeoJSON:通用地理数据格式,可直接解析,Natural Earth等公开数据源均提供该格式文件。
    • Highcharts预定义地图:直接使用官方提供的地图,比如custom/world-highres,通过map: 'custom/world-highres'加载,无需自行引入外部数据。
    • 自定义地图生成:通过官方地图编辑工具上传GeoJSON/TopoJSON,编辑后导出适配Highcharts的格式。

3. 选择多个国家组成的区域

  • 基于当前数据可实现多选:
    1. 在国家系列中添加配置:allowPointSelect: true, selectedMode: 'multiple',开启多点选中功能。
    2. 通过chart.getSelectedPoints()获取所有选中的国家,自定义高亮或交互逻辑。
  • 若需将多个国家视为统一区域:
    1. 使用Mapshaper等工具,将目标国家的GeoJSON合并为单个Feature。
    2. 加载合并后的地理数据作为新系列,或在现有数据中标记区域归属,通过筛选实现批量高亮。

4. 放大后平移跳帧的解决办法

  • 调整地图视图动画配置,仅在初始化时启用动画:
    mapView: {
      maxZoom: 4,
      projection: {
        name: "Orthographic",
        rotation: [50, -20]
      },
      animation: {
        duration: 0, // 交互时关闭动画
        defer: false
      },
      zoom: 3
    }
    
  • 关闭平滑平移功能:
    mapNavigation: {
      enabled: true,
      enableDoubleClickZoomTo: false,
      enableSmoothPan: false
    }
    
  • 优化renderSea函数的重绘逻辑,将后续更新改为直接属性赋值而非动画:
    this.chart.sea.attr({ // 替换animate为attr
      cx: (p1.x + p2.x) / 2,
      cy: (p1.y + p2.y) / 2,
      r: Math.min(p2.x - p1.x, p1.y - p2.y) / 2
    });
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 07:27:07