Published
Edited
Sep 27, 2020
1 fork
Importers
Comments locked
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require("d3@5")
Insert cell
Insert cell
monarchs = d3.csvParse(await FileAttachment("butterflies.csv").text())
Insert cell
Insert cell
dataCollectorByYear = {
//create two empty parallel arrays for keeping track of years and holding data
let seenYears = [];
let dataCollectorByYear = [];

//loop through all butterflies
monarchs.forEach(function(monarch) {
//record year and state for convenience
let year = monarch.year;
let state = monarch.stateProvince;

//check if we have seen this year already in our parallel array
if (seenYears.includes(year)) {
//we have seen this year already, so we can ask our parallel array for the index of this specific year
let yearPlace = seenYears.indexOf(year);

//check if we have this state in this year already
if (state in dataCollectorByYear[yearPlace].sightings) {
//we have this state already, so save this monarch in the right place
dataCollectorByYear[yearPlace].sightings[state].push(monarch);
} else {
//we have not seen this state, so make a new property and save this monarch in it.
dataCollectorByYear[yearPlace].sightings[state] = [monarch];
}
} else {
//we have not seen this year yet, so make a new datapoint.
// the brackets around state are confusing - but we need to ensure that JS treats it as the variable state
dataCollectorByYear.push({
name: year,
sightings: { [state]: [monarch] }
});
//record this new year in our parallel array.
seenYears.push(year);
}
});
//show us the result!
return dataCollectorByYear;
}
Insert cell
Insert cell
dataCollectorByYearByState = {
//create a list of objects for each year, with an array of states, with properties for each state, from our single object per year
dataCollectorByYear.forEach(function(eachYear) {
//collector for array of state objects
let collector = [];
//iterate through the properties, which are all the states...
for (let prop in eachYear.sightings) {
//record each state property as its own object
collector.push({
name: prop,
butterflies: eachYear.sightings[prop]
});
}
//assign the new structure to the same year objects
eachYear.states = collector;
});

return dataCollectorByYear;
}
Insert cell
Insert cell
fakeData = {
let fakeData = [];

let count = 100;

for (let i = 0; i < count; i++) {
fakeData.push({
x: Math.random() * 100,
y: Math.random() * 100,
z: Math.random() * 100
});
}

return fakeData;
}
Insert cell
Insert cell
random_bouncing = {
//svg variables
let width = 800;
let height = 400;
let margin = 50;

//create SVG artboard
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);

//create svg background color
let bg = svg
.append('rect')
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr('fill', '#002');

//create fake dataset
let fakeData = [];
let count = 100;

for (let i = 0; i < count; i++) {
fakeData.push({
x: Math.random() * 100,
y: Math.random() * 100,
z: Math.random() * 100
});
}

//scales for converting data to pixels
let xScale = d3
.scaleLinear()
.domain(d3.extent(fakeData, d => d.x))
.range([margin, width - margin]);

let yScale = d3
.scaleLinear()
.domain(d3.extent(fakeData, d => d.y))
.range([height - margin, margin]);

//z will be color in this example
let zScale = d3
.scaleLinear()
.domain(d3.extent(fakeData, d => d.z))
.range([0, 1]);

//standard data binding with our dataset and SVG circles
svg
.selectAll('.fakeDots')
.data(fakeData)
.enter()
.append("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr('fill', d => d3.interpolateInferno(zScale(d.z)))
.attr('r', 3)
.attr('opacity', 1)
.attr('class', 'fakeDots');

//----NEW STUFF HERE-----------------------
//create svg button and attach event listener
let button = svg
.append('rect')
.attr('fill', 'orange')
.attr('width', 50)
.attr('height', 20)
.attr('x', width - 60)
.attr('y', 10)
//let this button listen for a click
.on('click', function() {
//when the button is clicked...
//generate new random data
let fakeData = [];
for (let i = 0; i < 100; i++) {
fakeData.push({
x: Math.random() * 100,
y: Math.random() * 100,
z: Math.random() * 100
});
}

//look for existing dots!
//we have already made them, so we do not need .enter()
d3.selectAll('.fakeDots')
.data(fakeData)
//standard lines for animating
.transition()
.duration(1000)
.delay((d, i) => i * 500)
.ease(d3.easeElasticOut)
//use the new dataset to set the new positions and colors
.attr('fill', d => d3.interpolateViridis(zScale(d.z)))
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
//sequence a few more animations
.transition()
.duration(1000)
.attr('r', 6)
.transition()
.duration(500)
.attr('r', 3);
});

//label for the button
let label = svg
.append('text')
.attr('fill', 'white')
.attr('x', width - 58)
.attr('y', 22)
.text("Shuffle!")
.style('font-family', 'courier')
.style('font-size', 10)
//so the text doesn't get in the way of our button seeing the mouse click...
.attr('pointer-events', "none");

return svg.node();
}
Insert cell
Insert cell
Insert cell
events = {
//svg variables
let width = 800;
let height = 400;
let margin = 50;

//create SVG artboard
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);

//create svg background color
let bg = svg
.append('rect')
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr('fill', '#002');

//clickable circle
svg
.append('text')
.attr('x', 140)
.attr('y', 100)
.attr('fill', 'white')
.text('click me!');

svg
.append('circle')
.attr('cx', 100)
.attr('cy', 100)
.attr('r', 25)
.attr('fill', '#0ff')
.on('click', function() {
d3.select(this).attr('fill', '#f00');
});

//hoverable circle
svg
.append('text')
.attr('x', 140)
.attr('y', 200)
.attr('fill', 'white')
.text('hover on me!');

svg
.append('circle')
.attr('cx', 100)
.attr('cy', 200)
.attr('r', 25)
.attr('fill', '#0ff')
.on('mouseover', function() {
d3.select(this).attr('fill', '#ff0');
})
.on('mouseout', function() {
d3.select(this).attr('fill', '#0ff');
});

//double-clickable circle
let currentColor = "cyan";

svg
.append('text')
.attr('x', 140)
.attr('y', 300)
.attr('fill', 'white')
.text('double-click me many times!');

svg
.append('circle')
.attr('cx', 100)
.attr('cy', 300)
.attr('r', 25)
.attr('fill', '#0ff')
.on('dblclick', function() {
if (currentColor == "cyan") {
d3.select(this).attr('fill', '#0f0');
currentColor = "green";
} else {
d3.select(this).attr('fill', '#0ff');
currentColor = "cyan";
}
});

return svg.node();
}
Insert cell
Insert cell
smallDetails = {
let usPopulation = [
{ year: 1960, usPop: 180671000 },
{ year: 1970, usPop: 205052000 },
{ year: 1980, usPop: 227225000 },
{ year: 1990, usPop: 249623000 },
{ year: 2000, usPop: 282162411 },
{ year: 2010, usPop: 309321666 },
{ year: 2020, usPop: 326687501 }
];

//svg variables
let width = 800;
let height = 400;
let margin = 75;

//create SVG artboard
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);

//create svg background color
let bg = svg
.append('rect')
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr('fill', 'blue');

let barWidth = (height - margin * 2) / usPopulation.length;
let popExtents = d3.extent(usPopulation, d => d.usPop);
let barLengthExtents = [100, width - margin * 2];

let popScale = d3
.scaleLinear()
.domain(popExtents)
.range(barLengthExtents);

let bars = svg
.selectAll('.yearBars')
.data(usPopulation)
.enter()
.append('rect')
.attr('x', margin)
.attr('y', (d, i) => i * barWidth + margin)
.attr('width', d => popScale(d.usPop))
.attr('height', barWidth)
.attr('fill', 'red')
.on('mouseover', function(d) {
d3.select(this)
.attr('stroke', 'white')
.attr('stroke-width', '2');
})
.on('mouseout', function(d) {
d3.select(this)
.attr('stroke', 'none')
.attr('stroke-width', '0');
})
.on('click', function(d, i) {
d3.select('#infoText').text(
"In the year " +
d.year +
", item " +
i +
" of dataset, the US had " +
//toLocaleString() takes a long number and adds commas/periods to separate thousands
d.usPop.toLocaleString() +
" people."
);
});

let labels = svg
.selectAll('.yearLabels')
.data(usPopulation)
.enter()
.append('text')
.attr('x', margin - 5)
.attr('y', (d, i) => i * barWidth + margin + barWidth / 1.5)
.attr('fill', 'white')
.style('text-anchor', 'end')
.text(d => d.year)
.attr('font-family', 'courier')
.on('mouseover', function(d, i) {
d3.select(this)
.attr('stroke', 'white')
.attr('stroke-width', '1');
})
.on('mouseout', function(d) {
d3.select(this)
.attr('stroke', 'none')
.attr('stroke-width', '0');
});

let infoText = svg
.append('text')
.attr('x', width - margin / 2)
.attr('y', margin / 2)
.attr('fill', 'white')
.style('text-anchor', 'end')
.attr('font-size', '12')
.attr('font-family', 'courier')
.attr('id', 'infoText')
.text("Click on a red bar!");

return svg.node();
}
Insert cell
Insert cell
{
//svg variables
let width = 800;
let height = 400;
let margin = 75;

//create SVG artboard
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);

//create svg background color
let bg = svg
.append('rect')
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr('fill', '002');

let circle = svg
.append('circle')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('r', 50)
.attr('stroke', 'white')
//take the line below out to see what happens if a property isn't set before animation
.attr('stroke-width', '0')
.attr('fill', 'cyan')
.on('mouseover', function(d) {
d3.select(this)
.transition()
.duration(1000)
.attr('stroke-width', '10')
.attr('fill', 'magenta');
})
.on('mouseout', function(d) {
d3.select(this)
.transition()
.delay(500)
.duration(1500)
.attr('stroke-width', '0')
.attr('fill', 'cyan');
});

return svg.node();
}
Insert cell
Insert cell
{
//svg variables
let width = 800;
let height = 400;
let margin = 50;

//create SVG artboard
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);

//create svg background color
let bg = svg
.append('rect')
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr('fill', '002');

let colorData = [
{ startColor: "magenta", endColor: "cyan", travelTime: 2000 },
{ startColor: "cyan", endColor: "yellow", travelTime: 1500 },
{ startColor: "yellow", endColor: "magenta", travelTime: 1000 }
];

let radius = 40;

let circle = svg
.selectAll('.colorCircles')
.data(colorData)
.enter()
.append('circle')
.attr('cx', margin)
.attr('cy', (d, i) => i * 100 + radius + margin)
.attr('r', 40)
.attr('fill', d => d.startColor)
.attr('class', 'colorCircles');

//create svg button and attach event listener
let button = svg
.append('rect')
.attr('fill', 'orange')
.attr('width', 50)
.attr('height', 20)
.attr('x', width - 60)
.attr('y', 10)
.on('click', function() {
d3.selectAll('.colorCircles')
.transition()
.duration(d => d.travelTime)
.delay((d, i) => i * 500)
.ease(d3.easeBounce)
.attr('cx', width - margin)
.attr('fill', d => d.endColor);
});

//label for the button
let label = svg
.append('text')
.attr('fill', 'white')
.attr('x', width - 58)
.attr('y', 22)
.text("Animate!")
.style('font-family', 'courier')
.style('font-size', 10)
//so the text doesn't get in the way of our button seeing the mouse click...
.attr('pointer-events', "none");

return svg.node();
}
Insert cell
Insert cell
{
//svg variables
let width = 800;
let height = 400;
let margin = 75;

//create SVG artboard
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);

//create svg background color
let bg = svg
.append('rect')
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr('fill', '002');

let circle = svg
.append('circle')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('r', 50)
.attr('stroke', 'white')
//take the line below out to see what happens if a property isn't set before animation
.attr('stroke-width', '0')
.attr('fill', 'cyan')
.on('mouseover', function(d) {
d3.select(this)
//first transition - change color
.transition()
.duration(750)
.attr('fill', 'magenta')
//second transtition - grow stroke and change color
.transition()
.duration(750)
.attr('stroke-width', '10')
.attr('fill', 'yellow')
//third transition - grow radius and color and remove stroke
.transition()
.duration(750)
.attr('r', '75')
.attr('fill', 'orange')
.attr('stroke-width', '0');
})
.on('mouseout', function(d) {
d3.select(this)
.transition()
.delay(200)
.duration(750)
.attr('r', 50)
.attr('stroke-width', '0')
.attr('fill', 'cyan');
});

return svg.node();
}
Insert cell
Insert cell
usAnimated = {
let usPopulation = [
{ year: 1960, usPop: 180671000 },
{ year: 1970, usPop: 205052000 },
{ year: 1980, usPop: 227225000 },
{ year: 1990, usPop: 249623000 },
{ year: 2000, usPop: 282162411 },
{ year: 2010, usPop: 309321666 },
{ year: 2020, usPop: 326687501 }
];

//svg variables
let width = 800;
let height = 400;
let margin = 75;

//create SVG artboard
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);

//create svg background color
let bg = svg
.append('rect')
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr('fill', 'blue');

let barWidth = (height - margin * 2) / usPopulation.length;
let popExtents = d3.extent(usPopulation, d => d.usPop);
let barLengthExtents = [100, width - margin * 2];

let popScale = d3
.scaleLinear()
.domain(popExtents)
.range(barLengthExtents);

let bars = svg
.selectAll('.yearBars')
.data(usPopulation)
.enter()
.append('rect')
.attr('x', margin)
.attr('y', (d, i) => i * barWidth + margin)
.attr('width', d => popScale(d.usPop))
.attr('height', barWidth)
.attr('fill', 'red')
.attr('stroke', 'white')
.attr('stroke-width', '1')
.on('mouseover', function(d) {
d3.select(this)
.raise() //this line of code brings the hovered bar to the top of the SVG layers to prevent overlapping
.transition()
.duration(750)
.attr('stroke-width', '10');
})
.on('mouseout', function(d) {
d3.select(this)
.transition()
.duration(750)
.attr('stroke-width', '1');
})
.on('click', function(d, i) {
d3.select('#infoBlock')
.transition()
.duration(500)
.attr('opacity', 0)
.transition()
.duration(1)
.text(
"In the year " +
d.year +
", item " +
i +
" of dataset, the US had " +
//toLocaleString() takes a long number and adds commas/periods to separate thousands
d.usPop.toLocaleString() +
" people."
)
.transition()
.duration(500)
.attr('opacity', 1);
});

let labels = svg
.selectAll('.yearLabels')
.data(usPopulation)
.enter()
.append('text')
.attr('x', margin - 5)
.attr('y', (d, i) => i * barWidth + margin + barWidth / 1.5)
.attr('fill', 'white')
.style('text-anchor', 'end')
.text(d => d.year)
.attr('font-family', 'courier');

let infoText = svg
.append('text')
.attr('x', width - margin / 2)
.attr('y', margin / 2)
.attr('fill', 'white')
.style('text-anchor', 'end')
.attr('font-size', '12')
.attr('font-family', 'courier')
.attr('opacity', 1)
.attr('id', 'infoBlock')
.text("Click on a red bar!");

return svg.node();
}
Insert cell
Insert cell
Insert cell
{
let width = 800;
let height = 400;
let simpleData = {
x: Math.random() * width,
y: Math.random() * height,
z: Math.random()
};

//create SVG artboard, background color, buttons
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height);

let bg = svg
.append('rect')
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr('fill', '#0ab');

let button = svg.append('rect').attr('fill', 'orange');

let target = [{ x: 200, y: 200, z: .25, rad: 10, health: 10 }];

svg
.selectAll('.target')
.data(target)
.enter()
.append('circle')
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.rad)
.attr("fill", d => d3.interpolatePlasma(d.z))
.attr("stroke-width", d => d.health)
.attr('stroke', 'white')
.attr('id', 'target')
.on('mouseover', function(d) {
target[0].x = Math.random() * width;
target[0].y = Math.random() * height;
target[0].z = Math.random();
target[0].rad = Math.random() * 20 + 10;

if (d.health > 0) {
d.health--;
}

if (d.health == 0) {
//show winning message
svg
.append('text')
.text('you win!')
.attr('x', width / 2)
.attr('y', height / 2)
.attr('font-family', 'impact')
.attr('fill', 'white');

//remove the circular target from game
d3.select('#target').remove();
}

d3.select(this)
.data(target)
.transition()
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.rad)
.attr("fill", d => d3.interpolatePlasma(d.z))
.attr("stroke-width", d => d.health);
});

return svg.node();
}
Insert cell
Insert cell
cereals = d3.csvParse(await FileAttachment("cereal.csv").text())
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