Source code for OptiDamTool.visual

import matplotlib
import matplotlib.lines
import matplotlib.pyplot
import pandas
import geopandas
import numpy
import os
import typing
from . import utility


[docs] class Visual: ''' Provides utilities for visualizing data. ''' def _validate_figure_ext( self, figure_file: str ) -> None: ''' Validate the extension of give figure file. ''' # figure plot figure = matplotlib.pyplot.figure( figsize=(1, 1) ) # check figure file extension fig_ext = os.path.splitext(figure_file)[-1][1:] if fig_ext not in list(figure.canvas.get_supported_filetypes().keys()): raise TypeError( f'Input figure_file extension ".{fig_ext}" is not supported for saving the figure' ) matplotlib.pyplot.close(figure) return None
[docs] def sediment_inflow_to_stream( self, stream_file: str, figure_file: str, fig_width: int | float = 10, fig_height: int | float = 5, sed_title: str = 'Sediment inflow (%)', cumsed_title: str = 'Cumulative sediment inflow (%)', stream_linewidth: int | float = 1, sed_colormap: str = 'tab20', cumsed_colormap: str = 'Accent', sed_tickgap: int | float = 1, cumsed_tickgap: int = 20, tick_fontsize: int = 12, title_fontsize: int = 12, gui_window: bool = True ) -> matplotlib.figure.Figure: ''' Generates a figure with two horizontally arranged plots: - Sediment inflow percentage to individual stream segments. - Cumulative sediment inflow percentage to each stream segment, including all upstream connected segments. Both plots are normalized by the total sediment input across all stream segments. Parameters ---------- stream_file : str Path to the input stream vector file, created by :meth:`OptiDamTool.Analysis.sediment_delivery_to_stream_geojson` figure_file : str Path to the output figure file. fig_width : float, optional Width of the figure in inches. Default is 10. fig_height : float, optional Height of the figure in inches. Default is 5. sed_title : str, optional Title of the suplot for sediment inflow percentage. Default is 'Sediment inflow (%)'. cumsed_title : str, optional Title of the suplot for cumulative sediment inflow percentage. Default is 'Cumulative sediment inflow (%)'. stream_linewidth : float, optional Line width for plotting the stream. Default is 1. sed_colormap : str, optional Name of the `colormap <https://matplotlib.org/stable/users/explain/colors/colormaps.html>`_ used to generate colors for sediment percentage. Default is 'tab20'. sumsed_colormap : str, optional Name of the colormap used to generate colors for cumulative sediment percentage. Default is 'winter'. sed_tickgap : float, optional Gap between two y-axis ticks on the sediment inflow percentage colorbar. Default is 1. cumsed_tickgap : int, optional Gap between two y-axis ticks on cumulative sediment inflow percentage colorbar. Default is 20. tick_fontsize : int, optional Font size of the y-axis tick labels on both colorbars. Default is 12. title_fontsize : int, optional Font size of the subplot titles. Default is 12. gui_window : bool, optional If True (default), open a graphical user interface window for the plot. Returns ------- Figure A Figure object containing plots of sediment inflow to the stream path. ''' # check static type of input variable origin utility._validate_variable_origin_static_type( vars_types=typing.get_type_hints( obj=self.sediment_inflow_to_stream ), vars_values=locals() ) # check validity of figure file self._validate_figure_ext( figure_file=figure_file ) # figure plot figure = matplotlib.pyplot.figure( figsize=(fig_width, fig_height) ) subplot = figure.subplots(1, 2) # stream GeoDataFrame stream_gdf = geopandas.read_file( filename=stream_file ) total_sediment = stream_gdf['cumsed_kg'].max() stream_gdf['sed_%'] = 100 * stream_gdf['sed_kg'] / total_sediment stream_gdf['cumsed_%'] = 100 * stream_gdf['cumsed_kg'] / total_sediment # plot sediment percentage sed_min = int(stream_gdf['sed_%'].min()) sed_max = int(stream_gdf['sed_%'].max()) + 1 stream_gdf.plot( column='sed_%', ax=subplot[0], cmap=sed_colormap, vmin=sed_min, vmax=sed_max, legend=True, legend_kwds={"shrink": 0.75}, linewidth=stream_linewidth ) subplot[0].set_title( label=sed_title, fontsize=title_fontsize ) # plot cumulative sediment percentage cumsed_min = int(stream_gdf['cumsed_%'].min()) stream_gdf.plot( column='cumsed_%', ax=subplot[1], cmap=cumsed_colormap, vmin=cumsed_min, legend=True, legend_kwds={"shrink": 0.75}, linewidth=stream_linewidth ) subplot[1].set_title( label=cumsed_title, fontsize=title_fontsize ) # remove ticks and labels from both axes for i in [0, 1]: subplot[i].tick_params( axis='both', which='both', left=False, bottom=False, labelleft=False, labelbottom=False ) # fix tick locations and labels in sediment inflow colorbar sed_cb = figure.get_axes()[2] sed_yticks = numpy.arange(0, sed_max + 0.01, sed_tickgap, dtype=type(sed_tickgap)) sed_cb.set_yticks( ticks=sed_yticks ) sed_cb.set_yticklabels( labels=[str(yt) for yt in sed_yticks], fontsize=tick_fontsize ) # fix tick locations and labels in cumulative sediment inflow colorbar cumsed_cb = figure.get_axes()[3] cumsed_yticks = list(range(cumsed_min, 100 + 1, cumsed_tickgap)) cumsed_cb.set_yticks( ticks=cumsed_yticks ) cumsed_cb.set_yticklabels( labels=[str(yt) for yt in cumsed_yticks], fontsize=tick_fontsize ) # saving figure figure.tight_layout() figure.savefig( fname=figure_file, bbox_inches='tight' ) # figure display matplotlib.pyplot.show() if gui_window else None matplotlib.pyplot.close(figure) return figure
[docs] def dam_location_in_stream( self, stream_file: str, dam_file: str, figure_file: str, fig_width: int | float = 6, fig_height: int | float = 6, fig_title: str = 'Dam locations with stream identifiers', stream_linewidth: int | float = 1, dam_marker: str = 'o', dam_markersize: int = 50, plot_damid: bool = True, damid_fontsize: int = 9, title_fontsize: int = 15, gui_window: bool = True ) -> matplotlib.figure.Figure: ''' Generates a figure showing dam locations along the stream path, with an option to display the stream segment identifiers for each dam. Parameters ---------- stream_file : str Path to the input stream vector file, created by one of: - :meth:`OptiDamTool.WatemSedem.dem_to_stream` - :meth:`OptiDamTool.Analysis.sediment_delivery_to_stream_geojson` dam_file : str Path to the input dam location vector file ``year_<start_year>_dam_location_point.geojson``, created by :meth:`OptiDamTool.Network.stodym_plus_with_drainage_scenarios`. figure_file : str Path to the output figure file. fig_width : float, optional Width of the figure in inches. Default is 6. fig_height : float, optional Height of the figure in inches. Default is 6. fig_title : str, optional Title of the figure. Default is 'Dam locations with identifiers'. stream_linewidth : float, optional Line width for plotting the stream. Default is 1. dam_marker : str, optional Marker style for dam points. Default is 'o'. dam_markersize : int, optional Marker size for dam points. Default is 50. plot_damid : bool, optional If True (default), plot stream segment identifiers for dams. damid_fontsize : int, optional Font size for stream segment identifier labels. Default is 9. title_fontsize : int, optional Font size of the figure title. Default is 15. gui_window : bool, optional If True (default), open a graphical user interface window for the plot. Returns ------- Figure A Figure object containing the dam locations plotted on the stream path. ''' # check static type of input variable origin utility._validate_variable_origin_static_type( vars_types=typing.get_type_hints( obj=self.dam_location_in_stream ), vars_values=locals() ) # check validity of figure file self._validate_figure_ext( figure_file=figure_file ) # figure plot figure = matplotlib.pyplot.figure( figsize=(fig_width, fig_height) ) subplot = figure.subplots(1, 1) # stream GeoDataFrame stream_gdf = geopandas.read_file( filename=stream_file ) # dam GeoDataFrame dam_gdf = geopandas.read_file( filename=dam_file ) # plot data stream_gdf.plot( ax=subplot, color='deepskyblue', linewidth=stream_linewidth, zorder=1 ) dam_gdf.plot( ax=subplot, color='orangered', marker=dam_marker, markersize=dam_markersize, zorder=2 ) # remove ticks and labels from both axes subplot.tick_params( axis='both', which='both', left=False, bottom=False, labelleft=False, labelbottom=False ) # plot stream segment identifiers of dams if plot_damid: for dam_id, dam_coords in zip(dam_gdf['ws_id'], dam_gdf.geometry): # xc, yc = dam_coords.x, dam_coords.y subplot.text( x=dam_coords.x, y=dam_coords.y, s=str(dam_id), fontsize=damid_fontsize, fontweight='bold', ha='left', va='center', color='black', zorder=3 ) # stream legend handle stream_legend = matplotlib.lines.Line2D( xdata=[0], ydata=[0], color='deepskyblue', linewidth=2, label='Stream' ) # dam legend handle dam_legend = matplotlib.lines.Line2D( xdata=[0], ydata=[0], color='orangered', marker=dam_marker, markersize=10, linestyle='None', label='Dam' ) # add custom legend subplot.legend( handles=[ stream_legend, dam_legend ], loc='best' ) # figure title figure.suptitle( fig_title, fontsize=title_fontsize ) # saving figure figure.tight_layout() figure.savefig( fname=figure_file, bbox_inches='tight' ) # figure display matplotlib.pyplot.show() if gui_window else None matplotlib.pyplot.close(figure) return figure
[docs] def system_statistics( self, json_file: str, figure_file: str, fig_width: int | float = 10, fig_height: int | float = 5, fig_title: str = 'Dam system statistics', plot_storage: bool = True, plot_trap: bool = True, plot_release: bool = True, plot_drainage: bool = True, system_linewidth: int | float = 3, xtick_gap: int = 10, ytop_offset: int | float = 0, ybottom_offset: int | float = 0, legend_loc: str = 'best', legend_fontsize: int = 12, tick_fontsize: int = 12, axis_fontsize: int = 15, title_fontsize: int = 15, gui_window: bool = True ) -> matplotlib.figure.Figure: ''' Generates a figure summarizing dam system statistics with annual percent changes for key metrics: - **Total remaining storage** across all dams, relative to the initial total storage at the start of each simulation year. - **Total sediment trapped** by all dams, relative to the total sediment input across all stream segments during the simulation year. - **Sediment released** by terminal dams and by drainage areas not covered by the dam system, relative to the total sediment input across all stream segments during the simulation year. - **Total controlled drainage area** across all dams, relative to the total stream drainage area at the start of each simulation year. Parameters ---------- json_file : str Path to the input ``system_statistics.json`` file, created by one of the methods: - :meth:`OptiDamTool.Network.stodym_plus` - :meth:`OptiDamTool.Network.stodym_plus_with_drainage_scenarios` figure_file : str Path to the output figure file. fig_width : float, optional Width of the figure in inches. Default is 10. fig_height : float, optional Height of the figure in inches. Default is 5. fig_title : str, optional Title of the figure. Default is 'Dam system statistics'. plot_storage : bool, optional If True (default), include the annual percent change in total remaining storage across all dams. plot_trap : bool, optional If True (default), include the annual percent change in total sediment trapped by all dams. plot_release : bool, optional If True (default), include the annual percent change in sediment released by terminal dams and by drainage areas not covered by the dam system. plot_drainage : bool, optional If True (default), include the annual percent change in total controlled drainage area across all dams. system_linewidth : float, optional Line width for plotting the system statistics. Default is 3. xtick_gap : int, optional Gap between two x-axis ticks. Default is 10. ytop_offset : float, optional Positive offset to increase the upper y-axis limit above 100, improving visibility when plot values are close to 100. Default is 0. ybottom_offset : float, optional Negative offset to decrease the lower y-axis limit below 0, improving visibility when plot values are close to 0. Default is 0. legend_loc : str, optional Location of the legend in the figure. Default is 'best'. legend_fontsize : int, optional Font size of the legend. Default is 12. tick_fontsize : int, optional Font size of the tick labels on both axes. Default is 12. axis_fontsize : int, optional Font size of the axis labels. Default is 15. title_fontsize : int, optional Font size of the figure title. Default is 15. gui_window : bool, optional If True (default), open a graphical user interface window of the plot. Returns ------- Figure A Figure object containing the dam system statistics plots. .. note:: Users can choose to plot all four metrics or only a subset of them by setting the corresponding boolean parameters to ``False``. ''' # check static type of input variable origin utility._validate_variable_origin_static_type( vars_types=typing.get_type_hints( obj=self.system_statistics ), vars_values=locals() ) # check validity of figure file self._validate_figure_ext( figure_file=figure_file ) # figure plot figure = matplotlib.pyplot.figure( figsize=(fig_width, fig_height) ) subplot = figure.subplots(1, 1) # Check that at least one plot option is enabled check_plot = [plot_storage, plot_trap, plot_release, plot_drainage] if check_plot == [False] * len(check_plot): raise ValueError('At least one plot type must be set to True') # system statistics DataFrame df = pandas.read_json( path_or_buf=json_file, orient='records' ) # plot remaining storage percentage if plot_storage: subplot.plot( df['start_year'], df['storage_%'], linestyle='-', linewidth=system_linewidth, color='cyan', label='Remaining storage' ) # plot trapped sediment percentage if plot_trap: subplot.plot( df['start_year'], df['sedtrap_%'], linestyle='-', linewidth=system_linewidth, color='forestgreen', label='Sediment trapped' ) # plot released sediment percentage if plot_release: subplot.plot( df['start_year'], df['sedrelease_%'], linestyle='-', linewidth=system_linewidth, color='red', label='Sediment released' ) # plot controlled drainage area percentage if plot_drainage: subplot.plot( df['start_year'], df['drainage_%'], linestyle='-', linewidth=system_linewidth, color='goldenrod', label='Controlled drainage' ) # legend subplot.legend( loc=legend_loc, fontsize=legend_fontsize ) # x-axis customization year_max = df['start_year'].max() xaxis_max = (int(year_max / xtick_gap) + 1) * xtick_gap subplot.set_xlim( left=0, right=xaxis_max ) xticks = range(0, xaxis_max + 1, xtick_gap) subplot.set_xticks( ticks=xticks ) subplot.set_xticklabels( labels=[str(xt) for xt in xticks], fontsize=12 ) subplot.tick_params( axis='x', which='both', direction='in', length=6, width=1, top=True, bottom=True, labeltop=False, labelbottom=True ) subplot.grid( visible=True, which='major', axis='x', color='gray', linestyle='--', linewidth=0.3 ) subplot.set_xlabel( xlabel='Year', fontsize=axis_fontsize ) # y-axis customization subplot.set_ylim( bottom=0 + ybottom_offset, top=100 + ytop_offset ) yticks = range(0, 100 + 1, 10) subplot.set_yticks( ticks=yticks ) subplot.set_yticklabels( labels=[str(yt) for yt in yticks], fontsize=tick_fontsize ) subplot.tick_params( axis='y', which='both', direction='in', length=6, width=1, left=True, right=True, labelleft=True, labelright=False ) subplot.grid( visible=True, which='major', axis='y', color='gray', linestyle='--', linewidth=0.3 ) subplot.set_ylabel( ylabel='Percentage (%)', fontsize=axis_fontsize ) # figure title figure.suptitle( fig_title, fontsize=title_fontsize ) # saving figure figure.tight_layout() figure.savefig( fname=figure_file, bbox_inches='tight' ) # figure display matplotlib.pyplot.show() if gui_window else None matplotlib.pyplot.close(figure) return figure
[docs] def dam_individual_features( self, json_file: str, figure_file: str, fig_width: int | float = 10, fig_height: int | float = 5, fig_title: str = '', colormap_name: str = 'coolwarm', dam_linewidth: int | float = 2, xtick_gap: int = 10, ytick_gap: int | float = 10, ytop_offset: int | float = 0, ybottom_offset: int | float = 0, legend_cols: int = 1, legend_fontsize: int = 12, tick_fontsize: int = 12, axis_fontsize: int = 15, title_fontsize: int = 15, gui_window: bool = True ) -> matplotlib.figure.Figure: ''' Generate a figure illustrating the annual variability of key features for each dam in the system. The input data are produced by the methods :meth:`OptiDamTool.Network.stodym_plus` and :meth:`OptiDamTool.Network.stodym_plus_with_drainage_scenarios`. - ``dam_drainage_area.json`` Percentage of the controlled drainage area for each dam, relative to the total stream drainage area, evaluated at the start of the simulation year. - ``dam_remaining_storage.json`` Remaining storage capacity as a percentage of the dam’s initial storage, evaluated at the start of the simulation year. - ``dam_trap_efficiency.json`` Trap efficiency expressed as a percentage, evaluated at the start of the simulation year. - ``dam_trapped_sediment.json`` Percentage of sediment trapped by the dam, relative to the total sediment input across all stream segments, evaluated at the end of the simulation year. Parameters ---------- json_file : str Path to the JSON file containing the dam feature data. figure_file : str Path to the output figure file. fig_width : float, optional Width of the figure in inches. Default is 10. fig_height : float, optional Height of the figure in inches. Default is 5. fig_title : str, optional Title of the figure. Default is 'Dam annual sediment trapping'. colormap_name : str, optional Name of the `colormap <https://matplotlib.org/stable/users/explain/colors/colormaps.html>`_ used to generate colors for individual dams. Default is 'coolwarm'. dam_linewidth : float, optional Line width for plotting the storage variation of individual dams. Default is 2. xtick_gap : int, optional Gap between two x-axis ticks. Default is 10. ytick_gap : float, optional Gap between two y-axis ticks. Default is 10. ytop_offset : float, optional Positive offset to increase the upper y-axis limit above 100, improving visibility when plot values are close to 100. Default is 0. ybottom_offset : float, optional Negative offset to decrease the lower y-axis limit below 0, improving visibility when plot values are close to 0. Default is 0. legend_cols : int, optional Number of columns to arrange legend items. Default is 1. legend_fontsize : int, optional Font size of the legend. Default is 12. tick_fontsize : int, optional Font size of the tick labels on both axes. Default is 12. axis_fontsize : int, optional Font size of the axis labels. Default is 15. title_fontsize : int, optional Font size of the figure title. Default is 15. gui_window : bool, optional If True (default), open a graphical user interface window of the plot. Returns ------- Figure A Figure object containing the annual sediment trapping percentage by each dam in the system. ''' # check static type of input variable origin utility._validate_variable_origin_static_type( vars_types=typing.get_type_hints( obj=self.dam_individual_features ), vars_values=locals() ) # check validity of figure file self._validate_figure_ext( figure_file=figure_file ) # setting figure figure = matplotlib.pyplot.figure( figsize=(fig_width, fig_height) ) figure_grid = figure.add_gridspec( nrows=1, ncols=5 ) # setting subplot plot_data = figure.add_subplot(figure_grid[0, :4]) # setting subplot for legend plot_legend = figure.add_subplot(figure_grid[0, 4]) # DataFrame df = pandas.read_json( path_or_buf=json_file, orient='records' ) # remove values that are not required df = df.where( cond=(df >= 0) & (df <= 100) ) # sort dam columns dam_cols = sorted( [col for col in df.columns if col != 'start_year'], key=int ) # set colors colormap = matplotlib.colormaps.get_cmap( cmap=colormap_name ) color_dict = { dam_cols[i]: colormap(i / len(dam_cols)) for i in range(len(dam_cols)) } # plot dam features legend_handles = [] for dam in dam_cols: dam_line2d = plot_data.plot( df['start_year'], df[dam], linestyle='-', linewidth=dam_linewidth, color=color_dict[dam] ) legend_handles.append(dam_line2d[0]) # plot legend plot_legend.legend( handles=legend_handles, labels=dam_cols, loc='center', fontsize=legend_fontsize, ncols=legend_cols, frameon=False ) plot_legend.axis('off') # x-axis customization year_max = df['start_year'].max() xaxis_max = (int(year_max / xtick_gap) + 1) * xtick_gap plot_data.set_xlim( left=0, right=xaxis_max ) xticks = range(0, xaxis_max + 1, xtick_gap) plot_data.set_xticks( ticks=xticks ) plot_data.set_xticklabels( labels=[str(xt) for xt in xticks], fontsize=12 ) plot_data.tick_params( axis='x', which='both', direction='in', length=6, width=1, top=True, bottom=True, labeltop=False, labelbottom=True ) plot_data.grid( visible=True, which='major', axis='x', color='gray', linestyle='--', linewidth=0.3 ) plot_data.set_xlabel( xlabel='Year', fontsize=axis_fontsize ) # y-axis customization df_max = df[dam_cols].max().max() yaxis_ub = (int(df_max / ytick_gap) + 1) * ytick_gap yaxis_max = yaxis_ub if yaxis_ub < 100 else 100 plot_data.set_ylim( bottom=0 + ybottom_offset, top=yaxis_max + ytop_offset ) yticks = numpy.arange(0, yaxis_max + 0.01, ytick_gap, dtype=type(ytick_gap)) plot_data.set_yticks( ticks=yticks ) plot_data.set_yticklabels( labels=[str(yt) for yt in yticks], fontsize=tick_fontsize ) plot_data.tick_params( axis='y', which='both', direction='in', length=6, width=1, left=True, right=True, labelleft=True, labelright=False ) plot_data.grid( visible=True, which='major', axis='y', color='gray', linestyle='--', linewidth=0.3 ) plot_data.set_ylabel( ylabel='Percentage (%)', fontsize=axis_fontsize ) # figure title figure.suptitle( fig_title, fontsize=title_fontsize ) # saving figure figure.tight_layout() figure.savefig( fname=figure_file, bbox_inches='tight' ) # figure display matplotlib.pyplot.show() if gui_window else None matplotlib.pyplot.close(figure) return figure