Published
Edited
Feb 23, 2021
1 fork
Importers
Insert cell
Insert cell
Tuesday = md`## Tuesday
Topics:
- Recap
- [d3.group](https://github.com/d3/d3-array#group) and [Javascript Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
- scaleLog, scaleSqrt: Bubble chart from Gapminder data.
- Update a plot`
Insert cell
gapminderData
Insert cell
[...new Set(gapminderData.map(d => d.year))]
Insert cell
groupedbyCountryYear = d3.group(gapminderData, d => d.country, d => d.year)
Insert cell
groupedbyContinentYear = d3.group(gapminderData, d => d.continent, d => d.year)
Insert cell
groupedbyCountryYear.get("United States").get(2007)
Insert cell
groupeByYear = d3.group(gapminderData, d => d.year)
Insert cell
years = [...groupeByYear.keys()]
Insert cell
groupedByContinentCountry = d3.group(gapminderData, d => d.continent, d => d.country)
Insert cell
[...groupedByContinentCountry.keys()]
Insert cell
continents = [...new Set(gapminderData.map(d => d.continent))]
Insert cell
keyCountriesUsedInClass = ["China", "Ghana", "Germany", "United States"]
Insert cell
keyCountries = d3.merge(
continents.map(c => {
let data = groupedbyContinentYear.get(c).get(recentYear);
const pops = data.map(d => d.pop);
let minIndex = d3.minIndex(pops);
let maxIndex = d3.maxIndex(pops);
return [data[minIndex].country, data[maxIndex].country];
})
)
Insert cell
recentYear = years[years.length-1]
Insert cell
attributes = ["continent", "country", "gdpPercap", "lifeExp", "pop"]
Insert cell
keyObjects = keyCountries.map(c => {
const data = groupedbyCountryYear.get(c).get(recentYear)[0];
return Object.fromEntries(attributes.map(a => [a, data[a]]));
})
Insert cell
md`## Continuous NonLinear Scales: [scaleLog](https://github.com/d3/d3-scale#scaleLog) and [scaleSqrt](https://github.com/d3/d3-scale#scaleSqrt)`
Insert cell
d3.schemeCategory10
Insert cell
{
const [svgNode, svg] = getSVG();
const xScale = d3
.scalePoint()
.domain(keyCountries)
.range([0, W])
.padding(0.2);
const radScale = d3
.scaleSqrt()
.domain([0, d3.max(keyObjects, d => d.pop)])
.range([2, 60])
.nice();
const colorScale = d3
.scaleOrdinal()
.domain(continents)
.range(d3.schemeCategory10);
svg
.append("g")
.selectAll("circle")
.data(keyObjects)
.join("circle")
.attr('transform', d => `translate(${xScale(d.country)},${H / 2})`)
.attr("r", d => radScale(d.pop))
.style("fill", d => colorScale(d.continent))
.style("opacity", 0.5)
.append("title")
.text(d => Object.entries(d).join("\n"));
return svgNode;
}
Insert cell
drawBubbleMap(keyObjects)
Insert cell
Insert cell
"scale" + scaleOption
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
mutable feedback = ""
Insert cell
Insert cell
Insert cell
Insert cell
md`### Plot for Year: ${years[feedback]}.`
Insert cell
drawBubbleMapWithUpdateAxisWithBrush = {
d3.selectAll('.brush>.overlay').remove();
const [svgNode, svg] = getSVG();
const maxRad = 60;
const xScale = d3["scale" + scaleOption]()
.range([0, W])
.nice();
const yScale = d3
.scaleLinear()
.range([H, 0])
.nice();
const radScale = d3
.scaleSqrt()
.range([2, maxRad])
.nice();
const colorScale = d3
.scaleOrdinal()
.domain(continents)
.range(d3.schemeCategory10);

const yAxis = d3.axisLeft().scale(yScale);
svg
.append("g")
.attr("class", "y-axis")
.call(yAxis);
const xAxis = d3.axisBottom().scale(xScale);
svg
.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${H})`)
.call(xAxis);

const bubbles = svg.append("g");
mutable feedback = "enter";

const xScaleBrush = d3
.scaleLinear()
.domain([0, years.length - 1])
.range([0, W + 10]);

const brush = d3
.brushX()
.extent([[0, H], [W + 10, H + margin.bottom]])
.on("brush", brushMove);

const defaultSelection = [W - 10, W + 10];
const slider = svg
.append("g")
.attr("width", W + 10)
.attr("height", margin.bottom)
.attr("class", "brush")
.call(brush)
.call(brush.move, defaultSelection);
slider.selectAll(".resize").remove();

update(groupeByYear.get(recentYear));
function brushMove(event) {
const s = event.selection;
if (s) {
let yearIndex = Math.round(xScaleBrush.invert((s[0] + s[1]) / 2));
mutable feedback = yearIndex;
yearIndex =
yearIndex > years.length - 1
? years.length - 1
: yearIndex < 0
? 0
: yearIndex;
update(groupeByYear.get(years[yearIndex]));
}
}
function update(newData) {
xScale.domain(d3.extent(newData, d => d.gdpPercap));
yScale.domain(d3.extent(newData, d => d.lifeExp));
radScale.domain([0, d3.max(newData, d => d.pop)]);
svg.select(".x-axis").call(d3.axisBottom().scale(xScale));
svg.select(".y-axis").call(d3.axisLeft().scale(yScale));

bubbles
.selectAll("circle")
.data(newData)
.join("circle")
.attr(
'transform',
d => `translate(${xScale(d.gdpPercap)},${yScale(d.lifeExp)})`
)
.attr("r", d => radScale(d.pop))
.style("fill", d => colorScale(d.continent))
.style("opacity", 0.5)
.append("title")
.text(d => Object.entries(d).join("\n"));
}
return svgNode;
}
Insert cell
Insert cell
slide.img`${await FileAttachment("image.png").url()}`
Insert cell
md`Uses a D3 based tool: https://observablehq.com/@walterra/vikings-timeline`
Insert cell
gapminderData
Insert cell
md`### Getting started with Canvas Rendering`
Insert cell
slide`Canvas API
- provides a means for drawing *raster* graphics via JavaScript and the HTML *canvas* element.
- *context2d* for 2D graphics
- *webgl* for 3D GPU accelerated graphics
`
Insert cell
slide`SVG vs Canvas
- SVG for vector drawing. Canvas for raster drawing.
- SVG gives better performance with smaller surface number of elements. Canvas for larger number.
- SVG works on a retained mode (persists in an in-memory model), the canvas works on an “immediate mode”.
`
Insert cell
Insert cell
slide`context2d
- provides the 2D rendering context for the drawing surface of a *canvas* element.
- used for drawing shapes, text, images, ....
`
Insert cell
slide.js`
const context = DOM.context2d(width, 100);
... Draw using context2D api calls
return context.canvas
`
Insert cell
Insert cell
{
const context = DOM.context2d(width, 100);
context.canvas.style.background = "#999";
context.fillStyle = d3.schemeCategory10[0];
context.fillRect(25, 25, width - 50, 50);
context.strokeStyle = "black";
context.strokeRect(25, 25, width - 50, 50);
return context.canvas;
}
Insert cell
slide.js`Example: Rectangle (Contd...)
...
context.beginPath();
context.fillStyle = d3.schemeCategory10[0];
context.rect(25, 25, width - 50, 50);
context.fill()
context.strokeStyle = "black"
context.stroke()
...
`
Insert cell
Insert cell
{
const context = DOM.context2d(width, 100);
context.canvas.style.background = "#999";
context.beginPath(); // path commands must begin with beginPath

context.fillStyle = "red";
context.rect(10, 10, 10, 10); // blue
context.fillText("Hello world", 10, 10);
context.fill();

context.fillStyle = "green";
context.rect(20, 20, 10, 10); // blue
context.fillText("Hello world", 20, 20);
context.fill();

context.fillStyle = "blue"; // this is the last fillStyle, so it "wins"
context.rect(30, 30, 10, 10); // blue
context.fillText("Hello world", 30, 30);
context.fill();

// only 1 fillStyle is allowed per beginPath, so the last blue style fills all

context.fill();
return context.canvas;
}
Insert cell
{
const context = DOM.context2d(width, 100);
context.canvas.style.background = "#999";

context.fillStyle = "red";
context.fillRect(10, 10, 10, 10); // blue

context.fillStyle = "green";
context.fillRect(20, 20, 10, 10); // blue

context.fillStyle = "blue"; // this is the last fillStyle, so it "wins"
context.fillRect(30, 30, 10, 10); // blue

return context.canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
slide.js`Example: Text
...
context.font = '50px serif';
context.fillText('Hello world', 50, 90);
// context.strokeText('Hello world', 50, 90);
...
`
Insert cell
{
const context = DOM.context2d(width, 160);
context.canvas.style.background = "#999";
context.beginPath();
context.fillStyle = d3.schemeCategory10[0];
context.font = '50px serif';
context.fillText('Hello world', 50, 90);
return context.canvas;
}
Insert cell
{
const context = DOM.context2d(width, 160);
context.canvas.style.background = "#999";
context.beginPath();
context.font = '50px serif';
context.strokeStyle = d3.schemeCategory10[0];
context.strokeText('Hello world', 50, 90);
return context.canvas;
}
Insert cell
Insert cell
{
const context = DOM.context2d(width, 100);
context.canvas.style.background = "#999";
context.strokeStyle = "black";
[50, 200, 350, 500, 650, 800].forEach((x, i) => {
context.save();
context.beginPath();
context.translate(x, 25);
context.fillStyle = d3.schemeCategory10[i];
context.rect(0, 0, 50, 50);
//context.fill();
context.stroke();
context.restore();
});
return context.canvas;
}
Insert cell
Insert cell
Insert cell
{
const context = DOM.context2d(width, height);
context.clearRect(0, 0, width, height);
context.canvas.style.background = "#AAA";
context.translate(margin.left, margin.right);
context.strokeStyle = "black";
context.fillStyle = d3.schemeCategory10[0];
pointData.forEach(d => {
context.beginPath();
context.arc(d.x, d.y, 5, 0, Math.PI * 2, true); // ctx.arc(x, y, radius, startAngle, endAngle [, anticlockwise]);
context.fill();
context.stroke();
});
const hullPoints = d3.polygonHull(pointData.map(d => [d.x, d.y]));
const lineGenerator = d3
.line()
.curve(d3["curve" + curveType + "Closed"])
.x(function(d) {
return d[0];
})
.y(function(d) {
return d[1];
})
.context(context);

context.beginPath();
context.setLineDash([10, 10]);
lineGenerator(hullPoints);
//context.closePath();
context.stroke();

return context.canvas;
}
Insert cell
{
const context = DOM.context2d(width, height);
context.fillRect(0, 0, width, height);
context.fillStyle = "#000000";

const xScale = d3
.scalePoint()
.domain(keyCountries)
.range([0, W])
.padding(0.2);
const radScale = d3
.scaleSqrt()
.domain([0, d3.max(keyObjects, d => d.pop)])
.range([2, 60])
.nice();
const colorScale = d3
.scaleOrdinal()
.domain(continents)
.range(d3.schemeCategory10);
const bubbles = d3
.select(context.canvas)
.append("xyz")
.selectAll("circle")
.data(keyObjects)
.join("circle")
.attr('cx', d => xScale(d.country))
.attr('cy', H / 2)
.attr("r", d => radScale(d.pop))
.style("fill", d => colorScale(d.continent))
.style("opacity", 0.5);

context.save();
context.clearRect(0, 0, width, height);
context.translate(margin.left, margin.right);
bubbles.each((d, i, l) => {
const node = d3.select(l[i]);
/*
context.fillStyle = node.style('fill');
context.fillRect(
node.attr('cx') - node.attr('r'),
node.attr('cy') - node.attr('r'),
node.attr('r') * 2,
node.attr('r') * 2
);
*/
const cx = node.attr('cx'),
cy = node.attr('cy'),
r = node.attr('r');
context.beginPath();
//context.moveTo(cx + r, cy + r);
context.arc(cx, cy, r, 0, 2 * Math.PI);
context.fillStyle = node.style('fill');
context.fill();
});
context.restore();
return context.canvas;
}
Insert cell
md`## Data`
Insert cell
gapminderData = d3.csv(
"https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv",
d3.autoType
)
Insert cell
margin = ({ top: 20, right: 30, bottom: 30, left: 40 })
Insert cell
height = 500
Insert cell
H = height - margin.top - margin.bottom
Insert cell
W = width - margin.left - margin.bottom
Insert cell
md`## External Libraries, Imports`
Insert cell
import { generateRandomPointData } with { W, H } from "@spattana/week-5"
Insert cell
import { getSVG } with { width, height, margin } from "@spattana/week-5"
Insert cell
import { Table } from "@observablehq/inputs"
Insert cell
import { radio, select, slider } from "@jashkenas/inputs"
Insert cell
import { columns } from "@bcardiff/observable-columns"
Insert cell
Insert cell
import { slide, slide_style } from "@mbostock/slide"
Insert cell
d3 = require("d3@6")
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