MiniPlotQwt.cpp 14.1 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/MiniPlotQwt.h"
8
#include "MantidKernel/Logger.h"
Roman Tolchenov's avatar
Roman Tolchenov committed
9
#include "MantidQtWidgets/InstrumentView/PeakMarker2D.h"
10

11
#include "MantidQtWidgets/Plotting/Qwt/qwt_compat.h"
LamarMoore's avatar
LamarMoore committed
12
#include <qwt_plot_canvas.h>
13
#include <qwt_plot_curve.h>
LamarMoore's avatar
LamarMoore committed
14
#include <qwt_plot_zoomer.h>
15
16
#include <qwt_scale_div.h>
#include <qwt_scale_draw.h>
LamarMoore's avatar
LamarMoore committed
17
#include <qwt_scale_engine.h>
18
19
#include <qwt_scale_widget.h>

LamarMoore's avatar
LamarMoore committed
20
21
#include <QContextMenuEvent>
#include <QFont>
22
23
24
25
26
#include <QFontMetrics>
#include <QMouseEvent>
#include <QPainter>

#include <cmath>
David Fairbrother's avatar
David Fairbrother committed
27
#include <utility>
28

29
30
31
32
namespace {
Mantid::Kernel::Logger g_log("MiniPlotQwt");
}

33
34
35
namespace MantidQt {
namespace MantidWidgets {

Martyn Gigg's avatar
Martyn Gigg committed
36
MiniPlotQwt::MiniPlotQwt(QWidget *parent) : QwtPlot(parent), m_curve(nullptr), m_xUnits("") {
37
  const QFont &font = parent->font();
38
39
  setAxisFont(QwtPlot::xBottom, font);
  setAxisFont(QwtPlot::yLeft, font);
40
  setYAxisLabelRotation(-90);
41
42
43
44
45
  QwtText dummyText;
  dummyText.setFont(font);
  setAxisTitle(xBottom, dummyText);
  canvas()->setCursor(Qt::ArrowCursor);
  setContextMenuPolicy(Qt::DefaultContextMenu);
Martyn Gigg's avatar
Martyn Gigg committed
46
47
  m_zoomer = new QwtPlotZoomer(QwtPlot::xBottom, QwtPlot::yLeft, QwtPicker::DragSelection | QwtPicker::CornerToCorner,
                               QwtPicker::AlwaysOff, canvas());
48
49
  m_zoomer->setRubberBandPen(QPen(Qt::black));
  QList<QColor> colors;
Martyn Gigg's avatar
Martyn Gigg committed
50
51
52
  m_colors << Qt::red << Qt::green << Qt::blue << Qt::cyan << Qt::magenta << Qt::yellow << Qt::gray;
  m_colors << Qt::darkRed << Qt::darkGreen << Qt::darkBlue << Qt::darkCyan << Qt::darkMagenta << Qt::darkYellow
           << Qt::darkGray;
53
54
55
  m_colorIndex = 0;
  m_x0 = 0;
  m_y0 = 0;
56
57
58
59

  // Initial scales so the plot looks sensible
  setXScale(0, 1);
  setYScale(-1.2, 1.2);
60
61
62
}

/**
LamarMoore's avatar
LamarMoore committed
63
64
 * Destructor.
 */
65
MiniPlotQwt::~MiniPlotQwt() { clearAll(); }
66

67
68
69
70
71
/**
 * Set the X label of the plot
 * @param xunit
 */
void MiniPlotQwt::setXLabel(QString xunit) {
David Fairbrother's avatar
David Fairbrother committed
72
  m_xUnits = std::move(xunit);
73
74
75
  this->setAxisTitle(xBottom, m_xUnits);
}

76
/**
LamarMoore's avatar
LamarMoore committed
77
78
79
80
 * Set the scale of the horizontal axis
 * @param from :: Minimum value
 * @param to :: Maximum value
 */
81
void MiniPlotQwt::setXScale(double from, double to) {
82
83
84
85
86
87
88
89
90
91
92
93
94
  QFontMetrics fm(axisFont(QwtPlot::xBottom));
  int n = from != 0.0 ? abs(static_cast<int>(floor(log10(fabs(from))))) : 0;
  int n1 = to != 0.0 ? abs(static_cast<int>(floor(log10(fabs(to))))) : 0;
  if (n1 > n)
    n = n1;
  n += 4;
  // approxiamte width of a tick label in pixels
  int labelWidth = n * fm.width("0");
  // calculate number of major ticks
  int nMajorTicks = this->width() / labelWidth;
  if (nMajorTicks > 6)
    nMajorTicks = 6;
  // try creating a scale
Martyn Gigg's avatar
Martyn Gigg committed
95
  const QwtScaleDiv div = axisScaleEngine(QwtPlot::xBottom)->divideScale(from, to, nMajorTicks, nMajorTicks);
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
  // Major ticks are placed at round numbers so the first or last tick could be
  // missing making
  // scale look ugly. Trying to fix it if possible
  bool rescaled = false;
  // get actual tick positions
  const QwtValueList &ticks = div.ticks(QwtScaleDiv::MajorTick);
  if (!ticks.empty() && ticks.size() < nMajorTicks) {
    // how much first tick is shifted from the lower bound
    double firstShift = ticks.front() - div.lBound();
    // how much last tick is shifted from the upper bound
    double lastShift = div.hBound() - ticks.back();
    // range of the scale
    double range = fabs(div.hBound() - div.lBound());
    // we say that 1st tick is missing if first tick is father away from its end
    // of the scale
    // than the last tick is from its end
    bool isFirstMissing = fabs(firstShift) > fabs(lastShift);
    // if first tick is missing
    if (isFirstMissing) {
      // distance between nearest major ticks
      double tickSize = 0;
      if (ticks.size() == 1) {
        // guess the tick size in case of only one visible
        double tickLog = log10(firstShift);
        tickLog = tickLog > 0 ? ceil(tickLog) : floor(tickLog);
        tickSize = pow(10., tickLog);
      } else if (ticks.size() > 1) {
        // take the difference between the two first ticks
        tickSize = ticks[1] - ticks[0];
      }
      // claculate how much lower bound must be moved to make the missing tick
      // visible
      double shift = (ticks.front() - tickSize) - from;
      // if the shift is not very big rescale the axis
      if (fabs(shift / range) < 0.1) {
        from += shift;
        const QwtScaleDiv updatedDiv =
Martyn Gigg's avatar
Martyn Gigg committed
133
            axisScaleEngine(QwtPlot::xBottom)->divideScale(from, to, nMajorTicks, nMajorTicks);
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
        setAxisScaleDiv(xBottom, updatedDiv);
        rescaled = true;
      }
    } else // last tick is missing
    {
      // distance between nearest major ticks
      double tickSize = 0;
      if (ticks.size() == 1) {
        // guess the tick size in case of only one visible
        double tickLog = log10(lastShift);
        tickLog = tickLog > 0 ? ceil(tickLog) : floor(tickLog);
        tickSize = pow(10., tickLog);
      } else if (ticks.size() > 1) {
        // take the difference between the two first ticks
        tickSize = ticks[1] - ticks[0];
      }
      // claculate how much upper bound must be moved to make the missing tick
      // visible
      double shift = (ticks.back() + tickSize) - to;
      // if the shift is not very big rescale the axis
      if (fabs(shift / range) < 0.1) {
        to += shift;
        const QwtScaleDiv updatedDiv =
Martyn Gigg's avatar
Martyn Gigg committed
157
            axisScaleEngine(QwtPlot::xBottom)->divideScale(from, to, nMajorTicks, nMajorTicks);
158
159
160
161
162
163
164
165
166
167
168
169
170
        setAxisScaleDiv(xBottom, updatedDiv);
        rescaled = true;
      }
    }
  }

  if (!rescaled) {
    setAxisScaleDiv(xBottom, div);
  }
  m_zoomer->setZoomBase();
}

/**
LamarMoore's avatar
LamarMoore committed
171
172
173
174
 * Set the scale of the vertical axis
 * @param from :: Minimum value
 * @param to :: Maximum value
 */
175
void MiniPlotQwt::setYScale(double from, double to) {
176
177
178
179
180
181
182
  if (isYLogScale()) {
    if (from == 0 && to == 0) {
      from = 1;
      to = 10;
    } else {
      double yPositiveMin = to;
      QMap<QString, QwtPlotCurve *>::const_iterator cv = m_stored.begin();
183
      QwtPlotCurve *curve = nullptr;
184
185
186
187
188
      do {
        if (cv != m_stored.end()) {
          curve = cv.value();
          ++cv;
        } else if (curve == m_curve) {
189
          curve = nullptr;
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
          break;
        } else {
          curve = m_curve;
        }
        if (!curve)
          break;
        int n = curve->dataSize();
        for (int i = 0; i < n; ++i) {
          double y = curve->y(i);
          if (y > 0 && y < yPositiveMin) {
            yPositiveMin = y;
          }
        }
      } while (curve);
      from = yPositiveMin;
    }
  }
  setAxisScale(QwtPlot::yLeft, from, to);
  m_zoomer->setZoomBase();
}

/**
LamarMoore's avatar
LamarMoore committed
212
 * Set the data for the curve to display
213
214
 * @param x :: A vector of X values
 * @param y :: A vector of Y values
LamarMoore's avatar
LamarMoore committed
215
 * @param xUnits :: Units for the data
216
 * @param curveLabel :: A label for hthe die
LamarMoore's avatar
LamarMoore committed
217
 */
Martyn Gigg's avatar
Martyn Gigg committed
218
void MiniPlotQwt::setData(std::vector<double> x, std::vector<double> y, QString xunit, QString curveLabel) {
219
  if (x.empty()) {
220
221
222
    g_log.warning("setData(): X array is empty!");
    return;
  }
223
  if (y.empty()) {
224
225
226
227
    g_log.warning("setData(): Y array is empty!");
    return;
  }
  if (x.size() != y.size()) {
Martyn Gigg's avatar
Martyn Gigg committed
228
229
    g_log.warning(
        std::string("setData(): X/Y size mismatch! X=" + std::to_string(x.size()) + ", Y=" + std::to_string(y.size())));
230
231
    return;
  }
232

David Fairbrother's avatar
David Fairbrother committed
233
234
  m_xUnits = std::move(xunit);
  m_label = std::move(curveLabel);
235
236
237
238
  if (!m_curve) {
    m_curve = new QwtPlotCurve();
    m_curve->attach(this);
  }
239
240
  int dataSize = static_cast<int>(x.size());
  m_curve->setData(x.data(), y.data(), dataSize);
241
242
243
244
245
246
247
248
249
250
251
252
253
254
  setXScale(x[0], x[dataSize - 1]);
  double from = y[0];
  double to = from;
  for (int i = 0; i < dataSize; ++i) {
    const double &yy = y[i];
    if (yy < from)
      from = yy;
    if (yy > to)
      to = yy;
  }
  setYScale(from, to);
}

/**
LamarMoore's avatar
LamarMoore committed
255
256
 * Remove the curve. Rescale the axes if there are stored curves.
 */
257
void MiniPlotQwt::clearCurve() {
258
259
  // remove the curve
  if (m_curve) {
260
261
    m_curve->attach(nullptr);
    m_curve = nullptr;
262
263
264
265
266
267
268
269
270
271
272
273
  }
  clearPeakLabels();
  // if there are stored curves rescale axes to make them fully visible
  if (hasStored()) {
    QMap<QString, QwtPlotCurve *>::const_iterator curve = m_stored.begin();
    QwtDoubleRect br = (**curve).boundingRect();
    double xmin = br.left();
    double xmax = br.right();
    double ymin = br.top();
    double ymax = br.bottom();
    ++curve;
    for (; curve != m_stored.end(); ++curve) {
274
      br = (**curve).boundingRect();
275
276
277
278
279
280
281
282
283
284
285
286
287
288
      if (br.left() < xmin)
        xmin = br.left();
      if (br.right() > xmax)
        xmax = br.right();
      if (br.top() < ymin)
        ymin = br.top();
      if (br.bottom() > ymax)
        ymax = br.bottom();
    }
    setXScale(xmin, xmax);
    setYScale(ymin, ymax);
  }
}

289
void MiniPlotQwt::resizeEvent(QResizeEvent *e) {
290
291
292
293
294
  QwtPlot::resizeEvent(e);
  recalcAxisDivs();
}

/**
LamarMoore's avatar
LamarMoore committed
295
296
 * Recalculate axis divisions to make sure that tick labels don't overlap
 */
297
void MiniPlotQwt::recalcAxisDivs() {
298
299
300
301
302
  recalcXAxisDivs();
  recalcYAxisDivs();
}

/**
LamarMoore's avatar
LamarMoore committed
303
304
 * Recalculate x-axis divisions to make sure that tick labels don't overlap
 */
305
void MiniPlotQwt::recalcXAxisDivs() {
306
307
308
309
310
311
312
  const QwtScaleDiv *div0 = axisScaleDiv(QwtPlot::xBottom);
  double from = div0->lBound();
  double to = div0->hBound();
  setXScale(from, to);
}

/**
LamarMoore's avatar
LamarMoore committed
313
314
 * Recalculate y-axis divisions to make sure that tick labels don't overlap
 */
315
void MiniPlotQwt::recalcYAxisDivs() {
316
317
318
319
320
321
  const QwtScaleDiv *div0 = axisScaleDiv(QwtPlot::yLeft);
  double from = div0->lBound();
  double to = div0->hBound();
  setYScale(from, to);
}

322
void MiniPlotQwt::contextMenuEvent(QContextMenuEvent *e) {
323
324
325
326
  // context menu will be handled with mouse events
  e->accept();
}

327
void MiniPlotQwt::mousePressEvent(QMouseEvent *e) {
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
  if (e->buttons() & Qt::RightButton) {
    if (m_zoomer->zoomRectIndex() == 0) {
      e->accept();
      // plot owner will display and process context menu
      emit showContextMenu();
    }
    return;
  }
  if (e->buttons() & Qt::LeftButton) {
    e->accept();
    m_x0 = e->x();
    m_y0 = e->y();
  }
}

343
void MiniPlotQwt::mouseReleaseEvent(QMouseEvent *e) {
344
345
  if (e->button() == Qt::LeftButton) {
    if (m_x0 == e->x() && m_y0 == e->y()) { // there were no dragging
Martyn Gigg's avatar
Martyn Gigg committed
346
      emit clickedAt(invTransform(xBottom, e->x() - canvas()->x()), invTransform(yLeft, e->y() - canvas()->y()));
347
348
349
350
    }
  }
}

Martyn Gigg's avatar
Martyn Gigg committed
351
void MiniPlotQwt::setYAxisLabelRotation(double degrees) { axisScaleDraw(yLeft)->setLabelRotation(degrees); }
352
353

/**
LamarMoore's avatar
LamarMoore committed
354
355
 * Set the log scale on the y axis
 */
356
void MiniPlotQwt::setYLogScale() {
357
358
359
  const QwtScaleDiv *div = axisScaleDiv(QwtPlot::yLeft);
  double from = div->lBound();
  double to = div->hBound();
360
  auto *logEngine = new QwtLog10ScaleEngine();
361
362
363
364
365
366
367
  setAxisScaleEngine(yLeft, logEngine);
  setYScale(from, to);
  recalcYAxisDivs();
  replot();
}

/**
LamarMoore's avatar
LamarMoore committed
368
369
 * Set the linear scale on the y axis
 */
370
void MiniPlotQwt::setYLinearScale() {
371
  auto *engine = new QwtLinearScaleEngine();
372
373
374
375
376
  setAxisScaleEngine(yLeft, engine);
  replot();
}

/**
LamarMoore's avatar
LamarMoore committed
377
 * Add new peak label
378
 * @param marker :: A pointer to a PeakLabel, becomes owned by MiniPlotQwt
LamarMoore's avatar
LamarMoore committed
379
 */
380
void MiniPlotQwt::addPeakLabel(const PeakMarker2D *marker) {
381
  auto *label = new PeakLabel(marker, this);
382
383
384
385
386
  label->attach(this);
  m_peakLabels.append(label);
}

/**
LamarMoore's avatar
LamarMoore committed
387
388
 * Removes all peak labels.
 */
389
void MiniPlotQwt::clearPeakLabels() {
390
391
392
393
394
395
396
397
  foreach (PeakLabel *label, m_peakLabels) {
    label->detach();
    delete label;
  }
  m_peakLabels.clear();
}

/**
LamarMoore's avatar
LamarMoore committed
398
399
 * Returns true if the current curve isn't NULL
 */
400
bool MiniPlotQwt::hasCurve() const { return m_curve != nullptr; }
401
402

/**
LamarMoore's avatar
LamarMoore committed
403
404
 * Store current curve.
 */
405
void MiniPlotQwt::store() {
406
407
408
409
410
411
  if (m_curve) {
    removeCurve(m_label);
    m_stored.insert(m_label, m_curve);
    m_curve->setPen(QPen(m_colors[m_colorIndex]));
    ++m_colorIndex;
    m_colorIndex %= m_colors.size();
412
    m_curve = nullptr;
413
414
415
416
417
    m_label = "";
  }
}

/**
LamarMoore's avatar
LamarMoore committed
418
419
 * Returns true if there are some stored curves.
 */
420
bool MiniPlotQwt::hasStored() const { return !m_stored.isEmpty(); }
421

422
QStringList MiniPlotQwt::getLabels() const {
423
424
425
426
427
428
429
430
431
  QStringList out;
  QMap<QString, QwtPlotCurve *>::const_iterator it = m_stored.begin();
  for (; it != m_stored.end(); ++it) {
    out << it.key();
  }
  return out;
}

/**
LamarMoore's avatar
LamarMoore committed
432
433
434
 * Return the colour of a stored curve.
 * @param label :: The label of that curve.
 */
435
QColor MiniPlotQwt::getCurveColor(const QString &label) const {
436
437
438
439
440
441
442
  if (m_stored.contains(label)) {
    return m_stored[label]->pen().color();
  }
  return Qt::black;
}

/**
LamarMoore's avatar
LamarMoore committed
443
444
445
 * Remove a stored curve.
 * @param label :: The label of a curve to remove.
 */
446
void MiniPlotQwt::removeCurve(const QString &label) {
447
448
449
450
451
452
453
454
455
  QMap<QString, QwtPlotCurve *>::iterator it = m_stored.find(label);
  if (it != m_stored.end()) {
    it.value()->detach();
    delete it.value();
    m_stored.erase(it);
  }
}

/**
LamarMoore's avatar
LamarMoore committed
456
457
 * Does the y axis have the log scale?
 */
458
bool MiniPlotQwt::isYLogScale() const {
459
  const QwtScaleEngine *engine = axisScaleEngine(yLeft);
460
  return dynamic_cast<const QwtLog10ScaleEngine *>(engine) != nullptr;
461
462
463
}

/**
LamarMoore's avatar
LamarMoore committed
464
465
 * Remove all displayable objects from the plot.
 */
466
void MiniPlotQwt::clearAll() {
467
468
469
470
471
472
473
474
475
476
477
478
479
480
  QMap<QString, QwtPlotCurve *>::const_iterator it = m_stored.begin();
  for (; it != m_stored.end(); ++it) {
    it.value()->detach();
    delete it.value();
  }
  m_stored.clear();
  clearPeakLabels();
  clearCurve();
  m_colorIndex = 0;
}

/* ---------------------------- PeakLabel --------------------------- */

/**
LamarMoore's avatar
LamarMoore committed
481
482
 * Draw PeakLabel on a plot
 */
Martyn Gigg's avatar
Martyn Gigg committed
483
484
void PeakLabel::draw(QPainter *painter, const QwtScaleMap &xMap, const QwtScaleMap &yMap,
                     const QRect &canvasRect) const {
485
486
  (void)yMap;
  double peakX;
487
  if (m_plot->getXUnits().isEmpty())
488
489
490
491
492
493
494
495
496
    return;
  if (m_plot->getXUnits() == "dSpacing") {
    peakX = m_marker->getPeak().getDSpacing();
  } else if (m_plot->getXUnits() == "Wavelength") {
    peakX = m_marker->getPeak().getWavelength();
  } else {
    peakX = m_marker->getPeak().getTOF();
  }
  int x = xMap.transform(peakX);
Martyn Gigg's avatar
Martyn Gigg committed
497
  int y = static_cast<int>(canvasRect.top() + m_marker->getLabelRect().height());
498
499
  painter->drawText(x, y, m_marker->getLabel());
  // std::cerr << x << ' ' << y << ' ' << m_marker->getLabel().toStdString() <<
500
  // '\n';
501
}
LamarMoore's avatar
LamarMoore committed
502
503
} // namespace MantidWidgets
} // namespace MantidQt