skip to Main Content

I’m trying to build a Density Plot using React and D3.js.

I would like the user to be able to hover on any part of the graph and see a tooltip displaying additional info on that specific point, but I’m not sure how I would add tooltips to the path element. I couldn’t find any relevant examples. Has somebody worked on something similar?

Code: https://playcode.io/1640953

enter image description here

import {useMemo, useRef } from "react";
import * as d3 from "d3";

import  AxisBottom  from "./axisBottom";
import  AxisLeft  from "./axisTop";
import importedData from "./data"

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


export const DensityChart = ({ width = 700, height = 400}) => {
  const boundsWidth = width - MARGIN.right - MARGIN.left;
  const boundsHeight = height - MARGIN.top - MARGIN.bottom;

  const refs = useRef({
    dataMin: Math.min(...importedData),
    dataMax: Math.max(...importedData),
    domain: [Math.min(...importedData), Math.max(...importedData)]
  })

  const xScale = useMemo(() => {
    try {
      const result = d3.scaleLinear().domain([refs.current.dataMin, refs.current.dataMax]).range([10, boundsWidth - 10]);
      return result
    } catch (error) {
      console.error("Error creating xScale:", error);
      return d3.scaleLinear().domain([0, 1]).range([10, boundsWidth - 10]);
    }
  }, [importedData, width]);

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

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

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


  return (
    <section>
    <svg height="500" width="700">
      <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"
        />
        <g transform={`translate(0, ${boundsHeight})`}>
          <AxisLeft yScale={yScale} pixelsPerTick={40} />
        </g>
        <g transform={`translate(0, ${boundsHeight})`}>
          <AxisBottom xScale={xScale} pixelsPerTick={40} />
        </g>

      </g>
    </svg>
    </section>
  );
}

export default DensityChart;

// Function to compute density
function kernelDensityEstimator(kernel, X) {
  return function(V) {
    return X.map(function(x) {
      return [x, d3.mean(V, function(v) { return kernel(x - +v )})];
    });
  };
}
function kernelEpanechnikov(bandwidth) {
  return function(v) {
    const result = Math.abs(v /= bandwidth) <= 1 ? 0.75 * (1 - v * v) / bandwidth : 0;
    return result
  };
}

2

Answers


  1. Realistically your problem does not really have anything to do with d3. It can be resolved with normal javascript. SVG path still respect pointer events normally (as long as they have fill color or pointer event explicitly defined)

    For instance using css to make path sections change color:

    path {
        fill:blue;
        stroke:black;
    }
    path:hover{
        fill:red;
        stroke:blue;
    }
    <svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
     <g>
      <title>Layer 1</title>
      <path id="svg_3" d="m200,114l-114,95l26,97l99,20c0,0 19,-67 19,-68c0,-1 -1,-5 4,-8c5,-3 39,-10 40,-10c1,0 13,-2 14,-9c1,-7 -4,-36 -8,-40c-4,-4 -23,-15 -27,-17c-4,-2 -24,-16 -24,-23c0,-7 -1,-15 -1,-21c0,-6 -6,-19 -7,-19c-1,0 -21,3 -21,3z" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="none"/>
      <path id="svg_4" d="m244,101c0,0 14,55 16,56c2,1 22,11 24,13c2,2 23,17 24,18c1,1 1,28 -1,31c-2,3 -3,25 -9,30c-6,5 -32,14 -35,14c-3,0 -8,5 -10,8c-2,3 -10,37 -10,37c0,0 7,10 16,15c9,5 53,12 59,12c6,0 30,0 40,-8c10,-8 34,-7 35,-31c1,-24 1,-48 1,-65c0,-17 -13,-61 -15,-66c-2,-5 -21,-21 -21,-23c0,-2 34,-20 44,-15c10,5 29,24 33,28c4,4 10,20 16,5c6,-15 28,-31 -1,-46c-29,-15 -25,-24 -55,-25c-30,-1 -42,-5 -53,-5c-11,0 -46,-2 -52,1c-6,3 -46,16 -46,16z" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="none"/>
      <path id="svg_5" d="m428,180c0,0 -3,52 -4,53c-1,1 -2,27 -2,31c0,4 -8,29 -11,34c-3,5 -15,36 -21,38c-6,2 -77,18 -81,18c-4,0 -68,0 -68,7c0,7 -1,18 8,23c9,5 23,9 45,14c22,5 97,12 111,6c14,-6 44,-20 55,-30c11,-10 28,-28 37,-42c9,-14 14,-15 23,-40c9,-25 16,-109 12,-114c-4,-5 -32,-12 -45,-9c-13,3 -59,11 -59,11z" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="none"/>
     </g>
    </svg>

    You can add javascript events the same way:

    Array
      .from(document.querySelectorAll("path"))
      .forEach((el) => {
        el.onmouseover = () => console.log("Mouse Over " + el.id);
      })
    path {
        fill:blue;
        stroke:black;
    }
    <svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
     <g>
      <path id="svg_3" d="m200,114l-114,95l26,97l99,20c0,0 19,-67 19,-68c0,-1 -1,-5 4,-8c5,-3 39,-10 40,-10c1,0 13,-2 14,-9c1,-7 -4,-36 -8,-40c-4,-4 -23,-15 -27,-17c-4,-2 -24,-16 -24,-23c0,-7 -1,-15 -1,-21c0,-6 -6,-19 -7,-19c-1,0 -21,3 -21,3z" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="none"/>
      <path id="svg_4" d="m244,101c0,0 14,55 16,56c2,1 22,11 24,13c2,2 23,17 24,18c1,1 1,28 -1,31c-2,3 -3,25 -9,30c-6,5 -32,14 -35,14c-3,0 -8,5 -10,8c-2,3 -10,37 -10,37c0,0 7,10 16,15c9,5 53,12 59,12c6,0 30,0 40,-8c10,-8 34,-7 35,-31c1,-24 1,-48 1,-65c0,-17 -13,-61 -15,-66c-2,-5 -21,-21 -21,-23c0,-2 34,-20 44,-15c10,5 29,24 33,28c4,4 10,20 16,5c6,-15 28,-31 -1,-46c-29,-15 -25,-24 -55,-25c-30,-1 -42,-5 -53,-5c-11,0 -46,-2 -52,1c-6,3 -46,16 -46,16z" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="none"/>
      <path id="svg_5" d="m428,180c0,0 -3,52 -4,53c-1,1 -2,27 -2,31c0,4 -8,29 -11,34c-3,5 -15,36 -21,38c-6,2 -77,18 -81,18c-4,0 -68,0 -68,7c0,7 -1,18 8,23c9,5 23,9 45,14c22,5 97,12 111,6c14,-6 44,-20 55,-30c11,-10 28,-28 37,-42c9,-14 14,-15 23,-40c9,-25 16,-109 12,-114c-4,-5 -32,-12 -45,-9c-13,3 -59,11 -59,11z" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="none"/>
     </g>
    </svg>

    So you can either use simple css to show a tooltip or even javascript for more custom tooltip behaviour.

    .tooltip {
      position: relative;
      display: inline-block;
      border-bottom: 1px dotted black;
    }
    
    .tooltip .tooltiptext {
      visibility: hidden;
      width: 120px;
      background-color: #555;
      color: #fff;
      text-align: center;
      border-radius: 6px;
      padding: 5px 0;
      position: absolute;
      z-index: 1;
      top: 125%;
      left: 50%;
      margin-left: -60px;
      opacity: 0;
      transition: opacity 0.3s;
    }
    
    .tooltip:hover .tooltiptext {
      visibility: visible;
      opacity: 1;
    }
    <div class="tooltip">Hover over me
      <span class="tooltiptext">Tooltip text</span>
    </div>
    Login or Signup to reply.
  2. I had found error in X-Axis, I updated Code and Now it’s show correct way.

    d3.scaleLinear().domain([refs.current.dataMin, refs.current.dataMax])
    to
    d3.scaleLinear().domain([0 refs.current.dataMax])

    const xScale = useMemo(() => {
        try {
          const result = d3.scaleLinear().domain([0, refs.current.dataMax]).range([ 10 , boundsWidth - 10 ]);
          return result
        } catch (error) {
          console.error("Error creating xScale:", error);
          return d3.scaleLinear().domain([0, 1]).range([boundsWidth - 10 , 10]);
        }
      }, [importedData, width]);
    
    

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search