D3js分组散点图,无碰撞

时间:2019-05-19 23:58:03

标签: javascript d3.js

我正在使用此示例绘制散点图:

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

现在,此示例使用抖动将点的x位置随机化,以进行演示,但我的目标是以这种方式制作这些点,以使它们不会发生碰撞,并且在发生碰撞时位于同一行。

我试图(视觉上)做的最好的例子是某种蜂拥而至的方式,其中的数据表示像这样的小提琴:

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

第一个示例的片段:

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

我试图做这样的事情:

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")

但是它甚至不能防止碰撞,对此我有点困惑。

我还尝试通过此示例修改剧情:​​

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

蜂拥而至的地方正是我需要实现的目标。但是此代码过于扩展,因为它是为了适合可重用图表的目的而开发的,我无法跟踪使用什么确切的公式来实现此目的:

enter image description here

任何帮助都会很棒。 谢谢

1 个答案:

答案 0 :(得分:6)

这是一个快速示例,它将您的beeswarm示例的思想与初始箱线图结合在一起。我评论了以下棘手的部分:

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