From 88ccd48173c5efddbd4cce98e5da6d954857d98b Mon Sep 17 00:00:00 2001 From: Peinthor Rene Date: Sat, 19 Apr 2014 22:19:32 +0200 Subject: [PATCH] plotting: add a simple mechanism to visualize data added a plotting dock, where you can select x and y axis and it will draw a chart out of it --- CMakeLists.txt | 4 +- src/MainWindow.cpp | 165 +++++++++++++++++++++++++++++++++++++++++++++ src/MainWindow.h | 4 ++ src/MainWindow.ui | 81 ++++++++++++++++++++-- src/src.pro | 8 ++- 5 files changed, 253 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 95bf59db..95dc02f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ set(SQLB_MOC_HDR src/sqlitetablemodel.h src/sqltextedit.h src/DbStructureModel.h + libs/qcustomplot-source/qcustomplot.h ) set(SQLB_SRC @@ -72,6 +73,7 @@ set(SQLB_SRC src/DbStructureModel.cpp src/grammar/Sqlite3Lexer.cpp src/grammar/Sqlite3Parser.cpp + libs/qcustomplot-source/qcustomplot.cpp src/main.cpp ) @@ -141,7 +143,7 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR} ${ANTLR_DIR} ${QHEXEDIT_DIR} src add_executable(${PROJECT_NAME} ${SQLB_HDR} ${SQLB_SRC} ${SQLB_FORM_HDR} ${SQLB_MOC} ${SQLB_RESOURCES_RCC}) if(USE_QT5) - qt5_use_modules(${PROJECT_NAME} Gui Widgets Network Test) + qt5_use_modules(${PROJECT_NAME} Gui Widgets PrintSupport Network Test) set(QT_LIBRARIES "") endif() add_dependencies(${PROJECT_NAME} antlr qhexedit) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index e1899ad2..e51d06e4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -105,6 +105,11 @@ void MainWindow::init() ui->viewMenu->actions().at(0)->setIcon(QIcon(":/icons/log_dock")); ui->viewDBToolbarAction->setChecked(!ui->toolbarDB->isHidden()); + // Plot view menu + ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockPlot->toggleViewAction()); + ui->viewMenu->actions().at(1)->setShortcut(QKeySequence(tr("Ctrl+P"))); + ui->viewMenu->actions().at(1)->setIcon(QIcon(":/icons/log_dock")); + // Set statusbar fields statusEncodingLabel = new QLabel(ui->statusbar); statusEncodingLabel->setEnabled(false); @@ -123,6 +128,7 @@ void MainWindow::init() restoreGeometry(PreferencesDialog::getSettingsValue("MainWindow", "geometry").toByteArray()); restoreState(PreferencesDialog::getSettingsValue("MainWindow", "windowState").toByteArray()); ui->comboLogSubmittedBy->setCurrentIndex(ui->comboLogSubmittedBy->findText(PreferencesDialog::getSettingsValue("SQLLogDock", "Log").toString())); + ui->splitterForPlot->restoreState(PreferencesDialog::getSettingsValue("PlotDock", "splitterSize").toByteArray()); // Set other window settings setAcceptDrops(true); @@ -308,6 +314,9 @@ void MainWindow::populateTable( const QString & tablename) if(editWin) editWin->reset(); + // update plot + updatePlot(m_browseTableModel); + QApplication::restoreOverrideCursor(); } @@ -363,6 +372,7 @@ void MainWindow::closeEvent( QCloseEvent* event ) PreferencesDialog::setSettingsValue("MainWindow", "geometry", saveGeometry()); PreferencesDialog::setSettingsValue("MainWindow", "windowState", saveState()); PreferencesDialog::setSettingsValue("SQLLogDock", "Log", ui->comboLogSubmittedBy->currentText()); + PreferencesDialog::setSettingsValue("PlotDock", "splitterSize", ui->splitterForPlot->saveState()); clearCompleterModelsFields(); QMainWindow::closeEvent(event); } @@ -699,6 +709,7 @@ void MainWindow::executeQuery() } } while( tail && *tail != 0 && (sql3status == SQLITE_OK || sql3status == SQLITE_DONE)); sqlWidget->finishExecution(statusMessage); + updatePlot(sqlWidget->getModel()); if(!modified && !wasdirty) db.revert(); // better rollback, if the logic is not enough we can tune it. @@ -1308,3 +1319,157 @@ void MainWindow::httpresponse(QNetworkReply *reply) } } } + +namespace { +/*! + * \brief guessdatatype try to parse the first 10 rows and decide the datatype + * \param model model to check the data + * \param column index of the column to check + * \return the guessed datatype + */ +QVariant::Type 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)); + if(data.convert(QVariant::Double)) + { + type = QVariant::Double; + } + else + { + QString s = model->data(model->index(i, column)).toString(); + QDate d = QDate::fromString(s, Qt::ISODate); + if(d.isValid()) + type = QVariant::DateTime; + else + type = QVariant::String; + } + + } + return type; +} +} + +void MainWindow::updatePlot(SqliteTableModel *model, bool update) +{ + // add columns to x/y seleciton tree widget + if(update) + { + disconnect(ui->treePlotColumns, SIGNAL(itemChanged(QTreeWidgetItem*,int)), + this,SLOT(on_treePlotColumns_itemChanged(QTreeWidgetItem*,int))); + + m_currentPlotModel = model; + ui->treePlotColumns->clear(); + + for(int i = 0; i < model->columnCount(); ++i) + { + QVariant::Type columntype = guessdatatype(model, i); + if(columntype != QVariant::String && columntype != QVariant::Invalid) + { + QTreeWidgetItem* columnitem = new QTreeWidgetItem(ui->treePlotColumns); + // maybe i make this more complicated than i should + // but store the model column index in the first 16 bit and the type + // in the other 16 bits + uint itemdata = 0; + itemdata = i << 16; + itemdata |= columntype; + columnitem->setData(0, Qt::UserRole, itemdata); + + columnitem->setText(0, model->headerData(i, Qt::Horizontal).toString()); + columnitem->setCheckState(1, Qt::Unchecked); + columnitem->setCheckState(2, Qt::Unchecked); + ui->treePlotColumns->addTopLevelItem(columnitem); + } + } + + ui->plotWidget->yAxis->setLabel("Y"); + ui->plotWidget->xAxis->setLabel("X"); + connect(ui->treePlotColumns, SIGNAL(itemChanged(QTreeWidgetItem*,int)),this,SLOT(on_treePlotColumns_itemChanged(QTreeWidgetItem*,int))); + } + + // search for the x axis select + QTreeWidgetItem* xitem = 0; + for(int i = 0; i < ui->treePlotColumns->topLevelItemCount(); ++i) + { + xitem = ui->treePlotColumns->topLevelItem(i); + if(xitem->checkState(2) == Qt::Checked) + break; + + xitem = 0; + } + + QStringList yAxisLabels; + QVector colors; + colors << Qt::blue << Qt::red << Qt::green << Qt::darkYellow << Qt::darkCyan << Qt::darkGray; + + ui->plotWidget->clearGraphs(); + if(xitem) + { + uint xitemdata = xitem->data(0, Qt::UserRole).toUInt(); + int x = xitemdata >> 16; + int xtype = xitemdata & (uint)0xFF; + + + // check if we have a x axis with datetime data + if(xtype == QVariant::DateTime) + { + ui->plotWidget->xAxis->setTickLabelType(QCPAxis::ltDateTime); + ui->plotWidget->xAxis->setDateTimeFormat("yyyy-mm-dd"); + } + else + { + ui->plotWidget->xAxis->setTickLabelType(QCPAxis::ltNumber); + } + + // 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((1)) == Qt::Checked && ui->plotWidget->graphCount() < colors.size()) + { + uint itemdata = item->data(0, Qt::UserRole).toUInt(); + int column = itemdata >> 16; + QCPGraph* graph = ui->plotWidget->addGraph(); + + int y = column; + + graph->setPen(QPen(colors[y])); + + QVector xdata(model->rowCount()), ydata(model->rowCount()); + for(int i = 0; i < model->rowCount(); ++i) + { + // convert x type axis if it's datetime + if(xtype == QVariant::DateTime) + { + QString s = model->data(model->index(i, x)).toString(); + QDateTime d = QDateTime::fromString(s, Qt::ISODate); + xdata[i] = d.toTime_t(); + } + else + { + xdata[i] = model->data(model->index(i, x)).toDouble(); + } + + ydata[i] = model->data(model->index(i, y)).toDouble(); + } + graph->setData(xdata, ydata); + graph->setLineStyle(QCPGraph::lsLine); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 5)); + yAxisLabels << model->headerData(y, Qt::Horizontal).toString(); + graph->rescaleAxes(); + } + } + + // set axis labels + ui->plotWidget->xAxis->setLabel(model->headerData(x, Qt::Horizontal).toString()); + ui->plotWidget->yAxis->setLabel(yAxisLabels.join("|")); + } + ui->plotWidget->replot(); +} + +void MainWindow::on_treePlotColumns_itemChanged(QTreeWidgetItem *item, int column) +{ + updatePlot(m_currentPlotModel, false); +} diff --git a/src/MainWindow.h b/src/MainWindow.h index 9c76749d..3f820387 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -18,6 +18,7 @@ class SqliteTableModel; class DbStructureModel; class QNetworkReply; class QNetworkAccessManager; +class QTreeWidgetItem; namespace Ui { class MainWindow; @@ -58,6 +59,7 @@ private: Ui::MainWindow* ui; SqliteTableModel* m_browseTableModel; + SqliteTableModel* m_currentPlotModel; QMenu *popupTableMenu; QMenu *recentFilesMenu; @@ -154,6 +156,8 @@ private slots: virtual void loadExtension(); virtual void reloadSettings(); virtual void httpresponse(QNetworkReply* reply); + virtual void updatePlot(SqliteTableModel* model, bool update = true); + void on_treePlotColumns_itemChanged(QTreeWidgetItem *item, int column); }; #endif diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 5493375c..8b507ff6 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -18,7 +18,7 @@ :/oldimages/icon16:/oldimages/icon16 - + @@ -275,7 +275,7 @@ 0 0 - 296 + 452 494 @@ -761,7 +761,7 @@ 0 0 800 - 20 + 21 @@ -938,6 +938,71 @@ + + + QDockWidget::AllDockWidgetFeatures + + + Plot + + + 2 + + + + + + + + + + Qt::Vertical + + + + + 0 + 2 + + + + 3 + + + 100 + + + + Columns + + + + + Y + + + + + X + + + + + + + 0 + 8 + + + + + + + + + + + @@ -1228,7 +1293,7 @@ &Execute SQL - F5 + F5, Ctrl+Return @@ -1270,7 +1335,7 @@ Execute current line - Ctrl+F5 + Ctrl+E @@ -1291,6 +1356,12 @@ QTableWidget
ExtendedTableWidget.h
+ + QCustomPlot + QWidget +
libs/qcustomplot-source/qcustomplot.h
+ 1 +
dbTreeWidget diff --git a/src/src.pro b/src/src.pro index 67c4f9d2..d98c3fad 100644 --- a/src/src.pro +++ b/src/src.pro @@ -1,7 +1,7 @@ TEMPLATE = app QT += core gui network -greaterThan(QT_MAJOR_VERSION, 4): QT += widgets +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport TARGET = sqlitebrowser @@ -40,7 +40,8 @@ HEADERS += \ gen_version.h \ SqlExecutionArea.h \ VacuumDialog.h \ - DbStructureModel.h + DbStructureModel.h \ + ../../libs/qcustomplot-source/qcustomplot.h SOURCES += \ sqlitedb.cpp \ @@ -62,7 +63,8 @@ SOURCES += \ FilterTableHeader.cpp \ SqlExecutionArea.cpp \ VacuumDialog.cpp \ - DbStructureModel.cpp + DbStructureModel.cpp \ + ../../libs/qcustomplot-source/qcustomplot.cpp RESOURCES += icons/icons.qrc