javascriptcssreactjsd3.js

First zoom glitches after page load using D3


I'm using D3 to zoom onto an image on click and on Mousewheel. Everything is working fine but the first zoom glitches a lot.

Here is the demo of the app.

This is how I'm zooming towards the objects:

const star = "https://gmg-world-media.github.io/skymap-v1dev/static/media/star.19b34dbf.svg";
const galaxy = "https://gmg-world-media.github.io/skymap-v1dev/static/media/galaxy.c5e7b011.svg";
const nebula = "https://gmg-world-media.github.io/skymap-v1dev/static/media/nebula.d65f45e5.svg";
const exotic = "https://gmg-world-media.github.io/skymap-v1dev/static/media/exotic.21ad5d39.svg";

const sWidth = window.innerWidth;
const sHeight = window.innerHeight;

const x = d3.scaleLinear().range([0, sWidth]).domain([-180, 180]);
const y = d3.scaleLinear().range([0, sHeight]).domain([-90, 90]);

const svg = d3.select("#render_map").append("svg").attr("width", sWidth).attr("height", sHeight);
const node = svg.append("g").attr('class', 'scale-holder');

const zoom = d3
  .zoom()
  .scaleExtent([1, 30])
  .translateExtent([
    [0, 0],
    [sWidth, sHeight]
  ])

svg.call(zoom);

const imgG = node.append("g");
imgG
  .insert("svg:image")
  .attr("preserveAspectRatio", "none")
  .attr("x", 0)
  .attr("y", 0)
  .attr("width", sWidth)
  .attr("height", sHeight)
  .attr("xlink:href", "https://gmg-world-media.github.io/skymap-v1dev/background_image_set/image-1.jpg");
imgG
  .insert("svg:image")
  .attr("preserveAspectRatio", "none")
  .attr("x", 0)
  .attr("y", 0)
  .attr("width", sWidth)
  .attr("height", sHeight)
  .attr("xlink:href", "https://gmg-world-media.github.io/skymap-v1dev/background_image_set/image.jpg");


// Draw objects on map with icon size 8
drawObjects(8)

function drawObjects(size) {
  const dataArray = [];
  const to = -180;
  const from = 180;
  const fixed = 3;
  const objectType = ["ST", "G", "N", "E"];

  // Following loop is just for demo.
  // Actual data comes from a JSON file.
  for (let i = 0; i < 350; i++) {
    const latitude = (Math.random() * (to - from) + from).toFixed(fixed) * 1;
    const longitude = (Math.random() * (to - from) + from).toFixed(fixed) * 1;
    const random = Math.floor(Math.random() * objectType.length);
    dataArray.push({
      "Longitude": longitude,
      "Latitude": latitude,
      "Category": objectType[random]
    })
  }

  for (let index = 0; index < dataArray.length; index++) {
    // Loop over the data
    const item = dataArray[index]
    const mY = y(Number(item.Latitude))
    const mX = x(Number(item.Longitude))

    if (node.select(".coords[index='" + index + "']").size() === 0) {
      let shape = star;

      // Plot various icons based on Category
      switch (item.Category) {
        case "ST":
          shape = star;
          break;
        case "G":
          shape = galaxy;
          break;
        case "N":
          shape = nebula;
          break;
        case "E":
          shape = exotic;
          break;
      }

      const rect = node
        .insert("svg:image")
        .attr("class", "coords")
        .attr("preserveAspectRatio", "none")
        .attr("x", mX)
        .attr("y", mY)
        .attr("width", size)
        .attr("height", size)
        .attr("cursor", "pointer")
        .attr("index", index)
        .attr("xlink:href", shape)
        .attr("opacity", "0")
        .on("click", function() {
          handleObjectClick(index, mX, mY)
        })

      // Add the objects on the map
      rect.transition().duration(Math.random() * (2000 - 500) + 500).attr("opacity", "1")
    }

  }

}

function boxZoom(x, y) {
  // Zoom towards the selected object
  // This is the part responsible for zooming
  svg
    .transition()
    .duration(1000)
    .call(
      zoom.transform,
      d3.zoomIdentity
      .translate(sWidth / 2, sHeight / 2)
      .scale(6)
      .translate(-x, -y)
    );

}

function handleObjectClick(currentSelect, x, y) {

  // Appending some thumbnails to the clicked object here...
  //Call the zoom function
  boxZoom(x, y)

}
#render_map {
  width: 100vw;
  height: 100vh;
  margin: 0 auto;
  overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<div id="render_map">
</div>

This zoom doesn't seem to be working here. But it does definitely work in the app. I've not modified the piece of code responsible for zooming. (See this demo instead.) The problem is that zoom jumps when you do it for the first time after a page load, and then it fixes itself.

I don't understand what I'm doing wrong here. Any hints would be lovely.
TIA!


Solution

  • The issue seems caused by a very expensive CSS repaint cycle. I tested this in Firefox by going to Performance in the DEV tools and starting a recording, then zooming for the first time.

    enter image description here

    I saw the fps drop enormously, and that the repaint took as much as 250ms. Normally, that is 10-50ms.

    I have some pointers:

    1. Why do you have two images behind each other? Big images are definitely the reason why repainting takes this long, and your image is 8000x4000 pixels! Start by removing the image that we're not even seeing;
    2. Try adding an initial value of transform="translate(0, 0) scale(1)" to .scale-holder. I have a feeling that adding this the first time is what forces the entire screen to be repainted. Maybe changing an existing scale value is an easier mathematical operation than applying a scale value to something that was not scaled before;
    3. If that doesn't help, compress the image to at most 1600 or even 1080 pixels wide. Us mortals should not even be able to see the difference.