Skip to content

Commit 88ccd48

Browse files
committed
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
1 parent abeafa2 commit 88ccd48

File tree

5 files changed

+253
-9
lines changed

5 files changed

+253
-9
lines changed

CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ set(SQLB_MOC_HDR
4949
src/sqlitetablemodel.h
5050
src/sqltextedit.h
5151
src/DbStructureModel.h
52+
libs/qcustomplot-source/qcustomplot.h
5253
)
5354

5455
set(SQLB_SRC
@@ -72,6 +73,7 @@ set(SQLB_SRC
7273
src/DbStructureModel.cpp
7374
src/grammar/Sqlite3Lexer.cpp
7475
src/grammar/Sqlite3Parser.cpp
76+
libs/qcustomplot-source/qcustomplot.cpp
7577
src/main.cpp
7678
)
7779

@@ -141,7 +143,7 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR} ${ANTLR_DIR} ${QHEXEDIT_DIR} src
141143
add_executable(${PROJECT_NAME} ${SQLB_HDR} ${SQLB_SRC} ${SQLB_FORM_HDR} ${SQLB_MOC} ${SQLB_RESOURCES_RCC})
142144

143145
if(USE_QT5)
144-
qt5_use_modules(${PROJECT_NAME} Gui Widgets Network Test)
146+
qt5_use_modules(${PROJECT_NAME} Gui Widgets PrintSupport Network Test)
145147
set(QT_LIBRARIES "")
146148
endif()
147149
add_dependencies(${PROJECT_NAME} antlr qhexedit)

src/MainWindow.cpp

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ void MainWindow::init()
105105
ui->viewMenu->actions().at(0)->setIcon(QIcon(":/icons/log_dock"));
106106
ui->viewDBToolbarAction->setChecked(!ui->toolbarDB->isHidden());
107107

108+
// Plot view menu
109+
ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockPlot->toggleViewAction());
110+
ui->viewMenu->actions().at(1)->setShortcut(QKeySequence(tr("Ctrl+P")));
111+
ui->viewMenu->actions().at(1)->setIcon(QIcon(":/icons/log_dock"));
112+
108113
// Set statusbar fields
109114
statusEncodingLabel = new QLabel(ui->statusbar);
110115
statusEncodingLabel->setEnabled(false);
@@ -123,6 +128,7 @@ void MainWindow::init()
123128
restoreGeometry(PreferencesDialog::getSettingsValue("MainWindow", "geometry").toByteArray());
124129
restoreState(PreferencesDialog::getSettingsValue("MainWindow", "windowState").toByteArray());
125130
ui->comboLogSubmittedBy->setCurrentIndex(ui->comboLogSubmittedBy->findText(PreferencesDialog::getSettingsValue("SQLLogDock", "Log").toString()));
131+
ui->splitterForPlot->restoreState(PreferencesDialog::getSettingsValue("PlotDock", "splitterSize").toByteArray());
126132

127133
// Set other window settings
128134
setAcceptDrops(true);
@@ -308,6 +314,9 @@ void MainWindow::populateTable( const QString & tablename)
308314
if(editWin)
309315
editWin->reset();
310316

317+
// update plot
318+
updatePlot(m_browseTableModel);
319+
311320
QApplication::restoreOverrideCursor();
312321
}
313322

@@ -363,6 +372,7 @@ void MainWindow::closeEvent( QCloseEvent* event )
363372
PreferencesDialog::setSettingsValue("MainWindow", "geometry", saveGeometry());
364373
PreferencesDialog::setSettingsValue("MainWindow", "windowState", saveState());
365374
PreferencesDialog::setSettingsValue("SQLLogDock", "Log", ui->comboLogSubmittedBy->currentText());
375+
PreferencesDialog::setSettingsValue("PlotDock", "splitterSize", ui->splitterForPlot->saveState());
366376
clearCompleterModelsFields();
367377
QMainWindow::closeEvent(event);
368378
}
@@ -699,6 +709,7 @@ void MainWindow::executeQuery()
699709
}
700710
} while( tail && *tail != 0 && (sql3status == SQLITE_OK || sql3status == SQLITE_DONE));
701711
sqlWidget->finishExecution(statusMessage);
712+
updatePlot(sqlWidget->getModel());
702713

703714
if(!modified && !wasdirty)
704715
db.revert(); // better rollback, if the logic is not enough we can tune it.
@@ -1308,3 +1319,157 @@ void MainWindow::httpresponse(QNetworkReply *reply)
13081319
}
13091320
}
13101321
}
1322+
1323+
namespace {
1324+
/*!
1325+
* \brief guessdatatype try to parse the first 10 rows and decide the datatype
1326+
* \param model model to check the data
1327+
* \param column index of the column to check
1328+
* \return the guessed datatype
1329+
*/
1330+
QVariant::Type guessdatatype(SqliteTableModel* model, int column)
1331+
{
1332+
QVariant::Type type = QVariant::Invalid;
1333+
for(int i = 0; i < std::min(10, model->rowCount()) && type != QVariant::String; ++i)
1334+
{
1335+
QVariant data = model->data(model->index(i, column));
1336+
if(data.convert(QVariant::Double))
1337+
{
1338+
type = QVariant::Double;
1339+
}
1340+
else
1341+
{
1342+
QString s = model->data(model->index(i, column)).toString();
1343+
QDate d = QDate::fromString(s, Qt::ISODate);
1344+
if(d.isValid())
1345+
type = QVariant::DateTime;
1346+
else
1347+
type = QVariant::String;
1348+
}
1349+
1350+
}
1351+
return type;
1352+
}
1353+
}
1354+
1355+
void MainWindow::updatePlot(SqliteTableModel *model, bool update)
1356+
{
1357+
// add columns to x/y seleciton tree widget
1358+
if(update)
1359+
{
1360+
disconnect(ui->treePlotColumns, SIGNAL(itemChanged(QTreeWidgetItem*,int)),
1361+
this,SLOT(on_treePlotColumns_itemChanged(QTreeWidgetItem*,int)));
1362+
1363+
m_currentPlotModel = model;
1364+
ui->treePlotColumns->clear();
1365+
1366+
for(int i = 0; i < model->columnCount(); ++i)
1367+
{
1368+
QVariant::Type columntype = guessdatatype(model, i);
1369+
if(columntype != QVariant::String && columntype != QVariant::Invalid)
1370+
{
1371+
QTreeWidgetItem* columnitem = new QTreeWidgetItem(ui->treePlotColumns);
1372+
// maybe i make this more complicated than i should
1373+
// but store the model column index in the first 16 bit and the type
1374+
// in the other 16 bits
1375+
uint itemdata = 0;
1376+
itemdata = i << 16;
1377+
itemdata |= columntype;
1378+
columnitem->setData(0, Qt::UserRole, itemdata);
1379+
1380+
columnitem->setText(0, model->headerData(i, Qt::Horizontal).toString());
1381+
columnitem->setCheckState(1, Qt::Unchecked);
1382+
columnitem->setCheckState(2, Qt::Unchecked);
1383+
ui->treePlotColumns->addTopLevelItem(columnitem);
1384+
}
1385+
}
1386+
1387+
ui->plotWidget->yAxis->setLabel("Y");
1388+
ui->plotWidget->xAxis->setLabel("X");
1389+
connect(ui->treePlotColumns, SIGNAL(itemChanged(QTreeWidgetItem*,int)),this,SLOT(on_treePlotColumns_itemChanged(QTreeWidgetItem*,int)));
1390+
}
1391+
1392+
// search for the x axis select
1393+
QTreeWidgetItem* xitem = 0;
1394+
for(int i = 0; i < ui->treePlotColumns->topLevelItemCount(); ++i)
1395+
{
1396+
xitem = ui->treePlotColumns->topLevelItem(i);
1397+
if(xitem->checkState(2) == Qt::Checked)
1398+
break;
1399+
1400+
xitem = 0;
1401+
}
1402+
1403+
QStringList yAxisLabels;
1404+
QVector<QColor> colors;
1405+
colors << Qt::blue << Qt::red << Qt::green << Qt::darkYellow << Qt::darkCyan << Qt::darkGray;
1406+
1407+
ui->plotWidget->clearGraphs();
1408+
if(xitem)
1409+
{
1410+
uint xitemdata = xitem->data(0, Qt::UserRole).toUInt();
1411+
int x = xitemdata >> 16;
1412+
int xtype = xitemdata & (uint)0xFF;
1413+
1414+
1415+
// check if we have a x axis with datetime data
1416+
if(xtype == QVariant::DateTime)
1417+
{
1418+
ui->plotWidget->xAxis->setTickLabelType(QCPAxis::ltDateTime);
1419+
ui->plotWidget->xAxis->setDateTimeFormat("yyyy-mm-dd");
1420+
}
1421+
else
1422+
{
1423+
ui->plotWidget->xAxis->setTickLabelType(QCPAxis::ltNumber);
1424+
}
1425+
1426+
// add graph for each selected y axis
1427+
for(int i = 0; i < ui->treePlotColumns->topLevelItemCount(); ++i)
1428+
{
1429+
QTreeWidgetItem* item = ui->treePlotColumns->topLevelItem(i);
1430+
if(item->checkState((1)) == Qt::Checked && ui->plotWidget->graphCount() < colors.size())
1431+
{
1432+
uint itemdata = item->data(0, Qt::UserRole).toUInt();
1433+
int column = itemdata >> 16;
1434+
QCPGraph* graph = ui->plotWidget->addGraph();
1435+
1436+
int y = column;
1437+
1438+
graph->setPen(QPen(colors[y]));
1439+
1440+
QVector<double> xdata(model->rowCount()), ydata(model->rowCount());
1441+
for(int i = 0; i < model->rowCount(); ++i)
1442+
{
1443+
// convert x type axis if it's datetime
1444+
if(xtype == QVariant::DateTime)
1445+
{
1446+
QString s = model->data(model->index(i, x)).toString();
1447+
QDateTime d = QDateTime::fromString(s, Qt::ISODate);
1448+
xdata[i] = d.toTime_t();
1449+
}
1450+
else
1451+
{
1452+
xdata[i] = model->data(model->index(i, x)).toDouble();
1453+
}
1454+
1455+
ydata[i] = model->data(model->index(i, y)).toDouble();
1456+
}
1457+
graph->setData(xdata, ydata);
1458+
graph->setLineStyle(QCPGraph::lsLine);
1459+
graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 5));
1460+
yAxisLabels << model->headerData(y, Qt::Horizontal).toString();
1461+
graph->rescaleAxes();
1462+
}
1463+
}
1464+
1465+
// set axis labels
1466+
ui->plotWidget->xAxis->setLabel(model->headerData(x, Qt::Horizontal).toString());
1467+
ui->plotWidget->yAxis->setLabel(yAxisLabels.join("|"));
1468+
}
1469+
ui->plotWidget->replot();
1470+
}
1471+
1472+
void MainWindow::on_treePlotColumns_itemChanged(QTreeWidgetItem *item, int column)
1473+
{
1474+
updatePlot(m_currentPlotModel, false);
1475+
}

src/MainWindow.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class SqliteTableModel;
1818
class DbStructureModel;
1919
class QNetworkReply;
2020
class QNetworkAccessManager;
21+
class QTreeWidgetItem;
2122

2223
namespace Ui {
2324
class MainWindow;
@@ -58,6 +59,7 @@ class MainWindow : public QMainWindow
5859
Ui::MainWindow* ui;
5960

6061
SqliteTableModel* m_browseTableModel;
62+
SqliteTableModel* m_currentPlotModel;
6163
QMenu *popupTableMenu;
6264
QMenu *recentFilesMenu;
6365

@@ -154,6 +156,8 @@ private slots:
154156
virtual void loadExtension();
155157
virtual void reloadSettings();
156158
virtual void httpresponse(QNetworkReply* reply);
159+
virtual void updatePlot(SqliteTableModel* model, bool update = true);
160+
void on_treePlotColumns_itemChanged(QTreeWidgetItem *item, int column);
157161
};
158162

159163
#endif

src/MainWindow.ui

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<normaloff>:/oldimages/icon16</normaloff>:/oldimages/icon16</iconset>
1919
</property>
2020
<widget class="QWidget" name="centralwidget">
21-
<layout class="QVBoxLayout" name="verticalLayoutmain">
21+
<layout class="QVBoxLayout" name="verticalLayout_7">
2222
<item>
2323
<widget class="QTabWidget" name="mainTab">
2424
<property name="currentIndex">
@@ -275,7 +275,7 @@
275275
<rect>
276276
<x>0</x>
277277
<y>0</y>
278-
<width>296</width>
278+
<width>452</width>
279279
<height>494</height>
280280
</rect>
281281
</property>
@@ -761,7 +761,7 @@
761761
<x>0</x>
762762
<y>0</y>
763763
<width>800</width>
764-
<height>20</height>
764+
<height>21</height>
765765
</rect>
766766
</property>
767767
<widget class="QMenu" name="fileMenu">
@@ -938,6 +938,71 @@
938938
</layout>
939939
</widget>
940940
</widget>
941+
<widget class="QDockWidget" name="dockPlot">
942+
<property name="features">
943+
<set>QDockWidget::AllDockWidgetFeatures</set>
944+
</property>
945+
<property name="windowTitle">
946+
<string>Plot</string>
947+
</property>
948+
<attribute name="dockWidgetArea">
949+
<number>2</number>
950+
</attribute>
951+
<widget class="QWidget" name="dockWidgetContents_2">
952+
<layout class="QVBoxLayout" name="verticalLayout_6">
953+
<item>
954+
<widget class="QWidget" name="widget_2" native="true">
955+
<layout class="QVBoxLayout" name="verticalLayout_8">
956+
<item>
957+
<widget class="QSplitter" name="splitterForPlot">
958+
<property name="orientation">
959+
<enum>Qt::Vertical</enum>
960+
</property>
961+
<widget class="QTreeWidget" name="treePlotColumns">
962+
<property name="sizePolicy">
963+
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
964+
<horstretch>0</horstretch>
965+
<verstretch>2</verstretch>
966+
</sizepolicy>
967+
</property>
968+
<property name="columnCount">
969+
<number>3</number>
970+
</property>
971+
<attribute name="headerDefaultSectionSize">
972+
<number>100</number>
973+
</attribute>
974+
<column>
975+
<property name="text">
976+
<string>Columns</string>
977+
</property>
978+
</column>
979+
<column>
980+
<property name="text">
981+
<string>Y</string>
982+
</property>
983+
</column>
984+
<column>
985+
<property name="text">
986+
<string>X</string>
987+
</property>
988+
</column>
989+
</widget>
990+
<widget class="QCustomPlot" name="plotWidget" native="true">
991+
<property name="sizePolicy">
992+
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
993+
<horstretch>0</horstretch>
994+
<verstretch>8</verstretch>
995+
</sizepolicy>
996+
</property>
997+
</widget>
998+
</widget>
999+
</item>
1000+
</layout>
1001+
</widget>
1002+
</item>
1003+
</layout>
1004+
</widget>
1005+
</widget>
9411006
<action name="fileNewAction">
9421007
<property name="icon">
9431008
<iconset resource="icons/icons.qrc">
@@ -1228,7 +1293,7 @@
12281293
<string>&amp;Execute SQL</string>
12291294
</property>
12301295
<property name="shortcut">
1231-
<string>F5</string>
1296+
<string>F5, Ctrl+Return</string>
12321297
</property>
12331298
</action>
12341299
<action name="actionSqlOpenFile">
@@ -1270,7 +1335,7 @@
12701335
<string>Execute current line</string>
12711336
</property>
12721337
<property name="shortcut">
1273-
<string>Ctrl+F5</string>
1338+
<string>Ctrl+E</string>
12741339
</property>
12751340
</action>
12761341
<action name="actionExportCsvPopup">
@@ -1291,6 +1356,12 @@
12911356
<extends>QTableWidget</extends>
12921357
<header>ExtendedTableWidget.h</header>
12931358
</customwidget>
1359+
<customwidget>
1360+
<class>QCustomPlot</class>
1361+
<extends>QWidget</extends>
1362+
<header>libs/qcustomplot-source/qcustomplot.h</header>
1363+
<container>1</container>
1364+
</customwidget>
12941365
</customwidgets>
12951366
<tabstops>
12961367
<tabstop>dbTreeWidget</tabstop>

0 commit comments

Comments
 (0)