Skip to content

Commit

Permalink
Merge pull request #456 from openclimatefix/issue/identical-zoom-for-…
Browse files Browse the repository at this point in the history
…all-charts

Feat: global zoom for all charts
  • Loading branch information
braddf authored Apr 15, 2024
2 parents 4598f69 + f5f6108 commit 5476b35
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ const DeltaChart: FC<DeltaChartProps> = ({ 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ const GspPvRemixChart: FC<{
console.log(errors);
return <div>failed to load</div>;
}

const now30min = formatISODateString(get30MinNow());
const dataMissing = !gspForecastDataOneGSP || !pvRealDataIn || !pvRealDataAfter;
const forecastAtSelectedTime: NonNullable<typeof gspForecastDataOneGSP>[number] =
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 @@ -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,
Expand Down
117 changes: 113 additions & 4 deletions apps/nowcasting-app/components/charts/remix-line.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC } from "react";
import React, { FC, useEffect, useState } from "react";
import {
Area,
Bar,
Expand All @@ -7,6 +7,7 @@ import {
Line,
Rectangle,
ReferenceLine,
ReferenceArea,
ResponsiveContainer,
Tooltip,
XAxis,
Expand All @@ -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;
Expand Down Expand Up @@ -79,6 +84,7 @@ type RemixLineProps = {
timeNow: string;
resetTime?: () => void;
visibleLines: string[];
zoomEnabled?: boolean;
deltaView?: boolean;
deltaYMaxOverride?: number;
};
Expand Down Expand Up @@ -136,6 +142,7 @@ const RemixLine: React.FC<RemixLineProps> = ({
timeNow,
resetTime,
visibleLines,
zoomEnabled = true,
deltaView = false,
deltaYMaxOverride
}) => {
Expand All @@ -147,6 +154,13 @@ const RemixLine: React.FC<RemixLineProps> = ({
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(
Expand Down Expand Up @@ -182,10 +196,12 @@ const RemixLine: React.FC<RemixLineProps> = ({
.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();
Expand All @@ -198,13 +214,55 @@ const RemixLine: React.FC<RemixLineProps> = ({
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 (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
{zoomEnabled && globalIsZoomed && (
<div className={`absolute top-5 z-10 ${deltaView ? `right-16 mr-3` : `right-4`}`}>
<button
type="button"
onClick={handleZoomOut}
style={{ position: "relative", top: "0", left: "20" }}
className="flex font-bold items-center p-1.5 border-ocf-gray-800 text-white bg-ocf-gray-800 hover:bg-ocf-gray-700 focus:z-10 focus:text-white h-auto"
>
<ZoomOutIcon className="w-8 h-8" />
</button>
</div>
)}
<ResponsiveContainer>
<ComposedChart
className="select-none"
width={500}
height={400}
data={preppedData}
data={zoomEnabled && globalIsZoomed ? filteredPreppedData : preppedData}
margin={{
top: 20,
right: 16,
Expand All @@ -220,6 +278,42 @@ const RemixLine: React.FC<RemixLineProps> = ({
: 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);
}
}}
>
<CartesianGrid verticalFill={["#545454", "#6C6C6C"]} fillOpacity={0.5} />
<XAxis
Expand All @@ -235,6 +329,7 @@ const RemixLine: React.FC<RemixLineProps> = ({
interval={view === VIEWS.SOLAR_SITES ? undefined : 11}
/>
<XAxis
className="select-none"
dataKey="formattedDate"
xAxisId={"x-axis-2"}
tickFormatter={prettyPrintChartAxisLabelDate}
Expand Down Expand Up @@ -277,7 +372,9 @@ const RemixLine: React.FC<RemixLineProps> = ({
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,
Expand All @@ -289,6 +386,7 @@ const RemixLine: React.FC<RemixLineProps> = ({
dy: 0
}}
/>

{deltaView && (
<>
<YAxis
Expand Down Expand Up @@ -344,6 +442,7 @@ const RemixLine: React.FC<RemixLineProps> = ({
/>
}
/>

<ReferenceLine
x={
view === VIEWS.SOLAR_SITES ? new Date(localeTimeOfInterest).getTime() : timeOfInterest
Expand All @@ -361,6 +460,7 @@ const RemixLine: React.FC<RemixLineProps> = ({
></CustomizedLabel>
}
/>

{deltaView && (
<Bar
type="monotone"
Expand Down Expand Up @@ -459,7 +559,16 @@ const RemixLine: React.FC<RemixLineProps> = ({
strokeWidth={largeScreenMode ? 4 : 2}
hide={!visibleLines.includes("FORECAST")}
/>

{zoomEnabled && globalIsZooming && (
<ReferenceArea
x1={globalZoomArea?.x1}
x2={globalZoomArea?.x2}
fill="#FFD053"
fillOpacity={0.3}
xAxisId={"x-axis"}
yAxisId={"y-axis"}
/>
)}
<Tooltip
content={({ payload, label }) => {
const data = payload && payload[0]?.payload;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ const SolarSiteChart: FC<{
timeOfInterest={selectedTime}
setTimeOfInterest={setSelectedTime}
data={chartData}
zoomEnabled={false}
yMax={yMax}
visibleLines={visibleLines}
/>
Expand Down
24 changes: 24 additions & 0 deletions apps/nowcasting-app/components/helpers/chartUtils.ts
Original file line number Diff line number Diff line change
@@ -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];
}
};
7 changes: 7 additions & 0 deletions apps/nowcasting-app/components/helpers/globalState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<NationalEndpointStates>;
sitesLoadingState: LoadingState<SitesEndpointStates>;
};
Expand All @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions apps/nowcasting-app/components/icons/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,26 @@ export const ClockInlineSmall = (props: React.SVGProps<SVGSVGElement> & { title:
</svg>
</span>
);

export const ZoomOutIcon = (props: React.SVGProps<SVGSVGElement> & { title: string }) => (
<span title={props.title || ""}>
<svg viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg" {...props}>
<defs>
<style>
{
".cls-1,.cls-2{fill:none;}.cls-2{stroke:#FFFF;stroke-linecap:round;stroke-linejoin:round;}"
}
</style>
</defs>
<g data-name="Layer 2" id="Layer_2">
<g id="Workspace">
<rect className="cls-1" height={24} width={24} />
<circle className="cls-2" cx={11.5} cy={11.5} r={4.5} />
<line className="cls-2" x1={18} x2={14.68} y1={18} y2={14.68} />
<line className="cls-2" x1={9.5} x2={13.5} y1={11.5} y2={11.5} />
</g>
</g>
</svg>
</span>
);
export default ZoomOutIcon;

0 comments on commit 5476b35

Please sign in to comment.