artus9033 / chartjs-plugin-dragdata

Draggable data points plugin for Chart.js
MIT License
257 stars 55 forks source link

BUG: onDrag cannot read the updated series data #101

Closed riven314 closed 1 year ago

riven314 commented 1 year ago

Describe the bug @chrispahm I want to bound a datapoint within a range of its current value (e.g. +/-0.5) upon dragging (e.g. if the point is 1.5, it's bounded at (1.0, 2.0), if the point is 1.0, then it's bounded at (0.5, 1.5))

To achieve this, I use useState to initialise the series data (i.e. series). In onDrag callback, I make use of series to access the value before it's dragged. In onDragEnd, I update series by setSeries to make sure the state is sync with what the chart is showing.

Here is my brief code snippet:

const initSeries = [1, 3.5, 5, 2.8];
const [series, setSeries] = useState(initSeries);

// omit the details
...

onDrag: (e, datasetIndex, index, value) => {
          // for debugging
          console.log("series: ", series);
          if (value < series[index] - 0.5 || value > series[index] + 0.5) {
            return false;
          }
        }
onDragEnd: (e, datasetIndex, index, value) => {
          setSeries((arr) =>
            arr.map((x, xIndex) => (xIndex === index ? value : x))
          );
        }

However, it is not working as expected because I see series within onDrag is not properly updated after some drags. But I am sure series has been updated, as verified in my sample code below.

To Reproduce Here is my PoC code to reproduce the issue: https://codesandbox.io/s/draggablelinecharts-5dxk97?file=/src/App.js

chrispahm commented 1 year ago

Hey @riven314,

Sorry for taking so long to reply to your issue! Accessing up-to-date state from within a callback is actually non trivial:

In React hooks, due to the way state is encapsulated in the functions of React.useState(), if a callback gets the state through React.useState(), it will be stale (the value when the callback was setup)

Luckily this post from Stackoverflow provides an answer

  const initSeries = [1, 3.5, 5, 2.8];
  const [series, setSeries] = useState(initSeries);
  const stateRef = useRef();

  // make stateRef always have the current series state
  // the "fixed" callbacks from chartjs-plugin-dragdata
  // can refer to this object whenever they need the current value.
  // Note: the callbacks will not
  // be reactive - they will not re-run the instant state changes,
  // but they *will* see the current value whenever they do run
  stateRef.current = series;

        // ... later in the dragdata options, we need to reference 
        // stateRef.current instead of series:
        onDrag: (e, datasetIndex, index, value) => {
          console.log("onDrag series: ", stateRef.current);
          if (
            value < stateRef.current[index] - 0.5 ||
            value > stateRef.current[index] + 0.5
          ) {
            return false;
          }
        } 

One last gotcha is that the chartjs-plugin-dragdata mutates the charts input data (see #96), which is why we need to create a deep clone of the series when creating the chart instance:

const data = {
    labels: ["2020", "2021", "2022", "2023"],
    datasets: [
      {
        label: "alex",
        // this is necessary because the plugin
        // currently mutates all input data
        // see issue #96
        data: JSON.parse(JSON.stringify(series)),
        borderColor: "rgb(255, 99, 132)",
        backgroundColor: "rgba(255, 99, 132, 0.5)"
      }
    ]
  };

Here's a working fork of your Codesandbox example https://codesandbox.io/s/draggablelinecharts-forked-d36upe?file=/src/App.js

Hope this fixes your issue! If not, please re-open