Data - Dynamic Text Box

From Q
Jump to navigation Jump to search


Creates sentences and paragraphs to describe results from your data. Multiple lines of custom text can be created based on one or more tables, calculations, or variables.

How to Create a Dynamic Text Box

  1. Add the object by selecting from the menu Anything > Page Design > Dynamic Text BoxAutomate > Browse Online Library > Data > Dynamic Text Box
  2. Choose a Data Source.
  3. Select the Input data.
  4. Surround the text in commentary by typing into Text before and Text after.
  5. Once you have chosen your first result to display, additional sections of the menu will open up to allow you to add to the text by selecting other data sources and entering additional text.
  6. The format of the text may be changed using Properties > Appearance and wrapping of the text is controlled using Properties > Layout > Wrap text output.

Example

In this example, the Dynamic Text Box has been connected to a table showing the NPS for major tech brands, and a calculation of Displayr's NPS (done using R).

DynamicTextBox.PNG


Options

As you make selections, additional sections of the menu will be added. Each has the following options:

Data source Choose what type of data source you want to use to obtain the number. The choices are:

  • Type or paste value Enter the number you wish to show.
  • Table Obtain the number from a table of data.
  • Use an Existing Calculation Obtain the number from a calculation that has been done using R.
  • Variable - Average/Sum/Percentage Compute the number you wish to show directly from a variable.
  • Combo Box or List Box Show the selection in a combo box or list box.

Input data Select the data you wish to use. Options will be shown based on the type of Data source that has been selected.

Number Type Choose to format the number as it is, or convert it to a percentage.

Decimals shown Choose the number of decimal places to display.

Separate thousands with commas Choose whether or not larger numbers should have their thousands shown with commas. For example, show 1000> as 1,000.

Text before The text to display before the number.

Text after The text to display after the number.

Blank lines after Increase this to 1 to begin the next section of text on a new line, or increase it to 2 or more to add extra line spaces before the next section of text.

Code

var heading_text = "Dynamic Text Box";
if (!!form.setObjectInspectorTitle)
    form.setObjectInspectorTitle(heading_text, "Dynamic Text Boxes");
else 
    form.setHeading(heading_text);

function isEmpty(x) { return (x == undefined || x.getValue() == null &&
                  (x.getValues() == null ||
                   x.getValues().length == 0)) }
function isEmptyString(x) { return (x == undefined || x == null || x == "") }

var displayr = Q.isOnTheWeb();
var allow_control_groups = Q.fileFormatVersion() > 10.9;



let i = 1;
let controls = [];
let empty = false;


while (i == 1 || !empty) {
    form.group("Text " + i);
    var dataSourceControl = form.comboBox({name: "formDataSource" + i, label: "Data source", alternatives: ["Type or paste value", "Use an existing Calculation", "Table", "Variable - Average", "Variable - Sum", "Variable - Percentage", "Combo Box or List Box"], default_value: "Type or paste value"});
    controls.push(dataSourceControl);
    var dataSource = dataSourceControl.getValue();

    var showPercent = false;
    if (dataSource == "Type or paste value")
    { 
         var inputString = form.textBox({name: "formInputText"+i, label: "Input data", prompt: "Enter a numeric value, e.g. 45% or 0.45." + inputpromptextra, required: i == 1});
         controls.push(inputString) // for shapes, this can be text
         empty = isEmptyString(inputString.getValue());
    } else if (dataSource == "Use an existing Calculation" || dataSource == "Table")
    {
        if (dataSource == "Use an existing Calculation")
        {
            var inputR = form.dropBox({name: "formInputR" + i, label: "Input data", types: ["RItem:integer,numeric,character"], required: i == 1});
            controls.push(inputR);
            empty = isEmpty(inputR);
        }
        else
        {
            var inputTable = form.dropBox({name: "formInputTable"+i, label: "Input data", types: ["Table", "RItem:matrix,array,data.frame,table"], required: i == 1});
            controls.push(inputTable);
            empty = isEmpty(inputTable);
        }

        if ((dataSource == "Use an existing Calculation" && inputR.getValue() != null) ||
            (dataSource == "Table" && inputTable.getValue() != null))
        {
            var rowOpts = ["Typing row names or indices", "Choosing from Combo Box or List Box control"]
            var columnOpts = ["Typing column names or indices", "Choosing from Combo Box or List Box control"]
            
            if (displayr)
            {
                var qSelectRowsOpt = form.comboBox({label: "Select row to show by", name: "formSelectRowsOpt" + i, alternatives: rowOpts, default_value: rowOpts[0]});
                controls.push(qSelectRowsOpt);
            }

            var qSelectRowsCtrl = form.dropBox({label: "Row", name: "formTableRowCtrl" + i, types: ["Control: listbox,combobox"], required: dataSource == "Table"});
            var qSelectRowsText = form.textBox({name: "formTableRow" + i, label: "Row", prompt: "Enter name or index of row of the entry you want to display", required: dataSource == "Table", default_value: 1});


            if (!isEmpty(qSelectRowsCtrl) ||
                (displayr && qSelectRowsOpt.getValue() != rowOpts[0]))
                controls.push(qSelectRowsCtrl);
            else
                controls.push(qSelectRowsText);


            if (displayr)
            {
                var qSelectColumnsOpt = form.comboBox({label: "Select column to show by", name: "formSelectColumnsOpt" + i, alternatives: columnOpts, default_value: columnOpts[0]});
                controls.push(qSelectColumnsOpt);
            }

            var qSelectColumnsCtrl = form.dropBox({label: "Column", name: "formTableColumnCtrl" + i, types: ["Control: listbox,combobox"], required: false});
            var qSelectColumnsText = form.textBox({name: "formTableColumn" + i, label: "Column", prompt: "Enter name or index of column of the entry you want to display.", required: false, default_value: 1});

            if (!isEmpty(qSelectColumnsCtrl) ||
                (displayr && qSelectColumnsOpt.getValue() != columnOpts[0]))
               controls.push(qSelectColumnsCtrl); 
            else
                controls.push(qSelectColumnsText);
        }

    } else if (dataSource == "Variable - Percentage")
    {
        showPercent = true;
        var percentVar = form.dropBox({name: "formInputVariable" + i, label: "Input data", types: ["Variable: Numeric, Date/Time, Money, Text, Categorical, OrderedCategorical"], required: i == 1});
        controls.push(percentVar);
        if (percentVar.getValue() != null)
            controls.push(form.textBox({name: "formCategory" + i, label: "Category", required: false, prompt: "Enter name of category you want to display. For categorical variables, names can be separated by commas to show the total percentage; for numeric variables, use a dash to specify a range."}));
        empty = isEmpty(percentVar)

    } else if (dataSource.indexOf("Variable") > -1) {
        var inputVar = form.dropBox({name: "formInputVariable" + i, label: "Input data", types: ["Variable: Numeric, Date/Time, Money, Text, Categorical, OrderedCategorical"], required: i == 1});
        controls.push(inputVar);
        empty = isEmpty(inputVar);
    } else if (dataSource == "Combo Box or List Box") {
        var inputBox = form.dropBox({label: "Control", name: "formComboLabel" + i, types: ["Control: listbox,combobox"], required: i == 1});
        controls.push(inputBox);
        empty = isEmptyString(inputBox.getValue());
    }

    if (dataSource != "Combo Box or List Box"){
        var numberType = form.comboBox({name:"numberType" + i, label: "Number type", alternatives: ["Automatic", "Number", "Percentage", "Percentage (no sign)"], default_value: "Automatic"});
        controls.push(numberType)

        var decimals = form.numericUpDown({name:"formDecimals" + i, label: "Decimals shown", minimum: 0, maximum: 6, default_value: 0});
        controls.push(decimals);

        var separate = form.checkBox({name: "formSeparateThousands" + i, label: "Separate thousands with commas", default_value: true});
        controls.push(separate);
    }
        



    var textBefore = form.textBox({name: "formPrefix" + i, label: "Text before", required: false, prompt: "Enter the text to appear before the number."});
    controls.push(textBefore);
    var textAfter = form.textBox({name: "formSuffix" + i, label: "Text after", required: false, prompt: "Enter the text to appear after the number."});
    controls.push(textAfter);

    var lineSpaces = form.numericUpDown({name:"formLineSpaces" + i, label: "Blank lines after text", minimum: 0, maximum: 6, default_value: 0});
    controls.push(lineSpaces)

    
    i++;
}


form.setInputControls(controls);
library(flipTransformations)
library(flipTables)
library(flipPictographs)
library(flipFormat)

value <- NULL
getIndex <- function(index)
{
    if (nchar(index) > 0)
        return(index)
    else
        return(1)
}

.nInputs <- function(control.name.prefix = "formTable")
            length(ls(pattern = paste0("^", control.name.prefix, "[0-9]+$"),
                      envir = .GlobalEnv)) - 1


possibleStatistics <- function(x)
{
    if (!is.null(attr(x, "statistic")))
        return(attr(x, "statistic"))
    rev(dimnames(x))[[1]]
}


getFirstControlSelection <- function (x) { 
    if (length(x) > 1) warning("Only one selection may be used from a Combo Box or List Box. Consider using a Combo Box or List Box where the Control mode is set to Single selection.") 
    x[1]
}

weight.warnings = vector("character")


n.inputs <- .nInputs(control.name.prefix = "formDataSource")
out <- vector("list", n.inputs)
   
for (i in seq_len(n.inputs)){

    # Get the main value from the selected input
    source.i = get0(paste0("formDataSource", i))
    if ((!is.null(QPopulationWeight) || length(QFilter) > 1)  && !grepl("Variable", source.i)) {
        weight.warnings = c(weight.warnings, paste0("Text ", i))
    }

    select.row <- if (exists(paste0("formTableRowCtrl", i))) getFirstControlSelection(get0(paste0("formTableRowCtrl", i))) else get0(paste0("formTableRow", i))
    select.column <- if (exists(paste0("formTableColumnCtrl", i))) getFirstControlSelection(get0(paste0("formTableColumnCtrl", i))) else get0(paste0("formTableColumn", i)) 

    if (source.i == "Type or paste value") {
       value <- ParseText(get0(paste0("formInputText", i)))
    } else if (source.i == "Use an existing Calculation") {
       value <- SelectEntry(get0(paste0("formInputR", i)), getIndex(select.row), getIndex(select.column), return.single.value = TRUE, use.statistic.attribute = TRUE)
    } else if (source.i == "Table") {
        value <- SelectEntry(get0(paste0("formInputTable", i)), select.row, select.column, return.single.value = TRUE, use.statistic.attribute = TRUE)
    } else if (source.i == "Combo Box or List Box") {
        if (exists(paste0("formComboLabel", i)) && !is.null(get0(paste0("formComboLabel", i)))) {
            value <- ParseText(getFirstControlSelection(get0(paste0("formComboLabel", i))))
        } else {
            value <- ""
        }
        
        
    } else {
        value <- SummarizeVariable(get0(paste0("formInputVariable", i)), type = source.i, category = get0(paste0("formCategory", i)),
                    weights = QPopulationWeight, subset = QFilter) 
    }

    # If value is factor (e.g. because selected input is a data.frame)
    # convert to character.
    if (is.factor(value)) {
        value <- as.character(value)
    }

    if (source.i != "Combo Box or List Box" && mode(value) != "character") {
        # Convert the number type and format the number

        number.type.i <- get0(paste0("numberType", i))
        if (number.type.i == "Automatic") 
        {
            convert.percent <- FALSE
            stats = NULL
            if (source.i == "Table") {
                stats <- possibleStatistics(get0(paste0("formInputTable", i)))
            } else if (source.i == "Use an existing Calculation") {
                stats <- possibleStatistics(get0(paste0("formInputR", i)))
            } else if (source.i == "Variable - Percentage") {
                stats = "%"
            }

            if (!is.null(stats))
                convert.percent <- grepl("%", stats[1], fixed = TRUE)

            if (convert.percent) {
                value <- value / 100
                value <- FormatAsPercent(value, decimals = get0(paste0("formDecimals", i)), comma.for.thousands = get0(paste0("formSeparateThousands", i)))  
            } else {
                value <- FormatAsReal(value, decimals = get0(paste0("formDecimals", i)), comma.for.thousands = get0(paste0("formSeparateThousands", i))) 
            }

        } else if (startsWith(number.type.i, "Percentage")) {
            if (source.i == "Table" || source.i == "Variable - Percentage")
                value <- value / 100
            value <- FormatAsPercent(value, decimals = get0(paste0("formDecimals", i)), 
                                     comma.for.thousands = get0(paste0("formSeparateThousands", i)), 
                                     show.sign = !endsWith(number.type.i, "(no sign)")) 
        } else {
            value <- FormatAsReal(value, decimals = get0(paste0("formDecimals", i)), comma.for.thousands = get0(paste0("formSeparateThousands", i))) 
        }
    }

    # Paste the prefix and suffix
    prefix.i <- get0(paste0("formPrefix", i))
    suffix.i <- get0(paste0("formSuffix", i))

    # Add new lines
    blanks = paste0(rep("\n", get0(paste0("formLineSpaces", i))), collapse = "")

    # Paste it all together
    out[[i]] <- paste0(prefix.i, value, suffix.i, blanks)

}

if (length(weight.warnings) > 0) {
    warning.text = paste0(weight.warnings, collapse = ", ")
    if (!is.null(QPopulationWeight) && length(QFilter) > 1) {
        warning("Filter(s) and Weight cannot be applied to ", warning.text, ". Ensure the weight and filter(s) are applied to the source Tables or Calculations.")
    } else if (!is.null(QPopulationWeight)) {
        warning("The weight cannot be applied to ", warning.text, ". Ensure the weight is applied to the source Tables or Calculations.")
    } else if (length(QFilter) > 1) {
        warning("The filter(s) cannot be applied to ", warning.text, ". Ensure the filter(s) are applied to the source Tables or Calculations.")
    }
}

dynamic.text = paste0(out, collapse = "")