diff --git a/viewer/charts/JFJochDatasetInfoChartView.cpp b/viewer/charts/JFJochDatasetInfoChartView.cpp index 48af1a8c..7043ea79 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.cpp +++ b/viewer/charts/JFJochDatasetInfoChartView.cpp @@ -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(values.size())) + return; + + // For binning > 1, show the binned mean at bin center + if (binning > 1) { + const int64_t nBins = + static_cast(values.size()) / binning; + if (nBins <= 0) + return; + + int64_t binIdx = val / binning; + binIdx = std::clamp(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(values.size())) + break; + const double v = values[static_cast(idx)]; + if (std::isfinite(v)) { + sum += v; + ++count; + } + } + + if (count > 0) { + const double mean = sum / static_cast(count); + if (std::isfinite(mean)) { + const double centerX = + (static_cast(binIdx) + 0.5) * static_cast(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(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(values.size()) / binning; + if (nBins <= 0) { + QChartView::mousePressEvent(event); + return; + } + + int64_t binIdx = + static_cast(std::floor(xVal / static_cast(binning))); + binIdx = std::clamp(binIdx, 0, nBins - 1); + + int64_t centerIdx = binIdx * binning + binning / 2; + if (centerIdx >= static_cast(values.size())) + centerIdx = static_cast(values.size()) - 1; + + selectedIdx = centerIdx; + } + + if (selectedIdx >= 0 && + selectedIdx < static_cast(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(binning)) { // At least one full point series = new QLineSeries(this); @@ -80,20 +157,23 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { double dispMax = -std::numeric_limits::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(values.size()); i++) { + if (!std::isfinite(values[static_cast(i)])) continue; + const double disp = values[static_cast(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(values.size() / binning); i++) { + for (int i = 0; i < static_cast(values.size() / static_cast(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(i) * binning + b; + if (idx >= static_cast(values.size())) + break; + const double v = values[static_cast(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(values.size())) { + + if (binning > 1) { + const int64_t nBins = + static_cast(values.size()) / binning; + if (nBins > 0) { + int64_t binIdx = curr_image / binning; + binIdx = std::clamp(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(values.size())) + break; + const double v = values[static_cast(idx)]; + if (std::isfinite(v)) { + sum += v; + ++count; + } + } + if (count > 0) { + const double mean = + sum / static_cast(count); + if (std::isfinite(mean)) { + const double centerX = + (static_cast(binIdx) + 0.5) * + static_cast(binning); + currentSeries->append(centerX, mean); + } + } + } + } else if (std::isfinite(values[static_cast(curr_image)])) { + currentSeries->append(curr_image, + values[static_cast(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 sub‑menu (values are defined only once here) + QMenu *binMenu = menu.addMenu("Binning"); + + const std::array binValues{1, 5, 10, 25, 50, 100, 250, 1000}; + QList binActions; + binActions.reserve(static_cast(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(values.size() - 1)) { + return; + } + + int64_t idx = 0; + double yv = std::numeric_limits::quiet_NaN(); + + if (binning <= 1) { + // Original behavior: nearest frame index and per-image value + idx = std::lround(xVal); + if (idx < 0 || idx >= static_cast(values.size())) + return; + yv = values[static_cast(idx)]; + } else { + // Binned mode: map x to bin, then use bin mean as the "current point" + const int64_t nBins = + static_cast(values.size()) / binning; + if (nBins <= 0) + return; + + int64_t binIdx = + static_cast(std::floor(xVal / static_cast(binning))); + binIdx = std::clamp(binIdx, 0, nBins - 1); + + // Representative frame index for status text & image loading + int64_t centerIdx = binIdx * binning + binning / 2; + if (centerIdx >= static_cast(values.size())) + centerIdx = static_cast(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(values.size())) + break; + const double v = values[static_cast(vIdx)]; + if (std::isfinite(v)) { + sum += v; + ++count; + } + } + if (count > 0) + yv = sum / static_cast(count); + else + yv = std::numeric_limits::quiet_NaN(); + } - // Convert x to closest image index - int64_t idx = std::lround(xVal); if (idx < 0 || idx >= static_cast(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(idx), 0.0), series); + const QPointF ptOnChart = + chart()->mapToPosition(QPointF(static_cast(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(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) { diff --git a/viewer/charts/JFJochDatasetInfoChartView.h b/viewer/charts/JFJochDatasetInfoChartView.h index fad25f07..3ac3267d 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.h +++ b/viewer/charts/JFJochDatasetInfoChartView.h @@ -54,15 +54,13 @@ class JFJochDatasetInfoChartView : public QChartView { std::vector 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