[education/kstars] /: Add custom y-axes to Analyze Statistics plot.

Hy Murveit null at kde.org
Mon Jan 9 01:44:30 GMT 2023


Git commit 882a0a3603427905de1d2c61ff032e4baf91479a by Hy Murveit.
Committed on 09/01/2023 at 00:54.
Pushed by murveit into branch 'master'.

Add custom y-axes to Analyze Statistics plot.

M  +12   -3    doc/ekos-analyze.docbook
M  +-    --    doc/ekos_analyze.png
M  +2    -0    kstars/CMakeLists.txt
M  +421  -171  kstars/ekos/analyze/analyze.cpp
M  +40   -11   kstars/ekos/analyze/analyze.h
M  +22   -22   kstars/ekos/analyze/analyze.ui
A  +168  -0    kstars/ekos/analyze/yaxistool.cpp     [License: GPL(v2.0+)]
A  +108  -0    kstars/ekos/analyze/yaxistool.h     [License: GPL(v2.0+)]
A  +260  -0    kstars/ekos/analyze/yaxistool.ui
M  +3    -0    kstars/kstars.kcfg

https://invent.kde.org/education/kstars/commit/882a0a3603427905de1d2c61ff032e4baf91479a

diff --git a/doc/ekos-analyze.docbook b/doc/ekos-analyze.docbook
index 7702c7623..5746ea150 100644
--- a/doc/ekos-analyze.docbook
+++ b/doc/ekos-analyze.docbook
@@ -24,13 +24,16 @@
             The Analyze Module records and displays what happened in an imaging session. That is, it does not control any if your imaging, but rather reviews what occurred. Sessions are stored in an <filename class="directory">analyze</filename> folder, a sister folder to the main logging folder. The <literal role="extension">.analyze</literal> files written there can be loaded into the <guilabel>Analyze</guilabel> tab to be viewed. <guilabel>Analyze</guilabel> also can display data from the current imaging session.
         </para>
         <para>
-            There are two main graphs, <guilabel>Timeline</guilabel> and <guilabel>Stats</guilabel>. They are coordinated—they always display the same time interval from the Ekos session, though the x-axis of the <guilabel>Timeline</guilabel> shows seconds elapsed from the start of the log, and <guilabel>Stats</guilabel> shows clock time. The x-axis can be zoomed in and out with the <guibutton>+/-</guibutton> button, mouse <mousebutton>wheel</mousebutton>, as well as with standard keyboard shortcuts (⪚ zoom-in == <keycombo>&Ctrl;<keycap>+</keycap></keycombo>) The x-axis can be panned with the scroll bar as well as with the left and right arrow keys. You can view your current imaging session, or review old sessions by loading <literal role="extension">.analyze</literal> files using the <guilabel>Input</guilabel> dropdown. Checking <guilabel>Full Width</guilabel> displays all the data, and <guilabel>Latest</guilabel> displays the most recent data (you can control the width by zooming).
+            There are two main graphs, <guilabel>Timeline</guilabel> and <guilabel>Stats</guilabel>. They are coordinated—they always display the same time interval from the Ekos session, though the x-axis of the <guilabel>Timeline</guilabel> shows seconds elapsed from the start of the log, and <guilabel>Stats</guilabel> shows clock time. The x-axis can be zoomed in and out with the <guibutton>+/-</guibutton> buttons, as well as with standard keyboard shortcuts (⪚ zoom-in == <keycombo>&Ctrl;<keycap>+</keycap></keycombo>) The x-axis can be panned with the scroll bar as well as with the left and right arrow keys. You can view your current imaging session, or review old sessions by loading <literal role="extension">.analyze</literal> files using the <guilabel>Input</guilabel> dropdown. Checking <guilabel>Full Width</guilabel> displays all the data, and <guilabel>Latest</guilabel> displays the most recent data (you can control the width by zooming).
+        </para>
+        <para>
+            The three main displays can be hidden to make more room for the other displays. There are checkboxes to the left of the section titles (Timeline, Statistics, and Details) that enable and hide the displays.
         </para>
     </sect3>
     <sect3 id="analyze-timeline">
         <title>Timeline</title>
         <para>
-        Timeline shows the major Ekos processes, and when they were active. For instance, the <guilabel>Capture</guilabel> line shows when images were taken (green sections) and when imaging was aborted (red sections). Clicking on a green section gives information about that image, and double clicking on one brings up the image taken then in a fitsviewer, if it is available.
+        Timeline shows the major Ekos processes, and when they were active. For instance, the <guilabel>Capture</guilabel> line shows when images were taken (wither green for RGB or color-coded by the the filter) and when imaging was aborted (shown as red sections). Clicking on a capture section gives information about that image, and double clicking on one brings up the image taken then in a fitsviewer, if it is available.
         </para>
         <note>
             <para>
@@ -44,7 +47,13 @@
     <sect3 id="analyze-statistics">
         <title>Statistics</title>
         <para>
-        A variety of statistics can be displayed on the <guilabel>Stats</guilabel> graph. There are too many for all to be shown in a readable way, so select among them with the checkboxes. A reasonable way to start might be to use <guilabel>rms</guilabel>, <guilabel>snr</guilabel> (using the internal guider with SEP Multistar), and <guilabel>hfr</guilabel> (if you have auto-compute HFR in the FITS options). Experiment with others. The axis shown (0-5) is appropriate only for ra/dec error, drift, rms, pulses, and hfr. These may be y-axis scaled (awkwardly) using the mouse <mousebutton>wheel</mousebutton>, but the other graphs cannot be scaled. To reset y-axis zooming, right-click on the Stats plot. Clicking on the graph fills in the values of the displayed statistics. This graph is zoomed and panned horizontally in coordination with the timeline.
+          A variety of statistics can be displayed on the <guilabel>Statistics</guilabel> graph. There are too many for all to be shown in a readable way, so select among them with the checkboxes. A reasonable way to start might be to use <guilabel>rms</guilabel>, <guilabel>snr</guilabel> (using the internal guider with SEP Multistar), and <guilabel>hfr</guilabel> (if you have auto-compute HFR in the FITS options). Experiment with others.
+        </para>
+        <para>
+          The left axis shown is initially appropriate only for RA/DEC error, drift, RMS error, RA/DEC pulses, and HFR, plotted in arc-seconds and defaulting to a range of -2 to 5 arc-seconds. However, clicking on one of boxes below the Statistics graph (that shows a statistic's value) will set that statistic's range as the range shown on the left-axis. Double clicking on that box will bring up a menu allowing you to adjust the statistic's plotted y-range (e.g. setting it to auto, explicitly typing in the range, setting it back to its default value, and also changing the color of that statistic's plot).
+        </para>
+        <para>
+          The statistic shown on the left axis can also be scaled (awkwardly) using the mouse <mousebutton>wheel</mousebutton>. It can be panned by dragging the mouse up or down over the left axis' numbers.  Clicking anywhere inside the Statistics graph fills in the values of the displayed statistics. Checking the latest box causes the most recent values (from a live session) to be the statistics displayed. This graph is zoomed and panned horizontally in coordination with the timeline.
         </para>
     </sect3>
 </sect2>
diff --git a/doc/ekos_analyze.png b/doc/ekos_analyze.png
index 988772789..42c5612fa 100644
Binary files a/doc/ekos_analyze.png and b/doc/ekos_analyze.png differ
diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt
index 24eed8d97..ced280741 100644
--- a/kstars/CMakeLists.txt
+++ b/kstars/CMakeLists.txt
@@ -118,6 +118,7 @@ if (INDI_FOUND)
             ekos/profilewizard.ui
             # Analyze
             ekos/analyze/analyze.ui
+            ekos/analyze/yaxistool.ui
             # Scheduler
             ekos/scheduler/scheduler.ui
             ekos/scheduler/mosaic.ui
@@ -227,6 +228,7 @@ if (INDI_FOUND)
 
             # Analyze
             ekos/analyze/analyze.cpp
+            ekos/analyze/yaxistool.cpp
 
             # Scheduler
             ekos/scheduler/schedulerjob.cpp
diff --git a/kstars/ekos/analyze/analyze.cpp b/kstars/ekos/analyze/analyze.cpp
index 15405225e..339491fab 100644
--- a/kstars/ekos/analyze/analyze.cpp
+++ b/kstars/ekos/analyze/analyze.cpp
@@ -317,7 +317,60 @@ class RmsFilter
         double filteredRMS { 0 };
 };
 
-Analyze::Analyze()
+bool Analyze::eventFilter(QObject *obj, QEvent *ev)
+{
+    // Quit if click wasn't on a QLineEdit.
+    if (qobject_cast<QLineEdit*>(obj) == nullptr)
+        return false;
+
+    // This filter only applies to single or double clicks.
+    if (ev->type() != QEvent::MouseButtonDblClick && ev->type() != QEvent::MouseButtonPress)
+        return false;
+
+    auto axisEntry = yAxisMap.find(obj);
+    if (axisEntry == yAxisMap.end())
+        return false;
+
+    const bool isRightClick = (ev->type() == QEvent::MouseButtonPress) &&
+                              (static_cast<QMouseEvent*>(ev)->button() == Qt::RightButton);
+    const bool isControlClick = (ev->type() == QEvent::MouseButtonPress) &&
+                                (static_cast<QMouseEvent*>(ev)->modifiers() &
+                                 Qt::KeyboardModifier::ControlModifier);
+    const bool isShiftClick = (ev->type() == QEvent::MouseButtonPress) &&
+                              (static_cast<QMouseEvent*>(ev)->modifiers() &
+                               Qt::KeyboardModifier::ShiftModifier);
+
+    if (ev->type() == QEvent::MouseButtonDblClick || isRightClick || isControlClick || isShiftClick)
+    {
+        startYAxisTool(axisEntry->first, axisEntry->second);
+        clickTimer.stop();
+        return true;
+    }
+    else if (ev->type() == QEvent::MouseButtonPress)
+    {
+        clickTimer.setSingleShot(true);
+        clickTimer.setInterval(250);
+        clickTimer.start();
+        m_ClickTimerInfo = axisEntry->second;
+        // Wait 0.25 seconds to see if this is a double click or just a single click.
+        connect(&clickTimer, &QTimer::timeout, this, [&]()
+        {
+            m_YAxisTool.reject();
+            if (m_ClickTimerInfo.checkBox && !m_ClickTimerInfo.checkBox->isChecked())
+            {
+                // Enable the graph.
+                m_ClickTimerInfo.checkBox->setChecked(true);
+                statsPlot->graph(m_ClickTimerInfo.graphIndex)->setVisible(true);
+                statsPlot->graph(m_ClickTimerInfo.graphIndex)->addToLegend();
+            }
+            userSetLeftAxis(m_ClickTimerInfo.axis);
+        });
+        return true;
+    }
+    return false;
+}
+
+Analyze::Analyze() : m_YAxisTool(this)
 {
     setupUi(this);
 
@@ -328,7 +381,13 @@ Analyze::Analyze()
 
     initInputSelection();
     initTimelinePlot();
+
     initStatsPlot();
+    connect(&m_YAxisTool, &YAxisTool::axisChanged, this, &Analyze::userChangedYAxis);
+    connect(&m_YAxisTool, &YAxisTool::leftAxisChanged, this, &Analyze::userSetLeftAxis);
+    connect(&m_YAxisTool, &YAxisTool::axisColorChanged, this, &Analyze::userSetAxisColor);
+    qApp->installEventFilter(this);
+
     initGraphicsPlot();
     fullWidthCB->setChecked(true);
     keepCurrentCB->setChecked(true);
@@ -343,10 +402,10 @@ Analyze::Analyze()
     graphsCB->setChecked(true);
     timelineCB->setChecked(true);
     setVisibility();
-    connect(timelineCB, &QCheckBox::stateChanged, this, &Ekos::Analyze::setVisibility);
-    connect(graphsCB, &QCheckBox::stateChanged, this, &Ekos::Analyze::setVisibility);
-    connect(statsCB, &QCheckBox::stateChanged, this, &Ekos::Analyze::setVisibility);
-    connect(detailsCB, &QCheckBox::stateChanged, this, &Ekos::Analyze::setVisibility);
+    connect(timelineCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility);
+    connect(graphsCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility);
+    connect(statsCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility);
+    connect(detailsCB, &QCheckBox::stateChanged, this, &Analyze::setVisibility);
 
     connect(fullWidthCB, &QCheckBox::toggled, [ = ](bool checked)
     {
@@ -356,18 +415,18 @@ Analyze::Analyze()
 
     initStatsCheckboxes();
 
-    connect(zoomInB, &QPushButton::clicked, this, &Ekos::Analyze::zoomIn);
-    connect(zoomOutB, &QPushButton::clicked, this, &Ekos::Analyze::zoomOut);
-    connect(timelinePlot, &QCustomPlot::mousePress, this, &Ekos::Analyze::timelineMousePress);
-    connect(timelinePlot, &QCustomPlot::mouseDoubleClick, this, &Ekos::Analyze::timelineMouseDoubleClick);
-    connect(timelinePlot, &QCustomPlot::mouseWheel, this, &Ekos::Analyze::timelineMouseWheel);
-    connect(statsPlot, &QCustomPlot::mousePress, this, &Ekos::Analyze::statsMousePress);
-    connect(statsPlot, &QCustomPlot::mouseDoubleClick, this, &Ekos::Analyze::statsMouseDoubleClick);
-    connect(statsPlot, &QCustomPlot::mouseMove, this, &Ekos::Analyze::statsMouseMove);
-    connect(analyzeSB, &QScrollBar::valueChanged, this, &Ekos::Analyze::scroll);
+    connect(zoomInB, &QPushButton::clicked, this, &Analyze::zoomIn);
+    connect(zoomOutB, &QPushButton::clicked, this, &Analyze::zoomOut);
+    connect(timelinePlot, &QCustomPlot::mousePress, this, &Analyze::timelineMousePress);
+    connect(timelinePlot, &QCustomPlot::mouseDoubleClick, this, &Analyze::timelineMouseDoubleClick);
+    connect(timelinePlot, &QCustomPlot::mouseWheel, this, &Analyze::timelineMouseWheel);
+    connect(statsPlot, &QCustomPlot::mousePress, this, &Analyze::statsMousePress);
+    connect(statsPlot, &QCustomPlot::mouseDoubleClick, this, &Analyze::statsMouseDoubleClick);
+    connect(statsPlot, &QCustomPlot::mouseMove, this, &Analyze::statsMouseMove);
+    connect(analyzeSB, &QScrollBar::valueChanged, this, &Analyze::scroll);
     analyzeSB->setRange(0, MAX_SCROLL_VALUE);
-    connect(helpB, &QPushButton::clicked, this, &Ekos::Analyze::helpMessage);
-    connect(keepCurrentCB, &QCheckBox::stateChanged, this, &Ekos::Analyze::keepCurrent);
+    connect(helpB, &QPushButton::clicked, this, &Analyze::helpMessage);
+    connect(keepCurrentCB, &QCheckBox::stateChanged, this, &Analyze::keepCurrent);
 
     setupKeyboardShortcuts(this);
 
@@ -381,6 +440,7 @@ void Analyze::setVisibility()
     statsGridWidget->setVisible(statsCB->isChecked());
     timelinePlot->setVisible(timelineCB->isChecked());
     statsPlot->setVisible(graphsCB->isChecked());
+    replot();
 }
 
 // Mouse wheel over the Timeline plot causes an x-axis zoom.
@@ -477,24 +537,24 @@ void Analyze::setupKeyboardShortcuts(QWidget *plot)
 {
     // Shortcuts defined: https://doc.qt.io/archives/qt-4.8/qkeysequence.html#standard-shortcuts
     QShortcut *s = new QShortcut(QKeySequence(QKeySequence::ZoomIn), plot);
-    connect(s, &QShortcut::activated, this, &Ekos::Analyze::zoomIn);
+    connect(s, &QShortcut::activated, this, &Analyze::zoomIn);
     s = new QShortcut(QKeySequence(QKeySequence::ZoomOut), plot);
-    connect(s, &QShortcut::activated, this, &Ekos::Analyze::zoomOut);
+    connect(s, &QShortcut::activated, this, &Analyze::zoomOut);
 
     s = new QShortcut(QKeySequence(QKeySequence::MoveToNextChar), plot);
-    connect(s, &QShortcut::activated, this, &Ekos::Analyze::scrollRight);
+    connect(s, &QShortcut::activated, this, &Analyze::scrollRight);
     s = new QShortcut(QKeySequence(QKeySequence::MoveToPreviousChar), plot);
-    connect(s, &QShortcut::activated, this, &Ekos::Analyze::scrollLeft);
+    connect(s, &QShortcut::activated, this, &Analyze::scrollLeft);
     s = new QShortcut(QKeySequence(QKeySequence::MoveToNextLine), plot);
-    connect(s, &QShortcut::activated, this, &Ekos::Analyze::statsYZoomIn);
+    connect(s, &QShortcut::activated, this, &Analyze::statsYZoomIn);
     s = new QShortcut(QKeySequence(QKeySequence::MoveToPreviousLine), plot);
-    connect(s, &QShortcut::activated, this, &Ekos::Analyze::statsYZoomOut);
+    connect(s, &QShortcut::activated, this, &Analyze::statsYZoomOut);
     s = new QShortcut(QKeySequence("?"), plot);
-    connect(s, &QShortcut::activated, this, &Ekos::Analyze::helpMessage);
+    connect(s, &QShortcut::activated, this, &Analyze::helpMessage);
     s = new QShortcut(QKeySequence("h"), plot);
-    connect(s, &QShortcut::activated, this, &Ekos::Analyze::helpMessage);
+    connect(s, &QShortcut::activated, this, &Analyze::helpMessage);
     s = new QShortcut(QKeySequence(QKeySequence::HelpContents), plot);
-    connect(s, &QShortcut::activated, this, &Ekos::Analyze::helpMessage);
+    connect(s, &QShortcut::activated, this, &Analyze::helpMessage);
 }
 
 Analyze::~Analyze()
@@ -623,12 +683,6 @@ void Analyze::addGuideStatsInternal(double raDrift, double decDrift, double raPu
     if (!qIsNaN(numStars))
         numStarsMax = std::max(numStars, static_cast<double>(numStarsMax));
 
-    snrAxis->setRange(-1.05 * snrMax, std::max(10.0, 1.05 * snrMax));
-    medianAxis->setRange(-1.35 * medianMax, std::max(10.0, 1.35 * medianMax));
-    numCaptureStarsAxis->setRange(-1.45 * numCaptureStarsMax, std::max(10.0, 1.45 * numCaptureStarsMax));
-    skyBgAxis->setRange(0, std::max(10.0, 1.15 * skyBgMax));
-    numStarsAxis->setRange(0, std::max(10.0, 1.25 * numStarsMax));
-
     statsPlot->graph(SNR_GRAPH)->addData(time, snr);
     statsPlot->graph(NUMSTARS_GRAPH)->addData(time, numStars);
     statsPlot->graph(SKYBG_GRAPH)->addData(time, skyBackground);
@@ -1357,11 +1411,7 @@ void Analyze::processStatsClick(QMouseEvent *event, bool doubleClick)
 {
     Q_UNUSED(doubleClick);
     double xval = statsPlot->xAxis->pixelToCoord(event->x());
-    if (event->button() == Qt::RightButton || event->modifiers() == Qt::ControlModifier)
-        // Resets the range. Replot will take care of ra/dec needing negative values.
-        statsPlot->yAxis->setRange(0, 5);
-    else
-        setStatsCursor(xval);
+    setStatsCursor(xval);
     replot();
 }
 
@@ -1377,10 +1427,13 @@ void Analyze::timelineMouseDoubleClick(QMouseEvent *event)
 
 void Analyze::statsMousePress(QMouseEvent *event)
 {
+    QCPAxis *yAxis = activeYAxis;
+    if (!yAxis) return;
+
     // If we're on the legend, adjust the y-axis.
     if (statsPlot->xAxis->pixelToCoord(event->x()) < plotStart)
     {
-        yAxisInitialPos = statsPlot->yAxis->pixelToCoord(event->y());
+        yAxisInitialPos = yAxis->pixelToCoord(event->y());
         return;
     }
     processStatsClick(event, false);
@@ -1394,13 +1447,18 @@ void Analyze::statsMouseDoubleClick(QMouseEvent *event)
 // Allow the user to click and hold, causing the cursor to move in real-time.
 void Analyze::statsMouseMove(QMouseEvent *event)
 {
+    QCPAxis *yAxis = activeYAxis;
+    if (!yAxis) return;
+
     // If we're on the legend, adjust the y-axis.
     if (statsPlot->xAxis->pixelToCoord(event->x()) < plotStart)
     {
-        auto range = statsPlot->yAxis->range();
-        double yDiff = yAxisInitialPos - statsPlot->yAxis->pixelToCoord(event->y());
-        statsPlot->yAxis->setRange(range.lower + yDiff, range.upper + yDiff);
+        auto range = yAxis->range();
+        double yDiff = yAxisInitialPos - yAxis->pixelToCoord(event->y());
+        yAxis->setRange(range.lower + yDiff, range.upper + yDiff);
         replot();
+        if (m_YAxisTool.isVisible() && m_YAxisTool.getAxis() == yAxis)
+            m_YAxisTool.replot(true);
         return;
     }
     processStatsClick(event, false);
@@ -1464,15 +1522,19 @@ void Analyze::replot(bool adjustSlider)
 
     statsPlot->xAxis->setRange(plotStart, plotStart + plotWidth);
 
-    // Don't reset the range if the user has changed it.
-    auto yRange = statsPlot->yAxis->range();
-    if ((yRange.lower == 0 || yRange.lower == -2) && (yRange.upper == 5))
+    // Rescale any automatic y-axes.
+    if (statsPlot->isVisible())
     {
-        // Only need negative numbers on the stats plot if we're plotting RA or DEC
-        if (raCB->isChecked() || decCB->isChecked() || raPulseCB->isChecked() || decPulseCB->isChecked())
-            statsPlot->yAxis->setRange(-2, 5);
-        else
-            statsPlot->yAxis->setRange(0, 5);
+        for (auto &pairs : yAxisMap)
+        {
+            const YAxisInfo &info = pairs.second;
+            if (statsPlot->graph(info.graphIndex)->visible() && info.rescale)
+            {
+                QCPAxis *axis = info.axis;
+                axis->rescale();
+                axis->scaleRange(1.1, axis->range().center());
+            }
+        }
     }
 
     dateTicker->setOffset(displayStartTime.toMSecsSinceEpoch() / 1000.0);
@@ -1480,15 +1542,34 @@ void Analyze::replot(bool adjustSlider)
     timelinePlot->replot();
     statsPlot->replot();
     graphicsPlot->replot();
+
+    if (activeYAxis != nullptr)
+    {
+        // Adjust the statsPlot padding to align statsPlot and timelinePlot.
+        const int widthDiff = statsPlot->axisRect()->width() - timelinePlot->axisRect()->width();
+        const int paddingSize = activeYAxis->padding();
+        constexpr int maxPadding = 100;
+        // Don't quite following why a positive difference should INCREASE padding, but it works.
+        const int newPad = std::min(maxPadding, std::max(0, paddingSize + widthDiff));
+        if (newPad != paddingSize)
+        {
+            activeYAxis->setPadding(newPad);
+            statsPlot->replot();
+        }
+    }
     updateStatsValues();
 }
 
 void Analyze::statsYZoom(double zoomAmount)
 {
-    auto range = statsPlot->yAxis->range();
+    auto axis = activeYAxis;
+    if (!axis) return;
+    auto range = axis->range();
     const double halfDiff = (range.upper - range.lower) / 2.0;
     const double middle = (range.upper + range.lower) / 2.0;
-    statsPlot->yAxis->setRange(QCPRange(middle - halfDiff * zoomAmount, middle + halfDiff * zoomAmount));
+    axis->setRange(QCPRange(middle - halfDiff * zoomAmount, middle + halfDiff * zoomAmount));
+    if (m_YAxisTool.isVisible() && m_YAxisTool.getAxis() == axis)
+        m_YAxisTool.replot(true);
 }
 void Analyze::statsYZoomIn()
 {
@@ -1665,28 +1746,27 @@ void Analyze::zoomOut()
 
 namespace
 {
+
+void setupAxisDefaults(QCPAxis *axis)
+{
+    axis->setBasePen(QPen(Qt::white, 1));
+    axis->grid()->setPen(QPen(QColor(140, 140, 140, 140), 1, Qt::DotLine));
+    axis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 1, Qt::DotLine));
+    axis->grid()->setZeroLinePen(QPen(Qt::white, 1));
+    axis->setBasePen(QPen(Qt::white, 1));
+    axis->setTickPen(QPen(Qt::white, 1));
+    axis->setSubTickPen(QPen(Qt::white, 1));
+    axis->setTickLabelColor(Qt::white);
+    axis->setLabelColor(Qt::white);
+}
+
 // Generic initialization of a plot, applied to all plots in this tab.
 void initQCP(QCustomPlot *plot)
 {
     plot->setBackground(QBrush(Qt::black));
-    plot->xAxis->setBasePen(QPen(Qt::white, 1));
-    plot->yAxis->setBasePen(QPen(Qt::white, 1));
-    plot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140, 140), 1, Qt::DotLine));
-    plot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140, 140), 1, Qt::DotLine));
-    plot->xAxis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 1, Qt::DotLine));
-    plot->yAxis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 1, Qt::DotLine));
+    setupAxisDefaults(plot->yAxis);
+    setupAxisDefaults(plot->xAxis);
     plot->xAxis->grid()->setZeroLinePen(Qt::NoPen);
-    plot->yAxis->grid()->setZeroLinePen(QPen(Qt::white, 1));
-    plot->xAxis->setBasePen(QPen(Qt::white, 1));
-    plot->yAxis->setBasePen(QPen(Qt::white, 1));
-    plot->xAxis->setTickPen(QPen(Qt::white, 1));
-    plot->yAxis->setTickPen(QPen(Qt::white, 1));
-    plot->xAxis->setSubTickPen(QPen(Qt::white, 1));
-    plot->yAxis->setSubTickPen(QPen(Qt::white, 1));
-    plot->xAxis->setTickLabelColor(Qt::white);
-    plot->yAxis->setTickLabelColor(Qt::white);
-    plot->xAxis->setLabelColor(Qt::white);
-    plot->yAxis->setLabelColor(Qt::white);
 }
 }  // namespace
 
@@ -1717,7 +1797,7 @@ void Analyze::toggleGraph(int graph_id, bool show)
     replot();
 }
 
-int Analyze::initGraph(QCustomPlot *plot, QCPAxis *yAxis, QCPGraph::LineStyle lineStyle,
+int Analyze::initGraph(QCustomPlot * plot, QCPAxis * yAxis, QCPGraph::LineStyle lineStyle,
                        const QColor &color, const QString &name)
 {
     int num = plot->graphCount();
@@ -1728,12 +1808,27 @@ int Analyze::initGraph(QCustomPlot *plot, QCPAxis *yAxis, QCPGraph::LineStyle li
     return num;
 }
 
-template <typename Func>
-int Analyze::initGraphAndCB(QCustomPlot *plot, QCPAxis *yAxis, QCPGraph::LineStyle lineStyle,
-                            const QColor &color, const QString &name, QCheckBox *cb, Func setCb)
+void Analyze::updateYAxisMap(QObject * key, const YAxisInfo &axisInfo)
+{
+    if (key == nullptr) return;
+    auto axisEntry = yAxisMap.find(key);
+    if (axisEntry == yAxisMap.end())
+        yAxisMap.insert(std::make_pair(key, axisInfo));
+    else
+        axisEntry->second = axisInfo;
+}
 
+template <typename Func>
+int Analyze::initGraphAndCB(QCustomPlot * plot, QCPAxis * yAxis, QCPGraph::LineStyle lineStyle,
+                            const QColor &color, const QString &name, const QString &shortName,
+                            QCheckBox * cb, Func setCb, QLineEdit * out)
 {
-    const int num = initGraph(plot, yAxis, lineStyle, color, name);
+    const int num = initGraph(plot, yAxis, lineStyle, color, shortName);
+    if (out != nullptr)
+    {
+        const bool autoAxis = YAxisInfo::isRescale(yAxis->range());
+        updateYAxisMap(out, YAxisInfo(yAxis, yAxis->range(), autoAxis, num, plot, cb, name, shortName, color));
+    }
     if (cb != nullptr)
     {
         // Don't call toggleGraph() here, as it's too early for replot().
@@ -1754,10 +1849,176 @@ int Analyze::initGraphAndCB(QCustomPlot *plot, QCPAxis *yAxis, QCPGraph::LineSty
     return num;
 }
 
+
+void Analyze::userSetAxisColor(QObject *key, const YAxisInfo &axisInfo, const QColor &color)
+{
+    updateYAxisMap(key, axisInfo);
+    statsPlot->graph(axisInfo.graphIndex)->setPen(QPen(color));
+    Options::setAnalyzeStatsYAxis(serializeYAxes());
+    replot();
+}
+
+void Analyze::userSetLeftAxis(QCPAxis *axis)
+{
+    setLeftAxis(axis);
+    Options::setAnalyzeStatsYAxis(serializeYAxes());
+    replot();
+}
+
+void Analyze::userChangedYAxis(QObject *key, const YAxisInfo &axisInfo)
+{
+    updateYAxisMap(key, axisInfo);
+    Options::setAnalyzeStatsYAxis(serializeYAxes());
+    replot();
+}
+
+// TODO: Doesn't seem like this is ever getting called. Not sure why not receiving the rangeChanged signal.
+void Analyze::yAxisRangeChanged(const QCPRange &newRange)
+{
+    Q_UNUSED(newRange);
+    if (m_YAxisTool.isVisible() && m_YAxisTool.getAxis() == activeYAxis)
+        m_YAxisTool.replot(true);
+}
+
+void Analyze::setLeftAxis(QCPAxis *axis)
+{
+    if (axis != nullptr && axis != activeYAxis)
+    {
+        for (const auto &pair : yAxisMap)
+        {
+            // Couldn't get this to compile in the new-style connect syntax.
+            disconnect(pair.second.axis, SIGNAL(QCPAxis::rangeChanged(QCPRange)),
+                       this, SLOT(Ekos::Analyze::yAxisRangeChanged(QCPRange)));
+            //disconnect(pair.second.axis, &QCPAxis::rangeChanged, this, &Analyze::yAxisRangeChanged);
+            pair.second.axis->setVisible(false);
+        }
+        axis->setVisible(true);
+        activeYAxis = axis;
+        statsPlot->axisRect()->setRangeZoomAxes(0, axis);
+
+        // Couldn't get this to compile in the new-style connect syntax.
+        connect(axis, SIGNAL(QCPAxis::rangeChanged(QCPRange)),
+                this, SLOT(Ekos::Analyze::yAxisRangeChanged(QCPRange)));
+        //connect(axis, &QCPAxis::rangeChanged, this, &Analyze::yAxisRangeChanged);
+    }
+}
+
+void Analyze::startYAxisTool(QObject * key, const YAxisInfo &info)
+{
+    if (info.checkBox && !info.checkBox->isChecked())
+    {
+        // Enable the graph.
+        info.checkBox->setChecked(true);
+        statsPlot->graph(info.graphIndex)->setVisible(true);
+        statsPlot->graph(info.graphIndex)->addToLegend();
+    }
+
+    m_YAxisTool.reset(key, info, info.axis == activeYAxis);
+    m_YAxisTool.show();
+}
+
+QCPAxis *Analyze::newStatsYAxis(const QString &label, double lower, double upper)
+{
+    QCPAxis *axis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0); // 0 means QCP creates the axis.
+    axis->setVisible(false);
+    axis->setRange(lower, upper);
+    axis->setLabel(label);
+    setupAxisDefaults(axis);
+    return axis;
+}
+
+bool Analyze::restoreYAxes(const QString &encoding)
+{
+    constexpr int headerSize = 2;
+    constexpr int itemSize = 5;
+    QVector<QStringRef> items = encoding.splitRef(',');
+    if (items.size() <= headerSize) return false;
+    if ((items.size() - headerSize) % itemSize != 0) return false;
+    if (items[0] != "AnalyzeStatsYAxis1.0") return false;
+
+    // Restore the active Y axis
+    const QString leftID = "left=";
+    if (!items[1].startsWith(leftID)) return false;
+    QStringRef left = items[1].mid(leftID.size());
+    if (left.size() <= 0) return false;
+    for (const auto &pair : yAxisMap)
+    {
+        if (pair.second.axis->label() == left)
+        {
+            setLeftAxis(pair.second.axis);
+            break;
+        }
+    }
+
+    // Restore the various upper/lower/rescale axis values.
+    for (int i = headerSize; i < items.size(); i += itemSize)
+    {
+        const QString shortName = items[i].toString();
+        const double lower = items[i + 1].toDouble();
+        const double upper = items[i + 2].toDouble();
+        const bool rescale = items[i + 3] == "T";
+        const QColor color(items[i + 4]);
+        for (auto &pair : yAxisMap)
+        {
+            auto &info = pair.second;
+            if (info.axis->label() == shortName)
+            {
+                info.color = color;
+                statsPlot->graph(info.graphIndex)->setPen(QPen(color));
+                info.rescale = rescale;
+                if (rescale)
+                    info.axis->setRange(
+                        QCPRange(YAxisInfo::LOWER_RESCALE,
+                                 YAxisInfo::UPPER_RESCALE));
+                else
+                    info.axis->setRange(QCPRange(lower, upper));
+                break;
+            }
+        }
+    }
+    return true;
+}
+
+// This would be sensitive to short names with commas in them, but we don't do that.
+QString Analyze::serializeYAxes()
+{
+    QString encoding = QString("AnalyzeStatsYAxis1.0,left=%1").arg(activeYAxis->label());
+    QList<QString> savedAxes;
+    for (const auto &pair : yAxisMap)
+    {
+        const YAxisInfo &info = pair.second;
+        const bool rescale = info.rescale;
+
+        // Only save if something has changed.
+        bool somethingChanged = (info.initialColor != info.color) ||
+                                (rescale != YAxisInfo::isRescale(info.initialRange)) ||
+                                (!rescale && info.axis->range() != info.initialRange);
+
+        if (!somethingChanged) continue;
+
+        // Don't save the same axis twice
+        if (savedAxes.contains(info.axis->label())) continue;
+
+        double lower = rescale ? YAxisInfo::LOWER_RESCALE : info.axis->range().lower;
+        double upper = rescale ? YAxisInfo::UPPER_RESCALE : info.axis->range().upper;
+        encoding.append(QString(",%1,%2,%3,%4,%5")
+                        .arg(info.axis->label()).arg(lower).arg(upper)
+                        .arg(info.rescale ? "T" : "F").arg(info.color.name()));
+        savedAxes.append(info.axis->label());
+    }
+    return encoding;
+}
+
 void Analyze::initStatsPlot()
 {
     initQCP(statsPlot);
 
+    // Setup the main y-axis
+    statsPlot->yAxis->setVisible(true);
+    statsPlot->yAxis->setLabel("RA/DEC");
+    statsPlot->yAxis->setRange(-2, 5);
+    setLeftAxis(statsPlot->yAxis);
+
     // Setup the legend
     statsPlot->legend->setVisible(true);
     statsPlot->legend->setFont(QFont("Helvetica", 6));
@@ -1771,9 +2032,10 @@ void Analyze::initStatsPlot()
     statsPlot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignLeft | Qt::AlignTop);
 
     // Add the graphs.
-
-    HFR_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsStepRight, Qt::cyan, "HFR", hfrCB,
-                               Options::setAnalyzeHFR);
+    QString shortName = "HFR";
+    QCPAxis *hfrAxis = newStatsYAxis(shortName, -2, 6);
+    HFR_GRAPH = initGraphAndCB(statsPlot, hfrAxis, QCPGraph::lsStepRight, Qt::cyan, "Capture Image HFR", shortName, hfrCB,
+                               Options::setAnalyzeHFR, hfrOut);
     connect(hfrCB, &QCheckBox::clicked,
             [ = ](bool show)
     {
@@ -1785,11 +2047,11 @@ void Analyze::initStatsPlot()
                      "will have their HFRs computed."));
     });
 
-    numCaptureStarsAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    numCaptureStarsAxis->setVisible(false);
-    numCaptureStarsAxis->setRange(0, 1000);  // this will be reset.
-    NUM_CAPTURE_STARS_GRAPH = initGraphAndCB(statsPlot, numCaptureStarsAxis, QCPGraph::lsStepRight, Qt::darkGreen, "#SubStars",
-                              numCaptureStarsCB, Options::setAnalyzeNumCaptureStars);
+    shortName = "#SubStars";
+    QCPAxis *numCaptureStarsAxis = newStatsYAxis(shortName);
+    NUM_CAPTURE_STARS_GRAPH = initGraphAndCB(statsPlot, numCaptureStarsAxis, QCPGraph::lsStepRight, Qt::darkGreen,
+                              "#Stars in Capture", shortName,
+                              numCaptureStarsCB, Options::setAnalyzeNumCaptureStars, numCaptureStarsOut);
     connect(numCaptureStarsCB, &QCheckBox::clicked,
             [ = ](bool show)
     {
@@ -1801,106 +2063,93 @@ void Analyze::initStatsPlot()
                      "will have their stars detected."));
     });
 
-    medianAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    medianAxis->setVisible(false);
-    medianAxis->setRange(0, 1000);  // this will be reset.
-    MEDIAN_GRAPH = initGraphAndCB(statsPlot, medianAxis, QCPGraph::lsStepRight, Qt::darkGray, "median",
-                                  medianCB, Options::setAnalyzeMedian);
-
-    ECCENTRICITY_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsStepRight, Qt::darkMagenta, "ecc",
-                                        eccentricityCB, Options::setAnalyzeEccentricity);
-
-    numStarsAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    numStarsAxis->setVisible(false);
-    numStarsAxis->setRange(0, 15000);
-    NUMSTARS_GRAPH = initGraphAndCB(statsPlot, numStarsAxis, QCPGraph::lsStepRight, Qt::magenta, "#Stars", numStarsCB,
-                                    Options::setAnalyzeNumStars);
-
-    skyBgAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    skyBgAxis->setVisible(false);
-    skyBgAxis->setRange(0, 1000);
-    SKYBG_GRAPH = initGraphAndCB(statsPlot, skyBgAxis, QCPGraph::lsStepRight, Qt::darkYellow, "SkyBG", skyBgCB,
-                                 Options::setAnalyzeSkyBg);
-
-
-    temperatureAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    temperatureAxis->setVisible(false);
-    temperatureAxis->setRange(-40, 40);
-    TEMPERATURE_GRAPH = initGraphAndCB(statsPlot, temperatureAxis, QCPGraph::lsLine, Qt::yellow, "temp", temperatureCB,
-                                       Options::setAnalyzeTemperature);
-
-    targetDistanceAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    targetDistanceAxis->setVisible(false);
-    targetDistanceAxis->setRange(0, 60);
+    shortName = "median";
+    QCPAxis *medianAxis = newStatsYAxis(shortName);
+    MEDIAN_GRAPH = initGraphAndCB(statsPlot, medianAxis, QCPGraph::lsStepRight, Qt::darkGray, "Median Pixel", shortName,
+                                  medianCB, Options::setAnalyzeMedian, medianOut);
+
+    shortName = "ecc";
+    QCPAxis *eccAxis = newStatsYAxis(shortName, 0, 1.0);
+    ECCENTRICITY_GRAPH = initGraphAndCB(statsPlot, eccAxis, QCPGraph::lsStepRight, Qt::darkMagenta, "Eccentricity",
+                                        shortName, eccentricityCB, Options::setAnalyzeEccentricity, eccentricityOut);
+    shortName = "#Stars";
+    QCPAxis *numStarsAxis = newStatsYAxis(shortName);
+    NUMSTARS_GRAPH = initGraphAndCB(statsPlot, numStarsAxis, QCPGraph::lsStepRight, Qt::magenta, "#Stars in Guide Image",
+                                    shortName, numStarsCB, Options::setAnalyzeNumStars, numStarsOut);
+    shortName = "SkyBG";
+    QCPAxis *skyBgAxis = newStatsYAxis(shortName);
+    SKYBG_GRAPH = initGraphAndCB(statsPlot, skyBgAxis, QCPGraph::lsStepRight, Qt::darkYellow, "Sky Background Brightness",
+                                 shortName, skyBgCB, Options::setAnalyzeSkyBg, skyBgOut);
+
+    shortName = "temp";
+    QCPAxis *temperatureAxis = newStatsYAxis(shortName, -40, 40);
+    TEMPERATURE_GRAPH = initGraphAndCB(statsPlot, temperatureAxis, QCPGraph::lsLine, Qt::yellow, "Temperature", shortName,
+                                       temperatureCB, Options::setAnalyzeTemperature, temperatureOut);
+    shortName = "tDist";
+    QCPAxis *targetDistanceAxis = newStatsYAxis(shortName, 0, 60);
     TARGET_DISTANCE_GRAPH = initGraphAndCB(statsPlot, targetDistanceAxis, QCPGraph::lsLine,
                                            QColor(253, 185, 200),  // pink
-                                           "tDist", targetDistanceCB, Options::setAnalyzeTargetDistance);
-
-    snrAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    snrAxis->setVisible(false);
-    snrAxis->setRange(-100, 100);  // this will be reset.
-    SNR_GRAPH = initGraphAndCB(statsPlot, snrAxis, QCPGraph::lsLine, Qt::yellow, "SNR", snrCB, Options::setAnalyzeSNR);
-
+                                           "Distance to Target (arcsec)", shortName, targetDistanceCB, Options::setAnalyzeTargetDistance, targetDistanceOut);
+    shortName = "SNR";
+    QCPAxis *snrAxis = newStatsYAxis(shortName, -100, 100);
+    SNR_GRAPH = initGraphAndCB(statsPlot, snrAxis, QCPGraph::lsLine, Qt::yellow, "Guider SNR", shortName, snrCB,
+                               Options::setAnalyzeSNR, snrOut);
+    shortName = "RA";
     auto raColor = KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError");
-    RA_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, raColor, "RA", raCB, Options::setAnalyzeRA);
+    RA_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, raColor, "Guider RA Drift", shortName, raCB,
+                              Options::setAnalyzeRA, raOut);
+    shortName = "DEC";
     auto decColor = KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError");
-    DEC_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, decColor, "DEC", decCB, Options::setAnalyzeDEC);
-
-    QCPAxis *pulseAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    pulseAxis->setVisible(false);
-    // 150 is a typical value for pulse-ms/pixel
-    // This will roughtly co-incide with the -2,5 range for the ra/dec plots.
-    pulseAxis->setRange(-2 * 150, 5 * 150);
-
+    DEC_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, decColor, "Guider DEC Drift", shortName, decCB,
+                               Options::setAnalyzeDEC, decOut);
+    shortName = "RAp";
     auto raPulseColor = KStarsData::Instance()->colorScheme()->colorNamed("RAGuideError");
     raPulseColor.setAlpha(75);
-    RA_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, raPulseColor, "RAp", raPulseCB,
-                                    Options::setAnalyzeRAp);
+    QCPAxis *pulseAxis = newStatsYAxis(shortName, -2 * 150, 5 * 150);
+    RA_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, raPulseColor, "RA Correction Pulse (ms)", shortName,
+                                    raPulseCB, Options::setAnalyzeRAp, raPulseOut);
     statsPlot->graph(RA_PULSE_GRAPH)->setBrush(QBrush(raPulseColor, Qt::Dense4Pattern));
 
+    shortName = "DECp";
     auto decPulseColor = KStarsData::Instance()->colorScheme()->colorNamed("DEGuideError");
     decPulseColor.setAlpha(75);
-    DEC_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, decPulseColor, "DECp", decPulseCB,
-                                     Options::setAnalyzeDECp);
+    DEC_PULSE_GRAPH = initGraphAndCB(statsPlot, pulseAxis, QCPGraph::lsLine, decPulseColor, "DEC Correction Pulse (ms)",
+                                     shortName, decPulseCB, Options::setAnalyzeDECp, decPulseOut);
     statsPlot->graph(DEC_PULSE_GRAPH)->setBrush(QBrush(decPulseColor, Qt::Dense4Pattern));
 
-    DRIFT_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::lightGray, "Drift", driftCB,
-                                 Options::setAnalyzeDrift);
-    RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red, "RMS", rmsCB, Options::setAnalyzeRMS);
-    CAPTURE_RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red, "RMSc", rmsCCB,
-                                       Options::setAnalyzeRMSC);
-
-    QCPAxis *mountRaDecAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    mountRaDecAxis->setVisible(false);
-    mountRaDecAxis->setRange(-10, 370);
+    shortName = "Drift";
+    DRIFT_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::lightGray, "Guider Instantaneous Drift",
+                                 shortName, driftCB, Options::setAnalyzeDrift, driftOut);
+    shortName = "RMS";
+    RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red, "Guider RMS Drift", shortName, rmsCB,
+                               Options::setAnalyzeRMS, rmsOut);
+    shortName = "RMSc";
+    CAPTURE_RMS_GRAPH = initGraphAndCB(statsPlot, statsPlot->yAxis, QCPGraph::lsLine, Qt::red,
+                                       "Guider RMS Drift (during capture)", shortName, rmsCCB,
+                                       Options::setAnalyzeRMSC, rmsCOut);
+    shortName = "MOUNT_RA";
+    QCPAxis *mountRaDecAxis = newStatsYAxis(shortName, -10, 370);
     // Colors of these two unimportant--not really plotted.
-    MOUNT_RA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "MOUNT_RA", mountRaCB,
-                                    Options::setAnalyzeMountRA);
-    MOUNT_DEC_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "MOUNT_DEC", mountDecCB,
-                                     Options::setAnalyzeMountDEC);
-    MOUNT_HA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "MOUNT_HA", mountHaCB,
-                                    Options::setAnalyzeMountHA);
-
-    QCPAxis *azAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    azAxis->setVisible(false);
-    azAxis->setRange(-10, 370);
-    AZ_GRAPH = initGraphAndCB(statsPlot, azAxis, QCPGraph::lsLine, Qt::darkGray, "AZ", azCB, Options::setAnalyzeAz);
-
-    QCPAxis *altAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    altAxis->setVisible(false);
-    altAxis->setRange(0, 90);
-    ALT_GRAPH = initGraphAndCB(statsPlot, altAxis, QCPGraph::lsLine, Qt::white, "ALT", altCB, Options::setAnalyzeAlt);
-
-    QCPAxis *pierSideAxis = statsPlot->axisRect()->addAxis(QCPAxis::atLeft, 0);
-    pierSideAxis->setVisible(false);
-    pierSideAxis->setRange(-2, 2);
-    PIER_SIDE_GRAPH = initGraphAndCB(statsPlot, pierSideAxis, QCPGraph::lsLine, Qt::darkRed, "PierSide", pierSideCB,
-                                     Options::setAnalyzePierSide);
-
-    // TODO: Should figure out the margin
-    // on the timeline plot, and setting this one accordingly.
-    // doesn't look like that's possible with current code, though.
-    statsPlot->yAxis->setPadding(50);
+    MOUNT_RA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "Mount RA Degrees", shortName,
+                                    mountRaCB, Options::setAnalyzeMountRA, mountRaOut);
+    shortName = "MOUNT_DEC";
+    MOUNT_DEC_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "Mount DEC Degrees", shortName,
+                                     mountDecCB, Options::setAnalyzeMountDEC, mountDecOut);
+    shortName = "MOUNT_HA";
+    MOUNT_HA_GRAPH = initGraphAndCB(statsPlot, mountRaDecAxis, QCPGraph::lsLine, Qt::red, "Mount Hour Angle", shortName,
+                                    mountHaCB, Options::setAnalyzeMountHA, mountHaOut);
+    shortName = "AZ";
+    QCPAxis *azAxis = newStatsYAxis(shortName, -10, 370);
+    AZ_GRAPH = initGraphAndCB(statsPlot, azAxis, QCPGraph::lsLine, Qt::darkGray, "Mount Azimuth", shortName, azCB,
+                              Options::setAnalyzeAz, azOut);
+    shortName = "ALT";
+    QCPAxis *altAxis = newStatsYAxis(shortName, 0, 90);
+    ALT_GRAPH = initGraphAndCB(statsPlot, altAxis, QCPGraph::lsLine, Qt::white, "Mount Altitude", shortName, altCB,
+                               Options::setAnalyzeAlt, altOut);
+    shortName = "PierSide";
+    QCPAxis *pierSideAxis = newStatsYAxis(shortName, -2, 2);
+    PIER_SIDE_GRAPH = initGraphAndCB(statsPlot, pierSideAxis, QCPGraph::lsLine, Qt::darkRed, "Mount Pier Side", shortName,
+                                     pierSideCB, Options::setAnalyzePierSide, pierSideOut);
 
     // This makes mouseMove only get called when a button is pressed.
     statsPlot->setMouseTracking(false);
@@ -1912,7 +2161,8 @@ void Analyze::initStatsPlot()
 
     // Didn't include QCP::iRangeDrag as it  interacts poorly with the curson logic.
     statsPlot->setInteractions(QCP::iRangeZoom);
-    statsPlot->axisRect()->setRangeZoomAxes(0, statsPlot->yAxis);
+
+    restoreYAxes(Options::analyzeStatsYAxis());
 }
 
 // Clear the graphics and state when changing input data.
@@ -2157,7 +2407,7 @@ void Analyze::updateMaxX(double time)
 // This only happens with live data, not with data read from .analyze files.
 
 // Remove the graphic element.
-void Analyze::removeTemporarySession(Session *session)
+void Analyze::removeTemporarySession(Session * session)
 {
     if (session->rect != nullptr)
         timelinePlot->removeItem(session->rect);
@@ -2179,7 +2429,7 @@ void Analyze::removeTemporarySessions()
 }
 
 // Add a new temporary session.
-void Analyze::addTemporarySession(Session *session, double time, double duration,
+void Analyze::addTemporarySession(Session * session, double time, double duration,
                                   int y_offset, const QBrush &brush)
 {
     removeTemporarySession(session);
@@ -2194,7 +2444,7 @@ void Analyze::addTemporarySession(Session *session, double time, double duration
 // Extend a temporary session. That is, we don't know how long the session will last,
 // so when new data arrives (from any module, not necessarily the one with the temporary
 // session) we must extend that temporary session.
-void Analyze::adjustTemporarySession(Session *session)
+void Analyze::adjustTemporarySession(Session * session)
 {
     if (session->rect != nullptr && session->end < maxXValue)
     {
diff --git a/kstars/ekos/analyze/analyze.h b/kstars/ekos/analyze/analyze.h
index 07f38a853..26d642e4f 100644
--- a/kstars/ekos/analyze/analyze.h
+++ b/kstars/ekos/analyze/analyze.h
@@ -9,10 +9,11 @@
 
 #include <QtDBus>
 #include <memory>
-
+#include "qcustomplot.h"
 #include "ekos/ekos.h"
 #include "ekos/mount/mount.h"
 #include "indi/indimount.h"
+#include "yaxistool.h"
 #include "ui_analyze.h"
 
 class FITSViewer;
@@ -183,6 +184,13 @@ class Analyze : public QWidget, public Ui::Analyze
         void schedulerJobEnded(const QString &jobName, const QString &endReason);
         void newTargetDistance(double targetDistance);
 
+        // From YAxisTool
+        void userChangedYAxis(QObject *key, const YAxisInfo &axisInfo);
+        void userSetLeftAxis(QCPAxis *axis);
+        void userSetAxisColor(QObject *key, const YAxisInfo &axisInfo, const QColor &color);
+
+        void yAxisRangeChanged(const QCPRange &newRange);
+
     private slots:
 
     signals:
@@ -314,7 +322,8 @@ class Analyze : public QWidget, public Ui::Analyze
                       const QColor &color, const QString &name);
         template <typename Func>
         int initGraphAndCB(QCustomPlot *plot, QCPAxis *yAxis, QCPGraph::LineStyle lineStyle,
-                           const QColor &color, const QString &name, QCheckBox *cb, Func setCb);
+                           const QColor &color, const QString &name, const QString &shortName,
+                           QCheckBox *cb, Func setCb, QLineEdit *out = nullptr);
 
         // Make graphs visible/invisible & add/delete them from the legend.
         void toggleGraph(int graph_id, bool show);
@@ -373,6 +382,35 @@ class Analyze : public QWidget, public Ui::Analyze
         void startLog();
         void appendToLog(const QString &lines);
 
+        // Used to capture double clicks on stats output QLineEdits to set y-axis limits.
+        bool eventFilter(QObject *o, QEvent *e) override;
+        QTimer clickTimer;
+        YAxisInfo m_ClickTimerInfo;
+
+        // Utility that adds a y-axis to the stats plot.
+        QCPAxis *newStatsYAxis(const QString &label, double lower = YAxisInfo::LOWER_RESCALE,
+                               double upper = YAxisInfo::UPPER_RESCALE);
+
+        // Save and restore user-updated y-axis limits.
+        QString serializeYAxes();
+        bool restoreYAxes(const QString &encoding);
+
+        // Sets the y-axis to be displayed on the left of the statsPlot.
+        void setLeftAxis(QCPAxis *axis);
+        void updateYAxisMap(QObject *key, const YAxisInfo &axisInfo);
+
+        // The pop-up allowing users to edit y-axis lower and upper graph values.
+        YAxisTool m_YAxisTool;
+
+        // The y-axis values displayed to the left of the stat's graph.
+        QCPAxis *activeYAxis { nullptr };
+
+        void startYAxisTool(QObject *key, const YAxisInfo &info);
+
+        // Map connecting QLineEdits to Y-Axes, so when a QLineEdit is double clicked,
+        // the corresponding y-axis can be found.
+        std::map<QObject*, YAxisInfo> yAxisMap;
+
         // The .analyze log file being written.
         QString logFilename { "" };
         QFile logFile;
@@ -401,15 +439,6 @@ class Analyze : public QWidget, public Ui::Analyze
         std::unique_ptr<RmsFilter> guiderRms;
         std::unique_ptr<RmsFilter> captureRms;
 
-        // Y-axes for the for several plots where we rescale based on data.
-        // QCustomPlot owns these pointers' memory, don't free it.
-        QCPAxis *snrAxis;
-        QCPAxis *numStarsAxis;
-        QCPAxis *skyBgAxis;
-        QCPAxis *medianAxis;
-        QCPAxis *numCaptureStarsAxis;
-        QCPAxis *temperatureAxis;
-        QCPAxis *targetDistanceAxis;
         // Used to keep track of the y-axis position when moving it with the mouse.
         double yAxisInitialPos = { 0 };
 
diff --git a/kstars/ekos/analyze/analyze.ui b/kstars/ekos/analyze/analyze.ui
index d3ee53413..4a9443e36 100644
--- a/kstars/ekos/analyze/analyze.ui
+++ b/kstars/ekos/analyze/analyze.ui
@@ -403,7 +403,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The right ascension (RA) drift error in arc-seconds.</p></body></html></string>
+         <string><html><head/><body><p>The right ascension (RA) drift error in arc-seconds. Click here to view this axis on left-axis values. Shares axis with RA/DEC error, drift, and RMS values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -444,7 +444,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>Plot the declination (DEC) drift error in arc-seconds.</p></body></html></string>
+         <string><html><head/><body><p>Plot the declination (DEC) drift error in arc-seconds. Click here to view this axis on left-axis values. Shares axis with RA/DEC error, drift, and RMS values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -485,7 +485,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The right ascension (RA) guide pulses in milliseconds.</p></body></html></string>
+         <string><html><head/><body><p>The right ascension (RA) guide pulses in milliseconds. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -532,7 +532,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The declination (DEC) guide pulses in milliseconds.</p></body></html></string>
+         <string><html><head/><body><p>The declination (DEC) guide pulses in milliseconds. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -579,7 +579,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The combined RA and DEC drift error in arc-seconds.</p></body></html></string>
+         <string><html><head/><body><p>The combined RA and DEC drift error in arc-seconds. Click here to view this axis on left-axis values. Shares axis with RA/DEC error, drift, and RMS values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -626,7 +626,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The root-mean-squared (RMS) value of the combined RA and DEC drift in arc-seconds, averaged on approximately the past 40 samples.</p></body></html></string>
+         <string><html><head/><body><p>The root-mean-squared (RMS) value of the combined RA and DEC drift in arc-seconds, averaged on approximately the past 40 samples. Click here to view this axis on left-axis values. Shares axis with RA/DEC error, drift, and RMS values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -673,7 +673,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The sky background light level (computed by SEP from the guide images).</p></body></html></string>
+         <string><html><head/><body><p>The sky background light level (computed by SEP from the guide images). Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -714,7 +714,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The number of stars detected in the guide images.</p></body></html></string>
+         <string><html><head/><body><p>The number of stars detected in the guide images. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -761,7 +761,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The signal-to-noise ratio (SNR) of the guide star.</p></body></html></string>
+         <string><html><head/><body><p>The signal-to-noise ratio (SNR) of the guide star. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -814,7 +814,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The Right Ascension (RA) in HMS where the telescope is pointing.</p></body></html></string>
+         <string><html><head/><body><p>The Right Ascension (RA) in HMS where the telescope is pointing. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 8pt</string>
@@ -861,7 +861,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The Declination (DEC) in degrees:arc-minutes:arc-seconds where the telescope is pointing.</p></body></html></string>
+         <string><html><head/><body><p>The Declination (DEC) in degrees:arc-minutes:arc-seconds where the telescope is pointing. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 8pt</string>
@@ -902,7 +902,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The telescope's azimuth (degrees).</p></body></html></string>
+         <string><html><head/><body><p>The telescope's azimuth (degrees). Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -949,7 +949,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The telescope's altitude (degrees).</p></body></html></string>
+         <string><html><head/><body><p>The telescope's altitude (degrees). Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -996,7 +996,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The mount's pier side (left) -> where the mount is pointing.</p></body></html></string>
+         <string><html><head/><body><p>The mount's pier side (left) -> where the mount is pointing. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1049,7 +1049,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The mount's hour angle value.</p></body></html></string>
+         <string><html><head/><body><p>The mount's hour angle value. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 8pt</string>
@@ -1090,7 +1090,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The Half-Flux Radius (in pixels) of the captured images.</p></body></html></string>
+         <string><html><head/><body><p>The Half-Flux Radius (in pixels) of the captured images. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1131,7 +1131,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>Plot the number of stars detected in the captured images.</p></body></html></string>
+         <string><html><head/><body><p>Plot the number of stars detected in the captured images. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1172,7 +1172,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>Plot the median sample value in the captured images.</p></body></html></string>
+         <string><html><head/><body><p>Plot the median sample value in the captured images. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1213,7 +1213,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>Plot the median star eccentricity in the captured images.</p></body></html></string>
+         <string><html><head/><body><p>Plot the median star eccentricity in the captured images. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1254,7 +1254,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>Plot the ambient temperature.</p></body></html></string>
+         <string><html><head/><body><p>Plot the ambient temperature. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1301,7 +1301,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>The root-mean-squared (RMS) value of the combined RA and DEC drift in arc-seconds, averaged on approximately the past 40 samples, but only in during capture.</p></body></html></string>
+         <string><html><head/><body><p>The root-mean-squared (RMS) value of the combined RA and DEC drift in arc-seconds, averaged on approximately the past 40 samples, but only in during capture. Click here to view this axis on left-axis values. Shares axis with RA/DEC error, drift, and RMS values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1348,7 +1348,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string><html><head/><body><p>Plot the distance between the plate-solved captured image and the target position in arc-seconds. Must be enabled in scheduler options.</p></body></html></string>
+         <string><html><head/><body><p>Plot the distance between the plate-solved captured image and the target position in arc-seconds. Must be enabled in scheduler options. Click here to view this axis on left-axis values. Double click to update axis.</p></body></html></string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
diff --git a/kstars/ekos/analyze/yaxistool.cpp b/kstars/ekos/analyze/yaxistool.cpp
new file mode 100644
index 000000000..e4968c786
--- /dev/null
+++ b/kstars/ekos/analyze/yaxistool.cpp
@@ -0,0 +1,168 @@
+/*
+    SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja at ikarustech.com>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "yaxistool.h"
+
+#include <QColorDialog>
+#include "Options.h"
+#include <kstars_debug.h>
+
+YAxisToolUI::YAxisToolUI(QWidget *p) : QFrame(p)
+{
+    setupUi(this);
+}
+
+YAxisTool::YAxisTool(QWidget *w) : QDialog(w)
+{
+#ifdef Q_OS_OSX
+    setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+#endif
+    ui = new YAxisToolUI(this);
+
+    ui->setStyleSheet("QPushButton:checked { background-color: red; }");
+
+    setWindowTitle(i18nc("@title:window", "Y-Axis Tool"));
+
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    mainLayout->addWidget(ui);
+    setLayout(mainLayout);
+
+    connect(ui->lowerLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+    connect(ui->upperLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+    connect(ui->autoLimitsCB, &QCheckBox::clicked, this, &YAxisTool::updateAutoValues);
+    connect(ui->defaultB, &QPushButton::clicked, this, &YAxisTool::updateToDefaults);
+    connect(ui->colorB, &QPushButton::clicked, this, &YAxisTool::updateColor);
+    connect(ui->leftAxisCB, &QCheckBox::clicked, this, &YAxisTool::updateLeftAxis);
+}
+
+void YAxisTool::updateValues(const double value)
+{
+    Q_UNUSED(value);
+    YAxisInfo info = m_Info;
+    info.axis->setRange(QCPRange(ui->lowerLimitSpin->value(), ui->upperLimitSpin->value()));
+    info.rescale = ui->autoLimitsCB->isChecked();
+    m_Info = info;
+    updateSpins();
+    emit axisChanged(m_Key, m_Info);
+}
+
+void YAxisTool::updateLeftAxis()
+{
+    ui->leftAxisCB->setEnabled(!ui->leftAxisCB->isChecked());
+    emit leftAxisChanged(m_Info.axis);
+}
+
+void YAxisTool::computeAutoLimits()
+{
+    if (ui->autoLimitsCB->isChecked())
+    {
+        // The user checked the auto button.
+        // Need to set the lower/upper spin boxes with the new auto limits.
+        QCPAxis *axis = m_Info.axis;
+        axis->rescale();
+        axis->scaleRange(1.1, axis->range().center());
+
+        disconnect(ui->lowerLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+        disconnect(ui->upperLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+        ui->lowerLimitSpin->setValue(axis->range().lower);
+        ui->upperLimitSpin->setValue(axis->range().upper);
+        connect(ui->lowerLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+        connect(ui->upperLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+    }
+}
+
+void YAxisTool::updateAutoValues()
+{
+    computeAutoLimits();
+    updateValues(0);
+}
+
+void YAxisTool::updateToDefaults()
+{
+    m_Info.axis->setRange(m_Info.initialRange);
+    m_Info.rescale = YAxisInfo::isRescale(m_Info.axis->range());
+    m_Info.color = m_Info.initialColor;
+    replot(ui->leftAxisCB->isChecked());
+    computeAutoLimits();
+    emit axisChanged(m_Key, m_Info);
+}
+
+void YAxisTool::replot(bool isLeftAxis)
+{
+    // I was using editingFinished, but that signaled on change of focus.
+    // ValueChanged/valueChanged can output on the outputSpins call.
+    disconnect(ui->lowerLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+    disconnect(ui->upperLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+    ui->reset(m_Info);
+    connect(ui->lowerLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+    connect(ui->upperLimitSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &YAxisTool::updateValues);
+
+    ui->leftAxisCB->setChecked(isLeftAxis);
+    ui->leftAxisCB->setEnabled(!isLeftAxis);
+    updateSpins();
+
+    ui->colorLabel->setText("");
+    ui->colorLabel->setStyleSheet(QString("QLabel { background-color : %1; }").arg(m_Info.color.name()));
+}
+
+void YAxisTool::reset(QObject *key, const YAxisInfo &info, bool isLeftAxis)
+{
+    m_Info = info;
+    m_Key = key;
+    replot(isLeftAxis);
+}
+
+void YAxisTool::updateColor()
+{
+    QColor color = QColorDialog::getColor(m_Info.color, this);
+    if (color.isValid())
+    {
+        ui->colorLabel->setStyleSheet(QString("QLabel { background-color : %1; }").arg(color.name()));
+        m_Info.color = color;
+        emit axisColorChanged(m_Key, m_Info, color);
+    }
+    return;
+}
+
+void YAxisTool::updateSpins()
+{
+    ui->lowerLimitSpin->setEnabled(!m_Info.rescale);
+    ui->upperLimitSpin->setEnabled(!m_Info.rescale);
+    ui->lowerLimitLabel->setEnabled(!m_Info.rescale);
+    ui->upperLimitLabel->setEnabled(!m_Info.rescale);
+}
+
+void YAxisToolUI::reset(const YAxisInfo &info)
+{
+
+    statLabel->setText(info.name);
+    statShortLabel->setText(info.shortName);
+    lowerLimitSpin->setValue(info.axis->range().lower);
+    upperLimitSpin->setValue(info.axis->range().upper);
+    autoLimitsCB->setChecked(info.rescale);
+}
+
+// If the user hit's the 'X', still want to remove the live preview.
+void YAxisTool::closeEvent(QCloseEvent *event)
+{
+    Q_UNUSED(event);
+    slotClosed();
+}
+
+void YAxisTool::showEvent(QShowEvent *event)
+{
+    Q_UNUSED(event);
+}
+
+
+void YAxisTool::slotClosed()
+{
+}
+
+void YAxisTool::slotSaveChanges()
+{
+}
+
diff --git a/kstars/ekos/analyze/yaxistool.h b/kstars/ekos/analyze/yaxistool.h
new file mode 100644
index 000000000..91b77fc98
--- /dev/null
+++ b/kstars/ekos/analyze/yaxistool.h
@@ -0,0 +1,108 @@
+/*
+    SPDX-FileCopyrightText: 2023 Hy Murveit <hy at murveit.com>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+#include "ui_yaxistool.h"
+#include "qcustomplot.h"
+
+#include <QDialog>
+#include <QFrame>
+
+#include <memory>
+
+/**
+ * @class YAxisInfo
+ * @short Used to keep track of the various Y-axes and connect them to the QLineEdits.
+ *
+ * @version 1.0
+ * @author Hy Murveit
+ */
+struct YAxisInfo
+{
+    static constexpr double LOWER_RESCALE = -102, UPPER_RESCALE = -101;
+    QCPAxis *axis;
+    QCPRange initialRange;
+    bool rescale;
+    int graphIndex;
+    QCustomPlot *plot;
+    QCheckBox *checkBox;
+    QString name;
+    QString shortName;
+    QColor initialColor;
+    QColor color;
+    YAxisInfo() {}
+    YAxisInfo(QCPAxis *axis_, QCPRange initialRange_, bool rescale_, int graphIndex_,
+              QCustomPlot *plot_, QCheckBox *checkBox_, const QString &name_,
+              const QString &shortName_, const QColor &color_)
+        : axis(axis_), initialRange(initialRange_), rescale(rescale_), graphIndex(graphIndex_),
+          plot(plot_), checkBox(checkBox_), name(name_), shortName(shortName_),
+          initialColor(color_), color(color_) {}
+    static bool isRescale(const QCPRange &range)
+    {
+        return (range.lower == YAxisInfo::LOWER_RESCALE &&
+                range.upper == YAxisInfo::UPPER_RESCALE);
+    }
+};
+
+class YAxisToolUI : public QFrame, public Ui::YAxisTool
+{
+        Q_OBJECT
+
+    public:
+        explicit YAxisToolUI(QWidget *parent);
+        void reset(const YAxisInfo &info);
+    private:
+};
+
+/**
+ * @class YAxisTool
+ * @short Manages adjusting the Y-axis of Analyze stats graphs.
+ *
+ * @version 1.0
+ * @author Hy Murveit
+ */
+class YAxisTool : public QDialog
+{
+        Q_OBJECT
+    public:
+        explicit YAxisTool(QWidget *ks);
+        virtual ~YAxisTool() override = default;
+        void reset(QObject *key, const YAxisInfo &info, bool isLeftAxis);
+        void replot(bool isLeftAxis);
+        const QCPAxis *getAxis()
+        {
+            return m_Info.axis;
+        }
+
+    protected:
+        void closeEvent(QCloseEvent *event) override;
+        void showEvent(QShowEvent *event) override;
+
+    signals:
+        void axisChanged(QObject *key, const YAxisInfo &info);
+        void axisColorChanged(QObject *key, const YAxisInfo &info, const QColor &color);
+        void leftAxisChanged(QCPAxis *axis);
+
+    public slots:
+        void updateValues(const double value);
+        void slotClosed();
+
+    private slots:
+        void slotSaveChanges();
+
+    private:
+        void updateAutoValues();
+        void updateLeftAxis();
+        void updateToDefaults();
+        void updateSpins();
+        void updateColor();
+        void computeAutoLimits();
+
+        YAxisToolUI *ui { nullptr };
+        QObject *m_Key { nullptr };
+        YAxisInfo m_Info;
+};
diff --git a/kstars/ekos/analyze/yaxistool.ui b/kstars/ekos/analyze/yaxistool.ui
new file mode 100644
index 000000000..f6cc56c6f
--- /dev/null
+++ b/kstars/ekos/analyze/yaxistool.ui
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>YAxisTool</class>
+ <widget class="QWidget" name="YAxisTool">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>300</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_3">
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout">
+     <item>
+      <widget class="QLabel" name="statLabel">
+       <property name="maximumSize">
+        <size>
+         <width>16777215</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Stat Name</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLabel" name="statShortLabel">
+       <property name="font">
+        <font>
+         <pointsize>9</pointsize>
+        </font>
+       </property>
+       <property name="text">
+        <string>TextLabel</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="verticalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeType">
+        <enum>QSizePolicy::Fixed</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <widget class="QLabel" name="upperLimitLabel">
+       <property name="maximumSize">
+        <size>
+         <width>100</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Upper Limit</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QDoubleSpinBox" name="upperLimitSpin">
+       <property name="maximumSize">
+        <size>
+         <width>100</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="minimum">
+        <double>-100000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>100000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="lowerLimitLabel">
+       <property name="maximumSize">
+        <size>
+         <width>100</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Lower Limit</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QDoubleSpinBox" name="lowerLimitSpin">
+       <property name="maximumSize">
+        <size>
+         <width>100</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="minimum">
+        <double>-100000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>100000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="autoLimitsCB">
+     <property name="maximumSize">
+      <size>
+       <width>16777215</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="text">
+      <string>Automatic Limits</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="leftAxisCB">
+     <property name="text">
+      <string>Use for Left Axis</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <widget class="QLabel" name="label_2">
+       <property name="maximumSize">
+        <size>
+         <width>50</width>
+         <height>16777206</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Color</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignCenter</set>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLabel" name="colorLabel">
+       <property name="maximumSize">
+        <size>
+         <width>50</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="colorB">
+       <property name="maximumSize">
+        <size>
+         <width>75</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Change</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_3">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>30</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <widget class="QPushButton" name="defaultB">
+     <property name="text">
+      <string>Use Default Limits</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>10</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg
index f5389e2b2..f34ad9936 100644
--- a/kstars/kstars.kcfg
+++ b/kstars/kstars.kcfg
@@ -2778,6 +2778,9 @@
       <whatsthis>Display PierSide on the Analyze Statistics Plot.</whatsthis>
       <default>false</default>
     </entry>
+    <entry name="AnalyzeStatsYAxis" type="String">
+      <label>Stored Y-axis upper and lower limits for the Analyze Stats Plot.</label>
+    </entry>
    </group>
    <group name="INDI Lite">
       <entry name="LastServer" type="String">


More information about the kde-doc-english mailing list