Skip to content

Commit

Permalink
Add QChart to Python Chart (1 of 5)
Browse files Browse the repository at this point in the history
Updating the python chart to render via a Qt Chart in addition
to the existing web page rendering.

Five aspects are planned:
1. Add QT chart option, basic rendering of Line+Scatter (this commit)
2. Add legend and axes, support for Pie and Bar charts
3. Add interactivity / hover etc
4. Add options for annotations and markers
5. Add more advanced charts and chart objects

There is an example in the tests folder, but at this point the
chart is very basic, but the main plumbing is in place.
  • Loading branch information
liversedge committed Feb 10, 2020
1 parent d1e2f38 commit b4eb119
Show file tree
Hide file tree
Showing 11 changed files with 567 additions and 105 deletions.
212 changes: 197 additions & 15 deletions src/Charts/PythonChart.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,23 @@ void PythonConsole::contextMenuEvent(QContextMenuEvent *e)

PythonChart::PythonChart(Context *context, bool ridesummary) : GcChartWindow(context), context(context), ridesummary(ridesummary)
{
setControls(NULL);
// controls widget
QWidget *c = new QWidget;
setControls(c);
//HelpWhatsThis *helpConfig = new HelpWhatsThis(c);
//c->setWhatsThis(helpConfig->getWhatsThisText(HelpWhatsThis::ChartRides_Performance));

// settings
QVBoxLayout *clv = new QVBoxLayout(c);
web = new QCheckBox(tr("Web charting"), this);
web->setChecked(true);
clv->addWidget(web);
clv->addStretch();

// sert no render widget
charttype=0; // not set yet
chartview=NULL;
canvas=NULL;

QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->setSpacing(0);
Expand All @@ -297,6 +313,7 @@ PythonChart::PythonChart(Context *context, bool ridesummary) : GcChartWindow(con
showCon = new QCheckBox(tr("Show Console"), this);
showCon->setChecked(true);


rev->addStretch();
rev->addWidget(showCon);
rev->addStretch();
Expand Down Expand Up @@ -335,34 +352,31 @@ PythonChart::PythonChart(Context *context, bool ridesummary) : GcChartWindow(con

splitter->addWidget(leftsplitter);

canvas = new QWebEngineView(this);
canvas->setContentsMargins(0,0,0,0);
canvas->page()->view()->setContentsMargins(0,0,0,0);
canvas->setZoomFactor(dpiXFactor);
canvas->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
#if QT_VERSION >= 0x050800
// stop stealing focus!
canvas->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
#endif
splitter->addWidget(canvas);
// for Chart or webpage
render = new QWidget(this);
renderlayout = new QVBoxLayout(render);
splitter->addWidget(render);

// make splitter reasonable
QList<int> sizes;
sizes << 300 << 500;
splitter->setSizes(sizes);

// passing data across python and gui threads
connect(this, SIGNAL(setUrl(QUrl)), this, SLOT(webpage(QUrl)));
connect(this, SIGNAL(emitCurve(QString,QVector<double>,QVector<double>,QString,QString,int,int,int,QString,int,bool)),
this, SLOT( setCurve(QString,QVector<double>,QVector<double>,QString,QString,int,int,int,QString,int,bool)));

if (ridesummary) {
connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(runScript()));

// refresh when comparing
connect(context, SIGNAL(compareIntervalsStateChanged(bool)), this, SLOT(runScript()));
connect(context, SIGNAL(compareIntervalsChanged()), this, SLOT(runScript()));

// refresh when intervals changed / selected
connect(context, SIGNAL(intervalsChanged()), this, SLOT(runScript()));
connect(context, SIGNAL(intervalSelected()), this, SLOT(runScript()));
connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(runScript())); // not needed since get signal below
//connect(context, SIGNAL(intervalsChanged()), this, SLOT(runScript()));
//connect(context, SIGNAL(intervalSelected()), this, SLOT(runScript()));

} else {
connect(this, SIGNAL(dateRangeChanged(DateRange)), this, SLOT(runScript()));
Expand All @@ -378,6 +392,7 @@ PythonChart::PythonChart(Context *context, bool ridesummary) : GcChartWindow(con

// reveal controls
connect(showCon, SIGNAL(stateChanged(int)), this, SLOT(showConChanged(int)));
connect(web, SIGNAL(stateChanged(int)), this, SLOT(showWebChanged(int)));

// config changes
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
Expand All @@ -401,6 +416,66 @@ PythonChart::PythonChart(Context *context, bool ridesummary) : GcChartWindow(con
}
}


// switch between rendering to a web page and rendering to a chart page
void
PythonChart::setWeb(bool x)
{
// toggle the use of a web chart or a qt chart for rendering the data
if (x && canvas==NULL) {

// delete the chart view if exists
if (chartview) {
renderlayout->removeWidget(chartview);
delete chartview; // deletes associated chart too
chartview=NULL;
qchart=NULL;
}

// setup the canvas
canvas = new QWebEngineView(this);
canvas->setContentsMargins(0,0,0,0);
canvas->page()->view()->setContentsMargins(0,0,0,0);
canvas->setZoomFactor(dpiXFactor);
canvas->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
#if QT_VERSION >= 0x050800
// stop stealing focus!
canvas->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
#endif
renderlayout->insertWidget(0, canvas);
}

if (!x && chartview==NULL) {

// delete the canvas if exists
if (canvas) {
renderlayout->removeWidget(canvas);
delete canvas;
canvas = NULL;
}

// setup the chart
qchart = new QChart();
qchart->setBackgroundVisible(false); // draw on canvas
qchart->legend()->setVisible(false); // no legends
qchart->setTitle("No title set"); // none wanted
qchart->setAnimationOptions(QChart::NoAnimation);
qchart->setFont(QFont());

// set theme, but for now use a std one TODO: map color scheme to chart theme
qchart->setTheme(QChart::ChartThemeDark);

chartview = new QChartView(qchart, this);
renderlayout->insertWidget(0, chartview);
}

// set the check state!
web->setChecked(x);

// config changed...
configChanged(0);
}

bool
PythonChart::eventFilter(QObject *, QEvent *e)
{
Expand Down Expand Up @@ -435,6 +510,13 @@ PythonChart::configChanged(qint32)
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
palette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CPLOTBACKGROUND)));
setPalette(palette);

// chart colors
if (chartview) {
chartview->setBackgroundBrush(QBrush(GColor(CPLOTBACKGROUND)));
qchart->setBackgroundBrush(QBrush(GColor(CPLOTBACKGROUND)));
qchart->setBackgroundPen(QPen(GColor(CPLOTMARKER)));
}
}

void
Expand All @@ -449,6 +531,12 @@ PythonChart::showConChanged(int state)
if (leftsplitter) leftsplitter->setVisible(state);
}

void
PythonChart::showWebChanged(int state)
{
setWeb(state);
}

QString
PythonChart::getScript() const
{
Expand Down Expand Up @@ -571,8 +659,102 @@ PythonChart::runScript()
}
}

// rendering to a web page
void
PythonChart::webpage(QUrl url)
{
canvas->setUrl(url);
if (canvas) canvas->setUrl(url);
}

// rendering to qt chart
bool
PythonChart::setCurve(QString name, QVector<double> xseries, QVector<double> yseries, QString xname, QString yname,
int line, int symbol, int size, QString color, int opacity, bool opengl)
{
// if curve already exists, remove it
QAbstractSeries *existing = curves.value(name);
if (existing) qchart->removeSeries(existing);
delete existing;

switch (charttype) {
default:

case GC_CHART_LINE:
{
// set up the curves
QLineSeries *add = new QLineSeries();
add->setName(name);

// aesthetics
QColor col=QColor(color);
add->setBrush(Qt::NoBrush);
QPen pen(color);
pen.setStyle(Qt::SolidLine);
pen.setWidth(size);
add->setPen(pen);
add->setOpacity(double(opacity) / 100.0); // 0-100% to 0.0-1.0 values

// data
for (int i=0; i<xseries.size() && i<yseries.size(); i++)
add->append(xseries.at(i), yseries.at(i));

// hardware support?
chartview->setRenderHint(QPainter::Antialiasing);
add->setUseOpenGL(opengl); // for scatter or line only apparently

// chart
qchart->addSeries(add);
qchart->legend()->setMarkerShape(QLegend::MarkerShapeFromSeries);
qchart->createDefaultAxes();
qchart->setDropShadowEnabled(false);

// add to list of curves
curves.insert(name,add);
}
break;

case GC_CHART_SCATTER:
{
// set up the curves
QScatterSeries *add = new QScatterSeries();
add->setName(name);

// aesthetics
add->setMarkerShape(QScatterSeries::MarkerShapeCircle); //TODO: use 'symbol'
add->setMarkerSize(size);
QColor col=QColor(color);
add->setBrush(QBrush(col));
add->setPen(Qt::NoPen);
add->setOpacity(double(opacity) / 100.0); // 0-100% to 0.0-1.0 values

// data
for (int i=0; i<xseries.size() && i<yseries.size(); i++)
add->append(xseries.at(i), yseries.at(i));

// hardware support?
chartview->setRenderHint(QPainter::Antialiasing);
add->setUseOpenGL(opengl); // for scatter or line only apparently

// chart
qchart->addSeries(add);
qchart->legend()->setMarkerShape(QLegend::MarkerShapeFromSeries);
qchart->createDefaultAxes();
qchart->setDropShadowEnabled(false);

// add to list of curves
curves.insert(name,add);
}
break;
case GC_CHART_BAR:
{
// set up the curves
}
break;
case GC_CHART_PIE:
{
// set up the curves
}
break;

}
}
49 changes: 48 additions & 1 deletion src/Charts/PythonChart.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,19 @@
#include <string.h>
#include <QWebEngineView>
#include <QUrl>
#include <QtCharts>

#include "GoldenCheetah.h"
#include "Context.h"
#include "Athlete.h"
#include "RCanvas.h"

// keep aligned to library.py
#define GC_CHART_LINE 1
#define GC_CHART_SCATTER 2
#define GC_CHART_BAR 3
#define GC_CHART_PIE 4

class PythonChart;

class PythonHost {
Expand Down Expand Up @@ -94,21 +101,31 @@ class PythonChart : public GcChartWindow, public PythonHost {
Q_PROPERTY(QString script READ getScript WRITE setScript USER true)
Q_PROPERTY(QString state READ getState WRITE setState USER true)
Q_PROPERTY(bool showConsole READ showConsole WRITE setConsole USER true)
Q_PROPERTY(bool asWeb READ asWeb WRITE setWeb USER true)

public:
PythonChart(Context *context, bool ridesummary);

// reveal
bool hasReveal() { return true; }
QCheckBox *showCon;
QCheckBox *showCon, *web;

// receives all the events
QTextEdit *script;
PythonConsole *console;
QWidget *render;
QVBoxLayout *renderlayout;

// rendering via...
QWebEngineView *canvas; // not yet!!
QChartView *chartview;
QChart *qchart;

void emitUrl(QUrl x) { emit setUrl(x); }

bool asWeb() const { return (web ? web->isChecked() : true); }
void setWeb(bool);

bool showConsole() const { return (showCon ? showCon->isChecked() : true); }
void setConsole(bool);

Expand All @@ -121,16 +138,42 @@ class PythonChart : public GcChartWindow, public PythonHost {
PythonChart *chart() { return this; }
bool readOnly() { return true; }

// set chart settings
bool configChart(QString title, int type, bool animate) {

if (chartview) {
// if we changed the type, all series must go
if (charttype != type) qchart->removeAllSeries();
charttype=type;

// title is allowed to be blank
qchart->setTitle(title);

// would avoid animations as they get very tiresome and are not
// generally transition animations, so add very little value
// by default they are disabled anyway
qchart->setAnimationOptions(animate ? QChart::SeriesAnimations : QChart::NoAnimation);

return true;
}
}
signals:
void setUrl(QUrl);
bool emitCurve(QString name, QVector<double> xseries, QVector<double> yseries, QString xname, QString yname,
int line, int symbol, int size, QString color, int opacity, bool opengl);


public slots:
void configChanged(qint32);
void showConChanged(int state);
void showWebChanged(int state);
void runScript();
void webpage(QUrl);
static void execScript(PythonChart *);

bool setCurve(QString name, QVector<double> xseries, QVector<double> yseries, QString xname, QString yname,
int line, int symbol, int size, QString color, int opacity, bool opengl);

protected:
// enable stopping long running scripts
bool eventFilter(QObject *, QEvent *e);
Expand All @@ -142,6 +185,10 @@ class PythonChart : public GcChartWindow, public PythonHost {
Context *context;
QString text; // if Rtool not alive
bool ridesummary;
int charttype;

// curves
QMap<QString, QAbstractSeries *>curves;
};


Expand Down
Loading

0 comments on commit b4eb119

Please sign in to comment.