# -*- coding: utf-8 -*-
# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Contributors:
# INITIAL AUTHORS - initial API and implementation and/or initial
# documentation
# :author: Matthias De Lozzo
# OTHER AUTHORS - MACROSCOPIC CHANGES
"""An abstract class to plot data from a :class:`.Dataset`.
The :mod:`~gemseo.post.dataset.dataset_plot` module
implements the abstract :class:`.DatasetPlot` class
whose purpose is to build a graphical representation of a :class:`.Dataset`
and to display it on screen or save it to a file.
This abstract class has to be overloaded by concrete ones
implementing at least method :meth:`!DatasetPlot._run`.
"""
from __future__ import division, unicode_literals
from numbers import Number
from typing import (
TYPE_CHECKING,
Iterable,
List,
Mapping,
Optional,
Sequence,
Tuple,
Union,
)
import six
from custom_inherit import DocInheritMeta
from numpy import linspace
from gemseo.utils.file_path_manager import FilePathManager, FileType
from gemseo.utils.matplotlib_figure import save_show_figure
if TYPE_CHECKING:
from gemseo.core.dataset import Dataset
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from six import string_types
from gemseo.utils.py23_compat import Path
DatasetPlotPropertyType = Union[str, int, float, Sequence[Union[str, int, float]]]
[docs]@six.add_metaclass(
DocInheritMeta(
abstract_base_class=True,
style="google_with_merge",
include_special_methods=True,
)
)
class DatasetPlot(object):
"""Abstract class for plotting a dataset.
Attributes:
dataset (Dataset): The dataset to be plotted.
"""
COLOR = "color"
COLORMAP = "colormap"
FIGSIZE_X = "figsize_x"
FIGSIZE_Y = "figsize_y"
LINESTYLE = "linestyle"
def __init__(
self,
dataset, # type: Dataset
): # type: (...) -> None
"""
Args:
dataset: The dataset containing the data to plot.
Raises:
ValueError: If the dataset is empty.
"""
if dataset.is_empty():
raise ValueError("Dataset is empty.")
self.dataset = dataset
self.__title = None
self.__xlabel = None
self.__ylabel = None
self.__zlabel = None
self.__font_size = 10
self.__xmin = None
self.__xmax = None
self.__ymin = None
self.__ymax = None
self.__zmin = None
self.__zmax = None
self.__rmin = None
self.__rmax = None
self.__line_style = None
self.__color = None
self.__figsize = (8, 8)
self.__colormap = "rainbow"
self.__legend_location = "best"
default_name = FilePathManager.to_snake_case(self.__class__.__name__)
self.__file_path_manager = FilePathManager(
FileType.FIGURE, default_name=default_name
)
self.__output_files = []
@property
def output_files(self): # type: (...) -> List[str]
"""The paths to the output files."""
return self.__output_files
@property
def legend_location(self): # type: (...) -> str
"""The location of the legend."""
return self.__legend_location
@legend_location.setter
def legend_location(self, value):
self.__legend_location = value
@property
def colormap(self): # type: (...) -> str
"""The color map."""
return self.__colormap
@colormap.setter
def colormap(self, value):
self.__colormap = value
@property
def figsize(self): # type: (...) -> Tuple[int,int]
"""The figure size."""
return self.__figsize
@property
def figsize_x(self): # type: (...) -> int
"""The x-component of figure size."""
return self.__figsize[0]
@figsize_x.setter
def figsize_x(self, value):
self.__figsize = (value, self.figsize_y)
@property
def figsize_y(self): # type: (...) -> int
"""The y-component of figure size."""
return self.__figsize[1]
@figsize_y.setter
def figsize_y(self, value):
self.__figsize = (self.figsize_x, value)
@property
def color(self): # type: (...) -> str
"""The color of the series."""
return self.__color
@color.setter
def color(self, value):
self.__color = value
@property
def linestyle(self): # type: (...) -> str
"""The line style of the series."""
return self.__line_style
@linestyle.setter
def linestyle(self, value):
self.__line_style = value
@property
def title(self): # type: (...) -> str
"""The title of the plot."""
return self.__title
@title.setter
def title(self, value):
self.__title = value
@property
def xlabel(self): # type: (...) -> str
"""The label for the x-axis."""
return self.__xlabel
@xlabel.setter
def xlabel(self, value):
self.__xlabel = value
@property
def ylabel(self): # type: (...) -> str
"""The label for the y-axis."""
return self.__ylabel
@ylabel.setter
def ylabel(self, value):
self.__ylabel = value
@property
def zlabel(self): # type: (...) -> str
"""The label for the z-axis."""
return self.__zlabel
@zlabel.setter
def zlabel(self, value):
self.__zlabel = value
@property
def font_size(self): # type: (...) -> int
"""The font size."""
return self.__font_size
@font_size.setter
def font_size(self, value):
self.__font_size = value
@property
def xmin(self): # type: (...) -> float
"""The minimum value on the x-axis."""
return self.__xmin
@xmin.setter
def xmin(self, value):
self.__xmin = value
@property
def xmax(self): # type: (...) -> float
"""The maximum value on the x-axis."""
return self.__xmax
@xmax.setter
def xmax(self, value):
self.__xmax = value
@property
def ymin(self): # type: (...) -> float
"""The minimum value on the y-axis."""
return self.__ymin
@ymin.setter
def ymin(self, value):
self.__ymin = value
@property
def ymax(self): # type: (...) -> float
"""The maximum value on the y-axis."""
return self.__ymax
@ymax.setter
def ymax(self, value):
self.__ymax = value
@property
def rmin(self): # type: (...) -> float
"""The minimum value on the r-axis."""
return self.__rmin
@rmin.setter
def rmin(self, value):
self.__rmin = value
@property
def rmax(self): # type: (...) -> float
"""The maximum value on the r-axis."""
return self.__rmax
@rmax.setter
def rmax(self, value):
self.__rmax = value
@property
def zmin(self):
"""The minimum value on the z-axis."""
return self.__zmin
@zmin.setter
def zmin(self, value):
self.__zmin = value
@property
def zmax(self): # type: (...) -> float
"""The maximum value on the z-axis."""
return self.__zmax
@zmax.setter
def zmax(self, value):
self.__zmax = value
[docs] def execute(
self,
save=True, # type: bool
show=False, # type: bool
file_path=None, # type: Optional[Union[str,Path]]
directory_path=None, # type: Optional[Union[str,Path]]
file_name=None, # type: Optional[str]
file_format=None, # type: Optional[str]
properties=None, # type: Optional[Mapping[str,DatasetPlotPropertyType]]
**plot_options # type: Union[str,int,float,bool,Sequence[str]]
): # type: (...) -> List[Figure]
"""Execute the post processing.
Args:
save: If True, save the plot.
show: If True, display the plot.
file_path: The path of the file to save the figures.
If None,
create a file path
from ``directory_path``, ``file_name`` and ``file_format``.
directory_path: The path of the directory to save the figures.
If None, use the current working directory.
file_name: The name of the file to save the figures.
If None, use a default one generated by the post-processing.
file_format: A file format, e.g. 'png', 'pdf', 'svg', ...
If None, use a default file extension.
properties: The general properties of a :class:`.DatasetPlot`.
**plot_options: The options of the current class
inheriting from :class:`.DatasetPlot`.
Returns:
The figures.
"""
if file_path is not None:
file_path = Path(file_path)
file_path = self.__file_path_manager.create_file_path(
file_path=file_path,
directory_path=directory_path,
file_name=file_name,
file_extension=file_format,
)
return self._run(properties or {}, save, show, file_path, **plot_options)
def _run(
self,
properties, # type: Mapping[str,DatasetPlotPropertyType]
save, # type:bool
show, # type: bool
file_path, # type: Path
**plot_options
): # type: (...)-> List[Figure]
"""Create the post processing and save or display it.
Args:
properties: The general properties of a :class:`.DatasetPlot`.
save: If True, save the plot on the disk.
show: If True, display the plot.
file_path: The file path.
**plot_options: The options of the current class
inheriting from :class:`.DatasetPlot`.
Returns:
The figures.
"""
figures = self._plot(properties=properties, **plot_options)
for index, sub_figure in enumerate(figures):
if save:
if len(figures) > 1:
fig_file_path = self.__file_path_manager.add_suffix(
file_path, index
)
else:
fig_file_path = file_path
self.__output_files.append(str(fig_file_path))
else:
fig_file_path = None
sub_figure.tight_layout()
save_show_figure(
sub_figure,
show,
fig_file_path,
)
return figures
def _plot(
self,
properties, # type: Mapping[str,DatasetPlotPropertyType]
): # type: (...) -> List[Figure]
"""Define the way as the dataset is plotted.
Args:
properties: The general properties of a :class:`.DatasetPlot`.
Returns:
The figures.
"""
raise NotImplementedError
def _get_variables_names(
self,
dataframe_columns, # type: Iterable[Tuple]
): # type: (...) -> List[str]
"""Return the names of the variables from the columns of a pandas DataFrame.
Args:
dataframe_columns: The columns of a pandas DataFrame.
Returns:
The names of the variables.
"""
new_columns = []
for column in dataframe_columns:
if self.dataset.sizes[column[1]] == 1:
new_columns.append(column[1])
else:
new_columns.append("{}({})".format(column[1], column[2]))
return new_columns
def _get_label(
self,
variable, # type: Union[str,Tuple[str,int]]
): # type: (...) -> Tuple[str,Tuple[str, int]]
"""Return the label related to a variable name and a refactored variable name.
Args:
variable: The name of a variable,
either a string (e.g. "x") or a (name, component) tuple (e.g. ("x", 0)).
Returns:
The label related to a variable, e.g. "x(0)",
as well as the refactored variable name, e.g. (x,0).
"""
error_message = (
"'variable' must be either a string or a tuple"
" whose first component is a string and second"
" one is an integer"
)
if isinstance(variable, string_types):
label = variable
variable = (self.dataset.get_group(variable), variable, "0")
elif hasattr(variable, "__len__") and len(variable) == 3:
is_string = isinstance(variable[0], string_types)
is_string = is_string and isinstance(variable[1], string_types)
is_number = isinstance(variable[2], Number)
if is_string and is_number:
label = "{}({})".format(variable[1], variable[2])
variable[2] = str(variable[2])
variable = tuple(variable)
else:
raise TypeError(error_message)
else:
raise TypeError(error_message)
return label, variable
def _set_color(
self,
properties, # type: Mapping[str,DatasetPlotPropertyType],
n_items, # type: int
): # type: (...) -> None
"""Set the colors of the items to be plotted.
Args:
properties: The graphical properties of the :class:`.DatasetPlot`.
n_items: The number of items to be plotted.
"""
colormap = plt.cm.get_cmap(self.colormap)
default_color = [colormap(color) for color in linspace(0, 1, n_items)]
self.color = properties.get(self.COLOR) or self.color or default_color
if isinstance(self.color, string_types):
self.color = [self.color] * n_items
def _set_linestyle(
self,
properties, # type: Mapping[str,DatasetPlotPropertyType],
n_items, # type: int
default_value, # type: str
): # type: (...) -> None
"""Set the line style of the items to be plotted.
Args:
properties: The graphical properties of the :class:`.DatasetPlot`.
n_items: The number of items to be plotted.
default_value: The default line style.
"""
self.linestyle = (
properties.get(self.LINESTYLE) or self.linestyle or default_value
)
if isinstance(self.linestyle, string_types):
self.linestyle = [self.linestyle] * n_items