🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

river-data-widget

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

river-data-widget - npm Package Compare versions

Comparing version
1.1.0
to
1.2.0
+597
index.cjs
/*! RiverDataWidget v1.2.0 2023-06-17 01:06:58
*! https://github.com/pb-uk/river-data-widget#readme
*! Copyright (C) 2023 pbuk (https://github.com/pb-uk).
*! License MIT.
*/
'use strict';
const FONT_STACK = '-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif';
const round3 = (value) => value < 100 ? value.toPrecision(3) : Math.round(value).toString();
class FloodMonitoringApiError extends Error {
constructor(msg, info = {}) {
super(msg);
this.name = 'FloodMonitoringApiError';
this.info = info;
}
}
const parseMeasureId = (measureId) => {
// ............base/ stat-paramet-qualifi- type -interva-unit
const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;
const matches = measureId.match(regExp);
if (matches === null) {
throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });
}
const [unit, interval, type, qualifier, parameter, stationId] = matches.reverse();
const qualifiedParameter = qualifier.length
? `${parameter}-${qualifier}`
: parameter;
return {
stationId,
parameter,
qualifier,
type,
interval,
unit,
qualifiedParameter,
};
};
const measureTranslations = {
unit: {
m3_s: 'm³/s',
mAOD: 'm',
mASD: 'm',
},
qualifiedParameter: {
'level-stage': 'level',
'level-downstage': 'downstream level',
},
};
const translateMeasureProperties = (measure) => {
const translated = {};
for (const prop in measure) {
const value = measure[prop];
if (measureTranslations[prop] && measureTranslations[prop][value]) {
translated[prop] = measureTranslations[prop][value];
}
else {
translated[prop] = value;
}
}
return translated;
};
/**
* RiverDataWidget https://github.com/pb-uk/river-data-widget.
*
* @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.
* @license AGPL-3.0-or-later see LICENSE.md.
*/
const setAttributes = (el, attributes) => {
for (const [key, value] of Object.entries(attributes)) {
el.setAttribute(key, `${value}`);
}
return el;
};
const setStyles = (el, styles) => {
for (const [key, value] of Object.entries(styles)) {
// Workaround (el.style.setProperty uses kebab-case keys).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
el.style[key] = value;
}
return el;
};
const createSvgElement = (name = 'svg', attributes = {}, styles = {}, innerHTML = false) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', name);
if (innerHTML !== false) {
el.innerHTML = innerHTML;
}
return setStyles(setAttributes(el, attributes), styles);
};
const MINUTE_MS = 60000;
// const HOUR_MS = 3600000;
const DAY_MS = 86400000;
/**
* Get the Date at the start of a day in UTC or local time.
*
* @param offset
* @param timeZone The time zone offset in minutes, or set to `true` to use the
* local time zone (`false`, the default, uses UTC).
* @returns The reqested date.
*/
const startOfDay = (date = null, offset = 0, timeZone = false) => {
if (timeZone === false) {
// Use UTC.
const base = date === null ? Date.now() : date.valueOf();
return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);
}
const now = new Date();
const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;
const local = now.valueOf() + tz * MINUTE_MS;
return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);
};
const timeFormatter = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZoneName: 'short',
});
const dddFormatter = new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
});
const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'short',
});
class Chart {
constructor(el, series, options = {}) {
var _a;
this.strokeWidth = 2;
this.fontSizePx = 14;
this.width = 480; // 400;
this.height = 270; // 225;
this.plotHeight = this.height - this.fontSizePx * 4.5;
this.plotWidth = this.width - this.strokeWidth;
this.plotColor = '#77C';
this.labelBg = 'rgba(255,255,255,0.5)';
this.labelBgWidth = '0.5em';
this.attribution = 'Uses Environment Agency data from the real-time API (Beta)';
// CSS settings.
// Just readable at 320x180.
// Good from 400x225.
// Perfect at 480x270 (font is 12px);
this.styles = {
'font-family': FONT_STACK,
'font-size': `${this.fontSizePx}px`,
display: 'block',
margin: 'auto',
'max-width': '150vh',
};
this.series = series;
this.options = options;
const viewBox = `0 0 ${this.width} ${this.height}`;
this.attribution = (_a = options.attribution) !== null && _a !== void 0 ? _a : this.attribution;
this.el = createSvgElement('svg', { viewBox }, this.styles);
el.append(this.el);
}
getLimits() {
if (this.limits == null) {
throw new FloodMonitoringApiError('Chart axis limits have not been set');
}
return this.limits;
}
getHorizontalGridlines() {
const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } = this.getLimits();
const xOffset = this.strokeWidth / 2;
const yOffset = this.plotHeight;
const x1 = xOffset;
const x2 = xOffset + (maxTime - minTime) * timeScale;
// Horizontal grid lines.
const stroke = '#ddd';
const lines = createSvgElement('g', { stroke });
const labels = createSvgElement('g');
const valueRange = maxValue - minValue;
// Horizontal grid interval.
const [interval, exponent] = getInterval(valueRange, 9);
const factor = 10 ** -exponent;
const base = Math.ceil((minValue * factor) / interval + 1) * interval;
let i = 0;
let current = base / factor;
while (current < maxValue) {
const y1 = yOffset - (current - minValue) * valueScale;
lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));
labels.append(createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`));
++i;
current = (base + i * interval) / factor;
}
const timeAxisLine = createSvgElement('line', { x1, y1: yOffset, x2, y2: yOffset }, { stroke: '#777' });
return [lines, labels, timeAxisLine];
}
getTimeScale() {
const { minTime, maxTime, timeScale } = this.getLimits();
const xOffset = this.strokeWidth / 2;
const yOffset = this.plotHeight + this.strokeWidth / 2;
const y1 = yOffset + this.fontSizePx * 3;
const y2 = yOffset - this.plotHeight;
// Vertical grid lines.
const stroke = '#ddd';
const lines = createSvgElement('g', { stroke });
const labels = createSvgElement('g');
// Vertical grid interval.
const base = minTime;
const interval = 86400;
let i = 0;
let current = base;
const labelOffset = 43200 * timeScale;
const fill = '#444';
while (current <= maxTime) {
const x1 = xOffset + (current - minTime) * timeScale;
const d = new Date(current * 1000);
// lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));
lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));
labels.append(createSvgElement('text', {
x: x1 + labelOffset,
y: y1 - this.fontSizePx * 1.8,
'text-anchor': 'middle',
}, { fill }, `${dddFormatter.format(d)}`), createSvgElement('text', {
x: x1 + labelOffset,
y: y1 - this.fontSizePx * 0.5,
'text-anchor': 'middle',
}, { fill }, `${dMmmFormatter.format(d)}`));
++i;
current = base + i * interval;
}
return [lines, labels];
}
render() {
var _a, _b, _c, _d;
// Calculate axis scales.
const limits = getLimits(this.series[0].data);
limits.minValue = (_a = this.series[0].min) !== null && _a !== void 0 ? _a : limits.minValue;
limits.maxValue = (_b = this.series[0].max) !== null && _b !== void 0 ? _b : limits.maxValue;
limits.minTime = (_c = this.options.minTime) !== null && _c !== void 0 ? _c : limits.minTime;
limits.maxTime = (_d = this.options.maxTime) !== null && _d !== void 0 ? _d : limits.maxTime;
this.limits = Object.assign(Object.assign({}, limits), { valueScale: (this.plotHeight - this.strokeWidth) /
(limits.maxValue - limits.minValue), timeScale: (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime) });
// Time axis.
const [timeLines, timeLabels] = this.getTimeScale();
this.el.append(timeLines);
// Value axis.
const [valueLines, valueLabels, timeAxisLine] = this.getHorizontalGridlines();
this.el.append(valueLines);
this.el.append(timeAxisLine);
this.plotData();
// Plot labels on top of the line.
this.el.append(timeLabels);
this.el.append(valueLabels);
this.el.append(createSvgElement('text', {
x: this.width / 2,
'text-anchor': 'middle',
y: this.height - this.fontSizePx * 0.5,
}, { fill: '#595959' }, this.attribution));
this.plotLastValue();
}
plotLastValue() {
const { data, unit, formatter } = this.series[0];
// If there is no data show a message.
if (data.length === 0) {
const x = this.plotWidth / 2;
const y = this.plotHeight / 2;
this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle'));
return;
}
const [time, value] = data[data.length - 1];
const { minTime, timeScale, maxValue, minValue } = this.getLimits();
const v = formatter == null ? value : formatter(value);
const xOffset = this.strokeWidth / 2;
// const yOffset = this.plotHeight - this.strokeWidth / 2;
const x = xOffset + (time - minTime) * timeScale;
const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;
const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;
this.el.append(
// Value label.
...this.createLargeLabel(x, y, `${v} ${unit}`),
// Time label.
...this.createLabel(x, y, `${timeFormatter.format(new Date(time * 1000))}`));
}
plotData() {
const { data } = this.series[0];
// Don't do anything we don't have to!
if (data.length === 0)
return;
const xOffset = this.strokeWidth / 2;
const yOffset = this.plotHeight - this.strokeWidth / 2;
const { minTime, timeScale, minValue, valueScale } = this.getLimits();
// First data point.
const x = xOffset + (data[0][0] - minTime) * timeScale;
const y = yOffset - (data[0][1] - minValue) * valueScale;
const points = [`M${x},${y}`];
// Remaining data points.
for (let i = 1; i < data.length; ++i) {
const x = xOffset + (data[i][0] - minTime) * timeScale;
const y = yOffset - (data[i][1] - minValue) * valueScale;
points.push(`L${x},${y}`);
}
// Plot the data.
const path = createSvgElement('path', {
d: points.join(''),
stroke: this.plotColor,
'stroke-width': this.strokeWidth,
fill: 'none',
});
this.el.append(path);
}
createLabel(x, y, text, anchor = 'end') {
return [
// Background for time label.
createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor }, {
stroke: this.labelBg,
'stroke-width': this.labelBgWidth,
}, text),
// Time label.
createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor }, { fill: this.plotColor }, text),
];
}
createLargeLabel(x, y, text, anchor = 'end') {
return [
// Background for value label.
createSvgElement('text', { x, y, 'text-anchor': anchor }, {
'font-size': '1.5em',
'font-weight': 'bold',
stroke: this.labelBg,
'stroke-width': this.labelBgWidth,
}, text),
// Value label.
createSvgElement('text', { x, y, 'text-anchor': anchor }, { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' }, text),
];
}
}
const getLimits = (data) => {
if (data.length < 1) {
return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 };
}
const minTime = data[0][0];
const maxTime = data[data.length - 1][0];
let minValue = Infinity;
let maxValue = -minValue;
for (const [, value] of data) {
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
}
return { minTime, maxTime, minValue, maxValue };
};
const getInterval = (range, maxDivisions) => {
const exponent = Math.floor(Math.log10(range)) - 1;
const k = range / (maxDivisions * 10 ** exponent);
const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;
return [mantissa, exponent];
};
// There is no need to be secure about this!
const baseUrl = 'http://environment.data.gov.uk/flood-monitoring';
const apiFetch = async (path, query = {}) => {
const queryString = new URLSearchParams(query).toString();
const uri = queryString
? `${baseUrl}${path}?${queryString}`
: `${baseUrl}${path}`;
const response = await fetch(uri);
return { data: await response.json(), response };
};
/**
* Convert a Date to a format recognized by the EA API for a query parameter.
*
* @param date Convert from.
* @returns A string in the EA API query parameter format.
*/
const toTimeParameter = (date) => {
return date.toISOString().substring(0, 19) + 'Z';
};
/*
Useful response headers
Date: 'Sat, 13 May 2023 09:14:07 GMT',
last-modified: Sat, 13 May 2023 09:03:13 GMT,
Response meta:
publisher: 'Environment Agency',
license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',
documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',
version: '0.9',
comment: 'Status: Beta service',
hasFormat: [
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z",
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z",
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z",
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z"
],
*/
const prefix = 'riverDataWidget';
const addPrefix = (key) => `${prefix}|${key}`;
let instance;
class Store {
clear(destroy = false) {
for (const key of this.keys()) {
localStorage.removeItem(addPrefix(key));
}
if (destroy) {
localStorage.removeItem(prefix);
return;
}
localStorage.setItem(prefix, JSON.stringify([]));
}
get(key) {
const value = localStorage.getItem(addPrefix(key));
return value === null ? null : JSON.parse(value);
}
has(key) {
return this.keys().includes(key);
}
/**
* Detect active localStorage.
*
* @returns true iff localStorage for the widget is active.
*/
isActive() {
return localStorage.getItem(prefix) !== null;
}
keys() {
const storedKeys = localStorage.getItem(prefix);
return storedKeys === null ? [] : JSON.parse(storedKeys);
}
set(key, value) {
const json = JSON.stringify(value);
const storedKeys = localStorage.getItem(prefix);
const keys = storedKeys === null ? [] : JSON.parse(storedKeys);
if (!keys.includes(key)) {
keys.push(key);
localStorage.setItem(prefix, JSON.stringify(keys));
}
localStorage.setItem(addPrefix(key), json);
}
unset(key) {
// Remove it before we do anything else.
localStorage.removeItem(addPrefix(key));
// Then remove it from the list of keys.
const storedKeys = localStorage.getItem(prefix);
const keys = storedKeys === null ? [] : JSON.parse(storedKeys);
const index = keys.indexOf(key);
// If it doesn't exist we don't have to remove it.
if (index === -1)
return false;
keys.splice(index, 1);
localStorage.setItem(prefix, JSON.stringify(keys));
return true;
}
}
const useStore = () => {
if (!instance) {
instance = new Store();
}
return instance;
};
// Throttle requests to five minutes.
const THROTTLE_MS = 5 * MINUTE_MS;
/**
* Fetch the readings for a measure.
*
* @param id The EA measure id.
* @returns A promise for an array of readings for the measure.
*/
const fetchMeasureReadings = async (id, options = {}) => {
// Set the parameters for the request.
const params = { _sorted: '' };
if (options.since) {
params.since = toTimeParameter(options.since);
}
// Get the response, casting the items to ReadingDTOs.
const response = (await apiFetch(`/id/measures/${id}/readings`, params));
return [parseReadings(response.data.items)[id] || [], response];
};
const filterSince = (data, since) => {
const position = data.findIndex((reading) => reading[0] >= since);
return position < 0 ? [] : data.slice(position);
};
/**
* Get the readings for a measure.
*
* @todo Caching and throttling.
*
* @param id The EA measure id.
* @returns A promise for an array of readings for the measure.
*/
const getMeasureReadings = async (id, options = {}) => {
// Get the saved readings.
const key = `readings|${id}`;
const store = useStore();
const stored = store.get(key) || {
data: [],
lastCheck: 0,
storedSince: Infinity,
};
const { data, lastCheck } = stored;
let { storedSince } = stored;
const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;
// Discard any older than 30 days.
while (data.length && data[0][0] < discardBefore) {
[storedSince] = data[0];
data.shift();
}
// If we have data early enough apply throttle.
const lastStored = data.length ? data[data.length - 1][0] : 0;
const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;
if (storedSince <= requestedSince &&
Date.now() < lastCheck * 1000 + THROTTLE_MS) {
// Throttled.
return filterSince(data, requestedSince);
}
const fetchOptions = Object.assign(Object.assign({}, options), { since: new Date(Math.max(requestedSince, lastStored) * 1000) });
const [newData] = await fetchMeasureReadings(id, fetchOptions);
mergeReadings(data, newData);
storedSince = Math.min(requestedSince, storedSince);
store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });
return filterSince(data, requestedSince);
};
const mergeReadings = (first, second) => {
if (!second.length)
return;
let firstPos = first.length - 1;
while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {
--firstPos;
}
first.splice(firstPos + 1, Infinity, ...second);
};
const parseReadings = (items) => {
const ranges = {};
for (const { measure, dateTime, value } of items) {
if (ranges[measure] == null) {
ranges[measure] = [];
}
ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);
}
const rangesById = {};
for (const [key, range] of Object.entries(ranges)) {
rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;
}
return rangesById;
};
const drawMeasureWidget = async (parentEl, measureId, options = {}) => {
var _a;
// Get readings for the last 7 days in local time.
const since = startOfDay(null, -7, true);
let data = [];
// Get the right API.
const parts = measureId.split('/');
const id = (_a = parts.pop()) !== null && _a !== void 0 ? _a : '';
const api = parts.length === 0 ? 'flood' : parts[0];
switch (api) {
case 'flood':
data = await getMeasureReadings(id, { since });
}
// Clear the GUI deck.
parentEl.replaceChildren();
const measure = parseMeasureId(measureId);
const { unit } = translateMeasureProperties(measure);
const series1 = { data, unit, formatter: round3 };
// Set max/min options for plot from widget options.
if (options.riverDataWidgetMaxValue != null) {
series1.max = parseFloat(options.riverDataWidgetMaxValue);
}
if (options.riverDataWidgetMinValue != null) {
series1.min = parseFloat(options.riverDataWidgetMinValue);
}
// Deal with no data.
if (data.length === 0) {
const minTime = since.valueOf() / 1000;
const maxTime = minTime + 86400 * 7;
const chartOptions = { minTime, maxTime };
const chart = new Chart(parentEl, [series1], chartOptions);
chart.render();
return;
}
const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;
const maxTime = startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;
const chartOptions = {
minTime,
maxTime,
// attribution: `www.riverdata.co.uk/station/${measure.stationId}`,
};
const chart = new Chart(parentEl, [series1], chartOptions);
chart.render();
};
/**
* RiverDataWidget https://github.com/pb-uk/river-data-widget.
*
* @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.
* @license AGPL-3.0-or-later see LICENSE.md.
*/
const version = '1.2.0';
exports.drawMeasureWidget = drawMeasureWidget;
exports.version = version;
//# sourceMappingURL=index.cjs.map
{"version":3,"file":"index.cjs","sources":["src/helpers/format.ts","src/flood-monitoring-api/error.ts","src/flood-monitoring-api/measure.ts","src/helpers/dom.ts","src/helpers/time.ts","src/widget/chart.ts","src/flood-monitoring-api/api.ts","src/flood-monitoring-api/store.ts","src/flood-monitoring-api/reading.ts","src/widget/render.ts","src/index.ts"],"sourcesContent":["export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n for (const [key, value] of Object.entries(attributes)) {\n el.setAttribute(key, `${value}`);\n }\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n for (const [key, value] of Object.entries(styles)) {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n }\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected strokeWidth = 2;\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected plotWidth = this.width - this.strokeWidth;\n\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n\n // If there is no data show a message.\n if (data.length === 0) {\n const x = this.plotWidth / 2;\n const y = this.plotHeight / 2;\n this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle'));\n return;\n }\n\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Value label.\n ...this.createLargeLabel(x, y, `${v} ${unit}`),\n // Time label.\n ...this.createLabel(\n x,\n y,\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const { data } = this.series[0];\n // Don't do anything we don't have to!\n if (data.length === 0) return;\n\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n\n protected createLabel(x: number, y: number, text: string, anchor = 'end') {\n return [\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n { fill: this.plotColor },\n text\n ),\n ];\n }\n\n protected createLargeLabel(\n x: number,\n y: number,\n text: string,\n anchor = 'end'\n ) {\n return [\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n text\n ),\n ];\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 };\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n for (const [, value] of data) {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n }\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n for (const { measure, dateTime, value } of items) {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n }\n\n const rangesById: Record<string, Reading[]> = {};\n for (const [key, range] of Object.entries(ranges)) {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n }\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings as getFloodMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\nimport type { Reading } from '../flood-monitoring-api/reading';\n\nexport const drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n let data: Reading[] = [];\n\n // Get the right API.\n const parts = measureId.split('/');\n const id = parts.pop() ?? '';\n const api = parts.length === 0 ? 'flood' : parts[0];\n switch (api) {\n case 'flood':\n data = await getFloodMeasureReadings(id, { since });\n }\n\n // Clear the GUI deck.\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n\n // Deal with no data.\n if (data.length === 0) {\n const minTime = since.valueOf() / 1000;\n const maxTime = minTime + 86400 * 7;\n const chartOptions = { minTime, maxTime };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n return;\n }\n\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { drawMeasureWidget } from './widget/render';\n\nexport const version = '1.2.0';\n"],"names":["getFloodMeasureReadings"],"mappings":"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"}
/*! RiverDataWidget v1.2.0 2023-06-17 01:06:58
*! https://github.com/pb-uk/river-data-widget#readme
*! Copyright (C) 2023 pbuk (https://github.com/pb-uk).
*! License MIT.
*/
var RiverDataWidget=function(t){"use strict";class e extends Error{constructor(t,e={}){super(t),this.name="RiverDataWidgetError",this.info=e}}const i=t=>t<100?t.toPrecision(3):Math.round(t).toString();class n extends Error{constructor(t,e={}){super(t),this.name="FloodMonitoringApiError",this.info=e}}const s={unit:{m3_s:"m³/s",mAOD:"m",mASD:"m"},qualifiedParameter:{"level-stage":"level","level-downstage":"downstream level"}},a=(t="svg",e={},i={},n=!1)=>{const s=document.createElementNS("http://www.w3.org/2000/svg",t);return!1!==n&&(s.innerHTML=n),((t,e)=>{for(const[i,n]of Object.entries(e))t.style[i]=n;return t})(((t,e)=>{for(const[i,n]of Object.entries(e))t.setAttribute(i,`${n}`);return t})(s,e),i)},o=864e5,r=(t=null,e=0,i=!1)=>{if(!1===i){const i=null===t?Date.now():t.valueOf();return new Date(Math.floor(i/o+e)*o)}const n=new Date,s=!0===i?n.getTimezoneOffset():i,a=n.valueOf()+6e4*s;return new Date(Math.floor(a/o+e)*o)},l=new Intl.DateTimeFormat("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1,timeZoneName:"short"}),h=new Intl.DateTimeFormat("en-GB",{weekday:"short"}),m=new Intl.DateTimeFormat("en-GB",{day:"numeric",month:"short"});class c{constructor(t,e,i={}){var n;this.strokeWidth=2,this.fontSizePx=14,this.width=480,this.height=270,this.plotHeight=this.height-4.5*this.fontSizePx,this.plotWidth=this.width-this.strokeWidth,this.plotColor="#77C",this.labelBg="rgba(255,255,255,0.5)",this.labelBgWidth="0.5em",this.attribution="Uses Environment Agency data from the real-time API (Beta)",this.styles={"font-family":'-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif',"font-size":`${this.fontSizePx}px`,display:"block",margin:"auto","max-width":"150vh"},this.series=e,this.options=i;const s=`0 0 ${this.width} ${this.height}`;this.attribution=null!==(n=i.attribution)&&void 0!==n?n:this.attribution,this.el=a("svg",{viewBox:s},this.styles),t.append(this.el)}getLimits(){if(null==this.limits)throw new n("Chart axis limits have not been set");return this.limits}getHorizontalGridlines(){const{minTime:t,maxTime:e,timeScale:i,minValue:n,maxValue:s,valueScale:o}=this.getLimits(),r=this.strokeWidth/2,l=this.plotHeight,h=r,m=r+(e-t)*i,c=a("g",{stroke:"#ddd"}),d=a("g"),g=s-n,[f,p]=u(g,9),x=10**-p,v=Math.ceil(n*x/f+1)*f;let S=0,w=v/x;for(;w<s;){const t=l-(w-n)*o;c.append(a("line",{x1:h,y1:t,x2:m,y2:t})),d.append(a("text",{x:h+4,y:t+4},{},`${w}`)),++S,w=(v+S*f)/x}return[c,d,a("line",{x1:h,y1:l,x2:m,y2:l},{stroke:"#777"})]}getTimeScale(){const{minTime:t,maxTime:e,timeScale:i}=this.getLimits(),n=this.strokeWidth/2,s=this.plotHeight+this.strokeWidth/2,o=s+3*this.fontSizePx,r=s-this.plotHeight,l=a("g",{stroke:"#ddd"}),c=a("g"),d=t;let u=0,g=d;const f=43200*i,p="#444";for(;g<=e;){const e=n+(g-t)*i,s=new Date(1e3*g);l.append(a("line",{x1:e,y1:o,x2:e,y2:r})),c.append(a("text",{x:e+f,y:o-1.8*this.fontSizePx,"text-anchor":"middle"},{fill:p},`${h.format(s)}`),a("text",{x:e+f,y:o-.5*this.fontSizePx,"text-anchor":"middle"},{fill:p},`${m.format(s)}`)),++u,g=d+86400*u}return[l,c]}render(){var t,e,i,n;const s=d(this.series[0].data);s.minValue=null!==(t=this.series[0].min)&&void 0!==t?t:s.minValue,s.maxValue=null!==(e=this.series[0].max)&&void 0!==e?e:s.maxValue,s.minTime=null!==(i=this.options.minTime)&&void 0!==i?i:s.minTime,s.maxTime=null!==(n=this.options.maxTime)&&void 0!==n?n:s.maxTime,this.limits=Object.assign(Object.assign({},s),{valueScale:(this.plotHeight-this.strokeWidth)/(s.maxValue-s.minValue),timeScale:(this.width-this.strokeWidth)/(s.maxTime-s.minTime)});const[o,r]=this.getTimeScale();this.el.append(o);const[l,h,m]=this.getHorizontalGridlines();this.el.append(l),this.el.append(m),this.plotData(),this.el.append(r),this.el.append(h),this.el.append(a("text",{x:this.width/2,"text-anchor":"middle",y:this.height-.5*this.fontSizePx},{fill:"#595959"},this.attribution)),this.plotLastValue()}plotLastValue(){const{data:t,unit:e,formatter:i}=this.series[0];if(0===t.length){const t=this.plotWidth/2,e=this.plotHeight/2;return void this.el.append(...this.createLargeLabel(t,e,"No data","middle"))}const[n,s]=t[t.length-1],{minTime:a,timeScale:o,maxValue:r,minValue:h}=this.getLimits(),m=null==i?s:i(s),c=this.strokeWidth/2+(n-a)*o,d=(s-h)/(r-h)<.5,u=this.plotHeight*(d?0:.5)+2*this.fontSizePx;this.el.append(...this.createLargeLabel(c,u,`${m} ${e}`),...this.createLabel(c,u,`${l.format(new Date(1e3*n))}`))}plotData(){const{data:t}=this.series[0];if(0===t.length)return;const e=this.strokeWidth/2,i=this.plotHeight-this.strokeWidth/2,{minTime:n,timeScale:s,minValue:o,valueScale:r}=this.getLimits(),l=[`M${e+(t[0][0]-n)*s},${i-(t[0][1]-o)*r}`];for(let a=1;a<t.length;++a){const h=e+(t[a][0]-n)*s,m=i-(t[a][1]-o)*r;l.push(`L${h},${m}`)}const h=a("path",{d:l.join(""),stroke:this.plotColor,"stroke-width":this.strokeWidth,fill:"none"});this.el.append(h)}createLabel(t,e,i,n="end"){return[a("text",{x:t,y:e+1.5*this.fontSizePx,"text-anchor":n},{stroke:this.labelBg,"stroke-width":this.labelBgWidth},i),a("text",{x:t,y:e+1.5*this.fontSizePx,"text-anchor":n},{fill:this.plotColor},i)]}createLargeLabel(t,e,i,n="end"){return[a("text",{x:t,y:e,"text-anchor":n},{"font-size":"1.5em","font-weight":"bold",stroke:this.labelBg,"stroke-width":this.labelBgWidth},i),a("text",{x:t,y:e,"text-anchor":n},{fill:this.plotColor,"font-size":"1.5em","font-weight":"bold"},i)]}}const d=t=>{if(t.length<1)return{minTime:0,maxTime:1,minValue:0,maxValue:0};const e=t[0][0],i=t[t.length-1][0];let n=1/0,s=-n;for(const[,e]of t)n=Math.min(n,e),s=Math.max(s,e);return{minTime:e,maxTime:i,minValue:n,maxValue:s}},u=(t,e)=>{const i=Math.floor(Math.log10(t))-1,n=t/(e*10**i);return[n<=2?2:n<=5?5:10,i]},g="http://environment.data.gov.uk/flood-monitoring",f="riverDataWidget",p=t=>`${f}|${t}`;let x;class v{clear(t=!1){for(const t of this.keys())localStorage.removeItem(p(t));t?localStorage.removeItem(f):localStorage.setItem(f,JSON.stringify([]))}get(t){const e=localStorage.getItem(p(t));return null===e?null:JSON.parse(e)}has(t){return this.keys().includes(t)}isActive(){return null!==localStorage.getItem(f)}keys(){const t=localStorage.getItem(f);return null===t?[]:JSON.parse(t)}set(t,e){const i=JSON.stringify(e),n=localStorage.getItem(f),s=null===n?[]:JSON.parse(n);s.includes(t)||(s.push(t),localStorage.setItem(f,JSON.stringify(s))),localStorage.setItem(p(t),i)}unset(t){localStorage.removeItem(p(t));const e=localStorage.getItem(f),i=null===e?[]:JSON.parse(e),n=i.indexOf(t);return-1!==n&&(i.splice(n,1),localStorage.setItem(f,JSON.stringify(i)),!0)}}const S=async(t,e={})=>{const i={_sorted:""};e.since&&(i.since=e.since.toISOString().substring(0,19)+"Z");const n=await(async(t,e={})=>{const i=new URLSearchParams(e).toString(),n=i?`${g}${t}?${i}`:`${g}${t}`,s=await fetch(n);return{data:await s.json(),response:s}})(`/id/measures/${t}/readings`,i);return[k(n.data.items)[t]||[],n]},w=(t,e)=>{const i=t.findIndex((t=>t[0]>=e));return i<0?[]:t.slice(i)},y=async(t,e={})=>{const i=`readings|${t}`,n=(x||(x=new v),x),s=n.get(i)||{data:[],lastCheck:0,storedSince:1/0},{data:a,lastCheck:o}=s;let{storedSince:l}=s;const h=r(null,-8,!0).valueOf()/1e3;for(;a.length&&a[0][0]<h;)[l]=a[0],a.shift();const m=a.length?a[a.length-1][0]:0,c=e.since&&e.since.valueOf()/1e3||0;if(l<=c&&Date.now()<1e3*o+3e5)return w(a,c);const d=Object.assign(Object.assign({},e),{since:new Date(1e3*Math.max(c,m))}),[u]=await S(t,d);return b(a,u),l=Math.min(c,l),n.set(i,{lastCheck:Date.now()/1e3,data:a,storedSince:l}),w(a,c)},b=(t,e)=>{if(!e.length)return;let i=t.length-1;for(;i>=0&&t[i][0]>=e[0][0];)--i;t.splice(i+1,1/0,...e)},k=t=>{const e={};for(const{measure:i,dateTime:n,value:s}of t)null==e[i]&&(e[i]=[]),e[i].unshift([new Date(n).valueOf()/1e3,s]);const i={};for(const[t,n]of Object.entries(e))i[t.substring(t.lastIndexOf("/")+1)]=n;return i},T=async(t,e,a={})=>{var o;const l=r(null,-7,!0);let h=[];const m=e.split("/"),d=null!==(o=m.pop())&&void 0!==o?o:"";if("flood"===(0===m.length?"flood":m[0]))h=await y(d,{since:l});t.replaceChildren();const u=(t=>{const e=t.match(/(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/);if(null===e)throw new n("Cannot parse measure id",{measureId:t});const[i,s,a,o,r,l]=e.reverse();return{stationId:l,parameter:r,qualifier:o,type:a,interval:s,unit:i,qualifiedParameter:o.length?`${r}-${o}`:r}})(e),{unit:g}=(t=>{const e={};for(const i in t){const n=t[i];s[i]&&s[i][n]?e[i]=s[i][n]:e[i]=n}return e})(u),f={data:h,unit:g,formatter:i};if(null!=a.riverDataWidgetMaxValue&&(f.max=parseFloat(a.riverDataWidgetMaxValue)),null!=a.riverDataWidgetMinValue&&(f.min=parseFloat(a.riverDataWidgetMinValue)),0===h.length){const e=l.valueOf()/1e3;return void new c(t,[f],{minTime:e,maxTime:e+604800}).render()}const p=r(new Date(1e3*h[0][0])).valueOf()/1e3,x=r(new Date(1e3*h[h.length-1][0]),1).valueOf()/1e3;new c(t,[f],{minTime:p,maxTime:x}).render()},O=t=>{var i,n;const s="string"==typeof t?document.querySelector(t):t;if(null===s)throw new Error("Target element not found");const a=null!==(n=null===(i=s.dataset.riverDataWidget)||void 0===i?void 0:i.split(":"))&&void 0!==n?n:[],o=a.shift(),r=a.join(":"),l=s.dataset;if("measure"!==o)throw new e("Unknown widget definition",{type:o,id:r});T(s,r,l)},D=async()=>{for(const t of document.querySelectorAll("[data-river-data-widget]"))try{O(t)}catch(t){console.error(t,{error:t})}};return"loading"===document.readyState?document.addEventListener("DOMContentLoaded",D):D(),t.version="1.2.0",t}({});
//# sourceMappingURL=index.min.js.map
{"version":3,"file":"index.min.js","sources":["src/error.ts","src/helpers/format.ts","src/flood-monitoring-api/error.ts","src/flood-monitoring-api/measure.ts","src/helpers/dom.ts","src/helpers/time.ts","src/widget/chart.ts","src/flood-monitoring-api/api.ts","src/flood-monitoring-api/store.ts","src/flood-monitoring-api/reading.ts","src/widget/render.ts","src/autoload.ts","src/index.ts"],"sourcesContent":["export type RiverDataWidgetErrorInfo = Record<string, unknown>;\n\nexport class RiverDataWidgetError extends Error {\n public info: RiverDataWidgetErrorInfo;\n\n constructor(msg: string, info: RiverDataWidgetErrorInfo = {}) {\n super(msg);\n this.name = 'RiverDataWidgetError';\n this.info = info;\n }\n}\n","export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n for (const [key, value] of Object.entries(attributes)) {\n el.setAttribute(key, `${value}`);\n }\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n for (const [key, value] of Object.entries(styles)) {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n }\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected strokeWidth = 2;\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected plotWidth = this.width - this.strokeWidth;\n\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n\n // If there is no data show a message.\n if (data.length === 0) {\n const x = this.plotWidth / 2;\n const y = this.plotHeight / 2;\n this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle'));\n return;\n }\n\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Value label.\n ...this.createLargeLabel(x, y, `${v} ${unit}`),\n // Time label.\n ...this.createLabel(\n x,\n y,\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const { data } = this.series[0];\n // Don't do anything we don't have to!\n if (data.length === 0) return;\n\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n\n protected createLabel(x: number, y: number, text: string, anchor = 'end') {\n return [\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n { fill: this.plotColor },\n text\n ),\n ];\n }\n\n protected createLargeLabel(\n x: number,\n y: number,\n text: string,\n anchor = 'end'\n ) {\n return [\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n text\n ),\n ];\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 };\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n for (const [, value] of data) {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n }\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n for (const { measure, dateTime, value } of items) {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n }\n\n const rangesById: Record<string, Reading[]> = {};\n for (const [key, range] of Object.entries(ranges)) {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n }\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings as getFloodMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\nimport type { Reading } from '../flood-monitoring-api/reading';\n\nexport const drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n let data: Reading[] = [];\n\n // Get the right API.\n const parts = measureId.split('/');\n const id = parts.pop() ?? '';\n const api = parts.length === 0 ? 'flood' : parts[0];\n switch (api) {\n case 'flood':\n data = await getFloodMeasureReadings(id, { since });\n }\n\n // Clear the GUI deck.\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n\n // Deal with no data.\n if (data.length === 0) {\n const minTime = since.valueOf() / 1000;\n const maxTime = minTime + 86400 * 7;\n const chartOptions = { minTime, maxTime };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n return;\n }\n\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\nimport { loadWidget } from './widget/render';\n\nexport { version } from '.';\n\nconst autoload = async () => {\n for (const el of document.querySelectorAll('[data-river-data-widget]')) {\n try {\n loadWidget(<HTMLElement>el);\n } catch (error) {\n console.error(error, { error });\n }\n }\n};\n\nif (document.readyState === 'loading') {\n // Loading hasn't finished yet.\n document.addEventListener('DOMContentLoaded', autoload);\n} else {\n // `DOMContentLoaded` has already fired.\n autoload();\n}\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { drawMeasureWidget } from './widget/render';\n\nexport const version = '1.2.0';\n"],"names":["RiverDataWidgetError","Error","constructor","msg","info","super","this","name","round3","value","toPrecision","Math","round","toString","FloodMonitoringApiError","measureTranslations","unit","m3_s","mAOD","mASD","qualifiedParameter","createSvgElement","attributes","styles","innerHTML","el","document","createElementNS","key","Object","entries","style","setStyles","setAttribute","setAttributes","DAY_MS","startOfDay","date","offset","timeZone","base","Date","now","valueOf","floor","tz","getTimezoneOffset","local","timeFormatter","Intl","DateTimeFormat","hour","minute","hour12","timeZoneName","dddFormatter","weekday","dMmmFormatter","day","month","Chart","series","options","strokeWidth","fontSizePx","width","height","plotHeight","plotWidth","plotColor","labelBg","labelBgWidth","attribution","display","margin","viewBox","_a","append","getLimits","limits","getHorizontalGridlines","minTime","maxTime","timeScale","minValue","maxValue","valueScale","xOffset","yOffset","x1","x2","lines","stroke","labels","valueRange","interval","exponent","getInterval","factor","ceil","i","current","y1","y2","x","y","getTimeScale","labelOffset","fill","d","format","render","data","min","_b","max","_c","_d","assign","timeLines","timeLabels","valueLines","valueLabels","timeAxisLine","plotData","plotLastValue","formatter","length","createLargeLabel","time","v","isHighLabel","createLabel","points","push","path","join","text","anchor","Infinity","range","maxDivisions","log10","k","baseUrl","prefix","addPrefix","instance","Store","clear","destroy","keys","localStorage","removeItem","setItem","JSON","stringify","get","getItem","parse","has","includes","isActive","storedKeys","set","json","unset","index","indexOf","splice","fetchMeasureReadings","async","id","params","_sorted","since","toISOString","substring","response","query","queryString","URLSearchParams","uri","fetch","apiFetch","parseReadings","items","filterSince","position","findIndex","reading","slice","getMeasureReadings","store","stored","lastCheck","storedSince","discardBefore","shift","lastStored","requestedSince","fetchOptions","newData","mergeReadings","first","second","firstPos","ranges","measure","dateTime","unshift","rangesById","lastIndexOf","drawMeasureWidget","parentEl","measureId","parts","split","pop","getFloodMeasureReadings","replaceChildren","matches","match","type","qualifier","parameter","stationId","reverse","parseMeasureId","translated","prop","translateMeasureProperties","series1","riverDataWidgetMaxValue","parseFloat","riverDataWidgetMinValue","loadWidget","targetEl","querySelector","widgetIdParts","dataset","riverDataWidget","autoload","querySelectorAll","error","console","readyState","addEventListener"],"mappings":"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"}
/*! RiverDataWidget v1.2.0 2023-06-17 01:06:58
*! https://github.com/pb-uk/river-data-widget#readme
*! Copyright (C) 2023 pbuk (https://github.com/pb-uk).
*! License MIT.
*/
const FONT_STACK = '-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif';
const round3 = (value) => value < 100 ? value.toPrecision(3) : Math.round(value).toString();
class FloodMonitoringApiError extends Error {
constructor(msg, info = {}) {
super(msg);
this.name = 'FloodMonitoringApiError';
this.info = info;
}
}
const parseMeasureId = (measureId) => {
// ............base/ stat-paramet-qualifi- type -interva-unit
const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;
const matches = measureId.match(regExp);
if (matches === null) {
throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });
}
const [unit, interval, type, qualifier, parameter, stationId] = matches.reverse();
const qualifiedParameter = qualifier.length
? `${parameter}-${qualifier}`
: parameter;
return {
stationId,
parameter,
qualifier,
type,
interval,
unit,
qualifiedParameter,
};
};
const measureTranslations = {
unit: {
m3_s: 'm³/s',
mAOD: 'm',
mASD: 'm',
},
qualifiedParameter: {
'level-stage': 'level',
'level-downstage': 'downstream level',
},
};
const translateMeasureProperties = (measure) => {
const translated = {};
for (const prop in measure) {
const value = measure[prop];
if (measureTranslations[prop] && measureTranslations[prop][value]) {
translated[prop] = measureTranslations[prop][value];
}
else {
translated[prop] = value;
}
}
return translated;
};
/**
* RiverDataWidget https://github.com/pb-uk/river-data-widget.
*
* @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.
* @license AGPL-3.0-or-later see LICENSE.md.
*/
const setAttributes = (el, attributes) => {
for (const [key, value] of Object.entries(attributes)) {
el.setAttribute(key, `${value}`);
}
return el;
};
const setStyles = (el, styles) => {
for (const [key, value] of Object.entries(styles)) {
// Workaround (el.style.setProperty uses kebab-case keys).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
el.style[key] = value;
}
return el;
};
const createSvgElement = (name = 'svg', attributes = {}, styles = {}, innerHTML = false) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', name);
if (innerHTML !== false) {
el.innerHTML = innerHTML;
}
return setStyles(setAttributes(el, attributes), styles);
};
const MINUTE_MS = 60000;
// const HOUR_MS = 3600000;
const DAY_MS = 86400000;
/**
* Get the Date at the start of a day in UTC or local time.
*
* @param offset
* @param timeZone The time zone offset in minutes, or set to `true` to use the
* local time zone (`false`, the default, uses UTC).
* @returns The reqested date.
*/
const startOfDay = (date = null, offset = 0, timeZone = false) => {
if (timeZone === false) {
// Use UTC.
const base = date === null ? Date.now() : date.valueOf();
return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);
}
const now = new Date();
const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;
const local = now.valueOf() + tz * MINUTE_MS;
return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);
};
const timeFormatter = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZoneName: 'short',
});
const dddFormatter = new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
});
const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'short',
});
class Chart {
constructor(el, series, options = {}) {
var _a;
this.strokeWidth = 2;
this.fontSizePx = 14;
this.width = 480; // 400;
this.height = 270; // 225;
this.plotHeight = this.height - this.fontSizePx * 4.5;
this.plotWidth = this.width - this.strokeWidth;
this.plotColor = '#77C';
this.labelBg = 'rgba(255,255,255,0.5)';
this.labelBgWidth = '0.5em';
this.attribution = 'Uses Environment Agency data from the real-time API (Beta)';
// CSS settings.
// Just readable at 320x180.
// Good from 400x225.
// Perfect at 480x270 (font is 12px);
this.styles = {
'font-family': FONT_STACK,
'font-size': `${this.fontSizePx}px`,
display: 'block',
margin: 'auto',
'max-width': '150vh',
};
this.series = series;
this.options = options;
const viewBox = `0 0 ${this.width} ${this.height}`;
this.attribution = (_a = options.attribution) !== null && _a !== void 0 ? _a : this.attribution;
this.el = createSvgElement('svg', { viewBox }, this.styles);
el.append(this.el);
}
getLimits() {
if (this.limits == null) {
throw new FloodMonitoringApiError('Chart axis limits have not been set');
}
return this.limits;
}
getHorizontalGridlines() {
const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } = this.getLimits();
const xOffset = this.strokeWidth / 2;
const yOffset = this.plotHeight;
const x1 = xOffset;
const x2 = xOffset + (maxTime - minTime) * timeScale;
// Horizontal grid lines.
const stroke = '#ddd';
const lines = createSvgElement('g', { stroke });
const labels = createSvgElement('g');
const valueRange = maxValue - minValue;
// Horizontal grid interval.
const [interval, exponent] = getInterval(valueRange, 9);
const factor = 10 ** -exponent;
const base = Math.ceil((minValue * factor) / interval + 1) * interval;
let i = 0;
let current = base / factor;
while (current < maxValue) {
const y1 = yOffset - (current - minValue) * valueScale;
lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));
labels.append(createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`));
++i;
current = (base + i * interval) / factor;
}
const timeAxisLine = createSvgElement('line', { x1, y1: yOffset, x2, y2: yOffset }, { stroke: '#777' });
return [lines, labels, timeAxisLine];
}
getTimeScale() {
const { minTime, maxTime, timeScale } = this.getLimits();
const xOffset = this.strokeWidth / 2;
const yOffset = this.plotHeight + this.strokeWidth / 2;
const y1 = yOffset + this.fontSizePx * 3;
const y2 = yOffset - this.plotHeight;
// Vertical grid lines.
const stroke = '#ddd';
const lines = createSvgElement('g', { stroke });
const labels = createSvgElement('g');
// Vertical grid interval.
const base = minTime;
const interval = 86400;
let i = 0;
let current = base;
const labelOffset = 43200 * timeScale;
const fill = '#444';
while (current <= maxTime) {
const x1 = xOffset + (current - minTime) * timeScale;
const d = new Date(current * 1000);
// lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));
lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));
labels.append(createSvgElement('text', {
x: x1 + labelOffset,
y: y1 - this.fontSizePx * 1.8,
'text-anchor': 'middle',
}, { fill }, `${dddFormatter.format(d)}`), createSvgElement('text', {
x: x1 + labelOffset,
y: y1 - this.fontSizePx * 0.5,
'text-anchor': 'middle',
}, { fill }, `${dMmmFormatter.format(d)}`));
++i;
current = base + i * interval;
}
return [lines, labels];
}
render() {
var _a, _b, _c, _d;
// Calculate axis scales.
const limits = getLimits(this.series[0].data);
limits.minValue = (_a = this.series[0].min) !== null && _a !== void 0 ? _a : limits.minValue;
limits.maxValue = (_b = this.series[0].max) !== null && _b !== void 0 ? _b : limits.maxValue;
limits.minTime = (_c = this.options.minTime) !== null && _c !== void 0 ? _c : limits.minTime;
limits.maxTime = (_d = this.options.maxTime) !== null && _d !== void 0 ? _d : limits.maxTime;
this.limits = Object.assign(Object.assign({}, limits), { valueScale: (this.plotHeight - this.strokeWidth) /
(limits.maxValue - limits.minValue), timeScale: (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime) });
// Time axis.
const [timeLines, timeLabels] = this.getTimeScale();
this.el.append(timeLines);
// Value axis.
const [valueLines, valueLabels, timeAxisLine] = this.getHorizontalGridlines();
this.el.append(valueLines);
this.el.append(timeAxisLine);
this.plotData();
// Plot labels on top of the line.
this.el.append(timeLabels);
this.el.append(valueLabels);
this.el.append(createSvgElement('text', {
x: this.width / 2,
'text-anchor': 'middle',
y: this.height - this.fontSizePx * 0.5,
}, { fill: '#595959' }, this.attribution));
this.plotLastValue();
}
plotLastValue() {
const { data, unit, formatter } = this.series[0];
// If there is no data show a message.
if (data.length === 0) {
const x = this.plotWidth / 2;
const y = this.plotHeight / 2;
this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle'));
return;
}
const [time, value] = data[data.length - 1];
const { minTime, timeScale, maxValue, minValue } = this.getLimits();
const v = formatter == null ? value : formatter(value);
const xOffset = this.strokeWidth / 2;
// const yOffset = this.plotHeight - this.strokeWidth / 2;
const x = xOffset + (time - minTime) * timeScale;
const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;
const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;
this.el.append(
// Value label.
...this.createLargeLabel(x, y, `${v} ${unit}`),
// Time label.
...this.createLabel(x, y, `${timeFormatter.format(new Date(time * 1000))}`));
}
plotData() {
const { data } = this.series[0];
// Don't do anything we don't have to!
if (data.length === 0)
return;
const xOffset = this.strokeWidth / 2;
const yOffset = this.plotHeight - this.strokeWidth / 2;
const { minTime, timeScale, minValue, valueScale } = this.getLimits();
// First data point.
const x = xOffset + (data[0][0] - minTime) * timeScale;
const y = yOffset - (data[0][1] - minValue) * valueScale;
const points = [`M${x},${y}`];
// Remaining data points.
for (let i = 1; i < data.length; ++i) {
const x = xOffset + (data[i][0] - minTime) * timeScale;
const y = yOffset - (data[i][1] - minValue) * valueScale;
points.push(`L${x},${y}`);
}
// Plot the data.
const path = createSvgElement('path', {
d: points.join(''),
stroke: this.plotColor,
'stroke-width': this.strokeWidth,
fill: 'none',
});
this.el.append(path);
}
createLabel(x, y, text, anchor = 'end') {
return [
// Background for time label.
createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor }, {
stroke: this.labelBg,
'stroke-width': this.labelBgWidth,
}, text),
// Time label.
createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor }, { fill: this.plotColor }, text),
];
}
createLargeLabel(x, y, text, anchor = 'end') {
return [
// Background for value label.
createSvgElement('text', { x, y, 'text-anchor': anchor }, {
'font-size': '1.5em',
'font-weight': 'bold',
stroke: this.labelBg,
'stroke-width': this.labelBgWidth,
}, text),
// Value label.
createSvgElement('text', { x, y, 'text-anchor': anchor }, { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' }, text),
];
}
}
const getLimits = (data) => {
if (data.length < 1) {
return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 };
}
const minTime = data[0][0];
const maxTime = data[data.length - 1][0];
let minValue = Infinity;
let maxValue = -minValue;
for (const [, value] of data) {
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
}
return { minTime, maxTime, minValue, maxValue };
};
const getInterval = (range, maxDivisions) => {
const exponent = Math.floor(Math.log10(range)) - 1;
const k = range / (maxDivisions * 10 ** exponent);
const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;
return [mantissa, exponent];
};
// There is no need to be secure about this!
const baseUrl = 'http://environment.data.gov.uk/flood-monitoring';
const apiFetch = async (path, query = {}) => {
const queryString = new URLSearchParams(query).toString();
const uri = queryString
? `${baseUrl}${path}?${queryString}`
: `${baseUrl}${path}`;
const response = await fetch(uri);
return { data: await response.json(), response };
};
/**
* Convert a Date to a format recognized by the EA API for a query parameter.
*
* @param date Convert from.
* @returns A string in the EA API query parameter format.
*/
const toTimeParameter = (date) => {
return date.toISOString().substring(0, 19) + 'Z';
};
/*
Useful response headers
Date: 'Sat, 13 May 2023 09:14:07 GMT',
last-modified: Sat, 13 May 2023 09:03:13 GMT,
Response meta:
publisher: 'Environment Agency',
license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',
documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',
version: '0.9',
comment: 'Status: Beta service',
hasFormat: [
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z",
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z",
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z",
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z"
],
*/
const prefix = 'riverDataWidget';
const addPrefix = (key) => `${prefix}|${key}`;
let instance;
class Store {
clear(destroy = false) {
for (const key of this.keys()) {
localStorage.removeItem(addPrefix(key));
}
if (destroy) {
localStorage.removeItem(prefix);
return;
}
localStorage.setItem(prefix, JSON.stringify([]));
}
get(key) {
const value = localStorage.getItem(addPrefix(key));
return value === null ? null : JSON.parse(value);
}
has(key) {
return this.keys().includes(key);
}
/**
* Detect active localStorage.
*
* @returns true iff localStorage for the widget is active.
*/
isActive() {
return localStorage.getItem(prefix) !== null;
}
keys() {
const storedKeys = localStorage.getItem(prefix);
return storedKeys === null ? [] : JSON.parse(storedKeys);
}
set(key, value) {
const json = JSON.stringify(value);
const storedKeys = localStorage.getItem(prefix);
const keys = storedKeys === null ? [] : JSON.parse(storedKeys);
if (!keys.includes(key)) {
keys.push(key);
localStorage.setItem(prefix, JSON.stringify(keys));
}
localStorage.setItem(addPrefix(key), json);
}
unset(key) {
// Remove it before we do anything else.
localStorage.removeItem(addPrefix(key));
// Then remove it from the list of keys.
const storedKeys = localStorage.getItem(prefix);
const keys = storedKeys === null ? [] : JSON.parse(storedKeys);
const index = keys.indexOf(key);
// If it doesn't exist we don't have to remove it.
if (index === -1)
return false;
keys.splice(index, 1);
localStorage.setItem(prefix, JSON.stringify(keys));
return true;
}
}
const useStore = () => {
if (!instance) {
instance = new Store();
}
return instance;
};
// Throttle requests to five minutes.
const THROTTLE_MS = 5 * MINUTE_MS;
/**
* Fetch the readings for a measure.
*
* @param id The EA measure id.
* @returns A promise for an array of readings for the measure.
*/
const fetchMeasureReadings = async (id, options = {}) => {
// Set the parameters for the request.
const params = { _sorted: '' };
if (options.since) {
params.since = toTimeParameter(options.since);
}
// Get the response, casting the items to ReadingDTOs.
const response = (await apiFetch(`/id/measures/${id}/readings`, params));
return [parseReadings(response.data.items)[id] || [], response];
};
const filterSince = (data, since) => {
const position = data.findIndex((reading) => reading[0] >= since);
return position < 0 ? [] : data.slice(position);
};
/**
* Get the readings for a measure.
*
* @todo Caching and throttling.
*
* @param id The EA measure id.
* @returns A promise for an array of readings for the measure.
*/
const getMeasureReadings = async (id, options = {}) => {
// Get the saved readings.
const key = `readings|${id}`;
const store = useStore();
const stored = store.get(key) || {
data: [],
lastCheck: 0,
storedSince: Infinity,
};
const { data, lastCheck } = stored;
let { storedSince } = stored;
const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;
// Discard any older than 30 days.
while (data.length && data[0][0] < discardBefore) {
[storedSince] = data[0];
data.shift();
}
// If we have data early enough apply throttle.
const lastStored = data.length ? data[data.length - 1][0] : 0;
const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;
if (storedSince <= requestedSince &&
Date.now() < lastCheck * 1000 + THROTTLE_MS) {
// Throttled.
return filterSince(data, requestedSince);
}
const fetchOptions = Object.assign(Object.assign({}, options), { since: new Date(Math.max(requestedSince, lastStored) * 1000) });
const [newData] = await fetchMeasureReadings(id, fetchOptions);
mergeReadings(data, newData);
storedSince = Math.min(requestedSince, storedSince);
store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });
return filterSince(data, requestedSince);
};
const mergeReadings = (first, second) => {
if (!second.length)
return;
let firstPos = first.length - 1;
while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {
--firstPos;
}
first.splice(firstPos + 1, Infinity, ...second);
};
const parseReadings = (items) => {
const ranges = {};
for (const { measure, dateTime, value } of items) {
if (ranges[measure] == null) {
ranges[measure] = [];
}
ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);
}
const rangesById = {};
for (const [key, range] of Object.entries(ranges)) {
rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;
}
return rangesById;
};
const drawMeasureWidget = async (parentEl, measureId, options = {}) => {
var _a;
// Get readings for the last 7 days in local time.
const since = startOfDay(null, -7, true);
let data = [];
// Get the right API.
const parts = measureId.split('/');
const id = (_a = parts.pop()) !== null && _a !== void 0 ? _a : '';
const api = parts.length === 0 ? 'flood' : parts[0];
switch (api) {
case 'flood':
data = await getMeasureReadings(id, { since });
}
// Clear the GUI deck.
parentEl.replaceChildren();
const measure = parseMeasureId(measureId);
const { unit } = translateMeasureProperties(measure);
const series1 = { data, unit, formatter: round3 };
// Set max/min options for plot from widget options.
if (options.riverDataWidgetMaxValue != null) {
series1.max = parseFloat(options.riverDataWidgetMaxValue);
}
if (options.riverDataWidgetMinValue != null) {
series1.min = parseFloat(options.riverDataWidgetMinValue);
}
// Deal with no data.
if (data.length === 0) {
const minTime = since.valueOf() / 1000;
const maxTime = minTime + 86400 * 7;
const chartOptions = { minTime, maxTime };
const chart = new Chart(parentEl, [series1], chartOptions);
chart.render();
return;
}
const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;
const maxTime = startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;
const chartOptions = {
minTime,
maxTime,
// attribution: `www.riverdata.co.uk/station/${measure.stationId}`,
};
const chart = new Chart(parentEl, [series1], chartOptions);
chart.render();
};
/**
* RiverDataWidget https://github.com/pb-uk/river-data-widget.
*
* @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.
* @license AGPL-3.0-or-later see LICENSE.md.
*/
const version = '1.2.0';
export { drawMeasureWidget, version };
//# sourceMappingURL=index.mjs.map
{"version":3,"file":"index.mjs","sources":["src/helpers/format.ts","src/flood-monitoring-api/error.ts","src/flood-monitoring-api/measure.ts","src/helpers/dom.ts","src/helpers/time.ts","src/widget/chart.ts","src/flood-monitoring-api/api.ts","src/flood-monitoring-api/store.ts","src/flood-monitoring-api/reading.ts","src/widget/render.ts","src/index.ts"],"sourcesContent":["export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n for (const [key, value] of Object.entries(attributes)) {\n el.setAttribute(key, `${value}`);\n }\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n for (const [key, value] of Object.entries(styles)) {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n }\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected strokeWidth = 2;\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected plotWidth = this.width - this.strokeWidth;\n\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n\n // If there is no data show a message.\n if (data.length === 0) {\n const x = this.plotWidth / 2;\n const y = this.plotHeight / 2;\n this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle'));\n return;\n }\n\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Value label.\n ...this.createLargeLabel(x, y, `${v} ${unit}`),\n // Time label.\n ...this.createLabel(\n x,\n y,\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const { data } = this.series[0];\n // Don't do anything we don't have to!\n if (data.length === 0) return;\n\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n\n protected createLabel(x: number, y: number, text: string, anchor = 'end') {\n return [\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n { fill: this.plotColor },\n text\n ),\n ];\n }\n\n protected createLargeLabel(\n x: number,\n y: number,\n text: string,\n anchor = 'end'\n ) {\n return [\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n text\n ),\n ];\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 };\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n for (const [, value] of data) {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n }\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n for (const { measure, dateTime, value } of items) {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n }\n\n const rangesById: Record<string, Reading[]> = {};\n for (const [key, range] of Object.entries(ranges)) {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n }\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings as getFloodMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\nimport type { Reading } from '../flood-monitoring-api/reading';\n\nexport const drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n let data: Reading[] = [];\n\n // Get the right API.\n const parts = measureId.split('/');\n const id = parts.pop() ?? '';\n const api = parts.length === 0 ? 'flood' : parts[0];\n switch (api) {\n case 'flood':\n data = await getFloodMeasureReadings(id, { since });\n }\n\n // Clear the GUI deck.\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n\n // Deal with no data.\n if (data.length === 0) {\n const minTime = since.valueOf() / 1000;\n const maxTime = minTime + 86400 * 7;\n const chartOptions = { minTime, maxTime };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n return;\n }\n\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { drawMeasureWidget } from './widget/render';\n\nexport const version = '1.2.0';\n"],"names":["getFloodMeasureReadings"],"mappings":"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"}
export { version } from '.';
export type RiverDataWidgetErrorInfo = Record<string, unknown>;
export declare class RiverDataWidgetError extends Error {
info: RiverDataWidgetErrorInfo;
constructor(msg: string, info?: RiverDataWidgetErrorInfo);
}
export interface ApiResponse<T> {
data: {
items: T;
};
response: Response;
}
export interface ApiParameters {
since?: string;
_sorted?: '';
}
export declare const apiFetch: (path: string, query?: {}) => Promise<ApiResponse<unknown>>;
/**
* Convert a Date to a format recognized by the EA API for a query parameter.
*
* @param date Convert from.
* @returns A string in the EA API query parameter format.
*/
export declare const toTimeParameter: (date: Date) => string;
export declare class FloodMonitoringApiError extends Error {
info: Record<string, unknown>;
constructor(msg: string, info?: Record<string, unknown>);
}
export { getMeasureReadings } from './reading';
export { parseMeasureId };
declare const parseMeasureId: (measureId: string) => {
stationId: string;
parameter: string;
qualifier: string;
type: string;
interval: string;
unit: string;
qualifiedParameter: string;
};
export declare const translateMeasureProperties: (measure: Record<string, string>) => Record<string, string>;
/**
* Internal format for readings.
*/
export type Reading = [
timestamp: number,
value: number
];
/**
* Internal format for readings.
*/
export interface ReadingOptions {
since?: Date;
}
export declare const filterSince: (data: Reading[], since: number) => Reading[];
/**
* Get the readings for a measure.
*
* @todo Caching and throttling.
*
* @param id The EA measure id.
* @returns A promise for an array of readings for the measure.
*/
export declare const getMeasureReadings: (id: string, options?: ReadingOptions) => Promise<Reading[]>;
export declare const mergeReadings: (first: Reading[], second: Reading[]) => void;
declare class Store {
clear(destroy?: boolean): void;
get(key: string): any;
has(key: string): boolean;
/**
* Detect active localStorage.
*
* @returns true iff localStorage for the widget is active.
*/
isActive(): boolean;
keys(): string[];
set(key: string, value: unknown): void;
unset(key: string): boolean;
}
export declare const useStore: () => Store;
export {};
/**
* RiverDataWidget https://github.com/pb-uk/river-data-widget.
*
* @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.
* @license AGPL-3.0-or-later see LICENSE.md.
*/
export { createElement, createSvgElement, setAttributes, setStyles };
type AttributeList = Record<string, string | number>;
declare const setAttributes: <T extends HTMLElement | SVGElement>(el: T, attributes: AttributeList) => T;
declare const setStyles: <T extends HTMLElement | SVGElement>(el: T, styles: AttributeList) => T;
declare const createElement: (name?: string, attributes?: AttributeList, styles?: AttributeList, innerHTML?: string | false) => HTMLElement;
declare const createSvgElement: (name?: string, attributes?: AttributeList, styles?: AttributeList, innerHTML?: string | false) => SVGElement;
export declare const FONT_STACK = "-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif";
export declare const round3: (value: number) => string;
export declare const MINUTE_MS = 60000;
export declare const DAY_MS = 86400000;
/**
* Get the Date at the start of a day in UTC or local time.
*
* @param offset
* @param timeZone The time zone offset in minutes, or set to `true` to use the
* local time zone (`false`, the default, uses UTC).
* @returns The reqested date.
*/
export declare const startOfDay: (date?: Date | null, offset?: number, timeZone?: boolean | number) => Date;
/**
* | | long |short|narrow|numeric|2-digit|
* |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|
* | weekday | Monday | Mon | M | | |
* | era | Anno Domini | AD | A | | |
* | year | | | | 2012 | 12 |
* | month | March | Mar | M | 3 | 03 |
* | day | | | | 1 | 01 |
* | hour | | | | 1 | 01 |
* | minute | | | | 1 | 01 |
* | second | | | | 1 | 01 |
*
* * fractionalSecondDigits: 1, 2 or 3 for number of digits.
* * timeZoneName: long (Pacific Standard Time), short (PST),
* longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),
* shortGeneric (PT).
*/
export declare const dateFormatter: Intl.DateTimeFormat;
export declare const timeFormatter: Intl.DateTimeFormat;
export declare const dddFormatter: Intl.DateTimeFormat;
export declare const dMmmFormatter: Intl.DateTimeFormat;
/**
* RiverDataWidget https://github.com/pb-uk/river-data-widget.
*
* @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.
* @license AGPL-3.0-or-later see LICENSE.md.
*/
export { drawMeasureWidget } from './widget/render';
export declare const version = "1.2.0";
export interface ChartOptions {
minTime?: number;
maxTime?: number;
attribution?: string;
}
export interface ChartScaleLimits {
minTime: number;
maxTime: number;
timeScale: number;
minValue: number;
maxValue: number;
valueScale: number;
}
export interface ChartSeries {
data: TimeSeriesValue[];
min?: number;
max?: number;
unit?: string;
formatter?: (value: number) => string;
}
export type TimeSeriesValue = [
ts: number,
v: number
];
export declare class Chart {
protected strokeWidth: number;
protected fontSizePx: number;
protected el: SVGElement;
protected series: ChartSeries[];
protected options: ChartOptions;
protected width: number;
protected height: number;
protected plotHeight: number;
protected plotWidth: number;
protected limits?: ChartScaleLimits;
protected plotColor: string;
protected labelBg: string;
protected labelBgWidth: string;
protected attribution: string;
protected styles: {
'font-family': string;
'font-size': string;
display: string;
margin: string;
'max-width': string;
};
constructor(el: HTMLElement, series: ChartSeries[], options?: ChartOptions);
getLimits(): ChartScaleLimits;
getHorizontalGridlines(): SVGElement[];
getTimeScale(): SVGElement[];
render(): void;
plotLastValue(): void;
plotData(): void;
protected createLabel(x: number, y: number, text: string, anchor?: string): SVGElement[];
protected createLargeLabel(x: number, y: number, text: string, anchor?: string): SVGElement[];
}
export declare const getLimits: (data: TimeSeriesValue[]) => {
minTime: number;
maxTime: number;
minValue: number;
maxValue: number;
};
export declare const getInterval: (range: number, maxDivisions: number) => number[];
import type { WidgetOptions } from './index';
export declare const drawFlowGauge: (el: HTMLElement, value: number, options: WidgetOptions) => void;
export interface WidgetOptions {
flowSectors?: number[];
flowSectorBackgrounds?: string[];
}
export declare const drawMeasureWidget: (parentEl: HTMLElement, measureId: string, options?: Record<string, unknown>) => Promise<void>;
/**
* Load a widget specified by a DOM element.
*/
export declare const loadWidget: (el: HTMLElement | string) => void;
+12
-9
{
"name": "river-data-widget",
"version": "1.1.0",
"version": "1.2.0",
"description": "A web widget to display river flow and other data.",
"type": "module",
"browser": "dist/river-data-widget.min.js",
"types": "types",
"main": "index.cjs",
"module": "index.mjs",
"browser": "index.min.js",
"scripts": {
"build": "npm run clean && npm run lint && npm run test && rollup -c",
"clean": "rimraf dist",
"build": "npm run clean && npm run lint:check && npm run test && rollup -c && tsc --project tsconfig.types.json",
"clean": "rimraf types \"index.*\" --glob",
"coverage": "rimraf coverage && c8 -r html -r text npm run test:unit",
"lint": "eslint . && prettier . --check",
"lint:fix": "eslint . --fix && prettier . --write",
"lint": "eslint . --fix && prettier . --write",
"lint:check": "eslint . && prettier . --check",
"test": "npm run test:unit",

@@ -33,7 +36,7 @@ "test:unit": "mocha src/**/*.spec.ts"

"files": [
"dist"
"index.*",
"types"
],
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",

@@ -47,3 +50,3 @@ "@rollup/plugin-terser": "^0.4.2",

"@typescript-eslint/parser": "^5.44.0",
"c8": "^7.12.0",
"c8": "^8.0.0",
"camelcase": "^7.0.0",

@@ -50,0 +53,0 @@ "chai": "^4.3.7",

@@ -12,5 +12,5 @@ # river-data-widget

style="max-width: 480px; margin: 1em 0;"
data-river-data-widget="measure:3400TH-flow--i-15_min-m3_s"
data-river-data-widget="measure:flood/3400TH-flow--i-15_min-m3_s"
data-river-data-widget-min-value="0"
></div>
```
/*! RiverDataWidget v1.1.0 2023-05-31 14:34:01
*! https://github.com/pb-uk/river-data-widget#readme
*! Copyright (C) 2023 pbuk (https://github.com/pb-uk).
*! License MIT.
*/
var version = "1.1.0";
class RiverDataWidgetError extends Error {
constructor(msg, info = {}) {
super(msg);
this.name = 'RiverDataWidgetError';
this.info = info;
}
}
const FONT_STACK = '-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif';
const round3 = (value) => value < 100 ? value.toPrecision(3) : Math.round(value).toString();
class FloodMonitoringApiError extends Error {
constructor(msg, info = {}) {
super(msg);
this.name = 'FloodMonitoringApiError';
this.info = info;
}
}
const parseMeasureId = (measureId) => {
// ............base/ stat-paramet-qualifi- type -interva-unit
const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;
const matches = measureId.match(regExp);
if (matches === null) {
throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });
}
const [unit, interval, type, qualifier, parameter, stationId] = matches.reverse();
const qualifiedParameter = qualifier.length
? `${parameter}-${qualifier}`
: parameter;
return {
stationId,
parameter,
qualifier,
type,
interval,
unit,
qualifiedParameter,
};
};
const measureTranslations = {
unit: {
m3_s: 'm³/s',
mAOD: 'm',
mASD: 'm',
},
qualifiedParameter: {
'level-stage': 'level',
'level-downstage': 'downstream level',
},
};
const translateMeasureProperties = (measure) => {
const translated = {};
for (const prop in measure) {
const value = measure[prop];
if (measureTranslations[prop] && measureTranslations[prop][value]) {
translated[prop] = measureTranslations[prop][value];
}
else {
translated[prop] = value;
}
}
return translated;
};
/**
* RiverDataWidget https://github.com/pb-uk/river-data-widget.
*
* @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.
* @license AGPL-3.0-or-later see LICENSE.md.
*/
const setAttributes = (el, attributes) => {
Object.entries(attributes).forEach(([key, value]) => {
el.setAttribute(key, `${value}`);
});
return el;
};
const setStyles = (el, styles) => {
Object.entries(styles).forEach(([key, value]) => {
// Workaround (el.style.setProperty uses kebab-case keys).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
el.style[key] = value;
});
return el;
};
const createSvgElement = (name = 'svg', attributes = {}, styles = {}, innerHTML = false) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', name);
if (innerHTML !== false) {
el.innerHTML = innerHTML;
}
return setStyles(setAttributes(el, attributes), styles);
};
const MINUTE_MS = 60000;
// const HOUR_MS = 3600000;
const DAY_MS = 86400000;
/**
* Get the Date at the start of a day in UTC or local time.
*
* @param offset
* @param timeZone The time zone offset in minutes, or set to `true` to use the
* local time zone (`false`, the default, uses UTC).
* @returns The reqested date.
*/
const startOfDay = (date = null, offset = 0, timeZone = false) => {
if (timeZone === false) {
// Use UTC.
const base = date === null ? Date.now() : date.valueOf();
return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);
}
const now = new Date();
const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;
const local = now.valueOf() + tz * MINUTE_MS;
return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);
};
const timeFormatter = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZoneName: 'short',
});
const dddFormatter = new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
});
const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'short',
});
class Chart {
constructor(el, series, options = {}) {
var _a;
this.fontSizePx = 14;
this.width = 480; // 400;
this.height = 270; // 225;
this.plotHeight = this.height - this.fontSizePx * 4.5;
this.strokeWidth = 2;
this.plotColor = '#77C';
this.labelBg = 'rgba(255,255,255,0.5)';
this.labelBgWidth = '0.5em';
this.attribution = 'Uses Environment Agency data from the real-time API (Beta)';
// CSS settings.
// Just readable at 320x180.
// Good from 400x225.
// Perfect at 480x270 (font is 12px);
this.styles = {
'font-family': FONT_STACK,
'font-size': `${this.fontSizePx}px`,
display: 'block',
margin: 'auto',
'max-width': '150vh',
};
this.series = series;
this.options = options;
const viewBox = `0 0 ${this.width} ${this.height}`;
this.attribution = (_a = options.attribution) !== null && _a !== void 0 ? _a : this.attribution;
this.el = createSvgElement('svg', { viewBox }, this.styles);
el.append(this.el);
}
getLimits() {
if (this.limits == null) {
throw new FloodMonitoringApiError('Chart axis limits have not been set');
}
return this.limits;
}
getHorizontalGridlines() {
const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } = this.getLimits();
const xOffset = this.strokeWidth / 2;
const yOffset = this.plotHeight;
const x1 = xOffset;
const x2 = xOffset + (maxTime - minTime) * timeScale;
// Horizontal grid lines.
const stroke = '#ddd';
const lines = createSvgElement('g', { stroke });
const labels = createSvgElement('g');
const valueRange = maxValue - minValue;
// Horizontal grid interval.
const [interval, exponent] = getInterval(valueRange, 9);
const factor = 10 ** -exponent;
const base = Math.ceil((minValue * factor) / interval + 1) * interval;
let i = 0;
let current = base / factor;
while (current < maxValue) {
const y1 = yOffset - (current - minValue) * valueScale;
lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));
labels.append(createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`));
++i;
current = (base + i * interval) / factor;
}
const timeAxisLine = createSvgElement('line', { x1, y1: yOffset, x2, y2: yOffset }, { stroke: '#777' });
return [lines, labels, timeAxisLine];
}
getTimeScale() {
const { minTime, maxTime, timeScale } = this.getLimits();
const xOffset = this.strokeWidth / 2;
const yOffset = this.plotHeight + this.strokeWidth / 2;
const y1 = yOffset + this.fontSizePx * 3;
const y2 = yOffset - this.plotHeight;
// Vertical grid lines.
const stroke = '#ddd';
const lines = createSvgElement('g', { stroke });
const labels = createSvgElement('g');
// Vertical grid interval.
const base = minTime;
const interval = 86400;
let i = 0;
let current = base;
const labelOffset = 43200 * timeScale;
const fill = '#444';
while (current <= maxTime) {
const x1 = xOffset + (current - minTime) * timeScale;
const d = new Date(current * 1000);
// lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));
lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));
labels.append(createSvgElement('text', {
x: x1 + labelOffset,
y: y1 - this.fontSizePx * 1.8,
'text-anchor': 'middle',
}, { fill }, `${dddFormatter.format(d)}`), createSvgElement('text', {
x: x1 + labelOffset,
y: y1 - this.fontSizePx * 0.5,
'text-anchor': 'middle',
}, { fill }, `${dMmmFormatter.format(d)}`));
++i;
current = base + i * interval;
}
return [lines, labels];
}
render() {
var _a, _b, _c, _d;
// Calculate axis scales.
const limits = getLimits(this.series[0].data);
limits.minValue = (_a = this.series[0].min) !== null && _a !== void 0 ? _a : limits.minValue;
limits.maxValue = (_b = this.series[0].max) !== null && _b !== void 0 ? _b : limits.maxValue;
limits.minTime = (_c = this.options.minTime) !== null && _c !== void 0 ? _c : limits.minTime;
limits.maxTime = (_d = this.options.maxTime) !== null && _d !== void 0 ? _d : limits.maxTime;
this.limits = Object.assign(Object.assign({}, limits), { valueScale: (this.plotHeight - this.strokeWidth) /
(limits.maxValue - limits.minValue), timeScale: (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime) });
// Time axis.
const [timeLines, timeLabels] = this.getTimeScale();
this.el.append(timeLines);
// Value axis.
const [valueLines, valueLabels, timeAxisLine] = this.getHorizontalGridlines();
this.el.append(valueLines);
this.el.append(timeAxisLine);
this.plotData();
// Plot labels on top of the line.
this.el.append(timeLabels);
this.el.append(valueLabels);
this.el.append(createSvgElement('text', {
x: this.width / 2,
'text-anchor': 'middle',
y: this.height - this.fontSizePx * 0.5,
}, { fill: '#595959' }, this.attribution));
this.plotLastValue();
}
plotLastValue() {
const { data, unit, formatter } = this.series[0];
const [time, value] = data[data.length - 1];
const { minTime, timeScale, maxValue, minValue } = this.getLimits();
const v = formatter == null ? value : formatter(value);
const xOffset = this.strokeWidth / 2;
// const yOffset = this.plotHeight - this.strokeWidth / 2;
const x = xOffset + (time - minTime) * timeScale;
const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;
const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;
this.el.append(
// Background for value label.
createSvgElement('text', { x, y, 'text-anchor': 'end' }, {
'font-size': '1.5em',
'font-weight': 'bold',
stroke: this.labelBg,
'stroke-width': this.labelBgWidth,
}, `${v} ${unit}`),
// Value label.
createSvgElement('text', { x, y, 'text-anchor': 'end' }, { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' }, `${v} ${unit}`),
// Background for time label.
createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' }, {
stroke: this.labelBg,
'stroke-width': this.labelBgWidth,
}, `${timeFormatter.format(new Date(time * 1000))}`),
// Time label.
createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' }, { fill: this.plotColor }, `${timeFormatter.format(new Date(time * 1000))}`));
}
plotData() {
const xOffset = this.strokeWidth / 2;
const yOffset = this.plotHeight - this.strokeWidth / 2;
const { data } = this.series[0];
const { minTime, timeScale, minValue, valueScale } = this.getLimits();
// First data point.
const x = xOffset + (data[0][0] - minTime) * timeScale;
const y = yOffset - (data[0][1] - minValue) * valueScale;
const points = [`M${x},${y}`];
// Remaining data points.
for (let i = 1; i < data.length; ++i) {
const x = xOffset + (data[i][0] - minTime) * timeScale;
const y = yOffset - (data[i][1] - minValue) * valueScale;
points.push(`L${x},${y}`);
}
// Plot the data.
const path = createSvgElement('path', {
d: points.join(''),
stroke: this.plotColor,
'stroke-width': this.strokeWidth,
fill: 'none',
});
this.el.append(path);
}
}
const getLimits = (data) => {
if (data.length < 1) {
throw new Error('Readings must not be empty');
}
const minTime = data[0][0];
const maxTime = data[data.length - 1][0];
let minValue = Infinity;
let maxValue = -minValue;
data.forEach(([, value]) => {
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
});
return { minTime, maxTime, minValue, maxValue };
};
const getInterval = (range, maxDivisions) => {
const exponent = Math.floor(Math.log10(range)) - 1;
const k = range / (maxDivisions * 10 ** exponent);
const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;
return [mantissa, exponent];
};
// There is no need to be secure about this!
const baseUrl = 'http://environment.data.gov.uk/flood-monitoring';
const apiFetch = async (path, query = {}) => {
const queryString = new URLSearchParams(query).toString();
const uri = queryString
? `${baseUrl}${path}?${queryString}`
: `${baseUrl}${path}`;
const response = await fetch(uri);
return { data: await response.json(), response };
};
/**
* Convert a Date to a format recognized by the EA API for a query parameter.
*
* @param date Convert from.
* @returns A string in the EA API query parameter format.
*/
const toTimeParameter = (date) => {
return date.toISOString().substring(0, 19) + 'Z';
};
/*
Useful response headers
Date: 'Sat, 13 May 2023 09:14:07 GMT',
last-modified: Sat, 13 May 2023 09:03:13 GMT,
Response meta:
publisher: 'Environment Agency',
license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',
documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',
version: '0.9',
comment: 'Status: Beta service',
hasFormat: [
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z",
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z",
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z",
"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z"
],
*/
const prefix = 'riverDataWidget';
const addPrefix = (key) => `${prefix}|${key}`;
let instance;
class Store {
clear(destroy = false) {
for (const key of this.keys()) {
localStorage.removeItem(addPrefix(key));
}
if (destroy) {
localStorage.removeItem(prefix);
return;
}
localStorage.setItem(prefix, JSON.stringify([]));
}
get(key) {
const value = localStorage.getItem(addPrefix(key));
return value === null ? null : JSON.parse(value);
}
has(key) {
return this.keys().includes(key);
}
/**
* Detect active localStorage.
*
* @returns true iff localStorage for the widget is active.
*/
isActive() {
return localStorage.getItem(prefix) !== null;
}
keys() {
const storedKeys = localStorage.getItem(prefix);
return storedKeys === null ? [] : JSON.parse(storedKeys);
}
set(key, value) {
const json = JSON.stringify(value);
const storedKeys = localStorage.getItem(prefix);
const keys = storedKeys === null ? [] : JSON.parse(storedKeys);
if (!keys.includes(key)) {
keys.push(key);
localStorage.setItem(prefix, JSON.stringify(keys));
}
localStorage.setItem(addPrefix(key), json);
}
unset(key) {
// Remove it before we do anything else.
localStorage.removeItem(addPrefix(key));
// Then remove it from the list of keys.
const storedKeys = localStorage.getItem(prefix);
const keys = storedKeys === null ? [] : JSON.parse(storedKeys);
const index = keys.indexOf(key);
// If it doesn't exist we don't have to remove it.
if (index === -1)
return false;
keys.splice(index, 1);
localStorage.setItem(prefix, JSON.stringify(keys));
return true;
}
}
const useStore = () => {
if (!instance) {
instance = new Store();
}
return instance;
};
// Throttle requests to five minutes.
const THROTTLE_MS = 5 * MINUTE_MS;
/**
* Fetch the readings for a measure.
*
* @param id The EA measure id.
* @returns A promise for an array of readings for the measure.
*/
const fetchMeasureReadings = async (id, options = {}) => {
// Set the parameters for the request.
const params = { _sorted: '' };
if (options.since) {
params.since = toTimeParameter(options.since);
}
// Get the response, casting the items to ReadingDTOs.
const response = (await apiFetch(`/id/measures/${id}/readings`, params));
return [parseReadings(response.data.items)[id] || [], response];
};
const filterSince = (data, since) => {
const position = data.findIndex((reading) => reading[0] >= since);
return position < 0 ? [] : data.slice(position);
};
/**
* Get the readings for a measure.
*
* @todo Caching and throttling.
*
* @param id The EA measure id.
* @returns A promise for an array of readings for the measure.
*/
const getMeasureReadings = async (id, options = {}) => {
// Get the saved readings.
const key = `readings|${id}`;
const store = useStore();
const stored = store.get(key) || {
data: [],
lastCheck: 0,
storedSince: Infinity,
};
const { data, lastCheck } = stored;
let { storedSince } = stored;
const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;
// Discard any older than 30 days.
while (data.length && data[0][0] < discardBefore) {
[storedSince] = data[0];
data.shift();
}
// If we have data early enough apply throttle.
const lastStored = data.length ? data[data.length - 1][0] : 0;
const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;
if (storedSince <= requestedSince &&
Date.now() < lastCheck * 1000 + THROTTLE_MS) {
// Throttled.
return filterSince(data, requestedSince);
}
const fetchOptions = Object.assign(Object.assign({}, options), { since: new Date(Math.max(requestedSince, lastStored) * 1000) });
const [newData] = await fetchMeasureReadings(id, fetchOptions);
mergeReadings(data, newData);
storedSince = Math.min(requestedSince, storedSince);
store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });
return filterSince(data, requestedSince);
};
const mergeReadings = (first, second) => {
if (!second.length)
return;
let firstPos = first.length - 1;
while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {
--firstPos;
}
first.splice(firstPos + 1, Infinity, ...second);
};
const parseReadings = (items) => {
const ranges = {};
items.forEach(({ measure, dateTime, value }) => {
if (ranges[measure] == null) {
ranges[measure] = [];
}
ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);
});
const rangesById = {};
Object.entries(ranges).forEach(([key, range]) => {
rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;
});
return rangesById;
};
const drawMeasureWidget = async (parentEl, measureId, options = {}) => {
// Get readings for the last 7 days in local time.
const since = startOfDay(null, -7, true);
const data = await getMeasureReadings(measureId, { since });
parentEl.replaceChildren();
const measure = parseMeasureId(measureId);
const { unit } = translateMeasureProperties(measure);
// const [time, value] = data[data.length - 1];
// const v = round3(value);
// const param = m.qualifiedParameter;
// const station = measure.stationId;
// const unit = m.unit;
// let textEl = createElement('div');
// const d = dateFormatter.format(new Date(time * 1000));
// const t = timeFormatter.format(new Date(time * 1000));
// textEl.innerHTML = `The most recent ${param} reading for station ${station} was ${v} ${unit} at ${t} on ${d}.`;
// widgetEl.append(textEl);
const series1 = { data, unit, formatter: round3 };
// Set max/min options for plot from widget options.
if (options.riverDataWidgetMaxValue != null) {
series1.max = parseFloat(options.riverDataWidgetMaxValue);
}
if (options.riverDataWidgetMinValue != null) {
series1.min = parseFloat(options.riverDataWidgetMinValue);
}
const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;
const maxTime = startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;
const chartOptions = {
minTime,
maxTime,
// attribution: `www.riverdata.co.uk/station/${measure.stationId}`,
};
const chart = new Chart(parentEl, [series1], chartOptions);
chart.render();
};
/**
* Load a widget specified by a DOM element.
*/
const loadWidget = (el) => {
var _a, _b;
// Get the target element from a query selector if necessary and check it
// exists.
const targetEl = typeof el === 'string' ? document.querySelector(el) : el;
if (targetEl === null) {
throw new Error('Target element not found');
}
// Parse element for widget type and options.
const widgetIdParts = (_b = (_a = targetEl.dataset.riverDataWidget) === null || _a === void 0 ? void 0 : _a.split(':')) !== null && _b !== void 0 ? _b : [];
const type = widgetIdParts.shift();
const id = widgetIdParts.join(':');
const options = targetEl.dataset;
switch (type) {
case 'measure':
drawMeasureWidget(targetEl, id, options);
break;
// The 'station' widget is experimental in v1.0 and should not be used.
// case 'station':
// break;
default:
throw new RiverDataWidgetError('Unknown widget definition', { type, id });
}
};
const autoload = async () => {
document.querySelectorAll('[data-river-data-widget]').forEach((el) => {
try {
loadWidget(el);
}
catch (error) {
console.error(error, { error });
}
});
};
if (document.readyState === 'loading') {
// Loading hasn't finished yet.
document.addEventListener('DOMContentLoaded', autoload);
}
else {
// `DOMContentLoaded` has already fired.
autoload();
}
export { version };
//# sourceMappingURL=index.js.map
{"version":3,"file":"index.js","sources":["../src/error.ts","../src/helpers/format.ts","../src/flood-monitoring-api/error.ts","../src/flood-monitoring-api/measure.ts","../src/helpers/dom.ts","../src/helpers/time.ts","../src/widget/chart.ts","../src/flood-monitoring-api/api.ts","../src/flood-monitoring-api/store.ts","../src/flood-monitoring-api/reading.ts","../src/widget/render.ts","../src/autoload.ts"],"sourcesContent":["export type RiverDataWidgetErrorInfo = Record<string, unknown>;\n\nexport class RiverDataWidgetError extends Error {\n public info: RiverDataWidgetErrorInfo;\n\n constructor(msg: string, info: RiverDataWidgetErrorInfo = {}) {\n super(msg);\n this.name = 'RiverDataWidgetError';\n this.info = info;\n }\n}\n","export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n Object.entries(attributes).forEach(([key, value]) => {\n el.setAttribute(key, `${value}`);\n });\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n Object.entries(styles).forEach(([key, value]) => {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n });\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected strokeWidth = 2;\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': 'end' },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n `${v} ${unit}`\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': 'end' },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n `${v} ${unit}`\n ),\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n `${timeFormatter.format(new Date(time * 1000))}`\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' },\n { fill: this.plotColor },\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { data } = this.series[0];\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n throw new Error('Readings must not be empty');\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n data.forEach(([, value]) => {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n });\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n items.forEach(({ measure, dateTime, value }) => {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n });\n\n const rangesById: Record<string, Reading[]> = {};\n Object.entries(ranges).forEach(([key, range]) => {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n });\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\n\nconst drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n\n const data = await getMeasureReadings(measureId, { since });\n\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n // const [time, value] = data[data.length - 1];\n // const v = round3(value);\n // const param = m.qualifiedParameter;\n // const station = measure.stationId;\n // const unit = m.unit;\n\n // let textEl = createElement('div');\n // const d = dateFormatter.format(new Date(time * 1000));\n // const t = timeFormatter.format(new Date(time * 1000));\n // textEl.innerHTML = `The most recent ${param} reading for station ${station} was ${v} ${unit} at ${t} on ${d}.`;\n // widgetEl.append(textEl);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n\n // The 'station' widget is experimental in v1.0 and should not be used.\n // case 'station':\n // break;\n\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","import { loadWidget } from './widget/render';\n\nconst autoload = async () => {\n document.querySelectorAll('[data-river-data-widget]').forEach((el) => {\n try {\n loadWidget(<HTMLElement>el);\n } catch (error) {\n console.error(error, { error });\n }\n });\n};\n\nif (document.readyState === 'loading') {\n // Loading hasn't finished yet.\n document.addEventListener('DOMContentLoaded', autoload);\n} else {\n // `DOMContentLoaded` has already fired.\n autoload();\n}\n"],"names":[],"mappings":"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"}
/*! RiverDataWidget v1.1.0 2023-05-31 14:34:01
*! https://github.com/pb-uk/river-data-widget#readme
*! Copyright (C) 2023 pbuk (https://github.com/pb-uk).
*! License MIT.
*/
var RiverDataWidget=function(t){"use strict";class e extends Error{constructor(t,e={}){super(t),this.name="RiverDataWidgetError",this.info=e}}const i=t=>t<100?t.toPrecision(3):Math.round(t).toString();class n extends Error{constructor(t,e={}){super(t),this.name="FloodMonitoringApiError",this.info=e}}const s={unit:{m3_s:"m³/s",mAOD:"m",mASD:"m"},qualifiedParameter:{"level-stage":"level","level-downstage":"downstream level"}},a=(t="svg",e={},i={},n=!1)=>{const s=document.createElementNS("http://www.w3.org/2000/svg",t);return!1!==n&&(s.innerHTML=n),((t,e)=>(Object.entries(e).forEach((([e,i])=>{t.style[e]=i})),t))(((t,e)=>(Object.entries(e).forEach((([e,i])=>{t.setAttribute(e,`${i}`)})),t))(s,e),i)},o=864e5,r=(t=null,e=0,i=!1)=>{if(!1===i){const i=null===t?Date.now():t.valueOf();return new Date(Math.floor(i/o+e)*o)}const n=new Date,s=!0===i?n.getTimezoneOffset():i,a=n.valueOf()+6e4*s;return new Date(Math.floor(a/o+e)*o)},l=new Intl.DateTimeFormat("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1,timeZoneName:"short"}),h=new Intl.DateTimeFormat("en-GB",{weekday:"short"}),m=new Intl.DateTimeFormat("en-GB",{day:"numeric",month:"short"});class c{constructor(t,e,i={}){var n;this.fontSizePx=14,this.width=480,this.height=270,this.plotHeight=this.height-4.5*this.fontSizePx,this.strokeWidth=2,this.plotColor="#77C",this.labelBg="rgba(255,255,255,0.5)",this.labelBgWidth="0.5em",this.attribution="Uses Environment Agency data from the real-time API (Beta)",this.styles={"font-family":'-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif',"font-size":`${this.fontSizePx}px`,display:"block",margin:"auto","max-width":"150vh"},this.series=e,this.options=i;const s=`0 0 ${this.width} ${this.height}`;this.attribution=null!==(n=i.attribution)&&void 0!==n?n:this.attribution,this.el=a("svg",{viewBox:s},this.styles),t.append(this.el)}getLimits(){if(null==this.limits)throw new n("Chart axis limits have not been set");return this.limits}getHorizontalGridlines(){const{minTime:t,maxTime:e,timeScale:i,minValue:n,maxValue:s,valueScale:o}=this.getLimits(),r=this.strokeWidth/2,l=this.plotHeight,h=r,m=r+(e-t)*i,c=a("g",{stroke:"#ddd"}),d=a("g"),g=s-n,[f,p]=u(g,9),x=10**-p,w=Math.ceil(n*x/f+1)*f;let S=0,v=w/x;for(;v<s;){const t=l-(v-n)*o;c.append(a("line",{x1:h,y1:t,x2:m,y2:t})),d.append(a("text",{x:h+4,y:t+4},{},`${v}`)),++S,v=(w+S*f)/x}return[c,d,a("line",{x1:h,y1:l,x2:m,y2:l},{stroke:"#777"})]}getTimeScale(){const{minTime:t,maxTime:e,timeScale:i}=this.getLimits(),n=this.strokeWidth/2,s=this.plotHeight+this.strokeWidth/2,o=s+3*this.fontSizePx,r=s-this.plotHeight,l=a("g",{stroke:"#ddd"}),c=a("g"),d=t;let u=0,g=d;const f=43200*i,p="#444";for(;g<=e;){const e=n+(g-t)*i,s=new Date(1e3*g);l.append(a("line",{x1:e,y1:o,x2:e,y2:r})),c.append(a("text",{x:e+f,y:o-1.8*this.fontSizePx,"text-anchor":"middle"},{fill:p},`${h.format(s)}`),a("text",{x:e+f,y:o-.5*this.fontSizePx,"text-anchor":"middle"},{fill:p},`${m.format(s)}`)),++u,g=d+86400*u}return[l,c]}render(){var t,e,i,n;const s=d(this.series[0].data);s.minValue=null!==(t=this.series[0].min)&&void 0!==t?t:s.minValue,s.maxValue=null!==(e=this.series[0].max)&&void 0!==e?e:s.maxValue,s.minTime=null!==(i=this.options.minTime)&&void 0!==i?i:s.minTime,s.maxTime=null!==(n=this.options.maxTime)&&void 0!==n?n:s.maxTime,this.limits=Object.assign(Object.assign({},s),{valueScale:(this.plotHeight-this.strokeWidth)/(s.maxValue-s.minValue),timeScale:(this.width-this.strokeWidth)/(s.maxTime-s.minTime)});const[o,r]=this.getTimeScale();this.el.append(o);const[l,h,m]=this.getHorizontalGridlines();this.el.append(l),this.el.append(m),this.plotData(),this.el.append(r),this.el.append(h),this.el.append(a("text",{x:this.width/2,"text-anchor":"middle",y:this.height-.5*this.fontSizePx},{fill:"#595959"},this.attribution)),this.plotLastValue()}plotLastValue(){const{data:t,unit:e,formatter:i}=this.series[0],[n,s]=t[t.length-1],{minTime:o,timeScale:r,maxValue:h,minValue:m}=this.getLimits(),c=null==i?s:i(s),d=this.strokeWidth/2+(n-o)*r,u=(s-m)/(h-m)<.5,g=this.plotHeight*(u?0:.5)+2*this.fontSizePx;this.el.append(a("text",{x:d,y:g,"text-anchor":"end"},{"font-size":"1.5em","font-weight":"bold",stroke:this.labelBg,"stroke-width":this.labelBgWidth},`${c} ${e}`),a("text",{x:d,y:g,"text-anchor":"end"},{fill:this.plotColor,"font-size":"1.5em","font-weight":"bold"},`${c} ${e}`),a("text",{x:d,y:g+1.5*this.fontSizePx,"text-anchor":"end"},{stroke:this.labelBg,"stroke-width":this.labelBgWidth},`${l.format(new Date(1e3*n))}`),a("text",{x:d,y:g+1.5*this.fontSizePx,"text-anchor":"end"},{fill:this.plotColor},`${l.format(new Date(1e3*n))}`))}plotData(){const t=this.strokeWidth/2,e=this.plotHeight-this.strokeWidth/2,{data:i}=this.series[0],{minTime:n,timeScale:s,minValue:o,valueScale:r}=this.getLimits(),l=[`M${t+(i[0][0]-n)*s},${e-(i[0][1]-o)*r}`];for(let a=1;a<i.length;++a){const h=t+(i[a][0]-n)*s,m=e-(i[a][1]-o)*r;l.push(`L${h},${m}`)}const h=a("path",{d:l.join(""),stroke:this.plotColor,"stroke-width":this.strokeWidth,fill:"none"});this.el.append(h)}}const d=t=>{if(t.length<1)throw new Error("Readings must not be empty");const e=t[0][0],i=t[t.length-1][0];let n=1/0,s=-n;return t.forEach((([,t])=>{n=Math.min(n,t),s=Math.max(s,t)})),{minTime:e,maxTime:i,minValue:n,maxValue:s}},u=(t,e)=>{const i=Math.floor(Math.log10(t))-1,n=t/(e*10**i);return[n<=2?2:n<=5?5:10,i]},g="http://environment.data.gov.uk/flood-monitoring",f="riverDataWidget",p=t=>`${f}|${t}`;let x;class w{clear(t=!1){for(const t of this.keys())localStorage.removeItem(p(t));t?localStorage.removeItem(f):localStorage.setItem(f,JSON.stringify([]))}get(t){const e=localStorage.getItem(p(t));return null===e?null:JSON.parse(e)}has(t){return this.keys().includes(t)}isActive(){return null!==localStorage.getItem(f)}keys(){const t=localStorage.getItem(f);return null===t?[]:JSON.parse(t)}set(t,e){const i=JSON.stringify(e),n=localStorage.getItem(f),s=null===n?[]:JSON.parse(n);s.includes(t)||(s.push(t),localStorage.setItem(f,JSON.stringify(s))),localStorage.setItem(p(t),i)}unset(t){localStorage.removeItem(p(t));const e=localStorage.getItem(f),i=null===e?[]:JSON.parse(e),n=i.indexOf(t);return-1!==n&&(i.splice(n,1),localStorage.setItem(f,JSON.stringify(i)),!0)}}const S=async(t,e={})=>{const i={_sorted:""};e.since&&(i.since=e.since.toISOString().substring(0,19)+"Z");const n=await(async(t,e={})=>{const i=new URLSearchParams(e).toString(),n=i?`${g}${t}?${i}`:`${g}${t}`,s=await fetch(n);return{data:await s.json(),response:s}})(`/id/measures/${t}/readings`,i);return[D(n.data.items)[t]||[],n]},v=(t,e)=>{const i=t.findIndex((t=>t[0]>=e));return i<0?[]:t.slice(i)},y=async(t,e={})=>{const i=`readings|${t}`,n=(x||(x=new w),x),s=n.get(i)||{data:[],lastCheck:0,storedSince:1/0},{data:a,lastCheck:o}=s;let{storedSince:l}=s;const h=r(null,-8,!0).valueOf()/1e3;for(;a.length&&a[0][0]<h;)[l]=a[0],a.shift();const m=a.length?a[a.length-1][0]:0,c=e.since&&e.since.valueOf()/1e3||0;if(l<=c&&Date.now()<1e3*o+3e5)return v(a,c);const d=Object.assign(Object.assign({},e),{since:new Date(1e3*Math.max(c,m))}),[u]=await S(t,d);return k(a,u),l=Math.min(c,l),n.set(i,{lastCheck:Date.now()/1e3,data:a,storedSince:l}),v(a,c)},k=(t,e)=>{if(!e.length)return;let i=t.length-1;for(;i>=0&&t[i][0]>=e[0][0];)--i;t.splice(i+1,1/0,...e)},D=t=>{const e={};t.forEach((({measure:t,dateTime:i,value:n})=>{null==e[t]&&(e[t]=[]),e[t].unshift([new Date(i).valueOf()/1e3,n])}));const i={};return Object.entries(e).forEach((([t,e])=>{i[t.substring(t.lastIndexOf("/")+1)]=e})),i},$=async(t,e,a={})=>{const o=r(null,-7,!0),l=await y(e,{since:o});t.replaceChildren();const h=(t=>{const e=t.match(/(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/);if(null===e)throw new n("Cannot parse measure id",{measureId:t});const[i,s,a,o,r,l]=e.reverse();return{stationId:l,parameter:r,qualifier:o,type:a,interval:s,unit:i,qualifiedParameter:o.length?`${r}-${o}`:r}})(e),{unit:m}=(t=>{const e={};for(const i in t){const n=t[i];s[i]&&s[i][n]?e[i]=s[i][n]:e[i]=n}return e})(h),d={data:l,unit:m,formatter:i};null!=a.riverDataWidgetMaxValue&&(d.max=parseFloat(a.riverDataWidgetMaxValue)),null!=a.riverDataWidgetMinValue&&(d.min=parseFloat(a.riverDataWidgetMinValue));const u=r(new Date(1e3*l[0][0])).valueOf()/1e3,g=r(new Date(1e3*l[l.length-1][0]),1).valueOf()/1e3;new c(t,[d],{minTime:u,maxTime:g}).render()},b=async()=>{document.querySelectorAll("[data-river-data-widget]").forEach((t=>{try{(t=>{var i,n;const s="string"==typeof t?document.querySelector(t):t;if(null===s)throw new Error("Target element not found");const a=null!==(n=null===(i=s.dataset.riverDataWidget)||void 0===i?void 0:i.split(":"))&&void 0!==n?n:[],o=a.shift(),r=a.join(":"),l=s.dataset;if("measure"!==o)throw new e("Unknown widget definition",{type:o,id:r});$(s,r,l)})(t)}catch(t){console.error(t,{error:t})}}))};return"loading"===document.readyState?document.addEventListener("DOMContentLoaded",b):b(),t.version="1.1.0",t}({});
//# sourceMappingURL=river-data-widget.min.js.map
{"version":3,"file":"river-data-widget.min.js","sources":["../src/error.ts","../src/helpers/format.ts","../src/flood-monitoring-api/error.ts","../src/flood-monitoring-api/measure.ts","../src/helpers/dom.ts","../src/helpers/time.ts","../src/widget/chart.ts","../src/flood-monitoring-api/api.ts","../src/flood-monitoring-api/store.ts","../src/flood-monitoring-api/reading.ts","../src/widget/render.ts","../src/autoload.ts"],"sourcesContent":["export type RiverDataWidgetErrorInfo = Record<string, unknown>;\n\nexport class RiverDataWidgetError extends Error {\n public info: RiverDataWidgetErrorInfo;\n\n constructor(msg: string, info: RiverDataWidgetErrorInfo = {}) {\n super(msg);\n this.name = 'RiverDataWidgetError';\n this.info = info;\n }\n}\n","export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n Object.entries(attributes).forEach(([key, value]) => {\n el.setAttribute(key, `${value}`);\n });\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n Object.entries(styles).forEach(([key, value]) => {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n });\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected strokeWidth = 2;\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': 'end' },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n `${v} ${unit}`\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': 'end' },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n `${v} ${unit}`\n ),\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n `${timeFormatter.format(new Date(time * 1000))}`\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' },\n { fill: this.plotColor },\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { data } = this.series[0];\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n throw new Error('Readings must not be empty');\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n data.forEach(([, value]) => {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n });\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n items.forEach(({ measure, dateTime, value }) => {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n });\n\n const rangesById: Record<string, Reading[]> = {};\n Object.entries(ranges).forEach(([key, range]) => {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n });\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\n\nconst drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n\n const data = await getMeasureReadings(measureId, { since });\n\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n // const [time, value] = data[data.length - 1];\n // const v = round3(value);\n // const param = m.qualifiedParameter;\n // const station = measure.stationId;\n // const unit = m.unit;\n\n // let textEl = createElement('div');\n // const d = dateFormatter.format(new Date(time * 1000));\n // const t = timeFormatter.format(new Date(time * 1000));\n // textEl.innerHTML = `The most recent ${param} reading for station ${station} was ${v} ${unit} at ${t} on ${d}.`;\n // widgetEl.append(textEl);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n\n // The 'station' widget is experimental in v1.0 and should not be used.\n // case 'station':\n // break;\n\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","import { loadWidget } from './widget/render';\n\nconst autoload = async () => {\n document.querySelectorAll('[data-river-data-widget]').forEach((el) => {\n try {\n loadWidget(<HTMLElement>el);\n } catch (error) {\n console.error(error, { error });\n }\n });\n};\n\nif (document.readyState === 'loading') {\n // Loading hasn't finished yet.\n document.addEventListener('DOMContentLoaded', autoload);\n} else {\n // `DOMContentLoaded` has already fired.\n autoload();\n}\n"],"names":["RiverDataWidgetError","Error","constructor","msg","info","super","this","name","round3","value","toPrecision","Math","round","toString","FloodMonitoringApiError","measureTranslations","unit","m3_s","mAOD","mASD","qualifiedParameter","createSvgElement","attributes","styles","innerHTML","el","document","createElementNS","Object","entries","forEach","key","style","setStyles","setAttribute","setAttributes","DAY_MS","startOfDay","date","offset","timeZone","base","Date","now","valueOf","floor","tz","getTimezoneOffset","local","timeFormatter","Intl","DateTimeFormat","hour","minute","hour12","timeZoneName","dddFormatter","weekday","dMmmFormatter","day","month","Chart","series","options","fontSizePx","width","height","plotHeight","strokeWidth","plotColor","labelBg","labelBgWidth","attribution","display","margin","viewBox","_a","append","getLimits","limits","getHorizontalGridlines","minTime","maxTime","timeScale","minValue","maxValue","valueScale","xOffset","yOffset","x1","x2","lines","stroke","labels","valueRange","interval","exponent","getInterval","factor","ceil","i","current","y1","y2","x","y","getTimeScale","labelOffset","fill","d","format","render","data","min","_b","max","_c","_d","assign","timeLines","timeLabels","valueLines","valueLabels","timeAxisLine","plotData","plotLastValue","formatter","time","length","v","isHighLabel","points","push","path","join","Infinity","range","maxDivisions","log10","k","baseUrl","prefix","addPrefix","instance","Store","clear","destroy","keys","localStorage","removeItem","setItem","JSON","stringify","get","getItem","parse","has","includes","isActive","storedKeys","set","json","unset","index","indexOf","splice","fetchMeasureReadings","async","id","params","_sorted","since","toISOString","substring","response","query","queryString","URLSearchParams","uri","fetch","apiFetch","parseReadings","items","filterSince","position","findIndex","reading","slice","getMeasureReadings","store","stored","lastCheck","storedSince","discardBefore","shift","lastStored","requestedSince","fetchOptions","newData","mergeReadings","first","second","firstPos","ranges","measure","dateTime","unshift","rangesById","lastIndexOf","drawMeasureWidget","parentEl","measureId","replaceChildren","matches","match","type","qualifier","parameter","stationId","reverse","parseMeasureId","translated","prop","translateMeasureProperties","series1","riverDataWidgetMaxValue","parseFloat","riverDataWidgetMinValue","autoload","querySelectorAll","targetEl","querySelector","widgetIdParts","dataset","riverDataWidget","split","loadWidget","error","console","readyState","addEventListener"],"mappings":"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"}