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 (*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;

View File

@ -2,36 +2,36 @@
#include <graph/plugins/plugin.h>
#include <graph/server/spec.h>
#include <qevent.h>
#include <QAction>
#include <QChart>
#include <QChartView>
#include <QLineSeries>
#include <QListWidgetItem>
#include <QMenu>
#include <QValueAxis>
#include <QMutex>
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<QAbstractSeries*> 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<point_t> points;
double scaleX, scaleY;
int marginX = 40;
int marginY = 20;
QPointF pointToCoord(const point_t& point);
};
class ContextMenu : public QMenu {

View File

@ -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);

View File

@ -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;

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_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}")

View File

@ -1,116 +1,72 @@
#include <graph/server/gui/chart/chart.h>
#include <graph/server/gui/mainwindow.h>
#include <qabstractseries.h>
#include <qlineseries.h>
#include <qmutex.h>
#include <QIcon>
#include <qbrush.h>
#include <qnamespace.h>
#include <qpoint.h>
#include <qpolygon.h>
#include <qsize.h>
#include <QPainter>
#include <iostream>
#include <iterator>
#include <limits>
#include <string>
#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<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 (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));
this->points.clear();
} else if (this->points.size() + count > spec.keep_limit) {
std::list<point_t>::iterator end = std::next(this->points.begin(), this->points.size() - (spec.keep_limit - count));
this->points.erase(this->points.begin(), end);
}
}
std::cout << "starting min/max" << std::endl;
for (const QPointF& point : ((QLineSeries*)series)->points()) {
for (const point_t& point : this->points) {
if (spec.axis.x.auto_limit) {
minX = MIN(minX, point.x());
maxX = MAX(maxX, point.x());
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());
minY = MIN(minY, point.y);
maxY = MAX(maxY, point.y);
}
}
std::cout << "got min/max" << std::endl;
}
std::cout << "start adding" << std::endl;
QList<QPointF> newPoints;
for (int i = spec.keep_all ? 0 : MAX(0, count - spec.keep_limit); i < count; i++) {
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);
@ -119,32 +75,89 @@ void ChartWidget::addData(const char* seriesName, const point_t* points, int cou
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.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) {

View File

@ -4,6 +4,7 @@
#include <graph/server/plugin.h>
#include <graph/server/spec.h>
#include <graph/server/ui_mainwindow.h>
#include <qlistwidget.h>
#include <QFileDialog>
#include <QMessageBox>
#include <iostream>
@ -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);

View File

@ -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<unsigned long>(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()}},

View File

@ -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]}")