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
16 changes: 14 additions & 2 deletions apps/src/components/fields/skill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,27 @@ import { __ } from '@/context/locale-provider';
import TooltipWidget from '@/components/ui/tooltip-widget';
import React from 'react';

import { useClimateVariable } from '@/hooks/use-climate-variable';
import { ForecastDisplays } from '@/types/climate-variable-interface';

export interface S2DForecastDisplaySkillFieldCheckboxProps {
tooltip?: React.ReactNode;
}

/**
* @see {@link selectLowSkillVisibility} — "skill" is an S2D-specific concept
*/
export const MaskLowSkillField = (
props: S2DForecastDisplaySkillFieldCheckboxProps,
) => {
const { climateVariable } = useClimateVariable();

const dispatch = useAppDispatch();

const checked = useAppSelector(selectLowSkillVisibility());
const forecastDisplay = climateVariable?.getForecastDisplay();
const isForecast = forecastDisplay === ForecastDisplays.FORECAST;

const onCheckedChange = (checked: boolean) => {
dispatch(setLowSkillVisibility({visible: checked}));
};
Expand All @@ -29,19 +40,20 @@ export const MaskLowSkillField = (
const fieldProps = {
checked,
onCheckedChange,
disabled: !isForecast,
...propsRest,
};

return (
<div className="flex items-center space-x-2">
<Checkbox
id='mask-low-skill-checkbox'
className="text-brand-red"
className="text-brand-red disabled:data-[state=checked]:bg-neutral-grey-medium disabled:data-[state=checked]:border-neutral-grey-medium"
{...fieldProps}
/>
<label
htmlFor='mask-low-skill-checkbox'
className="text-sm font-medium leading-none cursor-pointer"
className="text-sm font-medium leading-none cursor-pointer peer-disabled:opacity-50 peer-disabled:cursor-default"
>
{__('Mask Low Skill')}
</label>
Expand Down
19 changes: 16 additions & 3 deletions apps/src/components/map-layers/low-skill-layer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useS2D } from '@/hooks/use-s2d';
import { useAppSelector } from '@/app/hooks';
import { selectLowSkillVisibility } from '@/features/map/map-slice';
import { buildSkillLayerName, buildSkillLayerTime } from '@/lib/s2d';
import { ForecastDisplays } from '@/types/climate-variable-interface';
import L from 'leaflet';

interface LowSkillLayerProps {
Expand All @@ -14,13 +15,17 @@ interface LowSkillLayerProps {

/**
* Leaflet layer for the "low skill".
*
* @see {@link selectLowSkillVisibility} — "skill" is an S2D-specific concept
*/
const LowSkillLayer = ({
pane,
}: LowSkillLayerProps): React.ReactElement | null => {
const { climateVariable } = useClimateVariable();
const forecastDisplay = climateVariable?.getForecastDisplay();
const isForecast = forecastDisplay === ForecastDisplays.FORECAST;
const { releaseDate } = useS2D();
const isLowSkillMasked = !useAppSelector(selectLowSkillVisibility());
const isLowSkillVisible = useAppSelector(selectLowSkillVisibility());
const {
opacity: { mapData },
} = useAppSelector((state) => state.map);
Expand All @@ -33,6 +38,9 @@ const LowSkillLayer = ({
timeValue = buildSkillLayerTime(climateVariable, releaseDate);
}

const hasLayerData = layerName && timeValue;
const shouldHideLayer = !isForecast || !isLowSkillVisible;

// Update the opacity on the *existing* layer if it exists. We do it like
// that because we don't want a change in opacity to recreate the layer.
// Instead, we want to update the existing layer (else it creates flashing
Expand All @@ -49,7 +57,7 @@ const LowSkillLayer = ({
// attributes change.
return useMemo(
() => {
if (isLowSkillMasked || !layerName || !timeValue) {
if (shouldHideLayer || !hasLayerData) {
return null;
}

Expand Down Expand Up @@ -78,7 +86,12 @@ const LowSkillLayer = ({
// above takes care of that). But we still use it as an initial value.
//
// eslint-disable-next-line react-hooks/exhaustive-deps
[pane, isLowSkillMasked, layerName, timeValue]
[
layerName,
pane,
shouldHideLayer,
timeValue,
]
);
};

Expand Down
4 changes: 3 additions & 1 deletion apps/src/features/map/map-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ export const {
} = mapSlice.actions;

/**
* Selector that returns the visibility of the "low skill" layer.
* Selector that returns the visibility of the low-skill vector mask overlay.
*
* @see {@link MapState.isLowSkillVisible}
*/
export const selectLowSkillVisibility =
() => (state: RootState) =>
Expand Down
4 changes: 4 additions & 0 deletions apps/src/lib/s2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ export function formatPeriodRange(periodRange: PeriodRange): [string, string] {
/**
* Create and return the GeoServer layer name for the Skill layer.
*
* "Skill" in meteorology refers to forecast accuracy relative to a baseline
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I wouldn't include this first sentence of the description. Feels like overexplaining the concept of skill, which is already documented for example in our Confluence.

Copy link
Copy Markdown
Contributor Author

@renoirb renoirb Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well. I was keeping bumping about what "skill" actually mean. That's why I wanted to clarify. The code has "forecast" and "climatology". With explanation about a "baseline" and that that base is... climatology. It makes sense.

* (e.g., climatology). The skill layer is a vector mask overlay showing
* diagonal lines over regions where forecast confidence is low.
*
* The name is `CDC:s2d-skill-<VAR>-<F>-<REF-PERIOD>`
*
* Where:
Expand Down
10 changes: 8 additions & 2 deletions apps/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,14 @@ export interface MapState {
*/
messageDisplayStates: {[key: string]: boolean};
/**
* True if the "low skill" layer is visible (i.e. true if "mask low skill"
* is checked)
* Whether low-skill areas are masked on the map.
*
* "Mask" is the verb — to visually cover low-confidence forecast areas
* with diagonal lines. When `true`, those areas are masked.
*
* Only meaningful for S2D variables when Forecast Display = "Forecast".
*
* Controlled by `MaskLowSkillField` in `components/fields/skill.tsx`.
*/
isLowSkillVisible: boolean;
/**
Expand Down