basic custom chart widget

This commit is contained in:
Benedek László 2024-05-19 15:31:54 +02:00
parent 966db4f4e1
commit df8cd70b0d
9 changed files with 176 additions and 152 deletions

View File

@ -13,7 +13,7 @@ typedef struct {
typedef int (*run_method_t)(); typedef int (*run_method_t)();
typedef int (*destroy_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 { typedef struct {
uint32_t version; uint32_t version;

View File

@ -2,36 +2,36 @@
#include <graph/plugins/plugin.h> #include <graph/plugins/plugin.h>
#include <graph/server/spec.h> #include <graph/server/spec.h>
#include <qevent.h>
#include <QAction> #include <QAction>
#include <QChart>
#include <QChartView>
#include <QLineSeries>
#include <QListWidgetItem>
#include <QMenu> #include <QMenu>
#include <QValueAxis>
#include <QMutex> #include <QMutex>
namespace Graph::GUI { namespace Graph::GUI {
class ChartWidget : public QListWidgetItem { class ChartWidget : public QWidget {
Q_OBJECT
public: public:
ChartWidget(QWidget* parent = nullptr, const chart_spec_t& spec = chart_spec_t()); ChartWidget(QWidget* parent = nullptr, const chart_spec_t& spec = chart_spec_t());
~ChartWidget(); ~ChartWidget();
void addData(const char* seriesName, const point_t* points, int count); void addData(const char* seriesName, const point_t* points, unsigned long count);
QChartView* getView() const { return view; }
QList<QAbstractSeries*> getSeries() const { return chart->series(); }
void clear(); void clear();
protected:
QSize sizeHint() const override;
void paintEvent(QPaintEvent *e) override;
private: private:
QChartView* view;
QChart* chart;
QValueAxis *x, *y;
chart_spec_t spec; chart_spec_t spec;
int decimateState = 0; int decimateState = 0;
QMutex mutex; QMutex mutex;
std::list<point_t> points;
double scaleX, scaleY;
int marginX = 40;
int marginY = 20;
QPointF pointToCoord(const point_t& point);
}; };
class ContextMenu : public QMenu { class ContextMenu : public QMenu {

View File

@ -28,7 +28,7 @@ class MainWindow : public QMainWindow {
static MainWindow* getInstance(); static MainWindow* getInstance();
static void close(); 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); void removeChart(ChartWidget* chartWidget);

View File

@ -15,7 +15,7 @@ struct chart_spec_t {
bool grid = false; bool grid = false;
bool legend = false; bool legend = false;
bool keep_all = true; bool keep_all = true;
int keep_limit; unsigned long keep_limit;
int decimate; int decimate;
struct { struct {
axis_spec_t x, y; axis_spec_t x, y;

View File

@ -1,4 +1,4 @@
find_package(Qt6 REQUIRED COMPONENTS Widgets Charts) find_package(Qt6 REQUIRED COMPONENTS Widgets)
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
@ -17,5 +17,5 @@ add_executable(server
) )
target_include_directories(server PRIVATE "${CMAKE_BINARY_DIR}/src") 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}") set_target_properties(server PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")

View File

@ -1,116 +1,72 @@
#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 <qabstractseries.h> #include <qbrush.h>
#include <qlineseries.h> #include <qnamespace.h>
#include <qmutex.h> #include <qpoint.h>
#include <QIcon> #include <qpolygon.h>
#include <qsize.h>
#include <QPainter>
#include <iostream> #include <iostream>
#include <iterator>
#include <limits> #include <limits>
#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)
namespace Graph::GUI { namespace Graph::GUI {
ChartWidget::ChartWidget(QWidget* parent, const chart_spec_t& spec) : QListWidgetItem(), spec(spec) { ChartWidget::ChartWidget(QWidget* parent, const chart_spec_t& spec) : QWidget(parent), spec(spec) {
view = new QChartView(parent); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
chart = new QChart();
// chart setMinimumSize(QSize(600, 350));
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);
// context menu // context menu
view->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
view->connect(view, &QChartView::customContextMenuRequested, view, [this](const QPoint& pos) { connect(this, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
ContextMenu menu(nullptr, this); ContextMenu menu(nullptr, this);
menu.exec(view->mapToGlobal(pos)); menu.exec(mapToGlobal(pos));
}); });
} }
ChartWidget::~ChartWidget() { ChartWidget::~ChartWidget() {}
// delete view;
// delete chart;
// delete x;
// delete y;
}
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); QMutexLocker locker(&mutex);
std::cout << "locked: " << locker.isLocked() << std::endl; double minX = std::numeric_limits<double>::max();
double minY = std::numeric_limits<double>::max();
double maxX = std::numeric_limits<double>::min();
double maxY = std::numeric_limits<double>::min();
QString name(seriesName);
double minX = spec.keep_all ? x->min() : std::numeric_limits<double>::max();
double minY = spec.keep_all ? y->min() : std::numeric_limits<double>::max();
double maxX = spec.keep_all ? x->max() : std::numeric_limits<double>::min();
double maxY = spec.keep_all ? y->max() : std::numeric_limits<double>::min();
for (QAbstractSeries* series : chart->series()) {
if (series->name() == name) {
if (!spec.keep_all) { if (!spec.keep_all) {
if (count >= spec.keep_limit) { if (count >= spec.keep_limit) {
((QLineSeries*)series)->clear(); this->points.clear();
} else if (((QLineSeries*)series)->count() + count > spec.keep_limit) { } else if (this->points.size() + count > spec.keep_limit) {
std::cout << "count: " << count << ", size: " << ((QLineSeries*)series)->count() << ", remove: " << ((QLineSeries*)series)->count() - (spec.keep_limit - count) << std::endl; std::list<point_t>::iterator end = std::next(this->points.begin(), this->points.size() - (spec.keep_limit - count));
((QLineSeries*)series)->removePoints(0, ((QLineSeries*)series)->count() - (spec.keep_limit - count)); this->points.erase(this->points.begin(), end);
}
} }
std::cout << "starting min/max" << std::endl; for (const point_t& point : this->points) {
for (const QPointF& point : ((QLineSeries*)series)->points()) {
if (spec.axis.x.auto_limit) { if (spec.axis.x.auto_limit) {
minX = MIN(minX, point.x()); minX = MIN(minX, point.x);
maxX = MAX(maxX, point.x()); maxX = MAX(maxX, point.x);
} }
if (spec.axis.y.auto_limit) { if (spec.axis.y.auto_limit) {
minY = MIN(minY, point.y()); minY = MIN(minY, point.y);
maxY = MAX(maxY, point.y()); maxY = MAX(maxY, point.y);
} }
} }
std::cout << "got min/max" << std::endl;
}
std::cout << "start adding" << std::endl; for (unsigned long i = spec.keep_all ? 0 : (count < spec.keep_limit ? 0 : count - spec.keep_limit); i < count; i++) {
QList<QPointF> newPoints;
for (int i = spec.keep_all ? 0 : MAX(0, count - spec.keep_limit); i < count; i++) {
if (decimateState >= spec.decimate) if (decimateState >= spec.decimate)
decimateState = 0; decimateState = 0;
if (decimateState++ % spec.decimate != 0) if (decimateState++ % spec.decimate != 0)
continue; continue;
this->points.push_back(points[i]);
if (spec.axis.x.auto_limit) { if (spec.axis.x.auto_limit) {
minX = MIN(minX, points[i].x); minX = MIN(minX, points[i].x);
maxX = MAX(maxX, points[i].x); maxX = MAX(maxX, points[i].x);
@ -119,32 +75,89 @@ void ChartWidget::addData(const char* seriesName, const point_t* points, int cou
minY = MIN(minY, points[i].y); minY = MIN(minY, points[i].y);
maxY = MAX(maxY, points[i].y); maxY = MAX(maxY, points[i].y);
} }
newPoints.append(QPointF(points[i].x, points[i].y));
} }
std::cout << "adding" << std::endl; if (spec.axis.x.auto_limit) {
((QLineSeries*)series)->append(newPoints); spec.axis.x.min = minX;
spec.axis.x.max = maxX;
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.axis.y.auto_limit) {
spec.axis.y.min = minY;
spec.axis.y.max = maxY;
} }
update();
} }
void ChartWidget::clear() { void ChartWidget::clear() {
QMutexLocker locker(&mutex); QMutexLocker locker(&mutex);
points.clear();
for (QAbstractSeries* series : chart->series()) {
((QLineSeries*)series)->clear();
} }
x->setRange(spec.axis.x.min, spec.axis.x.max);
y->setRange(spec.axis.y.min, spec.axis.y.max); QSize ChartWidget::sizeHint() const {
decimateState = 0; 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);
}
// 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) { ContextMenu::ContextMenu(QWidget* parent, ChartWidget* chartWidget) : QMenu(parent) {

View File

@ -4,6 +4,7 @@
#include <graph/server/plugin.h> #include <graph/server/plugin.h>
#include <graph/server/spec.h> #include <graph/server/spec.h>
#include <graph/server/ui_mainwindow.h> #include <graph/server/ui_mainwindow.h>
#include <qlistwidget.h>
#include <QFileDialog> #include <QFileDialog>
#include <QMessageBox> #include <QMessageBox>
#include <iostream> #include <iostream>
@ -33,11 +34,11 @@ void MainWindow::close() {
delete instance; 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); 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()) if (chartIndex >= charts.size())
return; return;
@ -83,8 +84,10 @@ void MainWindow::on_action_Add_triggered() {
charts.push_back(chart); charts.push_back(chart);
ui->graphsListWidget->addItem(chart); QListWidgetItem* item = new QListWidgetItem();
ui->graphsListWidget->setItemWidget(chart, chart->getView());
ui->graphsListWidget->addItem(item);
ui->graphsListWidget->setItemWidget(item, chart);
} }
void MainWindow::on_action_Load_triggered() { void MainWindow::on_action_Load_triggered() {
@ -136,6 +139,14 @@ void MainWindow::on_action_Load_triggered() {
} }
void MainWindow::removeChart(ChartWidget* chartWidget) { 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++) { for (auto it = charts.begin(); it != charts.end(); it++) {
if ((*it) == chartWidget) { if ((*it) == chartWidget) {
charts.erase(it); charts.erase(it);

View File

@ -45,7 +45,7 @@ chart_spec_t SpecDialog::getSpec() const {
.grid = ui->grid->isChecked(), .grid = ui->grid->isChecked(),
.legend = ui->legend->isChecked(), .legend = ui->legend->isChecked(),
.keep_all = ui->keepAll->isChecked(), .keep_all = ui->keepAll->isChecked(),
.keep_limit = ui->keep->value(), .keep_limit = static_cast<unsigned long>(ui->keep->value()),
.decimate = ui->decimate->value(), .decimate = ui->decimate->value(),
.axis = {.x = {.label = ui->xTitle->text(), .auto_limit = ui->xAutoRange->isChecked(), .min = ui->xMin->value(), .max = ui->xMax->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()}}, .y = {.label = ui->yTitle->text(), .auto_limit = ui->yAutoRange->isChecked(), .min = ui->yMin->value(), .max = ui->yMax->value()}},

View File

@ -21,7 +21,7 @@ def sine_array(args):
start = 0 start = 0
while True: while True:
try: try:
x = np.linspace(start, start+0.5, 100) x = np.linspace(start, start+0.5, args.num)
y = np.sin(x) y = np.sin(x)
start += 0.5 start += 0.5
@ -33,18 +33,18 @@ def sine_array(args):
} }
r = requests.post(f"http://{args.address}:{args.port}/add", json=data) r = requests.post(f"http://{args.address}:{args.port}/add", json=data)
sleep(0.25) sleep(args.delay)
except KeyboardInterrupt: except KeyboardInterrupt:
break break
def star(args): def star(args):
points = [ points = [
(2,2), (2/4,2/2),
(3,0), (3/4,0),
(0,1), (0,1/2),
(4,1), (4/4,1/2),
(1,0), (1/4,0),
(2,2) (2/4,2/2)
] ]
for point in points: 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]}") requests.get(f"http://{args.address}:{args.port}/add?chart={args.chart}&series={args.series}&x={point[0]}&y={point[1]}")