Lurker
Lurker

Reputation: 43

Making an interactive scatter plot with d3?

I have a scatter plot based on this example http://bl.ocks.org/mbostock/3887118.

What i want to do is add interactivity by allowing the user to choose the x and y dimensions, as well as the size of the circles based on one of the columns in the data file. When they choose something, the graph should automatically scale the axis.

How would i go about doing this? Here is my code so far: http://plnkr.co/edit/ZCdEBa79Y85Koz7MepXw?p=preview

Thanks.

<!DOCTYPE html>
<meta charset="utf-8">
<style>

/*set the axis line color, dot stroke, font size, and font position*/
body {
  font: 15px sans-serif;
}

.name{
  position: relative;
  top: 90px;
  text-align: left;
  font-weight: bold;
}

.title {
  position: relative;
  text-align: left;
  font-size: 25px;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.dot {
  stroke: #000;
}

#filter {
  position: absolute;
}

#mark {
  padding-left: 150px;
  position: inherit;
}

#xAXs {
  position: relative;
  left: 290px;
  bottom: 30px;
}

#yAXs {
position: relative;
bottom: 30px;
left: 315px;

}

#label {
position: absolute;
top: 599px;
bottom: 125px;
left: 300px;
right: 0px;
}

#label2 {
position: absolute;
top: 599px;
bottom: 125px;
left: 430px;
right: 0px;
}

</style>

<body>


<script src="d3.min.js"></script>

<script>

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;


var x = d3.scale.linear()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);


var color = d3.scale.category10();

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");


var svg = d3.select("body").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 + ")");


d3.csv("iris.csv", function(error, data) {
  data.forEach(function(d) {
    d.petalLength = +d.petalLength;
    d.petalWidth = +d.petalWidth;
    d.sepalLength = +d.sepalLength;
    d.sepalWidth = +d.sepalWidth;
  });

  x.domain(d3.extent(data, function(d) { return d.petalWidth; })).nice();
  y.domain(d3.extent(data, function(d) { return d.petalLength; })).nice();

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis)
    .append("text")
      .attr("class", "label")
      .attr("x", width)
      .attr("y", -6)
      .style("text-anchor", "end")
      .text("Petal Width (cm)");

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("class", "label")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("Petal Length (cm)")

 var circles = svg.selectAll(".dot")
      .data(data)
    .enter().append("circle")
      .attr("class", "dot")
      .attr("r", 3.5)
      .attr("cx", function(d) { return x(d.petalWidth); })
      .attr("cy", function(d) { return y(d.petalLength); })
      .style("fill", function(d) { return color(d.species); });


  var legend = svg.selectAll(".legend")
      .data(color.domain())
      .enter().append("g")
      .attr("class", "legend")
      .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });


  legend.append("rect")
      .attr("x", width - 18)
      .attr("width", 18)
      .attr("height", 18)
      .style("fill", color);


  legend.append("text")
      .attr("x", width - 24)
      .attr("y", 9)
      .attr("dy", ".35em")
      .style("text-anchor", "end")
      .text(function(d) { return d; });



  d3.selectAll("[name=v]").on("change", function() {
      var selected = this.value;
      display = this.checked ? "inline" : "none";


  svg.selectAll(".dot")
      .filter(function(d) {return selected == d.species;})
      .attr("display", display);
      });



  d3.selectAll("[name=sepal]").on("change", function(d) {
     radius = this.value;


  svg.selectAll(".dot")
     console.log(radius);
     circles.attr("r", radius);
     });



  d3.select("[name=xAX]").on("change", function(){
    xAxy = this.value;
    console.log(xAxy)
  })

  d3.select("[name=yAX]").on("change", function(){
    yAxy = this.value;
    console.log(yAxy)
  })

});

</script>
<br><br>


  <div id="filter">
    <b>Species Filter:</b>
        <br>
    <input name='v' value="Iris-setosa" type="checkbox" checked>Iris-setosa
    </input>
        <br>
    <input name="v" value="Iris-versicolor" type="checkbox" checked >Iris-versicolor
    </input>
        <br>
    <input name="v" value="Iris-virginica" type="checkbox" checked >Iris-virginica
    </input>
  </div>


  <form id="mark">
    <b>Size of Mark:</b>
    <div><input type="radio" name="sepal" value='sepalWidth'>Sepal Width</div>
    <div><input type="radio" name="sepal" value="sepalLength">Sepal Length</div>
  </form>

<div id="label"><b>x-Axis:</b></div>
  <select name="xAX" id="xAXs">
        <option value ="petalWidth">petalWidth</option>
        <option value ="petalLength">petalLength</option>
        <option value ="sepalLength">sepalLength</option>
        <option value ="sepalWidth">sepalWidth</option>
  </select>

<div id="label2"><b>y-Axis:</b></div>
  <select name="yAX" id="yAXs">
        <option value ="petalLength">petalLength</option>
        <option value ="petalWidth">petalWidth</option>
        <option value ="sepalLength">sepalLength</option>
        <option value ="sepalWidth">sepalWidth</option>
  </select>  

  <br>
</body>

Upvotes: 4

Views: 5041

Answers (2)

Ben Lyall
Ben Lyall

Reputation: 1976

Try this plunkr: http://plnkr.co/edit/MkZcXJPS7hrcWh3M0MZ1?p=preview

You were almost there, I just had to update a couple of your functions. The following describes the highlights:

d3.selectAll("[name=sepal]").on("change", function(d) {
   radius = this.value;

   svg.selectAll(".dot")
   console.log(radius);
   circles.attr("r", function(d) { return d[radius]; });
});

You are setting radius to the column in the csv that you want to read the radius from, so now you just have to update the radius of your circles in the svg, which is basically done the same way as when you originally set them up.

d3.select("[name=xAX]").on("change", function(){
  xAxy = this.value;
  console.log(xAxy)
  x.domain(d3.extent(data, function(d) { return d[xAxy]; })).nice();

  svg.select(".x.axis").transition().call(xAxis);

  svg.selectAll(".dot").transition().attr("cx", function(d) { 
      return x(d[xAxy]);
  });
  svg.selectAll(".x.axis").selectAll("text.label").text(axisNames[xAxy] + " (cm)");
});

d3.select("[name=yAX]").on("change", function(){
  yAxy = this.value;
  console.log(yAxy)
  y.domain(d3.extent(data, function(d) { return d[yAxy]; })).nice();
  svg.select(".y.axis").transition().call(yAxis);
  svg.selectAll(".dot").transition().attr("cy", function(d) { 
      return y(d[yAxy]);
  });
  svg.selectAll(".y.axis").selectAll("text.label").text(axisNames[yAxy] + " (cm)");
});

The changes for the x and y axes are basically the same as each other, with just the axis being referenced getting changed. Here I'm doing:

  1. Updating the domain with the new range (based on the value of xAxy or yAxy)
  2. Update the actual axis in the svg by setting up a transition and calling the xAxis or yAxis component again.
  3. Update the cx or cy position of each .dot
  4. Update the text on the axis by looking it up in a new array (axisNames) which gives a pretty print of the variable being graphed.

Upvotes: 2

Mark
Mark

Reputation: 108512

Your question is asking a lot. In general dynamic d3 visualizations follow the enter, exit, and update pattern. There's some great tutorial's out there to get you started.

To fix your specific code, you divide into three parts. First init the non-update-able portions of the plot:

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 500 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.linear()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);

var color = d3.scale.category10();

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var svg = d3.select("body").insert("svg",":first-child")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// keep reference to axises
var xg = svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")");
xg
  .append("text")
  .attr("class", "label")
  .attr("x", width)
  .attr("y", -6)
  .style("text-anchor", "end");

var yg = svg.append("g")
      .attr("class", "y axis");
yg
  .append("text")
  .attr("class", "label")
  .attr("transform", "rotate(-90)")
  .attr("y", 6)
  .attr("dy", ".71em")
  .style("text-anchor", "end");

// legend is always static
var legend = svg.selectAll(".legend")
    .data(color.domain())
    .enter().append("g")
    .attr("class", "legend")
    .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

legend.append("rect")
    .attr("x", width - 18)
    .attr("width", 18)
    .attr("height", 18)
    .style("fill", color);

legend.append("text")
    .attr("x", width - 24)
    .attr("y", 9)
    .attr("dy", ".35em")
    .style("text-anchor", "end")
    .text(function(d) { return d; });

Then wrap the update-able portion in a function:

function update(){

  // get user selections
  var xVar = d3.select('#xAXs').node().value,
    yVar = d3.select('#yAXs').node().value;

  var checks = {};
  d3.selectAll('input[type=checkbox]').each(function(){
    checks[this.value] = this.checked;
  });

  var radAttr = d3.select('input[type=radio]:checked').node().value;

  // filter data based on user selections
  var data = baseData.filter(function(d,i){
    d.x = d[xVar]; // create/modify a x,y so that d3 will know it's an update
    d.y = d[yVar];
    return checks[d.species];
  });

  // set domains
  x.domain(d3.extent(data, function(d) { return d.x; })).nice();
  y.domain(d3.extent(data, function(d) { return d.y; })).nice();

  xg.call(xAxis);
  yg.call(yAxis);

  xg.select("text").text(xVar);
  yg.select("text").text(yVar);

  // on enter
  var circles = svg.selectAll(".dot")
    .data(data);

  circles.enter()
    .append("circle")
    .attr("class", "dot");

  circles.exit().remove();

  // on update
  circles.attr("cx", function(d) { return x(d.x); })
    .attr("cy", function(d) { return y(d.y); })
    .style("fill", function(d) { return color(d.species); })
    .attr("r", function(d){ return d[radAttr]; });
}

Finally trigger update on user actions:

d3.selectAll('select').on('change',function(){
  update();
});

d3.selectAll('input').on('click', function(){
  update();
})

Here's an example putting this together.

Upvotes: 2

Related Questions