Real-Time Data Plotting with Python for Scientific Applications

Problem

Gathering data on events as they occur in real-time is a powerful and popular technique in scientific and industrial computing. If we can query an online REST API representing the position of the International Space Station’s (ISS), how can we visualize these data in real time? How do you plot the data points as soon as they arrive and observe changes in the station’s position immediately? Let’s look at using Python for a real time plot of data.

Solution

For real-time data plotting, we can use:

  • A Python library called pglive. It offers an efficient, thread-safe way to plot multiple data points as they happen or arrive. Pglive is based on pyqtgraph and it supports, among others PyQt6 and PySide6, which are powerful GUI toolkits.
  • The animation module of matplotlib. We can combine it with the map plotting capability to plot data points for object trajectory tracking.

Let us see both variants in action. In the process, we will use the attrs package. It will help us write concise and efficient object-oriented code.

Environment

Begin by creating a project folder and open it in VS Code. Then, create a requirements.txt file containing four lines: requests, pglive, PyQt6, and attrs. Next, hit Ctrl+Shift+P and choose Python: Create Environment.

Python: Create Environment

Follow the prompts for creating a local virtual environment. Make sure to check requirements.txt so the environment agent will install the required Python packages directly:

python environment requirements

Once you have your environment ready, please proceed to the next steps where we set up the solution to use Python for a real time plot of the data.

Solution Set-up

The solution for plotting the real-time position of the ISS will consist of three script files: a data generator class, one file for the real-time line chart, and map plot classes.

Data Generator Class

The first order of business is to write a simple class for accessing the real-time data. Create a file data.py and paste the following code in it:

01: import requests
02: from attrs import define
03: 
04: 
05: @define
06: class ISSData:
07: 
08:     latitude: float
09:     longitude: float
10: 
11:     @staticmethod
12:     def fetch_iss_data():
13:         url = 'https://api.wheretheiss.at/v1/satellites/25544'
14:         response = requests.get(url)
15:         data = response.json()
16:         return ISSData(
17:             latitude=data['latitude'],
18:             longitude=data['longitude']
19:         )
data generator class

Here is what this piece of code does line by line:

  • 1, 2: We need the requests module for making REST calls and the attrs module to streamline the class definition.
  • 5, 6: Define a class ISSData.
  • 8, 9: The class has two attributes, all float types: latitude and longitude.
  • 11 – 20: Additionally, the class has one single static method for fetching the data.

Using the known API endpoint for the ISS position, we construct an API call that will return the latitude and longitude of the ISS.

We will import and call the static method of this class from the main program entry point. With this class set up, you can proceed to the next step.

Real-time Line Chart Plotting Class

Now we must design a class that uses the pglive and PyQt6 modules to plot data points in real time. Here is the code:

01: import sys
02: from attrs import define, field
03: from data import ISSData
04: from threading import Thread
05: from time import sleep
06: import time
07: from pglive.kwargs import Axis
08: from pglive.sources.live_axis_range import LiveAxisRange
09: from PyQt6.QtWidgets import QApplication
10: from pglive.sources.live_axis import LiveAxis
11: from pglive.sources.data_connector import DataConnector
12: from pglive.sources.live_plot import LiveLinePlot
13: from pglive.sources.live_plot_widget import LivePlotWidget
14: 
15: 
16: @define
17: class LivePlot:
18:     app: QApplication = field(init=False, default=QApplication(sys.argv))
19:     running: bool = field(init=False, default=True)
20: 
21:     x_axis: LiveAxis = field(init=False)
22:     y_axis_left: LiveAxis = field(init=False)
23:     y_axis_right: LiveAxis = field(init=False)
24: 
25:     plot_widget_ISS_position: LivePlotWidget = field(init=False)
26:     lat_plot_curve: LiveLinePlot = field(init=False, default=LiveLinePlot(pen='red', name="Latitude"))
27:     lon_plot_curve: LiveLinePlot = field(init=False, default=LiveLinePlot(pen='green', name="Longitude"))
28:     lat_data_connector: DataConnector = field(init=False)
29:     lon_data_connector: DataConnector = field(init=False)
30: 
31:     def __attrs_post_init__(self):
32:         self.x_axis = LiveAxis('bottom', axisPen='yellow', textPen='yellow', **{Axis.TICK_FORMAT: Axis.DATETIME})
33:         self.y_axis_left = LiveAxis('left', axisPen='red', textPen='red')
34:         self.y_axis_right = LiveAxis('right', axisPen='green', textPen='green')
35:         self.plot_widget_ISS_position = LivePlotWidget(
36:             title="ISS Latitude and Longitude @ 1Hz",
37:             axisItems={'bottom': self.x_axis, 'left': self.y_axis_left, 'right': self.y_axis_right},
38:             labels={'bottom': "Timestamp", 'left': "Latitude", 'right': "Longitude"},
39:             background='black',
40:             x_range_controller=LiveAxisRange(roll_on_tick=10, offset_left=0.5)
41:         )
42:         self.plot_widget_ISS_position.addItem(self.lat_plot_curve)
43:         self.plot_widget_ISS_position.addItem(self.lon_plot_curve)
44: 
45:         self.lat_data_connector = DataConnector(self.lat_plot_curve, max_points=6000, update_rate=1)
46:         self.lon_data_connector = DataConnector(self.lon_plot_curve, max_points=6000, update_rate=1)
47: 
48:     def start(self):
49:         self.plot_widget_ISS_position.show()
50:         Thread(target=self.iss_data_generator).start()
51:         self.app.exec()
52: 
53:     def iss_data_generator(self):
54:         while self.running:
55:             iss_data = ISSData.fetch_iss_data()
56:             self.lat_data_connector.cb_append_data_point(iss_data.latitude, time.time())
57:             self.lon_data_connector.cb_append_data_point(iss_data.longitude, time.time())
58:             sleep(1)
59: 
60:     def stop(self):
61:         self.running = False
62: 
63: 
64: if __name__ == "__main__":
65:     plot = LivePlot()
66:     try:
67:         plot.start()
68:     except KeyboardInterrupt:
69:         plot.stop()
70:

Code Explanation

Here is what this code does:

  • 01 – 13: Import the necessary additional modules:
    • 2: The attrs library to help us define our class and its attributes.
    • 3: The custom data generator class we developed in the previous step and the rest of the required modules.
  • 16 – 30: Define the LivePlot class using @define decorator from attrs with the following attributes:
    • 18: The PyQt application app.
    • 19: The running flag, by default True.
    • 21, 22, and 23: The x axis and one left and right y axis.
    • 25: The LivePlotWidget that will take care of most of the formatting of the plot.
    • 26 and 27: Two LiveLinePlots for the latitude and longitude.
    • 28 and 29: Accordingly, two data connectors. We will initialize these attributes in the next step.
  • 31 – 46: Use the attrs_post_init method, which is run after initializing an instance of the class. It does the following:
    • 32: Initialize and configure the x axis to have a yellow color and be formatted as datetime.
    • 33 and 34: Initialize and configure the two y axes.
    • 36 – 41: Initialize the LivePlotWidget method, which takes care of drawing the plot. We pass a title, the x and y axes we defined previously, labels for the axes, background color, and set the range controller. The roll_on_tick parameter specifies how much points will be plotted on the x-axis at a time. For scenarios where performance may be an issue, higher values are recommended. In this case, the unit is seconds.
    • 42 and 43: Add the LiveLinePlots for latitude and longitude to the plotting plot_widget_ISS_position. The latitude and longitude values present the coordinates of the ISS position.
    • 45 and 46: define the DataConnector for our two variables, the latitude and longitude. The DataConnector is an important class from pglive which connects the data to the plot. The notable settings here are:
      • The max_points parameter. It defines the maximum count of data points that will be plotted.
      • The update_rate in Hz for how often the plot will be updated. Iin this case – every second.
  • 48 – 51: Define the start function for the plot class:
    • 49: To show the plot, call the show() function on the live plot widget class, which is our plot_widget_ISS_position class attribute.
    • 50: Start a new Thread where the target is the data generator class.
    • 51: Finally, start the app.
  • 53 – 58: Define the data generator function. While the app is running (by default, it is because we have set running=True), we get the data for the ISS position from the data generator class. The data generator class returns an object containing two attributes: latitude and longitude. Finally, we pause the loop for a second and run it again using sleep(1). This code will take care of getting the ISS position every second. Since it takes 90 minutes for the ISS to complete one turn around Earth and we are plotting a maximum of 6000 data points every second, our plot will be live for a little longer than the duration of the ISS’s full orbit trip (6000 seconds divided by 60 equals 100 minutes).
  • 60 – 61: Create a method for stopping the app, which sets the running attribute to False.
  • From line 64 onward, the program instantiates and starts the plot class LivePlot we just defined. Unless a keyboard interrupt event is detected, the program will run indefinitely.

Code in Editor

This is what the code looks like in the editor:

pglive code

Data Visualization

Running the program opens a new window with the plot. We can validate what we see with the map from https://wheretheiss.at/. The positive increase in degrees of longitude (right y-axis) indicates movement to the east of the prime meridian. The stable degrees of latitude (left y-axis) indicate a southern position. In this case, the plot is relatively zoomed out so we can observe the real-time change in both latitude and longitude.

pglive plot drawing latitude and longitude in real time

We can validate these data by looking at the map plot available at https://wheretheiss.at/:

ISS on the map

Real-time Map Plotting Class

It is interesting to observe the change in latitude and longitude in real-time. However, it is easier if we plot the ISS location directly on a map. Therefore, the next step is to visualize the location of the ISS on an actual map of Earth. Such a map provides more location context instead of just looking at the change of the latitude and longitude values.

First, install two additional packages by running pip install matplotlib basemap in your VS Code terminal:

vs code install additional packages

Next, create a new file realtime_map_plotting.py and paste the following code into it:

01: from data import ISSData
02: from attr import define, field
03: from matplotlib.animation import FuncAnimation
04: from mpl_toolkits.basemap import Basemap
05: import matplotlib.pyplot as plt
06: 
07: 
08: @define
09: class LiveMap:
10:     fig: plt.Figure = field(default=plt.figure(figsize=(8, 6), edgecolor='w'))
11:     ax: plt.Axes = field(init=False)
12:     m: Basemap = field(init=False)
13:     iss_plot: plt.Line2D = field(init=False)
14:     trajectory_plot: plt.Line2D = field(init=False)
15:     trajectory_data: list = field(default=([], []))
16: 
17:     def __attrs_post_init__(self):
18:         self.ax = self.fig.add_subplot(111)
19:         self.m = Basemap(projection='cyl',
20:                          resolution='i',
21:                          llcrnrlat=-90, urcrnrlat=90,
22:                          llcrnrlon=-180, urcrnrlon=180,
23:                          ax=self.ax)
24:         self.m.fillcontinents(color='#6E260E', lake_color='aqua', alpha=0.65)
25:         self.m.drawcoastlines(color='black', linestyle='solid')
26:         self.m.drawmapboundary(color='b', linewidth=2.0, fill_color='#82fff3')
27:         self.m.drawmeridians(range(-180, 180, 20), linewidth=1.0, labels=[0, 0, 1, 1])
28:         self.m.drawparallels(range(-90, 100, 10), linewidth=1.0, labels=[1, 1, 0, 0])
29:         plt.title('Real-time ISS Location', fontsize=16, pad=32)
30: 
31:         self.iss_plot, = self.m.plot([], [], '8', markersize=5, color='red', linewidth=1)
32:         self.trajectory_plot, = self.m.plot([], [], '-', color='black', linewidth=2)
33: 
34:     def update(self, frame) -> tuple[plt.Line2D, plt.Line2D]:
35:         iss_data = ISSData.fetch_iss_data()
36: 
37:         self.trajectory_data[0].append(iss_data.longitude)
38:         self.trajectory_data[1].append(iss_data.latitude)
39: 
40:         self.iss_plot.set_data([iss_data.longitude], [iss_data.latitude])
41: 
42:         self.trajectory_plot.set_data(self.trajectory_data[0], self.trajectory_data[1])
43: 
44:         return self.iss_plot, self.trajectory_plot
45: 
46:     def animate(self) -> None:
47:         animation = FuncAnimation(self.fig, self.update, interval=1000, blit=True, cache_frame_data=False)
48:         plt.show()
49: 
50: 
51: if __name__ == "__main__":
52:     try:
53:         live_map = LiveMap()
54:         live_map.animate()
55:     except KeyboardInterrupt:
56:         plt.close('all')
57: 

Code Explanation

This class is more compact and uses familiar types and functions from matplotlib with some additions. This is how it works:

  • 1 – 5: Define the necessary imports:
    • 3, 4: We need the FuncAnimation for creating an animated real-time plot and Basemap or the map visualization.
  • 8 – 15: Define the LiveMap class using @define decorator from attrs with the following attributes:
    • 10, 11: Much like any other (static) matplotlib plot we need a figure and an ax object.
    • 12: A Basemap instance m.
    • 13: An attribute for the plot of the real time location.
    • 14: An attribute for the plot of the trajectory data points plot, i.e. previous location coordinates.
    • 15: A list where it will store the past coordinates to plot the trajectory.
  • 17 – 29: Use again the attrs_post_init to initialize the following attributes:
    • 18: An axis with a single subplot.
    • 19 – 23: A basemap with a cylindrical projection, intermediate resolution and showing the whole surface of the planet configured by:
    • llcrnrlon: longitude of lower left-hand corner of the desired map domain (degrees).
    • llcrnrlat: latitude of lower left-hand corner of the desired map domain (degrees).
    • urcrnrlon: longitude of upper right-hand corner of the desired map domain (degrees).
    • urcrnrlat: latitude of upper right-hand corner of the desired map domain (degrees).
    • 24: We set the color of the land mass and the lake color.
    • 25: Draw the coastlines with black solid line.
    • 26: Draw a boundary and fill in with light aqua color.
    • 27 and 28: Draw meridians and parallels representing the plot grid lines.
    • 29: Give the map a title and pad it a tiny bit to accommodate the meridian labels.
  • 31: Using the plot method of the BaseMap instance, we will draw our data on the map. We start with two empty lists because the data will be updated in real-time. The plot generates a list of Line2D objects.
  • 32: Do the same for the trajectory plot.
  • 34: Define the update function that we will call continuously:
    • 37, 38: Append the current position of the ISS to the trajectory_date attribute defined earlier.
    • 40: Set the data on the plot using the set_data function. This plot will show the current ISS position.
    • 42: Add the data to the trajectory plot. This plot will show the trajectory the ISS traveled.
    • Finally return both plots.
  • 46: Define the animate function. We use the FuncAnimation class which must be assigned to a variable, although the variable itself will not be used. It takes care of calling our data function continuously at a 1000 millisecond interval.

Code in Editor

livemap code

Here is the result:

matplotlib livemap

Conclusion

In this article, we showed two OOP-based programs to plot live data using Python and the related libraries. First, we examined pglive using PyQT backend for live line chart plotting. Second, we looked further at visualizing the data on a live map using matplotlib livemap. These two programs are designed to showcase the power of the two approaches and give you ideas for your own implementations.

Next Steps

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *