mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
.. The diagram now displays on a chart, but there are a few issues. Will work through them as more testing done.
1415 lines
49 KiB
C++
1415 lines
49 KiB
C++
/*
|
|
* Copyright (c) 2020 Mark Liversedge (liversedge@gmail.com)
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
|
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include "GenericPlot.h"
|
|
#include "GenericChart.h"
|
|
|
|
#include "Colors.h"
|
|
#include "AbstractView.h"
|
|
#include "RideFileCommand.h"
|
|
#include "RideCache.h"
|
|
#include "Utils.h"
|
|
|
|
#include "ChartSpace.h"
|
|
#include "UserChartOverviewItem.h"
|
|
|
|
#include "Voronoi.h"
|
|
#include "GenericLines.h"
|
|
|
|
#include <limits>
|
|
|
|
// used to format dates/times on axes
|
|
QString GenericPlot::gl_dateformat = QString("dd MMM yy");
|
|
QString GenericPlot::gl_timeformat = QString("hh:mm:ss");
|
|
|
|
GenericPlot::GenericPlot(QWidget *parent, Context *context, QGraphicsItem *item) : QWidget(parent), context(context), item(item)
|
|
{
|
|
setAutoFillBackground(true);
|
|
|
|
// set a minimum height
|
|
setMinimumHeight(gl_minheight *dpiXFactor);
|
|
|
|
// intitialise state info
|
|
charttype=0;
|
|
chartview=NULL;
|
|
barseries=NULL;
|
|
stackbarseries=NULL;
|
|
percentbarseries=NULL;
|
|
voronoidiagram=NULL;
|
|
bottom=left=true;
|
|
|
|
mainLayout = new QVBoxLayout(this);
|
|
mainLayout->setSpacing(0);
|
|
mainLayout->setContentsMargins(0,0,0,0);
|
|
leftLayout = new QHBoxLayout();
|
|
|
|
// setup the chart
|
|
qchart = new QChart();
|
|
qchart->setBackgroundVisible(false); // draw on canvas
|
|
qchart->legend()->setVisible(false); // no legends --> custom todo
|
|
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);
|
|
chartview->setRenderHint(QPainter::Antialiasing);
|
|
|
|
// watch mouse hover etc on the chartview and scene
|
|
if (item == NULL) {
|
|
chartview->setMouseTracking(true);
|
|
chartview->scene()->installEventFilter(this);
|
|
installEventFilter(this);
|
|
} else {
|
|
|
|
// we get events from the chartspace becuase
|
|
// when we are embedded via QGraphicsProxyWidget
|
|
// mouse events go missing.
|
|
item->scene()->installEventFilter(this);
|
|
}
|
|
|
|
// add selector
|
|
selector = new GenericSelectTool(this);
|
|
chartview->scene()->addItem(selector);
|
|
|
|
// the legend at the top for now
|
|
legend = new GenericLegend(context, this);
|
|
|
|
// add all widgets to the view
|
|
mainLayout->addWidget(legend);
|
|
holder=mainLayout;
|
|
mainLayout->addLayout(leftLayout);
|
|
leftLayout->addWidget(chartview);
|
|
|
|
// watch for colors changing
|
|
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
|
|
|
|
// get notifications when values change
|
|
connect(selector, SIGNAL(seriesClicked(QAbstractSeries*,GPointF)), this, SLOT(seriesClicked(QAbstractSeries*,GPointF)));
|
|
connect(selector, SIGNAL(hover(GPointF,QString,QAbstractSeries*)), legend, SLOT(setValue(GPointF,QString)));
|
|
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)));
|
|
connect(qchart, SIGNAL(plotAreaChanged(QRectF)), this, SLOT(plotAreaChanged()));
|
|
|
|
// config changed...
|
|
configChanged(0);
|
|
}
|
|
|
|
void
|
|
GenericPlot::seriesClicked(QAbstractSeries*series, GPointF point)
|
|
{
|
|
// user clicked on a point, do we need to click thru?
|
|
QVector<QString> fseries = filenames.value(series, QVector<QString>());
|
|
if (point.index >= 0 && point.index < fseries.count()) {
|
|
// click thru
|
|
RideItem *item = context->athlete->rideCache->getRide(fseries.at(point.index));
|
|
if (item) context->notifyRideSelected(item);
|
|
}
|
|
}
|
|
|
|
// we can intercept events from the QT Chart's chartview or a chartspace
|
|
bool GenericPlot::eventFilter(QObject *obj, QEvent *e)
|
|
{
|
|
|
|
if (item == NULL) return eventHandler(0, obj, e);
|
|
|
|
// space is non null and not busy dragging and resizing etc
|
|
if ((e->type() == QEvent::GraphicsSceneMousePress ||
|
|
e->type() == QEvent::GraphicsSceneMouseRelease ||
|
|
e->type() == QEvent::GraphicsSceneMouseMove) &&
|
|
|
|
// the line below is naughty- it assumes we are embedded via a userchartoverview item- if we
|
|
// embed R or Python charts on the overview in the future this line will cause a SEGV- I took the
|
|
// decision that the performance improvement on screen when dragging and moving
|
|
// overview items was worth the filthy code. if you just delete it (or comment it out)
|
|
// the code will still be fine, its just here to optimise out unneccessary event processing.
|
|
static_cast<UserChartOverviewItem*>(static_cast<QGraphicsProxyWidget*>(item)->parent())->chartspace()->state == ChartSpace::NONE &&
|
|
|
|
item->sceneBoundingRect().contains(static_cast<QGraphicsSceneMouseEvent*>(e)->scenePos())) {
|
|
|
|
// send, as-is, they will have to map co-ordinates
|
|
return eventHandler(1, obj, e);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// source 0=chart scene, 1= chart space scene
|
|
bool
|
|
GenericPlot::eventHandler(int source, void *, QEvent *e)
|
|
{
|
|
static bool block=false;
|
|
|
|
// don't want to get interuppted
|
|
if (block) return false;
|
|
else block=true;
|
|
|
|
// where is the cursor?
|
|
QPointF spos;
|
|
if ((e->type() == QEvent::GraphicsSceneMousePress ||
|
|
e->type() == QEvent::GraphicsSceneMouseRelease ||
|
|
e->type() == QEvent::GraphicsSceneMouseMove)) {
|
|
|
|
// map to the proxy coordinates
|
|
if (source == 0) { // chartview coordinates already
|
|
spos = static_cast<QGraphicsSceneMouseEvent*>(e)->scenePos();
|
|
} else {
|
|
|
|
// map to proxy
|
|
spos = item->mapFromScene(static_cast<QGraphicsSceneMouseEvent*>(e)->scenePos());
|
|
|
|
// now add in our relative coordinates wthin the item (our topleft is relative to the proxy topleft)
|
|
spos -= chartview->geometry().topLeft();
|
|
}
|
|
}
|
|
|
|
// so we want to trigger a scene update?
|
|
bool updatescene = false;
|
|
|
|
//
|
|
// HANDLE EVENTS AND UPDATE STATE
|
|
//
|
|
switch(e->type()) {
|
|
|
|
// mouse clicked
|
|
case QEvent::GraphicsSceneMousePress:
|
|
{
|
|
updatescene = selector->clicked(spos);
|
|
}
|
|
break;
|
|
|
|
// mouse released
|
|
case QEvent::GraphicsSceneMouseRelease:
|
|
{
|
|
updatescene = selector->released(spos);
|
|
}
|
|
break;
|
|
|
|
// mouse move
|
|
case QEvent::GraphicsSceneMouseMove:
|
|
{
|
|
updatescene = selector->moved(spos);
|
|
}
|
|
break;
|
|
|
|
case QEvent::GraphicsSceneWheel:
|
|
{
|
|
QGraphicsSceneWheelEvent *w = static_cast<QGraphicsSceneWheelEvent*>(e);
|
|
updatescene = selector->wheel(w->delta());
|
|
}
|
|
break;
|
|
|
|
// resize
|
|
case QEvent::Resize:
|
|
break;
|
|
|
|
// tooltip, paused for a moment..
|
|
case QEvent::GraphicsSceneHoverEnter:
|
|
case QEvent::GraphicsSceneHelp:
|
|
case QEvent::GraphicsSceneHoverMove:
|
|
break;
|
|
|
|
// tooltip, paused for a moment..
|
|
case QEvent::GraphicsSceneHoverLeave:
|
|
break;
|
|
|
|
// tooltip, paused for a moment..
|
|
case QEvent::ToolTip:
|
|
break;
|
|
|
|
|
|
default:
|
|
//fprintf(stderr,"%s: some event %d for obj=%u\n", source ? "widget" : "scene", e->type(), (void*)obj); fflush(stderr);
|
|
break;
|
|
}
|
|
|
|
//
|
|
// UPDATE SCENE TO REFLECT STATE
|
|
//
|
|
if (updatescene) selector->updateScene(); // really only do selection right now.. more to come
|
|
|
|
// all done.
|
|
block = false;
|
|
return false;
|
|
}
|
|
|
|
void
|
|
GenericPlot::configChanged(qint32)
|
|
{
|
|
// tinted palette for headings etc
|
|
QPalette palette;
|
|
palette.setBrush(QPalette::Window, QBrush(bgcolor_));
|
|
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
|
|
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
|
|
palette.setColor(QPalette::Base, GCColor::alternateColor(bgcolor_));
|
|
setPalette(palette);
|
|
|
|
// chart colors
|
|
chartview->setBackgroundBrush(QBrush(bgcolor_));
|
|
qchart->setBackgroundBrush(QBrush(bgcolor_));
|
|
qchart->setBackgroundPen(QPen(GColor(CPLOTMARKER)));
|
|
}
|
|
|
|
void
|
|
GenericPlot::setBackgroundColor(QColor bgcolor)
|
|
{
|
|
this->bgcolor_ = bgcolor;
|
|
configChanged(0);
|
|
}
|
|
|
|
void
|
|
GenericPlot::setSeriesVisible(QString name, bool visible)
|
|
{
|
|
// find the curve
|
|
QAbstractSeries *series = curves.value(name, NULL);
|
|
QAbstractSeries *dec = series ? decorations.value(series) : NULL;
|
|
|
|
// does it exist and did it change?
|
|
if (series && series->isVisible() != visible) {
|
|
|
|
// show/hide along with any decoration
|
|
series->setVisible(visible);
|
|
if (dec) dec->setVisible(visible);
|
|
|
|
// tell selector we hid/show a series so it can respond.
|
|
selector->setSeriesVisible(name, visible);
|
|
}
|
|
}
|
|
|
|
// annotations
|
|
|
|
// line by user
|
|
void
|
|
GenericPlot::addAnnotation(AnnotationType , QAbstractSeries*series, double value)
|
|
{
|
|
fprintf(stderr, "add annotation line: for %s at value %f\n", series->name().toStdString().c_str(), value);
|
|
fflush(stderr);
|
|
}
|
|
|
|
void
|
|
GenericPlot::addAnnotation(AnnotationType, QString string, QColor color)
|
|
{
|
|
QFont std;
|
|
std.setPointSizeF(std.pointSizeF() * scale_);
|
|
QFontMetrics fm(std);
|
|
|
|
QLabel *add = new QLabel(this);
|
|
add->setText(string);
|
|
add->setStyleSheet(QString("color: %1").arg(color.name()));
|
|
add->setFixedWidth(fm.boundingRect(string).width() + (25*dpiXFactor));
|
|
add->setAlignment(Qt::AlignCenter);
|
|
labels << add;
|
|
}
|
|
|
|
void
|
|
GenericPlot::addVoronoi(QString name, QVector<double>x, QVector<double>y)
|
|
{
|
|
vname = name;
|
|
vx = x;
|
|
vy = y;
|
|
}
|
|
|
|
void
|
|
GenericPlot::pieHover(QPieSlice *slice, bool state)
|
|
{
|
|
if (havelegend.count() == 0) return;
|
|
if (state == true) legend->setValue(GPointF(0, round(slice->percentage()*1000)/10, -1), havelegend.first());
|
|
else legend->unhover(havelegend.first());
|
|
}
|
|
|
|
// handle hover on barset
|
|
void GenericPlot::barsetHover(bool status, int index, QBarSet *)
|
|
{
|
|
QString category;
|
|
if (categories.count() > index) category = categories[index];
|
|
|
|
foreach(QBarSet *barset, barsets) {
|
|
if (status) legend->setValue(GPointF(0, barset->at(index), -1), barset->label(), category);
|
|
else {
|
|
legend->unhover(barset->label());
|
|
legend->unhoverx();
|
|
}
|
|
}
|
|
}
|
|
|
|
struct axisrect {
|
|
bool operator< (axisrect right) const { return rect.x() < right.rect.x(); }
|
|
axisrect(QAbstractAxis *ax, QRectF ar) : rect(ar), axis(ax) {}
|
|
QRectF rect;
|
|
QAbstractAxis *axis;
|
|
};
|
|
|
|
// for sorting rectangles
|
|
static bool myqRectLess(const QRectF left, const QRectF right)
|
|
{
|
|
return (left.x() < right.x());
|
|
}
|
|
|
|
// resizing, so plot area changed and likely all the scene moved
|
|
void
|
|
GenericPlot::plotAreaChanged()
|
|
{
|
|
// we need to recalculate the axis geometries
|
|
// since the qchart methods do not make any of
|
|
// this public we have to search through the
|
|
// graphic items in the axis areas and associate
|
|
// them with an axis. Fortunately all label items
|
|
// have a common parentItem() and are actually
|
|
// added to the scene as QGraphicsTextItem so we
|
|
// find them safely and group them together.
|
|
//
|
|
// Sadly, there is not direct link between the parentItem
|
|
// and the axisitem but the title text is available
|
|
// so we use that.
|
|
//
|
|
|
|
// Step One: Hunt for labels and turn into bounding rectangles
|
|
QList<axisrect> ar; // axis + title rect
|
|
QList <QRectF> lr; // consolidated label rects
|
|
|
|
static Qt::AlignmentFlag sides[2]= { Qt::AlignLeft, Qt::AlignRight };
|
|
for(int i=0; i<2; i++) {
|
|
|
|
// create a rectangle that covers the area
|
|
// in the scene where labels will be located
|
|
QRectF zone;
|
|
switch (sides[i]) {
|
|
case Qt::AlignLeft:
|
|
zone =QRectF(QPointF(0, -10),
|
|
QPointF(qchart->plotArea().x(), qchart->scene()->height()));
|
|
break;
|
|
case Qt::AlignRight:
|
|
zone =QRectF(QPointF(qchart->plotArea().x()+qchart->plotArea().width(), -10),
|
|
QPointF(qchart->scene()->width(), qchart->scene()->height()));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// temporarily keep track of label rectangles
|
|
QMap<QGraphicsItem*, QRectF> bounds;
|
|
foreach(QGraphicsItem *item, qchart->scene()->items(zone, Qt::ItemSelectionMode::ContainsItemShape)) {
|
|
if (item->type() != QGraphicsTextItem::Type) continue;
|
|
|
|
// for some reason 5.10 or earlier has errant text items we should ignore
|
|
if (static_cast<QGraphicsTextItem*>(item)->toPlainText() == "") continue;
|
|
|
|
// is this an axis title?
|
|
QAbstractAxis *found=NULL;
|
|
foreach(QAbstractAxis *axis, qchart->axes()) {
|
|
if (axis->titleText() == static_cast<QGraphicsTextItem*>(item)->toPlainText()) {
|
|
found=axis;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// update the maps
|
|
if (found) {
|
|
ar.append(axisrect(found, QRectF(item->scenePos().x(), item->scenePos().y(),
|
|
item->boundingRect().width(), item->boundingRect().height())));
|
|
} else {
|
|
QRectF bound = bounds.value(item->parentItem(), QRectF());
|
|
QRectF ir = QRectF(item->scenePos().x(), item->scenePos().y(),
|
|
item->boundingRect().width(), item->boundingRect().height());
|
|
if (bound == QRectF()) bound=ir;
|
|
else bound = bound.united(ir);
|
|
bounds.insert(item->parentItem(), bound);
|
|
}
|
|
}
|
|
|
|
// take united label rects and make a list
|
|
QMapIterator<QGraphicsItem*, QRectF> rr(bounds);
|
|
while(rr.hasNext()) {
|
|
rr.next();
|
|
lr.append(rr.value());
|
|
}
|
|
}
|
|
|
|
// Step two: Sort both lists by ascending x values- both will match 1:1 by index
|
|
// because we're only doing y-axis- label rects and title rects have
|
|
// same sort order left to right (x-axis) so we can then associate
|
|
// the label rect at position [i] with the axisrect at position [i]
|
|
std::sort(ar.begin(), ar.end());
|
|
std::sort(lr.begin(), lr.end(), myqRectLess);
|
|
|
|
axisRect.clear(); // class member that tracks axis->scene rectangle
|
|
for(int i=0; i< ar.count() && i <lr.count(); i++) {
|
|
axisRect.insert(ar[i].axis, lr[i]);
|
|
}
|
|
}
|
|
|
|
bool
|
|
GenericPlot::initialiseChart(QString title, int type, bool animate, int legpos, double scale)
|
|
{
|
|
clearVoronoi();
|
|
|
|
// if we changed the type, all series must go
|
|
if (charttype != type) {
|
|
qchart->removeAllSeries();
|
|
curves.clear();
|
|
filenames.clear();
|
|
barseries=NULL;
|
|
stackbarseries=NULL;
|
|
percentbarseries=NULL;
|
|
}
|
|
|
|
|
|
foreach(QLabel *label, labels) delete label;
|
|
labels.clear();
|
|
|
|
foreach(Quadtree *tree, quadtrees) delete tree;
|
|
quadtrees.clear();
|
|
|
|
foreach(GenericAxisInfo *axisinfo, axisinfos) delete axisinfo;
|
|
axisinfos.clear();
|
|
|
|
left=true;
|
|
bottom=true;
|
|
barsets.clear();
|
|
havelegend.clear();
|
|
this->scale_ = scale;
|
|
|
|
// reset selections etc
|
|
selector->reset();
|
|
|
|
// remember type
|
|
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);
|
|
|
|
// lets move the legend, only support top or bottom for now
|
|
holder->removeWidget(legend);
|
|
switch (legpos) {
|
|
case 0: // bottom
|
|
legend->setOrientation(Qt::Horizontal);
|
|
mainLayout->insertWidget(-1, legend); // at end
|
|
holder=mainLayout;
|
|
legend->show();
|
|
break;
|
|
case 1: // left
|
|
legend->setOrientation(Qt::Vertical);
|
|
leftLayout->insertWidget(0, legend); // at beginning
|
|
holder=leftLayout;
|
|
legend->show();
|
|
break;
|
|
case 2: // top
|
|
legend->setOrientation(Qt::Horizontal);
|
|
mainLayout->insertWidget(0, legend); // at front
|
|
holder=mainLayout;
|
|
legend->show();
|
|
break;
|
|
case 3: // right
|
|
legend->setOrientation(Qt::Vertical);
|
|
leftLayout->insertWidget(-1, legend); // at end
|
|
holder=leftLayout;
|
|
legend->show();
|
|
break;
|
|
case 4: // none
|
|
legend->hide();
|
|
break;
|
|
}
|
|
|
|
// what kind of selector do we use?
|
|
if (charttype==GC_CHART_LINE) selector->setMode(GenericSelectTool::XRANGE);
|
|
else selector->setMode(GenericSelectTool::RECTANGLE);
|
|
|
|
return true;
|
|
}
|
|
|
|
// rendering to qt chart
|
|
bool
|
|
GenericPlot::addCurve(QString name, QVector<double> xseries, QVector<double> yseries, QVector<QString> fseries, QString xname, QString yname,
|
|
QStringList labels, QStringList colors,
|
|
int linestyle, int symbol, int size, QString color, int opacity, bool opengl, bool legend, bool datalabels, bool fill)
|
|
{
|
|
|
|
// a curve can have a decoration associated with it
|
|
// on a line chart the decoration is the symbols
|
|
// on a scatter chart the decoration is the line
|
|
QString dname = QString("d_%1").arg(name);
|
|
|
|
// standard colors are encoded 1,1,x - where x is the index into the colorset
|
|
QColor applyColor = RGBColor(QColor(color));
|
|
|
|
// labels font
|
|
QFont labelsfont;
|
|
labelsfont.setPointSizeF(labelsfont.pointSize() * scale_);
|
|
|
|
// if curve already exists, remove it
|
|
if (charttype==GC_CHART_LINE || charttype==GC_CHART_SCATTER || charttype==GC_CHART_PIE) {
|
|
QAbstractSeries *existing = curves.value(name);
|
|
if (existing) {
|
|
qchart->removeSeries(existing);
|
|
delete existing;
|
|
curves.remove(name);
|
|
|
|
QAbstractSeries *decor = decorations.value(existing);
|
|
if (decor) {
|
|
qchart->removeSeries(decor);
|
|
delete decor;
|
|
decorations.remove(existing);
|
|
}
|
|
}
|
|
}
|
|
|
|
// want a legend?
|
|
if (legend) havelegend << name;
|
|
|
|
// lets find that axis - even blank ones
|
|
GenericAxisInfo *xaxis, *yaxis;
|
|
xaxis=axisinfos.value(xname);
|
|
yaxis=axisinfos.value(yname);
|
|
if (xaxis==NULL) {
|
|
xaxis=new GenericAxisInfo(Qt::Horizontal, xname);
|
|
|
|
// default alignment toggles
|
|
xaxis->align = bottom ? Qt::AlignBottom : Qt::AlignTop;
|
|
bottom = !bottom;
|
|
|
|
// use marker color for x axes
|
|
xaxis->labelcolor = xaxis->axiscolor = GColor(CPLOTMARKER);
|
|
|
|
// add to list
|
|
axisinfos.insert(xname, xaxis);
|
|
|
|
}
|
|
if (yaxis==NULL) {
|
|
yaxis=new GenericAxisInfo(Qt::Vertical, yname);
|
|
|
|
// default alignment toggles
|
|
yaxis->align = left ? Qt::AlignLeft : Qt::AlignRight;
|
|
left = !left;
|
|
|
|
// yaxis color matches, but not done for xaxis above
|
|
yaxis->labelcolor = yaxis->axiscolor = applyColor;
|
|
|
|
// add to list
|
|
axisinfos.insert(yname, yaxis);
|
|
}
|
|
|
|
switch (charttype) {
|
|
default:
|
|
|
|
case GC_CHART_LINE:
|
|
{
|
|
// set up the curves
|
|
QLineSeries *add = new QLineSeries();
|
|
add->setName(name);
|
|
|
|
// get setup for click thru
|
|
connect(add, SIGNAL(clicked(QPointF)), selector, SLOT(seriesClicked())); // catch series clicks
|
|
filenames.insert(add, fseries);
|
|
|
|
// aesthetics
|
|
add->setBrush(Qt::NoBrush);
|
|
QPen pen(applyColor);
|
|
pen.setStyle(static_cast<Qt::PenStyle>(linestyle));
|
|
pen.setWidth(size * scale_);
|
|
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));
|
|
|
|
// tell axis about the data
|
|
xaxis->point(xseries.at(i), yseries.at(i));
|
|
yaxis->point(xseries.at(i), yseries.at(i));
|
|
}
|
|
|
|
// hardware support?
|
|
chartview->setRenderHint(QPainter::Antialiasing);
|
|
add->setUseOpenGL(opengl); // for scatter or line only apparently
|
|
qchart->setDropShadowEnabled(false);
|
|
|
|
// no line, we are invisible
|
|
if (linestyle == 0) add->setVisible(false);
|
|
else {
|
|
if (datalabels) {
|
|
add->setPointLabelsFont(labelsfont);
|
|
add->setPointLabelsVisible(true); // is false by default
|
|
add->setPointLabelsColor(applyColor);
|
|
add->setPointLabelsFormat("@yPoint");
|
|
}
|
|
// fill curve?
|
|
}
|
|
|
|
if (fill) {
|
|
|
|
// for fill effect we create an area series
|
|
QAreaSeries *area = new QAreaSeries(add);
|
|
area->setName(name);
|
|
|
|
QPen pen(applyColor);
|
|
pen.setStyle(static_cast<Qt::PenStyle>(linestyle));
|
|
pen.setWidth(size);
|
|
area->setPen(pen);
|
|
|
|
QColor col(applyColor);
|
|
col.setAlpha(64);
|
|
QBrush brush(col, Qt::SolidPattern);
|
|
area->setBrush(brush);
|
|
|
|
qchart->addSeries(area);
|
|
curves.insert(name,area);
|
|
xaxis->series.append(area);
|
|
yaxis->series.append(area);
|
|
|
|
} else {
|
|
|
|
// normal line series
|
|
qchart->addSeries(add);
|
|
curves.insert(name,add);
|
|
xaxis->series.append(add);
|
|
yaxis->series.append(add);
|
|
}
|
|
|
|
|
|
// so do we need to decorate with a symbol?
|
|
if (symbol > 0) {
|
|
|
|
// set up the curves
|
|
QScatterSeries *dec = new QScatterSeries();
|
|
dec->setName(dname);
|
|
|
|
// data
|
|
for (int i=0; i<xseries.size() && i<yseries.size(); i++)
|
|
dec->append(xseries.at(i), yseries.at(i));
|
|
|
|
// if no line, but we still want labels then show
|
|
// for our data points
|
|
if (linestyle == 0 && datalabels) {
|
|
add->setPointLabelsFont(labelsfont);
|
|
dec->setPointLabelsVisible(true); // is false by default
|
|
dec->setPointLabelsColor(applyColor);
|
|
dec->setPointLabelsFormat("@yPoint");
|
|
}
|
|
|
|
// aesthetics
|
|
if (symbol == 1) dec->setMarkerShape(QScatterSeries::MarkerShapeCircle);
|
|
else if (symbol == 2) dec->setMarkerShape(QScatterSeries::MarkerShapeRectangle);
|
|
dec->setMarkerSize(size*6*scale_);
|
|
QColor col=applyColor;
|
|
dec->setBrush(QBrush(col));
|
|
dec->setPen(Qt::NoPen);
|
|
dec->setOpacity(double(opacity) / 100.0); // 0-100% to 0.0-1.0 values
|
|
|
|
// hardware support?
|
|
chartview->setRenderHint(QPainter::Antialiasing);
|
|
dec->setUseOpenGL(opengl); // for scatter or line only apparently
|
|
qchart->setDropShadowEnabled(false);
|
|
|
|
// chart
|
|
qchart->addSeries(dec);
|
|
|
|
// add to list of curves
|
|
decorations.insert(add,dec);
|
|
xaxis->decorations.append(dec);
|
|
yaxis->decorations.append(dec);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case GC_CHART_SCATTER:
|
|
{
|
|
// set up the curves
|
|
QScatterSeries *add = new QScatterSeries();
|
|
add->setName(name);
|
|
|
|
// handle click thru
|
|
connect(add, SIGNAL(clicked(QPointF)), selector, SLOT(seriesClicked())); // catch series clicks
|
|
filenames.insert(add, fseries);
|
|
|
|
// aesthetics
|
|
if (symbol == 0) add->setVisible(false); // no marker !
|
|
else if (symbol == 1) add->setMarkerShape(QScatterSeries::MarkerShapeCircle);
|
|
else if (symbol == 2) add->setMarkerShape(QScatterSeries::MarkerShapeRectangle);
|
|
add->setMarkerSize(size*scale_);
|
|
QColor col=applyColor;
|
|
add->setBrush(QBrush(col));
|
|
add->setPen(Qt::NoPen);
|
|
add->setOpacity(double(opacity) / 100.0); // 0-100% to 0.0-1.0 values
|
|
|
|
// data
|
|
GenericCalculator calc; // watching as we add
|
|
for (int i=0; i<xseries.size() && i<yseries.size(); i++) {
|
|
add->append(xseries.at(i), yseries.at(i));
|
|
|
|
// tell axis about the data
|
|
xaxis->point(xseries.at(i), yseries.at(i));
|
|
yaxis->point(xseries.at(i), yseries.at(i));
|
|
|
|
// calculate stuff
|
|
calc.addPoint(QPointF(xseries.at(i), yseries.at(i)));
|
|
|
|
}
|
|
|
|
if (datalabels) {
|
|
add->setPointLabelsFont(labelsfont);
|
|
add->setPointLabelsVisible(true); // is false by default
|
|
add->setPointLabelsColor(applyColor);
|
|
add->setPointLabelsFormat("@yPoint");
|
|
}
|
|
|
|
// set the quadtree up - now we know the ranges...
|
|
Quadtree *tree = new Quadtree(QPointF(calc.x.min, calc.y.min), QPointF(calc.x.max, calc.y.max));
|
|
for (int i=0; i<xseries.size() && i<yseries.size(); i++)
|
|
if (xseries.at(i) != 0 && yseries.at(i) != 0) // 0,0 is common and lets ignore (usually means no data)
|
|
tree->insert(GPointF(xseries.at(i), yseries.at(i), i));
|
|
|
|
if (tree->nodes.count() || tree->root->contents.count()) quadtrees.insert(add, tree);
|
|
|
|
// hardware support?
|
|
chartview->setRenderHint(QPainter::Antialiasing);
|
|
add->setUseOpenGL(opengl); // for scatter or line only apparently
|
|
qchart->setDropShadowEnabled(false);
|
|
|
|
// chart
|
|
qchart->addSeries(add);
|
|
|
|
// add to list of curves
|
|
curves.insert(name,add);
|
|
xaxis->series.append(add);
|
|
yaxis->series.append(add);
|
|
|
|
if (linestyle > 0) {
|
|
// set up the curves
|
|
QLineSeries *dec = new QLineSeries();
|
|
dec->setName(dname);
|
|
|
|
// aesthetics
|
|
dec->setBrush(Qt::NoBrush);
|
|
QPen pen(applyColor);
|
|
pen.setStyle(static_cast<Qt::PenStyle>(linestyle));
|
|
pen.setWidth(size*scale_);
|
|
dec->setPen(pen);
|
|
dec->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++)
|
|
dec->append(xseries.at(i), yseries.at(i));
|
|
|
|
// hardware support?
|
|
chartview->setRenderHint(QPainter::Antialiasing);
|
|
dec->setUseOpenGL(opengl); // for scatter or line only apparently
|
|
qchart->setDropShadowEnabled(false);
|
|
|
|
// chart
|
|
qchart->addSeries(dec);
|
|
|
|
// add to list of curves
|
|
decorations.insert(add,dec);
|
|
xaxis->decorations.append(dec);
|
|
yaxis->decorations.append(dec);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case GC_CHART_BAR:
|
|
case GC_CHART_STACK:
|
|
case GC_CHART_PERCENT:
|
|
{
|
|
// set up the barsets
|
|
QBarSet *add= new QBarSet(name);
|
|
|
|
// aesthetics
|
|
add->setBrush(QBrush(applyColor));
|
|
if (charttype == GC_CHART_STACK) {
|
|
QPen outline(bgcolor_);
|
|
outline.setWidth(4 * scale_); // space between blocks
|
|
add->setPen(outline);
|
|
} else {
|
|
add->setPen(Qt::NoPen);
|
|
}
|
|
|
|
// data and min/max values- works fine for bar chart
|
|
// but for stacked bar we need to get max of sum so
|
|
// we do that in the finaliseChart section
|
|
for (int i=0; i<yseries.size(); i++) {
|
|
double value = yseries.at(i);
|
|
*add << value;
|
|
|
|
// don't try and calculate here in a stack chart- we need
|
|
// all the barsets to have been defined first
|
|
if (charttype != GC_CHART_STACK && charttype != GC_CHART_PERCENT) yaxis->point(i,value);
|
|
xaxis->point(i,value);
|
|
}
|
|
|
|
// we are very particular regarding axis
|
|
yaxis->type = GenericAxisInfo::CONTINUOUS;
|
|
xaxis->type = GenericAxisInfo::CATEGORY;
|
|
|
|
// shadows on bar charts
|
|
qchart->setDropShadowEnabled(false);
|
|
|
|
// add to list of barsets
|
|
barsets << add;
|
|
}
|
|
break;
|
|
|
|
case GC_CHART_PIE:
|
|
{
|
|
// set up the curves
|
|
QPieSeries *add = new QPieSeries();
|
|
havelegend << name.append(" %");
|
|
connect(add, SIGNAL(hovered(QPieSlice*,bool)), this, SLOT(pieHover(QPieSlice*,bool)));
|
|
add->setPieSize(0.7);
|
|
add->setHoleSize(0.5);
|
|
|
|
// setup the slices
|
|
for(int i=0; i<yseries.size(); i++) {
|
|
// get label?
|
|
if (i>=labels.size())
|
|
add->append(QString("%1").arg(i), yseries.at(i));
|
|
else {
|
|
if (labels.at(i) == "")add->append("(blank)", yseries.at(i));
|
|
else add->append(labels.at(i), yseries.at(i));
|
|
}
|
|
}
|
|
|
|
// now do the colors
|
|
int i=0;
|
|
foreach(QPieSlice *slice, add->slices()) {
|
|
|
|
slice->setExploded();
|
|
slice->setLabelVisible();
|
|
slice->setLabelBrush(QBrush(GColor(CPLOTMARKER)));
|
|
slice->setPen(Qt::NoPen);
|
|
if (i <colors.size()) slice->setBrush(QColor(colors.at(i)));
|
|
else slice->setBrush(Qt::red);
|
|
i++;
|
|
}
|
|
|
|
// shadows on pie
|
|
qchart->setDropShadowEnabled(false);
|
|
|
|
// set the pie chart
|
|
qchart->addSeries(add);
|
|
|
|
// add to list of curves
|
|
curves.insert(name,add);
|
|
}
|
|
break;
|
|
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// once python script has run polish the chart, fixup axes/ranges and so on.
|
|
void
|
|
GenericPlot::finaliseChart()
|
|
{
|
|
if (!qchart) return;
|
|
|
|
// remove voronoix if present
|
|
clearVoronoi();
|
|
|
|
// clear ALL axes
|
|
foreach(QAbstractAxis *axis, qchart->axes(Qt::Vertical)) {
|
|
qchart->removeAxis(axis);
|
|
delete axis;
|
|
}
|
|
foreach(QAbstractAxis *axis, qchart->axes(Qt::Horizontal)) {
|
|
qchart->removeAxis(axis);
|
|
delete axis;
|
|
}
|
|
|
|
// clear the legend
|
|
legend->removeAllSeries();
|
|
|
|
// basic aesthetics
|
|
qchart->legend()->setMarkerShape(QLegend::MarkerShapeRectangle);
|
|
qchart->setDropShadowEnabled(false);
|
|
|
|
// no more than 1 category axis since barsets are all assigned.
|
|
bool donecategory=false;
|
|
|
|
// only now can we calculate the max height for a stack chart
|
|
double maxystack=0;
|
|
if (charttype == GC_CHART_STACK && barsets.count()) {
|
|
|
|
bool havemore=true;
|
|
for(int index=0; havemore; index++) {
|
|
double maxcalc=0;
|
|
// calculate height of stack from constituent parts
|
|
foreach(QBarSet *bs, barsets) {
|
|
if (bs->count() > index) maxcalc += bs->at(index);
|
|
else {
|
|
havemore=false;
|
|
break;
|
|
}
|
|
}
|
|
if (maxcalc > maxystack) maxystack = maxcalc;
|
|
}
|
|
}
|
|
|
|
// Create axes - for everyone except pie charts that don't have any
|
|
if (charttype != GC_CHART_PIE) {
|
|
// create desired axis
|
|
foreach(GenericAxisInfo *axisinfo, axisinfos) {
|
|
//fprintf(stderr, "Axis: %s, orient:%s, type:%d\n",axisinfo->name.toStdString().c_str(),axisinfo->orientation==Qt::Vertical?"vertical":"horizontal",(int)axisinfo->type);
|
|
//fflush(stderr);
|
|
QAbstractAxis *add=NULL;
|
|
switch (axisinfo->type) {
|
|
case GenericAxisInfo::DATERANGE:
|
|
{
|
|
|
|
QDateTimeAxis *vaxis = new QDateTimeAxis(qchart);
|
|
add=vaxis; // gets added later
|
|
|
|
vaxis->setMin(QDateTime::fromMSecsSinceEpoch(axisinfo->min()));
|
|
vaxis->setMax(QDateTime::fromMSecsSinceEpoch(axisinfo->max()));
|
|
|
|
// attach to the chart
|
|
qchart->addAxis(add, axisinfo->locate());
|
|
vaxis->setFormat(GenericPlot::gl_dateformat);
|
|
}
|
|
break;
|
|
case GenericAxisInfo::TIME:
|
|
{
|
|
QDateTimeAxis *vaxis = new QDateTimeAxis(qchart);
|
|
add=vaxis; // gets added later
|
|
|
|
vaxis->setMin(QDateTime::fromMSecsSinceEpoch(axisinfo->min()));
|
|
vaxis->setMax(QDateTime::fromMSecsSinceEpoch(axisinfo->max()));
|
|
|
|
// attach to the chart
|
|
qchart->addAxis(add, axisinfo->locate());
|
|
vaxis->setFormat(GenericPlot::gl_timeformat);
|
|
}
|
|
break;
|
|
case GenericAxisInfo::CONTINUOUS:
|
|
{
|
|
QAbstractAxis *vaxis=NULL;
|
|
if (axisinfo->log) vaxis= new QLogValueAxis(qchart);
|
|
else vaxis= new QValueAxis(qchart);
|
|
add=vaxis; // gets added later
|
|
|
|
// we finally get to set the max value for stacked charts
|
|
// but only of not already passed by user
|
|
if (charttype == GC_CHART_STACK && axisinfo->max() <maxystack) vaxis->setMax(maxystack);
|
|
else vaxis->setMax(axisinfo->max());
|
|
vaxis->setMin(axisinfo->min());
|
|
|
|
if (charttype == GC_CHART_PERCENT) {
|
|
vaxis->setMax(100);
|
|
vaxis->setMin(0);
|
|
}
|
|
|
|
// attach to the chart
|
|
qchart->addAxis(add, axisinfo->locate());
|
|
}
|
|
break;
|
|
case GenericAxisInfo::CATEGORY:
|
|
{
|
|
if (!donecategory) {
|
|
|
|
donecategory=true;
|
|
|
|
QBarCategoryAxis *caxis = new QBarCategoryAxis(qchart);
|
|
add=caxis;
|
|
|
|
// add the bar series
|
|
if (charttype == GC_CHART_STACK) {
|
|
|
|
if (!stackbarseries) {
|
|
stackbarseries = new QStackedBarSeries();
|
|
qchart->addSeries(stackbarseries);
|
|
|
|
// connect hover events
|
|
connect(stackbarseries, SIGNAL(hovered(bool,int,QBarSet*)), this, SLOT(barsetHover(bool,int,QBarSet*)));
|
|
|
|
} else stackbarseries->clear();
|
|
|
|
// add the new barsets
|
|
foreach (QBarSet *bs, barsets)
|
|
stackbarseries->append(bs);
|
|
|
|
// attach before addig barseries
|
|
qchart->addAxis(add, axisinfo->locate());
|
|
|
|
// attach to category axis
|
|
stackbarseries->attachAxis(caxis);
|
|
|
|
} else if (charttype == GC_CHART_PERCENT) {
|
|
|
|
if (!percentbarseries) {
|
|
percentbarseries = new QPercentBarSeries();
|
|
qchart->addSeries(percentbarseries);
|
|
|
|
// connect hover events
|
|
connect(percentbarseries, SIGNAL(hovered(bool,int,QBarSet*)), this, SLOT(barsetHover(bool,int,QBarSet*)));
|
|
|
|
} else percentbarseries->clear();
|
|
|
|
// add the new barsets
|
|
foreach (QBarSet *bs, barsets)
|
|
percentbarseries->append(bs);
|
|
|
|
// attach before addig barseries
|
|
qchart->addAxis(add, axisinfo->locate());
|
|
|
|
// attach to category axis
|
|
percentbarseries->attachAxis(caxis);
|
|
|
|
} else {
|
|
|
|
// Bar and Pie
|
|
if (!barseries) {
|
|
barseries = new QBarSeries();
|
|
qchart->addSeries(barseries);
|
|
|
|
// connect hover events
|
|
connect(barseries, SIGNAL(hovered(bool,int,QBarSet*)), this, SLOT(barsetHover(bool,int,QBarSet*)));
|
|
|
|
} else barseries->clear();
|
|
|
|
// add the new barsets
|
|
foreach (QBarSet *bs, barsets)
|
|
barseries->append(bs);
|
|
|
|
// attach before addig barseries
|
|
qchart->addAxis(add, axisinfo->locate());
|
|
|
|
// attach to category axis
|
|
barseries->attachAxis(caxis);
|
|
}
|
|
|
|
// category labels
|
|
for(int i=axisinfo->categories.count(); i<=axisinfo->maxx; i++)
|
|
axisinfo->categories << QString("%1").arg(i+1);
|
|
// set blank to "(blank)"
|
|
for(int i=0; i<axisinfo->categories.count(); i++)
|
|
if (axisinfo->categories.at(i) == "") axisinfo->categories[i]="(blank)";
|
|
caxis->setCategories(axisinfo->categories);
|
|
categories = axisinfo->categories;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// at this point the basic settngs have been done and the axis
|
|
// is attached to the chart, so we can go ahead and apply common settings
|
|
if (add) {
|
|
|
|
// once we've done the basics, lets do the aesthetics
|
|
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
|
|
stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
|
|
stGiles.setPointSizeF(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt() * scale_);
|
|
add->setTitleFont(stGiles);
|
|
add->setLabelsFont(stGiles);
|
|
|
|
if (axisinfo->name != "x" && axisinfo->name != "y") // equivalent to being blank
|
|
add->setTitleText(axisinfo->name);
|
|
add->setLinePenColor(axisinfo->axiscolor);
|
|
if (axisinfo->orientation == Qt::Vertical) // we never have y axis lines
|
|
add->setLineVisible(false);
|
|
add->setLabelsColor(axisinfo->labelcolor);
|
|
add->setTitleBrush(QBrush(axisinfo->labelcolor));
|
|
|
|
// grid lines, just color for now xxx todo: ticks (sigh)
|
|
add->setGridLineColor(GColor(CPLOTGRID));
|
|
if (charttype != GC_CHART_SCATTER && add->orientation()==Qt::Horizontal) // no x grids unless a scatter
|
|
add->setGridLineVisible(false);
|
|
|
|
foreach(QAbstractSeries *series, axisinfo->series)
|
|
series->attachAxis(add);
|
|
foreach(QAbstractSeries *series, axisinfo->decorations)
|
|
series->attachAxis(add);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (charttype == GC_CHART_SCATTER || charttype == GC_CHART_LINE
|
|
|| charttype == GC_CHART_BAR || charttype == GC_CHART_STACK || charttype == GC_CHART_PERCENT) {
|
|
|
|
bool havexaxis=false;
|
|
foreach(QAbstractSeries *series, qchart->series()) {
|
|
// manufactured series to show X axis values
|
|
// pick the first X-axis we find and add it
|
|
// using the axis units
|
|
if (havexaxis == false) {
|
|
|
|
// look for first series with a horizontal axis (we will use this later)
|
|
foreach(QAbstractAxis *axis, series->attachedAxes()) {
|
|
if (axis->orientation() == Qt::Horizontal) {
|
|
if (axis->type()==QAbstractAxis::AxisTypeValue) legend->addX(static_cast<QValueAxis*>(axis)->titleText(), false, "");
|
|
else if (axis->type()==QAbstractAxis::AxisTypeLogValue) legend->addX(static_cast<QLogValueAxis*>(axis)->titleText(), false, "");
|
|
else if (axis->type()==QAbstractAxis::AxisTypeDateTime) {
|
|
QDateTimeAxis *dta = static_cast<QDateTimeAxis*>(axis);
|
|
legend->addX(dta->titleText(), true, dta->format());
|
|
}
|
|
havexaxis=true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// add to the legend
|
|
if (havelegend.contains(series->name())) legend->addSeries(series->name(), GenericPlot::seriesColor(series));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (charttype== GC_CHART_PIE) {
|
|
foreach(QString name, havelegend) legend->addSeries(name, GColor(CPLOTMARKER));
|
|
legend->setClickable(false);
|
|
}
|
|
|
|
|
|
// barseries special case - Bar chart
|
|
if (charttype==GC_CHART_BAR && barseries) {
|
|
|
|
// need to attach barseries to the value axes
|
|
foreach(QAbstractAxis *axis, qchart->axes(Qt::Vertical))
|
|
barseries->attachAxis(axis);
|
|
|
|
// add first X axis we find
|
|
foreach(QAbstractAxis *axis, qchart->axes(Qt::Horizontal)) {
|
|
legend->addX(static_cast<QCategoryAxis*>(axis)->titleText(), false, "");
|
|
break;
|
|
}
|
|
|
|
// and legend
|
|
foreach(QBarSet *set, barsets)
|
|
legend->addSeries(set->label(), set->color());
|
|
|
|
legend->setClickable(false);
|
|
}
|
|
// stacked bar uses stackbarseries, but otherwise very similar
|
|
if (charttype==GC_CHART_STACK && stackbarseries) {
|
|
|
|
// add first X axis we find
|
|
foreach(QAbstractAxis *axis, qchart->axes(Qt::Horizontal)) {
|
|
legend->addX(static_cast<QCategoryAxis*>(axis)->titleText(), false, "");
|
|
break;
|
|
}
|
|
|
|
// need to attach stackbarseries to the value axes
|
|
foreach(QAbstractAxis *axis, qchart->axes(Qt::Vertical))
|
|
stackbarseries->attachAxis(axis);
|
|
|
|
// and legend
|
|
foreach(QBarSet *set, barsets)
|
|
legend->addSeries(set->label(), set->color());
|
|
|
|
legend->setClickable(false);
|
|
}
|
|
|
|
// percent bar uses stackbarseries, but otherwise very similar
|
|
if (charttype==GC_CHART_PERCENT && percentbarseries) {
|
|
|
|
// add first X axis we find
|
|
foreach(QAbstractAxis *axis, qchart->axes(Qt::Horizontal)) {
|
|
legend->addX(static_cast<QCategoryAxis*>(axis)->titleText(), false, "");
|
|
break;
|
|
}
|
|
|
|
// need to attach stackbarseries to the value axes
|
|
foreach(QAbstractAxis *axis, qchart->axes(Qt::Vertical))
|
|
percentbarseries->attachAxis(axis);
|
|
|
|
// and legend
|
|
foreach(QBarSet *set, barsets)
|
|
legend->addSeries(set->label(), set->color());
|
|
|
|
legend->setClickable(false);
|
|
}
|
|
|
|
// install event filters on thes scene objects for Pie and Bar
|
|
// charts only, since for line/scatter we select and interact via
|
|
// collision detection (and don't want the double number of events).
|
|
if (charttype == GC_CHART_STACK || charttype == GC_CHART_STACK || charttype == GC_CHART_BAR || charttype == GC_CHART_PIE) {
|
|
|
|
// largely we just want the hover events coz they're handy
|
|
foreach(QGraphicsItem *item, chartview->scene()->items())
|
|
item->installSceneEventFilter(selector); // XXX create sceneitem to help us here!
|
|
|
|
}
|
|
|
|
// add labels after legend items
|
|
foreach(QLabel *label, labels) legend->addLabel(label);
|
|
|
|
// add voronoi if need to
|
|
plotVoronoi();
|
|
|
|
plotAreaChanged(); // make sure get updated before paint
|
|
}
|
|
|
|
bool
|
|
GenericPlot::configureAxis(QString name, bool visible, int align, double min, double max,
|
|
int type, QString labelcolor, QString color, bool log, QStringList categories)
|
|
{
|
|
GenericAxisInfo *axis = axisinfos.value(name);
|
|
if (axis == NULL) return false;
|
|
|
|
// lets update the settings then
|
|
axis->visible = visible;
|
|
|
|
// -1 if not passed
|
|
if (align == 0) axis->align = Qt::AlignBottom;
|
|
if (align == 1) axis->align = Qt::AlignLeft;
|
|
if (align == 2) axis->align = Qt::AlignTop;
|
|
if (align == 3) axis->align = Qt::AlignRight;
|
|
|
|
// -1 if not passed
|
|
if (min == -1) {
|
|
|
|
// automatically set the start/stop values for this axis- based upon
|
|
// the data in all the series attached to the axis
|
|
bool usey = axis->orientation == Qt::Vertical;
|
|
min=0;
|
|
bool setmin=false;
|
|
// min should be minimum value for all attached series
|
|
foreach(QAbstractSeries *series, axis->series) {
|
|
if (series->type() == QAbstractSeries::SeriesType::SeriesTypeScatter ||
|
|
series->type() == QAbstractSeries::SeriesType::SeriesTypeBar ||
|
|
series->type() == QAbstractSeries::SeriesType::SeriesTypeLine) {
|
|
foreach(QPointF point, static_cast<QXYSeries*>(series)->pointsVector()) {
|
|
if (usey) {
|
|
if (setmin && point.y() < min) min=point.y();
|
|
else if (!setmin) { min=point.y(); setmin=true; }
|
|
} else {
|
|
if (setmin && point.x() < min) min=point.x();
|
|
else if (!setmin) { min=point.x(); setmin=true; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
axis->minx = axis->miny = min;
|
|
|
|
if (max == -1) {
|
|
|
|
// automatically set the start/stop values for this axis- based upon
|
|
// the data in all the series attached to the axis
|
|
bool usey = axis->orientation == Qt::Vertical;
|
|
max=0;
|
|
bool setmax=false;
|
|
|
|
if (charttype != GC_CHART_STACK && charttype != GC_CHART_PERCENT) { // we insist on stacked being oriented this way
|
|
|
|
// min should be minimum value for all attached series
|
|
foreach(QAbstractSeries *series, axis->series) {
|
|
if (series->type() == QAbstractSeries::SeriesType::SeriesTypeScatter ||
|
|
series->type() == QAbstractSeries::SeriesType::SeriesTypeBar ||
|
|
series->type() == QAbstractSeries::SeriesType::SeriesTypeLine) {
|
|
foreach(QPointF point, static_cast<QXYSeries*>(series)->pointsVector()) {
|
|
if (usey) {
|
|
if (setmax && point.y() > max) max=point.y();
|
|
else if (!setmax) { max=point.y(); setmax=true; }
|
|
} else {
|
|
if (setmax && point.x() > max) max=point.x();
|
|
else if (!setmax) { max=point.x(); setmax=true; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
axis->maxx = axis->maxy = max;
|
|
|
|
// type
|
|
if (type != -1) axis->type = static_cast<GenericAxisInfo::AxisInfoType>(type);
|
|
else {
|
|
if (axis->orientation == Qt::Horizontal) {
|
|
// infer x-axis type from name, helps a little...
|
|
if (!axis->name.compare(tr("date"), Qt::CaseInsensitive)) axis->type = GenericAxisInfo::DATERANGE;
|
|
if (!axis->name.compare(tr("time"), Qt::CaseInsensitive)) axis->type = GenericAxisInfo::TIME;
|
|
}
|
|
}
|
|
|
|
// color
|
|
if (labelcolor != "") axis->labelcolor=RGBColor(QColor(labelcolor));
|
|
if (color != "") axis->axiscolor=RGBColor(QColor(color));
|
|
|
|
// log ..
|
|
axis->log = log;
|
|
if (min==0 && log) min = 1; // 0 are no-no on log axes
|
|
if (max==0 && log) max = 1; // 0 are no-no on log axes
|
|
|
|
// categories
|
|
if (categories.count()) axis->categories = categories;
|
|
|
|
return true;
|
|
}
|
|
|
|
QColor
|
|
GenericPlot::seriesColor(QAbstractSeries* series)
|
|
{
|
|
switch (series->type()) {
|
|
case QAbstractSeries::SeriesTypeScatter: return static_cast<QScatterSeries*>(series)->color(); break;
|
|
case QAbstractSeries::SeriesTypeLine: return static_cast<QLineSeries*>(series)->color(); break;
|
|
case QAbstractSeries::SeriesTypeArea: return static_cast<QAreaSeries*>(series)->color(); break;
|
|
default: return GColor(CPLOTMARKER);
|
|
}
|
|
}
|
|
|
|
void
|
|
GenericPlot::plotVoronoi()
|
|
{
|
|
// if there is one there already, lets remove it
|
|
clearVoronoi();
|
|
|
|
if (vx.count() < 2) return;
|
|
|
|
Voronoi v;
|
|
for(int i=0; i<vx.count(); i++) v.addSite(QPointF(vx[i],vy[i]));
|
|
v.run(QRectF());
|
|
|
|
#if 0
|
|
// how many lines?
|
|
fprintf(stderr, "voronoi diagram curve '%s' has %d lines\n", vname.toStdString().c_str(),v.lines().count()); fflush(stderr);
|
|
|
|
foreach(QLineF line, v.lines()) {
|
|
fprintf(stderr, "from %f,%f to %f,%f\n", line.p1().x(), line.p1().y(), line.p2().x(), line.p2().y());
|
|
}
|
|
#endif
|
|
|
|
// create a new diagram
|
|
voronoidiagram = new GenericLines(this);
|
|
voronoidiagram->setCurve(curves.value(vname,NULL));
|
|
voronoidiagram->setLines(v.lines());
|
|
|
|
chartview->scene()->addItem(voronoidiagram);
|
|
voronoidiagram->update();
|
|
}
|
|
|
|
void
|
|
GenericPlot::clearVoronoi()
|
|
{
|
|
if (voronoidiagram) {
|
|
voronoidiagram->setCurve(NULL);
|
|
voronoidiagram->setLines(QList<QLineF>());
|
|
voronoidiagram->prepare();
|
|
chartview->scene()->removeItem(voronoidiagram);
|
|
//delete voronoidiagram; // CRASH!
|
|
voronoidiagram = NULL;
|
|
}
|
|
}
|