Skip to content

Instantly share code, notes, and snippets.

@curran
Last active January 22, 2016 23:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save curran/9e04ccfebeb84bcdc76c to your computer and use it in GitHub Desktop.
Save curran/9e04ccfebeb84bcdc76c to your computer and use it in GitHub Desktop.
Scatter Plot

This program makes a scatter plot from Iris data set, showcasing how a single reusable visualization module can gracefully support optional visualization configuration properties like size and color.

Based on Data Canvas Part 3 - Bar Chart.

// Generates n distinct colors.
// l is the fixed lightness, the L in LAB color space.
// r is the radius of the circle in AB space along which points are taken.
// Documented at http://bl.ocks.org/curran/dd73d3d8925cdf50df86
define([], function (){
var lDefault = 50,
rDefault = 100;
return function generateColors(n, l, r){
var colors = [], a, b, θ;
l = l || lDefault;
r = r || rDefault;
for(var i = 0; i < n; i++){
θ = (i / n) * Math.PI * 2;
a = Math.sin(θ) * r;
b = Math.cos(θ) * r;
colors.push(d3.lab(l, a, b).toString());
}
return colors;
}
});
<!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 RequireJS paths for third party libraries. -->
<script>
requirejs.config({
paths: {
model: "//curran.github.io/cdn/model-v0.2.1/dist/model-min",
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", "generateColors"], function (ScatterPlot, generateColors) {
// 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
};
var scatterPlot1 = ScatterPlot(options);
var scatterPlot2 = ScatterPlot(options);
var scatterPlot3 = ScatterPlot(options);
var scatterPlot4 = 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);
scatterPlot3.set(xyOptions);
scatterPlot4.set(xyOptions);
// Use X, Y, and size for the second scatter plot.
scatterPlot2.sizeColumn = sizeColumn;
// Use X, Y, and color for the third scatter plot.
scatterPlot3.colorColumn = colorColumn;
scatterPlot3.colorRange = d3.scale.category10().range();
// Use X, Y, size, and color for the fourth scatter plot.
scatterPlot4.sizeColumn = sizeColumn;
scatterPlot4.colorColumn = colorColumn;
scatterPlot4.colorRange = scatterPlot3.colorRange;
// 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;
scatterPlot3.data = data;
scatterPlot4.data = data;
});
});
// 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 * 4) / 3,
plotHeight = (height - padding * 3) / 2;
scatterPlot1.box = {
x: padding,
y: height / 2 - plotHeight / 2,
width: plotWidth,
height: plotHeight
};
scatterPlot2.box = {
x: plotWidth + padding * 2,
y: padding,
width: plotWidth,
height: plotHeight
};
scatterPlot3.box = {
x: plotWidth + padding * 2,
y: plotHeight + padding * 2,
width: plotWidth,
height: plotHeight
};
scatterPlot4.box = {
x: plotWidth * 2 + padding * 3,
y: height / 2 - plotHeight / 2,
width: plotWidth,
height: plotHeight
};
}
// once to initialize `model.box`, and
computeBoxes();
// whenever the browser window resizes in the future.
window.addEventListener("resize", computeBoxes);
});
// 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 = Model.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");
});
// 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 color 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; };
}
});
// Add an SVG group to contain the marks.
model.when("g", function (g) {
model.circlesG = g.append("g");
});
// Draw the circles of the scatter plot.
model.when(["data", "circlesG", "getXScaled", "getYScaled", "getSizeScaled", "getColorScaled"],
function (data, circlesG, getXScaled, getYScaled, getSizeScaled, getColorScaled){
var circles = circlesG.selectAll("circle").data(data);
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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment