frappy/secop/gui/miniplot.py
Markus Zolliker c1164568ae further fixes of py3 issues
complaints by pylint are mainly related to
- remove object from base list in class definitions
- unnecessary else/elif after return/raise

Change-Id: I13d15449149cc8bba0562338d0c9c42e97163bdf
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/21325
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2019-09-26 14:15:48 +02:00

430 lines
14 KiB
Python

# -*- coding: utf-8 -*-
# *****************************************************************************
# Copyright (c) 2015-2016 by the authors, see LICENSE
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
from os import path
from secop.gui.qt import QBrush, QColor, QPainter, QPen, \
QPointF, QPolygonF, QRectF, QSize, Qt, QWidget
_magenta = QBrush(QColor('#A12F86'))
_yellow = QBrush(QColor('yellow'))
_white = QBrush(QColor('white'))
_lightgrey = QBrush(QColor('lightgrey'))
_grey = QBrush(QColor('grey'))
_darkgrey = QBrush(QColor('#404040'))
_black = QBrush(QColor('black'))
_blue = QBrush(QColor('blue'))
_green = QBrush(QColor('green'))
_red = QBrush(QColor('red'))
_olive = QBrush(QColor('olive'))
_orange = QBrush(QColor('#ffa500'))
my_uipath = path.dirname(__file__)
class MiniPlotCurve:
# placeholder for data
linecolor = _black
linewidth = 0 # set to 0 to disable lines
symbolcolors = (_black, _white) # line, fill
symbolsize = 3 # both symbol linewidth and symbolsize, set to 0 to disable
errorbarcolor = _darkgrey
errorbarwidth = 3 # set to 0 to disable errorbar
def __init__(self):
self.data = [] # tripels of x, y, err (err may be None)
@property
def xvalues(self):
return [p[0] for p in self.data] if self.data else [0]
@property
def yvalues(self):
return [p[1] for p in self.data] if self.data else [0]
@property
def errvalues(self):
return [p[2] or 0.0 for p in self.data] if self.data else [0]
@property
def xmin(self):
return min(self.xvalues)
@property
def xmax(self):
return max(self.xvalues)
@property
def ymin(self):
return min(self.yvalues)
@property
def ymax(self):
return max(self.yvalues)
@property
def yemin(self):
return min(y-(e or 0) for _, y, e in self.data) if self.data else 0
@property
def yemax(self):
return max(y+(e or 0) for _, y, e in self.data) if self.data else 0
def paint(self, scale, painter):
# note: scale returns a screen-XY tuple for data XY
# draw errorbars, lines and symbols in that order
if self.errorbarwidth > 0:
pen = QPen()
pen.setBrush(self.errorbarcolor)
pen.setWidth(self.errorbarwidth)
painter.setPen(pen)
for _x,_y,_e in self.data:
if _e is None:
continue
x, y = scale(_x,_y)
e = scale(_x,_y + _e)[1] - y
painter.drawLine(x, y-e, x, y+e)
painter.fillRect(x - self.errorbarwidth / 2., y - e,
self.errorbarwidth, 2 * e, self.errorbarcolor)
points = [QPointF(*scale(p[0], p[1])) for p in self.data]
if self.linewidth > 0:
pen = QPen()
pen.setBrush(self.linecolor)
pen.setWidth(self.linewidth)
painter.setPen(pen)
painter.drawPolyline(QPolygonF(points))
if self.symbolsize > 0:
pen = QPen()
pen.setBrush(self.symbolcolors[0]) # linecolor
pen.setWidth(self.symbolsize) # linewidth
painter.setPen(pen)
painter.setBrush(self.symbolcolors[1]) # fill color
if self.symbolsize > 0:
for p in points:
painter.drawEllipse(p, 2*self.symbolsize, 2*self.symbolsize)
def preparepainting(self, scale, xmin, xmax):
pass # nothing to do
class MiniPlotFitCurve(MiniPlotCurve):
# do not influence scaling of plotting window
@property
def xmin(self):
return float('inf')
@property
def xmax(self):
return float('-inf')
@property
def ymin(self):
return float('inf')
@property
def ymax(self):
return float('-inf')
@property
def yemin(self):
return float('inf')
@property
def yemax(self):
return float('-inf')
def __init__(self, formula, params):
super(MiniPlotFitCurve, self).__init__()
self.formula = formula
self.params = params
linecolor = _blue
linewidth = 5 # set to 0 to disable lines
symbolsize = 0 # both symbol linewidth and symbolsize, set to 0 to disable
errorbarwidth = 0 # set to 0 to disable errorbar
def preparepainting(self, scale, xmin, xmax):
# recalculate data
points = int(scale(xmax) - scale(xmin))
self.data = []
for idx in range(points+1):
x = xmin + idx * (xmax-xmin) / points
y = self.formula(x, *self.params)
self.data.append((x,y,None))
class MiniPlot(QWidget):
ticklinecolors = (_grey, _lightgrey) # ticks, subticks
ticklinewidth = 1
bordercolor = _black
borderwidth = 1
labelcolor = _black
xlabel = 'x'
ylabel = 'y'
xfmt = '%.1f'
yfmt = '%g'
autotickx = True
autoticky = True
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.xmin = self.xmax = None
self.ymin = self.ymax = None
self.curves = []
self.plotx = 0 # left of this are labels
self.ploty = self.height() # below this are labels
def scaleX(self, x):
if not self.curves:
return x # XXX: !!!!
x = self.plotx + (self.width() - self.plotx) * (x - self.xmin) / (self.xmax - self.xmin)
# x = max(min(x, self.width()), self.plotx)
return x
def scaleY(self, y):
if not self.curves:
return y # XXX: !!!!
y = self.ploty * (self.ymax - y) / (self.ymax - self.ymin)
# y = max(min(y, self.ploty), 0)
return y
def scale(self, x, y):
# scales a plotting xx/y to a screen x/y to be used for painting...
return self.scaleX(x), self.scaleY(y)
def removeCurve(self, curve):
if curve in self.curves:
self.curves.remove(curve)
self.updatePlot()
def addCurve(self, curve):
if curve is not None and curve not in self.curves:
# new curve, recalculate all
self.curves.append(curve)
self.updatePlot()
def updatePlot(self):
xmin,xmax = -1,1
ymin,ymax = -1,1
# find limits of known curves
if self.curves:
xmin = min(c.xmin for c in self.curves)
xmax = max(c.xmax for c in self.curves)
ymin = min(c.yemin for c in self.curves)
ymax = max(c.yemax for c in self.curves)
# fallback values for no curve
while xmin >= xmax:
xmin, xmax = xmin - 1, xmax + 1
while ymin >= ymax:
ymin, ymax = ymin - 1, ymax + 1
# adjust limits a little
self.xmin = xmin - 0.05 * (xmax - xmin)
self.xmax = xmax + 0.05 * (xmax - xmin)
self.ymin = ymin - 0.05 * (ymax - ymin)
self.ymax = ymax + 0.05 * (ymax - ymin)
# (re-)generate x/yticks
if self.autotickx:
self.calc_xticks(xmin, xmax)
if self. autoticky:
self.calc_yticks(ymin, ymax)
# redraw
self.update()
def calc_xticks(self, xmin, xmax):
self.xticks = self.calc_ticks(xmin, xmax, self.xfmt)
def calc_yticks(self, ymin, ymax):
self.yticks = self.calc_ticks(ymin, ymax, self.yfmt)
def calc_ticks(self, _min, _max, fmt):
min_intervals = 2
diff = _max - _min
if diff <= 0:
return [0]
# find a 'good' step size
step = abs(diff / min_intervals)
# split into mantissa and exp.
expo = 0
while step >= 10:
step /= 10.
expo += 1
while step < 1:
step *= 10.
expo -= 1
# make step 'latch' into smalle bigger magic number
subs = 1
for n, subs in reversed([(1,5.), (1.5,3.), (2,4.), (3,3.), (5,5.), (10,2.)]):
if step >= n:
step = n
break
# convert back to normal number
while expo > 0:
step *= 10.
expo -= 1
while expo < 0:
step /= 10.
expo += 1
substep = step / subs
# round lower
rounded_min = step * int(_min / step)
# generate ticks list
ticks = []
x = rounded_min
while x + substep < _min:
x += substep
for _ in range(100):
if x < _max + substep:
break
# check if x is a tick or a subtick
x = substep * int(x / substep)
if abs(x - step * int(x / step)) <= substep / 2:
# tick
ticks.append((x, fmt % x))
else:
# subtick
ticks.append((x, ''))
x += substep
return ticks
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# obtain a few properties we need for proper drawing
painter.setFont(self.font())
fm = painter.fontMetrics()
label_height = fm.height()
self.plotx = 3 + 2 * label_height
self.ploty = self.height() - 3 - 2 * label_height
# fill bg of plotting area
painter.fillRect(self.plotx ,0,self.width()-self.plotx, self.ploty,_white)
# paint ticklines
if self.curves and self.ticklinewidth > 0:
for e in self.xticks:
try:
_x = e[0] # pylint: disable=unsubscriptable-object
_l = e[1] # pylint: disable=unsubscriptable-object
except TypeError:
_x = e
_l = self.xfmt % _x
x = self.scaleX(_x)
pen = QPen()
pen.setBrush(self.ticklinecolors[0 if _l else 1])
pen.setWidth(self.ticklinewidth)
painter.setPen(pen)
painter.drawLine(x, 0, x, self.ploty)
for e in self.yticks:
try:
_y = e[0] # pylint: disable=unsubscriptable-object
_l = e[1] # pylint: disable=unsubscriptable-object
except TypeError:
_y = e
_l = self.xfmt % _x
y = self.scaleY(_y)
pen = QPen()
pen.setBrush(self.ticklinecolors[0 if _l else 1])
pen.setWidth(self.ticklinewidth)
painter.setPen(pen)
painter.drawLine(self.plotx, y, self.width(), y)
# paint curves
painter.setClipRect(QRectF(self.plotx, 0, self.width()-self.plotx, self.ploty))
for c in self.curves:
c.preparepainting(self.scaleX, self.xmin, self.xmax)
c.paint(self.scale, painter)
painter.setClipping(False)
# paint frame
pen = QPen()
pen.setBrush(self.bordercolor)
pen.setWidth(self.borderwidth)
painter.setPen(pen)
painter.drawPolyline(QPolygonF([
QPointF(self.plotx, 0),
QPointF(self.width()-1, 0),
QPointF(self.width()-1, self.ploty),
QPointF(self.plotx, self.ploty),
QPointF(self.plotx, 0),
]))
# draw labels
painter.setBrush(self.labelcolor)
h2 = (self.height()-self.ploty)/2.
# XXX: offset axis labels from axis a little
painter.drawText(self.plotx, self.ploty + h2,
self.width() - self.plotx, h2,
Qt.AlignCenter | Qt.AlignVCenter, self.xlabel)
# rotate ylabel?
painter.resetTransform()
painter.translate(0, self.ploty / 2.)
painter.rotate(-90)
w = fm.width(self.ylabel)
painter.drawText(-w, -fm.height() / 2., w * 2, self.plotx,
Qt.AlignCenter | Qt.AlignTop, self.ylabel)
painter.resetTransform()
if self.curves:
for e in self.xticks:
try:
_x = e[0] # pylint: disable=unsubscriptable-object
l = e[1] # pylint: disable=unsubscriptable-object
except TypeError:
_x = e
l = self.xfmt % _x
x = self.scaleX(_x)
w = fm.width(l)
painter.drawText(x - w, self.ploty + 2, 2 * w, h2,
Qt.AlignCenter | Qt.AlignVCenter, l)
for e in self.yticks:
try:
_y = e[0] # pylint: disable=unsubscriptable-object
l = e[1] # pylint: disable=unsubscriptable-object
except TypeError:
_y = e
l = self.yfmt % _y
y = self.scaleY(_y)
w = fm.width(l)
painter.resetTransform()
painter.translate(self.plotx - fm.height(), y + w)
painter.rotate(-90)
painter.drawText(0, -1,
2 * w, fm.height(),
Qt.AlignCenter | Qt.AlignBottom, l)
painter.resetTransform()
def sizeHint(self):
return QSize(320, 240)