Skip to content

Instantly share code, notes, and snippets.

@curran
Last active August 29, 2015 14:17
Show Gist options
  • Save curran/cf4b98fff0517ca04667 to your computer and use it in GitHub Desktop.
Save curran/cf4b98fff0517ca04667 to your computer and use it in GitHub Desktop.
Scatter Plot Zooming

This program makes a scatter plot from Iris data set. Brushing in one plot zooms in the other.

<!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>
{
"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"
}
}
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 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);
});
// 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;
}
})();
// 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;
};
});
/* 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