From 1eedcfa73523e91623d058ba80f510aee2cd3b8d Mon Sep 17 00:00:00 2001 From: Keyurx11 <58322871+Keyurx11@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:42:23 +0100 Subject: [PATCH 1/4] Fix/fsr 1237 latest level warning pages service (#844) * FSR-1237: Fix spacing in 'LATEST LEVELS' * FSR-1237: Display '(National Resources Wales)' for Welsh stations in flood warnings and add test * FSR-1237: Update time display for closed/offline/suspended gauges * FSR-1237: Add fullstop for multiple gauges link * FSR-1237: Revert link back to original * FSR-1237: Ensure visibility of LATEST LEVEL in high-contrast mode by adjusting background and border styles * FSR-1237: Reinstate levels auto-update message and refactor unit tests for consistency * FSR-1237: Refactor adjustThresholdValue logic and add tests for threshold adjustment functionality in getThresholdsForTargetArea --- server/models/views/lib/latest-levels.js | 23 +++++++++ .../sass/components/_latest-levels-box.scss | 8 +++ server/views/partials/latest-levels.html | 7 +-- server/views/target-area.html | 2 +- test/models/lib/latest-levels.js | 51 ++++++++++++------- test/routes/target-area-2.js | 4 +- test/routes/target-area.js | 17 +++++-- 7 files changed, 85 insertions(+), 27 deletions(-) diff --git a/server/models/views/lib/latest-levels.js b/server/models/views/lib/latest-levels.js index 718d4d49b..37fac6a2a 100644 --- a/server/models/views/lib/latest-levels.js +++ b/server/models/views/lib/latest-levels.js @@ -2,6 +2,19 @@ const { formatElapsedTime } = require('../../../util') const WARNING_THRESHOLD_TYPES = ['FW RES FW', 'FW ACT FW', 'FW ACTCON FW'] +function adjustThresholdValue (value, stageDatum, subtract, postProcess) { + if (postProcess) { + if (stageDatum && stageDatum > 0) { + value -= stageDatum + } else if (stageDatum <= 0 && subtract && subtract > 0) { + value -= subtract + } else { + return parseFloat(value).toFixed(2) + } + } + return parseFloat(value).toFixed(2) +} + function getThresholdsForTargetArea (thresholds) { const filteredThresholds = thresholds.filter(threshold => threshold.status !== 'Closed' && @@ -9,9 +22,19 @@ function getThresholdsForTargetArea (thresholds) { ) const warningThresholds = findPrioritisedThresholds(filteredThresholds, WARNING_THRESHOLD_TYPES) + return warningThresholds.map(threshold => { threshold.formatted_time = formatElapsedTime(threshold.value_timestamp) threshold.isSuspendedOrOffline = threshold.status === 'Suspended' || (threshold.status === 'Active' && threshold.latest_level === null) + + // Use adjustThresholdValue for threshold_value adjustment + threshold.threshold_value = adjustThresholdValue( + threshold.threshold_value, + threshold.stage_datum, + threshold.subtract, + threshold.post_process + ) + return threshold }) } diff --git a/server/src/sass/components/_latest-levels-box.scss b/server/src/sass/components/_latest-levels-box.scss index 98886a2b5..599291e5e 100644 --- a/server/src/sass/components/_latest-levels-box.scss +++ b/server/src/sass/components/_latest-levels-box.scss @@ -13,6 +13,14 @@ text-transform: uppercase; background-color: #1C70B8; color: white; + word-spacing: 0.15em; + + // Add contrast-specific styles for high contrast mode + @media (forced-colors: active) { + background-color: Canvas; // Ensures light background in contrast mode + border: 2px solid ButtonText; // Adds a visible border in high contrast + color: ButtonText; // Uses the system's high-contrast text color + } } &__supplementary { @include govuk-font($size: 16); diff --git a/server/views/partials/latest-levels.html b/server/views/partials/latest-levels.html index 099f7e447..99f091b21 100644 --- a/server/views/partials/latest-levels.html +++ b/server/views/partials/latest-levels.html @@ -3,21 +3,22 @@

Latest level{% if model.latestLevels.length > 1 %} {% for warnings in model.latestLevels %}
{% if warnings.isSuspendedOrOffline %} -

Latest Level

+

{{ warnings.formatted_time }}

The {{ warnings.river_name }} level at {{ warnings.agency_name }} is currently unavailable.

{% else %}

{{ warnings.formatted_time }}

The {{ warnings.river_name }} level at {{ warnings.agency_name }} was {{ warnings.latest_level | toFixed(2) }} metres. Property flooding is possible when it goes above {{ warnings.threshold_value | toFixed(2) }} metres. {% if model.latestLevels.length > 1 %} - Monitor the {{ warnings.river_name }} level at {{ warnings.agency_name }} + Monitor the {{ warnings.river_name }} level at {{ warnings.agency_name }}{% if warnings.iswales %} (Natural Resources Wales){% endif %}. {% endif %}

{% if model.latestLevels.length == 1 %}

- Monitor the latest{% if model.latestLevels.length > 1 %} {{ warnings.river_name }}{% endif %} level at {{ warnings.agency_name }} + Monitor the latest{% if model.latestLevels.length > 1 %} {{ warnings.river_name }}{% endif %} level at {{ warnings.agency_name }}{% if warnings.iswales %} (Natural Resources Wales){% endif %}

{% endif %} {% endif %}
{% endfor %} +

{% if model.latestLevels.length > 1 %}These levels{% else %}This level{% endif %} will update automatically

\ No newline at end of file diff --git a/server/views/target-area.html b/server/views/target-area.html index d5a2097b2..aa71b1969 100644 --- a/server/views/target-area.html +++ b/server/views/target-area.html @@ -75,7 +75,7 @@

{{ model.pageTitle }}

- Find other river and sea levels + Find a river, sea, groundwater or rainfall level in this area

diff --git a/test/models/lib/latest-levels.js b/test/models/lib/latest-levels.js index 414d7b52e..517e30a11 100644 --- a/test/models/lib/latest-levels.js +++ b/test/models/lib/latest-levels.js @@ -107,43 +107,60 @@ lab.experiment('getThresholdsForTargetArea', () => { expect(result).to.have.length(0) }) - lab.test('should prioritize the first type in the WARNING_THRESHOLD_TYPES array when all thresholds are of the same type', () => { + lab.test('should adjust threshold_value using stageDatum if postProcess is true and stageDatum > 0', () => { const thresholds = [ - { rloi_id: 1, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T11:45:00.000Z' }, - { rloi_id: 2, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T12:45:00.000Z' } + { + rloi_id: 1, + threshold_type: 'FW RES FW', + value_timestamp: '2024-08-12T11:45:00.000Z', + threshold_value: '5.00', + stage_datum: 2.5, + subtract: 1.0, + post_process: true + } ] const result = getThresholdsForTargetArea(thresholds) - expect(result).to.have.length(2) - expect(result[0].threshold_type).to.equal('FW RES FW') - expect(result[0].formatted_time).to.equal('More than 1 hour ago') - expect(result[1].formatted_time).to.equal('0 minutes ago') + expect(result).to.have.length(1) + expect(result[0].threshold_value).to.equal('2.50') // 5.00 - 2.5 }) - lab.test('should correctly handle multiple thresholds with the same RLOI ID and type', () => { + lab.test('should adjust threshold_value using subtract if postProcess is true, stageDatum <= 0, and subtract > 0', () => { const thresholds = [ - { rloi_id: 1, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T11:45:00.000Z' }, - { rloi_id: 1, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T12:45:00.000Z' } + { + rloi_id: 2, + threshold_type: 'FW ACT FW', + value_timestamp: '2024-08-12T10:45:00.000Z', + threshold_value: '5.00', + stage_datum: 0, + subtract: 1.5, + post_process: true + } ] const result = getThresholdsForTargetArea(thresholds) expect(result).to.have.length(1) - expect(result[0].threshold_type).to.equal('FW RES FW') - expect(result[0].formatted_time).to.equal('More than 1 hour ago') + expect(result[0].threshold_value).to.equal('3.50') // 5.00 - 1.5 }) - lab.test('should ignore invalid thresholds and process only valid ones', () => { + lab.test('should not adjust threshold_value if postProcess is false', () => { const thresholds = [ - { rloi_id: 1, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T11:45:00.000Z' }, - { rloi_id: 2, threshold_type: 'INVALID TYPE', value_timestamp: '2024-08-12T12:45:00.000Z' } + { + rloi_id: 3, + threshold_type: 'FW ACTCON FW', + value_timestamp: '2024-08-12T11:45:00.000Z', + threshold_value: '4.00', + stage_datum: 1.0, + subtract: 1.5, + post_process: false + } ] const result = getThresholdsForTargetArea(thresholds) expect(result).to.have.length(1) - expect(result[0].threshold_type).to.equal('FW RES FW') - expect(result[0].formatted_time).to.equal('More than 1 hour ago') + expect(result[0].threshold_value).to.equal('4.00') // No adjustment }) }) diff --git a/test/routes/target-area-2.js b/test/routes/target-area-2.js index 7dee2aa81..b217bfce9 100644 --- a/test/routes/target-area-2.js +++ b/test/routes/target-area-2.js @@ -147,7 +147,7 @@ describe('target-area route', () => { expect(response.statusCode).to.equal(200) const root = parse(response.payload) linkChecker(root.querySelectorAll('a'), - 'Find other river and sea levels', + 'Find a river, sea, groundwater or rainfall level in this area', `/river-and-sea-levels/target-area/${AREA_CODE}` ) }) @@ -175,7 +175,7 @@ describe('target-area route', () => { expect(response.statusCode).to.equal(200) const root = parse(response.payload) linkChecker(root.querySelectorAll('a'), - 'Find other river and sea levels', + 'Find a river, sea, groundwater or rainfall level in this area', `/river-and-sea-levels/target-area/${AREA_CODE}` ) }) diff --git a/test/routes/target-area.js b/test/routes/target-area.js index 19343980c..c16f2e9c5 100644 --- a/test/routes/target-area.js +++ b/test/routes/target-area.js @@ -192,9 +192,10 @@ 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.') + Code.expect(response.payload).to.contain('

These levels will update automatically

') }) lab.test('Check flood severity banner link for Flood warning', async () => { const floodService = require('../../server/services/flood') @@ -286,7 +287,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.statusCode).to.equal(200) const root = parse(response.payload) - Code.expect(response.payload).to.contain('Find other river and sea levels') + Code.expect(response.payload).to.contain('Find a river, sea, groundwater or rainfall level in this area') Code.expect(response.payload).to.contain('') const relatedContentLinks = root.querySelectorAll('.defra-related-items a') @@ -357,7 +358,7 @@ lab.experiment('Target-area tests', () => { // context footer check validateFooterPresent(response) Code.expect(response.payload).to.contain('Severe flood warning for Upper River Derwent, Stonethwaite Beck and Derwent Water') - Code.expect(response.payload).to.contain('Find other river and sea levels') + Code.expect(response.payload).to.contain('Find a river, sea, groundwater or rainfall level in this area') const anchorFound = root.querySelectorAll('a').some(a => a.attributes.href === '/river-and-sea-levels/target-area/011WAFDW' @@ -562,6 +563,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest level

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

The River Pinn level at Avenue Road is currently unavailable.

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

This level will update automatically

') }) lab.test('Displays latest level for a single suspended station', async () => { @@ -610,6 +612,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest level

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

The River Pinn level at Moss Close is currently unavailable.

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

This level will update automatically

') }) lab.test('Displays multiple levels with one active but offline, one normal, and one Welsh station with no values', async () => { @@ -708,6 +711,7 @@ 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 is currently unavailable.

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

These levels will update automatically

') }) lab.test('Displays multiple levels with one Closed and one normal station', async () => { @@ -756,6 +760,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest level

') 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('

This level will update automatically

') }) lab.test('Displays multiple levels with one normal, one active but offline, and one Welsh station with no values', async () => { @@ -806,6 +811,7 @@ lab.experiment('Target-area tests', () => { 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 is currently unavailable.

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

The River Welsh station level is currently unavailable.

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

These levels will update automatically

') }) lab.test('Displays multiple levels with one normal, one suspended, and one Closed station', async () => { @@ -856,6 +862,7 @@ lab.experiment('Target-area tests', () => { 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 is currently unavailable.

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

The River Test level at Test Road is currently unavailable.

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

These levels will update automatically

') }) lab.test('Does not display levels if all stations are Closed or Welsh with no values', async () => { @@ -950,5 +957,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest level

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

The River Welsh level at Welsh Station was 0.35 metres. Property flooding is possible when it goes above 1.40 metres.\n \n

') + Code.expect(response.payload).to.contain('
Monitor the latest level at Welsh Station (Natural Resources Wales)') + Code.expect(response.payload).to.contain('

This level will update automatically

') }) }) From f4ced23655027a7c71024a846bf298fce1417a63 Mon Sep 17 00:00:00 2001 From: Keyurx11 <58322871+Keyurx11@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:42:53 +0100 Subject: [PATCH 2/4] Feature/fsr 1261 update ta pay layout (#845) * FSR-1261: Improve visibility of map link and sign-up call to action on TA pages * FSR-1261: Change map button text colour to blue * FSR-1261: Change Icon on map button to be black * FSR-1261: Remove additional CSS to avoid duplication in upcoming merge --- server/src/sass/objects/_buttons.scss | 2 +- server/views/target-area.html | 34 +++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/server/src/sass/objects/_buttons.scss b/server/src/sass/objects/_buttons.scss index 2f350f634..cbbd584f5 100644 --- a/server/src/sass/objects/_buttons.scss +++ b/server/src/sass/objects/_buttons.scss @@ -143,4 +143,4 @@ button.defra-button-secondary { margin-bottom: -4px; top: 0px; } -} +} \ No newline at end of file diff --git a/server/views/target-area.html b/server/views/target-area.html index aa71b1969..039ffdeae 100644 --- a/server/views/target-area.html +++ b/server/views/target-area.html @@ -51,22 +51,35 @@

{{ model.pageTitle }}

+ + +

{% include "partials/sign-up-for-flood-warnings.html" %}

+

- - -
{{ model.situation | safe }}
+ + +

{{ model.areaDescription | safe }}

+ + {% if model.severity %} +

+ + Give feedback on this flood warning information + +

+ {% endif %} + {% if model.latestLevels and model.latestLevels.length > 0 and model.latestLevels.length <= 4 %} {% include "partials/latest-levels.html" %} @@ -79,19 +92,6 @@

{{ model.pageTitle }}

- - {% include "partials/sign-up-for-flood-warnings.html" %} - - - {% if model.severity %} -

- Could this information be better? - - Tell us how to improve it. - -

- {% endif %} - {% include "partials/context-footer.html" %} @@ -111,7 +111,7 @@

{{ model.pageTitle }}

window.flood = {} window.flood.model = {{ model | dump(2) | safe }} window.flood.model.mapButtonText = {{ model.mapButtonText | dump | safe }} - window.flood.model.mapButtonClass = 'defra-button-secondary defra-button-secondary--icon govuk-!-margin-top-4' + window.flood.model.mapButtonClass = 'defra-button-secondary govuk-!-margin-top-4 defra-button-blue-text-black-icon'; window.flood.model.mapLayers = 'mv,ts,tw,ta', window.flood.model.data = { button: 'Target Area:Map view:TA - Map view' From 18adaf0c366971d376bac0299ba7a194e7094528 Mon Sep 17 00:00:00 2001 From: Keyurx11 <58322871+Keyurx11@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:44:43 +0100 Subject: [PATCH 3/4] FSR-1294: Add logic to wrap text for the threshold label on the station chart (#847) --- server/src/js/components/chart.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/server/src/js/components/chart.js b/server/src/js/components/chart.js index 227a6ee4e..4fd5241d6 100644 --- a/server/src/js/components/chart.js +++ b/server/src/js/components/chart.js @@ -776,18 +776,24 @@ function LineChart (containerId, stationId, data, options = {}) { .attr('class', 'threshold__line') .attr('aria-hidden', true) .attr('x2', xScale(xExtent[1])).attr('y2', 0) - const label = thresholdContainer.append('g') - .attr('class', 'threshold-label') + // Construct label text and split into lines of up to 35 characters + const thresholdLabel = `${threshold.level}m ${threshold.name}` + const labelSegments = thresholdLabel.match(/.{1,35}(\s|$)/g).map(line => line.trim()) + const label = thresholdContainer.append('g').attr('class', 'threshold-label') const path = label.append('path') .attr('aria-hidden', true) .attr('class', 'threshold-label__bg') 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}`) + // Add each line of the split text (up to 35 characters per line) as separate tspans + labelSegments.forEach((line, i) => { + text.append('tspan').attr('x', 10).attr('y', (i + 1) * 22).text(line.trim()) + }) const textWidth = Math.round(text.node().getBBox().width) - 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)`) + const textHeight = Math.round(text.node().getBBox().height) + path.attr('d', `m-0.5,-0.5 l${textWidth + 20},0 l0,${19 + textHeight} 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,-${19 + textHeight} l0,0`) + label.attr('transform', `translate(${Math.round(width / 2 - ((textWidth + 20) / 2))}, -${29 + textHeight})`) const remove = thresholdContainer.append('a') .attr('role', 'button') .attr('class', 'threshold__remove') From 3f97c9adffe88d2971ef81612abad30ffcb8bb59 Mon Sep 17 00:00:00 2001 From: Keyurx11 <58322871+Keyurx11@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:47:58 +0100 Subject: [PATCH 4/4] Feature/fsr 1295 default station chart threshold (#848) * FSR-1295: Display correct chart thresholds based on presence of TID in URL * FSR-1295: Exclude TID in URL for Welsh stations on the latest level links * FSR-1295: Add tests * FSR-1295: Refactor code and fix sonar clould issues --- server/models/views/station.js | 55 +++++++++++++++--- server/routes/station.js | 7 ++- server/src/js/components/chart.js | 73 ++++++++++++++++-------- server/views/partials/latest-levels.html | 4 +- test/data/nullTelemetry.json | 3 +- test/data/stationAWSW.json | 3 +- test/data/stationActiveAlert.json | 3 +- test/data/stationActiveWarning.json | 3 +- test/data/stationCoastal.json | 3 +- test/data/stationForecastData.json | 3 +- test/data/stationGroudwater.json | 3 +- test/data/stationMultipleAW.json | 3 +- test/data/stationRiver.json | 3 +- test/data/stationRiverACTCON.json | 3 +- test/data/stationRiverSpike.json | 3 +- test/data/stationSevereWarning.json | 3 +- test/data/taThresholdsData.json | 6 ++ test/data/toggleTipBelowZeroStation.json | 3 +- test/data/toggleTipRiverBedStation.json | 3 +- test/data/toggleTipSeaLevelStation.json | 3 +- test/models/station.js | 60 ++++++++++++++++++- test/routes/target-area.js | 8 +-- 22 files changed, 202 insertions(+), 56 deletions(-) diff --git a/server/models/views/station.js b/server/models/views/station.js index 904cd05da..7ced1829a 100644 --- a/server/models/views/station.js +++ b/server/models/views/station.js @@ -12,9 +12,11 @@ const bannerIconId3 = 3 const outOfDateMax = 5 const dataStartDateTimeDaysToSubtract = 5 +const TOP_OF_NORMAL_RANGE = 'Top of normal range' + class ViewModel { constructor (options) { - const { station, telemetry, forecast, imtdThresholds, impacts, river, warningsAlerts } = options + const { station, telemetry, forecast, imtdThresholds, impacts, river, warningsAlerts, requestUrl } = options this.station = new Station(station) this.station.riverNavigation = river @@ -39,7 +41,6 @@ class ViewModel { const numSevereWarnings = warningsAlertsGroups['3'] ? warningsAlertsGroups['3'].length : 0 // Determine appropriate warning/alert text for banner - this.banner = numAlerts || numWarnings || numSevereWarnings switch (numAlerts) { @@ -230,12 +231,12 @@ class ViewModel { } this.metaDescription = `Check the latest recorded ${stationType.toLowerCase()} level and recent 5-day trend at ${stationLocation}` - // Thresholds + // Array to hold thresholds let thresholds = [] + // Check if recent value exists and add it to thresholds if (this.station.recentValue && !this.station.recentValue.err) { const tVal = this.station.type !== 'c' && this.station.recentValue._ <= 0 ? 0 : this.station.recentValue._.toFixed(2) - thresholds.push({ id: 'latest', value: tVal, @@ -243,6 +244,7 @@ class ViewModel { shortname: '' }) } + // Add the highest level threshold if available if (this.station.porMaxValue) { thresholds.push({ id: 'highest', @@ -253,7 +255,6 @@ class ViewModel { shortname: 'Highest level on record' }) } - this.imtdThresholds = imtdThresholds?.length > 0 ? filterImtdThresholds(imtdThresholds) : [] @@ -264,7 +265,6 @@ class ViewModel { this.station.subtract, this.station.post_process ) - thresholds.push(...processedImtdThresholds) if (this.station.percentile5) { @@ -273,10 +273,25 @@ class ViewModel { id: 'pc5', value: this.station.percentile5, description: 'This is the top of the normal range', - shortname: 'Top of normal range' + shortname: TOP_OF_NORMAL_RANGE }) } + // Handle chartThreshold: add tidThreshold if a valid tid is present; if not, fallback to 'pc5'; if 'pc5' is unavailable, use 'alertThreshold' with "Top of normal range" description. + // Extract tid from request URL if valid + let tid = null + try { + tid = requestUrl?.startsWith('http') ? new URL(requestUrl).searchParams.get('tid') : null + } catch (e) { + console.error('Invalid request URL:', e) + } + + // Retrieve the applicable threshold for chartThreshold + const chartThreshold = [getThresholdByThresholdId(tid, imtdThresholds, thresholds)].filter(Boolean) + + // Set chartThreshold property + this.chartThreshold = chartThreshold + // Add impacts if (impacts.length > 0) { this.station.hasImpacts = true @@ -363,7 +378,6 @@ class ViewModel { this.zoom = 14 // Forecast Data Calculations - let forecastData if (isForecast) { this.isFfoi = isForecast @@ -403,6 +417,7 @@ function stationTypeCalculator (stationTypeData) { } return stationType } + function telemetryForecastBuilder (telemetryRawData, forecastRawData, stationType) { const observed = telemetryRawData .filter(telemetry => telemetry._ !== null) // Filter out records where telemetry._ is null @@ -432,4 +447,28 @@ function telemetryForecastBuilder (telemetryRawData, forecastRawData, stationTyp } } +// Function to retrieve a threshold by tid or fall back to 'pc5' or 'alertThreshold' +const getThresholdByThresholdId = (tid, imtdThresholds, thresholds) => { + // Check if a threshold exists based on tid + const tidThreshold = tid && imtdThresholds?.find(thresh => thresh.station_threshold_id === tid) + if (tidThreshold) { + return { + id: tidThreshold.station_threshold_id, + value: Number(tidThreshold.value).toFixed(2), + description: `${tidThreshold.value}m ${tidThreshold.ta_name || ''}`, + shortname: tidThreshold.ta_name || 'Target Area Threshold' + } + } + + // Fallback to 'pc5' if present, else look for 'alertThreshold' + const pc5Threshold = thresholds.find(t => t.id === 'pc5') + if (pc5Threshold) { + return pc5Threshold + } + + // Fallback to 'alertThreshold' if description includes 'Top of normal range' + const alertThreshold = thresholds.find(t => t.id === 'alertThreshold' && t.description.includes(TOP_OF_NORMAL_RANGE)) + return alertThreshold ? { ...alertThreshold, shortname: TOP_OF_NORMAL_RANGE } : null +} + module.exports = ViewModel diff --git a/server/routes/station.js b/server/routes/station.js index 907d37d64..2743e3ed4 100644 --- a/server/routes/station.js +++ b/server/routes/station.js @@ -54,6 +54,9 @@ module.exports = { request.server.methods.flood.getRiverStationByStationId(id, direction) ]) + // const requestUrl = request.url.href + const requestUrl = request.url.toString() + if (station.status === 'Closed') { const river = [] const model = new ViewModel({ station, telemetry, imtdThresholds, impacts, river, warningsAlerts }) @@ -67,11 +70,11 @@ module.exports = { // Forecast station const values = await request.server.methods.flood.getStationForecastData(station.wiski_id) const forecast = { forecastFlag, values } - const model = new ViewModel({ station, telemetry, forecast, imtdThresholds, impacts, river, warningsAlerts }) + const model = new ViewModel({ station, telemetry, forecast, imtdThresholds, impacts, river, warningsAlerts, requestUrl }) return h.view('station', { model }) } else { // Non-forecast Station - const model = new ViewModel({ station, telemetry, imtdThresholds, impacts, river, warningsAlerts }) + const model = new ViewModel({ station, telemetry, imtdThresholds, impacts, river, warningsAlerts, requestUrl }) return h.view('station', { model }) } }, diff --git a/server/src/js/components/chart.js b/server/src/js/components/chart.js index 4fd5241d6..23874dd44 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 @@ -806,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)) + ')') }) @@ -937,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 @@ -1115,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) @@ -1176,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 = ` + +