cleanup, relative time fix
This commit is contained in:
@@ -3,15 +3,18 @@
|
||||
Basic plotter for EPICS or BS-based channels with output to either the terminal console or a web interface based on NiceGUI and Plotly.
|
||||
|
||||
````
|
||||
Usage: plot_channels.py [OPTIONS] [CHANNEL_NAMES]...
|
||||
Usage: plot_channels [OPTIONS] CHANNEL_NAMES...
|
||||
|
||||
Plot channels from BS (beam synchronous) or PV (EPICS) sources.
|
||||
Plot channel data from BS (beam synchronous) or PV (EPICS) sources. Use
|
||||
TEST_CHANNEL for test data.
|
||||
|
||||
Options:
|
||||
--acquire-interval FLOAT Interval in seconds for data collection.
|
||||
--plot-interval INTEGER Interval in seconds for plot update.
|
||||
--save-prefix TEXT Prefix for the save filenames.
|
||||
--in-subplots BOOLEAN Whether to plot in subplots.
|
||||
--in-subplots Enable plotting each channel in a subplot.
|
||||
--relative-time Use relative time since start of acquisition.
|
||||
--web Whether to use the local web interface as a GUI.
|
||||
--help Show this message and exit.
|
||||
````
|
||||
````
|
||||
|
||||
|
||||
+41
-27
@@ -4,24 +4,25 @@ from typing import Tuple, Optional
|
||||
from itertools import cycle, islice
|
||||
import re
|
||||
import time
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
from collections import deque
|
||||
import click
|
||||
import asyncio
|
||||
|
||||
import click
|
||||
import numpy as np
|
||||
|
||||
from bsread import dispatcher, source
|
||||
|
||||
import epics
|
||||
|
||||
try:
|
||||
from nicegui import ui, app, context, run
|
||||
from nicegui import ui, app
|
||||
from plots import PlotlyPlot
|
||||
except ImportError as e:
|
||||
print("nicegui not available, falling back to console plot.")
|
||||
print("Nicegui not available, falling back to console plot.")
|
||||
print(e)
|
||||
|
||||
from plots import ConsolePlot, PlotlyPlot
|
||||
from plots import ConsolePlot
|
||||
|
||||
|
||||
def clean_name(fname):
|
||||
@@ -79,7 +80,7 @@ class ChannelData:
|
||||
Append a new data point to the channel.
|
||||
|
||||
Args:
|
||||
x: The x value.
|
||||
x: The x value (timestamp).
|
||||
y: The y value.
|
||||
|
||||
"""
|
||||
@@ -137,7 +138,9 @@ def yield_bs_data(channel_names: list[str]):
|
||||
# message.data.data['SINSB0-'].timestamp # timestamp of the IOC
|
||||
# message.data.data['SINSB0-'].timestamp_offset # timestamp offset (ns) of the IOC
|
||||
|
||||
timestamp = message.data.global_timestamp + message.data.global_timestamp_offset / 1000_000_000
|
||||
timestamp = (
|
||||
message.data.global_timestamp + message.data.global_timestamp_offset / 1000_000_000
|
||||
)
|
||||
channel_data = {"pulse_id": {"value": message.data.pulse_id, "timestamp": timestamp}}
|
||||
|
||||
for key, value in message.data.data.items():
|
||||
@@ -165,18 +168,17 @@ def yield_pv_data(channel_names: list[str]):
|
||||
|
||||
def yield_test_data(channel_names: list[str]):
|
||||
"""Yields test data from the given channels with timestamp."""
|
||||
import random
|
||||
import time
|
||||
|
||||
while True:
|
||||
channel_data = {
|
||||
channel: {"value": random.random(), "timestamp": time.time()} for channel in channel_names
|
||||
channel: {"value": random.random(), "timestamp": time.time()}
|
||||
for channel in channel_names
|
||||
}
|
||||
yield channel_data
|
||||
|
||||
|
||||
def roundrobin(*iterables):
|
||||
"Visit input iterables in a cycle until each is exhausted."
|
||||
"""Visit input iterables in a cycle until each is exhausted."""
|
||||
# roundrobin('ABC', 'D', 'EF') → A D E B F C
|
||||
# Algorithm credited to George Sakkis, posted as recipe in itertools
|
||||
iterators = map(iter, iterables)
|
||||
@@ -212,7 +214,9 @@ def yield_data(channel_names: list, only_pv=False):
|
||||
|
||||
|
||||
def collect_data(channels: dict):
|
||||
"""Collects data from the given channels and yields the updated data dictionary."""
|
||||
"""Collects data from the given channels and respective sources
|
||||
and yields the updated data dictionary.
|
||||
"""
|
||||
|
||||
# typical channel_data:
|
||||
# {'pulse_id': {'value': 21289615168, 'timestamp': 1717427820.5366151},
|
||||
@@ -227,11 +231,9 @@ def collect_data(channels: dict):
|
||||
yield channels
|
||||
|
||||
|
||||
|
||||
|
||||
async def runner(
|
||||
channels: dict,
|
||||
plot_interface,
|
||||
plot_interface: ConsolePlot | PlotlyPlot,
|
||||
acquire_interval=0.1,
|
||||
plot_interval=1,
|
||||
in_subplots=False,
|
||||
@@ -279,13 +281,20 @@ async def runner(
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("channel_names", nargs=-1)
|
||||
@click.argument("channel_names", nargs=-1, type=str, required=True)
|
||||
@click.option("--acquire-interval", default=0.1, help="Interval in seconds for data collection.")
|
||||
@click.option("--plot-interval", default=1, help="Interval in seconds for plot update.")
|
||||
@click.option("--save-prefix", default=None, help="Prefix for the save filenames.")
|
||||
@click.option("--in-subplots", default=False, type=bool, help="Whether to plot in subplots.")
|
||||
@click.option("--in-subplots", is_flag=True, default=False, type=bool, help="Enable plotting each channel in a subplot.")
|
||||
@click.option(
|
||||
"--web", is_flag=True, default=False, type=bool, help="Whether to use the local web interface as a GUI."
|
||||
"--relative-time", default=True, is_flag=True, type=bool, help="Use relative time since start of acquisition."
|
||||
)
|
||||
@click.option(
|
||||
"--web",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
type=bool,
|
||||
help="Whether to use the local web interface as a GUI.",
|
||||
)
|
||||
def main(
|
||||
channel_names: Tuple[str, ...],
|
||||
@@ -293,15 +302,18 @@ def main(
|
||||
plot_interval=1,
|
||||
save_prefix: Optional[str] = None,
|
||||
in_subplots=False,
|
||||
relative_time=True,
|
||||
web=False,
|
||||
):
|
||||
"""Plot channels from BS (beam synchronous) or PV (EPICS) sources."""
|
||||
channels = {channel: ChannelData(channel, save_file_prefix=save_prefix) for channel in channel_names}
|
||||
"""Plot channel data from BS (beam synchronous) or PV (EPICS) sources.
|
||||
Use TEST_CHANNEL for test data.
|
||||
"""
|
||||
channels = {
|
||||
channel: ChannelData(channel, save_file_prefix=save_prefix) for channel in channel_names
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
if not web: # console plot version
|
||||
plot = ConsolePlot(channels, start_time=start_time, in_subplots=in_subplots)
|
||||
if not web: # console plot interface
|
||||
plot = ConsolePlot(channels, in_subplots=in_subplots)
|
||||
asyncio.run(
|
||||
runner(
|
||||
channels,
|
||||
@@ -309,11 +321,12 @@ def main(
|
||||
acquire_interval=float(acquire_interval),
|
||||
plot_interval=plot_interval,
|
||||
in_subplots=in_subplots,
|
||||
relative_time=relative_time,
|
||||
)
|
||||
)
|
||||
|
||||
else: # nicegui version
|
||||
plot = PlotlyPlot(channels, start_time=start_time, in_subplots=in_subplots)
|
||||
else: # nicegui plotly interface
|
||||
plot = PlotlyPlot(channels, in_subplots=in_subplots)
|
||||
|
||||
async def run_async():
|
||||
await runner(
|
||||
@@ -322,6 +335,7 @@ def main(
|
||||
acquire_interval=float(acquire_interval),
|
||||
plot_interval=plot_interval,
|
||||
in_subplots=in_subplots,
|
||||
relative_time=relative_time,
|
||||
)
|
||||
|
||||
app.on_startup(run_async)
|
||||
|
||||
@@ -4,27 +4,38 @@ import plotext as plt
|
||||
import plotly.express as px
|
||||
from plotly.subplots import make_subplots
|
||||
|
||||
import plotly.graph_objects as go
|
||||
from nicegui import ui, app, context
|
||||
from nicegui import ui, context
|
||||
|
||||
|
||||
class AbstractPlot(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def setup_plot(self):
|
||||
def __init__(
|
||||
self,
|
||||
channels,
|
||||
in_subplots=False,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_plot(self, channels, start_time=0, in_subplots=False, marker="hd"):
|
||||
def setup_plot(self):
|
||||
"""Set up the plot."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_plot(self, channels, start_time=0, **kwargs):
|
||||
"""Update the plot with new data from the given channels."""
|
||||
pass
|
||||
|
||||
|
||||
class PlotlyPlot(AbstractPlot):
|
||||
def __init__(self, channels, start_time=0, in_subplots=False, marker="hd"):
|
||||
def __init__(
|
||||
self,
|
||||
channels,
|
||||
start_time=0,
|
||||
in_subplots=False,
|
||||
) -> None:
|
||||
self.channels = channels
|
||||
self.start_time = start_time
|
||||
self.in_subplots = in_subplots
|
||||
self.marker = marker
|
||||
|
||||
self.plot, self.fig = self.setup_plot()
|
||||
|
||||
@@ -33,10 +44,10 @@ class PlotlyPlot(AbstractPlot):
|
||||
num_traces = len(self.channels)
|
||||
labels = [channel.name for channel in self.channels.values()]
|
||||
|
||||
context.client.content.classes("h-[100vh]") # 1
|
||||
context.client.content.classes("h-[100vh]") # full height and width of the container
|
||||
|
||||
colors = px.colors.qualitative.D3
|
||||
fig = make_subplots(rows=num_traces, cols=1, shared_xaxes=False, vertical_spacing=0.05)
|
||||
fig = make_subplots(rows=num_traces, cols=1, shared_xaxes=True, vertical_spacing=0.05)
|
||||
|
||||
subfigures = [
|
||||
px.line(
|
||||
@@ -63,15 +74,14 @@ class PlotlyPlot(AbstractPlot):
|
||||
# set xaxis title for last subplot at the bottom
|
||||
fig["layout"][f"xaxis{num_traces}"]["title"] = "time (s)"
|
||||
|
||||
# print(fig)
|
||||
plot = ui.plotly(fig).classes("w-full h-full")
|
||||
plot = ui.plotly(fig).classes("w-full h-full") # full height and width inside the container
|
||||
|
||||
return plot, fig
|
||||
|
||||
def update_plot(self, channels, start_time=0, **kwargs) -> None:
|
||||
|
||||
for i, channel in enumerate(channels.values()):
|
||||
self.fig["data"][i]["x"] = channel.x
|
||||
self.fig["data"][i]["x"] = channel.x - start_time
|
||||
self.fig["data"][i]["y"] = channel.y
|
||||
|
||||
self.plot.update_figure(self.fig)
|
||||
@@ -102,6 +112,7 @@ class ConsolePlot(AbstractPlot):
|
||||
|
||||
if in_subplots:
|
||||
plt.subplots(num_channels, 1)
|
||||
|
||||
plt.main().plot_size(plt.tw(), plt.th() - 1)
|
||||
plt.clear_data()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user