Add QChart to Python Chart (3h of 5)

Use legend to select which series are plotted by clicking to
show hide a series. Hover just shows a hover background and
does not temporarily isolate the series, this may be something
to consider for later.

Applies to scatter and line charts.
This commit is contained in:
Mark Liversedge
2020-02-28 14:31:50 +00:00
parent 21d1e071bf
commit 0ecfa0e67e
6 changed files with 285 additions and 170 deletions

View File

@@ -31,11 +31,16 @@ GenericLegendItem::GenericLegendItem(Context *context, QWidget *parent, QString
{
value=0;
enabled=true;
hasvalue=false;
// set height and width, gets reset when configchanges
configChanged(0);
// we want to track our own events - for hover and click
installEventFilter(this);
setMouseTracking(true);
// watch for changes...
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
@@ -45,8 +50,8 @@ GenericLegendItem::GenericLegendItem(Context *context, QWidget *parent, QString
void
GenericLegendItem::configChanged(qint32)
{
static const double gl_margin = 3 * dpiXFactor;
static const double gl_spacer = 3 * dpiXFactor;
static const double gl_margin = 5 * dpiXFactor;
static const double gl_spacer = 2 * dpiXFactor;
static const double gl_block = 7 * dpiXFactor;
static const double gl_linewidth = 1 * dpiXFactor;
@@ -62,7 +67,7 @@ GenericLegendItem::configChanged(qint32)
+ gl_spacer + fm.boundingRect(valuelabel).width() + gl_margin;
// maximum height of widget = margin + textheight + spacer + line
double height = gl_margin + fm.boundingRect(valuelabel).height() + gl_spacer + gl_linewidth;
double height = (gl_margin*2) + fm.boundingRect(valuelabel).height() + gl_spacer + gl_linewidth;
// now set geometry of widget
setFixedWidth(width);
@@ -70,15 +75,39 @@ GenericLegendItem::configChanged(qint32)
// calculate all the rects used by the painter now since static
blockrect = QRectF(gl_margin, gl_margin, gl_block, height-gl_margin);
linerect = QRectF(gl_margin+gl_block, height-gl_linewidth, width-gl_margin, gl_linewidth);
linerect = QRectF(gl_margin+gl_block, gl_spacer+height-gl_linewidth-(gl_margin*2), width-gl_block-(gl_margin*2), gl_linewidth);
namerect = QRectF(gl_margin + gl_block + gl_spacer, gl_margin, fm.boundingRect(name).width(), fm.boundingRect(name).height());
valuerect =QRectF(namerect.x() + namerect.width() + gl_spacer, gl_margin, fm.boundingRect(valuelabel).width(), fm.boundingRect(valuelabel).height());
hoverrect = QRectF(gl_block, 0, width-gl_block,height);
// redraw
update();
}
bool
GenericLegendItem::eventFilter(QObject *obj, QEvent *e)
{
if (obj != this) return false;
switch (e->type()) {
case QEvent::MouseButtonRelease: // for now just one event, but may do more later
{
if (underMouse()) {
enabled=!enabled;
if (!enabled) hasvalue=false;
emit clicked(name, enabled);
}
}
// fall through
default:
//fprintf(stderr, "event %d on %s\n", e->type(), name.toStdString().c_str()); fflush(stderr);
update();
break;
}
return false;
}
void
GenericLegendItem::paintEvent(QPaintEvent *)
{
@@ -90,8 +119,18 @@ GenericLegendItem::paintEvent(QPaintEvent *)
painter.setPen(Qt::NoPen);
painter.drawRect(0,0,geometry().width()-1, geometry().height()-1);
// block and line
painter.setBrush(QBrush(color));
// under mouse show
if (underMouse()) {
QColor mask=GCColor::invertColor(GColor(CPLOTBACKGROUND));
mask.setAlphaF(0.1);
painter.setBrush(mask);
painter.setPen(Qt::NoPen);
painter.drawRect(hoverrect);
}
// block and line - gray means disabled
if (enabled) painter.setBrush(QBrush(color));
else painter.setBrush(QBrush(Qt::gray));
painter.setPen(Qt::NoPen);
//painter.drawRect(blockrect);
painter.drawRect(linerect);
@@ -105,7 +144,8 @@ GenericLegendItem::paintEvent(QPaintEvent *)
string = Utils::removeDP(string);
// set pen to series color for now
painter.setPen(GCColor::invertColor(GColor(CPLOTBACKGROUND))); // use invert - usually black or white
if (enabled) painter.setPen(GCColor::invertColor(GColor(CPLOTBACKGROUND))); // use invert - usually black or white
else painter.setPen(Qt::gray);
painter.setFont(QFont());
// series
@@ -136,6 +176,9 @@ GenericLegend::addSeries(QString name, QAbstractSeries *series)
// lets see ya!
add->show();
// connect signals
connect(add, SIGNAL(clicked(QString,bool)), this, SIGNAL(clicked(QString,bool)));
}
void
@@ -153,6 +196,9 @@ GenericLegend::addX(QString name)
// remember the x axis
xname = name;
// we don't connect -- there is no such series, its a meta legend item
// NOPE: connect(add, SIGNAL(clicked(QString,bool)), this, SIGNAL(clicked(QString,bool)));
}
void

View File

@@ -50,11 +50,17 @@ class GenericLegendItem : public QWidget {
public:
GenericLegendItem(Context *context, QWidget *parent, QString name, QColor color);
Q_SIGNALS:
void clicked(QString name, bool enabled); // someone clicked on a legend and enabled/disabled it
protected:
bool eventFilter(QObject *, QEvent *e);
public slots:
void paintEvent(QPaintEvent *event);
void setValue(double p) { hasvalue=true; value=p; update(); } // set value to display
void noValue() { hasvalue=false; update(); } // no value to display
void setValue(double p) { if (enabled) { hasvalue=true; value=p; update(); } } // set value to display
void noValue() { if (enabled) { hasvalue=false; update(); } } // no value to display
void configChanged(qint32); // context changed
private:
@@ -63,10 +69,11 @@ class GenericLegendItem : public QWidget {
QColor color;
bool hasvalue;
bool enabled;
double value;
// geometry for painting fast / updated on config changes
QRectF blockrect, namerect, valuerect, linerect;
QRectF blockrect, namerect, valuerect, linerect, hoverrect;
};

View File

@@ -76,6 +76,7 @@ GenericPlot::GenericPlot(QWidget *parent, Context *context) : QWidget(parent), c
connect(selector, SIGNAL(hover(QPointF,QString,QAbstractSeries*)), legend, SLOT(hover(QPointF,QString,QAbstractSeries*)));
connect(selector, SIGNAL(unhover(QString)), legend, SLOT(unhover(QString)));
connect(selector, SIGNAL(unhoverx()), legend, SLOT(unhoverx()));
connect(legend, SIGNAL(clicked(QString,bool)), this, SLOT(setSeriesVisible(QString,bool)));
// config changed...
configChanged(0);
@@ -211,6 +212,23 @@ GenericPlot::configChanged(qint32)
qchart->setBackgroundPen(QPen(GColor(CPLOTMARKER)));
}
void
GenericPlot::setSeriesVisible(QString name, bool visible)
{
// find the curve
QAbstractSeries *series = curves.value(name, NULL);
// does it exist and did it change?
if (series && series->isVisible() != visible) {
// show/hide
series->setVisible(visible);
// tell selector we hid/show a series so it can respond.
selector->setSeriesVisible(name, visible);
}
}
bool
GenericPlot::initialiseChart(QString title, int type, bool animate)
{

View File

@@ -124,6 +124,10 @@ class GenericPlot : public QWidget {
static QColor seriesColor(QAbstractSeries* series);
public slots:
// do we want to see this series?
void setSeriesVisible(QString name, bool visible);
void configChanged(qint32);
// set chart settings

View File

@@ -64,6 +64,8 @@ void GenericSelectTool::paint(QPainter*painter, const QStyleOptionGraphicsItem *
// current position for each series - we only do first, coz only interested in x axis anyway
foreach(QAbstractSeries *series, host->qchart->series()) {
if (series->isVisible() == false) continue; // ignore invisble curves
// convert screen position to value for series
QPointF v = host->qchart->mapToValue(spos,series);
double miny=0;
@@ -96,7 +98,7 @@ void GenericSelectTool::paint(QPainter*painter, const QStyleOptionGraphicsItem *
QColor invert = GCColor::invertColor(GColor(CPLOTBACKGROUND));
painter->setBrush(invert);
painter->setPen(invert);
QRectF circle(0,0,5*dpiXFactor,5*dpiYFactor);
QRectF circle(0,0,gl_linemarker*dpiXFactor,gl_linemarker*dpiYFactor);
circle.moveCenter(pos);
painter->drawEllipse(circle);
painter->setBrush(Qt::NoBrush);
@@ -121,7 +123,7 @@ void GenericSelectTool::paint(QPainter*painter, const QStyleOptionGraphicsItem *
QColor invert = GCColor::invertColor(GColor(CPLOTBACKGROUND));
painter->setBrush(invert);
painter->setPen(invert);
QRectF circle(0,0,10*dpiXFactor,10*dpiYFactor);
QRectF circle(0,0,gl_scattermarker*dpiXFactor,gl_scattermarker*dpiYFactor);
circle.moveCenter(hoverpoint);
painter->drawEllipse(circle);
painter->setBrush(Qt::NoBrush);
@@ -131,6 +133,7 @@ void GenericSelectTool::paint(QPainter*painter, const QStyleOptionGraphicsItem *
// current position for each series
foreach(QAbstractSeries *series, host->qchart->series()) {
if (series->isVisible() == false) continue; // ignore invisble curves
// convert screen position to value for series
QPointF v = host->qchart->mapToValue(spos,series);
@@ -483,6 +486,8 @@ GenericSelectTool::moved(QPointF pos)
hoverseries = NULL;
foreach(QAbstractSeries *series, host->qchart->series()) {
if (series->isVisible() == false) continue; // ignore invisble curves
Quadtree *tree= host->quadtrees.value(series,NULL);
if (tree != NULL) {
@@ -539,7 +544,7 @@ GenericSelectTool::moved(QPointF pos)
double nearestx=-9999;
foreach(QAbstractSeries *series, host->qchart->series()) {
if (ignore.contains(series)) continue;
if (series->isVisible() == false || ignore.contains(series)) continue;
// get x value to search
double xvalue=host->qchart->mapToValue(spos,series).x();
@@ -672,193 +677,222 @@ GenericCalculator::finalise()
}
}
void
GenericSelectTool::setSeriesVisible(QString name, bool visible)
{
// a series got hidden or showm, so do whats needed.
QString selectionName = QString("%1_select").arg(name);
foreach(QAbstractSeries *x, host->qchart->series())
if (x->name() == selectionName)
x->setVisible(visible);
// as a special case we clear the hoverpoints.
// they will get redisplayed when the cursor moves
// but for now this is the quickes and simplest way
// to avoid artefacts
hoverpoints.clear();
hoverpoint=QPointF();
// hide/show and updatescenes may overlap/get out of sync
// so we update scene to be absolutely sure the scene
// reflects the current visible settings (and others later)
rectchanged = true;
updateScene();
}
// selector needs to update the chart for selections
void
GenericSelectTool::updateScene()
{
// is the selection active?
if (rectchanged) {
if (state != GenericSelectTool::INACTIVE && state != DRAGGING) {
// selection tool is active so set curves gray
// and create curves for highlighted points etc
QList<QAbstractSeries*> originallist=host->qchart->series();
foreach(QAbstractSeries *x, originallist) { // because we update it below (!)
// clear incidental state that gets reset when needed
stats.clear();
if (ignore.contains(x)) continue;
if (state != GenericSelectTool::INACTIVE && state != DRAGGING) {
// Run through all the curves, setting them gray
// selecting the points that fall into the selection
// and creating selection curves and stats for painting
// onto the plot
//
// We duplicate code by curve type (but not chart type)
// so we can e.g. put a scatter curve on a line chart
// and vice versa later.
// selection tool is active so set curves gray
// and create curves for highlighted points etc
QList<QAbstractSeries*> originallist=host->qchart->series();
foreach(QAbstractSeries *x, originallist) { // because we update it below (!)
switch(x->type()) {
case QAbstractSeries::SeriesTypeLine: {
QLineSeries *line = static_cast<QLineSeries*>(x);
if (x->isVisible() == false || ignore.contains(x)) continue;
// ignore empty series
if (line->count() < 1) continue;
// Run through all the curves, setting them gray
// selecting the points that fall into the selection
// and creating selection curves and stats for painting
// onto the plot
//
// We duplicate code by curve type (but not chart type)
// so we can e.g. put a scatter curve on a line chart
// and vice versa later.
// this will be used to plot selected points on the plot
QLineSeries *selection =NULL;
switch(x->type()) {
case QAbstractSeries::SeriesTypeLine: {
QLineSeries *line = static_cast<QLineSeries*>(x);
// the axes for the current series
QAbstractAxis *xaxis=NULL, *yaxis=NULL;
foreach (QAbstractAxis *ax, x->attachedAxes()) {
if (ax->orientation() == Qt::Vertical && yaxis==NULL) yaxis=ax;
if (ax->orientation() == Qt::Horizontal && xaxis==NULL) xaxis=ax;
}
// ignore empty series
if (line->count() < 1) continue;
if ((selection=static_cast<QLineSeries*>(selections.value(x, NULL))) == NULL) {
// this will be used to plot selected points on the plot
QLineSeries *selection =NULL;
selection = new QLineSeries();
// all of this curve cloning should be in a new method xxx todo
selection->setUseOpenGL(line->useOpenGL());
selection->setPen(line->pen());
if (line->useOpenGL())
selection->setColor(Qt::gray); // use opengl ignores changing colors
else {
selection->setColor(line->color());
static_cast<QLineSeries*>(x)->setColor(Qt::gray);
// the axes for the current series
QAbstractAxis *xaxis=NULL, *yaxis=NULL;
foreach (QAbstractAxis *ax, x->attachedAxes()) {
if (ax->orientation() == Qt::Vertical && yaxis==NULL) yaxis=ax;
if (ax->orientation() == Qt::Horizontal && xaxis==NULL) xaxis=ax;
}
selections.insert(x, selection);
ignore.append(selection);
// add after done all aesthetic for opengl snafus
host->qchart->addSeries(selection); // before adding data and axis
if ((selection=static_cast<QLineSeries*>(selections.value(x, NULL))) == NULL) {
// only do when creating it.
if (yaxis) selection->attachAxis(yaxis);
if (xaxis) selection->attachAxis(xaxis);
}
selection = new QLineSeries();
selection->setName(QString("%1_select").arg(line->name()));
// lets work out what range of values we need to be
// selecting is, reverse since possible to have a backwards
// rectangle in the selection tool
double minx=0,maxx=0;
minx =this->minx(x);
maxx =this->maxx(x);
if (maxx < minx) { double t=minx; minx=maxx; maxx=t; }
// all of this curve cloning should be in a new method xxx todo
selection->setUseOpenGL(line->useOpenGL());
selection->setPen(line->pen());
if (line->useOpenGL())
selection->setColor(Qt::gray); // use opengl ignores changing colors
else {
selection->setColor(line->color());
static_cast<QLineSeries*>(x)->setColor(Qt::gray);
}
selections.insert(x, selection);
ignore.append(selection);
//fprintf(stderr, "xaxis range %f-%f, yaxis range %f-%f, [%s] %d points to check\n", minx,maxx,miny,maxy,scatter->name().toStdString().c_str(), scatter->count());
// add after done all aesthetic for opengl snafus
host->qchart->addSeries(selection); // before adding data and axis
// add points to the selection curve and calculate as you go
QList<QPointF> points;
GenericCalculator calc;
calc.initialise();
calc.color = selection->color(); // should this go into constructor?! xxx todo
calc.xaxis = xaxis;
calc.yaxis = yaxis;
calc.series = line;
for(int i=0; i<line->count(); i++) {
QPointF point = line->at(i); // avoid deep copy
if (point.x() >= minx && point.x() <= maxx) {
if (!points.contains(point)) points << point; // avoid dupes
calc.addPoint(point);
// only do when creating it.
if (yaxis) selection->attachAxis(yaxis);
if (xaxis) selection->attachAxis(xaxis);
}
}
calc.finalise();
stats.insert(line, calc);
selection->clear();
if (points.count()) selection->append(points);
// lets work out what range of values we need to be
// selecting is, reverse since possible to have a backwards
// rectangle in the selection tool
double minx=0,maxx=0;
minx =this->minx(x);
maxx =this->maxx(x);
if (maxx < minx) { double t=minx; minx=maxx; maxx=t; }
}
break;
case QAbstractSeries::SeriesTypeScatter: {
//fprintf(stderr, "xaxis range %f-%f, yaxis range %f-%f, [%s] %d points to check\n", minx,maxx,miny,maxy,scatter->name().toStdString().c_str(), scatter->count());
QScatterSeries *scatter = static_cast<QScatterSeries*>(x);
// ignore empty series
if (scatter->count() < 1) continue;
// this will be used to plot selected points on the plot
QScatterSeries *selection =NULL;
// the axes for the current series
QAbstractAxis *xaxis=NULL, *yaxis=NULL;
foreach (QAbstractAxis *ax, x->attachedAxes()) {
if (ax->orientation() == Qt::Vertical && yaxis==NULL) yaxis=ax;
if (ax->orientation() == Qt::Horizontal && xaxis==NULL) xaxis=ax;
}
if ((selection=static_cast<QScatterSeries*>(selections.value(x, NULL))) == NULL) {
selection = new QScatterSeries();
// all of this curve cloning should be in a new method xxx todo
host->qchart->addSeries(selection); // before adding data and axis
selection->setUseOpenGL(scatter->useOpenGL());
if (selection->useOpenGL())
selection->setColor(Qt::gray); // use opengl ignores changing colors
else {
selection->setColor(scatter->color());
static_cast<QScatterSeries*>(x)->setColor(Qt::gray);
// add points to the selection curve and calculate as you go
QList<QPointF> points;
GenericCalculator calc;
calc.initialise();
calc.color = selection->color(); // should this go into constructor?! xxx todo
calc.xaxis = xaxis;
calc.yaxis = yaxis;
calc.series = line;
for(int i=0; i<line->count(); i++) {
QPointF point = line->at(i); // avoid deep copy
if (point.x() >= minx && point.x() <= maxx) {
if (!points.contains(point)) points << point; // avoid dupes
calc.addPoint(point);
}
}
selection->setMarkerSize(scatter->markerSize());
selection->setMarkerShape(scatter->markerShape());
selection->setPen(scatter->pen());
selections.insert(x, selection);
ignore.append(selection);
calc.finalise();
stats.insert(line, calc);
selection->clear();
if (points.count()) selection->append(points);
// only do when creating it.
if (yaxis) selection->attachAxis(yaxis);
if (xaxis) selection->attachAxis(xaxis);
}
// lets work out what range of values we need to be
// selecting is, reverse since possible to have a backwards
// rectangle in the selection tool
double miny=0,maxy=0,minx=0,maxx=0;
miny =this->miny(x);
maxy =this->maxy(x);
if (maxy < miny) { double t=miny; miny=maxy; maxy=t; }
minx =this->minx(x);
maxx =this->maxx(x);
if (maxx < minx) { double t=minx; minx=maxx; maxx=t; }
//fprintf(stderr, "xaxis range %f-%f, yaxis range %f-%f, [%s] %d points to check\n", minx,maxx,miny,maxy,scatter->name().toStdString().c_str(), scatter->count());
// add points to the selection curve and calculate as you go
QList<QPointF> points;
GenericCalculator calc;
calc.initialise();
calc.color = selection->color(); // should this go into constructor?! xxx todo
calc.xaxis = xaxis;
calc.yaxis = yaxis;
calc.series = scatter;
for(int i=0; i<scatter->count(); i++) {
QPointF point = scatter->at(i); // avoid deep copy
if (point.y() >= miny && point.y() <= maxy &&
point.x() >= minx && point.x() <= maxx) {
if (!points.contains(point)) points << point; // avoid dupes
calc.addPoint(point);
}
}
calc.finalise();
stats.insert(scatter, calc);
selection->clear();
if (points.count()) selection->append(points);
}
break;
default:
break;
case QAbstractSeries::SeriesTypeScatter: {
QScatterSeries *scatter = static_cast<QScatterSeries*>(x);
// ignore empty series
if (scatter->count() < 1) continue;
// this will be used to plot selected points on the plot
QScatterSeries *selection =NULL;
// the axes for the current series
QAbstractAxis *xaxis=NULL, *yaxis=NULL;
foreach (QAbstractAxis *ax, x->attachedAxes()) {
if (ax->orientation() == Qt::Vertical && yaxis==NULL) yaxis=ax;
if (ax->orientation() == Qt::Horizontal && xaxis==NULL) xaxis=ax;
}
if ((selection=static_cast<QScatterSeries*>(selections.value(x, NULL))) == NULL) {
selection = new QScatterSeries();
// all of this curve cloning should be in a new method xxx todo
host->qchart->addSeries(selection); // before adding data and axis
selection->setUseOpenGL(scatter->useOpenGL());
if (selection->useOpenGL())
selection->setColor(Qt::gray); // use opengl ignores changing colors
else {
selection->setColor(scatter->color());
static_cast<QScatterSeries*>(x)->setColor(Qt::gray);
}
selection->setMarkerSize(scatter->markerSize());
selection->setMarkerShape(scatter->markerShape());
selection->setPen(scatter->pen());
selection->setName(QString("%1_select").arg(scatter->name()));
selections.insert(x, selection);
ignore.append(selection);
// only do when creating it.
if (yaxis) selection->attachAxis(yaxis);
if (xaxis) selection->attachAxis(xaxis);
}
// lets work out what range of values we need to be
// selecting is, reverse since possible to have a backwards
// rectangle in the selection tool
double miny=0,maxy=0,minx=0,maxx=0;
miny =this->miny(x);
maxy =this->maxy(x);
if (maxy < miny) { double t=miny; miny=maxy; maxy=t; }
minx =this->minx(x);
maxx =this->maxx(x);
if (maxx < minx) { double t=minx; minx=maxx; maxx=t; }
//fprintf(stderr, "xaxis range %f-%f, yaxis range %f-%f, [%s] %d points to check\n", minx,maxx,miny,maxy,scatter->name().toStdString().c_str(), scatter->count());
// add points to the selection curve and calculate as you go
QList<QPointF> points;
GenericCalculator calc;
calc.initialise();
calc.color = selection->color(); // should this go into constructor?! xxx todo
calc.xaxis = xaxis;
calc.yaxis = yaxis;
calc.series = scatter;
for(int i=0; i<scatter->count(); i++) {
QPointF point = scatter->at(i); // avoid deep copy
if (point.y() >= miny && point.y() <= maxy &&
point.x() >= minx && point.x() <= maxx) {
if (!points.contains(point)) points << point; // avoid dupes
calc.addPoint(point);
}
}
calc.finalise();
stats.insert(scatter, calc);
selection->clear();
if (points.count()) selection->append(points);
}
break;
default:
break;
}
}
rectchanged = false;
} else {
resetSelections();
}
rectchanged = false;
} else {
resetSelections();
}
}
// repaint everything
@@ -873,7 +907,7 @@ GenericSelectTool::updateScene()
foreach(QAbstractSeries *x, host->qchart->series()) {
if (ignore.contains(x)) continue;
if (ignore.contains(x)) continue; // we still reset selections for invisible curves
switch(x->type()) {

View File

@@ -83,6 +83,9 @@ class GenericSelectTool : public QObject, public QGraphicsItem
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
static constexpr double gl_linemarker = 7;
static constexpr double gl_scattermarker = 10;
friend class ::GenericPlot;
public:
@@ -95,6 +98,9 @@ class GenericSelectTool : public QObject, public QGraphicsItem
// set mode
void setMode(SelectionMode mode) { this->mode=mode; }
// some series was shown or hidden...
void setSeriesVisible(QString name, bool visible);
// is invisible and tiny. we are just an observer
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event);