This program makes a scatter plot from Iris data set. Brushing in one plot zooms in the other.
Last active
August 29, 2015 14:17
-
-
Save curran/cf4b98fff0517ca04667 to your computer and use it in GitHub Desktop.
Scatter Plot Zooming
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<!-- Use RequireJS for module loading. --> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.js"></script> | |
<!-- Configure AMD modules. --> | |
<script> | |
requirejs.config({ | |
paths: { | |
d3: "//d3js.org/d3.v3.min", | |
jquery: "//code.jquery.com/jquery-2.1.1.min" | |
} | |
}); | |
</script> | |
<!-- Include CSS that styles the visualization. --> | |
<link rel="stylesheet" href="styles.css"> | |
<title>Scatter Plot</title> | |
</head> | |
<body> | |
<!-- The visualization will be injected into this div. --> | |
<div id="container"></div> | |
<!-- Run the main program. --> | |
<script src="main.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"sepal_length":{ | |
"type": "Q", | |
"label": "sepal length (cm)" | |
}, | |
"sepal_width": { | |
"type": "Q", | |
"label": "sepal width (cm)" | |
}, | |
"petal_length": { | |
"type": "Q", | |
"label": "petal length (cm)" | |
}, | |
"petal_width": { | |
"type": "Q", | |
"label": "petal width (cm)" | |
}, | |
"class": { | |
"type": "N", | |
"label": "species" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sepal_length | sepal_width | petal_length | petal_width | class | |
---|---|---|---|---|---|
5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa | |
4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa | |
4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa | |
4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa | |
5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa | |
5.4 | 3.9 | 1.7 | 0.4 | Iris-setosa | |
4.6 | 3.4 | 1.4 | 0.3 | Iris-setosa | |
5.0 | 3.4 | 1.5 | 0.2 | Iris-setosa | |
4.4 | 2.9 | 1.4 | 0.2 | Iris-setosa | |
4.9 | 3.1 | 1.5 | 0.1 | Iris-setosa | |
5.4 | 3.7 | 1.5 | 0.2 | Iris-setosa | |
4.8 | 3.4 | 1.6 | 0.2 | Iris-setosa | |
4.8 | 3.0 | 1.4 | 0.1 | Iris-setosa | |
4.3 | 3.0 | 1.1 | 0.1 | Iris-setosa | |
5.8 | 4.0 | 1.2 | 0.2 | Iris-setosa | |
5.7 | 4.4 | 1.5 | 0.4 | Iris-setosa | |
5.4 | 3.9 | 1.3 | 0.4 | Iris-setosa | |
5.1 | 3.5 | 1.4 | 0.3 | Iris-setosa | |
5.7 | 3.8 | 1.7 | 0.3 | Iris-setosa | |
5.1 | 3.8 | 1.5 | 0.3 | Iris-setosa | |
5.4 | 3.4 | 1.7 | 0.2 | Iris-setosa | |
5.1 | 3.7 | 1.5 | 0.4 | Iris-setosa | |
4.6 | 3.6 | 1.0 | 0.2 | Iris-setosa | |
5.1 | 3.3 | 1.7 | 0.5 | Iris-setosa | |
4.8 | 3.4 | 1.9 | 0.2 | Iris-setosa | |
5.0 | 3.0 | 1.6 | 0.2 | Iris-setosa | |
5.0 | 3.4 | 1.6 | 0.4 | Iris-setosa | |
5.2 | 3.5 | 1.5 | 0.2 | Iris-setosa | |
5.2 | 3.4 | 1.4 | 0.2 | Iris-setosa | |
4.7 | 3.2 | 1.6 | 0.2 | Iris-setosa | |
4.8 | 3.1 | 1.6 | 0.2 | Iris-setosa | |
5.4 | 3.4 | 1.5 | 0.4 | Iris-setosa | |
5.2 | 4.1 | 1.5 | 0.1 | Iris-setosa | |
5.5 | 4.2 | 1.4 | 0.2 | Iris-setosa | |
4.9 | 3.1 | 1.5 | 0.1 | Iris-setosa | |
5.0 | 3.2 | 1.2 | 0.2 | Iris-setosa | |
5.5 | 3.5 | 1.3 | 0.2 | Iris-setosa | |
4.9 | 3.1 | 1.5 | 0.1 | Iris-setosa | |
4.4 | 3.0 | 1.3 | 0.2 | Iris-setosa | |
5.1 | 3.4 | 1.5 | 0.2 | Iris-setosa | |
5.0 | 3.5 | 1.3 | 0.3 | Iris-setosa | |
4.5 | 2.3 | 1.3 | 0.3 | Iris-setosa | |
4.4 | 3.2 | 1.3 | 0.2 | Iris-setosa | |
5.0 | 3.5 | 1.6 | 0.6 | Iris-setosa | |
5.1 | 3.8 | 1.9 | 0.4 | Iris-setosa | |
4.8 | 3.0 | 1.4 | 0.3 | Iris-setosa | |
5.1 | 3.8 | 1.6 | 0.2 | Iris-setosa | |
4.6 | 3.2 | 1.4 | 0.2 | Iris-setosa | |
5.3 | 3.7 | 1.5 | 0.2 | Iris-setosa | |
5.0 | 3.3 | 1.4 | 0.2 | Iris-setosa | |
7.0 | 3.2 | 4.7 | 1.4 | Iris-versicolor | |
6.4 | 3.2 | 4.5 | 1.5 | Iris-versicolor | |
6.9 | 3.1 | 4.9 | 1.5 | Iris-versicolor | |
5.5 | 2.3 | 4.0 | 1.3 | Iris-versicolor | |
6.5 | 2.8 | 4.6 | 1.5 | Iris-versicolor | |
5.7 | 2.8 | 4.5 | 1.3 | Iris-versicolor | |
6.3 | 3.3 | 4.7 | 1.6 | Iris-versicolor | |
4.9 | 2.4 | 3.3 | 1.0 | Iris-versicolor | |
6.6 | 2.9 | 4.6 | 1.3 | Iris-versicolor | |
5.2 | 2.7 | 3.9 | 1.4 | Iris-versicolor | |
5.0 | 2.0 | 3.5 | 1.0 | Iris-versicolor | |
5.9 | 3.0 | 4.2 | 1.5 | Iris-versicolor | |
6.0 | 2.2 | 4.0 | 1.0 | Iris-versicolor | |
6.1 | 2.9 | 4.7 | 1.4 | Iris-versicolor | |
5.6 | 2.9 | 3.6 | 1.3 | Iris-versicolor | |
6.7 | 3.1 | 4.4 | 1.4 | Iris-versicolor | |
5.6 | 3.0 | 4.5 | 1.5 | Iris-versicolor | |
5.8 | 2.7 | 4.1 | 1.0 | Iris-versicolor | |
6.2 | 2.2 | 4.5 | 1.5 | Iris-versicolor | |
5.6 | 2.5 | 3.9 | 1.1 | Iris-versicolor | |
5.9 | 3.2 | 4.8 | 1.8 | Iris-versicolor | |
6.1 | 2.8 | 4.0 | 1.3 | Iris-versicolor | |
6.3 | 2.5 | 4.9 | 1.5 | Iris-versicolor | |
6.1 | 2.8 | 4.7 | 1.2 | Iris-versicolor | |
6.4 | 2.9 | 4.3 | 1.3 | Iris-versicolor | |
6.6 | 3.0 | 4.4 | 1.4 | Iris-versicolor | |
6.8 | 2.8 | 4.8 | 1.4 | Iris-versicolor | |
6.7 | 3.0 | 5.0 | 1.7 | Iris-versicolor | |
6.0 | 2.9 | 4.5 | 1.5 | Iris-versicolor | |
5.7 | 2.6 | 3.5 | 1.0 | Iris-versicolor | |
5.5 | 2.4 | 3.8 | 1.1 | Iris-versicolor | |
5.5 | 2.4 | 3.7 | 1.0 | Iris-versicolor | |
5.8 | 2.7 | 3.9 | 1.2 | Iris-versicolor | |
6.0 | 2.7 | 5.1 | 1.6 | Iris-versicolor | |
5.4 | 3.0 | 4.5 | 1.5 | Iris-versicolor | |
6.0 | 3.4 | 4.5 | 1.6 | Iris-versicolor | |
6.7 | 3.1 | 4.7 | 1.5 | Iris-versicolor | |
6.3 | 2.3 | 4.4 | 1.3 | Iris-versicolor | |
5.6 | 3.0 | 4.1 | 1.3 | Iris-versicolor | |
5.5 | 2.5 | 4.0 | 1.3 | Iris-versicolor | |
5.5 | 2.6 | 4.4 | 1.2 | Iris-versicolor | |
6.1 | 3.0 | 4.6 | 1.4 | Iris-versicolor | |
5.8 | 2.6 | 4.0 | 1.2 | Iris-versicolor | |
5.0 | 2.3 | 3.3 | 1.0 | Iris-versicolor | |
5.6 | 2.7 | 4.2 | 1.3 | Iris-versicolor | |
5.7 | 3.0 | 4.2 | 1.2 | Iris-versicolor | |
5.7 | 2.9 | 4.2 | 1.3 | Iris-versicolor | |
6.2 | 2.9 | 4.3 | 1.3 | Iris-versicolor | |
5.1 | 2.5 | 3.0 | 1.1 | Iris-versicolor | |
5.7 | 2.8 | 4.1 | 1.3 | Iris-versicolor | |
6.3 | 3.3 | 6.0 | 2.5 | Iris-virginica | |
5.8 | 2.7 | 5.1 | 1.9 | Iris-virginica | |
7.1 | 3.0 | 5.9 | 2.1 | Iris-virginica | |
6.3 | 2.9 | 5.6 | 1.8 | Iris-virginica | |
6.5 | 3.0 | 5.8 | 2.2 | Iris-virginica | |
7.6 | 3.0 | 6.6 | 2.1 | Iris-virginica | |
4.9 | 2.5 | 4.5 | 1.7 | Iris-virginica | |
7.3 | 2.9 | 6.3 | 1.8 | Iris-virginica | |
6.7 | 2.5 | 5.8 | 1.8 | Iris-virginica | |
7.2 | 3.6 | 6.1 | 2.5 | Iris-virginica | |
6.5 | 3.2 | 5.1 | 2.0 | Iris-virginica | |
6.4 | 2.7 | 5.3 | 1.9 | Iris-virginica | |
6.8 | 3.0 | 5.5 | 2.1 | Iris-virginica | |
5.7 | 2.5 | 5.0 | 2.0 | Iris-virginica | |
5.8 | 2.8 | 5.1 | 2.4 | Iris-virginica | |
6.4 | 3.2 | 5.3 | 2.3 | Iris-virginica | |
6.5 | 3.0 | 5.5 | 1.8 | Iris-virginica | |
7.7 | 3.8 | 6.7 | 2.2 | Iris-virginica | |
7.7 | 2.6 | 6.9 | 2.3 | Iris-virginica | |
6.0 | 2.2 | 5.0 | 1.5 | Iris-virginica | |
6.9 | 3.2 | 5.7 | 2.3 | Iris-virginica | |
5.6 | 2.8 | 4.9 | 2.0 | Iris-virginica | |
7.7 | 2.8 | 6.7 | 2.0 | Iris-virginica | |
6.3 | 2.7 | 4.9 | 1.8 | Iris-virginica | |
6.7 | 3.3 | 5.7 | 2.1 | Iris-virginica | |
7.2 | 3.2 | 6.0 | 1.8 | Iris-virginica | |
6.2 | 2.8 | 4.8 | 1.8 | Iris-virginica | |
6.1 | 3.0 | 4.9 | 1.8 | Iris-virginica | |
6.4 | 2.8 | 5.6 | 2.1 | Iris-virginica | |
7.2 | 3.0 | 5.8 | 1.6 | Iris-virginica | |
7.4 | 2.8 | 6.1 | 1.9 | Iris-virginica | |
7.9 | 3.8 | 6.4 | 2.0 | Iris-virginica | |
6.4 | 2.8 | 5.6 | 2.2 | Iris-virginica | |
6.3 | 2.8 | 5.1 | 1.5 | Iris-virginica | |
6.1 | 2.6 | 5.6 | 1.4 | Iris-virginica | |
7.7 | 3.0 | 6.1 | 2.3 | Iris-virginica | |
6.3 | 3.4 | 5.6 | 2.4 | Iris-virginica | |
6.4 | 3.1 | 5.5 | 1.8 | Iris-virginica | |
6.0 | 3.0 | 4.8 | 1.8 | Iris-virginica | |
6.9 | 3.1 | 5.4 | 2.1 | Iris-virginica | |
6.7 | 3.1 | 5.6 | 2.4 | Iris-virginica | |
6.9 | 3.1 | 5.1 | 2.3 | Iris-virginica | |
5.8 | 2.7 | 5.1 | 1.9 | Iris-virginica | |
6.8 | 3.2 | 5.9 | 2.3 | Iris-virginica | |
6.7 | 3.3 | 5.7 | 2.5 | Iris-virginica | |
6.7 | 3.0 | 5.2 | 2.3 | Iris-virginica | |
6.3 | 2.5 | 5.0 | 1.9 | Iris-virginica | |
6.5 | 3.0 | 5.2 | 2.0 | Iris-virginica | |
6.2 | 3.4 | 5.4 | 2.3 | Iris-virginica | |
5.9 | 3.0 | 5.1 | 1.8 | Iris-virginica |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is the main program that sets up a scatter plot to visualize the Iris data set. | |
// Curran Kelleher March 2015 | |
require(["scatterPlot"], function (ScatterPlot) { | |
// Initialize the scatter plot. | |
var options = { | |
// Tell the visualization which DOM element to insert itself into. | |
container: d3.select("#container").node(), | |
// Specify the margin and text label offsets. | |
margin: { | |
top: 10, | |
right: 10, | |
bottom: 45, | |
left: 55 | |
}, | |
yAxisLabelOffset: 1.8, // Unit is CSS "em"s | |
xAxisLabelOffset: 1.9, | |
titleOffset: 0.3 | |
}, | |
scatterPlot1 = ScatterPlot(options), | |
scatterPlot2 = ScatterPlot(options); | |
// Fetch the column metadata. | |
d3.json("iris-metadata.json", function (metadata) { | |
var xColumn = "sepal_length", | |
yColumn = "petal_length", | |
sizeColumn = "petal_width", | |
colorColumn = "class", | |
xyOptions = { | |
xColumn: xColumn, | |
xAxisLabel: metadata[xColumn].label, | |
yColumn: yColumn, | |
yAxisLabel: metadata[yColumn].label | |
}; | |
// Use the same X and Y for all plots. | |
scatterPlot1.set(xyOptions); | |
scatterPlot2.set(xyOptions); | |
// Load the data from a CSV file. | |
d3.csv("iris.csv", function (data){ | |
// Parse quantitative values from strings to numbers. | |
var quantitativeColumns = Object.keys(metadata).filter(function (column){ | |
return metadata[column].type === "Q"; | |
}); | |
data.forEach(function (d){ | |
quantitativeColumns.forEach(function (column){ | |
d[column] = parseFloat(d[column]); | |
}); | |
}); | |
// Pass the data into the plots. | |
scatterPlot1.data = data; | |
scatterPlot2.data = data; | |
}); | |
// Use the first plot to zoom in the second plot. | |
scatterPlot1.brushEnabled = true; | |
scatterPlot1.when("brushedIntervals", function (brushedIntervals){ | |
scatterPlot2.xDomainMin = brushedIntervals[xColumn][0]; | |
scatterPlot2.xDomainMax = brushedIntervals[xColumn][1]; | |
scatterPlot2.yDomainMin = brushedIntervals[yColumn][0]; | |
scatterPlot2.yDomainMax = brushedIntervals[yColumn][1]; | |
}); | |
// Initialize the default brush. | |
scatterPlot1.brushedIntervals = { | |
"sepal_length": [ 4.82, 7.77 ], | |
"petal_length": [ 2.84, 6.80 ] | |
}; | |
}); | |
// Sets the `box` model property | |
// based on the size of the container, | |
function computeBoxes(){ | |
var width = container.clientWidth, | |
height = container.clientHeight, | |
padding = 10, | |
plotWidth = (width - padding * 2) / 2, | |
plotHeight = height - padding * 2; | |
scatterPlot1.box = { | |
x: padding, | |
y: padding, | |
width: plotWidth, | |
height: plotHeight | |
}; | |
scatterPlot2.box = { | |
x: plotWidth + padding * 2, | |
y: padding, | |
width: plotWidth, | |
height: plotHeight | |
}; | |
} | |
// once to initialize `model.box`, and | |
computeBoxes(); | |
// whenever the browser window resizes in the future. | |
window.addEventListener("resize", computeBoxes); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A functional reactive model library. | |
// | |
(function(){ | |
// The D3 conventional graph representation. | |
// See https://github.com/mbostock/d3/wiki/Force-Layout#nodes | |
var nodes, links, idCounter, map; | |
function resetFlowGraph(){ | |
nodes = []; | |
links = []; | |
idCounter = 0; | |
map = {}; | |
} | |
function getFlowGraph(){ | |
return { | |
nodes: nodes, | |
links: links | |
}; | |
} | |
resetFlowGraph(); | |
// Adds the nodes and links to the data flow graph for one | |
// particular reactive function. | |
function updateLambda(modelId, lambdaId, inProperties, outProperties){ | |
var lambda = lambdaNode(lambdaId); | |
inProperties.forEach(function(property){ | |
link(propertyNode(modelId, property), lambda); | |
}); | |
outProperties.forEach(function(property){ | |
link(lambda, propertyNode(modelId, property)); | |
}); | |
} | |
function lambdaNode(id){ | |
return getOrCreate(id, nodes, createLambda); | |
} | |
function createLambda(index){ | |
return { | |
type: "lambda", | |
index: index | |
}; | |
} | |
function propertyNode(modelId, property){ | |
var id = modelId + "." + property; | |
return getOrCreate(id, nodes, createPropertyNode(property)); | |
} | |
function createPropertyNode(property){ | |
return function(index){ | |
return { | |
type: "property", | |
index: index, | |
property: property | |
}; | |
}; | |
} | |
function link(sourceNode, targetNode){ | |
var source = sourceNode.index, | |
target = targetNode.index, | |
id = source + "-" + target; | |
getOrCreate(id, links, createLink(source, target)); | |
} | |
function createLink(source, target){ | |
return function(index){ | |
return { | |
source: source, | |
target: target | |
}; | |
}; | |
} | |
function getOrCreate(id, things, createThing){ | |
var thing = map[id]; | |
if(!thing){ | |
thing = map[id] = createThing(things.length); | |
things.push(thing); | |
} | |
return thing; | |
} | |
// The constructor function, accepting default values. | |
function Model(defaults){ | |
// The returned public API object. | |
var model = {}, | |
// The internal stored values for tracked properties. { property -> value } | |
values = {}, | |
// The callback functions for each tracked property. { property -> [callback] } | |
listeners = {}, | |
// The set of tracked properties. { property -> true } | |
trackedProperties = {}, | |
modelId = idCounter++, | |
changedProperties = {}; | |
// The functional reactive "when" operator. | |
// | |
// * `properties` An array of property names (can also be a single property string). | |
// * `callback` A callback function that is called: | |
// * with property values as arguments, ordered corresponding to the properties array, | |
// * only if all specified properties have values, | |
// * once for initialization, | |
// * whenever one or more specified properties change, | |
// * on the next tick of the JavaScript event loop after properties change, | |
// * only once as a result of one or more synchronous changes to dependency properties. | |
function when(properties, callback, thisArg){ | |
var lambdaId = idCounter++; | |
// Make sure the default `this` becomes | |
// the object you called `.on` on. | |
thisArg = thisArg || this; | |
// Handle either an array or a single string. | |
properties = (properties instanceof Array) ? properties : [properties]; | |
// This function will trigger the callback to be invoked. | |
var triggerCallback = debounce(function (){ | |
var args = properties.map(function(property){ | |
return values[property]; | |
}); | |
if(allAreDefined(args)){ | |
changedProperties = {}; | |
callback.apply(thisArg, args); | |
updateLambda(modelId, lambdaId, properties, Object.keys(changedProperties)); | |
} | |
}); | |
// Trigger the callback once for initialization. | |
triggerCallback(); | |
// Trigger the callback whenever specified properties change. | |
properties.forEach(function(property){ | |
on(property, triggerCallback); | |
}); | |
// Return this function so it can be removed later. | |
return triggerCallback; | |
} | |
// Returns a debounced version of the given function. | |
// See http://underscorejs.org/#debounce | |
function debounce(callback){ | |
var queued = false; | |
return function () { | |
if(!queued){ | |
queued = true; | |
setTimeout(function () { | |
queued = false; | |
callback(); | |
}, 0); | |
} | |
}; | |
} | |
// Returns true if all elements of the given array are defined, false otherwise. | |
function allAreDefined(arr){ | |
return !arr.some(function (d) { | |
return typeof d === 'undefined' || d === null; | |
}); | |
} | |
// Adds a change listener for a given property with Backbone-like behavior. | |
// Similar to http://backbonejs.org/#Events-on | |
function on(property, callback, thisArg){ | |
// Make sure the default `this` becomes | |
// the object you called `.on` on. | |
thisArg = thisArg || this; | |
getListeners(property).push(callback); | |
track(property, thisArg); | |
} | |
// Gets or creates the array of listener functions for a given property. | |
function getListeners(property){ | |
return listeners[property] || (listeners[property] = []); | |
} | |
// Tracks a property if it is not already tracked. | |
function track(property, thisArg){ | |
if(!(property in trackedProperties)){ | |
trackedProperties[property] = true; | |
values[property] = model[property]; | |
Object.defineProperty(model, property, { | |
get: function () { return values[property]; }, | |
set: function(newValue) { | |
var oldValue = values[property]; | |
values[property] = newValue; | |
getListeners(property).forEach(function(callback){ | |
callback.call(thisArg, newValue, oldValue); | |
}); | |
changedProperties[property] = true; | |
} | |
}); | |
} | |
} | |
// Removes a listener added using `when()`. | |
function cancel(listener){ | |
for(var property in listeners){ | |
off(property, listener); | |
} | |
} | |
// Removes a change listener added using `on`. | |
function off(property, callback){ | |
listeners[property] = listeners[property].filter(function (listener) { | |
return listener !== callback; | |
}); | |
} | |
// Sets all of the given values on the model. | |
// `newValues` is an object { property -> value }. | |
function set(newValues){ | |
for(var property in newValues){ | |
model[property] = newValues[property]; | |
} | |
} | |
// Transfer defaults passed into the constructor to the model. | |
set(defaults); | |
// Expose the public API. | |
model.when = when; | |
model.cancel = cancel; | |
model.on = on; | |
model.off = off; | |
model.set = set; | |
return model; | |
} | |
Model.getFlowGraph = getFlowGraph; | |
Model.resetFlowGraph = resetFlowGraph; | |
// Support AMD (RequireJS), CommonJS (Node), and browser globals. | |
// Inspired by https://github.com/umdjs/umd | |
if (typeof define === "function" && define.amd) { | |
define([], function () { return Model; }); | |
} else if (typeof exports === "object") { | |
module.exports = Model; | |
} else { | |
this.Model = Model; | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A reusable scatter plot module. | |
// Curran Kelleher March 2015 | |
define(["d3", "model"], function (d3, Model) { | |
// A representation for an optional Model property that is not specified. | |
// This allows the "when" approach to support optional properties. | |
// Inspired by Scala's Option type. | |
// See http://alvinalexander.com/scala/using-scala-option-some-none-idiom-function-java-null | |
var None = "__none__"; | |
// The constructor function, accepting default values. | |
return function ScatterPlot(defaults) { | |
// Create a Model instance for the visualization. | |
// This will serve as its public API. | |
var model = Model(); | |
// Create an SVG element from the container DOM element. | |
model.when("container", function (container) { | |
model.svg = d3.select(container).append("svg") | |
// Use CSS `position: absolute;` | |
// so setting `left` and `top` later will | |
// position the SVG relative to the container div. | |
.style("position", "absolute"); | |
}); | |
// Adjust the size of the SVG based on the `box` property. | |
model.when(["svg", "box"], function (svg, box) { | |
// Set the CSS `left` and `top` properties | |
// to move the SVG to `(box.x, box.y)` | |
// relative to the container div. | |
svg | |
.style("left", box.x + "px") | |
.style("top", box.y + "px") | |
.attr("width", box.width) | |
.attr("height", box.height); | |
}); | |
// Create an SVG group that will contain the visualization. | |
model.when("svg", function (svg) { | |
model.g = svg.append("g"); | |
}); | |
model.when("g", function (g) { | |
// Add an SVG group to contain the marks. | |
model.circlesG = g.append("g"); | |
// Create a group for the brush. | |
model.brushG = g.append("g").attr("class", "brush"); | |
// The circles group is added first, before the brush group, | |
// so that mouse events go to the brush rather than to the | |
// circles, even when the mouse is on top of a circle. | |
}); | |
// Disable brushing by default. | |
model.brushEnabled = false; | |
// Set up brushing interactions to define `brushedIntervals` on the model. | |
model.when(["brushEnabled", "xColumn", "yColumn", "xScale", "yScale"], | |
function (brushEnabled, xColumn, yColumn, xScale, yScale) { | |
if(brushEnabled){ | |
var brush = d3.svg.brush(); | |
brush.on("brush", function () { | |
model.brushedIntervals = brushToIntervals(brush, xColumn, yColumn, xScale, yScale); | |
}); | |
model.brush = brush; | |
} | |
}); | |
function brushToIntervals(brush, xColumn, yColumn, xScale, yScale){ | |
var brushedIntervals = {}; | |
if(!brush.empty() | |
&& brush.extent() !== null){ | |
var e = brush.extent(), | |
xMin = e[0][0], | |
yMin = e[0][1], | |
xMax = e[1][0], | |
yMax = e[1][1], | |
epsilon = 0.01; | |
// Account for the edge case where the brush is at the | |
// X or Y min or max. Adding a small value ensures that all | |
// points are included when crossfilter's filterRange is used, | |
// because filterRange provides an exclusive range, not inclusive. | |
// See https://github.com/square/crossfilter/wiki/API-Reference#dimension_filterRange | |
if(xMax === xScale.domain()[1]){ xMax += epsilon; } | |
if(yMax === yScale.domain()[1]){ yMax += epsilon; } | |
if(xMin === xScale.domain()[0]){ xMin -= epsilon; } | |
if(yMin === yScale.domain()[0]){ yMin -= epsilon; } | |
brushedIntervals[xColumn] = [xMin, xMax]; | |
brushedIntervals[yColumn] = [yMin, yMax]; | |
} else { | |
brushedIntervals[xColumn] = [None, None]; | |
brushedIntervals[yColumn] = [None, None]; | |
} | |
return brushedIntervals; | |
} | |
function intervalsToBrush(brushedIntervals, xColumn, yColumn){ | |
return [ | |
[brushedIntervals[xColumn][0], brushedIntervals[yColumn][0]], | |
[brushedIntervals[xColumn][1], brushedIntervals[yColumn][1]] | |
]; | |
} | |
// Update the rendered brush. | |
model.when(["brushedIntervals", "brush", "brushG", "xColumn", "yColumn", "xScale", "yScale"], | |
function (brushedIntervals, brush, brushG, xColumn, yColumn, xScale, yScale) { | |
// Update the scales within the brush. | |
brush.x(xScale); | |
brush.y(yScale); | |
// Update the extent of the brush. | |
brush.extent(intervalsToBrush(brushedIntervals, xColumn, yColumn)); | |
// Render the brush onto the brush group. | |
brushG.call(brush); | |
}); | |
// Adjust the SVG group translation based on the margin. | |
model.when(["g", "margin"], function (g, margin) { | |
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
}); | |
// Create the title text element. | |
model.when("g", function (g){ | |
model.titleText = g.append("text").attr("class", "title-text"); | |
}); | |
// Center the title text when width changes. | |
model.when(["titleText", "width"], function (titleText, width) { | |
titleText.attr("x", width / 2); | |
}); | |
// Update the title text based on the `title` property. | |
model.when(["titleText", "title"], function (titleText, title){ | |
titleText.text(title); | |
}); | |
// Update the title text offset. | |
model.when(["titleText", "titleOffset"], function (titleText, titleOffset){ | |
titleText.attr("dy", titleOffset + "em"); | |
}); | |
// Compute the inner box from the outer box and margin. | |
// See Margin Convention 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; | |
}); | |
// Generate a function for getting the X value. | |
model.when(["data", "xColumn"], function (data, xColumn) { | |
model.getX = function (d) { return d[xColumn]; }; | |
}); | |
// Compute the domain of the X attribute. | |
// Allow the API client to optionally specify fixed min and max values. | |
model.xDomainMin = None; | |
model.xDomainMax = None; | |
model.when(["data", "getX", "xDomainMin", "xDomainMax"], | |
function (data, getX, xDomainMin, xDomainMax) { | |
if(xDomainMin === None && xDomainMax === None){ | |
model.xDomain = d3.extent(data, getX); | |
} else { | |
if(xDomainMin === None){ | |
xDomainMin = d3.min(data, getX); | |
} | |
if(xDomainMax === None){ | |
xDomainMax = d3.max(data, getX); | |
} | |
model.xDomain = [xDomainMin, xDomainMax] | |
} | |
}); | |
// Compute the X scale. | |
model.when(["xDomain", "width"], function (xDomain, width) { | |
model.xScale = d3.scale.linear().domain(xDomain).range([0, width]); | |
}); | |
// Generate a function for getting the scaled X value. | |
model.when(["data", "xScale", "getX"], function (data, xScale, getX) { | |
model.getXScaled = function (d) { return xScale(getX(d)); }; | |
}); | |
// Set up the X axis. | |
model.when("g", function (g) { | |
model.xAxisG = g.append("g").attr("class", "x axis"); | |
model.xAxisText = model.xAxisG.append("text").style("text-anchor", "middle"); | |
}); | |
// Move the X axis label based on its specified offset. | |
model.when(["xAxisText", "xAxisLabelOffset"], function (xAxisText, xAxisLabelOffset){ | |
xAxisText.attr("dy", xAxisLabelOffset + "em"); | |
}); | |
// Update the X axis transform when height changes. | |
model.when(["xAxisG", "height"], function (xAxisG, height) { | |
xAxisG.attr("transform", "translate(0," + height + ")"); | |
}); | |
// Center the X axis label when width changes. | |
model.when(["xAxisText", "width"], function (xAxisText, width) { | |
xAxisText.attr("x", width / 2); | |
}); | |
// Update the X axis based on the X scale. | |
model.when(["xAxisG", "xScale"], function (xAxisG, xScale) { | |
xAxisG.call(d3.svg.axis().orient("bottom").scale(xScale)); | |
}); | |
// Update X axis label. | |
model.when(["xAxisText", "xAxisLabel"], function (xAxisText, xAxisLabel) { | |
xAxisText.text(xAxisLabel); | |
}); | |
// Generate a function for getting the Y value. | |
model.when(["data", "yColumn"], function (data, yColumn) { | |
model.getY = function (d) { return d[yColumn]; }; | |
}); | |
// Compute the domain of the Y attribute. | |
// Allow the API client to optionally specify fixed min and max values. | |
model.yDomainMin = None; | |
model.yDomainMax = None; | |
model.when(["data", "getY", "yDomainMin", "yDomainMax"], | |
function (data, getY, yDomainMin, yDomainMax) { | |
if(yDomainMin === None && yDomainMax === None){ | |
model.yDomain = d3.extent(data, getY); | |
} else { | |
if(yDomainMin === None){ | |
yDomainMin = d3.min(data, getY); | |
} | |
if(yDomainMax === None){ | |
yDomainMax = d3.max(data, getY); | |
} | |
model.yDomain = [yDomainMin, yDomainMax] | |
} | |
}); | |
// Compute the Y scale. | |
model.when(["data", "yDomain", "height"], function (data, yDomain, height) { | |
model.yScale = d3.scale.linear().domain(yDomain).range([height, 0]); | |
}); | |
// Generate a function for getting the scaled Y value. | |
model.when(["data", "yScale", "getY"], function (data, yScale, getY) { | |
model.getYScaled = function (d) { return yScale(getY(d)); }; | |
}); | |
// Set up the Y axis. | |
model.when("g", function (g) { | |
model.yAxisG = g.append("g").attr("class", "y axis"); | |
model.yAxisText = model.yAxisG.append("text") | |
.style("text-anchor", "middle") | |
.attr("transform", "rotate(-90)") | |
.attr("y", 0); | |
}); | |
// Move the Y axis label based on its specified offset. | |
model.when(["yAxisText", "yAxisLabelOffset"], function (yAxisText, yAxisLabelOffset){ | |
yAxisText.attr("dy", "-" + yAxisLabelOffset + "em") | |
}); | |
// Center the Y axis label when height changes. | |
model.when(["yAxisText", "height"], function (yAxisText, height) { | |
yAxisText.attr("x", -height / 2); | |
}); | |
// Update Y axis label. | |
model.when(["yAxisText", "yAxisLabel"], function (yAxisText, yAxisLabel) { | |
yAxisText.text(yAxisLabel); | |
}); | |
// Update the Y axis based on the Y scale. | |
model.when(["yAxisG", "yScale"], function (yAxisG, yScale) { | |
yAxisG.call(d3.svg.axis().orient("left").scale(yScale)); | |
}); | |
// Allow the API client to optionally specify a size column. | |
model.sizeColumn = None; | |
// The default radius of circles in pixels. | |
model.sizeDefault = 2; | |
// The min and max circle radius in pixels. | |
model.sizeMin = 0.5; | |
model.sizeMax = 6; | |
// Set up the size scale. | |
model.when(["sizeColumn", "data", "sizeDefault", "sizeMin", "sizeMax"], | |
function (sizeColumn, data, sizeDefault, sizeMin, sizeMax){ | |
if(sizeColumn !== None){ | |
var getSize = function (d){ return d[sizeColumn] }, | |
sizeScale = d3.scale.linear() | |
.domain(d3.extent(data, getSize)) | |
.range([sizeMin, sizeMax]); | |
model.getSizeScaled = function (d){ return sizeScale(getSize(d)); }; | |
} else { | |
model.getSizeScaled = function (d){ return sizeDefault; }; | |
} | |
}); | |
// Allow the API client to optionally specify a color column. | |
model.colorColumn = None; | |
model.colorRange = None; | |
// The default color of circles (CSS color string). | |
model.colorDefault = "black"; | |
// Set up the size scale. | |
model.when(["colorColumn", "data", "colorDefault", "colorRange"], | |
function (colorColumn, data, colorDefault, colorRange){ | |
if(colorColumn !== None && colorRange !== None){ | |
var getColor = function (d){ return d[colorColumn] }, | |
colorScale = d3.scale.ordinal() | |
.domain(data.map(getColor)) | |
.range(colorRange); | |
model.getColorScaled = function (d){ return colorScale(getColor(d)); }; | |
} else { | |
model.getColorScaled = function (d){ return colorDefault; }; | |
} | |
}); | |
// Filter out points that go beyond the edges of the plot | |
// for the case that the domain is set explicitly and is | |
// smaller than the extent of the data. | |
model.when(["data", "getX", "getY", "xScale", "yScale"], | |
function(data, getX, getY, xScale, yScale){ | |
var xMin = xScale.domain()[0], xMax = xScale.domain()[1], | |
yMin = yScale.domain()[0], yMax = yScale.domain()[1]; | |
model.visibleData = data.filter(function(d){ | |
var x = getX(d), y = getY(d); | |
return x > xMin && x < xMax && y > yMin && y < yMax; | |
}); | |
}); | |
// Draw the circles of the scatter plot. | |
model.when(["visibleData", "circlesG", "getXScaled", "getYScaled", "getSizeScaled", "getColorScaled"], | |
function (visibleData, circlesG, getXScaled, getYScaled, getSizeScaled, getColorScaled){ | |
var circles = circlesG.selectAll("circle").data(visibleData); | |
circles.enter().append("circle"); | |
circles | |
.attr("cx", getXScaled) | |
.attr("cy", getYScaled) | |
.attr("r", getSizeScaled) | |
.attr("fill", getColorScaled); | |
circles.exit().remove(); | |
}); | |
// Set defaults at the end so they override optional properties set to None. | |
model.set(defaults); | |
return model; | |
}; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Remove the default margin. */ | |
body { | |
margin: 0px; | |
} | |
/* Make the visualization container fill the page. */ | |
#container { | |
/* Use the default size from bl.ocks.org */ | |
width: 960px; | |
height: 500px; | |
} | |
/* Put a border around each plot. */ | |
svg { | |
border-style: solid; | |
border-color: lightgray; | |
border-width: 1px; | |
} | |
/* Style the visualization. Draws from http://bl.ocks.org/mbostock/3887118 */ | |
/* Tick mark labels */ | |
.axis .tick text { | |
font: 8pt sans-serif; | |
} | |
/* Axis labels */ | |
.axis text { | |
font: 14pt sans-serif; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.line { | |
fill: none; | |
stroke: black; | |
stroke-width: 1.5px; | |
} | |
.title-text { | |
text-anchor: middle; | |
font: 24pt sans-serif; | |
} | |
/* Style the brush. Draws from http://bl.ocks.org/mbostock/4343214 */ | |
.brush .extent { | |
stroke: gray; | |
fill-opacity: .125; | |
shape-rendering: crispEdges; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment