Source code for qwt.plot_renderer

# -*- coding: utf-8 -*-
#
# Licensed under the terms of the Qwt License
# Copyright (c) 2002 Uwe Rathmann, for the original C++ code
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)

"""
QwtPlotRenderer
---------------

.. autoclass:: QwtPlotRenderer
   :members:
"""

import math
import os.path as osp

from qtpy.compat import getsavefilename
from qtpy.QtCore import QObject, QRect, QRectF, QSizeF, Qt
from qtpy.QtGui import (
    QColor,
    QImage,
    QImageWriter,
    QPageSize,
    QPaintDevice,
    QPainter,
    QPainterPath,
    QPalette,
    QPen,
    QTransform,
)
from qtpy.QtPrintSupport import QPrinter
from qtpy.QtSvg import QSvgGenerator
from qtpy.QtWidgets import QFileDialog

from qwt.painter import QwtPainter
from qwt.plot import QwtPlot
from qwt.plot_layout import QwtPlotLayout
from qwt.scale_draw import QwtScaleDraw
from qwt.scale_map import QwtScaleMap


def qwtCanvasClip(canvas, canvasRect):
    """
    The clip region is calculated in integers
    To avoid too much rounding errors better
    calculate it in target device resolution
    """
    x1 = math.ceil(canvasRect.left())
    x2 = math.floor(canvasRect.right())
    y1 = math.ceil(canvasRect.top())
    y2 = math.floor(canvasRect.bottom())
    r = QRect(x1, y1, x2 - x1 - 1, y2 - y1 - 1)
    return canvas.borderPath(r)


class QwtPlotRenderer_PrivateData(object):
    def __init__(self):
        self.discardFlags = QwtPlotRenderer.DiscardNone
        self.layoutFlags = QwtPlotRenderer.DefaultLayout


[docs] class QwtPlotRenderer(QObject): """ Renderer for exporting a plot to a document, a printer or anything else, that is supported by QPainter/QPaintDevice Discard flags: * `QwtPlotRenderer.DiscardNone`: Render all components of the plot * `QwtPlotRenderer.DiscardBackground`: Don't render the background of the plot * `QwtPlotRenderer.DiscardTitle`: Don't render the title of the plot * `QwtPlotRenderer.DiscardLegend`: Don't render the legend of the plot * `QwtPlotRenderer.DiscardCanvasBackground`: Don't render the background of the canvas * `QwtPlotRenderer.DiscardFooter`: Don't render the footer of the plot * `QwtPlotRenderer.DiscardCanvasFrame`: Don't render the frame of the canvas .. note:: The `QwtPlotRenderer.DiscardCanvasFrame` flag has no effect when using style sheets, where the frame is part of the background Layout flags: * `QwtPlotRenderer.DefaultLayout`: Use the default layout as on screen * `QwtPlotRenderer.FrameWithScales`: Instead of the scales a box is painted around the plot canvas, where the scale ticks are aligned to. """ # enum DiscardFlag DiscardNone = 0x00 DiscardBackground = 0x01 DiscardTitle = 0x02 DiscardLegend = 0x04 DiscardCanvasBackground = 0x08 DiscardFooter = 0x10 DiscardCanvasFrame = 0x20 # enum LayoutFlag DefaultLayout = 0x00 FrameWithScales = 0x01 def __init__(self, parent=None): QObject.__init__(self, parent) self.__data = QwtPlotRenderer_PrivateData()
[docs] def setDiscardFlag(self, flag, on=True): """ Change a flag, indicating what to discard from rendering :param int flag: Flag to change :param bool on: On/Off .. seealso:: :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlags()`, :py:meth:`discardFlags()` """ if on: self.__data.discardFlags |= flag else: self.__data.discardFlags &= ~flag
[docs] def testDiscardFlag(self, flag): """ :param int flag: Flag to be tested :return: True, if flag is enabled. .. seealso:: :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`, :py:meth:`discardFlags()` """ return self.__data.discardFlags & flag
[docs] def setDiscardFlags(self, flags): """ Set the flags, indicating what to discard from rendering :param int flags: Flags .. seealso:: :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlag()`, :py:meth:`discardFlags()` """ self.__data.discardFlags = flags
[docs] def discardFlags(self): """ :return: Flags, indicating what to discard from rendering .. seealso:: :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`, :py:meth:`testDiscardFlag()` """ return self.__data.discardFlags
[docs] def setLayoutFlag(self, flag, on=True): """ Change a layout flag :param int flag: Flag to change .. seealso:: :py:meth:`testLayoutFlag()`, :py:meth:`setLayoutFlags()`, :py:meth:`layoutFlags()` """ if on: self.__data.layoutFlags |= flag else: self.__data.layoutFlags &= ~flag
[docs] def testLayoutFlag(self, flag): """ :param int flag: Flag to be tested :return: True, if flag is enabled. .. seealso:: :py:meth:`setLayoutFlag()`, :py:meth:`setLayoutFlags()`, :py:meth:`layoutFlags()` """ return self.__data.layoutFlags & flag
[docs] def setLayoutFlags(self, flags): """ Set the layout flags :param int flags: Flags .. seealso:: :py:meth:`setLayoutFlag()`, :py:meth:`testLayoutFlag()`, :py:meth:`layoutFlags()` """ self.__data.layoutFlags = flags
[docs] def layoutFlags(self): """ :return: Layout flags .. seealso:: :py:meth:`setLayoutFlags()`, :py:meth:`setLayoutFlag()`, :py:meth:`testLayoutFlag()` """ return self.__data.layoutFlags
[docs] def renderDocument( self, plot, filename, sizeMM=(300, 200), resolution=85, format_=None ): """ Render a plot to a file The format of the document will be auto-detected from the suffix of the file name. :param qwt.plot.QwtPlot plot: Plot widget :param str fileName: Path of the file, where the document will be stored :param QSizeF sizeMM: Size for the document in millimeters :param int resolution: Resolution in dots per Inch (dpi) """ if isinstance(sizeMM, tuple): sizeMM = QSizeF(*sizeMM) if format_ is None: ext = osp.splitext(filename)[1] if not ext: raise TypeError("Unable to determine target format from filename") format_ = ext[1:] if plot is None or sizeMM.isEmpty() or resolution <= 0: return title = plot.title().text() if not title: title = "Plot Document" mmToInch = 1.0 / 25.4 size = sizeMM * mmToInch * resolution documentRect = QRectF(0.0, 0.0, size.width(), size.height()) fmt = format_.lower() if fmt in ("pdf", "ps"): printer = QPrinter() if fmt == "pdf": try: printer.setOutputFormat(QPrinter.PdfFormat) except AttributeError: # PyQt6 on Linux printer.setPrinterName("") else: printer.setOutputFormat(QPrinter.PostScriptFormat) try: printer.setColorMode(QPrinter.Color) except AttributeError: # PyQt6 on Linux pass printer.setFullPage(True) printer.setPageSize(QPageSize(sizeMM, QPageSize.Millimeter)) printer.setDocName(title) printer.setOutputFileName(filename) printer.setResolution(resolution) painter = QPainter(printer) self.render(plot, painter, documentRect) painter.end() elif fmt == "svg": generator = QSvgGenerator() generator.setTitle(title) generator.setFileName(filename) generator.setResolution(resolution) generator.setViewBox(documentRect) painter = QPainter(generator) self.render(plot, painter, documentRect) painter.end() elif fmt in QImageWriter.supportedImageFormats(): imageRect = documentRect.toRect() dotsPerMeter = int(round(resolution * mmToInch * 1000.0)) image = QImage(imageRect.size(), QImage.Format_ARGB32) image.setDotsPerMeterX(dotsPerMeter) image.setDotsPerMeterY(dotsPerMeter) image.fill(QColor(Qt.white).rgb()) painter = QPainter(image) self.render(plot, painter, imageRect) painter.end() image.save(filename, fmt) else: raise TypeError("Unsupported file format '%s'" % fmt)
[docs] def renderTo(self, plot, dest): """ Render a plot to a file Supported formats are: - pdf: Portable Document Format PDF - ps: Postcript - svg: Scalable Vector Graphics SVG - all image formats supported by Qt, see QImageWriter.supportedImageFormats() Scalable vector graphic formats like PDF or SVG are superior to raster graphics formats. :param qwt.plot.QwtPlot plot: Plot widget :param dest: QPaintDevice, QPrinter or QSvgGenerator instance .. seealso:: :py:meth:`render()`, :py:meth:`qwt.painter.QwtPainter.setRoundingAlignment()` """ if isinstance(dest, QPaintDevice): w = dest.width() h = dest.height() rect = QRectF(0, 0, w, h) elif isinstance(dest, QPrinter): w = dest.width() h = dest.height() rect = QRectF(0, 0, w, h) aspect = rect.width() / rect.height() if aspect < 1.0: rect.setHeight(aspect * rect.width()) elif isinstance(dest, QSvgGenerator): rect = dest.viewBoxF() if rect.isEmpty(): rect.setRect(0, 0, dest.width(), dest.height()) if rect.isEmpty(): rect.setRect(0, 0, 800, 600) else: raise TypeError("Unsupported destination type %s" % type(dest)) p = QPainter(dest) self.render(plot, p, rect)
[docs] def render(self, plot, painter, plotRect): """ Paint the contents of a QwtPlot instance into a given rectangle. :param qwt.plot.QwtPlot plot: Plot to be rendered :param QPainter painter: Painter :param str format: Format for the document :param QRectF plotRect: Bounding rectangle .. seealso:: :py:meth:`renderDocument()`, :py:meth:`renderTo()`, :py:meth:`qwt.painter.QwtPainter.setRoundingAlignment()` """ if ( painter == 0 or not painter.isActive() or not plotRect.isValid() or plot.size().isNull() ): return if not self.__data.discardFlags & self.DiscardBackground: QwtPainter.drawBackground(painter, plotRect, plot) # The layout engine uses the same methods as they are used # by the Qt layout system. Therefore we need to calculate the # layout in screen coordinates and paint with a scaled painter. transform = QTransform() transform.scale( float(painter.device().logicalDpiX()) / plot.logicalDpiX(), float(painter.device().logicalDpiY()) / plot.logicalDpiY(), ) invtrans, _ok = transform.inverted() layoutRect = invtrans.mapRect(plotRect) if not (self.__data.discardFlags & self.DiscardBackground): mg = plot.contentsMargins() layoutRect.adjust(mg.left(), mg.top(), -mg.right(), -mg.bottom()) layout = plot.plotLayout() baseLineDists = canvasMargins = [None] * len(QwtPlot.AXES) for axisId in QwtPlot.AXES: canvasMargins[axisId] = layout.canvasMargin(axisId) if self.__data.layoutFlags & self.FrameWithScales: scaleWidget = plot.axisWidget(axisId) if scaleWidget: mgn = scaleWidget.contentsMargins() baseLineDists[axisId] = max( [mgn.left(), mgn.top(), mgn.right(), mgn.bottom()] ) scaleWidget.setMargin(0) if not plot.axisEnabled(axisId): # When we have a scale the frame is painted on # the position of the backbone - otherwise we # need to introduce a margin around the canvas if axisId == QwtPlot.yLeft: layoutRect.adjust(1, 0, 0, 0) elif axisId == QwtPlot.yRight: layoutRect.adjust(0, 0, -1, 0) elif axisId == QwtPlot.xTop: layoutRect.adjust(0, 1, 0, 0) elif axisId == QwtPlot.xBottom: layoutRect.adjust(0, 0, 0, -1) # Calculate the layout for the document. layoutOptions = QwtPlotLayout.IgnoreScrollbars if ( self.__data.layoutFlags & self.FrameWithScales or self.__data.discardFlags & self.DiscardCanvasFrame ): layoutOptions |= QwtPlotLayout.IgnoreFrames if self.__data.discardFlags & self.DiscardLegend: layoutOptions |= QwtPlotLayout.IgnoreLegend if self.__data.discardFlags & self.DiscardTitle: layoutOptions |= QwtPlotLayout.IgnoreTitle if self.__data.discardFlags & self.DiscardFooter: layoutOptions |= QwtPlotLayout.IgnoreFooter layout.activate(plot, layoutRect, layoutOptions) maps = self.buildCanvasMaps(plot, layout.canvasRect()) if self.updateCanvasMargins(plot, layout.canvasRect(), maps): # recalculate maps and layout, when the margins # have been changed layout.activate(plot, layoutRect, layoutOptions) maps = self.buildCanvasMaps(plot, layout.canvasRect()) painter.save() painter.setWorldTransform(transform, True) self.renderCanvas(plot, painter, layout.canvasRect(), maps) if ( not self.__data.discardFlags & self.DiscardTitle ) and plot.titleLabel().text(): self.renderTitle(plot, painter, layout.titleRect()) if ( not self.__data.discardFlags & self.DiscardFooter ) and plot.titleLabel().text(): self.renderFooter(plot, painter, layout.footerRect()) if ( not self.__data.discardFlags & self.DiscardLegend ) and plot.titleLabel().text(): self.renderLegend(plot, painter, layout.legendRect()) for axisId in QwtPlot.AXES: scaleWidget = plot.axisWidget(axisId) if scaleWidget: mgn = scaleWidget.contentsMargins() baseDist = max([mgn.left(), mgn.top(), mgn.right(), mgn.bottom()]) startDist, endDist = scaleWidget.getBorderDistHint() self.renderScale( plot, painter, axisId, startDist, endDist, baseDist, layout.scaleRect(axisId), ) painter.restore() for axisId in QwtPlot.AXES: if self.__data.layoutFlags & self.FrameWithScales: scaleWidget = plot.axisWidget(axisId) if scaleWidget: scaleWidget.setMargin(baseLineDists[axisId]) layout.setCanvasMargin(canvasMargins[axisId]) layout.invalidate()
[docs] def renderTitle(self, plot, painter, rect): """ Render the title into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param QRectF rect: Bounding rectangle """ painter.setFont(plot.titleLabel().font()) color = plot.titleLabel().palette().color(QPalette.Active, QPalette.Text) painter.setPen(color) plot.titleLabel().text().draw(painter, rect)
[docs] def renderFooter(self, plot, painter, rect): """ Render the footer into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param QRectF rect: Bounding rectangle """ painter.setFont(plot.footerLabel().font()) color = plot.footerLabel().palette().color(QPalette.Active, QPalette.Text) painter.setPen(color) plot.footerLabel().text().draw(painter, rect)
[docs] def renderLegend(self, plot, painter, rect): """ Render the legend into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param QRectF rect: Bounding rectangle """ if plot.legend(): fillBackground = not self.__data.discardFlags & self.DiscardBackground plot.legend().renderLegend(painter, rect, fillBackground)
[docs] def renderScale(self, plot, painter, axisId, startDist, endDist, baseDist, rect): """ Paint a scale into a given rectangle. Paint the scale into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param int axisId: Axis :param int startDist: Start border distance :param int endDist: End border distance :param int baseDist: Base distance :param QRectF rect: Bounding rectangle """ if not plot.axisEnabled(axisId): return scaleWidget = plot.axisWidget(axisId) if scaleWidget.isColorBarEnabled() and scaleWidget.colorBarWidth() > 0: scaleWidget.drawColorBar(painter, scaleWidget.colorBarRect(rect)) baseDist += scaleWidget.colorBarWidth() + scaleWidget.spacing() painter.save() if axisId == QwtPlot.yLeft: x = rect.right() - 1.0 - baseDist y = rect.y() + startDist w = rect.height() - startDist - endDist align = QwtScaleDraw.LeftScale elif axisId == QwtPlot.yRight: x = rect.left() + baseDist y = rect.y() + startDist w = rect.height() - startDist - endDist align = QwtScaleDraw.RightScale elif axisId == QwtPlot.xTop: x = rect.left() + startDist y = rect.bottom() - 1.0 - baseDist w = rect.width() - startDist - endDist align = QwtScaleDraw.TopScale else: # QwtPlot.xBottom x = rect.left() + startDist y = rect.top() + baseDist w = rect.width() - startDist - endDist align = QwtScaleDraw.BottomScale scaleWidget.drawTitle(painter, align, rect) painter.setFont(scaleWidget.font()) sd = scaleWidget.scaleDraw() sdPos = sd.pos() sdLength = sd.length() sd.move(x, y) sd.setLength(w) palette = scaleWidget.palette() palette.setCurrentColorGroup(QPalette.Active) sd.draw(painter, palette) sd.move(sdPos) sd.setLength(sdLength) painter.restore()
[docs] def renderCanvas(self, plot, painter, canvasRect, maps): """ Render the canvas into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param QRectF rect: Bounding rectangle :param qwt.scale_map.QwtScaleMap maps: mapping between plot and paint device coordinates """ canvas = plot.canvas() r = canvasRect.adjusted(0.0, 0.0, -1.0, 1.0) if self.__data.layoutFlags & self.FrameWithScales: painter.save() r.adjust(-1.0, -1.0, 1.0, 1.0) painter.setPen(QPen(Qt.black)) if not (self.__data.discardFlags & self.DiscardCanvasBackground): bgBrush = canvas.palette().brush(plot.backgroundRole()) painter.setBrush(bgBrush) painter.drawRect(r) painter.restore() painter.save() painter.setClipRect(canvasRect) plot.drawItems(painter, canvasRect, maps) painter.restore() elif canvas.testAttribute(Qt.WA_StyledBackground): clipPath = QPainterPath() painter.save() if not self.__data.discardFlags & self.DiscardCanvasBackground: QwtPainter.drawBackground(painter, r, canvas) clipPath = qwtCanvasClip(canvas, canvasRect) painter.restore() painter.save() if clipPath.isEmpty(): painter.setClipRect(canvasRect) else: painter.setClipPath(clipPath) plot.drawItems(painter, canvasRect, maps) painter.restore() else: clipPath = QPainterPath() frameWidth = 0 if not self.__data.discardFlags & self.DiscardCanvasFrame: frameWidth = canvas.frameWidth() clipPath = qwtCanvasClip(canvas, canvasRect) innerRect = canvasRect.adjusted( frameWidth, frameWidth, -frameWidth, -frameWidth ) painter.save() if clipPath.isEmpty(): painter.setClipRect(innerRect) else: painter.setClipPath(clipPath) if not self.__data.discardFlags & self.DiscardCanvasBackground: QwtPainter.drawBackground(painter, innerRect, canvas) plot.drawItems(painter, innerRect, maps) painter.restore() if frameWidth > 0: painter.save() frameStyle = canvas.frameShadow() | canvas.frameShape() radius = canvas.borderRadius() if radius > 0.0: QwtPainter.drawRoundedFrame( painter, canvasRect, radius, radius, canvas.palette(), frameWidth, frameStyle, ) else: midLineWidth = canvas.midLineWidth() QwtPainter.drawFrame( painter, canvasRect, canvas.palette(), canvas.foregroundRole(), frameWidth, midLineWidth, frameStyle, ) painter.restore()
[docs] def buildCanvasMaps(self, plot, canvasRect): """ Calculated the scale maps for rendering the canvas :param qwt.plot.QwtPlot plot: Plot widget :param QRectF canvasRect: Target rectangle :return: Calculated scale maps """ maps = [] for axisId in QwtPlot.AXES: map_ = QwtScaleMap() map_.setTransformation(plot.axisScaleEngine(axisId).transformation()) sd = plot.axisScaleDiv(axisId) map_.setScaleInterval(sd.lowerBound(), sd.upperBound()) if plot.axisEnabled(axisId): s = plot.axisWidget(axisId) scaleRect = plot.plotLayout().scaleRect(axisId) if axisId in (QwtPlot.xTop, QwtPlot.xBottom): from_ = scaleRect.left() + s.startBorderDist() to = scaleRect.right() - s.endBorderDist() else: from_ = scaleRect.bottom() - s.endBorderDist() to = scaleRect.top() + s.startBorderDist() else: margin = 0 if not plot.plotLayout().alignCanvasToScale(axisId): margin = plot.plotLayout().canvasMargin(axisId) if axisId in (QwtPlot.yLeft, QwtPlot.yRight): from_ = canvasRect.bottom() - margin to = canvasRect.top() + margin else: from_ = canvasRect.left() + margin to = canvasRect.right() - margin map_.setPaintInterval(from_, to) maps.append(map_) return maps
def updateCanvasMargins(self, plot, canvasRect, maps): margins = plot.getCanvasMarginsHint(maps, canvasRect) marginsChanged = False for axisId in QwtPlot.AXES: if margins[axisId] >= 0.0: m = math.ceil(margins[axisId]) plot.plotLayout().setCanvasMargin(m, axisId) marginsChanged = True return marginsChanged
[docs] def exportTo(self, plot, documentname, sizeMM=None, resolution=85): """ Execute a file dialog and render the plot to the selected file :param qwt.plot.QwtPlot plot: Plot widget :param str documentName: Default document name :param QSizeF sizeMM: Size for the document in millimeters :param int resolution: Resolution in dots per Inch (dpi) :return: True, when exporting was successful .. seealso:: :py:meth:`renderDocument()` """ if plot is None: return if sizeMM is None: sizeMM = QSizeF(300, 200) filename = documentname imageFormats = QImageWriter.supportedImageFormats() filter_ = [ "PDF documents (*.pdf)", "SVG documents (*.svg)", "Postscript documents (*.ps)", ] if imageFormats: imageFilter = "Images" imageFilter += " (" for idx, fmt in enumerate(imageFormats): if idx > 0: imageFilter += " " imageFilter += "*." + str(fmt) imageFilter += ")" filter_ += [imageFilter] filename, _s = getsavefilename( plot, "Export File Name", filename, ";;".join(filter_), options=QFileDialog.DontConfirmOverwrite, ) if not filename: return False self.renderDocument(plot, filename, sizeMM, resolution) return True return True