step == 0, the step size is calculated automatically using the maxMajor setting.
+
+ .. seealso::
+
+ :py:meth:`setAxisMaxMajor()`, :py:meth:`setAxisAutoScale()`,
+ :py:meth:`axisStepSize()`,
+ :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()`
+ """
if self.axisValid(axisId):
d = self.__axisData[axisId]
d.doAutoScale = False
@@ -375,29 +740,108 @@ def setAxisScale(self, axisId, min_, max_, stepSize=0):
d.maxValue = max_
d.stepSize = stepSize
self.autoRefresh()
-
+
def setAxisScaleDiv(self, axisId, scaleDiv):
+ """
+ Disable autoscaling and specify a fixed scale for a selected axis.
+
+ The scale division will be stored locally only until the next call
+ of updateAxes(). So updates of the scale widget usually happen delayed with
+ the next replot.
+
+ :param int axisId: Axis index
+ :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division
+
+ .. seealso::
+
+ :py:meth:`setAxisScale()`, :py:meth:`setAxisAutoScale()`
+ """
if self.axisValid(axisId):
d = self.__axisData[axisId]
d.doAutoScale = False
d.scaleDiv = scaleDiv
d.isValid = True
self.autoRefresh()
-
+
def setAxisScaleDraw(self, axisId, scaleDraw):
+ """
+ Set a scale draw
+
+ :param int axisId: Axis index
+ :param qwt.scale_draw.QwtScaleDraw scaleDraw: Object responsible for drawing scales.
+
+ By passing scaleDraw it is possible to extend QwtScaleDraw
+ functionality and let it take place in QwtPlot. Please note
+ that scaleDraw has to be created with new and will be deleted
+ by the corresponding QwtScale member ( like a child object ).
+
+ .. seealso::
+
+ :py:class:`qwt.scale_draw.QwtScaleDraw`,
+ :py:class:`qwt.scale_widget.QwtScaleWigdet`
+
+ .. warning::
+
+ The attributes of scaleDraw will be overwritten by those of the
+ previous QwtScaleDraw.
+ """
if self.axisValid(axisId):
self.axisWidget(axisId).setScaleDraw(scaleDraw)
self.autoRefresh()
-
+
def setAxisLabelAlignment(self, axisId, alignment):
+ """
+ Change the alignment of the tick labels
+
+ :param int axisId: Axis index
+ :param Qt.Alignment alignment: Or'd Qt.AlignmentFlags
+
+ .. seealso::
+
+ :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAlignment()`
+ """
if self.axisValid(axisId):
self.axisWidget(axisId).setLabelAlignment(alignment)
-
+
def setAxisLabelRotation(self, axisId, rotation):
+ """
+ Rotate all tick labels
+
+ :param int axisId: Axis index
+ :param float rotation: Angle in degrees. When changing the label rotation, the label alignment might be adjusted too.
+
+ .. seealso::
+
+ :py:meth:`setLabelRotation()`, :py:meth:`setAxisLabelAlignment()`
+ """
if self.axisValid(axisId):
self.axisWidget(axisId).setLabelRotation(rotation)
-
+
+ def setAxisLabelAutoSize(self, axisId, state):
+ """
+ Set tick labels automatic size option (default: on)
+
+ :param int axisId: Axis index
+ :param bool state: On/off
+
+ .. seealso::
+
+ :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAutoSize()`
+ """
+ if self.axisValid(axisId):
+ self.axisWidget(axisId).setLabelAutoSize(state)
+
def setAxisMaxMinor(self, axisId, maxMinor):
+ """
+ Set the maximum number of minor scale intervals for a specified axis
+
+ :param int axisId: Axis index
+ :param int maxMinor: Maximum number of minor steps
+
+ .. seealso::
+
+ :py:meth:`axisMaxMinor()`
+ """
if self.axisValid(axisId):
maxMinor = max([0, min([maxMinor, 100])])
d = self.__axisData[axisId]
@@ -407,6 +851,16 @@ def setAxisMaxMinor(self, axisId, maxMinor):
self.autoRefresh()
def setAxisMaxMajor(self, axisId, maxMajor):
+ """
+ Set the maximum number of major scale intervals for a specified axis
+
+ :param int axisId: Axis index
+ :param int maxMajor: Maximum number of major steps
+
+ .. seealso::
+
+ :py:meth:`axisMaxMajor()`
+ """
if self.axisValid(axisId):
maxMajor = max([1, min([maxMajor, 10000])])
d = self.__axisData[axisId]
@@ -415,13 +869,67 @@ def setAxisMaxMajor(self, axisId, maxMajor):
d.isValid = False
self.autoRefresh()
+ def setAxisMargin(self, axisId, margin):
+ """
+ Set the relative margin of the axis, as a fraction of the full axis range
+
+ :param int axisId: Axis index
+ :param float margin: Relative margin (float between 0 and 1)
+
+ .. seealso::
+
+ :py:meth:`axisMargin()`
+ """
+ if not isinstance(margin, float) or margin < 0.0 or margin > 1.0:
+ raise ValueError("margin must be a float between 0 and 1")
+ if self.axisValid(axisId):
+ d = self.__axisData[axisId]
+ if margin != d.margin:
+ d.margin = margin
+ d.isValid = False
+ self.autoRefresh()
+
def setAxisTitle(self, axisId, title):
+ """
+ Change the title of a specified axis
+
+ :param int axisId: Axis index
+ :param title: axis title
+ :type title: qwt.text.QwtText or str
+ """
if self.axisValid(axisId):
self.axisWidget(axisId).setTitle(title)
self.updateLayout()
def updateAxes(self):
- intv = [QwtInterval() for _i in range(self.axisCnt)]
+ """
+ Rebuild the axes scales
+
+ In case of autoscaling the boundaries of a scale are calculated
+ from the bounding rectangles of all plot items, having the
+ `QwtPlotItem.AutoScale` flag enabled (`QwtScaleEngine.autoScale()`).
+ Then a scale division is calculated (`QwtScaleEngine.didvideScale()`)
+ and assigned to scale widget.
+
+ When the scale boundaries have been assigned with `setAxisScale()` a
+ scale division is calculated (`QwtScaleEngine.didvideScale()`)
+ for this interval and assigned to the scale widget.
+
+ When the scale has been set explicitly by `setAxisScaleDiv()` the
+ locally stored scale division gets assigned to the scale widget.
+
+ The scale widget indicates modifications by emitting a
+ `QwtScaleWidget.scaleDivChanged()` signal.
+
+ `updateAxes()` is usually called by `replot()`.
+
+ .. seealso::
+
+ :py:meth:`setAxisAutoScale()`, :py:meth:`setAxisScale()`,
+ :py:meth:`setAxisScaleDiv()`, :py:meth:`replot()`,
+ :py:meth:`QwtPlotItem.boundingRect()`
+ """
+ intv = [QwtInterval() for _i in self.AXES]
itmList = self.itemList()
for item in itmList:
if not item.testItemAttribute(QwtPlotItem.AutoScale):
@@ -430,11 +938,12 @@ def updateAxes(self):
continue
if self.axisAutoScale(item.xAxis()) or self.axisAutoScale(item.yAxis()):
rect = item.boundingRect()
- if rect.width() >= 0.:
+ if rect.width() >= 0.0:
intv[item.xAxis()] |= QwtInterval(rect.left(), rect.right())
- if rect.height() >= 0.:
+ if rect.height() >= 0.0:
intv[item.yAxis()] |= QwtInterval(rect.top(), rect.bottom())
- for axisId in range(self.axisCnt):
+
+ for axisId in self.AXES:
d = self.__axisData[axisId]
minValue = d.minValue
maxValue = d.maxValue
@@ -443,24 +952,44 @@ def updateAxes(self):
d.isValid = False
minValue = intv[axisId].minValue()
maxValue = intv[axisId].maxValue()
- d.scaleEngine.autoScale(d.maxMajor, minValue, maxValue, stepSize)
+ minValue, maxValue, stepSize = d.scaleEngine.autoScale(
+ d.maxMajor, minValue, maxValue, stepSize, d.margin
+ )
if not d.isValid:
- d.scaleDiv = d.scaleEngine.divideScale(minValue, maxValue,
- d.maxMajor, d.maxMinor, stepSize)
+ d.scaleDiv = d.scaleEngine.divideScale(
+ minValue, maxValue, d.maxMajor, d.maxMinor, stepSize
+ )
d.isValid = True
scaleWidget = self.axisWidget(axisId)
scaleWidget.setScaleDiv(d.scaleDiv)
- #TODO: see when it is *really* necessary to update border dist
-# startDist, endDist = scaleWidget.getBorderDistHint()
-# scaleWidget.setBorderDist(startDist, endDist)
+ # It is *really* necessary to update border dist!
+ # Otherwise, when tick labels are large enough, the ticks
+ # may not be aligned with canvas grid.
+ # See the following issues for more details:
+ # https://github.com/PlotPyStack/guiqwt/issues/57
+ # https://github.com/PlotPyStack/PythonQwt/issues/30
+ startDist, endDist = scaleWidget.getBorderDistHint()
+ scaleWidget.setBorderDist(startDist, endDist)
for item in itmList:
if item.testItemInterest(QwtPlotItem.ScaleInterest):
- item.updateScaleDiv(self.axisScaleDiv(item.xAxis()),
- self.axisScaleDiv(item.yAxis()))
-
+ item.updateScaleDiv(
+ self.axisScaleDiv(item.xAxis()), self.axisScaleDiv(item.yAxis())
+ )
+
def setCanvas(self, canvas):
+ """
+ Set the drawing canvas of the plot widget.
+
+ The default canvas is a `QwtPlotCanvas`.
+
+ :param QWidget canvas: Canvas Widget
+
+ .. seealso::
+
+ :py:meth:`canvas()`
+ """
if canvas == self.__data.canvas:
return
self.__data.canvas = canvas
@@ -469,34 +998,70 @@ def setCanvas(self, canvas):
canvas.installEventFilter(self)
if self.isVisible():
canvas.show()
-
+
def event(self, event):
- ok = QFrame.event(self, event)
if event.type() == QEvent.LayoutRequest:
self.updateLayout()
elif event.type() == QEvent.PolishRequest:
self.replot()
- return ok
+ return QFrame.event(self, event)
def eventFilter(self, obj, event):
if obj is self.__data.canvas:
if event.type() == QEvent.Resize:
self.updateCanvasMargins()
- elif event.type() == 178:#QEvent.ContentsRectChange:
+ elif event.type() == 178: # QEvent.ContentsRectChange:
self.updateLayout()
return QFrame.eventFilter(self, obj, event)
-
+
def autoRefresh(self):
+ """Replots the plot if :py:meth:`autoReplot()` is True."""
if self.__data.autoReplot:
self.replot()
-
- def setAutoReplot(self, tf):
+
+ def setAutoReplot(self, tf=True):
+ """
+ Set or reset the autoReplot option
+
+ If the autoReplot option is set, the plot will be
+ updated implicitly by manipulating member functions.
+ Since this may be time-consuming, it is recommended
+ to leave this option switched off and call :py:meth:`replot()`
+ explicitly if necessary.
+
+ The autoReplot option is set to false by default, which
+ means that the user has to call :py:meth:`replot()` in order
+ to make changes visible.
+
+ :param bool tf: True or False. Defaults to True.
+
+ .. seealso::
+
+ :py:meth:`autoReplot()`
+ """
self.__data.autoReplot = tf
-
+
def autoReplot(self):
+ """
+ :return: True if the autoReplot option is set.
+
+ .. seealso::
+
+ :py:meth:`setAutoReplot()`
+ """
return self.__data.autoReplot
-
+
def setTitle(self, title):
+ """
+ Change the plot's title
+
+ :param title: New title
+ :type title: str or qwt.text.QwtText
+
+ .. seealso::
+
+ :py:meth:`title()`
+ """
current_title = self.__data.titleLabel.text()
if isinstance(title, QwtText) and current_title == title:
return
@@ -504,14 +1069,34 @@ def setTitle(self, title):
return
self.__data.titleLabel.setText(title)
self.updateLayout()
-
+
def title(self):
+ """
+ :return: Title of the plot
+
+ .. seealso::
+
+ :py:meth:`setTitle()`
+ """
return self.__data.titleLabel.text()
-
+
def titleLabel(self):
+ """
+ :return: Title label widget.
+ """
return self.__data.titleLabel
-
+
def setFooter(self, text):
+ """
+ Change the text the footer
+
+ :param text: New text of the footer
+ :type text: str or qwt.text.QwtText
+
+ .. seealso::
+
+ :py:meth:`footer()`
+ """
current_footer = self.__data.footerLabel.text()
if isinstance(text, QwtText) and current_footer == text:
return
@@ -519,93 +1104,170 @@ def setFooter(self, text):
return
self.__data.footerLabel.setText(text)
self.updateLayout()
-
+
def footer(self):
+ """
+ :return: Text of the footer
+
+ .. seealso::
+
+ :py:meth:`setFooter()`
+ """
return self.__data.footerLabel.text()
-
+
def footerLabel(self):
+ """
+ :return: Footer label widget.
+ """
return self.__data.footerLabel
def setPlotLayout(self, layout):
+ """
+ Assign a new plot layout
+
+ :param layout: Layout
+ :type layout: qwt.plot_layout.QwtPlotLayout
+
+ .. seealso::
+
+ :py:meth:`plotLayout()`
+ """
if layout != self.__data.layout:
self.__data.layout = layout
self.updateLayout()
-
+
def plotLayout(self):
+ """
+ :return: the plot's layout
+
+ .. seealso::
+
+ :py:meth:`setPlotLayout()`
+ """
return self.__data.layout
-
+
def legend(self):
+ """
+ :return: the plot's legend
+
+ .. seealso::
+
+ :py:meth:`insertLegend()`
+ """
return self.__data.legend
-
+
def canvas(self):
+ """
+ :return: the plot's canvas
+ """
return self.__data.canvas
-
+
def sizeHint(self):
+ """
+ :return: Size hint for the plot widget
+
+ .. seealso::
+
+ :py:meth:`minimumSizeHint()`
+ """
dw = dh = 0
- for axisId in range(self.axisCnt):
+ for axisId in self.AXES:
if self.axisEnabled(axisId):
niceDist = 40
scaleWidget = self.axisWidget(axisId)
scaleDiv = scaleWidget.scaleDraw().scaleDiv()
majCnt = len(scaleDiv.ticks(QwtScaleDiv.MajorTick))
if axisId in (self.yLeft, self.yRight):
- hDiff = (majCnt-1)*niceDist-scaleWidget.minimumSizeHint().height()
+ hDiff = (
+ majCnt - 1
+ ) * niceDist - scaleWidget.minimumSizeHint().height()
if hDiff > dh:
dh = hDiff
else:
- wDiff = (majCnt-1)*niceDist-scaleWidget.minimumSizeHint().width()
+ wDiff = (
+ majCnt - 1
+ ) * niceDist - scaleWidget.minimumSizeHint().width()
if wDiff > dw:
dw = wDiff
return self.minimumSizeHint() + QSize(dw, dh)
-
+
def minimumSizeHint(self):
+ """
+ :return: Return a minimum size hint
+ """
hint = self.__data.layout.minimumSizeHint(self)
- hint += QSize(2*self.frameWidth(), 2*self.frameWidth())
+ hint += QSize(2 * self.frameWidth(), 2 * self.frameWidth())
return hint
-
+
def resizeEvent(self, e):
QFrame.resizeEvent(self, e)
self.updateLayout()
-
+
def replot(self):
+ """
+ Redraw the plot
+
+ If the `autoReplot` option is not set (which is the default)
+ or if any curves are attached to raw data, the plot has to
+ be refreshed explicitly in order to make changes visible.
+
+ .. seealso::
+
+ :py:meth:`updateAxes()`, :py:meth:`setAutoReplot()`
+ """
doAutoReplot = self.autoReplot()
self.setAutoReplot(False)
self.updateAxes()
-
+
+ # Maybe the layout needs to be updated, because of changed
+ # axes labels. We need to process them here before painting
+ # to avoid that scales and canvas get out of sync.
QApplication.sendPostedEvents(self, QEvent.LayoutRequest)
-
+
if self.__data.canvas:
try:
self.__data.canvas.replot()
except (AttributeError, TypeError):
self.__data.canvas.update(self.__data.canvas.contentsRect())
-
+
self.setAutoReplot(doAutoReplot)
def get_layout_state(self):
- return (self.contentsRect(),
- self.__data.titleLabel.text(), self.__data.footerLabel.text(),
- [(self.axisEnabled(axisId), self.axisTitle(axisId).text())
- for axisId in range(self.axisCnt)],
- self.__data.legend)
-
+ return (
+ self.contentsRect(),
+ self.__data.titleLabel.text(),
+ self.__data.footerLabel.text(),
+ [
+ (self.axisEnabled(axisId), self.axisTitle(axisId).text())
+ for axisId in self.AXES
+ ],
+ self.__data.legend,
+ )
+
def updateLayout(self):
-# state = self.get_layout_state()
-# if self.__layout_state is not None and\
-# state == self.__layout_state:
-# return
-# self.__layout_state = state
+ """
+ Adjust plot content to its current size.
+
+ .. seealso::
+
+ :py:meth:`resizeEvent()`
+ """
+ # state = self.get_layout_state()
+ # if self.__layout_state is not None and\
+ # state == self.__layout_state:
+ # return
+ # self.__layout_state = state
self.__data.layout.activate(self, self.contentsRect())
-
+
titleRect = self.__data.layout.titleRect().toRect()
footerRect = self.__data.layout.footerRect().toRect()
- scaleRect = [None] * self.axisCnt
- for axisId in range(self.axisCnt):
- scaleRect[axisId] = self.__data.layout.scaleRect(axisId).toRect()
+ scaleRect = [
+ self.__data.layout.scaleRect(axisId).toRect() for axisId in self.AXES
+ ]
legendRect = self.__data.layout.legendRect().toRect()
canvasRect = self.__data.layout.canvasRect().toRect()
-
+
if self.__data.titleLabel.text():
self.__data.titleLabel.setGeometry(titleRect)
if not self.__data.titleLabel.isVisibleTo(self):
@@ -619,101 +1281,173 @@ def updateLayout(self):
self.__data.footerLabel.show()
else:
self.__data.footerLabel.hide()
-
- for axisId in range(self.axisCnt):
+
+ for axisId in self.AXES:
+ scaleWidget = self.axisWidget(axisId)
if self.axisEnabled(axisId):
- self.axisWidget(axisId).setGeometry(scaleRect[axisId])
-
- if axisId in (self.xBottom, self.xTop):
- r = QRegion(scaleRect[axisId])
- if self.axisEnabled(self.yLeft):
- r = r.subtracted(QRegion(scaleRect[self.yLeft]))
- if self.axisEnabled(self.yRight):
- r = r.subtracted(QRegion(scaleRect[self.yRight]))
- r.translate(-scaleRect[axisId].x(), -scaleRect[axisId].y())
-
- self.axisWidget(axisId).setMask(r)
-
- if not self.axisWidget(axisId).isVisibleTo(self):
- self.axisWidget(axisId).show()
-
+ if scaleRect[axisId] != scaleWidget.geometry():
+ scaleWidget.setGeometry(scaleRect[axisId])
+ startDist, endDist = scaleWidget.getBorderDistHint()
+ scaleWidget.setBorderDist(startDist, endDist)
+
+ # -------------------------------------------------------------
+ # XXX: The following was commented to fix issue #35
+ # Note: the same code part in Qwt's original source code is
+ # annotated with the mention "do we need this code any
+ # longer ???"... I guess not :)
+ # if axisId in (self.xBottom, self.xTop):
+ # r = QRegion(scaleRect[axisId])
+ # if self.axisEnabled(self.yLeft):
+ # r = r.subtracted(QRegion(scaleRect[self.yLeft]))
+ # if self.axisEnabled(self.yRight):
+ # r = r.subtracted(QRegion(scaleRect[self.yRight]))
+ # r.translate(-scaleRect[axisId].x(), -scaleRect[axisId].y())
+ # scaleWidget.setMask(r)
+ # -------------------------------------------------------------
+
+ if not scaleWidget.isVisibleTo(self):
+ scaleWidget.show()
else:
- self.axisWidget(axisId).hide()
-
+ scaleWidget.hide()
+
if self.__data.legend:
if self.__data.legend.isEmpty():
self.__data.legend.hide()
else:
self.__data.legend.setGeometry(legendRect)
self.__data.legend.show()
-
+
self.__data.canvas.setGeometry(canvasRect)
-
+
def getCanvasMarginsHint(self, maps, canvasRect):
- """Calculate the canvas margins
- (``margins`` is a list which is modified inplace)"""
- left = top = right = bottom = -1.
+ """
+ Calculate the canvas margins
+
+ :param list maps: `QwtPlot.axisCnt` maps, mapping between plot and paint device coordinates
+ :param QRectF canvasRect: Bounding rectangle where to paint
+
+ Plot items might indicate, that they need some extra space
+ at the borders of the canvas by the `QwtPlotItem.Margins` flag.
+
+ .. seealso::
+
+ :py:meth:`updateCanvasMargins()`, :py:meth:`getCanvasMarginHint()`
+ """
+ left = top = right = bottom = -1.0
for item in self.itemList():
if item.testItemAttribute(QwtPlotItem.Margins):
- m = item.getCanvasMarginHint(maps, canvasRect)
+ m = item.getCanvasMarginHint(
+ maps[item.xAxis()], maps[item.yAxis()], canvasRect
+ )
left = max([left, m[self.yLeft]])
top = max([top, m[self.xTop]])
right = max([right, m[self.yRight]])
bottom = max([bottom, m[self.xBottom]])
return left, top, right, bottom
-
+
def updateCanvasMargins(self):
- maps = [self.canvasMap(axisId) for axisId in range(self.axisCnt)]
+ """
+ Update the canvas margins
+
+ Plot items might indicate, that they need some extra space
+ at the borders of the canvas by the `QwtPlotItem.Margins` flag.
+
+ .. seealso::
+
+ :py:meth:`getCanvasMarginsHint()`,
+ :py:meth:`QwtPlotItem.getCanvasMarginHint()`
+ """
+ maps = [self.canvasMap(axisId) for axisId in self.AXES]
margins = self.getCanvasMarginsHint(maps, self.canvas().contentsRect())
-
+
doUpdate = False
-
- for axisId in range(self.axisCnt):
- if margins[axisId] >= 0.:
- m = np.ceil(margins[axisId])
+
+ for axisId in self.AXES:
+ if margins[axisId] >= 0.0:
+ m = math.ceil(margins[axisId])
self.plotLayout().setCanvasMargin(m, axisId)
doUpdate = True
-
+
if doUpdate:
self.updateLayout()
-
+
def drawCanvas(self, painter):
- maps = [self.canvasMap(axisId) for axisId in range(self.axisCnt)]
- self.drawItems(painter, self.__data.canvas.contentsRect(), maps)
-
+ """
+ Redraw the canvas.
+
+ :param QPainter painter: Painter used for drawing
+
+ .. warning::
+
+ drawCanvas calls drawItems what is also used
+ for printing. Applications that like to add individual
+ plot items better overload drawItems()
+
+ .. seealso::
+
+ :py:meth:`getCanvasMarginsHint()`,
+ :py:meth:`QwtPlotItem.getCanvasMarginHint()`
+ """
+ maps = [self.canvasMap(axisId) for axisId in self.AXES]
+ self.drawItems(painter, QRectF(self.__data.canvas.contentsRect()), maps)
+
def drawItems(self, painter, canvasRect, maps):
+ """
+ Redraw the canvas.
+
+ :param QPainter painter: Painter used for drawing
+ :param QRectF canvasRect: Bounding rectangle where to paint
+ :param list maps: `QwtPlot.axisCnt` maps, mapping between plot and paint device coordinates
+
+ .. note::
+
+ Usually canvasRect is `contentsRect()` of the plot canvas.
+ Due to a bug in Qt this rectangle might be wrong for certain
+ frame styles ( f.e `QFrame.Box` ) and it might be necessary to
+ fix the margins manually using `QWidget.setContentsMargins()`
+ """
for item in self.itemList():
if item and item.isVisible():
painter.save()
- painter.setRenderHint(QPainter.Antialiasing,
- item.testRenderHint(QwtPlotItem.RenderAntialiased))
- painter.setRenderHint(QPainter.HighQualityAntialiasing,
- item.testRenderHint(QwtPlotItem.RenderAntialiased))
- item.draw(painter, maps[item.xAxis()], maps[item.yAxis()],
- canvasRect)
+ painter.setRenderHint(
+ QPainter.Antialiasing,
+ item.testRenderHint(QwtPlotItem.RenderAntialiased),
+ )
+ item.draw(painter, maps[item.xAxis()], maps[item.yAxis()], canvasRect)
painter.restore()
def canvasMap(self, axisId):
+ """
+ :param int axisId: Axis
+ :return: Map for the axis on the canvas. With this map pixel coordinates can translated to plot coordinates and vice versa.
+
+ .. seealso::
+
+ :py:class:`qwt.scale_map.QwtScaleMap`,
+ :py:meth:`transform()`, :py:meth:`invTransform()`
+ """
map_ = QwtScaleMap()
if not self.__data.canvas:
return map_
-
+
map_.setTransformation(self.axisScaleEngine(axisId).transformation())
sd = self.axisScaleDiv(axisId)
+ if sd is None:
+ return map_
map_.setScaleInterval(sd.lowerBound(), sd.upperBound())
-
+
if self.axisEnabled(axisId):
s = self.axisWidget(axisId)
if axisId in (self.yLeft, self.yRight):
y = s.y() + s.startBorderDist() - self.__data.canvas.y()
h = s.height() - s.startBorderDist() - s.endBorderDist()
- map_.setPaintInterval(y+h, y)
+ map_.setPaintInterval(y + h, y)
else:
x = s.x() + s.startBorderDist() - self.__data.canvas.x()
w = s.width() - s.startBorderDist() - s.endBorderDist()
- map_.setPaintInterval(x, x+w)
+ map_.setPaintInterval(x, x + w)
else:
canvasRect = self.__data.canvas.contentsRect()
if axisId in (self.yLeft, self.yRight):
@@ -723,8 +1457,9 @@ def canvasMap(self, axisId):
bottom = 0
if not self.plotLayout().alignCanvasToScale(self.xBottom):
bottom = self.plotLayout().canvasMargin(self.xBottom)
- map_.setPaintInterval(canvasRect.bottom()-bottom,
- canvasRect.top()+top)
+ map_.setPaintInterval(
+ canvasRect.bottom() - bottom, canvasRect.top() + top
+ )
else:
left = 0
if not self.plotLayout().alignCanvasToScale(self.yLeft):
@@ -732,39 +1467,104 @@ def canvasMap(self, axisId):
right = 0
if not self.plotLayout().alignCanvasToScale(self.yRight):
right = self.plotLayout().canvasMargin(self.yRight)
- map_.setPaintInterval(canvasRect.left()+left,
- canvasRect.right()-right)
+ map_.setPaintInterval(
+ canvasRect.left() + left, canvasRect.right() - right
+ )
return map_
-
+
def setCanvasBackground(self, brush):
+ """
+ Change the background of the plotting area
+
+ Sets brush to `QPalette.Window` of all color groups of
+ the palette of the canvas. Using `canvas().setPalette()`
+ is a more powerful way to set these colors.
+
+ :param QBrush brush: New background brush
+
+ .. seealso::
+
+ :py:meth:`canvasBackground()`
+ """
pal = self.__data.canvas.palette()
- pal.setBrush(QPalette.Window, brush)
+ pal.setBrush(QPalette.Window, QBrush(brush))
self.canvas().setPalette(pal)
-
+
def canvasBackground(self):
- return self.canvas().palette().brush(QPalette.Normal, QPalette.Window)
-
- def axisValid(self, axisId):
- return axisId in range(QwtPlot.axisCnt)
-
+ """
+ :return: Background brush of the plotting area.
+
+ .. seealso::
+
+ :py:meth:`setCanvasBackground()`
+ """
+ return self.canvas().palette().brush(QPalette.Active, QPalette.Window)
+
+ def axisValid(self, axis_id):
+ """
+ :param int axis_id: Axis
+ :return: True if the specified axis exists, otherwise False
+ """
+ return axis_id in QwtPlot.AXES
+
def insertLegend(self, legend, pos=None, ratio=-1):
+ """
+ Insert a legend
+
+ If the position legend is `QwtPlot.LeftLegend` or `QwtPlot.RightLegend`
+ the legend will be organized in one column from top to down.
+ Otherwise the legend items will be placed in a table
+ with a best fit number of columns from left to right.
+
+ insertLegend() will set the plot widget as parent for the legend.
+ The legend will be deleted in the destructor of the plot or when
+ another legend is inserted.
+
+ Legends, that are not inserted into the layout of the plot widget
+ need to connect to the legendDataChanged() signal. Calling updateLegend()
+ initiates this signal for an initial update. When the application code
+ wants to implement its own layout this also needs to be done for
+ rendering plots to a document ( see QwtPlotRenderer ).
+
+ :param qwt.legend.QwtAbstractLegend legend: Legend
+ :param QwtPlot.LegendPosition pos: The legend's position.
+ :param float ratio: Ratio between legend and the bounding rectangle of title, canvas and axes
+
+ .. note::
+
+ For top/left position the number of columns will be limited to 1,
+ otherwise it will be set to unlimited.
+
+ .. note::
+
+ The legend will be shrunk if it would need more space than the
+ given ratio. The ratio is limited to ]0.0 .. 1.0].
+ In case of <= 0.0 it will be reset to the default ratio.
+ The default vertical/horizontal ratio is 0.33/0.5.
+
+ .. seealso::
+
+ :py:meth:`legend()`,
+ :py:meth:`qwt.plot_layout.QwtPlotLayout.legendPosition()`,
+ :py:meth:`qwt.plot_layout.QwtPlotLayout.setLegendPosition()`
+ """
if pos is None:
pos = self.RightLegend
self.__data.layout.setLegendPosition(pos, ratio)
if legend != self.__data.legend:
if self.__data.legend and self.__data.legend.parent() is self:
+ self.__data.legend.setParent(None)
del self.__data.legend
self.__data.legend = legend
if self.__data.legend:
- self.SIG_LEGEND_DATA_CHANGED.connect(
- self.__data.legend.updateLegend)
+ self.legendDataChanged.connect(self.__data.legend.updateLegend)
if self.__data.legend.parent() is not self:
self.__data.legend.setParent(self)
-
- qwtEnableLegendItems(self, False)
+
+ self.legendDataChanged.disconnect(self.updateLegendItems)
self.updateLegend()
- qwtEnableLegendItems(self, True)
-
+ self.legendDataChanged.connect(self.updateLegendItems)
+
lpos = self.__data.layout.legendPosition()
if legend is not None:
@@ -773,7 +1573,7 @@ def insertLegend(self, legend, pos=None, ratio=-1):
legend.setMaxColumns(1)
elif lpos in (self.TopLegend, self.BottomLegend):
legend.setMaxColumns(0)
-
+
previousInChain = None
if lpos == self.LeftLegend:
previousInChain = self.axisWidget(QwtPlot.xTop)
@@ -783,15 +1583,25 @@ def insertLegend(self, legend, pos=None, ratio=-1):
previousInChain = self.axisWidget(QwtPlot.yRight)
elif lpos == self.BottomLegend:
previousInChain = self.footerLabel()
-
- if previousInChain:
- qwtSetTabOrder(previousInChain, legend, True)
-
+
+ if previousInChain is not None:
+ qwtSetTabOrder(previousInChain, legend, True)
+
self.updateLayout()
-
+
def updateLegend(self, plotItem=None):
+ """
+ If plotItem is None, emit QwtPlot.legendDataChanged for all
+ plot item. Otherwise, emit the signal for passed plot item.
+
+ :param qwt.plot.QwtPlotItem plotItem: Plot item
+
+ .. seealso::
+
+ :py:meth:`QwtPlotItem.legendData()`, :py:data:`QwtPlot.legendDataChanged`
+ """
if plotItem is None:
- items = [plotItem for plotItem in self.itemList()]
+ items = list(self.itemList())
else:
items = [plotItem]
for plotItem in items:
@@ -800,124 +1610,276 @@ def updateLegend(self, plotItem=None):
legendData = []
if plotItem.testItemAttribute(QwtPlotItem.Legend):
legendData = plotItem.legendData()
- self.SIG_LEGEND_DATA_CHANGED.emit(plotItem, legendData)
+ self.legendDataChanged.emit(plotItem, legendData)
def updateLegendItems(self, plotItem, legendData):
+ """
+ Update all plot items interested in legend attributes
+
+ Call `QwtPlotItem.updateLegend()`, when the
+ `QwtPlotItem.LegendInterest` flag is set.
+
+ :param qwt.plot.QwtPlotItem plotItem: Plot item
+ :param list legendData: Entries to be displayed for the plot item ( usually 1 )
+
+ .. seealso::
+
+ :py:meth:`QwtPlotItem.LegendInterest()`,
+ :py:meth:`QwtPlotItem.updateLegend`
+ """
if plotItem is not None:
for item in self.itemList():
if item.testItemInterest(QwtPlotItem.LegendInterest):
item.updateLegend(plotItem, legendData)
-
+
def attachItem(self, plotItem, on):
+ """
+ Attach/Detach a plot item
+
+ :param qwt.plot.QwtPlotItem plotItem: Plot item
+ :param bool on: When true attach the item, otherwise detach it
+ """
if plotItem.testItemInterest(QwtPlotItem.LegendInterest):
for item in self.itemList():
legendData = []
if on and item.testItemAttribute(QwtPlotItem.Legend):
legendData = item.legendData()
plotItem.updateLegend(item, legendData)
-
+
if on:
self.insertItem(plotItem)
else:
self.removeItem(plotItem)
-
- self.SIG_ITEM_ATTACHED.emit(plotItem, on)
-
+
+ self.itemAttached.emit(plotItem, on)
+
if plotItem.testItemAttribute(QwtPlotItem.Legend):
if on:
self.updateLegend(plotItem)
else:
- self.SIG_LEGEND_DATA_CHANGED.emit(plotItem, [])
-
+ self.legendDataChanged.emit(plotItem, [])
+
self.autoRefresh()
-
+
def print_(self, printer):
+ """
+ Print plot to printer
+
+ :param printer: Printer
+ :type printer: QPaintDevice or QPrinter or QSvgGenerator
+ """
from qwt.plot_renderer import QwtPlotRenderer
+
renderer = QwtPlotRenderer(self)
renderer.renderTo(self, printer)
-
- def exportTo(self, filename, size=(800, 600), size_mm=None,
- resolution=72., format_=None):
+
+ def exportTo(
+ self, filename, size=(800, 600), size_mm=None, resolution=85, format_=None
+ ):
+ """
+ Export plot to PDF or image file (SVG, PNG, ...)
+
+ :param str filename: Filename
+ :param tuple size: (width, height) size in pixels
+ :param tuple size_mm: (width, height) size in millimeters
+ :param int resolution: Resolution in dots per Inch (dpi)
+ :param str format_: File format (PDF, SVG, PNG, ...)
+ """
if size_mm is None:
- size_mm = tuple(25.4*np.array(size)/resolution)
+ size_mm = tuple(25.4 * np.array(size) / resolution)
from qwt.plot_renderer import QwtPlotRenderer
+
renderer = QwtPlotRenderer(self)
renderer.renderDocument(self, filename, size_mm, resolution, format_)
-class QwtPlotItem_PrivateData(object):
+class QwtPlotItem_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.plot = None
self.isVisible = True
self.attributes = 0
self.interests = 0
self.renderHints = 0
- self.renderThreadCount = 1
- self.z = 0.
+ self.z = 0.0
self.xAxis = QwtPlot.xBottom
self.yAxis = QwtPlot.yLeft
self.legendIconSize = QSize(8, 8)
- self.title = None # QwtText
+ self.title = None # QwtText
class QwtPlotItem(object):
-
+ """
+ Base class for items on the plot canvas
+
+ A plot item is "something", that can be painted on the plot canvas,
+ or only affects the scales of the plot widget. They can be categorized as:
+
+ - Representator
+
+ A "Representator" is an item that represents some sort of data
+ on the plot canvas. The different representator classes are organized
+ according to the characteristics of the data:
+
+ - :py:class:`qwt.plot_marker.QwtPlotMarker`: Represents a point or a
+ horizontal/vertical coordinate
+ - :py:class:`qwt.plot_curve.QwtPlotCurve`: Represents a series of
+ points
+
+ - Decorators
+
+ A "Decorator" is an item, that displays additional information, that
+ is not related to any data:
+
+ - :py:class:`qwt.plot_grid.QwtPlotGrid`
+
+ Depending on the `QwtPlotItem.ItemAttribute` flags, an item is included
+ into autoscaling or has an entry on the legend.
+
+ Before misusing the existing item classes it might be better to
+ implement a new type of plot item
+ ( don't implement a watermark as spectrogram ).
+ Deriving a new type of `QwtPlotItem` primarily means to implement
+ the `YourPlotItem.draw()` method.
+
+ .. seealso::
+
+ The cpuplot example shows the implementation of additional plot items.
+
+ .. py:class:: QwtPlotItem([title=None])
+
+ Constructor
+
+ :param title: Title of the item
+ :type title: qwt.text.QwtText or str
+ """
+
# enum RttiValues
- (Rtti_PlotItem, Rtti_PlotGrid, Rtti_PlotScale, Rtti_PlotLegend,
- Rtti_PlotMarker, Rtti_PlotCurve, Rtti_PlotSpectroCurve,
- Rtti_PlotIntervalCurve, Rtti_PlotHistogram, Rtti_PlotSpectrogram,
- Rtti_PlotSVG, Rtti_PlotTradingCurve, Rtti_PlotBarChart,
- Rtti_PlotMultiBarChart, Rtti_PlotShape, Rtti_PlotTextLabel,
- Rtti_PlotZone) = list(range(17))
+ (
+ Rtti_PlotItem,
+ Rtti_PlotGrid,
+ Rtti_PlotScale,
+ Rtti_PlotLegend,
+ Rtti_PlotMarker,
+ Rtti_PlotCurve,
+ Rtti_PlotSpectroCurve,
+ Rtti_PlotIntervalCurve,
+ Rtti_PlotHistogram,
+ Rtti_PlotSpectrogram,
+ Rtti_PlotSVG,
+ Rtti_PlotTradingCurve,
+ Rtti_PlotBarChart,
+ Rtti_PlotMultiBarChart,
+ Rtti_PlotShape,
+ Rtti_PlotTextLabel,
+ Rtti_PlotZone,
+ ) = list(range(17))
Rtti_PlotUserItem = 1000
-
+
# enum ItemAttribute
Legend = 0x01
AutoScale = 0x02
Margins = 0x04
-
+
# enum ItemInterest
ScaleInterest = 0x01
LegendInterest = 0x02
-
+
# enum RenderHint
RenderAntialiased = 0x1
-
- def __init__(self, title=None):
+
+ def __init__(self, title=None, icon=None):
"""title: QwtText"""
if title is None:
title = QwtText("")
- if hasattr(title, 'capitalize'): # avoids dealing with Py3K compat.
+ if hasattr(title, "capitalize"): # avoids dealing with Py3K compat.
title = QwtText(title)
assert isinstance(title, QwtText)
self.__data = QwtPlotItem_PrivateData()
self.__data.title = title
+ self.__data.icon = icon
def attach(self, plot):
+ """
+ Attach the item to a plot.
+
+ This method will attach a `QwtPlotItem` to the `QwtPlot` argument.
+ It will first detach the `QwtPlotItem` from any plot from a previous
+ call to attach (if necessary). If a None argument is passed, it will
+ detach from any `QwtPlot` it was attached to.
+
+ :param qwt.plot.QwtPlot plot: Plot widget
+
+ .. seealso::
+
+ :py:meth:`detach()`
+ """
if plot is self.__data.plot:
return
-
+
if self.__data.plot:
self.__data.plot.attachItem(self, False)
-
+
self.__data.plot = plot
-
+
if self.__data.plot:
self.__data.plot.attachItem(self, True)
-
+
def detach(self):
+ """
+ Detach the item from a plot.
+
+ This method detaches a `QwtPlotItem` from any `QwtPlot` it has been
+ associated with.
+
+ .. seealso::
+
+ :py:meth:`attach()`
+ """
self.attach(None)
-
+
def rtti(self):
+ """
+ Return rtti for the specific class represented. `QwtPlotItem` is
+ simply a virtual interface class, and base classes will implement
+ this method with specific rtti values so a user can differentiate
+ them.
+
+ :return: rtti value
+ """
return self.Rtti_PlotItem
-
+
def plot(self):
+ """
+ :return: attached plot
+ """
return self.__data.plot
-
+
def z(self):
+ """
+ Plot items are painted in increasing z-order.
+
+ :return: item z order
+
+ .. seealso::
+
+ :py:meth:`setZ()`, :py:meth:`QwtPlotDict.itemList()`
+ """
return self.__data.z
-
+
def setZ(self, z):
+ """
+ Set the z value
+
+ Plot items are painted in increasing z-order.
+
+ :param float z: Z-value
+
+ .. seealso::
+
+ :py:meth:`z()`, :py:meth:`QwtPlotDict.itemList()`
+ """
if self.__data.z != z:
if self.__data.plot:
self.__data.plot.attachItem(self, False)
@@ -925,18 +1887,45 @@ def setZ(self, z):
if self.__data.plot:
self.__data.plot.attachItem(self, True)
self.itemChanged()
-
+
def setTitle(self, title):
+ """
+ Set a new title
+
+ :param title: Title
+ :type title: qwt.text.QwtText or str
+
+ .. seealso::
+
+ :py:meth:`title()`
+ """
if not isinstance(title, QwtText):
title = QwtText(title)
if self.__data.title != title:
self.__data.title = title
self.legendChanged()
-
+
def title(self):
+ """
+ :return: Title of the item
+
+ .. seealso::
+
+ :py:meth:`setTitle()`
+ """
return self.__data.title
-
+
def setItemAttribute(self, attribute, on=True):
+ """
+ Toggle an item attribute
+
+ :param int attribute: Attribute type
+ :param bool on: True/False
+
+ .. seealso::
+
+ :py:meth:`testItemAttribute()`
+ """
if bool(self.__data.attributes & attribute) != on:
if on:
self.__data.attributes |= attribute
@@ -945,81 +1934,191 @@ def setItemAttribute(self, attribute, on=True):
if attribute == QwtPlotItem.Legend:
self.legendChanged()
self.itemChanged()
-
+
def testItemAttribute(self, attribute):
+ """
+ Test an item attribute
+
+ :param int attribute: Attribute type
+ :return: True/False
+
+ .. seealso::
+
+ :py:meth:`setItemAttribute()`
+ """
return bool(self.__data.attributes & attribute)
-
- def setItemInterest(self, interest, on):
+
+ def setItemInterest(self, interest, on=True):
+ """
+ Toggle an item interest
+
+ :param int attribute: Interest type
+ :param bool on: True/False
+
+ .. seealso::
+
+ :py:meth:`testItemInterest()`
+ """
if bool(self.__data.interests & interest) != on:
if on:
self.__data.interests |= interest
else:
self.__data.interests &= ~interest
self.itemChanged()
-
+
def testItemInterest(self, interest):
+ """
+ Test an item interest
+
+ :param int attribute: Interest type
+ :return: True/False
+
+ .. seealso::
+
+ :py:meth:`setItemInterest()`
+ """
return bool(self.__data.interests & interest)
-
+
def setRenderHint(self, hint, on=True):
+ """
+ Toggle a render hint
+
+ :param int hint: Render hint
+ :param bool on: True/False
+
+ .. seealso::
+
+ :py:meth:`testRenderHint()`
+ """
if bool(self.__data.renderHints & hint) != on:
if on:
self.__data.renderHints |= hint
else:
self.__data.renderHints &= ~hint
self.itemChanged()
-
+
def testRenderHint(self, hint):
+ """
+ Test a render hint
+
+ :param int attribute: Render hint
+ :return: True/False
+
+ .. seealso::
+
+ :py:meth:`setRenderHint()`
+ """
return bool(self.__data.renderHints & hint)
-
- def setRenderThreadCount(self, numThreads):
- self.__data.renderThreadCount = numThreads
-
- def renderThreadCount(self):
- return self.__data.renderThreadCount
-
+
def setLegendIconSize(self, size):
+ """
+ Set the size of the legend icon
+
+ The default setting is 8x8 pixels
+
+ :param QSize size: Size
+
+ .. seealso::
+
+ :py:meth:`legendIconSize()`, :py:meth:`legendIcon()`
+ """
if self.__data.legendIconSize != size:
self.__data.legendIconSize = size
self.legendChanged()
-
+
def legendIconSize(self):
+ """
+ :return: Legend icon size
+
+ .. seealso::
+
+ :py:meth:`setLegendIconSize()`, :py:meth:`legendIcon()`
+ """
return self.__data.legendIconSize
-
+
def legendIcon(self, index, size):
+ """
+ :param int index: Index of the legend entry (usually there is only one)
+ :param QSizeF size: Icon size
+ :return: Icon representing the item on the legend
+
+ The default implementation returns an invalid icon
+
+ .. seealso::
+
+ :py:meth:`setLegendIconSize()`, :py:meth:`legendData()`
+ """
return QwtGraphic()
-
- def defaultIcon(brush, size):
- icon = QwtGraphic()
- if not size.isEmpty():
- icon.setDefaultSize(size)
- r = QRectF(0, 0, size.width(), size.height())
- painter = QPainter(icon)
- painter.fillRect(r, brush)
- return icon
-
+
def show(self):
+ """Show the item"""
self.setVisible(True)
-
+
def hide(self):
+ """Hide the item"""
self.setVisible(False)
-
+
def setVisible(self, on):
+ """
+ Show/Hide the item
+
+ :param bool on: Show if True, otherwise hide
+
+ .. seealso::
+
+ :py:meth:`isVisible()`, :py:meth:`show()`, :py:meth:`hide()`
+ """
if on != self.__data.isVisible:
self.__data.isVisible = on
self.itemChanged()
-
+
def isVisible(self):
+ """
+ :return: True if visible
+
+ .. seealso::
+
+ :py:meth:`setVisible()`, :py:meth:`show()`, :py:meth:`hide()`
+ """
return self.__data.isVisible
-
+
def itemChanged(self):
+ """
+ Update the legend and call `QwtPlot.autoRefresh()` for the
+ parent plot.
+
+ .. seealso::
+
+ :py:meth:`QwtPlot.legendChanged()`, :py:meth:`QwtPlot.autoRefresh()`
+ """
if self.__data.plot:
self.__data.plot.autoRefresh()
-
+
def legendChanged(self):
+ """
+ Update the legend of the parent plot.
+
+ .. seealso::
+
+ :py:meth:`QwtPlot.updateLegend()`, :py:meth:`itemChanged()`
+ """
if self.testItemAttribute(QwtPlotItem.Legend) and self.__data.plot:
self.__data.plot.updateLegend(self)
-
+
def setAxes(self, xAxis, yAxis):
+ """
+ Set X and Y axis
+
+ The item will painted according to the coordinates of its Axes.
+
+ :param int xAxis: X Axis (`QwtPlot.xBottom` or `QwtPlot.xTop`)
+ :param int yAxis: Y Axis (`QwtPlot.yLeft` or `QwtPlot.yRight`)
+
+ .. seealso::
+
+ :py:meth:`setXAxis()`, :py:meth:`setYAxis()`,
+ :py:meth:`xAxis()`, :py:meth:`yAxis()`
+ """
if xAxis == QwtPlot.xBottom or xAxis == QwtPlot.xTop:
self.__data.xAxis = xAxis
if yAxis == QwtPlot.yLeft or yAxis == QwtPlot.yRight:
@@ -1027,35 +2126,121 @@ def setAxes(self, xAxis, yAxis):
self.itemChanged()
def setAxis(self, xAxis, yAxis):
+ """
+ Set X and Y axis
+
+ .. warning::
+
+ `setAxis` has been removed in Qwt6: please use
+ :py:meth:`setAxes()` instead
+ """
import warnings
- warnings.warn("`setAxis` has been removed in Qwt6: "\
- "please use `setAxes` instead", RuntimeWarning)
+
+ warnings.warn(
+ "`setAxis` has been removed in Qwt6: please use `setAxes` instead",
+ RuntimeWarning,
+ )
self.setAxes(xAxis, yAxis)
-
+
def setXAxis(self, axis):
+ """
+ Set the X axis
+
+ The item will painted according to the coordinates its Axes.
+
+ :param int axis: X Axis (`QwtPlot.xBottom` or `QwtPlot.xTop`)
+
+ .. seealso::
+
+ :py:meth:`setAxes()`, :py:meth:`setYAxis()`,
+ :py:meth:`xAxis()`, :py:meth:`yAxis()`
+ """
if axis in (QwtPlot.xBottom, QwtPlot.xTop):
self.__data.xAxis = axis
self.itemChanged()
-
+
def setYAxis(self, axis):
+ """
+ Set the Y axis
+
+ The item will painted according to the coordinates its Axes.
+
+ :param int axis: Y Axis (`QwtPlot.yLeft` or `QwtPlot.yRight`)
+
+ .. seealso::
+
+ :py:meth:`setAxes()`, :py:meth:`setXAxis()`,
+ :py:meth:`xAxis()`, :py:meth:`yAxis()`
+ """
if axis in (QwtPlot.yLeft, QwtPlot.yRight):
self.__data.yAxis = axis
self.itemChanged()
def xAxis(self):
+ """
+ :return: xAxis
+ """
return self.__data.xAxis
-
+
def yAxis(self):
+ """
+ :return: yAxis
+ """
return self.__data.yAxis
-
+
def boundingRect(self):
+ """
+ :return: An invalid bounding rect: QRectF(1.0, 1.0, -2.0, -2.0)
+
+ .. note::
+
+ A width or height < 0.0 is ignored by the autoscaler
+ """
return QRectF(1.0, 1.0, -2.0, -2.0)
-
+
def getCanvasMarginHint(self, xMap, yMap, canvasRect):
- left = top = right = bottom = 0.
+ """
+ Calculate a hint for the canvas margin
+
+ When the QwtPlotItem::Margins flag is enabled the plot item
+ indicates, that it needs some margins at the borders of the canvas.
+ This is f.e. used by bar charts to reserve space for displaying
+ the bars.
+
+ The margins are in target device coordinates ( pixels on screen )
+
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates
+
+ .. seealso::
+
+ :py:meth:`QwtPlot.getCanvasMarginsHint()`,
+ :py:meth:`QwtPlot.updateCanvasMargins()`,
+ """
+ left = top = right = bottom = 0.0
return left, top, right, bottom
-
+
def legendData(self):
+ """
+ Return all information, that is needed to represent
+ the item on the legend
+
+ `QwtLegendData` is basically a list of QVariants that makes it
+ possible to overload and reimplement legendData() to
+ return almost any type of information, that is understood
+ by the receiver that acts as the legend.
+
+ The default implementation returns one entry with
+ the title() of the item and the legendIcon().
+
+ :return: Data, that is needed to represent the item on the legend
+
+ .. seealso::
+
+ :py:meth:`title()`, :py:meth:`legendIcon()`,
+ :py:class:`qwt.legend.QwtLegend`
+ """
data = QwtLegendData()
label = self.title()
label.setRenderFlags(label.renderFlags() & Qt.AlignLeft)
@@ -1064,12 +2249,44 @@ def legendData(self):
if not graphic.isNull():
data.setValue(QwtLegendData.IconRole, graphic)
return [data]
-
+
def updateLegend(self, item, data):
+ """
+ Update the item to changes of the legend info
+
+ Plot items that want to display a legend ( not those, that want to
+ be displayed on a legend ! ) will have to implement updateLegend().
+
+ updateLegend() is only called when the LegendInterest interest
+ is enabled. The default implementation does nothing.
+
+ :param qwt.plot.QwtPlotItem item: Plot item to be displayed on a legend
+ :param list data: Attributes how to display item on the legend
+
+ .. note::
+
+ Plot items, that want to be displayed on a legend
+ need to enable the `QwtPlotItem.Legend` flag and to implement
+ legendData() and legendIcon()
+ """
pass
def scaleRect(self, xMap, yMap):
+ """
+ Calculate the bounding scale rectangle of 2 maps
+
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :return: Bounding scale rect of the scale maps, not normalized
+ """
return QRectF(xMap.s1(), yMap.s1(), xMap.sDist(), yMap.sDist())
-
+
def paintRect(self, xMap, yMap):
+ """
+ Calculate the bounding paint rectangle of 2 maps
+
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :return: Bounding paint rectangle of the scale maps, not normalized
+ """
return QRectF(xMap.p1(), yMap.p1(), xMap.pDist(), yMap.pDist())
diff --git a/qwt/plot_canvas.py b/qwt/plot_canvas.py
index 1845377..a72d9b8 100644
--- a/qwt/plot_canvas.py
+++ b/qwt/plot_canvas.py
@@ -5,15 +5,35 @@
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)
+"""
+QwtPlotCanvas
+-------------
+
+.. autoclass:: QwtPlotCanvas
+ :members:
+"""
+
+from collections.abc import Sequence
+
+from qtpy.QtCore import QEvent, QObject, QPoint, QPointF, QRect, QRectF, QSize, Qt
+from qtpy.QtGui import (
+ QBrush,
+ QGradient,
+ QImage,
+ QPaintEngine,
+ QPainter,
+ QPainterPath,
+ QPen,
+ QPixmap,
+ QPolygonF,
+ QRegion,
+ qAlpha,
+)
+from qtpy.QtWidgets import QFrame, QStyle, QStyleOption, QStyleOptionFrame
+
from qwt.null_paintdevice import QwtNullPaintDevice
from qwt.painter import QwtPainter
-from qwt.qt import PYQT5
-from qwt.qt.QtGui import (QFrame, QPaintEngine, QPen, QBrush, QRegion, QImage,
- QPainterPath, QPixmap, QGradient, QPainter, qAlpha,
- QPolygonF, QStyleOption, QStyle, QStyleOptionFrame)
-from qwt.qt.QtCore import Qt, QSizeF, QT_VERSION, QEvent, QPointF, QRectF
-
class Border(object):
def __init__(self):
@@ -39,7 +59,7 @@ def __init__(self, size):
self.clipRects = []
self.border = Border()
self.background = Background()
-
+
def updateState(self, state):
if state.state() & QPaintEngine.DirtyPen:
self.__pen = state.pen()
@@ -47,13 +67,20 @@ def updateState(self, state):
self.__brush = state.brush()
if state.state() & QPaintEngine.DirtyBrushOrigin:
self.__origin = state.brushOrigin()
-
+
def drawRects(self, rects, count):
- for i in range(count):
- self.border.rectList += rects[i]
-
+ if isinstance(rects, (QRect, QRectF)):
+ self.border.rectList = [QRectF(rects)]
+ elif isinstance(rects, Sequence):
+ self.border.rectList.extend(QRectF(rects[i]) for i in range(count))
+ else:
+ raise TypeError(
+ "drawRects() expects a QRect, QRectF or a sequence of them, "
+ f"but got {type(rects).__name__}"
+ )
+
def drawPath(self, path):
- rect = QRectF(QPointF(0., 0.), self.__size)
+ rect = QRectF(QPointF(0.0, 0.0), self.__size)
if path.controlPointRect().contains(rect.center()):
self.setCornerRects(path)
self.alignCornerRects(rect)
@@ -62,13 +89,12 @@ def drawPath(self, path):
self.background.origin = self.__origin
else:
self.border.pathlist += [path]
-
+
def setCornerRects(self, path):
- pos = QPointF(0., 0.)
+ pos = QPointF(0.0, 0.0)
for i in range(path.elementCount()):
el = path.elementAt(i)
- if el.type in (QPainterPath.MoveToElement,
- QPainterPath.LineToElement):
+ if el.type in (QPainterPath.MoveToElement, QPainterPath.LineToElement):
pos.setX(el.x)
pos.setY(el.y)
elif el.type == QPainterPath.CurveToElement:
@@ -79,15 +105,17 @@ def setCornerRects(self, path):
elif el.type == QPainterPath.CurveToDataElement:
if self.clipRects:
r = self.clipRects[-1]
- r.setCoords(min([r.left(), el.x]),
- min([r.top(), el.y]),
- max([r.right(), el.x]),
- max([r.bottom(), el.y]))
+ r.setCoords(
+ min([r.left(), el.x]),
+ min([r.top(), el.y]),
+ max([r.right(), el.x]),
+ max([r.bottom(), el.y]),
+ )
self.clipRects[-1] = r.normalized()
-
+
def sizeMetrics(self):
return self.__size
-
+
def alignCornerRects(self, rect):
for r in self.clipRects:
if r.center().x() < rect.center().x():
@@ -100,14 +128,6 @@ def alignCornerRects(self, rect):
r.setBottom(rect.bottom())
-def _rects_conv_PyQt5(rects):
- # PyQt5 compatibility: the conversion from QRect to QRectF should not
- # be necessary but it seems to be anyway... PyQt5 bug?
- if PYQT5:
- return [QRectF(rect) for rect in rects]
- else:
- return rects
-
def qwtDrawBackground(painter, canvas):
painter.save()
borderClip = canvas.borderPath(canvas.rect())
@@ -123,7 +143,7 @@ def qwtDrawBackground(painter, canvas):
if brush.gradient().coordinateMode() == QGradient.ObjectBoundingMode:
rects += [canvas.rect()]
else:
- rects += [painter.clipRegion().rects()]
+ rects += [painter.clipRegion().boundingRect()]
useRaster = False
if painter.paintEngine().type() == QPaintEngine.X11:
useRaster = True
@@ -135,20 +155,22 @@ def qwtDrawBackground(painter, canvas):
format_ = QImage.Format_ARGB32
break
image = QImage(canvas.size(), format_)
- p = QPainter(image)
- p.setPen(Qt.NoPen)
- p.setBrush(brush)
- p.drawRects(_rects_conv_PyQt5(rects))
- p.end()
+ pntr = QPainter(image)
+ pntr.setPen(Qt.NoPen)
+ pntr.setBrush(brush)
+ for rect in rects:
+ pntr.drawRect(rect)
+ pntr.end()
painter.drawImage(0, 0, image)
else:
painter.setPen(Qt.NoPen)
painter.setBrush(brush)
- painter.drawRects(_rects_conv_PyQt5(rects))
+ for rect in rects:
+ painter.drawRect(rect)
else:
painter.setPen(Qt.NoPen)
painter.setBrush(brush)
- painter.drawRects(_rects_conv_PyQt5(painter.clipRegion().rects()))
+ painter.drawRect(painter.clipRegion().boundingRect())
painter.restore()
@@ -164,19 +186,19 @@ def qwtRevertPath(path):
def qwtCombinePathList(rect, pathList):
if not pathList:
return QPainterPath()
-
+
ordered = [None] * 8
for subPath in pathList:
index = -1
br = subPath.controlPointRect()
if br.center().x() < rect.center().x():
if br.center().y() < rect.center().y():
- if abs(br.top()-rect.top()) < abs(br.left()-rect.left()):
+ if abs(br.top() - rect.top()) < abs(br.left() - rect.left()):
index = 1
else:
index = 0
else:
- if abs(br.bottom()-rect.bottom) < abs(br.left()-rect.left()):
+ if abs(br.bottom() - rect.bottom) < abs(br.left() - rect.left()):
index = 6
else:
index = 7
@@ -184,12 +206,12 @@ def qwtCombinePathList(rect, pathList):
qwtRevertPath(subPath)
else:
if br.center().y() < rect.center().y():
- if abs(br.top()-rect.top()) < abs(br.right()-rect.right()):
+ if abs(br.top() - rect.top()) < abs(br.right() - rect.right()):
index = 2
else:
index = 3
else:
- if abs(br.bottom()-rect.bottom()) < abs(br.right()-rect.right()):
+ if abs(br.bottom() - rect.bottom()) < abs(br.right() - rect.right()):
index = 5
else:
index = 4
@@ -197,16 +219,16 @@ def qwtCombinePathList(rect, pathList):
qwtRevertPath(subPath)
ordered[index] = subPath
for i in range(4):
- if ordered[2*i].isEmpty() != ordered[2*i+1].isEmpty():
+ if ordered[2 * i].isEmpty() != ordered[2 * i + 1].isEmpty():
return QPainterPath()
corners = QPolygonF(rect)
path = QPainterPath()
for i in range(4):
- if ordered[2*i].isEmpty():
+ if ordered[2 * i].isEmpty():
path.lineTo(corners[i])
else:
- path.connectPath(ordered[2*i])
- path.connectPath(ordered[2*i+1])
+ path.connectPath(ordered[2 * i])
+ path.connectPath(ordered[2 * i + 1])
path.closeSubpath()
return path
@@ -253,18 +275,20 @@ def qwtFillBackground(*args):
else:
r = canvas.rect()
radius = canvas.borderRadius()
- if radius > 0.:
- sz = QSizeF(radius, radius)
- rects += [QRectF(r.topLeft(), sz),
- QRectF(r.topRight()-QPointF(radius, 0), sz),
- QRectF(r.bottomRight()-QPointF(radius, radius), sz),
- QRectF(r.bottomLeft()-QPointF(0, radius), sz)]
+ if radius > 0.0:
+ sz = QSize(radius, radius)
+ rects += [
+ QRect(r.topLeft(), sz),
+ QRect(r.topRight() - QPoint(radius, 0), sz),
+ QRect(r.bottomRight() - QPoint(radius, radius), sz),
+ QRect(r.bottomLeft() - QPoint(0, radius), sz),
+ ]
qwtFillBackground(painter, canvas, rects)
elif len(args) == 3:
painter, widget, fillRects = args
-
+
if not fillRects:
return
if painter.hasClipping():
@@ -272,16 +296,19 @@ def qwtFillBackground(*args):
else:
clipRegion = widget.contentsRect()
bgWidget = qwtBackgroundWidget(widget.parentWidget())
- for fillRect in fillRects:
- rect = fillRect.toAlignedRect()
+ for rect in fillRects:
if clipRegion.intersects(rect):
pm = QPixmap(rect.size())
- QwtPainter.fillPixmap(bgWidget, pm, widget.mapTo(bgWidget, rect.topLeft()))
+ QwtPainter.fillPixmap(
+ bgWidget, pm, widget.mapTo(bgWidget, rect.topLeft())
+ )
painter.drawPixmap(rect, pm)
-
+
else:
- raise TypeError("%s() takes 2 or 3 argument(s) (%s given)"\
- % ("qwtFillBackground", len(args)))
+ raise TypeError(
+ "%s() takes 2 or 3 argument(s) (%s given)"
+ % ("qwtFillBackground", len(args))
+ )
class StyleSheetBackground(object):
@@ -289,15 +316,19 @@ def __init__(self):
self.brush = QBrush()
self.origin = QPointF()
+
class StyleSheet(object):
def __init__(self):
self.hasBorder = False
self.borderPath = QPainterPath()
self.cornerRects = []
self.background = StyleSheetBackground()
-
-class QwtPlotCanvas_PrivateData(object):
+
+
+class QwtPlotCanvas_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.focusIndicator = QwtPlotCanvas.NoFocusIndicator
self.borderRadius = 0
self.paintAttributes = 0
@@ -307,20 +338,112 @@ def __init__(self):
class QwtPlotCanvas(QFrame):
-
+ """
+ Canvas of a QwtPlot.
+
+ Canvas is the widget where all plot items are displayed
+
+ .. seealso::
+
+ :py:meth:`qwt.plot.QwtPlot.setCanvas()`
+
+ Paint attributes:
+
+ * `QwtPlotCanvas.BackingStore`:
+
+ Paint double buffered reusing the content of the pixmap buffer
+ when possible.
+
+ Using a backing store might improve the performance significantly,
+ when working with widget overlays (like rubber bands).
+ Disabling the cache might improve the performance for
+ incremental paints
+ (using :py:class:`qwt.plot_directpainter.QwtPlotDirectPainter`).
+
+ * `QwtPlotCanvas.Opaque`:
+
+ Try to fill the complete contents rectangle of the plot canvas
+
+ When using styled backgrounds Qt assumes, that the canvas doesn't
+ fill its area completely (f.e because of rounded borders) and
+ fills the area below the canvas. When this is done with gradients
+ it might result in a serious performance bottleneck - depending on
+ the size.
+
+ When the Opaque attribute is enabled the canvas tries to
+ identify the gaps with some heuristics and to fill those only.
+
+ .. warning::
+
+ Will not work for semitransparent backgrounds
+
+ * `QwtPlotCanvas.HackStyledBackground`:
+
+ Try to improve painting of styled backgrounds
+
+ `QwtPlotCanvas` supports the box model attributes for
+ customizing the layout with style sheets. Unfortunately
+ the design of Qt style sheets has no concept how to
+ handle backgrounds with rounded corners - beside of padding.
+
+ When HackStyledBackground is enabled the plot canvas tries
+ to separate the background from the background border
+ by reverse engineering to paint the background before and
+ the border after the plot items. In this order the border
+ gets perfectly antialiased and you can avoid some pixel
+ artifacts in the corners.
+
+ * `QwtPlotCanvas.ImmediatePaint`:
+
+ When ImmediatePaint is set replot() calls repaint()
+ instead of update().
+
+ .. seealso::
+
+ :py:meth:`replot()`, :py:meth:`QWidget.repaint()`,
+ :py:meth:`QWidget.update()`
+
+ Focus indicators:
+
+ * `QwtPlotCanvas.NoFocusIndicator`:
+
+ Don't paint a focus indicator
+
+ * `QwtPlotCanvas.CanvasFocusIndicator`:
+
+ The focus is related to the complete canvas.
+ Paint the focus indicator using paintFocus()
+
+ * `QwtPlotCanvas.ItemFocusIndicator`:
+
+ The focus is related to an item (curve, point, ...) on
+ the canvas. It is up to the application to display a
+ focus indication using f.e. highlighting.
+
+ .. py:class:: QwtPlotCanvas([plot=None])
+
+ Constructor
+
+ :param qwt.plot.QwtPlot plot: Parent plot widget
+
+ .. seealso::
+
+ :py:meth:`qwt.plot.QwtPlot.setCanvas()`
+ """
+
# enum PaintAttribute
BackingStore = 1
Opaque = 2
HackStyledBackground = 4
ImmediatePaint = 8
-
+
# enum FocusIndicator
NoFocusIndicator, CanvasFocusIndicator, ItemFocusIndicator = list(range(3))
-
+
def __init__(self, plot=None):
super(QwtPlotCanvas, self).__init__(plot)
self.__plot = plot
- self.setFrameStyle(QFrame.Panel|QFrame.Sunken)
+ self.setFrameStyle(QFrame.Panel | QFrame.Sunken)
self.setLineWidth(2)
self.__data = QwtPlotCanvas_PrivateData()
self.setCursor(Qt.CrossCursor)
@@ -328,11 +451,31 @@ def __init__(self, plot=None):
self.setPaintAttribute(QwtPlotCanvas.BackingStore, False)
self.setPaintAttribute(QwtPlotCanvas.Opaque, True)
self.setPaintAttribute(QwtPlotCanvas.HackStyledBackground, True)
-
+
def plot(self):
+ """
+ :return: Parent plot widget
+ """
return self.__plot
-
+
def setPaintAttribute(self, attribute, on=True):
+ """
+ Changing the paint attributes
+
+ Paint attributes:
+
+ * `QwtPlotCanvas.BackingStore`
+ * `QwtPlotCanvas.Opaque`
+ * `QwtPlotCanvas.HackStyledBackground`
+ * `QwtPlotCanvas.ImmediatePaint`
+
+ :param int attribute: Paint attribute
+ :param bool on: On/Off
+
+ .. seealso::
+
+ :py:meth:`testPaintAttribute()`, :py:meth:`backingStore()`
+ """
if bool(self.__data.paintAttributes & attribute) == on:
return
if on:
@@ -344,14 +487,7 @@ def setPaintAttribute(self, attribute, on=True):
if self.__data.backingStore is None:
self.__data.backingStore = QPixmap()
if self.isVisible():
- if QT_VERSION >= 0x050000:
- self.__data.backingStore = self.grab(self.rect())
- else:
- if PYQT5:
- pm = QPixmap.grabWidget(self, self.rect())
- else:
- pm = self.grab(self.rect())
- self.__data.backingStore = pm
+ self.__data.backingStore = self.grab(self.rect())
else:
self.__data.backingStore = None
elif attribute == self.Opaque:
@@ -359,29 +495,81 @@ def setPaintAttribute(self, attribute, on=True):
self.setAttribute(Qt.WA_OpaquePaintEvent, True)
elif attribute in (self.HackStyledBackground, self.ImmediatePaint):
pass
-
+
def testPaintAttribute(self, attribute):
+ """
+ Test whether a paint attribute is enabled
+
+ :param int attribute: Paint attribute
+ :return: True, when attribute is enabled
+
+ .. seealso::
+
+ :py:meth:`setPaintAttribute()`
+ """
return self.__data.paintAttributes & attribute
-
+
def backingStore(self):
+ """
+ :return: Backing store, might be None
+ """
return self.__data.backingStore
-
+
def invalidateBackingStore(self):
+ """Invalidate the internal backing store"""
if self.__data.backingStore:
self.__data.backingStore = QPixmap()
-
+
def setFocusIndicator(self, focusIndicator):
+ """
+ Set the focus indicator
+
+ Focus indicators:
+
+ * `QwtPlotCanvas.NoFocusIndicator`
+ * `QwtPlotCanvas.CanvasFocusIndicator`
+ * `QwtPlotCanvas.ItemFocusIndicator`
+
+ :param int focusIndicator: Focus indicator
+
+ .. seealso::
+
+ :py:meth:`focusIndicator()`
+ """
self.__data.focusIndicator = focusIndicator
-
+
def focusIndicator(self):
+ """
+ :return: Focus indicator
+
+ .. seealso::
+
+ :py:meth:`setFocusIndicator()`
+ """
return self.__data.focusIndicator
-
+
def setBorderRadius(self, radius):
- self.__data.borderRadius = max([0., radius])
-
+ """
+ Set the radius for the corners of the border frame
+
+ :param float radius: Radius of a rounded corner
+
+ .. seealso::
+
+ :py:meth:`borderRadius()`
+ """
+ self.__data.borderRadius = max([0.0, radius])
+
def borderRadius(self):
+ """
+ :return: Radius for the corners of the border frame
+
+ .. seealso::
+
+ :py:meth:`setBorderRadius()`
+ """
return self.__data.borderRadius
-
+
def event(self, event):
if event.type() == QEvent.PolishRequest:
if self.testPaintAttribute(self.Opaque):
@@ -389,14 +577,18 @@ def event(self, event):
if event.type() in (QEvent.PolishRequest, QEvent.StyleChange):
self.updateStyleSheetInfo()
return QFrame.event(self, event)
-
+
def paintEvent(self, event):
painter = QPainter(self)
painter.setClipRegion(event.region())
- if self.testPaintAttribute(self.BackingStore) and\
- self.__data.backingStore is not None:
+ if (
+ self.testPaintAttribute(self.BackingStore)
+ and self.__data.backingStore is not None
+ and not self.__data.backingStore.isNull()
+ ):
bs = self.__data.backingStore
- if bs.size() != self.size():
+ pixelRatio = bs.devicePixelRatio()
+ if bs.size() != self.size() * pixelRatio:
bs = QwtPainter.backingStore(self, self.size())
if self.testAttribute(Qt.WA_StyledBackground):
p = QPainter(bs)
@@ -404,8 +596,8 @@ def paintEvent(self, event):
self.drawCanvas(p, True)
else:
p = QPainter()
- if self.__data.borderRadius <= 0.:
-# print('**DEBUG: QwtPlotCanvas.paintEvent')
+ if self.__data.borderRadius <= 0.0:
+ # print('**DEBUG: QwtPlotCanvas.paintEvent')
QwtPainter.fillPixmap(self, bs)
p.begin(bs)
self.drawCanvas(p, False)
@@ -430,7 +622,7 @@ def paintEvent(self, event):
qwtFillBackground(painter, self)
qwtDrawBackground(painter, self)
else:
- if self.borderRadius() > 0.:
+ if self.borderRadius() > 0.0:
clipPath = QPainterPath()
clipPath.addRect(self.rect())
clipPath = clipPath.subtracted(self.borderPath(self.rect()))
@@ -444,18 +636,36 @@ def paintEvent(self, event):
self.drawBorder(painter)
if self.hasFocus() and self.focusIndicator() == self.CanvasFocusIndicator:
self.drawFocusIndicator(painter)
-
+
def drawCanvas(self, painter, withBackground):
hackStyledBackground = False
- if withBackground and self.testAttribute(Qt.WA_StyledBackground) and\
- self.testPaintAttribute(self.HackStyledBackground):
- if self.__data.styleSheet.hasBorder and\
- not self.__data.styleSheet.borderPath.isEmpty():
+ if (
+ withBackground
+ and self.testAttribute(Qt.WA_StyledBackground)
+ and self.testPaintAttribute(self.HackStyledBackground)
+ ):
+ # Antialiasing rounded borders is done by
+ # inserting pixels with colors between the
+ # border color and the color on the canvas,
+ # When the border is painted before the plot items
+ # these colors are interpolated for the canvas
+ # and the plot items need to be clipped excluding
+ # the anialiased pixels. In situations, where
+ # the plot items fill the area at the rounded
+ # borders this is noticeable.
+ # The only way to avoid these annoying "artefacts"
+ # is to paint the border on top of the plot items.
+ if (
+ self.__data.styleSheet.hasBorder
+ and not self.__data.styleSheet.borderPath.isEmpty()
+ ):
+ # We have a border with at least one rounded corner
hackStyledBackground = True
if withBackground:
painter.save()
if self.testAttribute(Qt.WA_StyledBackground):
if hackStyledBackground:
+ # paint background without border
painter.setPen(Qt.NoPen)
painter.setBrush(self.__data.styleSheet.background.brush)
painter.setBrushOrigin(self.__data.styleSheet.background.origin)
@@ -466,7 +676,7 @@ def drawCanvas(self, painter, withBackground):
elif self.autoFillBackground():
painter.setPen(Qt.NoPen)
painter.setBrush(self.palette().brush(self.backgroundRole()))
- if self.__data.borderRadius > 0. and self.rect() == self.frameRect():
+ if self.__data.borderRadius > 0.0 and self.rect() == self.frameRect():
if self.frameWidth() > 0:
painter.setClipPath(self.borderPath(self.rect()))
painter.drawRect(self.rect())
@@ -478,79 +688,114 @@ def drawCanvas(self, painter, withBackground):
painter.restore()
painter.save()
if not self.__data.styleSheet.borderPath.isEmpty():
- painter.setClipPath(self.__data.styleSheet.borderPath,
- Qt.IntersectClip)
+ painter.setClipPath(self.__data.styleSheet.borderPath, Qt.IntersectClip)
else:
- if self.__data.borderRadius > 0.:
- painter.setClipPath(self.borderPath(self.frameRect()),
- Qt.IntersectClip)
+ if self.__data.borderRadius > 0.0:
+ painter.setClipPath(self.borderPath(self.frameRect()), Qt.IntersectClip)
else:
-# print('**DEBUG: QwtPlotCanvas.drawCanvas')
+ # print('**DEBUG: QwtPlotCanvas.drawCanvas')
painter.setClipRect(self.contentsRect(), Qt.IntersectClip)
self.plot().drawCanvas(painter)
painter.restore()
if withBackground and hackStyledBackground:
+ # Now paint the border on top
opt = QStyleOptionFrame()
opt.initFrom(self)
self.style().drawPrimitive(QStyle.PE_Frame, opt, painter, self)
-
+
def drawBorder(self, painter):
+ """
+ Draw the border of the plot canvas
+
+ :param QPainter painter: Painter
+
+ .. seealso::
+
+ :py:meth:`setBorderRadius()`
+ """
if self.__data.borderRadius > 0:
if self.frameWidth() > 0:
- QwtPainter.drawRoundedFrame(painter, QRectF(self.frameRect()),
- self.__data.borderRadius, self.__data.borderRadius,
- self.palette(), self.frameWidth(), self.frameStyle())
+ QwtPainter.drawRoundedFrame(
+ painter,
+ QRectF(self.frameRect()),
+ self.__data.borderRadius,
+ self.__data.borderRadius,
+ self.palette(),
+ self.frameWidth(),
+ self.frameStyle(),
+ )
else:
- if QT_VERSION >= 0x040500:
- if PYQT5:
- from qwt.qt.QtGui import QStyleOptionFrame
- else:
- from qwt.qt.QtGui import QStyleOptionFrameV3 as\
- QStyleOptionFrame
- opt = QStyleOptionFrame()
- opt.initFrom(self)
- frameShape = self.frameStyle() & QFrame.Shape_Mask
- frameShadow = self.frameStyle() & QFrame.Shadow_Mask
- opt.frameShape = QFrame.Shape(int(opt.frameShape)|frameShape)
- if frameShape in (QFrame.Box, QFrame.HLine, QFrame.VLine,
- QFrame.StyledPanel, QFrame.Panel):
- opt.lineWidth = self.lineWidth()
- opt.midLineWidth = self.midLineWidth()
- else:
- opt.lineWidth = self.frameWidth()
- if frameShadow == self.Sunken:
- opt.state |= QStyle.State_Sunken
- elif frameShadow == self.Raised:
- opt.state |= QStyle.State_Raised
- self.style().drawControl(QStyle.CE_ShapedFrame, opt, painter, self)
+ opt = QStyleOptionFrame()
+ opt.initFrom(self)
+ try:
+ shape_mask = QFrame.Shape_Mask.value
+ shadow_mask = QFrame.Shadow_Mask.value
+ except AttributeError:
+ shape_mask = QFrame.Shape_Mask
+ shadow_mask = QFrame.Shadow_Mask
+ frameShape = self.frameStyle() & shape_mask
+ frameShadow = self.frameStyle() & shadow_mask
+ opt.frameShape = QFrame.Shape(int(opt.frameShape) | frameShape)
+ if frameShape in (
+ QFrame.Box,
+ QFrame.HLine,
+ QFrame.VLine,
+ QFrame.StyledPanel,
+ QFrame.Panel,
+ ):
+ opt.lineWidth = self.lineWidth()
+ opt.midLineWidth = self.midLineWidth()
else:
- self.drawFrame(painter)
-
+ opt.lineWidth = self.frameWidth()
+ if frameShadow == QFrame.Sunken:
+ opt.state |= QStyle.State_Sunken
+ elif frameShadow == QFrame.Raised:
+ opt.state |= QStyle.State_Raised
+ self.style().drawControl(QStyle.CE_ShapedFrame, opt, painter, self)
+
def resizeEvent(self, event):
QFrame.resizeEvent(self, event)
self.updateStyleSheetInfo()
-
+
def drawFocusIndicator(self, painter):
+ """
+ Draw the focus indication
+
+ :param QPainter painter: Painter
+ """
margin = 1
focusRect = self.contentsRect()
- focusRect.setRect(focusRect.x()+margin, focusRect.y()+margin,
- focusRect.width()-2*margin, focusRect.height()-2*margin)
+ focusRect.setRect(
+ focusRect.x() + margin,
+ focusRect.y() + margin,
+ focusRect.width() - 2 * margin,
+ focusRect.height() - 2 * margin,
+ )
QwtPainter.drawFocusRect(painter, self, focusRect)
-
+
def replot(self):
+ """
+ Invalidate the paint cache and repaint the canvas
+ """
self.invalidateBackingStore()
if self.testPaintAttribute(self.ImmediatePaint):
self.repaint(self.contentsRect())
else:
self.update(self.contentsRect())
-
+
def invalidatePaintCache(self):
import warnings
- warnings.warn("`invalidatePaintCache` has been removed: "\
- "please use `replot` instead", RuntimeWarning)
+
+ warnings.warn(
+ "`invalidatePaintCache` has been removed: please use `replot` instead",
+ RuntimeWarning,
+ )
self.replot()
def updateStyleSheetInfo(self):
+ """
+ Update the cached information about the current style sheet
+ """
if not self.testAttribute(Qt.WA_StyledBackground):
return
recorder = QwtStyleSheetRecorder(self.size())
@@ -559,18 +804,28 @@ def updateStyleSheetInfo(self):
opt.initFrom(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
painter.end()
- self.__data.styleSheet.hasBorder = not recorder.border.rectList.isEmpty()
+ self.__data.styleSheet.hasBorder = len(recorder.border.rectList) > 0
self.__data.styleSheet.cornerRects = recorder.clipRects
if recorder.background.path.isEmpty():
- if not recorder.border.rectList.isEmpty():
- self.__data.styleSheet.borderPath =\
- qwtCombinePathList(self.rect(), recorder.border.pathlist)
+ if self.__data.styleSheet.hasBorder:
+ self.__data.styleSheet.borderPath = qwtCombinePathList(
+ self.rect(), recorder.border.pathlist
+ )
else:
self.__data.styleSheet.borderPath = recorder.background.path
self.__data.styleSheet.background.brush = recorder.background.brush
self.__data.styleSheet.background.origin = recorder.background.origin
-
+
def borderPath(self, rect):
+ """
+ Calculate the painter path for a styled or rounded border
+
+ When the canvas has no styled background or rounded borders
+ the painter path is empty.
+
+ :param QRect rect: Bounding rectangle of the canvas
+ :return: Painter path, that can be used for clipping
+ """
if self.testAttribute(Qt.WA_StyledBackground):
recorder = QwtStyleSheetRecorder(rect.size())
painter = QPainter(recorder)
@@ -581,13 +836,12 @@ def borderPath(self, rect):
painter.end()
if not recorder.background.path.isEmpty():
return recorder.background.path
- if not recorder.border.rectList.isEmpty():
+ if len(recorder.border.rectList) > 0:
return qwtCombinePathList(rect, recorder.border.pathlist)
- elif self.__data.borderRadius > 0.:
- fw2 = self.frameWidth()*.5
+ elif self.__data.borderRadius > 0.0:
+ fw2 = self.frameWidth() * 0.5
r = QRectF(rect).adjusted(fw2, fw2, -fw2, -fw2)
path = QPainterPath()
- path.addRoundedRect(r, self.__data.borderRadius,
- self.__data.borderRadius)
+ path.addRoundedRect(r, self.__data.borderRadius, self.__data.borderRadius)
return path
return QPainterPath()
diff --git a/qwt/plot_curve.py b/qwt/plot_curve.py
index 99e1b0b..6b4105e 100644
--- a/qwt/plot_curve.py
+++ b/qwt/plot_curve.py
@@ -5,89 +5,217 @@
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)
-from qwt.curve_fitter import QwtSplineCurveFitter
-from qwt.text import QwtText
-from qwt.plot import QwtPlotItem, QwtPlotItem_PrivateData
-from qwt.painter import QwtPainter
-from qwt.point_mapper import QwtPointMapper
-from qwt.clipper import QwtClipper
-from qwt.math import qwtSqr
+"""
+QwtPlotCurve
+------------
+
+.. autoclass:: QwtPlotCurve
+ :members:
+"""
+
+import math
+import os
+
+import numpy as np
+from qtpy.QtCore import QLineF, QPointF, QRectF, QSize, Qt
+from qtpy.QtGui import QBrush, QColor, QPainter, QPen, QPolygonF
+
+from qwt._math import qwtSqr
from qwt.graphic import QwtGraphic
-from qwt.series_data import QwtPointSeriesData, QwtSeriesData
-from qwt.series_store import QwtSeriesStore
-from qwt.plot_seriesitem import QwtPlotSeriesItem
-from qwt.point_data import QwtPointArrayData, QwtCPointerData
-from qwt.symbol import QwtSymbol
+from qwt.plot import QwtPlot, QwtPlotItem, QwtPlotItem_PrivateData
from qwt.plot_directpainter import QwtPlotDirectPainter
+from qwt.plot_series import (
+ QwtPlotSeriesItem,
+ QwtPointArrayData,
+ QwtSeriesData,
+ QwtSeriesStore,
+)
+from qwt.qthelpers import qcolor_from_str
+from qwt.symbol import QwtSymbol
+from qwt.text import QwtText
-from qwt.qt.QtGui import (QPen, QBrush, QPaintEngine, QPainter, QPolygonF,
- QColor)
-from qwt.qt.QtCore import QSize, Qt, QT_VERSION, QRectF, QPointF
+QT_API = os.environ["QT_API"]
-import numpy as np
+if QT_API == "pyside6":
+ import ctypes
+
+ import shiboken6 as shiboken
def qwtUpdateLegendIconSize(curve):
- if curve.symbol() and\
- curve.testLegendAttribute(QwtPlotCurve.LegendShowSymbol):
+ if curve.symbol() and curve.testLegendAttribute(QwtPlotCurve.LegendShowSymbol):
sz = curve.symbol().boundingRect().size()
sz += QSize(2, 2)
if curve.testLegendAttribute(QwtPlotCurve.LegendShowLine):
- w = np.ceil(1.5*sz.width())
+ w = math.ceil(1.5 * sz.width())
if w % 2:
w += 1
sz.setWidth(max([8, w]))
curve.setLegendIconSize(sz)
+
def qwtVerifyRange(size, i1, i2):
if size < 1:
return 0
- i1 = max([0, min([i1, size-1])])
- i2 = max([0, min([i2, size-1])])
+ i1 = max([0, min([i1, size - 1])])
+ i2 = max([0, min([i2, size - 1])])
if i1 > i2:
i1, i2 = i2, i1
- return i2-i1+1
+ return i2 - i1 + 1
+
+
+def array2d_to_qpolygonf(xdata, ydata):
+ """
+ Utility function to convert two 1D-NumPy arrays representing curve data
+ (X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
+ This feature is compatible with PyQt5 and PySide6 (requires QtPy).
+
+ License/copyright: MIT License © Pierre Raybaut 2020-2021.
+
+ :param numpy.ndarray xdata: 1D-NumPy array
+ :param numpy.ndarray ydata: 1D-NumPy array
+ :return: Polyline
+ :rtype: QtGui.QPolygonF
+ """
+ if not (xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]):
+ raise ValueError("Arguments must be 1D NumPy arrays with same size")
+ size = xdata.size
+ if QT_API.startswith("pyside"): # PySide (obviously...)
+ polyline = QPolygonF()
+ polyline.resize(size)
+ address = shiboken.getCppPointer(polyline.data())[0]
+ buffer = (ctypes.c_double * 2 * size).from_address(address)
+ else: # PyQt
+ if QT_API == "pyqt6":
+ polyline = QPolygonF([QPointF(0, 0)] * size)
+ else:
+ polyline = QPolygonF(size)
+ buffer = polyline.data()
+ buffer.setsize(16 * size) # 16 bytes per point: 8 bytes per X,Y value (float64)
+ memory = np.frombuffer(buffer, np.float64)
+ memory[: (size - 1) * 2 + 1 : 2] = np.asarray(xdata, dtype=np.float64)
+ memory[1 : (size - 1) * 2 + 2 : 2] = np.asarray(ydata, dtype=np.float64)
+ return polyline
+
+
+def series_to_polyline(xMap, yMap, series, from_, to):
+ """
+ Convert series data to QPolygon(F) polyline
+ """
+ xdata = xMap.transform(series.xData()[from_ : to + 1])
+ ydata = yMap.transform(series.yData()[from_ : to + 1])
+ return array2d_to_qpolygonf(xdata, ydata)
class QwtPlotCurve_PrivateData(QwtPlotItem_PrivateData):
def __init__(self):
QwtPlotItem_PrivateData.__init__(self)
self.style = QwtPlotCurve.Lines
- self.baseline = 0.
+ self.baseline = 0.0
self.symbol = None
self.attributes = 0
- self.paintAttributes = QwtPlotCurve.FilterPoints
- #TODO: uncomment next line when QwtClipper will be implemented
-# self.paintAttributes = QwtPlotCurve.ClipPolygons|QwtPlotCurve.FilterPoints
self.legendAttributes = QwtPlotCurve.LegendShowLine
self.pen = QPen(Qt.black)
self.brush = QBrush()
- self.curveFitter = QwtSplineCurveFitter()
-
+
class QwtPlotCurve(QwtPlotSeriesItem, QwtSeriesStore):
-
+ """
+ A plot item, that represents a series of points
+
+ A curve is the representation of a series of points in the x-y plane.
+ It supports different display styles and symbols.
+
+ .. seealso::
+
+ :py:class:`qwt.symbol.QwtSymbol()`,
+ :py:class:`qwt.scale_map.QwtScaleMap()`
+
+ Curve styles:
+
+ * `QwtPlotCurve.NoCurve`:
+
+ Don't draw a curve. Note: This doesn't affect the symbols.
+
+ * `QwtPlotCurve.Lines`:
+
+ Connect the points with straight lines.
+
+ * `QwtPlotCurve.Sticks`:
+
+ Draw vertical or horizontal sticks ( depending on the
+ orientation() ) from a baseline which is defined by setBaseline().
+
+ * `QwtPlotCurve.Steps`:
+
+ Connect the points with a step function. The step function
+ is drawn from the left to the right or vice versa,
+ depending on the QwtPlotCurve::Inverted attribute.
+
+ * `QwtPlotCurve.Dots`:
+
+ Draw dots at the locations of the data points. Note:
+ This is different from a dotted line (see setPen()), and faster
+ as a curve in QwtPlotCurve::NoStyle style and a symbol
+ painting a point.
+
+ * `QwtPlotCurve.UserCurve`:
+
+ Styles >= QwtPlotCurve.UserCurve are reserved for derived
+ classes of QwtPlotCurve that overload drawCurve() with
+ additional application specific curve types.
+
+ Curve attributes:
+
+ * `QwtPlotCurve.Inverted`:
+
+ For `QwtPlotCurve.Steps` only.
+ Draws a step function from the right to the left.
+
+ Legend attributes:
+
+ * `QwtPlotCurve.LegendNoAttribute`:
+
+ `QwtPlotCurve` tries to find a color representing the curve
+ and paints a rectangle with it.
+
+ * `QwtPlotCurve.LegendShowLine`:
+
+ If the style() is not `QwtPlotCurve.NoCurve` a line
+ is painted with the curve pen().
+
+ * `QwtPlotCurve.LegendShowSymbol`:
+
+ If the curve has a valid symbol it is painted.
+
+ * `QwtPlotCurve.LegendShowBrush`:
+
+ If the curve has a brush a rectangle filled with the
+ curve brush() is painted.
+
+
+ .. py:class:: QwtPlotCurve([title=None])
+
+ Constructor
+
+ :param title: Curve title
+ :type title: qwt.text.QwtText or str or None
+ """
+
# enum CurveStyle
NoCurve = -1
Lines, Sticks, Steps, Dots = list(range(4))
UserCurve = 100
-
+
# enum CurveAttribute
Inverted = 0x01
- Fitted = 0x02
-
+
# enum LegendAttribute
LegendNoAttribute = 0x00
LegendShowLine = 0x01
LegendShowSymbol = 0x02
LegendShowBrush = 0x04
-
- # enum PaintAttribute
- ClipPolygons = 0x01
- FilterPoints = 0x02
- MinimizeMemory = 0x04
- ImageBuffer = 0x08
-
+
def __init__(self, title=None):
if title is None:
title = QwtText("")
@@ -97,27 +225,115 @@ def __init__(self, title=None):
QwtPlotSeriesItem.__init__(self, title)
QwtSeriesStore.__init__(self)
self.init()
-
+
+ @classmethod
+ def make(
+ cls,
+ xdata=None,
+ ydata=None,
+ title=None,
+ plot=None,
+ z=None,
+ x_axis=None,
+ y_axis=None,
+ style=None,
+ symbol=None,
+ linecolor=None,
+ linewidth=None,
+ linestyle=None,
+ antialiased=False,
+ size=None,
+ finite=None,
+ ):
+ """
+ Create and setup a new `QwtPlotCurve` object (convenience function).
+
+ :param xdata: List/array of x values
+ :param ydata: List/array of y values
+ :param title: Curve title
+ :type title: qwt.text.QwtText or str or None
+ :param plot: Plot to attach the curve to
+ :type plot: qwt.plot.QwtPlot or None
+ :param z: Z-value
+ :type z: float or None
+ :param x_axis: curve X-axis (default: QwtPlot.yLeft)
+ :type x_axis: int or None
+ :param y_axis: curve Y-axis (default: QwtPlot.xBottom)
+ :type y_axis: int or None
+ :param style: curve style (`QwtPlotCurve.NoCurve`, `QwtPlotCurve.Lines`, `QwtPlotCurve.Sticks`, `QwtPlotCurve.Steps`, `QwtPlotCurve.Dots`, `QwtPlotCurve.UserCurve`)
+ :type style: int or None
+ :param symbol: curve symbol
+ :type symbol: qwt.symbol.QwtSymbol or None
+ :param linecolor: curve line color
+ :type linecolor: QColor or str or None
+ :param linewidth: curve line width
+ :type linewidth: float or None
+ :param linestyle: curve pen style
+ :type linestyle: Qt.PenStyle or None
+ :param bool antialiased: if True, enable antialiasing rendering
+ :param size: size of xData and yData
+ :type size: int or None
+ :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements
+
+ .. seealso::
+
+ :py:meth:`setData()`, :py:meth:`setPen()`, :py:meth:`attach()`
+ """
+ item = cls(title)
+ if z is not None:
+ item.setZ(z)
+ if xdata is not None or ydata is not None:
+ if xdata is None:
+ raise ValueError("Missing xdata parameter")
+ if ydata is None:
+ raise ValueError("Missing ydata parameter")
+ item.setData(xdata, ydata, size=size, finite=finite)
+ x_axis = QwtPlot.xBottom if x_axis is None else x_axis
+ y_axis = QwtPlot.yLeft if y_axis is None else y_axis
+ item.setAxes(x_axis, y_axis)
+ if style is not None:
+ item.setStyle(style)
+ if symbol is not None:
+ item.setSymbol(symbol)
+ linecolor = qcolor_from_str(linecolor, Qt.black)
+ linewidth = 1.0 if linewidth is None else linewidth
+ linestyle = Qt.SolidLine if linestyle is None else linestyle
+ item.setPen(QPen(linecolor, linewidth, linestyle))
+ item.setRenderHint(cls.RenderAntialiased, antialiased)
+ if plot is not None:
+ item.attach(plot)
+ return item
+
def init(self):
+ """Initialize internal members"""
self.__data = QwtPlotCurve_PrivateData()
self.setItemAttribute(QwtPlotItem.Legend)
self.setItemAttribute(QwtPlotItem.AutoScale)
- self.setData(QwtPointSeriesData())
- self.setZ(20.)
-
+ self.setData(QwtPointArrayData())
+ self.setZ(20.0)
+
def rtti(self):
+ """:return: `QwtPlotItem.Rtti_PlotCurve`"""
return QwtPlotItem.Rtti_PlotCurve
-
- def setPaintAttribute(self, attribute, on=True):
- if on:
- self.__data.paintAttributes |= attribute
- else:
- self.__data.paintAttributes &= ~attribute
-
- def testPaintAttribute(self, attribute):
- return self.__data.paintAttributes & attribute
-
+
def setLegendAttribute(self, attribute, on=True):
+ """
+ Specify an attribute how to draw the legend icon
+
+ Legend attributes:
+
+ * `QwtPlotCurve.LegendNoAttribute`
+ * `QwtPlotCurve.LegendShowLine`
+ * `QwtPlotCurve.LegendShowSymbol`
+ * `QwtPlotCurve.LegendShowBrush`
+
+ :param int attribute: Legend attribute
+ :param bool on: On/Off
+
+ .. seealso::
+
+ :py:meth:`testLegendAttribute()`, :py:meth:`legendIcon()`
+ """
if on != self.testLegendAttribute(attribute):
if on:
self.__data.legendAttributes |= attribute
@@ -125,37 +341,120 @@ def setLegendAttribute(self, attribute, on=True):
self.__data.legendAttributes &= ~attribute
qwtUpdateLegendIconSize(self)
self.legendChanged()
-
+
def testLegendAttribute(self, attribute):
+ """
+ :param int attribute: Legend attribute
+ :return: True, when attribute is enabled
+
+ .. seealso::
+
+ :py:meth:`setLegendAttribute()`
+ """
return self.__data.legendAttributes & attribute
-
+
def setStyle(self, style):
+ """
+ Set the curve's drawing style
+
+ Valid curve styles:
+
+ * `QwtPlotCurve.NoCurve`
+ * `QwtPlotCurve.Lines`
+ * `QwtPlotCurve.Sticks`
+ * `QwtPlotCurve.Steps`
+ * `QwtPlotCurve.Dots`
+ * `QwtPlotCurve.UserCurve`
+
+ :param int style: Curve style
+
+ .. seealso::
+
+ :py:meth:`style()`
+ """
if style != self.__data.style:
self.__data.style = style
self.legendChanged()
self.itemChanged()
-
+
def style(self):
+ """
+ :return: Style of the curve
+
+ .. seealso::
+
+ :py:meth:`setStyle()`
+ """
return self.__data.style
-
+
def setSymbol(self, symbol):
+ """
+ Assign a symbol
+
+ The curve will take the ownership of the symbol, hence the previously
+ set symbol will be delete by setting a new one. If symbol is None no
+ symbol will be drawn.
+
+ :param qwt.symbol.QwtSymbol symbol: Symbol
+
+ .. seealso::
+
+ :py:meth:`symbol()`
+ """
if symbol != self.__data.symbol:
self.__data.symbol = symbol
qwtUpdateLegendIconSize(self)
self.legendChanged()
self.itemChanged()
-
+
def symbol(self):
+ """
+ :return: Current symbol or None, when no symbol has been assigned
+
+ .. seealso::
+
+ :py:meth:`setSymbol()`
+ """
return self.__data.symbol
-
+
def setPen(self, *args):
+ """
+ Build and/or assign a pen, depending on the arguments.
+
+ .. py:method:: setPen(color, width, style)
+ :noindex:
+
+ Build and assign a pen
+
+ In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ been introduced to hide this incompatibility.
+
+ :param QColor color: Pen color
+ :param float width: Pen width
+ :param Qt.PenStyle style: Pen style
+
+ .. py:method:: setPen(pen)
+ :noindex:
+
+ Assign a pen
+
+ :param QPen pen: New pen
+
+ .. seealso::
+
+ :py:meth:`pen()`, :py:meth:`brush()`
+ """
if len(args) == 3:
color, width, style = args
+ pen = QPen(color, width, style)
elif len(args) == 1:
- pen, = args
+ (pen,) = args
else:
- raise TypeError("%s().setPen() takes 1 or 3 argument(s) (%s given)"\
- % (self.__class__.__name__, len(args)))
+ raise TypeError(
+ "%s().setPen() takes 1 or 3 argument(s) (%s given)"
+ % (self.__class__.__name__, len(args))
+ )
if pen != self.__data.pen:
if isinstance(pen, QColor):
pen = QPen(pen)
@@ -164,11 +463,37 @@ def setPen(self, *args):
self.__data.pen = pen
self.legendChanged()
self.itemChanged()
-
+
def pen(self):
+ """
+ :return: Pen used to draw the lines
+
+ .. seealso::
+
+ :py:meth:`setPen()`, :py:meth:`brush()`
+ """
return self.__data.pen
-
+
def setBrush(self, brush):
+ """
+ Assign a brush.
+
+ In case of `brush.style() != QBrush.NoBrush`
+ and `style() != QwtPlotCurve.Sticks`
+ the area between the curve and the baseline will be filled.
+
+ In case `not brush.color().isValid()` the area will be filled by
+ `pen.color()`. The fill algorithm simply connects the first and the
+ last curve point to the baseline. So the curve data has to be sorted
+ (ascending or descending).
+
+ :param brush: New brush
+ :type brush: QBrush or QColor
+
+ .. seealso::
+
+ :py:meth:`brush()`, :py:meth:`setBaseline()`, :py:meth:`baseline()`
+ """
if isinstance(brush, QColor):
brush = QBrush(brush)
else:
@@ -177,45 +502,90 @@ def setBrush(self, brush):
self.__data.brush = brush
self.legendChanged()
self.itemChanged()
-
+
def brush(self):
+ """
+ :return: Brush used to fill the area between lines and the baseline
+
+ .. seealso::
+
+ :py:meth:`setBrush()`, :py:meth:`setBaseline()`,
+ :py:meth:`baseline()`
+ """
return self.__data.brush
-
+
def directPaint(self, from_, to):
- """When observing an measurement while it is running, new points have
- to be added to an existing seriesItem. drawSeries() can be used to
+ """
+ When observing a measurement while it is running, new points have
+ to be added to an existing seriesItem. This method can be used to
display them avoiding a complete redraw of the canvas.
- Setting plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)
- will result in faster painting, if the paint engine of the canvas widget
- supports this feature."""
+ Setting `plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)`
+ will result in faster painting, if the paint engine of the canvas
+ widget supports this feature.
+
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted
+
+ .. seealso::
+
+ :py:meth:`drawSeries()`
+ """
directPainter = QwtPlotDirectPainter(self.plot())
directPainter.drawSeries(self, from_, to)
-
+
def drawSeries(self, painter, xMap, yMap, canvasRect, from_, to):
+ """
+ Draw an interval of the curve
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
+
+ .. seealso::
+
+ :py:meth:`drawCurve()`, :py:meth:`drawSymbols()`
+ """
numSamples = self.dataSize()
if not painter or numSamples <= 0:
return
if to < 0:
- to = numSamples-1
+ to = numSamples - 1
if qwtVerifyRange(numSamples, from_, to) > 0:
painter.save()
painter.setPen(self.__data.pen)
- self.drawCurve(painter, self.__data.style, xMap, yMap, canvasRect,
- from_, to)
+ self.drawCurve(
+ painter, self.__data.style, xMap, yMap, canvasRect, from_, to
+ )
painter.restore()
- if self.__data.symbol and\
- self.__data.symbol.style() != QwtSymbol.NoSymbol:
+ if self.__data.symbol and self.__data.symbol.style() != QwtSymbol.NoSymbol:
painter.save()
- self.drawSymbols(painter, self.__data.symbol,
- xMap, yMap, canvasRect, from_, to)
+ self.drawSymbols(
+ painter, self.__data.symbol, xMap, yMap, canvasRect, from_, to
+ )
painter.restore()
-
+
def drawCurve(self, painter, style, xMap, yMap, canvasRect, from_, to):
+ """
+ Draw the line part (without symbols) of a curve interval.
+
+ :param QPainter painter: Painter
+ :param int style: curve style, see `QwtPlotCurve.CurveStyle`
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
+
+ .. seealso::
+
+ :py:meth:`draw()`, :py:meth:`drawDots()`, :py:meth:`drawLines()`,
+ :py:meth:`drawSteps()`, :py:meth:`drawSticks()`
+ """
if style == self.Lines:
- if self.testCurveAttribute(self.Fitted):
- from_ = 0
- to = self.dataSize()-1
self.drawLines(painter, xMap, yMap, canvasRect, from_, to)
elif style == self.Sticks:
self.drawSticks(painter, xMap, yMap, canvasRect, from_, to)
@@ -223,155 +593,151 @@ def drawCurve(self, painter, style, xMap, yMap, canvasRect, from_, to):
self.drawSteps(painter, xMap, yMap, canvasRect, from_, to)
elif style == self.Dots:
self.drawDots(painter, xMap, yMap, canvasRect, from_, to)
-
+
def drawLines(self, painter, xMap, yMap, canvasRect, from_, to):
+ """
+ Draw lines
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
+
+ .. seealso::
+
+ :py:meth:`draw()`, :py:meth:`drawDots()`,
+ :py:meth:`drawSteps()`, :py:meth:`drawSticks()`
+ """
if from_ > to:
return
- doAlign = QwtPainter.roundingAlignment(painter)
- doFit = (self.__data.attributes & self.Fitted)\
- and self.__data.curveFitter
- doFill = self.__data.brush.style() != Qt.NoBrush\
- and self.__data.brush.color().alpha() > 0
- clipRect = QRectF()
- if self.__data.paintAttributes & self.ClipPolygons:
- pw = max([1., painter.pen().widthF()])
- clipRect = canvasRect.adjusted(-pw, -pw, pw, pw)
- doIntegers = False
- if QT_VERSION < 0x040800:
- if painter.paintEngine().type() == QPaintEngine.Raster:
- if not doFit and not doFill:
- doIntegers = True
- noDuplicates = self.__data.paintAttributes & self.FilterPoints
- mapper = QwtPointMapper()
- mapper.setFlag(QwtPointMapper.RoundPoints, doAlign)
- mapper.setFlag(QwtPointMapper.WeedOutPoints, noDuplicates)
- mapper.setBoundingRect(canvasRect)
- if doIntegers:
- polyline = mapper.toPolygon(xMap, yMap, self.data(), from_, to)
- if self.__data.paintAttributes & self.ClipPolygons:
- polyline = QwtClipper().clipPolygon(clipRect.toAlignedRect(),
- polyline, False)
- QwtPainter.drawPolyline(painter, polyline)
- else:
- polyline = mapper.toPolygonF(xMap, yMap, self.data(), from_, to)
- if doFit:
- polyline = self.__data.curveFitter.fitCurve(polyline)
- if doFill:
- if painter.pen().style() != Qt.NoPen:
- filled = QPolygonF(polyline)
- self.fillCurve(painter, xMap, yMap, canvasRect, filled)
- filled.clear()
- if self.__data.paintAttributes & self.ClipPolygons:
- polyline = QwtClipper().clipPolygonF(clipRect,
- polyline, False)
- QwtPainter.drawPolyline(painter, polyline)
- else:
- self.fillCurve(painter, xMap, yMap, canvasRect, polyline)
- else:
- if self.__data.paintAttributes & self.ClipPolygons:
- polyline = QwtClipper().clipPolygonF(clipRect, polyline,
- False)
- QwtPainter.drawPolyline(painter, polyline)
-
+ doFill = (
+ self.__data.brush.style() != Qt.NoBrush
+ and self.__data.brush.color().alpha() > 0
+ )
+ polyline = series_to_polyline(xMap, yMap, self.data(), from_, to)
+ painter.drawPolyline(polyline)
+ if doFill:
+ self.fillCurve(painter, xMap, yMap, canvasRect, polyline)
+
def drawSticks(self, painter, xMap, yMap, canvasRect, from_, to):
+ """
+ Draw sticks
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
+
+ .. seealso::
+
+ :py:meth:`draw()`, :py:meth:`drawDots()`,
+ :py:meth:`drawSteps()`, :py:meth:`drawLines()`
+ """
painter.save()
painter.setRenderHint(QPainter.Antialiasing, False)
- doAlign = QwtPainter.roundingAlignment(painter)
x0 = xMap.transform(self.__data.baseline)
y0 = yMap.transform(self.__data.baseline)
- if doAlign:
- x0 = round(x0)
- y0 = round(y0)
o = self.orientation()
series = self.data()
- for i in range(from_, to+1):
+ for i in range(from_, to + 1):
sample = series.sample(i)
xi = xMap.transform(sample.x())
yi = yMap.transform(sample.y())
- if doAlign:
- xi = round(xi)
- yi = round(yi)
if o == Qt.Horizontal:
- QwtPainter.drawLine(painter, x0, yi, xi, yi)
+ painter.drawLine(QLineF(xi, y0, xi, yi))
else:
- QwtPainter.drawLine(painter, xi, y0, xi, yi)
+ painter.drawLine(QLineF(x0, yi, xi, yi))
painter.restore()
-
+
def drawDots(self, painter, xMap, yMap, canvasRect, from_, to):
- color = painter.pen().color()
- if painter.pen().style() == Qt.NoPen or color.alpha() == 0:
- return
- doFill = self.__data.brush.style() != Qt.NoBrush\
- and self.__data.brush.color().alpha() > 0
- doAlign = QwtPainter.roundingAlignment(painter)
- mapper = QwtPointMapper()
- mapper.setBoundingRect(canvasRect)
- mapper.setFlag(QwtPointMapper.RoundPoints, doAlign)
- if self.__data.paintAttributes & self.FilterPoints:
- if color.alpha() == 255\
- and not (painter.renderHints() & QPainter.Antialiasing):
- mapper.setFlag(QwtPointMapper.WeedOutPoints, True)
+ """
+ Draw dots
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
+
+ .. seealso::
+
+ :py:meth:`draw()`, :py:meth:`drawSticks()`,
+ :py:meth:`drawSteps()`, :py:meth:`drawLines()`
+ """
+ doFill = (
+ self.__data.brush.style() != Qt.NoBrush
+ and self.__data.brush.color().alpha() > 0
+ )
+ polyline = series_to_polyline(xMap, yMap, self.data(), from_, to)
+ painter.drawPoints(polyline)
if doFill:
- mapper.setFlag(QwtPointMapper.WeedOutPoints, False)
- points = mapper.toPointsF(xMap, yMap, self.data(), from_, to)
- QwtPainter.drawPoints(painter, points)
- self.fillCurve(painter, xMap, yMap, canvasRect, points)
- elif self.__data.paintAttributes & self.ImageBuffer:
- image = mapper.toImage(xMap, yMap, self.data(), from_, to,
- self.__data.pen,
- painter.testRenderHint(QPainter.Antialiasing),
- self.renderThreadCount())
- painter.drawImage(canvasRect.toAlignedRect(), image)
- elif self.__data.paintAttributes & self.MinimizeMemory:
- series = self.data()
- for i in range(from_, to+1):
- sample = series.sample(i)
- xi = xMap.transform(sample.x())
- yi = yMap.transform(sample.y())
- if doAlign:
- xi = round(xi)
- yi = round(yi)
- QwtPainter.drawPoint(painter, QPointF(xi, yi))
- else:
- if doAlign:
- points = mapper.toPoints(xMap, yMap, self.data(), from_, to)
- QwtPainter.drawPoints(painter, points)
- else:
- points = mapper.toPointsF(xMap, yMap, self.data(), from_, to)
- QwtPainter.drawPoints(painter, points)
-
+ self.fillCurve(painter, xMap, yMap, canvasRect, polyline)
+
def drawSteps(self, painter, xMap, yMap, canvasRect, from_, to):
- doAlign = QwtPainter.roundingAlignment(painter)
- polygon = QPolygonF(2*(to-from_)+1)
+ """
+ Draw steps
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
+
+ .. seealso::
+
+ :py:meth:`draw()`, :py:meth:`drawSticks()`,
+ :py:meth:`drawDots()`, :py:meth:`drawLines()`
+ """
+ size = 2 * (to - from_) + 1
+ if QT_API == "pyside6":
+ polygon = QPolygonF()
+ polygon.resize(size)
+ elif QT_API == "pyqt6":
+ polygon = QPolygonF([QPointF(0, 0)] * size)
+ else:
+ polygon = QPolygonF(size)
inverted = self.orientation() == Qt.Vertical
if self.__data.attributes & self.Inverted:
inverted = not inverted
series = self.data()
ip = 0
- for i in range(from_, to+1):
+ for i in range(from_, to + 1):
sample = series.sample(i)
xi = xMap.transform(sample.x())
yi = yMap.transform(sample.y())
- if doAlign:
- xi = round(xi)
- yi = round(yi)
if ip > 0:
- p0 = polygon[ip-2]
+ p0 = polygon[ip - 2]
if inverted:
- polygon[ip-1] = QPointF(p0.x(), yi)
+ polygon[ip - 1] = QPointF(p0.x(), yi)
else:
- polygon[ip-1] = QPointF(xi, p0.y())
+ polygon[ip - 1] = QPointF(xi, p0.y())
polygon[ip] = QPointF(xi, yi)
ip += 2
- if self.__data.paintAttributes & self.ClipPolygons:
- clipped = QwtClipper().clipPolygonF(canvasRect, polygon, False)
- QwtPainter.drawPolyline(painter, clipped)
- else:
- QwtPainter.drawPolyline(painter, polygon)
+ painter.drawPolyline(polygon)
if self.__data.brush.style() != Qt.NoBrush:
self.fillCurve(painter, xMap, yMap, canvasRect, polygon)
-
+
def setCurveAttribute(self, attribute, on=True):
+ """
+ Specify an attribute for drawing the curve
+
+ Supported curve attributes:
+
+ * `QwtPlotCurve.Inverted`
+
+ :param int attribute: Curve attribute
+ :param bool on: On/Off
+
+ .. seealso::
+
+ :py:meth:`testCurveAttribute()`
+ """
if (self.__data.attributes & attribute) == on:
return
if on:
@@ -379,18 +745,33 @@ def setCurveAttribute(self, attribute, on=True):
else:
self.__data.attributes &= ~attribute
self.itemChanged()
-
+
def testCurveAttribute(self, attribute):
+ """
+ :return: True, if attribute is enabled
+
+ .. seealso::
+
+ :py:meth:`setCurveAttribute()`
+ """
return self.__data.attributes & attribute
-
- def setCurveFitter(self, curveFitter):
- self.__data.curveFitter = curveFitter
- self.itemChanged()
-
- def curveFitter(self):
- return self.__data.curveFitter
-
+
def fillCurve(self, painter, xMap, yMap, canvasRect, polygon):
+ """
+ Fill the area between the curve and the baseline with
+ the curve brush
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ :param QPolygonF polygon: Polygon - will be modified !
+
+ .. seealso::
+
+ :py:meth:`setBrush()`, :py:meth:`setBaseline()`,
+ :py:meth:`setStyle()`
+ """
if self.__data.brush.style() == Qt.NoBrush:
return
self.closePolyline(painter, xMap, yMap, polygon)
@@ -399,59 +780,113 @@ def fillCurve(self, painter, xMap, yMap, canvasRect, polygon):
brush = self.__data.brush
if not brush.color().isValid():
brush.setColor(self.__data.pen.color())
- if self.__data.paintAttributes & self.ClipPolygons:
- polygon = QwtClipper().clipPolygonF(canvasRect, polygon, True)
painter.save()
painter.setPen(Qt.NoPen)
painter.setBrush(brush)
- QwtPainter.drawPolygon(painter, polygon)
+ painter.drawPolygon(polygon)
painter.restore()
-
+
def closePolyline(self, painter, xMap, yMap, polygon):
+ """
+ Complete a polygon to be a closed polygon including the
+ area between the original polygon and the baseline.
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QPolygonF polygon: Polygon to be completed
+ """
if polygon.size() < 2:
return
- doAlign = QwtPainter.roundingAlignment(painter)
baseline = self.__data.baseline
- if self.orientation() == Qt.Vertical:
+ if self.orientation() == Qt.Horizontal:
if yMap.transformation():
baseline = yMap.transformation().bounded(baseline)
refY = yMap.transform(baseline)
- if doAlign:
- refY = round(refY)
- polygon += QPointF(polygon.last().x(), refY)
- polygon += QPointF(polygon.first().x(), refY)
+ polygon.append(QPointF(polygon.last().x(), refY))
+ polygon.append(QPointF(polygon.first().x(), refY))
else:
if xMap.transformation():
baseline = xMap.transformation().bounded(baseline)
refX = xMap.transform(baseline)
- if doAlign:
- refX = round(refX)
- polygon += QPointF(refX, polygon.last().y())
- polygon += QPointF(refX, polygon.first().y())
-
+ polygon.append(QPointF(refX, polygon.last().y()))
+ polygon.append(QPointF(refX, polygon.first().y()))
+
def drawSymbols(self, painter, symbol, xMap, yMap, canvasRect, from_, to):
- mapper = QwtPointMapper()
- mapper.setFlag(QwtPointMapper.RoundPoints,
- QwtPainter.roundingAlignment(painter))
- mapper.setFlag(QwtPointMapper.WeedOutPoints,
- self.testPaintAttribute(QwtPlotCurve.FilterPoints))
- mapper.setBoundingRect(canvasRect)
+ """
+ Draw symbols
+
+ :param QPainter painter: Painter
+ :param qwt.symbol.QwtSymbol symbol: Curve symbol
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
+
+ .. seealso::
+
+ :py:meth:`setSymbol()`, :py:meth:`drawSeries()`,
+ :py:meth:`drawCurve()`
+ """
chunkSize = 500
- for i in range(from_, to+1, chunkSize):
- n = min([chunkSize, to-i+1])
- points = mapper.toPointsF(xMap, yMap, self.data(), i, i+n-1)
+ for i in range(from_, to + 1, chunkSize):
+ n = min([chunkSize, to - i + 1])
+ points = series_to_polyline(xMap, yMap, self.data(), i, i + n - 1)
if points.size() > 0:
symbol.drawSymbols(painter, points)
-
+
def setBaseline(self, value):
+ """
+ Set the value of the baseline
+
+ The baseline is needed for filling the curve with a brush or
+ the Sticks drawing style.
+
+ The interpretation of the baseline depends on the `orientation()`.
+ With `Qt.Horizontal`, the baseline is interpreted as a horizontal line
+ at y = baseline(), with `Qt.Vertical`, it is interpreted as a vertical
+ line at x = baseline().
+
+ The default value is 0.0.
+
+ :param float value: Value of the baseline
+
+ .. seealso::
+
+ :py:meth:`baseline()`, :py:meth:`setBrush()`,
+ :py:meth:`setStyle()`
+ """
if self.__data.baseline != value:
self.__data.baseline = value
self.itemChanged()
-
+
def baseline(self):
+ """
+ :return: Value of the baseline
+
+ .. seealso::
+
+ :py:meth:`setBaseline()`
+ """
return self.__data.baseline
-
+
def closestPoint(self, pos):
+ """
+ Find the closest curve point for a specific position
+
+ :param QPoint pos: Position, where to look for the closest curve point
+ :return: tuple `(index, dist)`
+
+ `dist` is the distance between the position and the closest curve
+ point. `index` is the index of the closest curve point, or -1 if
+ none can be found ( f.e when the curve has no points ).
+
+ .. note::
+
+ `closestPoint()` implements a dumb algorithm, that iterates
+ over all points
+ """
numSamples = self.dataSize()
if self.plot() is None or numSamples <= 0:
return -1
@@ -462,75 +897,158 @@ def closestPoint(self, pos):
dmin = 1.0e10
for i in range(numSamples):
sample = series.sample(i)
- cx = xMap.transform(sample.x())-pos.x()
- cy = yMap.transform(sample.y())-pos.y()
- f = qwtSqr(cx)+qwtSqr(cy)
+ cx = xMap.transform(sample.x()) - pos.x()
+ cy = yMap.transform(sample.y()) - pos.y()
+ f = qwtSqr(cx) + qwtSqr(cy)
if f < dmin:
index = i
dmin = f
- dist = np.sqrt(dmin)
+ dist = math.sqrt(dmin)
return index, dist
-
+
def legendIcon(self, index, size):
+ """
+ :param int index: Index of the legend entry (ignored as there is only one)
+ :param QSizeF size: Icon size
+ :return: Icon representing the curve on the legend
+
+ .. seealso::
+
+ :py:meth:`qwt.plot.QwtPlotItem.setLegendIconSize()`,
+ :py:meth:`qwt.plot.QwtPlotItem.legendData()`
+ """
if size.isEmpty():
return QwtGraphic()
graphic = QwtGraphic()
graphic.setDefaultSize(size)
graphic.setRenderHint(QwtGraphic.RenderPensUnscaled, True)
painter = QPainter(graphic)
- painter.setRenderHint(QPainter.Antialiasing,
- self.testRenderHint(QwtPlotItem.RenderAntialiased))
- if self.__data.legendAttributes == 0 or\
- (self.__data.legendAttributes & QwtPlotCurve.LegendShowBrush):
+ painter.setRenderHint(
+ QPainter.Antialiasing, self.testRenderHint(QwtPlotItem.RenderAntialiased)
+ )
+ if self.__data.legendAttributes == 0 or (
+ self.__data.legendAttributes & QwtPlotCurve.LegendShowBrush
+ ):
brush = self.__data.brush
if brush.style() == Qt.NoBrush and self.__data.legendAttributes == 0:
if self.style() != QwtPlotCurve.NoCurve:
brush = QBrush(self.pen().color())
- elif self.__data.symbol and\
- self.__data.symbol.style() != QwtSymbol.NoSymbol:
+ elif (
+ self.__data.symbol
+ and self.__data.symbol.style() != QwtSymbol.NoSymbol
+ ):
brush = QBrush(self.__data.symbol.pen().color())
if brush.style() != Qt.NoBrush:
r = QRectF(0, 0, size.width(), size.height())
painter.fillRect(r, brush)
if self.__data.legendAttributes & QwtPlotCurve.LegendShowLine:
if self.pen() != Qt.NoPen:
- pn = self.pen()
-# pn.setCapStyle(Qt.FlatCap)
- painter.setPen(pn)
- y = .5*size.height()
- QwtPainter.drawLine(painter, 0., y, size.width(), y)
+ painter.setPen(self.pen())
+ y = size.height() // 2
+ painter.drawLine(QLineF(0, y, size.width(), y))
if self.__data.legendAttributes & QwtPlotCurve.LegendShowSymbol:
if self.__data.symbol:
r = QRectF(0, 0, size.width(), size.height())
self.__data.symbol.drawSymbol(painter, r)
- return graphic
+ return graphic
+
+ def setData(self, *args, **kwargs):
+ """
+ Initialize data with a series data object or an array of points.
+
+ .. py:method:: setData(data):
+
+ :param data: Series data (e.g. `QwtPointArrayData` instance)
+ :type data: .plot_series.QwtSeriesData
+
+ .. py:method:: setData(xData, yData, [size=None], [finite=True]):
+
+ Initialize data with `x` and `y` arrays.
+
+ This signature was removed in Qwt6 and is temporarily maintained here to ensure compatibility with Qwt5.
+
+ Same as `setSamples(x, y, [size=None], [finite=True])`
+
+ :param x: List/array of x values
+ :param y: List/array of y values
+ :param size: size of xData and yData
+ :type size: int or None
+ :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements
- def setData(self, *args):
- """Compatibility with Qwt5"""
- if len(args) == 1:
+ .. seealso::
+
+ :py:meth:`setSamples()`
+ """
+ if len(args) == 1 and not kwargs:
super(QwtPlotCurve, self).setData(*args)
- elif len(args) == 2:
- self.setSamples(*args)
+ elif len(args) in (2, 3, 4):
+ self.setSamples(*args, **kwargs)
else:
- raise TypeError("%s().setData() takes 1 or 2 argument(s) (%s given)"\
- % (self.__class__.__name__, len(args)))
-
- def setSamples(self, *args):
- if len(args) == 1:
- samples, = args
+ raise TypeError(
+ "%s().setData() takes 1, 2, 3 or 4 argument(s) (%s given)"
+ % (self.__class__.__name__, len(args))
+ )
+
+ def setSamples(self, *args, **kwargs):
+ """
+ Initialize data with an array of points.
+
+ .. py:method:: setSamples(data):
+
+ :param data: Series data (e.g. `QwtPointArrayData` instance)
+ :type data: .plot_series.QwtSeriesData
+
+
+ .. py:method:: setSamples(samples):
+
+ Same as `setData(QwtPointArrayData(samples))`
+
+ :param samples: List/array of points
+
+ .. py:method:: setSamples(xData, yData, [size=None], [finite=True]):
+
+ Same as `setData(QwtPointArrayData(xData, yData, [size=None]))`
+
+ :param xData: List/array of x values
+ :param yData: List/array of y values
+ :param size: size of xData and yData
+ :type size: int or None
+ :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements
+
+ .. seealso::
+
+ :py:class:`.plot_series.QwtPointArrayData`
+ """
+ if len(args) == 1 and not kwargs:
+ (samples,) = args
if isinstance(samples, QwtSeriesData):
self.setData(samples)
else:
- self.setData(QwtPointSeriesData(samples))
- elif len(args) == 3:
- xData, yData, size = args
- self.setData(QwtPointArrayData(xData, yData, size))
- elif len(args) == 2:
- xData, yData = args
- self.setData(QwtPointArrayData(xData, yData))
+ self.setData(QwtPointArrayData(samples))
+ elif len(args) >= 2:
+ xData, yData = args[:2]
+ try:
+ size = kwargs.pop("size")
+ except KeyError:
+ size = None
+ try:
+ finite = kwargs.pop("finite")
+ except KeyError:
+ finite = None
+ if kwargs:
+ raise TypeError(
+ "%s().setSamples(): unknown %s keyword "
+ "argument(s)"
+ % (self.__class__.__name__, ", ".join(list(kwargs.keys())))
+ )
+ for arg in args[2:]:
+ if isinstance(arg, bool):
+ finite = arg
+ elif isinstance(arg, int):
+ size = arg
+ self.setData(QwtPointArrayData(xData, yData, size=size, finite=finite))
else:
- raise TypeError("%s().setSamples() takes 1, 2 or 3 argument(s) "\
- "(%s given)" % (self.__class__.__name__, len(args)))
-
- def setRawSamples(self, xData, yData, size):
- self.setData(QwtCPointerData(xData, yData, size))
+ raise TypeError(
+ "%s().setSamples() takes 1, 2 or 3 argument(s) "
+ "(%s given)" % (self.__class__.__name__, len(args))
+ )
diff --git a/qwt/plot_directpainter.py b/qwt/plot_directpainter.py
index a57b486..06697d3 100644
--- a/qwt/plot_directpainter.py
+++ b/qwt/plot_directpainter.py
@@ -5,30 +5,44 @@
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)
-from qwt.qt.QtGui import QPainter, QRegion
-from qwt.qt.QtCore import QObject, QT_VERSION, Qt, QEvent
+"""
+QwtPlotDirectPainter
+--------------------
+
+.. autoclass:: QwtPlotDirectPainter
+ :members:
+"""
+
+from qtpy.QtCore import QEvent, QObject, Qt
+from qtpy.QtGui import QPainter, QRegion
from qwt.plot import QwtPlotItem
from qwt.plot_canvas import QwtPlotCanvas
def qwtRenderItem(painter, canvasRect, seriesItem, from_, to):
- #TODO: A minor performance improvement is possible with caching the maps
+ # TODO: A minor performance improvement is possible with caching the maps
plot = seriesItem.plot()
xMap = plot.canvasMap(seriesItem.xAxis())
yMap = plot.canvasMap(seriesItem.yAxis())
- painter.setRenderHint(QPainter.Antialiasing,
- seriesItem.testRenderHint(QwtPlotItem.RenderAntialiased))
+ painter.setRenderHint(
+ QPainter.Antialiasing, seriesItem.testRenderHint(QwtPlotItem.RenderAntialiased)
+ )
seriesItem.drawSeries(painter, xMap, yMap, canvasRect, from_, to)
def qwtHasBackingStore(canvas):
- return canvas.testPaintAttribute(QwtPlotCanvas.BackingStore)\
- and canvas.backingStore()
+ return (
+ canvas.testPaintAttribute(QwtPlotCanvas.BackingStore)
+ and canvas.backingStore() is not None
+ and not canvas.backingStore().isNull()
+ )
-class QwtPlotDirectPainter_PrivateData(object):
+class QwtPlotDirectPainter_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.attributes = 0
self.hasClipping = False
self.seriesItem = None # QwtPlotSeriesItem
@@ -39,78 +53,185 @@ def __init__(self):
class QwtPlotDirectPainter(QObject):
-
+ """
+ Painter object trying to paint incrementally
+
+ Often applications want to display samples while they are
+ collected. When there are too many samples complete replots
+ will be expensive to be processed in a collection cycle.
+
+ `QwtPlotDirectPainter` offers an API to paint
+ subsets (f.e all additions points) without erasing/repainting
+ the plot canvas.
+
+ On certain environments it might be important to calculate a proper
+ clip region before painting. F.e. for Qt Embedded only the clipped part
+ of the backing store will be copied to a (maybe unaccelerated)
+ frame buffer.
+
+ .. warning::
+
+ Incremental painting will only help when no replot is triggered
+ by another operation (like changing scales) and nothing needs
+ to be erased.
+
+ Paint attributes:
+
+ * `QwtPlotDirectPainter.AtomicPainter`:
+
+ Initializing a `QPainter` is an expensive operation.
+ When `AtomicPainter` is set each call of `drawSeries()` opens/closes
+ a temporary `QPainter`. Otherwise `QwtPlotDirectPainter` tries to
+ use the same `QPainter` as long as possible.
+
+ * `QwtPlotDirectPainter.FullRepaint`:
+
+ When `FullRepaint` is set the plot canvas is explicitly repainted
+ after the samples have been rendered.
+
+ * `QwtPlotDirectPainter.CopyBackingStore`:
+
+ When `QwtPlotCanvas.BackingStore` is enabled the painter
+ has to paint to the backing store and the widget. In certain
+ situations/environments it might be faster to paint to
+ the backing store only and then copy the backing store to the canvas.
+ This flag can also be useful for settings, where Qt fills the
+ the clip region with the widget background.
+ """
+
# enum Attribute
AtomicPainter = 0x01
FullRepaint = 0x02
CopyBackingStore = 0x04
-
+
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.__data = QwtPlotDirectPainter_PrivateData()
-
+
def setAttribute(self, attribute, on=True):
+ """
+ Change an attribute
+
+ :param int attribute: Attribute to change
+ :param bool on: On/Off
+
+ .. seealso::
+
+ :py:meth:`testAttribute()`
+ """
if self.testAttribute(attribute) != on:
self.__data.attributes |= attribute
else:
self.__data.attributes &= ~attribute
if attribute == self.AtomicPainter and on:
self.reset()
-
+
def testAttribute(self, attribute):
+ """
+ :param int attribute: Attribute to be tested
+ :return: True, when attribute is enabled
+
+ .. seealso::
+
+ :py:meth:`setAttribute()`
+ """
return self.__data.attributes & attribute
-
+
def setClipping(self, enable):
+ """
+ En/Disables clipping
+
+ :param bool enable: Enables clipping is true, disable it otherwise
+
+ .. seealso::
+
+ :py:meth:`hasClipping()`, :py:meth:`clipRegion()`,
+ :py:meth:`setClipRegion()`
+ """
self.__data.hasClipping = enable
-
+
def hasClipping(self):
+ """
+ :return: Return true, when clipping is enabled
+
+ .. seealso::
+
+ :py:meth:`setClipping()`, :py:meth:`clipRegion()`,
+ :py:meth:`setClipRegion()`
+ """
return self.__data.hasClipping
-
+
def setClipRegion(self, region):
+ """
+ Assign a clip region and enable clipping
+
+ Depending on the environment setting a proper clip region might
+ improve the performance heavily. F.e. on Qt embedded only the clipped
+ part of the backing store will be copied to a (maybe unaccelerated)
+ frame buffer device.
+
+ :param QRegion region: Clip region
+
+ .. seealso::
+
+ :py:meth:`hasClipping()`, :py:meth:`setClipping()`,
+ :py:meth:`clipRegion()`
+ """
self.__data.clipRegion = region
self.__data.hasClipping = True
-
+
def clipRegion(self):
+ """
+ :return: Return Currently set clip region.
+
+ .. seealso::
+
+ :py:meth:`hasClipping()`, :py:meth:`setClipping()`,
+ :py:meth:`setClipRegion()`
+ """
return self.__data.clipRegion
-
+
def drawSeries(self, seriesItem, from_, to):
- """When observing an measurement while it is running, new points have
- to be added to an existing seriesItem. drawSeries() can be used to
+ """
+ Draw a set of points of a seriesItem.
+
+ When observing a measurement while it is running, new points have
+ to be added to an existing seriesItem. drawSeries() can be used to
display them avoiding a complete redraw of the canvas.
- Setting plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)
+ Setting `plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)`
will result in faster painting, if the paint engine of the canvas widget
- supports this feature."""
+ supports this feature.
+
+ :param qwt.plot_series.QwtPlotSeriesItem seriesItem: Item to be painted
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted. If to < 0 the series will be painted to its last point.
+ """
if seriesItem is None or seriesItem.plot() is None:
return
canvas = seriesItem.plot().canvas()
canvasRect = canvas.contentsRect()
- plotCanvas = canvas #XXX: cast to QwtPlotCanvas
- if plotCanvas and qwtHasBackingStore(plotCanvas):
- painter = QPainter(plotCanvas.backingStore()) #XXX: cast plotCanvas.backingStore() to QPixmap
+ if canvas and qwtHasBackingStore(canvas):
+ painter = QPainter(canvas.backingStore())
if self.__data.hasClipping:
painter.setClipRegion(self.__data.clipRegion)
qwtRenderItem(painter, canvasRect, seriesItem, from_, to)
+ painter.end()
if self.testAttribute(self.FullRepaint):
- plotCanvas.repaint()
+ canvas.repaint()
return
- immediatePaint = True
- if not canvas.testAttribute(Qt.WA_WState_InPaintEvent):
- if QT_VERSION >= 0x050000 or\
- not canvas.testAttribute(Qt.WA_PaintOutsidePaintEvent):
- immediatePaint = False
- if immediatePaint:
+ if canvas.testAttribute(Qt.WA_WState_InPaintEvent):
if not self.__data.painter.isActive():
self.reset()
self.__data.painter.begin(canvas)
canvas.installEventFilter(self)
if self.__data.hasClipping:
self.__data.painter.setClipRegion(
- QRegion(canvasRect) & self.__data.clipRegion)
+ QRegion(canvasRect) & self.__data.clipRegion
+ )
elif not self.__data.painter.hasClipping():
self.__data.painter.setClipRect(canvasRect)
- qwtRenderItem(self.__data.painter,
- canvasRect, seriesItem, from_, to)
+ qwtRenderItem(self.__data.painter, canvasRect, seriesItem, from_, to)
if self.__data.attributes & self.AtomicPainter:
self.reset()
elif self.__data.hasClipping:
@@ -127,33 +248,39 @@ def drawSeries(self, seriesItem, from_, to):
canvas.repaint(clipRegion)
canvas.removeEventFilter(self)
self.__data.seriesItem = None
-
+
def reset(self):
+ """Close the internal QPainter"""
if self.__data.painter.isActive():
- w = self.__data.painter.device() #XXX: cast to QWidget
+ w = self.__data.painter.device() # XXX: cast to QWidget
if w:
w.removeEventFilter(self)
self.__data.painter.end()
-
+
def eventFilter(self, obj_, event):
if event.type() == QEvent.Paint:
self.reset()
if self.__data.seriesItem:
- pe = event #XXX: cast to QPaintEvent
+ pe = event # XXX: cast to QPaintEvent
canvas = self.__data.seriesItem.plot().canvas()
painter = QPainter(canvas)
painter.setClipRegion(pe.region())
doCopyCache = self.testAttribute(self.CopyBackingStore)
if doCopyCache:
- plotCanvas = canvas #XXX: cast to QwtPlotCanvas
+ plotCanvas = canvas # XXX: cast to QwtPlotCanvas
if plotCanvas:
doCopyCache = qwtHasBackingStore(plotCanvas)
if doCopyCache:
- painter.drawPixmap(plotCanvas.contentsRect().topLeft(),
- plotCanvas.backingStore())
+ painter.drawPixmap(
+ plotCanvas.rect().topLeft(), plotCanvas.backingStore()
+ )
if not doCopyCache:
- qwtRenderItem(painter, canvas.contentsRect(),
- self.__data.seriesItem,
- self.__data.from_, self.__data.to)
+ qwtRenderItem(
+ painter,
+ canvas.contentsRect(),
+ self.__data.seriesItem,
+ self.__data.from_,
+ self.__data.to,
+ )
return True
return False
diff --git a/qwt/plot_grid.py b/qwt/plot_grid.py
index d46cdf9..a75a07c 100644
--- a/qwt/plot_grid.py
+++ b/qwt/plot_grid.py
@@ -5,18 +5,27 @@
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)
-from qwt.scale_div import QwtScaleDiv
-from qwt.plot import QwtPlotItem
-from qwt.text import QwtText
-from qwt.painter import QwtPainter
-from qwt.math import qwtFuzzyGreaterOrEqual, qwtFuzzyLessOrEqual
+"""
+QwtPlotGrid
+-----------
+
+.. autoclass:: QwtPlotGrid
+ :members:
+"""
-from qwt.qt.QtGui import QPen
-from qwt.qt.QtCore import Qt
+from qtpy.QtCore import QLineF, QObject, Qt
+from qtpy.QtGui import QPen
+
+from qwt._math import qwtFuzzyGreaterOrEqual, qwtFuzzyLessOrEqual
+from qwt.plot import QwtPlotItem
+from qwt.qthelpers import qcolor_from_str
+from qwt.scale_div import QwtScaleDiv
-class QwtPlotGrid_PrivateData(object):
+class QwtPlotGrid_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.xEnabled = True
self.yEnabled = True
self.xMinEnabled = False
@@ -28,160 +37,483 @@ def __init__(self):
class QwtPlotGrid(QwtPlotItem):
- def __init__(self):
- QwtPlotItem.__init__(self, QwtText("Grid"))
+ """
+ A class which draws a coordinate grid
+
+ The `QwtPlotGrid` class can be used to draw a coordinate grid.
+ A coordinate grid consists of major and minor vertical
+ and horizontal grid lines. The locations of the grid lines
+ are determined by the X and Y scale divisions which can
+ be assigned with `setXDiv()` and `setYDiv()`.
+ The `draw()` member draws the grid within a bounding
+ rectangle.
+ """
+
+ def __init__(self, title="Grid"):
+ QwtPlotItem.__init__(self, title)
self.__data = QwtPlotGrid_PrivateData()
self.setItemInterest(QwtPlotItem.ScaleInterest, True)
- self.setZ(10.)
-
+ self.setZ(10.0)
+
+ @classmethod
+ def make(
+ cls,
+ plot=None,
+ z=None,
+ enablemajor=None,
+ enableminor=None,
+ color=None,
+ width=None,
+ style=None,
+ mincolor=None,
+ minwidth=None,
+ minstyle=None,
+ ):
+ """
+ Create and setup a new `QwtPlotGrid` object (convenience function).
+
+ :param plot: Plot to attach the curve to
+ :type plot: qwt.plot.QwtPlot or None
+ :param z: Z-value
+ :type z: float or None
+ :param enablemajor: Tuple of two boolean values (x, y) for enabling major grid lines
+ :type enablemajor: bool or None
+ :param enableminor: Tuple of two boolean values (x, y) for enabling minor grid lines
+ :type enableminor: bool or None
+ :param color: Pen color for both major and minor grid lines (default: Qt.gray)
+ :type color: QColor or str or None
+ :param width: Pen width for both major and minor grid lines (default: 1.0)
+ :type width: float or None
+ :param style: Pen style for both major and minor grid lines (default: Qt.DotLine)
+ :type style: Qt.PenStyle or None
+ :param mincolor: Pen color for minor grid lines only (default: Qt.gray)
+ :type mincolor: QColor or str or None
+ :param minwidth: Pen width for minor grid lines only (default: 1.0)
+ :type minwidth: float or None
+ :param minstyle: Pen style for minor grid lines only (default: Qt.DotLine)
+ :type minstyle: Qt.PenStyle or None
+
+ .. seealso::
+
+ :py:meth:`setMinorPen()`, :py:meth:`setMajorPen()`
+ """
+ item = cls()
+ if z is not None:
+ item.setZ(z)
+ color = qcolor_from_str(color, Qt.gray)
+ width = 1.0 if width is None else float(width)
+ style = Qt.DotLine if style is None else style
+ item.setPen(QPen(color, width, style))
+ if mincolor is not None or minwidth is not None or minstyle is not None:
+ mincolor = qcolor_from_str(mincolor, Qt.gray)
+ minwidth = 1.0 if width is None else minwidth
+ minstyle = Qt.DotLine if style is None else minstyle
+ item.setMinorPen(QPen(mincolor, minwidth, minstyle))
+ if enablemajor is not None:
+ if isinstance(enablemajor, tuple) and len(enablemajor) == 2:
+ item.enableX(enablemajor[0])
+ item.enableY(enablemajor[1])
+ else:
+ raise TypeError(
+ "Invalid enablemajor %r (expecting tuple of two booleans)"
+ % enablemajor
+ )
+ if enableminor is not None:
+ if isinstance(enableminor, tuple) and len(enableminor) == 2:
+ item.enableXMin(enableminor[0])
+ item.enableYMin(enableminor[1])
+ else:
+ raise TypeError(
+ "Invalid enableminor %r (expecting tuple of two booleans)"
+ % enableminor
+ )
+ if plot is not None:
+ item.attach(plot)
+ return item
+
def rtti(self):
+ """
+ :return: Return `QwtPlotItem.Rtti_PlotGrid`
+ """
return QwtPlotItem.Rtti_PlotGrid
-
+
def enableX(self, on):
+ """
+ Enable or disable vertical grid lines
+
+ :param bool on: Enable (true) or disable
+
+ .. seealso::
+
+ :py:meth:`enableXMin()`
+ """
if self.__data.xEnabled != on:
self.__data.xEnabled = on
self.legendChanged()
self.itemChanged()
-
+
def enableY(self, on):
+ """
+ Enable or disable horizontal grid lines
+
+ :param bool on: Enable (true) or disable
+
+ .. seealso::
+
+ :py:meth:`enableYMin()`
+ """
if self.__data.yEnabled != on:
self.__data.yEnabled = on
self.legendChanged()
self.itemChanged()
-
+
def enableXMin(self, on):
+ """
+ Enable or disable minor vertical grid lines.
+
+ :param bool on: Enable (true) or disable
+
+ .. seealso::
+
+ :py:meth:`enableX()`
+ """
if self.__data.xMinEnabled != on:
self.__data.xMinEnabled = on
self.legendChanged()
self.itemChanged()
-
+
def enableYMin(self, on):
+ """
+ Enable or disable minor horizontal grid lines.
+
+ :param bool on: Enable (true) or disable
+
+ .. seealso::
+
+ :py:meth:`enableY()`
+ """
if self.__data.yMinEnabled != on:
self.__data.yMinEnabled = on
self.legendChanged()
self.itemChanged()
def setXDiv(self, scaleDiv):
+ """
+ Assign an x axis scale division
+
+ :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division
+ """
if self.__data.xScaleDiv != scaleDiv:
self.__data.xScaleDiv = scaleDiv
self.itemChanged()
def setYDiv(self, scaleDiv):
+ """
+ Assign an y axis scale division
+
+ :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division
+ """
if self.__data.yScaleDiv != scaleDiv:
self.__data.yScaleDiv = scaleDiv
self.itemChanged()
def setPen(self, *args):
+ """
+ Build and/or assign a pen for both major and minor grid lines
+
+ .. py:method:: setPen(color, width, style)
+ :noindex:
+
+ Build and assign a pen for both major and minor grid lines
+
+ In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ been introduced to hide this incompatibility.
+
+ :param QColor color: Pen color
+ :param float width: Pen width
+ :param Qt.PenStyle style: Pen style
+
+ .. py:method:: setPen(pen)
+ :noindex:
+
+ Assign a pen for both major and minor grid lines
+
+ :param QPen pen: New pen
+
+ .. seealso::
+
+ :py:meth:`pen()`, :py:meth:`brush()`
+ """
if len(args) == 3:
color, width, style = args
self.setPen(QPen(color, width, style))
elif len(args) == 1:
- pen, = args
+ (pen,) = args
if self.__data.majorPen != pen or self.__data.minorPen != pen:
self.__data.majorPen = pen
self.__data.minorPen = pen
self.legendChanged()
self.itemChanged()
else:
- raise TypeError("%s().setPen() takes 1 or 3 argument(s) (%s given)"\
- % (self.__class__.__name__, len(args)))
+ raise TypeError(
+ "%s().setPen() takes 1 or 3 argument(s) (%s given)"
+ % (self.__class__.__name__, len(args))
+ )
def setMajorPen(self, *args):
+ """
+ Build and/or assign a pen for both major grid lines
+
+ .. py:method:: setMajorPen(color, width, style)
+ :noindex:
+
+ Build and assign a pen for both major grid lines
+
+ In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ been introduced to hide this incompatibility.
+
+ :param QColor color: Pen color
+ :param float width: Pen width
+ :param Qt.PenStyle style: Pen style
+
+ .. py:method:: setMajorPen(pen)
+ :noindex:
+
+ Assign a pen for the major grid lines
+
+ :param QPen pen: New pen
+
+ .. seealso::
+
+ :py:meth:`majorPen()`, :py:meth:`setMinorPen()`,
+ :py:meth:`setPen()`, :py:meth:`pen()`, :py:meth:`brush()`
+ """
if len(args) == 3:
color, width, style = args
self.setMajorPen(QPen(color, width, style))
elif len(args) == 1:
- pen, = args
+ (pen,) = args
if self.__data.majorPen != pen:
self.__data.majorPen = pen
self.legendChanged()
self.itemChanged()
else:
- raise TypeError("%s().setMajorPen() takes 1 or 3 argument(s) (%s "\
- "given)" % (self.__class__.__name__, len(args)))
+ raise TypeError(
+ "%s().setMajorPen() takes 1 or 3 argument(s) (%s "
+ "given)" % (self.__class__.__name__, len(args))
+ )
def setMinorPen(self, *args):
+ """
+ Build and/or assign a pen for both minor grid lines
+
+ .. py:method:: setMinorPen(color, width, style)
+ :noindex:
+
+ Build and assign a pen for both minor grid lines
+
+ In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ been introduced to hide this incompatibility.
+
+ :param QColor color: Pen color
+ :param float width: Pen width
+ :param Qt.PenStyle style: Pen style
+
+ .. py:method:: setMinorPen(pen)
+ :noindex:
+
+ Assign a pen for the minor grid lines
+
+ :param QPen pen: New pen
+
+ .. seealso::
+
+ :py:meth:`minorPen()`, :py:meth:`setMajorPen()`,
+ :py:meth:`setPen()`, :py:meth:`pen()`, :py:meth:`brush()`
+ """
if len(args) == 3:
color, width, style = args
self.setMinorPen(QPen(color, width, style))
elif len(args) == 1:
- pen, = args
+ (pen,) = args
if self.__data.minorPen != pen:
self.__data.minorPen = pen
self.legendChanged()
self.itemChanged()
else:
- raise TypeError("%s().setMinorPen() takes 1 or 3 argument(s) (%s "\
- "given)" % (self.__class__.__name__, len(args)))
-
+ raise TypeError(
+ "%s().setMinorPen() takes 1 or 3 argument(s) (%s "
+ "given)" % (self.__class__.__name__, len(args))
+ )
+
def draw(self, painter, xMap, yMap, canvasRect):
+ """
+ Draw the grid
+
+ The grid is drawn into the bounding rectangle such that
+ grid lines begin and end at the rectangle's borders. The X and Y
+ maps are used to map the scale divisions into the drawing region
+ screen.
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: X axis map
+ :param qwt.scale_map.QwtScaleMap yMap: Y axis
+ :param QRectF canvasRect: Contents rectangle of the plot canvas
+ """
minorPen = QPen(self.__data.minorPen)
minorPen.setCapStyle(Qt.FlatCap)
painter.setPen(minorPen)
if self.__data.xEnabled and self.__data.xMinEnabled:
- self.drawLines(painter, canvasRect, Qt.Vertical, xMap,
- self.__data.xScaleDiv.ticks(QwtScaleDiv.MinorTick))
- self.drawLines(painter, canvasRect, Qt.Vertical, xMap,
- self.__data.xScaleDiv.ticks(QwtScaleDiv.MediumTick))
+ self.drawLines(
+ painter,
+ canvasRect,
+ Qt.Vertical,
+ xMap,
+ self.__data.xScaleDiv.ticks(QwtScaleDiv.MinorTick),
+ )
+ self.drawLines(
+ painter,
+ canvasRect,
+ Qt.Vertical,
+ xMap,
+ self.__data.xScaleDiv.ticks(QwtScaleDiv.MediumTick),
+ )
if self.__data.yEnabled and self.__data.yMinEnabled:
- self.drawLines(painter, canvasRect, Qt.Horizontal, yMap,
- self.__data.yScaleDiv.ticks(QwtScaleDiv.MinorTick))
- self.drawLines(painter, canvasRect, Qt.Horizontal, yMap,
- self.__data.yScaleDiv.ticks(QwtScaleDiv.MediumTick))
+ self.drawLines(
+ painter,
+ canvasRect,
+ Qt.Horizontal,
+ yMap,
+ self.__data.yScaleDiv.ticks(QwtScaleDiv.MinorTick),
+ )
+ self.drawLines(
+ painter,
+ canvasRect,
+ Qt.Horizontal,
+ yMap,
+ self.__data.yScaleDiv.ticks(QwtScaleDiv.MediumTick),
+ )
majorPen = QPen(self.__data.majorPen)
majorPen.setCapStyle(Qt.FlatCap)
painter.setPen(majorPen)
if self.__data.xEnabled:
- self.drawLines(painter, canvasRect, Qt.Vertical, xMap,
- self.__data.xScaleDiv.ticks(QwtScaleDiv.MajorTick))
+ self.drawLines(
+ painter,
+ canvasRect,
+ Qt.Vertical,
+ xMap,
+ self.__data.xScaleDiv.ticks(QwtScaleDiv.MajorTick),
+ )
if self.__data.yEnabled:
- self.drawLines(painter, canvasRect, Qt.Horizontal, yMap,
- self.__data.yScaleDiv.ticks(QwtScaleDiv.MajorTick))
-
+ self.drawLines(
+ painter,
+ canvasRect,
+ Qt.Horizontal,
+ yMap,
+ self.__data.yScaleDiv.ticks(QwtScaleDiv.MajorTick),
+ )
+
def drawLines(self, painter, canvasRect, orientation, scaleMap, values):
x1 = canvasRect.left()
- x2 = canvasRect.right()-1.
+ x2 = canvasRect.right() - 1.0
y1 = canvasRect.top()
- y2 = canvasRect.bottom()-1.
- doAlign = QwtPainter.roundingAlignment(painter)
+ y2 = canvasRect.bottom() - 1.0
for val in values:
value = scaleMap.transform(val)
- if doAlign:
- value = round(value)
if orientation == Qt.Horizontal:
- if qwtFuzzyGreaterOrEqual(value, y1) and\
- qwtFuzzyLessOrEqual(value, y2):
- QwtPainter.drawLine(painter, x1, value, x2, value)
+ if qwtFuzzyGreaterOrEqual(value, y1) and qwtFuzzyLessOrEqual(value, y2):
+ painter.drawLine(QLineF(x1, value, x2, value))
else:
- if qwtFuzzyGreaterOrEqual(value, x1) and\
- qwtFuzzyLessOrEqual(value, x2):
- QwtPainter.drawLine(painter, value, y1, value, y2)
-
+ if qwtFuzzyGreaterOrEqual(value, x1) and qwtFuzzyLessOrEqual(value, x2):
+ painter.drawLine(QLineF(value, y1, value, y2))
+
def majorPen(self):
+ """
+ :return: the pen for the major grid lines
+
+ .. seealso::
+
+ :py:meth:`setMajorPen()`, :py:meth:`setMinorPen()`,
+ :py:meth:`setPen()`
+ """
return self.__data.majorPen
-
+
def minorPen(self):
+ """
+ :return: the pen for the minor grid lines
+
+ .. seealso::
+
+ :py:meth:`setMinorPen()`, :py:meth:`setMajorPen()`,
+ :py:meth:`setPen()`
+ """
return self.__data.minorPen
-
+
def xEnabled(self):
+ """
+ :return: True if vertical grid lines are enabled
+
+ .. seealso::
+
+ :py:meth:`enableX()`
+ """
return self.__data.xEnabled
-
+
def yEnabled(self):
+ """
+ :return: True if horizontal grid lines are enabled
+
+ .. seealso::
+
+ :py:meth:`enableY()`
+ """
return self.__data.yEnabled
-
+
def xMinEnabled(self):
+ """
+ :return: True if minor vertical grid lines are enabled
+
+ .. seealso::
+
+ :py:meth:`enableXMin()`
+ """
return self.__data.xMinEnabled
-
+
def yMinEnabled(self):
+ """
+ :return: True if minor horizontal grid lines are enabled
+
+ .. seealso::
+
+ :py:meth:`enableYMin()`
+ """
return self.__data.yMinEnabled
-
+
def xScaleDiv(self):
+ """
+ :return: the scale division of the x axis
+ """
return self.__data.xScaleDiv
-
+
def yScaleDiv(self):
+ """
+ :return: the scale division of the y axis
+ """
return self.__data.yScaleDiv
-
+
def updateScaleDiv(self, xScaleDiv, yScaleDiv):
+ """
+ Update the grid to changes of the axes scale division
+
+ :param qwt.scale_map.QwtScaleMap xMap: Scale division of the x-axis
+ :param qwt.scale_map.QwtScaleMap yMap: Scale division of the y-axis
+
+ .. seealso::
+
+ :py:meth:`updateAxes()`
+ """
self.setXDiv(xScaleDiv)
self.setYDiv(yScaleDiv)
-
\ No newline at end of file
diff --git a/qwt/plot_histogram.py b/qwt/plot_histogram.py
deleted file mode 100644
index 94fa05c..0000000
--- a/qwt/plot_histogram.py
+++ /dev/null
@@ -1,299 +0,0 @@
-# -*- 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)
-
-from qwt.plot_seriesitem import QwtPlotSeriesItem
-from qwt.series_store import QwtSeriesStore
-from qwt.interval import QwtInterval
-from qwt.series_data import QwtIntervalSeriesData
-from qwt.plot import QwtPlotItem
-from qwt.painter import QwtPainter
-from qwt.sample import QwtIntervalSample
-from qwt.column_symbol import QwtColumnRect, QwtColumnSymbol
-
-from qwt.qt.QtGui import QPen, QBrush, QColor, QPolygonF
-from qwt.qt.QtCore import Qt, QPointF, QRectF
-
-
-def qwtIsCombinable(d1, d2):
- if d1.isValid() and d2.isValid():
- if d1.maxValue() == d2.minValue():
- if not d1.borderFlags() & QwtInterval.ExcludeMaximum\
- and d2.borderFlags() & QwtInterval.ExcludeMinimum:
- return True
- return False
-
-
-class QwtPlotHistogram_PrivateData(object):
- def __init__(self):
- self.baseline = 0.
- self.style = 0
- self.symbol = None
- self.pen = QPen()
- self.brush = QBrush()
-
-
-class QwtPlotHistogram(QwtPlotSeriesItem, QwtSeriesStore):
-
- # enum HistogramStyle
- Outline, Columns, Lines = list(range(3))
- UserStyle = 100
-
- def __init__(self, title=None):
- self.__data = None
- self.init()
-
- def init(self):
- self.__data = QwtPlotHistogram_PrivateData()
- self.setData(QwtIntervalSeriesData())
- self.setItemAttribute(QwtPlotItem.AutoScale, True)
- self.setItemAttribute(QwtPlotItem.Legend, True)
- self.setZ(20.)
-
- def setStyle(self, style):
- if style != self.__data.style:
- self.__data.style = style
- self.legendChanged()
- self.itemChanged()
-
- def style(self):
- return self.__data.style
-
- def setPen(self, *args):
- if len(args) not in (1, 2, 3):
- raise TypeError
- if isinstance(args[0], QColor):
- color = args[0]
- width = 0.
- style = Qt.PenStyle
- if len(args) > 1:
- width = args[1]
- if len(args) > 2:
- style = args[2]
- self.setPen(QPen(color, width, style))
- else:
- pen, = args
- if pen != self.__data.pen:
- self.__data.pen = pen
- self.legendChanged()
- self.itemChanged()
-
- def pen(self):
- return self.__data.pen
-
- def setBrush(self, brush):
- if brush != self.__data.brush:
- self.__data.brush = brush
- self.legendChanged()
- self.itemChanged()
-
- def brush(self):
- return self.__data.brush
-
- def setSymbol(self, symbol):
- if symbol != self.__data.symbol:
- self.__data.symbol = symbol
- self.legendChanged()
- self.itemChanged()
-
- def symbol(self):
- return self.__data.symbol
-
- def setBaseline(self, value):
- if value != self.__data.baseline:
- self.__data.baseline = value
- self.itemChanged()
-
- def baseline(self):
- return self.__data.baseline
-
- def boundingRect(self):
- rect = QRectF(self.data().boundingRect())
- if not rect.isValid():
- return rect
- if self.orientation() == Qt.Horizontal:
- rect = QRectF(rect.y(), rect.x(), rect.height(), rect.width())
- if rect.left() > self.__data.baseline:
- rect.setLeft(self.__data.baseline)
- elif rect.right() < self.__data.baseline:
- rect.setRight(self.__data.baseline)
- else:
- if rect.bottom() < self.__data.baseline:
- rect.setBottom(self.__data.baseline)
- elif rect.top() > self.__data.baseline:
- rect.setTop(self.__data.baseline)
- return rect
-
- def rtti(self):
- return QwtPlotItem.Rtti_PlotHistogram
-
- def setSamples(self, samples):
- if not isinstance(samples, QwtIntervalSeriesData):
- self.setData(QwtIntervalSeriesData(samples))
- else:
- self.setData(samples)
-
- def drawSeries(self, painter, xMap, yMap, canvasRect, from_, to):
- if not painter or self.dataSize() <= 0:
- return
- if to < 0:
- to = self.dataSize()-1
- if self.__data.style == self.Outline:
- self.drawOutline(painter, xMap, yMap, from_, to)
- elif self.__data.style == self.Lines:
- self.drawLines(painter, xMap, yMap, from_, to)
- elif self.__data.style == self.Columns:
- self.drawColumns(painter, xMap, yMap, from_, to)
-
- def drawOutline(self, painter, xMap, yMap, from_, to):
- doAlign = QwtPainter.roundingAlignment(painter)
- if self.orientation() == Qt.Horizontal:
- v0 = xMap.transform(self.baseline())
- else:
- v0 = yMap.transform(self.baseline())
- if doAlign:
- v0 = round(v0)
- previous = QwtIntervalSample()
- polygon = QPolygonF()
- for i in range(from_, to+1):
- sample = self.sample(i)
- if not sample.interval.isValid():
- self.flushPolygon(painter, v0, polygon)
- previous = sample
- continue
- if previous.interval.isValid():
- if not qwtIsCombinable(previous.interval, sample.interval):
- self.flushPolygon(painter, v0, polygon)
- if self.orientation() == Qt.Vertical:
- x1 = xMap.transform(sample.interval.minValue())
- x2 = xMap.transform(sample.interval.maxValue())
- y = yMap.transform(sample.value)
- if doAlign:
- x1 = round(x1)
- x2 = round(x2)
- y = round(y)
- if polygon.size() == 0:
- polygon += QPointF(x1, v0)
- polygon += QPointF(x1, y)
- polygon += QPointF(x2, y)
- else:
- y1 = yMap.transform(sample.interval.minValue())
- y2 = yMap.transform(sample.interval.maxValue())
- x = xMap.transform(sample.value)
- if doAlign:
- y1 = round(y1)
- y2 = round(y2)
- x = round(x)
- if polygon.size() == 0:
- polygon += QPointF(v0, y1)
- polygon += QPointF(x, y1)
- polygon += QPointF(x, y2)
- previous = sample
- self.flushPolygon(painter, v0, polygon)
-
- def drawColumns(self, painter, xMap, yMap, from_, to):
- painter.setPen(self.__data.pen)
- painter.setBrush(self.__data.brush)
- series = self.data()
- for i in range(from_, to+1):
- sample = series.sample(i)
- if not sample.interval.isNull():
- rect = self.columnRect(sample, xMap, yMap)
- self.drawColumn(painter, rect, sample)
-
- def drawLines(self, painter, xMap, yMap, from_, to):
- doAlign = QwtPainter.roundingAlignment(painter)
- painter.setPen(self.__data.pen)
- painter.setBrush(self.__data.brush)
- series = self.data()
- for i in range(from_, to+1):
- sample = series.sample(i)
- if not sample.interval.isNull():
- rect = self.columnRect(sample, xMap, yMap)
- r = QRectF(rect.toRect())
- if doAlign:
- r.setLeft(round(r.left()))
- r.setRight(round(r.right()))
- r.setTop(round(r.top()))
- r.setBottom(round(r.bottom()))
- if rect.direction == QwtColumnRect.LeftToRight:
- QwtPainter.drawLine(painter, r.topRight(), r.bottomRight())
- elif rect.direction == QwtColumnRect.RightToLeft:
- QwtPainter.drawLine(painter, r.topLeft(), r.bottomLeft())
- elif rect.direction == QwtColumnRect.TopToBottom:
- QwtPainter.drawLine(painter, r.bottomRight(), r.bottomLeft())
- elif rect.direction == QwtColumnRect.BottomToTop:
- QwtPainter.drawLine(painter, r.topRight(), r.topLeft())
-
- def flushPolygon(self, painter, baseline, polygon):
- if polygon.size() == 0:
- return
- if self.orientation() == Qt.Horizontal:
- polygon += QPointF(baseline, polygon[-1].y())
- else:
- polygon += QPointF(polygon[-1].x(), baseline)
- if self.__data.brush.style() != Qt.NoBrush:
- painter.setPen(Qt.NoPen)
- painter.setBrush(self.__data.brush)
- if self.orientation() == Qt.Horizontal:
- polygon += QPointF(polygon[-1].x(), baseline)
- polygon += QPointF(polygon[0].x(), baseline)
- else:
- polygon += QPointF(baseline, polygon[-1].y())
- polygon += QPointF(baseline, polygon[0].y())
- QwtPainter.drawPolygon(painter, polygon)
- polygon.pop(-1)
- polygon.pop(-1)
- if self.__data.pen.style != Qt.NoPen:
- painter.setBrush(Qt.NoBrush)
- painter.setPen(self.__data.pen)
- QwtPainter.drawPolyline(painter, polygon)
- polygon.clear()
-
- def columnRect(self, sample, xMap, yMap):
- rect = QwtColumnRect()
- iv = sample.interval
- if not iv.isValid():
- return rect
- if self.orientation() == Qt.Horizontal:
- x0 = xMap.transform(self.baseline())
- x = xMap.transform(sample.value)
- y1 = yMap.transform(iv.minValue())
- y2 = yMap.transform(iv.maxValue())
- rect.hInterval.setInterval(x0, x)
- rect.vInterval.setInterval(y1, y2, iv.borderFlags())
- if x < x0:
- rect.direction = QwtColumnRect.RightToLeft
- else:
- rect.direction = QwtColumnRect.LeftToRight
- else:
- x1 = xMap.transform(iv.minValue())
- x2 = xMap.transform(iv.maxValue())
- y0 = yMap.transform(self.baseline())
- y = yMap.transform(sample.value)
- rect.hInterval.setInterval(x1, x2, iv.borderFlags())
- rect.vInterval.setInterval(y0, y)
- if y < y0:
- rect.direction = QwtColumnRect.BottomToTop
- else:
- rect.direction = QwtColumnRect.TopToBottom
- return rect
-
- def drawColumn(self, painter, rect, sample):
- if self.__data.symbol and\
- self.__data.symbol.style() != QwtColumnSymbol.NoStyle:
- self.__data.symbol.draw(painter, rect)
- else:
- r = QRectF(rect.toRect())
- if QwtPainter.roundingAlignment(painter):
- r.setLeft(round(r.left()))
- r.setRight(round(r.right()))
- r.setTop(round(r.top()))
- r.setBottom(round(r.bottom()))
- QwtPainter.drawRect(painter, r)
-
- def legendIcon(self, index, size):
- return self.defaultIcon(self.__data.brush, size)
diff --git a/qwt/plot_layout.py b/qwt/plot_layout.py
index 57676d9..0919914 100644
--- a/qwt/plot_layout.py
+++ b/qwt/plot_layout.py
@@ -5,17 +5,25 @@
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)
-from qwt.text import QwtText
-from qwt.scale_widget import QwtScaleWidget
-from qwt.plot import QwtPlot
-from qwt.scale_draw import QwtAbstractScaleDraw
+"""
+QwtPlotLayout
+-------------
+
+.. autoclass:: QwtPlotLayout
+ :members:
+"""
-from qwt.qt.QtGui import QFont, QRegion
-from qwt.qt.QtCore import QSize, Qt, QRectF
+import math
-import numpy as np
+from qtpy.QtCore import QObject, QRectF, QSize, Qt
+from qtpy.QtGui import QFont, QRegion
+
+from qwt.plot import QwtPlot
+from qwt.scale_draw import QwtAbstractScaleDraw
+from qwt.scale_widget import QwtScaleWidget
+from qwt.text import QwtText
-QWIDGETSIZE_MAX = (1<<24)-1
+QWIDGETSIZE_MAX = (1 << 24) - 1
class LegendData(object):
@@ -25,16 +33,19 @@ def __init__(self):
self.vScrollExtent = None
self.hint = QSize()
+
class TitleData(object):
def __init__(self):
self.text = QwtText()
self.frameWidth = None
+
class FooterData(object):
def __init__(self):
self.text = QwtText()
self.frameWidth = None
+
class ScaleData(object):
def __init__(self):
self.isEnabled = None
@@ -46,31 +57,33 @@ def __init__(self):
self.tickOffset = None
self.dimWithoutTitle = None
+
class CanvasData(object):
def __init__(self):
- self.contentsMargins = [0 for _i in range(QwtPlot.axisCnt)]
+ self.contentsMargins = [0 for _i in QwtPlot.AXES]
+
class QwtPlotLayout_LayoutData(object):
def __init__(self):
self.legend = LegendData()
self.title = TitleData()
self.footer = FooterData()
- self.scale = [ScaleData() for _i in range(QwtPlot.axisCnt)]
+ self.scale = [ScaleData() for _i in QwtPlot.AXES]
self.canvas = CanvasData()
-
+
def init(self, plot, rect):
+ """Extract all layout relevant data from the plot components"""
# legend
- if plot.legend():
- self.legend.frameWidth = plot.legend().frameWidth()
- self.legend.hScrollExtent = plot.legend().scrollExtent(Qt.Horizontal)
- self.legend.vScrollExtent = plot.legend().scrollExtent(Qt.Vertical)
- hint = plot.legend().sizeHint()
- w = min([hint.width(), np.floor(rect.width())])
- h = plot.legend().heightForWidth(w)
+ legend = plot.legend()
+ if legend:
+ self.legend.frameWidth = legend.frameWidth()
+ self.legend.hScrollExtent = legend.scrollExtent(Qt.Horizontal)
+ self.legend.vScrollExtent = legend.scrollExtent(Qt.Vertical)
+ hint = legend.sizeHint()
+ w = min([hint.width(), math.floor(rect.width())])
+ h = legend.heightForWidth(w)
if h <= 0:
h = hint.height()
- if h > rect.height():
- w += self.legend.hScrollExtent
self.legend.hint = QSize(w, h)
# title
self.title.frameWidth = 0
@@ -91,7 +104,7 @@ def init(self, plot, rect):
self.footer.text.setFont(label.font())
self.footer.frameWidth = plot.footerLabel().frameWidth()
# scales
- for axis in range(QwtPlot.axisCnt):
+ for axis in QwtPlot.AXES:
if plot.axisEnabled(axis):
scaleWidget = plot.axisWidget(axis)
self.scale[axis].isEnabled = True
@@ -102,39 +115,73 @@ def init(self, plot, rect):
self.scale[axis].baseLineOffset = scaleWidget.margin()
self.scale[axis].tickOffset = scaleWidget.margin()
if scaleWidget.scaleDraw().hasComponent(QwtAbstractScaleDraw.Ticks):
- self.scale[axis].tickOffset += scaleWidget.scaleDraw().maxTickLength()
+ self.scale[
+ axis
+ ].tickOffset += scaleWidget.scaleDraw().maxTickLength()
self.scale[axis].dimWithoutTitle = scaleWidget.dimForLength(
- QWIDGETSIZE_MAX, self.scale[axis].scaleFont)
+ QWIDGETSIZE_MAX, self.scale[axis].scaleFont
+ )
if not scaleWidget.title().isEmpty():
- self.scale[axis].dimWithoutTitle -= \
- scaleWidget.titleHeightForWidth(QWIDGETSIZE_MAX)
+ self.scale[axis].dimWithoutTitle -= scaleWidget.titleHeightForWidth(
+ QWIDGETSIZE_MAX
+ )
else:
self.scale[axis].isEnabled = False
self.scale[axis].start = 0
self.scale[axis].end = 0
self.scale[axis].baseLineOffset = 0
- self.scale[axis].tickOffset = 0.
+ self.scale[axis].tickOffset = 0.0
self.scale[axis].dimWithoutTitle = 0
- self.canvas.contentsMargins = plot.canvas().getContentsMargins()
+ layout = plot.canvas().layout()
+ if layout is not None:
+ mgn = layout.contentsMargins()
+ self.canvas.contentsMargins = [
+ mgn.left(),
+ mgn.top(),
+ mgn.right(),
+ mgn.bottom(),
+ ]
-class QwtPlotLayout_PrivateData(object):
+class QwtPlotLayout_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.spacing = 5
self.titleRect = QRectF()
self.footerRect = QRectF()
self.legendRect = QRectF()
- self.scaleRect = [QRectF() for _i in range(QwtPlot.axisCnt)]
+ self.scaleRect = [QRectF() for _i in QwtPlot.AXES]
self.canvasRect = QRectF()
self.layoutData = QwtPlotLayout_LayoutData()
self.legendPos = None
self.legendRatio = None
- self.canvasMargin = [0] * QwtPlot.axisCnt
- self.alignCanvasToScales = [False] * QwtPlot.axisCnt
+ self.canvasMargin = [0] * len(QwtPlot.AXES)
+ self.alignCanvasToScales = [False] * len(QwtPlot.AXES)
class QwtPlotLayout(object):
-
+ """
+ Layout engine for QwtPlot.
+
+ It is used by the `QwtPlot` widget to organize its internal widgets
+ or by `QwtPlot.print()` to render its content to a QPaintDevice like
+ a QPrinter, QPixmap/QImage or QSvgRenderer.
+
+ .. seealso::
+
+ :py:meth:`qwt.plot.QwtPlot.setPlotLayout()`
+
+ Valid options:
+
+ * `QwtPlotLayout.AlignScales`: Unused
+ * `QwtPlotLayout.IgnoreScrollbars`: Ignore the dimension of the scrollbars. There are no scrollbars, when the plot is not rendered to widgets.
+ * `QwtPlotLayout.IgnoreFrames`: Ignore all frames.
+ * `QwtPlotLayout.IgnoreLegend`: Ignore the legend.
+ * `QwtPlotLayout.IgnoreTitle`: Ignore the title.
+ * `QwtPlotLayout.IgnoreFooter`: Ignore the footer.
+ """
+
# enum Option
AlignScales = 0x01
IgnoreScrollbars = 0x02
@@ -142,126 +189,381 @@ class QwtPlotLayout(object):
IgnoreLegend = 0x08
IgnoreTitle = 0x10
IgnoreFooter = 0x20
-
+
def __init__(self):
self.__data = QwtPlotLayout_PrivateData()
self.setLegendPosition(QwtPlot.BottomLegend)
self.setCanvasMargin(4)
self.setAlignCanvasToScales(False)
self.invalidate()
-
+
def setCanvasMargin(self, margin, axis=-1):
+ """
+ Change a margin of the canvas. The margin is the space
+ above/below the scale ticks. A negative margin will
+ be set to -1, excluding the borders of the scales.
+
+ :param int margin: New margin
+ :param int axisId: Axis index
+
+ .. seealso::
+
+ :py:meth:`canvasMargin()`
+
+ .. warning::
+
+ The margin will have no effect when `alignCanvasToScale()` is True
+ """
if margin < 1:
margin = -1
if axis == -1:
- for axis in range(QwtPlot.axisCnt):
+ for axis in QwtPlot.AXES:
self.__data.canvasMargin[axis] = margin
- elif axis >= 0 and axis < QwtPlot.axisCnt:
+ elif axis in QwtPlot.AXES:
self.__data.canvasMargin[axis] = margin
-
+
def canvasMargin(self, axisId):
- if axisId < 0 or axisId >= QwtPlot.axisCnt:
+ """
+ :param int axisId: Axis index
+ :return: Margin around the scale tick borders
+
+ .. seealso::
+
+ :py:meth:`setCanvasMargin()`
+ """
+ if axisId not in QwtPlot.AXES:
return 0
return self.__data.canvasMargin[axisId]
-
+
def setAlignCanvasToScales(self, *args):
+ """
+ Change the align-canvas-to-axis-scales setting.
+
+ .. py:method:: setAlignCanvasToScales(on):
+
+ Set the align-canvas-to-axis-scales flag for all axes
+
+ :param bool on: True/False
+
+ .. py:method:: setAlignCanvasToScales(axisId, on):
+
+ Change the align-canvas-to-axis-scales setting.
+ The canvas may:
+
+ - extend beyond the axis scale ends to maximize its size,
+ - align with the axis scale ends to control its size.
+
+ The axisId parameter is somehow confusing as it identifies a
+ border of the plot and not the axes, that are aligned. F.e when
+ `QwtPlot.yLeft` is set, the left end of the the x-axes
+ (`QwtPlot.xTop`, `QwtPlot.xBottom`) is aligned.
+
+ :param int axisId: Axis index
+ :param bool on: True/False
+
+ .. seealso::
+
+ :py:meth:`setAlignCanvasToScale()`,
+ :py:meth:`alignCanvasToScale()`
+ """
if len(args) == 1:
- on, = args
- for axis in range(QwtPlot.axisCnt):
+ (on,) = args
+ for axis in QwtPlot.AXES:
self.__data.alignCanvasToScales[axis] = on
elif len(args) == 2:
axisId, on = args
- if axis >= 0 and axis < QwtPlot.axisCnt:
+ if axisId in QwtPlot.AXES:
self.__data.alignCanvasToScales[axisId] = on
else:
- raise TypeError("%s().setAlignCanvasToScales() takes 1 or 2 "\
- "argument(s) (%s given)"\
- % (self.__class__.__name__, len(args)))
+ raise TypeError(
+ "%s().setAlignCanvasToScales() takes 1 or 2 "
+ "argument(s) (%s given)" % (self.__class__.__name__, len(args))
+ )
def alignCanvasToScale(self, axisId):
- if axisId < 0 or axisId >= QwtPlot.axisCnt:
+ """
+ Return the align-canvas-to-axis-scales setting.
+ The canvas may:
+
+ - extend beyond the axis scale ends to maximize its size
+ - align with the axis scale ends to control its size.
+
+ :param int axisId: Axis index
+ :return: align-canvas-to-axis-scales setting
+
+ .. seealso::
+
+ :py:meth:`setAlignCanvasToScale()`, :py:meth:`setCanvasMargin()`
+ """
+ if axisId not in QwtPlot.AXES:
return False
return self.__data.alignCanvasToScales[axisId]
-
+
def setSpacing(self, spacing):
+ """
+ Change the spacing of the plot. The spacing is the distance
+ between the plot components.
+
+ :param int spacing: New spacing
+
+ .. seealso::
+
+ :py:meth:`setCanvasMargin()`, :py:meth:`spacing()`
+ """
self.__data.spacing = max([0, spacing])
-
+
def spacing(self):
+ """
+ :return: Spacing
+
+ .. seealso::
+
+ :py:meth:`margin()`, :py:meth:`setSpacing()`
+ """
return self.__data.spacing
-
+
def setLegendPosition(self, *args):
+ """
+ Specify the position of the legend
+
+ .. py:method:: setLegendPosition(pos, [ratio=0.]):
+
+ Specify the position of the legend
+
+ :param QwtPlot.LegendPosition pos: Legend position
+ :param float ratio: Ratio between legend and the bounding rectangle of title, footer, canvas and axes
+
+ The legend will be shrunk if it would need more space than the
+ given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of
+ <= 0.0 it will be reset to the default ratio. The default
+ vertical/horizontal ratio is 0.33/0.5.
+
+ Valid position values:
+
+ * `QwtPlot.LeftLegend`,
+ * `QwtPlot.RightLegend`,
+ * `QwtPlot.TopLegend`,
+ * `QwtPlot.BottomLegend`
+
+ .. seealso::
+
+ :py:meth:`setLegendPosition()`
+ """
if len(args) == 2:
pos, ratio = args
- if ratio > 1.:
- ratio = 1.
+ if ratio > 1.0:
+ ratio = 1.0
if pos in (QwtPlot.TopLegend, QwtPlot.BottomLegend):
- if ratio <= 0.:
- ratio = .33
+ if ratio <= 0.0:
+ ratio = 0.33
self.__data.legendRatio = ratio
self.__data.legendPos = pos
elif pos in (QwtPlot.LeftLegend, QwtPlot.RightLegend):
- if ratio <= 0.:
- ratio = .5
+ if ratio <= 0.0:
+ ratio = 0.5
self.__data.legendRatio = ratio
self.__data.legendPos = pos
elif len(args) == 1:
- pos, = args
- self.setLegendPosition(pos, 0.)
+ (pos,) = args
+ self.setLegendPosition(pos, 0.0)
else:
- raise TypeError("%s().setLegendPosition() takes 1 or 2 argument(s)"\
- "(%s given)" % (self.__class__.__name__, len(args)))
-
+ raise TypeError(
+ "%s().setLegendPosition() takes 1 or 2 argument(s)"
+ "(%s given)" % (self.__class__.__name__, len(args))
+ )
+
def legendPosition(self):
+ """
+ :return: Position of the legend
+
+ .. seealso::
+
+ :py:meth:`legendPosition()`
+ """
return self.__data.legendPos
-
+
def setLegendRatio(self, ratio):
+ """
+ Specify the relative size of the legend in the plot
+
+ :param float ratio: Ratio between legend and the bounding rectangle of title, footer, canvas and axes
+
+ The legend will be shrunk if it would need more space than the
+ given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of
+ <= 0.0 it will be reset to the default ratio. The default
+ vertical/horizontal ratio is 0.33/0.5.
+
+ .. seealso::
+
+ :py:meth:`legendRatio()`
+ """
self.setLegendPosition(self.legendPosition(), ratio)
-
+
def legendRatio(self):
+ """
+ :return: The relative size of the legend in the plot.
+
+ .. seealso::
+
+ :py:meth:`setLegendRatio()`
+ """
return self.__data.legendRatio
-
+
def setTitleRect(self, rect):
+ """
+ Set the geometry for the title
+
+ This method is intended to be used from derived layouts
+ overloading `activate()`
+
+ :param QRectF rect: Rectangle
+
+ .. seealso::
+
+ :py:meth:`titleRect()`, :py:meth:`activate()`
+ """
self.__data.titleRect = rect
-
+
def titleRect(self):
+ """
+ :return: Geometry for the title
+
+ .. seealso::
+
+ :py:meth:`invalidate()`, :py:meth:`activate()`
+ """
return self.__data.titleRect
-
+
def setFooterRect(self, rect):
+ """
+ Set the geometry for the footer
+
+ This method is intended to be used from derived layouts
+ overloading `activate()`
+
+ :param QRectF rect: Rectangle
+
+ .. seealso::
+
+ :py:meth:`footerRect()`, :py:meth:`activate()`
+ """
self.__data.footerRect = rect
-
+
def footerRect(self):
+ """
+ :return: Geometry for the footer
+
+ .. seealso::
+
+ :py:meth:`invalidate()`, :py:meth:`activate()`
+ """
return self.__data.footerRect
-
+
def setLegendRect(self, rect):
+ """
+ Set the geometry for the legend
+
+ This method is intended to be used from derived layouts
+ overloading `activate()`
+
+ :param QRectF rect: Rectangle for the legend
+
+ .. seealso::
+
+ :py:meth:`footerRect()`, :py:meth:`activate()`
+ """
self.__data.legendRect = rect
-
+
def legendRect(self):
+ """
+ :return: Geometry for the legend
+
+ .. seealso::
+
+ :py:meth:`invalidate()`, :py:meth:`activate()`
+ """
return self.__data.legendRect
-
+
def setScaleRect(self, axis, rect):
- if axis >= 0 and axis < QwtPlot.axisCnt:
+ """
+ Set the geometry for an axis
+
+ This method is intended to be used from derived layouts
+ overloading `activate()`
+
+ :param int axisId: Axis index
+ :param QRectF rect: Rectangle for the scale
+
+ .. seealso::
+
+ :py:meth:`scaleRect()`, :py:meth:`activate()`
+ """
+ if axis in QwtPlot.AXES:
self.__data.scaleRect[axis] = rect
-
+
def scaleRect(self, axis):
- if axis < 0 or axis >= QwtPlot.axisCnt:
+ """
+ :param int axisId: Axis index
+ :return: Geometry for the scale
+
+ .. seealso::
+
+ :py:meth:`invalidate()`, :py:meth:`activate()`
+ """
+ if axis not in QwtPlot.AXES:
return QRectF()
return self.__data.scaleRect[axis]
-
+
def setCanvasRect(self, rect):
+ """
+ Set the geometry for the canvas
+
+ This method is intended to be used from derived layouts
+ overloading `activate()`
+
+ :param QRectF rect: Rectangle
+
+ .. seealso::
+
+ :py:meth:`canvasRect()`, :py:meth:`activate()`
+ """
self.__data.canvasRect = rect
-
+
def canvasRect(self):
+ """
+ :return: Geometry for the canvas
+
+ .. seealso::
+
+ :py:meth:`invalidate()`, :py:meth:`activate()`
+ """
return self.__data.canvasRect
-
+
def invalidate(self):
+ """
+ Invalidate the geometry of all components.
+
+ .. seealso::
+
+ :py:meth:`activate()`
+ """
self.__data.titleRect = QRectF()
self.__data.footerRect = QRectF()
self.__data.legendRect = QRectF()
self.__data.canvasRect = QRectF()
- for axis in range(QwtPlot.axisCnt):
+ for axis in QwtPlot.AXES:
self.__data.scaleRect[axis] = QRectF()
-
+
def minimumSizeHint(self, plot):
+ """
+ :param qwt.plot.QwtPlot plot: Plot widget
+ :return: Minimum size hint
+
+ .. seealso::
+
+ :py:meth:`qwt.plot.QwtPlot.minimumSizeHint()`
+ """
+
class _ScaleData(object):
def __init__(self):
self.w = 0
@@ -269,10 +571,21 @@ def __init__(self):
self.minLeft = 0
self.minRight = 0
self.tickOffset = 0
- scaleData = [_ScaleData() for _i in range(QwtPlot.axisCnt)]
- canvasBorder = [0 for _i in range(QwtPlot.axisCnt)]
- fw, _, _, _ = plot.canvas().getContentsMargins()
- for axis in range(QwtPlot.axisCnt):
+
+ scaleData = [_ScaleData() for _i in QwtPlot.AXES]
+ canvasBorder = [0 for _i in QwtPlot.AXES]
+ layout = plot.canvas().layout()
+ if layout is None:
+ left, top, right, bottom = 0, 0, 0, 0
+ else:
+ mgn = layout.contentsMargins()
+ left, top, right, bottom = (
+ mgn.left(),
+ mgn.top(),
+ mgn.right(),
+ mgn.bottom(),
+ )
+ for axis in QwtPlot.AXES:
if plot.axisEnabled(axis):
scl = plot.axisWidget(axis)
sd = scaleData[axis]
@@ -282,110 +595,139 @@ def __init__(self):
sd.minLeft, sd.minLeft = scl.getBorderDistHint()
sd.tickOffset = scl.margin()
if scl.scaleDraw().hasComponent(QwtAbstractScaleDraw.Ticks):
- sd.tickOffset += np.ceil(scl.scaleDraw().maxTickLength())
- canvasBorder[axis] = fw + self.__data.canvasMargin[axis] + 1
- for axis in range(QwtPlot.axisCnt):
+ sd.tickOffset += math.ceil(scl.scaleDraw().maxTickLength())
+ canvasBorder[axis] = left + self.__data.canvasMargin[axis] + 1
+ for axis in QwtPlot.AXES:
sd = scaleData[axis]
if sd.w and axis in (QwtPlot.xBottom, QwtPlot.xTop):
- if sd.minLeft > canvasBorder[QwtPlot.yLeft]\
- and scaleData[QwtPlot.yLeft].w:
+ if (
+ sd.minLeft > canvasBorder[QwtPlot.yLeft]
+ and scaleData[QwtPlot.yLeft].w
+ ):
shiftLeft = sd.minLeft - canvasBorder[QwtPlot.yLeft]
if shiftLeft > scaleData[QwtPlot.yLeft].w:
shiftLeft = scaleData[QwtPlot.yLeft].w
sd.w -= shiftLeft
- if sd.minRight > canvasBorder[QwtPlot.yRight]\
- and scaleData[QwtPlot.yRight].w:
+ if (
+ sd.minRight > canvasBorder[QwtPlot.yRight]
+ and scaleData[QwtPlot.yRight].w
+ ):
shiftRight = sd.minRight - canvasBorder[QwtPlot.yRight]
if shiftRight > scaleData[QwtPlot.yRight].w:
shiftRight = scaleData[QwtPlot.yRight].w
sd.w -= shiftRight
if sd.h and axis in (QwtPlot.yLeft, QwtPlot.yRight):
- if sd.minLeft > canvasBorder[QwtPlot.xBottom]\
- and scaleData[QwtPlot.xBottom].h:
+ if (
+ sd.minLeft > canvasBorder[QwtPlot.xBottom]
+ and scaleData[QwtPlot.xBottom].h
+ ):
shiftBottom = sd.minLeft - canvasBorder[QwtPlot.xBottom]
if shiftBottom > scaleData[QwtPlot.xBottom].tickOffset:
shiftBottom = scaleData[QwtPlot.xBottom].tickOffset
sd.h -= shiftBottom
- if sd.minLeft > canvasBorder[QwtPlot.xTop]\
- and scaleData[QwtPlot.xTop].h:
+ if (
+ sd.minLeft > canvasBorder[QwtPlot.xTop]
+ and scaleData[QwtPlot.xTop].h
+ ):
shiftTop = sd.minRight - canvasBorder[QwtPlot.xTop]
if shiftTop > scaleData[QwtPlot.xTop].tickOffset:
shiftTop = scaleData[QwtPlot.xTop].tickOffset
sd.h -= shiftTop
canvas = plot.canvas()
- left, top, right, bottom = canvas.getContentsMargins()
minCanvasSize = canvas.minimumSize()
w = scaleData[QwtPlot.yLeft].w + scaleData[QwtPlot.yRight].w
- cw = max([scaleData[QwtPlot.xBottom].w,
- scaleData[QwtPlot.xTop].w]) + left + 1 + right + 1
+ cw = (
+ max([scaleData[QwtPlot.xBottom].w, scaleData[QwtPlot.xTop].w])
+ + left
+ + 1
+ + right
+ + 1
+ )
w += max([cw, minCanvasSize.width()])
h = scaleData[QwtPlot.xBottom].h + scaleData[QwtPlot.xTop].h
- ch = max([scaleData[QwtPlot.yLeft].h,
- scaleData[QwtPlot.yRight].h]) + top + 1 + bottom + 1
+ ch = (
+ max([scaleData[QwtPlot.yLeft].h, scaleData[QwtPlot.yRight].h])
+ + top
+ + 1
+ + bottom
+ + 1
+ )
h += max([ch, minCanvasSize.height()])
for label in [plot.titleLabel(), plot.footerLabel()]:
if label and not label.text().isEmpty():
- centerOnCanvas = not plot.axisEnabled(QwtPlot.yLeft)\
- and plot.axisEnabled(QwtPlot.yRight)
+ centerOnCanvas = not plot.axisEnabled(
+ QwtPlot.yLeft
+ ) and plot.axisEnabled(QwtPlot.yRight)
labelW = w
if centerOnCanvas:
- labelW -= scaleData[QwtPlot.yLeft].w +\
- scaleData[QwtPlot.yRight].w
+ labelW -= scaleData[QwtPlot.yLeft].w + scaleData[QwtPlot.yRight].w
labelH = label.heightForWidth(labelW)
if labelH > labelW:
w = labelW = labelH
if centerOnCanvas:
- w += scaleData[QwtPlot.yLeft].w +\
- scaleData[QwtPlot.yRight].w
+ w += scaleData[QwtPlot.yLeft].w + scaleData[QwtPlot.yRight].w
labelH = label.heightForWidth(labelW)
h += labelH + self.__data.spacing
legend = plot.legend()
if legend and not legend.isEmpty():
- if self.__data.legendPos in (QwtPlot.LeftLegend,
- QwtPlot.RightLegend):
+ if self.__data.legendPos in (QwtPlot.LeftLegend, QwtPlot.RightLegend):
legendW = legend.sizeHint().width()
legendH = legend.heightForWidth(legendW)
if legend.frameWidth() > 0:
w += self.__data.spacing
if legendH > h:
legendW += legend.scrollExtent(Qt.Horizontal)
- if self.__data.legendRatio < 1.:
- legendW = min([legendW, int(w/(1.-self.__data.legendRatio))])
+ if self.__data.legendRatio < 1.0:
+ legendW = min([legendW, int(w / (1.0 - self.__data.legendRatio))])
w += legendW + self.__data.spacing
else:
legendW = min([legend.sizeHint().width(), w])
legendH = legend.heightForWidth(legendW)
if legend.frameWidth() > 0:
h += self.__data.spacing
- if self.__data.legendRatio < 1.:
- legendH = min([legendH, int(h/(1.-self.__data.legendRatio))])
+ if self.__data.legendRatio < 1.0:
+ legendH = min([legendH, int(h / (1.0 - self.__data.legendRatio))])
h += legendH + self.__data.spacing
- return QSize(w, h)
-
+ return QSize(int(w), int(h))
+
def layoutLegend(self, options, rect):
+ """
+ Find the geometry for the legend
+
+ :param options: Options how to layout the legend
+ :param QRectF rect: Rectangle where to place the legend
+ :return: Geometry for the legend
+ """
hint = self.__data.layoutData.legend.hint
if self.__data.legendPos in (QwtPlot.LeftLegend, QwtPlot.RightLegend):
- dim = min([hint.width(), int(rect.width()*self.__data.legendRatio)])
+ dim = min([hint.width(), int(rect.width() * self.__data.legendRatio)])
if not (options & self.IgnoreScrollbars):
if hint.height() > rect.height():
dim += self.__data.layoutData.legend.hScrollExtent
else:
- dim = min([hint.height(), int(rect.height()*self.__data.legendRatio)])
+ dim = min([hint.height(), int(rect.height() * self.__data.legendRatio)])
dim = max([dim, self.__data.layoutData.legend.vScrollExtent])
legendRect = QRectF(rect)
if self.__data.legendPos == QwtPlot.LeftLegend:
legendRect.setWidth(dim)
elif self.__data.legendPos == QwtPlot.RightLegend:
- legendRect.setX(rect.right()-dim)
+ legendRect.setX(rect.right() - dim)
legendRect.setWidth(dim)
elif self.__data.legendPos == QwtPlot.TopLegend:
legendRect.setHeight(dim)
elif self.__data.legendPos == QwtPlot.BottomLegend:
- legendRect.setY(rect.bottom()-dim)
+ legendRect.setY(rect.bottom() - dim)
legendRect.setHeight(dim)
return legendRect
-
+
def alignLegend(self, canvasRect, legendRect):
+ """
+ Align the legend to the canvas
+
+ :param QRectF canvasRect: Geometry of the canvas
+ :param QRectF legendRect: Maximum geometry for the legend
+ :return: Geometry for the aligned legend
+ """
alignedRect = legendRect
if self.__data.legendPos in (QwtPlot.BottomLegend, QwtPlot.TopLegend):
if self.__data.layoutData.legend.hint.width() < canvasRect.width():
@@ -396,88 +738,162 @@ def alignLegend(self, canvasRect, legendRect):
alignedRect.setY(canvasRect.y())
alignedRect.setHeight(canvasRect.height())
return alignedRect
-
+
def expandLineBreaks(self, options, rect):
+ """
+ Expand all line breaks in text labels, and calculate the height
+ of their widgets in orientation of the text.
+
+ :param options: Options how to layout the legend
+ :param QRectF rect: Bounding rectangle for title, footer, axes and canvas.
+ :return: tuple `(dimTitle, dimFooter, dimAxes)`
+
+ Returns:
+
+ * `dimTitle`: Expanded height of the title widget
+ * `dimFooter`: Expanded height of the footer widget
+ * `dimAxes`: Expanded heights of the axis in axis orientation.
+ """
dimTitle = dimFooter = 0
- dimAxis = [0 for axis in range(QwtPlot.axisCnt)]
- backboneOffset = [0 for _i in range(QwtPlot.axisCnt)]
- for axis in range(QwtPlot.axisCnt):
+ dimAxes = [0 for axis in QwtPlot.AXES]
+ backboneOffset = [0 for _i in QwtPlot.AXES]
+ for axis in QwtPlot.AXES:
if not (options & self.IgnoreFrames):
- backboneOffset[axis] += self.__data.layoutData.canvas.contentsMargins[axis]
+ backboneOffset[axis] += self.__data.layoutData.canvas.contentsMargins[
+ axis
+ ]
if not self.__data.alignCanvasToScales[axis]:
backboneOffset[axis] += self.__data.canvasMargin[axis]
done = False
while not done:
done = True
- if not ((options & self.IgnoreTitle) or \
- self.__data.layoutData.title.text.isEmpty()):
+ # the size for the 4 axis depend on each other. Expanding
+ # the height of a horizontal axis will shrink the height
+ # for the vertical axis, shrinking the height of a vertical
+ # axis will result in a line break what will expand the
+ # width and results in shrinking the width of a horizontal
+ # axis what might result in a line break of a horizontal
+ # axis ... . So we loop as long until no size changes.
+ if not (
+ (options & self.IgnoreTitle)
+ or self.__data.layoutData.title.text.isEmpty()
+ ):
w = rect.width()
- if self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled !=\
- self.__data.layoutData.scale[QwtPlot.yRight].isEnabled:
- w -= dimAxis[QwtPlot.yLeft]+dimAxis[QwtPlot.yRight]
- d = np.ceil(self.__data.layoutData.title.text.heightForWidth(w))
+ if (
+ self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled
+ != self.__data.layoutData.scale[QwtPlot.yRight].isEnabled
+ ):
+ w -= dimAxes[QwtPlot.yLeft] + dimAxes[QwtPlot.yRight]
+ d = math.ceil(self.__data.layoutData.title.text.heightForWidth(w))
if not (options & self.IgnoreFrames):
- d += 2*self.__data.layoutData.title.frameWidth
+ d += 2 * self.__data.layoutData.title.frameWidth
if d > dimTitle:
dimTitle = d
done = False
- if not ((options & self.IgnoreFooter) or \
- self.__data.layoutData.footer.text.isEmpty()):
+ if not (
+ (options & self.IgnoreFooter)
+ or self.__data.layoutData.footer.text.isEmpty()
+ ):
w = rect.width()
- if self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled !=\
- self.__data.layoutData.scale[QwtPlot.yRight].isEnabled:
- w -= dimAxis[QwtPlot.yLeft]+dimAxis[QwtPlot.yRight]
- d = np.ceil(self.__data.layoutData.footer.text.heightForWidth(w))
+ if (
+ self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled
+ != self.__data.layoutData.scale[QwtPlot.yRight].isEnabled
+ ):
+ w -= dimAxes[QwtPlot.yLeft] + dimAxes[QwtPlot.yRight]
+ d = math.ceil(self.__data.layoutData.footer.text.heightForWidth(w))
if not (options & self.IgnoreFrames):
- d += 2*self.__data.layoutData.footer.frameWidth
+ d += 2 * self.__data.layoutData.footer.frameWidth
if d > dimFooter:
dimFooter = d
done = False
- for axis in range(QwtPlot.axisCnt):
+ for axis in QwtPlot.AXES:
scaleData = self.__data.layoutData.scale[axis]
if scaleData.isEnabled:
if axis in (QwtPlot.xTop, QwtPlot.xBottom):
- length = rect.width()-dimAxis[QwtPlot.yLeft]-dimAxis[QwtPlot.yRight]
+ length = (
+ rect.width()
+ - dimAxes[QwtPlot.yLeft]
+ - dimAxes[QwtPlot.yRight]
+ )
length -= scaleData.start + scaleData.end
- if dimAxis[QwtPlot.yRight] > 0:
+ if dimAxes[QwtPlot.yRight] > 0:
length -= 1
- length += min([dimAxis[QwtPlot.yLeft],
- scaleData.start-backboneOffset[QwtPlot.yLeft]])
- length += min([dimAxis[QwtPlot.yRight],
- scaleData.end-backboneOffset[QwtPlot.yRight]])
+ length += min(
+ [
+ dimAxes[QwtPlot.yLeft],
+ scaleData.start - backboneOffset[QwtPlot.yLeft],
+ ]
+ )
+ length += min(
+ [
+ dimAxes[QwtPlot.yRight],
+ scaleData.end - backboneOffset[QwtPlot.yRight],
+ ]
+ )
else:
- length = rect.height()-dimAxis[QwtPlot.xTop]-dimAxis[QwtPlot.xBottom]
+ length = (
+ rect.height()
+ - dimAxes[QwtPlot.xTop]
+ - dimAxes[QwtPlot.xBottom]
+ )
length -= scaleData.start + scaleData.end
length -= 1
- if dimAxis[QwtPlot.xBottom] <= 0:
+ if dimAxes[QwtPlot.xBottom] <= 0:
length -= 1
- if dimAxis[QwtPlot.xTop] <= 0:
+ if dimAxes[QwtPlot.xTop] <= 0:
length -= 1
- if dimAxis[QwtPlot.xBottom] > 0:
- length += min([self.__data.layoutData.scale[QwtPlot.xBottom].tickOffset,
- float(scaleData.start-backboneOffset[QwtPlot.xBottom])])
- if dimAxis[QwtPlot.xTop] > 0:
- length += min([self.__data.layoutData.scale[QwtPlot.xTop].tickOffset,
- float(scaleData.end-backboneOffset[QwtPlot.xTop])])
+ if dimAxes[QwtPlot.xBottom] > 0:
+ length += min(
+ [
+ self.__data.layoutData.scale[
+ QwtPlot.xBottom
+ ].tickOffset,
+ float(
+ scaleData.start
+ - backboneOffset[QwtPlot.xBottom]
+ ),
+ ]
+ )
+ if dimAxes[QwtPlot.xTop] > 0:
+ length += min(
+ [
+ self.__data.layoutData.scale[
+ QwtPlot.xTop
+ ].tickOffset,
+ float(scaleData.end - backboneOffset[QwtPlot.xTop]),
+ ]
+ )
if dimTitle > 0:
length -= dimTitle + self.__data.spacing
d = scaleData.dimWithoutTitle
if not scaleData.scaleWidget.title().isEmpty():
- d += scaleData.scaleWidget.titleHeightForWidth(np.floor(length))
- if d > dimAxis[axis]:
- dimAxis[axis] = d
+ d += scaleData.scaleWidget.titleHeightForWidth(
+ math.floor(length)
+ )
+ if d > dimAxes[axis]:
+ dimAxes[axis] = d
done = False
- return dimTitle, dimFooter, dimAxis
-
+ return dimTitle, dimFooter, dimAxes
+
def alignScales(self, options, canvasRect, scaleRect):
- backboneOffset = [0 for _i in range(QwtPlot.axisCnt)]
- for axis in range(QwtPlot.axisCnt):
+ """
+ Align the ticks of the axis to the canvas borders using
+ the empty corners.
+
+ :param options: Options how to layout the legend
+ :param QRectF canvasRect: Geometry of the canvas ( IN/OUT )
+ :param QRectF scaleRect: Geometry of the scales ( IN/OUT )
+ """
+ backboneOffset = [0 for _i in QwtPlot.AXES]
+ for axis in QwtPlot.AXES:
backboneOffset[axis] = 0
if not self.__data.alignCanvasToScales[axis]:
backboneOffset[axis] += self.__data.canvasMargin[axis]
if not options & self.IgnoreFrames:
- backboneOffset[axis] += self.__data.layoutData.canvas.contentsMargins[axis]
- for axis in range(QwtPlot.axisCnt):
+ backboneOffset[axis] += self.__data.layoutData.canvas.contentsMargins[
+ axis
+ ]
+ for axis in QwtPlot.AXES:
if not scaleRect[axis].isValid():
continue
startDist = self.__data.layoutData.scale[axis].start
@@ -485,93 +901,114 @@ def alignScales(self, options, canvasRect, scaleRect):
axisRect = scaleRect[axis]
if axis in (QwtPlot.xTop, QwtPlot.xBottom):
leftScaleRect = scaleRect[QwtPlot.yLeft]
- leftOffset = backboneOffset[QwtPlot.yLeft]-startDist
+ leftOffset = backboneOffset[QwtPlot.yLeft] - startDist
if leftScaleRect.isValid():
dx = leftOffset + leftScaleRect.width()
- if self.__data.alignCanvasToScales[QwtPlot.yLeft] and dx < 0.:
+ if self.__data.alignCanvasToScales[QwtPlot.yLeft] and dx < 0.0:
cLeft = canvasRect.left()
- canvasRect.setLeft(max([cLeft, axisRect.left()-dx]))
+ canvasRect.setLeft(max([cLeft, axisRect.left() - dx]))
else:
minLeft = leftScaleRect.left()
- left = axisRect.left()+leftOffset
+ left = axisRect.left() + leftOffset
axisRect.setLeft(max([left, minLeft]))
else:
- if self.__data.alignCanvasToScales[QwtPlot.yLeft] and leftOffset < 0:
- canvasRect.setLeft(max([canvasRect.left(),
- axisRect.left()-leftOffset]))
+ if (
+ self.__data.alignCanvasToScales[QwtPlot.yLeft]
+ and leftOffset < 0
+ ):
+ canvasRect.setLeft(
+ max([canvasRect.left(), axisRect.left() - leftOffset])
+ )
else:
if leftOffset > 0:
- axisRect.setLeft(axisRect.left()+leftOffset)
+ axisRect.setLeft(axisRect.left() + leftOffset)
rightScaleRect = scaleRect[QwtPlot.yRight]
- rightOffset = backboneOffset[QwtPlot.yRight]-endDist+1
+ rightOffset = backboneOffset[QwtPlot.yRight] - endDist + 1
if rightScaleRect.isValid():
- dx = rightOffset+rightScaleRect.width()
+ dx = rightOffset + rightScaleRect.width()
if self.__data.alignCanvasToScales[QwtPlot.yRight] and dx < 0:
cRight = canvasRect.right()
- canvasRect.setRight(min([cRight, axisRect.right()+dx]))
+ canvasRect.setRight(min([cRight, axisRect.right() + dx]))
maxRight = rightScaleRect.right()
- right = axisRect.right()-rightOffset
+ right = axisRect.right() - rightOffset
axisRect.setRight(min([right, maxRight]))
else:
- if self.__data.alignCanvasToScales[QwtPlot.yRight] and rightOffset < 0:
- canvasRect.setRight(min([canvasRect.right(),
- axisRect.right()+rightOffset]))
+ if (
+ self.__data.alignCanvasToScales[QwtPlot.yRight]
+ and rightOffset < 0
+ ):
+ canvasRect.setRight(
+ min([canvasRect.right(), axisRect.right() + rightOffset])
+ )
else:
if rightOffset > 0:
- axisRect.setRight(axisRect.right()-rightOffset)
+ axisRect.setRight(axisRect.right() - rightOffset)
else:
bottomScaleRect = scaleRect[QwtPlot.xBottom]
- bottomOffset = backboneOffset[QwtPlot.xBottom]-endDist+1
+ bottomOffset = backboneOffset[QwtPlot.xBottom] - endDist + 1
if bottomScaleRect.isValid():
- dy = bottomOffset+bottomScaleRect.height()
+ dy = bottomOffset + bottomScaleRect.height()
if self.__data.alignCanvasToScales[QwtPlot.xBottom] and dy < 0:
cBottom = canvasRect.bottom()
- canvasRect.setBottom(min([cBottom, axisRect.bottom()+dy]))
+ canvasRect.setBottom(min([cBottom, axisRect.bottom() + dy]))
else:
- maxBottom = bottomScaleRect.top()+\
- self.__data.layoutData.scale[QwtPlot.xBottom].tickOffset
- bottom = axisRect.bottom()-bottomOffset
+ maxBottom = (
+ bottomScaleRect.top()
+ + self.__data.layoutData.scale[QwtPlot.xBottom].tickOffset
+ )
+ bottom = axisRect.bottom() - bottomOffset
axisRect.setBottom(min([bottom, maxBottom]))
else:
- if self.__data.alignCanvasToScales[QwtPlot.xBottom] and bottomOffset < 0:
- canvasRect.setBottom(min([canvasRect.bottom(),
- axisRect.bottom()+bottomOffset]))
+ if (
+ self.__data.alignCanvasToScales[QwtPlot.xBottom]
+ and bottomOffset < 0
+ ):
+ canvasRect.setBottom(
+ min([canvasRect.bottom(), axisRect.bottom() + bottomOffset])
+ )
else:
if bottomOffset > 0:
- axisRect.setBottom(axisRect.bottom()-bottomOffset)
+ axisRect.setBottom(axisRect.bottom() - bottomOffset)
topScaleRect = scaleRect[QwtPlot.xTop]
- topOffset = backboneOffset[QwtPlot.xTop]-startDist
+ topOffset = backboneOffset[QwtPlot.xTop] - startDist
if topScaleRect.isValid():
- dy = topOffset+topScaleRect.height()
+ dy = topOffset + topScaleRect.height()
if self.__data.alignCanvasToScales[QwtPlot.xTop] and dy < 0:
cTop = canvasRect.top()
- canvasRect.setTop(max([cTop, axisRect.top()-dy]))
+ canvasRect.setTop(max([cTop, axisRect.top() - dy]))
else:
- minTop = topScaleRect.bottom()-\
- self.__data.layoutData.scale[QwtPlot.xTop].tickOffset
- top = axisRect.top()+topOffset
+ minTop = (
+ topScaleRect.bottom()
+ - self.__data.layoutData.scale[QwtPlot.xTop].tickOffset
+ )
+ top = axisRect.top() + topOffset
axisRect.setTop(max([top, minTop]))
else:
if self.__data.alignCanvasToScales[QwtPlot.xTop] and topOffset < 0:
- canvasRect.setTop(max([canvasRect.top(),
- axisRect.top()-topOffset]))
+ canvasRect.setTop(
+ max([canvasRect.top(), axisRect.top() - topOffset])
+ )
else:
if topOffset > 0:
- axisRect.setTop(axisRect.top()+topOffset)
- for axis in range(QwtPlot.axisCnt):
+ axisRect.setTop(axisRect.top() + topOffset)
+ for axis in QwtPlot.AXES:
sRect = scaleRect[axis]
if not sRect.isValid():
continue
if axis in (QwtPlot.xBottom, QwtPlot.xTop):
if self.__data.alignCanvasToScales[QwtPlot.yLeft]:
- y = canvasRect.left()-self.__data.layoutData.scale[axis].start
+ y = canvasRect.left() - self.__data.layoutData.scale[axis].start
if not options & self.IgnoreFrames:
- y += self.__data.layoutData.canvas.contentsMargins[QwtPlot.yLeft]
+ y += self.__data.layoutData.canvas.contentsMargins[
+ QwtPlot.yLeft
+ ]
sRect.setLeft(y)
if self.__data.alignCanvasToScales[QwtPlot.yRight]:
- y = canvasRect.right()-1+self.__data.layoutData.scale[axis].end
+ y = canvasRect.right() - 1 + self.__data.layoutData.scale[axis].end
if not options & self.IgnoreFrames:
- y -= self.__data.layoutData.canvas.contentsMargins[QwtPlot.yRight]
+ y -= self.__data.layoutData.canvas.contentsMargins[
+ QwtPlot.yRight
+ ]
sRect.setRight(y)
if self.__data.alignCanvasToScales[axis]:
if axis == QwtPlot.xTop:
@@ -580,86 +1017,117 @@ def alignScales(self, options, canvasRect, scaleRect):
sRect.setTop(canvasRect.bottom())
else:
if self.__data.alignCanvasToScales[QwtPlot.xTop]:
- x = canvasRect.top()-self.__data.layoutData.scale[axis].start
+ x = canvasRect.top() - self.__data.layoutData.scale[axis].start
if not options & self.IgnoreFrames:
x += self.__data.layoutData.canvas.contentsMargins[QwtPlot.xTop]
sRect.setTop(x)
if self.__data.alignCanvasToScales[QwtPlot.xBottom]:
- x = canvasRect.bottom()-1+self.__data.layoutData.scale[axis].end
+ x = canvasRect.bottom() - 1 + self.__data.layoutData.scale[axis].end
if not options & self.IgnoreFrames:
- x -= self.__data.layoutData.canvas.contentsMargins[QwtPlot.xBottom]
+ x -= self.__data.layoutData.canvas.contentsMargins[
+ QwtPlot.xBottom
+ ]
sRect.setBottom(x)
if self.__data.alignCanvasToScales[axis]:
if axis == QwtPlot.yLeft:
sRect.setRight(canvasRect.left())
else:
sRect.setLeft(canvasRect.right())
-
+
def activate(self, plot, plotRect, options=0x00):
+ """
+ Recalculate the geometry of all components.
+
+ :param qwt.plot.QwtPlot plot: Plot to be layout
+ :param QRectF plotRect: Rectangle where to place the components
+ :param options: Layout options
+ """
self.invalidate()
rect = QRectF(plotRect)
self.__data.layoutData.init(plot, rect)
- if not (options & self.IgnoreLegend) and plot.legend() and\
- not plot.legend().isEmpty():
+ if (
+ not (options & self.IgnoreLegend)
+ and plot.legend()
+ and not plot.legend().isEmpty()
+ ):
self.__data.legendRect = self.layoutLegend(options, rect)
region = QRegion(rect.toRect())
- rect = region.subtracted(QRegion(self.__data.legendRect.toRect())
- ).boundingRect()
+ rect = QRectF(
+ region.subtracted(
+ QRegion(self.__data.legendRect.toRect())
+ ).boundingRect()
+ )
if self.__data.legendPos == QwtPlot.LeftLegend:
- rect.setLeft(rect.left()+self.__data.spacing)
+ rect.setLeft(rect.left() + self.__data.spacing)
elif self.__data.legendPos == QwtPlot.RightLegend:
- rect.setRight(rect.right()-self.__data.spacing)
+ rect.setRight(rect.right() - self.__data.spacing)
elif self.__data.legendPos == QwtPlot.TopLegend:
- rect.setTop(rect.top()+self.__data.spacing)
+ rect.setTop(rect.top() + self.__data.spacing)
elif self.__data.legendPos == QwtPlot.BottomLegend:
- rect.setBottom(rect.bottom()-self.__data.spacing)
-
-# +---+-----------+---+
-# | Title |
-# +---+-----------+---+
-# | | Axis | |
-# +---+-----------+---+
-# | A | | A |
-# | x | Canvas | x |
-# | i | | i |
-# | s | | s |
-# +---+-----------+---+
-# | | Axis | |
-# +---+-----------+---+
-# | Footer |
-# +---+-----------+---+
+ rect.setBottom(rect.bottom() - self.__data.spacing)
+
+ # +---+-----------+---+
+ # | Title |
+ # +---+-----------+---+
+ # | | Axis | |
+ # +---+-----------+---+
+ # | A | | A |
+ # | x | Canvas | x |
+ # | i | | i |
+ # | s | | s |
+ # +---+-----------+---+
+ # | | Axis | |
+ # +---+-----------+---+
+ # | Footer |
+ # +---+-----------+---+
+
+ # title, footer and axes include text labels. The height of each
+ # label depends on its line breaks, that depend on the width
+ # for the label. A line break in a horizontal text will reduce
+ # the available width for vertical texts and vice versa.
+ # expandLineBreaks finds the height/width for title, footer and axes
+ # including all line breaks.
dimTitle, dimFooter, dimAxes = self.expandLineBreaks(options, rect)
if dimTitle > 0:
- self.__data.titleRect.setRect(rect.left(), rect.top(),
- rect.width(), dimTitle)
- rect.setTop(self.__data.titleRect.bottom()+self.__data.spacing)
- if self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled !=\
- self.__data.layoutData.scale[QwtPlot.yRight].isEnabled:
- self.__data.titleRect.setX(rect.left()+dimAxes[QwtPlot.yLeft])
- self.__data.titleRect.setWidth(rect.width()\
- -dimAxes[QwtPlot.yLeft]-dimAxes[QwtPlot.yRight])
+ self.__data.titleRect.setRect(
+ rect.left(), rect.top(), rect.width(), dimTitle
+ )
+ rect.setTop(self.__data.titleRect.bottom() + self.__data.spacing)
+ if (
+ self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled
+ != self.__data.layoutData.scale[QwtPlot.yRight].isEnabled
+ ):
+ self.__data.titleRect.setX(rect.left() + dimAxes[QwtPlot.yLeft])
+ self.__data.titleRect.setWidth(
+ rect.width() - dimAxes[QwtPlot.yLeft] - dimAxes[QwtPlot.yRight]
+ )
if dimFooter > 0:
- self.__data.footerRect.setRect(rect.left(),
- rect.bottom()-dimFooter, rect.width(), dimFooter)
- rect.setBottom(self.__data.footerRect.top()-self.__data.spacing)
- if self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled !=\
- self.__data.layoutData.scale[QwtPlot.yRight].isEnabled:
- self.__data.footerRect.setX(rect.left()+dimAxes[QwtPlot.yLeft])
- self.__data.footerRect.setWidth(rect.width()\
- -dimAxes[QwtPlot.yLeft]-dimAxes[QwtPlot.yRight])
+ self.__data.footerRect.setRect(
+ rect.left(), rect.bottom() - dimFooter, rect.width(), dimFooter
+ )
+ rect.setBottom(self.__data.footerRect.top() - self.__data.spacing)
+ if (
+ self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled
+ != self.__data.layoutData.scale[QwtPlot.yRight].isEnabled
+ ):
+ self.__data.footerRect.setX(rect.left() + dimAxes[QwtPlot.yLeft])
+ self.__data.footerRect.setWidth(
+ rect.width() - dimAxes[QwtPlot.yLeft] - dimAxes[QwtPlot.yRight]
+ )
self.__data.canvasRect.setRect(
- rect.x()+dimAxes[QwtPlot.yLeft],
- rect.y()+dimAxes[QwtPlot.xTop],
- rect.width()-dimAxes[QwtPlot.yRight]-dimAxes[QwtPlot.yLeft],
- rect.height()-dimAxes[QwtPlot.xBottom]-dimAxes[QwtPlot.xTop])
- for axis in range(QwtPlot.axisCnt):
+ rect.x() + dimAxes[QwtPlot.yLeft],
+ rect.y() + dimAxes[QwtPlot.xTop],
+ rect.width() - dimAxes[QwtPlot.yRight] - dimAxes[QwtPlot.yLeft],
+ rect.height() - dimAxes[QwtPlot.xBottom] - dimAxes[QwtPlot.xTop],
+ )
+ for axis in QwtPlot.AXES:
if dimAxes[axis]:
dim = dimAxes[axis]
scaleRect = self.__data.scaleRect[axis]
scaleRect.setRect(*self.__data.canvasRect.getRect())
if axis == QwtPlot.yLeft:
- scaleRect.setX(self.__data.canvasRect.left()-dim)
+ scaleRect.setX(self.__data.canvasRect.left() - dim)
scaleRect.setWidth(dim)
elif axis == QwtPlot.yRight:
scaleRect.setX(self.__data.canvasRect.right())
@@ -668,27 +1136,32 @@ def activate(self, plot, plotRect, options=0x00):
scaleRect.setY(self.__data.canvasRect.bottom())
scaleRect.setHeight(dim)
elif axis == QwtPlot.xTop:
- scaleRect.setY(self.__data.canvasRect.top()-dim)
+ scaleRect.setY(self.__data.canvasRect.top() - dim)
scaleRect.setHeight(dim)
scaleRect = scaleRect.normalized()
-
-# +---+-----------+---+
-# | <- Axis -> |
-# +-^-+-----------+-^-+
-# | | | | | |
-# | | | |
-# | A | | A |
-# | x | Canvas | x |
-# | i | | i |
-# | s | | s |
-# | | | |
-# | | | | | |
-# +-V-+-----------+-V-+
-# | <- Axis -> |
-# +---+-----------+---+
-
- self.alignScales(options, self.__data.canvasRect,
- self.__data.scaleRect)
+
+ # +---+-----------+---+
+ # | <- Axis -> |
+ # +-^-+-----------+-^-+
+ # | | | | | |
+ # | | | |
+ # | A | | A |
+ # | x | Canvas | x |
+ # | i | | i |
+ # | s | | s |
+ # | | | |
+ # | | | | | |
+ # +-V-+-----------+-V-+
+ # | <- Axis -> |
+ # +---+-----------+---+
+
+ # The ticks of the axes - not the labels above - should
+ # be aligned to the canvas. So we try to use the empty
+ # corners to extend the axes, so that the label texts
+ # left/right of the min/max ticks are moved into them.
+
+ self.alignScales(options, self.__data.canvasRect, self.__data.scaleRect)
if not self.__data.legendRect.isEmpty():
- self.__data.legendRect = self.alignLegend(self.__data.canvasRect,
- self.__data.legendRect)
+ self.__data.legendRect = self.alignLegend(
+ self.__data.canvasRect, self.__data.legendRect
+ )
diff --git a/qwt/plot_marker.py b/qwt/plot_marker.py
index 87e05e4..1db25cd 100644
--- a/qwt/plot_marker.py
+++ b/qwt/plot_marker.py
@@ -5,35 +5,70 @@
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)
-from qwt.plot import QwtPlotItem
-from qwt.text import QwtText
-from qwt.painter import QwtPainter
+"""
+QwtPlotMarker
+-------------
+
+.. autoclass:: QwtPlotMarker
+ :members:
+"""
+
+from qtpy.QtCore import QLineF, QObject, QPointF, QRect, QRectF, QSizeF, Qt
+from qtpy.QtGui import QPainter, QPen
+
from qwt.graphic import QwtGraphic
+from qwt.plot import QwtPlot, QwtPlotItem
+from qwt.qthelpers import qcolor_from_str
from qwt.symbol import QwtSymbol
-
-from qwt.qt.QtGui import QPen, QPainter
-from qwt.qt.QtCore import Qt, QPointF, QRectF, QSizeF, QRect
+from qwt.text import QwtText
-class QwtPlotMarker_PrivateData(object):
+class QwtPlotMarker_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.labelAlignment = Qt.AlignCenter
self.labelOrientation = Qt.Horizontal
self.spacing = 2
self.symbol = None
self.style = QwtPlotMarker.NoLine
- self.xValue = 0.
- self.yValue = 0.
+ self.xValue = 0.0
+ self.yValue = 0.0
self.label = QwtText()
self.pen = QPen()
class QwtPlotMarker(QwtPlotItem):
-
+ """
+ A class for drawing markers
+
+ A marker can be a horizontal line, a vertical line,
+ a symbol, a label or any combination of them, which can
+ be drawn around a center point inside a bounding rectangle.
+
+ The `setSymbol()` member assigns a symbol to the marker.
+ The symbol is drawn at the specified point.
+
+ With `setLabel()`, a label can be assigned to the marker.
+ The `setLabelAlignment()` member specifies where the label is drawn. All
+ the Align*-constants in `Qt.AlignmentFlags` (see Qt documentation)
+ are valid. The interpretation of the alignment depends on the marker's
+ line style. The alignment refers to the center point of
+ the marker, which means, for example, that the label would be printed
+ left above the center point if the alignment was set to
+ `Qt.AlignLeft | Qt.AlignTop`.
+
+ Line styles:
+
+ * `QwtPlotMarker.NoLine`: No line
+ * `QwtPlotMarker.HLine`: A horizontal line
+ * `QwtPlotMarker.VLine`: A vertical line
+ * `QwtPlotMarker.Cross`: A crosshair
+ """
+
# enum LineStyle
NoLine, HLine, VLine, Cross = list(range(4))
-
-
+
def __init__(self, title=None):
if title is None:
title = ""
@@ -41,23 +76,127 @@ def __init__(self, title=None):
title = QwtText(title)
QwtPlotItem.__init__(self, title)
self.__data = QwtPlotMarker_PrivateData()
- self.setZ(30.)
-
+ self.setZ(30.0)
+
+ @classmethod
+ def make(
+ cls,
+ xvalue=None,
+ yvalue=None,
+ title=None,
+ label=None,
+ symbol=None,
+ plot=None,
+ z=None,
+ x_axis=None,
+ y_axis=None,
+ align=None,
+ orientation=None,
+ spacing=None,
+ linestyle=None,
+ color=None,
+ width=None,
+ style=None,
+ antialiased=False,
+ ):
+ """
+ Create and setup a new `QwtPlotMarker` object (convenience function).
+
+ :param xvalue: x position (optional, default: None)
+ :type xvalue: float or None
+ :param yvalue: y position (optional, default: None)
+ :type yvalue: float or None
+ :param title: Marker title
+ :type title: qwt.text.QwtText or str or None
+ :param label: Label text
+ :type label: qwt.text.QwtText or str or None
+ :param symbol: New symbol
+ :type symbol: qwt.symbol.QwtSymbol or None
+ :param plot: Plot to attach the curve to
+ :type plot: qwt.plot.QwtPlot or None
+ :param z: Z-value
+ :type z: float or None
+ :param int x_axis: curve X-axis (default: QwtPlot.yLeft)
+ :param int y_axis: curve Y-axis (default: QwtPlot.xBottom)
+ :param align: Alignment of the label
+ :type align: Qt.Alignment or None
+ :param orientation: Orientation of the label
+ :type orientation: Qt.Orientation or None
+ :param spacing: Spacing (distance between the position and the label)
+ :type spacing: int or None
+ :param int linestyle: Line style
+ :param color: Pen color
+ :type color: QColor or str or None
+ :param float width: Pen width
+ :param Qt.PenStyle style: Pen style
+ :param bool antialiased: if True, enable antialiasing rendering
+
+ .. seealso::
+
+ :py:meth:`setData()`, :py:meth:`setPen()`, :py:meth:`attach()`
+ """
+ item = cls(title)
+ if z is not None:
+ item.setZ(z)
+ if symbol is not None:
+ item.setSymbol(symbol)
+ if xvalue is not None:
+ item.setXValue(xvalue)
+ if yvalue is not None:
+ item.setYValue(yvalue)
+ if label is not None:
+ item.setLabel(label)
+ x_axis = QwtPlot.xBottom if x_axis is None else x_axis
+ y_axis = QwtPlot.yLeft if y_axis is None else y_axis
+ item.setAxes(x_axis, y_axis)
+ if align is not None:
+ item.setLabelAlignment(align)
+ if orientation is not None:
+ item.setLabelOrientation(orientation)
+ if spacing is not None:
+ item.setSpacing(spacing)
+ color = qcolor_from_str(color, Qt.black)
+ width = 1.0 if width is None else width
+ style = Qt.SolidLine if style is None else style
+ item.setLinePen(QPen(color, width, style))
+ item.setRenderHint(cls.RenderAntialiased, antialiased)
+ if linestyle is not None:
+ item.setLineStyle(linestyle)
+ if plot is not None:
+ item.attach(plot)
+ return item
+
def rtti(self):
+ """:return: `QwtPlotItem.Rtti_PlotMarker`"""
return QwtPlotItem.Rtti_PlotMarker
-
+
def value(self):
+ """:return: Value"""
return QPointF(self.__data.xValue, self.__data.yValue)
-
+
def xValue(self):
+ """:return: x Value"""
return self.__data.xValue
-
+
def yValue(self):
+ """:return: y Value"""
return self.__data.yValue
-
+
def setValue(self, *args):
+ """
+ Set Value
+
+ .. py:method:: setValue(pos):
+
+ :param QPointF pos: Position
+
+ .. py:method:: setValue(x, y):
+
+ :param float x: x position
+ :param float y: y position
+ """
if len(args) == 1:
- pos, = args
+ (pos,) = args
self.setValue(pos.x(), pos.y())
elif len(args) == 2:
x, y = args
@@ -66,59 +205,100 @@ def setValue(self, *args):
self.__data.yValue = y
self.itemChanged()
else:
- raise TypeError("%s() takes 1 or 2 argument(s) (%s given)"\
- % (self.__class__.__name__, len(args)))
+ raise TypeError(
+ "%s() takes 1 or 2 argument(s) (%s given)"
+ % (self.__class__.__name__, len(args))
+ )
def setXValue(self, x):
+ """
+ Set X Value
+
+ :param float x: x position
+ """
self.setValue(x, self.__data.yValue)
-
+
def setYValue(self, y):
+ """
+ Set Y Value
+
+ :param float y: y position
+ """
self.setValue(self.__data.xValue, y)
-
+
def draw(self, painter, xMap, yMap, canvasRect):
- pos = QPointF(xMap.transform(self.__data.xValue),
- yMap.transform(self.__data.yValue))
+ """
+ Draw the marker
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: x Scale Map
+ :param qwt.scale_map.QwtScaleMap yMap: y Scale Map
+ :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates
+ """
+ pos = QPointF(
+ xMap.transform(self.__data.xValue), yMap.transform(self.__data.yValue)
+ )
self.drawLines(painter, canvasRect, pos)
- if self.__data.symbol and\
- self.__data.symbol.style() != QwtSymbol.NoSymbol:
+ if self.__data.symbol and self.__data.symbol.style() != QwtSymbol.NoSymbol:
sz = self.__data.symbol.size()
- clipRect = QRectF(canvasRect.adjusted(-sz.width(), -sz.height(),
- sz.width(), sz.height()))
+ width, height = int(sz.width()), int(sz.height())
+ clipRect = QRectF(canvasRect.adjusted(-width, -height, width, height))
if clipRect.contains(pos):
self.__data.symbol.drawSymbols(painter, [pos])
self.drawLabel(painter, canvasRect, pos)
-
+
def drawLines(self, painter, canvasRect, pos):
+ """
+ Draw the lines marker
+
+ :param QPainter painter: Painter
+ :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates
+ :param QPointF pos: Position of the marker, translated into widget coordinates
+
+ .. seealso::
+
+ :py:meth:`drawLabel()`,
+ :py:meth:`qwt.symbol.QwtSymbol.drawSymbol()`
+ """
if self.__data.style == self.NoLine:
return
- doAlign = QwtPainter.roundingAlignment(painter)
painter.setPen(self.__data.pen)
if self.__data.style in (QwtPlotMarker.HLine, QwtPlotMarker.Cross):
y = pos.y()
- if doAlign:
- y = round(y)
- QwtPainter.drawLine(painter, canvasRect.left(),
- y, canvasRect.right()-1., y)
+ painter.drawLine(QLineF(canvasRect.left(), y, canvasRect.right() - 1.0, y))
if self.__data.style in (QwtPlotMarker.VLine, QwtPlotMarker.Cross):
x = pos.x()
- if doAlign:
- x = round(x)
- QwtPainter.drawLine(painter, x,
- canvasRect.top(), x, canvasRect.bottom()-1.)
-
+ painter.drawLine(QLineF(x, canvasRect.top(), x, canvasRect.bottom() - 1.0))
+
def drawLabel(self, painter, canvasRect, pos):
+ """
+ Align and draw the text label of the marker
+
+ :param QPainter painter: Painter
+ :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates
+ :param QPointF pos: Position of the marker, translated into widget coordinates
+
+ .. seealso::
+
+ :py:meth:`drawLabel()`,
+ :py:meth:`qwt.symbol.QwtSymbol.drawSymbol()`
+ """
if self.__data.label.isEmpty():
return
- align = Qt.Alignment(self.__data.labelAlignment)
+ align = self.__data.labelAlignment
alignPos = QPointF(pos)
symbolOff = QSizeF(0, 0)
if self.__data.style == QwtPlotMarker.VLine:
+ # In VLine-style the y-position is pointless and
+ # the alignment flags are relative to the canvas
if bool(self.__data.labelAlignment & Qt.AlignTop):
alignPos.setY(canvasRect.top())
align &= ~Qt.AlignTop
align |= Qt.AlignBottom
elif bool(self.__data.labelAlignment & Qt.AlignBottom):
- alignPos.setY(canvasRect.bottom()-1)
+ # In HLine-style the x-position is pointless and
+ # the alignment flags are relative to the canvas
+ alignPos.setY(canvasRect.bottom() - 1)
align &= ~Qt.AlignBottom
align |= Qt.AlignTop
else:
@@ -129,153 +309,324 @@ def drawLabel(self, painter, canvasRect, pos):
align &= ~Qt.AlignLeft
align |= Qt.AlignRight
elif bool(self.__data.labelAlignment & Qt.AlignRight):
- alignPos.setX(canvasRect.right()-1)
+ alignPos.setX(canvasRect.right() - 1)
align &= ~Qt.AlignRight
align |= Qt.AlignLeft
else:
alignPos.setX(canvasRect.center().x())
else:
- if self.__data.symbol and\
- self.__data.symbol.style() != QwtSymbol.NoSymbol:
- symbolOff = self.__data.symbol.size()+QSizeF(1, 1)
+ if self.__data.symbol and self.__data.symbol.style() != QwtSymbol.NoSymbol:
+ symbolOff = QSizeF(self.__data.symbol.size()) + QSizeF(1, 1)
symbolOff /= 2
- pw2 = self.__data.pen.widthF()/2.
- if pw2 == 0.:
- pw2 = .5
+ pw2 = self.__data.pen.widthF() / 2.0
+ if pw2 == 0.0:
+ pw2 = 0.5
spacing = self.__data.spacing
xOff = max([pw2, symbolOff.width()])
yOff = max([pw2, symbolOff.height()])
textSize = self.__data.label.textSize(painter.font())
if align & Qt.AlignLeft:
- alignPos.setX(alignPos.x()-(xOff+spacing))
+ alignPos.setX(alignPos.x() - (xOff + spacing))
if self.__data.labelOrientation == Qt.Vertical:
- alignPos.setX(alignPos.x()-textSize.height())
+ alignPos.setX(alignPos.x() - textSize.height())
else:
- alignPos.setX(alignPos.x()-textSize.width())
+ alignPos.setX(alignPos.x() - textSize.width())
elif align & Qt.AlignRight:
- alignPos.setX(alignPos.x()+xOff+spacing)
+ alignPos.setX(alignPos.x() + xOff + spacing)
else:
if self.__data.labelOrientation == Qt.Vertical:
- alignPos.setX(alignPos.x()-textSize.height()/2)
+ alignPos.setX(alignPos.x() - textSize.height() / 2)
else:
- alignPos.setX(alignPos.x()-textSize.width()/2)
+ alignPos.setX(alignPos.x() - textSize.width() / 2)
if align & Qt.AlignTop:
- alignPos.setY(alignPos.y()-(yOff+spacing))
+ alignPos.setY(alignPos.y() - (yOff + spacing))
if self.__data.labelOrientation != Qt.Vertical:
- alignPos.setY(alignPos.y()-textSize.height())
+ alignPos.setY(alignPos.y() - textSize.height())
elif align & Qt.AlignBottom:
- alignPos.setY(alignPos.y()+yOff+spacing)
+ alignPos.setY(alignPos.y() + yOff + spacing)
if self.__data.labelOrientation == Qt.Vertical:
- alignPos.setY(alignPos.y()+textSize.width())
+ alignPos.setY(alignPos.y() + textSize.width())
else:
if self.__data.labelOrientation == Qt.Vertical:
- alignPos.setY(alignPos.y()+textSize.width()/2)
+ alignPos.setY(alignPos.y() + textSize.width() / 2)
else:
- alignPos.setY(alignPos.y()-textSize.height()/2)
+ alignPos.setY(alignPos.y() - textSize.height() / 2)
painter.translate(alignPos.x(), alignPos.y())
if self.__data.labelOrientation == Qt.Vertical:
- painter.rotate(-90.)
+ painter.rotate(-90.0)
textRect = QRectF(0, 0, textSize.width(), textSize.height())
self.__data.label.draw(painter, textRect)
-
+
def setLineStyle(self, style):
+ """
+ Set the line style
+
+ :param int style: Line style
+
+ Line styles:
+
+ * `QwtPlotMarker.NoLine`: No line
+ * `QwtPlotMarker.HLine`: A horizontal line
+ * `QwtPlotMarker.VLine`: A vertical line
+ * `QwtPlotMarker.Cross`: A crosshair
+
+ .. seealso::
+
+ :py:meth:`lineStyle()`
+ """
if style != self.__data.style:
self.__data.style = style
self.legendChanged()
self.itemChanged()
-
+
def lineStyle(self):
+ """
+ :return: the line style
+
+ .. seealso::
+
+ :py:meth:`setLineStyle()`
+ """
return self.__data.style
-
+
def setSymbol(self, symbol):
+ """
+ Assign a symbol
+
+ :param qwt.symbol.QwtSymbol symbol: New symbol
+
+ .. seealso::
+
+ :py:meth:`symbol()`
+ """
if symbol != self.__data.symbol:
self.__data.symbol = symbol
if symbol is not None:
self.setLegendIconSize(symbol.boundingRect().size())
self.legendChanged()
self.itemChanged()
-
+
def symbol(self):
+ """
+ :return: the symbol
+
+ .. seealso::
+
+ :py:meth:`setSymbol()`
+ """
return self.__data.symbol
-
+
def setLabel(self, label):
+ """
+ Set the label
+
+ :param label: Label text
+ :type label: qwt.text.QwtText or str
+
+ .. seealso::
+
+ :py:meth:`label()`
+ """
+ if not isinstance(label, QwtText):
+ label = QwtText(label)
if label != self.__data.label:
self.__data.label = label
self.itemChanged()
-
+
def label(self):
+ """
+ :return: the label
+
+ .. seealso::
+
+ :py:meth:`setLabel()`
+ """
return self.__data.label
-
+
def setLabelAlignment(self, align):
+ """
+ Set the alignment of the label
+
+ In case of `QwtPlotMarker.HLine` the alignment is relative to the
+ y position of the marker, but the horizontal flags correspond to the
+ canvas rectangle. In case of `QwtPlotMarker.VLine` the alignment is
+ relative to the x position of the marker, but the vertical flags
+ correspond to the canvas rectangle.
+
+ In all other styles the alignment is relative to the marker's position.
+
+ :param Qt.Alignment align: Alignment
+
+ .. seealso::
+
+ :py:meth:`labelAlignment()`, :py:meth:`labelOrientation()`
+ """
if align != self.__data.labelAlignment:
self.__data.labelAlignment = align
self.itemChanged()
-
+
def labelAlignment(self):
+ """
+ :return: the label alignment
+
+ .. seealso::
+
+ :py:meth:`setLabelAlignment()`, :py:meth:`setLabelOrientation()`
+ """
return self.__data.labelAlignment
-
+
def setLabelOrientation(self, orientation):
+ """
+ Set the orientation of the label
+
+ When orientation is `Qt.Vertical` the label is rotated by 90.0 degrees
+ (from bottom to top).
+
+ :param Qt.Orientation orientation: Orientation of the label
+
+ .. seealso::
+
+ :py:meth:`labelOrientation()`, :py:meth:`setLabelAlignment()`
+ """
if orientation != self.__data.labelOrientation:
self.__data.labelOrientation = orientation
self.itemChanged()
-
+
def labelOrientation(self):
+ """
+ :return: the label orientation
+
+ .. seealso::
+
+ :py:meth:`setLabelOrientation()`, :py:meth:`labelAlignment()`
+ """
return self.__data.labelOrientation
-
+
def setSpacing(self, spacing):
+ """
+ Set the spacing
+
+ When the label is not centered on the marker position, the spacing
+ is the distance between the position and the label.
+
+ :param int spacing: Spacing
+
+ .. seealso::
+
+ :py:meth:`spacing()`, :py:meth:`setLabelAlignment()`
+ """
if spacing < 0:
spacing = 0
if spacing != self.__data.spacing:
self.__data.spacing = spacing
self.itemChanged()
-
+
def spacing(self):
+ """
+ :return: the spacing
+
+ .. seealso::
+
+ :py:meth:`setSpacing()`
+ """
return self.__data.spacing
-
def setLinePen(self, *args):
+ """
+ Build and/or assigna a line pen, depending on the arguments.
+
+ .. py:method:: setLinePen(color, width, style)
+ :noindex:
+
+ Build and assign a line pen
+
+ In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ been introduced to hide this incompatibility.
+
+ :param QColor color: Pen color
+ :param float width: Pen width
+ :param Qt.PenStyle style: Pen style
+
+ .. py:method:: setLinePen(pen)
+ :noindex:
+
+ Specify a pen for the line.
+
+ :param QPen pen: New pen
+
+ .. seealso::
+
+ :py:meth:`pen()`, :py:meth:`brush()`
+ """
if len(args) == 1 and isinstance(args[0], QPen):
- pen, = args
+ (pen,) = args
elif len(args) in (1, 2, 3):
color = args[0]
- width = 0.
+ width = 0.0
style = Qt.SolidLine
if len(args) > 1:
width = args[1]
if len(args) > 2:
style = args[2]
- self.setLinePen(QPen(color, width, style))
+ pen = QPen(color, width, style)
+ self.setLinePen(pen)
else:
- raise TypeError("%s().setLinePen() takes 1, 2 or 3 argument(s) "\
- "(%s given)" % (self.__class__.__name__, len(args)))
+ raise TypeError(
+ "%s().setLinePen() takes 1, 2 or 3 argument(s) "
+ "(%s given)" % (self.__class__.__name__, len(args))
+ )
if pen != self.__data.pen:
self.__data.pen = pen
self.legendChanged()
self.itemChanged()
-
+
def linePen(self):
+ """
+ :return: the line pen
+
+ .. seealso::
+
+ :py:meth:`setLinePen()`
+ """
return self.__data.pen
def boundingRect(self):
- return QRectF(self.__data.xValue, self.__data.yValue, 0., 0.)
-
+ if self.__data.style == QwtPlotMarker.HLine:
+ return QRectF(self.__data.xValue, self.__data.yValue, -1.0, 0.0)
+ elif self.__data.style == QwtPlotMarker.VLine:
+ return QRectF(self.__data.xValue, self.__data.yValue, 0.0, -1.0)
+ else:
+ return QRectF(self.__data.xValue, self.__data.yValue, 0.0, 0.0)
+
def legendIcon(self, index, size):
+ """
+ :param int index: Index of the legend entry (ignored as there is only one)
+ :param QSizeF size: Icon size
+ :return: Icon representing the marker on the legend
+
+ .. seealso::
+
+ :py:meth:`qwt.plot.QwtPlotItem.setLegendIconSize()`,
+ :py:meth:`qwt.plot.QwtPlotItem.legendData()`
+ """
if size.isEmpty():
return QwtGraphic()
icon = QwtGraphic()
icon.setDefaultSize(size)
icon.setRenderHint(QwtGraphic.RenderPensUnscaled, True)
painter = QPainter(icon)
- painter.setRenderHint(QPainter.Antialiasing,
- self.testRenderHint(QwtPlotItem.RenderAntialiased))
+ painter.setRenderHint(
+ QPainter.Antialiasing, self.testRenderHint(QwtPlotItem.RenderAntialiased)
+ )
if self.__data.style != QwtPlotMarker.NoLine:
painter.setPen(self.__data.pen)
if self.__data.style in (QwtPlotMarker.HLine, QwtPlotMarker.Cross):
- y = .5*size.height()
- QwtPainter.drawLine(painter, 0., y, size.width(), y)
+ y = 0.5 * size.height()
+ painter.drawLine(QLineF(0.0, y, size.width(), y))
if self.__data.style in (QwtPlotMarker.VLine, QwtPlotMarker.Cross):
- x = .5*size.width()
- QwtPainter.drawLine(painter, x, 0., x, size.height())
+ x = 0.5 * size.width()
+ painter.drawLine(QLineF(x, 0.0, x, size.height()))
if self.__data.symbol:
r = QRect(0, 0, size.width(), size.height())
self.__data.symbol.drawSymbol(painter, r)
diff --git a/qwt/plot_renderer.py b/qwt/plot_renderer.py
index 0591c11..d8b2640 100644
--- a/qwt/plot_renderer.py
+++ b/qwt/plot_renderer.py
@@ -5,7 +5,34 @@
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)
-from __future__ import division
+"""
+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
@@ -13,33 +40,55 @@
from qwt.scale_draw import QwtScaleDraw
from qwt.scale_map import QwtScaleMap
-from qwt.qt.QtGui import (QPrinter, QPainter, QImageWriter, QImage, QColor,
- QPaintDevice, QTransform, QPalette, QFileDialog,
- QPainterPath, QPen)
-from qwt.qt.QtCore import Qt, QRect, QRectF, QObject, QSizeF
-from qwt.qt.QtSvg import QSvgGenerator
-from qwt.qt.compat import getsavefilename
-
-import numpy as np
-import os.path as osp
-
def qwtCanvasClip(canvas, canvasRect):
- x1 = np.ceil(canvasRect.left())
- x2 = np.floor(canvasRect.right())
- y1 = np.ceil(canvasRect.top())
- y2 = np.floor(canvasRect.bottom())
- r = QRect(x1, y1, x2-x1-1, y2-y1-1)
+ """
+ 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):
+class QwtPlotRenderer_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.discardFlags = QwtPlotRenderer.DiscardNone
self.layoutFlags = QwtPlotRenderer.DefaultLayout
+
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
@@ -48,47 +97,134 @@ class QwtPlotRenderer(QObject):
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()
-
- def setDiscardFlag(self, flag, on):
+
+ 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
-
+
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
-
+
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
-
+
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
-
- def setLayoutFlag(self, flag, on):
+
+ 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
-
+
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
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
-
+
def layoutFlags(self):
+ """
+ :return: Layout flags
+
+ .. seealso::
+
+ :py:meth:`setLayoutFlags()`, :py:meth:`setLayoutFlag()`,
+ :py:meth:`testLayoutFlag()`
+ """
return self.__data.layoutFlags
-
- def renderDocument(self, plot, filename, sizeMM=(300, 200), resolution=85,
- format_=None):
+
+ 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:
@@ -101,19 +237,27 @@ def renderDocument(self, plot, filename, sizeMM=(300, 200), resolution=85,
title = plot.title().text()
if not title:
title = "Plot Document"
- mmToInch = 1./25.4
+ 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":
- printer.setOutputFormat(QPrinter.PdfFormat)
+ try:
+ printer.setOutputFormat(QPrinter.PdfFormat)
+ except AttributeError:
+ # PyQt6 on Linux
+ printer.setPrinterName("")
else:
printer.setOutputFormat(QPrinter.PostScriptFormat)
- printer.setColorMode(QPrinter.Color)
+ try:
+ printer.setColorMode(QPrinter.Color)
+ except AttributeError:
+ # PyQt6 on Linux
+ pass
printer.setFullPage(True)
- printer.setPaperSize(sizeMM, QPrinter.Millimeter)
+ printer.setPageSize(QPageSize(sizeMM, QPageSize.Millimeter))
printer.setDocName(title)
printer.setOutputFileName(filename)
printer.setResolution(resolution)
@@ -131,7 +275,7 @@ def renderDocument(self, plot, filename, sizeMM=(300, 200), resolution=85,
painter.end()
elif fmt in QImageWriter.supportedImageFormats():
imageRect = documentRect.toRect()
- dotsPerMeter = int(round(resolution*mmToInch*1000.))
+ dotsPerMeter = int(round(resolution * mmToInch * 1000.0))
image = QImage(imageRect.size(), QImage.Format_ARGB32)
image.setDotsPerMeterX(dotsPerMeter)
image.setDotsPerMeterY(dotsPerMeter)
@@ -144,6 +288,27 @@ def renderDocument(self, plot, filename, sizeMM=(300, 200), resolution=85,
raise TypeError("Unsupported file format '%s'" % fmt)
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()
@@ -152,48 +317,76 @@ def renderTo(self, plot, dest):
w = dest.width()
h = dest.height()
rect = QRectF(0, 0, w, h)
- aspect = rect.width()/rect.height()
- if aspect < 1.:
- rect.setHeight(aspect*rect.width())
+ 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)
-
+
def render(self, plot, painter, plotRect):
- if painter == 0 or not painter.isActive() or not plotRect.isValid()\
- or plot.size().isNull():
+ """
+ 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())
-
+ 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):
- left, top, right, bottom = plot.getContentsMargins()
- layoutRect.adjust(left, top, -right, -bottom)
+ mg = plot.contentsMargins()
+ layoutRect.adjust(mg.left(), mg.top(), -mg.right(), -mg.bottom())
layout = plot.plotLayout()
- baseLineDists = [None]*QwtPlot.axisCnt
- canvasMargins = [None]*QwtPlot.axisCnt
+ baseLineDists = canvasMargins = [None] * len(QwtPlot.AXES)
- for axisId in range(QwtPlot.axisCnt):
+ for axisId in QwtPlot.AXES:
canvasMargins[axisId] = layout.canvasMargin(axisId)
if self.__data.layoutFlags & self.FrameWithScales:
scaleWidget = plot.axisWidget(axisId)
if scaleWidget:
- baseLineDists[axisId] = scaleWidget.margin()
+ mgn = scaleWidget.contentsMargins()
+ baseLineDists[axisId] = max(
+ [mgn.left(), mgn.top(), mgn.right(), mgn.bottom()]
+ )
scaleWidget.setMargin(0)
if not plot.axisEnabled(axisId):
- left, right, top, bottom = 0, 0, 0, 0
+ # 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:
@@ -202,56 +395,71 @@ def render(self, plot, painter, plotRect):
layoutRect.adjust(0, 1, 0, 0)
elif axisId == QwtPlot.xBottom:
layoutRect.adjust(0, 0, 0, -1)
- layoutRect.adjust(left, top, right, bottom)
-
+
+ # Calculate the layout for the document.
layoutOptions = QwtPlotLayout.IgnoreScrollbars
-
- if self.__data.layoutFlags & self.FrameWithScales or\
- self.__data.discardFlags & self.DiscardCanvasFrame:
+
+ 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():
+
+ 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():
+
+ 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():
+
+ if (
+ not self.__data.discardFlags & self.DiscardLegend
+ ) and plot.titleLabel().text():
self.renderLegend(plot, painter, layout.legendRect())
-
- for axisId in range(QwtPlot.axisCnt):
+
+ for axisId in QwtPlot.AXES:
scaleWidget = plot.axisWidget(axisId)
if scaleWidget:
- baseDist = scaleWidget.margin()
+ 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))
-
+ self.renderScale(
+ plot,
+ painter,
+ axisId,
+ startDist,
+ endDist,
+ baseDist,
+ layout.scaleRect(axisId),
+ )
+
painter.restore()
-
- for axisId in range(QwtPlot.axisCnt):
+
+ for axisId in QwtPlot.AXES:
if self.__data.layoutFlags & self.FrameWithScales:
scaleWidget = plot.axisWidget(axisId)
if scaleWidget:
@@ -259,26 +467,58 @@ def render(self, plot, painter, plotRect):
layout.setCanvasMargin(canvasMargins[axisId])
layout.invalidate()
-
+
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)
-
+
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)
-
+
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)
-
- def renderScale(self, plot, painter, axisId, startDist, endDist,
- baseDist, rect):
+
+ 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)
@@ -301,12 +541,12 @@ def renderScale(self, plot, painter, axisId, startDist, endDist,
y = rect.bottom() - 1.0 - baseDist
w = rect.width() - startDist - endDist
align = QwtScaleDraw.TopScale
- elif axisId == QwtPlot.xBottom:
+ 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()
@@ -320,18 +560,26 @@ def renderScale(self, plot, painter, axisId, startDist, endDist,
sd.move(sdPos)
sd.setLength(sdLength)
painter.restore()
-
+
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., -1., 1.)
+ r = canvasRect.adjusted(0.0, 0.0, -1.0, 1.0)
if self.__data.layoutFlags & self.FrameWithScales:
painter.save()
- r.adjust(-1., -1., 1., 1.)
+ 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)
- QwtPainter.drawRect(painter, r)
+ painter.drawRect(r)
painter.restore()
painter.save()
painter.setClipRect(canvasRect)
@@ -357,8 +605,9 @@ def renderCanvas(self, plot, painter, canvasRect, maps):
if not self.__data.discardFlags & self.DiscardCanvasFrame:
frameWidth = canvas.frameWidth()
clipPath = qwtCanvasClip(canvas, canvasRect)
- innerRect = canvasRect.adjusted(frameWidth, frameWidth,
- -frameWidth, -frameWidth)
+ innerRect = canvasRect.adjusted(
+ frameWidth, frameWidth, -frameWidth, -frameWidth
+ )
painter.save()
if clipPath.isEmpty():
painter.setClipRect(innerRect)
@@ -371,25 +620,42 @@ def renderCanvas(self, plot, painter, canvasRect, maps):
if frameWidth > 0:
painter.save()
frameStyle = canvas.frameShadow() | canvas.frameShape()
- frameWidth = canvas.frameWidth()
- borderRadius = canvas.borderRadius()
- if borderRadius > 0.:
- QwtPainter.drawRoundedFrame(painter, canvasRect, r, r,
- canvas.palette(), frameWidth,
- frameStyle)
+ 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)
+ QwtPainter.drawFrame(
+ painter,
+ canvasRect,
+ canvas.palette(),
+ canvas.foregroundRole(),
+ frameWidth,
+ midLineWidth,
+ frameStyle,
+ )
painter.restore()
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 range(QwtPlot.axisCnt):
+ for axisId in QwtPlot.AXES:
map_ = QwtScaleMap()
- map_.setTransformation(
- plot.axisScaleEngine(axisId).transformation())
+ map_.setTransformation(plot.axisScaleEngine(axisId).transformation())
sd = plot.axisScaleDiv(axisId)
map_.setScaleInterval(sd.lowerBound(), sd.upperBound())
if plot.axisEnabled(axisId):
@@ -414,40 +680,60 @@ def buildCanvasMaps(self, plot, canvasRect):
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 range(QwtPlot.axisCnt):
- if margins[axisId] >= 0.:
- m = np.ceil(margins[axisId])
+ for axisId in QwtPlot.AXES:
+ if margins[axisId] >= 0.0:
+ m = math.ceil(margins[axisId])
plot.plotLayout().setCanvasMargin(m, axisId)
marginsChanged = True
return marginsChanged
-
+
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)"]
+ 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 += "*." + str(fmt)
imageFilter += ")"
filter_ += [imageFilter]
- filename, _s = getsavefilename(plot, "Export File Name", filename,
- ";;".join(filter_),
- options=QFileDialog.DontConfirmOverwrite)
+ 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
diff --git a/qwt/plot_series.py b/qwt/plot_series.py
new file mode 100644
index 0000000..e0f21f8
--- /dev/null
+++ b/qwt/plot_series.py
@@ -0,0 +1,384 @@
+# -*- 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)
+
+"""
+Plotting series item
+--------------------
+
+QwtPlotSeriesItem
+~~~~~~~~~~~~~~~~~
+
+.. autoclass:: QwtPlotSeriesItem
+ :members:
+
+QwtSeriesData
+~~~~~~~~~~~~~
+
+.. autoclass:: QwtSeriesData
+ :members:
+
+QwtPointArrayData
+~~~~~~~~~~~~~~~~~
+
+.. autoclass:: QwtPointArrayData
+ :members:
+
+QwtSeriesStore
+~~~~~~~~~~~~~~
+
+.. autoclass:: QwtSeriesStore
+ :members:
+"""
+
+import numpy as np
+from qtpy.QtCore import QPointF, QRectF, Qt
+
+from qwt.plot import QwtPlotItem, QwtPlotItem_PrivateData
+from qwt.text import QwtText
+
+
+class QwtPlotSeriesItem_PrivateData(QwtPlotItem_PrivateData):
+ def __init__(self):
+ QwtPlotItem_PrivateData.__init__(self)
+ self.orientation = Qt.Horizontal
+
+
+class QwtPlotSeriesItem(QwtPlotItem):
+ """
+ Base class for plot items representing a series of samples
+ """
+
+ def __init__(self, title):
+ if not isinstance(title, QwtText):
+ title = QwtText(title)
+ QwtPlotItem.__init__(self, title)
+ self.__data = QwtPlotSeriesItem_PrivateData()
+ self.setItemInterest(QwtPlotItem.ScaleInterest, True)
+
+ def setOrientation(self, orientation):
+ """
+ Set the orientation of the item. Default is `Qt.Horizontal`.
+
+ The `orientation()` might be used in specific way by a plot item.
+ F.e. a QwtPlotCurve uses it to identify how to display the curve
+ int `QwtPlotCurve.Steps` or `QwtPlotCurve.Sticks` style.
+
+ .. seealso::
+
+ :py:meth`orientation()`
+ """
+ if self.__data.orientation != orientation:
+ self.__data.orientation = orientation
+ self.legendChanged()
+ self.itemChanged()
+
+ def orientation(self):
+ """
+ :return: Orientation of the plot item
+
+ .. seealso::
+
+ :py:meth`setOrientation()`
+ """
+ return self.__data.orientation
+
+ def draw(self, painter, xMap, yMap, canvasRect):
+ """
+ Draw the complete series
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ """
+ self.drawSeries(painter, xMap, yMap, canvasRect, 0, -1)
+
+ def drawSeries(self, painter, xMap, yMap, canvasRect, from_, to):
+ """
+ Draw a subset of the samples
+
+ :param QPainter painter: Painter
+ :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
+ :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
+ :param QRectF canvasRect: Contents rectangle of the canvas
+ :param int from_: Index of the first point to be painted
+ :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
+
+ .. seealso::
+
+ This method is implemented in `qwt.plot_curve.QwtPlotCurve`
+ """
+ raise NotImplementedError
+
+ def boundingRect(self):
+ return self.dataRect() # dataRect method is implemented in QwtSeriesStore
+
+ def updateScaleDiv(self, xScaleDiv, yScaleDiv):
+ rect = QRectF(
+ xScaleDiv.lowerBound(),
+ yScaleDiv.lowerBound(),
+ xScaleDiv.range(),
+ yScaleDiv.range(),
+ )
+ self.setRectOfInterest(
+ rect
+ ) # setRectOfInterest method is implemented in QwtSeriesData
+
+ def dataChanged(self):
+ self.itemChanged()
+
+
+class QwtSeriesData(object):
+ """
+ Abstract interface for iterating over samples
+
+ `PythonQwt` offers several implementations of the QwtSeriesData API,
+ but in situations, where data of an application specific format
+ needs to be displayed, without having to copy it, it is recommended
+ to implement an individual data access.
+
+ A subclass of `QwtSeriesData` must implement:
+
+ - size():
+
+ Should return number of data points.
+
+ - sample()
+
+ Should return values x and y values of the sample at specific position
+ as QPointF object.
+
+ - boundingRect()
+
+ Should return the bounding rectangle of the data series.
+ It is used for autoscaling and might help certain algorithms for
+ displaying the data.
+ The member `_boundingRect` is intended for caching the calculated
+ rectangle.
+ """
+
+ def __init__(self):
+ self._boundingRect = QRectF(0.0, 0.0, -1.0, -1.0)
+
+ def setRectOfInterest(self, rect):
+ """
+ Set a the "rect of interest"
+
+ QwtPlotSeriesItem defines the current area of the plot canvas
+ as "rectangle of interest" ( QwtPlotSeriesItem::updateScaleDiv() ).
+ It can be used to implement different levels of details.
+
+ The default implementation does nothing.
+
+ :param QRectF rect: Rectangle of interest
+ """
+ pass
+
+ def size(self):
+ """
+ :return: Number of samples
+ """
+ pass
+
+ def sample(self, i):
+ """
+ Return a sample
+
+ :param int i: Index
+ :return: Sample at position i
+ """
+ pass
+
+ def boundingRect(self):
+ """
+ Calculate the bounding rect of all samples
+
+ The bounding rect is necessary for autoscaling and can be used
+ for a couple of painting optimizations.
+
+ :return: Bounding rectangle
+ """
+ pass
+
+
+class QwtPointArrayData(QwtSeriesData):
+ """
+ Interface for iterating over two array objects
+
+ .. py:class:: QwtCQwtPointArrayDataolorMap(x, y, [size=None])
+
+ :param x: Array of x values
+ :type x: list or tuple or numpy.array
+ :param y: Array of y values
+ :type y: list or tuple or numpy.array
+ :param int size: Size of the x and y arrays
+ :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements
+ """
+
+ def __init__(self, x=None, y=None, size=None, finite=None):
+ QwtSeriesData.__init__(self)
+ if x is None and y is not None:
+ x = np.arange(len(y))
+ elif y is None and x is not None:
+ y = x
+ x = np.arange(len(y))
+ elif x is None and y is None:
+ x = np.array([])
+ y = np.array([])
+ if isinstance(x, (tuple, list)):
+ x = np.array(x)
+ if isinstance(y, (tuple, list)):
+ y = np.array(y)
+ if size is not None:
+ x = np.resize(x, (size,))
+ y = np.resize(y, (size,))
+ if len(x) != len(y):
+ minlen = min(len(x), len(y))
+ x = np.resize(x, (minlen,))
+ y = np.resize(y, (minlen,))
+ if finite if finite is not None else True:
+ indexes = np.logical_and(np.isfinite(x), np.isfinite(y))
+ self.__x = x[indexes]
+ self.__y = y[indexes]
+ else:
+ self.__x = x
+ self.__y = y
+
+ def boundingRect(self):
+ """
+ Calculate the bounding rectangle
+
+ The bounding rectangle is calculated once by iterating over all
+ points and is stored for all following requests.
+
+ :return: Bounding rectangle
+ """
+ xmin = self.__x.min()
+ xmax = self.__x.max()
+ ymin = self.__y.min()
+ ymax = self.__y.max()
+ return QRectF(xmin, ymin, xmax - xmin, ymax - ymin)
+
+ def size(self):
+ """
+ :return: Size of the data set
+ """
+ return min([self.__x.size, self.__y.size])
+
+ def sample(self, index):
+ """
+ :param int index: Index
+ :return: Sample at position `index`
+ """
+ return QPointF(self.__x[index], self.__y[index])
+
+ def xData(self):
+ """
+ :return: Array of the x-values
+ """
+ return self.__x
+
+ def yData(self):
+ """
+ :return: Array of the y-values
+ """
+ return self.__y
+
+
+class QwtSeriesStore(object):
+ """
+ Class storing a `QwtSeriesData` object
+
+ `QwtSeriesStore` and `QwtPlotSeriesItem` are intended as base classes for
+ all plot items iterating over a series of samples.
+ """
+
+ def __init__(self):
+ self.__series = None
+
+ def setData(self, series):
+ """
+ Assign a series of samples
+
+ :param qwt.plot_series.QwtSeriesData series: Data
+
+ .. warning::
+
+ The item takes ownership of the data object, deleting it
+ when its not used anymore.
+ """
+ if self.__series != series:
+ self.__series = series
+ self.dataChanged()
+
+ def dataChanged(self):
+ raise NotImplementedError
+
+ def data(self):
+ """
+ :return: the series data
+ """
+ return self.__series
+
+ def sample(self, index):
+ """
+ :param int index: Index
+ :return: Sample at position index
+ """
+ if self.__series:
+ return self.__series.sample(index)
+ else:
+ return
+
+ def dataSize(self):
+ """
+ :return: Number of samples of the series
+
+ .. seealso::
+
+ :py:meth:`setData()`,
+ :py:meth:`qwt.plot_series.QwtSeriesData.size()`
+ """
+ if self.__series is None:
+ return 0
+ return self.__series.size()
+
+ def dataRect(self):
+ """
+ :return: Bounding rectangle of the series or an invalid rectangle, when no series is stored
+
+ .. seealso::
+
+ :py:meth:`qwt.plot_series.QwtSeriesData.boundingRect()`
+ """
+ if self.__series is None or self.dataSize() == 0:
+ return QRectF(1.0, 1.0, -2.0, -2.0)
+ return self.__series.boundingRect()
+
+ def setRectOfInterest(self, rect):
+ """
+ Set a the "rect of interest" for the series
+
+ :param QRectF rect: Rectangle of interest
+
+ .. seealso::
+
+ :py:meth:`qwt.plot_series.QwtSeriesData.setRectOfInterest()`
+ """
+ if self.__series:
+ self.__series.setRectOfInterest(rect)
+
+ def swapData(self, series):
+ """
+ Replace a series without deleting the previous one
+
+ :param qwt.plot_series.QwtSeriesData series: New series
+ :return: Previously assigned series
+ """
+ swappedSeries = self.__series
+ self.__series = series
+ return swappedSeries
diff --git a/qwt/plot_seriesitem.py b/qwt/plot_seriesitem.py
deleted file mode 100644
index b52ee42..0000000
--- a/qwt/plot_seriesitem.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# -*- 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)
-
-from qwt.plot import QwtPlotItem, QwtPlotItem_PrivateData
-from qwt.text import QwtText
-from qwt.series_store import QwtAbstractSeriesStore
-
-from qwt.qt.QtCore import Qt, QRectF
-
-
-class QwtPlotSeriesItem_PrivateData(QwtPlotItem_PrivateData):
- def __init__(self):
- QwtPlotItem_PrivateData.__init__(self)
- self.orientation = Qt.Vertical
-
-
-class QwtPlotSeriesItem(QwtPlotItem, QwtAbstractSeriesStore):
- def __init__(self, title):
- QwtAbstractSeriesStore.__init__(self)
- if not isinstance(title, QwtText):
- title = QwtText(title)
- QwtPlotItem.__init__(self, title)
- self.__data = QwtPlotSeriesItem_PrivateData()
-
- def setOrientation(self, orientation):
- if self.__data.orientation != orientation:
- self.__data.orientation = orientation
- self.legendChanged()
- self.itemChanged()
-
- def orientation(self):
- return self.__data.orientation
-
- def draw(self, painter, xMap, yMap, canvasRect):
- self.drawSeries(painter, xMap, yMap, canvasRect, 0, -1)
-
- def boundingRect(self):
- return self.dataRect()
-
- def updateScaleDiv(self, xScaleDiv, yScaleDiv):
- rect = QRectF(xScaleDiv.lowerBound(), yScaleDiv.lowerBound(),
- xScaleDiv.range(), yScaleDiv.range())
- self.setRectOfInterest(rect)
-
- def dataChanged(self):
- self.itemChanged()
diff --git a/qwt/point_data.py b/qwt/point_data.py
deleted file mode 100644
index 2048ffe..0000000
--- a/qwt/point_data.py
+++ /dev/null
@@ -1,106 +0,0 @@
-# -*- 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)
-
-from qwt.series_data import QwtSeriesData, qwtBoundingRect
-from qwt.interval import QwtInterval
-
-from qwt.qt.QtCore import QPointF, QRectF
-import numpy as np
-
-
-class QwtPointArrayData(QwtSeriesData):
- def __init__(self, x, y, size=None):
- QwtSeriesData.__init__(self)
- if isinstance(x, (tuple, list)):
- x = np.array(x)
- if isinstance(y, (tuple, list)):
- y = np.array(y)
- self.__x = x
- self.__y = y
-
- def boundingRect(self):
- xmin = self.__x.min()
- xmax = self.__x.max()
- ymin = self.__y.min()
- ymax = self.__y.max()
- return QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
-
- def size(self):
- return min([self.__x.size, self.__y.size])
-
- def sample(self, index):
- return QPointF(self.__x[index], self.__y[index])
-
- def xData(self):
- return self.__x
-
- def yData(self):
- return self.__y
-
-
-class QwtCPointerData(QwtPointArrayData):
- def __init__(self, x, y, size):
- QwtSeriesData.__init__(self)
- self.__x = x
- self.__y = y
- self.__size = size
-
- def size(self):
- return self.__size
-
-
-class QwtSyntheticPointData(QwtSeriesData):
- def __init__(self, size, interval):
- QwtSeriesData.__init__(self)
- self.__size = size
- self.__interval = interval
- self.__rectOfInterest = None
- self.__intervalOfInterest = None
-
- def setSize(self, size):
- self.__size = size
-
- def size(self):
- return self.__size
-
- def setInterval(self, interval):
- self.__interval = interval.normalized()
-
- def interval(self):
- return self.__interval
-
- def setRectOfInterest(self, rect):
- self.__rectOfInterest = rect
- self.__intervalOfInterest = QwtInterval(rect.left(), rect.right()
- ).normalized()
-
- def rectOfInterest(self):
- return self.__rectOfInterest
-
- def boundingRect(self):
- if self.__size == 0 or\
- not (self.__interval.isValid() or self.__intervalOfInterest.isValid()):
- return QRectF(1.0, 1.0, -2.0, -2.0)
- return qwtBoundingRect(self)
-
- def sample(self, index):
- if index >= self.__size:
- return QPointF(0, 0)
- xValue = self.x(index)
- yValue = self.y(xValue)
- return QPointF(xValue, yValue)
-
- def x(self, index):
- if self.__interval.isValid():
- interval = self.__interval
- else:
- interval = self.__intervalOfInterest
- if not interval.isValid() or self.__size == 0 or index >= self.__size:
- return 0.
- dx = interval.width()/self.__size
- return interval.minValue() + index*dx
-
\ No newline at end of file
diff --git a/qwt/point_mapper.py b/qwt/point_mapper.py
deleted file mode 100644
index c8745f4..0000000
--- a/qwt/point_mapper.py
+++ /dev/null
@@ -1,271 +0,0 @@
-# -*- 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)
-
-USE_THREADS = False # QtConcurrent is not supported by PyQt
-
-from qwt.qt.QtGui import QPolygon, QPolygonF, QImage, QPainter
-from qwt.qt.QtCore import QThread, Qt, QPoint, QPointF, QRectF
-
-from qwt.pixel_matrix import QwtPixelMatrix
-
-import numpy as np
-
-
-class QwtDotsCommand(object):
- def __init__(self):
- self.series = None
- self.from_ = None
- self.to = None
- self.rgb = None
-
-def qwtRenderDots(xMap, yMap, command, pos, image):
- rgb = command.rgb
- bits = image.bits()
- w = image.width()
- h = image.height()
- x0 = pos.x()
- y0 = pos.y()
- for i in range(command.from_, command.to+1):
- sample = command.series.sample(i)
- x = int(xMap.transform(sample.x())+0.5)-x0
- y = int(yMap.transform(sample.y())+0.5)-y0
- if x >= 0 and x < w and y >= 0 and y < h:
- bits[y*w+x] = rgb
-
-
-def qwtToPoints(boundingRect, xMap, yMap, series, from_, to, round_,
- Polygon):
- Point = QPointF if isinstance(Polygon, QPolygonF) else QPoint
- points = []
- if boundingRect.isValid():
- for i in range(from_, to+1):
- sample = series.sample(i)
- x = xMap.transform(sample.x())
- y = yMap.transform(sample.y())
- if boundingRect.contains(x, y):
- points.append(Point(round_(x), round_(y)))
- else:
- for i in range(from_, to+1):
- sample = series.sample(i)
- x = xMap.transform(sample.x())
- y = yMap.transform(sample.y())
- points.append(Point(round_(x), round_(y)))
- return Polygon(list(set(points)))
-
-def qwtToPointsI(boundingRect, xMap, yMap, series, from_, to):
- return qwtToPoints(boundingRect, xMap, yMap, series, from_, to, round,
- QPolygon)
-
-def qwtToPointsF(boundingRect, xMap, yMap, series, from_, to, round_):
- return qwtToPoints(boundingRect, xMap, yMap, series, from_, to, round_,
- QPolygonF)
-
-
-def qwtToPolylineFiltered(xMap, yMap, series, from_, to, round_,
- Polygon, Point):
- polyline = Polygon(to-from_+1)
- pointer = polyline.data()
- dtype = np.float if Polygon is QPolygonF else np.int
- pointer.setsize(2*polyline.size()*np.finfo(dtype).dtype.itemsize)
- memory = np.frombuffer(pointer, dtype)
- memory[from_*2:to*2+1:2] =\
- np.round(xMap.transform(series.xData()))[from_:to+1]
- memory[from_*2+1:to*2+2:2] =\
- np.round(yMap.transform(series.yData()))[from_:to+1]
- return polyline
-# points = polyline.data()
-# sample0 = series.sample(from_)
-# points[0].setX(round_(xMap.transform(sample0.x())))
-# points[0].setY(round_(yMap.transform(sample0.y())))
-# pos = 0
-# for i in range(from_, to+1):
-# sample = series.sample(i)
-# p = Point(round_(xMap.transform(sample.x())),
-# round_(yMap.transform(sample.y())))
-# if points[pos] != p:
-# pos += 1
-# points[pos] = p
-# polyline.resize(pos+1)
-# return polyline
-
-def qwtToPolylineFilteredI(xMap, yMap, series, from_, to):
- return qwtToPolylineFiltered(xMap, yMap, series, from_, to, round,
- QPolygon, QPoint)
-
-def qwtToPolylineFilteredF(xMap, yMap, series, from_, to, round_):
- return qwtToPolylineFiltered(xMap, yMap, series, from_, to, round_,
- QPolygonF, QPointF)
-
-
-def qwtToPointsFiltered(boundingRect, xMap, yMap, series, from_, to,
- Polygon):
- Point = QPointF if isinstance(Polygon, QPolygonF) else QPoint
- if isinstance(boundingRect, QRectF):
- pixelMatrix = QwtPixelMatrix(boundingRect.toAlignedRect())
- else:
- pixelMatrix = QwtPixelMatrix(boundingRect)
- points = []
- for i in range(from_, to+1):
- sample = series.sample(i)
- x = int(round(xMap.transform(sample.x())))
- y = int(round(yMap.transform(sample.y())))
- if pixelMatrix.testAndSetPixel(x, y, True) == False:
- points.append(Point(x, y))
- return Polygon(list(points))
-
-def qwtToPointsFilteredI(boundingRect, xMap, yMap, series, from_, to):
- return qwtToPointsFiltered(boundingRect, xMap, yMap, series, from_, to,
- QPolygon)
-
-def qwtToPointsFilteredF(boundingRect, xMap, yMap, series, from_, to):
- return qwtToPointsFiltered(boundingRect, xMap, yMap, series, from_, to,
- QPolygonF)
-
-
-class QwtPointMapper_PrivateData(object):
- def __init__(self):
- self.boundingRect = None
- self.flags = 0
-
-
-class QwtPointMapper(object):
- RoundPoints = 0x01
- WeedOutPoints = 0x02
-
- def __init__(self):
- self.__data = QwtPointMapper_PrivateData()
- self.qwtInvalidRect = QRectF(0.0, 0.0, -1.0, -1.0)
- self.setBoundingRect(self.qwtInvalidRect)
-
- def setFlags(self, flags):
- self.__data.flags = flags
-
- def flags(self):
- return self.__data.flags
-
- def setFlag(self, flag, on=True):
- if on:
- self.__data.flags |= flag
- else:
- self.__data.flags &= ~flag
-
- def testFlag(self, flag):
- return self.__data.flags & flag
-
- def setBoundingRect(self, rect):
- self.__data.boundingRect = rect
-
- def boundingRect(self):
- return self.__data.boundingRect
-
- def toPolygonF(self, xMap, yMap, series, from_, to):
- round_ = round
- no_round = lambda x: x
- if self.__data.flags & QwtPointMapper.WeedOutPoints:
- if self.__data.flags & QwtPointMapper.RoundPoints:
- polyline = qwtToPolylineFilteredF(xMap, yMap, series,
- from_, to, round_)
- else:
- polyline = qwtToPolylineFilteredF(xMap, yMap, series,
- from_, to, no_round)
- else:
- if self.__data.flags & QwtPointMapper.RoundPoints:
- polyline = qwtToPointsF(self.qwtInvalidRect, xMap, yMap,
- series, from_, to, round_)
- else:
- polyline = qwtToPointsF(self.qwtInvalidRect, xMap, yMap,
- series, from_, to, no_round)
- return polyline
-
- def toPolygon(self, xMap, yMap, series, from_, to):
- if self.__data.flags & QwtPointMapper.WeedOutPoints:
- polyline = qwtToPolylineFilteredI(xMap, yMap, series, from_, to)
- else:
- polyline = qwtToPointsI(self.qwtInvalidRect, xMap, yMap,
- series, from_, to)
- return polyline
-
- def toPointsF(self, xMap, yMap, series, from_, to):
- round_ = round
- no_round = lambda x: x
- if self.__data.flags & QwtPointMapper.WeedOutPoints:
- if self.__data.flags & QwtPointMapper.RoundPoints:
- if self.__data.boundingRect.isValid():
- points = qwtToPointsFilteredF(self.__data.boundingRect,
- xMap, yMap, series, from_, to)
- else:
- points = qwtToPolylineFilteredF(xMap, yMap, series,
- from_, to, round_)
- else:
- points = qwtToPolylineFilteredF(xMap, yMap, series,
- from_, to, no_round)
- else:
- if self.__data.flags & QwtPointMapper.RoundPoints:
- points = qwtToPointsF(self.__data.boundingRect,
- xMap, yMap, series, from_, to, round_)
- else:
- points = qwtToPointsF(self.__data.boundingRect,
- xMap, yMap, series, from_, to, no_round)
- return points
-
- def toPoints(self, xMap, yMap, series, from_, to):
- if self.__data.flags & QwtPointMapper.WeedOutPoints:
- if self.__data.boundingRect.isValid():
- points = qwtToPointsFilteredI(self.__data.boundingRect,
- xMap, yMap, series, from_, to)
- else:
- points = qwtToPolylineFilteredI(xMap, yMap, series, from_, to)
- else:
- points = qwtToPointsI(self.__data.boundingRect, xMap, yMap,
- series, from_, to)
- return points
-
- def toImage(self, xMap, yMap, series, from_, to, pen, antialiased,
- numThreads):
- if USE_THREADS:
- if numThreads == 0:
- numThreads = QThread.idealThreadCount()
- if numThreads <= 0:
- numThreads = 1
- rect = self.__data.boundingRect.toAlignedRect()
- image = QImage(rect.size(), QImage.Format_ARGB32)
- image.fill(Qt.transparent)
- if pen.width() <= 1 and pen.color().alpha() == 255:
- command = QwtDotsCommand()
- command.series = series
- command.rgb = pen.color().rgba()
- if USE_THREADS:
- numPoints = int((to-from_+1)/numThreads)
- futures = []
- for i in range(numThreads):
- pos = rect.topLeft()
- index0 = from_ + i*numPoints
- if i == numThreads-1:
- command.from_ = index0
- command.to = to
- qwtRenderDots(xMap, yMap, command, pos, image)
- else:
- command.from_ = index0
- command.to = index0 + numPoints - 1
- futures += [QtConcurrent.run(qwtRenderDots,
- xMap, yMap, command, pos, image)]
- for future in futures:
- future.waitForFinished()
- else:
- command.from_ = from_
- command.to = to
- qwtRenderDots(xMap, yMap, command, rect.topLeft(), image)
- else:
- painter = QPainter(image)
- painter.setPen(pen)
- painter.setRenderHint(QPainter.Antialiasing, antialiased)
- chunkSize = 1000
- for i in range(chunkSize):
- indexTo = min([i+chunkSize-1, to])
- points = self.toPoints(xMap, yMap, series, i, indexTo)
- painter.drawPoints(points)
- return image
diff --git a/qwt/py3compat.py b/qwt/py3compat.py
deleted file mode 100644
index a1929a0..0000000
--- a/qwt/py3compat.py
+++ /dev/null
@@ -1,230 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2012-2013 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-"""
-guidata.py3compat (exact copy of spyderlib.py3compat)
------------------------------------------------------
-
-Transitional module providing compatibility functions intended to help
-migrating from Python 2 to Python 3.
-
-This module should be fully compatible with:
- * Python >=v2.6
- * Python 3
-"""
-
-from __future__ import print_function
-
-import sys
-import os
-
-PY2 = sys.version[0] == '2'
-PY3 = sys.version[0] == '3'
-
-
-#==============================================================================
-# Data types
-#==============================================================================
-if PY2:
- # Python 2
- TEXT_TYPES = (str, unicode)
- INT_TYPES = (int, long)
-else:
- # Python 3
- TEXT_TYPES = (str,)
- INT_TYPES = (int,)
-NUMERIC_TYPES = tuple(list(INT_TYPES) + [float, complex])
-
-
-#==============================================================================
-# Renamed/Reorganized modules
-#==============================================================================
-if PY2:
- # Python 2
- import __builtin__ as builtins
- import ConfigParser as configparser
- try:
- import _winreg as winreg
- except ImportError:
- pass
- from sys import maxint as maxsize
- try:
- import CStringIO as io
- except ImportError:
- import StringIO as io
- try:
- import cPickle as pickle
- except ImportError:
- import pickle
- from UserDict import DictMixin as MutableMapping
-else:
- # Python 3
- import builtins
- import configparser
- try:
- import winreg
- except ImportError:
- pass
- from sys import maxsize
- import io
- import pickle
- from collections import MutableMapping
-
-
-#==============================================================================
-# Strings
-#==============================================================================
-def is_text_string(obj):
- """Return True if `obj` is a text string, False if it is anything else,
- like binary data (Python 3) or QString (Python 2, PyQt API #1)"""
- if PY2:
- # Python 2
- return isinstance(obj, basestring)
- else:
- # Python 3
- return isinstance(obj, str)
-
-def is_binary_string(obj):
- """Return True if `obj` is a binary string, False if it is anything else"""
- if PY2:
- # Python 2
- return isinstance(obj, str)
- else:
- # Python 3
- return isinstance(obj, bytes)
-
-def is_string(obj):
- """Return True if `obj` is a text or binary Python string object,
- False if it is anything else, like a QString (Python 2, PyQt API #1)"""
- return is_text_string(obj) or is_binary_string(obj)
-
-def is_unicode(obj):
- """Return True if `obj` is unicode"""
- if PY2:
- # Python 2
- return isinstance(obj, unicode)
- else:
- # Python 3
- return isinstance(obj, str)
-
-def to_text_string(obj, encoding=None):
- """Convert `obj` to (unicode) text string"""
- if PY2:
- # Python 2
- if encoding is None:
- return unicode(obj)
- else:
- return unicode(obj, encoding)
- else:
- # Python 3
- if encoding is None:
- return str(obj)
- elif isinstance(obj, str):
- # In case this function is not used properly, this could happen
- return obj
- else:
- return str(obj, encoding)
-
-def to_binary_string(obj, encoding=None):
- """Convert `obj` to binary string (bytes in Python 3, str in Python 2)"""
- if PY2:
- # Python 2
- if encoding is None:
- return str(obj)
- else:
- return obj.encode(encoding)
- else:
- # Python 3
- return bytes(obj, 'utf-8' if encoding is None else encoding)
-
-
-#==============================================================================
-# Function attributes
-#==============================================================================
-def get_func_code(func):
- """Return function code object"""
- if PY2:
- # Python 2
- return func.func_code
- else:
- # Python 3
- return func.__code__
-
-def get_func_name(func):
- """Return function name"""
- if PY2:
- # Python 2
- return func.func_name
- else:
- # Python 3
- return func.__name__
-
-def get_func_defaults(func):
- """Return function default argument values"""
- if PY2:
- # Python 2
- return func.func_defaults
- else:
- # Python 3
- return func.__defaults__
-
-
-#==============================================================================
-# Special method attributes
-#==============================================================================
-def get_meth_func(obj):
- """Return method function object"""
- if PY2:
- # Python 2
- return obj.im_func
- else:
- # Python 3
- return obj.__func__
-
-def get_meth_class_inst(obj):
- """Return method class instance"""
- if PY2:
- # Python 2
- return obj.im_self
- else:
- # Python 3
- return obj.__self__
-
-def get_meth_class(obj):
- """Return method class"""
- if PY2:
- # Python 2
- return obj.im_class
- else:
- # Python 3
- return obj.__self__.__class__
-
-
-#==============================================================================
-# Misc.
-#==============================================================================
-if PY2:
- # Python 2
- input = raw_input
- getcwd = os.getcwdu
- cmp = cmp
- import string
- str_lower = string.lower
-else:
- # Python 3
- input = input
- getcwd = os.getcwd
- def cmp(a, b):
- return (a > b) - (a < b)
- str_lower = str.lower
-
-def qbytearray_to_str(qba):
- """Convert QByteArray object to str in a way compatible with Python 2/3"""
- return str(bytes(qba.toHex()).decode())
-
-
-if __name__ == '__main__':
- pass
diff --git a/qwt/qt/QtCore.py b/qwt/qt/QtCore.py
deleted file mode 100644
index e43f631..0000000
--- a/qwt/qt/QtCore.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-import os
-
-if os.environ['QT_API'] == 'pyqt5':
- from PyQt5.QtCore import * # analysis:ignore
- from PyQt5.QtCore import QCoreApplication
- from PyQt5.QtCore import pyqtSignal as Signal
- from PyQt5.QtCore import pyqtSlot as Slot
- from PyQt5.QtCore import pyqtProperty as Property
- from PyQt5.QtCore import QT_VERSION_STR as __version__
-elif os.environ['QT_API'] == 'pyqt':
- from PyQt4.QtCore import * # analysis:ignore
- from PyQt4.Qt import QCoreApplication # analysis:ignore
- from PyQt4.Qt import Qt # analysis:ignore
- from PyQt4.QtCore import pyqtSignal as Signal # analysis:ignore
- from PyQt4.QtCore import pyqtSlot as Slot # analysis:ignore
- from PyQt4.QtCore import pyqtProperty as Property # analysis:ignore
- from PyQt4.QtCore import QT_VERSION_STR as __version__ # analysis:ignore
- # Forces new modules written by PyQt4 developers to be PyQt5-compatible
- del SIGNAL, SLOT
-else:
- import PySide.QtCore
- __version__ = PySide.QtCore.__version__ # analysis:ignore
- from PySide.QtCore import * # analysis:ignore
diff --git a/qwt/qt/QtGui.py b/qwt/qt/QtGui.py
deleted file mode 100644
index 9082327..0000000
--- a/qwt/qt/QtGui.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-import os
-
-if os.environ['QT_API'] == 'pyqt5':
- from PyQt5.QtCore import QSortFilterProxyModel # analysis:ignore
- from PyQt5.QtPrintSupport import (QPrinter, QPrintDialog, # analysis:ignore
- QAbstractPrintDialog)
- from PyQt5.QtPrintSupport import QPrintPreviewDialog # analysis:ignore
- from PyQt5.QtGui import * # analysis:ignore
- from PyQt5.QtWidgets import * # analysis:ignore
-elif os.environ['QT_API'] == 'pyqt':
- from PyQt4.Qt import QKeySequence, QTextCursor # analysis:ignore
- from PyQt4.QtGui import * # analysis:ignore
-else:
- from PySide.QtGui import * # analysis:ignore
diff --git a/qwt/qt/QtSvg.py b/qwt/qt/QtSvg.py
deleted file mode 100644
index c142165..0000000
--- a/qwt/qt/QtSvg.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2012 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-import os
-
-if os.environ['QT_API'] == 'pyqt5':
- from PyQt5.QtSvg import * # analysis:ignore
-elif os.environ['QT_API'] == 'pyqt':
- from PyQt4.QtSvg import * # analysis:ignore
-else:
- from PySide.QtSvg import * # analysis:ignore
diff --git a/qwt/qt/QtWebKit.py b/qwt/qt/QtWebKit.py
deleted file mode 100644
index e0ed4a8..0000000
--- a/qwt/qt/QtWebKit.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-import os
-
-if os.environ['QT_API'] == 'pyqt5':
- from PyQt5.QtWebKitWidgets import QWebPage, QWebView # analysis:ignore
- from PyQt5.QtWebKit import QWebSettings # analysis:ignore
-elif os.environ['QT_API'] == 'pyqt':
- from PyQt4.QtWebKit import (QWebPage, QWebView, # analysis:ignore
- QWebSettings)
-else:
- from PySide.QtWebKit import * # analysis:ignore
diff --git a/qwt/qt/__init__.py b/qwt/qt/__init__.py
deleted file mode 100644
index 13b421a..0000000
--- a/qwt/qt/__init__.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011-2012 Pierre Raybaut
-# © 2012-2014 anatoly techtonik
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-"""Transitional package (PyQt4 --> PySide)"""
-
-import os
-
-os.environ.setdefault('QT_API', 'pyqt')
-assert os.environ['QT_API'] in ('pyqt5', 'pyqt', 'pyside')
-
-API = os.environ['QT_API']
-API_NAME = {'pyqt5': 'PyQt5', 'pyqt': 'PyQt4', 'pyside': 'PySide'}[API]
-
-PYQT5 = False
-
-if API == 'pyqt5':
- try:
- from PyQt5.QtCore import PYQT_VERSION_STR as __version__
- is_old_pyqt = False
- is_pyqt46 = False
- PYQT5 = True
- except ImportError:
- pass
-elif API == 'pyqt':
- # Spyder 2.3 is compatible with both #1 and #2 PyQt API,
- # but to avoid issues with IPython and other Qt plugins
- # we choose to support only API #2 for 2.4+
- import sip
- try:
- sip.setapi('QString', 2)
- sip.setapi('QVariant', 2)
- sip.setapi('QDate', 2)
- sip.setapi('QDateTime', 2)
- sip.setapi('QTextStream', 2)
- sip.setapi('QTime', 2)
- sip.setapi('QUrl', 2)
- except AttributeError:
- # PyQt < v4.6. The actual check is done by requirements.check_qt()
- # call from spyder.py
- pass
-
- try:
- from PyQt4.QtCore import PYQT_VERSION_STR as __version__ # analysis:ignore
- except ImportError:
- # Switching to PySide
- API = os.environ['QT_API'] = 'pyside'
- API_NAME = 'PySide'
- else:
- is_old_pyqt = __version__.startswith(('4.4', '4.5', '4.6', '4.7'))
- is_pyqt46 = __version__.startswith('4.6')
- import sip
- try:
- API_NAME += (" (API v%d)" % sip.getapi('QString'))
- except AttributeError:
- pass
-
-
-if API == 'pyside':
- try:
- from PySide import __version__ # analysis:ignore
- except ImportError:
- raise ImportError("Spyder requires PySide or PyQt to be installed")
- else:
- is_old_pyqt = is_pyqt46 = False
diff --git a/qwt/qt/compat.py b/qwt/qt/compat.py
deleted file mode 100644
index 23ffcf3..0000000
--- a/qwt/qt/compat.py
+++ /dev/null
@@ -1,209 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011-2012 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-"""
-spyderlib.qt.compat
--------------------
-
-Transitional module providing compatibility functions intended to help
-migrating from PyQt to PySide.
-
-This module should be fully compatible with:
- * PyQt >=v4.4
- * both PyQt API #1 and API #2
- * PySide
-"""
-
-from __future__ import print_function
-
-import os
-import sys
-import collections
-
-from qwt.qt.QtGui import QFileDialog
-from qwt.py3compat import is_text_string, to_text_string, TEXT_TYPES
-
-#==============================================================================
-# QVariant conversion utilities
-#==============================================================================
-
-PYQT_API_1 = False
-if os.environ['QT_API'] == 'pyqt':
- import sip
- try:
- PYQT_API_1 = sip.getapi('QVariant') == 1 # PyQt API #1
- except AttributeError:
- # PyQt Developped by Pierre Raybaut
+
Copyright © 2020 Pierre Raybaut
+
%s""" + % (self.windowTitle(), get_lib_versions()), + ) + + +class TestOptions(QW.QGroupBox): + """Test options groupbox""" + + def __init__(self, parent=None): + super(TestOptions, self).__init__("Test options", parent) + self.setLayout(QW.QFormLayout()) + self.hide() + + def add_checkbox(self, title, label, slot): + """Add new checkbox to option panel""" + widget = QW.QCheckBox(label, self) + widget.stateChanged.connect(slot) + self.layout().addRow(title, widget) + self.show() + return widget + + +class TestCentralWidget(QW.QWidget): + """Test central widget""" + + def __init__(self, widget_name, parent=None): + super(TestCentralWidget, self).__init__(parent) + self.widget_name = widget_name + self.plots = None + self.setLayout(QW.QVBoxLayout()) + self.options = TestOptions(self) + self.add_widget(self.options) + + def get_widget_of_interest(self): + """Return widget of interest""" + if self.plots is not None and len(self.plots) == 1: + return self.plots[0] + return self.parent() + + def add_widget(self, widget): + """Add new sub-widget""" + self.layout().addWidget(widget) + if isinstance(widget, QwtPlot): + self.plots = [widget] + else: + self.plots = widget.findChildren(QwtPlot) + for index, plot in enumerate(self.plots): + plot_name = plot.objectName() + if not plot_name: + plot_name = "Plot #%d" % (index + 1) + widget = self.options.add_checkbox( + plot_name, "Enable new flat style option", plot.setFlatStyle + ) + widget.setChecked(plot.flatStyle()) + + +def take_screenshot(widget): + """Take screenshot and save it to the data folder""" + bname = (widget.objectName().lower() + ".png").replace("window", "") + bname = bname.replace("plot", "").replace("widget", "") + qth.take_screenshot(widget, osp.join(TEST_PATH, "data", bname), quit=True) + + +def close_widgets_and_quit() -> None: + """Close Qt top level widgets and quit Qt event loop""" + QW.QApplication.processEvents() + for widget in QW.QApplication.instance().topLevelWidgets(): + assert widget.close() + QC.QTimer.singleShot(0, QW.QApplication.instance().quit) + + +def test_widget(widget_class, size=None, title=None, options=True): + """Test widget""" + widget_name = widget_class.__name__ + app = QW.QApplication.instance() + if app is None: + app = QW.QApplication([]) + test_env = TestEnvironment() + if inspect.signature(widget_class).parameters.get("unattended") is None: + widget = widget_class() + else: + widget = widget_class(unattended=test_env.unattended) + window = widget + if options: + if isinstance(widget, QW.QMainWindow): + widget = window.centralWidget() + widget.setParent(None) + else: + window = QW.QMainWindow() + central_widget = TestCentralWidget(widget_name, parent=window) + central_widget.add_widget(widget) + window.setCentralWidget(central_widget) + widget_of_interest = central_widget.get_widget_of_interest() + else: + widget_of_interest = window + widget_of_interest.setObjectName(widget_name) + if title is None: + title = 'Test "%s" - PythonQwt %s' % (widget_name, qwt.__version__) + window.setWindowTitle(title) + if size is not None: + width, height = size + window.resize(width, height) + + window.show() + if test_env.screenshots: + QC.QTimer.singleShot(1000, lambda: take_screenshot(widget_of_interest)) + elif test_env.unattended: + QC.QTimer.singleShot(0, close_widgets_and_quit) + if QT_API == "pyside6": + app.exec() + else: + app.exec_() + return app diff --git a/qwt/text.py b/qwt/text.py index 3f09287..2fb0f9a 100644 --- a/qwt/text.py +++ b/qwt/text.py @@ -5,17 +5,577 @@ # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) +""" +Text widgets +------------ + +QwtText +~~~~~~~ + +.. autoclass:: QwtText + :members: + +QwtTextLabel +~~~~~~~~~~~~ + +.. autoclass:: QwtTextLabel + :members: + +Text engines +------------ + +QwtTextEngine +~~~~~~~~~~~~~ + +.. autoclass:: QwtTextEngine + :members: + +QwtPlainTextEngine +~~~~~~~~~~~~~~~~~~ + +.. autoclass:: QwtPlainTextEngine + :members: + +QwtRichTextEngine +~~~~~~~~~~~~~~~~~ + +.. autoclass:: QwtRichTextEngine + :members: +""" + +import math +import os +import struct + +from qtpy.QtCore import QObject, QRectF, QSize, QSizeF, Qt +from qtpy.QtGui import ( + QAbstractTextDocumentLayout, + QColor, + QFont, + QFontInfo, + QFontMetrics, + QFontMetricsF, + QPainter, + QPalette, + QPixmap, + QTextDocument, + QTextOption, + QTransform, +) +from qtpy.QtWidgets import QApplication, QFrame, QSizePolicy, QWidget + from qwt.painter import QwtPainter -from qwt.text_engine import QwtPlainTextEngine, QwtRichTextEngine +from qwt.qthelpers import qcolor_from_str + +QWIDGETSIZE_MAX = (1 << 24) - 1 + +QT_API = os.environ["QT_API"] + + +# Cache Qt alignment flags as plain ints once at import time. On PyQt6 these +# are ``Qt.AlignmentFlag`` enum members and every bitwise test goes through +# ``enum.__and__`` (~6 us each). The test code below combines them in hot +# paths called per-tick / per-label / per-paint event. +def _flag_int(flag): + """Return the integer value of a Qt enum/flag (PyQt5 and PyQt6).""" + try: + return flag.value + except AttributeError: + return int(flag) + + +_ALIGN_LEFT = _flag_int(Qt.AlignLeft) +_ALIGN_RIGHT = _flag_int(Qt.AlignRight) +_ALIGN_TOP = _flag_int(Qt.AlignTop) +_ALIGN_BOTTOM = _flag_int(Qt.AlignBottom) +_ALIGN_HCENTER = _flag_int(Qt.AlignHCenter) +_ALIGN_JUSTIFY = _flag_int(Qt.AlignJustify) +_ALIGN_CENTER = _flag_int(Qt.AlignCenter) + + +def taggedRichText(text, flags): + richText = text + if flags & _ALIGN_JUSTIFY: + richText = '