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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more