basic custom chart widget
This commit is contained in:
parent
966db4f4e1
commit
df8cd70b0d
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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}")
|
@ -1,150 +1,163 @@
|
||||
#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));
|
||||
}
|
||||
|
||||
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<QPointF> 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<point_t>::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) {
|
||||
|
@ -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);
|
||||
|
@ -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()}},
|
||||
|
16
test/test.py
16
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]}")
|
||||
|
Loading…
Reference in New Issue
Block a user