Source: diurnalPlot.js

// Luxon for timezone-aware datetime handling
import { DateTime } from "luxon";
// SunCalc for day-night shading
import SunCalc from "suncalc";
// Utility functions
import { pm25ToColor, pm25ToYMax, pm25_AQILines } from "./plot-utils.js";
import { requireLuxonDateTimeArray } from "./helpers.js";

/**
 * Returns a diurnalPlot chart configuration.
 * @param {Object} data The data required to create the chart.
 */
export function diurnalPlotConfig(data) {
  requireLuxonDateTimeArray(data.datetime, "datetime");

  const { datetime, nowcast, locationName, timezone, title, longitude, latitude, hour_average } = data;

  // ----- Data preparation --------------------------------

  const localHours = datetime.map(dt => dt.setZone(timezone).hour);
  const lastHour = localHours[localHours.length - 1];
  const today_end = localHours.length;
  const today_start = today_end - 1 - lastHour;
  const yesterday_end = today_start;
  const yesterday_start = today_start - 24;
  const yesterday = nowcast.slice(yesterday_start, yesterday_end);
  const today = nowcast.slice(today_start, today_end);

  // Midpoint time (in Luxon UTC) → to JS Date in UTC
  const middleDatetime = datetime[Math.floor(datetime.length / 2)];
  const sunTimes = SunCalc.getTimes(middleDatetime.toJSDate(), latitude, longitude);

  const sunrise = DateTime.fromJSDate(sunTimes.sunrise, { zone: timezone });
  const sunset = DateTime.fromJSDate(sunTimes.sunset, { zone: timezone });
  const sunriseHour = sunrise.hour + sunrise.minute / 60;
  const sunsetHour = sunset.hour + sunset.minute / 60;

  // Color-encoded points
  const yesterdayData = yesterday.map(y => ({ y, color: pm25ToColor(y) }));
  const todayData = today.map(y => ({ y, color: pm25ToColor(y) }));

  const ymin = 0;
  const ymax = pm25ToYMax(Math.max(...hour_average, ...yesterday, ...today));
  const chartTitle = title ?? locationName;

  return {
    accessibility: { enabled: false },
    chart: {
      plotBorderColor: "#ddd",
      plotBorderWidth: 1,
    },
    plotOptions: {
      line: { animation: false },
    },
    tooltip: {
      shared: true,
      headerFormat: "{point.key}:00<br/>",
    },
    title: {
      text: chartTitle,
    },
    xAxis: {
      tickInterval: 3,
      labels: {
        formatter: function () {
          return {
            0: "Midnight",
            3: "3am",
            6: "6am",
            9: "9am",
            12: "Noon",
            15: "3pm",
            18: "6pm",
            21: "9pm"
          }[this.value] ?? this.value;
        }
      },
      plotBands: [
        { color: "rgb(0,0,0,0.1)", from: 0, to: sunriseHour },
        { color: "rgb(0,0,0,0.1)", from: sunsetHour, to: 24 }
      ]
    },
    yAxis: {
      min: ymin,
      max: ymax,
      gridLineColor: "#ddd",
      gridLineDashStyle: "Dash",
      gridLineWidth: 1,
      title: {
        text: "PM2.5 (\u00b5g/m\u00b3)",
      },
      plotLines: pm25_AQILines(2),
    },
    legend: {
      enabled: true,
      verticalAlign: "top",
    },
    series: [
      {
        name: "7 Day Mean",
        type: "line",
        data: hour_average,
        color: "#aaa",
        lineWidth: 10,
        marker: { radius: 1, symbol: "square", fillColor: "transparent" },
      },
      {
        name: "Yesterday",
        type: "line",
        data: yesterdayData,
        color: "#888",
        lineWidth: 1,
        marker: {
          radius: 3,
          symbol: "circle",
          lineColor: "#888",
          lineWidth: 1,
        },
      },
      {
        name: "Today",
        type: "line",
        data: todayData,
        color: "#333",
        lineWidth: 2,
        marker: {
          radius: 5,
          symbol: "circle",
          lineColor: "#333",
          lineWidth: 1,
        },
      },
    ],
  };
}

/**
 * Returns a diurnalPlot chart configuration.
 * The 'small' version of this plot has no legend or axis labeling and is
 * appropriate for use in a display with "small multiples".
 * @param {Object} data The data required to create the chart.
 */
export function small_diurnalPlotConfig(data) {
  requireLuxonDateTimeArray(data.datetime, "datetime");

  const { datetime, nowcast, locationName, timezone, title, longitude, latitude, hour_average } = data;

  // ----- Data preparation --------------------------------

  const localHours = datetime.map(dt => dt.setZone(timezone).hour);
  const lastHour = localHours[localHours.length - 1];
  const today_end = localHours.length;
  const today_start = today_end - 1 - lastHour;
  const yesterday_end = today_start;
  const yesterday_start = today_start - 24;
  const yesterday = nowcast.slice(yesterday_start, yesterday_end);
  const today = nowcast.slice(today_start, today_end);

  const middleDatetime = datetime[Math.floor(datetime.length / 2)];
  const sunTimes = SunCalc.getTimes(middleDatetime.toJSDate(), latitude, longitude);

  const sunrise = DateTime.fromJSDate(sunTimes.sunrise, { zone: timezone });
  const sunset = DateTime.fromJSDate(sunTimes.sunset, { zone: timezone });
  const sunriseHour = sunrise.hour + sunrise.minute / 60;
  const sunsetHour = sunset.hour + sunset.minute / 60;

  const yesterdayData = yesterday.map(y => ({ y, color: pm25ToColor(y) }));
  const todayData = today.map(y => ({ y, color: pm25ToColor(y) }));

  const ymin = 0;
  const ymax = pm25ToYMax(Math.max(...hour_average, ...yesterday, ...today));
  const chartTitle = title ?? locationName;

  // ----- Chart configuration --------------------------------

  return {
    accessibility: { enabled: false },
    chart: {
      animation: false,
    },
    plotOptions: {
      line: {
        animation: false,
      },
    },
    title: {
      text: chartTitle,
      style: { color: "#333333", fontSize: "12px" },
    },
    xAxis: {
      visible: true,
      tickLength: 0,
      labels: { enabled: false },
      plotBands: [
        { color: "rgb(0,0,0,0.1)", from: 0, to: sunriseHour },
        { color: "rgb(0,0,0,0.1)", from: sunsetHour, to: 24 }
      ]
    },
    yAxis: {
      min: ymin,
      max: ymax,
      title: { text: "" },
      plotLines: pm25_AQILines(1),
    },
    legend: { enabled: false },
    series: [
      {
        name: "7 Day Mean",
        type: "line",
        data: hour_average,
        color: "#aaa",
        lineWidth: 5,
        marker: { enabled: false },
      },
      {
        name: "Yesterday",
        type: "line",
        data: yesterdayData,
        color: "#888",
        lineWidth: 0.5,
        marker: {
          radius: 1.5,
          symbol: "circle",
          lineColor: "#888",
          lineWidth: 0.3,
        },
      },
      {
        name: "Today",
        type: "line",
        data: todayData,
        color: "#333",
        lineWidth: 1,
        marker: {
          radius: 2,
          symbol: "circle",
          lineColor: "#333",
          lineWidth: 0.5,
        },
      },
    ],
  };
}