Published
Edited
Sep 12, 2021
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
//mpg dataset
mpg = Object.assign(d3.csvParse(await FileAttachment("mpg.csv").text(), ({origin: origin, weight: x, mpg: y}) => ({origin, x:+x, y: +y})), {x: "Weight →", y: "↑mpg"})
Insert cell
Insert cell
Insert cell
x = d3.scaleLinear()
.domain(d3.extent(mpg, d => d.x)).nice()
.range([margin.left, width - margin.right])
Insert cell
y = d3.scaleLinear()
.domain(d3.extent(mpg, d => d.y)).nice()
.range([height - margin.bottom, margin.top])
Insert cell
color = d3.scaleOrdinal(mpg.map(d => d.origin), d3.schemeCategory10) //using d3.schemeCategory10(An array of ten categorical colors represented as RGB hexadecimal strings). To know more about color schemes in d3, go: https://github.com/d3/d3-scale-chromatic/blob/main/README.md
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(20))
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", width)
.attr("y", margin.bottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.text(mpg.x))
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text(mpg.y))
Insert cell
grid = g => g
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
//Vertial Grid Lines: We fix the extent for y-direction and then change the x-coordinates accordingly to put vertical gridlines with common seperation-width. [a line is formed by joining (x1,y1) and (x2,y2)]
.call(g => g.append("g")
.selectAll("line")
.data(x.ticks(20))
.join("line")
.attr("x1", d => x(d))
.attr("x2", d => x(d))
.attr("y1", margin.top)
.attr("y2", height - margin.bottom))
//Horizontal grid lines: We fix the extent for x-direction and then change the y-coordinates accordingly to put horizontal gridlines with common seperation-height
.call(g => g.append("g")
.selectAll("line")
.data(y.ticks())
.join("line")
.attr("y1", d => y(d))
.attr("y2", d => y(d))
.attr("x1", margin.left)
.attr("x2", width - margin.right));
Insert cell
legend = svg => {
const g = svg
.attr("transform", `translate(${width},10)`)
.attr("text-anchor", "end")
.attr("font-size", 20)
.selectAll("g")
.data(color.domain().reverse())
.join("g")
.attr("transform", (d, i) => `translate(-${i * 100}, 0)`); //"-${i *100}" displaces on x-position and puts legends seperately from each other

g.append("circle")
.attr("cx", -19)
.attr("cy", 9)
.attr("r", 5)
.attr("fill", color);

g.append("text")
.attr("x", -24)
.attr("y", 9.5)
.attr("dy", "0.25em") //em is simply the font size, it defines the proportion of the letter width and height with respect to the point size of the current font
.text(d => d);
}
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

svg.append("g")
.call(xAxis);

svg.append("g")
.call(yAxis);

svg.append("g")
.call(grid);
svg.append("g")
.call(legend);

svg.append("g")
.selectAll("circle")
.data(mpg)
.join("circle")
.attr("cx", d => x(d.x))
.attr("cy", d => y(d.y))
.attr("r", 3)
.attr("fill", d => color(d.origin))
.append("title")
.text(d => `x: ${d.x}\ny: ${d.y}`)

return svg.node();
}
Insert cell
Insert cell
brushableChart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.property("value", []);
const brush = d3.brush()
.on("start brush end", brushed);
//start - at the start of a brush gesture, such as on mouse-click.
//brush - when the brush moves, such as on mouse-move.
//end - at the end of a brush gesture, such as on mouse-release.
//brush is turned on when above three activities are done.
svg.append("g")
.call(xAxis);

svg.append("g")
.call(yAxis);

svg.append("g")
.call(grid);

svg.append("g")
.call(legend);
const dot = svg.append("g")
.selectAll("circle")
.data(mpg)
.join("circle")
.attr("cx", d => x(d.x))
.attr("cy", d => y(d.y))
//Instead of "cx" and "cy", we can directly use linear transform, too.
//.attr("transform", d => `translate(${x(d.x)},${y(d.y)})`)
.attr("r", 3)
.attr("fill", d => color(d.origin))
svg.append("g")
.call(brush);

function brushed({selection}) {
let value = [];
if (selection) {
const [[x0, y0], [x1, y1]] = selection;
//colors the dot with respect to "origin" field, if it is within the brushed part (or inside the rectangular box) elsewise, leaves the points as gray
value = dot
.attr("fill", "gray")
.filter(d => x0 <= x(d.x) && x(d.x) < x1 && y0 <= y(d.y) && y(d.y) < y1)
.attr("fill", d => color(d.origin))
}
else{
//Colors all back to the original plain scatterplot after you exit the brush selection (for instance, clicking outside the box of selection)
dot.attr("fill", d => color(d.origin));
}
}
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more