remove web dependencies

This commit is contained in:
nathan committed 2026-02-06 16:13:54 +11:00
1 parent 730811b76e
commit c68c063ff1
11 files changed
+264 -41

No files matched your search

+10 -1
View File
@@ -47,6 +47,7 @@ func runWebServer(ctx context.Context, d *db.DB, site providers.Site, model, add
} }
mux := http.NewServeMux() mux := http.NewServeMux()
staticFiles := http.FileServer(http.FS(sub))
mux.HandleFunc("/api/dashboard", ws.handleDashboard) mux.HandleFunc("/api/dashboard", ws.handleDashboard)
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -58,7 +59,8 @@ func runWebServer(ctx context.Context, d *db.DB, site providers.Site, model, add
mux.HandleFunc("/chart/", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/chart/", func(w http.ResponseWriter, r *http.Request) {
serveIndex(w, sub) serveIndex(w, sub)
}) })
mux.Handle("/", http.FileServer(http.FS(sub))) mux.Handle("/vendor/", withCacheControl("public, max-age=31536000, immutable", staticFiles))
mux.Handle("/", staticFiles)
srv := &http.Server{ srv := &http.Server{
Addr: addr, Addr: addr,
@@ -196,3 +198,10 @@ func parseTimeParam(v string) (time.Time, error) {
} }
return time.Time{}, errors.New("unsupported time format") return time.Time{}, errors.New("unsupported time format")
} }
func withCacheControl(cacheControl string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", cacheControl)
next.ServeHTTP(w, r)
})
}
+143 -35
View File
@@ -363,6 +363,10 @@ function sum(values) {
return seen ? total : null; return seen ? total : null;
} }
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function updateText(id, text) { function updateText(id, text) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) el.textContent = text; if (el) el.textContent = text;
@@ -430,48 +434,123 @@ function lastNonNull(points, key) {
return null; return null;
} }
function computeRainProbability(latest, pressureTrend1h) { function classifyRainProbability(prob) {
if (!latest) { if (prob >= 0.6) return "High";
if (prob >= 0.35) return "Medium";
return "Low";
}
function computeDewPointC(tempC, rh) {
if (tempC === null || tempC === undefined || rh === null || rh === undefined) {
return null;
}
const safeRh = clamp(rh, 1, 100);
const a = 17.625;
const b = 243.04;
const gamma = Math.log(safeRh / 100) + (a * tempC) / (b + tempC);
return (b * gamma) / (a - gamma);
}
function computeRainProbabilityFromInputs(tempC, rh, pressureHpa) {
if (tempC === null || tempC === undefined || rh === null || rh === undefined || pressureHpa === null || pressureHpa === undefined) {
return null; return null;
} }
let prob = 0.1; const dewPointC = computeDewPointC(tempC, rh);
if (pressureTrend1h !== null && pressureTrend1h !== undefined) { if (dewPointC === null) {
if (pressureTrend1h <= -3.0) { return null;
prob += 0.5;
} else if (pressureTrend1h <= -2.0) {
prob += 0.35;
} else if (pressureTrend1h <= -1.0) {
prob += 0.2;
} else if (pressureTrend1h <= -0.5) {
prob += 0.1;
}
} }
if (latest.rh !== null && latest.rh !== undefined) { const saturationSpread = Math.max(0, tempC - dewPointC);
if (latest.rh >= 95) { const humidityFactor = clamp((rh - 55) / 45, 0, 1);
prob += 0.2; const pressureFactor = clamp((1016 - pressureHpa) / 18, 0, 1);
} else if (latest.rh >= 90) { const saturationFactor = clamp((6 - saturationSpread) / 6, 0, 1);
prob += 0.15;
} else if (latest.rh >= 85) { const score = 0.45 * humidityFactor + 0.35 * pressureFactor + 0.2 * saturationFactor;
prob += 0.1; const prob = clamp(0.02 + 0.93 * score, 0.02, 0.98);
return { prob, label: classifyRainProbability(prob) };
}
function computeRainProbability(latest) {
if (!latest) {
return null;
} }
return computeRainProbabilityFromInputs(latest.temp_c, latest.rh, latest.pressure_hpa);
}
function buildRainProbabilitySeries(points) {
const out = [];
for (const p of points) {
const t = new Date(p.ts).getTime();
if (Number.isNaN(t)) continue;
const rp = computeRainProbabilityFromInputs(p.temp_c, p.rh, p.pressure_hpa);
out.push({
x: t,
y: rp ? Math.round(rp.prob * 1000) / 10 : null,
});
}
return out;
}
function updateWeatherIcons(latest, rainProb) {
const sunEl = document.getElementById("live-icon-sun");
const cloudEl = document.getElementById("live-icon-cloud");
const rainEl = document.getElementById("live-icon-rain");
const textEl = document.getElementById("live-weather-text");
[sunEl, cloudEl, rainEl].forEach((el) => {
if (el) el.classList.remove("active");
});
if (!latest) {
if (textEl) textEl.textContent = "--";
return;
} }
if (latest.wind_m_s !== null && latest.wind_m_s !== undefined && latest.wind_m_s >= 6) { const prob = rainProb ? rainProb.prob : null;
prob += 0.05; const rh = latest.rh;
const pressure = latest.pressure_hpa;
const uvi = latest.uvi;
let sunActive = false;
let cloudActive = false;
let rainActive = false;
let label = "Partly cloudy";
if (prob !== null && prob >= 0.6) {
rainActive = true;
cloudActive = true;
label = "Rain likely";
} else if (
(prob !== null && prob >= 0.35) ||
(rh !== null && rh !== undefined && rh >= 80) ||
(pressure !== null && pressure !== undefined && pressure <= 1008)
) {
cloudActive = true;
label = "Cloudy";
} else if (
uvi !== null &&
uvi !== undefined &&
uvi >= 4 &&
rh !== null &&
rh !== undefined &&
rh < 75 &&
pressure !== null &&
pressure !== undefined &&
pressure >= 1012
) {
sunActive = true;
label = "Sunny";
} else {
sunActive = true;
cloudActive = true;
} }
prob = Math.max(0.05, Math.min(0.95, prob)); if (sunEl) sunEl.classList.toggle("active", sunActive);
if (cloudEl) cloudEl.classList.toggle("active", cloudActive);
let label = "Low"; if (rainEl) rainEl.classList.toggle("active", rainActive);
if (prob >= 0.6) { if (textEl) textEl.textContent = label;
label = "High";
} else if (prob >= 0.35) {
label = "Medium";
}
return { prob, label };
} }
function extendForecastTo(points, endTime) { function extendForecastTo(points, endTime) {
@@ -607,12 +686,13 @@ function renderDashboard(data) {
const forecast = filterRange(forecastAll, rangeStart, rangeEnd); const forecast = filterRange(forecastAll, rangeStart, rangeEnd);
const forecastLine = extendForecastTo(forecast, rangeEnd); const forecastLine = extendForecastTo(forecast, rangeEnd);
const lastPressureTrend = lastNonNull(obsFiltered, "pressure_trend_1h"); const lastPressureTrend = lastNonNull(obsFiltered, "pressure_trend_1h");
const rainProb = computeRainProbability(latest, lastPressureTrend); const rainProb = computeRainProbability(latest);
if (rainProb) { if (rainProb) {
updateText("live-rain-prob", `${Math.round(rainProb.prob * 100)}% (${rainProb.label})`); updateText("live-rain-prob", `${Math.round(rainProb.prob * 100)}% (${rainProb.label})`);
} else { } else {
updateText("live-rain-prob", "--"); updateText("live-rain-prob", "--");
} }
updateWeatherIcons(latest, rainProb);
updateText("baro-outlook", describeBarometer(latest ? latest.pressure_hpa : null, lastPressureTrend)); updateText("baro-outlook", describeBarometer(latest ? latest.pressure_hpa : null, lastPressureTrend));
const obsTemps = obsFiltered.map((p) => p.temp_c); const obsTemps = obsFiltered.map((p) => p.temp_c);
@@ -757,9 +837,12 @@ function renderDashboard(data) {
upsertChart("chart-power", powerChart); upsertChart("chart-power", powerChart);
const rainOptions = baseOptions(range); const rainOptions = baseOptions(range);
rainOptions.scales.y.ticks.color = colors.rain;
rainOptions.scales.y.title = { display: true, text: "Observed Rain (mm)", color: colors.rain };
rainOptions.scales.y1 = { rainOptions.scales.y1 = {
position: "right", position: "right",
ticks: { color: "#a4c4c4" }, ticks: { color: colors.forecast },
title: { display: true, text: "Forecast Rain (mm)", color: colors.forecast },
grid: { drawOnChartArea: false }, grid: { drawOnChartArea: false },
}; };
@@ -788,7 +871,7 @@ function renderDashboard(data) {
label: "forecast precip (mm)", label: "forecast precip (mm)",
data: series(forecastRain, "precip_mm"), data: series(forecastRain, "precip_mm"),
backgroundColor: colors.forecast, backgroundColor: colors.forecast,
yAxisID: "y", yAxisID: "y1",
}, },
], ],
}, },
@@ -796,6 +879,31 @@ function renderDashboard(data) {
}; };
upsertChart("chart-rain", rainChart); upsertChart("chart-rain", rainChart);
const rainProbOptions = baseOptions(range);
rainProbOptions.scales.y.min = 0;
rainProbOptions.scales.y.max = 100;
rainProbOptions.scales.y.ticks.color = colors.rain;
rainProbOptions.scales.y.ticks.callback = (value) => `${value}%`;
rainProbOptions.scales.y.title = { display: true, text: "Probability (%)", color: colors.rain };
const rainProbChart = {
type: "line",
data: {
datasets: [
{
label: "predicted rain probability (%)",
data: buildRainProbabilitySeries(obsFiltered),
borderColor: colors.rain,
backgroundColor: "rgba(78, 168, 222, 0.18)",
fill: true,
yAxisID: "y",
},
],
},
options: rainProbOptions,
};
upsertChart("chart-rain-prob", rainProbChart);
updateSingleChartMode(); updateSingleChartMode();
} }
+21 -5
View File
@@ -9,9 +9,7 @@
<link rel="icon" href="/favicon.ico" sizes="any"> <link rel="icon" href="/favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest"> <link rel="manifest" href="/site.webmanifest">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="stylesheet" href="/vendor/fonts/fonts.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Space+Grotesk:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css"> <link rel="stylesheet" href="/styles.css">
</head> </head>
<body> <body>
@@ -50,6 +48,15 @@
<div class="panel-meta" id="last-updated">--</div> <div class="panel-meta" id="last-updated">--</div>
</div> </div>
<div class="live-grid"> <div class="live-grid">
<div class="metric metric-weather">
<div class="label">Weather</div>
<div class="weather-icons" aria-label="weather icons">
<span class="weather-icon" id="live-icon-sun" title="Sunny">☀️</span>
<span class="weather-icon" id="live-icon-cloud" title="Cloudy">☁️</span>
<span class="weather-icon" id="live-icon-rain" title="Rain">🌧️</span>
</div>
<div class="weather-caption" id="live-weather-text">--</div>
</div>
<div class="metric"> <div class="metric">
<div class="label">Temp C</div> <div class="label">Temp C</div>
<div class="value" id="live-temp">--</div> <div class="value" id="live-temp">--</div>
@@ -189,12 +196,21 @@
<canvas id="chart-rain"></canvas> <canvas id="chart-rain"></canvas>
</div> </div>
</div> </div>
<div class="chart-card" data-chart="chart-rain-prob">
<div class="chart-header">
<div class="chart-title">Predicted Rain Probability (Observed Inputs)</div>
<button class="chart-link" data-chart="chart-rain-prob" title="Copy chart link">Share</button>
</div>
<div class="chart-canvas">
<canvas id="chart-rain-prob"></canvas>
</div>
</div>
</div> </div>
</section> </section>
</main> </main>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> <script src="/vendor/js/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script> <script src="/vendor/js/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/app.js"></script> <script src="/app.js"></script>
</body> </body>
</html> </html>
+32
View File
@@ -177,6 +177,38 @@ body {
margin-top: 6px; margin-top: 6px;
} }
.metric-weather .label {
margin-bottom: 8px;
}
.weather-icons {
display: flex;
align-items: center;
gap: 10px;
min-height: 28px;
}
.weather-icon {
font-size: 24px;
line-height: 1;
opacity: 0.28;
filter: saturate(0.65);
transition: opacity 0.2s ease, transform 0.2s ease, filter 0.2s ease;
}
.weather-icon.active {
opacity: 1;
filter: saturate(1);
transform: translateY(-1px);
}
.weather-caption {
margin-top: 8px;
color: var(--muted);
font-size: 13px;
font-family: "IBM Plex Mono", monospace;
}
.callouts { .callouts {
display: grid; display: grid;
gap: 12px; gap: 12px;
+31
View File
@@ -0,0 +1,31 @@
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/vendor/fonts/ibm-plex-mono-400.ttf") format("truetype");
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/vendor/fonts/ibm-plex-mono-600.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/vendor/fonts/space-grotesk-400.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/vendor/fonts/space-grotesk-600.ttf") format("truetype");
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/chart.js@4.4.1/dist/chart.umd.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*!
* Chart.js v4.4.1
* https://www.chartjs.org
* (c) 2023 Chart.js Contributors
* Released under the MIT License
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;a<r;a++)e.call(i,t[a],a);else if(o(t))for(l=Object.keys(t),r=l.length,a=0;a<r;a++)e.call(i,t[l[a]],l[a])}function f(t,e){let i,s,n,o;if(!t||!e||t.length!==e.length)return!1;for(i=0,s=t.length;i<s;++i)if(n=t[i],o=e[i],n.datasetIndex!==o.datasetIndex||n.index!==o.index)return!1;return!0}function g(t){if(n(t))return t.map(g);if(o(t)){const e=Object.create(null),i=Object.keys(t),s=i.length;let n=0;for(;n<s;++n)e[i[n]]=g(t[i[n]]);return e}return t}function p(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}function m(t,e,i,s){if(!p(t))return;const n=e[t],a=i[t];o(n)&&o(a)?b(n,a,s):e[t]=g(a)}function b(t,e,i){const s=n(e)?e:[e],a=s.length;if(!o(t))return t;const r=(i=i||{}).merger||m;let l;for(let e=0;e<a;++e){if(l=s[e],!o(l))continue;const n=Object.keys(l);for(let e=0,s=n.length;e<s;++e)r(n[e],t,l,i)}return t}function x(t,e){return b(t,e,{merger:_})}function _(t,e,i){if(!p(t))return;const s=e[t],n=i[t];o(s)&&o(n)?x(s,n):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=g(n))}const y={"":t=>t,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)<i}function B(t){const e=Math.round(t);t=V(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(z(t))),s=t/i;return(s<=1?1:s<=2?2:s<=5?5:10)*i}function W(t){const e=[],i=Math.sqrt(t);let s;for(s=1;s<i;s++)t%s==0&&(e.push(s),e.push(t/s));return i===(0|i)&&e.push(i),e.sort(((t,e)=>t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;s<n;s++)o=t[s][i],isNaN(o)||(e.min=Math.min(e.min,o),e.max=Math.max(e.max,o))}function $(t){return t*(C/180)}function Y(t){return t*(180/C)}function U(t){if(!a(t))return;let e=1,i=0;for(;Math.round(t*e)/e!==t;)e*=10,i++;return i}function X(t,e){const i=e.x-t.x,s=e.y-t.y,n=Math.sqrt(i*i+s*s);let o=Math.atan2(s,i);return o<-.5*C&&(o+=O),{angle:o,distance:n}}function q(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function K(t,e){return(t-e+A)%O-C}function G(t){return(t%O+O)%O}function Z(t,e,i,s){const n=G(t),o=G(e),a=G(i),r=G(o-n),l=G(a-n),h=G(n-o),c=G(n-a);return n===o||n===a||s&&o===a||r>l&&h<c}function J(t,e,i){return Math.max(e,Math.min(i,t))}function Q(t){return J(t,-32768,32767)}function tt(t,e,i,s=1e-6){return t>=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]<e);let s,n=t.length-1,o=0;for(;n-o>1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return n<i||n===i&&t[s+1][e]===i}:s=>t[s][e]<i),st=(t,e,i)=>et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;s<n&&t[s]<e;)s++;for(;n>s&&t[n-1]>i;)n--;return s>0||n<t.length?t.slice(s,n):t}const ot=["push","pop","shift","splice","unshift"];function at(t,e){t._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),ot.forEach((e=>{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){Line truncated
/*!
* @kurkle/color v0.3.2
* https://github.com/kurkle/color#readme
* (c) 2023 Jukka Kurkela
* Released under the MIT License
*/function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e<i?6:0):e===n?(i-t)/s+2:(t-e)/s+4}(e,i,s,h,n),r=60*r+.5),[0|r,l||0,a]}function zt(t,e,i,s){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,i,s)).map(Mt)}function Ft(t,e,i){return zt(Lt,t,e,i)}function Vt(t){return(t%360+360)%360}function Bt(t){const e=Tt.exec(t);let i,s=255;if(!e)return;e[5]!==i&&(s=e[6]?vt(+e[5]):Mt(+e[5]));const n=Vt(+e[2]),o=+e[3]/100,a=+e[4]/100;return i="hwb"===e[1]?function(t,e,i){return zt(Rt,t,e,i)}(n,o,a):"hsv"===e[1]?function(t,e,i){return zt(Et,t,e,i)}(n,o,a):Ft(n,o,a),{r:i[0],g:i[1],b:i[2],a:s}}const Wt={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Nt={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};let Ht;function jt(t){Ht||(Ht=function(){const t={},e=Object.keys(Nt),i=Object.keys(Wt);let s,n,o,a,r;for(s=0;s<e.length;s++){for(a=r=e[s],n=0;n<i.length;n++)o=i[n],r=r.replace(o,Wt[o]);o=parseInt(Nt[a],16),t[r]=[o>>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),Line truncated
//# sourceMappingURL=chart.umd.js.map
@@ -0,0 +1,7 @@
/*!
* chartjs-adapter-date-fns v3.0.0
* https://www.chartjs.org
* (c) 2022 chartjs-adapter-date-fns Contributors
* Released under the MIT license
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("chart.js")):"function"==typeof define&&define.amd?define(["chart.js"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Chart)}(this,(function(t){"use strict";function e(t){if(null===t||!0===t||!1===t)return NaN;var e=Number(t);return isNaN(e)?e:e<0?Math.ceil(e):Math.floor(e)}function r(t,e){if(e.length<t)throw new TypeError(t+" argument"+(t>1?"s":"")+" required, but only "+e.length+" present")}function n(t){r(1,arguments);var e=Object.prototype.toString.call(t);return t instanceof Date||"object"==typeof t&&"[object Date]"===e?new Date(t.getTime()):"number"==typeof t||"[object Number]"===e?new Date(t):("string"!=typeof t&&"[object String]"!==e||"undefined"==typeof console||(console.warn("Starting with v2.0.0-beta.1 date-fns doesn't accept strings as date arguments. Please use `parseISO` to parse strings. See: https://git.io/fjule"),console.warn((new Error).stack)),new Date(NaN))}function a(t,a){r(2,arguments);var i=n(t),o=e(a);return isNaN(o)?new Date(NaN):o?(i.setDate(i.getDate()+o),i):i}function i(t,a){r(2,arguments);var i=n(t),o=e(a);if(isNaN(o))return new Date(NaN);if(!o)return i;var u=i.getDate(),s=new Date(i.getTime());s.setMonth(i.getMonth()+o+1,0);var c=s.getDate();return u>=c?s:(i.setFullYear(s.getFullYear(),s.getMonth(),u),i)}function o(t,a){r(2,arguments);var i=n(t).getTime(),o=e(a);return new Date(i+o)}var u=36e5;function s(t,a){r(1,arguments);var i=a||{},o=i.locale,u=o&&o.options&&o.options.weekStartsOn,s=null==u?0:e(u),c=null==i.weekStartsOn?s:e(i.weekStartsOn);if(!(c>=0&&c<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var d=n(t),l=d.getDay(),f=(l<c?7:0)+l-c;return d.setDate(d.getDate()-f),d.setHours(0,0,0,0),d}function c(t){var e=new Date(Date.UTC(t.getFullYear(),t.getMonth(),t.getDate(),t.getHours(),t.getMinutes(),t.getSeconds(),t.getMilliseconds()));return e.setUTCFullYear(t.getFullYear()),t.getTime()-e.getTime()}function d(t){r(1,arguments);var e=n(t);return e.setHours(0,0,0,0),e}var l=864e5;function f(t,e){r(2,arguments);var n=d(t),a=d(e),i=n.getTime()-c(n),o=a.getTime()-c(a);return Math.round((i-o)/l)}function h(t,e){r(2,arguments);var a=n(t),i=n(e),o=a.getTime()-i.getTime();return o<0?-1:o>0?1:o}function m(t){r(1,arguments);var e=n(t);return!isNaN(e)}function w(t,e){r(2,arguments);var a=n(t),i=n(e),o=a.getFullYear()-i.getFullYear(),u=a.getMonth()-i.getMonth();return 12*o+u}function g(t,e){r(2,arguments);var a=n(t),i=n(e);return a.getFullYear()-i.getFullYear()}function v(t,e){var r=t.getFullYear()-e.getFullYear()||t.getMonth()-e.getMonth()||t.getDate()-e.getDate()||t.getHours()-e.getHours()||t.getMinutes()-e.getMinutes()||t.getSeconds()-e.getSeconds()||t.getMilliseconds()-e.getMilliseconds();return r<0?-1:r>0?1:r}function y(t,e){r(2,arguments);var a=n(t),i=n(e),o=v(a,i),u=Math.abs(f(a,i));a.setDate(a.getDate()-o*u);var s=v(a,i)===-o,c=o*(u-s);return 0===c?0:c}function b(t,e){r(2,arguments);var a=n(t),i=n(e);return a.getTime()-i.getTime()}var T=36e5;function p(t){r(1,arguments);var e=n(t);return e.setHours(23,59,59,999),e}function C(t){r(1,arguments);var e=n(t),a=e.getMonth();return e.setFullYear(e.getFullYear(),a+1,0),e.setHours(23,59,59,999),e}function M(t){r(1,arguments);var e=n(t);return p(e).getTime()===C(e).getTime()}function D(t,e){r(2,arguments);var a,i=n(t),o=n(e),u=h(i,o),s=Math.abs(w(i,o));if(s<1)a=0;else{1===i.getMonth()&&i.getDate()>27&&i.setDate(30),i.setMonth(i.getMonth()-u*s);var c=h(i,o)===-u;M(n(t))&&1===s&&1===h(t,o)&&(c=!1),a=u*(s-c)}return 0===a?0:a}var x={lessThanXSeconds:{one:"less than a second",other:"less than {{count}} seconds"},xSeconds:{one:"1 second",other:"{{count}} seconds"},halfAMinute:"half a minute",lessThanXMinutes:{one:"less than a minute",other:"less than {{count}} minutes"},xMinutes:{one:"1 minute",other:"{{count}} minutes"},aboutXHours:{one:"about 1 hour",other:"about {{count}} hours"},xHours:{one:"1 hour",other:"{{count}} hours"},xDays:{one:"1 day",other:"{{count}} days"},aboutXWeeks:{one:"about 1 week",other:"about {{count}} weeks"},xWeeks:{one:"1 week",other:"{{count}} weeks"},aboutXMonths:{one:"about 1 month",other:"about {{count}} months"},xMonths:{one:"1 month",other:"{{count}} months"},aboutXYears:{one:"about 1 year",other:"about {{count}} years"},xYears:{one:"1 year",other:"{{count}} years"},overXYears:{one:"over 1 year",other:"over {{count}} years"},almostXYears:{one:"almost 1 year",other:"almost {{count}} years"}};function k(t){return function(e){var r=e||{},n=r.width?String(r.width):t.defaultWidth;return t.formats[n]||t.formats[t.defaultWidth]}}var U={date:k({formats:{full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},defaultWidth:"full"}),time:k({formats:{full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},defaultWidth:"full"}),dateTime:k({formats:{full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},defauLine truncated