Marketing - Price Sensitivity Meter

From Q
Jump to: navigation, search

Plot the Van Westendorp Price Sensitivity Meter to determine what price most people consider is reasonable

VizIcon Price Sensitivity Meter.svg

Plots the Van Westendorp Price Sensitivity Meter to determine what price most people consider is reasonable.

Example

If additional questions about purchasing intent at the cheap and expensive prices are also provided, then the Newton-Miller-Smith (NMS) extension is used to examine how pricing affects demand and revenue

Options

Inputs

OUTPUT
Select the output type to show. By default this is Attitude of repondents which is the standard van Westendorp chart. If the extra data about purchasing intent is provided, then charts showing Likelihood to buy, Revenue or both of these together can be selected.
DATA SOURCE
The input data can be supplied using either Paste or type data or existing output in 'Pages'. These are expected to be tables where each row is the response to 4 columns. The columns contain the response to the question: At what price would you consider this product/brand to be 1) Too cheap, 2) Cheap, 3) Expensive, 4) Too expensive. Columns 5 and 6 which ask about purchasing intent is optional and can be left blank if the aim is only to create the van Westendorp chart.
Alternatively, if the survey responses are present as variables in the data set, the variables can be individually selected as Price considered 'Too cheap' , Price considered 'Cheap' , Price considered 'Expensive' , Price considered 'Too expensive' . If less than 4 of these variables are provided, then only the intersections of the variables provided will be shown.
Likelihood scale Map likelihood score to probability of buying product; used in NSM extension. The number of values in the list should match the likelihood scores in the data input. That is, if the likelihood to buy is given as a score from 1 to 7 then the likelihood scale should consist of a list of 7 comma separated values which map each of these scores to a probability.
Remove inconsistent prices Removes observations where prices are not supplied in increasing order.

Chart

Intersection Points/Optimal Points
Show intersection labels/Optimal points Label the critical points on the curve. On the van Westendorp chart, this is where the curves meet, i.e. the point of marginal cheapness, optimal price point, indifference point price and the point of marginal expensiveness. For the other outputs, the label is placed where the price is maximised.
Number of decimals Number of decimals to show in the intersection labels.
Wrap intersection labels Whether the intersection labels should be wrapped; otherwise they will be shown in a single line of text which may be quite long.
Intersection labels width (in characters) This specifies when the line breaks are introduced into the labels. Line breaks are added at the closest wordbreak before the number of characters specified.
Arrow color The color of the arrow next to the intersection labels
Arrow line width Line width of the arrow (in pixels).
Arrow length Controls the length of the arrows (and therefore how close the labels are to the intersection points).
Currency symbol The currency symbol used in the labels. These will also be used as the defaults in the CATEGORIES (X) AXIS, however, there is a separate control in that group to specify a different symbol (or no symbol).

Refer to Visualization Options for general chart formatting options.

Code

var heading_text = "Price Sensitivity Meter";
if (!!form.setObjectInspectorTitle)
    form.setObjectInspectorTitle(heading_text);
else 
    form.setHeading(heading_text);

var allow_control_groups = Q.fileFormatVersion() > 10.9; // Group controls for Displayr and later versions of Q
var controls = []; // All controls displayed must be inserted here
var displayr = Q.isOnTheWeb();
function isEmpty(x) { return (x == undefined || x.getValue() == null && (x.getValues() == null || x.getValues().length == 0)) }
function isBlankSheet(x) { return (x.getValue() == null || x.getValue().length <= 2) }
function isEmptyString(x) { return (x == undefined || x == null || x == "") }
function componentToHex(c) { var hex = c.toString(16); return hex.length == 1 ? "0" + hex : hex; }
function rgbToHex(x) { return "#" + componentToHex(x[0]) + componentToHex(x[1]) + componentToHex(x[2]); }

// * Combo box alternatives *
var template_prompt = "Create a template to control color palettes and fonts for all visualizations in the document using " + (displayr ? "'Insert > Utilities > Visualization > Create Template'" : "'Automate > Browse Online Library > Visualization > Create Template'")
var format_prompt = "Format automatically determined based on input data. Click on up/down buttons to adjust";
categories_number_formats = ["Automatic", "Number", "Category", "Percentage", "Date/Time", "Currency", "Metric units suffix", "Scientific", "Custom"];
values_number_formats = ["Automatic", "Number", "Category", "Percentage", "Date/Time", "Currency", "Metric units suffix", "Scientific", "Custom"];
hover_number_formats = ["Automatic", "Number", "Category", "Percentage", "Metric units suffix", "Scientific", "Custom"]; // no currency since cannot add as prefix and no date because of charting requirements
data_label_formats = ["Automatic", "Number", "Percentage"];
date_formats = ["YY (Year, 2 digit)", "DD Mon YY", "DD Month YY", "DD MM YY", "YYYY (Year, 4 digit)", "DD Mon YYYY", "DD Month YYYY", "DD MM YYYY", "Mon DD YY", "Month DD YY", "MM DD YY", "Mon DD YYYY", "Month DD YYYY", "MM DD YYYY", "YY Mon DD", "YY Month DD", "YY MM DD", "YYYY Mon DD", "YYYY Month DD", "YYYY MM DD", "Custom"];
 
// * Font alternatives *
font_families = ["Arial", "Arial Black", "Century Gothic", "Comic Sans MS",
                 "Courier New", "Georgia", "Impact", "Open Sans", "Tahoma", "Times New Roman", "Trebuchet MS", "Verdana"]
default_colors = ["#3e7dcc", "#04b5ac", "#f5c524", "#c44e41", "#8cc0ff", "#ff905a", "#345e8c", "#04827b", "#967f47", "#96362f", "#2c4374", "#4d525a"];;
var use_default_fonts = false;
var globalFontFamily = "Arial";
var globalFontColor = "#2C2C2C";
var globalFontSize = 7.5;


/* Creating the controls */
if (allow_control_groups)
    form.group("OUTPUT");
var typeList = ["Attitude of respondents", "Likelihood to buy", "Revenue", "Likelihood to buy and Revenue"];
var qOutputType = form.comboBox({name: "formOutputType", label: "Show", alternatives: typeList, default_value: typeList[0]});
controls.push(qOutputType);
var outputType = qOutputType.getValue();

// Creating the data inputs.
if (allow_control_groups)
    form.group("DATA SOURCE");

// Input as linked Q or R table(s)
var outputLabel = "Output";
if (displayr)
   outputLabel += " in 'Pages'";
var tableInput = form.dropBox({name: "formTable", label: outputLabel, types: ['Table', "RItem:!StandardChart!SignificanceTest:!KMeans:!phraseList:!tidyText:!wordBag"], required: false, multi:false, prompt: "Select table containing 4 columns of price data"});

// Input as variables in the Data tab
var variables_controls = []; 
var var1 = form.dropBox({label: "Price considered 'Too cheap'", name: "formV1", types: ["Variables:Numeric,Money"], prompt: "The price at which the product is so cheap you would have doubts about its quality", multi: false, required: false});
var var2 = form.dropBox({label: "Price considered 'Cheap'", name: "formV2", types: ["Variables:Numeric,Money"], prompt: "The price at which the product is inexpensive yet you have no doubts about its quality" , multi: false, required: false});
var var3 = form.dropBox({label: "Price considered 'Expensive'", name: "formV3", types: ["Variables:Numeric,Money"], prompt: "The price at which you begin to feel the product is expensive but still worth buying", multi: false, required: false});
var var4 = form.dropBox({label: "Price considered 'Too expensive'", name: "formV4", types: ["Variables:Numeric,Money"], prompt: "The price at which the product is too expensive and not worth buying", multi: false, required: false});
variables_controls.push(var1);
variables_controls.push(var2);
variables_controls.push(var3);
variables_controls.push(var4);
var anyVariables = !isEmpty(var1) || !isEmpty(var2) || !isEmpty(var3) || !isEmpty(var4)

// Pasted input data
var manual_default = [["Too cheap", "Cheap", "Expensive", "Too expensive", 
    "At cheap price, how likely are you to purchase product", 
    "At expensive price, how likely are you to purchase product"], ["", "", "", "", "", ""]];
var pastedInput = form.dataEntry({label: "Paste or type data", name: "formPastedData", default_value: manual_default, prompt: "Opens a spreadsheet into which you can enter data.", required: true, large_data_error: "The data entered is too large. The best alternative is to add your data as a Data Set, use Table > Raw Data > Variable(s), and connect that table to this analysis."});

// Hide other data sources if one of them is already selected
if (!allow_control_groups || !isEmpty(tableInput) || (isBlankSheet(pastedInput) && !anyVariables))
     controls.push(tableInput);
if (!allow_control_groups || anyVariables || (isEmpty(tableInput) && isBlankSheet(pastedInput)))
    controls = controls.concat(variables_controls);
if (!allow_control_groups || !isBlankSheet(pastedInput) || (isEmpty(tableInput) && !anyVariables)) 
     controls.push(pastedInput);

// Optional dropdown for NSM extension
if (anyVariables && outputType != typeList[0])
{
    var var5 = form.dropBox({label: "Likelihood of buying at 'cheap' price", name: "formV5", types: ["Variables:Numeric"], multi: false, required: false});
    controls.push(var5);
    var var6 = form.dropBox({label: "Likelihood of buying at 'expensive' price", name: "formV6", types: ["Variables:Numeric"], multi: false, required: false});
    controls.push(var6);
}
if (outputType != typeList[0])
    controls.push(form.textBox({label: "Likelihood scale", name: "formLikelihoodScale", default_value: "0.0, 0.0, 0.0, 0.1, 0.3, 0.5, 0.75", required: true, prompt: "Map likelihood score to probability of buying product; used in NSM extension. The number of values in the list should match the likelihood scores in the data input, i.e. 7 values are provided to map a 7-point scale (1='very unlikely' to 7='likely') to the probability of buying (0 to 1)."}));
controls.push(form.checkBox({label: "Remove inconsistent prices", name: "formCheckPrices", default_value: true, prompt: "Ignore respondents which are not specifying prices in increasing order. Missing values are retained."}));

// Creating controls on the Chart page (forced to be on Inputs tab for Q <= v5.2.1)
if (allow_control_groups)
    form.page('Chart');
if (allow_control_groups)
    form.group({label:'APPEARANCE', expanded: true});
var qTemplate = form.dropBox({name: "formTemplate", label: "Use template", types: ["RItem:AppearanceTemplate"], required: false, prompt: template_prompt});
controls.push(qTemplate);
use_default_fonts = !isEmpty(qTemplate);

form.group("Data series");
if (outputType == typeList[0])
{
    controls.push(form.colorPicker({name: "formColor1", label: "Color of Less than 'Very cheap'", default_value: "#FF0000"}));
    controls.push(form.comboBox({name: "formLineType1", label: "Line type of Less than 'Very cheap'", alternatives: ["Solid", "Dot", "Dash"], default_value: "Dot"}));
    controls.push(form.numericUpDown({name: "formLineWidth1", label: "Line width of Less than 'Very cheap'", default_value: 1}));

    controls.push(form.colorPicker({name: "formColor2", label: "Color of Less than 'Cheap'", default_value: "#FF0000"}));
    controls.push(form.comboBox({name: "formLineType2", label: "Line type of Less than 'Cheap'", alternatives: ["Solid", "Dot", "Dash"], default_value: "Solid"}));
    controls.push(form.numericUpDown({name: "formLineWidth2", label: "Line width of Less than 'Cheap'", default_value: 2}));

    controls.push(form.colorPicker({name: "formColor3", label: "Color of More than 'Expensive'", default_value: "#008000"}));
    controls.push(form.comboBox({name: "formLineType3", label: "Line type of More than 'Expensive", alternatives: ["Solid", "Dot", "Dash"], default_value: "Solid"}));
    controls.push(form.numericUpDown({name: "formLineWidth3", label: "Line width of More than 'Expensive'", default_value: 2}));

    controls.push(form.colorPicker({name: "formColor4", label: "Color of More than 'Very expensive'", default_value: "#008000"}));
    controls.push(form.comboBox({name: "formLineType4", label: "Line type of More than 'Very expensive", alternatives: ["Solid", "Dot", "Dash"], default_value: "Dot"}));
    controls.push(form.numericUpDown({name: "formLineWidth4", label: "Line width of More than 'Very expensive'", default_value: 1}));
}

if (outputType == typeList[1] || outputType == typeList[3])
{
    controls.push(form.colorPicker({name: "formColor1", label: "Color of 'Likelihood to buy'", default_value: "#FF0000"}));
    controls.push(form.comboBox({name: "formLineType1", label: "Line type of 'Likelihood to buy'", alternatives: ["Solid", "Dot", "Dash"], default_value: "Solid"}));
    controls.push(form.numericUpDown({name: "formLineWidth1", label: "Line width of 'Likelihood to buy'", default_value: 2}));
}

if (outputType == typeList[2] || outputType == typeList[3])
{
    controls.push(form.colorPicker({name: "formColor2", label: "Color of 'Revenue'", default_value: "#008000"}));
    controls.push(form.comboBox({name: "formLineType2", label: "Line type of 'Revenue'", alternatives: ["Solid", "Dot", "Dash"], default_value: "Solid"}));
    controls.push(form.numericUpDown({name: "formLineWidth2", label: "Line width of 'Revenue'", default_value: 2}));
}


/* FUNCTIONS */
numberFormatControls = function(name, prefix, controls, number_formats, addPrefix = true, default_currency_symbol = "$")
{
    var qNumberType = form.comboBox({name: "form" + name + "NumberType", label: prefix + " number type", alternatives: number_formats, default_value: number_formats[0], required: true});
    controls.push(qNumberType);
    var yNumberType = qNumberType.getValue();
    if (yNumberType == "Date/Time") // funny loop because date/time can be custom
    {
        var qDate = form.comboBox({name: "form" + name + "DateType", label: "Date type", alternatives: date_formats, default_value: date_formats[0], required: true});
        yNumberType = qDate.getValue();
        controls.push(qDate);
    }
    
    // Custom format
    var qNumberCustom = null;
    if (yNumberType == "Custom")
        qNumberCustom = form.textBox({name: "form" + name + "NumberCustom", label: "Custom code", default_value: "%d %b %y", required: true, prompt: "D3 formatting code, e.g. ,.2%"});
    else if (yNumberType == 'Currency')
        qNumberCustom = form.textBox({name: "form" + name + "Currency", label: "Currency symbol", default_value: default_currency_symbol, required: true}); 
    else if (yNumberType == 'Number' || yNumberType == 'Currency') 
        qNumberCustom = form.checkBox({name: "form" + name + "SeparateThousands", label: "Separate thousands by comma", default_value: true});
    if (qNumberCustom != null)
        controls.push(qNumberCustom);
    
    controls.push(form.numericUpDown({name: "form" + name + "Decimals", label: yNumberType == "Metric unit suffix" ? "Significant digits": "Decimal places", minimum: yNumberType == "Metric unit suffix" ? 1 : 0, required: false, prompt: format_prompt}));
    if (addPrefix)
    {
        controls.push(form.textBox({name: "form" + name + "Prefix", label: "Custom prefix", default_value: "", required: false, prompt: "Optional text to prepend to the labels"})); 
        controls.push(form.textBox({name: "form" + name + "Suffix", label: "Custom suffix", default_value: "", required: false, prompt: "Optional text to append to the labels"})); 
    }
    return(yNumberType);
}

fontControls = function(name, prefix, controls, fontSize = 7.5, adjustColor = true)
{
    var qFontDefault = form.checkBox({name: "form" + name + "FontDefault", label: "Use default or template fonts", default_value: use_default_fonts, prompt: template_prompt});
    controls.push(qFontDefault);
    if (!qFontDefault.getValue())
    {
        controls.push(form.comboBox({name: "form" + name + "FontFamily", label: prefix + " font family", alternatives: font_families, default_value: globalFontFamily, editable: true, prompt: "Select the font to use. You can also type the name of a font directly (including custom fonts)."}));
        if (adjustColor)
            controls.push(form.colorPicker({name: "form" + name + "FontColor", label: prefix + " font color", default_value: globalFontColor}));
        controls.push(form.numericUpDown({name: "form" + name + "FontSize", label: prefix + " font size", default_value: fontSize, increment: 0.5}));
    }
}


formattedTextControls = function(name, prefix, controls, fontSize, promptText, 
                                 alwaysFormat = false, wrap = false, wrapDefault = 20, textbox = true, defaultText = "")
{
    if (textbox)
    {
        var qText = form.textBox({name: "form" + name, label: prefix, required: false, prompt: promptText, default_value: defaultText});
        var textOpt = qText.getValue();
        controls.push(qText);
    }
    if (alwaysFormat || !textbox || !isEmptyString(textOpt))
    {
        var qTextFontDefault = form.checkBox({name: "form" + name + "FontDefault", label: "Use default or template fonts", default_value: use_default_fonts, prompt: template_prompt});
        controls.push(qTextFontDefault);
        if (!qTextFontDefault.getValue())
        {
            controls.push(form.comboBox({name: "form" + name + "FontFamily", label: prefix + " font family", alternatives: font_families, default_value: globalFontFamily, editable: true, prompt: "Select the font to use. You can also type the name of a font directly (including custom fonts)."}));
            controls.push(form.colorPicker({name: "form" + name + "FontColor", label: prefix + " font color", default_value: globalFontColor}));
            controls.push(form.numericUpDown({name:"form" + name + "FontSize", label: prefix + " font size", default_value: fontSize, increment: 0.5}));
        }
        if (wrap)
        {
            var qTextWrap = form.checkBox({name: "form" + name + "Wrap", label: "Wrap " + prefix, default_value: true});
            controls.push(qTextWrap);
            if (qTextWrap.getValue())
            {
                var qTextWrapN = form.numericUpDown({name: "form" + name + "WrapNchar", label: prefix + " width (in characters)", default_value: wrapDefault, minimum: 5, increment: 5, maximum: 5000});
                controls.push(qTextWrapN);
            }
        }
    }
}



if (allow_control_groups)
    form.group("FONT");
var qGlobalFontDefault = form.checkBox({name: "formGlobalFontDefault", label: "Use default or template fonts", default_value: use_default_fonts, prompt: template_prompt});
controls.push(qGlobalFontDefault);
use_default_fonts = qGlobalFontDefault.getValue();
if (!use_default_fonts)
{
    var qGlobalFontFamily = form.comboBox({name: "formFont", label: "Global font family", alternatives: font_families, default_value: "Arial", editable: true, prompt: "Select the font to use. You can also type the name of a font directly (including custom fonts)."});
    controls.push(qGlobalFontFamily);
    globalFontFamily = qGlobalFontFamily.getValue();
    var qGlobalFontColor = form.colorPicker({name: "formFontColor", label: !allow_control_groups ? "" : "Global font color", default_value: "#2C2C2C"});
    controls.push(qGlobalFontColor);
    globalFontColor = qGlobalFontColor.getValue();
    var qFontUnits = form.comboBox({name: "formFontUnits", label: "Font units", alternatives: ["pt", "px"], default_value: "pt", prompt: "Are font sizes specified in terms of points or pixels?"});
    controls.push(qFontUnits);
}

if (qOutputType.getValue() == typeList[0])
{
    if (allow_control_groups)
        form.group("Intersection points");
    var intersectionShow = form.checkBox({name: "formIntersectionShow", label: "Show intersection labels", prompt: "Add annotations to show points where lines intersect", default_value: true});
}
else
{
    if (allow_control_groups)
        form.group("Optimal points");
    var intersectionShow = form.checkBox({name: "formIntersectionShow", label: "Show labels at optimal points", prompt: "Add annotations to show optimal prices", default_value: true});
}
controls.push(intersectionShow);
if (intersectionShow.getValue())
{
    controls.push(form.numericUpDown({name: "formIntersectionLabelDecimals", label: "Number of decimals", default_value: 2}));
    formattedTextControls("IntersectionLabel", "Intersection labels", controls, 7.5, "", true, true, 21, false);
    controls.push(form.colorPicker({name: "formIntersectionArrowColor", label: "Arrow color", default_value: globalFontColor}));
    controls.push(form.numericUpDown({name: "formIntersectionArrowWidth", label: "Arrow line width", default_value: 0.7, increment: 0.1}));
    controls.push(form.numericUpDown({name: "formIntersectionArrowLength", label: "Arrow length", default_value: 10, prompt: "Increase to move labels further from intersection point"})); 
}
var currencySym = form.textBox({name: "formCurrencySymbol", label: "Currency symbol", default_value: "$", required: false});
controls.push(currencySym);


if (allow_control_groups)
    form.group("LEGEND");
var qLegShowAuto = form.comboBox({name: "formLegendShow", label: "Legend", alternatives: ["Show", "Hide"], default_value: outputType == typeList[0] ? "Show" : "Hide"});
controls.push(qLegShowAuto);
var hasleg = qLegShowAuto.getValue() != "Hide";

if (hasleg)
{
    fontControls("Legend", "Legend", controls)
    var qLegOrnt = form.comboBox({name: "formLegendOrientation", label: "Legend orientation", alternatives: ['Vertical', 'Horizontal'], default_value: 'Vertical'});
    controls.push(qLegOrnt); 
    var qLegCustom = form.checkBox({name: "formLegendCustom", label: "Customize legend position", default_value: false});
    controls.push(qLegCustom);
    var legendCustomPos = qLegCustom.getValue();
    var isLegendVertical = qLegOrnt.getValue() == "Vertical";

    if (legendCustomPos)
    {
        var qLegX = form.numericUpDown({name: "formLegendXPos", label: "Horizontal placement", default_value: isLegendVertical ? 1.02: 0.5, increment: 0.02, minimum: -2, maximum: 3, prompt: "Choose numeric value between -2 (far left) to 3 (far right)"});
        controls.push(qLegX);
        var qLegY = form.numericUpDown({name: "formLegendYPos", label: "Vertical placement", default_value: isLegendVertical ? 1.0 : -0.2, increment: 0.02, minimum: -2, maximum: 3, prompt: "Choose numeric value between -2 (below) and 3 (above)"});
        controls.push(qLegY);
    }
}

if (allow_control_groups)
    form.group('TITLE');
formattedTextControls("Title", "Title", controls, 12, "", true);
formattedTextControls("Subtitle", "Subtitle", controls, 9, "")
formattedTextControls("Footer", "Footer", controls, 6, "Optional footer. Leave blank to show default text", true, true, 100);


// X Axis
var categoriesAxisLabel = "CATEGORIES (X) AXIS";
var axisPrefix =  "Axis";
if (allow_control_groups)
    form.group(categoriesAxisLabel);
else
    axisPrefix = categoriesAxisLabel.charAt(0).toUpperCase() + categoriesAxisLabel.substr(1).toLowerCase();

var labelShowX = "Show " + axisPrefix.toLowerCase();
controls.push(form.textBox({name: "formCategoriesMin", label: "Minimum value", required: false, prompt: "Leave blank to determine automatically from input data. Otherwise, enter a numeric value (no prefix or suffix) for a numeric axis; a date-string for a date axis; or a 0-based index for a categorical axis."}));
controls.push(form.textBox({name: "formCategoriesMax", label: "Maximum value", required: false, prompt: "Leave blank to determine automatically from input data. Otherwise, enter a numeric value (no prefix or suffix) for a numeric axis; a date-string for a date axis; or a 0-based index for a categorical axis."}));
var qXGridWidth = form.numericUpDown({name: "formCategoriesGridWidth", label: "Grid line width", minimum: 0, default_value: 0});
controls.push(qXGridWidth);
if (qXGridWidth.getValue() > 0)
    controls.push(form.colorPicker({name: "formCategoriesGridColor", label: "Grid line color", default_value: "#E1E1E1"}));
var qXLineWidth = form.numericUpDown({name: "formCategoriesLineWidth", label: "Axis line width", minimum: 0, default_value: 0});
controls.push(qXLineWidth);
if (qXLineWidth.getValue() > 0)
    controls.push(form.colorPicker({name: "formCategoriesLineColor", label: "Axis line color", default_value: "#000000"}));
var qShowXTitle = form.checkBox({name: "formShowCategoriesTitle", label: labelShowX + " title", default_value: true});
var showCategoriesTitle = qShowXTitle.getValue();
controls.push(qShowXTitle);
if (showCategoriesTitle)
    formattedTextControls("CategoriesTitle", axisPrefix + " title", controls, 9, "", true, false, 20, true, "Price")

qShowX = form.checkBox({name: "formCategoriesAxisShow", label: labelShowX, default_value: true});
var showX = qShowX.getValue();
controls.push(qShowX);
if (showX)
{
    var categories_number_formats = ["Currency", "Number", "Category", "Percentage", "Date/Time", "Metric units suffix", "Scientific", "Custom"];
    numberFormatControls("Categories", axisPrefix, controls, categories_number_formats, true, currencySym.getValue())
    fontControls("CategoriesTick", axisPrefix + " label", controls), 
    controls.push(form.comboBox({name: "formCategoriesTickAngle", label: axisPrefix + " label orientation", alternatives: ["Automatic", "Horizontal", "Vertical", "Diagonal"], default_value: "Automatic"}));
    var qXWrap = form.checkBox({name: "formLabelWrap", label: "Wrap " + axisPrefix.toLowerCase() + " label", default_value: true});
    var xWrapOpt = qXWrap.getValue();
    controls.push(qXWrap);
    if (xWrapOpt)
        controls.push(form.numericUpDown({name: "formLabelWrapNchar", label: axisPrefix + " label width (in characters)", default_value: 21, minimum: 5, increment: 5, maximum: 1000}));
}

// Y AXIS
var valuesAxisLabel = "VALUES (Y) AXIS";
var axisPrefix = "Axis";
if (!allow_control_groups)
    axisPrefix = valuesAxisLabel.charAt(0).toUpperCase() + valuesAxisLabel.substr(1).toLowerCase();
var labelShowY = "Show " + axisPrefix.toLowerCase();

if (allow_control_groups)
    form.group(valuesAxisLabel);

controls.push(form.textBox({name: "formValuesMin", label: "Minimum value", required: false, prompt: "Leave blank to determine automatically from input data."}));
controls.push(form.textBox({name: "formValuesMax", label: "Maximum value", required: false, prompt: "Leave blank to determine automatically from input data."}));

var qYGridWidth = form.numericUpDown({name: "formValuesGridWidth", label: "Grid line width", minimum: 0, default_value: 1});
controls.push(qYGridWidth);
var showYgrid = qYGridWidth.getValue() > 0;
if (showYgrid)
    controls.push(form.colorPicker({name: "formValuesGridColor", label: "Grid line color", default_value: "#E1E1E1"}));
var qYLineWidth = form.numericUpDown({name: "formValuesLineWidth", label: "Axis line width", minimum: 0, default_value: 0});
controls.push(qYLineWidth);
if (qYLineWidth.getValue() > 0)
    controls.push(form.colorPicker({name: "formValuesLineColor", label: "Axis line color", default_value: "#000000"}));
var qShowValuesTitle = form.checkBox({name: "formShowValuesTitle", label: labelShowY + " title", default_value: true});
var showValuesTitle = qShowValuesTitle.getValue();
controls.push(qShowValuesTitle);
if (showValuesTitle)
    formattedTextControls("ValuesTitle", axisPrefix + " title", controls, 9, "", true, false, 20, true);
qShowY = form.checkBox({name: "formValuesAxisShow", label: labelShowY, default_value: true});
controls.push(qShowY);
var showY = qShowY.getValue();
if (showY) 
{
    var values_number_formats = ["Automatic", "Number", "Category", "Percentage", "Date/Time", "Currency", "Metric units suffix", "Scientific", "Custom"];
    var yNumberType = numberFormatControls("Values", axisPrefix, controls, values_number_formats)
    fontControls("ValuesTick", axisPrefix + " label", controls);
}

if (allow_control_groups)
    form.group("HOVER");
var hover_number_formats = ["Automatic", "Number", "Percentage"];
var hover_default_format = hover_number_formats[0];
var qHoverNumberType = form.comboBox({name: "formHoverNumberType", label: "Hovertext number type", alternatives: hover_number_formats, default_value: hover_default_format, required: true});
var hoverNumberType = qHoverNumberType.getValue();
controls.push(qHoverNumberType);
var qHoverCustom = null;
if (hoverNumberType == "Custom")
    qHoverCustom = form.textBox({name: "formHoverNumberCustom", label: "Custom code", default_value: "%d %b %y", required: true, prompt: "D3 formatting code, e.g. ,.2%"});
else if (hoverNumberType == 'Currency')
    qHoverCustom = form.textBox({name: "formHoverCurrency", label: "Currency symbol", default_value: "$", required: true}); 
else if (hoverNumberType == 'Number')
    qHoverCustom = form.checkBox({name: "formHoverSeparateThousands", label: "Separate thousands by comma", default_value: true});
if (qHoverCustom != null)
    controls.push(qHoverCustom);
var qHoverDecimals = form.numericUpDown({name:"formHoverDecimals", label: hoverNumberType == "Metric unit suffix" ? "Significant digits": "Decimal places", minimum: hoverNumberType == "Metric unit suffix" ? 1 : 0, required: false});
controls.push(qHoverDecimals);
var qHoverFontDefault = form.checkBox({name: "formHoverFontDefault", label: "Use default or template fonts", default_value: use_default_fonts, prompt: template_prompt});
controls.push(qHoverFontDefault);
var hoverFontDefault = qHoverFontDefault.getValue(); 
var qHoverFontFamily = form.comboBox({name: "formHoverFontFamily", label: "Hovertext font family", alternatives: font_families, default_value: globalFontFamily, editable: true, prompt: "Select the font to use. You can also type the name of a font directly (including custom fonts)."});
controls.push(qHoverFontFamily);
var qHoverFontSize = form.numericUpDown({name: "formHoverFontSize", label: "Hovertext font size", default_value: 8.5}); 
controls.push(qHoverFontSize);


if (allow_control_groups)
    form.group("MARGINS");
var qMarginContol = false; 
qCustomMargin = form.checkBox({name: "formCustomMargin", label: "Customize margins", default_value: false, prompt: "Select to manually specify margin sizes in pixels"});
controls.push(qCustomMargin);
qMarginControl = qCustomMargin.getValue();
if (qMarginControl)
{
    controls.push(form.numericUpDown({name: "formMarginTop", label: "Top", default_value: 30, increment: 1, minimum: 0, maximum: 1000}));
    controls.push(form.numericUpDown({name: "formMarginLeft", label: "Left", default_value: 80, increment: 1, minimum: 0, maximum: 1000}));
    controls.push(form.numericUpDown({name: "formMarginBottom", label: "Bottom", default_value: 50, increment: 1, minimum: 0, maximum: 1000}));
    controls.push(form.numericUpDown({name: "formMarginRight", label: "Right", default_value: 40, increment: 1, minimum: 0, maximum: 1000}));
}

if (allow_control_groups)
    form.group("BACKGROUND")
var qBg = form.checkBox({name: "formTransparent", label: "Transparent background", default_value: true}); 
controls.push(qBg);
var showBg = !qBg.getValue();
if (showBg)
    controls.push(form.colorPicker({name: "formBgColor", label: "Background color", default_value: "#FFFFFF"}));
if (showBg)
    controls.push(form.numericUpDown({name: "formBgOpacity", label: "Background opacity", minimum: 0.0, maximum: 1.0, increment: 0.1, default_value: 1.0}));
form.setInputControls(controls);


library(flipChart)
library(flipStartup)
template <- get0("formTemplate")

# Set defaults if no template
if (is.null(template))
{
    default.palette <- "Default colors"
    template <- list(colors = default.palette, global.font = list(family = "Arial", color = "#2C2C2C", 
        size = 7.5, units = "pt"), fonts = list(`Data labels` = list(
        family = "Arial", color = "#2C2C2C", size = 7.5), Legend = list(
        family = "Arial", color = "#2C2C2C", size = 7.5), Title = list(
        family = "Arial", color = "#2C2C2C", size = 12), Subtitle = list(
        family = "Arial", color = "#2C2C2C", size = 9), Footer = list(
        family = "Arial", color = "#2C2C2C", size = 6), `Categories axis title` = list(
        family = "Arial", color = "#2C2C2C", size = 9), `Categories axis tick labels` = list(
        family = "Arial", color = "#2C2C2C", size = 7.5), `Values axis title` = list(
        family = "Arial", color = "#2C2C2C", size = 9), `Values axis tick labels` = list(
        family = "Arial", color = "#2C2C2C", size = 7.5), `Panel title` = list(
        family = "Arial", color = "#2C2C2C", size = 9), `Hover text` = list(
        family = "Arial", color = "#2C2C2C", size = 8.625)), colors = "Default colors")
}
  
# Processing all the selections from the 'Inputs' and 'Charts' tab.
weights <- if (length(QFilter) > 1) QPopulationWeight[QFilter] else QPopulationWeight
lenV <- max(length(get0("formV1")), length(get0("formV2")), length(get0("formV3")), length(get0("formV4")))
getVector <- function(x.name) { x <- get0(x.name); if (length(x) == 0) return(rep(NA, lenV)) else return(x)}
pd <- PrepareData("Line",  QFilter, NULL, input.data.table = get0("formTable"),
    input.data.raw = if (lenV == 0) NULL else list(A=getVector("formV1"), B=getVector("formV2"), C=getVector("formV3"), D=getVector("formV4"), E=getVector("formV5"), F=getVector("formV6")),
    input.data.pasted = if (!exists("formPastedData") || NROW(formPastedData) <= 2) NULL else list(get0("formPastedData")),
    hide.empty.rows.and.columns = FALSE
)

pn <- PrepareNumbers(categories.format.list = list(get0("formCategoriesNumberType"), get0("formCategoriesDateType"), get0("formCategoriesNumberCustom"), get0("formCategoriesSeparateThousands"), get0("formCategoriesDecimals")), values.format.list = list(get0("formValuesNumberType"), get0("formValuesDateType"), get0("formValuesNumberCustom"), get0("formValuesSeparateThousands"), get0("formValuesDecimals")), hover.format.list = list(get0("formHoverNumberType"), get0("formHoverDateType"), get0("formHoverNumberCustom"), get0("formHoverSeparateThousands"), get0("formHoverDecimals")), data.labels.format.list = list(get0("formDataLabelNumberType"), get0("formDataLabelDateType"), get0("formDataLabelCustom"), get0("formDataLabelSeprateThousands"), get0("formDataLabelDecimals")), formOutputType != "Revenue")

        
# Creating the chart or table
psm <- PriceSensitivityMeter(x = pd$data, weights = pd$weights,
    output = formOutputType,
    likelihood.scale = get0("formLikelihoodScale"),
    check.prices.ordered = get0("formCheckPrices", ifnotfound = FALSE),
    # Chart: DATA SERIES
    colors = c(get0("formColor1"), get0("formColor2"), get0("formColor3"), get0("formColor4")),
    # Chart: APPEARANCE
    line.type = c(get0("formLineType1"), get0("formLineType2"), get0("formLineType3"), get0("formLineType4")),
    line.thickness = c(get0("formLineWidth1"), get0("formLineWidth2"), get0("formLineWidth3"), get0("formLineWidth4")),
    currency = formCurrencySymbol,
    # Chart: FONT
    global.font.family = get0("formFont", ifnotfound = template$global.font$family),
    global.font.color = get0("formFontColor", ifnotfound = template$global.font$color),
    # Labels for intersection points
    intersection.show = get0("formIntersectionShow", ifnotfound = TRUE),
    intersection.arrow.color = get0("formIntersectionArrowColor", ifnotfound = template$global.font$color),
    intersection.arrow.width = get0("formIntersectionArrowWidth", ifnotfound = 0.7),
    intersection.arrow.length = get0("formIntersectionArrowLength", ifnotfound = 10),
    intersection.arrow.size = 0.3,
    intersection.label.decimals = get0("formIntersectionLabelDecimals", ifnotfound = 2),
    intersection.label.font.family = get0("formIntersectionLabelFontFamily", ifnotfound = template$fonts$`Data labels`$family),
    intersection.label.font.color = get0("formIntersectionLabelFontColor", ifnotfound = template$fonts$`Data labels`$color),
    intersection.label.font.size = get0("formIntersectionLabelFontSize", ifnotfound = template$fonts$`Data labels`$size),
    intersection.label.wrap = get0("formIntersectionLabelWrap", ifnotfound=FALSE),
    intersection.label.wrap.nchar = get0("formIntersectionLabelWrapNchar"),
    #Chart: GRIDLINES
    y.line.width = get0("formValuesLineWidth", ifnotfound = 0),
    y.line.color = get0("formValuesLineColor", ifnotfound = "#000000"),
    y.grid.width = get0("formValuesGridWidth", ifnotfound = 0),
    y.grid.color = get0("formValuesGridColor", ifnotfound = "#E1E1E1"),
    x.line.width = get0("formCategoriesLineWidth", ifnotfound = 0),
    x.line.color = get0("formCategoriesLineColor", ifnotfound = "#000000"),
    x.grid.width = get0("formCategoriesGridWidth", ifnotfound = 0),
    x.grid.color = get0("formCategoriesGridColor", ifnotfound = "#E1E1E1"),
    # Chart: LEGEND
    legend.show = get0("formLegendShow", ifnotfound = TRUE),
    legend.orientation = get0("formLegendOrientation", ifnotfound = "Vertical"),
    legend.font.family = get0("formLegendFontFamily", ifnotfound = template$fonts$Legend$family),
    legend.font.color = get0("formLegendFontColor", ifnotfound = template$fonts$Legend$color),
    legend.font.size = get0("formLegendFontSize", ifnotfound = template$fonts$Legend$size),
    legend.position.x = get0("formLegendXPos"),
    legend.position.y = get0("formLegendYPos"),
    # Chart: TITLE
    title = paste0("", if (sum(nchar(get0("formTitle"))) > 0) formTitle else pd$chart.title),
    title.font.family = get0("formTitleFontFamily", ifnotfound = template$fonts$`Title`$family),
    title.font.color = get0("formTitleFontColor", ifnotfound = template$fonts$`Title`$color),
    title.font.size = get0("formTitleFontSize", ifnotfound = sum(template$fonts$`Title`$size)),
    subtitle = get0("formSubtitle", ifnotfound=""),
    subtitle.font.family = get0("formSubtitleFontFamily", ifnotfound = template$fonts$Subtitle$family),
    subtitle.font.color = get0("formSubtitleFontColor", ifnotfound = template$fonts$Subtitle$color),
    subtitle.font.size = get0("formSubtitleFontSize", ifnotfound = template$fonts$Subtitle$size),
    footer = paste0("", if (sum(nchar(get0("formFooter"))) > 0) formFooter else pd$chart.footer),
    footer.font.family = get0("formFooterFontFamily", ifnotfound = template$fonts$Footer$family),
    footer.font.color = get0("formFooterFontColor", ifnotfound = template$fonts$Footer$color),
    footer.font.size = get0("formFooterFontSize", ifnotfound = template$fonts$Footer$size),
    footer.wrap = get0("formFooterWrap", ifnotfound=FALSE),
    footer.wrap.nchar = get0("formFooterWrapNchar"),
    #Chart: CATEGORIES_AXIS
    x.bounds.minimum = get0("formCategoriesMin"), 
    x.bounds.maximum = get0("formCategoriesMax"), 
    x.tick.show = get0("formCategoriesAxisShow", ifnotfound=FALSE),
    x.tick.format = pn$categories.number.format,
    x.tick.prefix = paste0("", get0("formCategoriesPrefix"), get0("formCategoriesCurrency")),    # currency is just another prefix
    x.tick.suffix = get0("formCategoriesSuffix", ifnotfound=""),
    x.title = paste0("", if (sum(nchar(get0("formCategoriesTitle", ifnotfound = " "))) > 0) get0("formCategoriesTitle", ifnotfound = " ") else pd$categories.title),
    x.title.font.family = get0("formCategoriesTitleFontFamily", ifnotfound = template$fonts$`Categories axis title`$family),
    x.title.font.color = get0("formCategoriesTitleFontColor", ifnotfound = template$fonts$`Categories axis title`$color),
    x.title.font.size = get0("formCategoriesTitleFontSize", ifnotfound = sum(template$fonts$`Categories axis title`$size)), 
    x.tick.font.family = get0("formCategoriesTickFontFamily", ifnotfound = template$fonts$`Categories axis tick labels`$family),
    x.tick.font.color = get0("formCategoriesTickFontColor", ifnotfound = template$fonts$`Categories axis tick labels`$color),
    x.tick.font.size = get0("formCategoriesTickFontSize", ifnotfound = sum(template$fonts$`Categories axis tick labels`$size)),
    x.tick.angle = if (!exists("formCategoriesTickAngle")) NULL else switch(get0("formCategoriesTickAngle"), Automatic=NULL, Horizontal=0, Vertical=90, Diagonal=45),
    x.tick.label.wrap = get0("formLabelWrap", ifnotfound=FALSE),
    x.tick.label.wrap.nchar = get0("formLabelWrapNchar", ifnotfound=100),
    #Chart: VALUES_AXIS
    y.bounds.minimum = get0("formValuesMin"), 
    y.bounds.maximum = get0("formValuesMax"), 
    y.tick.show = get0("formValuesAxisShow", ifnotfound=FALSE),
    y.tick.format = pn$values.number.format,
    y.tick.prefix = paste0("", get0("formValuesPrefix"), get0("formValuesCurrency")),    # currency is just another prefix
    y.tick.suffix = get0("formValuesSuffix", ifnotfound=""),
    y.title =  paste0("", if (sum(nchar(get0("formValuesTitle", ifnotfound=" "))) > 0) get0("formValuesTitle", ifnotfound = " ") else pd$values.title),
    y.title.font.family = get0("formValuesTitleFontFamily", ifnotfound = template$fonts$`Values axis title`$family),
    y.title.font.color = get0("formValuesTitleFontColor", ifnotfound = template$fonts$`Values axis title`$color),
    y.title.font.size = get0("formValuesTitleFontSize", ifnotfound = sum(template$fonts$`Values axis title`$size)),
    y.tick.font.size = get0("formValuesTickFontSize", ifnotfound = sum(template$fonts$`Values axis tick labels`$size)),
    y.tick.font.family = get0("formValuesTickFontFamily", ifnotfound = template$fonts$`Values axis tick labels`$family),
    y.tick.font.color = get0("formValuesTickFontColor", ifnotfound = template$fonts$`Values axis tick labels`$color),
    # Chart: HOVER
    y.hovertext.format = pn$hover.number.format,
    hovertext.font.family = get0("formHoverFontFamily", ifnotfound = template$fonts$`Hover text`$family),
    hovertext.font.size = get0("formHoverFontSize", ifnotfound = template$fonts$`Hover text`$size),
    # Chart: MARGINS
    margin.top = get0("formMarginTop"),
    margin.left = get0("formMarginLeft"),
    margin.bottom = get0("formMarginBottom"),
    margin.right = get0("formMarginRight"),
    # General arguments
    font.units = get0("formFontUnits", ifnotfound = "pt"),
    background.fill.color = get0("formBgColor", ifnotfound = "transparent"),
    background.fill.opacity = get0("formBgOpacity", ifnotfound = 1.0))