Source code for pynetdesign.visualization.geometry

from pynetdesign.modelling.io import *
from pynetdesign.visualization.utils import *
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
#import cmcrameri.cm as cmc
import pandas as pd
import numpy as np
import warnings

[docs] def plot_stations_view(df: pd.DataFrame, view: str = 'xy', x_offset: float = None, y_offset: float = None, z_offset: float = None, offset_perc: float = 0.1, plot_names: bool = True, station_marker_size: int = 100, grid_points: np.ndarray = None, grid_marker_size: int = 1, use_warnings: bool = True): r""" Plots either a map view or side view of stations as filled triangles with name labels. Optionally plots grid points as well. Color-codes the triangles based on noise level and adds a colorbar. Parameters ---------- df : :obj:`pandas.DataFrame` Data frame containing station names, coordinates, and noise levels. Expected columns: 'Name', 'X', 'Y', 'Z' (for side view), 'NoiseLevel'. view : :obj:`str`, optional Specifies the view to plot. Options are 'xy' (X-Y), 'xz' (X-Z), or 'yz' (Y-Z). Default is 'xy'. x_offset : :obj:`float`, optional Offset added to the min and max X coordinates for axis limits. If None, defaults to 10% of the X coordinate range. y_offset : :obj:`float`, optional Offset added to the min and max Y coordinates for axis limits. If None, defaults to 10% of the Y coordinate range. z_offset : :obj:`float`, optional Offset added to the min and max Z coordinates for axis limits (side view only). If None, defaults to 10% of the Z coordinate range. offset_perc : :obj:`float`, optional, default: 0.1 Percentage of coordinate range to use as offset. Used only if offset value is None. plot_names : :obj:`bool`, optional, default: True Flag to plot station names or not (True or False) station_marker_size : :obj:`int`, optional, default: 100 Size of the station markers in the scatter plot grid_points: :obj:`np.ndarray`, optional, default: None Array of shape (ngrid, 3) representing grid points' coordinates (X, Y, Z). grid_marker_size: :obj:`int`, optional, default: 10 Size of the grid points markers in the scatter plot use_warnings: :obj:`bool`, optional, default: True If True, enable warnings, if False, disable them. Returns ------- fig : :obj:`matplotlib.figure.Figure` The figure object containing the plot. ax : :obj:`matplotlib.axes.Axes` The axes object of the plot. cax : :obj:`matplotlib.axes.Axes` The axes object for the colorbar. cbar : :obj:`matplotlib.colorbar.Colorbar` The colorbar object. """ # Validate view parameter if view not in ['xy', 'xz', 'yz']: raise ValueError("Invalid view specified. Choose 'xy', 'xz', or 'yz'.") # Check if required columns exist required_columns = ['X', 'Y', 'NoiseLevel'] if view in ['xz', 'yz']: required_columns.append('Z') for col in required_columns: if col not in df.columns: raise ValueError(f"The '{col}' column is missing in the geometry data.") # Extract data from the dataframe x = df['X'] y = df['Y'] if view in ['xz', 'yz']: z = df['Z'] # Check if noise level units are consistent if 'noise_unit' in df.attrs: noise_level = df['NoiseLevel'].to_numpy() clb_label = f"Noise Level ({df.attrs['noise_unit']})" plot_clb = True else: noise_level = np.full(len(x), 0) plot_clb = False # Check if Name column can be used if plot_names: if 'Name' in df.columns: names = df['Name'] else: plot_names = False if use_warnings: warnings.warn("Can't plot station names: Name column is missing in the geometry data.") if grid_points is not None: # Grid points are expected to be of shape (ngrid, 3), where each row is (x, y, z) gx = grid_points[:, 0] gy = grid_points[:, 1] gz = grid_points[:, 2] # Get limits from stations and grid points xmin = min([x.min(), gx.min()]) xmax = max([x.max(), gx.max()]) ymin = min([y.min(), gy.min()]) ymax = max([y.max(), gy.max()]) if view in ['xz', 'yz']: zmin = min([z.min(), gz.min()]) zmax = max([z.max(), gz.max()]) else: # Get limits only from stations xmin = x.min() xmax = x.max() ymin = y.min() ymax = y.max() if view in ['xz', 'yz']: zmin = z.min() zmax = z.max() # Calculate offsets if x_offset is None: x_offset = offset_perc * (xmax - xmin) if y_offset is None: y_offset = offset_perc * (ymax - ymin) if view in ['xz', 'yz'] and z_offset is None: z_offset = offset_perc * (zmax - zmin) if z_offset < 0.001 * (xmax - xmin + ymax - ymin): z_offset = offset_perc * min(xmax - xmin, ymax - ymin) # Create figure and axes fig, ax = plt.subplots(figsize=(10, 10)) # Ensure equal scaling ax.set_aspect('equal', 'box') # First plot grid points if provided if grid_points is not None: if view == 'xy': ax.scatter(gx, gy, color='gray', s=grid_marker_size, marker='s', zorder=1, label='Grid Points') elif view == 'xz': ax.scatter(gx, gz, color='gray', s=grid_marker_size, marker='s', zorder=1, label='Grid Points') else: # view == 'yz' ax.scatter(gy, gz, color='gray', s=grid_marker_size, marker='s', zorder=1, label='Grid Points') # Plot based on the view if view == 'xy': scatter = ax.scatter(x, y, c=noise_level, cmap='viridis', s=station_marker_size, marker='^', zorder=2) ax.set_xlim(xmin - x_offset, xmax + x_offset) ax.set_ylim(ymin - y_offset, ymax + y_offset) ax.set_xlabel('X Coordinate') ax.set_ylabel('Y Coordinate') ax.set_title('Map View of Stations') elif view == 'xz': scatter = ax.scatter(x, z, c=noise_level, cmap='viridis', s=station_marker_size, marker='^', zorder=2) ax.set_xlim(xmin - x_offset, xmax + x_offset) ax.set_ylim(zmax + z_offset, zmin - z_offset) ax.set_xlabel('X Coordinate') ax.set_ylabel('Z Coordinate') ax.set_title('Side View of Stations: X-Z') else: # view == 'yz' scatter = ax.scatter(y, z, c=noise_level, cmap='viridis', s=station_marker_size, marker='^', zorder=2) ax.set_xlim(ymin - y_offset, ymax + y_offset) ax.set_ylim(zmax + z_offset, zmin - z_offset) ax.set_xlabel('Y Coordinate') ax.set_ylabel('Z Coordinate') ax.set_title('Side View of Stations: Y-Z') # Add labels to the points if plot_names: for i, name in enumerate(names): name = name + " " # to make a gap between the name and the marker if view == 'xy': ax.text(x[i], y[i], name, fontsize=12, ha='right', va='bottom', zorder=3) elif view == 'xz': ax.text(x[i], z[i], name, fontsize=12, ha='right', va='bottom', zorder=3) else: # view == 'yz' ax.text(y[i], z[i], name, fontsize=12, ha='right', va='bottom', zorder=3) # Add grid ax.grid(True) if plot_clb: # Create colorbar if view in ['xz', 'yz']: bbox = ax.get_position() cax_size_perc = 5 * bbox.width / bbox.height divider = make_axes_locatable(ax) cax = divider.append_axes("bottom", size=f"{cax_size_perc}%", pad=0.65) cbar = plt.colorbar(scatter, cax=cax, orientation="horizontal") cbar.set_label(clb_label, labelpad=15) else: divider = make_axes_locatable(ax) cax = divider.append_axes("right", size="5%", pad=0.2) cbar = plt.colorbar(scatter, cax=cax) cbar.set_label(clb_label, rotation=270, labelpad=15) else: cax = None cbar = None plt.show() return fig, ax, cax, cbar