Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for secondary column support and faster datatable sort #8449

Merged
merged 4 commits into from Aug 18, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 24 additions & 16 deletions core/API/DataTableGenericFilter.php
Expand Up @@ -98,6 +98,9 @@ public static function getGenericFiltersInformation()
array(
'filter_sort_column' => array('string'),
'filter_sort_order' => array('string', 'desc'),
$naturalSort = true,
$recursiveSort = true,
$doSortBySecondaryColumn = true
)),
array('Truncate',
array(
Expand Down Expand Up @@ -159,22 +162,27 @@ protected function applyGenericFilters($datatable)
}

foreach ($filterParams as $name => $info) {
// parameter type to cast to
$type = $info[0];

// default value if specified, when the parameter doesn't have a value
$defaultValue = null;
if (isset($info[1])) {
$defaultValue = $info[1];
}

try {
$value = Common::getRequestVar($name, $defaultValue, $type, $this->request);
settype($value, $type);
$filterParameters[] = $value;
} catch (Exception $e) {
$exceptionRaised = true;
break;
if (!is_array($info)) {
// hard coded value that cannot be changed via API, see eg $naturalSort = true in 'Sort'
$filterParameters[] = $info;
} else {
// parameter type to cast to
$type = $info[0];

// default value if specified, when the parameter doesn't have a value
$defaultValue = null;
if (isset($info[1])) {
$defaultValue = $info[1];
}

try {
$value = Common::getRequestVar($name, $defaultValue, $type, $this->request);
settype($value, $type);
$filterParameters[] = $value;
} catch (Exception $e) {
$exceptionRaised = true;
break;
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions core/DataTable.php
Expand Up @@ -877,6 +877,14 @@ public function getRowsWithoutSummaryRow()
return $this->rows;
}

/**
* @ignore
*/
public function getRowsCountWithoutSummaryRow()
{
return count($this->rows);
}

/**
* Returns an array containing all column values for the requested column.
*
Expand Down
230 changes: 30 additions & 200 deletions core/DataTable/Filter/Sort.php
Expand Up @@ -13,6 +13,7 @@
use Piwik\DataTable\Simple;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\Metrics\Sorter;

/**
* Sorts a {@link DataTable} based on the value of a specific column.
Expand All @@ -25,7 +26,8 @@ class Sort extends BaseFilter
{
protected $columnToSort;
protected $order;
protected $sign;
protected $naturalSort;
protected $isSecondaryColumnSortEnabled;

const ORDER_DESC = 'desc';
const ORDER_ASC = 'asc';
Expand All @@ -38,8 +40,10 @@ class Sort extends BaseFilter
* @param string $order order `'asc'` or `'desc'`.
* @param bool $naturalSort Whether to use a natural sort or not (see {@link http://php.net/natsort}).
* @param bool $recursiveSort Whether to sort all subtables or not.
* @param bool $doSortBySecondaryColumn If true will sort by a secondary column. The column is automatically
* detected and will be either nb_visits or label, if possible.
*/
public function __construct($table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = true)
public function __construct($table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = true, $doSortBySecondaryColumn = false)
{
parent::__construct($table);

Expand All @@ -48,144 +52,9 @@ public function __construct($table, $columnToSort, $order = 'desc', $naturalSort
}

$this->columnToSort = $columnToSort;
$this->naturalSort = $naturalSort;
$this->setOrder($order);
}

/**
* Updates the order
*
* @param string $order asc|desc
*/
public function setOrder($order)
{
if ($order == 'asc') {
$this->order = 'asc';
$this->sign = 1;
} else {
$this->order = 'desc';
$this->sign = -1;
}
}

/**
* Sorting method used for sorting numbers
*
* @param array $rowA array[0 => value of column to sort, 1 => label]
* @param array $rowB array[0 => value of column to sort, 1 => label]
* @return int
*/
public function numberSort($rowA, $rowB)
{
if (isset($rowA[0]) && isset($rowB[0])) {
if ($rowA[0] != $rowB[0] || !isset($rowA[1])) {
return $this->sign * ($rowA[0] < $rowB[0] ? -1 : 1);
} else {
return -1 * $this->sign * strnatcasecmp($rowA[1], $rowB[1]);
}
} elseif (!isset($rowB[0]) && !isset($rowA[0])) {
return -1 * $this->sign * strnatcasecmp($rowA[1], $rowB[1]);
} elseif (!isset($rowA[0])) {
return 1;
}

return -1;
}

/**
* Sorting method used for sorting values natural
*
* @param mixed $valA
* @param mixed $valB
* @return int
*/
public function naturalSort($valA, $valB)
{
return !isset($valA)
&& !isset($valB)
? 0
: (!isset($valA)
? 1
: (!isset($valB)
? -1
: $this->sign * strnatcasecmp(
$valA,
$valB
)
)
);
}

/**
* Sorting method used for sorting values
*
* @param mixed $valA
* @param mixed $valB
* @return int
*/
public function sortString($valA, $valB)
{
return !isset($valA)
&& !isset($valB)
? 0
: (!isset($valA)
? 1
: (!isset($valB)
? -1
: $this->sign *
strcasecmp($valA,
$valB
)
)
);
}

protected function getColumnValue(Row $row)
{
$value = $row->getColumn($this->columnToSort);

if ($value === false || is_array($value)) {
return null;
}

return $value;
}

/**
* Sets the column to be used for sorting
*
* @param Row $row
* @return int
*/
protected function selectColumnToSort($row)
{
$value = $row->hasColumn($this->columnToSort);
if ($value) {
return $this->columnToSort;
}

$columnIdToName = Metrics::getMappingFromNameToId();
// sorting by "nb_visits" but the index is Metrics::INDEX_NB_VISITS in the table
if (isset($columnIdToName[$this->columnToSort])) {
$column = $columnIdToName[$this->columnToSort];
$value = $row->hasColumn($column);

if ($value) {
return $column;
}
}

// eg. was previously sorted by revenue_per_visit, but this table
// doesn't have this column; defaults with nb_visits
$column = Metrics::INDEX_NB_VISITS;
$value = $row->hasColumn($column);
if ($value) {
return $column;
}

// even though this column is not set properly in the table,
// we select it for the sort, so that the table's internal state is set properly
return $this->columnToSort;
$this->naturalSort = $naturalSort;
$this->order = strtolower($order);
$this->isSecondaryColumnSortEnabled = $doSortBySecondaryColumn;
}

/**
Expand All @@ -204,87 +73,48 @@ public function filter($table)
return;
}

if (!$table->getRowsCount()) {
if (!$table->getRowsCountWithoutSummaryRow()) {
return;
}

$row = $table->getFirstRow();

if ($row === false) {
return;
}

$this->columnToSort = $this->selectColumnToSort($row);
$config = new Sorter\Config();
$sorter = new Sorter($config);

$value = $this->getFirstValueFromDataTable($table);
$config->naturalSort = $this->naturalSort;
$config->primaryColumnToSort = $sorter->getPrimaryColumnToSort($table, $this->columnToSort);
$config->primarySortOrder = $sorter->getPrimarySortOrder($this->order);
$config->primarySortFlags = $sorter->getBestSortFlags($table, $config->primaryColumnToSort);
$config->secondaryColumnToSort = $sorter->getSecondaryColumnToSort($row, $config->primaryColumnToSort);
$config->secondarySortOrder = $sorter->getSecondarySortOrder($this->order, $config->secondaryColumnToSort);
$config->secondarySortFlags = $sorter->getBestSortFlags($table, $config->secondaryColumnToSort);

if (is_numeric($value) && $this->columnToSort !== 'label') {
$methodToUse = "numberSort";
} else {
if ($this->naturalSort) {
$methodToUse = "naturalSort";
} else {
$methodToUse = "sortString";
}
}
// secondary sort should not be needed for all other sort flags (eg string/natural sort) as label is unique and would make it slower
$isSecondaryColumnSortNeeded = $config->primarySortFlags === SORT_NUMERIC;
$config->isSecondaryColumnSortEnabled = $this->isSecondaryColumnSortEnabled && $isSecondaryColumnSortNeeded;

$this->sort($table, $methodToUse);
$this->sort($sorter, $table);
}

private function getFirstValueFromDataTable($table)
private function sort(Sorter $sorter, DataTable $table)
{
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$value = $this->getColumnValue($row);
if (!is_null($value)) {
return $value;
}
}
}

/**
* Sorts the DataTable rows using the supplied callback function.
*
* @param string $functionCallback A comparison callback compatible with {@link usort}.
* @param string $columnSortedBy The column name `$functionCallback` sorts by. This is stored
* so we can determine how the DataTable was sorted in the future.
*/
private function sort(DataTable $table, $functionCallback)
{
$table->setTableSortedBy($this->columnToSort);

$rows = $table->getRowsWithoutSummaryRow();

// get column value and label only once for performance tweak
$values = array();
if ($functionCallback === 'numberSort') {
foreach ($rows as $key => $row) {
$values[$key] = array($this->getColumnValue($row), $row->getColumn('label'));
}
} else {
foreach ($rows as $key => $row) {
$values[$key] = $this->getColumnValue($row);
}
}

uasort($values, array($this, $functionCallback));

$sortedRows = array();
foreach ($values as $key => $value) {
$sortedRows[] = $rows[$key];
}

$table->setRows($sortedRows);

unset($rows);
unset($sortedRows);
$sorter->sort($table);

if ($table->isSortRecursiveEnabled()) {
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$subTable = $row->getSubtable();

if ($subTable) {
$subTable->enableRecursiveSort();
$this->sort($subTable, $functionCallback);
$this->sort($sorter, $subTable);
}
}
}
}
}

}