diff --git a/apps/nowcasting-app/components/charts/delta-view/delta-view-chart.tsx b/apps/nowcasting-app/components/charts/delta-view/delta-view-chart.tsx index 7cd1e25c..6cdb0189 100644 --- a/apps/nowcasting-app/components/charts/delta-view/delta-view-chart.tsx +++ b/apps/nowcasting-app/components/charts/delta-view/delta-view-chart.tsx @@ -273,6 +273,7 @@ const DeltaChart: FC = ({ className, combinedData, combinedErro const [show4hView] = useGlobalState("show4hView"); const [clickedGspId, setClickedGspId] = useGlobalState("clickedGspId"); const [visibleLines] = useGlobalState("visibleLines"); + const [globalZoomArea] = useGlobalState("globalZoomArea"); const [selectedBuckets] = useGlobalState("selectedBuckets"); const [selectedISOTime, setSelectedISOTime] = useGlobalState("selectedISOTime"); const [timeNow] = useGlobalState("timeNow"); diff --git a/apps/nowcasting-app/components/charts/gsp-pv-remix-chart/index.tsx b/apps/nowcasting-app/components/charts/gsp-pv-remix-chart/index.tsx index 77309c73..47f57449 100644 --- a/apps/nowcasting-app/components/charts/gsp-pv-remix-chart/index.tsx +++ b/apps/nowcasting-app/components/charts/gsp-pv-remix-chart/index.tsx @@ -63,7 +63,6 @@ const GspPvRemixChart: FC<{ console.log(errors); return
failed to load
; } - const now30min = formatISODateString(get30MinNow()); const dataMissing = !gspForecastDataOneGSP || !pvRealDataIn || !pvRealDataAfter; const forecastAtSelectedTime: NonNullable[number] = diff --git a/apps/nowcasting-app/components/charts/pv-remix-chart.tsx b/apps/nowcasting-app/components/charts/pv-remix-chart.tsx index ba24e1e2..1bde1f43 100644 --- a/apps/nowcasting-app/components/charts/pv-remix-chart.tsx +++ b/apps/nowcasting-app/components/charts/pv-remix-chart.tsx @@ -27,6 +27,7 @@ const PvRemixChart: FC<{ const { stopTime, resetTime } = useStopAndResetTime(); const selectedTime = formatISODateString(selectedISOTime || new Date().toISOString()); const [loadingState] = useGlobalState("loadingState"); + const [globalZoomArea] = useGlobalState("globalZoomArea"); const { nationalForecastData, diff --git a/apps/nowcasting-app/components/charts/remix-line.tsx b/apps/nowcasting-app/components/charts/remix-line.tsx index 05477019..c8fa760a 100644 --- a/apps/nowcasting-app/components/charts/remix-line.tsx +++ b/apps/nowcasting-app/components/charts/remix-line.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, useEffect, useState } from "react"; import { Area, Bar, @@ -7,6 +7,7 @@ import { Line, Rectangle, ReferenceLine, + ReferenceArea, ResponsiveContainer, Tooltip, XAxis, @@ -26,6 +27,10 @@ import { import { theme } from "../../tailwind.config"; import useGlobalState, { getNext30MinSlot } from "../helpers/globalState"; import { DELTA_BUCKET, VIEWS } from "../../constant"; +import get from "@auth0/nextjs-auth0/dist/auth0-session/client"; +import SVGComponent, { CloseButtonIcon } from "../icons/icons"; +import { getZoomYMax } from "../helpers/chartUtils"; +import { ZoomOutIcon } from "@heroicons/react/solid"; const yellow = theme.extend.colors["ocf-yellow"].DEFAULT; const orange = theme.extend.colors["ocf-orange"].DEFAULT; @@ -79,6 +84,7 @@ type RemixLineProps = { timeNow: string; resetTime?: () => void; visibleLines: string[]; + zoomEnabled?: boolean; deltaView?: boolean; deltaYMaxOverride?: number; }; @@ -136,6 +142,7 @@ const RemixLine: React.FC = ({ timeNow, resetTime, visibleLines, + zoomEnabled = true, deltaView = false, deltaYMaxOverride }) => { @@ -147,6 +154,13 @@ const RemixLine: React.FC = ({ const currentTime = getNext30MinSlot(new Date()).toISOString().slice(0, 16); const localeTimeOfInterest = convertToLocaleDateString(timeOfInterest + "Z").slice(0, 16); const fourHoursFromNow = new Date(currentTime); + const defaultZoom = { x1: "", x2: "" }; + const [filteredPreppedData, setFilteredPreppedData] = useState(preppedData); + const [globalZoomArea, setGlobalZoomArea] = useGlobalState("globalZoomArea"); + const [globalIsZooming, setGlobalIsZooming] = useGlobalState("globalChartIsZooming"); + const [globalIsZoomed, setGlobalIsZoomed] = useGlobalState("globalChartIsZoomed"); + const [temporaryZoomArea, setTemporaryZoomArea] = useState(defaultZoom); + fourHoursFromNow.setHours(fourHoursFromNow.getHours() + 4); function prettyPrintYNumberWithCommas( @@ -182,10 +196,12 @@ const RemixLine: React.FC = ({ .map((d) => d.DELTA) .filter((n) => typeof n === "number") .sort((a, b) => Number(a) - Number(b))[0]; + // Take the max absolute value of the delta min and max as the y-axis max const deltaYMax = deltaYMaxOverride || getRoundedTickBoundary(Math.max(Number(deltaMax), 0 - Number(deltaMin)) || 0, deltaMaxTicks); + const roundTickMax = deltaYMax % 1000 === 0; const isGSP = !!deltaYMaxOverride && deltaYMaxOverride < 1000; const now = new Date(); @@ -198,13 +214,55 @@ const RemixLine: React.FC = ({ return new Date(now).setHours(o, 0, 0, 0); }); + //get Y axis boundary + + const yMaxZoom_Levels = [ + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 1000, 2000, 3000, 4000, 5000, 6000, + 7000, 8000, 9000, 10000, 11000, 12000 + ]; + + let zoomYMax = getZoomYMax(filteredPreppedData); + zoomYMax = getRoundedTickBoundary(zoomYMax || 0, yMaxZoom_Levels); + + //reset zoom state + function handleZoomOut() { + setGlobalIsZoomed(false); + setFilteredPreppedData(preppedData); + } + + useEffect(() => { + if (!zoomEnabled) return; + + if (!globalIsZooming) { + const { x1, x2 } = globalZoomArea; + + const dataInAreaRange = preppedData.filter( + (d) => d?.formattedDate >= x1 && d?.formattedDate <= x2 + ); + setFilteredPreppedData(dataInAreaRange); + } + }, [globalZoomArea, globalIsZooming, preppedData, zoomEnabled]); + return (
+ {zoomEnabled && globalIsZoomed && ( +
+ +
+ )} = ({ : setTimeOfInterest(e.activeLabel); } }} + onMouseDown={(e?: { activeLabel?: string }) => { + if (!zoomEnabled) return; + setTemporaryZoomArea(globalZoomArea); + setGlobalIsZooming(true); + let xValue = e?.activeLabel; + if (xValue) { + setGlobalZoomArea({ x1: xValue, x2: xValue }); + } + }} + onMouseMove={(e?: { activeLabel?: string }) => { + if (!zoomEnabled) return; + + if (globalIsZooming) { + let xValue = e?.activeLabel; + setGlobalZoomArea((zoom) => ({ ...zoom, x2: xValue || "" })); + } + }} + onMouseUp={(e?: { activeLabel?: string }) => { + if (!zoomEnabled) return; + + if (globalIsZooming) { + if (globalZoomArea.x1 == globalZoomArea.x2 && e?.activeLabel && setTimeOfInterest) { + setGlobalZoomArea(temporaryZoomArea); + setTimeOfInterest(e?.activeLabel); + } else { + let { x1 } = globalZoomArea; + let x2 = e?.activeLabel || ""; + if (x1 > x2) { + [x1, x2] = [x2, x1]; + } + setGlobalZoomArea({ x1, x2 }); + setGlobalIsZoomed(true); + } + setGlobalIsZooming(false); + } + }} > = ({ interval={view === VIEWS.SOLAR_SITES ? undefined : 11} /> = ({ yAxisId={"y-axis"} tick={{ fill: "white", style: { fontSize: "12px" } }} tickLine={false} - domain={[0, yMax]} + domain={ + globalIsZoomed && view !== VIEWS.SOLAR_SITES ? [0, Number(zoomYMax * 1.1)] : [0, yMax] + } label={{ value: view === VIEWS.SOLAR_SITES ? "Generation (KW)" : "Generation (MW)", angle: 270, @@ -289,6 +386,7 @@ const RemixLine: React.FC = ({ dy: 0 }} /> + {deltaView && ( <> = ({ /> } /> + = ({ > } /> + {deltaView && ( = ({ strokeWidth={largeScreenMode ? 4 : 2} hide={!visibleLines.includes("FORECAST")} /> - + {zoomEnabled && globalIsZooming && ( + + )} { const data = payload && payload[0]?.payload; diff --git a/apps/nowcasting-app/components/charts/solar-site-view/solar-site-chart.tsx b/apps/nowcasting-app/components/charts/solar-site-view/solar-site-chart.tsx index df14b15c..1bd715c7 100644 --- a/apps/nowcasting-app/components/charts/solar-site-view/solar-site-chart.tsx +++ b/apps/nowcasting-app/components/charts/solar-site-view/solar-site-chart.tsx @@ -255,6 +255,7 @@ const SolarSiteChart: FC<{ timeOfInterest={selectedTime} setTimeOfInterest={setSelectedTime} data={chartData} + zoomEnabled={false} yMax={yMax} visibleLines={visibleLines} /> diff --git a/apps/nowcasting-app/components/helpers/chartUtils.ts b/apps/nowcasting-app/components/helpers/chartUtils.ts new file mode 100644 index 00000000..7d15644c --- /dev/null +++ b/apps/nowcasting-app/components/helpers/chartUtils.ts @@ -0,0 +1,24 @@ +// tools for the chart +import { ChartData } from "../charts/remix-line"; + +// get the zoomYMax for either sites or national view +export const getZoomYMax = (filteredPreppedData: ChartData[]) => { + // if no probabilistic max value, get the max between the generation and the forecast as the zoomYMax + if (!filteredPreppedData.some((d) => d.PROBABILISTIC_UPPER_BOUND)) { + let genMax = + filteredPreppedData + .map((d) => d.GENERATION_UPDATED || d.GENERATION) + .filter((n) => typeof n === "number") + .sort((a, b) => Number(b) - Number(a))[0] || 0; + let forecastMax = + filteredPreppedData + .map((d) => d.PAST_FORECAST || d.FORECAST) + .sort((a, b) => Number(b) - Number(a))[0] || 0; + return Math.max(genMax, forecastMax); + } else { + return filteredPreppedData + .map((d) => d.PROBABILISTIC_UPPER_BOUND || d.GENERATION) + .filter((n) => typeof n === "number") + .sort((a, b) => Number(b) - Number(a))[0]; + } +}; diff --git a/apps/nowcasting-app/components/helpers/globalState.tsx b/apps/nowcasting-app/components/helpers/globalState.tsx index b22f1ecc..3dfbb686 100644 --- a/apps/nowcasting-app/components/helpers/globalState.tsx +++ b/apps/nowcasting-app/components/helpers/globalState.tsx @@ -8,6 +8,7 @@ import { CookieStorageKeys } from "./cookieStorage"; import { NationalEndpointStates, LoadingState, SitesEndpointStates } from "../types"; +import { ChartData } from "../charts/remix-line"; export function get30MinNow(offsetMinutes = 0) { // this is a function to get the date of now, but rounded up to the closest 30 minutes @@ -55,6 +56,9 @@ export type GlobalStateType = { dashboardMode: boolean; sortBy: SORT_BY; autoZoom: boolean; + globalChartIsZooming: boolean; + globalChartIsZoomed: boolean; + globalZoomArea: { x1: string; x2: string }; loadingState: LoadingState; sitesLoadingState: LoadingState; }; @@ -80,6 +84,9 @@ export const { useGlobalState, getGlobalState, setGlobalState } = lat: 54.70534432, zoom: 5, autoZoom: false, + globalChartIsZooming: false, + globalChartIsZoomed: false, + globalZoomArea: { x1: "", x2: "" }, showSiteCount: undefined, aggregationLevel: AGGREGATION_LEVELS.REGION, sortBy: SORT_BY.CAPACITY, diff --git a/apps/nowcasting-app/components/icons/icons.tsx b/apps/nowcasting-app/components/icons/icons.tsx index af477cb0..bcbe9af8 100644 --- a/apps/nowcasting-app/components/icons/icons.tsx +++ b/apps/nowcasting-app/components/icons/icons.tsx @@ -333,3 +333,26 @@ export const ClockInlineSmall = (props: React.SVGProps & { title: ); + +export const ZoomOutIcon = (props: React.SVGProps & { title: string }) => ( + + + + + + + + + + + + + + + +); +export default ZoomOutIcon;