Published
Edited
Feb 24, 2021
1 star
Insert cell
Insert cell
md`## Data`
Insert cell
earnings = await d3.csv(
"https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-02-23/earn.csv",
d3.autoType
)
Insert cell
employed = await d3.csv(
"https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-02-23/employed.csv",
d3.autoType
)
Insert cell
render_data_table(earnings)
Insert cell
// Calculate average earnings each year by race
race_year_data = T.tidy(
earnings,
T.groupBy(
["year", "race"],
[T.summarize({ avg_earning: T.mean('median_weekly_earn') })]
)
)
Insert cell
race_categories = d3
.groups(earnings, d => d.race, d => d.ethnic_origin)
.map(d => d[0])
Insert cell
md`## Scales, axes`
Insert cell
margin = ({ left: 40, right: 20, bottom: 20, top: 20 })
Insert cell
height = 400
Insert cell
chart_width = width - 220
Insert cell
x = d3
.scaleLinear()
.domain(d3.extent(race_year_data, d => +d.year))
.range([margin.left, chart_width - margin.right])
Insert cell
x_bar = d3
.scaleBand()
.range([margin.left, chart_width - margin.right])
.domain(d3.groups(race_year_data, d => d.year).map(d => d[0]))
.padding(.2)
Insert cell
x_bar.bandwidth()
Insert cell
y = d3
.scaleLinear()
.domain(d3.extent(race_year_data, d => +d.avg_earning))
.range([height - margin.bottom, margin.top])
Insert cell
line_fn = d3
.line()
.x(d => x(d.year))
.y(d => y(d.avg_earning))
Insert cell
// Keep track of the section so that updates only happen when the section changes
mutable currentSection = 0
Insert cell
color_scale = d3
.scaleOrdinal()
.domain(race_categories)
.range(d3.schemeCategory10)
Insert cell
chart = {
// Overall container
const container = d3.create("div").attr("class", "scroll-container");

// Container for the narrative section
const narrativeContainer = container
.append("div")
.attr("class", "narrative-container");

// Narrative section (holds the text on the left)
const narrative = narrativeContainer.append("div").attr("class", "narrative");

// Add text
narrative
.selectAll(".section")
.data(descriptions)
.join("div")
.attr("class", "section")
.append("h1")
.style("color", d => color_scale(d))
.text(d => d);

// Visualization container
const vis = container.append("div").attr("class", "vis-container");

// Add an svg, axes
const svg = vis
.append("svg")
.attr("width", width)
.attr("height", height);

const xAxis = svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("")));

const yAxis = svg
.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).tickFormat(d3.format("")));

// sample data
const sample_data = race_year_data.filter(d => d.race == "All Races");

// Draw the line
const line = svg
.selectAll("path.line")
.data([sample_data])
.join("path")
.attr("class", "line")
.attr("d", line_fn)
.attr("fill", "none")
.attr("stroke", "black");

const rects = svg
.selectAll("rect")
.data(sample_data)
.join("rect")
.attr("height", d => height - y(d.avg_earning) - margin.bottom)
.attr("y", d => y(d.avg_earning))
.attr("width", x_bar.bandwidth())
.attr("x", d => x_bar(d.year));

// Update function (just switches the colors)
const update = section => {
// If the section hasn't changed, don't do anything!
if (section === mutable currentSection) return;
mutable currentSection = section; // If this line of code is here, the cell re-renders

// Filter down the data!
const line_data = race_year_data.filter(
d => d.race == race_categories[section]
);

// Update the line!
svg
.selectAll("path.line")
.data([line_data])
.join("path")
.transition()
.attr("d", line_fn);

svg
.selectAll("rect")
.data(line_data)
.transition()
.delay(d => x(d.year))
.attr("fill", color_scale(race_categories[section]))
.attr("height", d => height - y(d.avg_earning) - margin.bottom)
.attr("y", d => y(d.avg_earning))
.attr("width", x_bar.bandwidth())
.attr("x", d => x_bar(d.year));
};

// Add the scrolling listener
narrativeContainer.on("scroll", () => {
// Compute section
const totalHeight = narrativeContainer.node().getBoundingClientRect().top;
const offset = narrative.node().getBoundingClientRect().y;
const section = Math.floor((totalHeight - offset) / sectionHeight);
update(section);
});

return container.node();
}
Insert cell
settings = md`## Settings`
Insert cell
// Descriptions for each section
descriptions = race_categories
Insert cell
fullHeight = 400
Insert cell
sectionHeight = 200
Insert cell
narrativeWidth = 200
Insert cell
Insert cell
T = require('@tidyjs/tidy/dist/umd/tidy.min.js')
Insert cell
chart_styles = html`<style>

.scroll-container {
width:${width}px;
height:${fullHeight}px;
}

.narrative-container {
height:${fullHeight}px;
overflow-y:scroll;
float:left;
}

.narrative {
width:${narrativeWidth}px;
}

.vis-container {
height:${fullHeight}px;
width:${width - narrativeWidth - 20}px;
float:right;
}

.section {
height: ${sectionHeight}px;
}
.section:last-of-type {
height:${fullHeight}px;
}
</style>`
Insert cell
d3 = require("d3")
Insert cell
import { render_data_table, table_styles } from "@uw-info474/utilities"
Insert cell
table_styles
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