Skip to content

Commit

Permalink
Feature/fsr 1295 default station chart threshold (#848)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Keyurx11 authored Oct 9, 2024
1 parent 18adaf0 commit 3f97c9a
Show file tree
Hide file tree
Showing 22 changed files with 202 additions and 56 deletions.
55 changes: 47 additions & 8 deletions server/models/views/station.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -230,19 +231,20 @@ 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,
description: 'Latest level',
shortname: ''
})
}
// Add the highest level threshold if available
if (this.station.porMaxValue) {
thresholds.push({
id: 'highest',
Expand All @@ -253,7 +255,6 @@ class ViewModel {
shortname: 'Highest level on record'
})
}

this.imtdThresholds = imtdThresholds?.length > 0
? filterImtdThresholds(imtdThresholds)
: []
Expand All @@ -264,7 +265,6 @@ class ViewModel {
this.station.subtract,
this.station.post_process
)

thresholds.push(...processedImtdThresholds)

if (this.station.percentile5) {
Expand All @@ -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
Expand Down Expand Up @@ -363,7 +378,6 @@ class ViewModel {
this.zoom = 14

// Forecast Data Calculations

let forecastData
if (isForecast) {
this.isFfoi = isForecast
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
7 changes: 5 additions & 2 deletions server/routes/station.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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 })
}
},
Expand Down
73 changes: 49 additions & 24 deletions server/src/js/components/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)) + ')')
})
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <span class="govuk-visually-hidden">(Visual only)</span>'
}
options = Object.assign({}, defaults, options)

Expand Down Expand Up @@ -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<span class="govuk-visually-hidden"> ${container.getAttribute('data-level')}m threshold</span> on chart <span class="govuk-visually-hidden">(Visual only)</span>`
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 = `
<button class="${options.btnAddThresholdClass}"
aria-labelledby="tooltip-${i}"
aria-controls="${containerId}-visualisation"
data-id="${container.getAttribute('data-id')}"
data-level="${container.getAttribute('data-level')}"
data-name="${container.getAttribute('data-name')}"
data-threshold-add >
<svg width="20" height="20" viewBox="0 0 20 20" fill-rule="evenodd" fill="currentColor">
<path d="M2.75 14.443v2.791H18v1.5H1.25V1.984h1.5v7.967L6.789 4.91l5.016 4.013 5.056-5.899 2.278 1.952-6.944 8.101L7.211 9.09 2.75 14.443z"/>
</svg>
</button>
<div id="tooltip-${i}" class="defra-tooltip__label" role="tooltip">
<div class="defra-tooltip__label-inner govuk-body-s">
${options.btnAddThresholdText}
<span class="govuk-visually-hidden>(visual only)</span>
</div>
</div>
`
container.parentElement.replaceChild(tooltip, container)
})

// Define globals
Expand Down Expand Up @@ -1247,8 +1263,8 @@ function LineChart (containerId, stationId, data, options = {}) {
significantContainer.node().parentNode.classList.remove('significant--visible')
// svg.select('.focussed-cell').remove()
// Add threshold button
if (!e.target.hasAttribute('data-threshold-add')) return
const button = e.target
const button = e.target.closest('button')
if (!(button && button.hasAttribute('data-threshold-add'))) return
addThreshold({
id: button.getAttribute('data-id'),
level: Number(button.getAttribute('data-level')),
Expand Down Expand Up @@ -1414,16 +1430,25 @@ if (document.getElementById('bar-chart')) {
window.flood.charts.createBarChart('bar-chart', window.flood.model.stationId, window.flood.model.telemetry)
}

// Line chart
if (document.getElementById('line-chart')) {
const lineChart = window.flood.charts.createLineChart('line-chart', window.flood.model.id, window.flood.model.telemetry)
const thresholdId = 'threshold-pc5'
const threshold = document.querySelector(`[data-id="${thresholdId}"]`)
// Line chart setup to display thresholds based on chartThreshold configuration -----
function addThresholdToChart (lineChart, threshold) {
if (threshold) {
lineChart.addThreshold({
id: thresholdId,
name: threshold.getAttribute('data-name'),
level: Number(threshold.getAttribute('data-level'))
id: threshold.id,
name: threshold.shortname,
level: Number(threshold.value)
})
} else {
console.error('No valid threshold found to add to the chart.')
}
}

// Initialize the line chart and set thresholds using chartThreshold from ViewModel
if (document.getElementById('line-chart')) {
const lineChart = window.flood.charts.createLineChart('line-chart', window.flood.model.id, window.flood.model.telemetry)

// Iterate through each threshold in chartThreshold and add it to the chart
window.flood.model.chartThreshold.forEach(threshold => {
addThresholdToChart(lineChart, threshold)
})
}
4 changes: 2 additions & 2 deletions server/views/partials/latest-levels.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ <h2 class="defra-live__title">Latest level{% if model.latestLevels.length > 1 %}
<p class="defra-flood-meta defra-flood-meta--no-border govuk-!-margin-bottom-0"><strong>{{ warnings.formatted_time }}</strong></p>
<p>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 %}
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}">Monitor the {{ warnings.river_name }} level at {{ warnings.agency_name }}{% if warnings.iswales %} (Natural Resources Wales){% endif %}.</a>
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}{% if not warnings.iswales %}?tid={{ warnings.station_threshold_id }}{% endif %}">Monitor the {{ warnings.river_name }} level at {{ warnings.agency_name }}</a>
{% endif %}
</p>
{% if model.latestLevels.length == 1 %}
<p>
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}">Monitor the latest{% if model.latestLevels.length > 1 %} {{ warnings.river_name }}{% endif %} level at {{ warnings.agency_name }}{% if warnings.iswales %} (Natural Resources Wales){% endif %}</a>
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}{% if not warnings.iswales %}?tid={{ warnings.station_threshold_id }}{% endif %}">Monitor the latest{% if model.latestLevels.length > 1 %} {{ warnings.river_name }}{% endif %} level at {{ warnings.agency_name }}</a>
</p>
{% endif %}
{% endif %}
Expand Down
3 changes: 2 additions & 1 deletion test/data/nullTelemetry.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,6 @@
"lon": -0.306835309255374,
"lat": 50.8828385361225
},
"warningsAlerts": []
"warningsAlerts": [],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationAWSW.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,6 @@
"severity":"Flood warning",
"geometry":"{\"type\":\"Point\",\"coordinates\":[-3.03674273776571,53.8811798090409]}"
}
]
],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationActiveAlert.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,6 @@
"severity_value":1,
"severity":"Flood alert",
"geometry":"{\"type\":\"Point\",\"coordinates\":[-0.992084272973095,51.1269040605477]}"}
]
],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationActiveWarning.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,6 @@
"severity_value":2,
"severity":"Flood warning",
"geometry":"{\"type\":\"Point\",\"coordinates\":[-3.03674273776571,53.8811798090409]}"}
]
],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationCoastal.json
Original file line number Diff line number Diff line change
Expand Up @@ -2482,5 +2482,6 @@
}
],
"impacts":[],
"warningsAlerts":[]
"warningsAlerts":[],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationForecastData.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,5 +356,6 @@
"value": "4.3",
"threshold_type": "FW ACT FW"
}
]
],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationGroudwater.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,6 @@
"centroid":"0101000020E6100000893AA31F375496BFB3A4EA31CFAB4940",
"lon":-0.0218056309757295,
"lat":51.3422605891914},
"warningsAlerts":[]
"warningsAlerts":[],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationMultipleAW.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,6 @@
"severity":"Flood warning",
"geometry":"{\"type\":\"Point\",\"coordinates\":[-3.03674273776571,53.8811798090409]}"
}
]
],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationRiver.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,5 +141,6 @@
"value": "4.3",
"threshold_type": "FW RES FW"
}
]
],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationRiverACTCON.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,6 @@
"value": "3.88",
"threshold_type": "FW ACTCON FW"
}
]
],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationRiverSpike.json
Original file line number Diff line number Diff line change
Expand Up @@ -2474,5 +2474,6 @@
"lon":-0.306835309255374,
"lat":50.8828385361225
},
"warningsAlerts":[]
"warningsAlerts":[],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationSevereWarning.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,6 @@
"severity_value":3,
"severity":"Severe flood warning",
"geometry":"{\"type\":\"Point\",\"coordinates\":[-3.03674273776571,53.8811798090409]}"}
]
],
"requestUrl": "http://localhost:3000/station/1001"
}
Loading

0 comments on commit 3f97c9a

Please sign in to comment.