Scatterplot Matrix

Scatterplot matrix design invented by J. A. Hartigan; see also R and GGobi. Data on Iris flowers collected by Edgar Anderson and published by Ronald Fisher.

Source Code

  1 d3.json("flowers.json", function(flower) {
  2 
  3   // Size parameters.
  4   var size = 150,
  5       padding = 20;
  6 
  7   // Color scale.
  8   var color = d3.scale.ordinal().range([
  9     "rgb(50%, 0%, 0%)",
 10     "rgb(0%, 50%, 0%)",
 11     "rgb(0%, 0%, 50%)"
 12   ]);
 13 
 14   // Position scales.
 15   var position = {};
 16   flower.traits.forEach(function(trait) {
 17     function value(d) { return d[trait]; }
 18     position[trait] = d3.scale.linear()
 19         .domain([d3.min(flower.values, value), d3.max(flower.values, value)])
 20         .range([padding / 2, size - padding / 2]);
 21   });
 22 
 23   // Root panel.
 24   var svg = d3.select("#chart")
 25     .append("svg:svg")
 26       .attr("width", size * flower.traits.length)
 27       .attr("height", size * flower.traits.length);
 28 
 29   // One column per trait.
 30   var column = svg.selectAll("g")
 31       .data(flower.traits)
 32     .enter().append("svg:g")
 33       .attr("transform", function(d, i) { return "translate(" + i * size + ",0)"; });
 34 
 35   // One row per trait.
 36   var row = column.selectAll("g")
 37       .data(cross(flower.traits))
 38     .enter().append("svg:g")
 39       .attr("transform", function(d, i) { return "translate(0," + i * size + ")"; });
 40 
 41   // X-ticks. TODO Cross the trait into the tick data?
 42   row.selectAll("line.x")
 43       .data(function(d) { return position[d.x].ticks(5).map(position[d.x]); })
 44     .enter().append("svg:line")
 45       .attr("class", "x")
 46       .attr("x1", function(d) { return d; })
 47       .attr("x2", function(d) { return d; })
 48       .attr("y1", padding / 2)
 49       .attr("y2", size - padding / 2);
 50 
 51   // Y-ticks. TODO Cross the trait into the tick data?
 52   row.selectAll("line.y")
 53       .data(function(d) { return position[d.y].ticks(5).map(position[d.y]); })
 54     .enter().append("svg:line")
 55       .attr("class", "y")
 56       .attr("x1", padding / 2)
 57       .attr("x2", size - padding / 2)
 58       .attr("y1", function(d) { return d; })
 59       .attr("y2", function(d) { return d; });
 60 
 61   // Frame.
 62   row.append("svg:rect")
 63       .attr("x", padding / 2)
 64       .attr("y", padding / 2)
 65       .attr("width", size - padding)
 66       .attr("height", size - padding)
 67       .style("fill", "none")
 68       .style("stroke", "#aaa")
 69       .style("stroke-width", 1.5)
 70       .attr("pointer-events", "all")
 71       .on("mousedown", mousedown);
 72 
 73   // Dot plot.
 74   var dot = row.selectAll("circle")
 75       .data(cross(flower.values))
 76     .enter().append("svg:circle")
 77       .attr("cx", function(d) { return position[d.x.x](d.y[d.x.x]); })
 78       .attr("cy", function(d) { return size - position[d.x.y](d.y[d.x.y]); })
 79       .attr("r", 3)
 80       .style("fill", function(d) { return color(d.y.species); })
 81       .style("fill-opacity", .5)
 82       .attr("pointer-events", "none");
 83 
 84   d3.select(window)
 85       .on("mousemove", mousemove)
 86       .on("mouseup", mouseup);
 87 
 88   var rect, x0, x1, count;
 89 
 90   function mousedown() {
 91     x0 = d3.svg.mouse(this);
 92     count = 0;
 93 
 94     rect = d3.select(this.parentNode)
 95       .append("svg:rect")
 96         .style("fill", "#999")
 97         .style("fill-opacity", .5);
 98 
 99     d3.event.preventDefault();
100   }
101 
102   function mousemove() {
103     if (!rect) return;
104     x1 = d3.svg.mouse(rect.node());
105 
106     x1[0] = Math.max(padding / 2, Math.min(size - padding / 2, x1[0]));
107     x1[1] = Math.max(padding / 2, Math.min(size - padding / 2, x1[1]));
108 
109     var minx = Math.min(x0[0], x1[0]),
110         maxx = Math.max(x0[0], x1[0]),
111         miny = Math.min(x0[1], x1[1]),
112         maxy = Math.max(x0[1], x1[1]);
113 
114     rect
115         .attr("x", minx - .5)
116         .attr("y", miny - .5)
117         .attr("width", maxx - minx + 1)
118         .attr("height", maxy - miny + 1);
119 
120     var v = rect.node().__data__,
121         x = position[v.x],
122         y = position[v.y],
123         mins = x.invert(minx),
124         maxs = x.invert(maxx),
125         mint = y.invert(size - maxy),
126         maxt = y.invert(size - miny);
127 
128     count = 0;
129     svg.selectAll("circle")
130         .style("fill", function(d) {
131           return mins <= d.y[v.x] && maxs >= d.y[v.x]
132               && mint <= d.y[v.y] && maxt >= d.y[v.y]
133               ? (count++, color(d.y.species))
134               : "#ccc";
135         });
136   }
137 
138   function mouseup() {
139     if (!rect) return;
140     rect.remove();
141     rect = null;
142 
143     if (!count) svg.selectAll("circle")
144         .style("fill", function(d) {
145           return color(d.y.species);
146         });
147   }
148 
149 });
150 function cross(a) {
151   return function(d) {
152     var c = [];
153     for (var i = 0, n = a.length; i < n; i++) c.push({x: d, y: a[i]});
154     return c;
155   };
156 }

This example uses a helper function, cross, to allow property functions to access both parent and child data. We’re looking for simpler ways to do this in future versions of D3:

1 function cross(a) {
2   return function(d) {
3     var c = [];
4     for (var i = 0, n = a.length; i < n; i++) c.push({x: d, y: a[i]});
5     return c;
6   };
7 }
Copyright © 2011 Mike Bostock
Fork me on GitHub