import xarray
import pandas as pd
import matplotlib.pyplot as plt
import ctd_tools.ctd_parameters as ctdparams
import numpy as np
import gsw
[docs]
class CtdPlotter:
"""
Plots different diagrams for CTD data from a xarray Dataset.
"""
def __init__(self, data: xarray.Dataset):
self.data = data
def __get_dataset_without_nan(self):
ds = self.data
return ds.dropna(dim=ctdparams.TIME)
def __plot_density_isolines(self, ds, plt):
""" Plots density isolines into a given T-S diagram plot """
# Define the min / max values for plotting isopycnals
t_min = ds[ctdparams.TEMPERATURE].values.min()
t_max = ds[ctdparams.TEMPERATURE].values.max()
s_min = ds[ctdparams.SALINITY].values.min()
s_max = ds[ctdparams.SALINITY].values.max()
# Calculate "padding" for temperature axis
t_width = t_max - t_min
t_min -= (t_width * 0.1)
t_max += (t_width * 0.1)
# Calcualte "padding" for salinity axis
s_width = s_max - s_min
s_min -= (s_width * 0.1)
s_max += (s_width * 0.1)
# Calculate how many gridcells we need in the x and y dimensions
factor = round(max([t_width,s_width]) / min([t_width, s_width]))
if s_width > t_width:
xdim = 150 # round(round(s_max - s_min, 3) / 0.001)
ydim = round(150 / factor) # round(round(t_max - t_min, 3) / 0.001)
else:
ydim = 150
xdim = round(150 / factor)
density = np.zeros((int(ydim), int(xdim)))
# Create temp and salt vectors of appropiate dimensions
ti = np.linspace(t_min,t_max,ydim)
si = np.linspace(s_min,s_max,xdim)
# Loop to fill in grid with densities
for j in range(0,int(ydim)):
for i in range(0, int(xdim)):
density[j,i] = gsw.rho(si[i], ti[j],0)
# Subtract 1000 to convert density to sigma-t
sigma_t = density - 1000
# Plot isolines
CS = plt.contour(si, ti, sigma_t, linewidths=1, linestyles='dashed', colors='gray')
plt.clabel(CS, fontsize=8, inline=1, fmt='%1.2f') # Label every second level
# Add sigma_0 in gray in the left upper corner
plt.text(0.02, 0.95, r"$\sigma_0$", color='gray', fontsize=18,
fontweight='bold', transform=plt.gca().transAxes)
[docs]
def plot_ts_diagram(self, output_file=None, title='T-S Diagram', dot_size=70, use_colormap=True,
show_density_isolines=True, colormap='jet', show_lines_between_dots=True,
show_grid=True):
''' Plots a T-S diagram. '''
# Check for necessary data keys
if ctdparams.TEMPERATURE not in self.data:
raise ValueError('Temperature data is missing. This is necessary for plotting a T-S diagram.')
if ctdparams.SALINITY not in self.data:
raise ValueError('Salinity data is missing. This is necessary for plotting a T-S diagram.')
if ctdparams.DEPTH not in self.data:
raise ValueError('Depth data is missing. This is necessary for plotting a T-S diagram.')
# Get dataset without NaN values
ds = self.__get_dataset_without_nan()
temperature = ds[ctdparams.TEMPERATURE]
salinity = ds[ctdparams.SALINITY]
depth = ds[ctdparams.DEPTH]
# Check for potential temperature
if ctdparams.POTENTIAL_TEMPERATURE in ds:
temperature = ds[ctdparams.POTENTIAL_TEMPERATURE]
# Create figure
fig = plt.figure(figsize=(15, 8))
# Create a line plot of temperature vs. salinity
if show_lines_between_dots:
plt.plot(salinity, temperature, color='gray', linestyle='-', linewidth=0.5)
# Create a scatter plot of temperature vs. salinity
if use_colormap:
plt.scatter(salinity, temperature, c=depth, cmap=colormap, marker='o', s=dot_size)
plt.colorbar(label='Depth [m]') # Plot legend for colormap
else:
plt.scatter(salinity, temperature, c='black', marker='o', s=dot_size)
# Add grid lines to the plot for better readability
if show_grid:
plt.grid(color='gray', linestyle='--', linewidth=0.5)
# Set plot labels and title
plt.title(title)
plt.xlabel('Salinity [PSU]')
plt.ylabel(ds[ctdparams.TEMPERATURE].attrs['long_name']+ \
" ["+ds[ctdparams.TEMPERATURE].attrs['units']+"]")
# Check for potential temperature
if ctdparams.POTENTIAL_TEMPERATURE in ds:
plt.ylabel(ds[ctdparams.POTENTIAL_TEMPERATURE].attrs['long_name']+ \
" ["+ds[ctdparams.POTENTIAL_TEMPERATURE].attrs['units']+"]")
# Integrate density isolines if wanted
if show_density_isolines:
self.__plot_density_isolines(ds, plt)
# Enable tight layout
plt.tight_layout()
# Show the plot
if not output_file:
plt.show()
# Save the plot as an image
elif output_file:
plt.savefig(output_file)
[docs]
def plot_profile(self, output_file=None, title='Salinity and Temperature Profiles', show_grid=True,
dot_size=3, show_lines_between_dots=True):
''' Plots a vertical CTD profile for temperature and salinity. '''
# Check for necessary data keys
if ctdparams.TEMPERATURE not in self.data:
raise ValueError('Temperature data is missing. This is necessary for plotting a T-S diagram.')
if ctdparams.SALINITY not in self.data:
raise ValueError('Salinity data is missing. This is necessary for plotting a T-S diagram.')
if ctdparams.DEPTH not in self.data:
raise ValueError('Depth data is missing. This is necessary for plotting a T-S diagram.')
# Get dataset without NaN values
ds = self.__get_dataset_without_nan()
# Extract temperature, salinity, and depth variables from the dataset
temperature = ds[ctdparams.TEMPERATURE]
salinity = ds[ctdparams.SALINITY]
depth = ds[ctdparams.DEPTH]
# Figure out if depth contains only positive or negative values
depth_min = (depth.min())
depth_max = (depth.max())
if (depth_min <= 0 and depth_max <= 0):
depth = depth * (-1)
# Create a scatter plot of salinity and temperature with depth as the y-axis
fig, ax1 = plt.subplots(figsize=(8, 6))
# Invert y-axis for depth
plt.gca().invert_yaxis()
# Calculate the range for salinity with some padding for aesthetics
salinity_padding = ((salinity.max() - salinity.min()) * 0.1)
salinity_range = ((salinity.min() - salinity_padding),
(salinity.max() + salinity_padding))
# Plot salinity on the primary y-axis
salinity_color = 'blue'
ax1.set_xlim(salinity_range)
ax1.scatter(salinity, depth, c=salinity_color, label='Salinity', s=dot_size)
ax1.tick_params(axis='x', labelcolor=salinity_color)
# Calculate the range for temperature with some padding for aesthetics
temperature_color = 'red'
temperature_padding = ((temperature.max() - temperature.min()) * 0.1)
temperature_range = ((temperature.min() - temperature_padding),
(temperature.max() + temperature_padding))
# Plot temperature on the secondary x-axis
ax2 = ax1.twiny() # Create a twin axis for temperature
ax2.set_xlim(temperature_range)
ax2.scatter(temperature, depth, c=temperature_color, label='Temperature', s=dot_size)
ax2.tick_params(axis='x', labelcolor=temperature_color)
# Plot lines between the dots
if show_lines_between_dots:
ax1.plot(salinity, depth, color=salinity_color, linestyle='-', linewidth=0.5)
ax2.plot(temperature, depth, color=temperature_color, linestyle='-', linewidth=0.5)
# Add grid lines to the plot for better readability
if show_grid:
ax1.grid(color='gray', linestyle='--', linewidth=0.5)
# Set axis labels and title
ax1.set_title(title)
ax1.set_xlabel('Salinity', color=salinity_color)
ax1.set_ylabel('Depth', color='black')
ax2.set_xlabel('Temperature', color=temperature_color)
# Add a legend
ax1.legend()
# Adjust layout
fig.tight_layout()
# Show the plot
if not output_file:
plt.show()
# Save the plot as an image
elif output_file:
plt.savefig(output_file)
[docs]
def plot_time_series(self, parameter_name, output_file=None,
ylim_min=None, ylim_max=10, xlim_min=None, xlim_max=None):
''' Plots a times series for a given parameter. '''
# Create a plot
fig, ax = plt.subplots(figsize=(10, 5)) # Customize figure size
# Plot the 'temperature' data variable
self.data[parameter_name].plot.line('b-', ax=ax)
# Creating string date range
first_date = np.min(self.data[ctdparams.TIME].values).astype('datetime64[D]')
last_date = np.max(self.data[ctdparams.TIME].values).astype('datetime64[D]')
if first_date == last_date:
dateline = f"on {first_date}"
else:
dateline = f"{first_date} to {last_date}"
# Customize the plot with titles and labels
long_name = parameter_name
if 'long_name' in self.data[parameter_name].attrs:
long_name = self.data[parameter_name].attrs['long_name']
ax.set_title(f"{long_name} over time ({dateline})")
ax.set_xlabel('Time')
ax.set_ylabel(long_name+" ["+self.data[parameter_name].attrs['units']+"]")
if ylim_min and ylim_max:
ax.set_ylim(ylim_min, ylim_max)
# Optionally, you can format the x-axis to better display dates
plt.gcf().autofmt_xdate() # Auto-format date on x-axis
# Show the plot
if not output_file:
plt.show()
# Save the plot as an image
elif output_file:
plt.savefig(output_file)