Tap on chart for hover causes 'styles invalidated' which triggers freeze on iOS Safari, what is a hover tap recalculating?

I am rendering around 4,000 values into a stacked bar chart to Safari on iOS. Displaying a hover, on only Safari iOS, takes 5-10 seconds, during which the browser freezes. (About once in every 10 reloads the browser doesn’t do this.)

I have dragmode=’pan’, so the user can scroll when zoomed.

When I tap the chart to display the hover, the browser freezes for around 5 seconds while it invalidates the styles and re-renders. Then the hover is then displayed correctly. This does not happen on Safari desktop, nor any other browser, desktop or Android.

The code that triggers the style invalidation is the below call to onStart(e) in src/components/dragelement/index.js, line 29798 in plotly-3.0.1.js or 29903 in plotly-3.1.0.js:

  var require_dragelement = __commonJS({
    "src/components/dragelement/index.js"(exports, module) {
      "use strict";

     ...

        var clampFn = options.clampFn || _clampFn;
        function onStart(e) {                 // <= this line triggers the style invalidation, according to the Safari timeline recorder
          gd._dragged = false;
          gd._dragging = true;
          var offset = pointerOffset(e);
          startX = offset[0];
          startY = offset[1];
          initialTarget = e.target;
          initialEvent = e;
          rightClick = e.buttons === 2 || e.ctrlKey;
          if (typeof e.clientX === "undefined" && typeof e.clientY === "undefined")     {
            e.clientX = startX;
            e.clientY = startY;
          }
          newMouseDownTime = (/* @__PURE__ */ new Date()).getTime();
...

Dragging works fine, except if there is a hover displayed, in which case the chart first takes 5-10 seconds to clear the hover.

I tried changing to a combined X hover but that does the same. Also tried removing all hovers.

It seems the browser is recalculating layout and re-rendering the chart before adding the hover.

If I set hovermode: falsethe big freeze doesn’t happen. (There is still a second or so pause, because it is allowing for a drag, but this is fine.)

Happy to implement my own hover on chart click if that’s easiest. My HTML layout isn’t very dynamic, all plain HTML and JS flex. I create the DOM elements but this has long completed by the time I render charts let alone hovers.

Huge thanks for any ideas, what is the heavy lift on rendering the hover? Tearing hair out a little and I haven’t much left!

  • Could you provide steps to reproduce (or a working example on Codepen or similar)?
  • Are there any log messages that indicate an error?
  • Do you know if this issue occurred in v2.X?
1 Like

Thanks for reading.

I tested on 2.35.3 and get the same issue.

There are no log messages and no error reported - it just takes 5-10 seconds.

I have removed the hovers, which has resolved the issue with the browser freezing.

However a drag of the chart still takes 5-10 seconds on every drag. (I missed this before as the browser is now responsive during the 5-10 seconds, sorry.)

I haven’t been able to reproduce this in a smaller sample. Unfortunately the chart is within a page with API calls, DOM generation, etc, And I’m not sure which parts I need to reproduce the issue.

If I comment out the line dragCover = document as below, the first drag takes a while, but subsequent drags are instant. Unfortunately this also means you can’t drag through the plot, so isn’t a workaround, but it suggests a cause.

        function onStart(e) {
          gd._dragged = false;
          gd._dragging = true;
          var offset = pointerOffset(e);
          startX = offset[0];
          startY = offset[1];
          initialTarget = e.target;
          initialEvent = e;
          rightClick = e.buttons === 2 || e.ctrlKey;
          if (typeof e.clientX === "undefined" && typeof e.clientY === "undefined") {
            e.clientX = startX;
            e.clientY = startY;
          }
          newMouseDownTime = (/* @__PURE__ */ new Date()).getTime();
          if (newMouseDownTime - gd._mouseDownTime < doubleClickDelay) {
            numClicks += 1;
          } else {
            numClicks = 1;
            gd._mouseDownTime = newMouseDownTime;
          }
          if (options.prepFn) options.prepFn(e, startX, startY);
          if (hasHover && !rightClick) {
            dragCover = coverSlip();
            dragCover.style.cursor = window.getComputedStyle(element).cursor;
          } else 
          if (!hasHover) {
            // dragCover = document;   <-- this line causes subsequent drags to freeze in iOS Safari
            cursor = window.getComputedStyle(document.documentElement).cursor;
            document.documentElement.style.cursor = window.getComputedStyle(element).cursor;
          }

Disabling drag and using a rangeslider exhibits the same 5-10 second delay.

It doesn’t seem to be the same cause, as I don’t see style invalidation, but the rangeslider demo in the plotly.js docs doesn’t work on iOS Safari. I can’t grab the slider ends, can’t drag the viewed window, tapping zoom fails or takes a long time. Not sure it is the same issue though. Range slider and selector in JavaScript

The dragmode=zoom demo works fine on iOS Safari, so I tried dragmode=zoom, but get no interaction and the same 5 seconds delay before re-render.

Thanks for that information. Is the page publicly accessible anywhere? That will be the easiest way to debug the issue. If not, could you please submit a bug report here with as much information as possible? We likely won’t be able to address it soon, but it will at least get on our board. If you have time, we’d gladly review a PR to fix the problem.

The page is behind credentials, which I can provide, but I’ll need to fix or work around this fairly urgently as it will soon be part of a paid mobile app. As part of this I will be pulling the page apart, so might find more info on the culprit. I’ll also try to create a serverless reproduction of the issue.

Is there anywhere I can find a broader overview of this part of the architecture, or how the dragCover works?

I am not familiar with Plotly.js’s internals but would love to submit a PR if I can in a day or few’s effort.

The cause was this piece of CSS

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        transition-duration: 0.01ms !important;
    }
}

Copied blindly from a 2019 CSS reset that the author has since updated..

A drag of a 5-6,000 point stacked bar takes about 10 seconds, which’d be about 2ms per 5-6,000 transitions . If I remove this line a drag takes half a second. Slightly embarrassed.

That’s awesome that you figured it out! Just to confirm: this is an issue with some CSS that you added to your project and there is no bug in plotly.js. Is that correct?

Correct, thanks, not a bug in plotly!

1 Like

On second thoughts it might be worth having a developer consider this. It might be possible to stop the transitions triggering within plotly when plotly doesn’t need them. It’s not plotly’s fault, but it was the effect of that CSS on plotly plots that caused the page freezing on iOS Safari.