Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrong chart size when using throttled #10935

Closed
abaksha-sc opened this issue Dec 2, 2022 · 10 comments · Fixed by #10942
Closed

Wrong chart size when using throttled #10935

abaksha-sc opened this issue Dec 2, 2022 · 10 comments · Fixed by #10942

Comments

@abaksha-sc
Copy link

abaksha-sc commented Dec 2, 2022

Expected behavior

All resizeObserver callbacks wrapped with throttled and when resize calls then it should be called with the latest actual size of chart container.
Code is here:

const resize = throttled((width, height) => {

Current behavior

Function throttled just skips calls when it's ticking and when real function call performs then throttled function calls with not actual arguments.
Source of throttled is here:

export function throttled<TArgs extends Array<any>>(
fn: (...args: TArgs) => void,
thisArg: any,
) {
let ticking = false;
return function(...args: TArgs) {
if (!ticking) {
ticking = true;
requestAnimFrame.call(window, () => {
ticking = false;
fn.apply(thisArg, args);
});
}
};
}

Reproducible sample

https://jsfiddle.net/fxcLgy7d/
It's not easy to reproduce with real chart and resizeObserver so that just example provided how throttled works with emulated resize calls.

Optional extra steps/info to reproduce

No response

Possible solution

Option #1. Just save latest arguments when throttled function calls:

export function throttled<TArgs extends Array<any>>(

function throttled(fn, thisArg) {
   let ticking = false;
   let latestArgs;
   return function(...args) {
       latestArgs = args; // <-- save latest actual arguments
       if (!ticking) {
           ticking = true;
           requestAnimFrame.call(window, ()=>{
               ticking = false;
               fn.apply(thisArg, latestArgs);  // <-- call function with the latest actual arguments
           });
       }
   };
}

Option #2. When listener of resize calls then always get actual size from getBoundingClientRect:

listener(width, height);

const resize = throttled((width, height) => {
    const w = container.clientWidth;
    const rect = container.getBoundingClientRect();
    listener(rect.width, rect.height); // <-- use here actual size from `getBoundingClientRect` because while this throttled callback calls then size could be changed already (but resize skipped, because another throttled call is in progress)
    if (w < container.clientWidth) {
      listener();
    }
  }, window);

Context

In my project this leads to wrong size of chart

chart.js version

4.0.1

Browser name and version

Chrome 107

Link to your project

No response

@kurkle
Copy link
Member

kurkle commented Dec 2, 2022

This must have happened while converting to typescript, right?

@abaksha-sc
Copy link
Author

I'm not sure I'm understanding your question.
TypeScript doesn't affect this issue.
I see the same code in my result bundle as here in fiddle: https://jsfiddle.net/fxcLgy7d/

P.S. Works as expected (with the latest args) with fix from Option #1: https://jsfiddle.net/z0gwx4ut/1/

@kurkle
Copy link
Member

kurkle commented Dec 4, 2022

Sorry, my question was really to @etimberg and I was referring to this: e6892a9

@etimberg
Copy link
Member

etimberg commented Dec 4, 2022

@kurkle I think this might have been caused by removing this code inside the throttled function

const updateArgs = updateFn || ((args) => Array.prototype.slice.call(args));

@abaksha-sc
Copy link
Author

abaksha-sc commented Dec 5, 2022

@kurkle I think this might have been caused by removing this code inside the throttled function

const updateArgs = updateFn || ((args) => Array.prototype.slice.call(args));

Yes, seems like previously there was saving of arguments per each function call:
e6892a9?diff=split#diff-b4f2c571c1aa3b8e81abfd0cd693ac6f1f50a6f0149b61026f7c85cea8cbc4b0L33

export function throttled(fn, thisArg, updateFn) {
  const updateArgs = updateFn || ((args) => Array.prototype.slice.call(args));
  let ticking = false;
  let args = [];

  return function(...rest) {
    args = updateArgs(rest); // !!! <-- here was saving of arguments !!!
    ...

@etimberg
Copy link
Member

etimberg commented Dec 5, 2022

@abaksha-sc are you able to test if #10942 resolves this for you?

@abaksha-sc
Copy link
Author

@etimberg , no. It will not work. Check here: https://jsfiddle.net/uhoL6cjz/
In your version const argsToUse will be saved to the closure of each function call.

You need to level up this variable to move out from closure:

export function throttled<TArgs extends Array<any>>(
  fn: (...args: TArgs) => void,
  thisArg: any,
) {
  let ticking = false;
  let argsToUse = [];

  return function(...args: TArgs) {
    // Save the args for use later
    argsToUse = Array.from(args) as TArgs;
    if (!ticking) {
      ticking = true;
      requestAnimFrame.call(window, () => {
        ticking = false;
        fn.apply(thisArg, argsToUse);
      });
    }
  };

@etimberg
Copy link
Member

etimberg commented Dec 6, 2022

@abaksha-sc I made that change if you're able to test again.

@abaksha-sc
Copy link
Author

@etimberg , yes, current version works fine. Thanks!

@etimberg
Copy link
Member

etimberg commented Dec 7, 2022

Amazing, thank you @abaksha-sc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants