"""Panels module."""
import pandas as pd
from trelliscope import utils
from trelliscope.panel_source import FilePanelSource, PanelSource
[docs]
class PanelOptions:
"""Stores settings associated with panel.
The PanelOptions object is used by the :func:`trelliscope.trelliscope.Trelliscope.set_panel_options`
method to pre-specify information about the :class:`.Panel` before it is actually
created. Then, later when the :class:`.Panel` object is inferred, data from this object will be used
to populate it.
"""
[docs]
def __init__(
self,
width: int = 600,
height: int = 400,
format: str = None,
force: bool = False,
prerender: bool = True,
type: str = None,
aspect: float = None,
) -> None:
"""Create PanelOptions objects.
Args:
width: Strictly positive integer representing the panel width in pixels.
height: Stricly positive integer representing the panel height in pixels.
format: One of 'png', 'svg' or 'html'. If `None` then is inferred.
force: (Default False) Whether to force creation of plots, even if they already exist.
prerender: (Default True) Prerender.
type: Type of panel. TODO: list possible types.
aspect: Aspect ratio of the panel, if not given it is determined from ``width`` / ``height``.
Raises:
ValueError: If ``width`` or ``height`` is not a positive numeric value.
Or, if ``force`` or ``prerender`` is not a boolean.
Or, if ``format`` not an allowed value.
"""
utils.check_positive_numeric(width, "width")
utils.check_positive_numeric(height, "height")
utils.check_bool(force, "force")
utils.check_bool(prerender, "prerender")
if format is not None:
utils.check_enum(format, ["png", "svg", "html"])
self.width = width
self.height = height
self.format = format
self.force = force
self.prerender = prerender
self.type = type
if aspect is None:
self.aspect = width / height
else:
self.aspect = aspect
[docs]
class Panel:
"""Base Panel object."""
_FIGURE_SUFFIX = "__FIGURE"
_PANEL_TYPE_IMAGE = "img"
_PANEL_TYPE_IFRAME = "iframe"
[docs]
def __init__(
self,
varname: str,
panel_type_str: str,
source: PanelSource,
aspect_ratio: float = 1.5,
is_image: bool = True,
writeable: bool = False,
) -> None:
"""Create Panel object.
Args:
varname: Variable name.
panel_type_str: Panel type, one of ['img', 'iframe'].
source: PanelSource object, defining where the load the panel content from.
aspect_ratio: Aspect ratio of the displayed panel.
is_image: Boolean flag whether panel is an image.
writeable: Boolean flag whether panel is writable.
"""
self.varname = varname
self.aspect_ratio = aspect_ratio
self.is_image = is_image
self.is_writeable = writeable
self.should_copy = False
self.source = source
self.panel_type_str = panel_type_str
self.panels_written = False
self.figure_varname = None
[docs]
def to_dict(self):
"""Return a dictionary of this Panel's attributes.."""
# Default serialization behavior is sufficient
return self.__dict__.copy()
[docs]
def get_extension(self) -> str:
"""Get the extension of this Panel.
Raise:
NotImplementedError: if not overwritten in subclasses.
"""
raise NotImplementedError("This type of panel does not provide for extensions.")
[docs]
def check_valid(self, df: pd.DataFrame):
"""Check that the dataframe contains the panel variable as a column."""
# Check that varname exists
utils.check_has_variable(df, self.varname)
[docs]
@staticmethod
def create_panel(
df: pd.DataFrame,
panel_column: str,
panel_options: PanelOptions = None,
is_known_figure_col: bool = False,
is_known_image_col: bool = False,
):
"""A factory method to create a new panel object by inferring the type of the panel.
This method infers the type and other parameters based on the data found in the
`panel_col` column in the data frame.
Params:
df: The pandas DataFrame to create the panel from.
panel_column: The name of the panel column.
panel_options: Optionally pass explicit panel options, otherwise defaults are used.
is_known_figure_col: Boolean flag to use if type of panel is a figure, inferred if `False`.
is_known_image_col: Boolean flag to use if type of panel is an image, inferred if `False`.
Returns:
Either a :class:`trelliscope.panels.FigurePanel` or a :class:`trelliscope.panels.ImagePanel`.
Raise:
NotImplementedError: If panel options are passed but `panel_options.prerender=False`.
ValueError: If `is_known_image_col` and `is_known_image_col` are False and panel type
cannot be inferred otherwise.
"""
# Note: In R, much of this logic is found in: infer_meta_variable
# It is not already determined to be an image or figure column
# Check to see if it is one of these before proceeding
if not (is_known_image_col or is_known_figure_col):
if utils.is_image_column(df, panel_column):
is_known_image_col = True
elif utils.is_figure_column(df, panel_column):
is_known_figure_col = True
panel = None
panel_source = None
# In R, there is a check here for lazy panels. This will need to be added when those
# are made available in Python.
if panel_options is not None and not panel_options.prerender:
# panel_source = LocalWebSocketPanelSource()
raise NotImplementedError(
"Local Web Socket Panel Source is not implemented yet."
)
else:
panel_source = FilePanelSource(is_local=True)
if is_known_figure_col:
panel_source.is_local = True
panel = FigurePanel(panel_column, source=panel_source)
if panel_options is not None and panel_options.format is not None:
panel.extension = panel_options.format
elif is_known_image_col or utils.is_image_column(df, panel_column):
is_local = not utils.is_all_remote(df[panel_column])
panel_source.is_local = is_local
# If it is a local image file, set to copy to the output directory
should_copy = is_local
panel = ImagePanel(
panel_column, source=panel_source, should_copy_to_output=should_copy
)
else:
raise ValueError(
f"Could not determine how to create a panel for {panel_column}"
)
# General settings that could come from panel options
if panel_options is not None:
if panel_options.aspect is not None:
panel.aspect_ratio = panel_options.aspect
# TODO: Implement infer aspect ratio logic if it is not specified explicitly.
# In R, this is found in the `infer_aspect_ratio` function in infer.R
return panel
[docs]
class ImagePanel(Panel):
"""Panel for displaying images directly."""
[docs]
def __init__(
self,
varname: str,
source: PanelSource,
aspect_ratio: float = 1.5,
should_copy_to_output: bool = True,
) -> None:
"""Create Panel object to display image.
Args:
varname: Variable name.
source: Panel source object to define where image comes from.
aspect_ratio: Aspect ratio to display.
should_copy_to_output: Boolean whether image files should be copied to output directory.
"""
super().__init__(
varname=varname,
panel_type_str=Panel._PANEL_TYPE_IMAGE,
source=source,
aspect_ratio=aspect_ratio,
is_image=True,
writeable=False,
)
self.should_copy = should_copy_to_output
# def get_panel_source(self) -> dict:
# # TODO: check if remote panels still have type=file
# return super().get_panel_source()
[docs]
class IFramePanel(Panel):
"""Create Panel object to embed iframes."""
[docs]
def __init__(
self, varname: str, source: PanelSource, aspect_ratio: float = 1.5
) -> None:
"""Create Panel object to embed iframes.
Args:
varname: Variable name.
source: Panel source object to define where html source to iframe comes from.
aspect_ratio: Aspect ratio to display.
"""
super().__init__(
varname=varname,
panel_type_str=Panel._PANEL_TYPE_IFRAME,
source=source,
aspect_ratio=aspect_ratio,
is_image=False,
writeable=False,
)
# def get_panel_source(self) -> dict:
# return {"type": "file"}