From c25f396c2452760b9c40c95b6889a3abdfcb317e Mon Sep 17 00:00:00 2001
From: Thomas Steur <thomas.steur@googlemail.com>
Date: Wed, 5 Mar 2014 00:53:31 +0100
Subject: [PATCH] refs #57 started to work on movers and shakers and lots of
 minor improvements

---
 core/Twig.php                                 |  10 +
 plugins/Insights/API.php                      | 235 +++++++++++++-----
 plugins/Insights/Controller.php               |  26 +-
 .../DataTable/Filter/ExcludeLowValue.php      |  28 ++-
 plugins/Insights/DataTable/Filter/Insight.php |  89 ++++---
 plugins/Insights/Insights.php                 |   3 +-
 plugins/Insights/Visualizations/Insight.php   |   4 +-
 plugins/Insights/lang/en.json                 |   9 +-
 .../stylesheets/insightVisualization.less     |   7 +
 plugins/Insights/templates/index.twig         |   5 -
 .../Insights/templates/insightControls.twig   |  50 ++--
 .../templates/insightVisualization.twig       |  52 +---
 .../Insights/templates/overviewWidget.twig    |  37 +++
 plugins/Insights/templates/table_header.twig  |  13 +
 plugins/Insights/templates/table_row.twig     |  15 ++
 .../tests/FilterExcludeLowValueTest.php       |  33 ++-
 plugins/Insights/tests/FilterInsightTest.php  |  23 ++
 17 files changed, 439 insertions(+), 200 deletions(-)
 delete mode 100644 plugins/Insights/templates/index.twig
 create mode 100644 plugins/Insights/templates/overviewWidget.twig
 create mode 100644 plugins/Insights/templates/table_header.twig
 create mode 100644 plugins/Insights/templates/table_row.twig

diff --git a/core/Twig.php b/core/Twig.php
index fd266d0b7b..4da7a364f8 100644
--- a/core/Twig.php
+++ b/core/Twig.php
@@ -9,6 +9,7 @@
 namespace Piwik;
 
 use Exception;
+use Piwik\Period\Range;
 use Piwik\Translate;
 use Piwik\Visualization\Sparkline;
 use Piwik\View\RenderTokenParser;
@@ -63,6 +64,7 @@ class Twig
         $this->addFilter_truncate();
         $this->addFilter_notificiation();
         $this->addFilter_percentage();
+        $this->addFilter_prettyDate();
         $this->twig->addFilter(new Twig_SimpleFilter('implode', 'implode'));
         $this->twig->addFilter(new Twig_SimpleFilter('ucwords', 'ucwords'));
 
@@ -187,6 +189,14 @@ class Twig
         $this->twig->addFilter($notificationFunction);
     }
 
+    protected function addFilter_prettyDate()
+    {
+        $prettyDate = new Twig_SimpleFilter('prettyDate', function ($dateString, $period) {
+            return Range::factory($period, $dateString)->getLocalizedShortString();
+        });
+        $this->twig->addFilter($prettyDate);
+    }
+
     protected function addFilter_percentage()
     {
         $percentage = new Twig_SimpleFilter('percentage', function ($string, $totalValue, $precision = 1) {
diff --git a/plugins/Insights/API.php b/plugins/Insights/API.php
index f29d301c7a..9a95624fcf 100644
--- a/plugins/Insights/API.php
+++ b/plugins/Insights/API.php
@@ -31,39 +31,57 @@ class API extends \Piwik\Plugin\API
     const ORDER_BY_ABSOLUTE = 'absolute';
     const ORDER_BY_IMPORTANCE = 'importance';
 
-    public function getInsightsOverview($idSite, $period, $date)
+    private $reportIds = array(
+        'Actions_getPageUrls',
+        'Actions_getPageTitles',
+        'Actions_getDownloads',
+        'Referrers_getAll',
+        'Referrers_getKeywords',
+        'Referrers_getCampaigns',
+        'Referrers_getSocials',
+        'Referrers_getSearchEngines',
+        'UserCountry_getCountry',
+    );
+
+    public function getInsightsOverview($idSite, $period, $date, $segment = false)
     {
         Piwik::checkUserHasViewAccess(array($idSite));
 
+        $reportTableIds = array();
+
         /** @var DataTable[] $tables */
-        $reports = array(
-            'Actions_getPageUrls',
-            'Actions_getPageTitles',
-            'Actions_getDownloads',
-            'Referrers_getAll',
-            'Referrers_getKeywords',
-            'Referrers_getCampaigns',
-            'Referrers_getSocials',
-            'Referrers_getSearchEngines',
-            'UserCountry_getCountry',
-        );
-        // post event to add other reports?
+        $tables = array();
+        foreach ($this->reportIds as $reportId) {
+            $firstTableId     = DataTable\Manager::getInstance()->getMostRecentTableId();
+            $table            = $this->getInsights($idSite, $period, $date, $reportId, $segment, 3, 3, '', 2, 25);
+            $reportTableIds[] = $table->getId();
+            $this->deleteDataTables($firstTableId, $reportTableIds);
 
-        $reportTableIds   = array();
-        $dataTableManager = DataTable\Manager::getInstance();
+            $tables[] = $table;
+        }
+
+        $map = new DataTable\Map();
+
+        foreach ($tables as $table) {
+            $map->addTable($table, $table->getMetadata('reportName'));
+        }
+
+        return $map;
+    }
 
+    public function getOverallMoversAndShakers($idSite, $period, $date, $segment = false)
+    {
+        Piwik::checkUserHasViewAccess(array($idSite));
+
+        $reportTableIds = array();
+
+        /** @var DataTable[] $tables */
         $tables = array();
-        foreach ($reports as $report) {
-            $firstTableId     = $dataTableManager->getMostRecentTableId();
-            $table            = $this->getInsightOverview($idSite, $period, $date, $report);
+        foreach ($this->reportIds as $reportId) {
+            $firstTableId     = DataTable\Manager::getInstance()->getMostRecentTableId();
+            $table            = $this->getMoversAndShakers($idSite, $period, $date, $reportId, $segment, 4, 4);
             $reportTableIds[] = $table->getId();
-            $lastTableId      = $dataTableManager->getMostRecentTableId();
-
-            for ($index = $firstTableId; $index <= $lastTableId; $index++) {
-                if (!in_array($index, $reportTableIds)) {
-                    $dataTableManager->deleteTable($index);
-                }
-            }
+            $this->deleteDataTables($firstTableId, $reportTableIds);
 
             $tables[] = $table;
         }
@@ -77,18 +95,21 @@ class API extends \Piwik\Plugin\API
         return $map;
     }
 
-    public function getInsightOverview($idSite, $period, $date, $reportUniqueId, $segment = false, $limitIncreaser = 4,
-                                       $limitDecreaser = 4, $minVisitsPercent = 3, $minGrowthPercent = 25, $orderBy = 'absolute',
-                                       $considerMovers = true, $considerNew = true, $considerDisappeared = false)
+    public function getMoversAndShakers($idSite, $period, $date, $reportUniqueId, $segment = false,
+                                        $limitIncreaser = 4, $limitDecreaser = 4)
     {
+        $orderBy = 'absolute';
+        $minVisitsPercent = 2;
+        $minGrowthPercent = 30;
+        $minMoversPercent = 2;
+        $minNewPercent = 2;
+        $minDisappearedPercent = 2;
+
         Piwik::checkUserHasViewAccess(array($idSite));
 
         $metric = 'nb_visits';
-        // consider disappeared if impact > 10%?
-
-        $totalValue = $this->getTotalValue($idSite, $period, $date, $metric);
-        $minVisits  = $this->getMinVisits($totalValue, $minVisitsPercent);
 
+        $totalValue    = $this->getTotalValue($idSite, $period, $date, $metric);
         $report        = $this->getReportByUniqueId($idSite, $reportUniqueId);
         $currentReport = $this->requestReport($idSite, $period, $date, $report, $metric, $segment);
 
@@ -97,28 +118,29 @@ class API extends \Piwik\Plugin\API
             // for faster performance just compare against last week?
             $pastDate   = Date::factory($date);
             $pastDate   = $pastDate->subDay(7);
-            $lastDate   = $pastDate->toString();
-            $lastReport = $this->requestReport($idSite, 'week', $lastDate, $report, $metric, $segment);
+            $pastDate   = $pastDate->toString();
+            $lastReport = $this->requestReport($idSite, 'week', $pastDate, $report, $metric, $segment);
             $lastReport->filter('Piwik\Plugins\Insights\DataTable\Filter\Average', array($metric, 7));
+            $lastDate   = Range::factory('week', $pastDate);
+            $lastDate   = $lastDate->getRangeString();
         } else {
             $pastDate = Range::getLastDate($date, $period);
 
             if (empty($pastDate[0])) {
-                return new DataTable();
+                throw new \Exception('Not possible to calculate movers and shakers for this date/period combination');
             }
 
             $lastDate   = $pastDate[0];
             $lastReport = $this->requestReport($idSite, $period, $lastDate, $report, $metric, $segment);
         }
 
-        return $this->buildInsightsReport($period, $date, $limitIncreaser, $limitDecreaser, $minGrowthPercent, $orderBy, $currentReport, $lastReport, $metric, $considerMovers, $considerNew, $considerDisappeared, $minVisits, $report, $lastDate, $totalValue);
+        return $this->buildDataTable($report, $period, $date, $lastDate, $metric, $currentReport, $lastReport, $totalValue, $minVisitsPercent, $minMoversPercent, $minNewPercent, $minDisappearedPercent, $minGrowthPercent, $orderBy, $limitIncreaser, $limitDecreaser);
     }
 
-    // force $limitX and ignore minVisitsPercent, minGrowthPercent
     public function getInsights(
-        $idSite, $period, $date, $reportUniqueId, $limitIncreaser = 5, $limitDecreaser = 5,
+        $idSite, $period, $date, $reportUniqueId, $segment = false, $limitIncreaser = 5, $limitDecreaser = 5,
         $filterBy = '', $minVisitsPercent = 2, $minGrowthPercent = 20,
-        $comparedToXPeriods = 1, $orderBy = 'absolute', $segment = false)
+        $comparedToXPeriods = 1, $orderBy = 'absolute')
     {
         Piwik::checkUserHasViewAccess(array($idSite));
 
@@ -127,33 +149,36 @@ class API extends \Piwik\Plugin\API
 
         $lastDate = Range::getDateXPeriodsAgo(abs($comparedToXPeriods), $date, $period);
 
+        if (empty($lastDate[0])) {
+            throw new \Exception('Not possible to calculate movers and shakers for this date/period combination');
+        }
+
         $currentReport = $this->requestReport($idSite, $period, $date, $report, $metric, $segment);
         $lastReport    = $this->requestReport($idSite, $period, $lastDate[0], $report, $metric, $segment);
 
         $totalValue = $this->getRelevantTotalValue($idSite, $period, $date, $currentReport, $metric);
-        $minVisits  = $this->getMinVisits($totalValue, $minVisitsPercent);
 
-        $considerMovers = false;
-        $considerNew = false;
-        $considerDisappeared = false;
+        $minMoversPercent = -1;
+        $minNewPercent = -1;
+        $minDisappearedPercent = -1;
 
         switch ($filterBy) {
             case self::FILTER_BY_MOVERS:
-                $considerMovers = true;
+                $minMoversPercent = 0;
                 break;
             case self::FILTER_BY_NEW:
-                $considerNew = true;
+                $minNewPercent = 0;
                 break;
             case self::FILTER_BY_DISAPPEARED:
-                $considerDisappeared = true;
+                $minDisappearedPercent = 0;
                 break;
             default:
-                $considerMovers = true;
-                $considerNew = true;
-                $considerDisappeared = true;
+                $minMoversPercent      = 0;
+                $minNewPercent         = 0;
+                $minDisappearedPercent = 0;
         }
 
-        return $this->buildInsightsReport($period, $date, $limitIncreaser, $limitDecreaser, $minGrowthPercent, $orderBy, $currentReport, $lastReport, $metric, $considerMovers, $considerNew, $considerDisappeared, $minVisits, $report, $lastDate[0], $totalValue);
+        return $this->buildDataTable($report, $period, $date, $lastDate[0], $metric, $currentReport, $lastReport, $totalValue, $minVisitsPercent, $minMoversPercent, $minNewPercent, $minDisappearedPercent, $minGrowthPercent, $orderBy, $limitIncreaser, $limitDecreaser);
     }
 
     private function requestReport($idSite, $period, $date, $report, $metric, $segment)
@@ -197,9 +222,9 @@ class API extends \Piwik\Plugin\API
         return $orderByColumn;
     }
 
-    private function getMinVisits($totalValue, $minVisitsPercent)
+    private function getMinVisits($totalValue, $percent)
     {
-        $minVisits = ceil(($totalValue / 100) * $minVisitsPercent);
+        $minVisits = ceil(($totalValue / 100) * $percent);
 
         return (int) $minVisits;
     }
@@ -218,8 +243,9 @@ class API extends \Piwik\Plugin\API
 
     private function getTotalValue($idSite, $period, $date, $metric)
     {
-        $visits = VisitsSummaryAPI::getInstance()->get($idSite, $period, $date, false, array($metric));
+        $visits     = VisitsSummaryAPI::getInstance()->get($idSite, $period, $date, false, array($metric));
         $totalValue = $visits->getFirstRow()->getColumn($metric);
+
         return $totalValue;
     }
 
@@ -236,8 +262,36 @@ class API extends \Piwik\Plugin\API
         return $totalValue;
     }
 
-    private function buildInsightsReport($period, $date, $limitIncreaser, $limitDecreaser, $minGrowthPercent, $orderBy, $currentReport, $lastReport, $metric, $considerMovers, $considerNew, $considerDisappeared, $minVisits, $report, $lastDate, $totalValue)
+    /**
+     * @param array $reportMetadata
+     * @param string $period
+     * @param string $date
+     * @param string $lastDate
+     * @param string $metric
+     * @param DataTable $currentReport
+     * @param DataTable $lastReport
+     * @param int $totalValue
+     * @param int $minVisitsPercent            Row must have at least min percent visits of totalVisits
+     * @param int $minVisitsMoversPercent      Exclude rows who moved and the difference is not at least min percent
+     *                                         visits of totalVisits. -1 excludes movers.
+     * @param int $minVisitsNewPercent         Exclude rows who are new and the difference is not at least min percent
+     *                                         visits of totalVisits. -1 excludes all new.
+     * @param int $minVisitsDisappearedPercent Exclude rows who are disappeared and the difference is not at least min
+     *                                         percent visits of totalVisits. -1 excludes all disappeared.
+     * @param int $minGrowthPercent            The actual growth of a row must be at least percent compared to the
+     *                                         previous value (not total value)
+     * @param string $orderBy                  Order by absolute, relative, importance
+     * @param int $limitIncreaser
+     * @param int $limitDecreaser
+     * @return DataTable
+     */
+    private function buildDataTable($reportMetadata, $period, $date, $lastDate, $metric, $currentReport, $lastReport, $totalValue, $minVisitsPercent, $minVisitsMoversPercent, $minVisitsNewPercent, $minVisitsDisappearedPercent, $minGrowthPercent, $orderBy, $limitIncreaser, $limitDecreaser)
     {
+        $minVisits = $this->getMinVisits($totalValue, $minVisitsPercent);
+        $minChangeMovers = 0;
+        $minIncreaseNew = 0;
+        $minDecreaseDisappeared = 0;
+
         $dataTable = new DataTable();
         $dataTable->filter(
             'Piwik\Plugins\Insights\DataTable\Filter\Insight',
@@ -245,9 +299,9 @@ class API extends \Piwik\Plugin\API
                 $currentReport,
                 $lastReport,
                 $metric,
-                $considerMovers,
-                $considerNew,
-                $considerDisappeared
+                $considerMovers = (-1 !== $minVisitsMoversPercent),
+                $considerNew = (-1 !== $minVisitsNewPercent),
+                $considerDisappeared = (-1 !== $minVisitsDisappearedPercent)
             )
         );
 
@@ -255,7 +309,7 @@ class API extends \Piwik\Plugin\API
             'Piwik\Plugins\Insights\DataTable\Filter\MinGrowth',
             array(
                 'growth_percent_numeric',
-                $minGrowthPercent
+                $minGrowthPercent,
             )
         );
 
@@ -267,6 +321,42 @@ class API extends \Piwik\Plugin\API
             )
         );
 
+        if ($minVisitsNewPercent) {
+            $minIncreaseNew = $this->getMinVisits($totalValue, $minVisitsNewPercent);
+            $dataTable->filter(
+                'Piwik\Plugins\Insights\DataTable\Filter\ExcludeLowValue',
+                array(
+                    'difference',
+                    $minIncreaseNew,
+                    'isNew'
+                )
+            );
+        }
+
+        if ($minVisitsMoversPercent) {
+            $minChangeMovers = $this->getMinVisits($totalValue, $minVisitsMoversPercent);
+            $dataTable->filter(
+                'Piwik\Plugins\Insights\DataTable\Filter\ExcludeLowValue',
+                array(
+                    'difference',
+                    $minChangeMovers,
+                    'isMover'
+                )
+            );
+        }
+
+        if ($minVisitsDisappearedPercent) {
+            $minDecreaseDisappeared = $this->getMinVisits($totalValue, $minVisitsDisappearedPercent);
+            $dataTable->filter(
+                'Piwik\Plugins\Insights\DataTable\Filter\ExcludeLowValue',
+                array(
+                    'difference',
+                    $minDecreaseDisappeared,
+                    'isDisappeared'
+                )
+            );
+        }
+
         $dataTable->filter(
             'Piwik\Plugins\Insights\DataTable\Filter\OrderBy',
             array(
@@ -284,14 +374,22 @@ class API extends \Piwik\Plugin\API
         );
 
         $dataTable->setMetadataValues(array(
-            'reportName' => $report['name'],
-            'metricName' => $report['metrics'][$metric],
+            'reportName' => $reportMetadata['name'],
+            'metricName' => $reportMetadata['metrics'][$metric],
             'date' => $date,
             'lastDate' => $lastDate,
             'period' => $period,
-            'report' => $report,
+            'report' => $reportMetadata,
             'totalValue' => $totalValue,
-            'minVisits' => $minVisits
+            'minValue'  => $minVisits,
+            'minChangeMovers' => $minChangeMovers,
+            'minIncreaseNew' => $minIncreaseNew,
+            'minDecreaseDisappeared' => $minDecreaseDisappeared,
+            'minValuePercent' => $minVisitsPercent,
+            'minGrowthPercent' => $minGrowthPercent,
+            'minVisitsMoversPercent' => $minVisitsMoversPercent,
+            'minVisitsNewPercent' => $minVisitsNewPercent,
+            'minVisitsDisappearedPercent' => $minVisitsDisappearedPercent
         ));
 
         return $dataTable;
@@ -301,6 +399,19 @@ class API extends \Piwik\Plugin\API
     {
         $processedReport = new ProcessedReport();
         $report = $processedReport->getReportMetadataByUniqueId($idSite, $reportUniqueId);
+
         return $report;
     }
+
+    private function deleteDataTables($firstTableId, $dataTableIdsToBeIgnored)
+    {
+        $dataTableManager = DataTable\Manager::getInstance();
+        $lastTableId = $dataTableManager->getMostRecentTableId();
+
+        for ($index = $firstTableId; $index <= $lastTableId; $index++) {
+            if (!in_array($index, $dataTableIdsToBeIgnored)) {
+                $dataTableManager->deleteTable($index);
+            }
+        }
+    }
 }
diff --git a/plugins/Insights/Controller.php b/plugins/Insights/Controller.php
index 8526052215..b9431f5a3c 100644
--- a/plugins/Insights/Controller.php
+++ b/plugins/Insights/Controller.php
@@ -18,7 +18,21 @@ use Piwik\View;
 class Controller extends \Piwik\Plugin\Controller
 {
 
-    public function getInsightOverview()
+    public function getInsightsOverview()
+    {
+        $view = $this->prepareWidget($apiReport = 'getInsightsOverview');
+
+        return $view->render();
+    }
+
+    public function getOverallMoversAndShakers()
+    {
+        $view = $this->prepareWidget($apiReport = 'getOverallMoversAndShakers');
+
+        return $view->render();
+    }
+
+    private function prepareWidget($apiReport)
     {
         $idSite = Common::getRequestVar('idSite', null, 'int');
         $period = Common::getRequestVar('period', null, 'string');
@@ -26,18 +40,14 @@ class Controller extends \Piwik\Plugin\Controller
 
         Piwik::checkUserHasViewAccess(array($idSite));
 
-        $view = new View('@Insights/index.twig');
+        $view = new View('@Insights/overviewWidget.twig');
         $this->setBasicVariablesView($view);
 
-        $view->moversAndShakers = API::getInstance()->getInsightsOverview($idSite, $period, $date);
-        $view->showNoDataMessage = false;
-        $view->showInsightsControls = false;
+        $view->reports = API::getInstance()->$apiReport($idSite, $period, $date);
         $view->properties = array(
-            'show_increase' => true,
-            'show_decrease' => true,
             'order_by' => 'absolute'
         );
 
-        return $view->render();
+        return $view;
     }
 }
diff --git a/plugins/Insights/DataTable/Filter/ExcludeLowValue.php b/plugins/Insights/DataTable/Filter/ExcludeLowValue.php
index aea5d1d9ea..2d2c3ede4b 100644
--- a/plugins/Insights/DataTable/Filter/ExcludeLowValue.php
+++ b/plugins/Insights/DataTable/Filter/ExcludeLowValue.php
@@ -14,11 +14,21 @@ class ExcludeLowValue extends DataTable\BaseFilter
 {
     private $minimumValue;
     private $columnToRead;
+    private $columnToCheckToBeTrue;
 
-    public function __construct($table, $columnToRead, $minimumValue)
+    /**
+     * @param DataTable $table
+     * @param string $columnToRead
+     * @param int    $minimumValue
+     * @param string $columnToCheckToBeTrue  if set, we will delete a row only if this column evaluates to true. If
+     *                                       column does not evaluate to true we will not delete the row even if
+     *                                       the value is lower than the minimumValue.
+     */
+    public function __construct($table, $columnToRead, $minimumValue, $columnToCheckToBeTrue = '')
     {
         $this->columnToRead = $columnToRead;
         $this->minimumValue = $minimumValue;
+        $this->columnToCheckToBeTrue = $columnToCheckToBeTrue;
     }
 
     public function filter($table)
@@ -27,11 +37,17 @@ class ExcludeLowValue extends DataTable\BaseFilter
             return;
         }
 
-        $minimumValue = $this->minimumValue;
-        $isValueLowPopulation = function ($value) use ($minimumValue) {
-            return $value < $minimumValue;
-        };
+        foreach ($table->getRows() as $key => $row) {
 
-        $table->filter('ColumnCallbackDeleteRow', array($this->columnToRead, $isValueLowPopulation));
+            if ($this->columnToCheckToBeTrue && !$row->getColumn($this->columnToCheckToBeTrue)) {
+                continue;
+            }
+
+            $value = $row->getColumn($this->columnToRead);
+
+            if ($this->minimumValue > $value) {
+                $table->deleteRow($key);
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/plugins/Insights/DataTable/Filter/Insight.php b/plugins/Insights/DataTable/Filter/Insight.php
index 44d767cccf..699d9a6614 100644
--- a/plugins/Insights/DataTable/Filter/Insight.php
+++ b/plugins/Insights/DataTable/Filter/Insight.php
@@ -30,53 +30,69 @@ class Insight extends DataTable\Filter\CalculateEvolutionFilter
 
     public function filter($table)
     {
-        foreach ($this->currentDataTable->getRows() as $key => $row) {
-            $pastRow  = $this->getPastRowFromCurrent($row);
-            $oldValue = 0;
+        foreach ($this->currentDataTable->getRows() as $row) {
+            $this->addRowIfNewOrMover($table, $row);
+        }
 
-            if (!$pastRow && !$this->considerNew) {
-                continue;
+        if ($this->considerDisappeared) {
+            foreach ($this->pastDataTable->getRows() as $row) {
+                $this->addRowIfDisappeared($table, $row);
             }
+        }
+    }
 
-            if ($pastRow && $this->considerMovers) {
-                $oldValue = $pastRow->getColumn($this->columnValueToRead);
-            } elseif ($pastRow) {
-                continue;
-            }
+    private function addRowIfDisappeared(DataTable $table, DataTable\Row $row)
+    {
+        if ($this->getRowFromTable($this->currentDataTable, $row)) {
+            return;
+        }
 
-            $difference = $this->getDividend($row);
-            if ($difference === false) {
-                continue;
-            }
+        $newValue   = 0;
+        $oldValue   = $row->getColumn($this->columnValueToRead);
+        $difference = $newValue - $oldValue;
+
+        if ($oldValue == 0 && $newValue == 0) {
+            $growthPercentage = '0%';
+        } else {
+            $growthPercentage = '-100%';
+        }
 
-            $newValue = $row->getColumn($this->columnValueToRead);
-            $divisor  = $this->getDivisor($row);
+        $this->addRow($table, $row, $growthPercentage, $newValue, $oldValue, $difference, $isDisappeared = true);
+    }
 
-            $growthPercentage = $this->formatValue($difference, $divisor);
+    private function addRowIfNewOrMover(DataTable $table, DataTable\Row $row)
+    {
+        $pastRow = $this->getPastRowFromCurrent($row);
 
-            $this->addRow($table, $row, $growthPercentage, $newValue, $oldValue, $difference);
+        if (!$pastRow && !$this->considerNew) {
+            return;
+        } elseif ($pastRow && !$this->considerMovers) {
+            return;
         }
 
-        if ($this->considerDisappeared) {
-            foreach ($this->pastDataTable->getRows() as $key => $row) {
+        $isNew   = false;
+        $isMover = false;
+        $isDisappeared = false;
+
+        if (!$pastRow) {
+            $isNew    = true;
+            $oldValue = 0;
+        } else {
+            $isMover  = true;
+            $oldValue = $pastRow->getColumn($this->columnValueToRead);
+        }
 
-                if ($this->getRowFromTable($this->currentDataTable, $row)) {
-                    continue;
-                }
+        $difference = $this->getDividend($row);
+        if ($difference === false) {
+            return;
+        }
 
-                $newValue   = 0;
-                $oldValue   = $row->getColumn($this->columnValueToRead);
-                $difference = $newValue - $oldValue;
+        $newValue = $row->getColumn($this->columnValueToRead);
+        $divisor  = $this->getDivisor($row);
 
-                if ($oldValue == 0 && $newValue == 0) {
-                    $growthPercentage = '0%';
-                } else {
-                    $growthPercentage = '-100%';
-                }
+        $growthPercentage = $this->formatValue($difference, $divisor);
 
-                $this->addRow($table, $row, $growthPercentage, $newValue, $oldValue, $difference);
-            }
-        }
+        $this->addRow($table, $row, $growthPercentage, $newValue, $oldValue, $difference, $isDisappeared, $isNew, $isMover);
     }
 
     private function getRowFromTable(DataTable $table, DataTable\Row $row)
@@ -84,7 +100,7 @@ class Insight extends DataTable\Filter\CalculateEvolutionFilter
         return $table->getRowFromLabel($row->getColumn('label'));
     }
 
-    private function addRow(DataTable $table, DataTable\Row $row, $growthPercentage, $newValue, $oldValue, $difference)
+    private function addRow(DataTable $table, DataTable\Row $row, $growthPercentage, $newValue, $oldValue, $difference, $disappeared = false, $isNew = false, $isMover = false)
     {
         $columns = $row->getColumns();
         $columns['growth_percent'] = $growthPercentage;
@@ -94,6 +110,9 @@ class Insight extends DataTable\Filter\CalculateEvolutionFilter
         $columns['value_new']  = $newValue;
         $columns['difference'] = $difference;
         $columns['importance'] = abs($difference);
+        $columns['isDisappeared'] = $disappeared;
+        $columns['isNew']   = $isNew;
+        $columns['isMover'] = $isMover;
 
         $table->addRowFromArray(array(DataTable\Row::COLUMNS => $columns));
     }
diff --git a/plugins/Insights/Insights.php b/plugins/Insights/Insights.php
index 1753b32aa8..9c915ce99d 100644
--- a/plugins/Insights/Insights.php
+++ b/plugins/Insights/Insights.php
@@ -34,7 +34,8 @@ class Insights extends \Piwik\Plugin
 
     public function addWidgets()
     {
-        WidgetsList::add('Insights_Category', 'Insights_OverviewWidgetTitle', 'Insights', 'getInsightOverview');
+        WidgetsList::add('Insights_WidgetCategory', 'Insights_OverviewWidgetTitle', 'Insights', 'getInsightsOverview');
+        WidgetsList::add('Insights_WidgetCategory', 'Insights_MoversAndShakersWidgetTitle', 'Insights', 'getOverallMoversAndShakers');
     }
 
     public function getStylesheetFiles(&$stylesheets)
diff --git a/plugins/Insights/Visualizations/Insight.php b/plugins/Insights/Visualizations/Insight.php
index f16c913b0f..cd68023dea 100644
--- a/plugins/Insights/Visualizations/Insight.php
+++ b/plugins/Insights/Visualizations/Insight.php
@@ -25,7 +25,7 @@ class Insight extends Visualization
 {
     const ID = 'insightsVisualization';
     const TEMPLATE_FILE     = '@Insights/insightVisualization.twig';
-    const FOOTER_ICON_TITLE = 'InsightsVisualization';
+    const FOOTER_ICON_TITLE = 'Insights';
     const FOOTER_ICON       = 'plugins/Insights/images/idea.png';
 
     public function beforeLoadDataTable()
@@ -80,8 +80,6 @@ class Insight extends Visualization
 
     public function afterAllFiltersAreApplied()
     {
-        $this->assignTemplateVar('showNoDataMessage', true);
-        $this->assignTemplateVar('showInsightsControls', true);
         $this->assignTemplateVar('period', Common::getRequestVar('period', null, 'string'));
     }
 
diff --git a/plugins/Insights/lang/en.json b/plugins/Insights/lang/en.json
index acc356241a..f142b6add0 100644
--- a/plugins/Insights/lang/en.json
+++ b/plugins/Insights/lang/en.json
@@ -1,7 +1,12 @@
 {
     "Insights": {
         "OverviewWidgetTitle": "Insights Overview",
-        "Category": "Insights",
-        "NoResultMatchesCriteria": "No rows match the criteria"
+        "WidgetCategory": "Insights",
+        "NoResultMatchesCriteria": "No rows match the criteria",
+        "MoversAndShakersWidgetTitle": "Movers and Shakers",
+        "TitleConsideredVisits": "Considered rows having at least %s visits (%s%% of %s visits) with a growth of at least %s%% compared to %s.",
+        "TitleConsideredChanges": "Considered movers only if they increased or decreased by more than %s visits, new entries only if they increase by more than %s visits, and disappeared rows if they decreased by more than %s visits.",
+        "TitleReportBasedOn": "Based on %s %s, rows less than %s %s were ignored",
+        "TitleRowChangeDetails": "'%s' changed from %s (%s) to %s (%s) %s"
     }
 }
\ No newline at end of file
diff --git a/plugins/Insights/stylesheets/insightVisualization.less b/plugins/Insights/stylesheets/insightVisualization.less
index b559c9e36a..a5ed362414 100644
--- a/plugins/Insights/stylesheets/insightVisualization.less
+++ b/plugins/Insights/stylesheets/insightVisualization.less
@@ -25,4 +25,11 @@
   .notGrown {
     color:red;
   }
+
+  table td {
+    &.labelodd, &.labeleven {
+
+      background-image: none;
+    }
+  }
 }
\ No newline at end of file
diff --git a/plugins/Insights/templates/index.twig b/plugins/Insights/templates/index.twig
deleted file mode 100644
index fc91a81330..0000000000
--- a/plugins/Insights/templates/index.twig
+++ /dev/null
@@ -1,5 +0,0 @@
-{% for dataTable in moversAndShakers.getDataTables() %}
-
-    {% include "@Insights/insightVisualization.twig" %}
-
-{% endfor %}
\ No newline at end of file
diff --git a/plugins/Insights/templates/insightControls.twig b/plugins/Insights/templates/insightControls.twig
index d976ecccf8..d2d3779bd6 100644
--- a/plugins/Insights/templates/insightControls.twig
+++ b/plugins/Insights/templates/insightControls.twig
@@ -1,5 +1,5 @@
 <div style="padding: 10px;padding-bottom: 0px;">
-    Minimum impact of
+    Minimum visits of
     <select name="minVisitsPercent" title="Based on a total of {{ dataTable.getMetadata('totalValue') }} visitors or metricname">
         {% for i in range(0, 10) %}
             <option {% if i == properties.min_visits_percent %}selected{% endif %} value="{{ i }}">{{ i }}%</option>
@@ -7,7 +7,7 @@
         {% for i in range(12, 30, 2) %}
             <option {% if i == properties.min_visits_percent %}selected{% endif %} value="{{ i }}">{{ i }}%</option>
         {% endfor %}
-        {% for i in range(35, 100, 5) %}
+        {% for i in range(40, 100, 10) %}
             <option {% if i == properties.min_visits_percent %}selected{% endif %} value="{{ i }}">{{ i }}%</option>
         {% endfor %}
     </select>
@@ -25,28 +25,30 @@
         {% endfor %}
     </select>
 
-    compared to
-    {% if period == 'day' %}
-        <select size="1" name="comparedToXPeriodsAgo">
-            <option value="1" {% if properties.compared_to_x_periods_ago == 1 %}selected{% endif %}>
-               previous day
-            </option>
-            <option value="7" {% if properties.compared_to_x_periods_ago == 7 %}selected{% endif %}>
-               same day in previous week
-            </option>
-            <option value="365" {% if properties.compared_to_x_periods_ago == 365 %}selected{% endif %}>
-               same day in previous year
-            </option>
-        </select>
-    {% elseif period == 'month' %}
-        <select size="1" name="comparedToXPeriodsAgo">
-            <option value="1" {% if properties.compared_to_x_periods_ago == 1 %}selected{% endif %}>
-                previous month
-            </option>
-            <option value="12" {% if properties.compared_to_x_periods_ago == 12 %}selected{% endif %}>
-                same month in previous year
-            </option>
-        </select>
+    {% if period == 'day' or period == 'month' %}
+        compared to
+        {% if period == 'day' %}
+            <select size="1" name="comparedToXPeriodsAgo">
+                <option value="1" {% if properties.compared_to_x_periods_ago == 1 %}selected{% endif %}>
+                   previous day
+                </option>
+                <option value="7" {% if properties.compared_to_x_periods_ago == 7 %}selected{% endif %}>
+                   same day in previous week
+                </option>
+                <option value="365" {% if properties.compared_to_x_periods_ago == 365 %}selected{% endif %}>
+                   same day in previous year
+                </option>
+            </select>
+        {% elseif period == 'month' %}
+            <select size="1" name="comparedToXPeriodsAgo">
+                <option value="1" {% if properties.compared_to_x_periods_ago == 1 %}selected{% endif %}>
+                    previous month
+                </option>
+                <option value="12" {% if properties.compared_to_x_periods_ago == 12 %}selected{% endif %}>
+                    same month in previous year
+                </option>
+            </select>
+        {% endif %}
     {% endif %}
 
     <hr style="height: 1px;border: 0px;background-color: #cccccc;" />
diff --git a/plugins/Insights/templates/insightVisualization.twig b/plugins/Insights/templates/insightVisualization.twig
index 9c9f72c479..1ca0fac5ec 100644
--- a/plugins/Insights/templates/insightVisualization.twig
+++ b/plugins/Insights/templates/insightVisualization.twig
@@ -1,54 +1,24 @@
-<div class="insightsDataTable">
+{% set metadata = dataTable.getAllTableMetadata%}
+{% set consideredVisits = 'Insights_TitleConsideredVisits'|translate(metadata.minValue, metadata.minValuePercent, metadata.totalValue, metadata.minGrowthPercent, metadata.lastDate|prettyDate(metadata.period)) %}
+
+<div class="insightsDataTable" title="{{ consideredVisits|e('html_attr') }}">
     {% if dataTable.getRowsCount > 0 %}
         <table class="dataTable">
             <thead>
-            <tr title="Based on {{ dataTable.getMetadata('totalValue') }} {{ dataTable.getMetadata('metricName') }}, rows less than {{ dataTable.getMetadata('minVisits') }} {{ dataTable.getMetadata('metricName') }} were ignored">
-                <th class="label">
-                    {{ dataTable.getMetadata('reportName') }}
-                </th>
-                <th class="label orderBy" name="orderBy" value="absolute"
-                    style="{% if 'absolute' == properties.order_by %}font-weight:bold;{% endif %}">
-                    {{ dataTable.getMetadata('metricName') }}
-                </th>
-                <th class="label orderBy" name="orderBy" value="relative"
-                    style="{% if 'relative' == properties.order_by %}font-weight:bold;{% endif %}">
-                    {{ 'MultiSites_Evolution'|translate }}
-                </th>
-            </tr>
+                {% include "@Insights/table_header.twig" %}
             </thead>
-            <tbody>
-
-            {% for row in dataTable.getRows %}
-                <tr title="{{ dataTable.getMetadata('metricName') }} changed from {{ row.getColumn('value_old') }} ({{ dataTable.getMetadata('lastDate') }}) to {{ row.getColumn('value_new') }} ({{ dataTable.getMetadata('date') }})">
-                    <td>
-                        <span class="title"
-                              title="{{ row.getColumn('label') }}">
-                            {{ row.getColumn('label') }}
-                        </span>
-                    </td>
-                    {% if row.getColumn('grown') %}
-                        <td>+{{ row.getColumn('difference') }}</td>
-                        <td class="grown">
-                            +{{ row.getColumn('growth_percent') }}
-                        </td>
-                    {% else %}
-                        <td>{{ row.getColumn('difference') }}</td>
-                        <td class="notGrown">
-                            {{ row.getColumn('growth_percent') }}
-                        </td>
-                    {% endif %}
-                </tr>
-            {% endfor %}
 
+            <tbody>
+                {% for row in dataTable.getRows %}
+                    {% include "@Insights/table_row.twig" %}
+                {% endfor %}
             </tbody>
         </table>
-    {% elseif showNoDataMessage %}
+    {% else %}
         <div class="pk-emptyDataTable">
             {{ 'Insights_NoResultMatchesCriteria'|translate }}
         </div>
     {% endif %}
 
-    {% if showInsightsControls %}
-        {% include "@Insights/insightControls.twig" %}
-    {% endif %}
+    {% include "@Insights/insightControls.twig" %}
 </div>
\ No newline at end of file
diff --git a/plugins/Insights/templates/overviewWidget.twig b/plugins/Insights/templates/overviewWidget.twig
new file mode 100644
index 0000000000..ca6898b398
--- /dev/null
+++ b/plugins/Insights/templates/overviewWidget.twig
@@ -0,0 +1,37 @@
+{% set allMetadata = reports.getFirstRow.getAllTableMetadata %}
+
+{% set consideredVisits = 'Insights_TitleConsideredVisits'|translate(allMetadata.minValue, allMetadata.minValuePercent, allMetadata.totalValue, allMetadata.minGrowthPercent, allMetadata.lastDate|prettyDate(allMetadata.period)) %}
+{% set consideredChanges = '' %}
+
+{% if allMetadata.minChangeMovers or allMetadata.minIncreaseNew or allMetadata.minDecreaseDisappeared %}
+    {% set consideredChanges = 'Insights_TitleConsideredChanges'|translate(allMetadata.minChangeMovers, allMetadata.minIncreaseNew, allMetadata.minDecreaseDisappeared) %}
+{% endif %}
+
+<div class="insightsDataTable" title="{{ consideredVisits|e('html_attr') }} {{ consideredChanges|e('html_attr') }}">
+    {% if reports.getColumns|length > 0 %}
+
+        <table class="dataTable">
+            {% for dataTable in reports.getDataTables() if dataTable.getRowsCount > 0 %}
+                {% set metadata = dataTable.getAllTableMetadata %}
+
+                <thead>
+                    {% include "@Insights/table_header.twig" %}
+                </thead>
+
+                <tbody>
+                    {% for row in dataTable.getRows %}
+                        {% include "@Insights/table_row.twig" %}
+                    {% endfor %}
+                </tbody>
+
+            {% endfor %}
+        </table>
+
+    {% else %}
+
+        <div class="pk-emptyDataTable">
+            {{ 'Insights_NoResultMatchesCriteria'|translate }}
+        </div>
+
+    {% endif %}
+</div>
\ No newline at end of file
diff --git a/plugins/Insights/templates/table_header.twig b/plugins/Insights/templates/table_header.twig
new file mode 100644
index 0000000000..7226f88450
--- /dev/null
+++ b/plugins/Insights/templates/table_header.twig
@@ -0,0 +1,13 @@
+<tr>
+    <th class="label">
+        {{ metadata.reportName }}
+    </th>
+    <th class="label orderBy" name="orderBy" value="absolute"
+        style="{% if 'absolute' == properties.order_by %}font-weight:bold;{% endif %}">
+        {{ metadata.metricName }}
+    </th>
+    <th class="label orderBy" name="orderBy" value="relative"
+        style="{% if 'relative' == properties.order_by %}font-weight:bold;{% endif %}">
+        {{ 'MultiSites_Evolution'|translate }}
+    </th>
+</tr>
\ No newline at end of file
diff --git a/plugins/Insights/templates/table_row.twig b/plugins/Insights/templates/table_row.twig
new file mode 100644
index 0000000000..bd3b04a2e7
--- /dev/null
+++ b/plugins/Insights/templates/table_row.twig
@@ -0,0 +1,15 @@
+<tr title="{{ 'Insights_TitleRowChangeDetails'|translate(row.getColumn('label'), row.getColumn('value_old'), metadata.lastDate|prettyDate(metadata.period), row.getColumn('value_new'), metadata.date|prettyDate(metadata.period), metadata.metricName)|e('html_attr') }}">
+    <td>
+        <span class="title">
+            {{ row.getColumn('label') }}
+        </span>
+    </td>
+
+    {% if row.getColumn('grown') %}
+        <td>+{{ row.getColumn('difference') }}</td>
+        <td class="grown">+{{ row.getColumn('growth_percent') }}</td>
+    {% else %}
+        <td>{{ row.getColumn('difference') }}</td>
+        <td class="notGrown">{{ row.getColumn('growth_percent') }}</td>
+    {% endif %}
+</tr>
\ No newline at end of file
diff --git a/plugins/Insights/tests/FilterExcludeLowValueTest.php b/plugins/Insights/tests/FilterExcludeLowValueTest.php
index e9080c85da..45ddf8075e 100644
--- a/plugins/Insights/tests/FilterExcludeLowValueTest.php
+++ b/plugins/Insights/tests/FilterExcludeLowValueTest.php
@@ -29,17 +29,17 @@ class FilterExcludeLowValueTest extends \PHPUnit_Framework_TestCase
     {
         $this->table = new DataTable();
         $this->table->addRowsFromArray(array(
-            array(Row::COLUMNS => array('label' => 'val1', 'growth' => 22)),
-            array(Row::COLUMNS => array('label' => 'val2', 'growth' => 14)),
-            array(Row::COLUMNS => array('label' => 'val3', 'growth' => 18)),
-            array(Row::COLUMNS => array('label' => 'val4', 'growth' => 20)),
-            array(Row::COLUMNS => array('label' => 'val5', 'growth' => 22)),
-            array(Row::COLUMNS => array('label' => 'val6', 'growth' => 25)),
-            array(Row::COLUMNS => array('label' => 'val7', 'growth' => 17)),
-            array(Row::COLUMNS => array('label' => 'val8', 'growth' => 20)),
-            array(Row::COLUMNS => array('label' => 'val9', 'growth' => 0)),
-            array(Row::COLUMNS => array('label' => 'val10', 'growth' => 15)),
-            array(Row::COLUMNS => array('label' => 'val11', 'growth' => 16))
+            array(Row::COLUMNS => array('label' => 'val1', 'growth' => 22, 'isFooBar' => false)),
+            array(Row::COLUMNS => array('label' => 'val2', 'growth' => 14, 'isFooBar' => true)),
+            array(Row::COLUMNS => array('label' => 'val3', 'growth' => 18, 'isFooBar' => false)),
+            array(Row::COLUMNS => array('label' => 'val4', 'growth' => 20, 'isFooBar' => true)),
+            array(Row::COLUMNS => array('label' => 'val5', 'growth' => 22, 'isFooBar' => true)),
+            array(Row::COLUMNS => array('label' => 'val6', 'growth' => 25, 'isFooBar' => true)),
+            array(Row::COLUMNS => array('label' => 'val7', 'growth' => 17, 'isFooBar' => false)),
+            array(Row::COLUMNS => array('label' => 'val8', 'growth' => 20, 'isFooBar' => false)),
+            array(Row::COLUMNS => array('label' => 'val9', 'growth' => 0, 'isFooBar' => false)),
+            array(Row::COLUMNS => array('label' => 'val10', 'growth' => 15, 'isFooBar' => false)),
+            array(Row::COLUMNS => array('label' => 'val11', 'growth' => 16, 'isFooBar' => true))
         ));
     }
 
@@ -74,15 +74,22 @@ class FilterExcludeLowValueTest extends \PHPUnit_Framework_TestCase
         $this->assertOrder(array());
     }
 
+    public function testShouldRemoveValuesOnlyIfColumnToCheckIsTrue()
+    {
+        $this->excludeLowValues(21, 'isFooBar');
+
+        $this->assertOrder(array('val1', 'val3', 'val5', 'val6', 'val7', 'val8', 'val9', 'val10'));
+    }
+
     private function assertOrder($expectedOrder)
     {
         $this->assertEquals($expectedOrder, $this->table->getColumn('label'));
         $this->assertEquals(count($expectedOrder), $this->table->getRowsCount());
     }
 
-    private function excludeLowValues($minimumValue)
+    private function excludeLowValues($minimumValue, $columnToCheck = null)
     {
-        $filter = new ExcludeLowValue($this->table, 'growth', $minimumValue);
+        $filter = new ExcludeLowValue($this->table, 'growth', $minimumValue, $columnToCheck);
         $filter->filter($this->table);
     }
 
diff --git a/plugins/Insights/tests/FilterInsightTest.php b/plugins/Insights/tests/FilterInsightTest.php
index 4a6431ce8a..f122f93aa7 100644
--- a/plugins/Insights/tests/FilterInsightTest.php
+++ b/plugins/Insights/tests/FilterInsightTest.php
@@ -148,6 +148,29 @@ class FilterInsightTest extends \PHPUnit_Framework_TestCase
         $this->assertColumnValues($values);
     }
 
+    public function testShouldDetectWhetherRowIsNewMoverOrDisappeared()
+    {
+        $this->applyInsightConsiderAll();
+
+        $values = array(
+            array('label' => 'val1', 'isNew' => false, 'isMover' => true, 'isDisappeared' => false),
+            array('label' => 'val2', 'isNew' => true, 'isMover' => false, 'isDisappeared' => false),
+            array('label' => 'val3', 'isNew' => true, 'isMover' => false, 'isDisappeared' => false),
+            array('label' => 'val4', 'isNew' => false, 'isMover' => true, 'isDisappeared' => false),
+            array('label' => 'val5', 'isNew' => true, 'isMover' => false, 'isDisappeared' => false),
+            array('label' => 'val6', 'isNew' => false, 'isMover' => true, 'isDisappeared' => false),
+            array('label' => 'val7', 'isNew' => true, 'isMover' => false, 'isDisappeared' => false),
+            array('label' => 'val8', 'isNew' => false, 'isMover' => true, 'isDisappeared' => false),
+            array('label' => 'val9', 'isNew' => false, 'isMover' => true, 'isDisappeared' => false),
+            array('label' => 'val10', 'isNew' => false, 'isMover' => true, 'isDisappeared' => false),
+            array('label' => 'val102', 'isNew' => false, 'isMover' => false, 'isDisappeared' => true),
+            array('label' => 'val109', 'isNew' => false, 'isMover' => false, 'isDisappeared' => true),
+            array('label' => 'val107', 'isNew' => false, 'isMover' => false, 'isDisappeared' => true)
+        );
+
+        $this->assertColumnValues($values);
+    }
+
     public function testShouldCalculateDifferenceAndGrowthPercentage()
     {
         $this->applyInsightConsiderAll();
-- 
GitLab