Dragging the world map in D3 causes it to appear jumpy and erratic

I'm currently working on a Vue project to create an interactive world map that allows users to drag and zoom. I've attempted to integrate D3 for this purpose, but encountered an issue where the map jumps to the bottom right of the page whenever I try to drag it. Despite my efforts, I haven't been able to pinpoint the exact cause of this problem. The following example demonstrates the sudden jump in the map as soon as I start dragging it:

https://i.stack.imgur.com/4QiBm.gif

All the available documentation and content on Stack Overflow seem to reference an older version of d3 with different syntax, making it challenging to find a solution other than one suggestion mentioning the need to calculate the zoom factor.

If anyone has insights into what I might be doing wrong, I would greatly appreciate the help!

<template>
  <div class="map full-width" ref="map">
    <svg class="canvas" :viewBox="`0, 0, ${defaultWidth}, ${defaultHeight}`">
      <g class="graphic"></g>
    </svg>
  </div>
</template>

<script>
import { defineComponent } from 'vue'

import { select } from 'd3-selection'
import { transition } from 'd3-transition' // required somehow..
import { transform } from 'd3-transform'
import { geoPath, geoNaturalEarth1 } from 'd3-geo'
import { zoom } from 'd3-zoom'
import { easeCubicInOut } from 'd3-ease'
import { drag } from 'd3-drag'

import json from 'assets/world.json'
import style from 'assets/style.js'

export default defineComponent({
  name: 'WorldMap',
  props: {
    data: {
      type: Object,
      required: true,
    },
    maxHeight: {
      type: Number,
      default: 500,
    },
    zoomFactor: {
      type: Number,
      default: 0.2,
      validator: (value) => value > 0.0 && value <= 1.0,
    },
  },
  computed: {
    map() {
      return select('.map')
    },
    graphic() {
      return select('.canvas')
    },
    path() {
      return geoPath().projection(this.projection)
    },
    projection() {
      return geoNaturalEarth1().fitSize(
        [this.defaultWidth, this.defaultHeight],
        json
      )
    },
  },
  data() {
    return {
      zoom: zoom().on('zoom', this.onZoom),
      defaultWidth: 950,
      defaultHeight: 550,
      lastTransform: { x: 0, y: 0 },
    }
  },
  watch: {
    data(val) {
      this.renderChart(val)
    },
  },
  methods: {
    renderChart(val) {
      let prefix = (Math.random() + 1).toString(36).substring(8)
      let count = 0

      this.graphic
        .on('wheel', this.onMouseScroll)
        .attr('transform', `translate(0, 0)`)
        .call(drag().on('drag', this.onDrag))
        .select('g')
        .selectAll('path')
        .data(json.features)
        .enter()
        .append('path')
        .attr('d', this.path)
        .attr('fill', style.secondary())
        .attr('stroke', style.primary())
        .attr('stroke-width', '0.7px')
        .attr('class', () => {
          return 'country'
        })
        .attr('id', () => {
          return `country-${prefix}-${count++}`
        })
        .on('mouseover', this.onMouseOver.bind(this))
        .on('mouseleave', this.onMouseLeave.bind(this))
    },
    onMouseOver(event, element) {
      this.graphic
        .selectAll('.country')
        .filter(function () {
          return this.id != event.target.id
        })
        .transition()
        .duration(200)
        .style('opacity', 0.5)

      this.graphic.select(event.target.id).transition().duration(200)
    },
    onMouseLeave(event, element) {
      this.graphic
        .selectAll('.country')
        .transition()
        .duration(200)
        .style('opacity', 1)
    },
    onMouseScroll(event) {
      event.preventDefault()
      const scrollDirection = event.deltaY > 0 ? 'down' : 'up'
      if (scrollDirection === 'down') {
        this.zoomOut()
      } else {
        this.zoomIn()
      }
    },
    onDrag(event) {
      const graphic = this.graphic
      const matrix = graphic.node().transform.baseVal.getItem(0).matrix

      console.log([matrix.e, matrix.f], [event.x, event.y])

      const x = event.x
      const y = event.y

      graphic.attr('transform', `translate(${x}, ${y})`)
    },
    onZoom(event) {
      this.graphic
        .transition()
        .duration(250)
        .ease(easeCubicInOut)
        .attr('transform', event.transform)
    },
    zoomIn() {
      console.log('Zooming in', this.zoomFactor)
      this.zoom.scaleBy(this.graphic, 1 + this.zoomFactor)
    },
    zoomOut() {
      console.log('Zooming out')
      this.zoom.scaleBy(this.graphic, 1 - this.zoomFactor)
    },
    resetZoom() {
      console.log('Resetting zoom scale.')
      this.lastTransform = { x: 0, y: 0 }
      this.zoom.scaleTo(this.graphic, 1)
    },
  },
  mounted() {
    this.renderChart()
  },
})
</script>

<style lang="sass" scoped>
.map
  overflow: hidden
</style>

Answer №1

After researching, I stumbled upon a solution in this post that solved my problem. It turns out I needed to utilize the dx and dy properties instead of x and y. The revised code snippet is as follows:

onDrag(event) {
  const graphic = this.graphic.select('g')
  const matrix = graphic.node().transform.baseVal.consolidate().matrix
  const [x, y] = [matrix.e + event.dx, matrix.f + event.dy]
  graphic.attr('transform', `translate(${x}, ${y})`)
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Ways to conceal components of an external widget on my site

I'm attempting to add a chat widget to my website and I'm looking to hide specific elements within the widget. The chat function is provided by Tidio, but I believe this applies to most widgets. My main goal is to conceal a button that minimize ...

The Angular Syncfusion schedule is unable to call upon an object that may potentially be 'undefined'

Currently, I am developing an application using Angular Syncfusion to allow users to view and book appointments. I found a helpful resource at the following link: Below you can find the code snippet I have been working on: <ejs-schedule #scheduleObj ...

Exploring the integration of react-leaflet with Nextjs: A step-by-step guide

Hello everyone, I'm currently facing an issue while trying to integrate a Leaflet map into my Next.js application. The error window is not defined keeps popping up and despite searching on stackoverflow, I haven't found a solution yet. The code ...

Re-rendering multiple components with the new `use` feature in React version 18.3.0

When trying to fetch and use data using React 18.3.0, I encountered an issue with multiple re-rendering. react: 18.3.0-canary-3ff846d10-20230724 next: 13.4.12 The code for SuspenseTest component is causing multiple console outputs (about 5 to 8 times) be ...

Error: JSON parsing failed due to an unexpected character, resulting in null data

Many people have asked similar questions on the same topic, but I have not been able to find a solution for my specific problem. Below is the code snippet that I am working with: $.post("show_search_results.php", {location_name: ""+location_name+"", key: ...

A beginner's guide to using Jasmine to test $http requests in AngularJS

I'm struggling with testing the data received from an $http request in my controller as I don't have much experience with Angular. Whenever I try to access $scope, it always comes back as undefined. Additionally, fetching the data from the test ...

The post method in Express.js is having difficulty parsing encoded data accurately

I'm currently working on an AngularJS code that sends a POST request like this: var req = { method: 'POST', url: 'http://localhost:3300/addInventoryItem', headers: { 'Content-Type': 'application/x-www-form- ...

What are the options for app directory routing and programmatic navigation in the upcoming 13 application

I am currently working on a project called Next 13 that involves using the app directory and MUI 5. The project's structure is organized as follows: ./src ./src/app ./src/app/dc ./src/app/dc/admin ./src/app/dc/admin/dc_types.jsx However, when I try t ...

I am interested in dynamically rendering the page on Next.js based on certain conditions

In my -app.js file, I have the code snippet below: import { useState, useEffect } from "react"; import PropTypes from "prop-types"; ... export default function MyApp(props) { const { Component, pageProps } = props; co ...

What is the best way to efficiently transmit Objects through AJAX utilizing bodyParser in node.js express?

Currently attempting to execute: $.ajax({ type:"POST", url:"/psychos", data:JSON.stringify(this.psycho) }) Upon reaching the server, I encounter the following: app.post("/psychos", function(request, respon ...

A step-by-step guide to loading a .php file via Ajax using JQuery when an item is selected from a dropdown

I have successfully populated a dropdown list using PHP from a database on the page named admin.php. Now, I am utilizing JQuery with Ajax functionality to display information when a surname is clicked from the dropdown menu. The goal is to call employerP ...

How to retrieve specific items from an array contained within an array of objects using Express.js and MongoDB

Within the users array, there is an array of friends. I am looking to retrieve all friends of a specific user based on their email where the approved field is set to true. In my Node.js application, I have defined a user schema in MongoDB: const UserSchem ...

router.query is returning an empty object when using Next.js

Here is how my folders are organized: https://i.stack.imgur.com/TfBtv.png In addition, here is a snippet of my code: const router = useRouter(); const { id } = router.query; The issue I'm facing is that the id is returning {} instead of the actual ...

Automatically Adjust Text Size to Fit Input Forms using jQuery

Does anyone know how to use jQuery to dynamically change the font size in a form input field so that it always remains visible and fits even as the user types more text? I want the font size to start at 13px and progressively shrink as the text reaches the ...

Tips for successfully implementing an Ocrad.js example

I've been working on an OCR (optical character recognition) web application and stumbled upon the Ocrad.js JavaScript library Ocrad.js. It seems like a perfect fit for what I need, but unfortunately, I'm having trouble getting it to function prop ...

Is there a way to extract data from a JSON file with dc.js?

As a beginner in programming, I am looking to learn how to import data from a JSON file using dc.js. ...

Utilizing EJS to display dynamic data from a JSON file in a visually appealing D

*Excited about learning express! Currently, I have two files - index.ejs and script.js. The script I've written successfully fetches JSON data from an api. const fetch = require("node-fetch"); const url = '...' fetch (url) .then(resp ...

Interactive Autocomplete Component

I am encountering issues with passing dynamic data to my autocomplete angularjs directive, which is built using jQuery-UI autocomplete. Below is the current code I am working with: HTML: <div ng-app="peopleApp"> <div ng-controller="indexCont ...

Why would someone use the `catch` method in Angular $http service when the `then` method already takes two arguments (success and error callbacks)?

When working with the Angular $http service, there is a then method that can take two arguments - one for success and one for error. But why would you use the catch method if there's already an error callback? And what is its purpose? Here's an ...

The Node.js application is up and running on the Node server, but unfortunately, no output is

Greetings, I am a beginner in nodejs. var io = require('socket.io').listen(server); users = []; connections = []; server.listen(process.env.PORT || 3000); console.log('server running....on Pro'); app.get ('/', function(re ...