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
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.

Your
xscalehasdomain([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.