Skip to content

Commit

Permalink
MoreCast: Forecast - Actual split (bcgov#3415)
Browse files Browse the repository at this point in the history
-splits out forecast and actual columns
-removes N/A for forecast-able cells
-disables cells for previous dates
-No 'Actual' label in Actual column

closes bcgov#3394
  • Loading branch information
brettedw authored Feb 16, 2024
1 parent 544c162 commit 1a8a148
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 71 deletions.
34 changes: 26 additions & 8 deletions web/src/features/moreCast2/components/ColumnDefBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const DEFAULT_FORECAST_COLUMN_WIDTH = 120

// Defines the order in which weather models display in the datagrid.
export const ORDERED_COLUMN_HEADERS: WeatherDeterminateType[] = [
WeatherDeterminate.ACTUAL,
WeatherDeterminate.HRDPS,
WeatherDeterminate.HRDPS_BIAS,
WeatherDeterminate.RDPS,
Expand All @@ -26,6 +27,14 @@ export const ORDERED_COLUMN_HEADERS: WeatherDeterminateType[] = [
WeatherDeterminate.GFS_BIAS
]

// Columns that can have values entered as part of a forecast
export const TEMP_HEADER = 'Temp'
export const RH_HEADER = 'RH'
export const WIND_SPEED_HEADER = 'Wind Speed'
export const WIND_DIR_HEADER = 'Wind Dir'
export const PRECIP_HEADER = 'Precip'
export const GC_HEADER = 'GC'

export interface ForecastColDefGenerator {
generateForecastColDef: (headerName?: string) => GridColDef
}
Expand All @@ -51,7 +60,7 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato
public generateForecastColDef = (headerName?: string) => {
return this.generateForecastColDefWith(
`${this.field}${WeatherDeterminate.FORECAST}`,
headerName ? headerName : this.headerName,
headerName ?? this.headerName,
this.precision,
DEFAULT_FORECAST_COLUMN_WIDTH
)
Expand All @@ -75,7 +84,12 @@ 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, DEFAULT_COLUMN_WIDTH)
this.generateColDefWith(
`${this.field}${header}`,
header,
this.precision,
header.includes('Actual') ? DEFAULT_FORECAST_COLUMN_WIDTH : DEFAULT_COLUMN_WIDTH
)
)
}

Expand All @@ -87,13 +101,15 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato
headerName,
sortable: false,
type: 'number',
width: width ? width : DEFAULT_COLUMN_WIDTH,
width: width ?? DEFAULT_COLUMN_WIDTH,
renderCell: (params: Pick<GridRenderCellParams, 'formattedValue'>) => {
return this.gridComponentRenderer.renderCellWith(params)
},
renderHeader: (params: GridColumnHeaderParams) => {
return this.gridComponentRenderer.renderHeaderWith(params)
},
valueGetter: (params: Pick<GridValueGetterParams, 'row' | 'value'>) =>
this.gridComponentRenderer.valueGetter(params, precision, field, headerName),
valueFormatter: (params: Pick<GridValueFormatterParams, 'value'>) => {
return this.valueFormatterWith(params, precision)
}
Expand Down Expand Up @@ -129,18 +145,20 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato
return this.valueFormatterWith(params, precision)
},
valueGetter: (params: Pick<GridValueGetterParams, 'row' | 'value'>) =>
this.gridComponentRenderer.valueGetter(params, precision, field),
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 valueGetterWith = (params: Pick<GridValueGetterParams, 'value'>, precision: number) =>
this.gridComponentRenderer.cellValueGetter(params, precision)
public valueGetter = (params: Pick<GridValueGetterParams, 'row' | 'value'>, field: string, precision: number) =>
this.gridComponentRenderer.valueGetter(params, precision, field)
public valueGetter = (
params: Pick<GridValueGetterParams, 'row' | 'value'>,
field: string,
precision: number,
headerName: string
) => this.gridComponentRenderer.valueGetter(params, precision, field, headerName)
public valueSetterWith = (params: Pick<GridValueSetterParams, 'row' | 'value'>, field: string, precision: number) =>
this.gridComponentRenderer.predictionItemValueSetter(params, field, precision)
}
98 changes: 72 additions & 26 deletions web/src/features/moreCast2/components/GridComponentRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ import {
GridValueGetterParams,
GridValueSetterParams
} from '@mui/x-data-grid'
import { ModelChoice } from 'api/moreCast2API'
import { createLabel } from 'features/moreCast2/util'
import { ModelChoice, WeatherDeterminate } from 'api/moreCast2API'
import { createWeatherModelLabel, isPreviousToToday } from 'features/moreCast2/util'
import { MoreCast2Row } from 'features/moreCast2/interfaces'
import {
GC_HEADER,
PRECIP_HEADER,
RH_HEADER,
TEMP_HEADER,
WIND_DIR_HEADER,
WIND_SPEED_HEADER
} from 'features/moreCast2/components/ColumnDefBuilder'

const NOT_AVAILABLE = 'N/A'
export const NOT_AVAILABLE = 'N/A'
export const NOT_REPORTING = 'N/R'

export class GridComponentRenderer {
public renderForecastHeaderWith = (params: GridColumnHeaderParams) => {
Expand Down Expand Up @@ -39,40 +49,66 @@ export class GridComponentRenderer {
return actualField
}

public rowContainsActual = (row: MoreCast2Row): boolean => {
for (const key in row) {
if (key.includes(WeatherDeterminate.ACTUAL)) {
const value = row[key as keyof MoreCast2Row]
if (typeof value === 'number' && !isNaN(value)) {
return true
}
}
}
return false
}

public valueGetter = (
params: Pick<GridValueGetterParams, 'row' | 'value'>,
precision: number,
field: string
field: string,
headerName: string
): string => {
const actualField = this.getActualField(field)
const actual = params.row[actualField]

if (!isNaN(actual)) {
return Number(actual).toFixed(precision)
// The grass curing column is the only column that shows both actuals and forecast in a single column
if (field.includes('grass')) {
const actualField = this.getActualField(field)
const actual = params.row[actualField]

if (!isNaN(actual)) {
return Number(actual).toFixed(precision)
}
}

const value = params?.value?.value
const value = params?.value?.value ?? params.value
// The 'Actual' column will show N/R for Not Reporting, instead of N/A
const noDataField = headerName === WeatherDeterminate.ACTUAL ? NOT_REPORTING : NOT_AVAILABLE

if (isNaN(value)) {
return 'NaN'
}
return Number(value).toFixed(precision)
const isPreviousDate = isPreviousToToday(params.row['forDate'])
const isForecastColumn = this.isForecastColumn(headerName)
const rowContainsActual = this.rowContainsActual(params.row)

// If a cell has no value, belongs to a Forecast column, is a future forDate, and the row doesn't contain any Actuals from today,
// we can leave it blank, so it's obvious that it can have a value entered into it.
if (isNaN(value) && !isPreviousDate && isForecastColumn && !rowContainsActual) {
return ''
} else return isNaN(value) ? noDataField : value.toFixed(precision)
}

public renderForecastCellWith = (params: Pick<GridRenderCellParams, 'row' | 'formattedValue'>, field: string) => {
// The value of field will be precipForecast, rhForecast, tempForecast, etc.
// We need the prefix to help us grab the correct 'actual' field (eg. tempACTUAL, precipACTUAL, etc.)
const actualField = this.getActualField(field)
// 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'])

const isGrassField = field.includes('grass')

const isActual = !isNaN(params.row[actualField])

return (
<TextField
disabled={isActual}
disabled={isActual || isPreviousDate}
size="small"
label={isGrassField ? '' : createLabel(isActual, params.row[field].choice)}
label={isGrassField ? '' : createWeatherModelLabel(params.row[field].choice)}
InputLabelProps={{
shrink: true
}}
value={params.formattedValue}
></TextField>
)
Expand All @@ -98,13 +134,23 @@ export class GridComponentRenderer {
return { ...params.row }
}

public isForecastColumn = (headerName: string): boolean => {
const forecastColumns = [
WeatherDeterminate.FORECAST,
TEMP_HEADER,
RH_HEADER,
WIND_DIR_HEADER,
WIND_SPEED_HEADER,
PRECIP_HEADER,
GC_HEADER
]

return forecastColumns.some(column => column === headerName)
}

public predictionItemValueFormatter = (params: Pick<GridValueFormatterParams, 'value'>, precision: number) => {
const value = Number.parseFloat(params?.value)

return isNaN(value) ? NOT_AVAILABLE : value.toFixed(precision)
}

public cellValueGetter = (params: Pick<GridValueGetterParams, 'value'>, precision: number) => {
return isNaN(params?.value) ? 'NaN' : params.value.toFixed(precision)
return isNaN(value) ? params.value : value.toFixed(precision)
}
}
20 changes: 13 additions & 7 deletions web/src/features/moreCast2/components/MoreCast2Column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { DateTime } from 'luxon'
import {
ColDefGenerator,
ColumnDefBuilder,
ForecastColDefGenerator
ForecastColDefGenerator,
GC_HEADER,
PRECIP_HEADER,
RH_HEADER,
TEMP_HEADER,
WIND_DIR_HEADER,
WIND_SPEED_HEADER
} from 'features/moreCast2/components/ColumnDefBuilder'
import { GridComponentRenderer } from 'features/moreCast2/components/GridComponentRenderer'

Expand Down Expand Up @@ -107,19 +113,19 @@ export class IndeterminateField implements ColDefGenerator, ForecastColDefGenera
}
}

export const tempForecastField = new IndeterminateField('temp', 'Temp', 'number', 1, true)
export const rhForecastField = new IndeterminateField('rh', 'RH', 'number', 0, true)
export const windDirForecastField = new IndeterminateField('windDirection', 'Wind Dir', 'number', 0, true)
export const windSpeedForecastField = new IndeterminateField('windSpeed', 'Wind Speed', 'number', 1, true)
export const precipForecastField = new IndeterminateField('precip', 'Precip', 'number', 1, true)
export const tempForecastField = new IndeterminateField('temp', TEMP_HEADER, 'number', 1, true)
export const rhForecastField = new IndeterminateField('rh', RH_HEADER, 'number', 0, true)
export const windDirForecastField = new IndeterminateField('windDirection', WIND_DIR_HEADER, 'number', 0, true)
export const windSpeedForecastField = new IndeterminateField('windSpeed', WIND_SPEED_HEADER, 'number', 1, true)
export const precipForecastField = new IndeterminateField('precip', PRECIP_HEADER, 'number', 1, true)
export const buiField = new IndeterminateField('buiCalc', 'BUI', 'number', 0, false)
export const isiField = new IndeterminateField('isiCalc', 'ISI', 'number', 1, false)
export const fwiField = new IndeterminateField('fwiCalc', 'FWI', 'number', 0, false)
export const ffmcField = new IndeterminateField('ffmcCalc', 'FFMC', 'number', 1, false)
export const dmcField = new IndeterminateField('dmcCalc', 'DMC', 'number', 0, false)
export const dcField = new IndeterminateField('dcCalc', 'DC', 'number', 0, false)
export const dgrField = new IndeterminateField('dgrCalc', 'DGR', 'number', 0, false)
export const gcField = new IndeterminateField('grassCuring', 'GC', 'number', 0, false)
export const gcField = new IndeterminateField('grassCuring', GC_HEADER, 'number', 0, false)

export const MORECAST2_STATION_DATE_FIELDS: ColDefGenerator[] = [
StationForecastField.getInstance(),
Expand Down
12 changes: 9 additions & 3 deletions web/src/features/moreCast2/components/TabbedDataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import {
GridColumnVisibilityModel,
GridEventListener
} from '@mui/x-data-grid'
import { ModelChoice, ModelType, submitMoreCastForecastRecords } from 'api/moreCast2API'
import {
ModelChoice,
ModelType,
WeatherDeterminate,
WeatherDeterminateType,
submitMoreCastForecastRecords
} from 'api/moreCast2API'
import { getColumnGroupingModel, ColumnVis, DataGridColumns } from 'features/moreCast2/components/DataGridColumns'
import ForecastDataGrid from 'features/moreCast2/components/ForecastDataGrid'
import ForecastSummaryDataGrid from 'features/moreCast2/components/ForecastSummaryDataGrid'
Expand Down Expand Up @@ -390,8 +396,8 @@ const TabbedDataGrid = ({ morecast2Rows, fromTo, setFromTo }: TabbedDataGridProp
// occurs on a cell in a weather model field/column and row where a forecast is being created (ie. the
// row has no actual value for the weather parameter of interest)
const handleCellDoubleClick = (params: GridCellParams) => {
const headerName = params.colDef.headerName as ModelType
if (!headerName || headerName === ModelChoice.ACTUAL || headerName === ModelChoice.FORECAST) {
const headerName = params.colDef.headerName as WeatherDeterminateType
if (!headerName || headerName === WeatherDeterminate.ACTUAL || headerName === WeatherDeterminate.FORECAST) {
// A forecast or actual column was clicked, or there is no value for headerName, nothing to do
return
}
Expand Down
34 changes: 27 additions & 7 deletions web/src/features/moreCast2/components/colDefBuilder.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('ColDefBuilder', () => {
})
)
})
it('should generate all col defs correcty', () => {
it('should generate all col defs correctly', () => {
const colDefs = colDefBuilder.generateColDefs()

const expected = [
Expand All @@ -62,7 +62,7 @@ describe('ColDefBuilder', () => {
headerName: determinate,
sortable: false,
type: 'number',
width: DEFAULT_COLUMN_WIDTH
width: determinate === WeatherDeterminate.ACTUAL ? DEFAULT_FORECAST_COLUMN_WIDTH : DEFAULT_COLUMN_WIDTH
})
)
)
Expand Down Expand Up @@ -146,14 +146,34 @@ describe('ColDefBuilder', () => {
)
expect(
forecastColDef.renderCell({ row: { testField: { choice: ModelChoice.GDPS, value: 1 } }, formattedValue: 1 })
).toEqual(<TextField disabled={false} label={ModelChoice.GDPS} size="small" value={1} />)
).toEqual(
<TextField
disabled={false}
InputLabelProps={{
shrink: true
}}
label={ModelChoice.GDPS}
size="small"
value={1}
/>
)

expect(
forecastColDef.renderCell({
row: { testField: { choice: ModelChoice.GDPS, value: 1 }, testActual: 2 },
row: { testField: { choice: ModelChoice.GDPS, value: 1 } },
formattedValue: 1
})
).toEqual(<TextField disabled={false} label={ModelChoice.GDPS} size="small" value={1} />)
).toEqual(
<TextField
disabled={false}
InputLabelProps={{
shrink: true
}}
label={ModelChoice.GDPS}
size="small"
value={1}
/>
)
expect(forecastColDef.valueFormatter({ value: 1.11 })).toEqual('1.1')
expect(
forecastColDef.valueGetter({
Expand Down Expand Up @@ -185,15 +205,15 @@ describe('ColDefBuilder', () => {

it('should delegate to GridComponentRenderer', () => {
expect(colDefBuilder.valueFormatterWith({ value: 1.11 }, 1)).toEqual('1.1')
expect(colDefBuilder.valueGetterWith({ value: 1.11 }, 1)).toEqual('1.1')
expect(
colDefBuilder.valueGetter(
{
row: { testField: { choice: ModelChoice.GDPS, value: 1.11 } },
value: { choice: ModelChoice.GDPS, value: 1.11 }
},
'testField',
1
1,
'testHeader'
)
).toEqual('1.1')
expect(
Expand Down
Loading

0 comments on commit 1a8a148

Please sign in to comment.