Merge pull request #1302 from sqlitebrowser/bar_charts

Stackable bar charts, Axis Type column and legend in plot.
This commit is contained in:
Manuel
2018-01-26 18:29:42 +01:00
committed by GitHub
3 changed files with 176 additions and 46 deletions
+161 -44
View File
@@ -9,7 +9,9 @@ PlotDock::PlotDock(QWidget* parent)
: QDialog(parent), : QDialog(parent),
ui(new Ui::PlotDock), ui(new Ui::PlotDock),
m_currentPlotModel(nullptr), m_currentPlotModel(nullptr),
m_currentTableSettings(nullptr) m_currentTableSettings(nullptr),
m_showLegend(false),
m_stackedBars(false)
{ {
ui->setupUi(this); ui->setupUi(this);
@@ -50,6 +52,18 @@ PlotDock::PlotDock(QWidget* parent)
copy(); copy();
}); });
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, connect(ui->plotWidget, &QTableView::customContextMenuRequested,
[=](const QPoint& pos) { [=](const QPoint& pos) {
// Show menu // Show menu
@@ -72,8 +86,8 @@ PlotDock::~PlotDock()
void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* settings, bool update, bool keepOrResetSelection) 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 // 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 in a 16bit integer. // the virtual 'Row #' column which needs a separate unique id for internal use. This id is defined here as -1.
const unsigned int RowNumId = 0xFFFF; const int RowNumId = -1;
// add columns to x/y selection tree widget // add columns to x/y selection tree widget
if(update) if(update)
@@ -119,16 +133,35 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
for(int i=0;i<model->columnCount();++i) for(int i=0;i<model->columnCount();++i)
{ {
QVariant::Type columntype = guessDataType(model, i); QVariant::Type columntype = guessDataType(model, i);
if(columntype != QVariant::String && columntype != QVariant::Invalid) if(columntype != QVariant::Invalid)
{ {
QTreeWidgetItem* columnitem = new QTreeWidgetItem(ui->treePlotColumns); 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 switch (columntype) {
// in the other 16 bits case QVariant::DateTime:
uint itemdata = 0; columnitem->setText(PlotColumnType, tr("Date/Time"));
itemdata = i << 16; break;
itemdata |= columntype; case QVariant::Date:
columnitem->setData(PlotColumnField, Qt::UserRole, itemdata); 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()); columnitem->setText(PlotColumnField, model->headerData(i, Qt::Horizontal).toString());
// restore previous check state // restore previous check state
@@ -137,7 +170,8 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
columnitem->setCheckState(PlotColumnY, mapItemsY[columnitem->text(PlotColumnField)].active ? Qt::Checked : Qt::Unchecked); columnitem->setCheckState(PlotColumnY, mapItemsY[columnitem->text(PlotColumnField)].active ? Qt::Checked : Qt::Unchecked);
columnitem->setBackgroundColor(PlotColumnY, mapItemsY[columnitem->text(PlotColumnField)].colour); columnitem->setBackgroundColor(PlotColumnY, mapItemsY[columnitem->text(PlotColumnField)].colour);
} else { } else {
columnitem->setCheckState(PlotColumnY, Qt::Unchecked); if (columntype == QVariant::Double)
columnitem->setCheckState(PlotColumnY, Qt::Unchecked);
} }
if(sItemX == columnitem->text(PlotColumnField)) if(sItemX == columnitem->text(PlotColumnField))
@@ -146,6 +180,7 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
columnitem->setCheckState(PlotColumnX, Qt::Unchecked); columnitem->setCheckState(PlotColumnX, Qt::Unchecked);
} }
} }
ui->treePlotColumns->resizeColumnToContents(PlotColumnField); ui->treePlotColumns->resizeColumnToContents(PlotColumnField);
// Add a row number column at the beginning of the column list, but only when there were (other) columns added // Add a row number column at the beginning of the column list, but only when there were (other) columns added
@@ -153,10 +188,11 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
{ {
QTreeWidgetItem* columnitem = new QTreeWidgetItem(ui->treePlotColumns); QTreeWidgetItem* columnitem = new QTreeWidgetItem(ui->treePlotColumns);
// Just set all bits in the user role information field here to somehow indicate what column this is // Just set RowNumId in the user role information field here to somehow indicate what column this is
uint itemdata = -1; columnitem->setData(PlotColumnField, Qt::UserRole, RowNumId);
columnitem->setData(PlotColumnField, Qt::UserRole, itemdata);
columnitem->setText(PlotColumnField, tr("Row #")); columnitem->setText(PlotColumnField, tr("Row #"));
columnitem->setData(PlotColumnType, Qt::UserRole, static_cast<int>(QVariant::Double));
columnitem->setText(PlotColumnType, tr("Numeric"));
// restore previous check state // restore previous check state
if(mapItemsY.contains(columnitem->text(PlotColumnField))) if(mapItemsY.contains(columnitem->text(PlotColumnField)))
@@ -202,11 +238,11 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
if(xitem) if(xitem)
{ {
// regain the model column index and the datatype // regain the model column index and the datatype
// leading 16 bit are column index, the other 16 bit are the datatype // right now datatype is only important for X axis (Y is always numeric)
// right now datatype is only important for X axis (date, non date) int x = xitem->data(PlotColumnField, Qt::UserRole).toInt();
uint xitemdata = xitem->data(PlotColumnField, Qt::UserRole).toUInt(); int xtype = xitem->data(PlotColumnType, Qt::UserRole).toInt();
int x = xitemdata >> 16;
int xtype = xitemdata & (uint)0xFF; ui->plotWidget->xAxis->setTickLabelRotation(0);
// check if we have a x axis with datetime data // check if we have a x axis with datetime data
switch (xtype) { switch (xtype) {
@@ -229,6 +265,11 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
ui->plotWidget->xAxis->setTicker(ticker); ui->plotWidget->xAxis->setTicker(ticker);
break; break;
} }
case QVariant::String: {
// Ticker is set when we have got the labels
ui->plotWidget->xAxis->setTickLabelRotation(60);
break;
}
default: { default: {
QSharedPointer<QCPAxisTickerFixed> ticker(new QCPAxisTickerFixed); QSharedPointer<QCPAxisTickerFixed> ticker(new QCPAxisTickerFixed);
ticker->setTickStepStrategy(QCPAxisTicker::tssReadability); ticker->setTickStepStrategy(QCPAxisTicker::tssReadability);
@@ -243,10 +284,8 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
QTreeWidgetItem* item = ui->treePlotColumns->topLevelItem(i); QTreeWidgetItem* item = ui->treePlotColumns->topLevelItem(i);
if(item->checkState((PlotColumnY)) == Qt::Checked) if(item->checkState((PlotColumnY)) == Qt::Checked)
{ {
// regain the model column index and the datatype // regain the model column index
// leading 16 bit are column index int column = item->data(PlotColumnField, Qt::UserRole).toInt();
uint itemdata = item->data(0, Qt::UserRole).toUInt();
int column = itemdata >> 16;
bool isSorted = true; bool isSorted = true;
@@ -254,6 +293,7 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
// possible improvement might be a QVector subclass that directly // possible improvement might be a QVector subclass that directly
// access the model data, to save memory, we are copying here // access the model data, to save memory, we are copying here
QVector<double> xdata(model->rowCount()), ydata(model->rowCount()), tdata(model->rowCount()); QVector<double> xdata(model->rowCount()), ydata(model->rowCount()), tdata(model->rowCount());
QVector<QString> labels;
for(int i = 0; i < model->rowCount(); ++i) for(int i = 0; i < model->rowCount(); ++i)
{ {
tdata[i] = i; tdata[i] = i;
@@ -272,6 +312,11 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
xdata[i] = t.msecsSinceStartOfDay() / 1000.0; xdata[i] = t.msecsSinceStartOfDay() / 1000.0;
break; break;
} }
case QVariant::String: {
xdata[i] = i+1;
labels << model->data(model->index(i, x)).toString();
break;
}
default: { 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 // 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. // instead of retrieving some value from the model.
@@ -299,38 +344,59 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
else else
ydata[i] = pointdata.toDouble(); 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 // WARN: ssDot is removed
int shapeIdx = ui->comboPointShape->currentIndex(); int shapeIdx = ui->comboPointShape->currentIndex();
if (shapeIdx > 0) shapeIdx += 1; if (shapeIdx > 0) shapeIdx += 1;
QCPScatterStyle scatterStyle = QCPScatterStyle(static_cast<QCPScatterStyle::ScatterShape>(shapeIdx), 5); QCPScatterStyle scatterStyle = QCPScatterStyle(static_cast<QCPScatterStyle::ScatterShape>(shapeIdx), 5);
QCPAbstractPlottable* plottable; 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 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 // 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. // 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. // TODO: how to make the user aware of this without disturbing.
if (isSorted) { if (xtype == QVariant::String) {
QCPGraph* graph = ui->plotWidget->addGraph(); QCPBars* bars = new QCPBars(ui->plotWidget->xAxis, ui->plotWidget->yAxis);
plottable = graph; plottable = bars;
graph->setData(xdata, ydata, /*alreadySorted*/ true); bars->setData(xdata, ydata);
// set some graph styles not supported by the abstract plottable // Set ticker once
graph->setLineStyle((QCPGraph::LineStyle) ui->comboLineType->currentIndex()); if (ui->plotWidget->plottableCount() == 1) {
graph->setScatterStyle(scatterStyle); 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 { } else {
QCPCurve* curve = new QCPCurve(ui->plotWidget->xAxis, ui->plotWidget->yAxis); if (isSorted) {
plottable = curve; QCPGraph* graph = ui->plotWidget->addGraph();
curve->setData(tdata, xdata, ydata, /*alreadySorted*/ true); plottable = graph;
// set some curve styles not supported by the abstract plottable graph->setData(xdata, ydata, /*alreadySorted*/ true);
if (ui->comboLineType->currentIndex() == QCPCurve::lsNone) // set some graph styles not supported by the abstract plottable
curve->setLineStyle(QCPCurve::lsNone); graph->setLineStyle((QCPGraph::LineStyle) ui->comboLineType->currentIndex());
else graph->setScatterStyle(scatterStyle);
curve->setLineStyle(QCPCurve::lsLine); } else {
curve->setScatterStyle(scatterStyle); 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->setPen(QPen(item->backgroundColor(PlotColumnY))); plottable->setSelectable(QCP::stDataRange);
plottable->setSelectable (QCP::stDataRange); plottable->setName(item->text(PlotColumnField));
// gather Y label column names // gather Y label column names
if(column == RowNumId) if(column == RowNumId)
@@ -341,6 +407,9 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
} }
ui->plotWidget->rescaleAxes(true); 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 // set axis labels
if(x == RowNumId) if(x == RowNumId)
@@ -349,6 +418,8 @@ void PlotDock::updatePlot(SqliteTableModel* model, BrowseDataTableSettings* sett
ui->plotWidget->xAxis->setLabel(model->headerData(x, Qt::Horizontal).toString()); ui->plotWidget->xAxis->setLabel(model->headerData(x, Qt::Horizontal).toString());
ui->plotWidget->yAxis->setLabel(yAxisLabels.join("|")); ui->plotWidget->yAxis->setLabel(yAxisLabels.join("|"));
} }
adjustBars();
ui->plotWidget->replot(); ui->plotWidget->replot();
// Warn user if not all data has been fetched and hint about the button for loading all the data // Warn user if not all data has been fetched and hint about the button for loading all the data
@@ -483,7 +554,9 @@ void PlotDock::on_treePlotColumns_itemDoubleClicked(QTreeWidgetItem* item, int c
// disable change updates, or we get unwanted redrawing and weird behavior // disable change updates, or we get unwanted redrawing and weird behavior
ui->treePlotColumns->blockSignals(true); ui->treePlotColumns->blockSignals(true);
if(column == PlotColumnY) int type = item->data(PlotColumnType, Qt::UserRole).toInt();
if(column == PlotColumnY && type == QVariant::Double)
{ {
// On double click open the colordialog // On double click open the colordialog
QColorDialog colordialog(this); QColorDialog colordialog(this);
@@ -744,3 +817,47 @@ void PlotDock::copy()
{ {
QApplication::clipboard()->setPixmap(ui->plotWidget->toPixmap()); 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();
}
+6 -1
View File
@@ -77,6 +77,7 @@ private:
PlotColumnField = 0, PlotColumnField = 0,
PlotColumnX = 1, PlotColumnX = 1,
PlotColumnY = 2, PlotColumnY = 2,
PlotColumnType = 3,
}; };
Ui::PlotDock* ui; Ui::PlotDock* ui;
@@ -84,6 +85,8 @@ private:
SqliteTableModel* m_currentPlotModel; SqliteTableModel* m_currentPlotModel;
BrowseDataTableSettings* m_currentTableSettings; BrowseDataTableSettings* m_currentTableSettings;
QMenu* m_contextMenu; QMenu* m_contextMenu;
bool m_showLegend;
bool m_stackedBars;
/*! /*!
* \brief guessdatatype try to parse the first 10 rows and decide the datatype * \brief guessdatatype try to parse the first 10 rows and decide the datatype
@@ -92,6 +95,7 @@ private:
* \return the guessed datatype * \return the guessed datatype
*/ */
QVariant::Type guessDataType(SqliteTableModel* model, int column); QVariant::Type guessDataType(SqliteTableModel* model, int column);
void adjustBars();
private slots: private slots:
void on_treePlotColumns_itemChanged(QTreeWidgetItem* item, int column); void on_treePlotColumns_itemChanged(QTreeWidgetItem* item, int column);
@@ -103,7 +107,8 @@ private slots:
void mousePress(); void mousePress();
void mouseWheel(); void mouseWheel();
void copy(); void copy();
void toggleLegendVisible(bool visible);
void toggleStackedBars(bool stacked);
}; };
#endif #endif
+9 -1
View File
@@ -26,11 +26,14 @@
<verstretch>2</verstretch> <verstretch>2</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This pane shows the list of columns of the currently browsed table or the just executed query. You can select the columns that you want to be used as X or Y axis for the plot pane below. The table shows detected axis type that will affect the resulting plot. For the Y axis you can only select numeric columns, but for the X axis you will be able to select:&lt;/p&gt;&lt;ul style=&quot;margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;&quot;&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Date/Time&lt;/span&gt;: strings with format &amp;quot;yyyy-MM-dd hh:mm:ss&amp;quot; or &amp;quot;yyyy-MM-ddThh:mm:ss&amp;quot;&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Date&lt;/span&gt;: strings with format &amp;quot;yyyy-MM-dd&amp;quot;&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Time&lt;/span&gt;: strings with format &amp;quot;hh:mm:ss&amp;quot;&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Label&lt;/span&gt;: other string formats. Selecting this column as X axis will produce a Bars plot with the column values as labels for the bars&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Numeric&lt;/span&gt;: integer or real values&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Double-clicking the Y cells you can change the used color for that graph.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="columnCount"> <property name="columnCount">
<number>3</number> <number>4</number>
</property> </property>
<attribute name="headerDefaultSectionSize"> <attribute name="headerDefaultSectionSize">
<number>100</number> <number>100</number>
@@ -53,6 +56,11 @@
<string>Y</string> <string>Y</string>
</property> </property>
</column> </column>
<column>
<property name="text">
<string>Axis Type</string>
</property>
</column>
</widget> </widget>
<widget class="QCustomPlot" name="plotWidget" native="true"> <widget class="QCustomPlot" name="plotWidget" native="true">
<property name="sizePolicy"> <property name="sizePolicy">