From 643add2a677fea823448c6375a56681dc19ecd35 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Fri, 4 Oct 2024 16:53:11 +0200 Subject: [PATCH 01/28] Increase opacity of LineChart inactive scenarios --- src/components/LineChart.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/LineChart.jsx b/src/components/LineChart.jsx index d923117..867f11e 100644 --- a/src/components/LineChart.jsx +++ b/src/components/LineChart.jsx @@ -59,7 +59,7 @@ export default function LineChart({ selectedIndex, isFahrenheit }) { borderColor: selectedIndex === 0 ? '#FFFFFFBF' - : 'rgba(239, 239, 240, 0.2)', + : 'rgba(239, 239, 240, 0.4)', borderWidth: selectedIndex === 0 ? 1 : 0.5, fill: false, pointRadius: 0 @@ -72,7 +72,7 @@ export default function LineChart({ selectedIndex, isFahrenheit }) { borderColor: selectedIndex === 1 ? '#FFFFFFBF' - : 'rgba(239, 239, 240, 0.2)', + : 'rgba(239, 239, 240, 0.4)', borderWidth: selectedIndex === 1 ? 1 : 0.5, fill: false, pointRadius: 0 @@ -85,7 +85,7 @@ export default function LineChart({ selectedIndex, isFahrenheit }) { borderColor: selectedIndex === 2 ? '#FFFFFFBF' - : 'rgba(239, 239, 240, 0.2)', + : 'rgba(239, 239, 240, 0.4)', borderWidth: selectedIndex === 2 ? 1 : 0.5, fill: false, pointRadius: 0 @@ -98,7 +98,7 @@ export default function LineChart({ selectedIndex, isFahrenheit }) { borderColor: selectedIndex === 3 ? '#FFFFFFBF' - : 'rgba(239, 239, 240, 0.2)', + : 'rgba(239, 239, 240, 0.4)', borderWidth: selectedIndex === 3 ? 1 : 0.5, fill: false, pointRadius: 0 From f8221f49236966c4970f49760e1fda697e2ef861 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Fri, 4 Oct 2024 16:54:07 +0200 Subject: [PATCH 02/28] Handle loading state when fetching samples --- src/App.jsx | 35 ++--- src/components/Map.jsx | 79 ++++++----- src/components/Panel.jsx | 199 +++++++++++++++++---------- src/contexts/DataFetchingContext.jsx | 26 ++++ src/utils/utils.js | 17 ++- 5 files changed, 229 insertions(+), 127 deletions(-) create mode 100644 src/contexts/DataFetchingContext.jsx diff --git a/src/App.jsx b/src/App.jsx index 9bc7a01..ceef109 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,6 +9,7 @@ import { ErrorContext } from './contexts/AppContext'; import { VideoProvider } from './contexts/VideoContext'; +import { DataFetchingProvider } from './contexts/DataFetchingContext'; import config from './config.json'; import RotateOverlay from './components/RotateOverlay'; import Tour from './components/Tour'; @@ -34,22 +35,24 @@ export default function App() { - {hasWebGLError ? ( -
- Your WebGL implementation doesn't seem to - support hardware accelerated rendering. - Check your browser settings or if your GPU - is in a blocklist. -
- ) : ( - <> - - - - - - - )} + + {hasWebGLError ? ( +
+ Your WebGL implementation doesn't seem + to support hardware accelerated + rendering. Check your browser settings + or if your GPU is in a blocklist. +
+ ) : ( + <> + + + + + + + )} +
diff --git a/src/components/Map.jsx b/src/components/Map.jsx index dce9a49..f578253 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -16,6 +16,7 @@ import SceneView from '@arcgis/core/views/SceneView'; import Search from '@arcgis/core/widgets/Search'; import Popup from '@arcgis/core/widgets/Popup'; import { VideoContext } from '../contexts/VideoContext'; +import { DataFetchingContext } from '../contexts/DataFetchingContext'; import { ChartDataContext, MapViewContext, @@ -59,6 +60,8 @@ export default function Home() { const { mapView, setMapView } = useContext(MapViewContext); const { setChartData } = useContext(ChartDataContext); const { dataSelection } = useContext(DataSelectionContext); + const { setIsLoading, setIsInvalidData } = useContext(DataFetchingContext); + const [selectedDataset, selectedVariable] = dataSelection; const selectedDatasetVariables = config.datasets[0].variables; @@ -456,48 +459,58 @@ export default function Home() { const dataIsValid = await handleImageServiceRequest( event, selectedVariable, - setChartData + setChartData, + setIsLoading, + setIsInvalidData ); if (!dataIsValid) { - const defaultScenePoint = new Point({ - longitude: -77.0369, - latitude: 38.9072, - spatialReference: { wkid: 4326 } - }); - - if ( - Math.abs( - event.mapPoint.longitude - defaultScenePoint.longitude - ) > 0.0001 || - Math.abs(event.mapPoint.latitude - defaultScenePoint.latitude) > - 0.0001 - ) { - await view.goTo({ - center: [ - defaultScenePoint.longitude, - defaultScenePoint.latitude - ], - zoom: 10 + setTimeout(async () => { + const defaultScenePoint = new Point({ + longitude: -77.0369, + latitude: 38.9072, + spatialReference: { wkid: 4326 } }); - await createBuffer(defaultScenePoint, pointLayer, bufferLayer); - - const eventForDC = { mapPoint: defaultScenePoint }; - const dataIsValidDC = await handleImageServiceRequest( - eventForDC, - selectedVariable, - setChartData - ); + if ( + Math.abs( + event.mapPoint.longitude - defaultScenePoint.longitude + ) > 0.0001 || + Math.abs( + event.mapPoint.latitude - defaultScenePoint.latitude + ) > 0.0001 + ) { + await view.goTo({ + center: [ + defaultScenePoint.longitude, + defaultScenePoint.latitude + ], + zoom: 10 + }); - if (!dataIsValidDC) { + await createBuffer( + defaultScenePoint, + pointLayer, + bufferLayer + ); + + const eventForDC = { mapPoint: defaultScenePoint }; + const dataIsValidDC = await handleImageServiceRequest( + eventForDC, + selectedVariable, + setChartData, + setIsLoading, + setIsInvalidData + ); + + if (!dataIsValidDC) { + console.error('Data is invalid even for Washington DC'); + } + } else { console.error('Data is invalid even for Washington DC'); setChartData([]); } - } else { - console.error('Data is invalid even for Washington DC'); - setChartData([]); - } + }, 1000); } }; diff --git a/src/components/Panel.jsx b/src/components/Panel.jsx index 323e737..3302b96 100644 --- a/src/components/Panel.jsx +++ b/src/components/Panel.jsx @@ -1,11 +1,12 @@ import LineChart from './LineChart'; -import { useContext, useState, useMemo } from 'react'; +import { useContext, useState, useMemo, useEffect } from 'react'; import config from '../config.json'; import { DataSelectionContext, MapViewContext, ChartDataContext } from '../contexts/AppContext'; +import { DataFetchingContext } from '../contexts/DataFetchingContext'; import { VideoContext } from '../contexts/VideoContext'; import { PlayIcon, PauseIcon } from '@heroicons/react/24/solid'; import { FPS, TOTAL_FRAMES } from '../utils/constants'; @@ -26,6 +27,9 @@ export default function Panel() { } = useContext(VideoContext); const { mapView } = useContext(MapViewContext); const { setDataSelection } = useContext(DataSelectionContext); + const { isLoading, isInvalidData } = useContext(DataFetchingContext); + const [wasInvalidShown, setWasInvalidShown] = useState(false); + const [isFahrenheit, setIsFahrenheit] = useState(true); const { chartData } = useContext(ChartDataContext); @@ -117,6 +121,16 @@ export default function Panel() { ); }; + useEffect(() => { + if (isInvalidData) { + setWasInvalidShown(true); + } else { + // Reset wasInvalidShown after a short delay or when the invalid message is gone + const timer = setTimeout(() => setWasInvalidShown(false), 2000); // 2-second delay + return () => clearTimeout(timer); // Cleanup the timer on component unmount or update + } + }, [isInvalidData]); + return ( <> {isModalOpen && ( @@ -215,85 +229,122 @@ export default function Panel() { className="flex-1" style={{ minWidth: '600px' }} > -
- {getMaxValuesForYears.map( - (item, idx) => ( -
- {item.value === 'N/A' ? ( -
- ) : ( + {/* Show fetching message only if loading, not showing invalid message, and if the invalid message wasn't recently shown */} + {isLoading && + !isInvalidData && + !wasInvalidShown && ( +
+
+

+ Fetching temperature + information for your + selected location... +

+
+ )} + + {/* Show invalid data message if data is invalid */} + {isInvalidData && ( +
+
+

+ This dataset does not provide + temperature information for + oceans. Moving the map marker to + the last land location... +

+
+ )} + + {/* Loading or Invalid data message handling */} + {!isLoading && !isInvalidData && ( + <> +
+ {getMaxValuesForYears.map( + (item, idx) => (
- handleYearClick( - item.year - ) - } + key={idx} + className="relative text-center" > -
+ ) : ( +
+ handleYearClick( + item.year ) - ) - }} - > - - {item.value} - - - ° - {isFahrenheit - ? 'F' - : 'C'} - -
- - {item.year} - +
+ + { + item.value + } + + + ° + {isFahrenheit + ? 'F' + : 'C'} + +
+ + { + item.year + } + +
+ )}
- )} -
- ) - )} -
+ ) + )} + -
- -
+
+ +
+ + )} diff --git a/src/contexts/DataFetchingContext.jsx b/src/contexts/DataFetchingContext.jsx new file mode 100644 index 0000000..b497f90 --- /dev/null +++ b/src/contexts/DataFetchingContext.jsx @@ -0,0 +1,26 @@ +import React, { useState, createContext } from 'react'; + +export const DataFetchingContext = createContext({ + isLoading: false, + setIsLoading: () => {}, + isInvalidData: false, + setIsInvalidData: () => {} +}); + +export const DataFetchingProvider = ({ children }) => { + const [isLoading, setIsLoading] = useState(false); + const [isInvalidData, setIsInvalidData] = useState(false); + + return ( + + {children} + + ); +}; diff --git a/src/utils/utils.js b/src/utils/utils.js index c40505f..c89d5dc 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,7 +1,9 @@ - -export const handleImageServiceRequest = async (event, variable, setChartData) => { +export const handleImageServiceRequest = async (event, variable, setChartData, setIsLoading, setIsInvalidData) => { const point = event.mapPoint; + setIsLoading(true); + setIsInvalidData(false); + const url = new URL(variable.service + "/getSamples"); url.searchParams.append("geometry", `${point.longitude},${point.latitude}`); @@ -33,7 +35,7 @@ export const handleImageServiceRequest = async (event, variable, setChartData) = try { const response = await fetch(url.toString(), { method: 'GET' }); const results = await response.json(); - // const results = mockData; + // const results = mockData; // Uncomment this line to use mockData during testing let invalidData = false; @@ -97,14 +99,21 @@ export const handleImageServiceRequest = async (event, variable, setChartData) = })); setChartData(chartData); + setIsLoading(false); + setIsInvalidData(false); + return true; + } else { + setIsInvalidData(true); } } else { invalidData = true; + setIsInvalidData(true); } return !invalidData; } catch (err) { console.error('Error fetching data from ImageService:', err); + setIsLoading(false); return false; } -}; \ No newline at end of file +}; From 5706b31c9f9c314bc95737dfc7e067418cd75abc Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Fri, 4 Oct 2024 17:26:06 +0200 Subject: [PATCH 03/28] Add circle buffer --- src/components/Map.jsx | 106 ++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 50 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index f578253..c805c50 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -28,11 +28,7 @@ import { FRAME_DURATION, TOTAL_FRAMES, FPS } from '../utils/constants'; import { Transition } from '@headlessui/react'; import Expand from '@arcgis/core/widgets/Expand'; import { isMobileDevice } from '../utils/helpers'; -import { - bufferSymbol, - crosshairSymbol, - createCornerAngles -} from '../utils/sceneHelpers'; +import { crosshairSymbol } from '../utils/sceneHelpers'; import ShareModal from './ShareModal'; @@ -95,67 +91,77 @@ export default function Home() { return { bufferLayer, pointLayer }; }; - const createBuffer = async (point, pointLayer, bufferLayer) => { - const sideLength = 10; - - const squarePolygon = { - type: 'polygon', - rings: [ - [ - [point.x - sideLength / 2, point.y - sideLength / 2], - [point.x + sideLength / 2, point.y - sideLength / 2], - [point.x + sideLength / 2, point.y + sideLength / 2], - [point.x - sideLength / 2, point.y + sideLength / 2], - [point.x - sideLength / 2, point.y - sideLength / 2] - ] - ], - spatialReference: point.spatialReference + const outerRadius = 500; + const middleRadius = 300; + const innerRadius = 100; + + const outerBufferSymbol = { + type: 'simple-fill', + color: [5, 80, 216, 0.3], + outline: { color: [255, 255, 255, 0], width: 0 } }; - const cornerAngles = createCornerAngles(point, sideLength); + const middleBufferSymbol = { + type: 'simple-fill', + color: [5, 50, 180, 0.4], + outline: { color: [255, 255, 255, 0], width: 0 } + }; - const angleSymbol = { - type: 'simple-line', - color: [255, 255, 255], - width: 1 + const innerBufferSymbol = { + type: 'simple-fill', + color: [5, 30, 150, 0.5], + outline: { color: [255, 255, 255, 0], width: 0 } }; - const bufferGraphic = new Graphic({ - geometry: squarePolygon, - symbol: bufferSymbol + const outerCircle = await geometryEngineAsync.geodesicBuffer( + point, + outerRadius, + 'kilometers' + ); + const middleCircle = await geometryEngineAsync.geodesicBuffer( + point, + middleRadius, + 'kilometers' + ); + const innerCircle = await geometryEngineAsync.geodesicBuffer( + point, + innerRadius, + 'kilometers' + ); + + const outerBufferGraphic = new Graphic({ + geometry: outerCircle, + symbol: outerBufferSymbol + }); + + const middleBufferGraphic = new Graphic({ + geometry: middleCircle, + symbol: middleBufferSymbol + }); + + const innerBufferGraphic = new Graphic({ + geometry: innerCircle, + symbol: innerBufferSymbol }); if (!pointLayer.graphics.length) { pointLayer.add( new Graphic({ geometry: point, symbol: crosshairSymbol }) ); - bufferLayer.add(bufferGraphic); - - cornerAngles.forEach((cornerGeometry) => { - bufferLayer.add( - new Graphic({ - geometry: cornerGeometry, - symbol: angleSymbol - }) - ); - }); + bufferLayer.add(outerBufferGraphic); + bufferLayer.add(middleBufferGraphic); + bufferLayer.add(innerBufferGraphic); } else { pointLayer.graphics.getItemAt(0).geometry = point; + bufferLayer.graphics.getItemAt(0).geometry = outerCircle; + bufferLayer.graphics.getItemAt(0).symbol = outerBufferSymbol; - bufferLayer.graphics.getItemAt(0).geometry = squarePolygon; - bufferLayer.graphics.getItemAt(0).symbol = bufferSymbol; + bufferLayer.graphics.getItemAt(1).geometry = middleCircle; + bufferLayer.graphics.getItemAt(1).symbol = middleBufferSymbol; - bufferLayer.removeAll(); - bufferLayer.add(bufferGraphic); - cornerAngles.forEach((cornerGeometry) => { - bufferLayer.add( - new Graphic({ - geometry: cornerGeometry, - symbol: angleSymbol - }) - ); - }); + bufferLayer.graphics.getItemAt(2).geometry = innerCircle; + bufferLayer.graphics.getItemAt(2).symbol = innerBufferSymbol; } }; From f0da987a9197b657a98cc0f18d70c822905fb611 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Fri, 4 Oct 2024 17:27:33 +0200 Subject: [PATCH 04/28] Update invalid data msg content --- src/components/Panel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Panel.jsx b/src/components/Panel.jsx index 3302b96..3e4de3c 100644 --- a/src/components/Panel.jsx +++ b/src/components/Panel.jsx @@ -251,7 +251,7 @@ export default function Panel() { This dataset does not provide temperature information for oceans. Moving the map marker to - the last land location... + the default location...

)} From 52f8d3d5955c59e2fc23005afca4b6822108be10 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Fri, 4 Oct 2024 18:26:09 +0200 Subject: [PATCH 05/28] Scale buffer on zoom --- src/components/Map.jsx | 65 +++++++++++++++++++++++++++++---------- src/utils/sceneHelpers.ts | 2 +- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index c805c50..3f06e0a 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -29,9 +29,16 @@ import { Transition } from '@headlessui/react'; import Expand from '@arcgis/core/widgets/Expand'; import { isMobileDevice } from '../utils/helpers'; import { crosshairSymbol } from '../utils/sceneHelpers'; +import { debounce } from 'lodash'; import ShareModal from './ShareModal'; +const defaultScenePoint = new Point({ + longitude: -77.0369, + latitude: 38.9072, + spatialReference: { wkid: 4326 } +}); + const createFeatureLayer = (url) => new FeatureLayer({ url, @@ -80,7 +87,7 @@ export default function Home() { let draggingInsideBuffer = false; let initialCamera; - let lastKnownPoint; + let lastKnownPoint = defaultScenePoint; let bufferLayer; let pointLayer; @@ -91,10 +98,19 @@ export default function Home() { return { bufferLayer, pointLayer }; }; - const createBuffer = async (point, pointLayer, bufferLayer) => { - const outerRadius = 500; - const middleRadius = 300; - const innerRadius = 100; + + const createBuffer = async (point, pointLayer, bufferLayer, view) => { + const zoomLevel = view.zoom; + + const baseOuterRadius = 500; + const baseMiddleRadius = 300; + const baseInnerRadius = 100; + + const scaleFactor = zoomLevel / 3; + + const outerRadius = baseOuterRadius / scaleFactor; + const middleRadius = baseMiddleRadius / scaleFactor; + const innerRadius = baseInnerRadius / scaleFactor; const outerBufferSymbol = { type: 'simple-fill', @@ -165,6 +181,8 @@ export default function Home() { } }; + const debouncedCreateBuffer = debounce(createBuffer, 100); + const handleDragStart = async (event, view, bufferLayer) => { const startPoint = view.toMap({ x: event.x, y: event.y }); const bufferGraphic = bufferLayer.graphics.getItemAt(0); @@ -189,7 +207,7 @@ export default function Home() { if (updatedPoint) { event.stopPropagation(); - await createBuffer(updatedPoint, pointLayer, bufferLayer); + await createBuffer(updatedPoint, pointLayer, bufferLayer, view); lastKnownPoint = updatedPoint; } } @@ -350,7 +368,14 @@ export default function Home() { ], zoom: 1 }); - await createBuffer(initialCenterPoint, pointLayer, bufferLayer); + + await createBuffer( + initialCenterPoint, + pointLayer, + bufferLayer, + view + ); + await handleMapClick({ mapPoint: initialCenterPoint }); view.on('drag', (event) => { @@ -367,11 +392,22 @@ export default function Home() { const mapPoint = view.toMap(event); if (mapPoint) { - await createBuffer(mapPoint, pointLayer, bufferLayer); + await createBuffer(mapPoint, pointLayer, bufferLayer, view); lastKnownPoint = mapPoint; await handleMapClick({ mapPoint }, view); } }); + + view.watch('zoom', () => { + if (lastKnownPoint) { + debouncedCreateBuffer( + lastKnownPoint, + pointLayer, + bufferLayer, + view + ); + } + }); }).catch((error) => { if (error.name.includes('webgl')) { setHasWebGLError(true); @@ -428,7 +464,7 @@ export default function Home() { view.graphics.removeAll(); - await createBuffer(point, pointLayer, bufferLayer); + await createBuffer(point, pointLayer, bufferLayer, view); lastKnownPoint = point; await handleMapClick({ mapPoint: point }, view); @@ -471,13 +507,9 @@ export default function Home() { ); if (!dataIsValid) { - setTimeout(async () => { - const defaultScenePoint = new Point({ - longitude: -77.0369, - latitude: 38.9072, - spatialReference: { wkid: 4326 } - }); + lastKnownPoint = defaultScenePoint; + setTimeout(async () => { if ( Math.abs( event.mapPoint.longitude - defaultScenePoint.longitude @@ -497,7 +529,8 @@ export default function Home() { await createBuffer( defaultScenePoint, pointLayer, - bufferLayer + bufferLayer, + view ); const eventForDC = { mapPoint: defaultScenePoint }; diff --git a/src/utils/sceneHelpers.ts b/src/utils/sceneHelpers.ts index d3014f7..164da93 100644 --- a/src/utils/sceneHelpers.ts +++ b/src/utils/sceneHelpers.ts @@ -8,7 +8,7 @@ export const crosshairSymbol = { type: 'simple-marker', style: 'cross', color: [5, 80, 216], - size: 12, + size: 4, outline: { color: [255, 255, 255], width: 1 From 51438049ac66cf0b6570bdd316cc72a66371b6f0 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Fri, 4 Oct 2024 18:28:32 +0200 Subject: [PATCH 06/28] Add deps --- package-lock.json | 38 ++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 40 insertions(+) diff --git a/package-lock.json b/package-lock.json index 51baf51..2ff3a37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,10 @@ "chartjs-plugin-annotation": "^3.0.1", "chartjs-plugin-crosshair": "^2.0.0", "date-fns": "^3.6.0", + "lodash": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", "react-joyride": "^2.9.2", "react-share": "^5.1.0", "styled-components": "^6.1.12" @@ -4250,6 +4252,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -4938,6 +4946,12 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, "node_modules/react-floater": { "version": "0.7.9", "resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz", @@ -4977,6 +4991,21 @@ "is-lite": "^0.8.2" } }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-innertext": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz", @@ -5048,6 +5077,15 @@ "react": "^17 || ^18" } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index a92f78d..8537f2a 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "chartjs-plugin-annotation": "^3.0.1", "chartjs-plugin-crosshair": "^2.0.0", "date-fns": "^3.6.0", + "lodash": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", "react-joyride": "^2.9.2", "react-share": "^5.1.0", "styled-components": "^6.1.12" From fa2b7408f243d2a9c04d82257ac328485833ad68 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Fri, 4 Oct 2024 18:33:09 +0200 Subject: [PATCH 07/28] Revert scaling of buffer --- src/components/Map.jsx | 65 +++++++++++------------------------------- 1 file changed, 16 insertions(+), 49 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 3f06e0a..c805c50 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -29,16 +29,9 @@ import { Transition } from '@headlessui/react'; import Expand from '@arcgis/core/widgets/Expand'; import { isMobileDevice } from '../utils/helpers'; import { crosshairSymbol } from '../utils/sceneHelpers'; -import { debounce } from 'lodash'; import ShareModal from './ShareModal'; -const defaultScenePoint = new Point({ - longitude: -77.0369, - latitude: 38.9072, - spatialReference: { wkid: 4326 } -}); - const createFeatureLayer = (url) => new FeatureLayer({ url, @@ -87,7 +80,7 @@ export default function Home() { let draggingInsideBuffer = false; let initialCamera; - let lastKnownPoint = defaultScenePoint; + let lastKnownPoint; let bufferLayer; let pointLayer; @@ -98,19 +91,10 @@ export default function Home() { return { bufferLayer, pointLayer }; }; - - const createBuffer = async (point, pointLayer, bufferLayer, view) => { - const zoomLevel = view.zoom; - - const baseOuterRadius = 500; - const baseMiddleRadius = 300; - const baseInnerRadius = 100; - - const scaleFactor = zoomLevel / 3; - - const outerRadius = baseOuterRadius / scaleFactor; - const middleRadius = baseMiddleRadius / scaleFactor; - const innerRadius = baseInnerRadius / scaleFactor; + const createBuffer = async (point, pointLayer, bufferLayer) => { + const outerRadius = 500; + const middleRadius = 300; + const innerRadius = 100; const outerBufferSymbol = { type: 'simple-fill', @@ -181,8 +165,6 @@ export default function Home() { } }; - const debouncedCreateBuffer = debounce(createBuffer, 100); - const handleDragStart = async (event, view, bufferLayer) => { const startPoint = view.toMap({ x: event.x, y: event.y }); const bufferGraphic = bufferLayer.graphics.getItemAt(0); @@ -207,7 +189,7 @@ export default function Home() { if (updatedPoint) { event.stopPropagation(); - await createBuffer(updatedPoint, pointLayer, bufferLayer, view); + await createBuffer(updatedPoint, pointLayer, bufferLayer); lastKnownPoint = updatedPoint; } } @@ -368,14 +350,7 @@ export default function Home() { ], zoom: 1 }); - - await createBuffer( - initialCenterPoint, - pointLayer, - bufferLayer, - view - ); - + await createBuffer(initialCenterPoint, pointLayer, bufferLayer); await handleMapClick({ mapPoint: initialCenterPoint }); view.on('drag', (event) => { @@ -392,22 +367,11 @@ export default function Home() { const mapPoint = view.toMap(event); if (mapPoint) { - await createBuffer(mapPoint, pointLayer, bufferLayer, view); + await createBuffer(mapPoint, pointLayer, bufferLayer); lastKnownPoint = mapPoint; await handleMapClick({ mapPoint }, view); } }); - - view.watch('zoom', () => { - if (lastKnownPoint) { - debouncedCreateBuffer( - lastKnownPoint, - pointLayer, - bufferLayer, - view - ); - } - }); }).catch((error) => { if (error.name.includes('webgl')) { setHasWebGLError(true); @@ -464,7 +428,7 @@ export default function Home() { view.graphics.removeAll(); - await createBuffer(point, pointLayer, bufferLayer, view); + await createBuffer(point, pointLayer, bufferLayer); lastKnownPoint = point; await handleMapClick({ mapPoint: point }, view); @@ -507,9 +471,13 @@ export default function Home() { ); if (!dataIsValid) { - lastKnownPoint = defaultScenePoint; - setTimeout(async () => { + const defaultScenePoint = new Point({ + longitude: -77.0369, + latitude: 38.9072, + spatialReference: { wkid: 4326 } + }); + if ( Math.abs( event.mapPoint.longitude - defaultScenePoint.longitude @@ -529,8 +497,7 @@ export default function Home() { await createBuffer( defaultScenePoint, pointLayer, - bufferLayer, - view + bufferLayer ); const eventForDC = { mapPoint: defaultScenePoint }; From 80e52f28f487cfc2ef62cec4c636f5c92e104748 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Fri, 4 Oct 2024 18:40:17 +0200 Subject: [PATCH 08/28] Apply rescaling --- src/components/Map.jsx | 63 +++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index c805c50..7b704ce 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -29,9 +29,16 @@ import { Transition } from '@headlessui/react'; import Expand from '@arcgis/core/widgets/Expand'; import { isMobileDevice } from '../utils/helpers'; import { crosshairSymbol } from '../utils/sceneHelpers'; +import { debounce } from 'lodash'; import ShareModal from './ShareModal'; +const defaultScenePoint = new Point({ + longitude: -77.0369, + latitude: 38.9072, + spatialReference: { wkid: 4326 } +}); + const createFeatureLayer = (url) => new FeatureLayer({ url, @@ -80,7 +87,7 @@ export default function Home() { let draggingInsideBuffer = false; let initialCamera; - let lastKnownPoint; + let lastKnownPoint = defaultScenePoint; let bufferLayer; let pointLayer; @@ -91,10 +98,19 @@ export default function Home() { return { bufferLayer, pointLayer }; }; - const createBuffer = async (point, pointLayer, bufferLayer) => { - const outerRadius = 500; - const middleRadius = 300; - const innerRadius = 100; + + const createBuffer = async (point, pointLayer, bufferLayer, view) => { + const zoomLevel = view.zoom; + + const baseOuterRadius = 500; + const baseMiddleRadius = 300; + const baseInnerRadius = 100; + + const scaleFactor = zoomLevel / 3; + + const outerRadius = baseOuterRadius / scaleFactor; + const middleRadius = baseMiddleRadius / scaleFactor; + const innerRadius = baseInnerRadius / scaleFactor; const outerBufferSymbol = { type: 'simple-fill', @@ -165,6 +181,8 @@ export default function Home() { } }; + const debouncedCreateBuffer = debounce(createBuffer, 100); + const handleDragStart = async (event, view, bufferLayer) => { const startPoint = view.toMap({ x: event.x, y: event.y }); const bufferGraphic = bufferLayer.graphics.getItemAt(0); @@ -189,7 +207,7 @@ export default function Home() { if (updatedPoint) { event.stopPropagation(); - await createBuffer(updatedPoint, pointLayer, bufferLayer); + await createBuffer(updatedPoint, pointLayer, bufferLayer, view); lastKnownPoint = updatedPoint; } } @@ -350,7 +368,12 @@ export default function Home() { ], zoom: 1 }); - await createBuffer(initialCenterPoint, pointLayer, bufferLayer); + await createBuffer( + initialCenterPoint, + pointLayer, + bufferLayer, + view + ); await handleMapClick({ mapPoint: initialCenterPoint }); view.on('drag', (event) => { @@ -367,11 +390,22 @@ export default function Home() { const mapPoint = view.toMap(event); if (mapPoint) { - await createBuffer(mapPoint, pointLayer, bufferLayer); + await createBuffer(mapPoint, pointLayer, bufferLayer, view); lastKnownPoint = mapPoint; await handleMapClick({ mapPoint }, view); } }); + + view.watch('zoom', () => { + if (lastKnownPoint) { + debouncedCreateBuffer( + lastKnownPoint, + pointLayer, + bufferLayer, + view + ); + } + }); }).catch((error) => { if (error.name.includes('webgl')) { setHasWebGLError(true); @@ -428,7 +462,7 @@ export default function Home() { view.graphics.removeAll(); - await createBuffer(point, pointLayer, bufferLayer); + await createBuffer(point, pointLayer, bufferLayer, view); lastKnownPoint = point; await handleMapClick({ mapPoint: point }, view); @@ -471,13 +505,9 @@ export default function Home() { ); if (!dataIsValid) { - setTimeout(async () => { - const defaultScenePoint = new Point({ - longitude: -77.0369, - latitude: 38.9072, - spatialReference: { wkid: 4326 } - }); + lastKnownPoint = defaultScenePoint; + setTimeout(async () => { if ( Math.abs( event.mapPoint.longitude - defaultScenePoint.longitude @@ -497,7 +527,8 @@ export default function Home() { await createBuffer( defaultScenePoint, pointLayer, - bufferLayer + bufferLayer, + view ); const eventForDC = { mapPoint: defaultScenePoint }; From 36889435530268aa2ecd2e2e463331bd4da948b7 Mon Sep 17 00:00:00 2001 From: Brian Freitag Date: Fri, 4 Oct 2024 11:53:38 -0500 Subject: [PATCH 09/28] Update tour text to geographic area --- src/components/Tour.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Tour.jsx b/src/components/Tour.jsx index 14f7254..6cc4a0f 100644 --- a/src/components/Tour.jsx +++ b/src/components/Tour.jsx @@ -18,14 +18,14 @@ export default function Tour() { title: 'Welcome to the', name: 'Mobile Climate Mapper', content: - 'The Mobile Climate Mapper is an extension of NASA’s Earth Information Center exhibit at the Smithsonian National Museum of Natural History. Use this tool to explore how climate change may affect temperatures at any location in the world.', + 'The Mobile Climate Mapper is an extension of NASA’s Earth Information Center exhibit at the Smithsonian National Museum of Natural History. Use this tool to explore how climate change may affect temperatures in any geographic area in the world.', placement: 'center', disableBeacon: true }, { target: '.map', content: - 'Tap anywhere on the map to view how temperatures are projected to change at that location', + 'Tap anywhere on the map to view how temperatures are projected to change in that geographic area', placement: 'bottom', disableBeacon: true }, From f35db90aa2cac36d1e53b972b5ddcd565a72f229 Mon Sep 17 00:00:00 2001 From: Brian Freitag Date: Fri, 4 Oct 2024 11:57:32 -0500 Subject: [PATCH 10/28] Update DataLayerModal.tsx with updated disclaimer --- src/components/DataLayerModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DataLayerModal.tsx b/src/components/DataLayerModal.tsx index 977497b..f36b9df 100644 --- a/src/components/DataLayerModal.tsx +++ b/src/components/DataLayerModal.tsx @@ -48,7 +48,7 @@ export default function DataLayerModal({

See estimates of annual maxima of daily maximum near-surface air temperature (TASMAX) from NASA Earth Exchange (NEX) Global Daily Downscaled Projections (GDDP) based on simulations of the Coupled Model Intercomparison Project Phase 6 (CMIP6).

- The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude grid and temperature in major cities could be higher than what is displayed within each gridded cell.

+ The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude, which is a system of lines used to map the sphere of the Earth. In some cases, temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average).

- -
+ { showPageTwo ? : } + ); } + +function PageTwo({}){ + return( + <> +
+ Example of spatial average of temperature for Los Angeles, CA +
+

+ The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude, which is a system of lines used to map the sphere of the Earth. + In some cases, temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. + For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average). +

+ + ); + +} +function PageOne({}){ + + return (<> +

+ How hot could it get on the hottest day in a given year, under different greenhouse gas emission scenarios? +

+

+ See estimates of annual maxima of daily maximum near-surface air temperature (TASMAX) from NASA Earth Exchange (NEX) Global Daily Downscaled Projections (GDDP) based on simulations of the Coupled Model Intercomparison Project Phase 6 (CMIP6). +

+ + ); +} + +function Buttons({isFahrenheit, setIsFahrenheit}){ + return ( +
+ + +
+ ); +} \ No newline at end of file From 4be017deb09d2b57e428f74ae62ac2774da6c861 Mon Sep 17 00:00:00 2001 From: Alex Gurvich Date: Fri, 4 Oct 2024 17:58:20 -0400 Subject: [PATCH 23/28] makings of a two page modal --- src/components/DataLayerModal.tsx | 71 ++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/src/components/DataLayerModal.tsx b/src/components/DataLayerModal.tsx index 61ac6d7..bfcddec 100644 --- a/src/components/DataLayerModal.tsx +++ b/src/components/DataLayerModal.tsx @@ -6,7 +6,7 @@ export default function DataLayerModal({ setIsFahrenheit }) { - const [ showPageTwo, setShowPageTwo ] = useState(true); + const [ showPageTwo, setShowPageTwo ] = useState(false); useEffect(() => { const handleKeyDown = (event) => { @@ -47,6 +47,17 @@ export default function DataLayerModal({
{ showPageTwo ? : } +
@@ -57,33 +68,42 @@ export default function DataLayerModal({ function PageTwo({}){ return( <> -
); +} + +export function Disclaimer({className}){ + + return ( +

+ The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude, which is a system of lines used to map the sphere of the Earth. + In some cases, temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. + For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average). +

+ + ); + } \ No newline at end of file From f380dab3764ee58d59bb5a1b1690bb000d0c8ef2 Mon Sep 17 00:00:00 2001 From: Alex Gurvich Date: Fri, 4 Oct 2024 18:04:30 -0400 Subject: [PATCH 24/28] move disclaimer out of temperature data modal and into joyride stage --- src/components/DataLayerModal.tsx | 29 +++++++++++++++++------------ src/components/Tour.jsx | 20 ++++++++++++++++++++ src/index.css | 4 ++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/components/DataLayerModal.tsx b/src/components/DataLayerModal.tsx index bfcddec..f4fe5b7 100644 --- a/src/components/DataLayerModal.tsx +++ b/src/components/DataLayerModal.tsx @@ -47,17 +47,7 @@ export default function DataLayerModal({
{ showPageTwo ? : } - + {/* */}
@@ -66,6 +56,7 @@ export default function DataLayerModal({ } function PageTwo({}){ + // ABG: https://cdn.dribbble.com/users/716122/screenshots/14300379/media/44f698e864671ffe25b844469e40de42.jpg?resize=400x300&vertical=center return( <>
@@ -97,7 +88,7 @@ function PageOne({}){ {' '} NEX-GDDP-CMIP6 dataset ! @@ -158,4 +149,18 @@ export function Disclaimer({className}){ ); +} + +function SwapButton({setShowPageTwo}){ + return } \ No newline at end of file diff --git a/src/components/Tour.jsx b/src/components/Tour.jsx index 6cc4a0f..b852c7c 100644 --- a/src/components/Tour.jsx +++ b/src/components/Tour.jsx @@ -3,6 +3,7 @@ import Joyride from 'react-joyride'; import TourButton from './TourButton'; import TourTooltip from './TourTooltip'; import useLocalStorage from '../hooks/useLocalStorage'; +import Disclaimer from './DataLayerModal'; export default function Tour() { const [tourComplete, setTourComplete] = useLocalStorage( @@ -22,6 +23,25 @@ export default function Tour() { placement: 'center', disableBeacon: true }, + { + target: 'body', + title: 'About the Data', + content:
+ Example of spatial average of temperature for Los Angeles, CA +

+ The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude, which is a system of lines used to map the sphere of the Earth. + In some cases, temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. + For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average). + +

+
, + placement: 'center', + disableBeacon: true + }, { target: '.map', content: diff --git a/src/index.css b/src/index.css index 6218bf5..37df19f 100644 --- a/src/index.css +++ b/src/index.css @@ -116,4 +116,8 @@ .chartjs-tooltip-hidden { opacity: 0; visibility: hidden; + } + + .tooltip__content{ + padding-bottom:0 !important; } \ No newline at end of file From 7e7f400e1346b9adcbf9ce90ea8b8cfead04cbeb Mon Sep 17 00:00:00 2001 From: Brian Freitag Date: Fri, 4 Oct 2024 17:47:43 -0500 Subject: [PATCH 25/28] fix grammar in data layer modal --- src/components/DataLayerModal.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/DataLayerModal.tsx b/src/components/DataLayerModal.tsx index f4fe5b7..11572b9 100644 --- a/src/components/DataLayerModal.tsx +++ b/src/components/DataLayerModal.tsx @@ -142,8 +142,8 @@ export function Disclaimer({className}){ ${className} `} > - The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude, which is a system of lines used to map the sphere of the Earth. - In some cases, temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. + The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude grid, which is a system of lines used to map the sphere of the Earth. + In some cases, the temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average).

@@ -163,4 +163,4 @@ function SwapButton({setShowPageTwo}){ > Swap pages -} \ No newline at end of file +} From c80fa73597b39c0eb4e467fa475cc6b86559ea21 Mon Sep 17 00:00:00 2001 From: Brian Freitag Date: Fri, 4 Oct 2024 17:48:08 -0500 Subject: [PATCH 26/28] Fix grammar in tour text --- src/components/Tour.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Tour.jsx b/src/components/Tour.jsx index b852c7c..cd33134 100644 --- a/src/components/Tour.jsx +++ b/src/components/Tour.jsx @@ -33,8 +33,8 @@ export default function Tour() { alt="Example of spatial average of temperature for Los Angeles, CA" />

- The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude, which is a system of lines used to map the sphere of the Earth. - In some cases, temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. + The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude grid, which is a system of lines used to map the sphere of the Earth. + In some cases, the temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average).

From 844884941911de315287849a4222bb55df66d08b Mon Sep 17 00:00:00 2001 From: Brian Freitag Date: Fri, 4 Oct 2024 17:54:27 -0500 Subject: [PATCH 27/28] adding back NEX-GDDP disclaimer text (can remove if approved) --- src/components/DataLayerModal.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/DataLayerModal.tsx b/src/components/DataLayerModal.tsx index 11572b9..6ce1d66 100644 --- a/src/components/DataLayerModal.tsx +++ b/src/components/DataLayerModal.tsx @@ -83,6 +83,19 @@ function PageOne({}){ See estimates of annual maxima of daily maximum near-surface air temperature (TASMAX) from NASA Earth Exchange (NEX) Global Daily Downscaled Projections (GDDP) based on simulations of the Coupled Model Intercomparison Project Phase 6 (CMIP6).


+

+ The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude grid, which is a system of lines used to map the sphere of the Earth. + In some cases, the temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. + For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average). +

Learn more about the {' '} From 1bf5ff18da30720b4313d499219f7455fbea43ea Mon Sep 17 00:00:00 2001 From: Brian Freitag Date: Fri, 4 Oct 2024 17:58:11 -0500 Subject: [PATCH 28/28] correct formatting for disclaimer --- src/components/DataLayerModal.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/DataLayerModal.tsx b/src/components/DataLayerModal.tsx index 6ce1d66..26b51a5 100644 --- a/src/components/DataLayerModal.tsx +++ b/src/components/DataLayerModal.tsx @@ -83,19 +83,12 @@ function PageOne({}){ See estimates of annual maxima of daily maximum near-surface air temperature (TASMAX) from NASA Earth Exchange (NEX) Global Daily Downscaled Projections (GDDP) based on simulations of the Coupled Model Intercomparison Project Phase 6 (CMIP6).


-

+

The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude grid, which is a system of lines used to map the sphere of the Earth. In some cases, the temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average).

+

Learn more about the {' '}

+
Example of spatial average of temperature for Los Angeles, CA
-

- The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude, which is a system of lines used to map the sphere of the Earth. - In some cases, temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. - For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average). -

+ ); } function PageOne({}){ - return (<> -

+ return ( +

+ ); } function Buttons({isFahrenheit, setIsFahrenheit}){ @@ -117,4 +137,25 @@ function Buttons({isFahrenheit, setIsFahrenheit}){