# -*- 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 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