Skip to content

Instantly share code, notes, and snippets.

@curran
Last active April 4, 2017 09:27
Show Gist options
  • Save curran/37aed2216fce3ecd8c2f5f13fccf812e to your computer and use it in GitHub Desktop.
Save curran/37aed2216fce3ecd8c2f5f13fccf812e to your computer and use it in GitHub Desktop.
[unlisted] Scatter Plot Template
license: mit

A pattern for data visualization programming where the goals are:

  • Make it easy for non-technical people to reconfigure the visualization.
  • Make the internal components open for composition and adding interactions.
<!DOCTYPE html>
<html>
<head>
<title>Scatter Plot Template</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
.axis .tick line {
stroke-width: 2px;
stroke: #dddddd;
}
.axis .tick text {
font-size: 30px;
fill: #8E8883;
}
.axis .domain {
display: none;
}
.axis__label {
text-anchor: middle;
font-size: 50px;
fill: #635F5D;
}
.legend .tick text {
font-size: 30px;
fill: #8E8883;
font-family: sans-serif;
alignment-baseline: middle;
}
.legend__label {
font-size: 40px;
fill: #635F5D;
font-family: sans-serif;
}
</style>
</head>
<body>
<svg width="960" height="500"></svg>
<script>
function scatterPlotMarks(selection, props){
const data = props.data;
const xScale = props.xScale;
const xValue = props.xValue;
const yScale = props.yScale;
const yValue = props.yValue;
const colorScale = props.colorScale;
const colorValue = props.colorValue;
const sizeScale = props.sizeScale;
const sizeValue = props.sizeValue;
const circles = selection
.selectAll("circle")
.data(data);
circles
.exit()
.remove();
circles
.enter().append("circle")
.merge(circles)
.attr("cx", d => xScale(xValue(d)))
.attr("cy", d => yScale(yValue(d)))
.attr("fill", d => colorScale(colorValue(d)))
.attr("r", d => sizeScale(sizeValue(d)));
}
function xAxis(selection, props){
const scale = props.scale;
const innerWidth = props.innerWidth;
const innerHeight = props.innerHeight;
const ticks = props.ticks;
const tickPadding = props.tickPadding;
const label = props.label;
const labelPadding = props.labelPadding;
const axis = d3.axisBottom()
.scale(scale)
.tickSizeInner(-innerHeight)
.ticks(ticks)
.tickPadding(tickPadding);
let g = selection
.selectAll(".axis--x")
.data([null]);
g = g
.enter().append("g")
.attr("class", "axis axis--x")
.merge(g)
.attr("transform", `translate(0, ${innerHeight})`)
.call(axis);
const labelText = g
.selectAll(".axis__label")
.data([null]);
labelText
.enter().append("text")
.attr("class", "axis__label")
.merge(labelText)
.attr("x", innerWidth / 2)
.attr("y", labelPadding)
.text(label);
}
function yAxis(selection, props){
const scale = props.scale;
const innerWidth = props.innerWidth;
const innerHeight = props.innerHeight;
const ticks = props.ticks;
const tickPadding = props.tickPadding;
const label = props.label;
const labelPadding = props.labelPadding;
const axis = d3.axisLeft()
.scale(scale)
.tickSizeInner(-innerWidth)
.ticks(ticks)
.tickPadding(tickPadding);
let g = selection
.selectAll(".axis--y")
.data([null]);
g = g
.enter().append("g")
.attr("class", "axis axis--y")
.merge(g)
.call(axis);
const labelText = g
.selectAll(".axis__label")
.data([null]);
labelText
.enter().append("text")
.attr("class", "axis__label")
.attr("transform", "rotate(-90)")
.merge(labelText)
.attr("x", -innerHeight / 2)
.attr("y", labelPadding)
.text(label);
}
function scatterPlot(selection, props){
const data = props.data;
const width = props.width;
const height = props.height;
const margin = props.margin;
const xValue = props.xValue;
const yValue = props.yValue;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = d3.scaleLinear()
.domain(d3.extent(data, xValue)).nice()
.range([0, innerWidth]);
const yScale = d3.scaleLinear()
.domain(d3.extent(data, yValue)).nice()
.range([innerHeight, 0]);
let g = selection
.selectAll("g")
.data([null]);
g = g
.enter().append("g")
.merge(g)
.attr("transform", `translate(${margin.left}, ${margin.top})`);
xAxis(g, {
scale: xScale,
ticks: 10,
tickPadding: 15,
innerWidth: innerWidth,
innerHeight: innerHeight,
label: props.xLabel,
labelPadding: 85
});
yAxis(g, {
scale: yScale,
ticks: 5,
tickPadding: 10,
labelPadding: -45,
innerWidth: innerWidth,
innerHeight: innerHeight,
label: props.yLabel
});
scatterPlotMarks(g, {
data,
xScale,
xValue,
yScale,
yValue,
colorScale: props.colorScale,
colorValue: props.colorValue,
sizeScale: props.sizeScale,
sizeValue: props.sizeValue
});
}
function sizeLegend(selection, props){
const x = props.x;
const y = props.y;
const scale = props.scale;
const spacing = props.spacing;
const ticks = props.ticks;
const tickPadding = props.tickPadding;
const tickFill = props.tickFill;
const label = props.label;
const labelX = props.labelX;
const labelY = props.labelY;
let g = selection
.selectAll(".legend--size")
.data([null]);
g = g
.enter().append("g")
.attr("class", "legend legend--size")
.merge(g)
.attr("transform", `translate(${x}, ${y})`);
const labelText = g
.selectAll(".legend__label")
.data([null]);
labelText
.enter().append("text")
.attr("class", "legend__label")
.merge(labelText)
.attr("x", labelX)
.attr("y", labelY)
.text(label);
const tick = g
.selectAll(".tick")
.data(scale.ticks(ticks).filter(d => d));
tick
.exit()
.remove();
const tickEnter = tick
.enter().append("g")
.attr("class", "tick");
tickEnter
.merge(tick)
.attr("transform", (d, i) => `translate(0, ${i * spacing})`);
tick
.select("circle")
.merge(tickEnter.append("circle"))
.attr("r", scale)
.attr("fill", tickFill);
tick
.select("text")
.merge(tickEnter.append("text"))
.attr("x", tickPadding)
.text(d => d);
}
function colorLegend(selection, props){
const x = props.x;
const y = props.y;
const scale = props.scale;
const spacing = props.spacing;
const ticks = props.ticks;
const tickPadding = props.tickPadding;
const tickRadius = props.tickRadius;
const label = props.label;
const labelX = props.labelX;
const labelY = props.labelY;
let g = selection
.selectAll(".legend--color")
.data([null]);
g = g
.enter().append("g")
.attr("class", "legend legend--color")
.merge(g)
.attr("transform", `translate(${x}, ${y})`);
const labelText = g
.selectAll(".legend__label")
.data([null]);
labelText
.enter().append("text")
.attr("class", "legend__label")
.merge(labelText)
.attr("x", labelX)
.attr("y", labelY)
.text(label);
const tick = g
.selectAll(".tick")
.data(scale.domain());
tick
.exit()
.remove();
const tickEnter = tick
.enter().append("g")
.attr("class", "tick");
tickEnter
.merge(tick)
.attr("transform", (d, i) => `translate(0, ${i * spacing})`);
tick
.select("circle")
.merge(tickEnter.append("circle"))
.attr("r", tickRadius)
.attr("fill", scale);
tick
.select("text")
.merge(tickEnter.append("text"))
.attr("x", tickPadding)
.text(d => d);
}
function main(){
const svg = d3.select("svg");
function type(d){
d.sepalLength = +d.sepalLength;
d.sepalWidth = +d.sepalWidth;
d.petalLength = +d.petalLength;
d.petalWidth = +d.petalWidth;
return d;
}
d3.csv("iris.csv", type, (data) => {
const colorValue = d => d.species;
const sizeValue = d => d.petalWidth;
const sizeMax = 12;
const colorScale = d3.scaleOrdinal()
.domain(["setosa", "versicolor", "virginica"])
.range(["#eb8e37", "#1ac6cf", "#e35dd4"]);
const sizeScale = d3.scaleSqrt()
.domain([0, d3.max(data, sizeValue)])
.range([0, sizeMax]);
svg
.call(scatterPlot, {
data,
width: +svg.attr("width"),
height: +svg.attr("height"),
margin: {top: 30, right: 287, bottom: 100, left: 100},
xValue: d => d.sepalLength,
xLabel: "Sepal Length",
yValue: d => d.petalLength,
yLabel: "Petal Length",
colorScale,
colorValue,
sizeScale,
sizeValue
})
.call(sizeLegend, {
scale: sizeScale,
label: "Petal Width",
x: 750,
y: 285,
spacing: 35,
ticks: 5,
tickPadding: 20,
tickFill: "gray",
labelX: -20,
labelY: -30,
})
.call(colorLegend, {
scale: colorScale,
x: 750,
y: 90,
tickRadius: 10,
spacing: 35,
tickPadding: 20,
label: "Species",
labelX: -20,
labelY: -30,
});
});
}
main();
</script>
</body>
</html>
sepalLength sepalWidth petalLength petalWidth species
5.1 3.5 1.4 0.2 setosa
4.9 3.0 1.4 0.2 setosa
4.7 3.2 1.3 0.2 setosa
4.6 3.1 1.5 0.2 setosa
5.0 3.6 1.4 0.2 setosa
5.4 3.9 1.7 0.4 setosa
4.6 3.4 1.4 0.3 setosa
5.0 3.4 1.5 0.2 setosa
4.4 2.9 1.4 0.2 setosa
4.9 3.1 1.5 0.1 setosa
5.4 3.7 1.5 0.2 setosa
4.8 3.4 1.6 0.2 setosa
4.8 3.0 1.4 0.1 setosa
4.3 3.0 1.1 0.1 setosa
5.8 4.0 1.2 0.2 setosa
5.7 4.4 1.5 0.4 setosa
5.4 3.9 1.3 0.4 setosa
5.1 3.5 1.4 0.3 setosa
5.7 3.8 1.7 0.3 setosa
5.1 3.8 1.5 0.3 setosa
5.4 3.4 1.7 0.2 setosa
5.1 3.7 1.5 0.4 setosa
4.6 3.6 1.0 0.2 setosa
5.1 3.3 1.7 0.5 setosa
4.8 3.4 1.9 0.2 setosa
5.0 3.0 1.6 0.2 setosa
5.0 3.4 1.6 0.4 setosa
5.2 3.5 1.5 0.2 setosa
5.2 3.4 1.4 0.2 setosa
4.7 3.2 1.6 0.2 setosa
4.8 3.1 1.6 0.2 setosa
5.4 3.4 1.5 0.4 setosa
5.2 4.1 1.5 0.1 setosa
5.5 4.2 1.4 0.2 setosa
4.9 3.1 1.5 0.1 setosa
5.0 3.2 1.2 0.2 setosa
5.5 3.5 1.3 0.2 setosa
4.9 3.1 1.5 0.1 setosa
4.4 3.0 1.3 0.2 setosa
5.1 3.4 1.5 0.2 setosa
5.0 3.5 1.3 0.3 setosa
4.5 2.3 1.3 0.3 setosa
4.4 3.2 1.3 0.2 setosa
5.0 3.5 1.6 0.6 setosa
5.1 3.8 1.9 0.4 setosa
4.8 3.0 1.4 0.3 setosa
5.1 3.8 1.6 0.2 setosa
4.6 3.2 1.4 0.2 setosa
5.3 3.7 1.5 0.2 setosa
5.0 3.3 1.4 0.2 setosa
7.0 3.2 4.7 1.4 versicolor
6.4 3.2 4.5 1.5 versicolor
6.9 3.1 4.9 1.5 versicolor
5.5 2.3 4.0 1.3 versicolor
6.5 2.8 4.6 1.5 versicolor
5.7 2.8 4.5 1.3 versicolor
6.3 3.3 4.7 1.6 versicolor
4.9 2.4 3.3 1.0 versicolor
6.6 2.9 4.6 1.3 versicolor
5.2 2.7 3.9 1.4 versicolor
5.0 2.0 3.5 1.0 versicolor
5.9 3.0 4.2 1.5 versicolor
6.0 2.2 4.0 1.0 versicolor
6.1 2.9 4.7 1.4 versicolor
5.6 2.9 3.6 1.3 versicolor
6.7 3.1 4.4 1.4 versicolor
5.6 3.0 4.5 1.5 versicolor
5.8 2.7 4.1 1.0 versicolor
6.2 2.2 4.5 1.5 versicolor
5.6 2.5 3.9 1.1 versicolor
5.9 3.2 4.8 1.8 versicolor
6.1 2.8 4.0 1.3 versicolor
6.3 2.5 4.9 1.5 versicolor
6.1 2.8 4.7 1.2 versicolor
6.4 2.9 4.3 1.3 versicolor
6.6 3.0 4.4 1.4 versicolor
6.8 2.8 4.8 1.4 versicolor
6.7 3.0 5.0 1.7 versicolor
6.0 2.9 4.5 1.5 versicolor
5.7 2.6 3.5 1.0 versicolor
5.5 2.4 3.8 1.1 versicolor
5.5 2.4 3.7 1.0 versicolor
5.8 2.7 3.9 1.2 versicolor
6.0 2.7 5.1 1.6 versicolor
5.4 3.0 4.5 1.5 versicolor
6.0 3.4 4.5 1.6 versicolor
6.7 3.1 4.7 1.5 versicolor
6.3 2.3 4.4 1.3 versicolor
5.6 3.0 4.1 1.3 versicolor
5.5 2.5 4.0 1.3 versicolor
5.5 2.6 4.4 1.2 versicolor
6.1 3.0 4.6 1.4 versicolor
5.8 2.6 4.0 1.2 versicolor
5.0 2.3 3.3 1.0 versicolor
5.6 2.7 4.2 1.3 versicolor
5.7 3.0 4.2 1.2 versicolor
5.7 2.9 4.2 1.3 versicolor
6.2 2.9 4.3 1.3 versicolor
5.1 2.5 3.0 1.1 versicolor
5.7 2.8 4.1 1.3 versicolor
6.3 3.3 6.0 2.5 virginica
5.8 2.7 5.1 1.9 virginica
7.1 3.0 5.9 2.1 virginica
6.3 2.9 5.6 1.8 virginica
6.5 3.0 5.8 2.2 virginica
7.6 3.0 6.6 2.1 virginica
4.9 2.5 4.5 1.7 virginica
7.3 2.9 6.3 1.8 virginica
6.7 2.5 5.8 1.8 virginica
7.2 3.6 6.1 2.5 virginica
6.5 3.2 5.1 2.0 virginica
6.4 2.7 5.3 1.9 virginica
6.8 3.0 5.5 2.1 virginica
5.7 2.5 5.0 2.0 virginica
5.8 2.8 5.1 2.4 virginica
6.4 3.2 5.3 2.3 virginica
6.5 3.0 5.5 1.8 virginica
7.7 3.8 6.7 2.2 virginica
7.7 2.6 6.9 2.3 virginica
6.0 2.2 5.0 1.5 virginica
6.9 3.2 5.7 2.3 virginica
5.6 2.8 4.9 2.0 virginica
7.7 2.8 6.7 2.0 virginica
6.3 2.7 4.9 1.8 virginica
6.7 3.3 5.7 2.1 virginica
7.2 3.2 6.0 1.8 virginica
6.2 2.8 4.8 1.8 virginica
6.1 3.0 4.9 1.8 virginica
6.4 2.8 5.6 2.1 virginica
7.2 3.0 5.8 1.6 virginica
7.4 2.8 6.1 1.9 virginica
7.9 3.8 6.4 2.0 virginica
6.4 2.8 5.6 2.2 virginica
6.3 2.8 5.1 1.5 virginica
6.1 2.6 5.6 1.4 virginica
7.7 3.0 6.1 2.3 virginica
6.3 3.4 5.6 2.4 virginica
6.4 3.1 5.5 1.8 virginica
6.0 3.0 4.8 1.8 virginica
6.9 3.1 5.4 2.1 virginica
6.7 3.1 5.6 2.4 virginica
6.9 3.1 5.1 2.3 virginica
5.8 2.7 5.1 1.9 virginica
6.8 3.2 5.9 2.3 virginica
6.7 3.3 5.7 2.5 virginica
6.7 3.0 5.2 2.3 virginica
6.3 2.5 5.0 1.9 virginica
6.5 3.0 5.2 2.0 virginica
6.2 3.4 5.4 2.3 virginica
5.9 3.0 5.1 1.8 virginica
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment