// File: host/src/GraphWidget.cpp #include "GraphWidget.h" #include #include #include #include #include #include // Simple Linear Regression Helper static void linearRegression(const QVector &x, const QVector &y, double &m, double &c) { double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; int n = x.size(); for (int i = 0; i < n; i++) { sumX += x[i]; sumY += y[i]; sumXY += x[i] * y[i]; sumX2 += x[i] * x[i]; } m = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); c = (sumY - m * sumX) / n; } GraphWidget::GraphWidget(QWidget *parent) : QWidget(parent) { mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->setSpacing(0); // Toolbar toolbar = new QWidget(this); toolbar->setStyleSheet("background-color: #2D2D2D;"); QHBoxLayout *toolLayout = new QHBoxLayout(toolbar); toolLayout->setContentsMargins(5, 2, 5, 2); btnScaleX = new QPushButton("Scale X", this); btnScaleY = new QPushButton("Scale Y", this); btnScaleBoth = new QPushButton("Scale Both", this); btnCenter = new QPushButton("Center", this); btnAnalyze = new QPushButton("Analyze", this); QString btnStyle = "QPushButton { background-color: #444; color: white; " "border: 1px solid #555; padding: 3px 8px; border-radius: " "3px; } QPushButton:hover { background-color: #555; }"; btnScaleX->setStyleSheet(btnStyle); btnScaleY->setStyleSheet(btnStyle); btnScaleBoth->setStyleSheet(btnStyle); btnCenter->setStyleSheet(btnStyle); btnAnalyze->setStyleSheet(btnStyle); toolLayout->addWidget(btnScaleX); toolLayout->addWidget(btnScaleY); toolLayout->addWidget(btnScaleBoth); toolLayout->addWidget(btnCenter); toolLayout->addStretch(); toolLayout->addWidget(btnAnalyze); mainLayout->addWidget(toolbar); plot = new QCustomPlot(this); mainLayout->addWidget(plot); plot->setBackground(QBrush(QColor(25, 25, 25))); auto styleAxis = [](QCPAxis *axis) { axis->setBasePen(QPen(Qt::white)); axis->setTickPen(QPen(Qt::white)); axis->setSubTickPen(QPen(Qt::white)); axis->setTickLabelColor(Qt::white); axis->setLabelColor(Qt::white); axis->grid()->setPen(QPen(QColor(60, 60, 60), 0, Qt::DotLine)); axis->grid()->setSubGridVisible(true); axis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 0, Qt::DotLine)); }; styleAxis(plot->xAxis); styleAxis(plot->yAxis); styleAxis(plot->yAxis2); // --- Setup Graphs --- graphReal = plot->addGraph(); graphReal->setPen(QPen(QColor(0, 255, 255), 2)); graphReal->setLineStyle(QCPGraph::lsLine); graphReal->setScatterStyle( QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(0, 255, 255), 3)); graphImag = plot->addGraph(plot->xAxis, plot->yAxis2); graphImag->setPen(QPen(QColor(255, 0, 255), 2)); graphImag->setLineStyle(QCPGraph::lsLine); graphImag->setScatterStyle( QCPScatterStyle(QCPScatterStyle::ssTriangle, QColor(255, 0, 255), 3)); graphHilbert = plot->addGraph(plot->xAxis, plot->yAxis2); QPen pen3(Qt::green); pen3.setWidth(2); pen3.setStyle(Qt::DashLine); graphHilbert->setPen(pen3); graphNyquistCorr = plot->addGraph(plot->xAxis, plot->yAxis); graphNyquistCorr->setPen(QPen(QColor(255, 165, 0), 2)); graphNyquistCorr->setLineStyle(QCPGraph::lsLine); graphNyquistCorr->setScatterStyle( QCPScatterStyle(QCPScatterStyle::ssCross, 4)); graphNyquistCorr->setName("De-embedded (True Cell)"); graphAmp = plot->addGraph(); graphAmp->setPen(QPen(QColor(50, 255, 50), 2)); graphAmp->setLineStyle(QCPGraph::lsLine); graphAmp->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 3)); graphAmp->setName("Current"); graphExtrapolated = plot->addGraph(plot->xAxis, plot->yAxis); graphExtrapolated->setLineStyle(QCPGraph::lsNone); graphExtrapolated->setScatterStyle( QCPScatterStyle(QCPScatterStyle::ssStar, QColor(255, 215, 0), 12)); graphExtrapolated->setPen(QPen(QColor(255, 215, 0), 3)); graphExtrapolated->setName("Rs (Extrapolated)"); graphLSVBlank = plot->addGraph(); QPen penBlank(QColor(150, 150, 150)); penBlank.setWidth(2); penBlank.setStyle(Qt::DashLine); graphLSVBlank->setPen(penBlank); graphLSVBlank->setName("Blank (Tap Water)"); graphLSVSample = plot->addGraph(); graphLSVSample->setPen(QPen(Qt::yellow, 2)); graphLSVSample->setName("Sample (Bleach)"); graphLSVDiff = plot->addGraph(); graphLSVDiff->setPen(QPen(Qt::cyan, 3)); graphLSVDiff->setName("Diff (Chlorine)"); graphFit = plot->addGraph(); graphFit->setPen(QPen(Qt::red, 2)); graphFit->setName("Fit"); graphNyquistRaw = graphReal; plot->yAxis2->setVisible(true); plot->yAxis2->setTickLabels(true); connect(plot->yAxis, SIGNAL(rangeChanged(QCPRange)), plot->yAxis2, SLOT(setRange(QCPRange))); connect(plot->yAxis2, SIGNAL(rangeChanged(QCPRange)), plot->yAxis, SLOT(setRange(QCPRange))); plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables); // Right click drag to zoom plot->axisRect()->setRangeDrag(Qt::Horizontal | Qt::Vertical); plot->axisRect()->setRangeZoom(Qt::Horizontal | Qt::Vertical); plot->legend->setVisible(true); QFont legendFont = font(); legendFont.setPointSize(9); plot->legend->setFont(legendFont); plot->legend->setBrush(QBrush(QColor(40, 40, 40, 200))); plot->legend->setBorderPen(QPen(Qt::white)); plot->legend->setTextColor(Qt::white); selectionRect = new QCPItemRect(plot); selectionRect->setPen(QPen(Qt::yellow, 1, Qt::DashLine)); selectionRect->setBrush(QBrush(QColor(255, 255, 0, 50))); selectionRect->setVisible(false); connect(btnScaleX, &QPushButton::clicked, this, &GraphWidget::scaleX); connect(btnScaleY, &QPushButton::clicked, this, &GraphWidget::scaleY); connect(btnScaleBoth, &QPushButton::clicked, this, &GraphWidget::scaleBoth); connect(btnCenter, &QPushButton::clicked, this, &GraphWidget::centerView); connect(btnAnalyze, &QPushButton::clicked, this, &GraphWidget::startAnalyze); // Custom mouse handling for selection connect(plot, &QCustomPlot::mousePress, [this](QMouseEvent *event) { if (isSelecting && event->button() == Qt::LeftButton) { selStartX = plot->xAxis->pixelToCoord(event->pos().x()); selStartY = plot->yAxis->pixelToCoord(event->pos().y()); selectionRect->topLeft->setCoords(selStartX, selStartY); selectionRect->bottomRight->setCoords(selStartX, selStartY); selectionRect->setVisible(true); plot->replot(); } }); connect(plot, &QCustomPlot::mouseMove, [this](QMouseEvent *event) { if (isSelecting && (event->buttons() & Qt::LeftButton)) { double x = plot->xAxis->pixelToCoord(event->pos().x()); double y = plot->yAxis->pixelToCoord(event->pos().y()); selectionRect->bottomRight->setCoords(x, y); plot->replot(); } }); connect(plot, &QCustomPlot::mouseRelease, [this](QMouseEvent *event) { if (isSelecting && event->button() == Qt::LeftButton) { isSelecting = false; plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); double x2 = plot->xAxis->pixelToCoord(event->pos().x()); double y2 = plot->yAxis->pixelToCoord(event->pos().y()); // Collect data in rect QVector xData, yData; double minX = std::min(selStartX, x2); double maxX = std::max(selStartX, x2); double minY = std::min(selStartY, y2); double maxY = std::max(selStartY, y2); // Iterate visible graphs to find data QList graphs = {graphReal, graphAmp, graphLSVSample, graphLSVDiff}; for (auto g : graphs) { if (!g->visible()) continue; for (auto it = g->data()->begin(); it != g->data()->end(); ++it) { if (it->key >= minX && it->key <= maxX && it->value >= minY && it->value <= maxY) { xData.append(it->key); yData.append(it->value); } } if (!xData.isEmpty()) break; // Only fit one graph } if (xData.size() > 2) { // Show Dialog QDialog dlg(this); dlg.setWindowTitle("Fit Data"); QFormLayout *layout = new QFormLayout(&dlg); QComboBox *type = new QComboBox(); type->addItems({"Linear", "Exponential", "Logarithmic", "Polynomial"}); QSpinBox *order = new QSpinBox(); order->setRange(1, 10); order->setValue(2); QCheckBox *inverse = new QCheckBox("Inverse"); layout->addRow("Type:", type); layout->addRow("Poly Order:", order); layout->addRow(inverse); QDialogButtonBox *btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); layout->addRow(btns); connect(btns, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(btns, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() == QDialog::Accepted) { performFit(xData, yData, type->currentIndex(), order->value(), inverse->isChecked()); } } selectionRect->setVisible(false); plot->replot(); } }); configureRawPlot(); } void GraphWidget::scaleX() { plot->rescaleAxes(true); plot->replot(); } void GraphWidget::scaleY() { plot->yAxis->rescale(true); plot->yAxis2->rescale(true); plot->replot(); } void GraphWidget::scaleBoth() { plot->rescaleAxes(true); plot->replot(); } void GraphWidget::centerView() { scaleBoth(); } void GraphWidget::startAnalyze() { isSelecting = true; // FIX: Use default constructor for empty flags instead of 0 plot->setInteractions(QCP::Interactions()); } void GraphWidget::performFit(const QVector &x, const QVector &y, int type, int order, bool inverse) { graphFit->data()->clear(); QVector xFit, yFit; // Simple Linear Fit Implementation for demo if (type == 0) { // Linear double m, c; QVector yProc = y; if (inverse) { for (int i = 0; i < y.size(); i++) yProc[i] = 1.0 / y[i]; } linearRegression(x, yProc, m, c); double minX = *std::min_element(x.begin(), x.end()); double maxX = *std::max_element(x.begin(), x.end()); for (int i = 0; i <= 100; i++) { double xv = minX + (maxX - minX) * i / 100.0; double yv = m * xv + c; if (inverse) yv = 1.0 / yv; xFit.append(xv); yFit.append(yv); } } // Add other fits here (Exp, Log, Poly) as needed graphFit->setData(xFit, yFit); graphFit->setVisible(true); plot->replot(); } // ... (Rest of configuration methods same as before) ... void GraphWidget::configureRawPlot() { plot->xAxis->setLabel("Frequency (Hz)"); plot->xAxis->setScaleType(QCPAxis::stLogarithmic); QSharedPointer logTicker(new QCPAxisTickerLog); plot->xAxis->setTicker(logTicker); plot->xAxis->setNumberFormat("eb"); plot->yAxis->setLabel("Magnitude (Ohms)"); plot->yAxis->setScaleType(QCPAxis::stLogarithmic); QSharedPointer logTickerY(new QCPAxisTickerLog); plot->yAxis->setTicker(logTickerY); plot->yAxis->setNumberFormat("eb"); plot->yAxis2->setLabel("Phase (Rad)"); plot->yAxis2->setScaleType(QCPAxis::stLinear); QSharedPointer linTicker(new QCPAxisTicker); plot->yAxis2->setTicker(linTicker); plot->yAxis2->setNumberFormat("f"); plot->yAxis2->setVisible(true); graphReal->setName("Magnitude"); graphImag->setName("Phase"); graphHilbert->setName("Hilbert"); graphReal->setVisible(true); graphImag->setVisible(true); graphHilbert->setVisible(true); graphNyquistCorr->setVisible(false); graphAmp->setVisible(false); graphExtrapolated->setVisible(false); graphLSVBlank->setVisible(false); graphLSVSample->setVisible(false); graphLSVDiff->setVisible(false); graphFit->setVisible(false); plot->replot(); } void GraphWidget::configureNyquistPlot() { plot->xAxis->setLabel("Real (Z')"); plot->xAxis->setScaleType(QCPAxis::stLinear); QSharedPointer linTicker(new QCPAxisTicker); plot->xAxis->setTicker(linTicker); plot->xAxis->setNumberFormat("f"); plot->yAxis->setLabel("-Imaginary (-Z'')"); plot->yAxis->setScaleType(QCPAxis::stLinear); plot->yAxis->setTicker(linTicker); plot->yAxis->setNumberFormat("f"); plot->yAxis2->setVisible(false); graphReal->setName("Measured (Raw)"); graphReal->setLineStyle(QCPGraph::lsLine); graphReal->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, 4)); graphReal->setVisible(true); graphImag->setVisible(false); graphHilbert->setVisible(false); graphNyquistCorr->setVisible(true); graphAmp->setVisible(false); graphExtrapolated->setVisible(true); graphLSVBlank->setVisible(false); graphLSVSample->setVisible(false); graphLSVDiff->setVisible(false); graphFit->setVisible(false); plot->replot(); } void GraphWidget::configureAmperometricPlot() { plot->xAxis->setLabel("Sample Index"); plot->xAxis->setScaleType(QCPAxis::stLinear); QSharedPointer linTicker(new QCPAxisTicker); plot->xAxis->setTicker(linTicker); plot->xAxis->setNumberFormat("f"); plot->yAxis->setLabel("Current (uA)"); plot->yAxis->setScaleType(QCPAxis::stLinear); plot->yAxis->setTicker(linTicker); plot->yAxis->setNumberFormat("f"); plot->yAxis2->setVisible(false); graphAmp->setName("Current"); graphAmp->setVisible(true); graphReal->setVisible(false); graphImag->setVisible(false); graphHilbert->setVisible(false); graphNyquistCorr->setVisible(false); graphExtrapolated->setVisible(false); graphLSVBlank->setVisible(false); graphLSVSample->setVisible(false); graphLSVDiff->setVisible(false); graphFit->setVisible(false); plot->replot(); } void GraphWidget::configureLSVPlot() { plot->xAxis->setLabel("Voltage (mV)"); plot->xAxis->setScaleType(QCPAxis::stLinear); QSharedPointer linTicker(new QCPAxisTicker); plot->xAxis->setTicker(linTicker); plot->xAxis->setNumberFormat("f"); plot->yAxis->setLabel("Current (uA)"); plot->yAxis->setScaleType(QCPAxis::stLinear); plot->yAxis->setTicker(linTicker); plot->yAxis->setNumberFormat("f"); plot->yAxis2->setVisible(false); graphLSVBlank->setVisible(true); graphLSVSample->setVisible(true); graphLSVDiff->setVisible(true); graphReal->setVisible(false); graphImag->setVisible(false); graphHilbert->setVisible(false); graphNyquistCorr->setVisible(false); graphExtrapolated->setVisible(false); graphAmp->setVisible(false); graphFit->setVisible(false); plot->replot(); } void GraphWidget::addBodeData(double freq, double val1, double val2) { if (plot->xAxis->scaleType() == QCPAxis::stLogarithmic && freq <= 0) return; graphReal->addData(freq, val1); graphImag->addData(freq, val2); graphReal->rescaleAxes(false); graphImag->rescaleAxes(false); graphHilbert->rescaleAxes(false); plot->replot(); } void GraphWidget::addNyquistData(double r_meas, double i_meas, double r_corr, double i_corr, bool showCorr) { graphNyquistRaw->addData(r_meas, -i_meas); if (showCorr) { graphNyquistCorr->addData(r_corr, -i_corr); } graphNyquistRaw->rescaleAxes(false); if (showCorr) { graphNyquistCorr->rescaleAxes(true); } plot->replot(); } void GraphWidget::addAmperometricData(double index, double current) { graphAmp->addData(index, current); graphAmp->rescaleAxes(false); plot->replot(); } void GraphWidget::addLSVData(double voltage, double current, LSVTrace traceType) { QCPGraph *target = nullptr; switch (traceType) { case LSV_BLANK: target = graphLSVBlank; break; case LSV_SAMPLE: target = graphLSVSample; break; case LSV_DIFF: target = graphLSVDiff; break; } if (target) { target->addData(voltage, current); target->rescaleAxes(false); plot->replot(); } } void GraphWidget::addHilbertData(const QVector &freq, const QVector &hilbertImag) { if (plot->xAxis->label() != "Frequency (Hz)") return; graphHilbert->setData(freq, hilbertImag); graphHilbert->rescaleAxes(false); plot->replot(); } void GraphWidget::setExtrapolatedPoint(double real, double imag) { graphExtrapolated->data()->clear(); graphExtrapolated->addData(real, -imag); plot->replot(); } void GraphWidget::clear() { graphReal->data()->clear(); graphImag->data()->clear(); graphHilbert->data()->clear(); graphNyquistCorr->data()->clear(); graphAmp->data()->clear(); graphExtrapolated->data()->clear(); graphFit->data()->clear(); plot->replot(); } void GraphWidget::clearLSV(LSVTrace traceType) { if (traceType == LSV_BLANK) graphLSVBlank->data()->clear(); if (traceType == LSV_SAMPLE) graphLSVSample->data()->clear(); if (traceType == LSV_DIFF) graphLSVDiff->data()->clear(); plot->replot(); }