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] 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 = ` + +