Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸš€ Quartz Solar v0.5.4 – Production #550

Merged
merged 25 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
333e524
add PoC UI component
braddf Nov 7, 2024
99b9900
WIP zones in map
braddf Nov 22, 2024
221d5d1
scale map legend for zones
braddf Nov 22, 2024
d443516
fix missing gsp Polygons in map data
braddf Nov 22, 2024
b9057c7
ng zone outlines for map
braddf Nov 22, 2024
87afcb4
enable selection of zones
braddf Nov 22, 2024
b609b05
add DNO aggregation to map
braddf Nov 25, 2024
7339a8f
update mapbox; add missed gsps 🏴󠁧󠁒󠁳󠁣󠁴󠁿
braddf Nov 25, 2024
e99ebda
allow click on DNO map aggregations
braddf Nov 26, 2024
44b5a58
fix type check on map source event
braddf Nov 26, 2024
300dac1
test aggregation on national charts
braddf Nov 26, 2024
361b701
fix zone y axis max + breakpoints
braddf Nov 28, 2024
3dae8bb
add national aggregation level
braddf Nov 29, 2024
3c60994
amend delta chart prop for gsp string
braddf Nov 29, 2024
9216f48
hide NG zones and show DNO map legend
braddf Dec 17, 2024
3a86ed9
remove console.logs
braddf Dec 17, 2024
7b53ad6
correct rounding on DNO map opacity
braddf Dec 17, 2024
b824ff5
update comments re N-hour chart aggregations
braddf Dec 18, 2024
2048d27
remove comments and unneeded code from PR
braddf Dec 18, 2024
48ce2d9
Merge pull request #548 from openclimatefix/feat/ng-zones
braddf Dec 18, 2024
d75cd7d
bump Quartz Solar version
braddf Dec 18, 2024
a987fe0
Merge pull request #549 from openclimatefix/development
braddf Dec 18, 2024
6731621
fix delta gsp selection bug
braddf Dec 19, 2024
42caeb0
explicit type on ngZones json const
braddf Dec 19, 2024
03b4345
Merge branch 'development' into staging
braddf Dec 19, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import DataLoadingChartStatus from "../DataLoadingChartStatus";

const GspDeltaColumn: FC<{
gspDeltas: Map<string, GspDeltaValue> | undefined;
setClickedGspId: Dispatch<SetStateAction<number | undefined>>;
setClickedGspId: Dispatch<SetStateAction<number | string | undefined>>;
negative?: boolean;
}> = ({ gspDeltas, setClickedGspId, negative = false }) => {
const [selectedBuckets] = useGlobalState("selectedBuckets");
Expand Down
33 changes: 25 additions & 8 deletions apps/nowcasting-app/components/charts/gsp-pv-remix-chart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,25 @@ import useGlobalState, { get30MinNow, getNext30MinSlot } from "../../helpers/glo
import Spinner from "../../icons/spinner";
import { ForecastValue } from "../../types";
import React, { FC } from "react";
import { NationalAggregation } from "../../map/types";
import { getTicks } from "../../helpers/chartUtils";

// We want to have the ymax of the graph to be related to the capacity of the GspPvRemixChart
// Static constant below of this function so we don't call dynamically unnecessarily.
// import { generateYMaxTickArray } from "../../helpers/chartUtils";
// console.log("Y_MAX_TICKS", generateYMaxTickArray());
//
// We want to have the yMax of the graph to be related to the capacity of the GspPvRemixChart.
// If we use the raw values, the graph looks funny, i.e y major ticks are 0 100 232
// So, we round these up to the following numbers
const yMax_levels = [
3, 9, 20, 28, 36, 45, 60, 80, 100, 120, 160, 200, 240, 300, 320, 360, 400, 450, 600
// So, we round these up to the following numbers, which hopefully split nicely into the y-axis.
// Uncomment the above function to get updated values should we need to change these
const Y_MAX_TICKS = [
1, 2, 3, 4, 5, 6, 9, 10, 12, 15, 18, 20, 25, 30, 40, 45, 50, 60, 75, 80, 90, 100, 150, 200, 250,
300, 350, 400, 450, 500, 600, 700, 800, 900, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000,
6000, 7000, 8000, 9000, 10000, 12000, 14000, 15000, 16000, 18000, 20000
];

const GspPvRemixChart: FC<{
gspId: number;
gspId: number | string;
selectedTime: string;
close: () => void;
setTimeOfInterest: (t: string) => void;
Expand All @@ -39,7 +48,8 @@ const GspPvRemixChart: FC<{
visibleLines,
deltaView = false
}) => {
const {
const [nationalAggregationLevel] = useGlobalState("nationalAggregationLevel");
let {
errors,
pvRealDataAfter,
pvRealDataIn,
Expand Down Expand Up @@ -110,14 +120,20 @@ const GspPvRemixChart: FC<{

// set ymax to the installed capacity of the graph
let yMax = gspInstalledCapacity || 100;
yMax = getRoundedTickBoundary(yMax, yMax_levels);
yMax = getRoundedTickBoundary(yMax, Y_MAX_TICKS);

let title = nationalAggregationLevel === NationalAggregation.GSP ? gspName || "" : String(gspId);

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

return (
<>
<div className="flex-initial">
<ForecastHeaderGSP
onClose={close}
title={gspName || ""}
title={title}
mwpercent={Math.round(pvPercentage)}
pvTimeOnly={convertISODateStringToLondonTime(latestPvActualDatetime)}
pvValue={Number(latestPvActualInMW)?.toFixed(1)}
Expand Down Expand Up @@ -156,6 +172,7 @@ const GspPvRemixChart: FC<{
visibleLines={visibleLines}
deltaView={deltaView}
deltaYMaxOverride={Math.ceil(Number(gspInstalledCapacity) / 200) * 100 || 500}
yTicks={getTicks(yMax, Y_MAX_TICKS)}
/>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,148 @@
import { API_PREFIX, getAllForecastUrl } from "../../../constant";
import { FcAllResData, ForecastData, GspEntities, PvRealData } from "../../types";
import dnoGspGroupings from "../../../data/dno_gsp_groupings.json";
import ngGspZoneGroupings from "../../../data/ng_gsp_zone_groupings.json";
import nationalGspZone from "../../../data/national_gsp_zone.json";
import useGlobalState from "../../helpers/globalState";
import { useLoadDataFromApi } from "../../hooks/useLoadDataFromApi";
import { NationalAggregation } from "../../map/types";
import { components } from "../../../types/quartz-api";

const useGetGspData = (gspId: number) => {
const aggregateTruthData = (
pvDataRaw: components["schemas"]["GSPYieldGroupByDatetime"][] | undefined,
gspIds: number[],
key: string
) => {
if (!pvDataRaw?.length) return [];
return pvDataRaw?.map((d) => {
return {
datetimeUtc: d.datetimeUtc,
[key]: Number(
gspIds.reduce((acc, gspId) => {
if (!d.generationKwByGspId?.[gspId]) return acc;
const value = Number(d.generationKwByGspId[gspId]);
if (isNaN(value)) return acc;
return acc + value;
}, 0)
)
};
}) as PvRealData;
};
const aggregateForecastData = (
pvDataRaw: components["schemas"]["OneDatetimeManyForecastValues"][] | undefined,
gspIds: number[]
) => {
if (!pvDataRaw?.length) return [];
return pvDataRaw?.map((d) => {
return {
targetTime: d.datetimeUtc,
expectedPowerGenerationMegawatts: Number(
gspIds.reduce((acc, gspId) => {
if (!d.forecastValues?.[gspId]) return acc;
const value = Number(d.forecastValues[gspId]);
if (isNaN(value)) return acc;
return acc + value;
}, 0)
)
};
}) as ForecastData;
};

const useGetGspData = (gspId: number | string) => {
const [show4hView] = useGlobalState("showNHourView");
const [nHourForecast] = useGlobalState("nHourForecast");
const [nationalAggregationLevel] = useGlobalState("nationalAggregationLevel");
let errors: Error[] = [];
let isZoneAggregation = [
NationalAggregation.DNO,
NationalAggregation.zone,
NationalAggregation.national
].includes(nationalAggregationLevel);

const { data: pvRealDataIn, error: pvRealInDat } = useLoadDataFromApi<PvRealData>(
`${API_PREFIX}/solar/GB/gsp/pvlive/${gspId}?regime=in-day`
let gspIds: number[] = typeof gspId === "number" ? [gspId] : [];
if (nationalAggregationLevel === NationalAggregation.DNO) {
// Get the GSP ids for the DNO
gspIds = dnoGspGroupings[gspId as keyof typeof dnoGspGroupings] || [];
}
if (nationalAggregationLevel === NationalAggregation.zone) {
gspIds = ngGspZoneGroupings[gspId as keyof typeof ngGspZoneGroupings] || [];
}
if (nationalAggregationLevel === NationalAggregation.national) {
gspIds = nationalGspZone[gspId as keyof typeof nationalGspZone] || [];
}

const { data: pvRealDataInRaw, error: pvRealInDayError } = useLoadDataFromApi<
components["schemas"]["GSPYieldGroupByDatetime"][]
>(
`${API_PREFIX}/solar/GB/gsp/pvlive/all?regime=in-day&gsp_ids=${encodeURIComponent(
gspIds.join(",")
)}&compact=true`
);
const pvRealDataIn = aggregateTruthData(pvRealDataInRaw, gspIds, "solarGenerationKw");

const { data: pvRealDataAfter, error: pvRealDayAfter } = useLoadDataFromApi<PvRealData>(
`${API_PREFIX}/solar/GB/gsp/pvlive/${gspId}?regime=day-after`
const { data: pvRealDataAfterRaw, error: pvRealDayAfterError } = useLoadDataFromApi<
components["schemas"]["GSPYieldGroupByDatetime"][]
>(
`${API_PREFIX}/solar/GB/gsp/pvlive/all?regime=day-after&gsp_ids=${encodeURIComponent(
gspIds.join(",")
)}&compact=true`
);
const pvRealDataAfter = aggregateTruthData(pvRealDataAfterRaw, gspIds, "solarGenerationKw");

//add new useSWR for gspChartData
const { data: gspForecastDataOneGSP, error: gspForecastDataOneGSPError } =
useLoadDataFromApi<ForecastData>(`${API_PREFIX}/solar/GB/gsp/${gspId}/forecast`, {
const { data: gspForecastDataOneGSPRaw, error: gspForecastDataOneGSPError } = useLoadDataFromApi<
components["schemas"]["OneDatetimeManyForecastValues"][]
>(
`${API_PREFIX}/solar/GB/gsp/forecast/all/?gsp_ids=${encodeURIComponent(
gspIds.join(",")
)}&compact=true&historic=true`,
{
dedupingInterval: 1000 * 30
});
}
);
const gspForecastDataOneGSP = aggregateForecastData(gspForecastDataOneGSPRaw, gspIds);

//add new useSWR for gspLocationInfo since this is not
const { data: gspLocationInfo, error: gspLocationError } = useLoadDataFromApi<GspEntities>(
`${API_PREFIX}/system/GB/gsp/?gsp_id=${gspId}`
const { data: gspLocationInfoRaw, error: gspLocationError } = useLoadDataFromApi<GspEntities>(
isZoneAggregation
? `${API_PREFIX}/system/GB/gsp/?zones=true` // TODO: API seems to struggle with UI flag if no other query params
: `${API_PREFIX}/system/GB/gsp/?gsp_id=${gspId}`
);
let gspLocationInfo = gspLocationInfoRaw?.filter((gsp) => gspIds.includes(gsp.gspId));
if (isZoneAggregation && gspLocationInfo) {
const zoneCapacity = gspLocationInfo.reduce((acc, gsp) => acc + gsp.installedCapacityMw, 0);
gspLocationInfo = [
{
gspId: gspId as number,
gspName: gspId as string,
regionName: gspId as string,
installedCapacityMw: zoneCapacity,
rmMode: true,
label: "Zone",
gspGroup: "Zone"
}
];
}

// TODO: nHour with aggregation when /forecast/all API endpoint has new forecast_horizon_minutes param
const nMinuteForecast = nHourForecast * 60;
const { data: gspNHourData, error: pvNHourError } = useLoadDataFromApi<ForecastData>(
show4hView
const { data: gspNHourDataRaw, error: pvNHourError } = useLoadDataFromApi<
components["schemas"]["ForecastValue"][]
>(
show4hView && !isZoneAggregation
? `${API_PREFIX}/solar/GB/gsp/${gspId}/forecast?forecast_horizon_minutes=${nMinuteForecast}&historic=true&only_forecast_values=true`
: null
);
let gspNHourData = gspNHourDataRaw || [];

return {
errors: [
pvRealInDat,
pvRealDayAfter,
pvRealInDayError,
pvRealDayAfterError,
gspForecastDataOneGSPError,
gspLocationError,
pvNHourError
].filter((e) => !!e),
gspNHourData: gspNHourData,
gspNHourData,
pvRealDataIn,
pvRealDataAfter,
gspForecastDataOneGSP,
Expand Down
1 change: 1 addition & 0 deletions apps/nowcasting-app/components/charts/pv-remix-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const PvRemixChart: FC<{
nationalNHourData,
allGspForecastData,
allGspRealData,
allGspSystemData,
gspDeltas
} = combinedData;
const {
Expand Down
5 changes: 4 additions & 1 deletion apps/nowcasting-app/components/charts/remix-line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type RemixLineProps = {
zoomEnabled?: boolean;
deltaView?: boolean;
deltaYMaxOverride?: number;
yTicks?: number[];
};
const CustomizedLabel: FC<any> = ({
value,
Expand Down Expand Up @@ -142,7 +143,8 @@ const RemixLine: React.FC<RemixLineProps> = ({
visibleLines,
zoomEnabled = true,
deltaView = false,
deltaYMaxOverride
deltaYMaxOverride,
yTicks
}) => {
// Set the y max. If national then set to 12000, for gsp plot use 'auto'
const preppedData = data.sort((a, b) => a.formattedDate.localeCompare(b.formattedDate));
Expand Down Expand Up @@ -412,6 +414,7 @@ const RemixLine: React.FC<RemixLineProps> = ({
yAxisId={"y-axis"}
tick={{ fill: "white", style: { fontSize: "12px" } }}
tickLine={false}
ticks={yTicks}
domain={
globalIsZoomed && view !== VIEWS.SOLAR_SITES
? [0, Number(zoomYMax * 1.1)]
Expand Down
89 changes: 89 additions & 0 deletions apps/nowcasting-app/components/helpers/chartUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,92 @@ export const getZoomYMax = (filteredPreppedData: ChartData[]) => {
.sort((a, b) => Number(b) - Number(a))[0];
}
};

// Function not "in use" but useful for regenerating yMax levels as a constant array for the chart
export const generateYMaxTickArray = () => {
// Generate yMax levels
// Small values
let yMax_levels = Array.from({ length: 4 }, (_, i) => i + 1);
// Multiples of 3
yMax_levels = [...yMax_levels, ...Array.from({ length: 6 }, (_, i) => (i + 1) * 3)];
// Multiples of 5
yMax_levels = [...yMax_levels, ...Array.from({ length: 6 }, (_, i) => (i + 1) * 5)];
// Multiples of 10
yMax_levels = [...yMax_levels, ...Array.from({ length: 5 }, (_, i) => (i + 1) * 10)];
// Multiples of 15
yMax_levels = [...yMax_levels, ...Array.from({ length: 6 }, (_, i) => (i + 1) * 15)];
// Multiples of 20
yMax_levels = [...yMax_levels, ...Array.from({ length: 5 }, (_, i) => (i + 1) * 20)];
// Multiples of 25
yMax_levels = [...yMax_levels, ...Array.from({ length: 3 }, (_, i) => (i + 1) * 25)];
// Multiples of 50
yMax_levels = [...yMax_levels, ...Array.from({ length: 10 }, (_, i) => (i + 1) * 50)];
// Multiples of 100
yMax_levels = [...yMax_levels, ...Array.from({ length: 10 }, (_, i) => (i + 1) * 100)];
// Multiples of 500
yMax_levels = [...yMax_levels, ...Array.from({ length: 10 }, (_, i) => (i + 1) * 500)];
// Multiples of 1000
yMax_levels = [...yMax_levels, ...Array.from({ length: 10 }, (_, i) => (i + 1) * 1000)];
// Remove duplicates
yMax_levels = [...new Set(yMax_levels)];
// Sort
yMax_levels.sort((a, b) => a - b);
return yMax_levels;
};

export const getTicks = (yMax: number, yMax_levels: number[]) => {
const ticks: number[] = [];
const third = yMax / 3;
const quarter = yMax / 4;
const fifth = yMax / 5;
const seventh = yMax / 7;
const testTicksToAdd = (fractionN: number) => {
let canSplit = true;
let tempTicks = [];
for (let i = fractionN; i <= yMax; i += fractionN) {
if (isRoundNumber(i) || i === yMax) {
tempTicks.push(i);
} else {
canSplit = false;
break;
}
}
if (canSplit) {
ticks.push(...tempTicks);
}
};
const isRoundNumber = (n: number) => {
if (n > 2000) {
return n % 500 === 0;
}
if (n > 1000) {
return n % 250 === 0 || n % 100 === 0;
}
if (n > 200) {
return n % 50 === 0;
}
if (n > 20) {
return n % 5 === 0;
}
if (n > 3) {
return n % 1 === 0;
}
return n % 0.5 === 0;
};
if (isRoundNumber(third)) {
testTicksToAdd(third);
}
if (ticks.length === 0 && isRoundNumber(quarter)) {
testTicksToAdd(quarter);
}
if (ticks.length === 0 && isRoundNumber(fifth)) {
testTicksToAdd(fifth);
}
if (ticks.length === 0 && isRoundNumber(seventh)) {
testTicksToAdd(seventh);
}
if (ticks.length === 0) {
testTicksToAdd(yMax > 500 ? 100 : 50);
}
return ticks;
};
Loading
Loading