Published
Edited
Jun 1, 2020
Insert cell
Insert cell
Insert cell
data = d3.csvParse(await FileAttachment("iris.csv").text(), d3.autoType)
Insert cell
attributes = data.columns.filter(d => d !== "species")
Insert cell
//debug purpose
console.log(data[1]);
Insert cell
Insert cell
height = width // set equal to width for a square visualization
Insert cell
Insert cell
//The width for each cell when excluding the total width of padding.
//Return the whole number

cellWidth = parseInt((width-(attributes.length+1)*padding)/attributes.length)

Insert cell

//height = width since it is square matrix
//The height for each cell when excluding the total height of padding.
//Return the whole number
cellHeight = parseInt((width-(attributes.length+1)*padding)/attributes.length)

Insert cell
Insert cell
//Jimmy: Scale reference: https://www.oreilly.com/library/view/interactive-data-visualization/9781449340223/ch07.html

//Jimmy: Giving a bunch of “things” and a bunch of values, and they will consistently return the same value for the same thing. https://observablehq.com/@d3/d3-scaleordinal?collection=@d3/d3-scale

//Jimmy: color = d3.scaleOrdinal(d3.schemeCategory10).domain(["setosa","versicolor","virginica"]);

color = d3.scaleOrdinal(d3.schemeTableau10)
//Jimmy: https://github.com/d3/d3-scale/blob/master/README.md#ordinal_domain
//Jimmy: Domain values are stored internally in a map from stringified value to index
//Jimmy: data.map(d => d.species) will return the total number of item's species but there are only 3 different values.
.domain(data.map(d => d.species));

// color = function(_) {
// return "#263238";
// }

Insert cell
//for debug purpose
console.log(data.map(d => d.species));
Insert cell
console.log(data[0]);
Insert cell
Insert cell
Insert cell
scatterplotMatrix = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

// set the style of hidden data items
svg.append("style").text("circle.hidden { fill: #000; fill-opacity: 0.1;}");

// setup a group-element for each cell with the respective transform
const cell = svg
//Jimmy: The <g> SVG element
//Jimmy: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
.append("g")
.selectAll("g")
.data(d3.cross(d3.range(attributes.length), d3.range(attributes.length)))
.join("g")
.attr(
"transform",
([i, j]) =>
`translate(${padding + i * (padding + cellWidth)},${padding +
j * (padding + cellHeight)})`
);

// create rectangles showing the plot area of each cell
cell
.append("rect")
.attr("fill", "#ECEFF1")
.attr("stroke", "none")
.attr("width", cellWidth)
.attr("height", cellHeight);

// the following function is called for each individual cell
// i, j are the column- and row-index of this cell
cell.each(function([i, j]) {
//Reference:
//https://observablehq.com/@d3/scatterplot-matrix
// TODO: draw the scatterplot for this cell here
//d3.select(this) creates a 1-element selection containing the current node. This allows you to use D3's operators to modify the element
//https://groups.google.com/forum/#!topic/d3-js/OexNvDXNFuk
//In this case, it return the current cell we are working on.
d3.select(this)
//https://www.tutorialspoint.com/d3js/d3js_data_join.htm
//map the elements of the existing document with the given data set
//In our case: "circle" element to each object in dataset
.selectAll("circle").data(data)
//https://observablehq.com/@d3/selection-join?collection=@d3/d3-selection
//selection.join appends a new SVG circle element for each data set object
.join("circle")
//Add the attributes using the attr
.attr("cx", d => x[i]( d[ attributes[i] ] ) )
//To change the diagonal of the scatterplot matrix:
//.attr("cy", d => y[3-j]( d[ attributes[3-j] ] ) )
.attr("cy", d => y[j]( d[ attributes[j] ] ) )
// create an x-axis below each cell
d3.select(this)
.append("g")
.attr("transform", `translate(0, ${cellHeight})`)
.call(d3.axisBottom(x[i]).ticks(cellWidth / 50));

// create a y-axis to the left of each cell
d3.select(this)
.append("g")
.call(d3.axisLeft(y[j]).ticks(cellHeight / 30));
});

// set attributes of the circles
const circle = cell
.selectAll("circle")
.attr("r", 4.5)
.attr("fill-opacity", 0.5)
.attr("fill", d => color(d.species));

// setup a brush for each cell
cell.call(brush, circle);

// create a label for each attribute on the diagonal
svg
.append("g")
.style("font", "18px Source Serif Pro")
.style("pointer-events", "none")
.selectAll("text")
.data(attributes)
.join("text")
.attr(
"transform",
(d, i) =>
`translate(${padding + i * (padding + cellWidth)},${padding +
i * (padding + cellHeight)})`
)
.attr("x", 10)
.attr("y", 10)
.attr("dy", ".71em")
.text(d => d);

return svg.node();
}
Insert cell
//debug purpose
console.log("Jimmy " + d3.select(this));

Insert cell
console.log("Jimmy " + attributes[3]);
Insert cell
Insert cell
function brush(cell, circle) {
const brush = d3
.brush()
.extent([[0, 0], [cellWidth, cellHeight]])
.on("start", startBrushing)
.on("brush", updateBrush)
.on("end", stopBrushing);

cell.call(brush);

let brushCell;

// clear any previous brush
function startBrushing() {
if (brushCell !== this) {
d3.select(brushCell).call(brush.move, null);
brushCell = this;
}
}

// highlight circles in the brush selection
function updateBrush([i, j]) {
if (d3.event.selection === null) return;
const [[x0, y0], [x1, y1]] = d3.event.selection;

// TODO: implement the linking by de-emphasizing data items outside the brush
//Jimmy: Brushing means selecting a subset of the data items with an input device (mouse). This is usually done to highlight this subset. Brushing is most interesting in connection with linking. For instance in a scatterplot matrix, the user could brush some points in one plot. This causes the brush effect (highlighting, etc.) to be applied on those points in the other plots that represent the same data items.
//https://infovis-wiki.net/wiki/Linking_and_Brushing

//selection.classed: if a value is specified, assigns or unassigns the specified CSS class names on the selected elements by setting the class attribute.
//selection.classed: Add/remove a CSS class from the selection based on data.
//https://github.com/d3/d3-selection
//http://objjob.phrogz.net/d3/method/1103
circle.classed("hidden", d => {
//return true for the circles that are outside selected rectangle by its position in each cell
return (
//the position in Axix x outside the range x0 - x1 has to be hidden
(x0 >= x[i](d[attributes[i]]))
||
(x1 <= x[i](d[attributes[i]]))
||
//the position in Axix y outside the range y0 - y1 has to be hidden
(y0 >= y[j](d[attributes[j]]))
||
(y1 <= y[j](d[attributes[j]]))
);
});
}

// lift the selection if the brush is empty
function stopBrushing() {
if (d3.event.selection !== null) return;
circle.classed("hidden", false);
}
}
Insert cell
md`## Appendix`
Insert cell
x = attributes.map(attribute =>
d3
.scaleLinear()
.domain(d3.extent(data, item => item[attribute]))
.range([0, cellWidth])
)
Insert cell
y = x.map(x => x.copy().range([cellHeight, 0]))
Insert cell
padding = 32
Insert cell
import { swatches } from "@d3/color-legend"
Insert cell
d3 = require("d3@5")
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