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&mdash;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 
(&eg; 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&mdash;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 (&eg; 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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The right 
ascension (RA) drift error in 
arc-seconds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -444,7 +444,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plot the 
declination (DEC) drift error in 
arc-seconds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -485,7 +485,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The right 
ascension (RA) guide pulses in 
milliseconds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The right 
ascension (RA) guide pulses in milliseconds. Click here to view this axis on 
left-axis values. Double click to update 
axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -532,7 +532,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The declination 
(DEC) guide pulses in milliseconds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The declination 
(DEC) guide pulses in milliseconds. Click here to view this axis on left-axis 
values. Double click to update 
axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -579,7 +579,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The combined RA 
and DEC drift error in arc-seconds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -626,7 +626,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The 
root-mean-squared (RMS) value of the combined RA and DEC drift in arc-seconds, 
averaged on approximately the past 40 
samples.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -673,7 +673,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The sky 
background light level (computed by SEP from the guide 
images).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -714,7 +714,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The number of 
stars detected in the guide images.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The number of 
stars detected in the guide images. Click here to view this axis on left-axis 
values. Double click to update 
axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -761,7 +761,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The 
signal-to-noise ratio (SNR) of the guide 
star.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -814,7 +814,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The Right 
Ascension (RA) in HMS where the telescope is 
pointing.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 8pt</string>
@@ -861,7 +861,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The Declination 
(DEC) in degrees:arc-minutes:arc-seconds where the telescope is 
pointing.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 8pt</string>
@@ -902,7 +902,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The telescope's 
azimuth (degrees).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The telescope's 
azimuth (degrees). Click here to view this axis on left-axis values. Double 
click to update axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -949,7 +949,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The telescope's 
altitude (degrees).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The telescope's 
altitude (degrees). Click here to view this axis on left-axis values. Double 
click to update axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -996,7 +996,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The mount's 
pier side (left) -&gt; where the mount is 
pointing.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The mount's 
pier side (left) -&gt; where the mount is pointing. Click here to view this 
axis on left-axis values. Double click to update 
axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1049,7 +1049,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The mount's 
hour angle value.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The mount's 
hour angle value. Click here to view this axis on left-axis values. Double 
click to update axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 8pt</string>
@@ -1090,7 +1090,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The Half-Flux 
Radius (in pixels) of the captured 
images.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1131,7 +1131,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plot the number 
of stars detected in the captured 
images.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1172,7 +1172,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plot the median 
sample value in the captured 
images.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plot the median 
sample value in the captured images. Click here to view this axis on left-axis 
values. Double click to update 
axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1213,7 +1213,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plot the median 
star eccentricity in the captured 
images.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plot the median 
star eccentricity in the captured images. Click here to view this axis on 
left-axis values. Double click to update 
axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1254,7 +1254,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plot the 
ambient temperature.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plot the 
ambient temperature. Click here to view this axis on left-axis values. Double 
click to update axis.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1301,7 +1301,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
         </property>
         <property name="styleSheet">
          <string notr="true">font-size: 9pt</string>
@@ -1348,7 +1348,7 @@
          </size>
         </property>
         <property name="toolTip">
-         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Plot the 
distance between the plate-solved captured image and the target position in 
arc-seconds. Must be enabled in scheduler 
options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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 <[email protected]>
+
+    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 <[email protected]>
+
+    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">

Reply via email to