// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochSimpleChartView.h" #include #include #include #include #include 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 &in_x, const std::vector &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(*minYIt); ymax = static_cast(*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(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(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(xVal)); if (it == x.end() && !x.empty()) it = std::prev(x.end()); if (it == x.end()) return; size_t idx = static_cast(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 }