A pattern for data visualization programming where the goals are:
xxxxxxxxxx
<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>
https://d3js.org/d3.v4.min.js