import { aq, op } from "@uwdata/arquero"
bucket_base = "https://d33173dkfxwa5x.cloudfront.net/"
bucket_www = bucket_base + "www/"
bucket_processed = bucket_base + "2-processed/"
FA = FileAttachmentHere’s how today compares…
mutable myLocation = null
mutable geolocateStatus = "default"
getLocation = {
yield null
yield navigator.geolocation.getCurrentPosition(
pos => {
mutable myLocation = {
lat: pos.coords.latitude,
lon: pos.coords.longitude
}
mutable geolocateStatus = "granted"
},
e => {
console.log("Geolocation error", e),
mutable geolocateStatus = "error"
},
{
enableHighAccuracy: false
})
}Is it hot in right now?
answers = [
{ message: "Hell no!", color: "white", fill1: "#5287d1", fill2: "#2166ac", size: 1 },
{ message: "No!", color: "white", fill1: "#a2cdeb", fill2: "#67a9cf", size: 3 },
{ message: "Nope", color: "#333333", fill1: "#d1e5f0", fill2: "#d1e5f0", size: 4 },
{ message: "Not really", color: "#333333", fill1: "#f7f7f7", fill2: "#ebebeb", size: 4 },
{ message: "Yup", color: "#333333", fill1: "#fddcca", fill2: "#f7cfb7", size: 4 },
{ message: "Yeah!", color: "white", fill1: "#de7b49", fill2: "#fc946a", size: 3 },
{ message: "Hell yeah!", color: "white", fill1: "#d1231d", fill2: "#b2182b", size: 1 }
]
// lookup by message, for spots that just want "the answer" (eg. statsPlace.isit_answer)
answersByMessage = new Map(answers.map(d => [d.message, d]))
answerColors = answers.flatMap(d => Array.from({ length: d.size }, () => d.fill1))
html`
<div class="answer-icon-and-label">
<span class="answer-icon" style="background-color: ${answersByMessage.get(statsPlace.isit_answer).fill2};">
</span>
<span>${statsPlace.isit_answer}</span>
</div>`md`${statsPlace.isit_comment}`datefns = import("https://cdn.skypack.dev/date-fns@3.3.1?min")
tmaxWhen = new Date(Date.parse(statsPlace.isit_maximum_dt || null))
tminWhen = new Date(Date.parse(statsPlace.isit_minimum_dt || null))
tmaxWhenFriendly = isFinite(tmaxWhen) ?
datefns.formatRelative(tmaxWhen, new Date()) : ""
tminWhenFriendly = isFinite(tminWhen) ?
datefns.formatRelative(tminWhen, new Date()) : ""
percentRating = statsPlace.isit_average.toFixed(1) + "%"
md`${statsPlace.isit_minimum} °C `md`${tminWhenFriendly}`
md`${statsPlace.isit_maximum} °C `md`${tmaxWhenFriendly}`html`
<div class="answer-icon-and-label">
<span class="answer-icon" style="background-color: ${answersByMessage.get(statsPlace.isit_answer).fill2};"></span>
<span>${statsPlace.isit_current.toFixed(1)} °C</span>
</div>`md`Average so far<br>hotter than **${percentRating}** of days for this time of year.`d3regression = require("d3-regression@1.3.10")
hist_path = `${bucket_processed}historical_${stationId}.txt`
historical = FA(hist_path).csv({typed: true})
plotCaption = "© isithotrightnow.com"
// same domain/range Plot's own colour scale below will build, so breakpoints
// line up exactly with the dots' colours. answerColors is 20 equal-sized
// (5%-wide) buckets, so the first/last breakpoints ARE the 5th/95th percentiles.
tavgQuantileScale = d3.scaleQuantile()
.domain(historical.map(d => d.Tavg))
.range(answerColors)
tavgBreaks = tavgQuantileScale.quantiles()
tavg05 = tavgBreaks[0]
tavg95 = tavgBreaks[tavgBreaks.length - 1]
// date formatting
today = ({
Date: new Date(Math.max(
new Date(statsPlace.isit_maximum_dt),
new Date(statsPlace.isit_minimum_dt))),
Tavg: statsPlace.isit_current
})
dayMonthOptions = ({
month: 'long',
day: 'numeric'
})
dayMonthLabel = today.Date.toLocaleDateString("en-AU", dayMonthOptions)
// fit against the raw timestamp; .a (the slope) is in °C/ms, used below for
// the trend label. Map the two fitted endpoints back to Date objects so
// Plot.line reads them on the same time x-scale as the dots
tavgFit = d3regression.regressionLinear()
.x(d => d.Date.getTime())
.y(d => d.Tavg)
(historical)
tavgTrend = tavgFit.map(([x, y]) => ({ Date: new Date(x), Tavg: y }))
tavgTrendPerCentury = tavgFit.a * 1000 * 60 * 60 * 24 * 365.25 * 100
leftmostDate = d3.min(historical, d => d.Date)
Plot.plot({
marks: [
// historical data
Plot.dot(historical, {
x: "Date",
y: "Tavg",
fill: "Tavg",
// labels live on channels (tip's `format` only takes formatters), so the
// default x/y/fill entries are suppressed in favour of these two
channels: {
date: { value: "Date", label: "" },
temperature: { value: "Tavg", label: "Daily temperature" }
},
tip: {
format: {
date: d => d3.utcFormat("%B %-d %Y")(d),
temperature: d => `${d.toFixed(1)}°C`,
x: false,
y: false,
fill: false
}
},
r: 2
}),
// extreme percentiles
Plot.ruleY([tavg05, tavg95], {
stroke: "#333333",
strokeDasharray: "4,2"
}),
Plot.text([
{ y: tavg05, text: `5th percentile: ${tavg05.toFixed(1)}°C` },
{ y: tavg95, text: `95th percentile: ${tavg95.toFixed(1)}°C` }
], {
x: leftmostDate,
y: "y",
text: "text",
textAnchor: "start",
dy: -15,
dx: 10,
fill: "#3333333",
stroke: "#eeeeee",
strokeWidth: 5,
}),
// trend
Plot.line(tavgTrend, {
x: "Date",
y: "Tavg",
stroke: "black",
strokeWidth: 2
}),
Plot.text(
[
{ text: `Trend: ${tavgTrendPerCentury >= 0 ? "+" : ""}${tavgTrendPerCentury.toFixed(1)}°C/century` }
], {
x: tavgTrend[0].Date,
y: tavgTrend[0].Tavg,
text: "text",
textAnchor: "start",
dy: -21,
dx: 10,
fill: "#3333333",
stroke: "#eeeeee",
strokeWidth: 5,
}),
// today
Plot.dot([today], {
x: "Date",
y: "Tavg",
fill: tavgQuantileScale(today.Tavg),
stroke: "#333333",
r: 6,
}),
Plot.text([today], {
x: "Date",
y: "Tavg",
text: () => "TODAY",
fill: "#333333",
stroke: "#eeeeee",
strokeWidth: 5,
dy: -20
}),
Plot.text([today], {
x: "Date",
y: "Tavg",
text: d => `${d.Tavg.toFixed(1)}°C`,
fill: "#333333",
stroke: "#eeeeee",
strokeWidth: 3,
dy: 20
}),
// plot frame
Plot.frame({
stroke: "#333333",
strokeWidth: 3,
anchor: "bottom"
}),
Plot.frame({
stroke: "#333333",
strokeWidth: 3,
anchor: "left"
})
],
color: {
type: "quantile",
range: answerColors
},
style: {
fontFamily: "Roboto Condensed",
fontWeight: "bold",
fontSize: 14
},
title: `${statsPlace.isit_label} daily average temperatures for the two weeks around ${dayMonthLabel}`,
caption: plotCaption
})This is the distribution of temperatures typical for this time of year, which we’ve defined as a seven day window either side of today. Just like a bell curve, the plot is higher at more common temperatures and lower at rare temperatures.
… and the rest of the year.
We’ve compared every day this year at to similar times of year in the past. Red days were hot compared to records; blue days were cold.
Here’s how the rest of Australia looks today so far.
nowHour = (new Date()).getUTCHours()
nowHour >= 10 && nowHour <= 22 ?
md`Some places may still be cooling off as the night continues.` :
md`Some places may still be heating up as the day continues.`currentStats = statsAll.filter(d => (!d.max_stale) && (!d.min_stale))
staleStats = statsAll.filter(d => d.max_stale || d.min_stale)// breaking out the pointer layer for readability
// (and in case we decide to customise it later)
tipTemplate = Plot.pointer({
x: "lon",
y: "lat",
channels: {
place: {
label: "",
value: d => d.isit_label,
},
label: {
label: "",
value: d => d.isit_comment
},
separator: {
label: "",
value: d => ""
},
isit_maximum: {
label: "Max:",
value: d => d.isit_maximum + "°C"
},
isit_minimum: {
label: "Min:",
value: d => d.isit_minimum + "°C"
}
},
fontSize: 20,
fontFamily: "Roboto Condensed",
format: {
x: null,
y: null
},
lineWidth: 20,
lineHeight: 1.25,
pointerSize: 0,
fillOpacity: 0.8,
strokeOpacity: 0
})See websites from our international colleagues!