Skip to content

Commit

Permalink
Forecast versus actual comparison icons (#3449)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgboss authored Mar 6, 2024
1 parent d3e525c commit 8f9d1ef
Show file tree
Hide file tree
Showing 8 changed files with 414 additions and 67 deletions.
59 changes: 52 additions & 7 deletions web/src/features/moreCast2/components/ColumnDefBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
GridAlignment,
GridCellParams,
GridColDef,
GridColumnHeaderParams,
Expand All @@ -12,7 +13,8 @@ import { modelColorClass, modelHeaderColorClass } from 'app/theme'
import { GridComponentRenderer } from 'features/moreCast2/components/GridComponentRenderer'

export const DEFAULT_COLUMN_WIDTH = 80
export const DEFAULT_FORECAST_COLUMN_WIDTH = 120
export const DEFAULT_FORECAST_COLUMN_WIDTH = 145
export const DEFAULT_FORECAST_SUMMARY_COLUMN_WIDTH = 100

// Defines the order in which weather models display in the datagrid.
export const ORDERED_COLUMN_HEADERS: WeatherDeterminateType[] = [
Expand All @@ -39,6 +41,7 @@ export const GC_HEADER = 'GC'

export interface ForecastColDefGenerator {
generateForecastColDef: (headerName?: string) => GridColDef
generateForecastSummaryColDef: () => GridColDef
}

export interface ColDefGenerator {
Expand Down Expand Up @@ -68,6 +71,15 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato
)
}

public generateForecastSummaryColDef = () => {
return this.generateForecastSummaryColDefWith(
`${this.field}${WeatherDeterminate.FORECAST}`,
this.headerName,
this.precision,
DEFAULT_FORECAST_SUMMARY_COLUMN_WIDTH
)
}

public generateColDefs = (headerName?: string, includeBiasFields = true) => {
const gridColDefs: GridColDef[] = []
// Forecast columns have unique requirement (eg. column header menu, editable, etc.)
Expand All @@ -86,12 +98,7 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato
? ORDERED_COLUMN_HEADERS
: ORDERED_COLUMN_HEADERS.filter(header => !header.endsWith('_BIAS'))
return fields.map(header =>
this.generateColDefWith(
`${this.field}${header}`,
header,
this.precision,
header.includes('Actual') ? DEFAULT_FORECAST_COLUMN_WIDTH : DEFAULT_COLUMN_WIDTH
)
this.generateColDefWith(`${this.field}${header}`, header, this.precision, DEFAULT_COLUMN_WIDTH)
)
}

Expand All @@ -100,6 +107,7 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato
field,
disableColumnMenu: true,
disableReorder: true,
headerAlign: 'center' as GridAlignment,
headerName,
sortable: false,
type: 'number',
Expand Down Expand Up @@ -135,6 +143,7 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato
disableColumnMenu: true,
disableReorder: true,
editable: true,
headerAlign: 'center' as GridAlignment,
headerName: headerName,
sortable: false,
type: 'number',
Expand All @@ -159,6 +168,42 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato
}
}

public generateForecastSummaryColDefWith = (field: string, headerName: string, precision: number, width?: number) => {
const isGrassField = field.includes('grass')
const isCalcField = field.includes('Calc')
if (isGrassField || isCalcField) {
width = DEFAULT_COLUMN_WIDTH
}
return {
field: field,
disableColumnMenu: true,
disableReorder: true,
editable: true,
headerAlign: 'center' as GridAlignment,
headerName: headerName,
sortable: false,
type: 'number',
width: width ?? DEFAULT_FORECAST_SUMMARY_COLUMN_WIDTH,
renderHeader: (params: GridColumnHeaderParams) => {
return isCalcField || isGrassField
? this.gridComponentRenderer.renderHeaderWith(params)
: this.gridComponentRenderer.renderForecastHeaderWith(params)
},
renderCell: (params: Pick<GridRenderCellParams, 'row' | 'formattedValue'>) => {
return isCalcField
? this.gridComponentRenderer.renderCellWith(params)
: this.gridComponentRenderer.renderForecastSummaryCellWith(params)
},
valueFormatter: (params: Pick<GridValueFormatterParams, 'value'>) => {
return this.valueFormatterWith(params, precision)
},
valueGetter: (params: Pick<GridValueGetterParams, 'row' | 'value'>) =>
this.gridComponentRenderer.valueGetter(params, precision, field, headerName),
valueSetter: (params: Pick<GridValueSetterParams, 'row' | 'value'>) =>
this.valueSetterWith(params, field, precision)
}
}

public valueFormatterWith = (params: Pick<GridValueFormatterParams, 'value'>, precision: number) =>
this.gridComponentRenderer.predictionItemValueFormatter(params, precision)
public valueGetter = (
Expand Down
2 changes: 1 addition & 1 deletion web/src/features/moreCast2/components/DataGridColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class DataGridColumns {

public static getSummaryColumns(): GridColDef[] {
return MORECAST2_STATION_DATE_FIELDS.map(field => field.generateColDef()).concat(
MORECAST2_FORECAST_FIELDS.map(forecastField => forecastField.generateForecastColDef()).concat(
MORECAST2_FORECAST_FIELDS.map(forecastField => forecastField.generateForecastSummaryColDef()).concat(
MORECAST2_INDEX_FIELDS.map(field => field.generateForecastColDef())
)
)
Expand Down
72 changes: 72 additions & 0 deletions web/src/features/moreCast2/components/ForecastCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react'
import { Grid, TextField, Tooltip } from '@mui/material'
import { GridRenderCellParams } from '@mui/x-data-grid'
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle'
import AddBoxIcon from '@mui/icons-material/AddBox'
import { MEDIUM_GREY } from 'app/theme'

interface ForecastCellProps {
disabled: boolean
label: string
showGreaterThan: boolean
showLessThan: boolean
value: Pick<GridRenderCellParams, 'formattedValue'>
}

const ForecastCell = ({ disabled, label, showGreaterThan, showLessThan, value }: ForecastCellProps) => {
// We should never display both less than and greater than icons at the same time
if (showGreaterThan && showLessThan) {
throw Error('ForecastCell cannot show both greater than and less than icons at the same time.')
}
return (
<Grid container sx={{ justifyContent: 'center', alignItems: 'center' }}>
<Grid item xs={2}>
{showLessThan && (
<Tooltip placement="bottom-end" title="Lower than actual">
<RemoveCircleIcon
data-testid="forecast-cell-less-than-icon"
sx={{ color: MEDIUM_GREY, fontSize: '1.15rem' }}
/>
</Tooltip>
)}
</Grid>
<Grid item xs={8}>
<TextField
data-testid="forecast-cell-text-field"
disabled={disabled}
size="small"
label={label}
InputLabelProps={{
shrink: true
}}
sx={{
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: '#737373',
borderWidth: '2px'
}
},
'& .Mui-disabled': {
'& fieldset': {
borderWidth: '1px'
}
}
}}
value={value}
></TextField>
</Grid>
<Grid item xs={2} sx={{ marginLeft: 'auto' }}>
{showGreaterThan && (
<Tooltip placement="bottom-start" title="Higher than actual">
<AddBoxIcon
data-testid="forecast-cell-greater-than-icon"
sx={{ color: MEDIUM_GREY, fontSize: '1.25rem', marginLeft: '2px' }}
/>
</Tooltip>
)}
</Grid>
</Grid>
)
}

export default ForecastCell
63 changes: 51 additions & 12 deletions web/src/features/moreCast2/components/GridComponentRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
WIND_SPEED_HEADER
} from 'features/moreCast2/components/ColumnDefBuilder'
import { theme } from 'app/theme'
import { isNumber } from 'lodash'
import ForecastCell from 'features/moreCast2/components/ForecastCell'

export const NOT_AVAILABLE = 'N/A'
export const NOT_REPORTING = 'N/R'
Expand Down Expand Up @@ -104,20 +106,57 @@ export class GridComponentRenderer {
// We can disable a cell if an Actual exists or the forDate is before today.
// Both forDate and today are currently in the system's time zone
const isPreviousDate = isPreviousToToday(params.row['forDate'])

const isGrassField = field.includes('grass')
const label = isGrassField || isPreviousDate ? '' : createWeatherModelLabel(params.row[field].choice)
const formattedValue = parseFloat(params.formattedValue)
const actualField = this.getActualField(field)
const actualValue = params.row[actualField]
let showLessThan = false
let showGreaterThan = false
// Only show + and - icons if an actual value exists, a forecast value exists and this is not a windDirection
// field.
if (!isNaN(actualValue) && isNumber(actualValue) && isNumber(formattedValue) && !field.includes('windDirection')) {
showLessThan = formattedValue < actualValue
showGreaterThan = formattedValue > actualValue
}

// The grass curing 'forecast' field is rendered differently
if (isGrassField) {
return (
<TextField
disabled={isActual || isPreviousDate}
size="small"
label={label}
InputLabelProps={{
shrink: true
}}
value={params.formattedValue}
></TextField>
)
} else {
// Forecast fields (except wind direction) have plus and minus icons indicating if the forecast was
// greater than or less than the actual
return (
<ForecastCell
disabled={isActual || isPreviousDate}
label={label}
showGreaterThan={showGreaterThan}
showLessThan={showLessThan}
value={params.formattedValue}
/>
)
}
}

public renderForecastSummaryCellWith = (params: Pick<GridRenderCellParams, 'row' | 'formattedValue'>) => {
// If a single cell in a row contains an Actual, no Forecast will be entered into the row anymore, so we can disable the whole row.
const isActual = this.rowContainsActual(params.row)
// We can disable a cell if an Actual exists or the forDate is before today.
// Both forDate and today are currently in the system's time zone
const isPreviousDate = isPreviousToToday(params.row['forDate'])

return (
<TextField
disabled={isActual || isPreviousDate}
size="small"
label={isGrassField ? '' : createWeatherModelLabel(params.row[field].choice)}
InputLabelProps={{
shrink: true
}}
value={params.formattedValue}
></TextField>
)
// The grass curing 'forecast' field and other weather parameter forecasts fields are rendered differently
return <TextField disabled={isActual || isPreviousDate} size="small" value={params.formattedValue}></TextField>
}

public predictionItemValueSetter = (
Expand Down
4 changes: 4 additions & 0 deletions web/src/features/moreCast2/components/MoreCast2Column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ export class IndeterminateField implements ColDefGenerator, ForecastColDefGenera
return this.colDefBuilder.generateForecastColDef(headerName ?? this.headerName)
}

public generateForecastSummaryColDef = () => {
return this.colDefBuilder.generateForecastSummaryColDef()
}

public generateColDef = () => {
return this.colDefBuilder.generateColDefWith(this.field, this.headerName, this.precision)
}
Expand Down
Loading

0 comments on commit 8f9d1ef

Please sign in to comment.