Files
Jungfraujoch/viewer/charts/JFJochSimpleChartView.cpp
T
leonarski_fandClaude Opus 4.8 9ba8096c0b viewer: chart top margin + settings dock width bounds
- The chart top margin was too tight (2 px), so the highest Y-axis label
  overlapped the top edge; give it 10 px of room (both chart views).
- The settings dock had no width constraints (unlike the inspector), so it
  stretched ugly when widened and would not fold back narrowly. Cap it to
  330–480 px like the inspector.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:26:15 +02:00

266 lines
9.3 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>
#include <QGraphicsLayout>
JFJochSimpleChartView::JFJochSimpleChartView(QWidget *parent)
: QChartView(new QChart(), parent) {
chart()->legend()->hide();
chart()->layout()->setContentsMargins(0, 0, 0, 0); // reclaim padding for the plot + labels
chart()->setMargins(QMargins(2, 10, 8, 2)); // top room so the highest Y label is not clipped
setMinimumHeight(140); // soft floor: enough for the axis labels to stay readable
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) {
if (std::isfinite(y[i]))
series->append(x[i], y[i]);
}
chart()->addSeries(series);
m_series = series;
// Compute Y range
double ymin = std::numeric_limits<double>::max();
double ymax = std::numeric_limits<double>::lowest();
bool found_finite = false;
for (float val : y) {
if (std::isfinite(val)) {
ymin = std::min(ymin, static_cast<double>(val));
ymax = std::max(ymax, static_cast<double>(val));
found_finite = true;
}
}
if (!found_finite) {
ymin = 0.0;
ymax = 1.0;
} else 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];
// Map that data point to scene coords to get the x position
// Use 0.0 for Y to ensure mapToPosition works even if yNearest is NaN
const QPointF ptOnSeries = chart()->mapToPosition(QPointF(xNearest, 0.0), 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
}