Here’s how today compares…

import { aq, op } from "@uwdata/arquero"

bucket_base = "https://d33173dkfxwa5x.cloudfront.net/"
bucket_www = bucket_base + "www/"
bucket_processed = bucket_base + "2-processed/"

FA = FileAttachment
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
})