D3 Scatter Plot Breaks on Mouse Events (ReactJS, Hooks)

My scatter plot breaks any time I mouse over any of the circles.

Edit: Updated link to newer version of codepen. Still same problem. Any help greatly appreciated!

https://codepen.io/dr_rompecabezas/pen/NWpbBBy

Console error:

Uncaught TypeError: d.Time.substring is not a function

This is the block where the substring method is used:

  data.forEach((d) => {
    let minutes = d.Time.substring(0, 2);
    let seconds = d.Time.substring(3);
    d.Time = new Date(1976, 6, 28, 0, minutes, seconds);
  });

This code runs fine as long as I do not mouse over the rendered circles. Its purpose is to reformat a string (mm:ss) as a Date Object in order to use the D3 time scale.

The rendered visualization passes all tests, except, of course, the ones that depend on the mouse events working.

Wondering whether the problem has to do with React re-renders, I tried to move this logic into the useEffect Hook that loads the JSON data, so this block runs only once, but it doesn’t work because the data state is still null. I am trying to figure out how to mutate the data inside the hook, but haven’t been able to do so far. All web examples I have seen are of csv files, using the row argument.

This is how the data is fetched:

  useEffect(() => {
    d3.json(url).then((json) => {
      setData(json);
    });
  }, []);

I have also rewritten the data mutating function copied at the top of this post in many different ways (e.g., map instead of forEach), to no avail. The scatter plot renders fine with all my approaches but breaks as soon as the mouse enters a circle.

This is how circles are rendered via the Marks component:

<circle
        key={data[i].Place}
        data-xvalue={xValue(d)}
        data-yvalue={yValue(d)}
        className="dot"
        cx={xScale(xValue(d))}
        cy={yScale(yValue(d))}
        r="7"
        fill={data[i].Doping ? "#FF4500" : "#228C22"}
        opacity="0.7"
        onMouseEnter={() => setHoveredValue([0, 0])} // [0, 0] will to be switched to [xValue(d), yValue(d)]
        onMouseLeave={() => setHoveredValue(null)}
        onMouseMove={handleMouseMove}
      />

Any help is greatly appreciated. Thank you.

Challenge: Visualize Data with a Scatterplot Graph

Link to the challenge:

Hi @felavid ,

The browser developer console shows the following error:

d.Time.substring is not a function

This line of code is in the handleMouseMove function. Is d.Time a date type?

Hi @manjupillai16,

d.Time is a string formatted like “mm:ss” (e.g., “39:11”)…

I use the substring method to convert all of the Time key values to Date objects.

  data.forEach((d) => {
    let minutes = d.Time.substring(0, 2);
    let seconds = d.Time.substring(3);
    d.Time = new Date(1976, 6, 28, 0, minutes, seconds);
  });

Here’s an example of the before and after-effects on the data of running this function.

  console.log(data[0].Time) // Returns "36:50"
  data.forEach((d) => {
    let minutes = d.Time.substring(0, 2);
    let seconds = d.Time.substring(3);
    d.Time = new Date(1976, 6, 28, 0, minutes, seconds);
  });
  console.log(data[0].Time) // Returns Date Wed Jul 28 1976 00:36:50 GMT-0400 (Eastern Daylight Time)

I tried console.log ( typeof d.Time ) inside the forEach as follows:

data.forEach((d) => {
    console.log(typeof d.Time);
    let minutes = d.Time.substring(0, 2);
    let seconds = d.Time.substring(3);
    d.Time = new Date(1976, 6, 28, 0, minutes, seconds);
  });

Initially the console shows ‘string’ but on mouse move, the type logged is ‘object’

When the visualization is rendered the dates have been used to build the marks (circle elements). So, it seems normal that they be objects at that point. They are only strings in the original data.

@felavid
Yeah, I am guessing here that when we move the mouse-maybe coz of a state change-, React re-renders the page and all the code gets executed again, thus throwing the error.

Sounds about right. I tried to embed the block of code that’s causing the error inside the useEffect hook that fetches the data, but I get a data null error (because the data hasn’t loaded yet and the default state is null). I have seen several examples of how to do this with csv files (using the row argument) but haven’t found any for json files.

I tried putting the piece of code inside a useEffect which is triggered only when data changes. Inside this useEffect, I change a state variable to trigger a render.

useEffect(() => {
 
    if (data){
      data.forEach((d) => {
       .....
      .....
      });
      setChanged(true);   //also created this state 'changed' and setting  it to trigger a render
    }
  
   },[data]);

This seems to be working as I don’t get the error but this is assuming that data only changes once, initially. Else it might still throw the error on subsequent setData.

The other workaround could be to just add the code inside an if loop to check typeof d.Time and execute only if it’s a string.

1 Like

:grinning:
Worked like a charm. Thank you so much. It seems so obvious now.

First, I tried the conditional statement.

  if (typeof data[0].Time === "string") {
    data.forEach((d) => {
      let minutes = d.Time.substring(0, 2);
      let seconds = d.Time.substring(3);
      d.Time = new Date(1976, 6, 28, 0, minutes, seconds);
    });
  }

As this block of code runs again in the re-render triggered by the mouse state change, Time had become a Date object. It was no longer the string it was when the data was first fetched.

Next, I tried the useEffect and it worked, too. I had tried something similar before but my mistake was trying to include the this block together with the fetch useEffect, as I have seen done for csv files, which was too early (or I couldn’t figure out the syntax).

I ended up going with this solution.

  useEffect(() => {
    if (data) {
      data.forEach((d) => {
        let minutes = d.Time.substring(0, 2);
        let seconds = d.Time.substring(3);
        d.Time = new Date(1976, 6, 28, 0, minutes, seconds);
      });
      setLoaded(true);
    }
  }, [data]);
1 Like

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.