Permalink
Browse files

Update SPLOM example to use d3.svg.brush.

This required a couple core changes. First, the brush shouldn't notify listeners
when redrawing, because this commonly causes an infinite loop if one brush
triggers a change in another brush (as in a scatterplot matrix, where only one
brush is active at a given time). I suppose an alternative implementation might
use just a single brush, and assign the axes dynmically; I might try that in a
future commit. Second, I added a clear convenience method to reset a brush.
  • Loading branch information...
mbostock committed Nov 2, 2011
1 parent 6ebc2fc commit af589a01fc4f8b6a4500484a563fb16bac494748
Showing with 152 additions and 152 deletions.
  1. +8 −6 d3.js
  2. +1 −1 d3.min.js
  3. +0 −7 examples/splom/cross.js
  4. +45 −4 examples/splom/splom.css
  5. +0 −1 examples/splom/splom.html
  6. +90 −127 examples/splom/splom.js
  7. +8 −6 src/svg/brush.js
View
14 d3.js
@@ -3908,12 +3908,6 @@ d3.svg.brush = function() {
bg.attr("y", e[0]).attr("height", e[1] - e[0]);
d3_svg_brushRedrawY(g, extent);
}
-
- // Notify listeners of the (possibly) changed extent.
- e = dispatcher(this, arguments);
- e("brushstart");
- e("brush");
- e("brushend");
});
}
@@ -4015,6 +4009,14 @@ d3.svg.brush = function() {
return brush;
};
+ brush.clear = function() {
+ extent[0][0] =
+ extent[0][1] =
+ extent[1][0] =
+ extent[1][1] = 0;
+ return brush;
+ };
+
brush.empty = function() {
return (x && extent[0][0] === extent[1][0])
|| (y && extent[0][1] === extent[1][1]);
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -1,7 +0,0 @@
-function cross(a) {
- return function(d) {
- var c = [];
- for (var i = 0, n = a.length; i < n; i++) c.push({x: d, y: a[i]});
- return c;
- };
-}
View
@@ -1,8 +1,49 @@
-line {
- stroke: #eee;
+svg {
+ font: 10px sans-serif;
+}
+
+.axis {
shape-rendering: crispEdges;
}
-rect {
- cursor: crosshair;
+.axis line {
+ stroke: #ddd;
+ stroke-width: .5px;
+}
+
+.axis path {
+ display: none;
+}
+
+rect.extent {
+ fill: #000;
+ fill-opacity: .125;
+ stroke: #fff;
+}
+
+rect.frame {
+ fill: #fff;
+ fill-opacity: .7;
+ stroke: #aaa;
+}
+
+circle {
+ fill: #ccc;
+ fill-opacity: .5;
+}
+
+.cell text {
+ pointer-events: none;
+}
+
+.setosa {
+ fill: #800;
+}
+
+.versicolor {
+ fill: #080;
+}
+
+.virginica {
+ fill: #008;
}
@@ -4,7 +4,6 @@
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Scatterplot Matrix</title>
<script type="text/javascript" src="../../d3.js"></script>
- <script type="text/javascript" src="cross.js"></script>
<link type="text/css" rel="stylesheet" href="splom.css"/>
</head>
<body>
View
@@ -2,148 +2,111 @@ d3.json("flowers.json", function(flower) {
// Size parameters.
var size = 150,
- padding = 20;
-
- // Color scale.
- var color = d3.scale.ordinal().range([
- "rgb(50%, 0%, 0%)",
- "rgb(0%, 50%, 0%)",
- "rgb(0%, 0%, 50%)"
- ]);
+ padding = 19.5,
+ n = flower.traits.length;
// Position scales.
- var position = {};
+ var x = {}, y = {};
flower.traits.forEach(function(trait) {
- function value(d) { return d[trait]; }
- position[trait] = d3.scale.linear()
- .domain([d3.min(flower.values, value), d3.max(flower.values, value)])
- .range([padding / 2, size - padding / 2]);
+ var value = function(d) { return d[trait]; },
+ domain = [d3.min(flower.values, value), d3.max(flower.values, value)],
+ range = [padding / 2, size - padding / 2];
+ x[trait] = d3.scale.linear().domain(domain).range(range);
+ y[trait] = d3.scale.linear().domain(domain).range(range.reverse());
});
+ // Axes.
+ var axis = d3.svg.axis()
+ .ticks(5)
+ .tickSize(size * n);
+
// Root panel.
- var svg = d3.select("#chart")
- .append("svg:svg")
- .attr("width", size * flower.traits.length)
- .attr("height", size * flower.traits.length);
+ var svg = d3.select("#chart").append("svg:svg")
+ .attr("width", size * n + padding)
+ .attr("height", size * n + padding);
- // One column per trait.
- var column = svg.selectAll("g")
+ // X-axis.
+ svg.selectAll("g.x.axis")
.data(flower.traits)
.enter().append("svg:g")
- .attr("transform", function(d, i) { return "translate(" + i * size + ",0)"; });
+ .attr("class", "x axis")
+ .attr("transform", function(d, i) { return "translate(" + i * size + ",0)"; })
+ .each(function(d) { d3.select(this).call(axis.scale(x[d]).orient("bottom")); });
- // One row per trait.
- var row = column.selectAll("g")
- .data(cross(flower.traits))
+ // Y-axis.
+ svg.selectAll("g.y.axis")
+ .data(flower.traits)
.enter().append("svg:g")
- .attr("transform", function(d, i) { return "translate(0," + i * size + ")"; });
-
- // X-ticks. TODO Cross the trait into the tick data?
- row.selectAll("line.x")
- .data(function(d) { return position[d.x].ticks(5).map(position[d.x]); })
- .enter().append("svg:line")
- .attr("class", "x")
- .attr("x1", function(d) { return d; })
- .attr("x2", function(d) { return d; })
- .attr("y1", padding / 2)
- .attr("y2", size - padding / 2);
-
- // Y-ticks. TODO Cross the trait into the tick data?
- row.selectAll("line.y")
- .data(function(d) { return position[d.y].ticks(5).map(position[d.y]); })
- .enter().append("svg:line")
- .attr("class", "y")
- .attr("x1", padding / 2)
- .attr("x2", size - padding / 2)
- .attr("y1", function(d) { return d; })
- .attr("y2", function(d) { return d; });
-
- // Frame.
- row.append("svg:rect")
- .attr("x", padding / 2)
- .attr("y", padding / 2)
- .attr("width", size - padding)
- .attr("height", size - padding)
- .style("fill", "none")
- .style("stroke", "#aaa")
- .style("stroke-width", 1.5)
- .attr("pointer-events", "all")
- .on("mousedown", mousedown);
-
- // Dot plot.
- var dot = row.selectAll("circle")
- .data(cross(flower.values))
- .enter().append("svg:circle")
- .attr("cx", function(d) { return position[d.x.x](d.y[d.x.x]); })
- .attr("cy", function(d) { return size - position[d.x.y](d.y[d.x.y]); })
- .attr("r", 3)
- .style("fill", function(d) { return color(d.y.species); })
- .style("fill-opacity", .5)
- .attr("pointer-events", "none");
-
- d3.select(window)
- .on("mousemove", mousemove)
- .on("mouseup", mouseup);
-
- var rect, x0, x1, count;
-
- function mousedown() {
- x0 = d3.svg.mouse(this);
- count = 0;
-
- rect = d3.select(this.parentNode)
- .append("svg:rect")
- .style("fill", "#999")
- .style("fill-opacity", .5);
-
- d3.event.preventDefault();
- }
+ .attr("class", "y axis")
+ .attr("transform", function(d, i) { return "translate(0," + i * size + ")"; })
+ .each(function(d) { d3.select(this).call(axis.scale(y[d]).orient("right")); });
- function mousemove() {
- if (!rect) return;
- x1 = d3.svg.mouse(rect.node());
-
- x1[0] = Math.max(padding / 2, Math.min(size - padding / 2, x1[0]));
- x1[1] = Math.max(padding / 2, Math.min(size - padding / 2, x1[1]));
-
- var minx = Math.min(x0[0], x1[0]),
- maxx = Math.max(x0[0], x1[0]),
- miny = Math.min(x0[1], x1[1]),
- maxy = Math.max(x0[1], x1[1]);
-
- rect
- .attr("x", minx - .5)
- .attr("y", miny - .5)
- .attr("width", maxx - minx + 1)
- .attr("height", maxy - miny + 1);
-
- var v = rect.node().__data__,
- x = position[v.x],
- y = position[v.y],
- mins = x.invert(minx),
- maxs = x.invert(maxx),
- mint = y.invert(size - maxy),
- maxt = y.invert(size - miny);
-
- count = 0;
- svg.selectAll("circle")
- .style("fill", function(d) {
- return mins <= d.y[v.x] && maxs >= d.y[v.x]
- && mint <= d.y[v.y] && maxt >= d.y[v.y]
- ? (count++, color(d.y.species))
- : "#ccc";
- });
+ // Cell and plot.
+ var cell = svg.selectAll("g.cell")
+ .data(cross(flower.traits, flower.traits))
+ .enter().append("svg:g")
+ .attr("class", "cell")
+ .attr("transform", function(d) { return "translate(" + d.i * size + "," + d.j * size + ")"; })
+ .each(plot);
+
+ // Titles for the diagonal.
+ cell.filter(function(d) { return d.i == d.j; }).append("svg:text")
+ .attr("x", padding)
+ .attr("y", padding)
+ .attr("dy", ".71em")
+ .text(function(d) { return d.x; });
+
+ function plot(p) {
+ var cell = d3.select(this);
+
+ // Plot frame.
+ cell.append("svg:rect")
+ .attr("class", "frame")
+ .attr("x", padding / 2)
+ .attr("y", padding / 2)
+ .attr("width", size - padding)
+ .attr("height", size - padding);
+
+ // Plot dots.
+ cell.selectAll("circle")
+ .data(flower.values)
+ .enter().append("svg:circle")
+ .attr("class", function(d) { return d.species; })
+ .attr("cx", function(d) { return x[p.x](d[p.x]); })
+ .attr("cy", function(d) { return y[p.y](d[p.y]); })
+ .attr("r", 3);
+
+ // Plot brush; stash in the data so that we can clear programmatically.
+ cell.call(p.brush = d3.svg.brush()
+ .x(x[p.x])
+ .y(y[p.y])
+ .on("brush", brush)
+ .on("brushend", brushend));
}
- function mouseup() {
- if (!rect) return;
- rect.remove();
- rect = null;
+ var b; // the active brush
+
+ // Clear the previously-active brush, if any, and highlight selected circles.
+ function brush(p) {
+ if (b && b !== d3.event.target) cell.filter(function(d) { return d.brush === b; }).call(b.clear());
+ var e = (b = d3.event.target).extent();
+ svg.selectAll("circle").attr("class", 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]
+ ? d.species : null;
+ });
+ }
- if (!count) svg.selectAll("circle")
- .style("fill", function(d) {
- return color(d.y.species);
- });
+ // If the active brush is empty, select all circles.
+ function brushend() {
+ if (b.empty()) svg.selectAll("circle").attr("class", function(d) {
+ return d.species;
+ });
}
+ 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;
+ }
});
View
@@ -53,12 +53,6 @@ d3.svg.brush = function() {
bg.attr("y", e[0]).attr("height", e[1] - e[0]);
d3_svg_brushRedrawY(g, extent);
}
-
- // Notify listeners of the (possibly) changed extent.
- e = dispatcher(this, arguments);
- e("brushstart");
- e("brush");
- e("brushend");
});
}
@@ -160,6 +154,14 @@ d3.svg.brush = function() {
return brush;
};
+ brush.clear = function() {
+ extent[0][0] =
+ extent[0][1] =
+ extent[1][0] =
+ extent[1][1] = 0;
+ return brush;
+ };
+
brush.empty = function() {
return (x && extent[0][0] === extent[1][0])
|| (y && extent[0][1] === extent[1][1]);

0 comments on commit af589a0

Please sign in to comment.