Search code examples
javascriptd3.jszoomingbrush

Trying to user scatterplots in a d3 chart with brush/zoom


I currently trying to learn d3, and wanted to do a small project but was running into an issue. I have been using this block as a base: Brush & Zoom

and replace the area fill with a scatterplot data I had. I was able to get the points to render but when moving the zoom/brush on the second axis, the x-axis for the .focus chart transforms but my scatterplots didn't. I tried a number of ways including to see if I was appending it to the right svg element but in my limited understanding of D3, I'm not too sure what is wrong. My sample code is as follows:

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<style>

.area {
  fill: #FDB827;
  clip-path: url(#clip);
}

.zoom {
  cursor: move;
  fill: none;
  pointer-events: all;
}

</style>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

var svg = d3.select("svg"),
    margin = {top: 20, right: 20, bottom: 110, left: 40},
    margin2 = {top: 430, right: 20, bottom: 30, left: 40},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    height2 = +svg.attr("height") - margin2.top - margin2.bottom;

var parseDate = d3.timeParse("%b %Y");

var x = d3.scaleTime().range([0, width]),
    x2 = d3.scaleTime().range([0, width]),
    y = d3.scaleLinear().range([height, 0]),
    y2 = d3.scaleLinear().range([height2, 0]);

var xAxis = d3.axisBottom(x),
    xAxis2 = d3.axisBottom(x2),
    yAxis = d3.axisLeft(y);

var brush = d3.brushX()
    .extent([[0, 0], [width, height2]])
    .on("brush end", brushed);

var zoom = d3.zoom()
    .scaleExtent([1, Infinity])
    .translateExtent([[0, 0], [width, height]])
    .extent([[0, 0], [width, height]])
    .on("zoom", zoomed);

var area = d3.area()
    .curve(d3.curveMonotoneX)
    .x(function(d) { return x(d.date); })
    .y0(height)
    .y1(function(d) { return y(d.PTS); });

var area2 = d3.area()
    .curve(d3.curveMonotoneX)
    .x(function(d) { return x2(d.date); })
    .y0(height2)
    .y1(function(d) { return y2(d.PTS); });

svg.append("defs").append("clipPath")
    .attr("id", "clip")
  .append("rect")
    .attr("width", width)
    .attr("height", height);

var focus = svg.append("g")
    .attr("class", "focus")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var context = svg.append("g")
    .attr("class", "context")
    .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");

d3.csv("./data/kobe-playoff1.csv", type, function(error, data) {
  if (error) throw error;

  x.domain(d3.extent(data, function(d) { return d.date; }));
  y.domain([0, d3.max(data, function(d) { return d.PTS; })]);
  x2.domain(x.domain());
  y2.domain(y.domain());


  let fixedData ={};
  let arr = [];
  data.forEach((d)=>{
    fixedData["date"] = d.date;
    fixedData["PTS"] = d.PTS;
    arr.push(fixedData);
    fixedData = {};
  })

  //
  focus.append("path")
      .datum(arr)
      .attr("class", "area")
      .attr("d", area);


  // draw dots
  focus.selectAll("circle")
      .data(data)
      .enter().append("circle")
        .attr("class", "brush")
        // .attr("class", "brush")
        .attr("r", (d)=>{return (d.PTS)})
        .attr("cx", (d)=>{return x(d.date)})
        .attr("cy", (d)=>{return y(d.PTS)})
        .style("fill", "#552583");
      //   .on("mouseover", function(d) {
      //     tooltip.transition()
      //          .duration(200)
      //          .style("opacity", .9);
      //     tooltip.html(d["date"] + "<br/> (" + xValue(d)
      //     + ", " + yValue(d) + ")")
      //          .style("left", (d3.event.pageX + 5) + "px")
      //          .style("top", (d3.event.pageY - 28) + "px");
      // })
      // .on("mouseout", function(d) {
      //     tooltip.transition()
      //          .duration(500)
      //          .style("opacity", 0);
      // });

  focus.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

  focus.append("g")
      .attr("class", "axis axis--y")
      .call(yAxis);

  context.append("path")
      .datum(data)
      .attr("class", "area")
      .attr("d", area2);

  context.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + height2 + ")")
      .call(xAxis2);

  context.append("g")
      .attr("class", "brush")
      .call(brush)
      .call(brush.move, x.range());

  focus.append("g")
      .attr("class", "zoom")
      .attr("width", width)
      .attr("height", height)
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .call(zoom);



});

function brushed() {
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
  var s = d3.event.selection || x2.range();
  x.domain(s.map(x2.invert, x2));
  focus.select(".area").attr("d", area);
  focus.select(".axis--x").call(xAxis);
  svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
      .scale(width / (s[1] - s[0]))
      .translate(-s[0], 0));
}

function zoomed() {
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
  var t = d3.event.transform;
  x.domain(t.rescaleX(x2).domain());
  focus.select(".area").attr("d", area);
  focus.select(".axis--x").call(xAxis);
  context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}

function type(d) {
  // d.date = parseDate(d.date);
  // d.price = +d.price;
  // d.date = new Date(d.Date);
  d.date = Date.parse(d.Date);
  d.PTS = +d.PTS;
  return d;
}

</script>

The sample data I am using is as follows:

Rk,G,Date,Age,Tm,,Opp,,GS,MP,FG,FGA,FG%,3P,3PA,3P%,FT,FTA,FT%,ORB,DRB,TRB,AST,STL,BLK,TOV,PF,PTS,GmSc
1,1,1997-04-25,18-245,LAL,,POR,W (+18),0,1:00,1,1,1.000,0,0,,0,0,,0,0,0,0,0,0,0,0,2,1.7
2,2,1997-04-27,18-247,LAL,,POR,W (+14),0,5:00,1,3,.333,0,1,.000,4,4,1.000,0,0,0,0,0,0,0,1,6,3.9
3,3,1997-04-30,18-250,LAL,@,POR,L (-8),0,27:00,7,13,.538,2,3,.667,6,8,.750,0,4,4,2,1,0,4,5,22,12.5
4,4,1997-05-02,18-252,LAL,@,POR,W (+4),0,6:00,0,0,,0,0,,0,0,,0,0,0,0,0,0,0,0,0,0.0
5,5,1997-05-04,18-254,LAL,@,UTA,L (-16),0,14:00,1,7,.143,1,5,.200,0,1,.000,1,1,2,3,0,1,0,5,3,-0.1
6,6,1997-05-06,18-256,LAL,@,UTA,L (-2),0,4:00,1,1,1.000,0,0,,0,0,,0,0,0,1,0,0,1,2,2,0.6
7,7,1997-05-08,18-258,LAL,,UTA,W (+20),0,19:00,3,7,.429,0,2,.000,13,14,.929,0,1,1,3,1,1,3,3,19,14.8
8,8,1997-05-10,18-260,LAL,,UTA,L (-15),0,28:00,3,9,.333,3,6,.500,0,0,,0,2,2,0,0,0,5,4,9,-2.1
9,9,1997-05-12,18-262,LAL,@,UTA,L (-5),0,29:00,4,14,.286,0,6,.000,3,3,1.000,0,2,2,2,1,0,1,3,11,3.6

Sample image of the static scatter plot

Any help would be greatly appreciated! Thank you.


Solution

  • In your brushed and zoomed functions, you need to select your circles and apply dynamic attributes. But before you do that, you need to rename the class for your circles. Something like:

    // draw dots
    focus.selectAll("circle")
      .data(data)
      .enter().append("circle")
        .attr("class", "circle")
    

    ...

    function brushed() {
      if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
      var s = d3.event.selection || x2.range();
      x.domain(s.map(x2.invert, x2));
      focus.select(".area").attr("d", area);
      focus.select(".axis--x").call(xAxis);
      focus.selectAll(".circle")
        .attr("r", (d)=>{return (d.PTS)})
        .attr("cx", (d)=>{return x(d.date)})
        .attr("cy", (d)=>{return y(d.PTS)})
    
      svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
          .scale(width / (s[1] - s[0]))
          .translate(-s[0], 0));
    }
    
    function zoomed() {
      if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
      var t = d3.event.transform;
      x.domain(t.rescaleX(x2).domain());
      focus.select(".area").attr("d", area);
      focus.select(".axis--x").call(xAxis);
      focus.selectAll(".circle")
        .attr("r", (d)=>{return (d.PTS)})
        .attr("cx", (d)=>{return x(d.date)})
        .attr("cy", (d)=>{return y(d.PTS)})
      context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
    }