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


import numpy as np
import scipy.integrate as spt
from guidata.qthelpers import qt_app_context

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


def test_computations():
    """Test computations"""
    x = np.linspace(-10, 10, 1000)
    y = np.sin(np.sin(np.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: spt.trapezoid(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
from plotpy.tests.data import gen_image1


def __create_dialog_with_contrast(item):
    """Create plot dialog with contrast panel

    Args:
        Item: item to be added to the plot
    """
    win = make.dialog(
        edit=False,
        toolbar=True,
        wintitle="Contrast test",
        show_contrast=True,
        type="image",
        size=(600, 600),
    )
    plot = win.get_plot()
    plot.add_item(item)
    plot.set_active_item(item)
    item.unselect()
    win.show()
    return win


def test_contrast1():
    """Contrast test 1"""
    with qt_app_context(exec_loop=True):
        item = make.image(filename=get_path("brain.png"), colormap="gray")
        win = __create_dialog_with_contrast(item)
        fname = "contrast.png"
        try:
            win.get_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)


def test_contrast2():
    """Contrast test 2

    Test if level histogram is really removed when the associated image is removed from
    the plot (the validation is not automatic)
    """
    with qt_app_context(exec_loop=True):
        item1 = make.image(filename=get_path("brain.png"), colormap="gray")
        win = __create_dialog_with_contrast(item1)
        plot = win.get_plot()
        plot.del_item(item1)
        item2 = make.image(gen_image1())
        plot.add_item(item2)
        plot.set_active_item(item2)


if __name__ == "__main__":
    # test_contrast1()
    test_contrast2()
../_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",
        size=(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, w = rct.height(), 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(h * w * 4)
    except AttributeError:
        data = img.bits()
    npy: np.ndarray = np.frombuffer(data, np.uint8)
    return npy.reshape(h, w, 4)[:, :, 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), dtype.__name__)
        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",
            size=(800, 600),
        )
        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.styles import RawImageParam
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 DotArrayRawImageParam(RawImageParam, DotArrayParam):
    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:
            itemparam = DotArrayRawImageParam()
            gds.update_dataset(itemparam, param)
            param._update_cb = lambda: self.stamp_gbox.get()
            self.item = DotArrayItem(itemparam)
            plot.add_item(self.item)
        else:
            gds.update_dataset(self.item.param, param)
            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 qtpy import QtWidgets as QW

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

TOOLBAR_ID = "toolbar2"


def create_window():
    win = make.dialog(
        edit=False,
        toolbar=True,
        wintitle="All image and plot tools test",
        type="image",
        size=(800, 600),
    )
    toolbar2 = QW.QToolBar()
    win.layout().addWidget(toolbar2)
    win.manager.add_toolbar(toolbar2, TOOLBAR_ID)

    for toolklass in (
        LabelTool,
        HRangeTool,
        VCursorTool,
        HCursorTool,
        XCursorTool,
        SegmentTool,
        RectangleTool,
        ObliqueRectangleTool,
        CircleTool,
        EllipseTool,
        MultiLineTool,
        PolygonTool,
        PlaceAxesTool,
        AnnotatedRectangleTool,
        AnnotatedObliqueRectangleTool,
        AnnotatedCircleTool,
        AnnotatedEllipseTool,
        AnnotatedSegmentTool,
        AnnotatedPointTool,
        AnnotatedPolygonTool,
    ):
        win.manager.add_tool(toolklass, toolbar_id=TOOLBAR_ID)
    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