Sorting and Reordering - Sort Rows (Automatically Updates when Data Changes)

From Q
Jump to: navigation, search

This rule sorts the rows of table from the highest to lowest (or lowest to highest) according to the values shown (i.e., ranks the data, so that the order of the rows reveals a ranking). When the table has more than one column you can choose which column should be used to do the sorting. Some rows, like NET, Don't Know, Other/Specify, All of these, or None of these will automatically be left at the bottom of the table. The rule allows you to control which rows should be left at the bottom.

While you can already sort individual tables by right-clicking on one of the columns and selecting an option form the Sort By menu, the table may no longer remain sorted if the values shown in the table change. This rule allows for the table to remain sorted even when the values in the table change. See also Sorting and Reordering - Sort from Highest to Lowest (Does Not Update When Data Changes).

When the table has spans in the rows, the rows of the table will be sorted within the spans.

Examples

In this example, the rule has been used to sort the table in descending order according to the values of the Column % in the NET column. The NET column has been chosen because it is the column with the largest sample size (Column n). To specify to sort by a particular row, use the Choose a column option under Column Selection. Four rows have not been sorted, and have been left in their original order at the bottom of the table: Don't Know, Nothing, Other, and NET. The rule automatically leaves rows like this fixed.

SortRuleDefault1.PNG

The next two examples describe how to control which rows are fixed at the bottom of the table.

Excluding a row from the sort

In this example, the Exclude from sort option has been used to make sure that any row label containing the text don't is not sorted with the other rows and is kept at the bottom of the table. This allows the row They don't drink diet or sugar-free cola to remain below the four sorted rows. Note that the row Don't know/Not applicable is automatically kept at the bottom because the rule can identify it as a Don't know option.

SortingRuleWithFixed2.png

Preventing a row from being excluded

In this example, the Never exclude from sort option has been used to make sure that any row containing the text Others is always included in the sorted rows. If this is not done then the row Others perceive me as wholesome may be mistaken for an Other/Specify option and left at the bottom of the table.

SortingRuleWithNotFixed2.png

Technical details

  • The table will be sorted using the first Statistic shown on the table.
  • You can choose whether the rows of the table will be sorted in descending or ascending order (from top to bottom).
  • By default, if there are multiple columns in the table, the rule will sort according to the largest column (according to the Column n in the Statistics - Below). Using this option:
    • This script will have no effect when Column n is not available from the Statistics - Below.
    • If two or more columns have the same value for Column n then the table will be sorted according to the right-most column with that value of Column n.
  • You can instead choose to sort by specific column by changing Column Selection to Choose a column, and then selecting a column from the list of column labels.
  • Rows that look like they are Don't Know, Other/Specify, All of these, or None of these responses will be left at the bottom of the table, as will NET and SUM rows.
  • The Exclude from sort option is used to exclude rows from the sort, keeping them at the bottom of the table. This text is not case sensitive.
  • The Never exclude from sort option is used to ensure that rows with labels containing certain text are always included among the sorted rows. This is used to override the rule's automatic detection of Don't Know, Other/Specify, All of these, or None of these options. This text is not case sensitive.
  • Values of NaN are treated as the largest possible negative number for the purpose of sorting, which means that they will always be placed below other rows when sorting from highest to lowest (excepting those rows that have been excluded from the sort).

How to apply this rule

For the first time in a project

  • Select the table(s)/chart(s) that you wish to apply the rule to.
  • Start typing the name of the Rule into the Search features and data box in the top right of the Q window.
  • Click on the Rule when it appears in the QScripts and Rules section of the search results.

OR

  • Select Automate > Browse Online Library.
  • Choose this rule from the list.

Additional applications of the rule

  • Select a table or chart that has the rule and any table(s)/chart(s) that you wish to apply the rule to.
  • Click on the Rules tab (bottom-left of the table/chart).
  • Select the rule that you wish to apply.
  • Click on the Apply drop-down and choose your desired option.
  • Check New items to have it automatically applied to new items that you create. Use Edit > Project Options > Save as Template to create a new project template that automatically uses this rule.

Removing the rule

  • Select the table(s)/chart(s) that you wish to remove the rule from.
  • Press the Rules tab (bottom-right corner).
  • Press Apply next to the rule you wish to remove and choose the appropriate option.

How to modify the rule

  • Click on the Rules tab (bottom-left of the table/chart).
  • Select the rule that you wish to modify.
  • Click Edit Rule and make the desired changes. Alternatively, you can use the JavaScript below to make your own rule (see Customizing Rules).

JavaScript

includeWeb("Table JavaScript Utility Functions");
includeWeb("JavaScript Text Analysis Functions");
includeWeb("JavaScript Array Functions");
 
Array.prototype.max = function() {
  return Math.max.apply(null, this);
};

Array.prototype.min = function() {
  return Math.min.apply(null, this);
};

// Prevent operating on empty tables
if (table.numberColumns < 1) 
    form.ruleNotApplicable("too few columns");

// Prevent operating on tables containing text
var primary_statistic = table.availableStatistics[0];
if (primary_statistic == "Text") 
    form.ruleNotApplicable("this rule does not work on text tables");


var sort_within_spans = table.rowSpans.length > 0


// Check for duplicate labels within spans (if any) or within the table as a whole.
if (sort_within_spans) {
    var row_labels = table.rowLabels;
    var spans = getLowestLevelSpans(table.rowSpans);
    var found_indices = getAllIndicesInSpans(spans);
    var leftovers = getLeftoverIndices(found_indices, table.numberRows);
    spans.forEach(function (span) {
        var labels = span.indices.map(function (x) { return row_labels[x]; });
        if (arrayHasDuplicateElements(labels))
            form.ruleNotApplicable("there are duplicate row labels in span " + span.label);
    });
    var leftover_labels = leftovers.map(function (x) { return row_labels[x]; });
    if (arrayHasDuplicateElements(leftover_labels))
        form.ruleNotApplicable("there are duplicate row labels in the table");
} else {
    if (arrayHasDuplicateElements(table.rowLabels))
        form.ruleNotApplicable("there are duplicate row labels in the table")
}
 
// User controls
var ascending_label = form.newLabel("Sort:");
var ascending_menu = form.newComboBox("am", ["Descending", "Ascending"]);
ascending_menu.setDefault("Descending");
ascending_menu.lineBreakAfter = true;
var column_selection_label = form.newLabel("Column selection:");
var column_selection_menu = form.newComboBox("csm", ["Largest column", "Choose a column"]);
column_selection_menu.setDefault("Largest column");
column_selection_menu.lineBreakAfter = true;
var control_array = [ascending_label, ascending_menu, column_selection_label, column_selection_menu];

var column_list = [];
if (column_selection_menu.getValue() == "Choose a column" && table.columnLabels != null)
{
    column_list = enumerateDuplicatesInStringArray(table.columnLabels, '(', ')');
    var column_choice_label = form.newLabel("Column:")
    var column_choice_menu = form.newComboBox("ccm", column_list);
    column_choice_menu.lineBreakAfter = true;
    control_array.push(column_choice_label);
    control_array.push(column_choice_menu);
}

form.setInputControls(control_array);

var ascending = ascending_menu.getValue() == "Ascending";
var low_high_label = ascending ? "Lowest to Highest" : "Highest to Lowest";
form.setHeading("Sort - Sort Rows");
form.setSummary("Sort - Sort Rows from " + low_high_label + " (Automatically Updates when Data Changes)");
 
// User specified labels to always exclude from the sort
// or never exclude from the sort. 
var exclude_labels = addExpandingTextBoxes(control_array, "Exclude from sort (in addition to NET/SUM):", "el").map(function (label) { return label.toLowerCase(); } );
var do_not_exclude_labels = addExpandingTextBoxes(control_array, "Never exclude from sort:", "dnel").map(function (label) { return label.toLowerCase(); } );
 
// Sort according to the first stat selected in the table
var stat = table.statistics[0];
var target_column_index;

if (table.numberColumns == 1) {
    target_column_index = 0;
} else if (column_selection_menu.getValue() == "Largest column") {
    // Obtain Column n from the Statistics - Below and make sure that the resulting array is the correct shape
    if (!belowTableExists() || below_table.availableStatistics.indexOf('Column n') == -1)
        form.ruleNotApplicable("Column n statistic does not exist for this table. Change Column Selection to 'Choose a column'");
    var column_ns = below_table.get('Column n');
    if (column_ns.length == 1)
        column_ns = column_ns[0];
    else
        column_ns = column_ns.map(function (a) { return a[0]; });
    var num_cols = table.numberColumns;
     
     
    // Find largest column n and the index of the column
    
    if (num_cols != 1) {
        var max_col_n = Number.NEGATIVE_INFINITY;
        for (var j = 0; j < num_cols; j++)
            if (!isNaN(column_ns[j]) && column_ns[j] >= max_col_n) {
                max_col_n = column_ns[j];
                target_column_index = j;
            }
        var columns_with_max = column_ns.filter(function (x) { return !isNaN(x) && x >= max_col_n} );
        // When there is more than one column with the same max value of column n then
        // default to the NET or SUM if found.
        if (columns_with_max.length > 1) {
            var net_columns = table.netColumns;
            if (net_columns.length > 0)
                target_column_index = net_columns[0];
        }
    }
} else {
    target_column_index = column_list.indexOf(column_choice_menu.getValue());
}
 
// Work out what to do with spans.
var spans;
if (table.rowSpans.length == 0){
    var ind = [];
    for (var j = 0; j < table.numberRows; j++)
        ind.push(j);
    spans = [{ indices: ind, label: "all rows" }];
} else {
    var spans = getLowestLevelSpans(table.rowSpans);
    var found_indices = getAllIndicesInSpans(spans);
    var leftovers = getLeftoverIndices(found_indices, table.numberRows);
    // Any indices not in spans get moved to the bottom of the table
    if (leftovers.length > 0) {
        var last = found_indices.max();
        var modifier = 0;
        leftovers.forEach(function (x) {
            if ( (x-modifier) < last) {
                table.moveRowAfter(x-modifier, last);
                modifier ++; 
            }
        });
        var spans = getLowestLevelSpans(table.rowSpans);
        var found_indices = getAllIndicesInSpans(spans);
        var leftovers = getLeftoverIndices(found_indices, table.numberRows);
        spans.push( {indices: leftovers, label: "Rows outside of spans"});
    }
}

// Get values to be used by the sorting and determine if each row should be sorted (based on its label)
var values = table.get(stat);


spans.forEach(function (span) {
    var labels = span.indices.map(function (x) { return table.rowLabels[x]; });
    var row_objects = [];
    var net_rows = table.netRows;

    for (var j = 0; j < labels.length; j++) {
        var current_row_index = span.indices[j];
        var is_net = net_rows.indexOf(current_row_index) != -1;
        row_objects.push({ label: labels[j], 
                           value: isNaN(values[current_row_index][target_column_index]) ? Number.NEGATIVE_INFINITY : values[current_row_index][target_column_index], // replace NaN with -Infinity so that NaNs don't get mixed through the table
                           sort: shouldSortRow(labels[j], exclude_labels, do_not_exclude_labels, is_net) });
    }

     
    // Sort the row objects based on their values
    // Tied will be sorted by the label
    row_objects.sort(function (row_x, row_y) {
        // Get the values for the two rows we are comparing.
        var value_x = row_x.value;
        var value_y = row_y.value;
        if (Math.abs(value_x - value_y) < Math.pow(10,-14) || (value_x == Number.NEGATIVE_INFINITY && value_y == Number.NEGATIVE_INFINITY))
            return row_y.label.charCodeAt(0) - row_x.label.charCodeAt(0);
        else
            return value_x - value_y;
    });
     
     
    // Perform the sorting of the table rows, ignoring any rows that have been marked with sort = false
    var top_index_in_span = span.indices.min();
    var top_row = top_index_in_span == 0 ? null : top_index_in_span - 1;
    if (ascending) {
        for (var j = row_objects.length - 1; j > -1; j--)
            if (row_objects[j].sort)
                moveRowAfterComplete(findRowInSpan(row_objects[j].label, span.indices), top_row);
    } else {
        for (var j = 0; j < row_objects.length; j++)
            if (row_objects[j].sort)
                moveRowAfterComplete(findRowInSpan(row_objects[j].label, span.indices), top_row);
    }
})

 
// Should we include this row in the sorting?
function shouldSortRow(label, exclude_labels, do_not_exclude_labels, is_net) {
    if (containsSubstring(label, do_not_exclude_labels))
        return true;
    if (containsSubstring(label, exclude_labels))
        return false;
    if (isNoneOfThese(label))
        return false;
    if (isAllOfThese(label))
        return false;
    if (isDontKnow(label))
        return false;
    if (isOther(label))
        return false;
    if (is_net)
        return false;
    return true;
}
 
// Add an expanding list of text boxes to control_array and return the
// values entered by the user as an array. "id" is required to uniquely
// identify the controls in this set - do not use the same value of id
// in different calls to this function.
function addExpandingTextBoxes(control_array, label, id) {
    var last_selection = " ";
    var entries = [];
    var control_label = form.newLabel(label);
    control_label.lineBreakAfter = true;
    control_array.push(control_label);
    var control_counter = 0;
    while (last_selection != "") {
        var text_box = form.newTextBox(id + control_counter);
        text_box.lineBreakAfter = true;
        text_box.setDefault("");
        control_array.push(text_box);
        form.setInputControls(control_array);
        var last_selection = text_box.getValue();
        if (last_selection != "")
            entries.push(last_selection);
        control_counter ++;
    }
    return entries;
}

// Work out which spans are lowest in the table.
// These are the spans that are tested within.
function getLowestLevelSpans(spans) {
    var lowest_spans = [];
    var found_indices = [];
    spans.forEach(function (span) {
        if (span.indices.every(function (x) { return found_indices.indexOf(x) == -1; })) {
            found_indices = found_indices.concat(span.indices);
            lowest_spans.push(span);
        }
    });
    return lowest_spans;
}

function getAllIndicesInSpans(spans) {
    var found_indices = [];
    spans.forEach(function (span) {
        found_indices = found_indices.concat(span.indices);
    });
    return found_indices;
}

function getLeftoverIndices(indices, num_rows) {
    var leftovers = [];
    for (var j = 0; j < num_rows; j++) {
        if (found_indices.indexOf(j) == -1)
            leftovers.push(j);
    }
    return leftovers
}

function findRowInSpan(row_label, span_indices) {
    var labels = span_indices.map(function (x) { return table.rowLabels[x]; });
    return span_indices[labels.indexOf(row_label)];
}

Prior to the 15th of December, 2015, this page was known as Sort - Sort Rows (Automatically Updates when Data Changes)

See also