jfjoch_viewer: Enable binning in the context menu for dataset info chart view
All checks were successful
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 9m45s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 10m2s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 10m53s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 11m6s
Build Packages / Create release (push) Has been skipped
Build Packages / Build documentation (push) Successful in 57s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 12m21s
Build Packages / build:rpm (rocky8) (push) Successful in 12m58s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m19s
Build Packages / build:rpm (rocky9) (push) Successful in 13m57s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 7m37s
Build Packages / Unit tests (push) Successful in 51m25s

This commit is contained in:
2025-12-04 13:56:50 +01:00
parent b8f1ab8f0b
commit f796fdca57
2 changed files with 221 additions and 32 deletions

View File

@@ -20,14 +20,51 @@ JFJochDatasetInfoChartView::JFJochDatasetInfoChartView(QWidget *parent)
connect(m_hoverLoadTimer, &QTimer::timeout, this, &JFJochDatasetInfoChartView::onHoverLoadTimeout);
}
void JFJochDatasetInfoChartView::setImage(int64_t val) {
if (!currentSeries)
return;
curr_image = val;
currentSeries->clear();
if (val < values.size() && val >= 0) {
currentSeries->clear();
if (values.empty() || val < 0 ||
val >= static_cast<int64_t>(values.size()))
return;
// For binning > 1, show the binned mean at bin center
if (binning > 1) {
const int64_t nBins =
static_cast<int64_t>(values.size()) / binning;
if (nBins <= 0)
return;
int64_t binIdx = val / binning;
binIdx = std::clamp<int64_t>(binIdx, 0, nBins - 1);
double sum = 0.0;
int64_t count = 0;
for (int64_t b = 0; b < binning; ++b) {
const int64_t idx = binIdx * binning + b;
if (idx >= static_cast<int64_t>(values.size()))
break;
const double v = values[static_cast<size_t>(idx)];
if (std::isfinite(v)) {
sum += v;
++count;
}
}
if (count > 0) {
const double mean = sum / static_cast<double>(count);
if (std::isfinite(mean)) {
const double centerX =
(static_cast<double>(binIdx) + 0.5) * static_cast<double>(binning);
currentSeries->append(centerX, mean);
}
}
} else {
// binning == 1: original behavior, per-image value
if (std::isfinite(values[curr_image])) {
const double disp = values[curr_image];
if (std::isfinite(disp))
@@ -36,13 +73,53 @@ void JFJochDatasetInfoChartView::setImage(int64_t val) {
}
}
void JFJochDatasetInfoChartView::mousePressEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
QPointF clickedPoint = event->pos();
QPointF chartCoord = chart()->mapToValue(clickedPoint);
int64_t val = std::lround(chartCoord.x());
if (val >= 0 && val < values.size())
emit imageSelected(val);
if (values.empty()) {
QChartView::mousePressEvent(event);
return;
}
const QPointF clickedPoint = event->pos();
const QPointF chartCoord = chart()->mapToValue(clickedPoint, series);
const double xVal = chartCoord.x();
if (!std::isfinite(xVal) || xVal < 0.0 ||
xVal > static_cast<double>(values.size() - 1)) {
QChartView::mousePressEvent(event);
return;
}
int64_t selectedIdx = 0;
if (binning <= 1) {
// Original behavior: pick nearest frame index
selectedIdx = std::lround(xVal);
} else {
// Binned mode: pick bin index from x, then representative frame
const int64_t nBins =
static_cast<int64_t>(values.size()) / binning;
if (nBins <= 0) {
QChartView::mousePressEvent(event);
return;
}
int64_t binIdx =
static_cast<int64_t>(std::floor(xVal / static_cast<double>(binning)));
binIdx = std::clamp<int64_t>(binIdx, 0, nBins - 1);
int64_t centerIdx = binIdx * binning + binning / 2;
if (centerIdx >= static_cast<int64_t>(values.size()))
centerIdx = static_cast<int64_t>(values.size()) - 1;
selectedIdx = centerIdx;
}
if (selectedIdx >= 0 &&
selectedIdx < static_cast<int64_t>(values.size())) {
emit imageSelected(selectedIdx);
}
}
QChartView::mousePressEvent(event); // Call the base implementation
}
@@ -70,7 +147,7 @@ void JFJochDatasetInfoChartView::updateChart() {
}
void JFJochDatasetInfoChartView::buildTimeDomainChart() {
if (values.size() >= binning) {
if (values.size() >= static_cast<size_t>(binning)) {
// At least one full point
series = new QLineSeries(this);
@@ -80,20 +157,23 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() {
double dispMax = -std::numeric_limits<double>::infinity();
if (binning == 1) {
for (int i = 0; i < values.size(); i++) {
if (!std::isfinite(values[i])) continue;
const double disp = values[i];
for (int i = 0; i < static_cast<int>(values.size()); i++) {
if (!std::isfinite(values[static_cast<size_t>(i)])) continue;
const double disp = values[static_cast<size_t>(i)];
if (!std::isfinite(disp)) continue;
series->append(i, disp);
if (disp < dispMin) dispMin = disp;
if (disp > dispMax) dispMax = disp;
}
} else {
for (int i = 0; i < static_cast<int>(values.size() / binning); i++) {
for (int i = 0; i < static_cast<int>(values.size() / static_cast<size_t>(binning)); i++) {
double tmp = 0.0;
int64_t count = 0;
for (int b = 0; b < binning; b++) {
const double v = values[i * binning + b];
const int64_t idx = static_cast<int64_t>(i) * binning + b;
if (idx >= static_cast<int64_t>(values.size()))
break;
const double v = values[static_cast<size_t>(idx)];
if (std::isfinite(v)) {
tmp += v;
count++;
@@ -111,8 +191,44 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() {
}
}
if (curr_image < values.size() && curr_image >= 0 && std::isfinite(values[curr_image])) {
currentSeries->append(curr_image, values[curr_image]);
// ---- current point marker as binned value when binning > 1 ----
if (curr_image >= 0 &&
curr_image < static_cast<int64_t>(values.size())) {
if (binning > 1) {
const int64_t nBins =
static_cast<int64_t>(values.size()) / binning;
if (nBins > 0) {
int64_t binIdx = curr_image / binning;
binIdx = std::clamp<int64_t>(binIdx, 0, nBins - 1);
double sum = 0.0;
int64_t count = 0;
for (int64_t b = 0; b < binning; ++b) {
const int64_t idx = binIdx * binning + b;
if (idx >= static_cast<int64_t>(values.size()))
break;
const double v = values[static_cast<size_t>(idx)];
if (std::isfinite(v)) {
sum += v;
++count;
}
}
if (count > 0) {
const double mean =
sum / static_cast<double>(count);
if (std::isfinite(mean)) {
const double centerX =
(static_cast<double>(binIdx) + 0.5) *
static_cast<double>(binning);
currentSeries->append(centerX, mean);
}
}
}
} else if (std::isfinite(values[static_cast<size_t>(curr_image)])) {
currentSeries->append(curr_image,
values[static_cast<size_t>(curr_image)]);
}
}
chart()->addSeries(series);
@@ -248,7 +364,6 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() {
}
}
void JFJochDatasetInfoChartView::setBinning(int64_t val) {
if (val >= 1) {
binning = val;
@@ -273,6 +388,21 @@ void JFJochDatasetInfoChartView::contextMenuEvent(QContextMenuEvent *event) {
actXGoniometer->setChecked(m_xUseGoniometerAxis);
actXGoniometer->setEnabled(goniometer_axis.has_value());
// Binning submenu (values are defined only once here)
QMenu *binMenu = menu.addMenu("Binning");
const std::array<int, 8> binValues{1, 5, 10, 25, 50, 100, 250, 1000};
QList<QAction *> binActions;
binActions.reserve(static_cast<int>(binValues.size()));
for (int v : binValues) {
QAction *act = binMenu->addAction(QString::number(v));
act->setCheckable(true);
act->setChecked(binning == v);
act->setData(v); // remember which bin this action represents
binActions.push_back(act);
}
#ifdef JFJOCH_USE_FFTW
QAction *actShowFFT = menu.addAction("Show FFT (amplitude vs Hz)");
actShowFFT->setCheckable(true);
@@ -299,6 +429,13 @@ void JFJochDatasetInfoChartView::contextMenuEvent(QContextMenuEvent *event) {
} else if (chosen == actXGoniometer) {
m_xUseGoniometerAxis = !m_xUseGoniometerAxis;
updateChart();
} else if (binActions.contains(chosen)) {
// Any binning action selected: read the bin value from QAction::data
bool ok = false;
int v = chosen->data().toInt(&ok);
if (ok && v >= 1) {
setBinning(v);
}
#ifdef JFJOCH_USE_FFTW
} else if (chosen == actShowFFT) {
m_showFFT = !m_showFFT;
@@ -371,16 +508,67 @@ void JFJochDatasetInfoChartView::mouseMoveEvent(QMouseEvent *event) {
// Map mouse position to chart coordinates
const QPointF chartPos = chart()->mapToValue(event->pos(), series);
double xVal = chartPos.x();
const double xVal = chartPos.x();
if (!std::isfinite(xVal) || xVal < 0.0 ||
xVal > static_cast<double>(values.size() - 1)) {
return;
}
int64_t idx = 0;
double yv = std::numeric_limits<double>::quiet_NaN();
if (binning <= 1) {
// Original behavior: nearest frame index and per-image value
idx = std::lround(xVal);
if (idx < 0 || idx >= static_cast<int64_t>(values.size()))
return;
yv = values[static_cast<size_t>(idx)];
} else {
// Binned mode: map x to bin, then use bin mean as the "current point"
const int64_t nBins =
static_cast<int64_t>(values.size()) / binning;
if (nBins <= 0)
return;
int64_t binIdx =
static_cast<int64_t>(std::floor(xVal / static_cast<double>(binning)));
binIdx = std::clamp<int64_t>(binIdx, 0, nBins - 1);
// Representative frame index for status text & image loading
int64_t centerIdx = binIdx * binning + binning / 2;
if (centerIdx >= static_cast<int64_t>(values.size()))
centerIdx = static_cast<int64_t>(values.size()) - 1;
idx = centerIdx;
// Compute bin mean for hover display / "current" value
double sum = 0.0;
int64_t count = 0;
for (int64_t b = 0; b < binning; ++b) {
const int64_t vIdx = binIdx * binning + b;
if (vIdx >= static_cast<int64_t>(values.size()))
break;
const double v = values[static_cast<size_t>(vIdx)];
if (std::isfinite(v)) {
sum += v;
++count;
}
}
if (count > 0)
yv = sum / static_cast<double>(count);
else
yv = std::numeric_limits<double>::quiet_NaN();
}
// Convert x to closest image index
int64_t idx = std::lround(xVal);
if (idx < 0 || idx >= static_cast<int64_t>(values.size()))
return;
// Map that x position to scene coords for the vertical line
// Map that x position to scene coords for the vertical line.
// In binned mode this is the bin center index, in unbinned mode the exact frame.
const QRectF plotArea = chart()->plotArea();
const QPointF ptOnChart = chart()->mapToPosition(QPointF(static_cast<double>(idx), 0.0), series);
const QPointF ptOnChart =
chart()->mapToPosition(QPointF(static_cast<double>(idx), 0.0), series);
if (!m_hoverLine) {
m_hoverLine = new QGraphicsLineItem;
@@ -391,10 +579,8 @@ void JFJochDatasetInfoChartView::mouseMoveEvent(QMouseEvent *event) {
m_hoverLine->setLine(QLineF(ptOnChart.x(), plotArea.top(),
ptOnChart.x(), plotArea.bottom()));
// Status bar text
const double yv = values[static_cast<size_t>(idx)];
// Status bar text based on yv (bin mean in binned mode)
QString text;
if (m_yOneOverD) {
if (std::isfinite(yv) && yv > 0.0) {
const double d = 1.0 / std::sqrt(yv);
@@ -405,9 +591,13 @@ void JFJochDatasetInfoChartView::mouseMoveEvent(QMouseEvent *event) {
text = QString("image = %1, no resolution estimate").arg(idx);
}
} else {
text = QString("image = %1 value = %2")
.arg(idx)
.arg(yv, 0, 'g', 6);
if (std::isfinite(yv)) {
text = QString("image = %1 value = %2")
.arg(idx)
.arg(yv, 0, 'g', 6);
} else {
text = QString("image = %1, no value").arg(idx);
}
}
emit writeStatusBar(text, 6000);
@@ -418,14 +608,16 @@ void JFJochDatasetInfoChartView::mouseMoveEvent(QMouseEvent *event) {
if (idx != curr_image)
emit imageSelected(idx);
m_hoverLoadTimer->start(500); // debounce
} else
} else {
m_hoverPendingIdx = idx;
}
} else {
m_hoverLoadTimer->stop();
m_hoverPendingIdx = -1;
}
}
void JFJochDatasetInfoChartView::leaveEvent(QEvent *event) {
QChartView::leaveEvent(event);
if (m_hoverLine) {

View File

@@ -54,15 +54,13 @@ class JFJochDatasetInfoChartView : public QChartView {
std::vector<double> m_fftFrequenciesHz;
#endif
signals:
void imageSelected(int64_t number);
void writeStatusBar(QString string, int timeout_ms = 0);
private slots:
void onHoverLoadTimeout();
void setBinning(int64_t val);
private:
void mousePressEvent(QMouseEvent *event) override;
@@ -76,7 +74,6 @@ public:
public slots:
void resetZoom();
void setBinning(int64_t val);
public:
template<class T>