Update Explosion plot widget with event counts and add theme support

This commit is contained in:
2026-07-15 13:56:12 -07:00
parent e0de9800c4
commit ccfbb15434
8 changed files with 36 additions and 24 deletions
@@ -1,12 +1,16 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Box, Flex, Spinner, Text } from '@chakra-ui/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 Plot from 'react-plotly.js';
import VdapApi from '../../hooks/useVdapApi.js'; // Adjust path as needed import VdapApi from '../../hooks/useVdapApi.js'; // Adjust path as needed
export default function ExplosionEvents({ settings }) { export default function ExplosionEvents({ settings }) {
console.log("3. Chart received settings:", settings);
// Plotly expects 'linear' for normal axes // Plotly expects 'linear' for normal axes
const plotYAxisType = settings?.yAxisScale === 'symlog' ? 'symlog' : 'linear'; const plotYAxisType = settings?.yAxisScale === 'log' ? 'log' : 'linear';
// 🎨 Let's check color mode so we can make people happy with light and dark
const { colorMode } = useColorMode();
const isDark = colorMode === 'dark';
// Clean, single-line data fetching // Clean, single-line data fetching
const { data: apiData, isLoading, error } = VdapApi('/api/v2/explosion_events?start_date=2025-05-01&end_date=2025-05-30'); const { data: apiData, isLoading, error } = VdapApi('/api/v2/explosion_events?start_date=2025-05-01&end_date=2025-05-30');
@@ -23,16 +27,17 @@ export default function ExplosionEvents({ settings }) {
'hexagon', 'octagon', 'cross', 'x' 'hexagon', 'octagon', 'cross', 'x'
]; ];
// Sort data descending by flight level (matching Observable behavior) // Sort data descending by number of events so smaller dots draw on top of larger ones
const sortedData = [...apiData].sort((a, b) => b.obs_fl - a.obs_fl); const sortedData = [...apiData].sort((a, b) => b.number_events - a.number_events);
// Efficiently group into Plotly traces by volcano // Efficiently group into Plotly traces by volcano
const groupedData = sortedData.reduce((acc, curr) => { const groupedData = sortedData.reduce((acc, curr) => {
const name = curr.volc_name || 'Unknown'; const name = curr.volcano_name || 'Unknown';
if (!acc[name]) acc[name] = { x: [], y: [] }; if (!acc[name]) acc[name] = { x: [], y: [] };
acc[name].x.push(new Date(curr.obs_va_epoch)); // Map the explosion-specific keys
acc[name].y.push(curr.obs_fl); acc[name].x.push(new Date(curr.ts_epoch));
acc[name].y.push(curr.number_events);
return acc; return acc;
}, {}); }, {});
@@ -47,11 +52,11 @@ export default function ExplosionEvents({ settings }) {
line: { width: .5, color: 'rgba(255, 255, 255, 0.7)' }, line: { width: .5, color: 'rgba(255, 255, 255, 0.7)' },
symbol: Symbolway[index % Symbolway.length], symbol: Symbolway[index % Symbolway.length],
}, },
// Replicating the clean Observable tooltip // Replicating the clean D3/Observable tooltip
hovertemplate: hovertemplate:
`<b>${volcanoName}</b><br>` + `<b>${volcanoName}</b><br>` +
'Date: %{x|%b %d %Y %H:%M}<br>' + 'Date (UTC): %{x|%b %d %Y %H:%M}<br>' +
'Flight Level: %{y}<extra></extra>' 'Number of Explosions: %{y}<extra></extra>'
})); }));
}, [apiData]); }, [apiData]);
@@ -75,14 +80,14 @@ export default function ExplosionEvents({ settings }) {
font: { color: 'white', size: 12 } font: { color: 'white', size: 12 }
}, },
type: 'date', type: 'date',
tickformat: '%b %d<br>%Y %H:%M', // Multiline ticks from Observable tickformat: '%b %d<br>%Y', // D3 requested %b %d \n %Y formatting
gridcolor: '#4b5563', gridcolor: '#4b5563',
zerolinecolor: '#4b5563', zerolinecolor: '#4b5563',
automargin: true, automargin: true,
}, },
yaxis: { yaxis: {
title: { title: {
text: 'Flight Level', text: 'Number of Explosions',
font: { color: 'white', size: 12 } font: { color: 'white', size: 12 }
}, },
type: plotYAxisType, type: plotYAxisType,
@@ -126,12 +131,12 @@ export default function ExplosionEvents({ settings }) {
if (plotTraces.length === 0) return ( if (plotTraces.length === 0) return (
<Flex height="100%" width="100%" align="center" justify="center" bg="gray.800" borderRadius="md"> <Flex height="100%" width="100%" align="center" justify="center" bg="gray.800" borderRadius="md">
<Text color="gray.400">No flight level data available.</Text> <Text color="gray.400">No data available for current filter.</Text>
</Flex> </Flex>
); );
return ( return (
<Box height="100%" width="100%" bg="gray.800" borderRadius="md" p={2} overflow="hidden"> <Box height="100%" width="100%" bg="transparent" borderRadius="md" p={2} overflow="hidden">
<Plot <Plot
key={plotYAxisType} key={plotYAxisType}
data={plotTraces} data={plotTraces}
@@ -5,14 +5,14 @@ export default function ExplosionEventsSettings({ widgetId, currentSettings }) {
const updateSettings = useDashboardStore(state => state.updateWidgetSettings); const updateSettings = useDashboardStore(state => state.updateWidgetSettings);
// 1. Convert the saved string into a boolean for the toggle UI // 1. Convert the saved string into a boolean for the toggle UI
const isSymlog = currentSettings?.yAxisScale === 'symlog'; const isSymlog = currentSettings?.yAxisScale === 'log';
const handleToggle = (details) => { const handleToggle = (details) => {
// 2. Chakra v3 passes { checked: boolean } in the details object. // 2. Chakra v3 passes { checked: boolean } in the details object.
// Convert it back to the string Plotly expects. // Convert it back to the string Plotly expects.
console.log("1. Toggle Clicked! Raw details from Chakra:", details); console.log("1. Toggle Clicked! Raw details from Chakra:", details);
const newScale = details.checked ? 'symlog' : 'normal'; const newScale = details.checked ? 'log' : 'normal';
updateSettings(widgetId, { yAxisScale: newScale }); updateSettings(widgetId, { yAxisScale: newScale });
}; };
@@ -30,7 +30,7 @@ export default function ExplosionEventsSettings({ widgetId, currentSettings }) {
<Switch.Control> <Switch.Control>
<Switch.Thumb /> <Switch.Thumb />
</Switch.Control> </Switch.Control>
<Switch.Label>Use Symlog Scale</Switch.Label> <Switch.Label>Use Log Scale</Switch.Label>
</Switch.Root> </Switch.Root>
</Box> </Box>
); );
@@ -1,5 +1,5 @@
import { Box, Menu, Portal } from '@chakra-ui/react' import { Box, Menu, Portal } from '@chakra-ui/react'
import AlertLevel from "@/components/Widgets/AlertLevelWidget.jsx"; import AlertLevel from "@/components/deprecated/AlertLevelWidget.jsx";
export default function FirstWidgetButton({ appendWidget }) { export default function FirstWidgetButton({ appendWidget }) {
const triggerItem = ( const triggerItem = (
@@ -1,5 +1,5 @@
import { Menu, Portal } from "@chakra-ui/react"; import { Menu, Portal } from "@chakra-ui/react";
import AlertLevel from "../Widgets/AlertLevelWidget.jsx"; import AlertLevel from "./AlertLevelWidget.jsx";
export default function WidgetMenu({appendWidget, triggerItem, positioning}) { export default function WidgetMenu({appendWidget, triggerItem, positioning}) {
return ( return (
+2 -1
View File
@@ -83,5 +83,6 @@ export const WIDGET_REGISTRY = {
allowedSizes: { allowedSizes: {
medium: { w: 6, h: 12 } medium: { w: 6, h: 12 }
} }
} },
}; };
+10 -4
View File
@@ -3,7 +3,7 @@ import { X, MoreVertical } from "lucide-react";
import { Responsive, WidthProvider } from "react-grid-layout"; import { Responsive, WidthProvider } from "react-grid-layout";
import { useDashboardStore } from "@/store/useDashboardStore.jsx"; import { useDashboardStore } from "@/store/useDashboardStore.jsx";
import { Suspense, useEffect, useRef } from 'react'; import { Suspense, useEffect, useRef } from 'react';
import { useColorModeValue } from "@/components/ui/color-mode";
// 1. Import database messenger // 1. Import database messenger
import useAuthStore from "@/store/authStore"; import useAuthStore from "@/store/authStore";
@@ -14,6 +14,12 @@ import { WIDGET_REGISTRY } from '@/constants/widgetRegistry.jsx';
const ResponsiveGridLayout = WidthProvider(Responsive); const ResponsiveGridLayout = WidthProvider(Responsive);
export default function MyDashboard() { export default function MyDashboard() {
// 🌗 Let's adjust for our light and dark mode
const frameBg = useColorModeValue("gray.100", "gray.800");
const titleColor = useColorModeValue("gray.800", "gray.100");
const iconColor = useColorModeValue("gray.600", "gray.400");
const iconHoverBg = useColorModeValue("gray.100", "gray.700");
// ☁️ Database Connection // ☁️ Database Connection
const { user } = useAuthStore(); const { user } = useAuthStore();
@@ -67,7 +73,7 @@ export default function MyDashboard() {
return ( return (
// The "Frame" that holds both the Title and the Chart // The "Frame" that holds both the Title and the Chart
<Flex direction="column" height="100%" width="100%" bg="gray.800"> <Flex direction="column" height="100%" width="100%" bg={frameBg}>
{/* THE TITLE BAR */} {/* THE TITLE BAR */}
<Flex <Flex
@@ -76,7 +82,7 @@ export default function MyDashboard() {
px={3} px={3}
py={2} py={2}
> >
<Text color="gray.100" fontWeight="semibold" fontSize="md" isTruncated> <Text color={titleColor} fontWeight="semibold" fontSize="md" isTruncated>
{registryEntry.name} {registryEntry.name}
</Text> </Text>
@@ -166,7 +172,7 @@ export default function MyDashboard() {
right={2} right={2}
size="xs" size="xs"
bg="white" bg="white"
color="gray.400" color={iconColor}
border="1px solid" border="1px solid"
borderColor="gray.200" borderColor="gray.200"
_hover={{ bg: "red.50", color: "red.500", borderColor: "red.200" }} _hover={{ bg: "red.50", color: "red.500", borderColor: "red.200" }}