All checks were successful
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 7m51s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 7m19s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 7m46s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 8m32s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 8m6s
Build Packages / build:rpm (rocky8) (push) Successful in 8m7s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 7m37s
Build Packages / Generate python client (push) Successful in 17s
Build Packages / Create release (push) Has been skipped
Build Packages / Build documentation (push) Successful in 32s
Build Packages / build:rpm (rocky9) (push) Successful in 9m6s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 6m53s
Build Packages / Unit tests (push) Successful in 1h9m39s
This is an UNSTABLE release. * jfjoch_viewer: Minor improvements to the viewer * jfjoch_broker: Change behavior for modular detectors: coordinates of 0-th pixel can be now arbitrary and detector will be cropped to the smallest rectangle limited by module coordinates Reviewed-on: #8 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch> Co-committed-by: Filip Leonarski <filip.leonarski@psi.ch>
251 lines
8.7 KiB
C++
251 lines
8.7 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "JFJochSimpleChartView.h"
|
|
|
|
#include <QMenu>
|
|
#include <QClipboard>
|
|
#include <QApplication>
|
|
#include <QCategoryAxis>
|
|
#include <QRegularExpression>
|
|
|
|
JFJochSimpleChartView::JFJochSimpleChartView(QWidget *parent)
|
|
: QChartView(new QChart(), parent) {
|
|
chart()->legend()->hide();
|
|
setFixedHeight(300);
|
|
setRenderHint(QPainter::Antialiasing);
|
|
setMouseTracking(true);
|
|
//setRubberBand(QChartView::RubberBand::HorizontalRubberBand);
|
|
}
|
|
|
|
void JFJochSimpleChartView::UpdateData(const std::vector<float> &in_x, const std::vector<float> &in_y,
|
|
QString legend_x, QString legend_y, bool in_one_over_d) {
|
|
one_over_d = in_one_over_d;
|
|
x = in_x;
|
|
y = in_y;
|
|
|
|
// Remove hover line if any
|
|
if (m_hoverLine) {
|
|
chart()->scene()->removeItem(m_hoverLine);
|
|
delete m_hoverLine;
|
|
m_hoverLine = nullptr;
|
|
}
|
|
m_series = nullptr;
|
|
|
|
chart()->removeAllSeries();
|
|
// Remove all axes to avoid duplicates
|
|
for (auto ax: chart()->axes()) chart()->removeAxis(ax);
|
|
|
|
if (x.empty() || x.size() != y.size()) return;
|
|
|
|
auto *series = new QLineSeries(this);
|
|
for (size_t i = 0; i < x.size(); ++i) series->append(x[i], y[i]);
|
|
chart()->addSeries(series);
|
|
m_series = series;
|
|
|
|
// Compute Y range
|
|
double ymin = 0.0, ymax = 0.0;
|
|
{
|
|
auto [minYIt, maxYIt] = std::minmax_element(y.begin(), y.end());
|
|
ymin = static_cast<double>(*minYIt);
|
|
ymax = static_cast<double>(*maxYIt);
|
|
if (ymin == ymax) {
|
|
const double eps = (std::abs(ymax) > 0.0) ? std::abs(ymax) * 1e-6 : 1.0;
|
|
ymin -= eps;
|
|
ymax += eps;
|
|
}
|
|
}
|
|
|
|
// Hidden value axis (left): range + grid
|
|
auto *axYvalue = new QValueAxis();
|
|
axYvalue->setTitleText(legend_y);
|
|
axYvalue->setRange(ymin, ymax);
|
|
axYvalue->setTickCount(5); // ensure ticks exist now (avoid 0 before layout)
|
|
axYvalue->setLabelsVisible(false); // hide numeric labels (grid only)
|
|
chart()->addAxis(axYvalue, Qt::AlignRight);
|
|
series->attachAxis(axYvalue);
|
|
|
|
// SI formatter (Plotly/D3-like ".3~s")
|
|
int precision = 3;
|
|
const auto formatSI = [precision](double v) -> QString {
|
|
if (v == 0.0 || std::isnan(v)) return QStringLiteral("0");
|
|
static const struct { int exp; const char* pre; } si[] = {
|
|
{24,"Y"},{21,"Z"},{18,"E"},{15,"P"},{12,"T"},{9,"G"},{6,"M"},{3,"k"},
|
|
{0,""},{-3,"m"},{-6,"µ"},{-9,"n"},{-12,"p"},{-15,"f"},{-18,"a"},{-21,"z"},{-24,"y"}
|
|
};
|
|
double av = std::fabs(v);
|
|
int chosenExp = 0; const char* chosenPre = "";
|
|
if (av > 0.0) {
|
|
int exp10 = static_cast<int>(std::floor(std::log10(av)));
|
|
int exp3 = (exp10 >= 0) ? (exp10 / 3) * 3 : -(((-exp10 + 2) / 3) * 3);
|
|
if (exp3 > 24) exp3 = 24;
|
|
if (exp3 < -24) exp3 = -24;
|
|
for (auto &e : si) if (e.exp == exp3) { chosenExp = e.exp; chosenPre = e.pre; break; }
|
|
}
|
|
const double scaled = v / std::pow(10.0, chosenExp);
|
|
QString num = QString::number(scaled, 'g', std::max(1, precision)); // ~ trimming effect
|
|
return (chosenPre && *chosenPre) ? num + QStringLiteral(" ") + QString::fromUtf8(chosenPre) : num;
|
|
};
|
|
|
|
// Visible category axis (right) with formatted labels
|
|
auto *axYcat = new QCategoryAxis();
|
|
axYcat->setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue);
|
|
axYcat->setGridLineVisible(false);
|
|
axYcat->setMinorGridLineVisible(false);
|
|
axYcat->setTitleText(legend_y);
|
|
axYcat->setLabelsVisible(true); // make sure labels are actually shown
|
|
|
|
// Build ticks from hidden axis tick count immediately (stable even before layout)
|
|
const int tickCountY = std::max(2, axYvalue->tickCount());
|
|
const double ystep = (ymax - ymin) / (tickCountY - 1);
|
|
for (int i = 0; i < tickCountY; ++i) {
|
|
const double yv = (i == tickCountY - 1) ? ymax : (ymin + i * ystep);
|
|
axYcat->append(formatSI(yv), yv);
|
|
}
|
|
chart()->addAxis(axYcat, Qt::AlignLeft);
|
|
series->attachAxis(axYcat);
|
|
|
|
// Give a bit more room on the right so labels are not clipped
|
|
QMargins m = chart()->margins();
|
|
if (m.right() < 12) {
|
|
m.setRight(12);
|
|
chart()->setMargins(m);
|
|
}
|
|
|
|
// Build X range
|
|
const float xmin = x[0], xmax = x[x.size() - 1];
|
|
|
|
if (one_over_d) {
|
|
auto *axXTop = new QValueAxis();
|
|
axXTop->setRange(xmin, xmax);
|
|
axXTop->setTickCount(5);
|
|
axXTop->setLabelsVisible(false);
|
|
chart()->addAxis(axXTop, Qt::AlignTop);
|
|
series->attachAxis(axXTop);
|
|
|
|
auto *axXcat = new QCategoryAxis();
|
|
axXcat->setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue);
|
|
axXcat->setGridLineVisible(false);
|
|
axXcat->setMinorGridLineVisible(false);
|
|
axXcat->setTitleText(legend_x);
|
|
|
|
const int tickCount = axXTop->tickCount();
|
|
const double step = (tickCount > 1) ? (xmax - xmin) / (tickCount - 1) : 0.0;
|
|
for (int i = 0; i < tickCount; ++i) {
|
|
const double xv = xmin + i * step;
|
|
const QString lab = (std::abs(xv) < 1e-12)
|
|
? QStringLiteral("∞")
|
|
: QString::number(1.0 / sqrtf(xv), 'f', 2);
|
|
axXcat->append(lab, xv);
|
|
}
|
|
chart()->addAxis(axXcat, Qt::AlignBottom);
|
|
series->attachAxis(axXcat);
|
|
} else {
|
|
auto *axX = new QValueAxis();
|
|
axX->setRange(xmin, xmax);
|
|
axX->setTitleText(legend_x);
|
|
chart()->addAxis(axX, Qt::AlignBottom);
|
|
series->attachAxis(axX);
|
|
}
|
|
}
|
|
|
|
|
|
void JFJochSimpleChartView::ClearData() {
|
|
x.clear();
|
|
y.clear();
|
|
m_series = nullptr;
|
|
if (m_hoverLine) {
|
|
chart()->scene()->removeItem(m_hoverLine);
|
|
delete m_hoverLine;
|
|
m_hoverLine = nullptr;
|
|
}
|
|
chart()->removeAllSeries();
|
|
}
|
|
|
|
void JFJochSimpleChartView::contextMenuEvent(QContextMenuEvent *event) {
|
|
QMenu menu(this);
|
|
QAction *copyXY = menu.addAction("Copy (x y) points");
|
|
copyXY->setEnabled(!x.empty() && x.size() == y.size());
|
|
|
|
QAction *chosen = menu.exec(event->globalPos());
|
|
if (chosen == copyXY) {
|
|
QString out;
|
|
out.reserve(static_cast<int>(x.size() * 16)); // rough prealloc
|
|
for (size_t i = 0; i < x.size() && i < y.size(); ++i) {
|
|
out.append(QString::number(x[i], 'g', 10));
|
|
out.append(' ');
|
|
out.append(QString::number(y[i], 'g', 10));
|
|
if (i + 1 < x.size()) out.append('\n');
|
|
}
|
|
QClipboard *cb = QApplication::clipboard();
|
|
cb->setText(out);
|
|
}
|
|
}
|
|
|
|
void JFJochSimpleChartView::mouseMoveEvent(QMouseEvent *event) {
|
|
QChartView::mouseMoveEvent(event);
|
|
if (!m_series || x.empty() || x.size() != y.size())
|
|
return;
|
|
|
|
// Map mouse position to chart coordinates
|
|
const QPointF chartPos = chart()->mapToValue(event->pos(), m_series);
|
|
const double xVal = chartPos.x();
|
|
|
|
// Find closest index in x[]
|
|
auto it = std::lower_bound(x.begin(), x.end(), static_cast<float>(xVal));
|
|
if (it == x.end() && !x.empty())
|
|
it = std::prev(x.end());
|
|
if (it == x.end())
|
|
return;
|
|
|
|
size_t idx = static_cast<size_t>(std::distance(x.begin(), it));
|
|
if (idx > 0) {
|
|
const float xPrev = x[idx - 1];
|
|
if (std::abs(xPrev - xVal) < std::abs(x[idx] - xVal))
|
|
--idx;
|
|
}
|
|
|
|
const float xNearest = x[idx];
|
|
const float yNearest = y[idx];
|
|
|
|
// Map that data point to scene coords to get the x position
|
|
const QPointF ptOnSeries = chart()->mapToPosition(QPointF(xNearest, yNearest), m_series);
|
|
const QRectF plotArea = chart()->plotArea();
|
|
|
|
if (!m_hoverLine) {
|
|
m_hoverLine = new QGraphicsLineItem;
|
|
m_hoverLine->setPen(QPen(QColor(200, 0, 0, 150), 1.0));
|
|
chart()->scene()->addItem(m_hoverLine);
|
|
}
|
|
|
|
m_hoverLine->setLine(QLineF(ptOnSeries.x(), plotArea.top(),
|
|
ptOnSeries.x(), plotArea.bottom()));
|
|
|
|
// Send to status bar
|
|
emit writeStatusBar(getText(idx), 6000);
|
|
}
|
|
|
|
QString JFJochSimpleChartView::getText(size_t idx) {
|
|
if (idx > x.size())
|
|
return {};
|
|
|
|
if (one_over_d)
|
|
return QString("d = %1 Å, y = %2")
|
|
.arg(1 / sqrt(x[idx]), 0, 'g', 4)
|
|
.arg(y[idx], 0, 'g', 6);
|
|
else
|
|
return QString("x = %1, y = %2")
|
|
.arg(x[idx], 0, 'g', 6)
|
|
.arg(y[idx], 0, 'g', 6);
|
|
}
|
|
|
|
void JFJochSimpleChartView::leaveEvent(QEvent *event) {
|
|
QChartView::leaveEvent(event);
|
|
if (m_hoverLine) {
|
|
chart()->scene()->removeItem(m_hoverLine);
|
|
delete m_hoverLine;
|
|
m_hoverLine = nullptr;
|
|
}
|
|
emit writeStatusBar(QString(), 6000); // clear status bar when leaving
|
|
}
|