简体   繁体   中英

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( dx )}) .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:

在此输入图像描述

Any help would be great.. Thanks

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> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM