Skip to content

Commit

Permalink
Merge pull request #166 from wpfff/betterplotting
Browse files Browse the repository at this point in the history
refactoring the plotting system
  • Loading branch information
wpfff committed Jun 4, 2021
2 parents de90817 + ba44f82 commit e648047
Show file tree
Hide file tree
Showing 18 changed files with 1,857 additions and 1,271 deletions.
61 changes: 58 additions & 3 deletions doc/api/plot.rst
Original file line number Diff line number Diff line change
@@ -1,12 +1,67 @@
Plotting elements
-----------------
#################

.. _Base plot API:

Base plotting elements
^^^^^^^^^^^^^^^^^^^^^^

Overview
========

Classes for plotting functionality
----------------------------------

* :class:`.PlotNode` : The base class for a `.Node` with the purpose of receiving data for visualization.
* :class:`.PlotWidgetContainer` : A class that contains a `PlotWidget` (and can change it during runtime)
* :class:`.PlotWidget` : An abstract widget that can be inherited to implement actual plotting.
* :class:`.AutoFigureMaker` : A convenience class for semi-automatic generation of figures.
The purpose is to keep actual plotting code out of the plot widget. This is not mandatory, just convenient.

Data structures
---------------

* :class:`.PlotDataType` : Enum with types of data that can be plotted.
* :class:`.ComplexRepresentation`: Enum with ways to represent complex-valued data.


Additional tools
----------------

* :func:`.makeFlowchartWithPlot` : convenience function for creating a flowchart that leads to a plot node.
* :func:`.determinePlotDataType` : try to infer which type of plot data is in a data set.

Object Documentation
====================

Base elements
^^^^^^^^^^^^^
.. automodule:: plottr.plot.base
:members:

.. _MPL plot API:

Matplotlib plotting tools
^^^^^^^^^^^^^^^^^^^^^^^^^

Overview
========

.. automodule:: plottr.plot.mpl
:members:

Object Documentation
====================

General Widgets
---------------
.. automodule:: plottr.plot.mpl.widgets
:members:

General plotting tools
----------------------
.. automodule:: plottr.plot.mpl.plotting
:members:

Autoplot
--------
.. automodule:: plottr.plot.mpl.autoplot
:members:
2 changes: 1 addition & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# -- Project information -----------------------------------------------------

project = 'plottr'
copyright = '2020, Wolfgang Pfaff'
copyright = '2019-2021, Wolfgang Pfaff'
author = 'Wolfgang Pfaff'


Expand Down
7 changes: 6 additions & 1 deletion doc/plotnode.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ The reason why we don't connect the widget directly to the node is that the cont

.. image:: img/plot-node-system.png

See the :ref:`API documentation<Base plot API>` for more details.


Automatic plotting with Matplotlib
----------------------------------
The most commonly used plot widget is based on matplotlib: :class:`AutoPlot <plottr.plot.mpl.AutoPlot>`.
The most commonly used plot widget is based on matplotlib: :class:`AutoPlot <plottr.plot.mpl.autoplot.AutoPlot>`.
It determines automatically what an appropriate visualization of the received data is, and then plots that (at least if it can determine a good way to plot).
At the same time it gives the user a little bit of control over the appearance (partially through native matplotlib tools).
To separate plotting from the GUI elements we use :class:`FigureMaker <plottr.plot.mpl.autoplot.FigureMaker>`.

See the :ref:`API documentation<MPL plot API>` for more details.
96 changes: 92 additions & 4 deletions plottr/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from typing import TYPE_CHECKING
import os
from typing import TYPE_CHECKING, List, Tuple, Dict, Any, Optional
from importlib.abc import Loader
from importlib.util import spec_from_file_location, module_from_spec
import logging
import os
import sys

if TYPE_CHECKING:
from PyQt5 import QtCore, QtGui, QtWidgets
Expand All @@ -15,11 +18,96 @@
Flowchart = pgFlowchart
NodeBase = pgNode

plottrPath = os.path.split(os.path.abspath(__file__))[0]

from ._version import get_versions
__version__ = get_versions()['version']
del get_versions

logger = logging.getLogger(__name__)
logger.info(f"Imported plottr version: {__version__}")


plottrPath = os.path.split(os.path.abspath(__file__))[0]


def configPaths() -> Tuple[str, str, str]:
"""Get the folders where plottr looks for config files.
:return: List of absolute paths, in order of priority:
(1) current working directory
(2) ~/.plottr
(3) config directory in the package.
"""
builtIn = os.path.join(plottrPath, 'config')
user = os.path.join(os.path.expanduser("~"), '.plottr')
cwd = os.getcwd()
return cwd, user, builtIn


def configFiles(fileName: str) -> List[str]:
"""Get available config files with the given file name.
:param fileName: file name, without path
:return: List of found config files with the provided name, in order
or priority.
"""
ret = []
for path in configPaths():
fp = os.path.join(path, fileName)
if os.path.exists(fp):
ret.append(fp)
return ret


def config(names: Optional[List[str]] = None) -> \
Dict[str, Any]:
"""Return the plottr configuration as a dictionary.
Each config file found is expected to contain a dictionary with name
``config``. The returned configuration is of the form
``
{
cfg_1: {...},
cfg_2: {...},
}
``
The keys in the returned dictionary are the names given, and the contents
of each entry the dictionary found in the corresponding files.
Values returned are determined in hierarchical order:
If configs are found on package and user levels, we first look at the
package-provided config, and then update with user-provided ones (see doc
of :func:`.configPaths`). I.e., user-provided config has the highest
priority and overrides package-provided config.
Note: currently, exceptions raised when trying to import config objects are
not caught. Erroneous config files may thus crash the program.
:param names: List of files. For given ``name`` will look
for ``plottrcfg_<name>.py`` in the config directories.
if ``None``, will look only for ``plottrcfg_main.py``
:param forceReload: If True, will not use a cached config file if present.
will thus get the most recent config from file, without need to restart
the program.
"""
if names is None:
names = ['main']

config = {}
for name in names:
modn = f"plottrcfg_{name}"
filen = f"{modn}.py"
this_cfg = {}
for filep in configFiles(filen)[::-1]:
spec = spec_from_file_location(modn, filep)
mod = module_from_spec(spec)
sys.modules[modn] = mod
assert isinstance(spec.loader, Loader)
spec.loader.exec_module(mod)
this_cfg.update(getattr(mod, 'config', {}))

config[name] = this_cfg
return config




6 changes: 1 addition & 5 deletions plottr/apps/autoplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,6 @@ def stop(self) -> None:

class AutoPlotMainWindow(PlotWindow):

#: Signal() -- emitted when the window is closed
windowClosed = Signal()

def __init__(self, fc: Flowchart,
parent: Optional[QtWidgets.QMainWindow] = None,
monitor: bool = False,
Expand Down Expand Up @@ -185,8 +182,7 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None:
"""
if self.monitorToolBar is not None:
self.monitorToolBar.stop()
self.windowClosed.emit()
return event.accept()
return super().closeEvent(event)

def showTime(self) -> None:
"""
Expand Down
29 changes: 29 additions & 0 deletions plottr/config/plottrcfg_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from matplotlib import cycler

config = {
'matplotlibrc': {
'axes.grid': True,
'axes.prop_cycle': cycler('color', ['1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b',
'e377c2', '7f7f7f', 'bcbd22', '17becf']),
'figure.dpi': 150,
'figure.figsize': (4.5, 3),
'font.size': 6,
'font.family': ['Helvetica', 'Arial', 'DejaVu Sans', 'Bitstream Vera Sans'],
'grid.linewidth': 0.5,
'grid.linestyle': '--',
'image.cmap': 'magma',
'legend.fontsize': 6,
'legend.frameon': True,
'legend.numpoints': 1,
'legend.scatterpoints': 1,
'lines.marker': 'o',
'lines.markersize': '3',
'lines.markeredgewidth': 1,
'lines.markerfacecolor': 'w',
'lines.linestyle': '-',
'lines.linewidth': 1,
'savefig.dpi': 300,
'savefig.transparent': False,
},

}
9 changes: 9 additions & 0 deletions plottr/gui/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@
"""
from typing import List, Dict, Union

from numpy import rint

from .. import QtWidgets

__author__ = 'Wolfgang Pfaff'
__license__ = 'MIT'


def dpiScalingFactor(widget: QtWidgets.QWidget) -> float:
"""based on the logical DPI of ``widget``, return a scaling factor
that can be used to compute display size on screens relative to 96 DPI."""
scaling = rint(widget.logicalDpiX() / 96.0)
return scaling


def widgetDialog(widget: QtWidgets.QWidget, title: str = '',
show: bool = True) -> QtWidgets.QDialog:
win = QtWidgets.QDialog()
Expand Down
32 changes: 23 additions & 9 deletions plottr/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
from numpy import rint
from typing import Union, List, Tuple, Optional, Type, Sequence, Dict, Any

from .tools import dictToTreeWidgetItems
from plottr import QtCore, Flowchart, QtWidgets, Signal, Slot
from .tools import dictToTreeWidgetItems, dpiScalingFactor
from plottr import QtGui, QtCore, Flowchart, QtWidgets, Signal, Slot
from plottr.node import Node, linearFlowchart
from ..plot import PlotNode, PlotWidgetContainer, MPLAutoPlot
from ..plot import PlotNode, PlotWidgetContainer, PlotWidget

__author__ = 'Wolfgang Pfaff'
__license__ = 'MIT'
Expand Down Expand Up @@ -74,15 +74,23 @@ class PlotWindow(QtWidgets.QMainWindow):
:meth:`addNodeWidgetFromFlowchart`.
"""

plotWidgetClass = MPLAutoPlot
#: Signal() -- emitted when the window is closed
windowClosed = Signal()

def __init__(self, parent: Optional[QtWidgets.QMainWindow] = None,
fc: Optional[Flowchart] = None, **kw: Any):
fc: Optional[Flowchart] = None,
plotWidgetClass: Optional[Any] = None,
**kw: Any):
super().__init__(parent)

if plotWidgetClass is None:
from ..plot.mpl import AutoPlot
plotWidgetClass = AutoPlot

self.plotWidgetClass = plotWidgetClass
self.plot = PlotWidgetContainer(parent=self)
self.setCentralWidget(self.plot)
self.plotWidget: Optional[MPLAutoPlot] = None
self.plotWidget: Optional[PlotWidget] = None

self.nodeToolBar = QtWidgets.QToolBar('Node control', self)
self.addToolBar(self.nodeToolBar)
Expand All @@ -94,8 +102,7 @@ def __init__(self, parent: Optional[QtWidgets.QMainWindow] = None,
self.setDefaultStyle()

def setDefaultStyle(self) -> None:
scaling = rint(self.logicalDpiX() / 96.0)
fontSize = 10*scaling
fontSize = 10*dpiScalingFactor(self)
self.setStyleSheet(
f"""
QToolButton {{
Expand Down Expand Up @@ -180,6 +187,14 @@ def addNodeWidgetsFromFlowchart(self, fc: Flowchart,
self.plotWidget = self.plotWidgetClass(parent=self.plot)
self.plot.setPlotWidget(self.plotWidget)

def closeEvent(self, event: QtGui.QCloseEvent) -> None:
"""
When closing the inspectr window, do some house keeping:
* stop the monitor, if running
"""
self.windowClosed.emit()
return event.accept()


def makeFlowchartWithPlotWindow(nodes: List[Tuple[str, Type[Node]]], **kwargs: Any) \
-> Tuple[PlotWindow, Flowchart]:
Expand Down Expand Up @@ -211,7 +226,6 @@ def loadSnapshot(self, snapshotDict : Optional[dict]) -> None:
self.addTopLevelItem(item)
item.setExpanded(True)

#self.expandAll()
for i in range(2):
self.resizeColumnToContents(i)

4 changes: 2 additions & 2 deletions plottr/plot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .base import PlotNode, PlotWidgetContainer, makeFlowchartWithPlot
from .mpl import AutoPlot as MPLAutoPlot
from .base import PlotNode, PlotWidgetContainer, makeFlowchartWithPlot, \
PlotWidget

0 comments on commit e648047

Please sign in to comment.