full chart widget

This commit is contained in:
Benedek László 2024-05-19 16:57:15 +02:00
parent df8cd70b0d
commit 590b302016
3 changed files with 129 additions and 58 deletions

View File

@ -5,6 +5,7 @@
#include <QAction> #include <QAction>
#include <QMenu> #include <QMenu>
#include <QMutex> #include <QMutex>
#include <limits>
namespace Graph::GUI { namespace Graph::GUI {
class ChartWidget : public QWidget { class ChartWidget : public QWidget {
@ -19,18 +20,26 @@ class ChartWidget : public QWidget {
protected: protected:
QSize sizeHint() const override; QSize sizeHint() const override;
void paintEvent(QPaintEvent *e) override; void paintEvent(QPaintEvent* e) override;
private: private:
chart_spec_t spec; chart_spec_t spec;
int decimateState = 0; int decimateState = 0;
QMutex mutex; QMutex mutex;
std::list<point_t> points; struct series_t {
double minX = std::numeric_limits<double>::max();
double maxX = std::numeric_limits<double>::min();
double minY = std::numeric_limits<double>::max();
double maxY = std::numeric_limits<double>::min();
std::list<point_t> points;
};
std::map<std::string, series_t> series;
double scaleX, scaleY; double scaleX, scaleY;
int marginX = 40; int marginX = 40;
int marginY = 20; int marginY = 40;
QPointF pointToCoord(const point_t& point); QPointF pointToCoord(const point_t& point);
}; };

View File

@ -1,16 +1,10 @@
#include <graph/server/gui/chart/chart.h> #include <graph/server/gui/chart/chart.h>
#include <graph/server/gui/mainwindow.h> #include <graph/server/gui/mainwindow.h>
#include <qbrush.h>
#include <qnamespace.h>
#include <qpoint.h>
#include <qpolygon.h>
#include <qsize.h>
#include <QPainter> #include <QPainter>
#include <iostream> #include <iostream>
#include <iterator> #include <iterator>
#include <limits> #include <limits>
#include <string> #include <string>
#include "graph/plugins/plugin.h"
#define MIN(a, b) (a < b ? a : b) #define MIN(a, b) (a < b ? a : b)
#define MAX(a, b) (a > b ? a : b) #define MAX(a, b) (a > b ? a : b)
@ -18,9 +12,13 @@
namespace Graph::GUI { namespace Graph::GUI {
ChartWidget::ChartWidget(QWidget* parent, const chart_spec_t& spec) : QWidget(parent), spec(spec) { ChartWidget::ChartWidget(QWidget* parent, const chart_spec_t& spec) : QWidget(parent), spec(spec) {
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setMinimumSize(QSize(600, 350)); setMinimumSize(QSize(600, 350));
for (const QString& name : spec.series) {
series.insert(std::pair<std::string, series_t>(name.toStdString(), series_t()));
std::cout << "create a series named " << name.toStdString() << std::endl;
}
// context menu // context menu
setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
connect(this, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) { connect(this, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
@ -34,28 +32,33 @@ ChartWidget::~ChartWidget() {}
void ChartWidget::addData(const char* seriesName, const point_t* points, unsigned long count) { void ChartWidget::addData(const char* seriesName, const point_t* points, unsigned long count) {
QMutexLocker locker(&mutex); QMutexLocker locker(&mutex);
double minX = std::numeric_limits<double>::max(); if (!this->series.contains(std::string(seriesName)))
double minY = std::numeric_limits<double>::max(); return;
double maxX = std::numeric_limits<double>::min();
double maxY = std::numeric_limits<double>::min(); series_t& series = this->series.at(std::string(seriesName));
series.minX = std::numeric_limits<double>::max();
series.minY = std::numeric_limits<double>::max();
series.maxX = std::numeric_limits<double>::min();
series.maxY = std::numeric_limits<double>::min();
if (!spec.keep_all) { if (!spec.keep_all) {
if (count >= spec.keep_limit) { if (count >= spec.keep_limit) {
this->points.clear(); series.points.clear();
} else if (this->points.size() + count > spec.keep_limit) { } else if (series.points.size() + count > spec.keep_limit) {
std::list<point_t>::iterator end = std::next(this->points.begin(), this->points.size() - (spec.keep_limit - count)); std::list<point_t>::iterator end = std::next(series.points.begin(), series.points.size() - (spec.keep_limit - count));
this->points.erase(this->points.begin(), end); series.points.erase(series.points.begin(), end);
} }
} }
for (const point_t& point : this->points) { for (const point_t& point : series.points) {
if (spec.axis.x.auto_limit) { if (spec.axis.x.auto_limit) {
minX = MIN(minX, point.x); series.minX = MIN(series.minX, point.x);
maxX = MAX(maxX, point.x); series.maxX = MAX(series.maxX, point.x);
} }
if (spec.axis.y.auto_limit) { if (spec.axis.y.auto_limit) {
minY = MIN(minY, point.y); series.minY = MIN(series.minY, point.y);
maxY = MAX(maxY, point.y); series.maxY = MAX(series.maxY, point.y);
} }
} }
@ -65,33 +68,30 @@ void ChartWidget::addData(const char* seriesName, const point_t* points, unsigne
if (decimateState++ % spec.decimate != 0) if (decimateState++ % spec.decimate != 0)
continue; continue;
this->points.push_back(points[i]); series.points.push_back(points[i]);
if (spec.axis.x.auto_limit) { if (spec.axis.x.auto_limit) {
minX = MIN(minX, points[i].x); series.minX = MIN(series.minX, points[i].x);
maxX = MAX(maxX, points[i].x); series.maxX = MAX(series.maxX, points[i].x);
} }
if (spec.axis.y.auto_limit) { if (spec.axis.y.auto_limit) {
minY = MIN(minY, points[i].y); series.minY = MIN(series.minY, points[i].y);
maxY = MAX(maxY, points[i].y); series.maxY = MAX(series.maxY, points[i].y);
} }
} }
if (spec.axis.x.auto_limit) {
spec.axis.x.min = minX;
spec.axis.x.max = maxX;
}
if (spec.axis.y.auto_limit) {
spec.axis.y.min = minY;
spec.axis.y.max = maxY;
}
update(); update();
} }
void ChartWidget::clear() { void ChartWidget::clear() {
QMutexLocker locker(&mutex); QMutexLocker locker(&mutex);
points.clear(); for (auto& series : this->series) {
series.second.points.clear();
series.second.minX = std::numeric_limits<double>::max();
series.second.maxX = std::numeric_limits<double>::min();
series.second.minY = std::numeric_limits<double>::max();
series.second.maxY = std::numeric_limits<double>::min();
}
} }
QSize ChartWidget::sizeHint() const { QSize ChartWidget::sizeHint() const {
@ -104,9 +104,35 @@ QPointF ChartWidget::pointToCoord(const point_t& point) {
return QPointF((point.x - spec.axis.x.min) * scaleX + marginX, height() - (point.y - spec.axis.y.min) * scaleY - marginY); return QPointF((point.x - spec.axis.x.min) * scaleX + marginX, height() - (point.y - spec.axis.y.min) * scaleY - marginY);
} }
QColor interpolateColor(const QColor& a, const QColor& b, float ratio) {
return QColor(255 * (b.redF() * ratio + a.redF() * (1 - ratio)), 255 * (b.greenF() * ratio + a.greenF() * (1 - ratio)),
255 * (b.blueF() * ratio + a.blueF() * (1 - ratio)), 255 * (b.alphaF() * ratio + a.alphaF() * (1 - ratio)));
}
void ChartWidget::paintEvent(QPaintEvent* e) { void ChartWidget::paintEvent(QPaintEvent* e) {
QMutexLocker locker(&mutex); QMutexLocker locker(&mutex);
// axis ranges
if (spec.axis.x.auto_limit) {
spec.axis.x.min = std::numeric_limits<double>::max();
spec.axis.x.max = std::numeric_limits<double>::min();
}
if (spec.axis.y.auto_limit) {
spec.axis.y.min = std::numeric_limits<double>::max();
spec.axis.y.max = std::numeric_limits<double>::min();
}
for (const auto& series : this->series) {
if (spec.axis.x.auto_limit) {
spec.axis.x.min = MIN(spec.axis.x.min, series.second.minX);
spec.axis.x.max = MAX(spec.axis.x.max, series.second.maxX);
}
if (spec.axis.y.auto_limit) {
spec.axis.y.min = MIN(spec.axis.y.min, series.second.minY);
spec.axis.y.max = MAX(spec.axis.y.max, series.second.maxY);
}
}
QPainter painter(this); QPainter painter(this);
// background // background
@ -119,15 +145,24 @@ void ChartWidget::paintEvent(QPaintEvent* e) {
// limits // limits
// y max-mid-min // y max-mid-min
painter.drawText(QRect(0, marginY, marginX, marginY), Qt::AlignCenter, QString::number(spec.axis.y.max)); painter.drawText(QRect(marginX / 2, marginY, marginX / 2, marginY), Qt::AlignCenter, QString::number(spec.axis.y.max));
painter.drawText(QRect(0, height() / 2 - marginY / 2, marginX, marginY), Qt::AlignCenter, QString::number((spec.axis.y.max + spec.axis.y.min) / 2)); painter.drawText(QRect(marginX / 2, height() / 2 - marginY / 2, marginX / 2, marginY), Qt::AlignCenter,
painter.drawText(QRect(0, height() - marginY * 2, marginX, marginY), Qt::AlignCenter, QString::number(spec.axis.y.min)); QString::number((spec.axis.y.max + spec.axis.y.min) / 2));
painter.drawText(QRect(marginX / 2, height() - marginY * 2, marginX / 2, marginY), Qt::AlignCenter, QString::number(spec.axis.y.min));
// x min-mid-max // x min-mid-max
painter.drawText(QRect(marginX, height() - marginY, marginX, marginY), Qt::AlignCenter, QString::number(spec.axis.x.min)); painter.drawText(QRect(marginX, height() - marginY, marginX, marginY / 2), Qt::AlignCenter, QString::number(spec.axis.x.min));
painter.drawText(QRect(width() / 2 - marginX / 2, height() - marginY, marginX, marginY), Qt::AlignCenter, painter.drawText(QRect(width() / 2 - marginX / 2, height() - marginY, marginX, marginY / 2), Qt::AlignCenter,
QString::number((spec.axis.x.min + spec.axis.x.max) / 2)); QString::number((spec.axis.x.min + spec.axis.x.max) / 2));
painter.drawText(QRect(width() - marginX * 2, height() - marginY, marginX, marginY), Qt::AlignCenter, QString::number(spec.axis.x.max)); painter.drawText(QRect(width() - marginX * 2, height() - marginY, marginX, marginY / 2), Qt::AlignCenter, QString::number(spec.axis.x.max));
// axis labels
painter.drawText(QRect(0, height() - marginY / 2, width(), marginY / 2), Qt::AlignCenter, spec.axis.x.label);
painter.save();
painter.rotate(-90);
painter.translate(-height(), 0);
painter.drawText(QRect(0, 0, height(), marginX / 2), Qt::AlignCenter, spec.axis.y.label);
painter.restore();
// grid // grid
if (spec.grid) { if (spec.grid) {
@ -150,14 +185,29 @@ void ChartWidget::paintEvent(QPaintEvent* e) {
painter.setPen(QPen(Qt::black, 1)); painter.setPen(QPen(Qt::black, 1));
QPolygonF polygon; int i = 0;
for (const point_t point : points) { for (const auto& series : this->series) {
// TODO: only paint visible points QPolygonF polygon;
// (+ 1-1 before and after) for (const point_t point : series.second.points) {
polygon << pointToCoord(point); // TODO: only paint visible points
} // (+ 1-1 before and after)
polygon << pointToCoord(point);
}
painter.drawPolyline(polygon); painter.setBrush(interpolateColor(Qt::darkBlue, Qt::green, i / (float)this->series.size()));
painter.setPen(interpolateColor(Qt::darkBlue, Qt::green, i / (float)this->series.size()));
// line
painter.drawPolyline(polygon);
// legend
if (spec.legend) {
painter.fillRect(QRect(marginX + 5, marginY + 5 + i * 15, 10, 10), painter.brush());
painter.drawText(QRect(marginX + 17, marginY + 2 + i * 15, width() - 2 * marginX, 15), QString::fromStdString(series.first));
}
i++;
}
} }
ContextMenu::ContextMenu(QWidget* parent, ChartWidget* chartWidget) : QMenu(parent) { ContextMenu::ContextMenu(QWidget* parent, ChartWidget* chartWidget) : QMenu(parent) {
@ -169,11 +219,11 @@ ContextMenu::ContextMenu(QWidget* parent, ChartWidget* chartWidget) : QMenu(pare
connect(clear, &QAction::triggered, this, [chartWidget]() { chartWidget->clear(); }); connect(clear, &QAction::triggered, this, [chartWidget]() { chartWidget->clear(); });
addAction(clear); addAction(clear);
edit = new QAction(tr("&Edit"), this); // edit = new QAction(tr("&Edit"), this);
connect(edit, &QAction::triggered, this, []() { // connect(edit, &QAction::triggered, this, []() {
// TODO: implement chart editing // // TODO: implement chart editing
}); // });
addAction(edit); // addAction(edit);
} }
ContextMenu::~ContextMenu() {} ContextMenu::~ContextMenu() {}

View File

@ -1,7 +1,7 @@
#!/bin/env python3 #!/bin/env python3
import requests import requests
from math import sin from math import sin, cos
from time import sleep from time import sleep
import numpy as np import numpy as np
from argparse import ArgumentParser from argparse import ArgumentParser
@ -17,6 +17,16 @@ def sine(args):
except KeyboardInterrupt: except KeyboardInterrupt:
break break
def cosine(args):
x = 0.0
while True:
try:
requests.get(f"http://{args.address}:{args.port}/add?chart={args.chart}&series={args.series}&x={x}&y={cos(x)}")
x += 0.01
sleep(0.01)
except KeyboardInterrupt:
break
def sine_array(args): def sine_array(args):
start = 0 start = 0
while True: while True:
@ -80,7 +90,7 @@ def star(args):
if __name__ == "__main__": if __name__ == "__main__":
parser = ArgumentParser() parser = ArgumentParser()
parser.add_argument("-t", action="store", dest="test", help="test type", type=str, choices=["sine", "sine_array", "star"], required=False, default="sin") parser.add_argument("-t", action="store", dest="test", help="test type", type=str, choices=["sine", "cosine", "sine_array", "star"], required=False, default="sine")
parser.add_argument("-n", action="store", dest="num", help="sin_array size/iteration", type=int, required=False, default=100) parser.add_argument("-n", action="store", dest="num", help="sin_array size/iteration", type=int, required=False, default=100)
parser.add_argument("-d", action="store", dest="delay", help="delay between iteration", type=float, required=False, default=0.25) parser.add_argument("-d", action="store", dest="delay", help="delay between iteration", type=float, required=False, default=0.25)
parser.add_argument("-c", action="store", dest="chart", help="chart id", type=int, required=True) parser.add_argument("-c", action="store", dest="chart", help="chart id", type=int, required=True)
@ -91,6 +101,8 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
if args.test == "sine": if args.test == "sine":
sine(args) sine(args)
if args.test == "cosine":
cosine(args)
elif args.test == "sine_array": elif args.test == "sine_array":
sine_array(args) sine_array(args)
elif args.test == "star": elif args.test == "star":