Firefox scrolling while it should not.

Hi!

I have been trying to isolate a Firefox scrolling problem when zooming X-axis with D3 that should not trigger a scrolling.

When there are too many D3 objects (about 50000 ‘rect’ in my test case) and using the wheel to zoom the graph, Firefox is “scrolling” the web page while it should not: the graph should only zoom.

Note that this problem occurs only with Firefox, it works fine with Edge/Chrome.

I have done a simple example (packed in a single html file) that triggers the problem for me both in windows and Linux: just try do some quick “scroll” in the graph. Note that you might need to increase the variable “numberOfBoxes” to trigger the problem.

Note that even the following code doesn’t seems to prevent the scrolling!
window.addEventListener(‘wheel’, preventDefault, { passive: false });

Do you know what is happening, is this a firefox bug and if there is a work around to the problem?

<!DOCTYPE html>
<head>
   <title>test</title>
   <script src="https://d3js.org/d3.v7.js"></script>
   <style>
   .bar-chart {background-color: #C7D9D9;}
   .box {border: 1px solid black;padding: 10px;margin: 10px;}
   </style>
</head>
<body>
   <svg></svg>
   <script>

function createGraph(){

   svgWidth = 1000;
   svgHeight = 1000;
   barWidth = 10;
   numberOfTicks = svgHeight/barWidth;

   //boxes = [{"row": 10, "start" : 10, "end" : 20}];
   numberOfBoxes = 50000;
   var boxes = [];

   for (var i = 1; i <= numberOfBoxes; i++) {
      var row = (i%(numberOfTicks - 2)) + 1;
      var start = (i%(svgWidth - 10));
      var end = start + 10;
      boxes.push({"row": row, "start" : start, "end" : end});
   }

   var x = d3.scaleLinear().range([0, svgWidth]).domain([0, svgWidth]);
   var y = d3.scaleLinear().range([svgHeight, 0]).domain([numberOfTicks, 0]);


   var xAxis = d3.axisBottom()
                 .scale(x)
                 .ticks(svgWidth/100);


   var yAxis =d3.axisLeft()
                .scale(y)
                .ticks(numberOfTicks);

   var svg = d3.select('svg')
       .attr("width", svgWidth + 200)
       .attr("height", svgHeight+ 200)
       .append("g")
       .attr("transform" , "translate(100, 100)");//move to the right and down


   //Update axes
   var gX = svg.append("g")
       .attr("class", "axis")
       .attr("transform","translate(0, " + svgHeight + ")") //move down
       .call(xAxis);

   var gY = svg.append("g")
       .attr("class" , "axis")
       .call(yAxis);

   var graphContainer = svg.append("g");
   var listenerRect  = graphContainer.append("g")
                 .append("rect")
                 .attr("class", "view")
                 .attr("x", 0)
                 .attr("y", 0)
                 .attr("width", svgWidth - 1)
                 .attr("height", svgHeight - 1)
                 .style('opacity', 0.04);

   var rectangles = graphContainer.append("g")
             .selectAll("rect")
             .data(boxes)
             .enter().append("svg:rect")
             .attr("class" , "bar")  //css
             .attr("x", function(d) { return x(d.start); })
             .attr("y", function(d) { return y(d.row) - barWidth/2; })
             .attr("width", function(d) { return x(d.end) - x(d.start)})
             .attr("height", barWidth)
             .style("opacity", '1')
             .style("fill", "yellow")
             .style("stroke", "gray")
             .style("stroke-width", 2);

   function inXPlotGraph(xParam)
   {
       return Math.max(0, Math.min(svgWidth, xParam));
   }

   //Define the zoom function
   var zoom = d3.zoom()
                .on("zoom",
                   function zoomed(event) {
                      var newX = event.transform.rescaleX(x);
                      //Update scale
                      gX.call(xAxis.scale(newX));
                      //Update rect
                      rectangles
                         .attr("x", function(d) { return inXPlotGraph(newX(d.start)); })
                         .attr("width", function(d) { return  inXPlotGraph(newX(d.end)) - inXPlotGraph(newX(d.start))});
                      });


   graphContainer
        .call(zoom);
             
}
   
   createGraph();
   </script>
</body>

Did your code get cut off? Can you reproduce in a codepen or in Observable?

Quick thing to try: document.addEventListener instead of window.

Thanks for the quick reply.
Maybe I was unclear, very sorry about that . ‘window.addEventListener(‘wheel’, preventDefault, { passive: false });’ is something I added in another test to try prevent scrolling but even this line of code didn’t stop the vertical scrolling of the window.

The code pasted in the ticket (hope you see it!, I had some trouble to make it visible) by itself should trigger the fault. Did you succeeded to reproduce the issue ?

CodePen here: https://codepen.io/CAYdenberg/pen/ExMOMMo

The best I could manage was to wrap the SVG in a div and ask the browser to prevent wheel events on the div. It’s very unreliable: it mostly works and it works better than attaching the zoom event alone, but it’s not perfect when a lot of wheel events occur together.

My conclusion is that you’re just asking too much of the browser. If you decrease numOfBoxes to 5 it works great. And as I’m sure you’ve noticed, the response is very janky even if you don’t have to worry about scrolling. I’m not sure what d3 does under the hood but it must involve attaching and detaching the raw event listeners, so sometimes they get blocked and other times they don’t.

Your better bet is to look at improving performance (big topic).

Another strategy would be to look at disabling body scrolling entirely when the mouse enters the svg space, see Prevent Page Scrolling When a Modal is Open | CSS-Tricks - CSS-Tricks, but that’s a bulldozer solution that will prevent all scrolling (eg, spacebar). And of course will do nothing for the jank.

Thanks a lot for the codepen. I totally agree that I have ‘too many’ events but this was just to make the problem visible in Firefox as in Edge/Chrome no such problem occurs (In my real application, the bad scrolling happens randomly). This could be a sign of a underlaying bug in Firefox or D3. I need guidance here for reporting, what do you think? Maybe this will help resolving issue for others. I also realized yesterday that if I press ‘shift’ while zooming by wheel, the problem doesn’t occurs and the zooming ‘feels faster’. This might be a hint that there are some kind of race condition between events somehow. Sorry about my bad knowledge in this field.
By the way, do you know if there is a way one can re-send (or transform) event to itself in a browser? Would it be possible to triggering a “shift + wheel” event when a “wheel” is received? Or am I making things overcomplicated?

While testing a bit more, I realized that just loading the zoom function will cause the issue: firefox scoll problem (codepen.io)

Hi again, I think i forgot to save my codepen somehow so it didn’t work. Sorry about that, this is new to me. ( firefox scroll problem (codepen.io)).
Now the example should trigger the bug in Firefox. To explain it a bit more, I just put some ‘dead code’ in the zoomed() function to ‘cpu load’ the javascript. Firefox will scroll the page while it should not (page/graph should not move). In this example, we are not touching any D3 objects. So i don’t think the problem is related to D3 but how Firefox handle events, or what do you think?

I mean it is, but if your event handler isn’t able to complete within a reasonable timeframe, I don’t know that that counts as a bug in the browser. Don’t let me stop you from reporting it :slight_smile:

Ideally you would track the number of events and/or what the zoom “should” be after each event, and then asynchronously run the zoom code when there’s time. This is really just equivalent to debouncing. I’m not sure it’ll work as a drop-in with d3.zoom, because it’s doing so much stuff under the hood.

Thanks a lot for the support. That was an interesting exercise for me.