Published
Edited
Oct 18, 2021
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
airQualityCalendar = () => {
const weekday = 'sunday'
const cellSize = 13
const height = 150
const xOffset = -300 // Shifts calendar labels

const mobileBreakPoint = 500
const formatMonth = d3.utcFormat("%b.")
const formatDate = d3.utcFormat("%x")
const formatDay = i => "SMTWTFS"[i]
const formatValue = d3.format("+.2%")
const timeWeek = d3.utcSunday
const countDay = weekday === "sunday" ? i => i : i => (i + 6) % 7

const fill = d => airQualityColor(+d.aqi_category + 1)

// const years = [
// [2021, data.filter(d => d.sitename === selSite)]
// ]
const stations = monitoringSites
// .slice(0, width > 400 ? monitoringSites.length : 5) // temporary fix for mobile clipping isssue
.map(station => {
return [station.city, data.filter(d => d.sitename === station.site)]
})

function pathMonth(t) {
const n = 7
const d = Math.max(0, Math.min(n, countDay(t.getUTCDay())));
const w = d3.utcSunday.count(d3.utcYear(t), t);
return `${d === 0 ? `M${w * cellSize},0`
: d === n ? `M${(w + 1) * cellSize},0`
: `M${(w + 1) * cellSize},0V${d * cellSize}H${w * cellSize}`}V${n * cellSize}`;
}

const graphicHeight = (width > mobileBreakPoint) ? height * stations.length / 2 + 40 : height * stations.length+ 40
const svg = d3.create("svg")
.attr('width', width)
.attr('height', graphicHeight)
.attr("viewBox", [0, 0, width, graphicHeight])
.attr("font-family", "Arial, sans-serif")
.attr("font-size", 10);

const station = svg.selectAll("g")
.data(stations)
.join("g")
.attr("transform", (d, i) => {
if (width > mobileBreakPoint) return `translate(${(i%2 === 0) ? 350 : 40},${height* Math.floor(i/2) + cellSize * 3})`
else return `translate(40,${height*i + cellSize * 3})`
})
const stationShifted = station.append('g')
.attr('transform',`translate(${xOffset},0)`)

// Station label
station.append("text")
.attr("x", -15)
.attr("y", -20)
.attr("font-size", '12px')
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.text(([key]) => key);

station.append("g")
.attr("text-anchor", "end")
.selectAll("text")
.data(weekday === "weekday" ? d3.range(1, 6) : d3.range(7))
.join("text")
.attr("x", -5)
.attr("y", i => (countDay(i) + 0.5) * cellSize)
.attr("dy", "0.31em")
.text(formatDay);
stationShifted.append("g")
.selectAll("rect")
.data(weekday === "weekday"
? ([, values]) => values.filter(d => ![0, 6].includes(d.date.getUTCDay()))
: ([, values]) => values)
.join("rect")
.attr("width", cellSize - 1)
.attr("height", cellSize - 1)
.attr("x", d => timeWeek.count(d3.utcYear(d.date), d.date) * cellSize + 0.5)
.attr("y", d => countDay(d.date.getUTCDay()) * cellSize + 0.5)
.attr("fill", fill)
.append("title")
.text(d => `${formatDate(d.date)}
${d.sitename}
AQI value: ${d.aqi_value}
AQI category: ${d.aqi_category}`);

const month = stationShifted.append("g")
.selectAll("g")
.data(([, values]) => d3.utcMonths(d3.utcMonth(values[0].date), values[values.length - 1].date))
.join("g");

month.filter((d, i) => i)
.append("path")
.attr("fill", "none")
.attr("stroke", "#fff")
.attr("stroke-width", 3)
.attr("d", pathMonth);

month.append("text")
.attr("x", d => timeWeek.count(d3.utcYear(d), timeWeek.ceil(d)) * cellSize + 2)
.attr("y", -5)
.text(formatMonth);

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
style = `<style>

.fire-icon {
opacity: 0.8;
}
.fire-marker {
}
.fire-marker:hover .fire-icon{
opacity: 1;
stroke: #444;
}
.legend {
font-family: sans-serif;
}
.legend-row {
display: flex;
flex-wrap: wrap;
font-size: 0.9em;
position: relative;
}
.legend-item {
display: inline-block;
margin-right: 1em;
margin-bottom: 0.2em;
}


.svg-tooltip {
border: 1px solid #806f47;
background-color: white;
font-family: Arial, sans-serif;
font-size: 14px;
line-height: 1.2em;
padding: 0.2em 0.5em;
opacity: 0.8em;
max-width: 300px;
}
.note {
font-family: Arial, sans-serif;
font-size: 0.8em;
font-style: italic;
line-height: 1em;
margin-bottom: 0.5em;
margin-top: 0.2em;
}
.table-embed {
max-width: 100%;
overflow-x: scroll;
}
table {
width: 100%;
max-width: 800px;
border-spacing: 0;
font-family: "Arial", sans-serif;
font-size: 14px;
white-space: nowrap;
}
@media screen and (max-width: 400px) {
table {
font-size: 12px;
}
}
tr {
max-width: 100%;
}
th, td, tr {
border: none;
}
th {
text-align: left;
font-weight: normal;
font-style: italic;
}
thead th {
border: none;
border-bottom: solid 1px #ccc;
}
tbody tr td {
border: none;
}
tbody tr:not(:last-child) td {
border-bottom: solid 1px #eee;
}

.air-quality-marker:hover circle {
stroke-width: 2px;
}

.aq-item {
font-family: Arial, sans-serif;
display: inline-block;
padding: 0.3em 0.5em;
color: white;
margin-right: 0.2em;
margin-bottom: 0.2em;
font-size: 12px;
opacity: 0.9;
pointer-events: none;
}
.explanation-item {
margin-bottom: 1em;
}
</style>`
Insert cell
Insert cell
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