Horizontal waterfall plot
Hi,
I'm just getting my feet wet with Qwt and the first thing I've attempted is a waterfall plot that shows frequency bins (from an FFT) on the vertical axis, time horizontally and represents intensity as Color. From reading a few posts here and following the rasterview and cpuplot examples I've got the orientation and scaling of the data within the plot... I'm using QwtMatrixRasterData (like in the rasterview example) with newest records at the end (like a vertical reverse waterfall) but Displaying the newest data on the rightmost vertical.
That works fine except that I only get as many records as there are frequency bins.... That is it only plots a square matrix, stopping at the number of frequency bins instead of the number of records in the data. I'm not quite sure I understand how (for which coordinates) the value method is called because it seems to only get called for that many records.
Is it possible to re-orient the data in the plot in this way using QwtMatrixRasterData just re-implementing value(), or will I have to implement my own QwtRasterData? Or can anyone suggest another type of plot item that would work for this?
Here's my code (so far just a small self-contained example but this will eventually be getting updates around 4 times per sec):
-----------------------------------------specplot.h --------------------------------------------------
#ifndef SPECPLOT_H
#define SPECPLOT_H
#include <qwt_plot.h>
class QwtPlotSpectrogram;
class SpectrumPlot: public QwtPlot
{
Q_OBJECT
public:
SpectrumPlot( QWidget * = NULL );
private:
QwtPlotSpectrogram *d_spectrogram;
};
#endif // SPECPLOT_H
-----------------------------------------specplot.cpp --------------------------------------------------
#include <QApplication>
#include <QVBoxLayout>
#include <QTime>
#include <qwt_plot_layout.h>
#include <qwt_scale_draw.h>
#include <qwt_scale_widget.h>
#include <qwt_plot_canvas.h>
#include <qwt_color_map.h>
#include <qwt_plot_spectrogram.h>
#include <qwt_matrix_raster_data.h>
#include <qwt_plot_magnifier.h>
#include <qwt_plot_panner.h>
#include <iostream>
#include "specplot.h"
#define HISTORY 60 // seconds
//================================================== ============================
class RasterData: public QwtMatrixRasterData
{
public:
RasterData() :
numBins(10),
numRecs(36)
{
const double matrix[] =
{
100,100,100,100,100,100,100,100,100,100,
00, 00, 00, 00, 00, 00, 00, 00, 00, 00,
63, 69, 98, 79, 75, 75, 73, 71, 69, 70,
63, 63, 99, 77, 76, 73, 71, 69, 68, 70,
62, 63, 96, 75, 75, 70, 66, 65, 66, 66,
60, 60, 95, 75, 76, 72, 67, 65, 66, 66,
60, 60, 91, 70, 72, 68, 67, 67, 67, 68,
61, 60, 91, 65, 65, 62, 64, 65, 66, 66,
64, 63, 91, 63, 62, 63, 65, 66, 66, 68,
64, 63, 90, 60, 61, 61, 63, 65, 66, 67,
63, 61, 90, 63, 63, 62, 64, 66, 65, 66,
60, 60, 90, 60, 61, 61, 63, 65, 65, 66,
68, 71, 95, 80, 76, 71, 71, 71, 70, 69,
64, 65, 99, 75, 72, 69, 68, 68, 67, 67,
60, 60, 90, 65, 66, 63, 64, 65, 66, 66,
64, 67, 92, 82, 81, 73, 73, 76, 76, 76,
72, 76, 98, 87, 86, 79, 79, 82, 79, 76,
70, 74, 96, 83, 82, 78, 79, 80, 78, 77,
67, 75, 96, 93, 91, 88, 89, 83, 77, 76,
64, 77, 98, 93, 91, 87, 84, 77, 76, 77,
70, 74, 92, 88, 86, 83, 80, 74, 72, 75,
68, 73, 99, 85, 82, 79, 79, 75, 72, 70,
60, 60, 95, 70, 72, 70, 68, 69, 68, 69,
60, 60, 90, 63, 64, 65, 65, 65, 66, 66,
60, 60, 90, 62, 63, 64, 65, 66, 66, 67,
60, 60, 90, 60, 61, 63, 66, 67, 67, 66,
60, 60, 90, 65, 64, 62, 64, 66, 66, 66,
60, 60, 90, 64, 64, 63, 64, 66, 66, 66,
60, 60, 90, 60, 61, 61, 63, 65, 66, 66,
60, 60, 90, 60, 61, 61, 63, 65, 65, 65,
60, 60, 90, 63, 65, 63, 64, 65, 66, 66,
60, 60, 90, 60, 62, 62, 63, 65, 66, 66,
60, 60, 90, 60, 60, 61, 63, 64, 65, 66,
60, 60, 90, 60, 61, 62, 63, 65, 66, 66,
60, 60, 90, 60, 60, 61, 63, 64, 65, 66,
100,100,100,100,100,100,100,100,100,100
};
QVector<double> values;
for ( uint i = 0; i < sizeof( matrix ) / sizeof( double ); i++ )
values += matrix[i];
std::cout << "Added " << std::to_string(values.size()/numBins) << " recs" << std::endl;
// fill up the values matrix to match the size of the plot...
// can I avoid this??
for ( int i = values.size(); i < (HISTORY*numBins); i++ )
values += -1;
std::cout << "Padded to " << std::to_string(values.size()/numBins) << " recs" << std::endl;
setValueMatrix( values, numBins );
setInterval( Qt::XAxis,
QwtInterval( 0, numBins, QwtInterval::ExcludeMaximum ) );
setInterval( Qt::YAxis,
QwtInterval( 0, HISTORY, QwtInterval::ExcludeMaximum ) );
setInterval( Qt::ZAxis, QwtInterval( 1.0, 100.0 ) );
//setResampleMode(ResampleMode::BilinearInterpolatio n);
setResampleMode(ResampleMode::NearestNeighbour);
}
// x = plot freq bin
// y = plot time
virtual double value( double x, double y ) const
{
double dataX, dataY;
// frequency bins are columns in data, rows on plot
dataX = y;
// time - newest on the right
dataY = numRecs - x;
return QwtMatrixRasterData::value( dataX , dataY);;
}
private:
const int numBins;
int numRecs;
};
//================================================== ============================
class ColorMap: public QwtLinearColorMap
{
public:
ColorMap():
QwtLinearColorMap( Qt::black, Qt::darkRed )
{
addColorStop( 0.2, Qt::blue );
addColorStop( 0.4, Qt::cyan );
addColorStop( 0.5, Qt::green);
addColorStop( 0.6, Qt::yellow );
addColorStop( 0.8, Qt::red );
}
};
//================================================== ============================
class TimeScaleDraw: public QwtScaleDraw
{
public:
TimeScaleDraw( const QTime &base ):
baseTime( base )
{
}
virtual QwtText label( double v ) const
{
QTime upTime = baseTime.addSecs( static_cast<int>( HISTORY - v ) );
return upTime.toString();
}
private:
QTime baseTime;
};
//================================================== ============================
SpectrumPlot::SpectrumPlot( QWidget *parent ):
QwtPlot( parent )
{
QwtPlotCanvas *canvas = new QwtPlotCanvas();
canvas->setBorderRadius( 0 );
setCanvas( canvas );
d_spectrogram = new QwtPlotSpectrogram();
d_spectrogram->setRenderThreadCount( 1 ); // 0 = use system specific thread count
d_spectrogram->setColorMap( new ColorMap() );
d_spectrogram->setData( new RasterData() );
d_spectrogram->attach( this );
d_spectrogram->setXAxis(QwtPlot::xBottom);
d_spectrogram->setYAxis(QwtPlot::yRight);
// Color scale map on left
const QwtInterval zInterval = d_spectrogram->data()->interval( Qt::ZAxis );
QwtScaleWidget *leftAxis = axisWidget( QwtPlot::yLeft );
leftAxis->setColorBarEnabled( true );
leftAxis->setColorBarWidth( 10 );
leftAxis->setColorMap( zInterval, new ColorMap() );
setAxisScale( QwtPlot::yLeft, zInterval.minValue(), zInterval.maxValue() );
enableAxis( QwtPlot::yLeft, true );
enableAxis( QwtPlot::yRight );
plotLayout()->setAlignCanvasToScales( true );
setAxisTitle( QwtPlot::xBottom, "Time [h:m:s]" );
setAxisScaleDraw( QwtPlot::xBottom,
new TimeScaleDraw( QTime::currentTime() ) );
setAxisScale( QwtPlot::xBottom, HISTORY, 0);
setAxisLabelRotation( QwtPlot::xBottom, -50.0 );
setAxisLabelAlignment( QwtPlot::xBottom, Qt::AlignLeft | Qt::AlignBottom );
setAxisScale( QwtPlot::yRight, 0.0, 10.0 );
setAxisMaxMinor( QwtPlot::yRight, 10 );
QwtPlotMagnifier *magnifier = new QwtPlotMagnifier( canvas );
magnifier->setAxisEnabled( QwtPlot::yLeft, false );
magnifier->setAxisEnabled( QwtPlot::yRight, false );
QwtPlotPanner *panner = new QwtPlotPanner( canvas );
panner->setAxisEnabled( QwtPlot::yLeft, false );
panner->setAxisEnabled( QwtPlot::yRight, false );
}
//== MAIN ================================================== ==================
int main( int argc, char **argv )
{
QApplication a( argc, argv );
QWidget vBox;
vBox.setWindowTitle( "Horizontal Waterfall" );
SpectrumPlot *plot = new SpectrumPlot(&vBox);
QVBoxLayout *layout = new QVBoxLayout(&vBox);
layout->addWidget(plot);
vBox.resize( 800, 600 );
vBox.show();
return a.exec();
}
Re: Horizontal waterfall plot
It's been a while but I'm working on this again and I've made some progress but I'm getting some confusing results when I update the data.
I ended up subclassing QwtRasterData and making a class very similar to QwtMatrixRasterData, but with a method like this:
Code:
void OslSpectrumRasterData::addSpectrumRecord(const uint8_t * const values)
{
std::copy(values, values + p_data->numBins, std::back_inserter(p_data->values));
// extend the plot time interval to include the new data
const QwtInterval timeIval = interval(Qt::XAxis);
QwtInterval newIval(timeIval.minValue(), timeIval.maxValue() + p_data->delta_t);
setInterval(Qt::XAxis, newIval);
}
It its value() method, the coords are translated to the integer indexes in the raw data:
Code:
uint32_t row = uint32_t ( (f - fInterval.minValue()) / p_data->delta_f );
uint32_t col = uint32_t ( (t_flip - tInterval.minValue()) / p_data->delta_t );
Then if there are more data columns than there are pixels I average the surrounding pixels.
This works fine until I zoom out a little on the time (horizontal) axis. The plot columns don't seem to be all the same width... When I zoom out to the point where a single data column is only a few pixels wide, certain columns in the plot make that data column a pixel wider than the others. If a column is only one pixel wide it is sometimes is rendered as pixel wide, sometimes two depending on where it is on the plot.
I'm not sure why this is happening... maybe the way I'm checking if there are more data columns than pixels is wrong... When the axis is scaled I do this:
Code:
connect(magnifier, &TimeScaleZoom::timeScaleChanged,
[this, plotCanvas](double lengthSeconds)
{
double colsInDisplay = lengthSeconds/UPDATE_RATE;
double rpp = (double)colsInDisplay/(double)plotCanvas->contentsRect().width();
p_rasterData->setRecsPerPixel(std::ceil(rpp));
});
And then in the value() method, I do the time average only if recsPerPixel > 1 Is this a valid way to check?
Or is this simply because of rounding error in the time values requested for each pixel not lining up with my data? Is there some way to get around that?