|
<!-- |
|
An example of linked views using model.js. |
|
|
|
Curran Kelleher August 2014 |
|
--> |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<link rel="stylesheet" href="styles.css"> |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script> |
|
|
|
<!-- A functional reactive model library. github.com/curran/model --> |
|
<script src="http://curran.github.io/model/cdn/model-v0.2.4.js"></script> |
|
<style> |
|
|
|
/* CSS for the visualization. |
|
* Curran Kelleher 4/17/2014 */ |
|
|
|
/* Size the visualization container. */ |
|
#container { |
|
position: fixed; |
|
top: 30px; |
|
bottom: 30px; |
|
left: 30px; |
|
right: 30px; |
|
} |
|
|
|
/* Style the visualization. |
|
* This CSS is copied verbatim from the |
|
* D3 scatter plot example found at |
|
* http://bl.ocks.org/mbostock/3887118 */ |
|
|
|
body { |
|
font: 10px sans-serif; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.dot { |
|
fill: black; |
|
} |
|
|
|
/* The following CSS is for brushing, |
|
* adapted from http://bl.ocks.org/mbostock/4343214 */ |
|
|
|
.dot.selected { |
|
fill: red; |
|
} |
|
|
|
.brush .extent { |
|
stroke: gray; |
|
fill-opacity: .125; |
|
shape-rendering: crispEdges; |
|
} |
|
</style> |
|
|
|
</head> |
|
<body> |
|
<div id="container"></div> |
|
<script> |
|
|
|
function BarChart (container) { |
|
var defaults = { |
|
margin: { |
|
top: 20, |
|
right: 20, |
|
bottom: 30, |
|
left: 40 |
|
}, |
|
yAxisNumTicks: 10, |
|
yAxisTickFormat: "" |
|
}, |
|
model = Model(defaults), |
|
xAxis = d3.svg.axis().orient("bottom"), |
|
yAxis = d3.svg.axis().orient("left") |
|
svg = d3.select(container).append('svg') |
|
|
|
// Use absolute positioning on the SVG element |
|
// so that CSS can be used to position the div later |
|
// according to the model `box.x` and `box.y` properties. |
|
.style('position', 'absolute'), |
|
|
|
g = svg.append("g"), |
|
xAxisG = g.append("g").attr("class", "x axis"), |
|
yAxisG = g.append("g").attr("class", "y axis"), |
|
yAxisText = yAxisG.append("text") |
|
.attr("transform", "rotate(-90)") |
|
.attr("y", 6) |
|
.attr("dy", ".71em") |
|
.style("text-anchor", "end"); |
|
|
|
// Encapsulate D3 Conventional Margins. |
|
// See also http://bl.ocks.org/mbostock/3019563 |
|
model.when(["box", "margin"], function (box, margin) { |
|
model.width = box.width - margin.left - margin.right, |
|
model.height = box.height - margin.top - margin.bottom; |
|
}); |
|
model.when("margin", function (margin) { |
|
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
}); |
|
|
|
// Adjust Y axis tick mark parameters. |
|
// See https://github.com/mbostock/d3/wiki/Quantitative-Scales#linear_tickFormat |
|
model.when(['yAxisNumTicks', 'yAxisTickFormat'], function (count, format) { |
|
yAxis.ticks(count, format); |
|
}); |
|
|
|
// Respond to changes in size and offset. |
|
model.when("box", function (box) { |
|
|
|
// Resize the svg element that contains the visualization. |
|
svg.attr("width", box.width).attr("height", box.height); |
|
|
|
// Set the CSS `left` and `top` properties |
|
// to move the SVG element to `(box.x, box.y)` |
|
// relative to the container div to apply the offset. |
|
svg |
|
.style('left', box.x + 'px') |
|
.style('top', box.y + 'px'); |
|
}); |
|
|
|
model.when("height", function (height) { |
|
xAxisG.attr("transform", "translate(0," + height + ")"); |
|
}); |
|
|
|
model.when(["data", "xAttribute", "width"], function (data, xAttribute, width) { |
|
model.xScale = d3.scale.ordinal() |
|
.rangeRoundBands([0, width], .1) |
|
.domain(data.map(function(d) { return d[xAttribute]; })); |
|
}); |
|
|
|
model.when(["data", "yAttribute", "height"], function (data, yAttribute, height) { |
|
model.yScale = d3.scale.linear() |
|
.range([height, 0]) |
|
.domain([0, d3.max(data, function(d) { return d[yAttribute]; })]); |
|
}); |
|
|
|
model.when(["xScale"], function (xScale) { |
|
xAxis.scale(xScale) |
|
xAxisG.call(xAxis); |
|
}); |
|
|
|
model.when(["yScale"], function (yScale) { |
|
yAxis.scale(yScale) |
|
yAxisG.call(yAxis); |
|
}); |
|
|
|
model.when("yAxisLabel", yAxisText.text, yAxisText); |
|
|
|
model.when(["data", "xAttribute", "yAttribute", "xScale", "yScale", "height"], |
|
function (data, xAttribute, yAttribute, xScale, yScale, height) { |
|
var bars = g.selectAll(".bar").data(data); |
|
|
|
bars.enter().append("rect").attr("class", "bar"); |
|
|
|
bars |
|
.attr("x", function(d) { return xScale(d[xAttribute]); }) |
|
.attr("width", xScale.rangeBand()) |
|
.attr("y", function(d) { return yScale(d[yAttribute]); }) |
|
.attr("height", function(d) { return height - yScale(d[yAttribute]); }); |
|
|
|
bars.exit().remove(); |
|
}); |
|
|
|
return model; |
|
} |
|
|
|
|
|
// An adaptation of the [D3 scatter plot example](http://bl.ocks.org/mbostock/3887118) |
|
// that uses `model.js`. This version, unlike the original example, |
|
// is model driven and reactive. When a part of the model updates, |
|
// only the parts of the visualization that depend on those parts |
|
// of the model are updated. There are no redundant calls to visualization |
|
// update code when multiple properties are changed simultaneously. |
|
// |
|
// Draws from this brushing example for interaction: |
|
// http://bl.ocks.org/mbostock/4343214 |
|
// |
|
// See also docs on quadtree: |
|
// https://github.com/mbostock/d3/wiki/Quadtree-Geom |
|
// |
|
// Define the AMD module using the `define()` function |
|
// provided by Require.js. |
|
//define(['d3', 'model'], function (d3, Model) { |
|
function ScatterPlot (div){ |
|
var x = d3.scale.linear(), |
|
y = d3.scale.linear(), |
|
xAxis = d3.svg.axis().scale(x).orient('bottom'), |
|
yAxis = d3.svg.axis().scale(y).orient('left'), |
|
|
|
// Use absolute positioning so that CSS can be used |
|
// to position the div according to the model. |
|
svg = d3.select(div).append('svg').style('position', 'absolute'), |
|
g = svg.append('g'), |
|
xAxisG = g.append('g').attr('class', 'x axis'), |
|
yAxisG = g.append('g').attr('class', 'y axis'), |
|
xAxisLabel = xAxisG.append('text') |
|
.attr('class', 'label') |
|
.attr('y', -6) |
|
.style('text-anchor', 'end'), |
|
yAxisLabel = yAxisG.append('text') |
|
.attr('class', 'label') |
|
.attr('transform', 'rotate(-90)') |
|
.attr('y', 6) |
|
.attr('dy', '.71em') |
|
.style('text-anchor', 'end'), |
|
|
|
// Add the dots group before the brush group, |
|
// so that mouse events go to the brush |
|
// rather than to the dots, even when the mouse is |
|
// on top of a dot. |
|
dotsG = g.append('g'), |
|
brushG = g.append('g') |
|
.attr('class', 'brush'), |
|
brush = d3.svg.brush() |
|
.on('brush', brushed), |
|
dots, |
|
quadtree, |
|
model = Model(); |
|
|
|
model.when('xLabel', xAxisLabel.text, xAxisLabel); |
|
model.when('yLabel', yAxisLabel.text, yAxisLabel); |
|
|
|
|
|
model.when('margin', function (margin) { |
|
g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); |
|
}); |
|
|
|
model.when('box', function (box) { |
|
svg.attr('width', box.width) |
|
.attr('height', box.height); |
|
|
|
// Set the CSS `left` and `top` properties |
|
// to move the SVG element to `(box.x, box.y)` |
|
// relative to the container div. |
|
svg |
|
.style('left', box.x + 'px') |
|
.style('top', box.y + 'px') |
|
}); |
|
|
|
model.when(['box', 'margin'], function (box, margin) { |
|
model.width = box.width - margin.left - margin.right; |
|
model.height = box.height - margin.top - margin.bottom; |
|
}); |
|
|
|
model.when('width', function (width) { |
|
xAxisLabel.attr('x', width); |
|
}); |
|
|
|
model.when('height', function (height) { |
|
xAxisG.attr('transform', 'translate(0,' + height + ')'); |
|
}); |
|
|
|
model.when(['width', 'height'], function (width, height) { |
|
brush.x(d3.scale.identity().domain([0, width])); |
|
brush.y(d3.scale.identity().domain([0, height])); |
|
brushG |
|
.call(brush) |
|
.call(brush.event); |
|
}); |
|
|
|
|
|
model.when(['width', 'height', 'data', 'xField', 'yField'], function (width, height, data, xField, yField) { |
|
|
|
// Updated the scales |
|
x.domain(d3.extent(data, function(d) { return d[xField]; })).nice(); |
|
y.domain(d3.extent(data, function(d) { return d[yField]; })).nice(); |
|
|
|
x.range([0, width]); |
|
y.range([height, 0]); |
|
|
|
// update the quadtree |
|
quadtree = d3.geom.quadtree() |
|
.x(function(d) { return x(d[xField]); }) |
|
.y(function(d) { return y(d[yField]); }) |
|
(data); |
|
|
|
// update the axes |
|
xAxisG.call(xAxis); |
|
yAxisG.call(yAxis); |
|
|
|
// Plot the data as dots |
|
dots = dotsG.selectAll('.dot').data(data); |
|
dots.enter().append('circle') |
|
.attr('class', 'dot') |
|
.attr('r', 3.5); |
|
dots |
|
.attr('cx', function(d) { return x(d[xField]); }) |
|
.attr('cy', function(d) { return y(d[yField]); }); |
|
dots.exit().remove(); |
|
}); |
|
return model; |
|
|
|
function brushed() { |
|
var e = brush.extent(), selectedData; |
|
if(dots) { |
|
dots.each(function(d) { d.selected = false; }); |
|
selectedData = search(e[0][0], e[0][1], e[1][0], e[1][1]); |
|
dots.classed('selected', function(d) { return d.selected; }); |
|
} |
|
model.selectedData = brush.empty() ? model.data : selectedData; |
|
} |
|
|
|
// Find the nodes within the specified rectangle. |
|
function search(x0, y0, x3, y3) { |
|
var selectedData = []; |
|
quadtree.visit(function(node, x1, y1, x2, y2) { |
|
var d = node.point, x, y; |
|
if (d) { |
|
x = node.x; |
|
y = node.y; |
|
d.visited = true; |
|
if(x >= x0 && x < x3 && y >= y0 && y < y3){ |
|
d.selected = true; |
|
selectedData.push(d); |
|
} |
|
} |
|
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0; |
|
}); |
|
return selectedData; |
|
} |
|
} |
|
|
|
// The main program that assembles the linked views. |
|
// |
|
// Curran Kelleher 4/17/2014 |
|
//require(['d3', 'scatterPlot', 'barChart'], function (d3, ScatterPlot, BarChart) { |
|
function main(){ |
|
|
|
// Grab the container div from the DOM. |
|
var div = document.getElementById('container'), |
|
|
|
// Add both visualizations to the same div. |
|
// Each will create its own SVG element. |
|
scatterPlot = ScatterPlot(div), |
|
barChart = BarChart(div); |
|
|
|
// Configure the scatter plot to use the iris data. |
|
scatterPlot.set({ |
|
xField: 'sepalWidth', |
|
yField: 'sepalLength', |
|
xLabel: 'Sepal Width (cm)', |
|
yLabel: 'Sepal Length (cm)', |
|
margin: { 'top': 20, 'right': 20, 'bottom': 30, 'left': 40 } |
|
}); |
|
|
|
// Configure the bar chart to use the aggregated iris data. |
|
barChart.set({ |
|
xAttribute: 'species', |
|
yAttribute: 'count', |
|
yAxisLabel: 'number of irises', |
|
margin: { 'top': 20, 'right': 20, 'bottom': 30, 'left': 40 } |
|
}); |
|
|
|
// Compute the aggregated iris data in response to brushing |
|
// in the scatter plot, and pass it into the bar chart. |
|
scatterPlot.when('selectedData', function (scatterData) { |
|
var speciesCounts = {}; |
|
|
|
// Aggregate scatter plot data by counting |
|
// the number of irises for each species. |
|
scatterData.forEach(function (d) { |
|
if(!speciesCounts[d.species]){ |
|
speciesCounts[d.species] = 0; |
|
} |
|
speciesCounts[d.species]++; |
|
}); |
|
|
|
// Flatten the object containing species counts into an array. |
|
// Update the bar chart with the aggregated data. |
|
barChart.data = Object.keys(speciesCounts).map(function (species) { |
|
return { |
|
species: species, |
|
count: speciesCounts[species] |
|
}; |
|
}); |
|
}); |
|
|
|
// Load the iris data. |
|
d3.tsv('data.tsv', function (d) { |
|
d.sepalLength = +d.sepalLength; |
|
d.sepalWidth = +d.sepalWidth; |
|
return d; |
|
}, function(error, data) { |
|
|
|
// Set sizes once to initialize. |
|
setSizes(); |
|
|
|
// Set sizes when the user resizes the browser. |
|
window.addEventListener('resize', setSizes); |
|
|
|
// Set the data. |
|
scatterPlot.data = data; |
|
}); |
|
|
|
// Sets the `box` property on each visualization |
|
// to arrange them within the container div. |
|
function setSizes(){ |
|
|
|
// Put the scatter plot on the left. |
|
scatterPlot.box = { |
|
x: 0, |
|
y: 0, |
|
width: div.clientWidth / 2, |
|
height: div.clientHeight |
|
}; |
|
|
|
// Put the bar chart on the right. |
|
barChart.box = { |
|
x: div.clientWidth / 2, |
|
y: 0, |
|
width: div.clientWidth / 2, |
|
height: div.clientHeight |
|
}; |
|
} |
|
} |
|
main(); |
|
|
|
</script> |
|
</body> |
|
</html> |