Skip to content
Permalink
Browse files
[FEATURE] New statistical summary dock widget
Can display summary statistics (eg mean, standard deviation, ...)
for a field or expression from a vector layer.
  • Loading branch information
nyalldawson committed May 22, 2015
1 parent 1078daf commit e7b7549c29712f89c9ba56884ca748149c4c0778
@@ -69,6 +69,12 @@ class QgsStatisticalSummary
*/
void calculate( const QList<double>& values );

/** Returns the value of a specified statistic
* @param stat statistic to return
* @returns calculated value of statistic
*/
double statistic( Statistic stat ) const;

/** Returns calculated count of values
*/
int count() const;
@@ -151,6 +157,11 @@ class QgsStatisticalSummary
*/
double interQuartileRange() const;

/** Returns the friendly display name for a statistic
* @param statistic statistic to return name for
*/
static QString displayName( Statistic statistic );

};

QFlags<QgsStatisticalSummary::Statistic> operator|(QgsStatisticalSummary::Statistic f1, QFlags<QgsStatisticalSummary::Statistic> f2);
@@ -103,6 +103,7 @@ SET(QGIS_APP_SRCS
qgsprojectproperties.cpp
qgsrastercalcdialog.cpp
qgsrasterlayerproperties.cpp
qgsstatisticalsummarydockwidget.cpp
qgstextannotationdialog.cpp
qgsshortcutsmanager.cpp
qgsguivectorlayertools.h
@@ -248,6 +249,7 @@ SET (QGIS_APP_MOC_HDRS
qgsrasterlayerproperties.h
qgssnappingdialog.h
qgssponsors.h
qgsstatisticalsummarydockwidget.h
qgssvgannotationdialog.h
qgstextannotationdialog.h
qgstipgui.h
@@ -195,6 +195,7 @@
#include "qgssinglebandgrayrenderer.h"
#include "qgssnappingdialog.h"
#include "qgssponsors.h"
#include "qgsstatisticalsummarydockwidget.h"
#include "qgssvgannotationitem.h"
#include "qgstextannotationitem.h"
#include "qgstipgui.h"
@@ -598,6 +599,10 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, QWidget * parent,
mAdvancedDigitizingDockWidget = new QgsAdvancedDigitizingDockWidget( mMapCanvas, this );
mAdvancedDigitizingDockWidget->setObjectName( "AdvancedDigitizingTools" );

// Statistical Summary dock
mStatisticalSummaryDockWidget = new QgsStatisticalSummaryDockWidget( this );
mStatisticalSummaryDockWidget->setObjectName( "StatistalSummaryDockWidget" );

mSnappingUtils = new QgsMapCanvasSnappingUtils( mMapCanvas, this );
mMapCanvas->setSnappingUtils( mSnappingUtils );
connect( QgsProject::instance(), SIGNAL( snapSettingsChanged() ), mSnappingUtils, SLOT( readConfigFromProject() ) );
@@ -642,6 +647,9 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, QWidget * parent,
addDockWidget( Qt::LeftDockWidgetArea, mAdvancedDigitizingDockWidget );
mAdvancedDigitizingDockWidget->hide();

addDockWidget( Qt::LeftDockWidgetArea, mStatisticalSummaryDockWidget );
mStatisticalSummaryDockWidget->hide();

QMainWindow::addDockWidget( Qt::BottomDockWidgetArea, mUserInputDockWidget );
mUserInputDockWidget->setFloating( true );

@@ -1202,6 +1210,7 @@ void QgisApp::createActions()
connect( mActionSvgAnnotation, SIGNAL( triggered() ), this, SLOT( addSvgAnnotation() ) );
connect( mActionAnnotation, SIGNAL( triggered() ), this, SLOT( modifyAnnotation() ) );
connect( mActionLabeling, SIGNAL( triggered() ), this, SLOT( labeling() ) );
connect( mActionStatisticalSummary, SIGNAL( triggered( ) ), this, SLOT( showStatisticsDockWidget() ) );

// Layer Menu Items

@@ -10483,6 +10492,12 @@ void QgisApp::osmExportDialog()
dlg.exec();
}

void QgisApp::showStatisticsDockWidget()
{
mStatisticalSummaryDockWidget->show();
mStatisticalSummaryDockWidget->raise();
}


#ifdef HAVE_TOUCH
bool QgisApp::gestureEvent( QGestureEvent *event )
@@ -76,6 +76,7 @@ class QgsBrowserDockWidget;
class QgsAdvancedDigitizingDockWidget;
class QgsSnappingDialog;
class QgsGPSInformationWidget;
class QgsStatisticalSummaryDockWidget;

class QgsDecorationItem;

@@ -1226,6 +1227,10 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
/** Make the user feel dizzy */
void dizzy();

/** Shows the statistical summary dock widget and brings it to the foreground
*/
void showStatisticsDockWidget();

signals:
/** emitted when a key is pressed and we want non widget sublasses to be able
to pick up on this (e.g. maplayer) */
@@ -1615,6 +1620,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
QgsBrowserDockWidget *mBrowserWidget2;

QgsAdvancedDigitizingDockWidget *mAdvancedDigitizingDockWidget;
QgsStatisticalSummaryDockWidget* mStatisticalSummaryDockWidget;

QgsSnappingDialog *mSnappingDialog;

@@ -0,0 +1,206 @@
/***************************************************************************
qgsstatisticalsummarydockwidget.cpp
-----------------------------------
begin : May 2015
copyright : (C) 2015 by Nyall Dawson
email : nyall dot dawson at gmail dot 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. *
* *
***************************************************************************/
#include "qgsstatisticalsummarydockwidget.h"
#include "qgsstatisticalsummary.h"
#include <QTableWidget>
#include <QAction>
#include <QSettings>

QList< QgsStatisticalSummary::Statistic > QgsStatisticalSummaryDockWidget::mDisplayStats =
QList< QgsStatisticalSummary::Statistic > () << QgsStatisticalSummary::Count
<< QgsStatisticalSummary::Sum
<< QgsStatisticalSummary::Mean
<< QgsStatisticalSummary::Median
<< QgsStatisticalSummary::StDev
<< QgsStatisticalSummary::StDevSample
<< QgsStatisticalSummary::Min
<< QgsStatisticalSummary::Max
<< QgsStatisticalSummary::Range
<< QgsStatisticalSummary::Minority
<< QgsStatisticalSummary::Majority
<< QgsStatisticalSummary::Variety
<< QgsStatisticalSummary::FirstQuartile
<< QgsStatisticalSummary::ThirdQuartile
<< QgsStatisticalSummary::InterQuartileRange;

#define MISSING_VALUES -1

QgsStatisticalSummaryDockWidget::QgsStatisticalSummaryDockWidget( QWidget *parent )
: QDockWidget( parent )
, mLayer( 0 )
{
setupUi( this );

mLayerComboBox->setFilters( QgsMapLayerProxyModel::VectorLayer );
mFieldExpressionWidget->setFilters( QgsFieldProxyModel::Numeric );
connect( mLayerComboBox, SIGNAL( layerChanged( QgsMapLayer* ) ), this, SLOT( layerChanged( QgsMapLayer* ) ) );
connect( mFieldExpressionWidget, SIGNAL( fieldChanged( QString ) ), this, SLOT( refreshStatistics() ) );
connect( mSelectedOnlyCheckBox, SIGNAL( toggled( bool ) ), this, SLOT( refreshStatistics() ) );
connect( mButtonRefresh, SIGNAL( clicked( bool ) ), this, SLOT( refreshStatistics() ) );

if ( mLayerComboBox->currentLayer() )
{
mFieldExpressionWidget->setLayer( mLayerComboBox->currentLayer() );
}

QSettings settings;
foreach ( QgsStatisticalSummary::Statistic stat, mDisplayStats )
{
QAction* action = new QAction( QgsStatisticalSummary::displayName( stat ), mOptionsToolButton );
action->setCheckable( true );
bool checked = settings.value( QString( "/StatisticalSummaryDock/checked_%1" ).arg( stat ), true ).toBool();
action->setChecked( checked );
action->setData( stat );
mStatsActions.insert( stat, action );
connect( action, SIGNAL( triggered( bool ) ), this, SLOT( statActionTriggered( bool ) ) );
mOptionsToolButton->addAction( action );
}

//count of null values statistic:
QAction* nullCountAction = new QAction( tr( "Missing (null) values" ), mOptionsToolButton );
nullCountAction->setCheckable( true );
bool checked = settings.value( QString( "/StatisticalSummaryDock/checked_missing_values" ), true ).toBool();
nullCountAction->setChecked( checked );
nullCountAction->setData( MISSING_VALUES );
mStatsActions.insert( MISSING_VALUES, nullCountAction );
connect( nullCountAction, SIGNAL( triggered( bool ) ), this, SLOT( statActionTriggered( bool ) ) );
mOptionsToolButton->addAction( nullCountAction );
}

QgsStatisticalSummaryDockWidget::~QgsStatisticalSummaryDockWidget()
{

}

void QgsStatisticalSummaryDockWidget::refreshStatistics()
{
if ( !mLayer || !mFieldExpressionWidget->isValidExpression() )
{
mStatisticsTable->setRowCount( 0 );
return;
}

QString sourceFieldExp = mFieldExpressionWidget->currentField();

bool ok;
bool selectedOnly = mSelectedOnlyCheckBox->isChecked();
int missingValues = 0;
QList< double > values = mLayer->getDoubleValues( sourceFieldExp, ok, selectedOnly, &missingValues );

if ( ! ok )
{
return;
}

QList< QgsStatisticalSummary::Statistic > statsToDisplay;
foreach ( QgsStatisticalSummary::Statistic stat, mDisplayStats )
{
if ( mStatsActions.value( stat )->isChecked() )
statsToDisplay << stat;
}

int extraRows = 0;
if ( mStatsActions.value( MISSING_VALUES )->isChecked() )
extraRows++;

QgsStatisticalSummary stats;
stats.setStatistics( QgsStatisticalSummary::All );
stats.calculate( values );

mStatisticsTable->setRowCount( statsToDisplay.count() + extraRows );
mStatisticsTable->setColumnCount( 2 );

int row = 0;
foreach ( QgsStatisticalSummary::Statistic stat, statsToDisplay )
{
QTableWidgetItem* nameItem = new QTableWidgetItem( QgsStatisticalSummary::displayName( stat ) );
nameItem->setToolTip( nameItem->text() );
nameItem->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
mStatisticsTable->setItem( row, 0, nameItem );

QTableWidgetItem* valueItem = new QTableWidgetItem();
if ( stats.count() != 0 )
{
valueItem->setText( QString::number( stats.statistic( stat ) ) );
}
valueItem->setToolTip( valueItem->text() );
valueItem->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
mStatisticsTable->setItem( row, 1, valueItem );

row++;
}

if ( mStatsActions.value( MISSING_VALUES )->isChecked() )
{
QTableWidgetItem* nameItem = new QTableWidgetItem( tr( "Missing (null) values" ) );
nameItem->setToolTip( nameItem->text() );
nameItem->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
mStatisticsTable->setItem( row, 0, nameItem );

QTableWidgetItem* valueItem = new QTableWidgetItem();
if ( stats.count() != 0 || missingValues != 0 )
{
valueItem->setText( QString::number( missingValues ) );
}
valueItem->setToolTip( valueItem->text() );
valueItem->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
mStatisticsTable->setItem( row, 1, valueItem );
row++;
}
}

void QgsStatisticalSummaryDockWidget::layerChanged( QgsMapLayer *layer )
{
QgsVectorLayer* newLayer = dynamic_cast< QgsVectorLayer* >( layer );
if ( mLayer && mLayer != newLayer )
{
disconnect( mLayer, SIGNAL( selectionChanged() ), this, SLOT( refreshStatistics() ) );
}

mLayer = newLayer;

if ( mLayer )
{
connect( mLayer, SIGNAL( selectionChanged() ), this, SLOT( refreshStatistics() ) );
}

mFieldExpressionWidget->setLayer( mLayer );

if ( mFieldExpressionWidget->currentField().isEmpty() )
{
mStatisticsTable->setRowCount( 0 );
}
else
{
refreshStatistics();
}
}

void QgsStatisticalSummaryDockWidget::statActionTriggered( bool checked )
{
refreshStatistics();
QAction* action = dynamic_cast<QAction*>( sender() );
int stat = action->data().toInt();

QSettings settings;
if ( stat >= 0 )
{
settings.setValue( QString( "/StatisticalSummaryDock/checked_%1" ).arg( stat ), checked );
}
else if ( stat == MISSING_VALUES )
{
settings.setValue( QString( "/StatisticalSummaryDock/checked_missing_values" ).arg( stat ), checked );
}
}
@@ -0,0 +1,60 @@
/***************************************************************************
qgsstatisticalsummarydockwidget.h
---------------------------------
begin : May 2015
copyright : (C) 2015 by Nyall Dawson
email : nyall dot dawson at gmail dot 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. *
* *
***************************************************************************/
#ifndef QGSSTATISTICALSUMMARYDOCKWIDGET_H
#define QGSSTATISTICALSUMMARYDOCKWIDGET_H

#include <QDockWidget>
#include <QMap>
#include "ui_qgsstatisticalsummarybase.h"

#include "qgsstatisticalsummary.h"

class QgsBrowserModel;
class QModelIndex;
class QgsDockBrowserTreeView;
class QgsLayerItem;
class QgsDataItem;
class QgsBrowserTreeFilterProxyModel;

/**A dock widget which displays a statistical summary of the values in a field or expression
*/
class APP_EXPORT QgsStatisticalSummaryDockWidget : public QDockWidget, private Ui::QgsStatisticalSummaryWidgetBase
{
Q_OBJECT

public:
QgsStatisticalSummaryDockWidget( QWidget *parent = 0 );
~QgsStatisticalSummaryDockWidget();

public slots:

/**Recalculates the displayed statistics
*/
void refreshStatistics();

private slots:

void layerChanged( QgsMapLayer* layer );
void statActionTriggered( bool checked );

private:

QgsVectorLayer* mLayer;

QMap< int, QAction* > mStatsActions;
static QList< QgsStatisticalSummary::Statistic > mDisplayStats;
};

#endif // QGSSTATISTICALSUMMARYDOCKWIDGET_H

3 comments on commit e7b7549

@nirvn

This comment has been minimized.

Copy link
Contributor

@nirvn nirvn replied May 23, 2015

Nice job @nyalldawson . Any chance of introducing a way to copy the statistical values? A right-click menu?

I noticed a bug. When you open the statistics panel (via the toolbar button), a layer is selected by default (good) but the list of columns isn't computed (bad :) ). You have to use the layer drop down to select a layer (even if the layer your interested with happens to be the one selected by default) to see the list of columns.

@nirvn

This comment has been minimized.

Copy link
Contributor

@nirvn nirvn replied May 23, 2015

@nyalldawson it'd also be nice to allow to restrict statistics to visible features (the same way you allow for statistics to be restricted to selected features only).

@olivierdalang

This comment has been minimized.

Copy link
Contributor

@olivierdalang olivierdalang replied Jun 12, 2015

@nyalldawson Hi ! Great addition !

Did you know about my (old) plugin which aimed to do the same ( LiveStats ) ?

One thing which I miss in your implementation is the ability to run the statistic on the current layer. I often need to display the sum of the area of the selected polygons using the "$area" expression, and would love it to work on the current layer instead of a fixed layer.

Also at first sight I was thinking the UI would work in a different way, closer to my plugin's implementation. I thought the user would be able to add statistics from different fields/layers (eg sum of population of the selected countries, sum of gdp of selected countries, etc) each in a row of the table. So the user could fine tune what he wants to see, rather than having to focus on one specific attribute only. Of course the user could still focus on that, by adding several row with different aggregate functions.

And last point (detail) it would be great if the columns were fitted to the dockWidget's width.

Please sign in to comment.