Skip to content

Commit

Permalink
plot refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucien950 committed May 24, 2024
1 parent a6e425a commit 4cf52b3
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 174 deletions.
157 changes: 54 additions & 103 deletions software/tracksight/frontend/src/app/visualize/graph.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
// used api endpoints
// /signal/measurement
// /signal/fields/<measurement>
// /query
/**
* REST API Endpoints:
* /signal/measurement
* /signal/fields/<measurement>
* /query
*/

'use client';
import { Dispatch, MouseEventHandler, SetStateAction, useEffect, useState } from 'react';
import { PlotRelayoutEvent } from 'plotly.js';
import { PlotData, PlotRelayoutEvent } from 'plotly.js';
import { Button } from 'antd';
import { GraphI } from '@/types/Graph';
import DropdownMenu from './dropdown_menu';
import TimeStampPicker from './timestamp_picker';
import { FLASK_URL } from '@/app/constants';
import dynamic from "next/dynamic";

const Plot = dynamic(() => import("react-plotly.js"), { ssr: false, })

const DEFAULT_LAYOUT: Partial<Plotly.Layout> = {
width: 620,
height: 500,
title: "Empty",
xaxis: {},
yaxis: {},
legend: { "orientation": "h" },
}
import TimePlot, { FormattedData } from './timeplot';

export const getRandomColor = () => {
const r = Math.floor(Math.random() * 256);
Expand Down Expand Up @@ -113,49 +104,33 @@ const FieldDropdown = ({ fields, setFields, measurement }: {
)
}

function usePlotlyFormat(setGraphTitle: (title: string) => void): [Plotly.Data[], Dispatch<SetStateAction<Record<string, { times: Array<string>; values: Array<number>; }>>>] {
function usePlotlyFormat(setGraphTitle: (title: string) => void): [
FormattedData[],
Dispatch<SetStateAction<Record<string, { times: Array<string>; values: Array<number>; }>>>
] {
const [data, setData] = useState<Record<string, { times: Array<string>; values: Array<number>; }>>({});
const [formattedData, setFormattedData] = useState<Plotly.Data[]>([]);
const [formattedData, setFormattedData] = useState<FormattedData[]>([]);
useEffect(() => {
setGraphTitle(Object.keys(data).join(" + "));
setFormattedData(Object.entries(data).map(([graphName, { times, values }]) => ({
name: graphName,
x: times, y: values,
type: 'scatter', mode: 'lines+markers', line: { color: getRandomColor() }
} as Plotly.Data)));
x: times.map(x=>new Date(x)), y: values,
type: "scatter", mode: "lines+markers", line: { color: getRandomColor() }
})));
}, [data]);

return [formattedData, setData];
}

export default function Graph({ syncZoom, sharedZoomData, setSharedZoomData, handleDelete }: {
export default function Graph({ syncZoom, sharedZoomData, setSharedZoomData, deletePlot }: {
graphInfo: GraphI,
deletePlot: MouseEventHandler<HTMLButtonElement>,
syncZoom: boolean,
sharedZoomData: PlotRelayoutEvent,
setSharedZoomData: Dispatch<SetStateAction<PlotRelayoutEvent>>
handleDelete: MouseEventHandler<HTMLButtonElement>,
}) {
//default graph layout
const [graphLayout, setGraphLayout] = useState<Partial<Plotly.Layout>>(DEFAULT_LAYOUT);
// updates graph layout when zoomed
useEffect(() => {
if (!(sharedZoomData && 'xaxis.range[0]' in sharedZoomData)) {
// TODO error in some way
return;
}
// Update the graph's layout with the new axis ranges
setGraphLayout(prevLayout => ({
...prevLayout,
xaxis: {
range: [sharedZoomData['xaxis.range[0]'], sharedZoomData['xaxis.range[1]']],
},
yaxis: {
range: [sharedZoomData['yaxis.range[0]'], sharedZoomData['yaxis.range[1]']],
},
}));
}, [sharedZoomData]);

const [plotData, setPlotData] = usePlotlyFormat((t) => setGraphLayout(p => ({ ...p, title: t, })))
const [plotTitle, setPlotTitle] = useState<string>("");
const [plotData, setPlotData] = usePlotlyFormat(setPlotTitle);

// Top Form Information
const [measurement, setMeasurement] = useState<string>("");
Expand All @@ -164,66 +139,42 @@ export default function Graph({ syncZoom, sharedZoomData, setSharedZoomData, han
const [endEpoch, setEndEpoch] = useState<string>("");

return (
<div className="flex flex-col p-4 border-[1.5px] rounded-xl">
{/* Measurement Selector */}
<div className="flex flex-col gap-y-2">
<MeasurementDropdown measurement={measurement} setMeasurement={setMeasurement} />
<FieldDropdown fields={fields} setFields={setFields} measurement={measurement} />
<TimeStampPicker setStart={setStartEpoch} setEnd={setEndEpoch} />
<Button onClick={async (e) => {
const missingQueryEls = !startEpoch || !endEpoch || !measurement || fields.length == 0;
if (missingQueryEls) {
<TimePlot plotTitle={plotTitle} deletePlot={deletePlot}
plotData={plotData} clearPlotData={e => setPlotData({})}
syncZoom={syncZoom} sharedZoomData={sharedZoomData} setSharedZoomData={setSharedZoomData}
>
<MeasurementDropdown measurement={measurement} setMeasurement={setMeasurement} />
<FieldDropdown fields={fields} setFields={setFields} measurement={measurement} />
<TimeStampPicker setStart={setStartEpoch} setEnd={setEndEpoch} />
<Button onClick={async (e) => {
const missingQueryEls = !startEpoch || !endEpoch || !measurement || fields.length == 0;
if (missingQueryEls) {
// TODO add message
// "Please fill out all fields properly"
return;
}
const fetchUrl = new URL("/signal/query", FLASK_URL);
fetchUrl.search = new URLSearchParams({
measurement: measurement,
start_epoch: startEpoch.slice(0, -2 - 3), end_epoch: endEpoch.slice(0, -2 - 3), // apparently for some reason the time is given in ms
fields: fields.join(",")
}).toString();
console.log(fetchUrl.toString()) // TODO remove after testing

try {
const res = await fetch(fetchUrl, { method: 'get' })
if (!res.ok) {
// TODO add message
// "Please fill out all fields properly"
console.error(await res.json())
return;
}
const fetchUrl = new URL("/signal/query", FLASK_URL);
fetchUrl.search = new URLSearchParams({
measurement: measurement,
start_epoch: startEpoch.slice(0, -2 - 3), end_epoch: endEpoch.slice(0, -2 - 3), // apparently for some reason the time is given in ms
fields: fields.join(",")
}).toString();
console.log(fetchUrl.toString()) // TODO remove after testing

try {
const res = await fetch(fetchUrl, { method: 'get' })
if (!res.ok) {
// TODO add message
console.error(await res.json())
return;
}
setPlotData(await res.json())
} catch (error) {
console.error(error)
}
}}>
Submit
</Button>
</div>

{/* TODO better plotting library :(((( */}
<Plot layout={graphLayout} data={plotData} // Pass the array of formatted data objects
config={{ displayModeBar: true, displaylogo: false, scrollZoom: true, }}
onRelayout={(e: PlotRelayoutEvent) => { if (syncZoom) setSharedZoomData(e) }}
/>

<div className="flex flex-row gap-x-2">
<button className="bg-[#1890ff] hover:bg-blue-400 text-white text-sm
transition-colors duration-100 border-0 block p-2 rounded-md flex-1"
onClick={() => {
setGraphLayout(DEFAULT_LAYOUT);
setPlotData({});
}}
>
Clear Data
</button>
<button className="bg-[#ff4d4f] hover:bg-red-400 text-white text-sm
transition-colors duration-100 border-0 block p-2 rounded-md flex-1"
onClick={handleDelete}
>
Delete This Graph
</button>
</div>
</div>
setPlotData(await res.json())
} catch (error) {
console.error(error)
}
}}>
Submit
</Button>
</TimePlot>
);
}
103 changes: 34 additions & 69 deletions software/tracksight/frontend/src/app/visualize/livegraph.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
/**
* Websockets that are used (handling signals, and handling available signals)
* - signal_sub (emit), signal_unsub (emit)
* - signal_response
* - signal_stopped
* - available_signals_sub (emit)
* - available_signals_response
*/

'use client'
import { MouseEventHandler, useEffect, useState } from 'react';
import { Dispatch, MouseEventHandler, SetStateAction, useEffect, useState } from 'react';
import { Switch } from 'antd';
import { useSocket } from '@/app/useSocket';
import { FLASK_URL } from '@/app/constants';
import DropdownMenu from './dropdown_menu';
import { assertType } from '@/types/Assert';
import { PlotData } from 'plotly.js';
import { PlotData, PlotRelayoutEvent, PlotType } from 'plotly.js';
import dynamic from "next/dynamic";
import { Socket } from 'socket.io-client';
import TimePlot, { FormattedData } from './timeplot';

const Plot = dynamic(() => import("react-plotly.js"), { ssr: false, })

function SignalSelector({ socket, loading, setGraphTitle }: {
function SignalSelector({ socket, loading, setPlotTitle: setGraphTitle }: {
socket: Socket | null,
loading: boolean,
setGraphTitle: (title: string) => void,
setPlotTitle: (title: string) => void,
}) {
// Handles which signals we care about
const [prevWatchSignals, setPrevWatchSignals] = useState<string[]>([]); // technically an anti-pattern but the handler is inside an opaque component
Expand Down Expand Up @@ -60,51 +70,16 @@ function SignalSelector({ socket, loading, setGraphTitle }: {
)
}

interface FormattedData {
x: Date[];
y: number[];
type: string;
mode: string;
name: string;
}

const EMPTY_FORMATTED_DATA: FormattedData = {
x: [],
y: [],
type: 'scatter',
mode: 'lines+markers',
name: 'test',
};

const DEFAULT_LAYOUT: Partial<Plotly.Layout> = {
width: 620,
height: 500,
title: "Live Data",
xaxis: {
// for formatting time
tickformat: "%H:%M:%S.%L", // TODO fix this formatting
automargin: true,
},
yaxis: {},
legend: { "orientation": "h" },
};

/**
* Websockets that are used (handling signals, and handling available signals)
* - signal_sub (emit), signal_unsub (emit)
* - signal_response
* - signal_stopped
* - available_signals_sub (emit)
* - available_signals_response
*/
export default function LiveGraph(props: {
export default function LiveGraph({ id, deletePlot, syncZoom, sharedZoomData, setSharedZoomData }: {
id: number,
onDelete: MouseEventHandler<HTMLElement>,
deletePlot: MouseEventHandler<HTMLElement>,
syncZoom: boolean,
sharedZoomData: PlotRelayoutEvent,
setSharedZoomData: Dispatch<SetStateAction<PlotRelayoutEvent>>
}) {
// Plot Data
// Plot Data
const [plotTitle, setPlotTitle] = useState<string>("");
const [plotData, setPlotData] = useState<FormattedData[]>([]);
const [graphLayout, setGraphLayout] = useState<Partial<Plotly.Layout>>(DEFAULT_LAYOUT);

const { socket, loading } = useSocket(FLASK_URL);
const [isSubscribed, setIsSubscribed] = useState<boolean>(true);
useEffect(() => {
Expand All @@ -125,7 +100,13 @@ export default function LiveGraph(props: {
// extracts the most recent 100 data points to display to ensure the graph doesn't get too cluttered
// const sortedDates = Object.keys(signalData).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
// const maxDataPoints = sortedDates.slice(-MAX_DATA_POINTS);
const existingSignal = p.find((d) => d.name === packet.id) || EMPTY_FORMATTED_DATA;
const existingSignal = p.find((d) => d.name === packet.id) || {
x: [],
y: [],
type: 'scatter',
mode: 'lines+markers',
name: 'test',
};
// TODO new data!!!
existingSignal.x.push(new Date());
existingSignal.y.push(Math.random());
Expand All @@ -143,34 +124,18 @@ export default function LiveGraph(props: {
}, [socket, isSubscribed])

return (
<div className="flex flex-col p-4 border-2 rounded-xl">
<TimePlot plotTitle={plotTitle} deletePlot={deletePlot}
plotData={plotData} clearPlotData={e => setPlotData([])}
syncZoom={syncZoom} sharedZoomData={sharedZoomData} setSharedZoomData={setSharedZoomData}
>
<div className="flex flex-row items-center gap-x-2 mb-2">
<p>Live</p>
<Switch onChange={setIsSubscribed} checked={isSubscribed} />
</div>
<div className="flex flex-row gap-x-2">
<SignalSelector socket={socket} loading={loading}
setGraphTitle={(t: string) => setGraphLayout(p => ({ ...p, title: t, }))} />
</div>
<Plot data={plotData as Partial<PlotData>[]} layout={graphLayout} />
<div className="flex flex-row gap-x-2">
<button className="bg-[#1890ff] hover:bg-blue-400 text-white text-sm
transition-colors duration-100 border-0 block p-2 rounded-md flex-1"
onClick={() => {
setGraphLayout(DEFAULT_LAYOUT);
setPlotData([]);
}}
>
Clear Data
</button>
<button className="bg-[#ff4d4f] hover:bg-red-400 text-white text-sm
transition-colors duration-100 border-0 block p-2 rounded-md flex-1"
onClick={props.onDelete}
>
Delete This Graph
</button>
<SignalSelector socket={socket} loading={loading} setPlotTitle={setPlotTitle} />
</div>
</div>
</TimePlot>
);
}

Expand Down
7 changes: 5 additions & 2 deletions software/tracksight/frontend/src/app/visualize/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function Visualize() {
<Graph
key={graph.id}
graphInfo={graph}
handleDelete={(e) =>
deletePlot={(e) =>
setGraphs(prevGraphs => prevGraphs.filter(g => g.id !== graph.id))}
syncZoom={shouldSyncZoom}
sharedZoomData={sharedZoomData}
Expand All @@ -98,8 +98,11 @@ export default function Visualize() {
<LiveGraph
key={graph.id}
id={graph.id}
onDelete={(e) =>
deletePlot={(e) =>
setGraphs(prevGraphs => prevGraphs.filter(g => g.id !== graph.id))}
syncZoom={shouldSyncZoom}
sharedZoomData={sharedZoomData}
setSharedZoomData={setSharedZoomData}
/>
)
}
Expand Down
Loading

0 comments on commit 4cf52b3

Please sign in to comment.