From 3b7922ab45455ba9609fd3d4c91b06f43521cf39 Mon Sep 17 00:00:00 2001 From: LeeGordon83 Date: Fri, 27 Sep 2024 16:24:32 +0100 Subject: [PATCH] added links to flood warnings target areas in thresholds --- server/models/views/lib/find-min-threshold.js | 2 +- .../views/lib/process-imtd-thresholds.js | 27 ++- server/models/views/station.js | 6 +- server/routes/station.js | 1 + server/src/js/components/chart.js | 62 ++++--- server/src/js/components/map/tooltip.js | 158 ------------------ server/src/js/components/tooltip.js | 66 ++++++++ server/src/js/core.js | 4 + server/src/sass/application.scss | 1 + .../src/sass/components/_chart-controls.scss | 126 +++++++------- .../sass/components/_flood-impact-list.scss | 149 ++++++++++------- .../sass/components/_toggle-list-display.scss | 5 +- server/src/sass/components/_toggletip.scss | 42 ++--- server/src/sass/components/_tooltip.scss | 121 ++++++++++++++ server/views/partials/latest-levels.html | 4 +- server/views/station.html | 6 +- test/{models => data}/floods.js | 0 test/data/taThresholdsData.json | 30 ++++ test/data/warning.json | 19 +++ test/models/lib/process-imtd-thresholds.js | 21 +-- test/routes/target-area.js | 4 +- 21 files changed, 489 insertions(+), 365 deletions(-) delete mode 100644 server/src/js/components/map/tooltip.js create mode 100644 server/src/js/components/tooltip.js create mode 100644 server/src/sass/components/_tooltip.scss rename test/{models => data}/floods.js (100%) create mode 100644 test/data/warning.json diff --git a/server/models/views/lib/find-min-threshold.js b/server/models/views/lib/find-min-threshold.js index cf0909c36..360990474 100644 --- a/server/models/views/lib/find-min-threshold.js +++ b/server/models/views/lib/find-min-threshold.js @@ -31,7 +31,7 @@ function filterImtdThresholds (imtdThresholds) { return { alert: minObjectA ? minObjectA.value : null, - warning: minObjectW ? minObjectW.value : null + warning: minObjectW || null } } diff --git a/server/models/views/lib/process-imtd-thresholds.js b/server/models/views/lib/process-imtd-thresholds.js index 0d84f7e8f..549b6c17f 100644 --- a/server/models/views/lib/process-imtd-thresholds.js +++ b/server/models/views/lib/process-imtd-thresholds.js @@ -17,17 +17,26 @@ function processThreshold (threshold, stationStageDatum, stationSubtract, postPr function processImtdThresholds (imtdThresholds, stationStageDatum, stationSubtract, postProcess) { const thresholds = [] - const imtdThresholdWarning = processThreshold(imtdThresholds?.warning, stationStageDatum, stationSubtract, postProcess) + const imtdThresholdWarning = processThreshold(imtdThresholds?.warning?.value, stationStageDatum, stationSubtract, postProcess) + // Correct threshold value if value > zero (Above Ordnance Datum) [FSR-595] if (imtdThresholdWarning) { - // Correct threshold value if value > zero (Above Ordnance Datum) [FSR-595] - thresholds.push({ - id: 'warningThreshold', - description: 'Property flooding is possible above this level. One or more flood warnings may be issued', - shortname: 'Possible flood warnings', - value: imtdThresholdWarning - }) + if (imtdThresholds.warning.severity_value) { + const warningType = imtdThresholds.warning.severity_value === 3 ? 'Severe Flood Warning' : 'Flood Warning' + thresholds.push({ + id: 'warningThreshold', + description: `${warningType} issued: ${imtdThresholds.warning.ta_name}`, + shortname: 'Possible flood warnings', + value: imtdThresholdWarning + }) + } else { + thresholds.push({ + id: 'warningThreshold', + description: 'Property flooding is possible above this level. One or more flood warnings may be issued', + shortname: 'Possible flood warnings', + value: imtdThresholdWarning + }) + } } - const imtdThresholdAlert = processThreshold(imtdThresholds?.alert, stationStageDatum, stationSubtract, postProcess) if (imtdThresholdAlert) { thresholds.push({ diff --git a/server/models/views/station.js b/server/models/views/station.js index 904cd05da..0c1886542 100644 --- a/server/models/views/station.js +++ b/server/models/views/station.js @@ -170,7 +170,7 @@ class ViewModel { oneHourAgo.setHours(oneHourAgo.getHours() - 1) - // check if recent value is over one hour old0 + // check if recent value is over one hour old this.dataOverHourOld = new Date(this.recentValue.ts) < oneHourAgo this.recentValue.dateWhen = 'on ' + moment.tz(this.recentValue.ts, tz).format('D/MM/YY') @@ -248,7 +248,7 @@ class ViewModel { id: 'highest', value: this.station.porMaxValue, description: this.station.thresholdPorMaxDate - ? 'Water reaches the highest level recorded at this measuring station (recorded on ' + this.station.thresholdPorMaxDate + ')' + ? `Water reaches the highest level recorded at this measuring station (${this.station.thresholdPorMaxDate})` : 'Water reaches the highest level recorded at this measuring station', shortname: 'Highest level on record' }) @@ -268,7 +268,7 @@ class ViewModel { thresholds.push(...processedImtdThresholds) if (this.station.percentile5) { - // Only push typical range if it has a percentil5 + // Only push typical range if it has a percentile5 thresholds.push({ id: 'pc5', value: this.station.percentile5, diff --git a/server/routes/station.js b/server/routes/station.js index 907d37d64..23664da2e 100644 --- a/server/routes/station.js +++ b/server/routes/station.js @@ -10,6 +10,7 @@ module.exports = { handler: async (request, h) => { const { id } = request.params let { direction } = request.params + // const thresholdId = request.query.tid? // Convert human readable url to service parameter direction = direction === 'downstream' ? 'd' : 'u' diff --git a/server/src/js/components/chart.js b/server/src/js/components/chart.js index 227a6ee4e..914f90f42 100644 --- a/server/src/js/components/chart.js +++ b/server/src/js/components/chart.js @@ -2,13 +2,13 @@ // Chart component import '../utils' +import { area as d3Area, line as d3Line, curveMonotoneX } from 'd3-shape' import { axisBottom, axisLeft } from 'd3-axis' import { scaleLinear, scaleBand, scaleTime } from 'd3-scale' import { timeFormat } from 'd3-time-format' +import { timeHour, timeMinute } from 'd3-time' import { select, selectAll, pointer } from 'd3-selection' import { max, bisector, extent } from 'd3-array' -import { timeHour, timeMinute } from 'd3-time' -import { area as d3Area, line as d3Line, curveMonotoneX } from 'd3-shape' const { forEach, simplify } = window.flood.utils @@ -776,6 +776,9 @@ function LineChart (containerId, stationId, data, options = {}) { .attr('class', 'threshold__line') .attr('aria-hidden', true) .attr('x2', xScale(xExtent[1])).attr('y2', 0) + + // Label + const copy = `${threshold.level}m ${threshold.name}`.match(/[\s\S]{1,35}(?!\S)/g, '$&\n') const label = thresholdContainer.append('g') .attr('class', 'threshold-label') const path = label.append('path') @@ -784,10 +787,13 @@ function LineChart (containerId, stationId, data, options = {}) { const text = label.append('text') .attr('class', 'threshold-label__text') text.append('tspan').attr('font-size', 0).text('Threshold: ') - text.append('tspan').attr('x', 10).attr('y', 22).text(`${threshold.level}m ${threshold.name}`) + copy.map((l, i) => text.append('tspan').attr('x', 10).attr('y', (i + 1) * 22).text(l.trim())) const textWidth = Math.round(text.node().getBBox().width) + const textHeight = Math.round(text.node().getBBox().height) path.attr('d', `m-0.5,-0.5 l${textWidth + 20},0 l0,36 l-${((textWidth + 20) / 2) - 7.5},0 l-7.5,7.5 l-7.5,-7.5 l-${((textWidth + 20) / 2) - 7.5},0 l0,-36 l0,0`) - label.attr('transform', `translate(${Math.round(width / 2 - ((textWidth + 20) / 2))}, -46)`) + label.attr('transform', `translate(${Math.round(width / 2 - ((textWidth + 20) / 2))}, -${29 + textHeight})`) + + // Remove button const remove = thresholdContainer.append('a') .attr('role', 'button') .attr('class', 'threshold__remove') @@ -800,6 +806,7 @@ function LineChart (containerId, stationId, data, options = {}) { remove.append('circle').attr('class', 'threshold__remove-button').attr('r', 11) remove.append('line').attr('x1', -3).attr('y1', -3).attr('x2', 3).attr('y2', 3) remove.append('line').attr('y1', -3).attr('x2', -3).attr('x1', 3).attr('y2', 3) + // Set individual elements size and position thresholdContainer.attr('transform', 'translate(0,' + Math.round(yScale(threshold.level)) + ')') }) @@ -931,7 +938,7 @@ function LineChart (containerId, stationId, data, options = {}) { const showTooltip = (tooltipY = 10) => { if (!dataPoint) return // Hide threshold label - thresholdsContainer.select('.threshold--selected .threshold-label').style('visibility', 'hidden') + // thresholdsContainer.select('.threshold--selected .threshold-label').style('visibility', 'hidden') // Set tooltip text const value = dataCache.type === 'river' && (Math.round(dataPoint.value * 100) / 100) <= 0 ? '0' : dataPoint.value.toFixed(2) // *DBL below zero addition tooltipValue.text(`${value}m`) // *DBL below zero addition @@ -1109,7 +1116,8 @@ function LineChart (containerId, stationId, data, options = {}) { // const defaults = { - btnAddThresholdClass: 'defra-button-text-s' + btnAddThresholdClass: 'defra-button-text-s', + btnAddThresholdText: 'Show on chart (Visual only)' } options = Object.assign({}, defaults, options) @@ -1170,16 +1178,30 @@ function LineChart (containerId, stationId, data, options = {}) { const tooltipDescription = tooltipText.append('tspan').attr('class', 'tooltip-text').attr('x', 12).attr('dy', '1.4em') // Add optional 'Add threshold' buttons - document.querySelectorAll('[data-threshold-add]').forEach(container => { - const button = document.createElement('button') - button.className = options.btnAddThresholdClass - button.innerHTML = `Show ${container.getAttribute('data-level')}m threshold on chart (Visual only)` - button.setAttribute('aria-controls', `${containerId}-visualisation`) - button.setAttribute('data-id', container.getAttribute('data-id')) - button.setAttribute('data-threshold-add', '') - button.setAttribute('data-level', container.getAttribute('data-level')) - button.setAttribute('data-name', container.getAttribute('data-name')) - container.parentElement.replaceChild(button, container) + document.querySelectorAll('[data-threshold-add]').forEach((container, i) => { + const tooltip = document.createElement('div') + tooltip.className = 'defra-tooltip defra-tooltip--left' + tooltip.setAttribute('data-tooltip', '') + tooltip.innerHTML = ` + + \ No newline at end of file + diff --git a/server/views/station.html b/server/views/station.html index d68dc9008..d10da525e 100644 --- a/server/views/station.html +++ b/server/views/station.html @@ -263,10 +263,12 @@

Height {% if model.thresholds.length > 0 %}
-

How levels here could affect nearby areas

- {% if model.station.hasImpacts %} +

How levels here could affect nearby areas

+
+ {% if model.station.hasImpacts %} {% endif %} +
{% for band in model.thresholds %}
diff --git a/test/models/floods.js b/test/data/floods.js similarity index 100% rename from test/models/floods.js rename to test/data/floods.js diff --git a/test/data/taThresholdsData.json b/test/data/taThresholdsData.json index 6297a90d0..e3ebf315b 100644 --- a/test/data/taThresholdsData.json +++ b/test/data/taThresholdsData.json @@ -2,6 +2,7 @@ "singleActiveOffline": [ { "rloi_id": 7173, + "station_threshold_id": 123465, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Active", @@ -16,6 +17,7 @@ "singleSuspended": [ { "rloi_id": 7201, + "station_threshold_id": 123457, "river_name": "River Pinn", "agency_name": "Moss Close", "status": "Suspended", @@ -30,6 +32,7 @@ "singleClosed": [ { "rloi_id": 7202, + "station_threshold_id": 123458, "river_name": "River Test", "agency_name": "Test Road", "status": "Closed", @@ -44,6 +47,7 @@ "multipleNormalClosed": [ { "rloi_id": 7174, + "station_threshold_id": 123459, "river_name": "River Pinn", "agency_name": "Eastcote Road", "status": "Active", @@ -56,6 +60,7 @@ }, { "rloi_id": 7202, + "station_threshold_id": 123458, "river_name": "River Test", "agency_name": "Test Road", "status": "Closed", @@ -70,6 +75,7 @@ "multipleNormalActiveOfflineWelshNoValues": [ { "rloi_id": 7174, + "station_threshold_id": 123459, "river_name": "River Pinn", "agency_name": "Eastcote Road", "status": "Active", @@ -82,6 +88,7 @@ }, { "rloi_id": 7173, + "station_threshold_id": 123465, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Active", @@ -94,6 +101,7 @@ }, { "rloi_id": 7201, + "station_threshold_id": 123457, "river_name": "River Welsh", "agency_name": "Welsh Station", "status": "Active", @@ -108,6 +116,7 @@ "multipleNormalSuspendedClosed": [ { "rloi_id": 7174, + "station_threshold_id": 123459, "river_name": "River Pinn", "agency_name": "Eastcote Road", "status": "Active", @@ -120,6 +129,7 @@ }, { "rloi_id": 7173, + "station_threshold_id": 123465, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Suspended", @@ -132,6 +142,7 @@ }, { "rloi_id": 7202, + "station_threshold_id": 123458, "river_name": "River Test", "agency_name": "Test Road", "status": "Closed", @@ -146,6 +157,7 @@ "allClosedOrWelshNoValues": [ { "rloi_id": 7202, + "station_threshold_id": 123458, "river_name": "River Test", "agency_name": "Test Road", "status": "Closed", @@ -158,6 +170,7 @@ }, { "rloi_id": 7201, + "station_threshold_id": 123457, "river_name": "River Welsh", "agency_name": "Welsh Station", "status": "Active", @@ -172,6 +185,7 @@ "singleWelshWithValues": [ { "rloi_id": 7201, + "station_threshold_id": 123457, "river_name": "River Welsh", "agency_name": "Welsh Station", "status": "Active", @@ -186,6 +200,7 @@ "multipleLatestLevels": [ { "rloi_id": 7174, + "station_threshold_id": 123459, "river_name": "River Pinn", "agency_name": "Eastcote Road", "status": "Active", @@ -198,6 +213,7 @@ }, { "rloi_id": 7174, + "station_threshold_id": 123459, "river_name": "River Pinn", "agency_name": "Eastcote Road", "status": "Active", @@ -210,6 +226,7 @@ }, { "rloi_id": 7173, + "station_threshold_id": 123465, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Active", @@ -222,6 +239,7 @@ }, { "rloi_id": 7173, + "station_threshold_id": 123465, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Active", @@ -234,6 +252,7 @@ }, { "rloi_id": 7201, + "station_threshold_id": 123457, "river_name": "River Pinn", "agency_name": "Moss Close", "status": "Active", @@ -246,6 +265,7 @@ }, { "rloi_id": 7201, + "station_threshold_id": 123457, "river_name": "River Pinn", "agency_name": "Moss Close", "status": "Active", @@ -260,6 +280,7 @@ "overLimitLatestLevels": [ { "rloi_id": 7174, + "station_threshold_id": 123459, "river_name": "River Pinn", "agency_name": "Eastcote Road", "status": "Active", @@ -272,6 +293,7 @@ }, { "rloi_id": 7174, + "station_threshold_id": 123459, "river_name": "River Pinn", "agency_name": "Eastcote Road", "status": "Active", @@ -284,6 +306,7 @@ }, { "rloi_id": 7173, + "station_threshold_id": 123465, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Active", @@ -296,6 +319,7 @@ }, { "rloi_id": 7173, + "station_threshold_id": 123465, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Active", @@ -308,6 +332,7 @@ }, { "rloi_id": 7201, + "station_threshold_id": 123457, "river_name": "River Pinn", "agency_name": "Moss Close", "status": "Active", @@ -320,6 +345,7 @@ }, { "rloi_id": 7201, + "station_threshold_id": 123457, "river_name": "River Pinn", "agency_name": "Moss Close", "status": "Active", @@ -332,6 +358,7 @@ }, { "rloi_id": 1111, + "station_threshold_id": 123500, "river_name": "River Pinn", "agency_name": "Moss Close", "status": "Active", @@ -344,6 +371,7 @@ }, { "rloi_id": 1111, + "station_threshold_id": 123500, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Active", @@ -356,6 +384,7 @@ }, { "rloi_id": 1112, + "station_threshold_id": 123501, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Active", @@ -368,6 +397,7 @@ }, { "rloi_id": 1113, + "station_threshold_id": 123502, "river_name": "River Pinn", "agency_name": "Avenue Road", "status": "Active", diff --git a/test/data/warning.json b/test/data/warning.json new file mode 100644 index 000000000..cd72105e4 --- /dev/null +++ b/test/data/warning.json @@ -0,0 +1,19 @@ +{ + "station_threshold_id":"1749487", + "station_id":"7332", + "fwis_code":"062FWF46Harpendn", + "fwis_type":"W", + "direction":"u", + "value":"2.1", + "threshold_type":"FW RES FW", + "rloi_id":7332, + "river_name":"River Lee", + "agency_name":"Harpenden", + "status":"Active", + "iswales":false, + "latest_level":"0.935", + "threshold_value":"1.6", + "value_timestamp":"2024-09-26T15:30:00.000Z", + "ta_name":"River Lee at Harpenden", + "severity_value":2 +} diff --git a/test/models/lib/process-imtd-thresholds.js b/test/models/lib/process-imtd-thresholds.js index aecbb088c..628c7214d 100644 --- a/test/models/lib/process-imtd-thresholds.js +++ b/test/models/lib/process-imtd-thresholds.js @@ -1,10 +1,11 @@ const Lab = require('@hapi/lab') const Code = require('@hapi/code') +const data = require('../../data') const lab = exports.lab = Lab.script() const processImtdThresholds = require('../../../server/models/views/lib/process-imtd-thresholds') const alertExpectedText = { id: 'alertThreshold', description: 'Low lying land flooding is possible above this level. One or more flood alerts may be issued', shortname: 'Possible flood alerts' } -const warningExpectedText = { id: 'warningThreshold', description: 'Property flooding is possible above this level. One or more flood warnings may be issued', shortname: 'Possible flood warnings' } +const warningExpectedText = { id: 'warningThreshold', description: 'Flood Warning issued: River Lee at Harpenden', shortname: 'Possible flood warnings' } function expectThresholds (thresholds, warningThreshold, alertThreshold) { Code.expect(thresholds.length).to.equal(2) @@ -15,7 +16,7 @@ function expectThresholds (thresholds, warningThreshold, alertThreshold) { lab.experiment('process IMTD thresholds test', () => { lab.experiment('given post processing is set to false', () => { lab.test('then thresholds should be returned as is', async () => { - const thresholds = processImtdThresholds({ alert: 1.1, warning: 2.1 }, 0, 0, false) + const thresholds = processImtdThresholds({ alert: 1.1, warning: data.warning }, 0, 0, false) expectThresholds(thresholds, '2.10', '1.10') }) lab.test('then single null thresholds should not be returned', async () => { @@ -34,35 +35,35 @@ lab.experiment('process IMTD thresholds test', () => { }) lab.experiment('given post processing is set to true', () => { lab.test('then thresholds should be returned as is when stageDatum is zero', async () => { - const thresholds = processImtdThresholds({ alert: 1.1, warning: 2.1 }, 0, 0, true) + const thresholds = processImtdThresholds({ alert: 1.1, warning: data.warning }, 0, 0, true) expectThresholds(thresholds, '2.10', '1.10') }) lab.test('then thresholds should be returned as adjusted when stageDatum is greater than zero', async () => { - const thresholds = processImtdThresholds({ alert: 1.1, warning: 2.1 }, 2.2, 0, true) + const thresholds = processImtdThresholds({ alert: 1.1, warning: data.warning }, 2.2, 0, true) expectThresholds(thresholds, '-0.10', '-1.10') }) lab.test('then thresholds should be returned as is when stageDatum is less than zero', async () => { - const thresholds = processImtdThresholds({ alert: 1.1, warning: 2.1 }, -2.2, 0, true) + const thresholds = processImtdThresholds({ alert: 1.1, warning: data.warning }, -2.2, 0, true) expectThresholds(thresholds, '2.10', '1.10') }) lab.test('then thresholds should be returned as adjusted when stageDatum is less than zero and stationSubtract is greater than zero', async () => { - const thresholds = processImtdThresholds({ alert: 1.1, warning: 2.1 }, -2.2, 0.5, true) + const thresholds = processImtdThresholds({ alert: 1.1, warning: data.warning }, -2.2, 0.5, true) expectThresholds(thresholds, '1.60', '0.60') }) lab.test('then thresholds should be returned as is when stageDatum is less than zero and stationSubtract is less than zero', async () => { - const thresholds = processImtdThresholds({ alert: 1.1, warning: 2.1 }, -2.2, -0.5, true) + const thresholds = processImtdThresholds({ alert: 1.1, warning: data.warning }, -2.2, -0.5, true) expectThresholds(thresholds, '2.10', '1.10') }) lab.test('then thresholds should be returned as is when stageDatum is equal to zero and stationSubtract is less than zero', async () => { - const thresholds = processImtdThresholds({ alert: 1.1, warning: 2.1 }, 0, -0.5, true) + const thresholds = processImtdThresholds({ alert: 1.1, warning: data.warning }, 0, -0.5, true) expectThresholds(thresholds, '2.10', '1.10') }) lab.test('then thresholds should be returned as adjusted when stageDatum is equal to zero and stationSubtract is greater than than zero', async () => { - const thresholds = processImtdThresholds({ alert: 1.1, warning: 2.1 }, 0, 0.5, true) + const thresholds = processImtdThresholds({ alert: 1.1, warning: data.warning }, 0, 0.5, true) expectThresholds(thresholds, '1.60', '0.60') }) lab.test('then thresholds should be returned as is when stageDatum is equal to zero and stationSubtract is zero', async () => { - const thresholds = processImtdThresholds({ alert: 1.1, warning: 2.1 }, 0, 0, true) + const thresholds = processImtdThresholds({ alert: 1.1, warning: data.warning }, 0, 0, true) expectThresholds(thresholds, '2.10', '1.10') }) }) diff --git a/test/routes/target-area.js b/test/routes/target-area.js index 19343980c..04ab378a2 100644 --- a/test/routes/target-area.js +++ b/test/routes/target-area.js @@ -192,9 +192,9 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest levels

') Code.expect(response.payload).to.contain('

The River Pinn level at Eastcote Road was 0.35 metres. Property flooding is possible when it goes above 1.40 metres.') Code.expect(response.payload).to.contain('

The River Pinn level at Avenue Road was 0.18 metres. Property flooding is possible when it goes above 1.46 metres.') - Code.expect(response.payload).to.contain(' Monitor the River Pinn level at Avenue Road') + Code.expect(response.payload).to.contain('Monitor the River Pinn level at Avenue Road') Code.expect(response.payload).to.contain('

The River Pinn level at Moss Close was 0.13 metres. Property flooding is possible when it goes above 1.15 metres.') - Code.expect(response.payload).to.contain('Monitor the River Pinn level at Moss Close') + Code.expect(response.payload).to.contain('Monitor the River Pinn level at Moss Close') }) lab.test('Check flood severity banner link for Flood warning', async () => { const floodService = require('../../server/services/flood')