Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# Check if npm is available
if ! command -v npm &> /dev/null; then
echo "Warning: npm not found in PATH. Skipping pre-commit hook."
exit 0
fi

npm run git:pre-commit-hook
40 changes: 28 additions & 12 deletions src/client/javascripts/line-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { extent, bisector } from 'd3-array'

const DISPLAYED_HOUR_ON_X_AXIS = 6

export function LineChart (containerId, stationId, data, options = {}) {
export function LineChart(containerId, stationId, data, options = {}) {
const container = document.getElementById(containerId)

if (!container) {
Expand Down Expand Up @@ -86,6 +86,13 @@ export function LineChart (containerId, stationId, data, options = {}) {
.tickFormat('')
)

// Remove grid lines after the latest data point
svg.select('.x.grid').selectAll('.tick').each(function (d) {
if (d > xExtent[1]) {
select(this).remove()
}
})

// Grid lines don't have labels, so we don't need to remove anything from grid

svg.select('.y.grid')
Expand All @@ -111,7 +118,7 @@ export function LineChart (containerId, stationId, data, options = {}) {
.text(timeFormat('%-e %b')(new Date()))

// Add height to locator line
svg.select('.locator-line').attr('y1', 0).attr('y2', height)
inner.select('.locator__line').attr('y1', 0).attr('y2', height)

// Draw lines and areas
if (dataCache.observed.length) {
Expand Down Expand Up @@ -202,6 +209,7 @@ export function LineChart (containerId, stationId, data, options = {}) {
const isForecast = (new Date(dataPoint.dateTime)) > (new Date(dataCache.latestDateTime))
locator.classed('locator--forecast', isForecast)
locator.attr('transform', 'translate(' + locatorX + ',' + 0 + ')')
locator.select('.locator__line').attr('y2', height)
locator.select('.locator-point').attr('transform', 'translate(' + 0 + ',' + locatorY + ')')
}

Expand All @@ -222,10 +230,12 @@ export function LineChart (containerId, stationId, data, options = {}) {

const setScaleX = () => {
xExtent = extent(dataCache.observed.concat(dataCache.forecast), (d, i) => { return new Date(d.dateTime) })
// Don't extend beyond the last recorded time
xScaleInitial = scaleTime().domain(xExtent)
// Add padding to the right side (10% of the time range)
const timeRange = xExtent[1] - xExtent[0]
const paddedMax = new Date(xExtent[1].getTime() + (timeRange * 0.05))
xScaleInitial = scaleTime().domain([xExtent[0], paddedMax])
xScaleInitial.range([0, width])
xScale = scaleTime().domain(xExtent)
xScale = scaleTime().domain([xExtent[0], paddedMax])
}

const setScaleY = () => {
Expand All @@ -250,15 +260,21 @@ export function LineChart (containerId, stationId, data, options = {}) {
lines = []

if (dataCache.observed && dataCache.observed.length) {
dataCache.observed = simplify(dataCache.observed, dataCache.type === 'tide' ? 10000000 : 1000000)
// Don't simplify river data to preserve 15-minute intervals
if (dataCache.type !== 'river') {
dataCache.observed = simplify(dataCache.observed, dataCache.type === 'tide' ? 10000000 : 1000000)
}
const errorFilter = l => !l.err
const errorAndNegativeFilter = l => errorFilter(l)
const filterNegativeValues = ['groundwater', 'tide', 'sea'].includes(dataCache.type) ? errorFilter : errorAndNegativeFilter
lines = dataCache.observed.filter(filterNegativeValues).map(l => ({ ...l, type: 'observed' })).reverse()
dataPoint = lines[lines.length - 1] || null
}
if (dataCache.forecast && dataCache.forecast.length) {
dataCache.forecast = simplify(dataCache.forecast, dataCache.type === 'tide' ? 10000000 : 1000000)
// Don't simplify river forecast data to preserve 15-minute intervals
if (dataCache.type !== 'river') {
dataCache.forecast = simplify(dataCache.forecast, dataCache.type === 'tide' ? 10000000 : 1000000)
}
const latestTime = (new Date(dataCache.observed[0].dateTime).getTime())
const forecastStartTime = (new Date(dataCache.forecast[0].dateTime).getTime())
const latestValue = dataCache.observed[0].value
Expand Down Expand Up @@ -334,8 +350,8 @@ export function LineChart (containerId, stationId, data, options = {}) {
timeLabel.append('tspan').attr('class', 'time-now-text__date').attr('text-anchor', 'middle').attr('x', 0).attr('dy', '15')

const locator = inner.append('g').attr('class', 'locator')
locator.append('line').attr('class', 'locator-line')
locator.append('circle').attr('r', 4.5).attr('class', 'locator-point')
locator.append('line').attr('class', 'locator__line').attr('x1', 0).attr('x2', 0).attr('y1', 0).attr('y2', 0)
locator.append('circle').attr('r', 5).attr('class', 'locator-point')

const significantContainer = mainGroup.append('g').attr('class', 'significant').attr('role', 'grid').append('g').attr('role', 'row')

Expand Down Expand Up @@ -381,7 +397,7 @@ export function LineChart (containerId, stationId, data, options = {}) {
})

svg.on('click', (e) => {
getDataPointByX(pointer(e)[0])
getDataPointByX(pointer(e)[0] - margin.left)
showTooltip(pointer(e)[1])
})

Expand All @@ -396,7 +412,7 @@ export function LineChart (containerId, stationId, data, options = {}) {
return
}
interfaceType = 'mouse'
getDataPointByX(pointer(e)[0])
getDataPointByX(pointer(e)[0] - margin.left)
showTooltip(pointer(e)[1])
})

Expand All @@ -408,7 +424,7 @@ export function LineChart (containerId, stationId, data, options = {}) {
if (!xScale) return
const touchEvent = e.targetTouches[0]
const elementOffsetX = svg.node().getBoundingClientRect().left
getDataPointByX(pointer(touchEvent)[0] - elementOffsetX)
getDataPointByX(pointer(touchEvent)[0] - elementOffsetX - margin.left)
showTooltip(10)
})

Expand Down
64 changes: 42 additions & 22 deletions src/client/stylesheets/components/_charts.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Chart styling to match Check for Flooding service
@use "govuk-frontend" as govuk;

.defra-line-chart {
margin-bottom: 2rem;
Expand All @@ -18,7 +19,9 @@

.grid {
line {
stroke: #b1b4b6;
fill: none;
color: rgba(govuk.$govuk-text-colour, 0.1);
stroke: currentColor;
stroke-width: 1;
shape-rendering: crispEdges;
}
Expand All @@ -30,6 +33,7 @@

.axis {
line {
color: govuk.$govuk-border-colour;
stroke: #0b0c0c;
stroke-width: 1;
shape-rendering: crispEdges;
Expand All @@ -42,13 +46,33 @@

text {
fill: #0b0c0c;
font-size: 14px;
font-size: 16px;
}
}

.y.axis {
path {
stroke-width: 0;
}

.tick line {
stroke-width: 0;
}
}

.x.axis {
.tick line {
stroke-width: 0;
}

path {
stroke: govuk.$govuk-border-colour;
}
}

.observed-area {
fill: #1d70b8;
fill-opacity: 0.2;
fill: govuk.govuk-colour("blue");
fill-opacity: 0.1;
}

.observed-line {
Expand All @@ -71,22 +95,22 @@

.time-line {
stroke: #0b0c0c;
stroke-width: 2;
stroke-dasharray: 3, 3;
stroke-width: 1;
shape-rendering: crispEdges;
}

.time-now-text {
fill: #0b0c0c;
font-size: 12px;
font-size: 16px;
text-anchor: middle;
font-weight: bold;
}

.locator {
&__line {
stroke: #0b0c0c;
stroke-width: 1;
opacity: 0;
stroke: #b1b4b6;
stroke-width: 0.25;
opacity: 1;

&--visible {
opacity: 1;
Expand All @@ -113,12 +137,6 @@
opacity: 1;
}
}

&--forecast {
.locator-point {
stroke: #f47738;
}
}
}

.tooltip {
Expand All @@ -130,16 +148,18 @@
}

&-bg {
fill: #0b0c0c;
fill: #fff;
stroke: #0b0c0c;
stroke-width: 0.5;
}

&-text {
fill: #fff;
font-size: 14px;
fill: #0b0c0c;
font-size: 16px;

&__strong {
font-weight: 700;
font-size: 16px;
font-size: 18px;
}

&__small {
Expand Down Expand Up @@ -192,7 +212,7 @@
}

&--forecast circle {
stroke: #f47738;
stroke: #1d70b8;
}

text {
Expand Down Expand Up @@ -241,4 +261,4 @@
&__text {
font-size: 16px;
}
}
}
7 changes: 4 additions & 3 deletions src/common/helpers/serve-static-files.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import http2 from 'node:http2'
import path from 'node:path'
import { config } from '../../config/config.js'

const { constants: httpConstants } = http2

export const serveStaticFiles = {
plugin: {
name: 'staticFiles',
register (server) {
register(server) {
server.route([
{
options: {
Expand All @@ -18,7 +19,7 @@ export const serveStaticFiles = {
},
method: 'GET',
path: '/favicon.ico',
handler (_request, h) {
handler(_request, h) {
return h.response().code(httpConstants.HTTP_STATUS_NO_CONTENT).type('image/x-icon')
}
},
Expand All @@ -34,7 +35,7 @@ export const serveStaticFiles = {
path: `${config.get('assetPath')}/{param*}`,
handler: {
directory: {
path: '.',
path: path.join(config.get('root'), '.public'),
redirectToSlash: true
}
}
Expand Down
21 changes: 13 additions & 8 deletions src/lib/flood-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const API_BASE_URL = config.get('api.floodMonitoring.baseUrl')
* Fetch via proxy using Node.js native fetch
* To use the fetch dispatcher option on Node.js native fetch, Node.js v18.2.0 or greater is required
*/
export function proxyFetch (url, options = {}) {
export function proxyFetch(url, options = {}) {
const proxyUrlConfig = config.get('httpProxy') // bound to HTTP_PROXY

if (!proxyUrlConfig) {
Expand All @@ -34,13 +34,13 @@ export function proxyFetch (url, options = {}) {
/**
* Fetch station details by RLOI ID (Check for Flooding ID)
*/
export async function getStation (stationId) {
export async function getStation(stationId) {
const url = `${API_BASE_URL}/id/stations?RLOIid=${stationId}`
try {
console.log(`Fetching station from: ${url}`)
const response = await proxyFetch(url)
console.log(`Station API response status: ${response.status} ${response.statusText}`)

if (!response.ok) {
const errorText = await response.text().catch(() => 'Unable to read error response')
throw new Error(`Failed to fetch station: ${response.status} ${response.statusText} - ${errorText}`)
Expand Down Expand Up @@ -70,7 +70,7 @@ export async function getStation (stationId) {
/**
* Fetch station readings/measurements
*/
export async function getStationReadings (stationId, since = null) {
export async function getStationReadings(stationId, since = null) {
const stationUrl = `${API_BASE_URL}/id/stations?RLOIid=${stationId}`
try {
// First get the station to find its measures
Expand Down Expand Up @@ -130,10 +130,15 @@ export async function getStationReadings (stationId, since = null) {
/**
* Format station data for the view
*/
export function formatStationData (station, readings) {
export function formatStationData(station, readings) {
if (!station) return null

const latestReading = readings.length > 0 ? readings[readings.length - 1] : null
// Get the most recent reading by date (readings might not be sorted correctly)
const latestReading = readings.length > 0
? readings.reduce((latest, current) =>
new Date(current.dateTime) > new Date(latest.dateTime) ? current : latest
)
: null
const latestValue = latestReading?.value || 0

// Calculate trend (simplified - compare to reading from 1 hour ago)
Expand Down Expand Up @@ -185,7 +190,7 @@ export function formatStationData (station, readings) {
/**
* Format readings for chart
*/
export function formatTelemetryData (readings) {
export function formatTelemetryData(readings) {
// Filter to last 5 days only
const fiveDaysAgo = new Date()
fiveDaysAgo.setDate(fiveDaysAgo.getDate() - 5)
Expand Down Expand Up @@ -214,7 +219,7 @@ export function formatTelemetryData (readings) {
/**
* Search for stations
*/
export async function searchStations (query = {}) {
export async function searchStations(query = {}) {
try {
const params = new URLSearchParams()
if (query.label) params.append('label', query.label)
Expand Down
5 changes: 3 additions & 2 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ export const index = [
method: 'POST',
path: '/station',
handler: async function (request, h) {
const { dataType, stationType, stationId } = request.payload
const { dataType, stationType, stationId, chartStyle } = request.payload

// Build query params
const params = new URLSearchParams({
dataType: dataType || 'existing',
stationType: stationType || 'S'
stationType: stationType || 'S',
chartStyle: chartStyle || 'styleA'
})

// Add stationId if provided
Expand Down
Loading