From 2dcdcf8362bfdddec78bda45a0b06bb834cf6ea8 Mon Sep 17 00:00:00 2001 From: Florian Graule Date: Mon, 15 Jul 2024 13:06:25 +0200 Subject: [PATCH] Orbital chart done, using backend data without refetching from the backend server --- .../ScrollBarThumb.tsx | 10 +- .../[satelliteSlug]/orbitDataGraph.tsx | 148 ++++++++++++++---- package.json | 2 + 3 files changed, 123 insertions(+), 37 deletions(-) diff --git a/frontend/src/app/satellites/[satelliteSlug]/_orbitDataGraphComponents/ScrollBarThumb.tsx b/frontend/src/app/satellites/[satelliteSlug]/_orbitDataGraphComponents/ScrollBarThumb.tsx index b412ff6..a779b29 100644 --- a/frontend/src/app/satellites/[satelliteSlug]/_orbitDataGraphComponents/ScrollBarThumb.tsx +++ b/frontend/src/app/satellites/[satelliteSlug]/_orbitDataGraphComponents/ScrollBarThumb.tsx @@ -2,12 +2,13 @@ import React, { useState, useEffect, useRef } from 'react'; -interface ScrollBarThumbProps { +export interface ScrollBarThumbProps { scrollBarThumbWidth: number; svgContainerRect: {topLeft: number, width: number, height: number}; + handleChartScroll: (thumbX: number, svgContainerRect: ScrollBarThumbProps['svgContainerRect']) => void; } -const ScrollBarThumb : React.FC = ({ scrollBarThumbWidth, svgContainerRect}) => { +const ScrollBarThumb : React.FC = ({ scrollBarThumbWidth, svgContainerRect, handleChartScroll}) => { const isDragging = useRef(false); {/* SB is used for ScrollBar */} const [sBThumbX, setSBThumbX] = useState(0); @@ -38,9 +39,12 @@ const ScrollBarThumb : React.FC = ({ scrollBarThumbWidth, s return minX; } else if (newPos >= maxX) { return maxX; - } + } return newPos; }); + + // Change the displayed data on the chart + handleChartScroll(Math.round(thumbRef.current.getBoundingClientRect().x * 10) / 10, svgContainerRect); } } diff --git a/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx b/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx index 7fc4b8e..bf83305 100644 --- a/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx +++ b/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx @@ -1,25 +1,35 @@ "use client"; -import React, { useState, useLayoutEffect, useRef, SyntheticEvent } from 'react' +import React, { useState, useLayoutEffect, useRef } from 'react' import { SatelliteNumber } from '@/lib/store' -import { Line } from 'react-chartjs-2' -import Chart from 'chart.js' +import { XAxis, CartesianGrid, Line, LineChart, Tooltip, YAxis, Label, ResponsiveContainer, Legend } from 'recharts'; import { LaunchDateCountDownProps } from './launchDateCountDown'; -import ScrollBarThumb from './_orbitDataGraphComponents/ScrollBarThumb'; +import ScrollBarThumb, { ScrollBarThumbProps } from './_orbitDataGraphComponents/ScrollBarThumb'; type OrbitDataProps = { satNum : SatelliteNumber; launchDateString: LaunchDateCountDownProps['launchDate']; orbitalData: any; -} +} + +type ChartData = { + epoch: Date; + inclination: number; + eccentricity: number; + semiMajorAxis: number; +} const OrbitDataGraph : React.FC = ({ satNum, launchDateString, orbitalData }) => { // href for the svg component, tracking the size of the container const svgContainer = useRef(null); - const [svgSize, setSvgSize] = useState({width: 0, height: 0}); + const [svgSize, setSvgSize] = useState({width: 0, height: 200}); {/* SB use for ScrollBar*/} const [scrollBarThumbWidth, setSBThumbWidth] = useState(0); + // scrollBarTimeFrame is how many months the scrollbar thumb represents + const scrollBarTimeFrame = useRef(0); + // Chart Data + const [chartData, setChartData] = useState([]); // Handling button for zooming in and out of the graph on a time scale const launchDate = launchDateString? new Date(launchDateString) : new Date(); @@ -39,29 +49,76 @@ const OrbitDataGraph : React.FC = ({ satNum, launchDateString, o if (period === "All") { SBThumbWidth = svgSize.width*0.8-40.5; + scrollBarTimeFrame.current = months; setSBThumbWidth(SBThumbWidth); } else { const number = match ? parseInt(match[1]) : 0; const periodType = match? match[2] : ""; if (periodType === "m") { + scrollBarTimeFrame.current = number; SBThumbWidth = Math.round((svgSize.width*0.8 * number *10) / months) / 10; setSBThumbWidth(SBThumbWidth); } else if (periodType === "y") { + scrollBarTimeFrame.current = number * 12; SBThumbWidth = Math.round((svgSize.width*0.8 * number * 12 *10) / months) / 10 setSBThumbWidth(SBThumbWidth); } } + // Updating chart due to resizing the scrollbar thumb + if (svgContainer.current) { + handleChartScroll( + svgContainer.current.getBoundingClientRect().x + 0.9*svgContainer.current.getBoundingClientRect().width - 20.5 - SBThumbWidth, + {topLeft: svgContainer.current.getBoundingClientRect().x + svgSize.width*0.1 + 20.5, width: svgSize.width*0.8-40.5, height: 20} + ); + } } + + // Callback function for updating the chart when the scrollbar thumb is moved + const handleChartScroll = (thumbX: number, svgContainerRect: ScrollBarThumbProps['svgContainerRect']) => { + + // Ratio of the thumb left border position to the svg container width + const dateRatio = (thumbX - svgContainerRect.topLeft)/svgContainerRect.width; + // Taking the last date of the data period (first date is the launch date) + const lastDataDate = new Date(orbitalData[orbitalData.length-1].epoch.slice(0, 23) + "Z"); + // Calculating the displayed period in milliseconds + const displayedPeriodMs = lastDataDate.getTime() - launchDate.getTime(); + // Calculating the first and last date of the chart + const firstChartDate = new Date(launchDate.getTime() + displayedPeriodMs * dateRatio); + const lastChartDate = new Date(firstChartDate); + lastChartDate.setMonth(firstChartDate.getMonth() + scrollBarTimeFrame.current); + + // Filtering the data to display only the data in the selected period + const filteredData = orbitalData.filter((data: any) => { + const dataDate = new Date(data.epoch.slice(0, 23) + "Z"); + return dataDate >= firstChartDate && dataDate <= lastChartDate; + }).map((data: any) => { + return { + ...data, + epoch: new Date(data.epoch.slice(0, 23) + "Z") + }; + }); + + setChartData(filteredData); + }; + // Layout effect to track the size of the container and update the svg size useLayoutEffect(() => { const updateSize = () => { if (svgContainer.current) { + // Update svg container size const width = svgContainer.current.offsetWidth; - const height = width / 2; - setSvgSize({width, height}); - setSBThumbWidth( width*0.1 ); + setSvgSize((prevSvgSize) => {return { width: width, height: prevSvgSize.height }}); + // Initially set the scrollbar thumb width to represent 1/10 of the total period + !scrollBarTimeFrame.current? scrollBarTimeFrame.current = Math.round(months/10) : null; + // Setting the scrollbar thumb width + const newSBThumbWidth = Math.round((width*0.8 * scrollBarTimeFrame.current *10) / months) / 10; + setSBThumbWidth(newSBThumbWidth); + handleChartScroll( + svgContainer.current.getBoundingClientRect().x + 0.9*svgContainer.current.getBoundingClientRect().width - 20.5 - newSBThumbWidth, + {topLeft: svgContainer.current.getBoundingClientRect().x + width*0.1 + 20.5, width: width*0.8-40.5, height: 20} + ); } } window.addEventListener('resize', updateSize); @@ -72,12 +129,9 @@ const OrbitDataGraph : React.FC = ({ satNum, launchDateString, o return ( <> -

- Graph goes here. -

-
-

Zoom :

+
+

Zoom :

{/* Scrollbar thumb represents the zoom period selected, in case it fits bad we don't display ie. containerSize > SBThumbWidth > 20px */} { (Math.round((svgSize.width*0.8 * 1 * 10) / months) / 10 < svgSize.width) && (Math.round((svgSize.width*0.8 * 1 * 10) / months) / 10 > 20) && } @@ -90,27 +144,53 @@ const OrbitDataGraph : React.FC = ({ satNum, launchDateString, o
-
- - {/* Scrollbar for time navigation */} - - - - - - {/* Scrollbar left navigation arrow */} - - - - - {/* Scrollbar right navigation arrow */} - - - - - {/* Scrollbar thumb */} - +
+ {/* Chart */} + + + + + + {return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(date);}} interval={Math.floor(chartData.length / 6)-1} /> + (0.999*dataMin), (dataMax: number) => (dataMax * 1.001)]} tick={{ fill: '#8884d8' }} tickFormatter={(tick: number) => tick.toFixed(1)} tickLine={false}> + + (0.9*dataMin), (dataMax: number) => (dataMax * 1.1)]} tick={{ fill: '#82ca9d'}} tickFormatter={(tick: number) => tick.toFixed(4)} tickLine={false}> + + (0.999*dataMin), (dataMax: number) => (dataMax * 1.001)]} tick={{ fill: '#ff0000'}} tickFormatter={(tick: number) => tick.toFixed(0)} tickLine={false}> + + + {return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year:'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }).format(date)}} + contentStyle={{ backgroundColor: 'black' }} + /> + + + + + {/* Scrollbar for time navigation */} + + + + + + + {/* Scrollbar left navigation arrow */} + + + + + {/* Scrollbar right navigation arrow */} + + + + {/* Scrollbar thumb */} + +
diff --git a/package.json b/package.json index f4635ff..4f7d6e4 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ "author": "", "license": "", "dependencies": { + "@types/recharts": "^1.8.29", "concurrently": "^8.2.2", "openapi-typescript": "^6.7.4", + "recharts": "^2.12.7", "three-glow-mesh": "^0.1.2" } }