diff --git a/apps/src/components/fields/skill.tsx b/apps/src/components/fields/skill.tsx
index 99eee296b..5d423a198 100644
--- a/apps/src/components/fields/skill.tsx
+++ b/apps/src/components/fields/skill.tsx
@@ -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}));
};
@@ -29,6 +40,7 @@ export const MaskLowSkillField = (
const fieldProps = {
checked,
onCheckedChange,
+ disabled: !isForecast,
...propsRest,
};
@@ -36,12 +48,12 @@ export const MaskLowSkillField = (
diff --git a/apps/src/components/map-layers/low-skill-layer.tsx b/apps/src/components/map-layers/low-skill-layer.tsx
index 5fe60ec87..37a0798ca 100644
--- a/apps/src/components/map-layers/low-skill-layer.tsx
+++ b/apps/src/components/map-layers/low-skill-layer.tsx
@@ -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 {
@@ -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);
@@ -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
@@ -49,7 +57,7 @@ const LowSkillLayer = ({
// attributes change.
return useMemo(
() => {
- if (isLowSkillMasked || !layerName || !timeValue) {
+ if (shouldHideLayer || !hasLayerData) {
return null;
}
@@ -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,
+ ]
);
};
diff --git a/apps/src/features/map/map-slice.ts b/apps/src/features/map/map-slice.ts
index 81ee9c5ad..ecdd63368 100644
--- a/apps/src/features/map/map-slice.ts
+++ b/apps/src/features/map/map-slice.ts
@@ -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) =>
diff --git a/apps/src/lib/s2d.ts b/apps/src/lib/s2d.ts
index ed9fc1be0..9c7ecbd20 100644
--- a/apps/src/lib/s2d.ts
+++ b/apps/src/lib/s2d.ts
@@ -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
+ * (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---`
*
* Where:
diff --git a/apps/src/types/types.ts b/apps/src/types/types.ts
index ea3447eb1..9f69dc390 100644
--- a/apps/src/types/types.ts
+++ b/apps/src/types/types.ts
@@ -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;
/**