MiniPlotMpl.cpp 10.3 KB
Newer Older
1
2
3
// Mantid Repository : https://github.com/mantidproject/mantid
//
// Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
4
5
//   NScD Oak Ridge National Laboratory, European Spallation Source,
//   Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
6
// SPDX - License - Identifier: GPL - 3.0 +
7
#include "MantidQtWidgets/InstrumentView/MiniPlotMpl.h"
8
#include "MantidPythonInterface/core/GlobalInterpreterLock.h"
9
#include "MantidPythonInterface/core/VersionCompat.h"
10
#include "MantidQtWidgets/InstrumentView/PeakMarker2D.h"
11
#include "MantidQtWidgets/MplCpp/FigureCanvasQt.h"
12
13

#include "MantidKernel/Logger.h"
14
#include <QApplication>
15
#include <QContextMenuEvent>
16
17
18
#include <QDir>
#include <QGridLayout>
#include <QIcon>
19
#include <QMouseEvent>
20
21
#include <QPushButton>
#include <QSpacerItem>
22
#include <QVBoxLayout>
David Fairbrother's avatar
David Fairbrother committed
23
#include <utility>
24

25
using Mantid::PythonInterface::GlobalInterpreterLock;
Samuel Jones's avatar
Samuel Jones committed
26
using MantidQt::Widgets::MplCpp::cycler;
27
using MantidQt::Widgets::MplCpp::FigureCanvasQt;
28

29
30
namespace {
const char *ACTIVE_CURVE_FORMAT = "k-";
31
const char *STORED_LINE_COLOR_CYCLE = "bgrcmyk";
32
33
const char *LIN_SCALE_NAME = "linear";
const char *LOG_SCALE_NAME = "symlog";
34
35
36
Mantid::Kernel::Logger g_log("MiniPlotMpl");

QPushButton *createHomeButton() {
Samuel Jones's avatar
Samuel Jones committed
37
38
  using MantidQt::Widgets::Common::Python::NewRef;
  using MantidQt::Widgets::Common::Python::Object;
39
40
41

  auto mpl(NewRef(PyImport_ImportModule("matplotlib")));
  QDir dataPath(TO_CSTRING(Object(mpl.attr("get_data_path")()).ptr()));
42
43
44
45
46
47
48
  dataPath.cd("images");
  QIcon icon(dataPath.absoluteFilePath("home.png"));
  auto iconSize(icon.availableSizes().front());
  auto button = new QPushButton(icon, "");
  button->setMaximumSize(iconSize + QSize(5, 5));
  return button;
}
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

/**
 * Check if size(X)==size(Y) and both are not empty
 * @param x A reference to the X data vector
 * @param y A reference to the Y data vector
 * @return True if a warning was produced, false otherwise
 */
bool warnDataInvalid(const std::vector<double> &x,
                     const std::vector<double> &y) {
  if (x.size() != y.size()) {
    g_log.warning(std::string(
        "setData(): X/Y size mismatch! X=" + std::to_string(x.size()) +
        ", Y=" + std::to_string(y.size())));
    return true;
  }
  if (x.empty()) {
    g_log.warning("setData(): X & Y arrays are empty!");
    return true;
  }
  return false;
}

71
72
} // namespace

73
74
75
76
namespace MantidQt {
namespace MantidWidgets {

/**
77
 * Construct a blank miniplot with a single subplot
78
79
 * @param parent A pointer to its parent widget
 */
80
MiniPlotMpl::MiniPlotMpl(QWidget *parent)
81
82
    : QWidget(parent), m_canvas(new FigureCanvasQt(111)),
      m_homeBtn(createHomeButton()), m_lines(), m_peakLabels(),
83
      m_colorCycler(cycler("color", STORED_LINE_COLOR_CYCLE)), m_xunit(),
84
      m_activeCurveLabel(), m_storedCurveLabels(), m_zoomTool(m_canvas),
85
      m_mousePressPt() {
86
87
88
89
90
91
92
93
94
95
  auto plotLayout = new QGridLayout(this);
  plotLayout->setContentsMargins(0, 0, 0, 0);
  plotLayout->setSpacing(0);
  // We intentionally place the canvas and home button in the same location
  // in the grid layout so that they overlap and take up less space.
  plotLayout->addWidget(m_canvas, 0, 0);
  plotLayout->addWidget(m_homeBtn, 0, 0, Qt::AlignLeft | Qt::AlignBottom);
  setLayout(plotLayout);

  // Capture mouse events destined for the plot canvas
96
  m_canvas->installEventFilterToMplCanvas(this);
97
98
  // Mouse events cause zooming by default. See mouseReleaseEvent
  // for exceptions
99
  m_zoomTool.enableZoom(true);
100
  connect(m_homeBtn, SIGNAL(clicked()), this, SLOT(zoomOutOnPlot()));
101
102
}

103
104
105
106
107
108
109
/**
 * Set data and metadata for a new curve
 * @param x The X-axis data
 * @param y The Y-axis data
 * @param xunit The X unit a label
 * @param curveLabel A label for the curve data
 */
110
void MiniPlotMpl::setData(std::vector<double> x, std::vector<double> y,
111
112
113
114
                          QString xunit, QString curveLabel) {
  if (warnDataInvalid(x, y))
    return;

115
  clearCurve();
116
  auto axes = m_canvas->gca();
117
  // plot automatically calls "scalex=True, scaley=True"
118
119
  m_lines.emplace_back(
      axes.plot(std::move(x), std::move(y), ACTIVE_CURVE_FORMAT));
David Fairbrother's avatar
David Fairbrother committed
120
  m_activeCurveLabel = std::move(curveLabel);
121
  setXLabel(std::move(xunit));
122
123
124
125
126
  // If the current axis limits can fit the data then matplotlib
  // won't change the axis scale. If the intensity of different plots
  // is very different we need ensure the scale is tight enough to
  // see newer plots so we force a recalculation from the data
  axes.relim();
127
128
129
  replot();
}

130
131
132
133
134
135
136
137
138
/**
 * Se the X unit label on the axis
 * @param xunit A string giving the X unit
 */
void MiniPlotMpl::setXLabel(QString xunit) {
  m_canvas->gca().setXLabel(xunit.toLatin1().constData());
  m_xunit = std::move(xunit);
}

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
 * Add a label to mark a peak to the plot
 * @param peakMarker A pointer to a PeakMarker2D object defining
 * the marker added to the instrument widget
 */
void MiniPlotMpl::addPeakLabel(const PeakMarker2D *peakMarker) {
  if (m_xunit.isEmpty())
    return;
  const auto &peak = peakMarker->getPeak();
  double peakX(0.0);
  if (m_xunit == "dSpacing") {
    peakX = peak.getDSpacing();
  } else if (m_xunit == "Wavelength") {
    peakX = peak.getWavelength();
  } else {
    peakX = peak.getTOF();
  }
156
157
158
  double ymax(1.0), _;
  std::tie(_, ymax) = m_canvas->gca().getYLim();
  // arbitrarily place the label at 85% of the y-axis height
159
160
  const double peakY = 0.85 * ymax;
  const QString label(peakMarker->getLabel());
161
162
  m_peakLabels.emplace_back(
      m_canvas->gca().text(peakX, peakY, label, "center"));
163
164
165
166
167
}

/**
 * Clear all peak labels from the canvas
 */
168
169
170
171
172
173
void MiniPlotMpl::clearPeakLabels() {
  for (auto &label : m_peakLabels) {
    label.remove();
  }
  m_peakLabels.clear();
}
174

175
176
177
178
179
180
181
182
183
184
185
186
/**
 * @return True if an active curve exists
 */
bool MiniPlotMpl::hasCurve() const { return !m_activeCurveLabel.isEmpty(); }

/**
 * Store the active curve so it is not overridden
 * by future plotting. The curve's color is updated using the color cycler
 */
void MiniPlotMpl::store() {
  m_storedCurveLabels.append(m_activeCurveLabel);
  m_activeCurveLabel.clear();
187
188
189
  // lock required when the dict returned by cycler iterator
  // is destroyed
  GlobalInterpreterLock lock;
190
191
192
193
194
195
196
197
  m_lines.back().set(m_colorCycler());
}

/**
 * @return True if the plot has stored curves
 */
bool MiniPlotMpl::hasStored() const { return !m_storedCurveLabels.isEmpty(); }

198
199
200
201
202
203
204
205
206
/**
 * Remove the stored curve with the given label. If the label is not found this
 * does nothing
 * @param label A string label for a curve
 */
void MiniPlotMpl::removeCurve(const QString &label) {
  auto labelIndex = m_storedCurveLabels.indexOf(label);
  if (labelIndex < 0)
    return;
207
  m_storedCurveLabels.removeAt(labelIndex);
208
  m_lines.erase(std::next(std::begin(m_lines), labelIndex));
209
210
  m_canvas->gca().relim();
  m_canvas->gca().autoscaleView();
211
212
213
214
215
216
217
218
219
220
221
}

/**
 * Retrieve the color of the curve with the given label
 * @param label
 * @return A QColor defining the color of the curve
 */
QColor MiniPlotMpl::getCurveColor(const QString &label) const {
  auto labelIndex = m_storedCurveLabels.indexOf(label);
  if (labelIndex < 0)
    return QColor();
222
  auto lineIter = std::next(std::begin(m_lines), labelIndex);
Martyn Gigg's avatar
Martyn Gigg committed
223
  return lineIter->getColor();
224
225
}

226
227
228
229
230
231
232
/**
 * @return True if the Y scale is logarithmic
 */
bool MiniPlotMpl::isYLogScale() const {
  return m_canvas->gca().getYScale() == LOG_SCALE_NAME;
}

233
234
235
236
237
238
239
240
241
/**
 * Redraws the canvas
 */
void MiniPlotMpl::replot() { m_canvas->draw(); }

/**
 * Remove the active curve, keeping any stored curves
 */
void MiniPlotMpl::clearCurve() {
242
243
244
245
  // setData places the latest curve at the back of the vector
  if (hasCurve()) {
    m_lines.pop_back();
  }
246
  m_activeCurveLabel.clear();
247
  clearPeakLabels();
248
249
}

250
251
252
253
254
255
256
257
258
259
260
261
/**
 * Set the Y scale to logarithmic
 */
void MiniPlotMpl::setYLogScale() { m_canvas->gca().setYScale(LOG_SCALE_NAME); }

/**
 * Set the Y scale to linear
 */
void MiniPlotMpl::setYLinearScale() {
  m_canvas->gca().setYScale(LIN_SCALE_NAME);
}

262
263
264
265
/**
 * Clear all artists from the canvas
 */
void MiniPlotMpl::clearAll() {
266
267
  // active curve, labels etc
  clearCurve();
268
  // any stored curves and labels
269
  m_lines.clear();
270
  m_storedCurveLabels.clear();
271
  replot();
272
}
273

274
275
276
277
278
279
280
281
/**
 * Filter events from the underlying matplotlib canvas
 * @param watched A pointer to the object being watched
 * @param evt A pointer to the generated event
 * @return True if the event was filtered, false otherwise
 */
bool MiniPlotMpl::eventFilter(QObject *watched, QEvent *evt) {
  Q_UNUSED(watched);
282
  bool stopEvent{false};
283
  switch (evt->type()) {
284
  case QEvent::ContextMenu:
285
286
287
    // handled by mouse press events below as we need to
    // stop the canvas getting mouse events in some circumstances
    // to disable zooming
288
    stopEvent = true;
289
    break;
290
  case QEvent::MouseButtonPress:
291
292
293
294
295
    stopEvent = handleMousePressEvent(static_cast<QMouseEvent *>(evt));
    break;
  case QEvent::MouseButtonRelease:
    stopEvent = handleMouseReleaseEvent(static_cast<QMouseEvent *>(evt));
    break;
296
  default:
297
    break;
298
  }
299
  return stopEvent;
300
301
}

302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
/**
 * Handler called when the event filter recieves a mouse press event
 * @param evt A pointer to the event
 * @return True if the event propagation should be stopped, false otherwise
 */
bool MiniPlotMpl::handleMousePressEvent(QMouseEvent *evt) {
  bool stopEvent(false);
  // right-click events are reserved for the context menu
  // show when the mouse click is released
  if (evt->buttons() & Qt::LeftButton) {
    m_mousePressPt = evt->pos();
  } else if (evt->buttons() & Qt::RightButton) {
    m_mousePressPt = QPoint();
    stopEvent = true;
  }
  return stopEvent;
}

/**
 * Handler called when the event filter recieves a mouse release event
 * @param evt A pointer to the event
 * @return True if the event propagation should be stopped, false otherwise
 */
bool MiniPlotMpl::handleMouseReleaseEvent(QMouseEvent *evt) {
  bool stopEvent(false);
  if (evt->button() == Qt::LeftButton) {
    auto mouseReleasePt = evt->pos();
    // A click and release at the same point implies picking on the canvas
    // and not a zoom so stop matplotlib getting hold it it
    if (mouseReleasePt == m_mousePressPt) {
      const auto dataCoords = m_canvas->toDataCoords(mouseReleasePt);
      emit clickedAt(dataCoords.x(), dataCoords.y());
    }
  } else if (evt->button() == Qt::RightButton) {
    stopEvent = true;
    emit showContextMenu();
  }
  return stopEvent;
}

342
/**
343
 * Wire to the home button click
344
 */
345
void MiniPlotMpl::zoomOutOnPlot() { m_zoomTool.zoomOut(); }
346

347
348
} // namespace MantidWidgets
} // namespace MantidQt