使用 D3 和 Vue 创建交互式地图

2025-06-08

使用 D3 和 Vue 创建交互式地图

源代码|实时预览

我为什么需要它

有很多方法可以将地图添加到您的网站或应用程序中:Google 地图、Mapbox、Leaflet 等等。这很简单。有些服务只需点击几下即可完成。

但是当你需要自定义设计、显示数据集或执行任何你想做的事情时,情况就变得很糟糕了。此外,在 Vue 或 React 中你不能使用 JSX,而必须使用命令式抽象 JavaScript API(但我使用 Vue,因为我对模板和响应式非常感兴趣)。

此外,一些图书馆对于私人项目来说并不是免费的。

因此,我不得不再次在地图上显示一些数据,我决定:我希望完全控制我的代码,并且我将使用二十一点和妓女创建自己的地图。

步骤1:创建静态地图。

让我们从带有 Babel 和 sass 的简单 vue-cli 3 应用程序开始。

我们需要 D3 和d3-tile(它不包含在 d3 npm 包中)来渲染地图图块。

yarn add d3 d3-tile
Enter fullscreen mode Exit fullscreen mode

实际上我们不需要完整的 d3 代码。对于简单的地图,我们只需要 d3-geo 用于地图投影,以及 d3-tile 用于生成图块,所以我们只包含这两个包。

接下来我们应该定义一些设置,例如比例、宽度、高度和初始坐标。通常,我会通过计算挂载时元素的大小来使所有图表响应容器。

<script>
const d3 = {
  ...require('d3-geo'),
  ...require('d3-tile'),
};

export default {
  props: {
    center: {
      type: Array,
      default: () => [33.561041, -7.584838],
    },
    scale: {
      type: [Number, String],
      default: 1 << 20,
    },
  },
  data () {
    return {
      width: 0,
      height: 0,
    };
  },
  mounted () {
    const rect = this.$el.getBoundingClientRect();

    this.width = rect.width;
    this.height = rect.height;
  },
  render () {
    if (this.width <= 0 || this.height <= 0) {
      // the dummy for calculating element size
      return <div class="map" />;
    }

    return (
      <div class="map">our map will be here</div>
    );
  },
};
</script>

<style lang="scss" scoped>
.map {
  width: 100%;
  height: 100%;
}
</style>

Enter fullscreen mode Exit fullscreen mode

现在定义投影和图块生成器。

export default {
  // ... 
  computed: {
    projection () {
      return d3.geoMercator()
        .scale(+this.scale / (2 * Math.PI))
        .translate([this.width / 2, this.height / 2])
        .center(this.center)
      ;
    },
    tiles () {
      return d3.tile()
        .size([this.width, this.height])
        .scale(+this.scale)
        .translate(this.projection([0, 0]))()
      ;
    },
  },
  // ...
};
Enter fullscreen mode Exit fullscreen mode

我总是将 d3 辅助函数定义为计算属性,因此当某些参数发生变化时,Vue 会重新计算它们并更新我们的组件。

现在我们已经拥有显示地图所需的一切,我们只需渲染生成的图块:

export default {
  render () {
    if (this.width <= 0 || this.height <= 0) {
      return <div class="map" />;
    }

    return (
      <div class="map">
        <svg viewBox={`0 0 ${this.width} ${this.height}`}>
          <g>
            {this.tiles.map(t => (
              <image
                key={`${t.x}_${t.y}_${t.z}`}
                class="map__tile"
                xlinkHref={`https://a.tile.openstreetmap.org/${t.z}/${t.x}/${t.y}.png `}
                x={(t.x + this.tiles.translate[0]) * this.tiles.scale}
                y={(t.y + this.tiles.translate[1]) * this.tiles.scale}
                width={this.tiles.scale}
                height={this.tiles.scale}
              />
            ))}
          </g>
        </svg>
      </div>
    );
  },
};
Enter fullscreen mode Exit fullscreen mode

在这里,我们浏览由 d3-tile 生成的图块并从图块服务器请求图像。

您可以在这里找到其他服务器,甚至可以使用自定义样式托管自己的图块服务器。

不要忘记添加版权。

<div class="map__copyright">
  ©&nbsp;
  <a
    href="https://www.openstreetmap.org/copyright"
    target="_blank"
  >OpenStreetMap&nbsp;</a>
  contributors
</div>
Enter fullscreen mode Exit fullscreen mode
.map {
  // ...
  position: relative;
  font-family: Arial, sans, sans-serif;

  &__copyright {
    position: absolute;
    bottom: 8px;
    right: 8px;
    padding: 2px 4px;
    background-color: rgba(#ffffff, .6);
    font-size: 14px;
  }
}
Enter fullscreen mode Exit fullscreen mode

现在我们有了卡萨布兰卡的静态地图。还不是很精彩。

卡萨布兰卡地图

第 2 步:添加地图控件。

对我来说最激动人心的是 Vue 如何简化了创建交互式地图的过程。我们只需更新投影参数并更新地图即可。这就像第一次使用魔术一样简单!

我们将通过拖动地图来制作缩放按钮和位置控制。

让我们从拖动开始。我们需要在组件数据中定义投影平移属性,并在 svg 元素上定义一些鼠标事件监听器(或者你也可以在 tiles 组上监听它们)。

<script>
// ...

export default {
  // ...
  data () {
    return {
      // ...
      translateX: 0,
      translateY: 0,

      touchStarted: false,
      touchLastX: 0,
      touchLastY: 0,
    };
  },
  computed: {
    projection () {
      return d3.geoMercator()
        .scale(+this.scale / (2 * Math.PI))
        .translate([this.translateX, this.translateY])
        .center(this.center)
      ;
    },
    // ...
  },
  mounted () {
    // ...
    this.translateX = this.width / 2;
    this.translateY = this.height / 2;
  },
  methods: {
    onTouchStart (e) {
      this.touchStarted = true;

      this.touchLastX = e.clientX;
      this.touchLastY = e.clientY;
    },
    onTouchEnd () {
      this.touchStarted = false;
    },
    onTouchMove (e) {
      if (this.touchStarted) {
        this.translateX = this.translateX + e.clientX - this.touchLastX;
        this.translateY = this.translateY + e.clientY - this.touchLastY;

        this.touchLastX = e.clientX;
        this.touchLastY = e.clientY;
      }
    },
  },
  render () {
    // ...
    return (
      <div class="map">
        <svg
          viewBox={`0 0 ${this.width} ${this.height}`}
          onMousedown={this.onTouchStart}
          onMousemove={this.onTouchMove}
          onMouseup={this.onTouchEnd}
          onMouseleave={this.onTouchEnd}
        >
          // ...
        </svg>
        // ...
      </div>
    );
  },
};
</script>

<style lang="scss" scoped>
.map {
  // ...

  &__tile {
    // reset pointer events on images to prevent image dragging in Firefox
    pointer-events: none;
  }
  // ...
}
</style>
Enter fullscreen mode Exit fullscreen mode

哇!我们只需更新翻译值,新的图块就会加载,这样我们就可以探索世界了。但是如果没有缩放控件,操作起来会不太方便,所以让我们实现它。

我们需要scale在组件的数据中移动道具,添加zoom属性并渲染缩放按钮。

根据我的经验,最小和最大图块的缩放级别分别为 10 和 27(老实说,我不太确定这是否适用于所有图块提供商)。

<script>
// ...

const MIN_ZOOM = 10;
const MAX_ZOOM = 27;

export default {
  props: {
    center: {
      type: Array,
      default: () => [-7.584838, 33.561041],
    },
    initialZoom: {
      type: [Number, String],
      default: 20,
    },
  },
  data () {
    return {
      // ...
      zoom: +this.initialZoom,
      scale: 1 << +this.initialZoom,
    };
  },
  // ...
  watch: {
    zoom (zoom, prevZoom) {
      const k = zoom - prevZoom > 0 ? 2 : .5;

      this.scale = 1 << zoom;
      this.translateY = this.height / 2 - k * (this.height / 2 - this.translateY);
      this.translateX = this.width / 2 - k * (this.width / 2 - this.translateX);
    },
  },
  // ...
  methods: {
    // ...
    zoomIn () {
      this.zoom = Math.min(this.zoom + 1, MAX_ZOOM);
    },
    zoomOut () {
      this.zoom = Math.max(this.zoom - 1, MIN_ZOOM);
    },
  },
  render () {
    // ...
    return (
      <div class="map">
        <div class="map__controls">
          <button
            class="map__button"
            disabled={this.zoom >= MAX_ZOOM}
            onClick={this.zoomIn}
          >+</button>
          <button
            class="map__button"
            disabled={this.zoom <= MIN_ZOOM}
            onClick={this.zoomOut}
          >-</button>
        </div>
        //...
      </div>
    );
  },
};
</script>

<style lang="scss" scoped>
.map {
  // ...
  &__controls {
    position: absolute;
    left: 16px;
    top: 16px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    height: 56px;
  }
  &__button {
    border: 0;
    padding: 0;
    width: 24px;
    height: 24px;
    line-height: 24px;
    border-radius: 50%;
    font-size: 18px;
    background-color: #ffffff;
    color: #343434;
    box-shadow: 0 1px 4px rgba(0, 0, 0, .4);

    &:hover,
    &:focus {
      background-color: #eeeeee;
    }

    &:disabled {
      background-color: rgba(#eeeeee, .4);
    }
  }
  // ...
}
</style>

Enter fullscreen mode Exit fullscreen mode

就是这样。我们仅用两个步骤就用 Vue、D3 和 OpenStreetMap 创建了简单的交互式地图。

结论

借助 D3 的强大功能和 Vue 的响应式特性,创建自己的地图视图组件并不难。我认为最重要的一点是完全控制 DOM,而不是使用一些抽象的地图渲染器 API,因为这些 API 会用我可爱的元素做一些晦涩难懂的事情。

当然,为了制作出功能强大的地图,我们需要实现更多功能,例如平滑缩放、最大边界等。但所有内容都是完全可定制的,因此您可以做任何您想做或需要做的事情。

如果您发现这篇文章有用,我可以写更多有关如何改进此地图并在其上显示数据的内容。

请随时提出您的问题。

鏂囩珷鏉ユ簮锛�https://dev.to/denisinvader/creating-an-interactive-map-with-d3-and-vue-4158
PREV
自动化节点依赖项更新
NEXT
不要害怕命令行:文本编辑器