Update Explosion plot widget with event counts and add theme support
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -83,5 +83,6 @@ export const WIDGET_REGISTRY = {
|
|||||||
allowedSizes: {
|
allowedSizes: {
|
||||||
medium: { w: 6, h: 12 }
|
medium: { w: 6, h: 12 }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -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" }}
|
||||||
|
|||||||
Reference in New Issue
Block a user