You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
488 lines
16 KiB
488 lines
16 KiB
#!/usr/bin/env python |
|
# -*- coding: utf-8 -*- |
|
from gnuradio import gr |
|
from itertools import cycle |
|
from PyQt5.Qt import * |
|
|
|
DEFAULT_COLORS = [0x3366cc, 0xdc3912, 0xff9900, 0x109618, 0x990099, |
|
0x0099c6, 0xdd4477, 0x66aa00, 0xb82e2e, 0x316395, |
|
0x994499, 0x22aa99, 0xaaaa11, 0x6633cc, 0x16d620] |
|
|
|
class Chart(object): |
|
def __init__(self, data, colors=None): |
|
self.data = data |
|
self.colors = colors |
|
|
|
self._ref_col = 0 |
|
self._ref_isv = True |
|
|
|
def setVerticalAxisColumn(self, column): |
|
self._ref_col = column |
|
self._ref_isv = True |
|
|
|
def setHorizontalAxisColumn(self, column): |
|
self._ref_col = column |
|
self._ref_isv = False |
|
|
|
def save(self, filename, chart_size, legend_width=None): |
|
image_size = chart_size |
|
if legend_width is not None: |
|
image_size = image_size + QSize(legend_width, 0) |
|
|
|
image = QImage(image_size, QImage.Format_ARGB32_Premultiplied) |
|
painter = QPainter(image) |
|
painter.setRenderHint(QPainter.Antialiasing) |
|
painter.fillRect(image.rect(), Qt.white) |
|
self.draw(painter, QRect(QPoint(0, 0), chart_size)) |
|
if legend_width is not None: |
|
self.drawLegend(painter, QRect(QPoint(chart_size.width(), 10), QSize(legend_width, chart_size.height()))) |
|
painter.end() |
|
return image.save(filename) |
|
|
|
def draw(self, painter, rectangle): |
|
raise NotImplementedError |
|
|
|
def drawLegend(self, painter, rectangle): |
|
SPACE = 2 |
|
|
|
font_metrics = painter.fontMetrics() |
|
size = font_metrics.xHeight() * 2 |
|
|
|
y = SPACE |
|
x0 = SPACE |
|
x1 = x0 + size + SPACE * 3 |
|
|
|
w = rectangle.width() - size - SPACE |
|
tw = w - x1 |
|
|
|
painter.save() |
|
painter.translate(rectangle.x(), rectangle.y()) |
|
|
|
color = self._icolors() |
|
for i, column in enumerate(self._fetchLegendData()): |
|
if (y + size + SPACE * 2) >= (rectangle.y() + rectangle.height()) and i < (len(self.data.columns) - 1): |
|
painter.drawText(x1, y, tw, size, Qt.AlignLeft | Qt.AlignVCenter, "...") |
|
y += size + SPACE |
|
break |
|
|
|
text = font_metrics.elidedText(column, Qt.ElideRight, tw) |
|
painter.fillRect(x0, y, size, size, QColor(next(color))) |
|
painter.drawText(x1, y, tw, size, Qt.AlignLeft | Qt.AlignVCenter, text) |
|
y += size + SPACE |
|
|
|
painter.setPen(Qt.lightGray) |
|
painter.drawRect(0, 0, w, y) |
|
painter.restore() |
|
|
|
def _fetchLegendData(self): |
|
for i, column in enumerate(self.data.columns): |
|
if i != self._ref_col: |
|
yield column |
|
|
|
def _icolors(self): |
|
if self.colors is None: |
|
return cycle(DEFAULT_COLORS) |
|
return cycle(self.colors) |
|
|
|
class PieChart(Chart): |
|
def draw(self, painter, rectangle): |
|
painter.save() |
|
painter.translate(rectangle.x(), rectangle.y()) |
|
|
|
# Calculate Values |
|
vtotal = float(sum(row[not self._ref_col] for row in self.data.rows)) |
|
values = [row[not self._ref_col] / vtotal for row in self.data.rows] |
|
|
|
# Draw Char |
|
start_angle = 90 * 16 |
|
for color, v in zip(self._icolors(), values): |
|
span_angle = v * -360.0 * 16 |
|
painter.setPen(Qt.white) |
|
painter.setBrush(QColor(color)) |
|
painter.drawPie(rectangle, start_angle, span_angle) |
|
start_angle += span_angle |
|
|
|
painter.restore() |
|
|
|
def _fetchLegendData(self): |
|
for row in self.data.rows: |
|
yield row[self._ref_col] |
|
|
|
class ScatterChart(Chart): |
|
SPAN = 10 |
|
|
|
def __init__(self, data, **kwargs): |
|
super(ScatterChart, self).__init__(data, **kwargs) |
|
|
|
self.haxis_title = None |
|
self.haxis_vmin = None |
|
self.haxis_vmax = None |
|
self.haxis_step = None |
|
self.haxis_grid = True |
|
|
|
self.vaxis_title = None |
|
self.vaxis_vmin = None |
|
self.vaxis_vmax = None |
|
self.vaxis_step = None |
|
self.vaxis_grid = True |
|
|
|
def draw(self, painter, rectangle): |
|
self._setupDefaultValues() |
|
|
|
font_metrics = painter.fontMetrics() |
|
h = font_metrics.xHeight() * 2 |
|
x = font_metrics.width(self._vToString(self.vaxis_vmax)) |
|
|
|
# Calculate X steps |
|
nxstep = int(round((self.haxis_vmax - self.haxis_vmin) / self.haxis_step)) |
|
nystep = int(round((self.vaxis_vmax - self.vaxis_vmin) / self.vaxis_step)) |
|
|
|
# Calculate chart space |
|
xmin = h + self.SPAN + x |
|
xstep = (rectangle.width() - xmin - (h + self.SPAN)) / nxstep |
|
xmax = xmin + xstep * nxstep |
|
|
|
# Calculate Y steps |
|
ymin = h + self.SPAN |
|
ystep = (rectangle.height() - ymin - (h * 2 + self.SPAN)) / nystep |
|
ymax = ymin + ystep * nystep |
|
|
|
painter.save() |
|
painter.translate(rectangle.x(), rectangle.y()) |
|
|
|
# Draw Axis Titles |
|
painter.save() |
|
self._drawAxisTitles(painter, xmin, xmax, ymin, ymax) |
|
painter.restore() |
|
|
|
# Draw Axis Labels |
|
painter.save() |
|
self._drawAxisLabels(painter, xmin, xmax, ymin, ymax, xstep, nxstep, ystep, nystep) |
|
painter.restore() |
|
|
|
# Draw Data |
|
painter.save() |
|
painter.setClipRect(xmin + 1, ymin, xmax - xmin - 2, ymax - ymin - 1) |
|
self._drawData(painter, xmin, xmax, ymin, ymax) |
|
painter.restore() |
|
|
|
# Draw Border |
|
painter.setPen(Qt.black) |
|
painter.drawLine(xmin, ymin, xmin, ymax) |
|
painter.drawLine(xmin, ymax, xmax, ymax) |
|
|
|
painter.restore() |
|
|
|
def _drawAxisTitles(self, painter, xmin, xmax, ymin, ymax): |
|
font_metrics = painter.fontMetrics() |
|
h = font_metrics.xHeight() * 2 |
|
hspan = self.SPAN / 2 |
|
|
|
font = painter.font() |
|
font.setItalic(True) |
|
painter.setFont(font) |
|
|
|
if self.haxis_title is not None: |
|
painter.drawText(xmin + ((xmax - xmin) / 2 - font_metrics.width(self.haxis_title) / 2), ymax + h * 2 + hspan, self.haxis_title) |
|
|
|
if self.vaxis_title is not None: |
|
painter.rotate(90) |
|
painter.drawText(ymin + (ymax - ymin) / 2 - font_metrics.width(self.vaxis_title) / 2, -hspan, self.vaxis_title) |
|
|
|
def _drawAxisLabels(self, painter, xmin, xmax, ymin, ymax, xstep, nxstep, ystep, nystep): |
|
font_metrics = painter.fontMetrics() |
|
h = font_metrics.xHeight() * 2 |
|
|
|
# Draw Internal Grid |
|
painter.setPen(Qt.lightGray) |
|
x = xmin + xstep |
|
ys = ymin if self.haxis_grid else ymax - 4 |
|
for _ in xrange(nxstep): |
|
painter.drawLine(x, ys, x, ymax) |
|
x += xstep |
|
|
|
y = ymin |
|
xe = xmax if self.vaxis_grid else xmin + 4 |
|
for _ in xrange(nystep): |
|
painter.drawLine(xmin, y, xe, y) |
|
y += ystep |
|
|
|
# Draw Axis Labels |
|
painter.setPen(Qt.black) |
|
for i in xrange(1 + nxstep): |
|
x = xmin + (i * xstep) |
|
v = self._hToString(self.haxis_vmin + i * self.haxis_step) |
|
painter.drawText(x - font_metrics.width(v) / 2, 2 + ymax + h, v) |
|
|
|
for i in xrange(1 + nystep): |
|
y = ymin + (i * ystep) |
|
v = self._vToString(self.vaxis_vmin + (nystep - i) * self.vaxis_step) |
|
painter.drawText(xmin - font_metrics.width(v) - 2, y, v) |
|
|
|
def _drawData(self, painter, xmin, xmax, ymin, ymax): |
|
c = 0 |
|
color = self._icolors() |
|
while c < len(self.data.columns): |
|
if c != self._ref_col: |
|
if self._ref_isv: |
|
a, b = c, self._ref_col |
|
else: |
|
a, b = self._ref_col, c |
|
self._drawColumnData(painter, next(color), a, b, xmin, xmax, ymin, ymax) |
|
c += 1 |
|
|
|
def _drawColumnData(self, painter, color, xcol, ycol, xmin, xmax, ymin, ymax): |
|
painter.setPen(QPen(QColor(color), 7, Qt.SolidLine, Qt.RoundCap)) |
|
for row in self.data.rows: |
|
x, y = self._xyFromData(row[xcol], row[ycol], xmin, xmax, ymin, ymax) |
|
painter.drawPoint(x, y) |
|
|
|
def _xyFromData(self, xdata, ydata, xmin, xmax, ymin, ymax): |
|
x = xmin + (float(xdata - self.haxis_vmin) / (self.haxis_vmax - self.haxis_vmin)) * (xmax - xmin) |
|
y = ymin + (1.0 - (float(ydata - self.vaxis_vmin) / (self.vaxis_vmax - self.vaxis_vmin))) * (ymax - ymin) |
|
return x, y |
|
|
|
def _vToString(self, value): |
|
if isinstance(self.vaxis_step, float): |
|
return '%.2f' % value |
|
if isinstance(self.vaxis_step, int): |
|
return '%d' % value |
|
return '%s' % value |
|
|
|
def _hToString(self, value): |
|
if isinstance(self.haxis_step, float): |
|
return '%.2f' % value |
|
if isinstance(self.haxis_step, int): |
|
return '%d' % value |
|
return '%s' % value |
|
|
|
def _setupDefaultValues(self): |
|
def _minMaxDelta(col): |
|
vmin = None |
|
vmax = None |
|
vdelta = 0 |
|
ndelta = 1 |
|
|
|
last_value = self.data.rows[0][col] |
|
vmin = vmax = last_value |
|
for row in self.data.rows[1:]: |
|
vdelta += abs(row[col] - last_value) |
|
ndelta += 1 |
|
if row[col] > vmax: |
|
vmax = row[col] |
|
elif row[col] < vmin: |
|
vmin = row[col] |
|
|
|
return vmin, vmax, vdelta / ndelta |
|
|
|
ref_min, ref_max, ref_step = _minMaxDelta(self._ref_col) |
|
oth_min = oth_max = oth_step = None |
|
for col in xrange(len(self.data.columns)): |
|
if col == self._ref_col: |
|
continue |
|
|
|
cmin, cmax, cstep = _minMaxDelta(col) |
|
oth_min = cmin if oth_min is None else min(cmin, oth_min) |
|
oth_max = cmax if oth_max is None else max(cmax, oth_max) |
|
oth_step = cstep if oth_step is None else (oth_step + cstep) / 2 |
|
|
|
if self._ref_isv: |
|
if self.vaxis_vmin is None: self.vaxis_vmin = ref_min |
|
if self.vaxis_vmax is None: self.vaxis_vmax = ref_max |
|
if self.vaxis_step is None: self.vaxis_step = ref_step |
|
if self.haxis_vmin is None: self.haxis_vmin = oth_min |
|
if self.haxis_vmax is None: self.haxis_vmax = oth_max |
|
if self.haxis_step is None: self.haxis_step = oth_step |
|
else: |
|
if self.haxis_vmin is None: self.haxis_vmin = ref_min |
|
if self.haxis_vmax is None: self.haxis_vmax = ref_max |
|
if self.haxis_step is None: self.haxis_step = ref_step |
|
if self.vaxis_vmin is None: self.vaxis_vmin = oth_min |
|
if self.vaxis_vmax is None: self.vaxis_vmax = oth_max |
|
if self.vaxis_step is None: self.vaxis_step = oth_step |
|
|
|
class LineChart(ScatterChart): |
|
def _drawColumnData(self, painter, color, xcol, ycol, xmin, xmax, ymin, ymax): |
|
painter.setPen(QPen(QColor(color), 2, Qt.SolidLine, Qt.RoundCap)) |
|
|
|
path = QPainterPath() |
|
|
|
row = self.data.rows[0] |
|
x, y = self._xyFromData(row[xcol], row[ycol], xmin, xmax, ymin, ymax) |
|
path.moveTo(x, y) |
|
|
|
for row in self.data.rows[1:]: |
|
x, y = self._xyFromData(row[xcol], row[ycol], xmin, xmax, ymin, ymax) |
|
path.lineTo(x, y) |
|
painter.drawPath(path) |
|
|
|
class AreaChart(ScatterChart): |
|
def _drawColumnData(self, painter, color, xcol, ycol, xmin, xmax, ymin, ymax): |
|
painter.setPen(QPen(QColor(color), 2, Qt.SolidLine, Qt.RoundCap)) |
|
|
|
color = QColor(color) |
|
color.setAlpha(40) |
|
painter.setBrush(color) |
|
|
|
path = QPainterPath() |
|
path.moveTo(xmin, ymax) |
|
for row in self.data.rows: |
|
x, y = self._xyFromData(row[xcol], row[ycol], xmin, xmax, ymin, ymax) |
|
path.lineTo(x, y) |
|
path.lineTo(xmax, ymax) |
|
path.moveTo(xmin, ymax) |
|
painter.drawPath(path) |
|
|
|
class Viewer(QWidget): |
|
def __init__(self): |
|
QWidget.__init__(self) |
|
self.graph = None |
|
|
|
def setGraph(self, func): |
|
self.graph = func |
|
self.update() |
|
|
|
def paintEvent(self, event): |
|
painter = QPainter(self) |
|
painter.setRenderHint(QPainter.Antialiasing) |
|
|
|
if self.graph is not None: |
|
self.graph.draw(painter, QRect(0, 0, event.rect().width() - 120, event.rect().height())) |
|
self.graph.drawLegend(painter, QRect(event.rect().width() - 120, 20, 120, event.rect().height() - 20)) |
|
|
|
painter.end() |
|
|
|
class DialogViewer(QDialog): |
|
def __init__(self): |
|
QDialog.__init__(self) |
|
self.viewer = Viewer() |
|
#self.viewer.resize(360, 240) |
|
self.viewer.setMinimumSize(360,240) |
|
#self.setMinimumSize(100,100) |
|
self.setLayout(QVBoxLayout()) |
|
self.layout().setContentsMargins(0, 0, 0, 0) |
|
self.layout().addWidget(self.viewer) |
|
|
|
def setGraph(self, func): |
|
self.viewer.setGraph(func) |
|
|
|
class DataTable(object): |
|
def __init__(self): |
|
self.columns = [] |
|
self.rows = [] |
|
|
|
def addColumn(self, label): |
|
self.columns.append(label) |
|
|
|
def addRow(self, row): |
|
assert len(row) == len(self.columns) |
|
self.rows.append(row) |
|
|
|
def _pieChartDemo(): |
|
table = DataTable() |
|
table.addColumn('Lang') |
|
table.addColumn('Rating') |
|
table.addRow(['Java', 17.874]) |
|
table.addRow(['C', 17.322]) |
|
table.addRow(['C++', 8.084]) |
|
table.addRow(['C#', 7.319]) |
|
table.addRow(['PHP', 6.096]) |
|
|
|
chart = PieChart(table) |
|
#chart.save('pie.png', QSize(240, 240), 100) |
|
|
|
view = DialogViewer() |
|
view.setGraph(chart) |
|
view.resize(360, 240) |
|
view.exec_() |
|
|
|
def _scatterChartDemo(): |
|
table = DataTable() |
|
table.addColumn('Quality') |
|
table.addColumn('Test 1') |
|
table.addColumn('Test 2') |
|
table.addRow([ 92, 4.9, 8.0]) |
|
table.addRow([ 94, 2.0, 2.5]) |
|
table.addRow([ 96, 7.2, 6.9]) |
|
table.addRow([ 98, 3.5, 1.2]) |
|
table.addRow([100, 8.0, 5.3]) |
|
table.addRow([102, 15.0, 14.2]) |
|
|
|
chart = ScatterChart(table) |
|
chart.haxis_title = 'Process input' |
|
chart.haxis_vmin = 0 |
|
chart.haxis_vmax = 16 |
|
chart.haxis_step = 2 |
|
chart.vaxis_title = 'Quality' |
|
chart.vaxis_vmin = 90 |
|
chart.vaxis_vmax = 104 |
|
chart.vaxis_step = 1 |
|
|
|
#chart.save('scatter.png', QSize(400, 240), 100) |
|
|
|
view = DialogViewer() |
|
view.setGraph(chart) |
|
view.resize(400, 240) |
|
view.exec_() |
|
|
|
def _lineChartDemo(): |
|
table = DataTable() |
|
table.addColumn('Time') |
|
table.addColumn('Site 1') |
|
table.addColumn('Site 2') |
|
table.addColumn('Site 3') |
|
table.addRow([ 4.00, 120, 80, 400]) |
|
table.addRow([ 6.00, 270, 850, 320]) |
|
table.addRow([ 8.30, 50, 1200, 280]) |
|
table.addRow([10.15, 320, 1520, 510]) |
|
table.addRow([12.00, 150, 930, 1100]) |
|
table.addRow([18.20, 62, 1100, 240]) |
|
|
|
chart = LineChart(table) |
|
chart.setHorizontalAxisColumn(0) |
|
chart.haxis_title = 'Time' |
|
chart.haxis_vmin = 0.0 |
|
chart.haxis_vmax = 20.0 |
|
chart.haxis_step = 2 |
|
|
|
#chart.save('line.png', QSize(400, 240), 100) |
|
|
|
view = DialogViewer() |
|
view.setGraph(chart) |
|
view.resize(400, 240) |
|
view.exec_() |
|
|
|
def _areaChartDemo(): |
|
table = DataTable() |
|
table.addColumn('Time') |
|
table.addColumn('Site 1') |
|
table.addColumn('Site 2') |
|
table.addRow([ 4.00, 120, 500]) |
|
table.addRow([ 6.00, 270, 460]) |
|
table.addRow([ 8.30, 1260, 1120]) |
|
table.addRow([10.15, 2030, 540]) |
|
table.addRow([12.00, 520, 890]) |
|
table.addRow([18.20, 1862, 1500]) |
|
|
|
chart = AreaChart(table) |
|
chart.setHorizontalAxisColumn(0) |
|
chart.haxis_title = 'Time' |
|
chart.haxis_vmin = 0.0 |
|
chart.haxis_vmax = 20.0 |
|
chart.haxis_step = 5 |
|
|
|
#chart.save('area.png', QSize(400, 240), 100) |
|
|
|
view = DialogViewer() |
|
view.setGraph(chart) |
|
view.resize(400, 240) |
|
view.exec_() |
|
|
|
if __name__ == '__main__': |
|
import sys |
|
app = QApplication(sys.argv) |
|
|
|
_pieChartDemo() |
|
_scatterChartDemo() |
|
_lineChartDemo() |
|
_areaChartDemo()
|
|
|