From c064e6630fb508c0eb76da0e5a26837e7352cbe8 Mon Sep 17 00:00:00 2001 From: Georgi Tsipov <76964999+georgi4444@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:19:36 +0100 Subject: [PATCH 01/12] feat: implement loading skeleton for map legend component (#118) * feat: implement loading skeleton for map legend component * fix: update skeleton bg color --- src/app/page.tsx | 4 +-- .../HungerAlert/HungerAlertSkeleton.tsx | 2 +- src/components/Legend/MapLegendLoader.tsx | 14 ++++++++++ src/components/Legend/MapLegendSkeleton.tsx | 26 +++++++++++++++++++ tailwind.config.js | 5 +++- 5 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/components/Legend/MapLegendLoader.tsx create mode 100644 src/components/Legend/MapLegendSkeleton.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 3350467d..5a362971 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,7 +2,7 @@ import AccordionModal from '@/components/Accordions/AccordionModal'; import { AlertsMenuWrapper } from '@/components/AlertsMenu/AlertsMenuWrapper'; import Chatbot from '@/components/Chatbot/Chatbot'; import HungerAlertLoader from '@/components/HungerAlert/HungerAlertLoader'; -import MapLegend from '@/components/Legend/MapLegend'; +import MapLegendLoader from '@/components/Legend/MapLegendLoader'; import MapLoader from '@/components/Map/MapLoader'; import { Sidebar } from '@/components/Sidebar/Sidebar'; import container from '@/container'; @@ -28,7 +28,7 @@ export default async function Home() { - + ); diff --git a/src/components/HungerAlert/HungerAlertSkeleton.tsx b/src/components/HungerAlert/HungerAlertSkeleton.tsx index e75b39a3..b92c92ff 100644 --- a/src/components/HungerAlert/HungerAlertSkeleton.tsx +++ b/src/components/HungerAlert/HungerAlertSkeleton.tsx @@ -4,7 +4,7 @@ export default function HungerAlertSkeleton() { return (
diff --git a/src/components/Legend/MapLegendLoader.tsx b/src/components/Legend/MapLegendLoader.tsx new file mode 100644 index 00000000..7a08562b --- /dev/null +++ b/src/components/Legend/MapLegendLoader.tsx @@ -0,0 +1,14 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +import MapLegendSkeleton from '@/components/Legend/MapLegendSkeleton'; + +const LazyMapLegendLoader = dynamic(() => import('@/components/Legend/MapLegend'), { + ssr: false, + loading: () => , +}); + +export default function MapLegendLoader() { + return ; +} diff --git a/src/components/Legend/MapLegendSkeleton.tsx b/src/components/Legend/MapLegendSkeleton.tsx new file mode 100644 index 00000000..410cd473 --- /dev/null +++ b/src/components/Legend/MapLegendSkeleton.tsx @@ -0,0 +1,26 @@ +import { Skeleton } from '@nextui-org/skeleton'; + +export default function MapLegendSkeleton() { + return ( + <> + {/* Desktop version */} +
+
+ +
+ + +
+ +
+
+ + {/* Mobile version */} +
+ +
+ +
+ + ); +} diff --git a/tailwind.config.js b/tailwind.config.js index ed1bbaad..6ee304ac 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -62,6 +62,9 @@ const config = { brand: '#157DBC', brandHover: '#0F6396', }, + screens: { + sm700: '700px', + }, width: { '215px': '215px', '179px': '179px', @@ -210,7 +213,7 @@ const config = { nutritionPredicted: '#E3F2FD', nutritionNotAnalyzed: '#D2D1D1', - //animation + //animation nutritionAnimation: '#F7B750', fcsAnimation: '#338ef7', ipcAnimation: '#cd1919', From 1b73c1d1caf8d0cfe36cf2b27068ce493eb8841b Mon Sep 17 00:00:00 2001 From: Haidong Xu Date: Tue, 10 Dec 2024 20:22:12 +0100 Subject: [PATCH 02/12] Feature/f 139 subscribe country selection (#120) * fix: test build * fix: test * fix: test new yarn lock * fix: yarn * fix: remove console * fix: show nested menu even if move out mouse * fix: change name --------- Co-authored-by: Haidong Xu --- src/components/Subscribe/NestedPopover.tsx | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/components/Subscribe/NestedPopover.tsx b/src/components/Subscribe/NestedPopover.tsx index bcdffef2..724421a1 100644 --- a/src/components/Subscribe/NestedPopover.tsx +++ b/src/components/Subscribe/NestedPopover.tsx @@ -82,9 +82,20 @@ export function NestedPopover({ items, onSelectionChange }: NestedPopoverProps) * take care of nested menu open state * @param itemId is the id of the nested menu item */ - const handleNestedMenuToggle = (itemId: string): void => { - setOpenNestedMenu(openNestedMenu === itemId ? null : itemId); - setIsNestedOpen(true); + const hoverNestedMenuItem = (itemId: string): void => { + const hoveredItem = items.find((it) => it.topic_id === itemId); + if (hoveredItem?.options && hoveredItem.options.length > 0) { + setOpenNestedMenu(itemId); + setIsNestedOpen(true); + } + }; + + const hoverMainMenuItem = (itemId: string): void => { + const hoveredMainItem = items.find((it) => it.topic_id === itemId); + if (hoveredMainItem?.options === undefined || hoveredMainItem?.options.length === 0) { + setOpenNestedMenu(null); + setIsNestedOpen(false); + } }; const ifOptionsSelected = (option: IOption): boolean => { @@ -132,6 +143,7 @@ export function NestedPopover({ items, onSelectionChange }: NestedPopoverProps) key={item.topic_id} className="m-1 h-10 text-left text-gray-700 hover:bg-blue-100 hover:text-black dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white p-2" onClick={() => selectMainMenuItem(item)} + onMouseEnter={() => hoverMainMenuItem(item.topic_id)} > {item.topic_description}
@@ -145,18 +157,17 @@ export function NestedPopover({ items, onSelectionChange }: NestedPopoverProps)
  • handleNestedMenuToggle(item.topic_id)} - onMouseEnter={() => handleNestedMenuToggle(item.topic_id)} + onMouseEnter={() => hoverNestedMenuItem(item.topic_id)} >

    {item.topic_description}

    {/* nested menu items */} - {isNestedOpen && openNestedMenu?.includes(item.topic_id) && ( + {isNestedOpen && openNestedMenu === item.topic_id && (
      {item.options?.map((option) => ( From ad32f75b62f17e43b73c5864e48f24fd4dc45f6f Mon Sep 17 00:00:00 2001 From: Georgi Tsipov <76964999+georgi4444@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:38:50 +0100 Subject: [PATCH 03/12] feat: add zoom control skeleton (#122) * feat: add zoom control skeleton * fix: copy Map.tsx structure for the skeleton --- src/components/Map/MapSkeleton.tsx | 8 +++++++- src/components/Map/ZoomControlSkeleton.tsx | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/components/Map/ZoomControlSkeleton.tsx diff --git a/src/components/Map/MapSkeleton.tsx b/src/components/Map/MapSkeleton.tsx index b8999cd3..f6b73bef 100644 --- a/src/components/Map/MapSkeleton.tsx +++ b/src/components/Map/MapSkeleton.tsx @@ -1,5 +1,11 @@ +import ZoomControlSkeleton from '@/components/Map/ZoomControlSkeleton'; import MapSkeletonData from '@/domain/constant/map/MapSkeletonData'; export default function MapSkeleton() { - return ; + return ( +
      + + +
      + ); } diff --git a/src/components/Map/ZoomControlSkeleton.tsx b/src/components/Map/ZoomControlSkeleton.tsx new file mode 100644 index 00000000..65390e9b --- /dev/null +++ b/src/components/Map/ZoomControlSkeleton.tsx @@ -0,0 +1,9 @@ +import { Skeleton } from '@nextui-org/skeleton'; + +export default function ZoomControlSkeleton() { + return ( +
      + +
      + ); +} From 784cf7acbc81d916cf2b8247f7ab395b998b2d5e Mon Sep 17 00:00:00 2001 From: Georgi Tsipov <76964999+georgi4444@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:41:12 +0100 Subject: [PATCH 04/12] fix: update Topbar responsivness (#123) --- src/components/Topbar/Topbar.tsx | 6 +++--- src/domain/constant/PageLinks.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Topbar/Topbar.tsx b/src/components/Topbar/Topbar.tsx index 0289ab40..2a869196 100644 --- a/src/components/Topbar/Topbar.tsx +++ b/src/components/Topbar/Topbar.tsx @@ -24,15 +24,15 @@ export function Topbar() { return ( - + - + {pageLinks.map((item) => ( - + {item.label} diff --git a/src/domain/constant/PageLinks.ts b/src/domain/constant/PageLinks.ts index 89d932f5..c56eb073 100644 --- a/src/domain/constant/PageLinks.ts +++ b/src/domain/constant/PageLinks.ts @@ -5,4 +5,5 @@ export const pageLinks = [ { href: '/wiki', label: 'Wiki' }, { href: '/disclaimer', label: 'Disclaimer' }, { href: '/download-portal', label: 'Download Portal' }, + { href: '/comparison-portal', label: 'Comparison Portal' }, ]; From d88d7b1bac460657d02b1455af6f4cf93d4f5db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1muel=20Fekete?= Date: Wed, 11 Dec 2024 22:03:38 +0100 Subject: [PATCH 05/12] Remove mapbox, use leaflet everywhere (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: use leaflet for raster layers and background * feat: add disputed areas (without fill) * fix: hover on fcs map * fix: less props thanks to context * fix: fcs colors, merge conflict thingies * feat: Delete mapbox **dancing crabs 🦀🦀🦀** * feat: mouse: pointer on clickable countries * fix: animations, code refactor --- package.json | 2 - src/app/providers.tsx | 21 +- .../AlertsMenu/AlertsMenuWrapper.tsx | 2 +- src/components/Legend/LegendContainer.tsx | 2 +- src/components/Map/Alerts/ConflictLayer.tsx | 1 - src/components/Map/Alerts/HazardLayer.tsx | 1 - src/components/Map/BackToGlobalButton.tsx | 3 +- src/components/Map/FcsChoropleth.tsx | 12 +- src/components/Map/IpcMap/IpcChoropleth.tsx | 16 +- src/components/Map/Map.tsx | 116 ++++--- src/components/Map/NutritionChoropleth.tsx | 8 +- src/components/Map/VectorTileLayer.tsx | 63 ---- src/domain/constant/map/Map.ts | 25 +- src/domain/contexts/SelectedMapContext.tsx | 5 - .../contexts/SelectedMapVisibilityContext.tsx | 30 -- .../entities/country/CountryMimiData.ts | 4 - src/domain/props/FcsChoroplethProps.ts | 2 - src/domain/props/IpcChoroplethProps.tsx | 2 - .../props/IpcCountryChoroplethProps.tsx | 1 - src/domain/props/NutritionChoroplethProps.tsx | 2 - src/domain/props/VectorTileLayerProps.tsx | 7 - src/operations/map/FcsChoroplethOperations.ts | 10 +- .../map/FcsCountryChoroplethOperations.tsx | 4 +- .../map/IpcChoroplethOperations.tsx | 12 +- .../{MapOperations.ts => MapOperations.tsx} | 27 +- src/operations/map/MapboxMapOperations.tsx | 295 ------------------ .../map/NutritionChoroplethOperations.ts | 3 + src/styles/MapColors.ts | 6 +- src/styles/globals.css | 10 +- tailwind.config.js | 9 +- yarn.lock | 210 ------------- 31 files changed, 183 insertions(+), 728 deletions(-) delete mode 100644 src/components/Map/VectorTileLayer.tsx delete mode 100644 src/domain/contexts/SelectedMapVisibilityContext.tsx delete mode 100644 src/domain/props/VectorTileLayerProps.tsx rename src/operations/map/{MapOperations.ts => MapOperations.tsx} (72%) delete mode 100644 src/operations/map/MapboxMapOperations.tsx diff --git a/package.json b/package.json index 7c11e178..d30fba38 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,6 @@ "leaflet-defaulticon-compatibility": "^0.1.2", "leaflet-geosearch": "^4.0.0", "lucide-react": "^0.454.0", - "mapbox-gl": "^3.7.0", - "mapbox-gl-leaflet": "^0.0.16", "next": "14.2.10", "next-themes": "^0.2.1", "nextui-cli": "^0.3.4", diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 2ffa0530..200865a1 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -13,7 +13,6 @@ import { AccordionsModalProvider } from '@/domain/contexts/AccodionsModalContext import { SelectedAlertProvider } from '@/domain/contexts/SelectedAlertContext'; import { SelectedCountryIdProvider } from '@/domain/contexts/SelectedCountryIdContext'; import { SelectedMapProvider } from '@/domain/contexts/SelectedMapContext'; -import { SelectedMapVisibilityProvider } from '@/domain/contexts/SelectedMapVisibilityContext'; import { SidebarProvider } from '@/domain/contexts/SidebarContext'; import { SnackbarProvider } from '@/domain/contexts/SnackbarContext'; @@ -30,17 +29,15 @@ export function Providers({ children, themeProps }: ProvidersProps) { - - - - - - {children} - - - - - + + + + + {children} + + + + diff --git a/src/components/AlertsMenu/AlertsMenuWrapper.tsx b/src/components/AlertsMenu/AlertsMenuWrapper.tsx index b1657739..86695e74 100644 --- a/src/components/AlertsMenu/AlertsMenuWrapper.tsx +++ b/src/components/AlertsMenu/AlertsMenuWrapper.tsx @@ -11,7 +11,7 @@ export function AlertsMenuWrapper() { return null; } return ( -
      +
      ); diff --git a/src/components/Legend/LegendContainer.tsx b/src/components/Legend/LegendContainer.tsx index a4075e02..3348ba7d 100644 --- a/src/components/Legend/LegendContainer.tsx +++ b/src/components/Legend/LegendContainer.tsx @@ -19,7 +19,7 @@ export default function LegendContainer({ items, loading = false }: LegendContai const [showInfoPopup, setInfoPopup] = useState(false); return !isMobile ? ( -
      +
      ( ConflictOperations.createClusterCustomIcon(cluster, conflictType)} showCoverageOnHover={false} diff --git a/src/components/Map/Alerts/HazardLayer.tsx b/src/components/Map/Alerts/HazardLayer.tsx index fdbf5203..532a016a 100644 --- a/src/components/Map/Alerts/HazardLayer.tsx +++ b/src/components/Map/Alerts/HazardLayer.tsx @@ -26,7 +26,6 @@ export function HazardLayer() { zoomToBoundsOnClick maxClusterRadius={60} spiderfyOnMaxZoom={false} - animate={false} > {hazardsByType[hazardType].map((hazard) => ( diff --git a/src/components/Map/BackToGlobalButton.tsx b/src/components/Map/BackToGlobalButton.tsx index 59bb6a18..0b4437af 100644 --- a/src/components/Map/BackToGlobalButton.tsx +++ b/src/components/Map/BackToGlobalButton.tsx @@ -8,14 +8,13 @@ import { useAccordionsModal } from '@/domain/contexts/AccodionsModalContext'; import { useSelectedCountryId } from '@/domain/contexts/SelectedCountryIdContext'; export default function BackToGlobalButton() { - const { selectedCountryId, setSelectedCountryId } = useSelectedCountryId(); + const { selectedCountryId } = useSelectedCountryId(); const { clearAccordionModal } = useAccordionsModal(); const map = useMap(); const handleBackButtonClick = (): void => { map.zoomOut(4); clearAccordionModal(); - setSelectedCountryId(null); }; return selectedCountryId ? ( diff --git a/src/components/Map/FcsChoropleth.tsx b/src/components/Map/FcsChoropleth.tsx index 52b0fb13..ffad7960 100644 --- a/src/components/Map/FcsChoropleth.tsx +++ b/src/components/Map/FcsChoropleth.tsx @@ -4,11 +4,12 @@ import { useTheme } from 'next-themes'; import React, { useEffect, useRef } from 'react'; import { GeoJSON } from 'react-leaflet'; +import { useSelectedCountryId } from '@/domain/contexts/SelectedCountryIdContext'; import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts'; import { LayerWithFeature } from '@/domain/entities/map/LayerWithFeature.ts'; import FcsChoroplethProps from '@/domain/props/FcsChoroplethProps'; import FcsChoroplethOperations from '@/operations/map/FcsChoroplethOperations'; -import { MapboxMapOperations } from '@/operations/map/MapboxMapOperations'; +import { MapOperations } from '@/operations/map/MapOperations'; import CountryLoadingLayer from './CountryLoading'; import FscCountryChoropleth from './FcsCountryChoropleth'; @@ -16,8 +17,6 @@ import FscCountryChoropleth from './FcsCountryChoropleth'; export default function FcsChoropleth({ data, countryId, - selectedCountryId, - setSelectedCountryId, loading, regionData, countryData, @@ -26,6 +25,7 @@ export default function FcsChoropleth({ fcsData, }: FcsChoroplethProps) { const geoJsonRef = useRef(null); + const { selectedCountryId, setSelectedCountryId } = useSelectedCountryId(); const { theme } = useTheme(); const handleBackClick = () => { @@ -38,8 +38,8 @@ export default function FcsChoropleth({ geoJsonRef.current.eachLayer((layer: LayerWithFeature) => { if (!layer) return; const feature = layer.feature as Feature; - if (FcsChoroplethOperations.checkIfActive(data.features[0] as CountryMapData, fcsData)) { - const tooltipContainer = MapboxMapOperations.createCountryNameTooltipElement(feature?.properties?.adm0_name); + if (FcsChoroplethOperations.checkIfActive(feature as CountryMapData, fcsData)) { + const tooltipContainer = MapOperations.createCountryNameTooltipElement(feature?.properties?.adm0_name); layer.bindTooltip(tooltipContainer, { className: 'leaflet-tooltip', sticky: true }); } else { layer.unbindTooltip(); @@ -57,7 +57,7 @@ export default function FcsChoropleth({ data={data} style={FcsChoroplethOperations.countryStyle(data.features[0], theme === 'dark', fcsData)} onEachFeature={(feature, layer) => - FcsChoroplethOperations.onEachFeature(feature, layer, setSelectedCountryId, theme === 'dark', fcsData) + FcsChoroplethOperations.onEachFeature(feature, layer, setSelectedCountryId, fcsData) } /> )} diff --git a/src/components/Map/IpcMap/IpcChoropleth.tsx b/src/components/Map/IpcMap/IpcChoropleth.tsx index 8fc4d444..5dcb627a 100644 --- a/src/components/Map/IpcMap/IpcChoropleth.tsx +++ b/src/components/Map/IpcMap/IpcChoropleth.tsx @@ -2,25 +2,16 @@ import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; import React from 'react'; import CountryLoadingLayer from '@/components/Map/CountryLoading'; +import { useSelectedCountryId } from '@/domain/contexts/SelectedCountryIdContext'; import { useIpcQuery } from '@/domain/hooks/globalHooks'; import { IpcChoroplethProps } from '@/domain/props/IpcChoroplethProps'; import IpcCountryChoropleth from './IpcCountryChoropleth'; import IpcGlobalChoropleth from './IpcGlobalChoropleth'; -function IpcChoropleth({ - countries, - selectedCountryId, - setSelectedCountryId, - countryData, - ipcRegionData, - selectedCountryName, -}: IpcChoroplethProps) { - const handleBackClick = () => { - setSelectedCountryId(null); - }; - +function IpcChoropleth({ countries, countryData, ipcRegionData, selectedCountryName }: IpcChoroplethProps) { const { data: ipcData } = useIpcQuery(true); + const { selectedCountryId, setSelectedCountryId } = useSelectedCountryId(); return ( <> @@ -50,7 +41,6 @@ function IpcChoropleth({ regionIpcData={ipcRegionData} countryData={countryData} countryName={selectedCountryName} - handleBackButtonClick={handleBackClick} /> )} diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index e286f3da..195ebbed 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -3,33 +3,37 @@ import 'leaflet/dist/leaflet.css'; import { Feature, FeatureCollection, GeoJSON, GeoJsonProperties, Geometry } from 'geojson'; import L, { Map as LeafletMap } from 'leaflet'; import { useEffect, useRef, useState } from 'react'; -import { MapContainer } from 'react-leaflet'; +import { GeoJSON as LeafletGeoJSON, MapContainer, Pane, SVGOverlay, TileLayer } from 'react-leaflet'; import BackToGlobalButton from '@/components/Map/BackToGlobalButton'; -import { MAP_MAX_ZOOM, MAP_MIN_ZOOM } from '@/domain/constant/map/Map'; +import { + countryBaseStyle, + countryBorderStyle, + disputedAreaStyle, + MAP_MAX_ZOOM, + MAP_MIN_ZOOM, + oceanBounds, +} from '@/domain/constant/map/Map'; import { useSelectedAlert } from '@/domain/contexts/SelectedAlertContext'; import { useSelectedCountryId } from '@/domain/contexts/SelectedCountryIdContext'; import { useSelectedMap } from '@/domain/contexts/SelectedMapContext'; -import { useSelectedMapVisibility } from '@/domain/contexts/SelectedMapVisibilityContext'; import { useSidebar } from '@/domain/contexts/SidebarContext'; import { CountryData } from '@/domain/entities/country/CountryData.ts'; import { CountryIso3Data } from '@/domain/entities/country/CountryIso3Data.ts'; import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts'; import { GlobalInsight } from '@/domain/enums/GlobalInsight'; import { MapProps } from '@/domain/props/MapProps'; -import { MapOperations } from '@/operations/map/MapOperations.ts'; +import { MapOperations } from '@/operations/map/MapOperations'; import { AlertContainer } from './Alerts/AlertContainer'; import FcsChoropleth from './FcsChoropleth'; import IpcChoropleth from './IpcMap/IpcChoropleth'; import NutritionChoropleth from './NutritionChoropleth'; -import VectorTileLayer from './VectorTileLayer'; import ZoomControl from './ZoomControl'; export default function Map({ countries, disputedAreas, fcsData, alertData }: MapProps) { const mapRef = useRef(null); const { selectedMapType } = useSelectedMap(); - const { setSelectedMapVisibility } = useSelectedMapVisibility(); const { resetAlert } = useSelectedAlert(); const { selectedCountryId, setSelectedCountryId } = useSelectedCountryId(); const { closeSidebar } = useSidebar(); @@ -43,8 +47,6 @@ export default function Map({ countries, disputedAreas, fcsData, alertData }: Ma const [selectedCountryName, setSelectedCountryName] = useState(undefined); const onZoomThresholdReached = () => { - setSelectedCountryId(null); - setSelectedMapVisibility(true); MapOperations.resetSelectedCountryData( setRegionData, setCountryData, @@ -52,13 +54,11 @@ export default function Map({ countries, disputedAreas, fcsData, alertData }: Ma setRegionNutritionData, setIpcRegionData ); + setSelectedCountryId(null); }; useEffect(() => { if (selectedCountryId) { - setSelectedMapVisibility( - selectedMapType === GlobalInsight.VEGETATION || selectedMapType === GlobalInsight.RAINFALL - ); closeSidebar(); resetAlert(); @@ -103,54 +103,92 @@ export default function Map({ countries, disputedAreas, fcsData, alertData }: Ma maxZoom={MAP_MAX_ZOOM} maxBoundsViscosity={1.0} zoomControl={false} - markerZoomAnimation={false} - zoomAnimation={false} style={{ height: '100%', width: '100%', zIndex: 1 }} > - {countries && } - {selectedMapType === GlobalInsight.FOOD && - countries.features && + + + + + + + + + + + + {selectedMapType === GlobalInsight.FOOD && countries.features && ( + <> + {countries.features.map((country) => ( + ] }} + loading={countryClickLoading} + countryData={countryData} + countryIso3Data={countryIso3Data} + regionData={regionData} + selectedCountryName={selectedCountryName} + fcsData={fcsData} + /> + ))} + {!selectedCountryId && ( + + + + )} + + )} + + {selectedMapType === GlobalInsight.NUTRITION && countries.features.map((country) => ( - ] }} - selectedCountryId={selectedCountryId} - setSelectedCountryId={setSelectedCountryId} - loading={countryClickLoading} - countryData={countryData} - countryIso3Data={countryIso3Data} - regionData={regionData} + regionNutritionData={regionNutritionData} selectedCountryName={selectedCountryName} - fcsData={fcsData} /> ))} + {selectedMapType === GlobalInsight.VEGETATION && ( + + + + )} + + {selectedMapType === GlobalInsight.RAINFALL && ( + + + + )} + {selectedMapType === GlobalInsight.IPC && ( )} - {selectedMapType === GlobalInsight.NUTRITION && - countries.features && - countries.features.map((country) => ( - ] }} - selectedCountryId={selectedCountryId} - setSelectedCountryId={setSelectedCountryId} - regionNutritionData={regionNutritionData} - selectedCountryName={selectedCountryName} - /> - ))} + + + + + + + diff --git a/src/components/Map/NutritionChoropleth.tsx b/src/components/Map/NutritionChoropleth.tsx index a6fa9ce1..3dd4d627 100644 --- a/src/components/Map/NutritionChoropleth.tsx +++ b/src/components/Map/NutritionChoropleth.tsx @@ -4,11 +4,12 @@ import { useTheme } from 'next-themes'; import React, { useEffect, useRef } from 'react'; import { GeoJSON } from 'react-leaflet'; +import { useSelectedCountryId } from '@/domain/contexts/SelectedCountryIdContext'; import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts'; import { LayerWithFeature } from '@/domain/entities/map/LayerWithFeature.ts'; import { useNutritionQuery } from '@/domain/hooks/globalHooks'; import NutritionChoroplethProps from '@/domain/props/NutritionChoroplethProps'; -import { MapboxMapOperations } from '@/operations/map/MapboxMapOperations'; +import { MapOperations } from '@/operations/map/MapOperations'; import NutritionChoroplethOperations from '@/operations/map/NutritionChoroplethOperations'; import CountryLoadingLayer from './CountryLoading'; @@ -17,12 +18,11 @@ import NutritionStateChoropleth from './NutritionStateChoropleth'; export default function NutritionChoropleth({ data, countryId, - selectedCountryId, - setSelectedCountryId, regionNutritionData, selectedCountryName, }: NutritionChoroplethProps) { const geoJsonRef = useRef(null); + const { selectedCountryId, setSelectedCountryId } = useSelectedCountryId(); const { theme } = useTheme(); const { data: nutritionData } = useNutritionQuery(true); @@ -34,7 +34,7 @@ export default function NutritionChoropleth({ if (!layer) return; const feature = layer.feature as Feature; if (NutritionChoroplethOperations.checkIfActive(data.features[0] as CountryMapData, nutritionData)) { - const tooltipContainer = MapboxMapOperations.createCountryNameTooltipElement(feature?.properties?.adm0_name); + const tooltipContainer = MapOperations.createCountryNameTooltipElement(feature?.properties?.adm0_name); layer.bindTooltip(tooltipContainer, { className: 'leaflet-tooltip', sticky: true }); } else { layer.unbindTooltip(); diff --git a/src/components/Map/VectorTileLayer.tsx b/src/components/Map/VectorTileLayer.tsx deleted file mode 100644 index 66d13f43..00000000 --- a/src/components/Map/VectorTileLayer.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import 'mapbox-gl/dist/mapbox-gl.css'; - -import { LeafletContextInterface, useLeafletContext } from '@react-leaflet/core'; -import mapboxgl from 'mapbox-gl'; -import { useTheme } from 'next-themes'; -import React, { RefObject, useEffect, useRef, useState } from 'react'; - -import { useSelectedMap } from '@/domain/contexts/SelectedMapContext'; -import { useSelectedMapVisibility } from '@/domain/contexts/SelectedMapVisibilityContext'; -import { VectorTileLayerProps } from '@/domain/props/VectorTileLayerProps'; -import { MapboxMapOperations } from '@/operations/map/MapboxMapOperations'; - -export default function VectorTileLayer({ countries, disputedAreas }: VectorTileLayerProps) { - const { theme } = useTheme(); - const context: LeafletContextInterface = useLeafletContext(); - const mapContainer: RefObject = useRef(null); - const { selectedMapType } = useSelectedMap(); - const [map, setMap] = useState(); - const { selectedMapVisibility } = useSelectedMapVisibility(); - - mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string; - - useEffect(() => { - const baseMap: mapboxgl.Map = MapboxMapOperations.createMapboxMap( - theme === 'dark', - { countries, disputedAreas }, - mapContainer - ); - baseMap.on('load', () => { - setMap(baseMap); - }); - - MapboxMapOperations.synchronizeLeafletMapbox(baseMap, mapContainer, context); - MapboxMapOperations.initDisputedLayer(baseMap); - // The following layers currently don't work due to CORS issues. - MapboxMapOperations.initRainfallLayer(baseMap); - MapboxMapOperations.initVegetationLayer(baseMap); - MapboxMapOperations.initFCSLayer(baseMap); - - return () => { - baseMap.remove(); - context.map.off('move'); - setMap(undefined); - }; - }, [context]); - - useEffect(() => { - if (map && selectedMapVisibility) { - MapboxMapOperations.removeActiveMapLayer(map, theme === 'dark'); - MapboxMapOperations.addMapAsLayer(map, selectedMapType); - } else if (map) { - MapboxMapOperations.removeActiveMapLayer(map, theme === 'dark'); - } - }, [map, selectedMapType, selectedMapVisibility]); - - useEffect(() => { - if (map) { - MapboxMapOperations.changeMapTheme(map, theme === 'dark'); - } - }, [theme]); - - return
      ; -} diff --git a/src/domain/constant/map/Map.ts b/src/domain/constant/map/Map.ts index e74a01ad..8bcea761 100644 --- a/src/domain/constant/map/Map.ts +++ b/src/domain/constant/map/Map.ts @@ -1,2 +1,25 @@ -export const MAP_MAX_ZOOM = 8; +import { LatLngBoundsExpression } from 'leaflet'; + +export const MAP_MAX_ZOOM = 6; export const MAP_MIN_ZOOM = 3; +export const oceanBounds: LatLngBoundsExpression = [ + [-90, -180], + [90, 180], +]; +export const countryBaseStyle = { + fillColor: 'hsl(var(--nextui-countriesBase))', + fillOpacity: 1, + weight: 0, +}; + +export const countryBorderStyle = { + color: 'hsl(var(--nextui-countryBorders))', + weight: 1, + fillOpacity: 0, +}; +export const disputedAreaStyle = { + fillOpacity: 0, + color: 'black', + weight: 1, + dashArray: '5,5', +}; diff --git a/src/domain/contexts/SelectedMapContext.tsx b/src/domain/contexts/SelectedMapContext.tsx index fea9beeb..0a80ddc3 100644 --- a/src/domain/contexts/SelectedMapContext.tsx +++ b/src/domain/contexts/SelectedMapContext.tsx @@ -1,7 +1,6 @@ import { createContext, ReactNode, useContext, useMemo, useState } from 'react'; import { GlobalInsight } from '../enums/GlobalInsight'; -import { useSelectedMapVisibility } from './SelectedMapVisibilityContext'; interface SelectedMapTypeState { selectedMapType: GlobalInsight; @@ -12,11 +11,7 @@ const SelectedMapContext = createContext(undef export function SelectedMapProvider({ children }: { children: ReactNode }) { const [selectedMapType, setSelectedMapTypeState] = useState(GlobalInsight.FOOD); - const { setSelectedMapVisibility } = useSelectedMapVisibility(); const setSelectedMapType = (value: GlobalInsight) => { - if (value !== selectedMapType) { - setSelectedMapVisibility(true); - } setSelectedMapTypeState(value); window.gtag('event', `${value}_map_selected`); }; diff --git a/src/domain/contexts/SelectedMapVisibilityContext.tsx b/src/domain/contexts/SelectedMapVisibilityContext.tsx deleted file mode 100644 index d368d384..00000000 --- a/src/domain/contexts/SelectedMapVisibilityContext.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { createContext, useContext, useMemo, useState } from 'react'; - -interface SelectedMapVisibilityState { - selectedMapVisibility: boolean; - setSelectedMapVisibility: (value: boolean) => void; -} - -const SelectedMapVisibilityContext = createContext(undefined); - -export function SelectedMapVisibilityProvider({ children }: { children: React.ReactNode }) { - const [selectedMapVisibility, setSelectedMapVisibility] = useState(true); - - const value = useMemo( - () => ({ - selectedMapVisibility, - setSelectedMapVisibility, - }), - [selectedMapVisibility] - ); - - return {children}; -} - -export function useSelectedMapVisibility() { - const context = useContext(SelectedMapVisibilityContext); - if (!context) { - throw new Error('useSelectedMapVisibility must be used within a SelectedMapVisibilityProvider'); - } - return context; -} diff --git a/src/domain/entities/country/CountryMimiData.ts b/src/domain/entities/country/CountryMimiData.ts index 2ccab121..dbc4746f 100644 --- a/src/domain/entities/country/CountryMimiData.ts +++ b/src/domain/entities/country/CountryMimiData.ts @@ -1,8 +1,5 @@ -import { LatLngExpression } from 'leaflet'; - import { Feature } from '@/domain/entities/common/Feature.ts'; -import { Geometry } from '../common/Geometry'; import { RegionNutritionProperties } from '../region/RegionNutritionProperties'; export interface CountryMimiData { @@ -13,7 +10,6 @@ export interface CountryMimiData { }; }; features: (Feature & { - geometry: Geometry; id: string; })[]; } diff --git a/src/domain/props/FcsChoroplethProps.ts b/src/domain/props/FcsChoroplethProps.ts index 07d1034d..b9ce9ed8 100644 --- a/src/domain/props/FcsChoroplethProps.ts +++ b/src/domain/props/FcsChoroplethProps.ts @@ -7,8 +7,6 @@ import { CountryIso3Data } from '@/domain/entities/country/CountryIso3Data.ts'; export default interface FcsChoroplethProps { data: FeatureCollection; countryId: number; - selectedCountryId: number | null; - setSelectedCountryId: (countryId: number | null) => void; loading: boolean; regionData?: FeatureCollection; countryData?: CountryData; diff --git a/src/domain/props/IpcChoroplethProps.tsx b/src/domain/props/IpcChoroplethProps.tsx index ce9d47d2..28728fa1 100644 --- a/src/domain/props/IpcChoroplethProps.tsx +++ b/src/domain/props/IpcChoroplethProps.tsx @@ -5,8 +5,6 @@ import { CountryMapDataWrapper } from '@/domain/entities/country/CountryMapData' export interface IpcChoroplethProps { countries: CountryMapDataWrapper; - selectedCountryId: number | null; - setSelectedCountryId: (countryId: number | null) => void; handleBackButtonClick?: () => void; countryData?: CountryData; ipcRegionData?: FeatureCollection; diff --git a/src/domain/props/IpcCountryChoroplethProps.tsx b/src/domain/props/IpcCountryChoroplethProps.tsx index 24e00fd8..4ecd7366 100644 --- a/src/domain/props/IpcCountryChoroplethProps.tsx +++ b/src/domain/props/IpcCountryChoroplethProps.tsx @@ -6,5 +6,4 @@ export default interface IpcCountryChoroplethProps { regionIpcData: FeatureCollection; countryData: CountryData | undefined; countryName?: string; - handleBackButtonClick?: () => void; } diff --git a/src/domain/props/NutritionChoroplethProps.tsx b/src/domain/props/NutritionChoroplethProps.tsx index 2e316ae0..290190d0 100644 --- a/src/domain/props/NutritionChoroplethProps.tsx +++ b/src/domain/props/NutritionChoroplethProps.tsx @@ -3,8 +3,6 @@ import { FeatureCollection } from 'geojson'; export default interface NutritionChoroplethProps { data: FeatureCollection; countryId: number; - selectedCountryId?: number | null; - setSelectedCountryId: (countryId: number | null) => void; regionNutritionData?: FeatureCollection; selectedCountryName?: string; } diff --git a/src/domain/props/VectorTileLayerProps.tsx b/src/domain/props/VectorTileLayerProps.tsx deleted file mode 100644 index 80d38472..00000000 --- a/src/domain/props/VectorTileLayerProps.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { CountryMapDataWrapper } from '../entities/country/CountryMapData'; -import { DisputedAreas } from '../entities/DisputedAreas'; - -export interface VectorTileLayerProps { - countries: CountryMapDataWrapper; - disputedAreas?: DisputedAreas; -} diff --git a/src/operations/map/FcsChoroplethOperations.ts b/src/operations/map/FcsChoroplethOperations.ts index 88dd361d..8acd0760 100644 --- a/src/operations/map/FcsChoroplethOperations.ts +++ b/src/operations/map/FcsChoroplethOperations.ts @@ -3,8 +3,7 @@ import L from 'leaflet'; import { CountryFcsData } from '@/domain/entities/country/CountryFcsData.ts'; import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts'; -import { MapColorsType } from '@/domain/entities/map/MapColorsType'; -import { getColors, inactiveCountryOverlayStyling } from '@/styles/MapColors'; +import { inactiveCountryOverlayStyling } from '@/styles/MapColors'; class FcsChoroplethOperations { static async handleCountryClick( @@ -26,26 +25,27 @@ class FcsChoroplethOperations { feature: Feature, layer: L.Layer, setSelectedCountryId: (countryId: number) => void, - isDark: boolean, fcsData: Record ) { const pathLayer = layer as L.Path; - const mapColors: MapColorsType = getColors(isDark); pathLayer.on({ click: async () => { if (this.checkIfActive(feature as CountryMapData, fcsData)) { FcsChoroplethOperations.handleCountryClick(feature, setSelectedCountryId); + document.getElementsByClassName('leaflet-container').item(0)?.classList.remove('interactive'); } }, mouseover: () => { if (this.checkIfActive(feature as CountryMapData, fcsData)) { - pathLayer.setStyle({ fillOpacity: 0.5, fillColor: mapColors.outline }); + pathLayer.setStyle({ fillOpacity: 0.3, fillColor: 'hsl(var(--nextui-countryHover))' }); + document.getElementsByClassName('leaflet-container').item(0)?.classList.add('interactive'); } }, mouseout: () => { if (this.checkIfActive(feature as CountryMapData, fcsData)) { pathLayer.setStyle({ fillOpacity: 0 }); + document.getElementsByClassName('leaflet-container').item(0)?.classList.remove('interactive'); } }, }); diff --git a/src/operations/map/FcsCountryChoroplethOperations.tsx b/src/operations/map/FcsCountryChoroplethOperations.tsx index 84655019..6fa81171 100644 --- a/src/operations/map/FcsCountryChoroplethOperations.tsx +++ b/src/operations/map/FcsCountryChoroplethOperations.tsx @@ -7,7 +7,7 @@ import FcsRegionTooltip from '@/components/Map/FcsRegionTooltip'; export class FcsCountryChoroplethOperations { static fcsFill(fcs?: number): string { - if (fcs === undefined) return '#2D6092'; + if (fcs === undefined) return 'hsl(var(--nextui-countriesBase))'; if (fcs <= 0.05) return '#29563A'; if (fcs <= 0.1) return '#73B358'; if (fcs <= 0.2) return '#CBCC58'; @@ -19,7 +19,7 @@ export class FcsCountryChoroplethOperations { static styleFunction(feature?: Feature): L.PathOptions { return { fillColor: FcsCountryChoroplethOperations.fcsFill(feature?.properties?.fcs?.score), - color: '#000', + color: 'hsl(var(--nextui-countryBorders))', weight: 1, fillOpacity: 0.6, }; diff --git a/src/operations/map/IpcChoroplethOperations.tsx b/src/operations/map/IpcChoroplethOperations.tsx index e833288e..9a0025bb 100644 --- a/src/operations/map/IpcChoroplethOperations.tsx +++ b/src/operations/map/IpcChoroplethOperations.tsx @@ -14,7 +14,7 @@ export class IpcChoroplethOperations { ipcData: CountryIpcData[], isDark: boolean ) => { - const country = ipcData.find((c) => parseInt(c.adm0_code, 10) === adm0code); + const country = ipcData.find((c) => parseInt(c.adm0_code, 10) === adm0code); // TODO refactor this, this is crazy inefficient, there should never be such an expensive calculation in a render method return feature.properties?.ipcData ? { color: '#000', @@ -37,9 +37,8 @@ export class IpcChoroplethOperations { }; static ipcCountryStyle = (feature: Feature | undefined) => ({ - color: '#fff', - opacity: 0.8, - weight: 0.5, + color: 'hsl(var(--nextui-countryBorders))', + weight: 1, fillOpacity: 1, fillColor: IpcChoroplethOperations.fillCountryIpc(feature?.properties?.ipcPhase), }); @@ -96,13 +95,18 @@ export class IpcChoroplethOperations { layer.on({ click: () => { setSelectedCountryId(feature?.properties?.adm0_id); + document.getElementsByClassName('leaflet-container').item(0)?.classList.remove('interactive'); }, mouseover: () => { pathLayer.setStyle({ ...originalStyle, fillOpacity: 0.7 }); + document.getElementsByClassName('leaflet-container').item(0)?.classList.add('interactive'); + pathLayer.openTooltip(); }, mouseout: () => { pathLayer.setStyle(originalStyle); + document.getElementsByClassName('leaflet-container').item(0)?.classList.remove('interactive'); + pathLayer.closeTooltip(); }, }); diff --git a/src/operations/map/MapOperations.ts b/src/operations/map/MapOperations.tsx similarity index 72% rename from src/operations/map/MapOperations.ts rename to src/operations/map/MapOperations.tsx index fcfad145..cba69e42 100644 --- a/src/operations/map/MapOperations.ts +++ b/src/operations/map/MapOperations.tsx @@ -1,6 +1,9 @@ -import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; +import { Feature as GeoJsonFeature, FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; +import { createRoot } from 'react-dom/client'; +import CountryHoverPopover from '@/components/CountryHoverPopover/CountryHoverPopover'; import container from '@/container'; +import { Feature } from '@/domain/entities/common/Feature'; import { CountryData } from '@/domain/entities/country/CountryData.ts'; import { CountryIso3Data } from '@/domain/entities/country/CountryIso3Data.ts'; import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts'; @@ -27,7 +30,7 @@ export class MapOperations { if (newRegionData && newRegionData.features) { setRegionData({ type: 'FeatureCollection', - features: newRegionData.features as Feature[], + features: newRegionData.features as GeoJsonFeature[], }); } } @@ -38,7 +41,7 @@ export class MapOperations { if (newIpcRegionData && newIpcRegionData.features) { setIpcRegionData({ type: 'FeatureCollection', - features: newIpcRegionData?.features as Feature[], + features: newIpcRegionData?.features as GeoJsonFeature[], }); } } @@ -60,7 +63,7 @@ export class MapOperations { if (newRegionNutritionData && newRegionNutritionData.features) { setRegionNutritionData({ type: 'FeatureCollection', - features: newRegionNutritionData.features as Feature[], + features: newRegionNutritionData.features as GeoJsonFeature[], }); } } @@ -84,4 +87,20 @@ export class MapOperations { setRegionNutritionData(undefined); setIpcRegionData(undefined); } + + static convertCountriesToFeatureCollection = (countryFeatures: Feature[]): FeatureCollection => ({ + type: 'FeatureCollection', + features: countryFeatures as GeoJsonFeature[], + }); + + /** + * Create a 'HTMLDivElement' rending the given 'countryName' within a 'CountryHoverPopover'. + * Needed cause leaflet tooltips do not accept React components. + */ + static createCountryNameTooltipElement(countryName: string): HTMLDivElement { + const tooltipContainer = document.createElement('div'); + const root = createRoot(tooltipContainer); + root.render(); + return tooltipContainer; + } } diff --git a/src/operations/map/MapboxMapOperations.tsx b/src/operations/map/MapboxMapOperations.tsx deleted file mode 100644 index 9942eadb..00000000 --- a/src/operations/map/MapboxMapOperations.tsx +++ /dev/null @@ -1,295 +0,0 @@ -/* eslint-disable */ -import { LeafletContextInterface } from '@react-leaflet/core'; -import { FeatureCollection } from 'geojson'; -import mapboxgl from 'mapbox-gl'; -import React, { RefObject } from 'react'; - -import { MapColorsType } from '@/domain/entities/map/MapColorsType.ts'; -import { GlobalInsight } from '@/domain/enums/GlobalInsight.ts'; -import { getColors } from '@/styles/MapColors.ts'; -import disputedPattern from '../../../public/disputed_pattern.png'; -import { VectorTileLayerProps } from '@/domain/props/VectorTileLayerProps'; -import { createRoot } from 'react-dom/client'; -import CountryHoverPopover from '@/components/CountryHoverPopover/CountryHoverPopover.tsx'; - - -export class MapboxMapOperations { - static createMapboxMap(isDark: boolean, { countries, disputedAreas }: VectorTileLayerProps, mapContainer: RefObject): mapboxgl.Map { - const mapColors: MapColorsType = getColors(isDark); - - return new mapboxgl.Map({ - container: mapContainer.current as unknown as string | HTMLElement, - logoPosition: 'bottom-left', // default which can be changed to 'bottom-right' - style: { - version: 8, - name: 'HungerMap LIVE', - metadata: '{metadata}', - sources: { - countries: { - type: 'geojson', - data: countries as FeatureCollection, - generateId: true, - }, - disputedAreas: { - type: 'geojson', - data: disputedAreas as FeatureCollection, - generateId: true, - }, - mapboxStreets: { - type: 'vector', - url: 'mapbox://mapbox.mapbox-streets-v8', - bounds: [-180, -60, 180, 90], - }, - }, - layers: [ - { - id: 'ocean', - type: 'background', - paint: { 'background-color': mapColors.ocean }, - }, - { - id: 'countries-base', - type: 'fill', - source: 'countries', - paint: { 'fill-color': mapColors.countriesBase }, - }, - // additional layers (FCS, vegetation etc.) are being placed here - { - id: 'country-borders', - type: 'line', - source: 'countries', - filter: ['==', ['get', 'disp_area'], 'NO'], - paint: { - 'line-color': mapColors.outline, - 'line-width': 0.7, - }, - }, - { - id: 'disputed-borders', - type: 'line', - source: 'disputedAreas', - paint: { - 'line-color': mapColors.outline, - 'line-width': 1.5, - 'line-dasharray': [10, 10], - }, - }, - { - id: 'mapbox-roads', - type: 'line', - source: 'mapboxStreets', - 'source-layer': 'road', - filter: ['in', 'class', 'motorway', 'trunk'], - paint: { - 'line-color': mapColors.roads, - 'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 5, 0.5, 18, 10], - }, - minzoom: 5, - }, - ], - }, - interactive: false, - }); - } - - static synchronizeLeafletMapbox( - baseMap: mapboxgl.Map, - mapContainer: RefObject, - context: LeafletContextInterface - ): void { - baseMap.dragRotate.disable(); - - const syncZoom = () => { - baseMap.setZoom(context.map.getZoom() - 1); - baseMap.setMaxZoom(context.map.getMaxZoom() - 1); - baseMap.setMinZoom(context.map.getMinZoom() - 1); - }; - - const container = context.layerContainer || context.map; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const leafletMap = container.getContainer(); - leafletMap.appendChild(mapContainer.current); - - baseMap.setZoom(context.map.getZoom()); - baseMap.setMaxZoom(context.map.getMaxZoom() - 1); - baseMap.setMinZoom(context.map.getMinZoom() - 1); - - const { lat, lng } = context.map.getCenter(); - baseMap.setCenter([lng, lat]); - baseMap.setZoom(context.map.getZoom() - 1); - - context.map.on('move', () => { - const { lat: moveLat, lng: moveLng } = context.map.getCenter(); - baseMap.setCenter([moveLng, moveLat]); - syncZoom(); - }); - - context.map.on('zoom', () => { - const { lat: zoomLat, lng: zoomLng } = context.map.getCenter(); - baseMap.setCenter([zoomLng, zoomLat]); - syncZoom(); - }); - - context.map.on('movestart', () => { - const { lat: moveStartLat, lng: moveStartLng } = context.map.getCenter(); - baseMap.setCenter([moveStartLng, moveStartLat]); - syncZoom(); - }); - - context.map.on('zoomstart', () => { - syncZoom(); - }); - - context.map.on('moveend', () => { - const { lat: moveEndLat, lng: moveEndLng } = context.map.getCenter(); - baseMap.setCenter([moveEndLng, moveEndLat]); - syncZoom(); - }); - - context.map.on('zoomend', () => { - const { lat: zoomEndLat, lng: zoomEndLng } = context.map.getCenter(); - baseMap.setCenter([zoomEndLng, zoomEndLat]); - syncZoom(); - }); - } - - static changeMapTheme(baseMap: mapboxgl.Map, isDark: boolean) { - const mapColors: MapColorsType = getColors(isDark); - baseMap.setPaintProperty('ocean', 'background-color', mapColors.ocean); - baseMap.setPaintProperty('countries-base', 'fill-color', mapColors.countriesBase); - baseMap.setPaintProperty('countries-inactive', 'fill-color', mapColors.inactiveCountriesOverlay); - baseMap.setPaintProperty('countries-hover', 'fill-color', mapColors.outline); - baseMap.setPaintProperty('country-borders', 'line-color', mapColors.outline); - baseMap.setPaintProperty('disputed-borders', 'line-color', mapColors.outline); - baseMap.setPaintProperty('mapbox-roads', 'line-color', mapColors.roads); - } - - static initDisputedLayer(baseMap: mapboxgl.Map) { - baseMap.on('load', () => { - baseMap.loadImage(disputedPattern.src, (error, image) => { - if (error) throw error; - baseMap.addImage('diagonal-stripe-pattern', image!, { pixelRatio: 8 }); - baseMap.addLayer({ - id: 'disputed-area-pattern', - type: 'fill', - source: 'disputedAreas', - paint: { - 'fill-pattern': 'diagonal-stripe-pattern', - }, - }); - }); - }); - } - - static FCS_RASTER = 'fcsRaster'; - - static FCS_LAYER = 'fcsLayer'; - - static RAINFALL_RASTER = 'rainfallRaster'; - - static RAINFALL_LAYER = 'rainfallLayer'; - - static VEGETATION_RASTER = 'vegetationRaster'; - - static VEGETATION_LAYER = 'vegetationLayer'; - - static initFCSLayer(baseMap: mapboxgl.Map) { - baseMap.on('load', () => { - baseMap.addSource(this.FCS_RASTER, { - type: 'raster', - tiles: ['https://static.hungermapdata.org/proteus_tiles/{z}/{x}/{y}.png'], - tileSize: 256, - scheme: 'tms', - maxzoom: 6 - }); - }); - } - - static initRainfallLayer(baseMap: mapboxgl.Map) { - baseMap.on('load', () => { - baseMap.addSource(this.RAINFALL_RASTER, { - type: 'raster', - tiles: [`https://dev.api.earthobservation.vam.wfp.org/tiles/latest/r3q_dekad/{z}/{x}/{y}.png`], - tileSize: 256, - scheme: 'xyz', - maxzoom: 7, - bounds: [-180, -49, 180, 49], - }); - }); - } - - static initVegetationLayer(baseMap: mapboxgl.Map) { - baseMap.on('load', () => { - baseMap.addSource(this.VEGETATION_RASTER, { - type: 'raster', - tiles: [`https://dev.api.earthobservation.vam.wfp.org/tiles/latest/viq_dekad/{z}/{x}/{y}.png`], - tileSize: 256, - scheme: 'xyz', - maxzoom: 7, - bounds: [-180, -60, 180, 80], - }); - }); - } - - static addMapAsLayer(baseMap: mapboxgl.Map, selectedMapType: GlobalInsight) { - switch (selectedMapType) { - case GlobalInsight.FOOD: - baseMap.addLayer( - { - id: this.FCS_LAYER, - type: 'raster', - source: this.FCS_RASTER, - }, - 'country-borders' - ); - break; - case GlobalInsight.VEGETATION: - baseMap.addLayer( - { - id: this.VEGETATION_LAYER, - type: 'raster', - source: this.VEGETATION_RASTER, - }, - 'country-borders' - ); - break; - case GlobalInsight.RAINFALL: - baseMap.addLayer( - { - id: this.RAINFALL_LAYER, - type: 'raster', - source: this.RAINFALL_RASTER, - }, - 'country-borders' - ); - break; - default: - } - } - - static removeActiveMapLayer(baseMap: mapboxgl.Map, isDark: boolean) { - const mapColors: MapColorsType = getColors(isDark); - const layers = baseMap.getStyle()?.layers; - if (!layers) return; - const layerToRemove = layers.find((layer) => - [this.FCS_LAYER, this.VEGETATION_LAYER, this.RAINFALL_LAYER].includes(layer.id) - ); - if (!layerToRemove) { - baseMap.setPaintProperty('countries-base', 'fill-color', mapColors.countriesBase); - return; - } - baseMap.removeLayer(layerToRemove.id); - } - - /** - * Create a 'HTMLDivElement' rending the given 'countryName' within a 'CountryHoverPopover'. - * Needed cause leaflet tooltips or mapbox popups does not accept React components. - */ - static createCountryNameTooltipElement(countryName: string): HTMLDivElement { - const tooltipContainer = document.createElement('div'); - const root = createRoot(tooltipContainer); - root.render(); - return tooltipContainer; - } -} diff --git a/src/operations/map/NutritionChoroplethOperations.ts b/src/operations/map/NutritionChoroplethOperations.ts index b235648f..39567292 100644 --- a/src/operations/map/NutritionChoroplethOperations.ts +++ b/src/operations/map/NutritionChoroplethOperations.ts @@ -40,6 +40,7 @@ export default class NutritionChoroplethOperations { if (this.checkIfActive(feature, nutritionData)) { NutritionChoroplethOperations.handleCountryClick(feature, setSelectedCountryId); } + document.getElementsByClassName('leaflet-container').item(0)?.classList.remove('interactive'); }, }); layer.on('mouseover', () => { @@ -47,6 +48,7 @@ export default class NutritionChoroplethOperations { pathLayer.setStyle({ fillOpacity: 0.8, }); + document.getElementsByClassName('leaflet-container').item(0)?.classList.add('interactive'); } }); pathLayer.on('mouseout', () => { @@ -54,6 +56,7 @@ export default class NutritionChoroplethOperations { pathLayer.setStyle({ fillOpacity: 0.5, }); + document.getElementsByClassName('leaflet-container').item(0)?.classList.remove('interactive'); } }); } diff --git a/src/styles/MapColors.ts b/src/styles/MapColors.ts index 5552e2fc..99ccc8e5 100644 --- a/src/styles/MapColors.ts +++ b/src/styles/MapColors.ts @@ -1,15 +1,15 @@ import { MapColorsType } from '@/domain/entities/map/MapColorsType.ts'; export const getColors = (isDark: boolean): MapColorsType => ({ - countriesBase: isDark ? '#0e6397' : '#fefeff', + countriesBase: isDark ? '#002129' : '#fefeff', inactiveCountriesOverlay: isDark ? '#a69f9f' : '#d2d1d1', - ocean: isDark ? '#111111' : '#91cccb', + ocean: isDark ? '#002a38' : '#91cccb', outline: isDark ? '#0e2a3a' : '#306f96', roads: isDark ? '#404040' : '#808080', }); export const inactiveCountryOverlayStyling = (isDark: boolean) => ({ color: getColors(isDark).inactiveCountriesOverlay, - fillOpacity: 0.5, + fillOpacity: isDark ? 0.2 : 0.5, stroke: false, }); diff --git a/src/styles/globals.css b/src/styles/globals.css index be83f7fd..38d903ab 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -83,7 +83,7 @@ path.leaflet-interactive:focus { font-size: 0.875rem !important; line-height: 1.25rem !important; } - /* shrink mapbox-popup tip triangle */ +/* shrink mapbox-popup tip triangle */ .mapbox-popup-transparent .mapboxgl-popup-tip { width: 6px !important; height: 12px !important; @@ -126,6 +126,10 @@ path.leaflet-interactive:focus { scrollbar-color: theme('colors.content3') transparent; } -.leaflet-interactive:hover { - cursor: default !important; +.leaflet-container .leaflet-interactive { + cursor: default; +} + +.leaflet-container.interactive .leaflet-interactive { + cursor: pointer; } diff --git a/tailwind.config.js b/tailwind.config.js index 6ee304ac..6d5b7014 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -215,10 +215,12 @@ const config = { //animation nutritionAnimation: '#F7B750', - fcsAnimation: '#338ef7', + fcsAnimation: '#157dbc', ipcAnimation: '#cd1919', countriesBase: '#fefeff', + countryBorders: '#6f7475', + countryHover: '#8a8a8a', ocean: '#91cccb', fatalityAlert: '#742280', climateWetAlert: '#4295D3', @@ -267,8 +269,8 @@ const config = { conflictCivil: '#96badc', conflictExplosion: '#eaaf75', conflictStrategic: '#bec0c1', - countriesBase: '#0e6397', - ocean: '#111111', + countriesBase: '#002129', + ocean: '#002a38', fatalityAlert: '#742280', climateWetAlert: '#4295D3', climateDryAlert: '#B95926', @@ -277,6 +279,7 @@ const config = { chartsGridLine: '#424242', chartForecast: '#0e6983', nutritionNotAnalyzed: '#A69F9F', + fcsAnimation: '#014a5e', }, }, }, diff --git a/yarn.lock b/yarn.lock index 7bcb548d..29686b62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1240,16 +1240,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@mapbox/jsonlint-lines-primitives@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" - integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== - -"@mapbox/mapbox-gl-supported@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz#bebd3d5da3c1fd988011bb79718a39f63f5e16ac" - integrity sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg== - "@mapbox/node-pre-gyp@^1.0.0": version "1.0.11" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" @@ -1265,33 +1255,6 @@ semver "^7.3.5" tar "^6.1.11" -"@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" - integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ== - -"@mapbox/tiny-sdf@^2.0.6": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz#9a1d33e5018093e88f6a4df2343e886056287282" - integrity sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA== - -"@mapbox/unitbezier@^0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz#d32deb66c7177e9e9dfc3bbd697083e2e657ff01" - integrity sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw== - -"@mapbox/vector-tile@^1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz#d3a74c90402d06e89ec66de49ec817ff53409666" - integrity sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw== - dependencies: - "@mapbox/point-geometry" "~0.1.0" - -"@mapbox/whoots-js@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe" - integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q== - "@next/env@14.2.10": version "14.2.10" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.10.tgz#1d3178340028ced2d679f84140877db4f420333c" @@ -5283,13 +5246,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== -"@types/geojson-vt@^3.2.5": - version "3.2.5" - resolved "https://registry.yarnpkg.com/@types/geojson-vt/-/geojson-vt-3.2.5.tgz#b6c356874991d9ab4207533476dfbcdb21e38408" - integrity sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g== - dependencies: - "@types/geojson" "*" - "@types/geojson@*", "@types/geojson@^7946.0.10", "@types/geojson@^7946.0.14": version "7946.0.14" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" @@ -5338,20 +5294,6 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb" integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg== -"@types/mapbox__point-geometry@*", "@types/mapbox__point-geometry@^0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz#0ef017b75eedce02ff6243b4189210e2e6d5e56d" - integrity sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA== - -"@types/mapbox__vector-tile@^1.3.4": - version "1.3.4" - resolved "https://registry.yarnpkg.com/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz#ad757441ef1d34628d9e098afd9c91423c1f8734" - integrity sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg== - dependencies: - "@types/geojson" "*" - "@types/mapbox__point-geometry" "*" - "@types/pbf" "*" - "@types/mdast@^4.0.0": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" @@ -5376,11 +5318,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.7.tgz#4b8ecac87fbefbc92f431d09c30e176fc0a7c377" integrity sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA== -"@types/pbf@*", "@types/pbf@^3.0.5": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.5.tgz#a9495a58d8c75be4ffe9a0bd749a307715c07404" - integrity sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA== - "@types/prop-types@*", "@types/prop-types@^15.7.13": version "15.7.13" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" @@ -5408,13 +5345,6 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/supercluster@^7.1.3": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/supercluster/-/supercluster-7.1.3.tgz#1a1bc2401b09174d9c9e44124931ec7874a72b27" - integrity sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA== - dependencies: - "@types/geojson" "*" - "@types/tinycolor2@^1.4.0": version "1.4.6" resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz#670cbc0caf4e58dd61d1e3a6f26386e473087f06" @@ -5943,11 +5873,6 @@ character-reference-invalid@^2.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== -cheap-ruler@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cheap-ruler/-/cheap-ruler-4.0.0.tgz#bdc984de7e0e3f748bdfd2dbe23ec6b9dc820a09" - integrity sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw== - chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -6209,11 +6134,6 @@ css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -csscolorparser@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" - integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== - cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -6447,11 +6367,6 @@ earcut@^2.2.4: resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== -earcut@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/earcut/-/earcut-3.0.0.tgz#a8d5bf891224eaea8287201b5e787c6c0318af89" - integrity sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg== - eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -7109,11 +7024,6 @@ geojson-polygon-self-intersections@^1.2.1: dependencies: rbush "^2.0.1" -geojson-vt@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-4.0.2.tgz#1162f6c7d61a0ba305b1030621e6e111f847828a" - integrity sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A== - geojson@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0" @@ -7170,11 +7080,6 @@ git-raw-commits@^4.0.0: meow "^12.0.1" split2 "^4.0.0" -gl-matrix@^3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.4.3.tgz#fc1191e8320009fd4d20e9339595c6041ddc22c9" - integrity sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA== - glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -7265,11 +7170,6 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -grid-index@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/grid-index/-/grid-index-1.1.0.tgz#97f8221edec1026c8377b86446a7c71e79522ea7" - integrity sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA== - has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -7393,11 +7293,6 @@ iconsax-react@^0.0.8: dependencies: prop-types "^15.7.2" -ieee754@^1.1.12: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ignore@^5.2.0, ignore@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -7840,11 +7735,6 @@ jsts@2.7.1: object.assign "^4.1.4" object.values "^1.1.6" -kdbush@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-4.0.2.tgz#2f7b7246328b4657dd122b6c7f025fbc2c868e39" - integrity sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA== - keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -8074,45 +7964,6 @@ make-event-props@^1.6.0: resolved "https://registry.yarnpkg.com/make-event-props/-/make-event-props-1.6.2.tgz#c8e0e48eb28b9b808730de38359f6341de7ec5a2" integrity sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA== -mapbox-gl-leaflet@^0.0.16: - version "0.0.16" - resolved "https://registry.yarnpkg.com/mapbox-gl-leaflet/-/mapbox-gl-leaflet-0.0.16.tgz#25208897bbffbe8e2c1f15d3710f253c19a0a533" - integrity sha512-w4bpZrKHOWDZqUzhDOjIPL6Pc4tD10TVR/z8Iwp3hlUaf8PVqfxPINrcBLkcOg0+xFZSX3uka6Vl6NeO7KUYXw== - -mapbox-gl@^3.7.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-3.8.0.tgz#81330a033e34b8a3b62f01c3f5d853e2307ef755" - integrity sha512-7iQ6wxAf8UedbNYTzNsyr2J25ozIBA4vmKY0xUDXQlHEokulzPENwjjmLxHQGRylDpOmR0c8kPEbtHCaQE2eMw== - dependencies: - "@mapbox/jsonlint-lines-primitives" "^2.0.2" - "@mapbox/mapbox-gl-supported" "^3.0.0" - "@mapbox/point-geometry" "^0.1.0" - "@mapbox/tiny-sdf" "^2.0.6" - "@mapbox/unitbezier" "^0.0.1" - "@mapbox/vector-tile" "^1.3.1" - "@mapbox/whoots-js" "^3.1.0" - "@types/geojson" "^7946.0.14" - "@types/geojson-vt" "^3.2.5" - "@types/mapbox__point-geometry" "^0.1.4" - "@types/mapbox__vector-tile" "^1.3.4" - "@types/pbf" "^3.0.5" - "@types/supercluster" "^7.1.3" - cheap-ruler "^4.0.0" - csscolorparser "~1.0.3" - earcut "^3.0.0" - geojson-vt "^4.0.2" - gl-matrix "^3.4.3" - grid-index "^1.1.0" - kdbush "^4.0.2" - murmurhash-js "^1.0.0" - pbf "^3.2.1" - potpack "^2.0.0" - quickselect "^3.0.0" - serialize-to-js "^3.1.2" - supercluster "^8.0.1" - tinyqueue "^3.0.0" - vt-pbf "^3.1.3" - marchingsquares@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/marchingsquares/-/marchingsquares-1.3.3.tgz#67404af4b883ade3a589221f4e9dd010a1f706fc" @@ -8690,11 +8541,6 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -murmurhash-js@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" - integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw== - mz@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -9038,14 +8884,6 @@ pathe@1.1.2: resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== -pbf@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.3.0.tgz#1790f3d99118333cc7f498de816028a346ef367f" - integrity sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q== - dependencies: - ieee754 "^1.1.12" - resolve-protobuf-schema "^2.1.0" - pdfjs-dist@4.4.168: version "4.4.168" resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz#4487716376a33c68753ed37f782ae91d1c9ef8fa" @@ -9178,11 +9016,6 @@ postcss@^8.4.23: picocolors "^1.1.1" source-map-js "^1.2.1" -potpack@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/potpack/-/potpack-2.0.0.tgz#61f4dd2dc4b3d5e996e3698c0ec9426d0e169104" - integrity sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -9214,11 +9047,6 @@ property-information@^6.0.0: resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== -protocol-buffers-schema@^3.3.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" - integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== - punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -9239,11 +9067,6 @@ quickselect@^2.0.0: resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== -quickselect@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-3.0.0.tgz#a37fc953867d56f095a20ac71c6d27063d2de603" - integrity sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g== - rbush@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/rbush/-/rbush-2.0.2.tgz#bb6005c2731b7ba1d5a9a035772927d16a614605" @@ -9551,13 +9374,6 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve-protobuf-schema@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" - integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ== - dependencies: - protocol-buffers-schema "^3.3.1" - resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.2, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" @@ -9675,11 +9491,6 @@ semver@^7.3.5, semver@^7.6.0, semver@^7.6.3: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== -serialize-to-js@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/serialize-to-js/-/serialize-to-js-3.1.2.tgz#844b8a1c2d72412f68ea30da55090b3fc8e95790" - integrity sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w== - set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -9980,13 +9791,6 @@ sucrase@^3.32.0: pirates "^4.0.1" ts-interface-checker "^0.1.9" -supercluster@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-8.0.1.tgz#9946ba123538e9e9ab15de472531f604e7372df5" - integrity sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ== - dependencies: - kdbush "^4.0.2" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -10146,11 +9950,6 @@ tinyqueue@^2.0.0, tinyqueue@^2.0.3: resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== -tinyqueue@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-3.0.0.tgz#101ea761ccc81f979e29200929e78f1556e3661e" - integrity sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -10450,15 +10249,6 @@ vfile@^6.0.0: "@types/unist" "^3.0.0" vfile-message "^4.0.0" -vt-pbf@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac" - integrity sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA== - dependencies: - "@mapbox/point-geometry" "0.1.0" - "@mapbox/vector-tile" "^1.3.1" - pbf "^3.2.1" - warning@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" From 92497604163d196741439fcb1980fa5d2416dd8f Mon Sep 17 00:00:00 2001 From: Linus <126587385+plaume8@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:26:54 +0100 Subject: [PATCH 06/12] Feature/f 155 charts download button should be available on minimized (#121) * feat: chart download button to own componentn - and added button to LineChart component * fix: added colors to LineChartOperations as hex - fixing the chart png and svg download * fix: using tailwind-util.getTailwindColor to use nextui colors for highcharts options - fixes the chart export color problem --------- Co-authored-by: Bohdan Garchu --- src/app/elements/page.tsx | 2 +- src/components/Charts/LineChart.tsx | 55 +++++---- src/components/Charts/LineChartModal.tsx | 106 +++--------------- .../helpers/LineChartDownloadButton.tsx | 72 ++++++++++++ .../Charts/helpers/LineChartXAxisSlider.tsx | 4 +- src/domain/props/LineChartModalProps.tsx | 1 + src/domain/props/LineChartProps.tsx | 11 +- src/operations/charts/LineChartOperations.ts | 46 ++++---- src/utils/tailwind-util.ts | 5 +- tailwind.config.js | 14 ++- 10 files changed, 166 insertions(+), 150 deletions(-) create mode 100644 src/components/Charts/helpers/LineChartDownloadButton.tsx diff --git a/src/app/elements/page.tsx b/src/app/elements/page.tsx index 5c85d07a..e29a13ee 100644 --- a/src/app/elements/page.tsx +++ b/src/app/elements/page.tsx @@ -205,7 +205,7 @@ export default async function Elements() {
      - +
      (null); + // handling the line and bar chart switch and the theme switch; // also handling changing the x-axis range using the `LineChartXAxisSlider`; // special: if the selected x-axis range has length 1 -> bar chart is displayed @@ -121,27 +118,24 @@ export function LineChart({
      - { - // button to hide/show the slider to manipulate the plotted x-axis range of the chart; - // can be disabled via `xAxisSlider` - xAxisSlider && ( - - ) - } - { - // button to switch between line and bar chart; can be disabled via `barChartSwitch` - barChartSwitch && ( - - ) - } + {xAxisSlider && ( + + )} + + {barChartSwitch && ( + + )} + + {!disableDownload && } + { // button to trigger the full screen modal; rendered if `expandable` expandable && ( @@ -166,6 +160,7 @@ export function LineChart({ (null); // full screen modal by the 'LineChart' component that can be opened if `expandable==true`; @@ -66,84 +55,19 @@ export function LineChartModal({

      {title}

      - { - // button to hide/show the slider to manipulate the plotted x-axis range of the chart; - // can be disabled via `xAxisSlider` - xAxisSlider && ( - - ) - } - { - // button to switch between line and bar chart; can be disabled via `barChartSwitch` - barChartSwitch && ( - - ) - } + {xAxisSlider && ( + + )} + + {barChartSwitch && ( + + )} - {/* chart download dropdown */} - - - - - - - - - - - + {!disableDownload && } {/* close model button */} diff --git a/src/components/Charts/helpers/LineChartDownloadButton.tsx b/src/components/Charts/helpers/LineChartDownloadButton.tsx new file mode 100644 index 00000000..777acec0 --- /dev/null +++ b/src/components/Charts/helpers/LineChartDownloadButton.tsx @@ -0,0 +1,72 @@ +import { Button } from '@nextui-org/button'; +import { Popover, PopoverContent, PopoverTrigger } from '@nextui-org/popover'; +import { DocumentDownload, GalleryImport } from 'iconsax-react'; + +import { Tooltip } from '@/components/Tooltip/Tooltip'; +import { LineChartDownloadButtonProps } from '@/domain/props/LineChartProps'; +import LineChartOperations from '@/operations/charts/LineChartOperations.ts'; + +/** + * This component is tied to the `LineChart` and `LineChartModal` component + * and should not be used independently. + * It renders a button to open a dropdown menu to download the chart as csv, png, etc. + */ +export default function LineChartDownloadButton({ chartRef, lineChartData }: LineChartDownloadButtonProps) { + return ( + + + + + + + + + + + + ); +} diff --git a/src/components/Charts/helpers/LineChartXAxisSlider.tsx b/src/components/Charts/helpers/LineChartXAxisSlider.tsx index fee2ee3d..90d29448 100644 --- a/src/components/Charts/helpers/LineChartXAxisSlider.tsx +++ b/src/components/Charts/helpers/LineChartXAxisSlider.tsx @@ -1,6 +1,6 @@ import { Slider } from '@nextui-org/slider'; -import { LineChartXAxisSlider } from '@/domain/props/LineChartProps'; +import { LineChartXAxisSliderProps } from '@/domain/props/LineChartProps'; import LineChartOperations from '@/operations/charts/LineChartOperations.ts'; /** @@ -13,7 +13,7 @@ export default function LineChartXAxisSlider({ selectedXAxisRange, setSelectedXAxisRange, data, -}: LineChartXAxisSlider) { +}: LineChartXAxisSliderProps) { const xAxisValues: number[] = LineChartOperations.getDistinctXAxisValues(data); return ( diff --git a/src/domain/props/LineChartModalProps.tsx b/src/domain/props/LineChartModalProps.tsx index 71390639..cc5dda41 100644 --- a/src/domain/props/LineChartModalProps.tsx +++ b/src/domain/props/LineChartModalProps.tsx @@ -5,6 +5,7 @@ import { LineChartData } from '@/domain/entities/charts/LineChartData.ts'; export default interface LineChartModalProps { title?: string; description?: string; + disableDownload?: boolean; barChartSwitch?: boolean; xAxisSlider?: boolean; diff --git a/src/domain/props/LineChartProps.tsx b/src/domain/props/LineChartProps.tsx index 61f07c1a..4211ce02 100644 --- a/src/domain/props/LineChartProps.tsx +++ b/src/domain/props/LineChartProps.tsx @@ -1,3 +1,6 @@ +import HighchartsReact from 'highcharts-react-official'; +import { MutableRefObject } from 'react'; + import { BalanceOfTradeGraph } from '@/domain/entities/charts/BalanceOfTradeGraph.ts'; import { CurrencyExchangeGraph } from '@/domain/entities/charts/CurrencyExchangeGraph.ts'; import { InflationGraphs } from '@/domain/entities/charts/InflationGraphs.ts'; @@ -10,6 +13,7 @@ export default interface LineChartProps { barChartSwitch?: boolean; xAxisSlider?: boolean; small?: boolean; + disableDownload?: boolean; roundLines?: boolean; noPadding?: boolean; transparentBackground?: boolean; @@ -32,8 +36,13 @@ export interface LineChartBarLineSwitchButtonProps { size: number; } -export interface LineChartXAxisSlider { +export interface LineChartXAxisSliderProps { selectedXAxisRange: number[]; // [xAxisRangeMinIndex, xAxisRangeMaxIndex] setSelectedXAxisRange: (ns: number[]) => void; data: LineChartData; } + +export interface LineChartDownloadButtonProps { + chartRef: MutableRefObject; + lineChartData: LineChartData; +} diff --git a/src/operations/charts/LineChartOperations.ts b/src/operations/charts/LineChartOperations.ts index ba7bb7ef..03f9cc3e 100644 --- a/src/operations/charts/LineChartOperations.ts +++ b/src/operations/charts/LineChartOperations.ts @@ -7,6 +7,9 @@ import Highcharts, { TooltipFormatterContextObject, } from 'highcharts'; import highchartsMore from 'highcharts/highcharts-more'; +import ExportData from 'highcharts/modules/export-data'; +import Exporting from 'highcharts/modules/exporting'; +import OfflineExporting from 'highcharts/modules/offline-exporting'; import patternFill from 'highcharts/modules/pattern-fill'; import HighchartsReact from 'highcharts-react-official'; @@ -16,12 +19,16 @@ import { InflationGraphs } from '@/domain/entities/charts/InflationGraphs.ts'; import { LineChartData } from '@/domain/entities/charts/LineChartData.ts'; import { LineChartDataType } from '@/domain/enums/LineChartDataType.ts'; import { formatToMillion } from '@/utils/formatting.ts'; +import { getTailwindColor } from '@/utils/tailwind-util.ts'; +// initialize the exporting module if (typeof Highcharts === 'object') { highchartsMore(Highcharts); patternFill(Highcharts); + Exporting(Highcharts); + ExportData(Highcharts); + OfflineExporting(Highcharts); } - /** * Using LineChartOperations, the LineChart component can convert its received data into LineChartData * and then generate the chart `Highcharts.Options` object required by the Highcharts component. @@ -45,10 +52,10 @@ export default class LineChartOperations { */ private static getLineColorList() { return [ - 'hsl(var(--nextui-clusterRed))', - 'hsl(var(--nextui-clusterGreen))', - 'hsl(var(--nextui-clusterBlue))', - 'hsl(var(--nextui-clusterOrange))', + getTailwindColor('--nextui-clusterRed'), + getTailwindColor('--nextui-clusterGreen'), + getTailwindColor('--nextui-clusterBlue'), + getTailwindColor('--nextui-clusterOrange'), ]; } @@ -70,7 +77,7 @@ export default class LineChartOperations { if (p.point.options.y) { tooltip += `
      \u25CF
      ${p.point.options.y}
      `; } else if (p.point.options.high !== undefined && p.point.options.low !== undefined) { - tooltip += `
      (
      ${p.point.options.low} - ${p.point.options.high}
      )
      `; + tooltip += `
      (
      ${p.point.options.low} - ${p.point.options.high}
      )
      `; } }); return tooltip; @@ -182,6 +189,7 @@ export default class LineChartOperations { * can be manipulated; important: if a min is defined a max must be defined as well and vice versa. * * @param data `LineChartData` object, containing all data to be plotted in the chart + * @param isDark - `true` if dark mode is selected * @param roundLines if true, all plotted lines will be rounded * @param barChart if true, bars are plotted instead of lines * @param xAxisSelectedMinIdx index of selected x-axis range min value @@ -213,7 +221,7 @@ export default class LineChartOperations { if (lineData.color) { categoryColor = lineData.color; } else if (lineData.prediction) { - categoryColor = 'hsl(var(--nextui-chartForecast))'; + categoryColor = getTailwindColor('--nextui-chartForecast'); } else { categoryColor = defaultLineColors.pop(); } @@ -346,14 +354,14 @@ export default class LineChartOperations { label: { text: b.label || '', style: { - color: 'hsl(var(--nextui-secondary))', + color: getTailwindColor('--nextui-secondary'), fontSize: '0.7rem', }, }, })); const plotLines = verticalLines.map((l) => ({ value: l.x, - color: l.color || 'hsl(var(--nextui-chartsGridLine))', + color: l.color || getTailwindColor('--nextui-chartsGridLine'), dashStyle: l.dashStyle, zIndex: 2, })); @@ -369,25 +377,25 @@ export default class LineChartOperations { legend: { itemStyle: { fontSize: '0.7rem', - color: 'hsl(var(--nextui-secondary))', + color: getTailwindColor('--nextui-secondary'), }, itemHoverStyle: { - color: 'hsl(var(--nextui-hover))', + color: getTailwindColor('--nextui-hover'), }, }, xAxis: { type: data.xAxisType, labels: { style: { - color: 'hsl(var(--nextui-secondary))', + color: getTailwindColor('--nextui-secondary'), fontSize: '0.7rem', }, formatter() { return LineChartOperations.chartXAxisFormatter(data.xAxisType, this.value); }, }, - lineColor: 'hsl(var(--nextui-chartsXAxisLine))', - tickColor: 'hsl(var(--nextui-chartsXAxisLine))', + lineColor: getTailwindColor('--nextui-chartsXAxisLine'), + tickColor: getTailwindColor('--nextui-chartsXAxisLine'), tickLength: 4, plotBands, plotLines, @@ -396,12 +404,12 @@ export default class LineChartOperations { title: { text: data.yAxisLabel, style: { - color: 'hsl(var(--nextui-secondary))', + color: getTailwindColor('--nextui-secondary'), }, }, labels: { style: { - color: 'hsl(var(--nextui-secondary))', + color: getTailwindColor('--nextui-secondary'), fontSize: '0.7rem', }, formatter() { @@ -409,16 +417,16 @@ export default class LineChartOperations { }, }, lineColor: 'transparent', - gridLineColor: 'hsl(var(--nextui-chartsGridLine))', + gridLineColor: getTailwindColor('--nextui-chartsGridLine'), }, tooltip: { shared: true, formatter() { return LineChartOperations.chartTooltipFormatter(data.xAxisType, this.x, this.points); }, - backgroundColor: 'hsl(var(--nextui-chartsLegendBackground))', + backgroundColor: getTailwindColor('--nextui-chartsLegendBackground'), style: { - color: 'hsl(var(--nextui-foreground))', + color: getTailwindColor('--nextui-foreground'), fontSize: '0.7rem', }, }, diff --git a/src/utils/tailwind-util.ts b/src/utils/tailwind-util.ts index 2ee0a864..97c0f434 100644 --- a/src/utils/tailwind-util.ts +++ b/src/utils/tailwind-util.ts @@ -3,7 +3,10 @@ * @param colorVariable a TailwindCSS color parameter * @returns the color in hex format, as a string */ -export const getTailwindColor = (colorVariable: string) => { +export const getTailwindColor = (colorVariable: string): string => { + if (typeof window === 'undefined' || !document || !document.documentElement) { + return ''; + } const [hue, saturation, lightness] = getComputedStyle(document.documentElement) .getPropertyValue(colorVariable) .split(' '); diff --git a/tailwind.config.js b/tailwind.config.js index 6d5b7014..4f24a79b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -172,6 +172,11 @@ const config = { hazarAdvisory: '#57D66F', hazardInformation: '#0098EB', hazardTermination: '#B4B5B7', + + // charts + chartsLegendBackground: '#F5F5F5', + chartsXAxisLine: '#a6a6a6', + chartsGridLine: '#E6E6E6', chartForecast: '#3896a2', //fcs gradient @@ -230,9 +235,6 @@ const config = { ipcPhase3: '#e88519', ipcPhase4: '#cd1919', ipcPhase5: '#731919', - chartsLegendBackground: '#F5F5F5', - chartsXAxisLine: '#a6a6a6', - chartsGridLine: '#E6E6E6', }, }, dark: { @@ -274,12 +276,14 @@ const config = { fatalityAlert: '#742280', climateWetAlert: '#4295D3', climateDryAlert: '#B95926', + nutritionNotAnalyzed: '#A69F9F', + fcsAnimation: '#014a5e', + + // charts chartsLegendBackground: '#2a2a2a', chartsXAxisLine: '#757575', chartsGridLine: '#424242', chartForecast: '#0e6983', - nutritionNotAnalyzed: '#A69F9F', - fcsAnimation: '#014a5e', }, }, }, From 646177f2e0ecc664a4989044324a8c15217468d3 Mon Sep 17 00:00:00 2001 From: Arman <142525678+ArmanpreetGhotra@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:29:26 +0100 Subject: [PATCH 07/12] feat: add loading to accordions of download portal with the help loading.tsx (#119) Co-authored-by: Armanpreet Ghotra --- src/app/download-portal/loading.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/app/download-portal/loading.tsx diff --git a/src/app/download-portal/loading.tsx b/src/app/download-portal/loading.tsx new file mode 100644 index 00000000..c2a6192a --- /dev/null +++ b/src/app/download-portal/loading.tsx @@ -0,0 +1,23 @@ +import AccordionContainer from '@/components/Accordions/AccordionContainer'; +import { TITLE } from '@/domain/entities/download/Country'; + +export default function Loading() { + const loading = true; + + return ( +
      +

      Download Portal

      + +
      + ); +} From 404edbae0758f094c539e7f071c09958bc967a0a Mon Sep 17 00:00:00 2001 From: Ahmed Farouk <93868173+ahmedfarouk2000@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:29:37 +0100 Subject: [PATCH 08/12] Feature/f 154 legend color hovering (#126) * feat: add hovering effect on legend colors * feat: update tooltip comp * feat: update legend data with hovering values * fix: replace ternarys with clsx --- src/components/Legend/GradientLegend.tsx | 48 +++++++++--- src/components/Tooltip/Tooltip.tsx | 11 ++- src/domain/constant/legend/mapLegendData.tsx | 76 +++++++++++-------- src/domain/props/ColorsData.tsx | 5 ++ .../props/GradientLegendContainerItem.ts | 4 +- src/domain/props/GradientLegendProps.ts | 4 +- src/domain/props/TooltipProps.tsx | 2 + src/operations/legends/LegendOperations.ts | 4 +- 8 files changed, 105 insertions(+), 49 deletions(-) create mode 100644 src/domain/props/ColorsData.tsx diff --git a/src/components/Legend/GradientLegend.tsx b/src/components/Legend/GradientLegend.tsx index 79a673ce..b7f9e7d5 100644 --- a/src/components/Legend/GradientLegend.tsx +++ b/src/components/Legend/GradientLegend.tsx @@ -1,13 +1,20 @@ +import clsx from 'clsx'; + +import { ColorsData } from '@/domain/props/ColorsData'; import GradientLegendProps from '@/domain/props/GradientLegendProps'; -export default function GradientLegend({ colors, startLabel, endLabel, hasNotAnalyzedPoint }: GradientLegendProps) { - const gradients: string = colors - .map((color: string, index: number) => { - const percentage = (index / (colors.length - 1)) * 100; - return `hsl(var(--nextui-${color})) ${percentage}%`; +import { Tooltip } from '../Tooltip/Tooltip'; + +export default function GradientLegend({ colorsData, startLabel, endLabel, hasNotAnalyzedPoint }: GradientLegendProps) { + const gradients: string = colorsData + .map((colorData: ColorsData, index: number) => { + const percentage = (index / (colorsData.length - 1)) * 100; + return `hsl(var(--nextui-${colorData.color})) ${percentage}%`; }) .join(', '); + const segmentWidth: number = 100 / colorsData.length; + return (
      {hasNotAnalyzedPoint && ( @@ -20,12 +27,31 @@ export default function GradientLegend({ colors, startLabel, endLabel, hasNotAna
      )} -
      +
      + {colorsData.map((colorData, index) => ( +
      + +
      + +
      + ))} +
      +
      {startLabel} {endLabel} diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index b1b532a0..231b9ac6 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -1,4 +1,5 @@ import { Tooltip as NextUITooltip } from '@nextui-org/tooltip'; +import clsx from 'clsx'; import TooltipProps from '@/domain/props/TooltipProps'; @@ -11,9 +12,11 @@ import TooltipProps from '@/domain/props/TooltipProps'; * @param text textual content of the tooltip * @param delay delay with which tooltip appears on hover in milliseconds; default is 0 * @param warning selected if the tooltip should be highlighted (optional) + * @param titleStyle tailwind classes to style the title (optional) + * @param textStyle tailwind classes to style the text (optional) * @constructor */ -export function Tooltip({ children, title, text, delay, warning }: TooltipProps) { +export function Tooltip({ children, title, text, delay, warning, titleStyle, textStyle }: TooltipProps) { const OFFSET: number = 10; const RADIUS = 'sm'; const SHADOW = 'md'; @@ -23,11 +26,11 @@ export function Tooltip({ children, title, text, delay, warning }: TooltipProps) const tooltipContent = title ? (
      -

      {title}

      -

      {text}

      +

      {title}

      +

      {text}

      ) : ( -

      {text}

      +

      {text}

      ); return ( diff --git a/src/domain/constant/legend/mapLegendData.tsx b/src/domain/constant/legend/mapLegendData.tsx index 17cdb644..6dff0678 100644 --- a/src/domain/constant/legend/mapLegendData.tsx +++ b/src/domain/constant/legend/mapLegendData.tsx @@ -106,7 +106,15 @@ export function mapLegendData( case GlobalInsight.FOOD: legendData.push({ title: 'Prevalence of insufficient food consumption', - colors: ['fcsGradient1', 'fcsGradient2', 'fcsGradient3', 'fcsGradient4', 'fcsGradient5', 'fcsGradient6'], + colorsData: [ + { color: 'fcsGradient1', title: 'Very Low', value: '0-5%' }, + { color: 'fcsGradient2', title: 'Low', value: '5-10%' }, + { color: 'fcsGradient3', title: 'Moderately Low', value: '10-20%' }, + { color: 'fcsGradient4', title: 'Moderately High', value: '20-30%' }, + { color: 'fcsGradient5', title: 'High', value: '30-40%' }, + { color: 'fcsGradient6', title: 'Very High', value: 'Above 40%' }, + ], + startLabel: '0%', endLabel: 'above 40%', popoverInfo: ( @@ -158,17 +166,18 @@ export function mapLegendData( case GlobalInsight.RAINFALL: legendData.push({ title: 'Rainfall', - colors: [ - 'vegetationGradient1', - 'vegetationGradient2', - 'vegetationGradient3', - 'vegetationGradient4', - 'vegetationGradient5', - 'rainfallGradient6', - 'rainfallGradient7', - 'rainfallGradient8', - 'rainfallGradient9', + colorsData: [ + { color: 'vegetationGradient1', value: '<40%' }, + { color: 'vegetationGradient2', value: '40-60%' }, + { color: 'vegetationGradient3', value: '60-80%' }, + { color: 'vegetationGradient4', value: '80-90%' }, + { color: 'vegetationGradient5', value: '90-110%' }, + { color: 'rainfallGradient6', value: '110-120%' }, + { color: 'rainfallGradient7', value: '120-140%' }, + { color: 'rainfallGradient8', value: '140-180%' }, + { color: 'rainfallGradient9', value: '>180%' }, ], + startLabel: '<40%', endLabel: '>180%', popoverInfo: ( @@ -196,17 +205,18 @@ export function mapLegendData( case GlobalInsight.VEGETATION: legendData.push({ title: 'Vegetation', - colors: [ - 'vegetationGradient1', - 'vegetationGradient2', - 'vegetationGradient3', - 'vegetationGradient4', - 'vegetationGradient5', - 'vegetationGradient6', - 'vegetationGradient7', - 'vegetationGradient8', - 'vegetationGradient9', + colorsData: [ + { color: 'vegetationGradient1', value: '<50%' }, + { color: 'vegetationGradient2', value: '50-70%' }, + { color: 'vegetationGradient3', value: '70-80%' }, + { color: 'vegetationGradient4', value: '80-90%' }, + { color: 'vegetationGradient5', value: '90-110%' }, + { color: 'vegetationGradient6', value: '110-120%' }, + { color: 'vegetationGradient7', value: '120-130%' }, + { color: 'vegetationGradient8', value: '130-150%' }, + { color: 'vegetationGradient9', value: '>150%' }, ], + startLabel: '<50%', endLabel: '>150%', popoverInfo: ( @@ -236,14 +246,14 @@ export function mapLegendData( legendData.push({ title: 'Number of people in IPC/CH Phase 3 or above (millions)', hasNotAnalyzedPoint: true, - colors: [ - 'ipcGradient1', - 'ipcGradient2', - 'ipcGradient3', - 'ipcGradient4', - 'ipcGradient5', - 'ipcGradient6', - 'ipcGradient7', + colorsData: [ + { color: 'ipcGradient1', value: '0-0.099' }, + { color: 'ipcGradient2', value: '0.1-0.49' }, + { color: 'ipcGradient3', value: '0.5-0.99' }, + { color: 'ipcGradient4', value: '1.0-2.99' }, + { color: 'ipcGradient5', value: '3.0-4.99' }, + { color: 'ipcGradient6', value: '5.0-9.99' }, + { color: 'ipcGradient7', value: '>10' }, ], startLabel: '0', endLabel: '>10', @@ -356,7 +366,13 @@ export function mapLegendData( } else { legendData.push({ title: 'Risk of Inadequate Micronutrient Intake', - colors: ['ipcGradient1', 'ipcGradient2', 'ipcGradient3', 'ipcGradient4', 'ipcGradient5'], + colorsData: [ + { color: 'ipcGradient1', title: 'Lowest', value: '0-19%' }, + { color: 'ipcGradient2', title: 'Low', value: '20-39%' }, + { color: 'ipcGradient3', title: 'Moderate', value: '40-59%' }, + { color: 'ipcGradient4', title: 'High', value: '60-79%' }, + { color: 'ipcGradient5', title: 'Highest', value: '80-100%' }, + ], startLabel: '0%', endLabel: '100%', popoverInfo: ( diff --git a/src/domain/props/ColorsData.tsx b/src/domain/props/ColorsData.tsx new file mode 100644 index 00000000..786f491b --- /dev/null +++ b/src/domain/props/ColorsData.tsx @@ -0,0 +1,5 @@ +export interface ColorsData { + color: string; + title?: string; + value: string; +} diff --git a/src/domain/props/GradientLegendContainerItem.ts b/src/domain/props/GradientLegendContainerItem.ts index cc301b29..c8df3110 100644 --- a/src/domain/props/GradientLegendContainerItem.ts +++ b/src/domain/props/GradientLegendContainerItem.ts @@ -1,7 +1,9 @@ import { ReactNode } from 'react'; +import { ColorsData } from './ColorsData'; + export interface GradientLegendContainerItem { - colors: string[]; + colorsData: ColorsData[]; title: string; startLabel: string; endLabel: string; diff --git a/src/domain/props/GradientLegendProps.ts b/src/domain/props/GradientLegendProps.ts index c10256fd..bf76f913 100644 --- a/src/domain/props/GradientLegendProps.ts +++ b/src/domain/props/GradientLegendProps.ts @@ -1,5 +1,7 @@ +import { ColorsData } from './ColorsData'; + export default interface GradientLegendProps { - colors: string[]; + colorsData: ColorsData[]; startLabel: string; endLabel: string; hasNotAnalyzedPoint?: boolean; diff --git a/src/domain/props/TooltipProps.tsx b/src/domain/props/TooltipProps.tsx index 325d7473..9879bae0 100644 --- a/src/domain/props/TooltipProps.tsx +++ b/src/domain/props/TooltipProps.tsx @@ -6,4 +6,6 @@ export default interface TooltipProps { text: string; delay?: number; warning?: boolean; + titleStyle?: string; + textStyle?: string; } diff --git a/src/operations/legends/LegendOperations.ts b/src/operations/legends/LegendOperations.ts index bbe17c2a..b025e051 100644 --- a/src/operations/legends/LegendOperations.ts +++ b/src/operations/legends/LegendOperations.ts @@ -6,8 +6,8 @@ export class LegendOperations { return false; } return ( - Array.isArray((value as GradientLegendContainerItem).colors) && - (value as GradientLegendContainerItem).colors.every((color) => typeof color === 'string') + Array.isArray((value as GradientLegendContainerItem).colorsData) && + (value as GradientLegendContainerItem).colorsData.every((colorsData) => typeof colorsData.color === 'string') ); } } From ab7e186bf36a37bcfbd03881ff44c79e474938af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1muel=20Fekete?= Date: Fri, 13 Dec 2024 12:30:30 +0100 Subject: [PATCH 09/12] fix: disable cache max size (#125) --- next.config.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/next.config.js b/next.config.js index 1e801f0b..fbffc9ab 100644 --- a/next.config.js +++ b/next.config.js @@ -1,15 +1,16 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - webpack(config, { isServer }) { - // Add rule to handle SVG imports - config.module.rules.push({ - test: /\.svg$/, - use: ['@svgr/webpack', 'url-loader'], - }); - config.resolve.alias.canvas = false; - - return config; - }, - }; - - module.exports = nextConfig; + webpack(config, { isServer }) { + // Add rule to handle SVG imports + config.module.rules.push({ + test: /\.svg$/, + use: ['@svgr/webpack', 'url-loader'], + }); + config.resolve.alias.canvas = false; + + return config; + }, + cacheHandler: require.resolve('next/dist/server/lib/incremental-cache/file-system-cache.js'), +}; + +module.exports = nextConfig; From fb6bd51250db1680f269bb52928ee459e9d07c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20Selman=20G=C3=BCcl=C3=BC?= <77407148+ahmtslmngcl@users.noreply.github.com> Date: Sun, 15 Dec 2024 14:57:33 +0100 Subject: [PATCH 10/12] feat: configure error page (#132) * feat: configure error page * feat: not-found 404 * feat: log error * Update not-found.tsx --------- Co-authored-by: Lachezar Marinov <99186919+marinovl7@users.noreply.github.com> --- src/app/error.tsx | 34 ++++++++++++++++++++++------------ src/app/not-found.tsx | 25 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 src/app/not-found.tsx diff --git a/src/app/error.tsx b/src/app/error.tsx index 6847fc17..64f60e16 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -2,7 +2,10 @@ import { useEffect } from 'react'; -export default function Error({ error, reset }: { error: Error; reset: () => void }) { +import { CustomButton } from '@/components/Buttons/CustomButton'; +import { Topbar } from '@/components/Topbar/Topbar'; + +export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { useEffect(() => { // Log the error to an error reporting service /* eslint-disable no-console */ @@ -10,17 +13,24 @@ export default function Error({ error, reset }: { error: Error; reset: () => voi }, [error]); return ( -
      -

      Something went wrong!

      - +
      + +
      +
      +

      An Error Occurred

      +

      Please try again or go back to the Home page.

      +
      +
      + reset()}> + Try again + + + + Go to Home page + + +
      +
      ); } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 00000000..c3655215 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { CustomButton } from '@/components/Buttons/CustomButton'; +import { Topbar } from '@/components/Topbar/Topbar'; + +export default function NotFound() { + return ( +
      + +
      +
      +

      Ooops

      +

      The requested page could not be found.

      +
      + +
      +
      + ); +} From 94964279274b1a1c17a79968bd310128d10898e1 Mon Sep 17 00:00:00 2001 From: Linus <126587385+plaume8@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:04:16 +0100 Subject: [PATCH 11/12] Feature/f 165 linechart barchart support (#134) * feat: LineCharts now support vertical lines and vertical bands * feat: added labels to vertical bands and lines * feat: updated dummy lineCharts in /elements * feat: chart special forecast color * feat: param in LineChartData sets color and dashstyle atumatically to reflect predcition styling * feat: processing implemented * feat: cleanup * feat: chart download button to own componentn - and added button to LineChart component * fix: added colors to LineChartOperations as hex - fixing the chart png and svg download * feat: introduced ChartContainer component - general component that will later be used by the CategoricalChart and ContinuousChart copmpeontn * feat: refactored helper chart componentne (e.g. buttons, slider) to later support categorical and continues data * feat: LineChart now using the new ChartContainer component * feat: cleanup and refactor all chart props * feat: done transfering line chart to new chart components structure * feat: antoher quick cleanup * fix: using tailwind-util.getTailwindColor to use nextui colors for highcharts options - fixes the chart export color problem * feat: first dummy version of CategoricalChartOperations to build the categorical chart options for highchart * feat: first setup of CategoricalChart component - incl elemnts page dummy cat chart * feat: cleanup CategoricalChart and CategoricalChartOptions * feat: quick chart props refactoring * feat: concat slternativeChartSwitch props and sliderProps * feat: chartAlternativeSwitchButton logic to deticated operations file * feat: final pie chart styling adjustments * feat: final cleanup and comments --- src/app/elements/page.tsx | 80 ++------ src/components/Charts/CategoricalChart.tsx | 77 +++++++ src/components/Charts/LineChart.tsx | 188 +++++------------- .../Charts/helpers/ChartContainer.tsx | 139 +++++++++++++ .../ChartModal.tsx} | 63 +++--- src/components/Charts/helpers/ChartSlider.tsx | 41 ++++ .../helpers/LineChartBarLineSwitchButton.tsx | 23 --- .../Charts/helpers/LineChartXAxisSlider.tsx | 42 ---- .../buttons/ChartAlternativeSwitchButton.tsx | 33 +++ .../ChartDownloadButton.tsx} | 19 +- .../ChartSliderButton.tsx} | 13 +- src/components/Map/FcsAccordion.tsx | 32 +-- src/components/Map/FcsRegionTooltip.tsx | 4 + .../entities/charts/CategoricalChartData.ts | 12 ++ src/domain/entities/charts/LineChartData.ts | 5 +- src/domain/enums/ChartType.ts | 5 + src/domain/props/CategoricalChartProps.tsx | 16 ++ src/domain/props/ChartContainerProps.tsx | 61 ++++++ src/domain/props/ChartModalProps.tsx | 28 +++ src/domain/props/LineChartModalProps.tsx | 25 --- src/domain/props/LineChartProps.tsx | 42 +--- .../charts/CategoricalChartOperations.ts | 183 +++++++++++++++++ ...ChartAlternativeSwitchButtonOperations.tsx | 32 +++ .../charts/ChartDownloadButtonOperations.ts | 71 +++++++ src/operations/charts/LineChartOperations.ts | 65 +----- 25 files changed, 818 insertions(+), 481 deletions(-) create mode 100644 src/components/Charts/CategoricalChart.tsx create mode 100644 src/components/Charts/helpers/ChartContainer.tsx rename src/components/Charts/{LineChartModal.tsx => helpers/ChartModal.tsx} (52%) create mode 100644 src/components/Charts/helpers/ChartSlider.tsx delete mode 100644 src/components/Charts/helpers/LineChartBarLineSwitchButton.tsx delete mode 100644 src/components/Charts/helpers/LineChartXAxisSlider.tsx create mode 100644 src/components/Charts/helpers/buttons/ChartAlternativeSwitchButton.tsx rename src/components/Charts/helpers/{LineChartDownloadButton.tsx => buttons/ChartDownloadButton.tsx} (67%) rename src/components/Charts/helpers/{LineChartSliderButton.tsx => buttons/ChartSliderButton.tsx} (52%) create mode 100644 src/domain/entities/charts/CategoricalChartData.ts create mode 100644 src/domain/enums/ChartType.ts create mode 100644 src/domain/props/CategoricalChartProps.tsx create mode 100644 src/domain/props/ChartContainerProps.tsx create mode 100644 src/domain/props/ChartModalProps.tsx delete mode 100644 src/domain/props/LineChartModalProps.tsx create mode 100644 src/operations/charts/CategoricalChartOperations.ts create mode 100644 src/operations/charts/ChartAlternativeSwitchButtonOperations.tsx create mode 100644 src/operations/charts/ChartDownloadButtonOperations.ts diff --git a/src/app/elements/page.tsx b/src/app/elements/page.tsx index e29a13ee..8db4e253 100644 --- a/src/app/elements/page.tsx +++ b/src/app/elements/page.tsx @@ -4,9 +4,11 @@ import { useState } from 'react'; import AccordionContainer from '@/components/Accordions/AccordionContainer'; import { CustomButton } from '@/components/Buttons/CustomButton'; +import { CategoricalChart } from '@/components/Charts/CategoricalChart'; import { LineChart } from '@/components/Charts/LineChart'; import MapSkeleton from '@/components/Map/MapSkeleton'; import SearchBar from '@/components/Search/SearchBar'; +import { CategoricalChartData } from '@/domain/entities/charts/CategoricalChartData.ts'; import { LineChartData } from '@/domain/entities/charts/LineChartData.ts'; import { LineChartDataType } from '@/domain/enums/LineChartDataType.ts'; import AccordionsOperations from '@/operations/accordions/AccordionOperations'; @@ -35,23 +37,6 @@ export default async function Elements() { ], }; - const simpleLineChartData: LineChartData = { - type: LineChartDataType.LINE_CHART_DATA, - xAxisType: 'linear', - yAxisLabel: 'yield', - lines: [ - { - name: 'Category A', - dataPoints: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 4 }, - { x: 3, y: 8 }, - ], - }, - ], - }; - const maxedOutLineChartData: LineChartData = { type: LineChartDataType.LINE_CHART_DATA, xAxisType: 'linear', @@ -144,50 +129,19 @@ export default async function Elements() { ], }; - const multiplePredictionsDummyChartData: LineChartData = { - type: LineChartDataType.LINE_CHART_DATA, - xAxisType: 'linear', + const categoricalDummyChartData1: CategoricalChartData = { yAxisLabel: 'Mill', - predictionVerticalLineX: 3, - lines: [ + categories: [ { name: 'Category A', - showRange: true, - dataPoints: [ - { x: 0, y: 4, yRangeMin: 3.5, yRangeMax: 5 }, - { x: 1, y: 3, yRangeMin: 2, yRangeMax: 4 }, - { x: 2, y: 4, yRangeMin: 3.5, yRangeMax: 4.5 }, - { x: 3, y: 8, yRangeMin: 7.5, yRangeMax: 8.5 }, - ], + dataPoint: { y: 5 }, }, { - name: 'Prediction 1', - prediction: true, - dataPoints: [ - { x: 0, y: 4 }, - { x: 1, y: 7 }, - { x: 2, y: 5 }, - { x: 3, y: 7 }, - { x: 4, y: 8 }, - { x: 5, y: 5 }, - { x: 6, y: 6 }, - ], - }, - { - name: 'Prediction 2', - prediction: true, - dataPoints: [ - { x: 0, y: 6 }, - { x: 1, y: 5 }, - { x: 2, y: 6 }, - { x: 3, y: 7 }, - { x: 4, y: 7 }, - { x: 5, y: 8 }, - ], + name: 'Category B', + dataPoint: { y: 8 }, }, ], }; - return (
      @@ -204,17 +158,14 @@ export default async function Elements() { />
      -
      - -
      @@ -222,16 +173,13 @@ export default async function Elements() { title="Maxed Out Line Chart" description="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor onsetetur sadipscing elitr." data={maxedOutLineChartData} - expandable - xAxisSlider - barChartSwitch />
      - +
      - +
      diff --git a/src/components/Charts/CategoricalChart.tsx b/src/components/Charts/CategoricalChart.tsx new file mode 100644 index 00000000..24fe166a --- /dev/null +++ b/src/components/Charts/CategoricalChart.tsx @@ -0,0 +1,77 @@ +'use client'; + +import Highcharts from 'highcharts'; +import { useTheme } from 'next-themes'; +import { useEffect, useState } from 'react'; + +import { ChartContainer } from '@/components/Charts/helpers/ChartContainer'; +import { ChartType } from '@/domain/enums/ChartType.ts'; +import CategoricalChartProps from '@/domain/props/CategoricalChartProps'; +import CategoricalChartOperations from '@/operations/charts/CategoricalChartOperations.ts'; + +/** + * The `CategoricalChart` component is a box that primarily renders a title, description text, and a bar chart. + * It should be used to plot categorical data. For continues data please use the `LineChart` component. + * This component has a width of 100%, so it adjusts to the width of its parent element in which it is used. + * The height of the entire box depends on the provided text, while the chart itself has a fixed height. + * It also provides the option to open the chart in a full-screen modal, where one can download the data as well. + * + * @param data the actual data to be used in the chart + * @param title chart title (optional) + * @param description chart description text (optional) + * @param small when selected, all components in the line chart box become slightly smaller (optional) + * @param noPadding when selected, the main box has no padding on all sides (optional) + * @param transparentBackground when selected, the background of the entire component is transparent (optional) + * @param disableExpandable when selected, the functionality to open the chart in a larger modal is disabled (optional) + * @param disablePieChartSwitch when selected, the functionality to switch to a pie chart is disabled (optional) + * @param disableDownload when selected, the functionality to download the chart is disabled (optional) + */ +export function CategoricalChart({ + data, + title, + description, + small, + noPadding, + transparentBackground, + disableExpandable, + disablePieChartSwitch, + disableDownload, +}: CategoricalChartProps) { + const { theme } = useTheme(); + + // build chart options for 'Highcharts' + const defaultChartOptions: Highcharts.Options = CategoricalChartOperations.getHighChartOptions(data); + + // controlling if a bar or pie chart is rendered; bar chart is the default + const [showPieChart, setShowPieChart] = useState(false); + const [chartOptions, setChartOptions] = useState(defaultChartOptions); + + // handling the bar and pie chart switch and the theme switch; + useEffect(() => { + setChartOptions(CategoricalChartOperations.getHighChartOptions(data, showPieChart)); + }, [showPieChart, theme]); + + const alternativeSwitchButtonProps = disablePieChartSwitch + ? undefined + : { + defaultChartType: ChartType.COLUMN, + alternativeChartType: ChartType.PIE, + showAlternativeChart: showPieChart, + setShowAlternativeChart: setShowPieChart, + }; + + return ( + + ); +} diff --git a/src/components/Charts/LineChart.tsx b/src/components/Charts/LineChart.tsx index e8fd9a5e..52891746 100644 --- a/src/components/Charts/LineChart.tsx +++ b/src/components/Charts/LineChart.tsx @@ -1,27 +1,20 @@ 'use client'; -import { Button } from '@nextui-org/button'; -import { useDisclosure } from '@nextui-org/modal'; import Highcharts from 'highcharts'; -import HighchartsReact from 'highcharts-react-official'; -import { Maximize4 } from 'iconsax-react'; import { useTheme } from 'next-themes'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; -import LineChartBarLineSwitchButton from '@/components/Charts/helpers/LineChartBarLineSwitchButton'; -import LineChartDownloadButton from '@/components/Charts/helpers/LineChartDownloadButton'; -import LineChartSliderButton from '@/components/Charts/helpers/LineChartSliderButton'; -import LineChartXAxisSlider from '@/components/Charts/helpers/LineChartXAxisSlider'; -import { LineChartModal } from '@/components/Charts/LineChartModal'; -import { Tooltip } from '@/components/Tooltip/Tooltip'; +import { ChartContainer } from '@/components/Charts/helpers/ChartContainer'; import { LineChartData } from '@/domain/entities/charts/LineChartData'; +import { ChartType } from '@/domain/enums/ChartType.ts'; import LineChartProps from '@/domain/props/LineChartProps'; import LineChartOperations from '@/operations/charts/LineChartOperations'; /** * The LineChart component is a box that primarily renders a title, description text, and a line chart. + * It should be used to plot categorical data. For continues data please use the `CategoricalChart` component. * This component has a width of 100%, so it adjusts to the width of its parent element in which it is used. - * The height of the line chart box depends on the provided text, while the chart itself has a fixed height. + * The height of the entire box depends on the provided text, while the chart itself has a fixed height. * It also provides the option to open the chart in a full-screen modal, where one can download the data as well. * * The data to be displayed in the chart can be provided in different types (see `LineChartProps.data`). @@ -31,46 +24,34 @@ import LineChartOperations from '@/operations/charts/LineChartOperations'; * 1. Define an interface and add it to `LineChartProps.data`. * 2. Add another switch case in `LineChartOperations.convertToLineChartData` to convert the new interface to `LineChartData`. * + * @param data the actual data to be used in the chart * @param title chart title (optional) * @param description chart description text (optional) - * @param expandable when selected, the user is given the option to open the chart in a larger modal (optional) - * @param barChartSwitch when selected, the user is given the option to switch to a bar chart (optional) - * @param xAxisSlider when selected, the user is given the option to change the x-axis range via a slider (optional) * @param small when selected, all components in the line chart box become slightly smaller (optional) - * @param small when selected, the download button dropdown is not shown (optional) - * @param roundLines when selected, all plotted lines will be rounded (optional) * @param noPadding when selected, the main box has no padding on all sides (optional) * @param transparentBackground when selected, the background of the entire component is transparent (optional) - * @param data the actual data to be used in the chart + * @param disableExpandable when selected, the functionality to open the chart in a larger modal is disabled (optional) + * @param disableBarChartSwitch when selected, the functionality to switch to a bar chart is disabled (optional) + * @param disableXAxisSlider when selected, the functionality to change the x-axis range via a slider is disabled (optional) + * @param disableDownload when selected, the functionality to download the chart is disabled (optional) */ export function LineChart({ + data, title, description, - expandable, - barChartSwitch, - xAxisSlider, small, - disableDownload, - roundLines, noPadding, transparentBackground, - data, + disableExpandable, + disableBarChartSwitch, + disableXAxisSlider, + disableDownload, }: LineChartProps) { - const TITLE_TEXT_SIZE = small ? 'text-sm' : 'text-md'; - const DESCRIPTION_TEXT_SIZE = small ? 'text-tiny' : 'text-sm'; - const CHART_HEIGHT = small ? 12 : 16; - const ICON_BUTTON_SIZE = small ? 3 : 4; - const HEADER_PADDING = title ? 3 : 0; - const MAIN_BOX_PADDING_FACTOR = noPadding ? 0 : 1; - - // full screen modal state handling - const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure(); - // the 'chartOptions' are dependent on the theme const { theme } = useTheme(); // convert data to `LineChartData` and build chart options for 'Highcharts' (line and bar chart) const lineChartData: LineChartData = LineChartOperations.convertToLineChartData(data); - const lineChartOptions: Highcharts.Options = LineChartOperations.getHighChartOptions(lineChartData, roundLines); + const lineChartOptions: Highcharts.Options = LineChartOperations.getHighChartOptions(lineChartData); // the `selectedXAxisRange` saves the to be rendered x-axis range of the chart // can be changed using the `LinkeChartXAxisSlider` if the param `xAxisSlider==true` @@ -80,10 +61,6 @@ export function LineChart({ // controlling if a line or bar chart is rendered; line chart is the default const [showBarChart, setShowBarChart] = useState(false); const [chartOptions, setChartOptions] = useState(lineChartOptions); - // handling the x-axis range slider visibility - const [showXAxisSlider, setShowXAxisSlider] = useState(false); - - const chartRef = useRef(null); // handling the line and bar chart switch and the theme switch; // also handling changing the x-axis range using the `LineChartXAxisSlider`; @@ -91,113 +68,48 @@ export function LineChart({ useEffect(() => { if (showBarChart || selectedXAxisRange[1] - selectedXAxisRange[0] === 0) { setChartOptions( - LineChartOperations.getHighChartOptions( - lineChartData, - roundLines, - selectedXAxisRange[0], - selectedXAxisRange[1], - true - ) + LineChartOperations.getHighChartOptions(lineChartData, selectedXAxisRange[0], selectedXAxisRange[1], true) ); } else { setChartOptions( - LineChartOperations.getHighChartOptions(lineChartData, roundLines, selectedXAxisRange[0], selectedXAxisRange[1]) + LineChartOperations.getHighChartOptions(lineChartData, selectedXAxisRange[0], selectedXAxisRange[1]) ); } }, [showBarChart, theme, selectedXAxisRange]); - return ( - <> -
      -
      -

      - {title} -

      -
      - {xAxisSlider && ( - - )} - - {barChartSwitch && ( - - )} - - {!disableDownload && } + // chart slider props - to manipulate the shown x-axis range + const sliderProps = disableXAxisSlider + ? undefined + : { + title: 'Adjusting x-axis range:', + sliderMin: 0, + sliderMax: xAxisLength - 1, + selectedSliderRange: selectedXAxisRange, + setSelectedSliderRange: setSelectedXAxisRange, + }; - { - // button to trigger the full screen modal; rendered if `expandable` - expandable && ( - - - - ) - } -
      -
      - { - // description text element should only be rendered if description is available - description && ( -

      - {description} -

      - ) - } - {/* the actual chart */} - - { - // slider to manipulate the plotted x-axis range of the chart; can be disabled via `xAxisSlider` - showXAxisSlider && ( - - ) - } -
      + const alternativeSwitchButtonProps = disableBarChartSwitch + ? undefined + : { + defaultChartType: ChartType.LINE, + alternativeChartType: ChartType.COLUMN, + showAlternativeChart: showBarChart, + setShowAlternativeChart: setShowBarChart, + }; - - + return ( + ); } diff --git a/src/components/Charts/helpers/ChartContainer.tsx b/src/components/Charts/helpers/ChartContainer.tsx new file mode 100644 index 00000000..85d006c7 --- /dev/null +++ b/src/components/Charts/helpers/ChartContainer.tsx @@ -0,0 +1,139 @@ +'use client'; + +import { Button } from '@nextui-org/button'; +import { useDisclosure } from '@nextui-org/modal'; +import Highcharts from 'highcharts'; +import HighchartsReact from 'highcharts-react-official'; +import { Maximize4 } from 'iconsax-react'; +import { useRef, useState } from 'react'; + +import ChartAlternativeSwitchButton from '@/components/Charts/helpers/buttons/ChartAlternativeSwitchButton'; +import ChartDownloadButton from '@/components/Charts/helpers/buttons/ChartDownloadButton'; +import ChartSliderButton from '@/components/Charts/helpers/buttons/ChartSliderButton'; +import { ChartModal } from '@/components/Charts/helpers/ChartModal'; +import ChartSlider from '@/components/Charts/helpers/ChartSlider'; +import { Tooltip } from '@/components/Tooltip/Tooltip'; +import ChartContainerProps from '@/domain/props/ChartContainerProps'; + +/** + * This component is the general component, which renders a box that primarily displays a title, description text, and a chart. + * This component has a width of 100%, so it adjusts to the width of its parent element in which it is used. + * The height of the entire box depends on the provided text, while the chart itself has a fixed height. + * It also provides the option to open the chart in a full-screen modal, where one can download the data as well. + * + * It is used by the `CategoricalChart` and `LineChart` components, which define the type of chart through the passed `chartOptions`. + * The main goal of this component is to prevents code redundancy between `LineChart` and `CategoricalChart`. + */ +export function ChartContainer({ + chartOptions, + chartData, + title, + description, + small, + noPadding, + transparentBackground, + disableExpandable, + disableDownload, + alternativeSwitchButtonProps, + sliderProps, +}: ChartContainerProps) { + const TITLE_TEXT_SIZE = small ? 'text-sm' : 'text-md'; + const DESCRIPTION_TEXT_SIZE = small ? 'text-tiny' : 'text-sm'; + const CHART_HEIGHT = small ? 12 : 16; + const ICON_BUTTON_SIZE = small ? 3 : 4; + const HEADER_PADDING = title ? 3 : 0; + const MAIN_BOX_PADDING_FACTOR = noPadding ? 0 : 1; + + const chartRef = useRef(null); + + // full screen modal state handling + const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure(); + + // handling the x-axis range slider visibility + const [showSlider, setShowSlider] = useState(false); + return ( + <> +
      +
      +

      + {title} +

      +
      + { + // button to hide/show the slider to e.g. manipulate the plotted x-axis range of the chart + sliderProps && ( + + ) + } + { + // button to switch between different chart types + alternativeSwitchButtonProps && ( + + ) + } + { + // button to download chart as png, svg, etc. + !disableDownload && ( + + ) + } + { + // button to trigger the full screen modal; rendered if `disableExpandable` is not selected + !disableExpandable && ( + + + + ) + } +
      +
      + { + // description text element should only be rendered if description is available + description && ( +

      + {description} +

      + ) + } + {/* the actual chart */} + + { + // slider to e.g. manipulate the plotted x-axis range of the chart + showSlider && sliderProps && + } +
      + + + + ); +} diff --git a/src/components/Charts/LineChartModal.tsx b/src/components/Charts/helpers/ChartModal.tsx similarity index 52% rename from src/components/Charts/LineChartModal.tsx rename to src/components/Charts/helpers/ChartModal.tsx index 53412759..eb10f838 100644 --- a/src/components/Charts/LineChartModal.tsx +++ b/src/components/Charts/helpers/ChartModal.tsx @@ -5,41 +5,34 @@ import HighchartsReact from 'highcharts-react-official'; import { Minus } from 'iconsax-react'; import { useRef } from 'react'; -import LineChartBarLineSwitchButton from '@/components/Charts/helpers/LineChartBarLineSwitchButton'; -import LineChartDownloadButton from '@/components/Charts/helpers/LineChartDownloadButton'; -import LineChartSliderButton from '@/components/Charts/helpers/LineChartSliderButton'; -import LineChartXAxisSlider from '@/components/Charts/helpers/LineChartXAxisSlider'; +import ChartAlternativeSwitchButton from '@/components/Charts/helpers/buttons/ChartAlternativeSwitchButton'; +import ChartDownloadButton from '@/components/Charts/helpers/buttons/ChartDownloadButton'; +import ChartSliderButton from '@/components/Charts/helpers/buttons/ChartSliderButton'; +import ChartSlider from '@/components/Charts/helpers/ChartSlider'; import { Tooltip } from '@/components/Tooltip/Tooltip'; -import LineChartModalProps from '@/domain/props/LineChartModalProps'; +import ChartModalProps from '@/domain/props/ChartModalProps'; /** - * This component is tied to the `LineChart` component and should not be used independently. - * It renders the modal, which can be opened by the user from the `LineChart` to display the chart - * in a larger view and access additional functionalities, such as downloading the chart as a PNG. - * For more details, please refer to the `LineChart` component. + * This component is tied to the `ChartContainer` component and should not be used independently. + * It renders the modal, which can be opened by the user from the `ChartContainer` + * to display the chart in a larger view. */ -export function LineChartModal({ +export function ChartModal({ + chartOptions, + chartData, title, description, disableDownload, - barChartSwitch, - xAxisSlider, - lineChartData, - chartOptions, isOpen, onClose, onOpenChange, - showXAxisSlider, - setShowXAxisSlider, - showBarChart, - setShowBarChart, - selectedXAxisRange, - setSelectedXAxisRange, -}: LineChartModalProps) { + alternativeSwitchButtonProps, + sliderProps, + showSlider, + setShowSlider, +}: ChartModalProps) { const chartRef = useRef(null); - // full screen modal by the 'LineChart' component that can be opened if `expandable==true`; - // offers a larger chart and an additional features (see buttons) return (

      {title}

      - {xAxisSlider && ( - + {sliderProps && showSlider && setShowSlider && ( + )} - {barChartSwitch && ( - + {alternativeSwitchButtonProps && ( + )} - {!disableDownload && } + {!disableDownload && } {/* close model button */} @@ -92,15 +81,11 @@ export function LineChartModal({
      { - // slider to manipulate the plotted x-axis range of the chart; can be disabled via `xAxisSlider` - showXAxisSlider && ( + // slider to e.g. manipulate the plotted x-axis range of the chart + sliderProps && (
      - +
      ) diff --git a/src/components/Charts/helpers/ChartSlider.tsx b/src/components/Charts/helpers/ChartSlider.tsx new file mode 100644 index 00000000..97564604 --- /dev/null +++ b/src/components/Charts/helpers/ChartSlider.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { Slider } from '@nextui-org/slider'; + +import { ChartSliderProps } from '@/domain/props/ChartContainerProps'; + +/** + * This component is tied to the `ChartContainer` and `ChartModal` component and should not be used independently. + * It renders a simple NextUI slider. + */ +export default function ChartSlider({ + title, + sliderMin, + sliderMax, + selectedSliderRange, + setSelectedSliderRange, +}: ChartSliderProps) { + return ( +
      +

      {title || ''}

      + setSelectedSliderRange(e as number[])} + color="secondary" + size="sm" + showOutline + classNames={{ + base: 'max-w-md', + track: 'bg-clickableSecondary h-0.5', + filler: 'bg-surfaceGrey', + step: 'bg-clickableSecondary data-[in-range=true]:bg-surfaceGrey h-1.5 w-0.5', + thumb: 'w-5 h-5 bg-clickableSecondary data-[dragging=true]:bg-primary', + }} + /> +
      + ); +} diff --git a/src/components/Charts/helpers/LineChartBarLineSwitchButton.tsx b/src/components/Charts/helpers/LineChartBarLineSwitchButton.tsx deleted file mode 100644 index 09e72e70..00000000 --- a/src/components/Charts/helpers/LineChartBarLineSwitchButton.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Button } from '@nextui-org/button'; -import { Chart, Diagram } from 'iconsax-react'; - -import { Tooltip } from '@/components/Tooltip/Tooltip'; -import { LineChartBarLineSwitchButtonProps } from '@/domain/props/LineChartProps'; - -/** - * This component is tied to the `LineChart` and `LineChartModal` component - * and should not be used independently. - */ -export default function LineChartBarLineSwitchButton({ - showBarChart, - setShowBarChart, - size, -}: LineChartBarLineSwitchButtonProps) { - return ( - - - - ); -} diff --git a/src/components/Charts/helpers/LineChartXAxisSlider.tsx b/src/components/Charts/helpers/LineChartXAxisSlider.tsx deleted file mode 100644 index 90d29448..00000000 --- a/src/components/Charts/helpers/LineChartXAxisSlider.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Slider } from '@nextui-org/slider'; - -import { LineChartXAxisSliderProps } from '@/domain/props/LineChartProps'; -import LineChartOperations from '@/operations/charts/LineChartOperations.ts'; - -/** - * This component is tied to the `LineChart` and `LineChartModal` component - * and should not be used independently. - * It renders a NextUI slider with which the shown x-axis range of the chart - * within `LineChart` and `LineChartModal` can be manipulated. - */ -export default function LineChartXAxisSlider({ - selectedXAxisRange, - setSelectedXAxisRange, - data, -}: LineChartXAxisSliderProps) { - const xAxisValues: number[] = LineChartOperations.getDistinctXAxisValues(data); - - return ( -
      -

      Adjusting x-axis range:

      - setSelectedXAxisRange(e as number[])} - color="secondary" - size="sm" - showOutline - classNames={{ - base: 'max-w-md', - track: 'bg-clickableSecondary h-0.5', - filler: 'bg-surfaceGrey', - step: 'bg-clickableSecondary data-[in-range=true]:bg-surfaceGrey h-1.5 w-0.5', - thumb: 'w-5 h-5 bg-clickableSecondary data-[dragging=true]:bg-primary', - }} - /> -
      - ); -} diff --git a/src/components/Charts/helpers/buttons/ChartAlternativeSwitchButton.tsx b/src/components/Charts/helpers/buttons/ChartAlternativeSwitchButton.tsx new file mode 100644 index 00000000..962e965f --- /dev/null +++ b/src/components/Charts/helpers/buttons/ChartAlternativeSwitchButton.tsx @@ -0,0 +1,33 @@ +import { Button } from '@nextui-org/button'; +import React from 'react'; + +import { Tooltip } from '@/components/Tooltip/Tooltip'; +import { ChartAlternativeSwitchButtonProps } from '@/domain/props/ChartContainerProps'; +import ChartDownloadButtonOperations from '@/operations/charts/ChartAlternativeSwitchButtonOperations'; + +/** + * This component is tied to the `ChartContainer` and `ChartModal` component and should not be used independently. + */ +export default function ChartAlternativeSwitchButton({ + defaultChartType, + alternativeChartType, + showAlternativeChart, + setShowAlternativeChart, + size = 4, +}: ChartAlternativeSwitchButtonProps) { + const chartSwitchTitle = showAlternativeChart + ? ChartDownloadButtonOperations.getChartTypeTitle(defaultChartType) + : ChartDownloadButtonOperations.getChartTypeTitle(alternativeChartType); + + const chartSwitchIcon = showAlternativeChart + ? ChartDownloadButtonOperations.getChartTypeIcon(defaultChartType, size) + : ChartDownloadButtonOperations.getChartTypeIcon(alternativeChartType, size); + + return ( + + + + ); +} diff --git a/src/components/Charts/helpers/LineChartDownloadButton.tsx b/src/components/Charts/helpers/buttons/ChartDownloadButton.tsx similarity index 67% rename from src/components/Charts/helpers/LineChartDownloadButton.tsx rename to src/components/Charts/helpers/buttons/ChartDownloadButton.tsx index 777acec0..d36f76d7 100644 --- a/src/components/Charts/helpers/LineChartDownloadButton.tsx +++ b/src/components/Charts/helpers/buttons/ChartDownloadButton.tsx @@ -3,21 +3,20 @@ import { Popover, PopoverContent, PopoverTrigger } from '@nextui-org/popover'; import { DocumentDownload, GalleryImport } from 'iconsax-react'; import { Tooltip } from '@/components/Tooltip/Tooltip'; -import { LineChartDownloadButtonProps } from '@/domain/props/LineChartProps'; -import LineChartOperations from '@/operations/charts/LineChartOperations.ts'; +import { ChartDownloadButtonProps } from '@/domain/props/ChartContainerProps'; +import ChartDownloadButtonOperations from '@/operations/charts/ChartDownloadButtonOperations.ts'; /** - * This component is tied to the `LineChart` and `LineChartModal` component - * and should not be used independently. + * This component is tied to the `ChartContainer` and `ChartModal` component and should not be used independently. * It renders a button to open a dropdown menu to download the chart as csv, png, etc. */ -export default function LineChartDownloadButton({ chartRef, lineChartData }: LineChartDownloadButtonProps) { +export default function ChartDownloadButton({ chartRef, chartData, size }: ChartDownloadButtonProps) { return ( @@ -27,7 +26,7 @@ export default function LineChartDownloadButton({ chartRef, lineChartData }: Lin size="sm" className="w-full justify-start" onPress={() => { - if (chartRef.current) LineChartOperations.downloadChartPNG(chartRef.current); + if (chartRef.current) ChartDownloadButtonOperations.downloadChartPNG(chartRef.current); }} startContent={} > @@ -38,7 +37,7 @@ export default function LineChartDownloadButton({ chartRef, lineChartData }: Lin size="sm" className="w-full justify-start" onPress={() => { - if (chartRef.current) LineChartOperations.downloadChartDataSVG(chartRef.current); + if (chartRef.current) ChartDownloadButtonOperations.downloadChartDataSVG(chartRef.current); }} startContent={} > @@ -49,7 +48,7 @@ export default function LineChartDownloadButton({ chartRef, lineChartData }: Lin size="sm" className="w-full justify-start" onPress={() => { - if (chartRef.current) LineChartOperations.downloadChartDataCSV(chartRef.current); + if (chartRef.current) ChartDownloadButtonOperations.downloadChartDataCSV(chartRef.current); }} startContent={} > @@ -60,7 +59,7 @@ export default function LineChartDownloadButton({ chartRef, lineChartData }: Lin size="sm" className="w-full justify-start" onPress={() => { - LineChartOperations.downloadDataJSON(lineChartData); + ChartDownloadButtonOperations.downloadDataJSON(chartData); }} startContent={} > diff --git a/src/components/Charts/helpers/LineChartSliderButton.tsx b/src/components/Charts/helpers/buttons/ChartSliderButton.tsx similarity index 52% rename from src/components/Charts/helpers/LineChartSliderButton.tsx rename to src/components/Charts/helpers/buttons/ChartSliderButton.tsx index b68dae90..b9337eaa 100644 --- a/src/components/Charts/helpers/LineChartSliderButton.tsx +++ b/src/components/Charts/helpers/buttons/ChartSliderButton.tsx @@ -2,17 +2,12 @@ import { Button } from '@nextui-org/button'; import { Settings } from 'iconsax-react'; import { Tooltip } from '@/components/Tooltip/Tooltip'; -import { LineChartSliderButtonProps } from '@/domain/props/LineChartProps'; +import { LineChartSliderButtonProps } from '@/domain/props/ChartContainerProps'; /** - * This component is tied to the `LineChart` and `LineChartModal` component - * and should not be used independently. + * This component is tied to the `ChartContainer` and `ChartModal` component and should not be used independently. */ -export default function LineChartSliderButton({ - showXAxisSlider, - setShowXAxisSlider, - size, -}: LineChartSliderButtonProps) { +export default function ChartSliderButton({ showSlider, setShowSlider, size }: LineChartSliderButtonProps) { return (