d3 / d3-zoom

Pan and zoom SVG, HTML or Canvas using mouse or touch input.
https://d3js.org/d3-zoom
ISC License
501 stars 144 forks source link

[Vue.js] Synchronised zoom between components #226

Closed Insectslayer closed 3 years ago

Insectslayer commented 3 years ago

Hello, I'm working on visualization featuring multiple line charts as separate components. Due to the separation (at least I think), I could not interlink the zoom events together. My code looks roughly like this:

DatasetView.vue The main view, storing every chart and also the zoom object.

<template>
  <div>
    <D3Chart id="chart1" :zoom="zoom" />
    <D3Chart id="chart2" :zoom="zoom" />
    <D3Chart id="chart3" :zoom="zoom" />
  </div>
</template>

<script>

import D3Chart from '@/components/D3Chart.vue'
import * as d3 from 'd3'

export default {
  name: 'DatasetView',
  components: { D3Chart },
  data() {
    return {
      zoom: d3.zoom()
    }
  }
}
</script>

D3Chart.vue All charts are created equal, only given a unique id, sent as a prop from the parent component.

<template>
  <svg :id="id" />
</template>

<script>
import * as d3 from 'd3'

export default {
  props: {
    id: {
      type: String,
      required: true
    },
    zoom: {
      type: Function,
      required: true
    }
  },

I decided to track the mouse to appoint the master chart. It should send zoom.transform to all other charts, while the others will only update the transformation on themselves.

  methods: {
    onMouseEnter() {
      this.zoom.on('zoom', this.zoomedAll)
    },
    onMouseLeave() {
      this.zoom.on('zoom', this.zoomed)
    },
    zoomedAll(event) {
      var currViz = d3.select('#' + this.id)
      var transform = event.transform
      currViz.select('g').attr('transform', transform)

      // Filter to get the other charts
      var otherSVGs = d3.selectAll('svg').filter(function() {
        return d3.select(this).attr('id') !== currViz.attr('id')
      })
      otherSVGs.call(this.zoom.transform, transform)
    },
    zoomed(event) {
      var currViz = d3.select('#' + this.id)
      var transform = event.transform
      currViz.select('g').attr('transform', transform)
    }
  },
  mounted() {
    var svg = d3
      .select('#' + this.id)
      .attr('width', 500)
      .attr('height', 200)

    var container = svg.append('g')

    svg.on('mouseenter', this.onMouseEnter).on('mouseleave', this.onMouseLeave)

    svg.call(this.zoom.scaleExtent([0.5, 10]).on('zoom', this.zoomed))

    container
      .append('circle')
      .attr('fill', '#000')
      .attr('cx', 50)
      .attr('cy', 50)
      .attr('r', 10)
  }
}
</script>

This code ends up in an infinite loop, but only looping through the zoomedAll event. Somehow it calls itself (even if it shouldn't) and doesn't trigger the zoom event on other charts.

Do you have any ideas for the solution? Also, do you have any general recommendations for using D3.js and Vue.js together?