Public
Edited
Oct 16, 2024
2 forks
1 star
Insert cell
Insert cell
Insert cell
import { birthData } from "@zachpino/coding-challenge-2d-heatmap"
Insert cell
//remove 2/29 from count visualizations
birthDataWithoutFeb29 = birthData.filter(
(d) => d.month + " " + d.date !== "2 29"
)
Insert cell
Insert cell
//https://web.library.yale.edu/cataloging/months
monthLabels = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"June",
"July",
"Aug",
"Sept",
"Oct",
"Nov",
"Dec"
]
Insert cell
dayLabels = d3.range(1, 32)
Insert cell
Insert cell
// evenly divide by maximum numbers of months per year
rectHeight = (height - margin * 2) / 12
Insert cell
// evenly divide by maximum numbers of days per month
rectWidth = (width - margin * 2) / 31
Insert cell
Insert cell
//month -> y
monthScale = d3
.scaleLinear()
.domain([1, 13]) //because we're not starting at 0, we need to go to 13!
.range([margin, height - margin])
Insert cell
//day -> x
dayScale = d3
.scaleLinear()
.domain([1, 32]) //because we're not starting at 0, we need to go to 32!
.range([margin, width - margin])
Insert cell
//if we want to show total births by day
birthCountExtents = d3.extent(birthDataWithoutFeb29, (d) => d.births)
Insert cell
//if we want to show average births by day
birthAverageExtents = d3.extent(birthData, (d) => d.births / d.count)
Insert cell
//convert births to number between 0 and 1 for use with color scales
//heatmap below uses average to match original source visualization
birthParameterScale = d3
.scaleLinear()
.domain(dataMode == "Average" ? birthAverageExtents : birthCountExtents)
.range([0, 1])
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viz = {
//create SVG artboard
const svg = d3.create("svg").attr("width", width).attr("height", height);

//create svg background color
svg
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", bgColor);

//draw heatmap rectangles
svg
.selectAll(".rects")
.data(dataMode == "Average" ? birthData : birthDataWithoutFeb29)
.enter()
.append("rect")
.attr("width", rectWidth - internalMargin)
.attr("height", rectHeight - internalMargin)
.attr("x", (d) => dayScale(d.date) + internalMargin / 2)
.attr("y", (d) => monthScale(d.month) + internalMargin / 2)
.attr("fill", (d) =>
d3[colorScale](
birthParameterScale(
dataMode == "Average" ? d.births / d.count : d.births
)
)
)
.attr("class", "rects")
//to show interactivity associated with next week's homework!
.on("mouseover", (e, d) => showData(svg, d))
.on("mouseout", (e, d) => hideData(svg, d));

//month labels
svg
.selectAll(".monthLabels")
.data(monthLabels)
.enter()
.append("text")
.text((d) => d)
.attr("x", margin - labelOffset)
.attr("y", (d, i) => monthScale(i + 1) + 0.5 * rectHeight)
.attr("fill", "white")
.attr("alignment-baseline", "middle")
.attr("text-anchor", "end")
.style("text-transform", "uppercase")
.attr("font-family", font)
.attr("font-size", labelFontSize)
.attr("class", "monthLabels");

//day labels
svg
.selectAll(".dayLabels")
.data(dayLabels)
.enter()
.append("text")
.text((d) => d)
.attr("x", (d) => dayScale(d) + rectWidth / 2)
.attr("y", (d, i) => margin - labelOffset)
.attr("fill", "white")
.attr("font-family", font)
.attr("font-size", labelFontSize)
.attr("text-anchor", "middle")
.attr("class", "dayLabels");

//add title
svg
.append("text")
.text(
dataMode == "Average"
? "Average US Births by Day, 1995-2014"
: " Daily US Births, 1995-2014"
)
.attr("x", width - margin)
.attr("y", margin / 2)
.attr("fill", "white")
.attr("font-family", font)
.attr("text-anchor", "end");

//add text element to fill with info when users mouse over rectangles
svg
.append("text")
.text("")
.attr("x", width / 2)
.attr("y", height - margin / 2)
.attr("fill", "white")
.attr("font-family", font)
.attr("text-anchor", "middle")
.attr("id", "textHolder");

//add line to show approximate date of conception
svg
.append("line")
.attr("stroke", "white")
.attr("id", "conceptionLine")
.attr("stroke-width", 5)
.attr("stroke-linecap", "round")
.attr("pointer-events", "none");

//Legends
const legendParams = d3.range(legendBoxCount).map((d) => d / legendBoxCount);

svg
.selectAll(".legendBoxes")
.data(legendParams)
.enter()
.append("rect")
.attr("width", legendSize)
.attr("height", legendSize)
.attr("x", (d, i) => i * legendSize + margin)
.attr("y", (d) => margin / 2 - legendSize / 4)
.attr("fill", (d) => d3[colorScale](d))
.attr("class", "rects");

svg
.selectAll(".legendLabels")
.data(legendParams)
.enter()
.append("text")
.text((d) =>
birthParameterScale
.invert(d)
.toLocaleString(undefined, { maximumFractionDigits: 0 })
)
.attr("x", (d, i) => i * legendSize + margin + legendSize * 0.5)
.attr("y", (d) => margin / 2 - legendSize / 2)
.attr(
"transform",
(d, i) =>
`rotate(-45 ${i * legendSize + margin + legendSize * 0.5} ${
margin / 2 - legendSize / 2
})`
)
.attr("font-family", font)
.attr("font-size", labelFontSize)
.attr("fill", "white")
.attr("class", "rects");

return svg.node();
}
Insert cell
//show and change the text on the bottom on mouse over
showData = (svg, d) => {
d3.select("#textHolder")
.transition()
.attr("opacity", 1)
.text(
`On ${monthLabels[d.month - 1]} ${d.date} there were ${
dataMode == "Average"
? (d.births / d.count).toLocaleString(undefined, {
maximumFractionDigits: 2 //https://stackoverflow.com/questions/31581011/how-to-use-tolocalestring-and-tofixed2-in-javascript
}) + " Average Daily Births"
: d.births.toLocaleString() + " Total Births"
}`
);

d3.select("#conceptionLine")
.attr("x1", dayScale(d.date) + rectWidth / 2)
.attr("y1", monthScale(d.month) + rectHeight / 2)
.attr("x2", dayScale(d.date) + rectWidth / 2)
.attr(
"y2",
(d.month - 9 > 0
? monthScale(d.month - 9)
: monthScale(12 - Math.abs(d.month - 9))) +
rectHeight / 2
);
}
Insert cell
//hide the text on the bottom on mouse out
hideData = (svg, d) => {
d3.select("#textHolder").transition().attr("opacity", 0);
}
Insert cell
Insert cell
Insert cell
simplifiedForLoop = {
//THIS WILL ONLY WORK WITH "AVERAGE" MODE ABOVE

//create SVG artboard
const svg = d3.create("svg").attr("width", width).attr("height", height);

//create svg background color
svg
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", bgColor);

for (let i = 0; i < birthData.length; i++) {
//store each day in a reusable variable
const currentDay = birthData[i];

//make rectangles
svg
.append("rect")
.attr("width", rectWidth)
.attr("height", rectHeight)
.attr("x", dayScale(currentDay.date))
.attr("y", monthScale(currentDay.month))
.attr(
"fill",
d3.interpolateMagma(
birthParameterScale(currentDay.births / currentDay.count)
)
);
}
return svg.node();
}
Insert cell
Insert cell
simplifiedD3 = {
//THIS WILL ONLY WORK WITH "AVERAGE" MODE ABOVE
//create SVG artboard
const svg = d3.create("svg").attr("width", width).attr("height", height);

//create svg background color
svg
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", bgColor);

//draw heatmap rectangles
svg
.selectAll(".rects") //select everything that we are yet to create
.data(birthData) // compare with dataset
.enter() //for every element *in dataset* but not *in element selection*
.append("rect") //add a rectangle
.attr("width", rectWidth)
.attr("height", rectHeight)
.attr("x", (d) => dayScale(d.date))
.attr("y", (d) => monthScale(d.month))
.attr("fill", (d) =>
d3.interpolateMagma(birthParameterScale(d.births / d.count))
)

.attr("class", "rects");

return svg.node();
}
Insert cell
Insert cell
simplifiedForLoopWithoutAnyObservableGlobalVariables = {
//THIS WILL ONLY WORK WITH "AVERAGE" MODE ABOVE

//svg variables
const height = 600;
const margin = 75;
//width comes from Observable

//calculate sizes
const rectHeight = (height - margin * 2) / 12;
const rectWidth = (width - margin * 2) / 31;

//month -> y
const monthToYScale = d3
.scaleLinear()
.domain([1, 13]) //because we're not starting at 0, we need to go to 13!
.range([margin, height - margin]);

//day -> x
const dayToXScale = d3
.scaleLinear()
.domain([1, 32]) //because we're not starting at 0, we need to go to 32!
.range([margin, width - margin]);

//extract high and low birthday averages
const birthAverageMinMax = d3.extent(birthData, (d) => d.births / d.count);

//birthAverages -> 0-1 for color scale use
const birthAveragesToZeroToOne = d3
.scaleLinear()
.domain(birthAverageMinMax)
.range([0, 1]);

//create SVG artboard
const svg = d3.create("svg").attr("width", width).attr("height", height);

//create svg background color
svg
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", "#333344");

for (let i = 0; i < birthData.length; i++) {
//store each day in a reusable variable
const currentDay = birthData[i];

//make rectangles
svg
.append("rect")
.attr("width", rectWidth)
.attr("height", rectHeight)
.attr("x", dayToXScale(currentDay.date))
.attr("y", monthToYScale(currentDay.month))
.attr(
"fill",
d3.interpolateMagma(
birthAveragesToZeroToOne(currentDay.births / currentDay.count)
)
);
}
return svg.node();
}
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