Skip to content

Commit

Permalink
Orbital chart done, using backend data without refetching from the ba…
Browse files Browse the repository at this point in the history
…ckend server
  • Loading branch information
Florian Graule committed Jul 15, 2024
1 parent 44a7505 commit 2dcdcf8
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScrollBarThumbProps> = ({ scrollBarThumbWidth, svgContainerRect}) => {
const ScrollBarThumb : React.FC<ScrollBarThumbProps> = ({ scrollBarThumbWidth, svgContainerRect, handleChartScroll}) => {
const isDragging = useRef<boolean>(false);
{/* SB is used for ScrollBar */}
const [sBThumbX, setSBThumbX] = useState(0);
Expand Down Expand Up @@ -38,9 +39,12 @@ const ScrollBarThumb : React.FC<ScrollBarThumbProps> = ({ 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);
}
}

Expand Down
148 changes: 114 additions & 34 deletions frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx
Original file line number Diff line number Diff line change
@@ -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<OrbitDataProps> = ({ satNum, launchDateString, orbitalData }) => {

// href for the svg component, tracking the size of the container
const svgContainer = useRef<HTMLDivElement>(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<ChartData[]>([]);

// Handling button for zooming in and out of the graph on a time scale
const launchDate = launchDateString? new Date(launchDateString) : new Date();
Expand All @@ -39,29 +49,76 @@ const OrbitDataGraph : React.FC<OrbitDataProps> = ({ 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);
Expand All @@ -72,12 +129,9 @@ const OrbitDataGraph : React.FC<OrbitDataProps> = ({ satNum, launchDateString, o

return (
<>
<h1>
Graph goes here.
</h1>
<div ref={svgContainer} className="w-full flex flex-col">
<div className="zoom-container flex">
<h2 className="mx-5 text-white">Zoom : </h2>
<div className="zoom-container flex items-center mb-5">
<h2 className="mx-5 text-grey-400 text-2xl">Zoom : </h2>
{/* 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) && <button onClick={handleZoomClick} className="mx-2 bg-[#f2f2f2] text-black w-10 h-8 rounded-lg hover:bg-[#cccccc]">1m</button> }
Expand All @@ -90,27 +144,53 @@ const OrbitDataGraph : React.FC<OrbitDataProps> = ({ satNum, launchDateString, o

<button onClick={handleZoomClick} className="mx-2 bg-[#f2f2f2] text-black w-10 h-8 rounded-lg hover:bg-[#cccccc]">All</button>
</div>
<div className="w-full flex justify-center items-center">
<svg className="relative" width="100%" height="600">
{/* Scrollbar for time navigation */}
<g transform={`translate(${svgSize.width*0.1}, ${svgSize.height-75})`} fill="#f2f2f2">
<g>
<rect y="-50" width="80%" height="1"/>
</g>
<rect width="80%" height="20"/>
{/* Scrollbar left navigation arrow */}
<g>
<rect x="0.5" y="0.5" width="19" height="19" fill="#e6e6e6" stroke="#cccccc" strokeWidth="1"/>
<path d="M 13 5 L 6 10 L 13 15" fill="#333333"/>
</g>
{/* Scrollbar right navigation arrow */}
<g transform={`translate(${svgSize.width*0.8 - 20}, 0)`}>
<rect x="0.5" y="0.5" width="19" height="19" fill="#e6e6e6" stroke="#cccccc" strokeWidth="1"/>
<path d="M 7 5 L 14 10 L 7 15" fill="#333333"/>
</g>
{/* Scrollbar thumb */}
<ScrollBarThumb scrollBarThumbWidth={scrollBarThumbWidth} svgContainerRect={{topLeft: (svgContainer.current?.getBoundingClientRect()?(svgContainer.current?.getBoundingClientRect().x) : 0) + svgSize.width*0.1 + 20.5 , width: (svgSize.width*0.8-40.5), height: 20}}/>
<div className="w-full flex flex-col justify-center items-center">
{/* Chart */}
<ResponsiveContainer width="100%" height={500}>
<LineChart data={chartData}>
<Line name="Inclination (deg)" type="monotone" dataKey="inclination" stroke="#8884d8" strokeWidth={3} yAxisId="y-incl" dot={false}/>
<Line name="Eccentricity" type="monotone" dataKey="eccentricity" stroke="#82ca9d" strokeWidth={3} yAxisId="y-ecc" dot={false}/>
<Line name="Semi-major Axis (km)" type="monotone" dataKey="semiMajorAxis" stroke="#ff0000" strokeWidth={3} yAxisId="y-sma" dot={false}/>
<XAxis dataKey="epoch" tickFormatter={(date: Date) => {return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(date);}} interval={Math.floor(chartData.length / 6)-1} />
<YAxis axisLine={false} yAxisId="y-incl" type='number' domain={[(dataMin: number) => (0.999*dataMin), (dataMax: number) => (dataMax * 1.001)]} tick={{ fill: '#8884d8' }} tickFormatter={(tick: number) => tick.toFixed(1)} tickLine={false}>
</YAxis>
<YAxis axisLine={false} orientation='right' yAxisId="y-ecc" type='number' domain={[(dataMin: number) => (0.9*dataMin), (dataMax: number) => (dataMax * 1.1)]} tick={{ fill: '#82ca9d'}} tickFormatter={(tick: number) => tick.toFixed(4)} tickLine={false}>
</YAxis>
<YAxis axisLine={false} yAxisId="y-sma" type='number' domain={[(dataMin: number) => (0.999*dataMin), (dataMax: number) => (dataMax * 1.001)]} tick={{ fill: '#ff0000'}} tickFormatter={(tick: number) => tick.toFixed(0)} tickLine={false}>
</YAxis>
<CartesianGrid stroke="#ccc"/>
<Tooltip
labelFormatter={(date: Date) => {return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year:'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }).format(date)}}
contentStyle={{ backgroundColor: 'black' }}
/>
<Legend />
</LineChart>
</ResponsiveContainer>
<svg className="relative" width="100%" height="100">
{/* Scrollbar for time navigation */}

<g transform={`translate(${svgSize.width*0.1}, 50)`} fill="#f2f2f2">
<g>
<rect y="-50" width="80%" height="1"/>
</g>
<rect width="80%" height="20"/>
{/* Scrollbar left navigation arrow */}
<g>
<rect x="0.5" y="0.5" width="19" height="19" fill="#e6e6e6" stroke="#cccccc" strokeWidth="1"/>
<path d="M 13 5 L 6 10 L 13 15" fill="#333333"/>
</g>
{/* Scrollbar right navigation arrow */}
<g transform={`translate(${svgSize.width*0.8 - 20}, 0)`}>
<rect x="0.5" y="0.5" width="19" height="19" fill="#e6e6e6" stroke="#cccccc" strokeWidth="1"/>
<path d="M 7 5 L 14 10 L 7 15" fill="#333333"/>
</g>
{/* Scrollbar thumb */}
<ScrollBarThumb
scrollBarThumbWidth={scrollBarThumbWidth}
svgContainerRect={{topLeft: (svgContainer.current?.getBoundingClientRect()?(svgContainer.current?.getBoundingClientRect().x) : 0) + svgSize.width*0.1 + 20.5 , width: (svgSize.width*0.8-40.5), height: 20}}
handleChartScroll={handleChartScroll}
/>
</g>
</svg>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

0 comments on commit 2dcdcf8

Please sign in to comment.