Filter - Apply Filters to Columns or Rows

From Q
Jump to: navigation, search

The rule applies separate filters to specified columns or rows of a table. These filters are applied in addition to existing filters applied to the whole table. Optionally, the filtered rows or columns can be added at the bottom or right of the table.

Example

In the table below, the row shown in green has an additional filter applied, so that only respondents who prefer coke are included. In this case the numbers in that row have been replaced by the filtered statistics.

FilteringRowRule.PNG

Adding new rows or columns

This table shows a SUMMARY of a Pick One - Multi question. In this table we can compare the proportion of respondents who Love Coca Cola and Pepsi with the complete scale showing. If we would also like to split these results by Gender then we can use the rule to provide results filtered by Males and Females on the same table.

FilteringNewColumnsBefore.PNG

The options for doing so are shown below. Note that we have two filters, Male and Female already set up in the project, and we have specified labels and spans for the new columns.

FilteringNewColumnsOptions.PNG

The resulting table now has four additional columns added, one for each of the filters that we have specified. Note that the statistics below the table (Average and Column n) have also been filtered.

FilteringNewColumnsAfter.PNG

Technical details

  1. To remove a column or row from filtering, select "(none)" in the combo box. The filtered columns or rows are shaded light green by default, although shading can be turned off in the Rule's settings.
  2. Combo boxes remember the last selected column, row and filter labels. If the table questions or filters are changed but happen to have column, row or filter labels matching the previous selections, then the current selections will correspond to the new columns, rows or filters with the matching labels. If there is no matching label, then the selection will be empty. Be aware that the table or chart does not provide an alert when its values have changed due to changes to questions or filters.
  3. Significance testing results are not available on the table due to the nature of the modifications made by this Rule. A warning will be placed in the footer of the table, and there is an option to turn off the warning in the Rule's settings. Some statistical testing can be added manually using the rule Significance Testing - Independent Samples Column Means and Proportions Tests.
  4. Tables showing Date questions cannot be filtered by this rule.
  5. When not adding columns or rows only a single filter may be selected for each column or row.
  6. Filtering changes the numbers only within the filtered column or row, and any rows or columns which are not selected for filtering remain unchanged.

Marginal statistics

When filtering the columns of the table the Statistics - Below are also filtered but the Statistics - Right are not available.

When filtering the rows of the table the Statistics - Right are filtered but the Statistics - Below are not available.

The exceptions to this are Ranking and Experiment questions for which both Statistics - Below and Statistics - Right are unavailable when this rule is applied.

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 Array Functions');
includeWeb('QScript Utility Functions');

if (fileFormatVersion() > 8.41 && !(fileFormatVersion() > 8.81))
    form.ruleNotApplicable('this rule currently only works in Q 4.9.7 and Q 4.11.2 and higher');

let no_selection_string = '(none)';
let rule_name = 'Apply Filters to Columns or Rows';

form.setHeading(rule_name);
form.setSummary('Apply filters to columns or rows');
table.requireNumericTable();
table.requireOriginalRowsColumns();
let description = form.newLabel('The rule applies separate filters to specified columns or rows of a table in addition to existing filters applied to the whole table');
description.lineBreakAfter = true;

let no_net_available_types = ['Date', 'Ranking', 'Experiment'];
let blue_question = table.blueQuestion;
let brown_question = table.brownQuestion;
let blue_question_type = blue_question.questionType;
let brown_question_type = 'SUMMARY';
if (brown_question === 'RAW DATA')
    form.ruleNotApplicable('this rule does not work with RAW DATA');
if (brown_question != 'SUMMARY')
    brown_question_type = brown_question.questionType;

// Check for availability of marginal statistics
let below_table_exists = belowTableExists();
let right_table_exists = rightTableExists();

// Specify which statistics will be copied across from the filtered tables
// regardless of whether or not they are selected on the table by the user.
// This ensures that the resulting table has all the necessary statistics
// for manual statistical testing (which is done in a separate rule).
let mandatory_cell_statistics = ['Standard Deviation',
                                 'Column Standard Error',
                                 'Base Population',
                                 'Effective Base n',
                                 'Base n',
                                 'Column Population',
                                 'Column n',
                                 'Average',
                                 'Column %',
                                 'Row %',
                                 '%',
                                 'Standard Error'];

let mandatory_below_statistics = ['Standard Deviation',
                                  'Standard Error',
                                  'Column Population',
                                  'Base Population',
                                  'Average',
                                  'Column n',
                                  'Base n',
                                  'Base Population'];
let table_stats_to_filter = table.statistics
                                 .concat(mandatory_cell_statistics.filter(function (stat) {
                                                                             return table.availableStatistics.indexOf(stat) > -1
                                                                                    && table.statistics.indexOf(stat) === -1;
                                                                         }));
let below_stats_to_filter;
if (below_table_exists)
    below_stats_to_filter = below_table.statistics
                                       .concat(mandatory_below_statistics.filter(function (stat) {
                                                                                     return below_table.availableStatistics.indexOf(stat) > -1
                                                                                            && below_table.statistics.indexOf(stat) === -1;
                                                                                 }));

// Check that there are filters in the project, and get the list
let filters = [];
project.dataFiles.forEach(function (data_file) {
    filters = filters.concat(data_file.variables.filter(function(v) { return v.question.isFilter; }));
});
if (filters.length === 0) {
    table.suppressOutput('The Rule "' + rule_name + '" requires filters in the project. Add filters to the project or remove the Rule from the table.');
    form.ruleNotApplicable('there are no filters in the project.');
}
let filter_labels = project.dataFiles.length === 1 ?
                    filters.map(function(v) { return v.label.trim(); }) :
                    filters.map(function(v) { return v.label.trim() + ' [' + v.question.dataFile.name + ']'; });
filter_labels = enumerateDuplicatesInStringArray(filter_labels, '(', ')');

// Add a none option to the items in the filter menu and append a corresponding blank element to the list of filters
filter_labels = [no_selection_string].concat(filter_labels);
filters = [''].concat(filters);

// Column or row combobox
let controls = [description];
let filter_cols_filter_rows = ['Filter columns', 'Filter rows'];
let filter_col_row_combobox = form.newComboBox('columnOrRow', filter_cols_filter_rows);
filter_col_row_combobox.setDefault(table.columnLabels == null ? filter_cols_filter_rows[1] : filter_cols_filter_rows[0]);
let filter_columns = filter_col_row_combobox.getValue() == filter_cols_filter_rows[0];
filter_col_row_combobox.lineBreakAfter = true;
controls.push(filter_col_row_combobox);

// Check that there are valid columns
if (filter_columns && table.columnLabels == null) {
    table.suppressOutput('The Rule "' + rule_name + '" requires columns that are not statistics. Change the questions in the table or remove the Rule from the table.');
    form.ruleNotApplicable('the columns in this table are statistics.');
}

// Check box for adding new columns instead of modifying existing ones
let add_new_box = form.newCheckBox('addnew', 'Add new ' + (filter_columns ? 'columns' : 'rows'));
add_new_box.setDefault(false);
add_new_box.lineBreakAfter = true;
controls.push(add_new_box);
let adding_new = add_new_box.getValue();

// Trim column/row labels and change column/row labels that collide with no_selection_string
let col_row_combobox_alternatives = (filter_columns ? table.columnLabels : table.rowLabels).map(function (label) {
    return label.trim();
}).map(function (label) {
    if (label == no_selection_string)
        return label + (filter_columns ? ' (column)' : ' (row)');
    else
        return label;
});
col_row_combobox_alternatives = [no_selection_string].concat(enumerateDuplicatesInStringArray(col_row_combobox_alternatives, '(', ')'));

let selected_cols_rows = [];
let selected_filters = [];
let new_labels = [];
let entered_span_labels = [];
let last_index = 0;
let current_index = 0;

// Loop to allow user to specify information about which rows/columns should be filtered
// with which variables.
let col_row_label = filter_columns ? 'Column:' : 'Row:';
while (true) {
    let col_row_combobox = form.newComboBox((filter_columns ? 'column' : 'row') + current_index, col_row_combobox_alternatives);
    let selected_col_row = col_row_combobox.getValue();
    if (selected_col_row == null) // stop adding when the combobox selection is null
        break;
    else if (selected_col_row != no_selection_string) { // only add comboboxes with a column/row selected
        controls.push(form.newLabel(col_row_label));
        controls.push(col_row_combobox);
        controls.push(form.newLabel('Filter:'));
        let filter_combobox = form.newComboBox((filter_columns ? 'columnFilter' : 'rowFilter') + current_index, filter_labels);
        filter_combobox.setDefault(no_selection_string);
        if (!adding_new)
            filter_combobox.lineBreakAfter = true;
        controls.push(filter_combobox);

        // If adding new columns/rows also provide inputs for new labels and span labels
        if (adding_new) {
            controls.push(form.newLabel((filter_columns ? 'Column' : 'Row') + ' label:'));
            var new_label_box = form.newTextBox('chb' + current_index);
            controls.push(new_label_box);
            controls.push(form.newLabel((filter_columns ? 'Column' : 'Row') + ' span:'));
            var span_label_box = form.newTextBox('sl' + current_index);
            span_label_box.lineBreakAfter = true;
            controls.push(span_label_box);
        }

        let filter_value = filter_combobox.getValue();
        // If the user has selected "(none)" then don't do anything
        if (filter_value != no_selection_string) {
            selected_filters.push(filter_value);
            selected_cols_rows.push(selected_col_row);
            if (adding_new) {
                new_labels.push(new_label_box.getValue());
                entered_span_labels.push(span_label_box.getValue());
            }
        }
        last_index = current_index + 1;
    }
    current_index++;
}

// Add last combobox at last_index
controls.push(form.newLabel(col_row_label));
let last_col_row_combobox = form.newComboBox((filter_columns ? 'column' : 'row') + last_index, col_row_combobox_alternatives);
last_col_row_combobox.setDefault(no_selection_string);
last_col_row_combobox.lineBreakAfter = true;
controls.push(last_col_row_combobox);

// Add checkbox for displaying footer message
let checkbox_footer = form.newCheckBox('footer', 'Show significance testing message in footer');
checkbox_footer.setDefault(true);
controls.push(checkbox_footer);

let cell_higlight_color;
let shade_cells = false;
if (!table.forPlot) {
    checkbox_footer.lineBreakAfter = true;
    let checkbox_shading = form.newCheckBox('shading', 'Shade ' + (filter_columns ? 'columns' : 'rows') + ' that have filters applied');
    checkbox_shading.setDefault(!adding_new);
    controls.push(checkbox_shading);
    shade_cells = checkbox_shading.getValue();
    if (shade_cells) {
        let picker = form.newColorPicker('colorPicker');
        picker.setDefault('LightGreen');
        controls.push(picker);
        cell_higlight_color = picker.getValue();
        form.setOutputColor(cell_higlight_color);
    } else
        form.setOutputColor('');
}

form.setInputControls(controls);

// Prevent the user from applying multiple filters to the same row/column when not adding
// rows/columns to the table.
let unique_cols_rows = uniqueElementsInArray(selected_cols_rows);
if (!adding_new && unique_cols_rows.length < selected_cols_rows.length)
    form.ruleNotApplicable('multiple filters are selected for the same ' + (filter_columns ? 'column' : 'row'));

// Can't filter tables with Date questions because such tables can change the number of rows/columns that are available
// when filtered
if (blue_question_type === 'Date' || brown_question_type === 'Date')
    form.ruleNotApplicable('table has a Date question selected');

// Can't apply to SUMMARY tables for Ranking or Experiment questions
if (brown_question === 'SUMMARY' && no_net_available_types.indexOf(blue_question_type) > -1)
    form.ruleNotApplicable('this rule is not compatible with SUMMARY tables of ' + blue_question_type + ' questions');

// Check for Statistics - Right and Statistics - Below
// These can't be filtered for Date, Ranking, and Experiment questions.
// When filtering columns, Statistics - Right should not be shown as there is no way to recalculate them.
// The same is true for Statistics - Below when the rows are being filtered.
if (below_table_exists && below_table.statistics.length > 0 && (no_net_available_types.indexOf(blue_question_type) > -1 || !filter_columns) ) {
    table.suppressOutput('"Statistics - Below" are not compatible with this Rule. Unselect them from the table or remove the Rule from the table.');
    form.ruleNotApplicable('Statistics - Below are not compatible with this rule for this table');
}
if (right_table_exists && right_table.statistics.length > 0 && (no_net_available_types.indexOf(brown_question_type) > -1 || filter_columns) ) {
    table.suppressOutput('"Statistics - Right" are not compatible with this Rule. Unselect them from the table or remove the Rule from the table.');
    form.ruleNotApplicable('Statistics - Right are not compatible with this rule for this table');
}

// Turn off significance testing
preventAllSignificanceTesting(table, rule_name, checkbox_footer.getValue());
if (below_table_exists)
    preventAllSignificanceTesting(below_table, rule_name, false);
if (right_table_exists)
    preventAllSignificanceTesting(right_table, rule_name, false);

// Combine information about each filter into an object for easy reference
let filter_objects = [];
let new_spans = uniqueElementsInArray(entered_span_labels).map(function (span) { return {label: span, indices: [] }; });
for (let j = 0; j < selected_filters.length; j++) {
    let col_row_index = col_row_combobox_alternatives.indexOf(selected_cols_rows[j]) - 1;
    filter_objects.push({ filter: filterNameAndDataFileName(filters[filter_labels.indexOf(selected_filters[j])]),
                          source_index: col_row_index,
                          destination_index: NaN, // To be filled in once new columns/rows have been added in next step
                          span: entered_span_labels[j],
                          new_label: new_labels[j] });
}

// Add new columns and spans when requested by the user.
// These are added so that rows/columns belonging to the same span are adjacent.
// Works out the source and destination column for each filter.
let tidied_filter_objects;
if (adding_new) {
    let span_labels = new_spans.map(function (span) { return span.label; });
    tidied_filter_objects = [];
    span_labels.forEach(function (span) {
        tidied_filter_objects = tidied_filter_objects.concat(filter_objects.filter(function (obj) { return obj.span == span; }));
    });

    // Add a new column for each form entry.
    tidied_filter_objects.forEach(function (obj) {
        let last_index = (filter_columns ? table.numberColumns : table.numberRows) - 1;
        if (filter_columns)
            insertColumnAfterComplete(last_index, obj.new_label);
        else
            insertRowAfterComplete(last_index, obj.new_label);
        // Add the index of the new column or row to the set of indices for its span
        let spandex = span_labels.indexOf(obj.span);
        new_spans[spandex].indices.push(last_index + 1);
        obj.destination_index = last_index + 1;
    });
} else {
    tidied_filter_objects = filter_objects.map(function (obj) {
                                                   obj.destination_index = obj.source_index;
                                                   return obj;
                                               });
}

// Add spans
new_spans.forEach(function (obj) {
    if (obj.label != '') {
        if (filter_columns)
            table.spanColumns(obj.indices, obj.label);
        else
            table.spanRows(obj.indices, obj.label);
    }
});

// Color the appropriate cells when specified
if (shade_cells) {
    let colors = table.cellColors;

    // Obtain array for colors of marginal statistics
    let marginal_colors = null;
    if (filter_columns && below_table_exists) {
        marginal_colors = below_table.cellColors;
    } else if (!filter_columns && right_table_exists) {
        marginal_colors = right_table.cellColors;
    }

    // Change the colors
    tidied_filter_objects.forEach(function (obj) {
        let target_index = obj.destination_index;
        if (filter_columns) {
            for (let row_index = 0; row_index < table.numberRows; row_index++)
                colors[row_index][target_index] = cell_higlight_color;
        } else {
            for (let col_index = 0; col_index < table.numberColumns; col_index++)
                colors[target_index][col_index] = cell_higlight_color;
        }
        if (marginal_colors != null) {
            if (marginal_colors.length === 1)
                marginal_colors[0][target_index] = cell_higlight_color;
            else
                marginal_colors[target_index][0] = cell_higlight_color;
        }
    });
    table.cellColors = colors;
    if (marginal_colors != null) {
        if (filter_columns)
            below_table.cellColors = marginal_colors;
        else
            right_table.cellColors = marginal_colors;
    }
}

// Do we need to filter any marginal stats?
let filter_below = filter_columns && below_table_exists && no_net_available_types.indexOf(blue_question_type) === -1;
let filter_right = !filter_columns && right_table_exists && right_table.statistics.length > 0;

// Copy filtered statistics in the main table and marginal table
tidied_filter_objects.forEach(function (obj) {
    let filter_names = ['!UseQFilters'];
    filter_names.push(obj.filter);
    let filtered_table_stats = getFilteredTableStatistics(filter_names);
    table_stats_to_filter.forEach(function (stat) {
        let current_stats = table.get(stat);
        if (filter_columns) {
            for (let row = 0; row < table.numberRows; row++)
                current_stats[row][obj.destination_index] = filtered_table_stats[stat][row][obj.source_index];
        } else {
            for (let col = 0; col < table.numberColumns; col++)
                current_stats[obj.destination_index][col] = filtered_table_stats[stat][obj.source_index][col];
        }
        table.set(stat, current_stats);
    });

    // Handle marginal stats
    if (filter_below || filter_right) {
        let filtered_marginals = getFilteredMarginalStatistics(filter_columns, filter_names);
        let target_table = filter_columns ? below_table : right_table;
        let marginal_stats_to_filter = filter_columns ? below_stats_to_filter : right_table.statistics;
        marginal_stats_to_filter.forEach(function (stat) {
            // Copy values
            let current_values = target_table.get(stat);
            let marginal_is_row = current_values.length === 1;
            let new_value;

            // Determine the new value to insert.
            // Statistics related to significance testing are left as blank or NaN.
            // It doesn't make sense to compute the % Share for the filtered row/column.
            if (['% Share', 't-Statistic', 'z-Statistic', 'p', 'Corrected p', 'Multiple Comparison Adjustement'].indexOf(stat) > -1 || stat.indexOf('Confidence') != -1)
                new_value = NaN;
            else if (['Column Names', 'Column Comparisons', 'Columns Compared'].indexOf(stat) > -1 ) {
                new_value = '';
            } else {
                new_value = filtered_marginals[stat][obj.source_index];
            }

            // Insert the new value.
            if (marginal_is_row) {
                current_values[0][obj.destination_index] = new_value;
            } else {
                current_values[obj.destination_index][0] = new_value;
            }
            target_table.set(stat, current_values);
        });
    }
});

// Adjust column names when extra columns have been added.
if (adding_new) {
    let LETTERS = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];
    if (table.numberColumns > 26) {
        let new_lower = new Array(260);
        let new_upper = new Array(260);
        for (let l = 0; l < 26; l++)
            for (let i = 0; i < 10; i++) {
                let counter = i * 26 + l;
                new_upper[counter] = LETTERS[l] + i;
            }
        LETTERS = new_upper;
    }
    if (table.availableStatistics.indexOf('Column Names') > -1) {
        let column_names = table.get('Column Names');
        for (let col = 0; col < table.numberColumns; col++) {
            for (let row = 0; row < table.numberRows; row++) {
                column_names[row][col] = LETTERS[col];
            }
        }
        table.set('Column Names', column_names);
    }
    if (below_table_exists && below_table.availableStatistics.indexOf('Column Names') > -1) {
        let column_names = below_table.get('Column Names');
        let by_row = column_names.length === 1;
        for (let col = 0; col < table.numberColumns; col++) {
            if (by_row)
                column_names[0][col] = LETTERS[col];
            else
                column_names[col][0] = LETTERS[col];
        }
        below_table.set('Column Names', column_names);
    }
}

// Modify footer
let footers = table.extraFooters;
footers.push('Some ' + (filter_columns ? 'columns' : 'rows') + ' in this table have filters applied');
table.extraFooters = footers;

// Append the data file name to the name of a filter variable
function filterNameAndDataFileName(filter) {
    return filter.name + ' [' + filter.question.dataFile.name + ']';
}

// Generate an array of all of the statistics from the filtered version of table
function getFilteredTableStatistics(filter_array) {
    if (!Q.matrix){
        // Returns a new array with the value repeated.
        // If you want a 1D array, only supply one length.
        // If you want a 2D array, supply both lengths.
        Q.matrix = function (value, num_rows, num_cols) {
            let rows_array = [];
            if (arguments.length > 2) {
                for (let row = 0; row < num_rows; row++) {
                    let col_array = [];
                    for (let col = 0; col < num_cols; col++)
                        col_array[col] = value;
                    rows_array[row] = col_array;
                }
            } else {
                for (let row = 0; row < num_rows; row++)
                    rows_array[row] = value;
            }
            return rows_array;
        };
    }
    let nan_array = Q.matrix(NaN, table.numberRows, table.numberColumns);
    let blank_string_array = Q.matrix(' ', table.numberRows, table.numberColumns);
    let filtered_table = Q.calculateTable(table.blueQuestion, table.brownQuestion, filter_array, '!UseQWeight');
    let filtered_stats = {};
    table.availableStatistics.forEach(function (stat) {
        if (['t-Statistic', 'z-Statistic', 'p', 'Corrected p', 'Multiple Comparison Adjustement'].indexOf(stat) > -1)
            filtered_stats[stat] = nan_array;
        else if (['Column Names', 'Column Comparisons', 'Columns Compared'].indexOf(stat) > -1)
            filtered_stats[stat] = blank_string_array;
        else
            filtered_stats[stat] = filtered_table.get(stat);
    });
    return filtered_stats;
}

// Obtain the filtered version of the marginal statistics.
// If use_below is true we use below_table, otherwise right_table.
// Returns an object which is indexed by the statistic name.
// Each element in the object is a one-dimensional array regardless
// of whether we are filtering below or right
function getFilteredMarginalStatistics(use_below, filter_array) {
    let filtered_stats = {};
    let blue_q = table.blueQuestion;
    let brown_q = table.brownQuestion;
    // In newer versions of Q we can calcultate the filtered marginal stats directly
    if (fileFormatVersion() > 8.41) {
        let margin = use_below ? 'Below' : 'Right';
        let filtered_marginal_table = Q.calculateTable(blue_q, brown_q, filter_array, '!UseQWeight', margin);
        filtered_marginal_table.availableStatistics.forEach(function (stat) {
            let temp_stats = filtered_marginal_table.get(stat);
            filtered_stats[stat] = temp_stats.length === 1 ? temp_stats[0] : temp_stats.map(function (arr) { return arr[0]; });
        });
    } else {
        // In older versions we need to look at the NET or compute a numeric version of
        // the relevant question to derive the marginal stats
        let target_table = use_below ? below_table : right_table;

        let blue_q_type = blue_q.questionType;
        let brown_q_type = 'SUMMARY';
        if (brown_q != 'SUMMARY')
            brown_q_type = brown_q.questionType;

        // We need to know the index of the original NET/SUM row or column
        // because we derive some of the marginal stats from the NET. The
        // only time we are allow a table which is missing the important
        // NET is a crosstab with a Number question, in which case there
        // is only a single column/row
        let net_index = use_below ? table.netRows[0] : table.netColumns[0];
        if (net_index == undefined) {
            if (use_below && blue_q_type === 'Number')
                net_index = 0;
            else if (!use_below && brown_q_type === 'Number')
                net_index = 0;
            else {
                table.suppressOutput('The Rule "' + rule_name + '" cannot be used when there is no NET or SUM ' + (use_below ? 'row' : 'column'));
                form.ruleNotApplicable('this rule cannot run when there is no NET or SUM ' + (use_below ? 'row' : 'column'));
            }
        }

        // Figure out if we need to make a temporary numeric question & table, and which dropdown
        // will have the numeric version
        let temp_needed = false;
        let temp_in = null;
        if (use_below) {
            if (blue_q_type.indexOf('Pick One') === 0) {
                if (blue_q_type === 'Pick One' && ['SUMMARY', 'Pick One', 'Pick Any', 'Number', 'Number - Multi', 'Date'].indexOf(brown_q_type) > -1) {
                    temp_needed = true;
                    temp_in = 'blue';
                } else if (blue_q_type === 'Pick One - Multi' && !blue_q.dataReduction.transposed) {
                    temp_needed = true;
                    temp_in = 'blue';
                }
            }
        } else {
            if (brown_q_type === 'Pick One' && ['Pick One', 'Pick Any', 'Number', 'Number - Multi', 'Date'].indexOf(blue_q_type) > -1) {
                temp_needed = true;
                temp_in = 'brown';
            } else if (blue_q_type === 'Pick One - Multi' && blue_q.dataReduction.transposed) {
                temp_needed = true;
                temp_in = 'blue';
            }
        }

        // If necessary, create a temporary numeric version of the question, generate its table output and then
        // remove the temporary question.
        let temp_table = null;
        let temp_stat_list = null;
        let std_error_in_temp = false;
        if (temp_needed) {
            if (table.forPlot) {
                table.suppressOutput('The Rule "' + rule_name + '" cannot be used on a plot when there is a Pick One or Pick One - Multi question selected, and Statistics - Below are selected.');
                form.ruleNotApplicable('this rule cannot run on a plot with Statistics - Below selected with a Pick One or Pick One - Multi question.');
            }

            let question_to_convert = temp_in === 'blue' ? blue_q : brown_q;
            let temp_question = createNumericDuplicate(question_to_convert);

            // Make the temporary table
            if (temp_in === 'blue')
                temp_table = Q.calculateTable(temp_question, brown_q, filter_array, '!UseQWeight');
            else
                temp_table = Q.calculateTable(blue_q, temp_question, filter_array, '!UseQWeight');
            temp_stat_list = temp_table.availableStatistics;
            std_error_in_temp = temp_stat_list.indexOf('Standard Error') > -1;
        }

        // Generate the filtered table for the NET/SUM
        let filtered_table = Q.calculateTable(blue_q, brown_q, filter_array, '!UseQWeight');
        let filtered_stat_list = filtered_table.availableStatistics;

        // Loop over the stats, figure out where the stat should be drawn from
        // and store them in an object
        target_table.availableStatistics.forEach(function (stat) {
            if (stat === 'Standard Error') {
                // If the Standard Error statistic is found in the temp table then use that standard error
                // over the one in the NET/SUM
                if (std_error_in_temp) {
                    let temp_stats = temp_table.get(stat);
                    filtered_stats[stat] = temp_stats.length === 1 ? temp_stats[0] : temp_stats.map(function (arr) { return arr[0]; });
                } else {
                    let table_stats = filtered_table.get(stat);
                    filtered_stats[stat] = use_below ? table_stats[net_index] : table_stats.map(function (arr) { return arr[net_index]; });
                }
            } else if (filtered_stat_list.indexOf(stat) != -1) {
                // Get the statistic from the net row or column
                let table_stats = filtered_table.get(stat);
                filtered_stats[stat] = use_below ? table_stats[net_index] : table_stats.map(function (arr) { return arr[net_index]; });
            } else if (temp_needed && temp_stat_list.indexOf(stat) > -1){
                // Get stat from temporary table
                let temp_stats = temp_table.get(stat);
                filtered_stats[stat] = temp_stats.length === 1 ? temp_stats[0] : temp_stats.map(function (arr) { return arr[0]; });
            } else {
                let marginal_equivalent = null;
                // Some stats change their name between the NET and main table, even though
                // the figures are the same.
                if (stat === 'Column n' || stat === 'Row n') {
                    if (['Pick One - Multi', 'Pick Any - Grid'].indexOf(blue_question_type) != -1) {
                        marginal_equivalent = 'n';
                    } else if (blue_question_type === 'Number - Grid'
                             || (blue_question_type === 'Pick Any' && brown_question_type.indexOf('Number') === 0)
                             || (brown_question_type === 'Pick Any' && blue_question_type.indexOf('Number') === 0)) {
                        marginal_equivalent = 'Base n';
                    }
                } else if (stat === 'Column Population' || stat === 'Row Population') {
                    if (['Pick One - Multi', 'Pick Any - Grid'].indexOf(blue_question_type) != -1) {
                        marginal_equivalent = 'Population';
                    } else if (blue_question_type === 'Number - Grid'
                             || (blue_question_type === 'Pick Any' && brown_question_type.indexOf('Number') === 0)
                             || (brown_question_type === 'Pick Any' && blue_question_type.indexOf('Number') === 0)) {
                        marginal_equivalent = 'Base Population';
                    }
                }
                if (marginal_equivalent != null) {
                    let table_stats = filtered_table.get(marginal_equivalent);
                    filtered_stats[stat] = use_below ? table_stats[net_index] : table_stats.map(function (arr) { return arr[net_index]; });
                }
             }
        });

        // Tidy up temporary variables
        if (temp_needed)
            deleteQuestion(temp_question);
    }
    return filtered_stats;
}

// Returns true if Statistics - Below are available for this table.
function belowTableExists() {
    let exists = true;
    try {
        below_table.statistics;
    } catch (e) {
        exists = false;
    }
    return exists;
}

// Returns true if Statistics - Right are available for this table.
function rightTableExists() {
    let exists = true;
    try {
        right_table.statistics;
    } catch (e) {
        exists = false;
    }
    return exists;
}

// Duplicate the question as a numeric version and make sure that the codes are in the same order as the original
// so that the necessary statistics can be drawn out correctly from the numeric version.
// Returns the duplicate question. This should always be deleted prior to the end of the Rule so that
// we don't keep adding questions to the user's project.
function createNumericDuplicate(question_to_convert) {
    let temp_question = question_to_convert.duplicate(preventDuplicateQuestionName(question_to_convert.dataFile, question_to_convert.name + 'TEMP'));

    // Convert to numeric
    if (question_to_convert.questionType === 'Pick One - Multi') {
        // Sort data reduction of converted temp copy according to original question.
        // Changing the question type mucks up the ordering.
        let original_underlying_variables = getAllUnderlyingVariables(temp_question);
        original_underlying_variables = original_underlying_variables.map(function (obj) { return {label: obj.label, array: obj.array.map(function (v) { return v.name; }) }; });
        temp_question.questionType = 'Number - Multi';
        let temp_data_reduction = temp_question.dataReduction;
        let temp_underlying_variables = temp_data_reduction.rowLabels.map(function (label) { return {label: label, array: temp_data_reduction.getUnderlyingVariables(label).map(function (v) { return v.name; })}; });
        // Check for duplicates - can't handle duplicate labels
        let temp_codes = temp_underlying_variables.map(function (obj) { return obj.label; });
        if (uniqueElementsInArray(temp_codes).length != temp_codes.length) {
            deleteQuestion(temp_question);
            table.suppressOutput('The Rule "' + rule_name + '" cannot run because the question contains duplicate labels.');
            form.ruleNotApplicable('the question contains duplicate labels');
        }

        // Sort temp codes according to original order
        let last_code = null;
        original_underlying_variables.forEach(function (obj) {
            let target_vname = obj.array[0];
            let target_code = temp_underlying_variables.filter(function (temp_obj) {
                                                                   return temp_obj.array[0] == target_vname;
                                                               })[0].label;
            temp_data_reduction.moveAfter(target_code, last_code);
            last_code = target_code;
        });
    } else if (question_to_convert.questionType === 'Pick One')
        temp_question.questionType = 'Number';
    else {
        deleteQuestion(temp_question);
        throw('Invalid Question Type: ' + question_to_convert.questionType);
    }
    return temp_question;
}

// Returns an array of objects corresponding to the codes associated with variables in the
// data reduction.
//
// Each object has the properties:
//
// 1. label:    The data reduction label for this code.
// 2. array:    The array of variable names associated with this code.
function getAllUnderlyingVariables(question) {
    let question_type = question.questionType;
    if (question_type != 'Pick Any' && question_type != 'Pick One - Multi')
        return null;
    let data_reduction = question.dataReduction;
    let labels;
    if (question_type === 'Pick Any')
        labels = data_reduction.rowLabels;
    else
        labels = data_reduction.transposed ? data_reduction.rowLabels : data_reduction.columnLabels;
    let num_variable_codes = labels.length;
    let all_underlying_variables = [];
    for (let j = 0; j < num_variable_codes; j++) {
        if (question_type === 'Pick One - Multi')
            all_underlying_variables.push({label: labels[j], array: data_reduction.getUnderlyingVariables(~j)});
        else
            all_underlying_variables.push({label: labels[j], array: data_reduction.getUnderlyingVariables(j)});
    }
    return all_underlying_variables;
}

function deleteQuestion(question) {
    question.variables.forEach(function (v) { v.deleteVariable(); } );
}

See also