Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internal refactor of figures and views #231

Merged
merged 13 commits into from
Aug 4, 2023
6 changes: 3 additions & 3 deletions docs/about/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ Graphics

graphics.Camera
graphics.ColorMapper
graphics.FigImage
graphics.FigLine
graphics.FigScatter3d
graphics.figure1d
graphics.figure2d
graphics.figure3d
graphics.ImageView
graphics.LineView
graphics.Scatter3dView
graphics.tiled

Widgets and tools
Expand Down
2 changes: 1 addition & 1 deletion src/plopp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

from . import data
from .core import Node, View, input_node, node, show_graph, widget_node
from .functions import inspector, plot, scatter3d, slicer, superplot
from .graphics import Camera, figure1d, figure2d, figure3d, tiled
from .plotting import inspector, plot, scatter3d, slicer, superplot


def patch_scipp():
Expand Down
62 changes: 8 additions & 54 deletions src/plopp/backends/matplotlib/figure.py
Original file line number Diff line number Diff line change
@@ -1,85 +1,39 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)

from ...graphics import BaseFig

class Figure:

class Figure(BaseFig):
"""
Mixin class for Matplotlib figures
"""

def __init_figure__(self, FigConstructor, *args, **kwargs):
self._fig = FigConstructor(*args, **kwargs)
def __init_figure__(self, View, *args, **kwargs):
self._view = View(*args, **kwargs)
self._args = args
self._kwargs = kwargs
self._fig_constructor = FigConstructor

@property
def fig(self):
"""
Get the underlying Matplotlib figure.
"""
return self._fig.canvas.fig
return self._view.canvas.fig

@property
def ax(self):
"""
Get the underlying Matplotlib axes.
"""
return self._fig.canvas.ax
return self._view.canvas.ax

@property
def cax(self):
"""
Get the underlying Matplotlib colorbar axes.
"""
return self._fig.canvas.cax

@property
def canvas(self):
return self._fig.canvas

@property
def artists(self):
return self._fig.artists

@property
def graph_nodes(self):
return self._fig.graph_nodes

@property
def id(self):
return self._fig.id

def crop(self, **limits):
"""
Set the axes limits according to the crop parameters.

Parameters
----------
**limits:
Min and max limits for each dimension to be cropped.
"""
return self._fig.crop(**limits)

def save(self, filename, **kwargs):
"""
Save the figure to file.
The default directory for writing the file is the same as the
directory where the script or notebook is running.

Parameters
----------
filename:
Name of the output file. Possible file extensions are ``.jpg``, ``.png``,
``.svg``, and ``.pdf``.
"""
return self._fig.canvas.save(filename, **kwargs)

def update(self, *args, **kwargs):
return self._fig.update(*args, **kwargs)

def notify_view(self, *args, **kwargs):
return self._fig.notify_view(*args, **kwargs)
return self._view.canvas.cax

def __add__(self, other):
from .tiled import hstack
Expand Down
9 changes: 5 additions & 4 deletions src/plopp/backends/matplotlib/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ class InteractiveFig(Figure, VBox):
Create an interactive Matplotlib figure.
"""

def __init__(self, FigConstructor, *args, **kwargs):
self.__init_figure__(FigConstructor, *args, **kwargs)
def __init__(self, View, *args, **kwargs):
self.__init_figure__(View, *args, **kwargs)
self.toolbar = make_toolbar_canvas2d(
canvas=self._fig.canvas, colormapper=getattr(self._fig, 'colormapper', None)
canvas=self._view.canvas,
colormapper=getattr(self._view, 'colormapper', None),
)
self.left_bar = VBar([self.toolbar])
self.right_bar = VBar()
Expand All @@ -25,7 +26,7 @@ def __init__(self, FigConstructor, *args, **kwargs):
super().__init__(
[
self.top_bar,
HBox([self.left_bar, self._fig.canvas.to_widget(), self.right_bar]),
HBox([self.left_bar, self._view.canvas.to_widget(), self.right_bar]),
self.bottom_bar,
]
)
14 changes: 7 additions & 7 deletions src/plopp/backends/matplotlib/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,25 @@ class StaticFig(Figure):
canvas.
"""

def __init__(self, FigConstructor, *args, **kwargs):
self.__init_figure__(FigConstructor, *args, **kwargs)
def __init__(self, View, *args, **kwargs):
self.__init_figure__(View, *args, **kwargs)

def _repr_mimebundle_(self, include=None, exclude=None) -> dict:
"""
Mimebundle display representation for jupyter notebooks.
"""
str_repr = str(self.fig)
out = {'text/plain': str_repr[:-1] + f', {len(self.artists)} artists)'}
if self._fig._repr_format is not None:
repr_maker = get_repr_maker(form=self._fig._repr_format)
if self._view._repr_format is not None:
repr_maker = get_repr_maker(form=self._view._repr_format)
else:
npoints = sum(len(line.get_xdata()) for line in self._fig.canvas.ax.lines)
npoints = sum(len(line.get_xdata()) for line in self._view.canvas.ax.lines)
repr_maker = get_repr_maker(npoints=npoints)
out.update(repr_maker(self._fig.canvas.fig))
out.update(repr_maker(self._view.canvas.fig))
return out

def to_widget(self):
"""
Convert the Matplotlib figure to an image widget.
"""
return self._fig.canvas.to_image()
return self._view.canvas.to_image()
2 changes: 1 addition & 1 deletion src/plopp/backends/matplotlib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def is_sphinx_build() -> bool:

def copy_figure(fig: FigureLike, **kwargs) -> FigureLike:
out = fig.__class__(
fig._fig_constructor,
fig._view.__class__,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case we are not keeping the callable anymore,
should we make sure if the _view.__class__ is the View in def __init_figure__(self, View, *args, **kwargs):...?

For example,

class FigBase:
  def __init_figure__(self, View, *args, **kwargs):
    self._view = View(*args, **kwargs)
    if not self._view.__class__ is View:
        raise TypeError

Cause, you might have something like

@dataclasses
class Figure:
  width: int
  height: int

def create_figure(size) -> Figure:
  return Figure(size[0], size[1])

fig = InteractiveFigure(create_figure, size=(10, 10))

In this case above, it will create the fig as intended, but copy_figure will not work.

Or is it too much of worry...?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think any user should every be constructing InteractiveFig manually, so I would say in this case it is not worth worrying about?

*fig._args,
**{**fig._kwargs, **kwargs},
)
Expand Down
58 changes: 6 additions & 52 deletions src/plopp/backends/plotly/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@

from ipywidgets import HBox, VBox

from ...graphics import BaseFig
from ...widgets import HBar, VBar, make_toolbar_canvas2d


class Figure(VBox):
class Figure(BaseFig, VBox):
"""
Create an interactive figure to represent one-dimensional data.
"""

def __init__(self, FigConstructor, *args, **kwargs):
self._fig = FigConstructor(*args, **kwargs)
self.toolbar = make_toolbar_canvas2d(canvas=self._fig.canvas)
def __init__(self, View, *args, **kwargs):
self._view = View(*args, **kwargs)
self.toolbar = make_toolbar_canvas2d(canvas=self._view.canvas)
self.left_bar = VBar([self.toolbar])
self.right_bar = VBar()
self.bottom_bar = HBar()
Expand All @@ -22,54 +23,7 @@ def __init__(self, FigConstructor, *args, **kwargs):
super().__init__(
[
self.top_bar,
HBox([self.left_bar, self._fig.canvas.to_widget(), self.right_bar]),
HBox([self.left_bar, self._view.canvas.to_widget(), self.right_bar]),
self.bottom_bar,
]
)

@property
def canvas(self):
return self._fig.canvas

@property
def artists(self):
return self._fig.artists

@property
def graph_nodes(self):
return self._fig.graph_nodes

@property
def id(self):
return self._fig.id

def crop(self, **limits):
"""
Set the axes limits according to the crop parameters.

Parameters
----------
**limits:
Min and max limits for each dimension to be cropped.
"""
return self._fig.crop(**limits)

def save(self, filename, **kwargs):
"""
Save the figure to file.
The default directory for writing the file is the same as the
directory where the script or notebook is running.

Parameters
----------
filename:
Name of the output file. Possible file extensions are ``.jpg``, ``.png``,
``.svg``, ``.pdf``, and ``html``.
"""
return self._fig.canvas.save(filename, **kwargs)

def update(self, *args, **kwargs):
return self._fig.update(*args, **kwargs)

def notify_view(self, *args, **kwargs):
return self._fig.notify_view(*args, **kwargs)
42 changes: 34 additions & 8 deletions src/plopp/core/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

import uuid
from abc import abstractmethod
from typing import Any, Dict

import scipp as sc


class View:
Expand All @@ -23,19 +26,42 @@ def __init__(self, *nodes):
self.graph_nodes = {}
for node in nodes:
node.add_view(self)
self.artists = {}

@property
def id(self):
"""
The unique id of the view.
"""
return self._id

def notify_view(self, message: Dict[str, Any]):
"""
When a notification is received, request data from the corresponding parent node
and update the relevant artist.

Parameters
----------
*message:
The notification message containing the node id it originated from.
"""
node_id = message["node_id"]
new_values = self.graph_nodes[node_id].request_data()
self.update(new_values=new_values, key=node_id)

@abstractmethod
def notify_view(self, _):
def update(self, new_values: sc.DataArray, key: str, draw: bool):
"""
The function that will be called when a parent node is told to notify its
children and its views.
Update function which is called when a notification is received.
This has to be overridden by any child class.
"""
return
...

@property
def id(self):
def render(self):
"""
The unique id of the view.
At the end of figure creation, this function is called to request data from
all parent nodes and draw the figure.
"""
return self._id
for node in self.graph_nodes.values():
new_values = node.request_data()
self.update(new_values=new_values, key=node.id)
7 changes: 4 additions & 3 deletions src/plopp/graphics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

# flake8: noqa E402, F401

from .basefig import BaseFig
from .camera import Camera
from .colormapper import ColorMapper
from .figimage import FigImage
from .figline import FigLine
from .figscatter3d import FigScatter3d
from .figure import figure1d, figure2d, figure3d
from .imageview import ImageView
from .lineview import LineView
from .scatter3dview import Scatter3dView
from .tiled import tiled
Loading