Search code examples
javascriptd3.jsdata-visualizationvisualization

How to draw a scatter plot with multiple y values for each x value?


I have data in the following format which I would like to plot using d3:

data = [
        { x: 0.2, y: [ 1, 2, 4 ] },
        { x: 0.3, y: [ 2 ] },
        { x: 0.5, y: [ 4, 7, 8, 12, 19 ] }
        { x: 1.4, y: [ 1, 3 ] }
       ]

Normally the y-axis values are integers, but here they are arrays, so the following code doesn't work as intended:

svg.selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
        .attr("cx", function(d){ return x(d.x) })
        .attr("cy", function(d){ return y(d.y) })
        .attr("r", 2)

Instead of getting multiple circles plotted for each of the values in the array, I only get one.

Other similar questions on this website deal only with data that has a fixed number of y-axis values, so I haven't found a way to modify those solutions for this problem.


Solution

  • The traditional D3 answer here would be appending a group for each object and then appending a circle for each y value for each group.

    However, since you seem to be a D3 beginner (correct me if I'm wrong), I'd suggest to just create a single array of objects, that you can pass to data.

    There are several ways for doing this, such as:

    const newData = data.reduce(function(a, c) {
      return a.concat(c.y.map(function(d) {
        return {
          x: c.x,
          y: d
        }
      }));
    }, []);
    

    Here is your code with that change:

    const data = [{
        x: 0.2,
        y: [1, 2, 4]
      },
      {
        x: 0.3,
        y: [2]
      },
      {
        x: 0.5,
        y: [4, 7, 8, 12, 19]
      }, {
        x: 1.4,
        y: [1, 3]
      }
    ];
    
    const newData = data.reduce(function(a, c) {
      return a.concat(c.y.map(function(d) {
        return {
          x: c.x,
          y: d
        }
      }));
    }, []);
    
    const x = d3.scaleLinear()
      .domain([0, 2])
      .range([0, 300]);
    
    const y = d3.scaleLinear()
      .domain([0, 20])
      .range([0, 150]);
    
    const svg = d3.select("svg");
    svg.selectAll("circle")
      .data(newData)
      .enter()
      .append("circle")
      .attr("cx", function(d) {
        return x(d.x)
      })
      .attr("cy", function(d) {
        return y(d.y)
      })
      .attr("r", 4)
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    <svg></svg>