Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3js Grouped Scatter plot with no collision

I am using this example to make scatter plot:

https://www.d3-graph-gallery.com/graph/boxplot_show_individual_points.html

Now this example uses jitter to randomize x position of the dots for demonstration purpose, but my goal is to make these dots in that way so they don't collide and to be in the same row if there is collision.

Best example of what I am trying to do (visually) is some sort of beeswarm where data is represented like in this fiddle:

https://jsfiddle.net/n444k759/4/

Snippet of first example:

// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 40},
    width = 460 - margin.left - margin.right,
    height = 400 - margin.top - margin.bottom;

// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
  .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")");

// Read the data and compute summary statistics for each specie
d3.csv("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/iris.csv", function(data) {

  // Compute quartiles, median, inter quantile range min and max --> these info are then used to draw the box.
  var sumstat = d3.nest() // nest function allows to group the calculation per level of a factor
    .key(function(d) { return d.Species;})
    .rollup(function(d) {
      q1 = d3.quantile(d.map(function(g) { return g.Sepal_Length;}).sort(d3.ascending),.25)
      median = d3.quantile(d.map(function(g) { return g.Sepal_Length;}).sort(d3.ascending),.5)
      q3 = d3.quantile(d.map(function(g) { return g.Sepal_Length;}).sort(d3.ascending),.75)
      interQuantileRange = q3 - q1
      min = q1 - 1.5 * interQuantileRange
      max = q3 + 1.5 * interQuantileRange
      return({q1: q1, median: median, q3: q3, interQuantileRange: interQuantileRange, min: min, max: max})
    })
    .entries(data)

  // Show the X scale
  var x = d3.scaleBand()
    .range([ 0, width ])
    .domain(["setosa", "versicolor", "virginica"])
    .paddingInner(1)
    .paddingOuter(.5)
  svg.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x))

  // Show the Y scale
  var y = d3.scaleLinear()
    .domain([3,9])
    .range([height, 0])
  svg.append("g").call(d3.axisLeft(y))

  // Show the main vertical line
  svg
    .selectAll("vertLines")
    .data(sumstat)
    .enter()
    .append("line")
      .attr("x1", function(d){return(x(d.key))})
      .attr("x2", function(d){return(x(d.key))})
      .attr("y1", function(d){return(y(d.value.min))})
      .attr("y2", function(d){return(y(d.value.max))})
      .attr("stroke", "black")
      .style("width", 40)

  // rectangle for the main box
  var boxWidth = 100
  svg
    .selectAll("boxes")
    .data(sumstat)
    .enter()
    .append("rect")
        .attr("x", function(d){return(x(d.key)-boxWidth/2)})
        .attr("y", function(d){return(y(d.value.q3))})
        .attr("height", function(d){return(y(d.value.q1)-y(d.value.q3))})
        .attr("width", boxWidth )
        .attr("stroke", "black")
        .style("fill", "#69b3a2")

  // Show the median
  svg
    .selectAll("medianLines")
    .data(sumstat)
    .enter()
    .append("line")
      .attr("x1", function(d){return(x(d.key)-boxWidth/2) })
      .attr("x2", function(d){return(x(d.key)+boxWidth/2) })
      .attr("y1", function(d){return(y(d.value.median))})
      .attr("y2", function(d){return(y(d.value.median))})
      .attr("stroke", "black")
      .style("width", 80)
      
var simulation = d3.forceSimulation(data)
    .force("x", d3.forceX(function(d) { return x(d.Species); }))
    // .force("y", d3.forceX(function(d) { return y(d.Sepal_lenght) }))
    .force("collide", d3.forceCollide()
             .strength(1)
             .radius(4+1))
    .stop();

      for (var i = 0; i < data.length; ++i) simulation.tick();

// Add individual points with jitter
var jitterWidth = 50
svg
  .selectAll("points")
  .data(data)
  .enter()
  .append("circle")
    .attr("cx", function(d){return( d.x )})
    .attr("cy", function(d){return(y(d.Sepal_Length))})
    .attr("r", 4)
    .style("fill", "white")
    .attr("stroke", "black")


})
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>

<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>

I tried to make something like this:

var simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(function(d) { return x(d.Species); }))
  .force("collide", d3.forceCollide(4)
            .strength(1)
            .radius(4+1))
  .stop();

  for (var i = 0; i < 120; ++i) simulation.tick();

// Append circle points
svg.selectAll(".point")
.data(data)
.enter()
.append("circle")
    .attr("cx", function(d){ 
        return(x(d.x))
    })
    .attr("cy", function(d){
        return(y(d.y))
    })
    .attr("r", 4)
    .attr("fill", "white")
    .attr("stroke", "black")

but it does not even prevent collision and I am a bit confused with it.

I also tried to modify plot from this example:

http://bl.ocks.org/asielen/92929960988a8935d907e39e60ea8417

where beeswarm looks exactly what I need to achieve. But this code is way too expanded as it is made to fit the purpose of reusable charts and I can't track what exact formula is used to achieve this:

enter image description here

Any help would be great.. Thanks

like image 903
Hidajet Tuzlak Avatar asked May 19 '19 23:05

Hidajet Tuzlak


1 Answers

Here's a quick example which combines the ideas of your beeswarm example with your initial boxplot. I've commented the tricky parts below:

<!DOCTYPE html>
<html>

<head>
</head>

<body>
  <!-- Load d3.js -->
  <script src="https://d3js.org/d3.v4.js"></script>

  <!-- Create a div where the graph will take place -->
  <div id="my_dataviz"></div>

  <script>
    // set the dimensions and margins of the graph
    var margin = {
        top: 10,
        right: 30,
        bottom: 30,
        left: 40
      },
      width = 460 - margin.left - margin.right,
      height = 400 - margin.top - margin.bottom;

    // append the svg object to the body of the page
    var svg = d3.select("#my_dataviz")
      .append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform",
        "translate(" + margin.left + "," + margin.top + ")");

    // Read the data and compute summary statistics for each specie
    d3.csv("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/iris.csv", function(data) {

      // Compute quartiles, median, inter quantile range min and max --> these info are then used to draw the box.
      var sumstat = d3.nest() // nest function allows to group the calculation per level of a factor
        .key(function(d) {
          return d.Species;
        })
        .rollup(function(d) {
          q1 = d3.quantile(d.map(function(g) {
            return g.Sepal_Length;
          }).sort(d3.ascending), .25)
          median = d3.quantile(d.map(function(g) {
            return g.Sepal_Length;
          }).sort(d3.ascending), .5)
          q3 = d3.quantile(d.map(function(g) {
            return g.Sepal_Length;
          }).sort(d3.ascending), .75)
          interQuantileRange = q3 - q1
          min = q1 - 1.5 * interQuantileRange
          max = q3 + 1.5 * interQuantileRange
          return ({
            q1: q1,
            median: median,
            q3: q3,
            interQuantileRange: interQuantileRange,
            min: min,
            max: max
          })
        })
        .entries(data)

      // Show the X scale
      var x = d3.scaleBand()
        .range([0, width])
        .domain(["setosa", "versicolor", "virginica"])
        .paddingInner(1)
        .paddingOuter(.5)
      svg.append("g")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x))

      // Show the Y scale
      var y = d3.scaleLinear()
        .domain([3, 9])
        .range([height, 0])
      svg.append("g").call(d3.axisLeft(y))

      // Show the main vertical line
      svg
        .selectAll("vertLines")
        .data(sumstat)
        .enter()
        .append("line")
        .attr("x1", function(d) {
          return (x(d.key))
        })
        .attr("x2", function(d) {
          return (x(d.key))
        })
        .attr("y1", function(d) {
          return (y(d.value.min))
        })
        .attr("y2", function(d) {
          return (y(d.value.max))
        })
        .attr("stroke", "black")
        .style("width", 40)

      // rectangle for the main box
      var boxWidth = 100
      svg
        .selectAll("boxes")
        .data(sumstat)
        .enter()
        .append("rect")
        .attr("x", function(d) {
          return (x(d.key) - boxWidth / 2)
        })
        .attr("y", function(d) {
          return (y(d.value.q3))
        })
        .attr("height", function(d) {
          return (y(d.value.q1) - y(d.value.q3))
        })
        .attr("width", boxWidth)
        .attr("stroke", "black")
        .style("fill", "#69b3a2")

      // Show the median
      svg
        .selectAll("medianLines")
        .data(sumstat)
        .enter()
        .append("line")
        .attr("x1", function(d) {
          return (x(d.key) - boxWidth / 2)
        })
        .attr("x2", function(d) {
          return (x(d.key) + boxWidth / 2)
        })
        .attr("y1", function(d) {
          return (y(d.value.median))
        })
        .attr("y2", function(d) {
          return (y(d.value.median))
        })
        .attr("stroke", "black")
        .style("width", 80)

      var r = 8;
      // create a scale that'll return a discreet value
      // so that close y values fall in a line
      var yPtScale = y.copy()
        .range([Math.floor(y.range()[0] / r), 0])
        .interpolate(d3.interpolateRound)
        .domain(y.domain());
      
      // bucket the data
      var ptsObj = {};
      data.forEach(function(d,i) {
        var yBucket = yPtScale(d.Sepal_Length);
        if (!ptsObj[d.Species]){
          ptsObj[d.Species] = {};
        }
        if (!ptsObj[d.Species][yBucket]){
          ptsObj[d.Species][yBucket] = [];
        }
        ptsObj[d.Species][yBucket].push({
          cy: yPtScale(d.Sepal_Length) * r,
          cx: x(d.Species)
        });
      });
      
      // determine the x position
      for (var x in ptsObj){
        for (var row in ptsObj[x]) {
          var v = ptsObj[x][row], // array of points
              m = v[0].cx, // mid-point
              l = m - (((v.length / 2) * r) - r/2); // left most position based on count of points in the bucket

          v.forEach(function(d,i){
            d.cx = l + (r * i); // x position
          });
        }
      }

      // flatten the data structure
      var flatData = Object.values(ptsObj)
                      .map(function(d){return Object.values(d)})
                      .flat(2);

      svg
        .selectAll("points")
        .data(flatData)
        .enter()
        .append("circle")
        .attr("cx", function(d) {
          return d.cx;
        })
        .attr("cy", function(d) {
          return d.cy;
        })
        .attr("r", 4)
        .style("fill", "white")
        .attr("stroke", "black")


    })
  </script>
</body>

</html>
like image 110
Mark Avatar answered Nov 17 '22 22:11

Mark