React/D3.js - Density Plot - Error: <path> attribute d: Expected number, "M10,NaNL12,NaNC14,Na…"

60 Views Asked by At

I've been asked to implement a Density Plot in React using D3.js, but my plot doesn't show anything on screen. I'm not very familiar with charts and data visualisation in general, so I'm struggling. Mostly I've been following the example shown here: https://www.react-graph-gallery.com/density-plot

enter image description here

In the console, I can see this error

Error: <path> attribute d: Expected number, "M10,NaNL12,NaNC14,Na…".

And the dataset I'm using looks like this

[1768, 1809, 3091, 2314, 2845, 3068, 2402, 2514, 2899, 2793, 3297, 2550, 3525, 2863, 3193, 1726, 2356, 2275, 2591, 2496, 1644, 2912, 2801, 2394, 3147, 3373, 2446, 2473, 2350, 1854, 3022, 3274, 3059, 1483, 1844, 2234, 2295, 2053, 2111, 2410, 2256, 2224, 2756, 2733, 2244, 1980, 2558, 3166, 2713, 2177, 2231, 1942]

Below is the work-in-progress code of my Chart component:

import { useEffect, useMemo, useState } from "react";
import * as d3 from "d3";
import * as statsPackage from "simple-statistics";
import importedData from "./data"

import  AxisBottom  from "./axisBottom";
import  AxisLeft  from "./AxisTop";

const MARGIN = { top: 30, right: 30, bottom: 50, left: 50 };

type DensityChartProps = {
  width?: number;
  height?: number;
  filteredDataset: number[];
};

export const DensityChart = ({ width = 700, height = 400, filteredDataset}: DensityChartProps) => {
  const boundsWidth = width - MARGIN.right - MARGIN.left;
  const boundsHeight = height - MARGIN.top - MARGIN.bottom;
  const usedData = filteredDataset?.map(num => Math.round(num) ) ?? importedData;

  const [states, setStates] = useState({
    stDev: statsPackage.standardDeviation(usedData),
    mean: statsPackage.mean(usedData),
    stDevs: 1
  })

  useEffect(() => {
    console.log("states", states);
    console.log("all numbers? ", filteredData.every(num => typeof num === "number"))
  }, [states])
  

  const filteredData = usedData.filter((value) =>
  {
    try {
      return Math.abs(value - states.mean) <= states.stDevs * states.stDev;
    } catch (error) {
      console.error("Error filtering data:", error);
      return false;
    }
  }
) ;

  const xScale = useMemo(() => {
    try {
      return d3.scaleLinear().domain([0, 1000]).range([10, boundsWidth - 10]);
    } catch (error) {
      console.error("Error creating xScale:", error);
      return d3.scaleLinear().domain([0, 1]).range([10, boundsWidth - 10]);
    }
  }, [filteredData, width, states.stDevs]);

  // Compute kernel density estimation
  const density = useMemo(() => {
    const kde = kernelDensityEstimator(kernelEpanechnikov(7), xScale.ticks(40));
    return kde(filteredData);
  }, [xScale]);

  const yScale = useMemo(() => {
    try {
      const max = Math.max(...density.map((d) => d[1]));
      console.log("d3.scaleLinear().range([boundsHeight, 0]).domain([0, max])", d3.scaleLinear().range([boundsHeight, 0]).domain([0, max]))
      return d3.scaleLinear().range([boundsHeight, 0]).domain([0, max]);
    } catch (error) {
      console.error("Error creating yScale:", error);
      return d3.scaleLinear().range([boundsHeight, 0]).domain([0, 1]);
    }
  }, [filteredData, height, states.stDevs]);

  const path = useMemo(() => {
    try {
      const lineGenerator = d3.line().x((d) => {
        console.log("xScale(d[0]", xScale(d[0]));
       return xScale(d[0])
      })
      
      .y((d) => {
      console.log("yScale(d[1]", yScale(d[1]));
        return yScale(d[1])
      })
      
      .curve(d3.curveBasis);
      console.log("lineGenerator(density)", lineGenerator(density));
      return lineGenerator(density);
    } catch (error) {
      console.error("Error creating path:", error);
      return "";
    }
  }, [density]);

  const handleStdDeviationChange = (e) => {
    try {
      setStates({ ...states, stDevs: Number(e.target.value) });
    } catch (error) {
      console.error("Error setting standard deviation:", error);
    }
  };

  return (
    <>
    <svg width={width} height={height}>
      <g
        width={boundsWidth}
        height={boundsHeight}
        transform={`translate(${[MARGIN.left, MARGIN.top].join(",")})`}
      >
        <path
          d={path}
          fill="blue"
          opacity={0.4}
          stroke="black"
          strokeWidth={1}
          strokeLinejoin="round"
        />

        {/* X axis, use an additional translation to appear at the bottom */}
        <g transform={`translate(0, ${boundsHeight})`}>
          <AxisLeft yScale={yScale} pixelsPerTick={40} />
        </g>
        <g transform={`translate(0, ${boundsHeight})`}>
          <AxisBottom xScale={xScale} pixelsPerTick={40} />
        </g>

      </g>
    </svg>
    <p>Standard deviation: {states.stDev}</p>
    <br />
    <p>Mean: {states.mean}</p>
    <br />
    <label>Display Data Within N Standard Deviations: </label>
      <input
        type="number"
        value={states.stDevs}
        onChange={handleStdDeviationChange}
        step="0.1"
      />
    </>
  );
};
export default DensityChart;

// TODO: improve types
// Function to compute density
function kernelDensityEstimator(kernel: (v: number) => number, X: number[]) {
  return function (V: number[]) {
    return X.map((x) => [x, d3.mean(V, (v) => kernel(x - v))]);
  };
}

function kernelEpanechnikov(k: number) {
  return function (v: number) {
    return Math.abs((v /= k)) <= 1 ? (0.75 * (1 - v * v)) / k : 0;
  };
}

However, if I use a dummy dataset looking like below, it seems to work fine:

75, 104, 369, 300, 92, 64, 265, 35, 287, 69, 52, 23, 287, 87, 114, 114, 98, 137, 87, 90, 63, 69, 80, 113, 58, 115, 30, 35, 92, 460, 74, 72, 63, 115, 60, 75, 31, 277, 52, 218, 132, 316, 127, 87, 449, 46, 345, 48, 184, 149, 345, 92, 749, 93, 9502, 138, 48, 87, 103, 32, 93, 57, 109, 127, 149, 78, 162, 173, 87, 184, 288, 576, 460, 150, 127, 92, 84, 115, 218, 404, 52, 85, 66, 52, 201, 287, 69, 114, 379, 115, 161, 91, 231, 230, 822, 115, 80, 58, 207, 171, …]

I imagine I must be making a really silly error, but I'd appreciate any help.

1

There are 1 best solutions below

2
rioV8 On

Your xscale has domain([0, 1000])

Resulting in that all your KDE buckets (xScale.ticks(40)) are from [0 ... 1000]

All your sample data is in range [2000 ... 3000].

Not a good match.

Result your KDE values are all 0.

Your graph is a horizontal line.