Examples

The test launcher

A lot of examples are available in the plotpy.tests test module

from plotpy.tests import run
run()

The two lines above execute the test launcher:

../_images/__init__.png

Curve plotting

Basic curve plotting

../_images/plot.png

Computations on curves


from guidata.qthelpers import qt_app_context
from numpy import linspace, sin, trapz

from plotpy.builder import make
from plotpy.tests import vistools as ptv


def test_computations():
    """Test computations"""
    x = linspace(-10, 10, 1000)
    y = sin(sin(sin(x)))
    with qt_app_context(exec_loop=True):
        curve = make.curve(x, y, "ab", "b")
        range = make.range(-2, 2)
        disp0 = make.range_info_label(
            range, "BR", "x = %.1f ± %.1f cm", title="Range infos"
        )

        disp1 = make.computation(
            range, "BL", "trapz=%g", curve, lambda x, y: trapz(y, x)
        )

        disp2 = make.computations(
            range,
            "TL",
            [
                (curve, "min=%.5f", lambda x, y: y.min()),
                (curve, "max=%.5f", lambda x, y: y.max()),
                (curve, "avg=%.5f", lambda x, y: y.mean()),
            ],
        )
        legend = make.legend("TR")
        _win = ptv.show_items(
            wintitle="Plot computations",
            items=[curve, range, disp0, disp1, disp2, legend],
            plot_type="curve",
        )


if __name__ == "__main__":
    test_computations()
../_images/computations.png

Curve fitting


import numpy as np

from plotpy.widgets.fit import FitParam, guifit


def test_fit():
    """Test the curve fitting tool"""
    x = np.linspace(-10, 10, 1000)
    y = np.cos(1.5 * x) + np.random.rand(x.shape[0]) * 0.2

    def fit(x, params):
        a, b = params
        return np.cos(b * x) + a

    a = FitParam("Offset", 1.0, 0.0, 2.0)
    b = FitParam("Frequency", 2.0, 1.0, 10.0, logscale=True)
    params = [a, b]
    values = guifit(x, y, fit, params, xlabel="Time (s)", ylabel="Power (a.u.)")

    print(values)
    print([param.value for param in params])


if __name__ == "__main__":
    test_fit()
../_images/fit.png

Image visualization

Image contrast adjustment


import os
import os.path as osp

from guidata.env import execenv
from guidata.qthelpers import qt_app_context

from plotpy.builder import make
from plotpy.tests import get_path


def test_contrast():
    """Test"""
    # -- Create QApplication
    with qt_app_context(exec_loop=True):
        filename = get_path("brain.png")
        image = make.image(filename=filename, title="Original", colormap="gray")
        win = make.dialog(
            edit=False,
            toolbar=True,
            wintitle="Contrast test",
            show_contrast=True,
            type="image",
        )
        plot = win.manager.get_plot()
        plot.add_item(image)
        win.resize(600, 600)
        win.show()
        fname = "contrast.png"
        try:
            plot.save_widget(fname)
        except IOError:
            # Skipping this part of the test
            # because user has no write permission on current directory
            pass
        if execenv.unattended and osp.isfile(fname):
            os.unlink(fname)


if __name__ == "__main__":
    test_contrast()
../_images/contrast.png

Image cross-sections


import numpy as np
from guidata.qthelpers import qt_app_context

from plotpy.builder import make
from plotpy.tests import get_path


def create_window():
    win = make.dialog(
        edit=False,
        toolbar=True,
        wintitle="Cross sections test",
        show_xsection=True,
        show_ysection=True,
        type="image",
    )
    win.resize(640, 600)
    return win


def test_cross_section():
    """Test cross section"""
    with qt_app_context(exec_loop=True):
        filename = get_path("brain.png")
        win = create_window()
        win.show()
        image = make.image(filename=filename, colormap="bone")
        data2 = np.array(image.data.T[200:], copy=True)
        image2 = make.image(data2, title="Modified", alpha_function="linear")
        plot = win.manager.get_plot()
        plot.add_item(image)
        plot.add_item(image2, z=1)


if __name__ == "__main__":
    test_cross_section()
../_images/cross_section.png

Transformable images

Affine transforms example on 3000x3000 images (real-time transforms):


from __future__ import annotations

import os

import numpy as np
import pytest
from guidata.env import execenv
from guidata.qthelpers import qt_app_context
from qtpy import QtCore as QC
from qtpy import QtGui as QG

from plotpy import io
from plotpy.builder import make
from plotpy.constants import LUTAlpha
from plotpy.items import TrImageItem, assemble_imageitems
from plotpy.tests import vistools as ptv
from plotpy.tests.data import gen_image4

DEFAULT_CHARS = "".join([chr(c) for c in range(32, 256)])


def get_font_array(sz: int, chars: str = DEFAULT_CHARS) -> np.ndarray | None:
    """Return array of font characters

    Args:
        sz: Font size
        chars: Characters to include (default: all printable characters)

    Returns:
        Array of font characters
    """
    font = QG.QFont()
    font.setFixedPitch(True)
    font.setPixelSize(sz)
    font.setStyleStrategy(QG.QFont.NoAntialias)
    dummy = QG.QImage(10, 10, QG.QImage.Format_ARGB32)
    pnt = QG.QPainter(dummy)
    pnt.setFont(font)
    metric = pnt.fontMetrics()
    rct = metric.boundingRect(chars)
    pnt.end()
    h = rct.height()
    w = rct.width()
    img = QG.QImage(w, h, QG.QImage.Format_ARGB32)
    paint = QG.QPainter()
    paint.begin(img)
    paint.setFont(font)
    paint.setBrush(QG.QColor(255, 255, 255))
    paint.setPen(QG.QColor(255, 255, 255))
    paint.drawRect(0, 0, w + 1, h + 1)
    paint.setPen(QG.QColor(0, 0, 0))
    paint.setBrush(QG.QColor(0, 0, 0))
    paint.drawText(0, paint.fontMetrics().ascent(), chars)
    paint.end()
    try:
        data = img.bits().asstring(img.sizeInBytes())
    except AttributeError:
        data = img.bits()
    npy = np.frombuffer(data, np.uint8)
    npy.shape = img.height(), img.bytesPerLine() // 4, 4
    return npy[:, :, 0]


def write_text_on_array(
    data: np.ndarray,
    x: int,
    y: int,
    sz: int,
    txt: str,
    range: tuple[int, int] | None = None,
) -> None:
    """Write text in image (in-place)

    Args:
        data: Image data
        x: X-coordinate of top-left corner
        y: Y-coordinate of top-left corner
        sz: Font size
        txt: Text to write
        range: Range of values to map to 0-255 (default: None)
    """
    arr = get_font_array(sz, txt)
    if arr is None:
        return
    if range is None:
        m, M = data.min(), data.max()
    else:
        m, M = range
    z = (float(M) - float(m)) * np.array(arr, float) / 255.0 + m
    arr = np.array(z, data.dtype)
    dy, dx = arr.shape
    data[y : y + dy, x : x + dx] = arr


def make_items(N: int) -> list[TrImageItem]:
    """Make test TrImageItem items

    Args:
        N: Image size (N x N)

    Returns:
        List of image items
    """
    data = gen_image4(N, N)
    m = data.min()
    M = data.max()
    items = [make.trimage(data, alpha_function=LUTAlpha.LINEAR, colormap="jet")]
    for dtype in (np.uint8, np.uint16, np.int8, np.int16):
        info = np.iinfo(dtype().dtype)  # pylint: disable=no-value-for-parameter
        s = float((info.max - info.min))
        a1 = s * (data - m) / (M - m)
        img = np.array(a1 + info.min, dtype)
        write_text_on_array(img, 0, 0, int(N / 15.0), str(dtype))
        items.append(make.trimage(img, colormap="jet"))
    nc = int(np.sqrt(len(items)) + 1.0)
    maxy, x, y = 0, 0, 0
    w = None
    for index, item in enumerate(items):
        h = item.boundingRect().height()
        if index % nc == 0:
            x = 0
            y += maxy
            maxy = h
        else:
            x += w
            maxy = max(maxy, h)
        w = item.boundingRect().width()
        item.set_transform(x, y, 0.0)
    return items


def save_image(name: str, data: np.ndarray) -> None:
    """Save image to file

    Args:
        name: Base name of file
        data: Image data
    """
    for fname in (name + ".u16.tif", name + ".u8.png"):
        if os.path.exists(fname):
            os.remove(fname)
    size = int(data.nbytes / 1024.0)
    print(f"Saving image: {data.shape[0]} x {data.shape[1]} ({size} KB):")
    print(" --> uint16")
    io.imwrite(name + ".u16.tif", data, dtype=np.uint16, max_range=True)
    print(" --> uint8")
    io.imwrite(name + ".u8.png", data, dtype=np.uint8, max_range=True)


def get_bbox(items: list[TrImageItem]) -> QC.QRectF:
    """Get bounding box of items

    Args:
        items: List of image items

    Returns:
        Bounding box of items
    """
    rectf = QC.QRectF()
    for item in items:
        rectf = rectf.united(item.boundingRect())
    return rectf


def build_image(items: list[TrImageItem]) -> None:
    """Build image from items

    Args:
        items: List of image items
    """
    r = get_bbox(items)
    _x, _y, w, h = r.getRect()
    print("-" * 80)
    print(f"Assemble test1: {int(w)} x {int(h)}")
    dest = assemble_imageitems(items, r, w, h)
    if not execenv.unattended:
        save_image("test1", dest)
    print("-" * 80)
    print(f"Assemble test1: {int(w/4)} x {int(h/4)}")
    dest = assemble_imageitems(items, r, w / 4, h / 4)
    if not execenv.unattended:
        save_image("test2", dest)
    print("-" * 80)


@pytest.mark.parametrize("N", [500])
@pytest.mark.parametrize("assemble_images", [False, True])
def test_transform(N: int, assemble_images: bool) -> None:
    """Test image transforms

    Args:
        N: Image size (N x N)
        assemble_images: If True, assemble images (default: False)
    """
    with qt_app_context(exec_loop=True):
        items = make_items(N)
        _win = ptv.show_items(
            items,
            wintitle="Transform test ({}x{} images)".format(N, N),
            plot_type="image",
            show_itemlist=False,
        )
    if assemble_images:
        build_image(items)


if __name__ == "__main__":
    test_transform(N=500, assemble_images=True)
../_images/transform.png

Image rectangular filter


import numpy as np
from guidata.qthelpers import qt_app_context
from scipy.ndimage import gaussian_filter

from plotpy import io
from plotpy.builder import make
from plotpy.tests import data as ptd
from plotpy.tests import get_path


def imshow(x, y, data, filter_area, yreverse=True):
    with qt_app_context(exec_loop=True):
        win = make.dialog(
            edit=False,
            toolbar=True,
            wintitle="Image filter demo",
            xlabel="x (cm)",
            ylabel="y (cm)",
            yreverse=yreverse,
            type="image",
        )
        image = make.xyimage(x, y, data)
        plot = win.manager.get_plot()
        plot.add_item(image)
        xmin, xmax, ymin, ymax = filter_area

        def ifilter(x, y, data):
            """Image filter function"""
            return gaussian_filter(data, 5)

        flt = make.imagefilter(xmin, xmax, ymin, ymax, image, filter=ifilter)
        plot.add_item(flt, z=1)
        plot.replot()
        win.show()


def test_imagefilter():
    """Test image filter"""
    x, y, data = ptd.gen_xyimage()
    imshow(x, y, data, filter_area=(-3.0, -1.0, 0.0, 2.0), yreverse=False)

    filename = get_path("brain.png")
    data = io.imread(filename, to_grayscale=True)
    x = np.linspace(0, 30.0, data.shape[1])
    y = np.linspace(0, 30.0, data.shape[0])
    imshow(x, y, data, filter_area=(10, 20, 5, 15))


if __name__ == "__main__":
    test_imagefilter()
../_images/imagefilter.png

Histograms

2-D histogram


from guidata.qthelpers import qt_app_context
from numpy import array, concatenate, dot, random

from plotpy.builder import make
from plotpy.config import _


def hist2d_func(X, Y, Z):
    with qt_app_context(exec_loop=True):
        win = make.dialog(
            edit=True,
            toolbar=True,
            wintitle="2-D Histogram X0=(0,1), X1=(-1,-1)",
            type="image",
        )
        hist2d = make.histogram2D(X, Y, 200, 200, Z=Z, computation=2)
        curve = make.curve(
            X[::50], Y[::50], linestyle="", marker="+", title=_("Markers")
        )
        plot = win.manager.get_plot()
        plot.set_aspect_ratio(lock=False)
        plot.set_antialiasing(False)
        plot.add_item(hist2d)
        plot.add_item(curve)
        plot.set_item_visible(curve, False)
        win.show()


def hist2d(X, Y):
    with qt_app_context(exec_loop=True):
        win = make.dialog(
            edit=True,
            toolbar=True,
            wintitle="2-D Histogram X0=(0,1), X1=(-1,-1)",
            type="image",
        )
        hist2d = make.histogram2D(X, Y, 200, 200)
        curve = make.curve(
            X[::50], Y[::50], linestyle="", marker="+", title=_("Markers")
        )
        plot = win.manager.get_plot()
        plot.set_aspect_ratio(lock=False)
        plot.set_antialiasing(False)
        plot.add_item(hist2d)
        plot.add_item(curve)
        plot.set_item_visible(curve, False)
        win.show()


def test_hist_2d():
    N = 150000
    m = array([[1.0, 0.2], [-0.2, 3.0]])
    X1 = random.normal(0, 0.3, size=(N, 2))
    X2 = random.normal(0, 0.3, size=(N, 2))
    X = concatenate((X1 + [0, 1.0], dot(X2, m) + [-1, -1.0]))
    hist2d(X[:, 0], X[:, 1])


def test_hist_2d_func():
    N = 150000
    m = array([[1.0, 0.2], [-0.2, 3.0]])
    X1 = random.normal(0, 0.3, size=(N, 2))
    X2 = random.normal(0, 0.3, size=(N, 2))
    X = concatenate((X1 + [0, 1.0], dot(X2, m) + [-1, -1.0]))
    hist2d_func(X[:, 0], X[:, 1], X[:, 0] + X[:, 1])


if __name__ == "__main__":
    test_hist_2d()
    test_hist_2d_func()
../_images/hist2d.png

Other examples

Dot Array Demo


from __future__ import annotations

from typing import TYPE_CHECKING

import guidata.dataset as gds
import guidata.dataset.qtwidgets as gdq
import numpy as np
from guidata.configtools import get_image_file_path
from guidata.qthelpers import qt_app_context
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW

import plotpy.config  # Loading icons  # noqa: F401
from plotpy.interfaces import IImageItemType
from plotpy.items import RawImageItem
from plotpy.items.curve.errorbar import vmap
from plotpy.plot import PlotDialog, PlotOptions
from plotpy.tools import CopyToClipboardTool, HelpTool, PrintTool, SaveAsTool

if TYPE_CHECKING:
    from plotpy.interfaces import IItemType


class DotArrayParam(gds.DataSet):
    """Dot array"""

    def _update_cb(self, *args):
        """Update callback, to be overriden"""
        pass

    g1 = gds.BeginGroup("Size of the area")
    dim_h = gds.FloatItem("Width", default=20, min=0, unit="mm")
    dim_v = gds.FloatItem("Height", default=20, min=0, unit="mm")
    _g1 = gds.EndGroup("Size of the area")

    g2 = gds.BeginGroup("Grid pattern properties")
    step_x = gds.FloatItem("Step in X-axis", default=1, min=1, unit="mm")
    step_y = gds.FloatItem("Step in Y-axis", default=1, min=1, unit="mm")
    size = gds.FloatItem("Dot size", default=0.2, min=0, max=2, slider=True, unit="mm")
    color = gds.ColorItem("Dot color", default="red")
    _g2 = gds.EndGroup("Grid pattern properties")

    def update_item(self, obj):
        """Update item from parameters"""
        self._update_cb()

    def update_param(self, obj):
        """Update parameters from object"""
        pass


class DotArrayItem(RawImageItem):
    """Dot array item"""

    def __init__(self, param=None):
        super().__init__(np.zeros((1, 1)), param)
        self.update_border()

    def boundingRect(self):
        """Reimplemented to return the bounding rectangle of the item"""
        param = self.param
        if param is not None:
            return QC.QRectF(
                QC.QPointF(-0.5 * param.size, -0.5 * param.size),
                QC.QPointF(
                    param.dim_h + 0.5 * param.size, param.dim_v + 0.5 * param.size
                ),
            )

    def types(self) -> tuple[type[IItemType], ...]:
        """Returns a group or category for this item.
        This should be a tuple of class objects inheriting from IItemType

        Returns:
            tuple: Tuple of class objects inheriting from IItemType
        """
        return (IImageItemType,)

    def draw_image(self, painter, canvasRect, srcRect, dstRect, xMap, yMap):
        """Draw image"""
        painter.setRenderHint(QG.QPainter.Antialiasing, True)
        param = self.param
        xcoords = vmap(xMap, np.arange(0, param.dim_h + 1, param.step_x))
        ycoords = vmap(yMap, np.arange(0, param.dim_v + 1, param.step_y))
        rx = 0.5 * param.size * xMap.pDist() / xMap.sDist()
        ry = 0.5 * param.size * yMap.pDist() / yMap.sDist()
        color = QG.QColor(param.color)
        painter.setPen(QG.QPen(color))
        painter.setBrush(QG.QBrush(color))
        for xc in xcoords:
            for yc in ycoords:
                painter.drawEllipse(QC.QPointF(xc, yc), rx, ry)


class CustomHelpTool(HelpTool):
    """Custom help tool"""

    def activate_command(self, plot, checked):
        """Activate command"""
        QW.QMessageBox.information(
            plot,
            "Help",
            """**to be customized**
Keyboard/mouse shortcuts:
  - single left-click: item (curve, image, ...) selection
  - single right-click: context-menu relative to selected item
  - shift: on-active-curve (or image) cursor
  - alt: free cursor
  - left-click + mouse move: move item (when available)
  - middle-click + mouse move: pan
  - right-click + mouse move: zoom""",
        )


class DotArrayDialog(PlotDialog):
    """Dot array dialog"""

    def __init__(self):
        self.item = None
        self.stamp_gbox = None
        super().__init__(
            title="Dot array example",
            options=PlotOptions(title="Main plot", type="image"),
            toolbar=True,
            edit=True,
        )
        self.resize(900, 600)

    def register_tools(self):
        """Register tools"""
        manager = self.plot_widget.manager
        manager.register_standard_tools()
        manager.add_separator_tool()
        manager.add_tool(SaveAsTool)
        manager.add_tool(CopyToClipboardTool)
        manager.add_tool(PrintTool)
        manager.add_tool(CustomHelpTool)
        manager.activate_default_tool()
        plot = manager.get_plot()
        plot.enableAxis(plot.yRight, False)
        plot.set_aspect_ratio(lock=True)

    def populate_plot_layout(self):
        """Populate the plot layout

        Reimplements the method from PlotDialog"""
        self.add_widget(self.plot_widget, row=0, column=0, rowspan=3, columnspan=1)
        logo_path = get_image_file_path("plotpy.svg")
        logo = QW.QLabel()
        logo.setPixmap(QG.QPixmap(logo_path))
        logo.setAlignment(QC.Qt.AlignCenter)
        self.add_widget(logo, 1, 1)
        logo_txt = QW.QLabel("Powered by <b>plotpy</b>")
        logo_txt.setAlignment(QC.Qt.AlignHCenter | QC.Qt.AlignTop)
        self.add_widget(logo_txt, 2, 1)
        self.stamp_gbox = gdq.DataSetEditGroupBox("Dots", DotArrayParam)
        self.stamp_gbox.SIG_APPLY_BUTTON_CLICKED.connect(self.apply_params)
        self.add_widget(self.stamp_gbox, 0, 1)

    def show_data(self, param):
        """Show data"""
        plot = self.plot_widget.plot
        if self.item is None:
            param._update_cb = lambda: self.stamp_gbox.get()
            self.item = DotArrayItem(param)
            plot.add_item(self.item)
        else:
            self.item.update_border()
        plot.do_autoscale()

    def apply_params(self):
        """Apply parameters"""
        param = self.stamp_gbox.dataset
        self.show_data(param)


def test_dot_array():
    """Test dot array dialog"""
    with qt_app_context(exec_loop=True):
        dlg = DotArrayDialog()
        dlg.apply_params()
        dlg.show()


if __name__ == "__main__":
    test_dot_array()
../_images/dotarraydemo.png

Image plot tools


from guidata.qthelpers import qt_app_context

from plotpy.builder import make
from plotpy.items import Marker
from plotpy.tests import get_path
from plotpy.tools import (
    AnnotatedCircleTool,
    AnnotatedEllipseTool,
    AnnotatedObliqueRectangleTool,
    AnnotatedPointTool,
    AnnotatedRectangleTool,
    AnnotatedSegmentTool,
    CircleTool,
    EllipseTool,
    FreeFormTool,
    HCursorTool,
    HRangeTool,
    LabelTool,
    MultiLineTool,
    ObliqueRectangleTool,
    PlaceAxesTool,
    RectangleTool,
    SegmentTool,
    VCursorTool,
    XCursorTool,
)


def create_window():
    win = make.dialog(
        edit=False,
        toolbar=True,
        wintitle="All image and plot tools test",
        type="image",
    )
    for toolklass in (
        LabelTool,
        HRangeTool,
        VCursorTool,
        HCursorTool,
        XCursorTool,
        SegmentTool,
        RectangleTool,
        ObliqueRectangleTool,
        CircleTool,
        EllipseTool,
        MultiLineTool,
        FreeFormTool,
        PlaceAxesTool,
        AnnotatedRectangleTool,
        AnnotatedObliqueRectangleTool,
        AnnotatedCircleTool,
        AnnotatedEllipseTool,
        AnnotatedSegmentTool,
        AnnotatedPointTool,
    ):
        win.manager.add_tool(toolklass)
    return win


def test_image_plot_tools():
    """Test"""
    with qt_app_context(exec_loop=True):
        filename = get_path("brain.png")
        win = create_window()
        win.show()
        image = make.image(filename=filename, colormap="bone")
        plot = win.manager.get_plot()
        plot.add_item(image)
        title = "toto"
        marker1 = Marker(label_cb=lambda x, y: f"{title}x = {x:g}<br>y = {y:g}")
        plot.add_item(marker1)
        marker2 = Marker(label_cb=lambda x, y: f"{title}x = {x:g}<br>y = {y:g}")
        plot.add_item(marker2)


if __name__ == "__main__":
    test_image_plot_tools()
../_images/image_plot_tools.png

Real-time Mandelbrot plotting


import numpy as np
from guidata.qthelpers import qt_app_context
from qtpy import QtCore as QC

from plotpy.builder import make
from plotpy.config import _
from plotpy.items import RawImageItem
from plotpy.mandelbrot import mandelbrot
from plotpy.tools import ToggleTool


class FullScale(ToggleTool):
    def __init__(self, parent, image):
        super().__init__(parent, _("MAX resolution"), None)
        self.image = image
        self.minprec = image.IMAX
        self.maxprec = 5 * image.IMAX

    def activate_command(self, plot, checked):
        if self.image.IMAX == self.minprec:
            self.image.IMAX = self.maxprec
        else:
            self.image.IMAX = self.minprec
        self.image.set_lut_range([0, self.image.IMAX])
        plot.replot()

    def update_status(self, plot):
        self.action.setChecked(self.image.IMAX == self.maxprec)


class MandelItem(RawImageItem):
    def __init__(self, xmin, xmax, ymin, ymax):
        super().__init__(np.zeros((1, 1), np.uint8))
        self.bounds = QC.QRectF(QC.QPointF(xmin, ymin), QC.QPointF(xmax, ymax))
        self.update_border()
        self.IMAX = 80
        self.set_lut_range([0, self.IMAX])

    # ---- QwtPlotItem API ------------------------------------------------------
    def draw_image(self, painter, canvasRect, srcRect, dstRect, xMap, yMap):
        x1, y1 = canvasRect.left(), canvasRect.top()
        x2, y2 = canvasRect.right(), canvasRect.bottom()
        i1, j1, i2, j2 = srcRect

        NX = x2 - x1
        NY = y2 - y1
        if self.data.shape != (NX, NY):
            self.data = np.zeros((NY, NX), np.int16)
        mandelbrot(i1, j1, i2, j2, self.data, self.IMAX)

        srcRect = (0, 0, NX, NY)
        x1, y1, x2, y2 = canvasRect.getCoords()
        RawImageItem.draw_image(
            self, painter, canvasRect, srcRect, (x1, y1, x2, y2), xMap, yMap
        )


def create_mandelbrot_window():
    """Create a Mandelbrot set window"""
    win = make.window(
        toolbar=True,
        wintitle="Mandelbrot",
        yreverse=False,
        type="image",
    )
    mandel = MandelItem(-1.5, 0.5, -1.0, 1.0)
    fstool = win.manager.add_tool(FullScale, mandel)
    plot = win.get_plot()
    plot.set_aspect_ratio(lock=False)
    plot.add_item(mandel)
    return win, mandel, fstool


def test_mandel():
    """Test Mandelbrot set window"""
    with qt_app_context(exec_loop=True):
        win, _mandel, _fstool = create_mandelbrot_window()
        win.show()


if __name__ == "__main__":
    test_mandel()
../_images/mandelbrot.png

Simple application


import sys

import numpy as np
from guidata.configtools import get_icon
from guidata.dataset import (
    ChoiceItem,
    DataSet,
    FloatArrayItem,
    GetAttrProp,
    IntItem,
    StringItem,
    update_dataset,
)
from guidata.dataset.qtwidgets import DataSetEditGroupBox
from guidata.qthelpers import (
    add_actions,
    create_action,
    get_std_icon,
    qt_app_context,
    win32_fix_title_bar_background,
)
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW

from plotpy import io
from plotpy.builder import make
from plotpy.config import _
from plotpy.plot import PlotOptions, PlotWidget
from plotpy.tests import get_path
from plotpy.widgets import about


class ImageParam(DataSet):
    _hide_data = False
    _hide_size = True
    title = StringItem(_("Title"), default=_("Untitled"))
    data = FloatArrayItem(_("Data")).set_prop("display", hide=GetAttrProp("_hide_data"))
    width = IntItem(
        _("Width"), help=_("Image width (pixels)"), min=1, default=100
    ).set_prop("display", hide=GetAttrProp("_hide_size"))
    height = IntItem(
        _("Height"), help=_("Image height (pixels)"), min=1, default=100
    ).set_prop("display", hide=GetAttrProp("_hide_size"))


class ImageParamNew(ImageParam):
    _hide_data = True
    _hide_size = False
    type = ChoiceItem(_("Type"), (("rand", _("random")), ("zeros", _("zeros"))))


class ImageListWithProperties(QW.QSplitter):
    def __init__(self, parent):
        QW.QSplitter.__init__(self, parent)
        self.imagelist = QW.QListWidget(self)
        self.addWidget(self.imagelist)
        self.properties = DataSetEditGroupBox(_("Properties"), ImageParam)
        self.properties.setEnabled(False)
        self.addWidget(self.properties)


class CentralWidget(QW.QSplitter):
    def __init__(self, parent, toolbar):
        QW.QSplitter.__init__(self, parent)
        self.setContentsMargins(10, 10, 10, 10)
        self.setOrientation(QC.Qt.Vertical)

        imagelistwithproperties = ImageListWithProperties(self)
        self.addWidget(imagelistwithproperties)
        self.imagelist = imagelistwithproperties.imagelist
        self.imagelist.currentRowChanged.connect(self.current_item_changed)
        self.imagelist.itemSelectionChanged.connect(self.selection_changed)
        self.properties = imagelistwithproperties.properties
        self.properties.SIG_APPLY_BUTTON_CLICKED.connect(self.properties_changed)

        self.plot_widget = PlotWidget(
            self,
            options=PlotOptions(type="image", show_contrast=True),
            auto_tools=False,
        )
        self.plot_widget.plot.SIG_LUT_CHANGED.connect(self.lut_range_changed)
        self.item = None  # image item

        self.plot_widget.manager.add_toolbar(toolbar, "default")
        self.plot_widget.register_tools()

        self.addWidget(self.plot_widget)

        self.images = []  # List of ImageParam instances
        self.lut_ranges = []  # List of LUT ranges

        self.setStretchFactor(0, 0)
        self.setStretchFactor(1, 1)
        self.setHandleWidth(10)
        self.setSizes([1, 2])

    def refresh_list(self):
        """Refresh image list"""
        self.imagelist.clear()
        self.imagelist.addItems([image.title for image in self.images])

    def selection_changed(self):
        """Image list: selection changed"""
        row = self.imagelist.currentRow()
        self.properties.setDisabled(row == -1)

    def current_item_changed(self, row):
        """Image list: current image changed"""
        if row == -1:
            return
        image, lut_range = self.images[row], self.lut_ranges[row]
        self.show_data(image.data, lut_range)
        update_dataset(self.properties.dataset, image)
        self.properties.get()

    def lut_range_changed(self):
        """LUT range changed"""
        row = self.imagelist.currentRow()
        self.lut_ranges[row] = self.item.get_lut_range()

    def show_data(self, data, lut_range=None):
        """Show image data"""
        plot = self.plot_widget.plot
        if self.item is not None:
            self.item.set_data(data)
            if lut_range is None:
                lut_range = self.item.get_lut_range()
            self.plot_widget.manager.set_contrast_range(*lut_range)
            self.plot_widget.manager.update_cross_sections()
        else:
            self.item = make.image(data, interpolation="nearest")
            plot.add_item(self.item, z=0)
        plot.select_item(self.item)
        plot.do_autoscale()
        plot.replot()

    def properties_changed(self):
        """The properties 'Apply' button was clicked: updating image"""
        row = self.imagelist.currentRow()
        image = self.images[row]
        update_dataset(image, self.properties.dataset)
        self.refresh_list()
        self.show_data(image.data)

    def add_image(self, image):
        """Add image"""
        self.images.append(image)
        self.lut_ranges.append(None)
        self.refresh_list()
        self.imagelist.setCurrentRow(len(self.images) - 1)
        plot = self.plot_widget.plot
        plot.do_autoscale()

    def add_image_from_file(self, filename):
        """Add image from file"""
        image = ImageParam()
        image.title = str(filename)
        image.data = io.imread(filename, to_grayscale=True)
        image.height, image.width = image.data.shape
        self.add_image(image)

    def remove_image(self, index=None):
        """Remove image"""
        if index is None:
            index = self.imagelist.currentRow()
        del self.images[index]
        del self.lut_ranges[index]
        self.refresh_list()
        if self.imagelist.count() > 0:
            self.imagelist.setCurrentRow(0)
        else:
            self.item = None
            self.plot_widget.plot.del_all_items()
            self.plot_widget.plot.replot()


class MainWindow(QW.QMainWindow):
    """Main Window"""

    def __init__(self):
        super().__init__()
        win32_fix_title_bar_background(self)
        self.setup()

    def setup(self):
        """Setup window parameters"""
        self.setWindowIcon(get_icon("python.png"))
        self.setWindowTitle(_("Application example"))
        self.resize(QC.QSize(600, 800))

        # Welcome message in statusbar:
        status = self.statusBar()
        status.showMessage(_("Welcome to plotpy application example!"), 5000)

        # Set central widget:
        main_toolbar = self.addToolBar("Main")
        toolbar = self.addToolBar("Image")
        self.mainwidget = CentralWidget(self, toolbar)
        self.setCentralWidget(self.mainwidget)

        # File menu
        file_menu = self.menuBar().addMenu(_("File"))
        new_action = create_action(
            self,
            _("New..."),
            shortcut="Ctrl+N",
            icon=get_icon("filenew.png"),
            tip=_("Create a new image"),
            triggered=self.new_image,
        )
        open_action = create_action(
            self,
            _("Open..."),
            shortcut="Ctrl+O",
            icon=get_icon("fileopen.png"),
            tip=_("Open an image"),
            triggered=self.open_image,
        )
        quit_action = create_action(
            self,
            _("Quit"),
            shortcut="Ctrl+Q",
            icon=get_std_icon("DialogCloseButton"),
            tip=_("Quit application"),
            triggered=self.close,
        )
        add_actions(file_menu, (new_action, open_action, None, quit_action))

        # Edit menu
        edit_menu = self.menuBar().addMenu(_("Edit"))
        del_action = create_action(
            self,
            _("Delete"),
            shortcut="Del",
            icon=get_icon("editdelete.png"),
            tip=_("Delete selected image"),
            triggered=self.mainwidget.remove_image,
        )
        add_actions(edit_menu, (del_action,))

        # Help menu
        help_menu = self.menuBar().addMenu("?")
        about_action = create_action(
            self,
            _("About %s...") % "PlotPy",
            icon=get_std_icon("MessageBoxInformation"),
            triggered=about.show_about_dialog,
        )
        add_actions(help_menu, (about_action,))

        add_actions(main_toolbar, (new_action, open_action))

    # ------I/O
    def new_image(self, imagenew=None):
        """Create a new image"""
        if imagenew is None:
            imagenew = ImageParamNew(title=_("Create a new image"))
            if not imagenew.edit(self):
                return
        image = ImageParam()
        image.title = imagenew.title
        if imagenew.type == "zeros":
            image.data = np.zeros((imagenew.width, imagenew.height))
        elif imagenew.type == "rand":
            image.data = np.random.randn(imagenew.width, imagenew.height)
        self.mainwidget.add_image(image)

    def open_image(self, filename=None):
        """Open image file"""
        if filename is None:
            saved_in, saved_out, saved_err = sys.stdin, sys.stdout, sys.stderr
            sys.stdout = None
            filename, _filter = QW.QFileDialog.getOpenFileName(
                self,
                _("Open"),
                "",
                io.iohandler.get_filters("load"),
                "",
                options=QW.QFileDialog.ShowDirsOnly,
            )
            sys.stdin, sys.stdout, sys.stderr = saved_in, saved_out, saved_err
        if filename:
            self.mainwidget.add_image_from_file(filename)


def test_simple_window():
    """Test simple window"""
    with qt_app_context(exec_loop=True):
        window = MainWindow()
        window.show()
        window.new_image(imagenew=ImageParamNew.create(type="rand"))
        window.open_image(filename=get_path("brain.png"))


if __name__ == "__main__":
    test_simple_window()
../_images/simple_window.png