diff --git a/client/src/components/Widgets/AlertChanges.jsx b/client/src/components/Widgets/AlertChanges.jsx
new file mode 100644
index 0000000..9f6dbd2
--- /dev/null
+++ b/client/src/components/Widgets/AlertChanges.jsx
@@ -0,0 +1,219 @@
+import {useDashboardStore} from "@/store/useDashboardStore.jsx";
+import VdapApi from "../../hooks/useVdapApi.js";
+import useBuildUrl from "../../hooks/useBuildUrl.js";
+import { Footprints, Plane, Sailboat, TrendingUp, TrendingDown } from 'lucide-react';
+import {Box, Collapsible, Flex, Spinner, Text, Timeline, Center} from "@chakra-ui/react";
+import { useMemo, useCallback } from "react";
+
+
+export default function AlertChanges({ settings, widgetID}) {
+ // Find this specific widgets object in the state list of active & displayed widgets
+ const thisWidget = useDashboardStore(useCallback((state) =>
+ state.widgets.find((widget) => widget.id === widgetID), [widgetID]
+ ));
+
+ // build url, default is 7 days past
+ const url = useBuildUrl('/api/v2/alert_changes', settings?.startDate, settings?.endDate);
+
+ // call api hook to retrieve data
+ const {data: apiData, isLoading, error} = VdapApi(url);
+
+ const data = useMemo(() => {
+ if (apiData.length === 0) return [];
+
+ // .reduce() is a built-in array method used to transform an entire array into a single value (like an object)
+ // here it returns a single massive object w/ volcano names as keys
+ const grouped = apiData.reduce((accumulator, item) => {
+ if (!accumulator[item.volcano]) accumulator[item.volcano] = [];
+ accumulator[item.volcano].push(item);
+ return accumulator;
+ }, {});
+
+ // converts the 'grouped' object back into an array of [key, value] pairs
+ const group_arr = Object.entries(grouped);
+
+ // sort data newest to oldest within each volcano
+ const sorted = group_arr.map((item) =>
+ [
+ item[0], // volcano name
+ [...(item[1])].sort((a, b) =>
+ new Date(b.alert_start_date_local) - new Date(a.alert_start_date_local))
+ ]
+ );
+
+ // volcano with most recent alert level change appears at top of list
+ const sorted_entries = [...sorted].sort((a, b) => new Date(b[1][0].alert_start_date_local) - new Date(a[1][0].alert_start_date_local));
+
+ if (thisWidget?.size === "small") {
+ return (sorted_entries.map((volcano) => {
+ const domain = volcano[1].reduce((acc, curr) => {
+ if (!acc[curr.alert_scheme_domain]) acc[curr.alert_scheme_domain] = [];
+ acc[curr.alert_scheme_domain].push(curr);
+ return acc;
+ }, {});
+
+ const domain_arr = Object.entries(domain);
+ return [volcano[0], domain_arr];
+ }));
+ }
+
+ return sorted_entries;
+ }, [apiData, thisWidget]);
+
+ // list of alert colors used for coloring text below
+ const alertColors = {
+ Red: "red.400",
+ Orange: "orange.400",
+ Yellow: "yellow.400",
+ Green: "green.400",
+ };
+
+ if (isLoading) return (
+
+
+
+ );
+
+ if (error) return (
+
+ {error}
+
+ );
+
+ if (data.length === 0) return (
+
+ No flight level data available for the selected dates.
+
+ );
+
+ return (
+
+ {data.map((volcano, index) => {
+ if (thisWidget?.size === "large") return (
+
+ {/* Using context to dynamically change the border radii of the triggers */}
+
+ {(collapsible) => (
+ <>
+
+
+
+ {volcano[0]} {/* volcano name */}
+ {volcano[1][0]?.alert_start_date_local}
+
+
+ {volcano[1][0]?.alert_level_name}
+
+
+
+
+
+ {volcano[1]?.map((object, index) => (
+
+
+ {/**/}
+
+ {object?.alert_scheme_domain === "terrestrial" && }
+ {object?.alert_scheme_domain === "aviation" && }
+ {object?.alert_scheme_domain === "maritime" && }
+
+
+ {object?.change_type === "increase" ? : }
+
+
+
+ {/*{object?.volcano}*/}
+
+
+ {object?.alert_start_date_local}
+
+ {object?.alert_level_name}
+
+
+ Alert Scale:
+ {(Array.from({ length: Number(object?.alert_scheme_highest) - Number(object?.alert_scheme_lowest) + 1}, (v, i) => Number(object?.alert_scheme_lowest) + i))
+ .map((num) => (
+
+ {num}
+
+ ))
+ }
+
+
+
+
+
+ ))}
+
+
+ >
+ )}
+
+
+ )
+ else if (thisWidget?.size === "small") return (
+ volcano[1]?.map((domain, index) => (
+
+
+
+
+ {domain[0] === "aviation" && }
+ {domain[0] === "terrestrial" && }
+ {domain[0] === "maritime" && }
+
+ {volcano[0] || ""} {/* volcano name */}
+
+ {domain[1]?.[0]?.alert_start_date_local || ""}
+
+
+
+
+ {domain[1]?.[0]?.change_type === "increase" ? : }
+
+
+ {domain[1]?.[0]?.alert_level_name}
+
+
+
+ ))
+ );
+ })}
+
+ )
+}
\ No newline at end of file
diff --git a/client/src/components/Widgets/ExplosionEvents.jsx b/client/src/components/Widgets/ExplosionEvents.jsx
index 17e6b68..2ddbf51 100644
--- a/client/src/components/Widgets/ExplosionEvents.jsx
+++ b/client/src/components/Widgets/ExplosionEvents.jsx
@@ -2,7 +2,8 @@ import { useMemo } from 'react';
import { Box, Flex, Spinner, Text } from '@chakra-ui/react';
import { useColorMode } from "@/components/ui/color-mode.jsx";
import Plot from 'react-plotly.js';
-import VdapApi from '../../hooks/useVdapApi.js'; // Adjust path as needed
+import VdapApi from '../../hooks/useVdapApi.js';
+import useBuildUrl from "@/hooks/useBuildUrl.js";
export default function ExplosionEvents({ settings, widgetID }) {
// Plotly expects 'linear' for normal axes
@@ -12,22 +13,8 @@ export default function ExplosionEvents({ settings, widgetID }) {
const { colorMode } = useColorMode();
const isDark = colorMode === 'dark';
- // Generate url with input dates are default to 7 day range
- const url = useMemo(() =>{
- if (settings?.startDate && settings?.endDate)
- return '/api/v2/explosion_events?start_date=' + settings?.startDate + '&end_date=' + settings?.endDate;
- else {
- const base_url = '/api/v2/explosion_events';
- let today = new Date();
- let sevenDaysAgo = new Date(today);
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
-
- sevenDaysAgo = sevenDaysAgo.toLocaleDateString('en-CA');
- today = today.toLocaleDateString('en-CA');
-
- return base_url + '?start_date=' + sevenDaysAgo + '&end_date=' + today;
- }
- }, [settings?.startDate, settings?.endDate]);
+ // Generate url with input dates, default to 7 day range
+ const url = useBuildUrl('/api/v2/explosion_events', settings?.startDate, settings?.endDate);
// Clean, single-line data fetching
const { data: apiData, isLoading, error } = VdapApi(url);
diff --git a/client/src/components/Widgets/IvansAlerts.jsx b/client/src/components/Widgets/IvansAlerts.jsx
index d0054a7..b6982a5 100644
--- a/client/src/components/Widgets/IvansAlerts.jsx
+++ b/client/src/components/Widgets/IvansAlerts.jsx
@@ -2,6 +2,7 @@ import {Button, Flex, Spinner, Text, Box, Collapsible} from "@chakra-ui/react";
import {useEffect, useMemo } from 'react';
import {useDashboardStore} from "@/store/useDashboardStore.jsx";
import VdapApi from '../../hooks/useVdapApi.js';
+import useBuildUrl from "@/hooks/useBuildUrl.js";
export default function IvansAlerts({ settings, widgetID }) {
console.log("IvansAlerts widget ID: ", widgetID);
@@ -9,22 +10,8 @@ export default function IvansAlerts({ settings, widgetID }) {
const isCopy = settings?.copyToClipboard === true; // acts as a fail-safe if no default setting is set
const sorts = settings?.activeSorts;
- // build url once on mount
- const url = useMemo(() =>{
- if (settings?.startDate && settings?.endDate)
- return '/api/v2/comm_coor_ivans?start_date=' + settings?.startDate + '&end_date=' + settings?.endDate;
- else {
- const base_url = '/api/v2/comm_coor_ivans';
- let today = new Date();
- let sevenDaysAgo = new Date(today);
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
-
- sevenDaysAgo = sevenDaysAgo.toLocaleDateString('en-CA');
- today = today.toLocaleDateString('en-CA');
-
- return base_url + '?start_date=' + sevenDaysAgo + '&end_date=' + today;
- }
- }, [settings?.startDate, settings?.endDate]);
+ // build url, default is 7 days past
+ const url = useBuildUrl('/api/v2/comm_coor_ivans', settings?.startDate, settings?.endDate);
// VdapApi hook only calls api if the url changes and apiData remains stable since vdapApi uses state to track the data
const {data: apiData, isLoading, error} = VdapApi(url);
@@ -145,7 +132,7 @@ export default function IvansAlerts({ settings, widgetID }) {
if (data.length === 0) return (
- No flight level data available this week.
+ No flight level data available for the selected dates.
);
diff --git a/client/src/components/Widgets/PlumeEvents.jsx b/client/src/components/Widgets/PlumeEvents.jsx
index e61def1..925a96d 100644
--- a/client/src/components/Widgets/PlumeEvents.jsx
+++ b/client/src/components/Widgets/PlumeEvents.jsx
@@ -2,24 +2,13 @@ import VdapApi from '../../hooks/useVdapApi.js';
import { useMemo } from 'react';
import Plot from 'react-plotly.js';
import {Box, Flex, Spinner, Text} from "@chakra-ui/react";
+import useBuildUrl from "@/hooks/useBuildUrl.js";
export default function PlumeEvents({ settings, widgetID}) {
- const url = useMemo(() =>{
- if (settings?.startDate && settings?.endDate)
- return '/api/v2/plume_events?start_date=' + settings?.startDate + '&end_date=' + settings?.endDate;
- else {
- const base_url = '/api/v2/plume_events';
- let today = new Date();
- let sevenDaysAgo = new Date(today);
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
-
- sevenDaysAgo = sevenDaysAgo.toLocaleDateString('en-CA');
- today = today.toLocaleDateString('en-CA');
-
- return base_url + '?start_date=' + sevenDaysAgo + '&end_date=' + today;
- }
- }, [settings?.startDate, settings?.endDate]);
+ // Build url, defaults to 7 days past
+ const url = useBuildUrl('/api/v2/plume_events', settings?.startDate, settings?.endDate);
+ // Retrieve data from api
const { data: apiData, isLoading, error } = VdapApi(url);
const plotTraces = useMemo(() => {
diff --git a/client/src/components/Widgets/RemoteSensing.jsx b/client/src/components/Widgets/RemoteSensing.jsx
index d2dac1a..2aadaf8 100644
--- a/client/src/components/Widgets/RemoteSensing.jsx
+++ b/client/src/components/Widgets/RemoteSensing.jsx
@@ -2,6 +2,7 @@ import {Button, Flex, Spinner, Text, Box, Collapsible} from "@chakra-ui/react";
import {useEffect, useMemo } from 'react';
import {useDashboardStore} from "@/store/useDashboardStore.jsx";
import VdapApi from '../../hooks/useVdapApi.js';
+import useBuildUrl from "@/hooks/useBuildUrl.js";
export default function RemoteSensing({ settings, widgetID }) {
console.log("RS widget ID: ", widgetID);
@@ -9,22 +10,8 @@ export default function RemoteSensing({ settings, widgetID }) {
const isCopy = settings?.copyToClipboard === true; // acts as a fail-safe if no default setting is set
const sorts = settings?.activeSorts;
- // build url
- const url = useMemo(() =>{
- if (settings?.startDate && settings?.endDate)
- return '/api/v2/remote_sensing?start_date=' + settings?.startDate + '&end_date=' + settings?.endDate;
- else {
- const base_url = '/api/v2/remote_sensing';
- let today = new Date();
- let sevenDaysAgo = new Date(today);
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
-
- sevenDaysAgo = sevenDaysAgo.toLocaleDateString('en-CA');
- today = today.toLocaleDateString('en-CA');
-
- return base_url + '?start_date=' + sevenDaysAgo + '&end_date=' + today;
- }
- }, [settings?.startDate, settings?.endDate]);
+ // build url, defaults to 7 days past
+ const url = useBuildUrl('/api/v2/remote_sensing', settings?.startDate, settings?.endDate);
// VdapApi hook only calls api if the url changes and apiData remains stable since vdapApi uses state to track the data
const {data: apiData, isLoading, error} = VdapApi(url);
diff --git a/client/src/components/Widgets/SeismicEvents.jsx b/client/src/components/Widgets/SeismicEvents.jsx
index 037ab48..d01dd75 100644
--- a/client/src/components/Widgets/SeismicEvents.jsx
+++ b/client/src/components/Widgets/SeismicEvents.jsx
@@ -2,25 +2,13 @@ import VdapApi from '../../hooks/useVdapApi.js';
import { useMemo } from 'react';
import Plot from 'react-plotly.js';
import {Box, Flex, Spinner, Text} from "@chakra-ui/react";
+import useBuildUrl from "@/hooks/useBuildUrl.js";
export default function SeismicEvents({ settings, widgetID}) {
- const url = useMemo(() =>{
- if (settings?.startDate && settings?.endDate)
- return '/api/v2/seismic_events?start_date=' + settings?.startDate + '&end_date=' + settings?.endDate;
- else {
- const base_url = '/api/v2/seismic_events';
- let today = new Date();
- let sevenDaysAgo = new Date(today);
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
+ // build url, defaults to 7 days past
+ const url = useBuildUrl('/api/v2/seismic_events', settings?.startDate, settings?.endDate);
- sevenDaysAgo = sevenDaysAgo.toLocaleDateString('en-CA');
- today = today.toLocaleDateString('en-CA');
-
- return base_url + '?start_date=' + sevenDaysAgo + '&end_date=' + today;
- }
- }, [settings?.startDate, settings?.endDate]);
-
- console.log(url);
+ // retrieve data from api
const { data: apiData, isLoading, error } = VdapApi(url);
const plotTraces = useMemo(() => {
diff --git a/client/src/components/Widgets/settings/AlertChangeSettings.jsx b/client/src/components/Widgets/settings/AlertChangeSettings.jsx
new file mode 100644
index 0000000..bc1103b
--- /dev/null
+++ b/client/src/components/Widgets/settings/AlertChangeSettings.jsx
@@ -0,0 +1,36 @@
+import {Flex, Slider, Switch} from "@chakra-ui/react";
+import {useDashboardStore} from "@/store/useDashboardStore.jsx";
+
+export default function IvansAlertsSettings({ widgetId, currentSettings }) {
+ const updateSettings = useDashboardStore(state => state.updateWidgetSettings);
+
+ return (
+
+ {/* SLIDER DATE RANGE SELECTION */}
+ {
+ const day = new Date(Date.now() - (e.value[0])*86400000).toLocaleDateString('en-CA');
+ console.log("new start: ", day);
+ updateSettings(widgetId, {...currentSettings, startDate: day});
+ }}
+ >
+ {currentSettings?.endDate} to {currentSettings?.startDate}
+
+
+
+
+
+
+ today
+ 1 week ago
+ 2 weeks ago
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/client/src/constants/widgetRegistry.jsx b/client/src/constants/widgetRegistry.jsx
index 039763d..dd6fbfe 100644
--- a/client/src/constants/widgetRegistry.jsx
+++ b/client/src/constants/widgetRegistry.jsx
@@ -7,6 +7,9 @@ import { lazy } from 'react';
// w: 4 = one-third screen width
// w: 3 = one-quarter screen width
+const getStartDate = () => (new Date(Date.now() - 604800000)).toLocaleDateString('en-CA'); // 7 days = 604800000 = 7*24*60*60*1000
+const getEndDate = () => (new Date()).toLocaleDateString('en-CA'); // 'en-CA' formats the date as YYYY-MM-DD
+
export const WIDGET_REGISTRY = {
"vaa_activity": {
name: "VAA 7 Day Activity",
@@ -19,7 +22,9 @@ export const WIDGET_REGISTRY = {
// The baseline data every new VAA widget starts with
defaultSettings: {
- yAxisScale: 'normal' // Options: 'normal' | 'symlog'
+ // startDate: getStartDate(),
+ // endDate: getEndDate(),
+ yAxisScale: 'normal', // Options: 'normal' | 'symlog'
},
allowedSizes: {
@@ -44,6 +49,8 @@ export const WIDGET_REGISTRY = {
component: lazy(() => import('../components/Widgets/IvansAlerts.jsx')),
settingsComponent: lazy(() => import('../components/Widgets/settings/IvansAlertsSettings.jsx')),
defaultSettings: {
+ startDate: getStartDate(),
+ endDate: getEndDate(),
copyToClipboard: false,
activeSorts: {
timeSort: "desc",
@@ -64,6 +71,8 @@ export const WIDGET_REGISTRY = {
component: lazy(() => import('../components/Widgets/RemoteSensing.jsx')),
settingsComponent: lazy(() => import('../components/Widgets/Settings/RemoteSensingSettings.jsx')),
defaultSettings: {
+ startDate: getStartDate(),
+ endDate: getEndDate(),
copyToClipboard: false,
activeSorts: {
timeSort: "desc",
@@ -81,9 +90,15 @@ export const WIDGET_REGISTRY = {
name: "Alert Level Changes",
category: "Daily Alerts",
description: "List of 7-day alert level changes with trend arrows.",
+ component: lazy(() => import('../components/Widgets/AlertChanges.jsx')),
+ settingsComponent: lazy(() => import('../components/Widgets/settings/AlertChangeSettings.jsx')),
+ defaultSettings: {
+ startDate: getStartDate(),
+ endDate: getEndDate(),
+ },
allowedSizes: {
- small: { w: 3, h: 12 },
- large: { w: 9, h: 12 }
+ small: { w: 3, h: 6 },
+ large: { w: 3, h: 12 }
}
},
@@ -99,6 +114,8 @@ export const WIDGET_REGISTRY = {
// The baseline data every new VAA widget starts with
defaultSettings: {
+ startDate: getStartDate(),
+ endDate: getEndDate(),
yAxisScale: 'normal' // Options: 'normal' | 'symlog'
},
allowedSizes: {
@@ -112,7 +129,10 @@ export const WIDGET_REGISTRY = {
description: "Plot of explosion heights over a weeks time.",
component: lazy(() => import('../components/Widgets/PlumeEvents.jsx')),
settingsComponent: lazy(() => import('../components/Widgets/settings/PlumeEventsSettings.jsx')),
- defaultSettings: {},
+ defaultSettings: {
+ startDate: getStartDate(),
+ endDate: getEndDate(),
+ },
allowedSizes: {
medium: { w: 6, h: 12 }
}
@@ -124,7 +144,10 @@ export const WIDGET_REGISTRY = {
description: "Plot of number of seismic events over a weeks time.",
component: lazy(() => import('../components/Widgets/SeismicEvents.jsx')),
settingsComponent: lazy(() => import('../components/Widgets/settings/SeismicEventsSettings.jsx')),
- defaultSettings: {},
+ defaultSettings: {
+ startDate: getStartDate(),
+ endDate: getEndDate(),
+ },
allowedSizes: {
medium: { w: 6, h: 12 }
}
diff --git a/client/src/hooks/useBuildUrl.js b/client/src/hooks/useBuildUrl.js
new file mode 100644
index 0000000..3e99e3b
--- /dev/null
+++ b/client/src/hooks/useBuildUrl.js
@@ -0,0 +1,16 @@
+import { useMemo } from "react";
+
+export default function useBuildUrl(base_url, start, end) {
+ return useMemo(() =>{
+ if (start && end)
+ return `${base_url}?start_date=${start}&end_date=${end}`;
+ else {
+ let today = new Date();
+ let sevenDaysAgo = new Date(today);
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
+ sevenDaysAgo = sevenDaysAgo.toLocaleDateString('en-CA');
+ today = today.toLocaleDateString('en-CA');
+ return `${base_url}?start_date=${sevenDaysAgo}&end_date=${today}`;
+ }
+ }, [base_url, start, end]);
+}
\ No newline at end of file