viewer: overlay one plot line per processing run (with editable names)

The dataset-info plot now overlays every run as a separate named line instead of
replacing the plot when a snapshot is activated:

- Reader gains AllSnapshotDatasets() (every snapshot's dataset, Original first).
- The worker owns the run collection: a stable snapshot id plus an editable
  display label (RunData), emitted as runsChanged(runs, active_id) on file open,
  snapshot register, activate and rename. RenameRun(id, label) updates the legend
  label without touching the reader key (decoupled id vs label).
- The chart view draws the active run as the primary series (markers, hover,
  binning, axes) and the other runs as overlay lines sharing its axes, with a
  legend shown when overlaying (appendSeries factored out for both).
- The dataset-info widget holds the run list + the live run, extracts the selected
  metric from each (ExtractMetric), and unions the available metrics across runs.
- The live run is its own "Live" overlay (lightweight per-tick refresh, no combo
  rebuild); it is cleared on finish so the persisted snapshot takes over.
- The processing jobs table gets a "Started" column and an editable Name column;
  editing a name renames that run's legend label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 11:28:47 +02:00
co-authored by Claude Opus 4.8
parent b735aec1c4
commit aceff23ce2
14 changed files with 358 additions and 163 deletions
+14
View File
@@ -178,3 +178,17 @@ std::string JFJochHDF5Reader::ActiveSnapshot() const {
std::unique_lock ul(hdf5_mutex);
return active_snapshot_;
}
std::vector<std::pair<std::string, std::shared_ptr<const JFJochReaderDataset>>>
JFJochHDF5Reader::AllSnapshotDatasets() const {
std::unique_lock ul(hdf5_mutex);
std::vector<std::pair<std::string, std::shared_ptr<const JFJochReaderDataset>>> out;
out.reserve(snapshots_.size());
// "Original" first, then the rest, so overlay colours stay stable across updates.
if (auto it = snapshots_.find("Original"); it != snapshots_.end())
out.emplace_back(it->first, it->second->Dataset());
for (const auto &[name, source]: snapshots_)
if (name != "Original")
out.emplace_back(name, source->Dataset());
return out;
}
+3
View File
@@ -63,4 +63,7 @@ public:
void SetActiveSnapshot(const std::string &name);
std::vector<std::string> SnapshotNames() const;
std::string ActiveSnapshot() const;
// All snapshot datasets (name -> dataset), for overlaying every run's plots at once.
std::vector<std::pair<std::string, std::shared_ptr<const JFJochReaderDataset>>> AllSnapshotDatasets() const;
};
+1
View File
@@ -29,6 +29,7 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
image_viewer/JFJochSimpleImage.h
JFJochImageReadingWorker.cpp
JFJochImageReadingWorker.h
RunData.h
JFJochProcessController.cpp
JFJochProcessController.h
JFJochViewerDatasetInfo.cpp
+33 -4
View File
@@ -97,6 +97,8 @@ JFJochImageReadingWorker::JFJochImageReadingWorker(const SpotFindingSettings &se
qRegisterMetaType<ROIDefinition>("ROIDefinition");
qRegisterMetaType<BraggIntegrationSettings>("BraggIntegrationSettings");
qRegisterMetaType<ScalingSettings>("ScalingSettings");
qRegisterMetaType<RunData>("RunData");
qRegisterMetaType<QVector<RunData>>("QVector<RunData>");
spot_finding_settings = settings;
indexing = std::make_unique<IndexerThreadPool>(indexing_settings);
@@ -287,6 +289,12 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_
emit datasetLoaded(dataset);
// Reset the run collection for the freshly opened file (file mode only has snapshots).
run_labels_.clear();
if (!http_mode)
run_labels_["Original"] = "Original";
EmitRuns_i();
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
logger.Info(fmt::format("Loaded file {} in {} ms", filename.toStdString(), duration));
@@ -948,6 +956,7 @@ void JFJochImageReadingWorker::ActivateSnapshot_i(const QString &name) {
for (const auto &n: file_reader.SnapshotNames())
names << QString::fromStdString(n);
emit snapshotsChanged(names, QString::fromStdString(file_reader.ActiveSnapshot()));
EmitRuns_i();
if (current_image.has_value()) {
// A snapshot carries its own per-image results; just re-read the image against the new
@@ -959,17 +968,37 @@ void JFJochImageReadingWorker::ActivateSnapshot_i(const QString &name) {
}
}
void JFJochImageReadingWorker::RegisterProcessingSnapshot(QString name, QString master_path) {
void JFJochImageReadingWorker::EmitRuns_i() {
// Assumes m locked! Build the run list (every snapshot dataset + its display label).
QVector<RunData> runs;
if (!http_mode) {
for (const auto &[id, dataset]: file_reader.AllSnapshotDatasets()) {
const auto it = run_labels_.find(id);
const QString label = (it != run_labels_.end()) ? it->second : QString::fromStdString(id);
runs.push_back(RunData{QString::fromStdString(id), label, dataset});
}
}
emit runsChanged(runs, http_mode ? QString() : QString::fromStdString(file_reader.ActiveSnapshot()));
}
void JFJochImageReadingWorker::RenameRun(QString id, QString label) {
QMutexLocker ul(&m);
run_labels_[id.toStdString()] = label;
EmitRuns_i();
}
void JFJochImageReadingWorker::RegisterProcessingSnapshot(QString id, QString label, QString master_path) {
QMutexLocker ul(&m);
if (http_mode) {
logger.Error("Processing snapshots are only available for files");
return;
}
try {
file_reader.RegisterSnapshot(name.toStdString(), master_path.toStdString());
ActivateSnapshot_i(name);
file_reader.RegisterSnapshot(id.toStdString(), master_path.toStdString());
run_labels_[id.toStdString()] = label;
ActivateSnapshot_i(id); // activates + emits datasetLoaded / snapshotsChanged / runsChanged
} catch (const std::exception &e) {
logger.Error("Failed to register snapshot {}: {}", name.toStdString(), e.what());
logger.Error("Failed to register snapshot {}: {}", id.toStdString(), e.what());
emit fileLoadError("Snapshot error", QString::fromStdString(e.what()));
}
}
+7 -1
View File
@@ -46,6 +46,8 @@ struct ReprocessingInputs {
SpotFindingSettings spot_finding;
};
#include "RunData.h"
class JFJochImageReadingWorker : public QObject {
Q_OBJECT
@@ -69,6 +71,7 @@ private:
// Once the user edits ROIs they override whatever the file carried, for this and
// every subsequently loaded image, until a new file is opened.
std::optional<ROIDefinition> roi_override_;
std::map<std::string, QString> run_labels_; // snapshot id -> editable display label
IndexingSettings indexing_settings;
AzimuthalIntegrationSettings azint_settings;
BraggIntegrationSettings bragg_settings;
@@ -138,6 +141,7 @@ private:
void ApplyROIOverrideToImage_i(); // rewrite current_image_ptr's dataset with roi_override_
void setAutoLoadMode_i(AutoloadMode mode);
void ActivateSnapshot_i(const QString &name);
void EmitRuns_i(); // emit runsChanged with every snapshot dataset + its label
signals:
void datasetLoaded(std::shared_ptr<const JFJochReaderDataset>);
@@ -154,6 +158,7 @@ signals:
void httpConnectionChanged(bool connected, QString addr);
void liveRateChanged(double hz);
void snapshotsChanged(QStringList names, QString active);
void runsChanged(QVector<RunData> runs, QString active_id);
public:
JFJochImageReadingWorker(const SpotFindingSettings &settings, const DiffractionExperiment& experiment, QObject *parent = nullptr);
@@ -202,6 +207,7 @@ public slots:
// Register a reprocessing result (_process.h5) as a metadata snapshot over the same images and
// make it the active dataset; SetActiveSnapshot switches between registered snapshots.
void RegisterProcessingSnapshot(QString name, QString master_path);
void RegisterProcessingSnapshot(QString id, QString label, QString master_path);
void SetActiveSnapshot(QString name);
void RenameRun(QString id, QString label); // change a run's display label (legend)
};
+100 -72
View File
@@ -83,7 +83,20 @@ void JFJochViewerDatasetInfo::UpdateLabels() {
combo_box->addItem("Spot count (ice rings)", 3);
combo_box->addItem("Spot count (low res.)", 4);
if (!dataset->indexing_result.empty()) {
// Offer indexing/scaling metrics if the active run, any overlay run, or the live run has them.
bool has_indexing = !dataset->indexing_result.empty();
bool has_scale = !dataset->image_scale_factor.empty();
for (const auto &r: runs_)
if (r.dataset) {
has_indexing = has_indexing || !r.dataset->indexing_result.empty();
has_scale = has_scale || !r.dataset->image_scale_factor.empty();
}
if (live_run_) {
has_indexing = has_indexing || !live_run_->indexing_result.empty();
has_scale = has_scale || !live_run_->image_scale_factor.empty();
}
if (has_indexing) {
combo_box->insertSeparator(1000);
combo_box->addItem("Indexing result", 5);
combo_box->addItem("Profile radius", 6);
@@ -93,7 +106,7 @@ void JFJochViewerDatasetInfo::UpdateLabels() {
combo_box->addItem("Indexing lattice count", 13);
}
if (!dataset->image_scale_factor.empty()) {
if (has_scale) {
combo_box->insertSeparator(1000);
combo_box->addItem("Scale factor", 10);
combo_box->addItem("CC", 11);
@@ -122,7 +135,7 @@ void JFJochViewerDatasetInfo::datasetLoaded(std::shared_ptr<const JFJochReaderDa
combo_box->setCurrentIndex(0);
UpdatePlot();
} else {
chart_view->loadValues<float>({}, 0, false);
chart_view->loadValues({}, 0, false);
grid_scan_image->clear();
UpdateLabels();
}
@@ -140,6 +153,46 @@ void JFJochViewerDatasetInfo::imageSelectedInChart(int64_t number) {
emit imageSelected(number, 1);
}
std::vector<float> JFJochViewerDatasetInfo::ExtractMetric(const JFJochReaderDataset &ds, int val,
bool &one_over_d2) const {
one_over_d2 = false;
std::vector<float> data;
if (val == 0) data = ds.bkg_estimate;
else if (val == 1) data = ds.spot_count;
else if (val == 2) data = ds.spot_count_indexed;
else if (val == 3) data = ds.spot_count_ice_rings;
else if (val == 4) data = ds.spot_count_low_res;
else if (val == 5) data = ds.indexing_result;
else if (val == 6) data = ds.profile_radius;
else if (val == 7) { data = ds.resolution_estimate; one_over_d2 = true; }
else if (val == 8) data = ds.b_factor;
else if (val == 9) data = ds.mosaicity_deg;
else if (val == 10) data = ds.image_scale_factor;
else if (val == 11) data = ds.image_scale_cc;
else if (val == 12) data = ds.integrated_reflections;
else if (val == 13) data = ds.indexing_lattice_count;
else if (val >= 100) {
const int roi_index = (val - 100) / 4;
if (val % 4 == 0) {
if (roi_index < ds.roi_sum.size() && ds.roi_sum.size() == ds.roi_npixel.size())
for (int i = 0; i < ds.roi_sum[roi_index].size(); i++)
data.push_back(static_cast<float>(ds.roi_sum[roi_index][i])
/ static_cast<float>(ds.roi_npixel[roi_index][i]));
} else if (val % 4 == 1) {
if (roi_index < ds.roi_sum.size()) {
data.reserve(ds.roi_sum[roi_index].size());
for (auto &v: ds.roi_sum[roi_index])
data.push_back(v);
}
} else if (val % 4 == 2) {
if (roi_index < ds.roi_x.size()) data = ds.roi_x[roi_index];
} else if (val % 4 == 3) {
if (roi_index < ds.roi_y.size()) data = ds.roi_y[roi_index];
}
}
return data;
}
void JFJochViewerDatasetInfo::UpdatePlot() {
int index = combo_box->currentIndex();
if (combo_box->count() == 0 || index < 0)
@@ -147,83 +200,58 @@ void JFJochViewerDatasetInfo::UpdatePlot() {
int val = combo_box->itemData(index).toInt();
if (dataset) {
int64_t image_number = 0;
if (image)
image_number = image->ImageData().number;
if (!dataset) {
grid_button->setEnabled(false);
return;
}
bool one_over_d2 = false;
std::vector<float> data;
if (val == 0)
data = dataset->bkg_estimate;
else if (val == 1)
data = dataset->spot_count;
else if (val == 2)
data = dataset->spot_count_indexed;
else if (val == 3)
data = dataset->spot_count_ice_rings;
else if (val == 4)
data = dataset->spot_count_low_res;
else if (val == 5)
data = dataset->indexing_result;
else if (val == 6)
data = dataset->profile_radius;
else if (val == 7) {
data = dataset->resolution_estimate;
one_over_d2 = true;
} else if (val == 8) {
data = dataset->b_factor;
} else if (val == 9) {
data = dataset->mosaicity_deg;
} else if (val == 10) {
data = dataset->image_scale_factor;
} else if (val == 11) {
data = dataset->image_scale_cc;
} else if (val == 12) {
data = dataset->integrated_reflections;
} else if (val == 13) {
data = dataset->indexing_lattice_count;
} else if (val >= 100) {
int roi_index = (val - 100) / 4;
const int64_t image_number = image ? image->ImageData().number : 0;
if (val % 4 == 0) {
if (roi_index < dataset->roi_sum.size() && dataset->roi_sum.size() == dataset->roi_npixel.size()) {
for(int i = 0; i < dataset->roi_sum[roi_index].size(); i++) {
data.push_back(static_cast<float>(dataset->roi_sum[roi_index][i])
/ static_cast<float>(dataset->roi_npixel[roi_index][i]));
}
}
} else if (val % 4 == 1) {
if (roi_index < dataset->roi_sum.size()) {
data.reserve(dataset->roi_sum[roi_index].size());
for (auto &v: dataset->roi_sum[roi_index])
data.push_back(v);
}
} else if (val % 4 == 2) {
if (roi_index < dataset->roi_x.size())
data = dataset->roi_x[roi_index];
} else if (val % 4 == 3) {
if (roi_index < dataset->roi_y.size())
data = dataset->roi_y[roi_index];
}
}
bool one_over_d2 = false;
std::vector<float> data = ExtractMetric(*dataset, val, one_over_d2);
chart_view->loadValues(data, image_number, one_over_d2, dataset.get());
if (dataset->experiment.GetGridScan()) {
stack->setCurrentWidget(grid_scan_image);
grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2);
grid_button->setEnabled(true);
grid_button->setChecked(true);
} else {
grid_scan_image->clear();
stack->setCurrentWidget(chart_view);
grid_button->setEnabled(false);
}
// One overlay line per other run, plus the in-progress live run.
std::vector<JFJochDatasetInfoChartView::NamedSeries> overlays;
QString primary_name;
for (const auto &run: runs_) {
if (run.id == active_id_) { primary_name = run.label; continue; }
if (!run.dataset || run.dataset == dataset) continue; // never draw the active run twice
bool ignore = false;
overlays.push_back({run.label, ExtractMetric(*run.dataset, val, ignore)});
}
if (live_run_ && live_run_ != dataset) {
bool ignore = false;
overlays.push_back({QStringLiteral("Live"), ExtractMetric(*live_run_, val, ignore)});
}
chart_view->loadValues(data, image_number, one_over_d2, dataset.get(), primary_name, std::move(overlays));
if (dataset->experiment.GetGridScan()) {
stack->setCurrentWidget(grid_scan_image);
grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2);
grid_button->setEnabled(true);
grid_button->setChecked(true);
} else {
grid_scan_image->clear();
stack->setCurrentWidget(chart_view);
grid_button->setEnabled(false);
}
}
void JFJochViewerDatasetInfo::runsChanged(QVector<RunData> runs, QString active_id) {
runs_ = std::move(runs);
active_id_ = std::move(active_id);
UpdateLabels();
if (last_selection < combo_box->count())
combo_box->setCurrentIndex(last_selection);
UpdatePlot();
}
void JFJochViewerDatasetInfo::liveRunUpdated(std::shared_ptr<const JFJochReaderDataset> in_dataset) {
live_run_ = std::move(in_dataset);
UpdatePlot(); // lightweight refresh; no combo rebuild on every live tick
}
void JFJochViewerDatasetInfo::comboBoxSelected(int index) {
UpdatePlot();
}
+12 -1
View File
@@ -10,21 +10,28 @@
#include "charts/JFJochDatasetInfoChartView.h"
#include "../reader/JFJochReader.h"
#include "image_viewer/JFJochGridScanImage.h"
#include "RunData.h"
class JFJochViewerDatasetInfo : public QWidget {
Q_OBJECT
QComboBox *combo_box;
JFJochDatasetInfoChartView *chart_view;
const std::vector<float> *GetDataset();
std::shared_ptr<const JFJochReaderDataset> dataset;
std::shared_ptr<const JFJochReaderDataset> dataset; // the active (primary) run
std::shared_ptr<const JFJochReaderImage> image;
JFJochGridScanImage *grid_scan_image = nullptr;
QVector<RunData> runs_; // all runs, for overlay lines
QString active_id_; // which run is primary
std::shared_ptr<const JFJochReaderDataset> live_run_; // in-progress reprocessing, if any
QStackedWidget *stack = nullptr;
QPushButton *grid_button = nullptr;
int last_selection;
void UpdatePlot();
// Pull the metric selected by combo value `val` out of one run's dataset.
std::vector<float> ExtractMetric(const JFJochReaderDataset &ds, int val, bool &one_over_d2) const;
void UpdateLabels();
signals:
@@ -41,6 +48,10 @@ public slots:
void datasetLoaded(std::shared_ptr<const JFJochReaderDataset> dataset);
void imageLoaded(std::shared_ptr<const JFJochReaderImage> image);
void setColorMap(int color_map);
// All runs (original + reprocessing snapshots) and which one is active/primary.
void runsChanged(QVector<RunData> runs, QString active_id);
// Live results of the in-progress reprocessing run (updated as it goes), or nullptr to clear.
void liveRunUpdated(std::shared_ptr<const JFJochReaderDataset> dataset);
};
+13 -2
View File
@@ -401,8 +401,15 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
reading_worker, &JFJochImageReadingWorker::SetActiveSnapshot);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::writeStatusBar,
statusbar, &JFJochViewerStatusBar::display);
connect(processingJobsWindow, &JFJochProcessingJobsWindow::renameRun,
reading_worker, &JFJochImageReadingWorker::RenameRun);
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
processingJobsWindow, &JFJochProcessingJobsWindow::onHttpConnectionChanged);
connect(reading_worker, &JFJochImageReadingWorker::runsChanged, this,
[this](QVector<RunData> runs, QString active) {
lastRuns = std::move(runs);
lastActiveRunId = std::move(active);
});
// Dock the processing panel in the bottom-right corner, next to the dataset-info plots.
auto processingDock = new QDockWidget("Processing", this);
@@ -442,6 +449,7 @@ void JFJochViewerWindow::NewDatasetInfo() {
auto info = new JFJochViewerDatasetInfo(this);
info->datasetLoaded(lastDataset);
info->imageLoaded(lastImage);
info->runsChanged(lastRuns, lastActiveRunId);
auto dock = new QDockWidget(QString("Dataset info"), this);
dock->setAllowedAreas(Qt::BottomDockWidgetArea);
@@ -458,9 +466,12 @@ void JFJochViewerWindow::NewDatasetInfo() {
info, &JFJochViewerDatasetInfo::datasetLoaded);
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded,
info, &JFJochViewerDatasetInfo::imageLoaded);
// Live processing results: refresh the plots while a job runs.
// All runs (original + reprocessing snapshots) overlay as separate lines.
connect(reading_worker, &JFJochImageReadingWorker::runsChanged,
info, &JFJochViewerDatasetInfo::runsChanged);
// Live processing results: the in-progress run as its own overlay line.
connect(processingJobsWindow, &JFJochProcessingJobsWindow::liveDataset,
info, &JFJochViewerDatasetInfo::datasetLoaded);
info, &JFJochViewerDatasetInfo::liveRunUpdated);
connect(info, &JFJochViewerDatasetInfo::imageSelected,
reading_worker, &JFJochImageReadingWorker::LoadImage);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged,
+2
View File
@@ -32,6 +32,8 @@ private:
std::shared_ptr<const JFJochReaderDataset> lastDataset; // added
std::shared_ptr<const JFJochReaderImage> lastImage; // added
QVector<RunData> lastRuns; // for dataset-info docks opened later
QString lastActiveRunId;
QProgressDialog *retryDialog = nullptr;
+22
View File
@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QString>
#include <QVector>
#include <QMetaType>
#include <memory>
#include "../reader/JFJochReaderDataset.h"
// One processing run (the original file or a reprocessing snapshot), for overlaying its plots.
// id is the stable reader-snapshot key; label is the editable display name shown in the legend.
struct RunData {
QString id;
QString label;
std::shared_ptr<const JFJochReaderDataset> dataset;
};
Q_DECLARE_METATYPE(RunData)
Q_DECLARE_METATYPE(QVector<RunData>)
+87 -33
View File
@@ -128,6 +128,71 @@ void JFJochDatasetInfoChartView::resetZoom() {
chart()->zoomReset();
}
void JFJochDatasetInfoChartView::loadValues(const std::vector<float> &input, int64_t image, bool one_over_d2,
const JFJochReaderDataset *dataset, const QString &primaryName,
std::vector<NamedSeries> overlays) {
m_yOneOverD = one_over_d2;
primaryName_ = primaryName;
// d -> 1/d^2 for resolution plots; identity otherwise. Applied to every series alike.
auto transform = [one_over_d2](const std::vector<float> &in, std::vector<float> &out) {
out.resize(in.size());
for (size_t i = 0; i < in.size(); i++) {
if (one_over_d2) {
const float d = in[i];
out[i] = std::isfinite(d) ? 1.0f / (d * d) : 0.0f;
} else
out[i] = in[i];
}
};
transform(input, values);
overlays_ = std::move(overlays);
for (auto &ov: overlays_)
transform(ov.values, ov.values); // in-place
if (dataset != nullptr) {
goniometer_axis = dataset->experiment.GetGoniometer();
image_time_us = dataset->experiment.GetImageTime().count();
} else {
goniometer_axis = {};
image_time_us = {};
}
curr_image = image;
updateChart();
}
void JFJochDatasetInfoChartView::appendSeries(QLineSeries *s, const std::vector<float> &vals,
double &mn, double &mx) const {
if (binning == 1) {
for (int i = 0; i < static_cast<int>(vals.size()); i++) {
const double v = vals[static_cast<size_t>(i)];
if (!std::isfinite(v)) continue;
s->append(i, v);
mn = std::min(mn, v);
mx = std::max(mx, v);
}
} else {
for (int i = 0; i < static_cast<int>(vals.size() / static_cast<size_t>(binning)); i++) {
double tmp = 0.0;
int64_t count = 0;
for (int b = 0; b < binning; b++) {
const int64_t idx = static_cast<int64_t>(i) * binning + b;
if (idx >= static_cast<int64_t>(vals.size())) break;
const double v = vals[static_cast<size_t>(idx)];
if (std::isfinite(v)) { tmp += v; count++; }
}
if (count > 0) {
const double mean = tmp / static_cast<double>(count);
s->append((i + 0.5) * binning, mean);
mn = std::min(mn, mean);
mx = std::max(mx, mean);
}
}
}
}
void JFJochDatasetInfoChartView::updateChart() {
// Important: drop any stale QObject pointers BEFORE rebuilding the chart.
series = nullptr;
@@ -156,44 +221,23 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() {
// At least one full point
series = new QLineSeries(this);
if (!primaryName_.isEmpty())
series->setName(primaryName_);
currentSeries = new QScatterSeries(this);
double dispMin = std::numeric_limits<double>::infinity();
double dispMax = -std::numeric_limits<double>::infinity();
if (binning == 1) {
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() / static_cast<size_t>(binning)); i++) {
double tmp = 0.0;
int64_t count = 0;
for (int b = 0; b < 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++;
}
}
if (count > 0) {
const double mean = tmp / static_cast<double>(count);
const double disp = mean;
if (std::isfinite(disp)) {
series->append((i + 0.5) * binning, disp);
if (disp < dispMin) dispMin = disp;
if (disp > dispMax) dispMax = disp;
}
}
}
appendSeries(series, values, dispMin, dispMax);
// Overlay runs share the primary's axes and range; build them now so the Y range fits all.
std::vector<QLineSeries *> overlayLines;
overlayLines.reserve(overlays_.size());
for (const auto &ov: overlays_) {
auto *line = new QLineSeries(this);
line->setName(ov.name);
appendSeries(line, ov.values, dispMin, dispMax);
overlayLines.push_back(line);
}
// ---- current point marker as binned value when binning > 1 ----
@@ -366,6 +410,16 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() {
axisY->setLabelsVisible(true);
}
}
// Attach overlay lines to the primary series' final axes; show the legend when overlaying.
const auto finalAxes = series->attachedAxes();
for (auto *line: overlayLines) {
chart()->addSeries(line);
for (auto *ax: finalAxes)
line->attachAxis(ax);
}
chart()->legend()->setVisible(!overlays_.empty());
chart()->legend()->setAlignment(Qt::AlignBottom);
}
}
+15 -28
View File
@@ -29,6 +29,7 @@ class JFJochDatasetInfoChartView : public QChartView {
int64_t m_hoverPendingIdx = -1;
std::vector<float> values;
QString primaryName_; // legend name of the active (primary) run
int64_t binning = 1;
int64_t curr_image = 0;
@@ -44,6 +45,14 @@ class JFJochDatasetInfoChartView : public QChartView {
// Build regular (index or goniometer) chart
void buildTimeDomainChart();
public:
// One overlay line (a non-active run), drawn alongside the primary series with a legend entry.
struct NamedSeries { QString name; std::vector<float> values; };
private:
std::vector<NamedSeries> overlays_;
// Append (binned) finite points of vals to s, extending the [mn, mx] data range.
void appendSeries(QLineSeries *s, const std::vector<float> &vals, double &mn, double &mx) const;
// FFT view toggle
bool m_showFFT = false;
@@ -76,36 +85,14 @@ public slots:
void resetZoom();
public:
template<class T>
void loadValues(const std::vector<T> &input,
// Load the primary (active) run's values plus any overlay runs. one_over_d2 applies the same
// d -> 1/d^2 transform to every series; primaryName/overlay names drive the legend.
void loadValues(const std::vector<float> &input,
int64_t image,
bool one_over_d2,
const JFJochReaderDataset *dataset = nullptr) {
m_yOneOverD = one_over_d2;
values.resize(input.size());
if (dataset != nullptr) {
goniometer_axis = dataset->experiment.GetGoniometer();
image_time_us = dataset->experiment.GetImageTime().count();
} else {
goniometer_axis = {};
image_time_us = {};
}
for (int i = 0; i < input.size(); i++) {
if (one_over_d2) {
float d = static_cast<float>(input[i]);
if (!std::isfinite(d))
values[i] = 0.0f;
else
values[i] = 1.0f / (d * d);
} else
values[i] = static_cast<float>(input[i]);
}
curr_image = image;
updateChart();
}
const JFJochReaderDataset *dataset = nullptr,
const QString &primaryName = QString(),
std::vector<NamedSeries> overlays = {});
};
+43 -18
View File
@@ -8,6 +8,7 @@
#include <QCheckBox>
#include <QClipboard>
#include <QComboBox>
#include <QDateTime>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileInfo>
@@ -27,7 +28,7 @@
#include <thread>
namespace {
enum Column { COL_NAME = 0, COL_MODE, COL_IMAGES, COL_STATUS, COL_INDEX, COL_CELL, COL_COUNT };
enum Column { COL_NAME = 0, COL_STARTED, COL_MODE, COL_IMAGES, COL_STATUS, COL_INDEX, COL_CELL, COL_COUNT };
int default_threads() {
const unsigned hc = std::thread::hardware_concurrency();
@@ -55,11 +56,21 @@ JFJochProcessingJobsWindow::JFJochProcessingJobsWindow(JFJochImageReadingWorker
toolbar_->addAction("Show original", this, &JFJochProcessingJobsWindow::showOriginal);
table_ = new QTableWidget(0, COL_COUNT, this);
table_->setHorizontalHeaderLabels({"Name", "Mode", "Images", "Status", "Index %", "Unit cell"});
table_->setHorizontalHeaderLabels({"Name", "Started", "Mode", "Images", "Status", "Index %", "Unit cell"});
table_->horizontalHeader()->setStretchLastSection(true);
table_->setSelectionBehavior(QAbstractItemView::SelectRows);
table_->setSelectionMode(QAbstractItemView::SingleSelection);
table_->setEditTriggers(QAbstractItemView::NoEditTriggers);
// Only the Name column is editable (per-item flags); editing it renames the run's legend label.
table_->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
connect(table_, &QTableWidget::itemChanged, this, [this](QTableWidgetItem *item) {
if (item->column() != COL_NAME)
return;
const int row = item->row();
if (row < 0 || row >= static_cast<int>(jobs_.size()) || item->text() == jobs_[row].label)
return;
jobs_[row].label = item->text();
emit renameRun(jobs_[row].id, jobs_[row].label);
});
auto *message = new QLabel("Dataset re-processing is currently available only in File mode.\n\n"
"Open a stored HDF5 file to run processing jobs.", this);
@@ -210,16 +221,33 @@ void JFJochProcessingJobsWindow::newJob() {
}
const bool full = spec.mode == ProcessMode::FullAnalysis;
const QString name = QStringLiteral("%1 %2").arg(full ? "Full" : "AzInt").arg(++job_counter_);
const int run_number = ++job_counter_;
const QString label = QStringLiteral("%1 %2").arg(full ? "Full" : "AzInt").arg(run_number);
const QString id = QStringLiteral("run-%1").arg(run_number);
JobInfo info;
info.id = id;
info.label = label;
if (spec.save)
info.snapshot_path = QString::fromStdString(config.output_prefix) + "_process.h5";
const int row = table_->rowCount();
table_->insertRow(row);
table_->setItem(row, COL_NAME, new QTableWidgetItem(name));
table_->setItem(row, COL_MODE, new QTableWidgetItem(full ? "Full" : "AzInt"));
table_->setItem(row, COL_IMAGES, new QTableWidgetItem(spec.images > 0 ? QString::number(spec.images) : "all"));
table_->setItem(row, COL_STATUS, new QTableWidgetItem("queued"));
table_->setItem(row, COL_INDEX, new QTableWidgetItem("-"));
table_->setItem(row, COL_CELL, new QTableWidgetItem("-"));
jobs_.push_back(info); // jobs_[row] must exist before setItem(COL_NAME) fires itemChanged
auto fixed = [](const QString &text) { // a non-editable cell
auto *cell = new QTableWidgetItem(text);
cell->setFlags(cell->flags() & ~Qt::ItemIsEditable);
return cell;
};
table_->setItem(row, COL_NAME, new QTableWidgetItem(label)); // editable (default flags)
table_->setItem(row, COL_STARTED, fixed(QDateTime::currentDateTime().toString("HH:mm:ss")));
table_->setItem(row, COL_MODE, fixed(full ? "Full" : "AzInt"));
table_->setItem(row, COL_IMAGES, fixed(spec.images > 0 ? QString::number(spec.images) : "all"));
table_->setItem(row, COL_STATUS, fixed("queued"));
table_->setItem(row, COL_INDEX, fixed("-"));
table_->setItem(row, COL_CELL, fixed("-"));
// A progress bar lives in the Status cell while the job runs; it shows the phase as text until
// image processing starts, then "<done> / <expected>" with the bar filling in the background.
@@ -230,15 +258,10 @@ void JFJochProcessingJobsWindow::newJob() {
running_bar_->setFormat("queued");
table_->setCellWidget(row, COL_STATUS, running_bar_);
JobInfo info;
info.name = name;
if (spec.save)
info.snapshot_path = QString::fromStdString(config.output_prefix) + "_process.h5";
jobs_.push_back(info);
running_row_ = row;
controller_->start(inputs.file, experiment, inputs.pixel_mask, config);
emit writeStatusBar("Started processing job " + name);
emit writeStatusBar("Started processing job " + label);
}
void JFJochProcessingJobsWindow::cancelJob() {
@@ -256,7 +279,7 @@ void JFJochProcessingJobsWindow::viewResults() {
QMessageBox::information(this, "Processing", "This job has no saved results to view.");
return;
}
emit activateSnapshot(jobs_[row].name);
emit activateSnapshot(jobs_[row].id);
}
void JFJochProcessingJobsWindow::showOriginal() {
@@ -287,6 +310,7 @@ void JFJochProcessingJobsWindow::onProgress(quint64 done, quint64 total) {
}
void JFJochProcessingJobsWindow::onFinished(ProcessResult result) {
emit liveDataset(nullptr); // the finished run takes over from the live overlay
const int row = running_row_;
running_row_ = -1;
if (row >= 0)
@@ -308,12 +332,13 @@ void JFJochProcessingJobsWindow::onFinished(ProcessResult result) {
if (!result.cancelled && result.written_master_path.has_value() && !jobs_[row].snapshot_path.isEmpty()) {
jobs_[row].has_result = true;
emit registerSnapshot(jobs_[row].name, jobs_[row].snapshot_path); // also activates it
emit registerSnapshot(jobs_[row].id, jobs_[row].label, jobs_[row].snapshot_path); // also activates it
}
emit writeStatusBar(result.cancelled ? "Processing cancelled" : "Processing finished");
}
void JFJochProcessingJobsWindow::onFailed(QString error) {
emit liveDataset(nullptr); // clear the live overlay
const int row = running_row_;
running_row_ = -1;
if (row >= 0)
+6 -4
View File
@@ -26,10 +26,11 @@ public:
explicit JFJochProcessingJobsWindow(JFJochImageReadingWorker *worker, QWidget *parent = nullptr);
signals:
void registerSnapshot(QString name, QString master_path);
void activateSnapshot(QString name);
void registerSnapshot(QString id, QString label, QString master_path);
void activateSnapshot(QString id);
void renameRun(QString id, QString label);
void writeStatusBar(QString message, int timeout_ms = 0);
// Live per-image results while a job runs, for the dataset-info plots.
// Live per-image results while a job runs, for the dataset-info plots (nullptr clears it).
void liveDataset(std::shared_ptr<const JFJochReaderDataset> dataset);
public slots:
@@ -47,7 +48,8 @@ private slots:
private:
struct JobInfo {
QString name;
QString id; // stable reader-snapshot key
QString label; // editable display name (legend / table)
QString snapshot_path; // empty unless saved
bool has_result = false;
};