Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to adjust brushing on a scatterplot matrix in d3?

Tags:

d3.js

I am using Mike Bostok's Block for brushing a scatterplot matrix.

I want to brush the diagonal plots of the matrix using an opacity number; middle of brush range opacity = 1 and opacity value = low on the sides of the brush range.

I couldn't find a hint online, besides this question, which does an identical action but using dc.js (with help of pretransition attribute).

Can this be achieved in d3?

Running fiddle

like image 481
Antonio Avatar asked Apr 26 '18 17:04

Antonio


People also ask

How does a scatterplot matrix work?

A scatter plot matrix is a grid (or matrix) of scatter plots used to visualize bivariate relationships between combinations of variables. Each scatter plot in the matrix visualizes the relationship between a pair of variables, allowing many relationships to be explored in one chart.


1 Answers

Interesting question.

Here's a code snippet that'll set opacity on the selected circles by their distance from the center of the selection. It does this on the brush end with a nice transition. Here's the relevant method:

function brushend(p) {

    // reset opacity
    svg.selectAll("circle").style('opacity', 1);

    // if no brush then bail
    if (brush.empty()) {
      svg.selectAll(".hidden").classed("hidden", false);
      return;
    }

    // some calculations
    var e = brush.extent(),
        xC = (e[1][0] + e[0][0]) / 2,
        yC = (e[1][1] + e[0][1]) / 2,
        // maxD is the maximum of the distance to the two edges
        maxD = Math.max( Math.sqrt((xC - e[0][0])**2), Math.sqrt ((yC - e[0][1])**2) );

    // those circles not hidden
    svg.selectAll("circle:not(.hidden)")
      .transition()
      .duration(1000)
      .style("opacity", function(d) {
        // distance from furthest edge
        var xD = Math.sqrt( (xC - d[p.x])**2 + (yC - d[p.y])**2 );
        // set opacity as percent of distance
        return (maxD - xD) / maxD;
    });

  }

Running Code:

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

svg {
  font: 10px sans-serif;
  padding: 10px;
}

.axis,
.frame {
  shape-rendering: crispEdges;
}

.axis line {
  stroke: #ddd;
}

.axis path {
  display: none;
}

.cell text {
  font-weight: bold;
  text-transform: capitalize;
}

.frame {
  fill: none;
  stroke: #aaa;
}

circle {
  fill-opacity: .7;
}

circle.hidden {
  fill: #ccc !important;
}

.extent {
  fill: #000;
  fill-opacity: .125;
  stroke: #fff;
}

</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>

var width = 960,
    size = 230,
    padding = 20;

var x = d3.scale.linear()
    .range([padding / 2, size - padding / 2]);

var y = d3.scale.linear()
    .range([size - padding / 2, padding / 2]);

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

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

var color = d3.scale.category10();

d3.csv("https://gist.githubusercontent.com/mbostock/4063663/raw/13276d1a3d8e99e74c9fe33685da3afa6ed3a926/flowers.csv", function(error, data) {
  if (error) throw error;

  var domainByTrait = {},
      traits = d3.keys(data[0]).filter(function(d) { return d !== "species"; }),
      n = traits.length;

  traits.forEach(function(trait) {
    domainByTrait[trait] = d3.extent(data, function(d) { return d[trait]; });
  });

  xAxis.tickSize(size * n);
  yAxis.tickSize(-size * n);

  var brush = d3.svg.brush()
      .x(x)
      .y(y)
      .on("brushstart", brushstart)
      .on("brush", brushmove)
      .on("brushend", brushend);

  var svg = d3.select("body").append("svg")
      .attr("width", size * n + padding)
      .attr("height", size * n + padding)
    .append("g")
      .attr("transform", "translate(" + padding + "," + padding / 2 + ")");

  svg.selectAll(".x.axis")
      .data(traits)
    .enter().append("g")
      .attr("class", "x axis")
      .attr("transform", function(d, i) { return "translate(" + (n - i - 1) * size + ",0)"; })
      .each(function(d) { x.domain(domainByTrait[d]); d3.select(this).call(xAxis); });

  svg.selectAll(".y.axis")
      .data(traits)
    .enter().append("g")
      .attr("class", "y axis")
      .attr("transform", function(d, i) { return "translate(0," + i * size + ")"; })
      .each(function(d) { y.domain(domainByTrait[d]); d3.select(this).call(yAxis); });

  var cell = svg.selectAll(".cell")
      .data(cross(traits, traits))
    .enter().append("g")
      .attr("class", "cell")
      .attr("transform", function(d) { return "translate(" + (n - d.i - 1) * size + "," + d.j * size + ")"; })
      .each(plot);

  // Titles for the diagonal.
  cell.filter(function(d) { return d.i === d.j; }).append("text")
      .attr("x", padding)
      .attr("y", padding)
      .attr("dy", ".71em")
      .text(function(d) { return d.x; });

  cell.call(brush);

  function plot(p) {
    var cell = d3.select(this);

    x.domain(domainByTrait[p.x]);
    y.domain(domainByTrait[p.y]);

    cell.append("rect")
        .attr("class", "frame")
        .attr("x", padding / 2)
        .attr("y", padding / 2)
        .attr("width", size - padding)
        .attr("height", size - padding);

    cell.selectAll("circle")
        .data(data)
      .enter().append("circle")
        .attr("cx", function(d) { return x(d[p.x]); })
        .attr("cy", function(d) { return y(d[p.y]); })
        .attr("r", 4)
        .style("fill", function(d) { return "#4682b4"; });
  }

  var brushCell;

  // Clear the previously-active brush, if any.
  function brushstart(p) {
    if (brushCell !== this) {
      d3.select(brushCell).call(brush.clear());
      x.domain(domainByTrait[p.x]);
      y.domain(domainByTrait[p.y]);
      brushCell = this;
    }
  }

  // Highlight the selected circles.
  function brushmove(p) {
    var e = brush.extent();
    svg.selectAll("circle").classed("hidden", function(d) {
      return e[0][0] > d[p.x] || d[p.x] > e[1][0]
          || e[0][1] > d[p.y] || d[p.y] > e[1][1];
    });
  }

  function brushend(p) {
    
    // reset opacity
    svg.selectAll("circle").style('opacity', 1);
    
    // if no brush then bail
    if (brush.empty()) {
      svg.selectAll(".hidden").classed("hidden", false);
      return;
    }
    
    // some calculations
    var e = brush.extent(),
        xC = (e[1][0] + e[0][0]) / 2,
        yC = (e[1][1] + e[0][1]) / 2,
        // maxD is the maximum of the distance to the two edges
        maxD = Math.max( Math.sqrt((xC - e[0][0])**2), Math.sqrt ((yC - e[0][1])**2) );
    
    // those circles not hidden
    svg.selectAll("circle:not(.hidden)")
      .transition()
      .duration(1000)
      .style("opacity", function(d) {
        // distance from furthest edge
        var xD = Math.sqrt( (xC - d[p.x])**2 + (yC - d[p.y])**2 );
        // set opacity as percent of distance
        return (maxD - xD) / maxD;
    });
    
  }
});

function cross(a, b) {
  var c = [], n = a.length, m = b.length, i, j;
  for (i = -1; ++i < n;) for (j = -1; ++j < m;) c.push({x: a[i], i: i, y: b[j], j: j});
  return c;
}

</script>
like image 92
Mark Avatar answered Oct 21 '22 10:10

Mark