diff --git a/inc/graph/plugins/plugin.h b/inc/graph/plugins/plugin.h index 7a93002..1ed4275 100644 --- a/inc/graph/plugins/plugin.h +++ b/inc/graph/plugins/plugin.h @@ -13,7 +13,7 @@ typedef struct { typedef int (*run_method_t)(); typedef int (*destroy_t)(); -typedef void (*update_callback_t)(unsigned int chartIndex, const char* seriesName, const point_t* points, int count); +typedef void (*update_callback_t)(unsigned int chartIndex, const char* seriesName, const point_t* points, unsigned long count); typedef struct { uint32_t version; diff --git a/inc/graph/server/gui/chart/chart.h b/inc/graph/server/gui/chart/chart.h index 3204163..af542a7 100644 --- a/inc/graph/server/gui/chart/chart.h +++ b/inc/graph/server/gui/chart/chart.h @@ -2,36 +2,36 @@ #include #include -#include #include -#include -#include -#include -#include #include -#include #include namespace Graph::GUI { -class ChartWidget : public QListWidgetItem { +class ChartWidget : public QWidget { + Q_OBJECT + public: ChartWidget(QWidget* parent = nullptr, const chart_spec_t& spec = chart_spec_t()); ~ChartWidget(); - void addData(const char* seriesName, const point_t* points, int count); - - QChartView* getView() const { return view; } - QList getSeries() const { return chart->series(); } + void addData(const char* seriesName, const point_t* points, unsigned long count); void clear(); + protected: + QSize sizeHint() const override; + void paintEvent(QPaintEvent *e) override; + private: - QChartView* view; - QChart* chart; - QValueAxis *x, *y; chart_spec_t spec; int decimateState = 0; - QMutex mutex; + + std::list points; + + double scaleX, scaleY; + int marginX = 40; + int marginY = 20; + QPointF pointToCoord(const point_t& point); }; class ContextMenu : public QMenu { diff --git a/inc/graph/server/gui/mainwindow.h b/inc/graph/server/gui/mainwindow.h index 971cc53..f79146e 100644 --- a/inc/graph/server/gui/mainwindow.h +++ b/inc/graph/server/gui/mainwindow.h @@ -28,7 +28,7 @@ class MainWindow : public QMainWindow { static MainWindow* getInstance(); static void close(); - void addData(unsigned int chartIndex, const char* seriesName, const point_t* points, int count); + void addData(unsigned int chartIndex, const char* seriesName, const point_t* points, unsigned long count); void removeChart(ChartWidget* chartWidget); diff --git a/inc/graph/server/spec.h b/inc/graph/server/spec.h index a893e6e..f6383c1 100644 --- a/inc/graph/server/spec.h +++ b/inc/graph/server/spec.h @@ -15,7 +15,7 @@ struct chart_spec_t { bool grid = false; bool legend = false; bool keep_all = true; - int keep_limit; + unsigned long keep_limit; int decimate; struct { axis_spec_t x, y; diff --git a/src/graph/server/CMakeLists.txt b/src/graph/server/CMakeLists.txt index e16342f..951fa5c 100644 --- a/src/graph/server/CMakeLists.txt +++ b/src/graph/server/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 REQUIRED COMPONENTS Widgets Charts) +find_package(Qt6 REQUIRED COMPONENTS Widgets) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) @@ -17,5 +17,5 @@ add_executable(server ) target_include_directories(server PRIVATE "${CMAKE_BINARY_DIR}/src") -target_link_libraries(server PRIVATE dl Qt6::Widgets Qt6::Charts) +target_link_libraries(server PRIVATE dl Qt6::Widgets) set_target_properties(server PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") \ No newline at end of file diff --git a/src/graph/server/gui/chart/chart.cpp b/src/graph/server/gui/chart/chart.cpp index dba14cf..4d498d2 100644 --- a/src/graph/server/gui/chart/chart.cpp +++ b/src/graph/server/gui/chart/chart.cpp @@ -1,150 +1,163 @@ #include #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include +#include #include +#include +#include "graph/plugins/plugin.h" #define MIN(a, b) (a < b ? a : b) #define MAX(a, b) (a > b ? a : b) namespace Graph::GUI { -ChartWidget::ChartWidget(QWidget* parent, const chart_spec_t& spec) : QListWidgetItem(), spec(spec) { - view = new QChartView(parent); - chart = new QChart(); +ChartWidget::ChartWidget(QWidget* parent, const chart_spec_t& spec) : QWidget(parent), spec(spec) { + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - // chart - if (spec.legend) - chart->legend()->show(); - else - chart->legend()->hide(); - - chart->setTitle(spec.title); - - // axis - x = new QValueAxis(); - y = new QValueAxis(); - - if (!spec.axis.x.auto_limit) - x->setRange(spec.axis.x.min, spec.axis.x.max); - if (!spec.axis.y.auto_limit) - y->setRange(spec.axis.y.min, spec.axis.y.max); - - x->setTitleText(spec.axis.x.label); - y->setTitleText(spec.axis.y.label); - - chart->addAxis(x, Qt::AlignBottom); - chart->addAxis(y, Qt::AlignLeft); - - for (const QString& title : spec.series) { - QLineSeries* series = new QLineSeries(view); - series->setName(title); - chart->addSeries(series); - series->attachAxis(x); - series->attachAxis(y); - } - - view->setRenderHint(QPainter::Antialiasing); - view->setChart(chart); - view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - setSizeHint(view->sizeHint() * 4); + setMinimumSize(QSize(600, 350)); // context menu - view->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); - view->connect(view, &QChartView::customContextMenuRequested, view, [this](const QPoint& pos) { + setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); + connect(this, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) { ContextMenu menu(nullptr, this); - menu.exec(view->mapToGlobal(pos)); + menu.exec(mapToGlobal(pos)); }); } -ChartWidget::~ChartWidget() { - // delete view; - // delete chart; - // delete x; - // delete y; -} +ChartWidget::~ChartWidget() {} -void ChartWidget::addData(const char* seriesName, const point_t* points, int count) { +void ChartWidget::addData(const char* seriesName, const point_t* points, unsigned long count) { QMutexLocker locker(&mutex); - std::cout << "locked: " << locker.isLocked() << std::endl; + double minX = std::numeric_limits::max(); + double minY = std::numeric_limits::max(); + double maxX = std::numeric_limits::min(); + double maxY = std::numeric_limits::min(); - QString name(seriesName); - - double minX = spec.keep_all ? x->min() : std::numeric_limits::max(); - double minY = spec.keep_all ? y->min() : std::numeric_limits::max(); - double maxX = spec.keep_all ? x->max() : std::numeric_limits::min(); - double maxY = spec.keep_all ? y->max() : std::numeric_limits::min(); - - for (QAbstractSeries* series : chart->series()) { - if (series->name() == name) { - if (!spec.keep_all) { - if (count >= spec.keep_limit) { - ((QLineSeries*)series)->clear(); - } else if (((QLineSeries*)series)->count() + count > spec.keep_limit) { - std::cout << "count: " << count << ", size: " << ((QLineSeries*)series)->count() << ", remove: " << ((QLineSeries*)series)->count() - (spec.keep_limit - count) << std::endl; - ((QLineSeries*)series)->removePoints(0, ((QLineSeries*)series)->count() - (spec.keep_limit - count)); - } - - std::cout << "starting min/max" << std::endl; - for (const QPointF& point : ((QLineSeries*)series)->points()) { - if (spec.axis.x.auto_limit) { - minX = MIN(minX, point.x()); - maxX = MAX(maxX, point.x()); - } - if (spec.axis.y.auto_limit) { - minY = MIN(minY, point.y()); - maxY = MAX(maxY, point.y()); - } - } - std::cout << "got min/max" << std::endl; - } - - std::cout << "start adding" << std::endl; - QList newPoints; - - for (int i = spec.keep_all ? 0 : MAX(0, count - spec.keep_limit); i < count; i++) { - if (decimateState >= spec.decimate) - decimateState = 0; - if (decimateState++ % spec.decimate != 0) - continue; - - if (spec.axis.x.auto_limit) { - minX = MIN(minX, points[i].x); - maxX = MAX(maxX, points[i].x); - } - if (spec.axis.y.auto_limit) { - minY = MIN(minY, points[i].y); - maxY = MAX(maxY, points[i].y); - } - - newPoints.append(QPointF(points[i].x, points[i].y)); - } - - std::cout << "adding" << std::endl; - ((QLineSeries*)series)->append(newPoints); - - if (spec.axis.x.auto_limit) - x->setRange(MIN(x->min(), minX), MAX(x->max(), maxX)); - if (spec.axis.y.auto_limit) - y->setRange(MIN(y->min(), minY), MAX(y->max(), maxY)); - - break; + if (!spec.keep_all) { + if (count >= spec.keep_limit) { + this->points.clear(); + } else if (this->points.size() + count > spec.keep_limit) { + std::list::iterator end = std::next(this->points.begin(), this->points.size() - (spec.keep_limit - count)); + this->points.erase(this->points.begin(), end); } } + + for (const point_t& point : this->points) { + if (spec.axis.x.auto_limit) { + minX = MIN(minX, point.x); + maxX = MAX(maxX, point.x); + } + if (spec.axis.y.auto_limit) { + minY = MIN(minY, point.y); + maxY = MAX(maxY, point.y); + } + } + + for (unsigned long i = spec.keep_all ? 0 : (count < spec.keep_limit ? 0 : count - spec.keep_limit); i < count; i++) { + if (decimateState >= spec.decimate) + decimateState = 0; + if (decimateState++ % spec.decimate != 0) + continue; + + this->points.push_back(points[i]); + + if (spec.axis.x.auto_limit) { + minX = MIN(minX, points[i].x); + maxX = MAX(maxX, points[i].x); + } + if (spec.axis.y.auto_limit) { + minY = MIN(minY, points[i].y); + maxY = MAX(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(); } void ChartWidget::clear() { QMutexLocker locker(&mutex); + points.clear(); +} - for (QAbstractSeries* series : chart->series()) { - ((QLineSeries*)series)->clear(); +QSize ChartWidget::sizeHint() const { + if (width() < minimumWidth() || height() < minimumHeight()) + return minimumSize(); + return QSize(width(), height()); +} + +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); +} + +void ChartWidget::paintEvent(QPaintEvent* e) { + QMutexLocker locker(&mutex); + + QPainter painter(this); + + // background + painter.fillRect(QRectF(0, 0, width(), height()), Qt::lightGray); + painter.fillRect(QRectF(marginX, marginY, width() - 2 * marginX, height() - 2 * marginY), Qt::white); + + // title + painter.setPen(Qt::black); + painter.drawText(QRect(0, 0, width(), marginY), Qt::AlignCenter, spec.title); + + // limits + // y max-mid-min + painter.drawText(QRect(0, marginY, marginX, 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(0, height() - marginY * 2, marginX, marginY), Qt::AlignCenter, QString::number(spec.axis.y.min)); + + // x min-mid-max + painter.drawText(QRect(marginX, height() - marginY, marginX, marginY), Qt::AlignCenter, QString::number(spec.axis.x.min)); + painter.drawText(QRect(width() / 2 - marginX / 2, height() - marginY, marginX, marginY), Qt::AlignCenter, + 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)); + + // grid + if (spec.grid) { + painter.setPen(Qt::lightGray); + + int innerHeight = height() - 2 * marginY; + painter.drawLine(marginX, marginY + innerHeight / 4, width() - marginX, marginY + innerHeight / 4); + painter.drawLine(marginX, marginY + innerHeight / 2, width() - marginX, marginY + innerHeight / 2); + painter.drawLine(marginX, marginY + 3 * innerHeight / 4, width() - marginX, marginY + 3 * innerHeight / 4); + + int innerWidth = width() - 2 * marginX; + painter.drawLine(marginX + innerWidth / 4, marginY, marginX + innerWidth / 4, height() - marginY); + painter.drawLine(marginX + innerWidth / 2, marginY, marginX + innerWidth / 2, height() - marginY); + painter.drawLine(marginX + 3 * innerWidth / 4, marginY, marginX + 3 * innerWidth / 4, height() - marginY); } - x->setRange(spec.axis.x.min, spec.axis.x.max); - y->setRange(spec.axis.y.min, spec.axis.y.max); - decimateState = 0; + + // points + scaleX = (width() - marginX * 2) / (spec.axis.x.max - spec.axis.x.min); + scaleY = (height() - marginY * 2) / (spec.axis.y.max - spec.axis.y.min); + + painter.setPen(QPen(Qt::black, 1)); + + QPolygonF polygon; + for (const point_t point : points) { + // TODO: only paint visible points + // (+ 1-1 before and after) + polygon << pointToCoord(point); + } + + painter.drawPolyline(polygon); } ContextMenu::ContextMenu(QWidget* parent, ChartWidget* chartWidget) : QMenu(parent) { diff --git a/src/graph/server/gui/mainwindow.cpp b/src/graph/server/gui/mainwindow.cpp index 9d1966a..c8d0c31 100644 --- a/src/graph/server/gui/mainwindow.cpp +++ b/src/graph/server/gui/mainwindow.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -33,11 +34,11 @@ void MainWindow::close() { delete instance; } -void addData(unsigned int chartIndex, const char* seriesName, const point_t* points, int count) { +void addData(unsigned int chartIndex, const char* seriesName, const point_t* points, unsigned long count) { MainWindow::getInstance()->addData(chartIndex, seriesName, points, count); } -void MainWindow::addData(unsigned int chartIndex, const char* seriesName, const point_t* points, int count) { +void MainWindow::addData(unsigned int chartIndex, const char* seriesName, const point_t* points, unsigned long count) { if (chartIndex >= charts.size()) return; @@ -83,8 +84,10 @@ void MainWindow::on_action_Add_triggered() { charts.push_back(chart); - ui->graphsListWidget->addItem(chart); - ui->graphsListWidget->setItemWidget(chart, chart->getView()); + QListWidgetItem* item = new QListWidgetItem(); + + ui->graphsListWidget->addItem(item); + ui->graphsListWidget->setItemWidget(item, chart); } void MainWindow::on_action_Load_triggered() { @@ -136,6 +139,14 @@ void MainWindow::on_action_Load_triggered() { } void MainWindow::removeChart(ChartWidget* chartWidget) { + for (int i = 0; i < ui->graphsListWidget->count(); i++) { + ChartWidget* widget = (ChartWidget*)ui->graphsListWidget->itemWidget(ui->graphsListWidget->item(i)); + if (widget == chartWidget) { + ui->graphsListWidget->takeItem(i); + break; + } + } + for (auto it = charts.begin(); it != charts.end(); it++) { if ((*it) == chartWidget) { charts.erase(it); diff --git a/src/graph/server/gui/specdialog/specdialog.cpp b/src/graph/server/gui/specdialog/specdialog.cpp index 4db45e1..a62d1d6 100644 --- a/src/graph/server/gui/specdialog/specdialog.cpp +++ b/src/graph/server/gui/specdialog/specdialog.cpp @@ -45,7 +45,7 @@ chart_spec_t SpecDialog::getSpec() const { .grid = ui->grid->isChecked(), .legend = ui->legend->isChecked(), .keep_all = ui->keepAll->isChecked(), - .keep_limit = ui->keep->value(), + .keep_limit = static_cast(ui->keep->value()), .decimate = ui->decimate->value(), .axis = {.x = {.label = ui->xTitle->text(), .auto_limit = ui->xAutoRange->isChecked(), .min = ui->xMin->value(), .max = ui->xMax->value()}, .y = {.label = ui->yTitle->text(), .auto_limit = ui->yAutoRange->isChecked(), .min = ui->yMin->value(), .max = ui->yMax->value()}}, diff --git a/test/test.py b/test/test.py index 4897f35..08c9ad2 100755 --- a/test/test.py +++ b/test/test.py @@ -21,7 +21,7 @@ def sine_array(args): start = 0 while True: try: - x = np.linspace(start, start+0.5, 100) + x = np.linspace(start, start+0.5, args.num) y = np.sin(x) start += 0.5 @@ -33,18 +33,18 @@ def sine_array(args): } r = requests.post(f"http://{args.address}:{args.port}/add", json=data) - sleep(0.25) + sleep(args.delay) except KeyboardInterrupt: break def star(args): points = [ - (2,2), - (3,0), - (0,1), - (4,1), - (1,0), - (2,2) + (2/4,2/2), + (3/4,0), + (0,1/2), + (4/4,1/2), + (1/4,0), + (2/4,2/2) ] for point in points: requests.get(f"http://{args.address}:{args.port}/add?chart={args.chart}&series={args.series}&x={point[0]}&y={point[1]}")