mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-01-20 11:00:44 -06:00
891 lines
36 KiB
C++
891 lines
36 KiB
C++
#include "PlotDock.h"
|
|
#include "ui_PlotDock.h"
|
|
#include "Settings.h"
|
|
#include "sqlitetablemodel.h"
|
|
#include "FileDialog.h"
|
|
#include "MainWindow.h" // Just for BrowseDataTableSettings, not for the actual main window class
|
|
|
|
#include <QPrinter>
|
|
#include <QPrintPreviewDialog>
|
|
|
|
PlotDock::PlotDock(QWidget* parent)
|
|
: QDialog(parent),
|
|
ui(new Ui::PlotDock),
|
|
m_currentPlotModel(nullptr),
|
|
m_currentTableSettings(nullptr),
|
|
m_showLegend(false),
|
|
m_stackedBars(false)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
// Init widgets
|
|
ui->treePlotColumns->setSelectionMode(QAbstractItemView::NoSelection);
|
|
|
|
// Restore state
|
|
ui->splitterForPlot->restoreState(Settings::getValue("PlotDock", "splitterSize").toByteArray());
|
|
ui->comboLineType->setCurrentIndex(Settings::getValue("PlotDock", "lineType").toInt());
|
|
ui->comboPointShape->setCurrentIndex(Settings::getValue("PlotDock", "pointShape").toInt());
|
|
|
|
// Connect signals
|
|
connect(ui->treePlotColumns, &QTreeWidget::itemChanged, this, &PlotDock::on_treePlotColumns_itemChanged);
|
|
connect(ui->plotWidget, SIGNAL(selectionChangedByUser()), this, SLOT(selectionChanged()));
|
|
|
|
// connect slots that takes care that when an axis is selected, only that direction can be dragged and zoomed:
|
|
connect(ui->plotWidget, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(mousePress()));
|
|
connect(ui->plotWidget, SIGNAL(mouseWheel(QWheelEvent*)), this, SLOT(mouseWheel()));
|
|
|
|
// Enable: click on items to select them, Ctrl+Click for multi-selection, mouse-wheel for zooming and mouse drag for
|
|
// changing the visible range.
|
|
// Select one axis for zoom and drag applying only to that orientation.
|
|
ui->plotWidget->setInteractions(QCP::iSelectPlottables | QCP::iMultiSelect | QCP::iRangeZoom | QCP::iRangeDrag | QCP::iSelectAxes);
|
|
ui->plotWidget->setSelectionRectMode(QCP::srmNone);
|
|
|
|
QShortcut* shortcutCopy = new QShortcut(QKeySequence::Copy, ui->plotWidget, nullptr, nullptr, Qt::WidgetShortcut);
|
|
connect(shortcutCopy, SIGNAL(activated()), this, SLOT(copy()));
|
|
|
|
QShortcut* shortcutPrint = new QShortcut(QKeySequence::Print, ui->plotWidget, nullptr, nullptr, Qt::WidgetShortcut);
|
|
connect(shortcutPrint, &QShortcut::activated, this, &PlotDock::openPrintDialog);
|
|
|
|
ui->plotWidget->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
// Set up context menu
|
|
m_contextMenu = new QMenu(this);
|
|
|
|
QAction* copyAction = new QAction(QIcon(":/icons/copy"), tr("Copy"), m_contextMenu);
|
|
copyAction->setShortcut(shortcutCopy->key());
|
|
m_contextMenu->addAction(copyAction);
|
|
connect(copyAction, &QAction::triggered, [&]() {
|
|
copy();
|
|
});
|
|
|
|
QAction* printAction = new QAction(QIcon(":/icons/print"), tr("Print..."), m_contextMenu);
|
|
printAction->setShortcut(shortcutPrint->key());
|
|
m_contextMenu->addAction(printAction);
|
|
connect(printAction, &QAction::triggered, [&]() {
|
|
openPrintDialog();
|
|
});
|
|
|
|
QAction* showLegendAction = new QAction(tr("Show legend"), m_contextMenu);
|
|
showLegendAction->setCheckable(true);
|
|
m_contextMenu->addAction(showLegendAction);
|
|
|
|
connect(showLegendAction, SIGNAL(toggled(bool)), this, SLOT(toggleLegendVisible(bool)));
|
|
|
|
QAction* stackedBarsAction = new QAction(tr("Stacked bars"), m_contextMenu);
|
|
stackedBarsAction->setCheckable(true);
|
|
m_contextMenu->addAction(stackedBarsAction);
|
|
|
|
connect(stackedBarsAction, SIGNAL(toggled(bool)), this, SLOT(toggleStackedBars(bool)));
|
|
|
|
connect(ui->plotWidget, &QTableView::customContextMenuRequested,
|
|
[=](const QPoint& pos) {
|
|
// Show menu
|
|
m_contextMenu->popup(ui->plotWidget->mapToGlobal(pos));
|
|
});
|
|
|
|
}
|
|
|
|
PlotDock::~PlotDock()
|
|
{
|
|
// Save state
|
|
Settings::setValue("PlotDock", "splitterSize", ui->splitterForPlot->saveState());
|
|
Settings::setValue("PlotDock", "lineType", ui->comboLineType->currentIndex());
|
|
Settings::setValue("PlotDock", "pointShape", ui->comboPointShape->currentIndex());
|
|
|
|
// Finally, delete all widgets
|
|
delete ui;
|
|
}
|
|
|
|
void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* settings, bool update, bool keepOrResetSelection)
|
|
{
|
|
// Each column has an id that we use internally, starting from 0. However, at the beginning of the columns list we want to add
|
|
// the virtual 'Row #' column which needs a separate unique id for internal use. This id is defined here as -1.
|
|
const int RowNumId = -1;
|
|
|
|
// add columns to x/y selection tree widget
|
|
if(update)
|
|
{
|
|
// Store pointer to the current browse table settings in the main window
|
|
m_currentTableSettings = settings;
|
|
|
|
// disable tree plot columns item changed updates
|
|
ui->treePlotColumns->blockSignals(true);
|
|
|
|
m_currentPlotModel = model;
|
|
|
|
// save current selected columns, so we can restore them after the update
|
|
QString sItemX; // selected X column
|
|
QMap<QString, PlotSettings> mapItemsY; // selected Y columns with color
|
|
|
|
if(keepOrResetSelection)
|
|
{
|
|
// Store the currently selected plot columns to restore them later
|
|
for(int i = 0; i < ui->treePlotColumns->topLevelItemCount(); ++i)
|
|
{
|
|
QTreeWidgetItem* item = ui->treePlotColumns->topLevelItem(i);
|
|
if(item->checkState(PlotColumnX) == Qt::Checked)
|
|
sItemX = item->text(PlotColumnField);
|
|
|
|
if(item->checkState(PlotColumnY) == Qt::Checked)
|
|
mapItemsY[item->text(PlotColumnField)] = PlotSettings(0, 0, item->backgroundColor(PlotColumnY), item->checkState(PlotColumnY) == Qt::Checked);
|
|
}
|
|
} else {
|
|
// Get the plot columns to select from the stored browse table information
|
|
sItemX = m_currentTableSettings->plotXAxis;
|
|
|
|
mapItemsY = m_currentTableSettings->plotYAxes;
|
|
}
|
|
|
|
ui->treePlotColumns->clear();
|
|
|
|
if(model)
|
|
{
|
|
// Add each column with a supported data type to the column selection view
|
|
for(int i=0;i<model->columnCount();++i)
|
|
{
|
|
QVariant::Type columntype = guessDataType(model, i);
|
|
if(columntype != QVariant::Invalid)
|
|
{
|
|
QTreeWidgetItem* columnitem = new QTreeWidgetItem(ui->treePlotColumns);
|
|
|
|
switch (columntype) {
|
|
case QVariant::DateTime:
|
|
columnitem->setText(PlotColumnType, tr("Date/Time"));
|
|
break;
|
|
case QVariant::Date:
|
|
columnitem->setText(PlotColumnType, tr("Date"));
|
|
break;
|
|
case QVariant::Time:
|
|
columnitem->setText(PlotColumnType, tr("Time"));
|
|
break;
|
|
case QVariant::Double:
|
|
columnitem->setText(PlotColumnType, tr("Numeric"));
|
|
break;
|
|
case QVariant::String:
|
|
columnitem->setText(PlotColumnType, tr("Label"));
|
|
break;
|
|
default:
|
|
// This is not actually expected
|
|
columnitem->setText(PlotColumnType, tr("Invalid"));
|
|
}
|
|
|
|
// Store the model column index in the PlotColumnField and the type
|
|
// in the PlotColumnType, both using the User Role.
|
|
columnitem->setData(PlotColumnField, Qt::UserRole, i);
|
|
columnitem->setData(PlotColumnType, Qt::UserRole, static_cast<int>(columntype));
|
|
columnitem->setText(PlotColumnField, model->headerData(i, Qt::Horizontal).toString());
|
|
|
|
// restore previous check state
|
|
if(mapItemsY.contains(columnitem->text(PlotColumnField)))
|
|
{
|
|
columnitem->setCheckState(PlotColumnY, mapItemsY[columnitem->text(PlotColumnField)].active ? Qt::Checked : Qt::Unchecked);
|
|
columnitem->setBackgroundColor(PlotColumnY, mapItemsY[columnitem->text(PlotColumnField)].colour);
|
|
} else {
|
|
if (columntype == QVariant::Double)
|
|
columnitem->setCheckState(PlotColumnY, Qt::Unchecked);
|
|
}
|
|
|
|
if(sItemX == columnitem->text(PlotColumnField))
|
|
columnitem->setCheckState(PlotColumnX, Qt::Checked);
|
|
else
|
|
columnitem->setCheckState(PlotColumnX, Qt::Unchecked);
|
|
}
|
|
}
|
|
|
|
ui->treePlotColumns->resizeColumnToContents(PlotColumnField);
|
|
|
|
// Add a row number column at the beginning of the column list, but only when there were (other) columns added
|
|
if(ui->treePlotColumns->topLevelItemCount())
|
|
{
|
|
QTreeWidgetItem* columnitem = new QTreeWidgetItem(ui->treePlotColumns);
|
|
|
|
// Just set RowNumId in the user role information field here to somehow indicate what column this is
|
|
columnitem->setData(PlotColumnField, Qt::UserRole, RowNumId);
|
|
columnitem->setText(PlotColumnField, tr("Row #"));
|
|
columnitem->setData(PlotColumnType, Qt::UserRole, static_cast<int>(QVariant::Double));
|
|
columnitem->setText(PlotColumnType, tr("Numeric"));
|
|
|
|
// restore previous check state
|
|
if(mapItemsY.contains(columnitem->text(PlotColumnField)))
|
|
{
|
|
columnitem->setCheckState(PlotColumnY, mapItemsY[columnitem->text(PlotColumnField)].active ? Qt::Checked : Qt::Unchecked);
|
|
columnitem->setBackgroundColor(PlotColumnY, mapItemsY[columnitem->text(PlotColumnField)].colour);
|
|
} else {
|
|
columnitem->setCheckState(PlotColumnY, Qt::Unchecked);
|
|
}
|
|
if(sItemX == columnitem->text(PlotColumnField))
|
|
columnitem->setCheckState(PlotColumnX, Qt::Checked);
|
|
else
|
|
columnitem->setCheckState(PlotColumnX, Qt::Unchecked);
|
|
|
|
ui->treePlotColumns->takeTopLevelItem(ui->treePlotColumns->indexOfTopLevelItem(columnitem));
|
|
ui->treePlotColumns->insertTopLevelItem(0, columnitem);
|
|
}
|
|
}
|
|
|
|
ui->plotWidget->yAxis->setLabel("Y");
|
|
ui->plotWidget->xAxis->setLabel("X");
|
|
ui->treePlotColumns->blockSignals(false);
|
|
}
|
|
|
|
// search for the x axis select
|
|
QTreeWidgetItem* xitem = nullptr;
|
|
for(int i = 0; i < ui->treePlotColumns->topLevelItemCount(); ++i)
|
|
{
|
|
xitem = ui->treePlotColumns->topLevelItem(i);
|
|
if(xitem->checkState(PlotColumnX) == Qt::Checked)
|
|
break;
|
|
|
|
xitem = nullptr;
|
|
}
|
|
|
|
QStringList yAxisLabels;
|
|
|
|
// Clear graphs and axis labels
|
|
ui->plotWidget->clearPlottables();
|
|
ui->plotWidget->xAxis->setLabel(QString());
|
|
ui->plotWidget->yAxis->setLabel(QString());
|
|
|
|
if(xitem)
|
|
{
|
|
// regain the model column index and the datatype
|
|
// right now datatype is only important for X axis (Y is always numeric)
|
|
int x = xitem->data(PlotColumnField, Qt::UserRole).toInt();
|
|
int xtype = xitem->data(PlotColumnType, Qt::UserRole).toInt();
|
|
|
|
ui->plotWidget->xAxis->setTickLabelRotation(0);
|
|
|
|
// check if we have a x axis with datetime data
|
|
switch (xtype) {
|
|
case QVariant::Date: {
|
|
QSharedPointer<QCPAxisTickerDateTime> ticker(new QCPAxisTickerDateTime);
|
|
ticker->setDateTimeFormat("yyyy-MM-dd");
|
|
ui->plotWidget->xAxis->setTicker(ticker);
|
|
break;
|
|
}
|
|
case QVariant::DateTime: {
|
|
QSharedPointer<QCPAxisTickerDateTime> ticker(new QCPAxisTickerDateTime);
|
|
ticker->setDateTimeFormat("yyyy-MM-dd\nhh:mm:ss");
|
|
ui->plotWidget->xAxis->setTicker(ticker);
|
|
break;
|
|
}
|
|
case QVariant::Time: {
|
|
QSharedPointer<QCPAxisTickerDateTime> ticker(new QCPAxisTickerDateTime);
|
|
ticker->setDateTimeFormat("hh:mm:ss");
|
|
ticker->setDateTimeSpec(Qt::UTC);
|
|
ui->plotWidget->xAxis->setTicker(ticker);
|
|
break;
|
|
}
|
|
case QVariant::String: {
|
|
// Ticker is set when we have got the labels
|
|
ui->plotWidget->xAxis->setTickLabelRotation(60);
|
|
break;
|
|
}
|
|
default: {
|
|
QSharedPointer<QCPAxisTickerFixed> ticker(new QCPAxisTickerFixed);
|
|
ticker->setTickStepStrategy(QCPAxisTicker::tssReadability);
|
|
ticker->setScaleStrategy(QCPAxisTickerFixed::ssMultiples);
|
|
ui->plotWidget->xAxis->setTicker(ticker);
|
|
}
|
|
}
|
|
|
|
// add graph for each selected y axis
|
|
for(int i = 0; i < ui->treePlotColumns->topLevelItemCount(); ++i)
|
|
{
|
|
QTreeWidgetItem* item = ui->treePlotColumns->topLevelItem(i);
|
|
if(item->checkState((PlotColumnY)) == Qt::Checked)
|
|
{
|
|
// regain the model column index
|
|
int column = item->data(PlotColumnField, Qt::UserRole).toInt();
|
|
|
|
bool isSorted = true;
|
|
|
|
// prepare the data vectors for qcustomplot
|
|
// possible improvement might be a QVector subclass that directly
|
|
// access the model data, to save memory, we are copying here
|
|
|
|
auto nrows = model->rowCount();
|
|
|
|
QVector<double> xdata(nrows), ydata(nrows), tdata(nrows);
|
|
QVector<QString> labels;
|
|
for(int i = 0; i < nrows; ++i)
|
|
{
|
|
tdata[i] = i;
|
|
// convert x type axis if it's datetime
|
|
switch (xtype) {
|
|
case QVariant::DateTime:
|
|
case QVariant::Date: {
|
|
QString s = model->data(model->index(i, x)).toString();
|
|
QDateTime d = QDateTime::fromString(s, Qt::ISODate);
|
|
xdata[i] = d.toMSecsSinceEpoch() / 1000.0;
|
|
break;
|
|
}
|
|
case QVariant::Time: {
|
|
QString s = model->data(model->index(i, x)).toString();
|
|
QTime t = QTime::fromString(s);
|
|
xdata[i] = t.msecsSinceStartOfDay() / 1000.0;
|
|
break;
|
|
}
|
|
case QVariant::String: {
|
|
xdata[i] = i+1;
|
|
labels << model->data(model->index(i, x)).toString();
|
|
break;
|
|
}
|
|
default: {
|
|
// Get the x value for this point. If the selected column is -1, i.e. the row number, just use the current row number from the loop
|
|
// instead of retrieving some value from the model.
|
|
if(x == RowNumId)
|
|
xdata[i] = i+1;
|
|
|
|
else
|
|
xdata[i] = model->data(model->index(i, x)).toDouble();
|
|
}
|
|
}
|
|
|
|
if (i != 0)
|
|
isSorted &= (xdata[i-1] <= xdata[i]);
|
|
|
|
// Get the y value for this point. If the selected column is -1, i.e. the row number, just use the current row number from the loop
|
|
// instead of retrieving some value from the model.
|
|
QVariant pointdata;
|
|
if(column == RowNumId)
|
|
pointdata = i+1;
|
|
else
|
|
pointdata = model->data(model->index(i, column), Qt::EditRole);
|
|
|
|
if(pointdata.isNull())
|
|
ydata[i] = qQNaN();
|
|
else
|
|
ydata[i] = pointdata.toDouble();
|
|
}
|
|
|
|
// Line type and point shape are not supported by the String X type (Bars)
|
|
ui->comboLineType->setEnabled(xtype != QVariant::String);
|
|
ui->comboPointShape->setEnabled(xtype != QVariant::String);
|
|
|
|
// WARN: ssDot is removed
|
|
int shapeIdx = ui->comboPointShape->currentIndex();
|
|
if (shapeIdx > 0) shapeIdx += 1;
|
|
QCPScatterStyle scatterStyle = QCPScatterStyle(static_cast<QCPScatterStyle::ScatterShape>(shapeIdx), 5);
|
|
|
|
QCPAbstractPlottable* plottable;
|
|
// When the X type is String, we draw a bar chart.
|
|
// When it is already sorted by x, we draw a graph.
|
|
// When it is not sorted by x, we draw a curve, so the order selected by the user in the table or in the query is
|
|
// respected. In this case the line will have loops and only None and Line is supported as line style.
|
|
// TODO: how to make the user aware of this without disturbing.
|
|
if (xtype == QVariant::String) {
|
|
QCPBars* bars = new QCPBars(ui->plotWidget->xAxis, ui->plotWidget->yAxis);
|
|
plottable = bars;
|
|
bars->setData(xdata, ydata);
|
|
// Set ticker once
|
|
if (ui->plotWidget->plottableCount() == 1) {
|
|
QSharedPointer<QCPAxisTickerText> ticker(new QCPAxisTickerText);
|
|
ticker->addTicks(xdata, labels);
|
|
ui->plotWidget->xAxis->setTicker(ticker);
|
|
}
|
|
QColor color = item->backgroundColor(PlotColumnY);
|
|
bars->setBrush(color);
|
|
plottable->setPen(QPen(color.darker(150)));
|
|
} else {
|
|
if (isSorted) {
|
|
QCPGraph* graph = ui->plotWidget->addGraph();
|
|
plottable = graph;
|
|
graph->setData(xdata, ydata, /*alreadySorted*/ true);
|
|
// set some graph styles not supported by the abstract plottable
|
|
graph->setLineStyle((QCPGraph::LineStyle) ui->comboLineType->currentIndex());
|
|
graph->setScatterStyle(scatterStyle);
|
|
} else {
|
|
QCPCurve* curve = new QCPCurve(ui->plotWidget->xAxis, ui->plotWidget->yAxis);
|
|
plottable = curve;
|
|
curve->setData(tdata, xdata, ydata, /*alreadySorted*/ true);
|
|
// set some curve styles not supported by the abstract plottable
|
|
if (ui->comboLineType->currentIndex() == QCPCurve::lsNone)
|
|
curve->setLineStyle(QCPCurve::lsNone);
|
|
else
|
|
curve->setLineStyle(QCPCurve::lsLine);
|
|
curve->setScatterStyle(scatterStyle);
|
|
}
|
|
plottable->setPen(QPen(item->backgroundColor(PlotColumnY)));
|
|
}
|
|
|
|
plottable->setSelectable(QCP::stDataRange);
|
|
plottable->setName(item->text(PlotColumnField));
|
|
|
|
// gather Y label column names
|
|
if(column == RowNumId)
|
|
yAxisLabels << tr("Row #");
|
|
else
|
|
yAxisLabels << model->headerData(column, Qt::Horizontal).toString();
|
|
}
|
|
}
|
|
|
|
ui->plotWidget->rescaleAxes(true);
|
|
ui->plotWidget->legend->setVisible(m_showLegend);
|
|
// Legend with slightly transparent background brush:
|
|
ui->plotWidget->legend->setBrush(QColor(255, 255, 255, 150));
|
|
|
|
// set axis labels
|
|
if(x == RowNumId)
|
|
ui->plotWidget->xAxis->setLabel(tr("Row #"));
|
|
else
|
|
ui->plotWidget->xAxis->setLabel(model->headerData(x, Qt::Horizontal).toString());
|
|
ui->plotWidget->yAxis->setLabel(yAxisLabels.join("|"));
|
|
}
|
|
|
|
adjustBars();
|
|
ui->plotWidget->replot();
|
|
|
|
// Warn user if not all data has been fetched and hint about the button for loading all the data
|
|
if (model && (model->rowCountAvailable() != SqliteTableModel::RowCount::Complete || !model->isCacheComplete())) {
|
|
ui->buttonLoadAllData->setEnabled(true);
|
|
ui->buttonLoadAllData->setStyleSheet("QToolButton {color: white; background-color: rgb(255, 102, 102)}");
|
|
ui->buttonLoadAllData->setToolTip(tr("Load all data and redraw plot.\n"
|
|
"Warning: not all data has been fetched from the table yet due to the partial fetch mechanism."));
|
|
} else {
|
|
ui->buttonLoadAllData->setEnabled(false);
|
|
ui->buttonLoadAllData->setStyleSheet("");
|
|
ui->buttonLoadAllData->setToolTip(tr("Load all data and redraw plot"));
|
|
}
|
|
}
|
|
|
|
void PlotDock::resetPlot()
|
|
{
|
|
updatePlot(nullptr);
|
|
}
|
|
|
|
void PlotDock::on_treePlotColumns_itemChanged(QTreeWidgetItem* changeitem, int column)
|
|
{
|
|
// disable change updates, or we get unwanted redrawing and weird behavior
|
|
ui->treePlotColumns->blockSignals(true);
|
|
|
|
// make sure only 1 X axis is selected
|
|
if(column == PlotColumnX)
|
|
{
|
|
for(int i = 0; i < ui->treePlotColumns->topLevelItemCount(); ++i)
|
|
{
|
|
QTreeWidgetItem* item = ui->treePlotColumns->topLevelItem(i);
|
|
if(item->checkState(column) == Qt::Checked && item != changeitem)
|
|
{
|
|
item->setCheckState(column, Qt::Unchecked);
|
|
}
|
|
}
|
|
|
|
// Save settings for this table
|
|
if(m_currentTableSettings)
|
|
{
|
|
if(changeitem->checkState(column) == Qt::Checked)
|
|
m_currentTableSettings->plotXAxis = changeitem->text(PlotColumnField);
|
|
else
|
|
m_currentTableSettings->plotXAxis = QString();
|
|
}
|
|
} else if(column == PlotColumnY) {
|
|
// Save check state of this column
|
|
if(m_currentTableSettings)
|
|
{
|
|
PlotSettings& plot_settings = m_currentTableSettings->plotYAxes[changeitem->text(PlotColumnField)];
|
|
plot_settings.active = (changeitem->checkState(column) == Qt::Checked);
|
|
}
|
|
|
|
if(changeitem->checkState(column) == Qt::Checked)
|
|
{
|
|
// Generate a default colour if none isn't set yet
|
|
QColor colour = changeitem->backgroundColor(column);
|
|
if(!colour.isValid())
|
|
{
|
|
static int last_colour_index = 0;
|
|
switch(last_colour_index++)
|
|
{
|
|
case 0:
|
|
colour = QColor(0, 69, 134);
|
|
break;
|
|
case 1:
|
|
colour = QColor(255, 66, 14);
|
|
break;
|
|
case 2:
|
|
colour = QColor(255, 211, 32);
|
|
break;
|
|
case 3:
|
|
colour = QColor(87, 157, 28);
|
|
break;
|
|
case 4:
|
|
colour = QColor(126, 0, 33);
|
|
break;
|
|
case 5:
|
|
colour = QColor(131, 202, 255);
|
|
break;
|
|
case 6:
|
|
colour = QColor(49, 64, 4);
|
|
break;
|
|
case 7:
|
|
colour = QColor(174, 207, 0);
|
|
break;
|
|
case 8:
|
|
colour = QColor(75, 31, 111);
|
|
break;
|
|
case 9:
|
|
colour = QColor(255, 149, 14);
|
|
break;
|
|
case 10:
|
|
colour = QColor(197, 00, 11);
|
|
break;
|
|
case 11:
|
|
colour = QColor(0, 132, 209);
|
|
|
|
// Since this is the last colour in our table, reset the counter back
|
|
// to the first colour
|
|
last_colour_index = 0;
|
|
break;
|
|
default:
|
|
// NOTE: This shouldn't happen!
|
|
colour = QColor(0, 0, 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Set colour
|
|
changeitem->setBackgroundColor(column, colour);
|
|
|
|
// Save settings for this table
|
|
if(m_currentTableSettings)
|
|
{
|
|
PlotSettings& plot_settings = m_currentTableSettings->plotYAxes[changeitem->text(PlotColumnField)];
|
|
plot_settings.colour = colour;
|
|
plot_settings.lineStyle = ui->comboLineType->currentIndex();
|
|
plot_settings.pointShape = (ui->comboPointShape->currentIndex() > 0 ? (ui->comboPointShape->currentIndex()+1) : ui->comboPointShape->currentIndex());
|
|
}
|
|
}
|
|
}
|
|
|
|
ui->treePlotColumns->blockSignals(false);
|
|
|
|
updatePlot(m_currentPlotModel, m_currentTableSettings, false);
|
|
}
|
|
|
|
void PlotDock::on_treePlotColumns_itemDoubleClicked(QTreeWidgetItem* item, int column)
|
|
{
|
|
// disable change updates, or we get unwanted redrawing and weird behavior
|
|
ui->treePlotColumns->blockSignals(true);
|
|
|
|
int type = item->data(PlotColumnType, Qt::UserRole).toInt();
|
|
|
|
if(column == PlotColumnY && type == QVariant::Double)
|
|
{
|
|
// On double click open the colordialog
|
|
QColorDialog colordialog(this);
|
|
QColor curbkcolor = item->backgroundColor(column);
|
|
QColor precolor = !curbkcolor.isValid() ? (Qt::GlobalColor)(qrand() % 13 + 5) : curbkcolor;
|
|
QColor color = colordialog.getColor(precolor, this, tr("Choose an axis color"));
|
|
if(color.isValid())
|
|
{
|
|
item->setCheckState(column, Qt::Checked);
|
|
item->setBackgroundColor(column, color);
|
|
|
|
// Save settings for this table
|
|
if(m_currentTableSettings)
|
|
{
|
|
PlotSettings& plot_settings = m_currentTableSettings->plotYAxes[item->text(PlotColumnField)];
|
|
plot_settings.active = (item->checkState(column) == Qt::Checked);
|
|
plot_settings.colour = color;
|
|
plot_settings.lineStyle = ui->comboLineType->currentIndex();
|
|
plot_settings.pointShape = (ui->comboPointShape->currentIndex() > 0 ? (ui->comboPointShape->currentIndex()+1) : ui->comboPointShape->currentIndex());
|
|
}
|
|
} else {
|
|
item->setCheckState(column, Qt::Unchecked);
|
|
|
|
// Save settings for this table
|
|
if(m_currentTableSettings)
|
|
m_currentTableSettings->plotYAxes.remove(item->text(PlotColumnField));
|
|
}
|
|
}
|
|
|
|
ui->treePlotColumns->blockSignals(false);
|
|
|
|
updatePlot(m_currentPlotModel, m_currentTableSettings, false);
|
|
}
|
|
|
|
void PlotDock::on_butSavePlot_clicked()
|
|
{
|
|
QString fileName = FileDialog::getSaveFileName(this,
|
|
tr("Choose a filename to save under"),
|
|
tr("PNG(*.png);;JPG(*.jpg);;PDF(*.pdf);;BMP(*.bmp);;All Files(*)")
|
|
);
|
|
if(!fileName.isEmpty())
|
|
{
|
|
if(fileName.endsWith(".png", Qt::CaseInsensitive))
|
|
{
|
|
ui->plotWidget->savePng(fileName);
|
|
}
|
|
else if(fileName.endsWith(".jpg", Qt::CaseInsensitive))
|
|
{
|
|
ui->plotWidget->saveJpg(fileName);
|
|
}
|
|
else if(fileName.endsWith(".pdf", Qt::CaseInsensitive))
|
|
{
|
|
ui->plotWidget->savePdf(fileName);
|
|
}
|
|
else if(fileName.endsWith(".bmp", Qt::CaseInsensitive))
|
|
{
|
|
ui->plotWidget->saveBmp(fileName);
|
|
}
|
|
else
|
|
{
|
|
fileName += ".png";
|
|
ui->plotWidget->savePng(fileName);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlotDock::on_comboLineType_currentIndexChanged(int index)
|
|
{
|
|
Q_ASSERT(index >= QCPGraph::lsNone &&
|
|
index <= QCPGraph::lsImpulse);
|
|
|
|
bool hasCurves = (ui->plotWidget->plottableCount() > ui->plotWidget->graphCount());
|
|
QCPGraph::LineStyle lineStyle = (QCPGraph::LineStyle) index;
|
|
if (lineStyle > QCPGraph::lsLine && hasCurves) {
|
|
QMessageBox::warning(this, qApp->applicationName(),
|
|
tr("There are curves in this plot and the selected line style can only be applied to graphs sorted by X. "
|
|
"Either sort the table or query by X to remove curves or select one of the styles supported by curves: "
|
|
"None or Line."));
|
|
return;
|
|
}
|
|
for (int i = 0, ie = ui->plotWidget->graphCount(); i < ie; ++i)
|
|
{
|
|
QCPGraph * graph = ui->plotWidget->graph(i);
|
|
if (graph)
|
|
graph->setLineStyle(lineStyle);
|
|
}
|
|
// We have changed the style only for graphs, but not for curves.
|
|
// If there are any in the plot, we have to update it completely in order to apply the new style
|
|
if (hasCurves)
|
|
updatePlot(m_currentPlotModel, m_currentTableSettings, false);
|
|
else
|
|
ui->plotWidget->replot();
|
|
|
|
// Save settings for this table
|
|
if(m_currentTableSettings)
|
|
{
|
|
QMap<QString, PlotSettings>& graphs = m_currentTableSettings->plotYAxes;
|
|
auto it = graphs.begin();
|
|
while(it != graphs.end())
|
|
{
|
|
it.value().lineStyle = lineStyle;
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlotDock::on_comboPointShape_currentIndexChanged(int index)
|
|
{
|
|
// WARN: because ssDot point shape is removed
|
|
if (index > 0) index += 1;
|
|
Q_ASSERT(index >= QCPScatterStyle::ssNone &&
|
|
index < QCPScatterStyle::ssPixmap);
|
|
|
|
bool hasCurves = (ui->plotWidget->plottableCount() > ui->plotWidget->graphCount());
|
|
QCPScatterStyle::ScatterShape shape = (QCPScatterStyle::ScatterShape) index;
|
|
for (int i = 0, ie = ui->plotWidget->graphCount(); i < ie; ++i)
|
|
{
|
|
QCPGraph * graph = ui->plotWidget->graph(i);
|
|
if (graph)
|
|
graph->setScatterStyle(QCPScatterStyle(shape, 5));
|
|
}
|
|
// We have changed the style only for graphs, but not for curves.
|
|
// If there are any in the plot, we have to update it completely in order to apply the new style
|
|
if (hasCurves)
|
|
updatePlot(m_currentPlotModel, m_currentTableSettings, false);
|
|
else
|
|
ui->plotWidget->replot();
|
|
|
|
// Save settings for this table
|
|
if(m_currentTableSettings)
|
|
{
|
|
QMap<QString, PlotSettings>& graphs = m_currentTableSettings->plotYAxes;
|
|
auto it = graphs.begin();
|
|
while(it != graphs.end())
|
|
{
|
|
it.value().pointShape = shape;
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVariant::Type PlotDock::guessDataType(SqliteTableModel* model, int column)
|
|
{
|
|
QVariant::Type type = QVariant::Invalid;
|
|
for(int i = 0; i < std::min(10, model->rowCount()) && type != QVariant::String; ++i)
|
|
{
|
|
QVariant data = model->data(model->index(i, column), Qt::EditRole);
|
|
if(data.isNull() || data.convert(QVariant::Double))
|
|
{
|
|
type = QVariant::Double;
|
|
} else {
|
|
QString s = model->data(model->index(i, column)).toString();
|
|
QDateTime dt = QDateTime::fromString(s, Qt::ISODate);
|
|
QTime t = QTime::fromString(s);
|
|
if (dt.isValid())
|
|
// Since the way to discriminate dates with times and pure dates is that the time part is 0, we must take into account
|
|
// that some DateTimes could have "00:00:00" as time part and still the entire column has time information, so a single
|
|
// final Date should not set the type to Date if it has already been guessed as DateTime.
|
|
if (type != QVariant::DateTime && dt.time().msecsSinceStartOfDay() == 0)
|
|
type = QVariant::Date;
|
|
else
|
|
type = QVariant::DateTime;
|
|
else if (t.isValid())
|
|
type = QVariant::Time;
|
|
else
|
|
type = QVariant::String;
|
|
}
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
void PlotDock::fetchAllData()
|
|
{
|
|
if(m_currentPlotModel)
|
|
{
|
|
// Make sure all data is loaded
|
|
m_currentPlotModel->completeCache();
|
|
|
|
// Update plot
|
|
updatePlot(m_currentPlotModel, m_currentTableSettings);
|
|
}
|
|
}
|
|
|
|
void PlotDock::selectionChanged()
|
|
{
|
|
|
|
for (QCPAbstractPlottable* plottable : ui->plotWidget->selectedPlottables()) {
|
|
|
|
for (QCPDataRange dataRange : plottable->selection().dataRanges()) {
|
|
|
|
int index = dataRange.begin();
|
|
if (dataRange.length() != 0) {
|
|
emit pointsSelected(index, dataRange.length());
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
void PlotDock::mousePress()
|
|
{
|
|
// Allow user to reset the plot
|
|
ui->buttonLoadAllData->setEnabled(true);
|
|
|
|
// if an axis (or axis labels) is selected, only allow the direction of that axis to be dragged
|
|
// if no axis (or axis labels) is selected, both directions may be dragged
|
|
if (ui->plotWidget->xAxis->selectedParts().testFlag(QCPAxis::spAxis) ||
|
|
ui->plotWidget->xAxis->selectedParts().testFlag(QCPAxis::spTickLabels) ||
|
|
ui->plotWidget->xAxis->selectedParts().testFlag(QCPAxis::spAxisLabel))
|
|
ui->plotWidget->axisRect()->setRangeDrag(ui->plotWidget->xAxis->orientation());
|
|
else if (ui->plotWidget->yAxis->selectedParts().testFlag(QCPAxis::spAxis) ||
|
|
ui->plotWidget->yAxis->selectedParts().testFlag(QCPAxis::spTickLabels) ||
|
|
ui->plotWidget->yAxis->selectedParts().testFlag(QCPAxis::spAxisLabel))
|
|
ui->plotWidget->axisRect()->setRangeDrag(ui->plotWidget->yAxis->orientation());
|
|
else
|
|
ui->plotWidget->axisRect()->setRangeDrag(Qt::Horizontal | Qt::Vertical);
|
|
}
|
|
|
|
void PlotDock::mouseWheel()
|
|
{
|
|
// Allow user to reset the plot
|
|
ui->buttonLoadAllData->setEnabled(true);
|
|
|
|
// if an axis (or axis labels) is selected, only allow the direction of that axis to be zoomed
|
|
// if no axis (or axis labels) is selected, both directions may be zoomed
|
|
if (ui->plotWidget->xAxis->selectedParts().testFlag(QCPAxis::spAxis) ||
|
|
ui->plotWidget->xAxis->selectedParts().testFlag(QCPAxis::spTickLabels) ||
|
|
ui->plotWidget->xAxis->selectedParts().testFlag(QCPAxis::spAxisLabel))
|
|
ui->plotWidget->axisRect()->setRangeZoom(ui->plotWidget->xAxis->orientation());
|
|
else if (ui->plotWidget->yAxis->selectedParts().testFlag(QCPAxis::spAxis) ||
|
|
ui->plotWidget->yAxis->selectedParts().testFlag(QCPAxis::spTickLabels) ||
|
|
ui->plotWidget->yAxis->selectedParts().testFlag(QCPAxis::spAxisLabel))
|
|
ui->plotWidget->axisRect()->setRangeZoom(ui->plotWidget->yAxis->orientation());
|
|
else
|
|
ui->plotWidget->axisRect()->setRangeZoom(Qt::Horizontal | Qt::Vertical);
|
|
}
|
|
|
|
void PlotDock::copy()
|
|
{
|
|
QApplication::clipboard()->setPixmap(ui->plotWidget->toPixmap());
|
|
}
|
|
|
|
void PlotDock::toggleLegendVisible(bool visible)
|
|
{
|
|
m_showLegend = visible;
|
|
ui->plotWidget->legend->setVisible(m_showLegend);
|
|
ui->plotWidget->replot();
|
|
}
|
|
|
|
// Stack or group bars and set the appropiate bar width (since it is not automatically done by QCustomPlot).
|
|
void PlotDock::adjustBars()
|
|
{
|
|
const double padding = 0.15;
|
|
const double groupedWidth = ui->plotWidget->plottableCount()? 1.0 / ui->plotWidget->plottableCount() : 0.0;
|
|
QCPBars* previousBar = nullptr;
|
|
QCPBarsGroup* barsGroup = m_stackedBars? nullptr : new QCPBarsGroup(ui->plotWidget);
|
|
for (int i = 0, ie = ui->plotWidget->plottableCount(); i < ie; ++i)
|
|
{
|
|
QCPBars* bar = qobject_cast<QCPBars*>(ui->plotWidget->plottable(i));
|
|
if (bar) {
|
|
if (m_stackedBars) {
|
|
// Ungroup if grouped
|
|
bar->setBarsGroup(nullptr);
|
|
if (previousBar)
|
|
bar->moveAbove(previousBar);
|
|
// Set width to ocuppy the full coordinate space, less padding
|
|
bar->setWidth(1.0 - padding);
|
|
} else {
|
|
// Unstack if stacked
|
|
bar->moveAbove(nullptr);
|
|
bar->setBarsGroup(barsGroup);
|
|
// Set width to a plot coordinate width, less padding
|
|
bar->setWidth(groupedWidth - padding);
|
|
}
|
|
previousBar = bar;
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlotDock::toggleStackedBars(bool stacked)
|
|
{
|
|
m_stackedBars = stacked;
|
|
adjustBars();
|
|
ui->plotWidget->replot();
|
|
}
|
|
|
|
void PlotDock::reject()
|
|
{
|
|
// We override this, to ensure the Escape key doesn't make this dialog
|
|
// dock go away
|
|
return;
|
|
}
|
|
|
|
void PlotDock::openPrintDialog()
|
|
{
|
|
QPrinter printer;
|
|
QPrintPreviewDialog previewDialog(&printer, this);
|
|
connect(&previewDialog, &QPrintPreviewDialog::paintRequested, this, &PlotDock::renderPlot);
|
|
previewDialog.exec();
|
|
}
|
|
|
|
void PlotDock::renderPlot(QPrinter* printer)
|
|
{
|
|
QCPPainter painter(printer);
|
|
QRectF pageRect = printer->pageRect(QPrinter::DevicePixel);
|
|
|
|
int plotWidth = ui->plotWidget->viewport().width();
|
|
int plotHeight = ui->plotWidget->viewport().height();
|
|
double scale = pageRect.width()/(double)plotWidth;
|
|
|
|
painter.setMode(QCPPainter::pmVectorized);
|
|
painter.setMode(QCPPainter::pmNoCaching);
|
|
|
|
painter.scale(scale, scale);
|
|
ui->plotWidget->toPainter(&painter, plotWidth, plotHeight);
|
|
}
|