Skip to content

Commit

Permalink
[FEATURE] New statistical summary dock widget
Browse files Browse the repository at this point in the history
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 e7b7549
Show file tree
Hide file tree
Showing 10 changed files with 542 additions and 1 deletion.
11 changes: 11 additions & 0 deletions python/core/qgsstatisticalsummary.sip
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/app/CMakeLists.txt
Expand Up @@ -103,6 +103,7 @@ SET(QGIS_APP_SRCS
qgsprojectproperties.cpp
qgsrastercalcdialog.cpp
qgsrasterlayerproperties.cpp
qgsstatisticalsummarydockwidget.cpp
qgstextannotationdialog.cpp
qgsshortcutsmanager.cpp
qgsguivectorlayertools.h
Expand Down Expand Up @@ -248,6 +249,7 @@ SET (QGIS_APP_MOC_HDRS
qgsrasterlayerproperties.h
qgssnappingdialog.h
qgssponsors.h
qgsstatisticalsummarydockwidget.h
qgssvgannotationdialog.h
qgstextannotationdialog.h
qgstipgui.h
Expand Down
15 changes: 15 additions & 0 deletions src/app/qgisapp.cpp
Expand Up @@ -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"
Expand Down Expand Up @@ -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() ) );
Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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

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

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


#ifdef HAVE_TOUCH
bool QgisApp::gestureEvent( QGestureEvent *event )
Expand Down
6 changes: 6 additions & 0 deletions src/app/qgisapp.h
Expand Up @@ -76,6 +76,7 @@ class QgsBrowserDockWidget;
class QgsAdvancedDigitizingDockWidget;
class QgsSnappingDialog;
class QgsGPSInformationWidget;
class QgsStatisticalSummaryDockWidget;

class QgsDecorationItem;

Expand Down Expand Up @@ -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) */
Expand Down Expand Up @@ -1615,6 +1620,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
QgsBrowserDockWidget *mBrowserWidget2;

QgsAdvancedDigitizingDockWidget *mAdvancedDigitizingDockWidget;
QgsStatisticalSummaryDockWidget* mStatisticalSummaryDockWidget;

QgsSnappingDialog *mSnappingDialog;

Expand Down
206 changes: 206 additions & 0 deletions src/app/qgsstatisticalsummarydockwidget.cpp
@@ -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 );
}
}
60 changes: 60 additions & 0 deletions src/app/qgsstatisticalsummarydockwidget.h
@@ -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
Copy link
Contributor

@nirvn nirvn commented on e7b7549 May 23, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

@nirvn nirvn commented on e7b7549 May 23, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.