Skip to content

Commit 3476e9a

Browse files
authored
Merge pull request #627 from openclimatefix/staging
πŸš€ Quartz Solar v0.6.0 –> Production
2 parents c0ffe02 + 58db415 commit 3476e9a

File tree

20 files changed

+405
-133
lines changed

20 files changed

+405
-133
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
type TipPosition = "left" | "right" | "middle" | "top";
2+
3+
type LegendTooltipProps = {
4+
children: React.ReactNode;
5+
tip: string | React.ReactNode;
6+
position?: TipPosition;
7+
className?: string;
8+
};
9+
10+
const getPositionClass = (position: TipPosition) => {
11+
if (position === "left") return "left-0";
12+
if (position === "right") return "-right-3";
13+
if (position === "middle") return "bottom-1 left-1/2 transform -translate-x-1/2 translate-y-full";
14+
if (position === "top")
15+
return "-top-[calc(100%+0.75rem)] left-1/2 transform -translate-x-1/2 -translate-y-full";
16+
17+
return "top-0 left-0"; // Default case for top-left
18+
};
19+
20+
const LegendTooltip: React.FC<LegendTooltipProps> = ({
21+
children,
22+
tip,
23+
position = "left",
24+
className
25+
}) => {
26+
return (
27+
<div className={`relative z-[100] overflow-visible cursor-default group ${className || ""}`}>
28+
{children}
29+
{tip && (
30+
<div
31+
className={`absolute flex ${getPositionClass(
32+
position
33+
)} hidden w-64 mt-6 group-hover:flex flex-wrap`}
34+
>
35+
<span
36+
className={`flex top-0 text-center mb-0 mt-2 text-xs px-3 py-1 leading-snug bg-mapbox-black bg-opacity-95 rounded-lg text-white drop-shadow-lg`}
37+
>
38+
{tip}
39+
</span>
40+
</div>
41+
)}
42+
</div>
43+
);
44+
};
45+
46+
export default LegendTooltip;

β€Žapps/nowcasting-app/components/charts/ChartLegend.tsxβ€Ž

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,51 @@ import { FC, useEffect } from "react";
55
import useGlobalState from "../helpers/globalState";
66
import LegendItem from "./LegendItem";
77
import { N_HOUR_FORECAST_OPTIONS } from "../../constant";
8+
import LegendTooltip from "../LegendTooltop";
9+
import { NationalAggregation } from "../map/types";
810

911
type ChartLegendProps = {
1012
className?: string;
1113
};
1214
export const ChartLegend: FC<ChartLegendProps> = ({ className }) => {
1315
const [showNHourView] = useGlobalState("showNHourView");
1416
const [nHourForecast, setNHourForecast] = useGlobalState("nHourForecast");
17+
const [selectedMapRegionIds] = useGlobalState("selectedMapRegionIds");
18+
const [visibleLines] = useGlobalState("visibleLines");
19+
const [nationalAggregationLevel] = useGlobalState("nationalAggregationLevel");
1520

16-
const legendItemContainerClasses = `flex flex-initial ${
21+
const legendItemContainerClasses = `flex flex-initial overflow-y-visible ${
1722
showNHourView
1823
? "flex-col @sm:gap-1 @6xl:gap-6 @6xl:flex-row"
1924
: "flex-col @md:gap-1 @3xl:gap-12 @3xl:flex-row"
2025
}${className ? ` ${className}` : ""}`;
26+
27+
let nHrTipText;
28+
if (showNHourView && visibleLines.includes("N_HOUR_FORECAST")) {
29+
if (selectedMapRegionIds && selectedMapRegionIds.length > 1) {
30+
nHrTipText =
31+
"As you have multiple regions selected, N-hour view is (currently) unavailable with multi-select. " +
32+
"\nSelect a single region to see the \nN-hour forecast.";
33+
}
34+
if (
35+
nationalAggregationLevel === NationalAggregation.DNO &&
36+
selectedMapRegionIds &&
37+
selectedMapRegionIds.length > 0
38+
) {
39+
nHrTipText =
40+
"N-hour view is not (currently) available for DNO-level data. " +
41+
"\nSelect GSP-level aggregation to see the N-hour forecast.";
42+
}
43+
}
44+
2145
return (
2246
<div className="@container flex flex-initial">
2347
<div className="flex flex-1 flex-col justify-between align-items:baseline px-4 text-xs tracking-wider text-ocf-gray-300 py-3 gap-3 bg-mapbox-black-500 overflow-y-visible @sm:flex-row @xl:gap-6">
2448
<div
2549
className={`flex flex-initial pr-2 justify-between flex-col overflow-x-auto ${
2650
showNHourView ? "@sm:gap-1" : ""
2751
} @md:pr-0 @md:flex-col @md:gap-1 @lg:flex-row @lg:gap-8`}
52+
style={{ overflow: "visible" }}
2853
>
2954
<div className={legendItemContainerClasses}>
3055
<LegendItem
@@ -52,12 +77,18 @@ export const ChartLegend: FC<ChartLegendProps> = ({ className }) => {
5277
{/* dataKey={`PAST_FORECAST`}*/}
5378
{/*/>*/}
5479
{showNHourView && (
55-
<LegendItem
56-
iconClasses={"text-ocf-orange"}
57-
dashStyle={"both"}
58-
label={`OCF ${nHourForecast}hr Forecast`}
59-
dataKey={`N_HOUR_FORECAST`}
60-
/>
80+
<LegendTooltip
81+
tip={nHrTipText}
82+
position={"top"}
83+
className="relative w-full whitespace-pre-wrap"
84+
>
85+
<LegendItem
86+
iconClasses={"text-ocf-orange"}
87+
dashStyle={"both"}
88+
label={`OCF ${nHourForecast}hr Forecast`}
89+
dataKey={`N_HOUR_FORECAST`}
90+
/>
91+
</LegendTooltip>
6192
)}
6293
</div>
6394
</div>

β€Žapps/nowcasting-app/components/charts/delta-view/delta-view-chart.tsxβ€Ž

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,10 @@ import { getTicks } from "../../helpers/chartUtils";
2525

2626
const GspDeltaColumn: FC<{
2727
gspDeltas: Map<string, GspDeltaValue> | undefined;
28-
setClickedGspId: Dispatch<SetStateAction<number | string | undefined>>;
2928
negative?: boolean;
30-
}> = ({ gspDeltas, setClickedGspId, negative = false }) => {
29+
}> = ({ gspDeltas, negative = false }) => {
3130
const [selectedBuckets] = useGlobalState("selectedBuckets");
32-
const [clickedGspId] = useGlobalState("clickedGspId");
31+
const [selectedMapRegionIds, setSelectedMapRegionIds] = useGlobalState("selectedMapRegionIds");
3332
const deltaArray = useMemo(() => Array.from(gspDeltas?.values() || []), [gspDeltas]);
3433
if (!gspDeltas?.size) return null;
3534

@@ -108,7 +107,9 @@ const GspDeltaColumn: FC<{
108107
hasRows = true;
109108
}
110109

111-
const isSelectedGsp = Number(clickedGspId) === Number(gspDelta.gspId);
110+
const isSelectedGsp =
111+
gspDelta.gspId &&
112+
selectedMapRegionIds?.map((gspId) => Number(gspId)).includes(Number(gspDelta.gspId));
112113

113114
// this is normalized putting the delta value over the installed capacity of a gsp
114115
const deltaNormalizedPercentage = Math.abs(
@@ -136,7 +137,7 @@ const GspDeltaColumn: FC<{
136137
} box-content cursor-pointer relative flex w-full transition duration-200 ease-out
137138
hover:bg-ocf-gray-900 hover:ease-in`}
138139
key={`gspCol${gspDelta.gspId}`}
139-
onClick={() => setClickedGspId(gspDelta.gspId)}
140+
onClick={() => setSelectedMapRegionIds([String(gspDelta.gspId)])}
140141
>
141142
<div
142143
className={`items-start xl:items-center text-xs grid grid-cols-12 flex-1 py-1.5 justify-between px-2
@@ -254,21 +255,15 @@ type DeltaChartProps = {
254255
combinedErrors: CombinedErrors;
255256
};
256257
const DeltaChart: FC<DeltaChartProps> = ({ className, combinedData, combinedErrors }) => {
257-
// const [view] = useGlobalState("view");
258-
const [show4hView] = useGlobalState("showNHourView");
259-
const [clickedGspId, setClickedGspId] = useGlobalState("clickedGspId");
258+
const [selectedMapRegionIds, setSelectedMapRegionIds] = useGlobalState("selectedMapRegionIds");
260259
const [visibleLines] = useGlobalState("visibleLines");
261-
const [globalZoomArea] = useGlobalState("globalZoomArea");
262260
const [selectedBuckets] = useGlobalState("selectedBuckets");
263261
const [selectedISOTime, setSelectedISOTime] = useGlobalState("selectedISOTime");
264262
const [timeNow] = useGlobalState("timeNow");
265-
const [forecastCreationTime] = useGlobalState("forecastCreationTime");
266263
const [loadingState] = useGlobalState("loadingState");
267264
const { stopTime, resetTime } = useStopAndResetTime();
268265
const selectedTime = formatISODateString(selectedISOTime || new Date().toISOString());
269266
const selectedTimeHalfHourSlot = get30MinSlot(new Date(convertToLocaleDateString(selectedTime)));
270-
// const halfHourAgoDate = new Date(timeNow).setMinutes(new Date(timeNow).getMinutes() - 30);
271-
// const halfHourAgo = `${formatISODateString(new Date(halfHourAgoDate).toISOString())}:00Z`;
272267
const hasGspPvInitialForSelectedTime = combinedData.pvRealDayInData?.find(
273268
(d) =>
274269
d.datetimeUtc.slice(0, 16) ===
@@ -343,6 +338,11 @@ const DeltaChart: FC<DeltaChartProps> = ({ className, combinedData, combinedErro
343338
setSelectedISOTime(time + ":00.000Z");
344339
};
345340

341+
let selectedRegions: string[] = [];
342+
if (selectedMapRegionIds && selectedMapRegionIds.length > 0) {
343+
selectedRegions = selectedMapRegionIds.map((id) => String(id));
344+
}
345+
346346
return (
347347
<>
348348
<div className={`flex flex-col flex-1 ${className || ""}`}>
@@ -367,15 +367,15 @@ const DeltaChart: FC<DeltaChartProps> = ({ className, combinedData, combinedErro
367367
/>
368368
</div>
369369
</div>
370-
{clickedGspId && (
370+
{selectedMapRegionIds?.length && (
371371
<div className="flex-1 flex flex-col relative dash:h-auto">
372372
<GspPvRemixChart
373373
close={() => {
374-
setClickedGspId(undefined);
374+
setSelectedMapRegionIds([]);
375375
}}
376376
setTimeOfInterest={setSelectedTime}
377377
selectedTime={selectedTime}
378-
gspId={clickedGspId}
378+
selectedRegions={selectedRegions}
379379
timeNow={formatISODateString(timeNow)}
380380
resetTime={resetTime}
381381
visibleLines={visibleLines}
@@ -386,7 +386,7 @@ const DeltaChart: FC<DeltaChartProps> = ({ className, combinedData, combinedErro
386386
<div
387387
className={`flex flex-col flex-grow-0 flex-shrink${
388388
hasGspPvInitialForSelectedTime ? " overflow-y-scroll" : ""
389-
} ${clickedGspId ? "h-[30%]" : "h-[40%]"}`}
389+
} ${selectedMapRegionIds?.length ? "h-[30%]" : "h-[40%]"}`}
390390
>
391391
<DeltaBuckets bucketSelection={selectedBuckets} gspDeltas={gspDeltas} />
392392
{!hasGspPvInitialForSelectedTime && (
@@ -396,8 +396,8 @@ const DeltaChart: FC<DeltaChartProps> = ({ className, combinedData, combinedErro
396396
)}
397397
{hasGspPvInitialForSelectedTime && gspDeltas && (
398398
<div className="flex pt-2 mx-3 max-h-96">
399-
<GspDeltaColumn gspDeltas={gspDeltas} negative setClickedGspId={setClickedGspId} />
400-
<GspDeltaColumn gspDeltas={gspDeltas} setClickedGspId={setClickedGspId} />
399+
<GspDeltaColumn gspDeltas={gspDeltas} negative />
400+
<GspDeltaColumn gspDeltas={gspDeltas} />
401401
</div>
402402
)}
403403
</div>

β€Žapps/nowcasting-app/components/charts/gsp-pv-remix-chart/forecast-header-gsp.tsxβ€Ž

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CloseButtonIcon, DownArrow, UpArrow } from "../../icons/icons";
22
import { ForecastHeadlineFigure } from "../forecast-header/ui";
33
import { DeltaHeaderBlock } from "../delta-view/delta-header-block";
44
import React, { FC } from "react";
5+
import ForecastLabel from "../../national_forecast_labels";
56

67
type ForecastHeaderGSPProps = {
78
title: string;
@@ -15,6 +16,7 @@ type ForecastHeaderGSPProps = {
1516
forecastNextTimeOnly?: string;
1617
forecastNextPV?: string;
1718
children?: React.ReactNode;
19+
titleTooltipText?: string[];
1820
};
1921

2022
const ForecastHeaderGSP: FC<ForecastHeaderGSPProps> = ({
@@ -26,13 +28,29 @@ const ForecastHeaderGSP: FC<ForecastHeaderGSPProps> = ({
2628
pvValue,
2729
forecastNextPV,
2830
forecastNextTimeOnly,
29-
onClose
31+
onClose,
32+
titleTooltipText = []
3033
}) => {
3134
const height = title.length < 12 ? "dash:h-[4.25rem]" : "dash:h-[5.5rem]";
35+
const titleTooltipContent = (
36+
<ul className="text-left">
37+
{titleTooltipText.map((gspName) => (
38+
<li key={gspName} className="text-ocf-gray-300 text-xs font-normal">
39+
{gspName}
40+
</li>
41+
))}
42+
</ul>
43+
);
3244
return (
3345
<div className={`flex content-between bg-ocf-gray-800 h-12 mb-4 ${height}`}>
3446
<div className="dash:xl:text-2xl dash:2xl:text-3xl dash:3xl:text-4xl text-white lg:text-xl md:text-lg text-lg font-black m-auto ml-5 flex justify-evenly">
35-
{title}
47+
{titleTooltipText.length ? (
48+
<ForecastLabel className="" position={"left"} tip={titleTooltipContent}>
49+
{title}
50+
</ForecastLabel>
51+
) : (
52+
title
53+
)}
3654
</div>
3755
<div className="flex justify-between items-center flex-2 my-2 dash:3xl:my-3 px-2 2xl:px-4 3xl:px-6">
3856
{forecastPV && (

β€Žapps/nowcasting-app/components/charts/gsp-pv-remix-chart/index.tsxβ€Ž

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { getTicks } from "../../helpers/chartUtils";
1717
import { Y_MAX_TICKS } from "../../../constant";
1818

1919
const GspPvRemixChart: FC<{
20-
gspId: number | string;
20+
selectedRegions: string[];
2121
selectedTime: string;
2222
close: () => void;
2323
setTimeOfInterest: (t: string) => void;
@@ -26,7 +26,7 @@ const GspPvRemixChart: FC<{
2626
visibleLines: string[];
2727
deltaView?: boolean;
2828
}> = ({
29-
gspId,
29+
selectedRegions,
3030
selectedTime,
3131
close,
3232
setTimeOfInterest,
@@ -44,9 +44,10 @@ const GspPvRemixChart: FC<{
4444
gspLocationInfo,
4545
gspForecastDataOneGSP,
4646
gspNHourData
47-
} = useGetGspData(gspId);
47+
} = useGetGspData(selectedRegions);
4848
// const gspData = fcAll?.forecasts.find((fc) => fc.location.gspId === gspId);
49-
const gspInstalledCapacity = gspLocationInfo?.[0]?.installedCapacityMw;
49+
const gspInstalledCapacity =
50+
gspLocationInfo?.reduce((acc, gsp) => acc + gsp.installedCapacityMw, 0) || 0;
5051
const gspName = gspLocationInfo?.[0]?.regionName;
5152
const chartData = useFormatChartData({
5253
forecastData: gspForecastDataOneGSP,
@@ -118,12 +119,27 @@ const GspPvRemixChart: FC<{
118119
let yMax = gspInstalledCapacity || 100;
119120
yMax = getRoundedTickBoundary(yMax, Y_MAX_TICKS);
120121

121-
let title = nationalAggregationLevel === NationalAggregation.GSP ? gspName || "" : String(gspId);
122+
let title =
123+
nationalAggregationLevel === NationalAggregation.GSP
124+
? gspName || ""
125+
: String(selectedRegions[0]);
126+
let selectedGSPNames =
127+
selectedRegions.length > 1 ? gspLocationInfo?.map((gsp) => gsp.regionName) || [] : [];
128+
129+
if (selectedRegions.length > 1) {
130+
title = `${selectedRegions.length} ${String(nationalAggregationLevel)}s selected`;
131+
}
122132

123133
if (nationalAggregationLevel === NationalAggregation.national) {
124134
title = "National GSP Sum";
125135
}
126136

137+
// If multiple GSPs are selected, hide the N-hour data, if any
138+
let filteredLines = visibleLines;
139+
if (selectedRegions.length > 1) {
140+
filteredLines = visibleLines.filter((line) => !line.includes("N_HOUR_FORECAST"));
141+
}
142+
127143
return (
128144
<>
129145
<div className="flex-initial">
@@ -140,6 +156,7 @@ const GspPvRemixChart: FC<{
140156
forecastNextPV={followingPvForecastInMW?.toFixed(1)}
141157
deltaValue={deltaValue.toString()}
142158
deltaView={deltaView}
159+
titleTooltipText={selectedGSPNames}
143160
>
144161
<span className="font-semibold dash:3xl:text-5xl dash:xl:text-4xl xl:text-3xl lg:text-2xl md:text-xl text-lg leading-none text-ocf-yellow-500">
145162
{Math.round(forecastAtSelectedTime.expectedPowerGenerationMegawatts || 0)}
@@ -165,7 +182,7 @@ const GspPvRemixChart: FC<{
165182
yMax={yMax!}
166183
timeNow={timeNow}
167184
resetTime={resetTime}
168-
visibleLines={visibleLines}
185+
visibleLines={filteredLines}
169186
deltaView={deltaView}
170187
deltaYMaxOverride={Math.ceil(Number(gspInstalledCapacity) / 200) * 100 || 500}
171188
yTicks={getTicks(yMax, Y_MAX_TICKS)}

0 commit comments

Comments
Β (0)