Added plot of seismic events count with date picker feature in the settings
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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";
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
const { data: apiData, isLoading, error } = VdapApi(url);
|
||||
|
||||
const plotTraces = useMemo(() => {
|
||||
if (!apiData || apiData.length === 0) return [];
|
||||
|
||||
// Array of distinct Plotly shapes
|
||||
const Symbolway = [
|
||||
'circle', 'square', 'diamond',
|
||||
'triangle-up', 'triangle-down',
|
||||
'star', 'hexagram', 'pentagon',
|
||||
'hexagon', 'octagon', 'cross', 'x'
|
||||
];
|
||||
|
||||
// Sort data descending by seismic events count so smaller dots draw on top of larger ones
|
||||
const sortedData = [...apiData].sort((a, b) => b.number_events - a.number_events);
|
||||
|
||||
// Efficiently group into Plotly traces by volcano
|
||||
const groupedData = sortedData.reduce((acc, curr) => {
|
||||
const name = curr.volcano_name || 'Unknown';
|
||||
if (!acc[name]) acc[name] = { x: [], y: [] };
|
||||
|
||||
// Map the seismic-specific keys
|
||||
acc[name].x.push(new Date(curr.ts_epoch));
|
||||
acc[name].y.push(curr.number_events);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.keys(groupedData).map((volcanoName, index) => ({
|
||||
name: volcanoName,
|
||||
x: groupedData[volcanoName].x,
|
||||
y: groupedData[volcanoName].y,
|
||||
type: 'scatter',
|
||||
mode: 'markers',
|
||||
marker: {
|
||||
size: 7,
|
||||
line: { width: .5, color: 'rgba(255, 255, 255, 0.7)' },
|
||||
symbol: Symbolway[index % Symbolway.length],
|
||||
},
|
||||
// Replicating the clean D3/Observable tooltip
|
||||
hovertemplate:
|
||||
`<b>${volcanoName}</b><br>` +
|
||||
'Date (UTC): %{x|%b %d %Y %H:%M}<br>' +
|
||||
'Number of Seismic Events: %{y}<extra></extra>'
|
||||
}));
|
||||
}, [apiData]);
|
||||
|
||||
const layout = useMemo(() => {
|
||||
|
||||
return {
|
||||
autosize: true,
|
||||
paper_bgcolor: 'transparent', // Let Chakra UI background show through
|
||||
plot_bgcolor: 'transparent',
|
||||
font: { color: '#d1d5db' }, // gray.300
|
||||
margin: { t: 40, r: 20, l: 20, b: 20 },
|
||||
// title: {
|
||||
// text: '',
|
||||
// font: { size: 16, color: 'white' },
|
||||
// x: 0.05
|
||||
// },
|
||||
xaxis: {
|
||||
title: {
|
||||
text: 'Date (UTC)',
|
||||
font: { color: 'white', size: 12 }
|
||||
},
|
||||
type: 'date',
|
||||
tickformat: '%b %d<br>%Y', // D3 requested %b %d \n %Y formatting
|
||||
gridcolor: '#4b5563',
|
||||
zerolinecolor: '#4b5563',
|
||||
automargin: true,
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: 'Seismic Count',
|
||||
font: { color: 'white', size: 12 }
|
||||
},
|
||||
type: 'linear',
|
||||
gridcolor: '#4b5563',
|
||||
zerolinecolor: '#ffffff',
|
||||
automargin: true,
|
||||
},
|
||||
// legend: {
|
||||
// orientation: 'h',
|
||||
// y: -0.2,
|
||||
// x: 0.5,
|
||||
// xanchor: 'center'
|
||||
// },
|
||||
hovermode: 'closest',
|
||||
shapes: [
|
||||
{
|
||||
type: 'line',
|
||||
x0: 0,
|
||||
x1: 1,
|
||||
xref: 'paper', // Spans entire chart width
|
||||
y0: 0,
|
||||
y1: 0,
|
||||
yref: 'y',
|
||||
line: { color: 'white', width: 1 }
|
||||
}
|
||||
],
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isLoading) return (
|
||||
<Flex w="100%" h="100%" bg="gray.800" borderRadius="md" align="center" justify="center">
|
||||
<Spinner size="xl" color="blue.400" thickness="4px" />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
if (error) return (
|
||||
<Flex w="100%" h="100%" bg="gray.800" borderRadius="md" align="center" justify="center" p={4}>
|
||||
<Text color="red.400">{error}</Text>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
if (plotTraces.length === 0) return (
|
||||
<Flex height="100%" width="100%" align="center" justify="center" bg="gray.800" borderRadius="md">
|
||||
<Text color="gray.400">No data available for current filter.</Text>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box height="100%" width="100%" bg="transparent" borderRadius="md" p={2} overflow="hidden">
|
||||
<Plot
|
||||
data={plotTraces}
|
||||
layout={layout}
|
||||
useResizeHandler={true} // Essential for dashboard grid resizing!
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
config={{ responsive: true, displayModeBar: true, displaylogo: false }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {Input, Field, Text, Flex} from "@chakra-ui/react";
|
||||
import {useDashboardStore} from "@/store/useDashboardStore.jsx";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function SeismicEventsSettings({ widgetId, currentSettings }) {
|
||||
const [startError, setStartError] = useState(null);
|
||||
const [endError, setEndError] = useState(null);
|
||||
const updateSettings = useDashboardStore(state => state.updateWidgetSettings);
|
||||
|
||||
return (
|
||||
<Field.Root invalid={!!startError || !!endError}>
|
||||
<Flex direction="column" w="100%" h="100%" align="center" justify="center" gap="2">
|
||||
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
|
||||
<Text w="50%" align="center" justify="center" mt={2} color="gray.700">
|
||||
Start Date
|
||||
</Text>
|
||||
<Flex direction="column" w="100%">
|
||||
<Input
|
||||
w="100%"
|
||||
type="date"
|
||||
value={currentSettings?.startDate || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
updateSettings(widgetId, {...currentSettings, startDate: value});
|
||||
|
||||
const dateSelected = new Date(value);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (value && (dateSelected > today)) setStartError("Can't Choose a Future Date");
|
||||
else setStartError("");
|
||||
}}
|
||||
/>
|
||||
{startError && <Field.ErrorText>{startError}</Field.ErrorText>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="row" w="100%" h="100%" align="center" justify="center">
|
||||
<Text w="50%" align="center" justify="center" mt={2} color="gray.700">
|
||||
End Date
|
||||
</Text>
|
||||
<Flex direction="column" w="100%">
|
||||
<Input
|
||||
w="100%"
|
||||
type="date"
|
||||
value={currentSettings?.endDate || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
updateSettings(widgetId, {...currentSettings, endDate: value});
|
||||
|
||||
const dateSelected = new Date(value);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (value && (dateSelected > today)) setEndError("Can't Choose a Future Date");
|
||||
else setEndError("");
|
||||
}}
|
||||
/>
|
||||
{endError && <Field.ErrorText>{endError}</Field.ErrorText>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Field.Root>
|
||||
)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export const WIDGET_REGISTRY = {
|
||||
allowedSizes: {
|
||||
small: { w: 3, h: 6 },
|
||||
medium: { w: 3, h: 9 },
|
||||
large: { w: 6, h: 9 },
|
||||
large: { w: 3, h: 12 },
|
||||
}
|
||||
},
|
||||
|
||||
@@ -106,6 +106,18 @@ export const WIDGET_REGISTRY = {
|
||||
}
|
||||
},
|
||||
|
||||
"seismic_events": {
|
||||
name: "Seismic Events",
|
||||
category: "Daily Volcanic Activity",
|
||||
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: {},
|
||||
allowedSizes: {
|
||||
medium: { w: 6, h: 12 }
|
||||
}
|
||||
},
|
||||
|
||||
"llm_al_changes_quad_plot": {
|
||||
name: "Alert Level Changes Quad Plot",
|
||||
category: "Computer Science",
|
||||
|
||||
Reference in New Issue
Block a user