/****** cx.0.js *******/ 

/* eslint-disable vars-on-top */
/* global DX:false, CR:false, FMT:false, Props:false, ContextMenu:false, KlipFactory:false, dashboard:false, page:false, isWorkspace:false,
    bindValues:false, deleteTempProperties:false, encodeForId:false, convertToVirtualColumnId:false, findDefaultAggregationRule:false,
    KF:false, eachComponent:false, DST:false, updateManager:false */


var Component = {};
var CX = {};

/**
 * Constants
 */
// this is going to be used by input control to indicate no condition filter to be applied for a component if this value is selected.
CX.KF_ALL_ID = "_all_";

/**
 */
Component.Base = $.klass(
        /** @lends Component.Base# */
{

    /** our jQuery DOM element */
    el : false,

    /** the human-readable name of the component when referenced in the UI */
    displayName: "Component",
    isRenamed: false,

    /**
     * what data structure does it accept.
     * none - does not accept data
     * single - a single value
     * series - a list of values
     * table - a 2d table of data
     */
    accepts: "single",

    /**
     * the unique name of this component that is used to create new instances
     * from the factory
     */
    factory_id : false,


    /**
     * child components
     */
    components : false,

    /**
     * parent component
     */
    parent: false,

    /**
     * a layout constraints object/value.  components defined through a DSL need to have
     * a way to specify their layout.
     */
    layoutConfig : false,

    /** object with 'w' & 'h' properties for pixel dimensions */
    dimensions : false,

    /** conditions: predicates & reactions*/
    conditions : false,


    /**
     *
     */
    visible : true,

    /**
     *
     */
    visibleInWorkspace : true,

    /**
     * whether or not to propogate updates to the parent of a given component
     * @param config
     */
    updateParent: false,


    fmtArgs: false,

    /**
     * a list of reactions applies as a result of conditions evaluating to true
     */
    reactions: false,

    /**
     * if false, update will abort early and requeue a cx for update.  If a component
     * declares dependencies via getDependencies() the factory will flag dependenciesLoaded as false and
     * 'require' them. When loaded set cx.dependenciesLoaded to true, and will update the component
     */
    dependenciesLoaded: true,

    drilldownSupported : false,
    drilldownEnabled : false,
    drilldownConfig : false,
    drilldownStack : false,

    /**
     * if true, the component will be deletable from the component tree
     */
    isDeletable: true,

    /**
     * if true, the component can be dragged
     */
    isDraggable: true,


    /**
     * if true, the component can have a dstContext defined on it
     */
    isDstRoot: true,
    dstContext: false,

    /**
     * Options that affect the context menu
     * - isMovable: true or { label:"" } if component can be moved; default label: "Series"
     * - variableSubComponents: list of sub-components that can have a variable number, i.e. you can add/remove them
     */
    isMovable: false,
    variableSubComponents: false,
    canGroup: false,
    canAggregate: false,
    canSetAggregation: false,
    canSort: false,
    canFilter: false,
    canHaveDataSlots: false,
    canMakeDstRoot: false,

    useLegacyDateFormat: true,

    dependentFormulas: null,

    /**
     * @class base/abstract component class
     * @constructs
     */
    initialize: function(config) {
        this.fmtArgs = {};
        this.dimensions = {w:false,h:false};
        this._dst = new DST(this);
        if (config) this.configure(config);
        this.deferedUpdate = _.debounce( _.bind(this.update,this), 200 );
        this._dependenciesFromSameRoot = {};
        this._allDependencies = {};
//	    this.applyConditions = _.debounce(_.bind(this.applyConditions,this),100)
    },

    /**
     * renders the initial DOM elements that will be needed by this component {@link Component.Base}
     * subsclasses must call renderDom up the parent chain to ensure they are properly constructed.
     * @returns a single container DOM element that will be assigned to this.el
     */
    renderDom : function() {
        this.el = $("<div>").addClass("comp").attr("id", this.id).attr("cx-id", this.id);

        if ( this.role ) this.el.addClass("role-"+this.role);
        if ( this.factory_id ) this.el.addClass("cx-" + this.factory_id );
        return this.el;
    },

    /**
     * update this component's properties based on a configuration DSL. if the configuration
     * parameters affect the display,  'update' should be called.
     * @this { Component.Base }
     * @param config
     */
    configure : function(config){
        bindValues(this, config, ["role", "displayName", "dstContext", "layoutConfig", "updateParent"]);

        if (config.renamed) this.isRenamed = config.renamed;
        if (config.namerenamed) this.isNameRenamed = config.namerenamed;

        if (config.vis !== undefined) this.visible = config.vis;
        if (config.height) this.setHeight(config.height);
        if (config.size) this.size = config.size;

        if (config.fmt) this.fmt = config.fmt;
        if (config.fmtArgs) {
            this._configureFmtArgs(config.fmtArgs);
        }

        if (config.isDeletable !== undefined) this.isDeletable = config.isDeletable;
        if (config.isDraggable !== undefined) this.isDraggable = config.isDraggable;

        if (config.drilldownEnabled !== undefined) {
            this.drilldownEnabled = config.drilldownEnabled;
            this.drilldownStack = this.drilldownEnabled ? [] : false;
        }
        if (config.drilldownConfig) this.drilldownConfig = config.drilldownConfig;

        this.determineDateFormat();

        if (this.canMakeDstRoot) {
            bindValues(this, config, ["isDstRoot"]);
        }

        if (config.components){
            this.components = [];
            for(var i in config.components ){
                var childCx = KlipFactory.instance.createComponent( config.components[i], dashboard );
                this.addComponent(childCx);
            }
        }
    },

    _configureFmtArgs: function(newFmtArgs) {
        var isOldPrefixFontStyleDefined = typeof this.fmtArgs.prefixFontStyle !== "undefined";
        var isOldPrefixFontColourDefined = typeof this.fmtArgs.prefixFontColour !== "undefined";
        var isNewPrefixFontStyleDefined = typeof newFmtArgs.prefixFontStyle !== "undefined";
        var isNewPrefixFontColourDefined = typeof newFmtArgs.prefixFontColour !== "undefined";

        var isOldSuffixFontStyleDefined = typeof this.fmtArgs.suffixFontStyle !== "undefined";
        var isOldSuffixFontColourDefined = typeof this.fmtArgs.suffixFontColour !== "undefined";
        var isNewSuffixFontStyleDefined = typeof newFmtArgs.suffixFontStyle !== "undefined";
        var isNewSuffixFontColourDefined = typeof newFmtArgs.suffixFontColour !== "undefined";

        this.fmtArgs = $.extend(this.fmtArgs, newFmtArgs);

        // Until changed the prefix/suffix will get the same style/colour as the data
        // Once any style/colour is changed for the prefix/suffix, it becomes independent from the data style/colour
        // Below copies the other property from the data to keep it the same but make it independent

        if (!isOldPrefixFontStyleDefined && isNewPrefixFontStyleDefined && (!isOldPrefixFontColourDefined && !isNewPrefixFontColourDefined)) {
            this.fmtArgs.prefixFontColour = this.font_colour;
        }

        if (!isOldPrefixFontColourDefined && isNewPrefixFontColourDefined && (!isOldPrefixFontStyleDefined && !isNewPrefixFontStyleDefined)) {
            this.fmtArgs.prefixFontStyle = this.font_style;
        }

        if (!isOldSuffixFontStyleDefined && isNewSuffixFontStyleDefined && (!isOldSuffixFontColourDefined && !isNewSuffixFontColourDefined)) {
            this.fmtArgs.suffixFontColour = this.font_colour;
        }

        if (!isOldSuffixFontColourDefined && isNewSuffixFontColourDefined && (!isOldSuffixFontStyleDefined && !isNewSuffixFontStyleDefined)) {
            this.fmtArgs.suffixFontStyle = this.font_style;
        }
    },

    getFmtArgs: function() {
        return $.extend({}, this.fmtArgs, {
            disableStyles: this.disableStyles,
            fontStyle: this.font_style,
            fontColour: this.font_colour
        });
    },

    /**
     * adds a child component
     * @param cx
     * @param idx
     */
    addComponent : function(cx,idx){
        if ( !this.components ) this.components = [];
        if ( cx.parent ) {
            if (cx.usePostDSTReferences()) {
                cx.parent.removeComponent(cx, undefined, true);
            } else {
                cx.parent.removeComponent(cx);
            }
        }
        cx.parent = this;

        cx.onParentAssignment();

        if (cx.getKlip()) { // the klip may not have been assigned yet
            cx.onKlipAssignment();
        }

        if (idx == undefined) this.components.push(cx);
        else this.components.splice(idx,0,cx);

        if ( this.layoutMananger ) {
            this.layoutManager.layoutComponent(cx);
        }

        this.invalidate();

        PubSub.publish("cx-added",cx);
    },

    /**
     * Event occurs after component has been added to a parent
     * - i.e. `this.parent` will be defined
     */
    onParentAssignment: function() {

    },

    addChildByType : function(config) {
        var newChild = null;
        if (this.canHaveDataSlots && config.type == "data_slot") {
            newChild = KlipFactory.instance.createComponent(config);
            this.addComponent(newChild);
            return newChild;
        }
        return false;
    },


    removeComponent : function(cx, nextActive, detach) {
        var len = this.components.length;
        while(--len != -1) {
            if (this.components[len] == cx) {
                if (cx.el) cx.el.detach();
                this.components.splice(len, 1);
            }
        }
        this.invalidate();

        PubSub.publish("cx-removed",cx);

        if (!detach && this.usePostDSTReferences()) {
            cx.destroy();
        }

    },

    /**
     * store the state and configuration of this component into a DSL object that can
     * sent to KlipFactory to create a new instance.
     */

    serialize : function() {
        var config = {};

        config.type = this.factory_id;

        bindValues(config, this, ["role","id","displayName","layoutConfig","updateParent"],
            {
                layoutConfig:false,
                updateParent:false
            }, true);

        config.renamed = this.isRenamed ? this.isRenamed : undefined;
        config.namerenamed = this.isNameRenamed ? this.isNameRenamed : undefined;

        if (!this.visible) config.vis = this.visible;
        config.size = this.size;

        config.fmt = this.fmt;
        if (!_.isEmpty(this.fmtArgs)) config.fmtArgs = this.fmtArgs;

        if (this.dstContext) {
            config.dstContext = this.dstContext;
        }
        if (this.canMakeDstRoot) {
            config.isDstRoot = this.isDstRoot;
        }

        config.drilldownEnabled = this.drilldownEnabled ? this.drilldownEnabled : undefined;
        config.drilldownConfig = this.drilldownConfig ? this.drilldownConfig : undefined;

        if ( this.conditions ) {
            deleteTempProperties(this.conditions);
            config.conditions = this.conditions;
        }

        if ( this.components ) {
            config.components = [];
            for (var i = 0; i < this.components.length; i++) {
                config.components.push(this.components[i].serialize());
            }
        }

        return config;
    },


    setVisible: function(v) {
        this.visible = v;

        this.toggleElVisibility();
    },

    toggleElVisibility: function() {
        if (this.el) {
            if (!this.visible) {
                this.el.css("display", "none");
            } else {
                this.el.css("display", "");
                this.invalidateAndQueue();
            }
        }
    },


    /**
     * updates the display.  should be called when the current display should be considered
     * invalid either because new data is available, or because layout/environment has changed
     */
    update : function() {
        if (!this.el) this.el = this.renderDom();

        if (this.updateParent && this.parent) this.parent.update();

        if( this.parent && !this.parent.isKlip && !this.fromPanelUpdate) {
            setTimeout( _.bind(this.parent.onChildrenUpdated, this.parent, this),0 );
        }

    },


    /**
     * goes up the parent hierarchy and returns the klip this CX is contained in
     */
    getKlip : function() {
        var p = this.parent;
        while(p && !p.isKlip)
            p = p.parent;

        if ( p.isKlip ) return p;
        else return false;
    },

    /**
     * goes up the parent hierarchy and returns the root component
     */
    getRootComponent : function() {
        var parent = this.parent;

        if (parent.isKlip || parent.isPanel) {
            return this;
        } else {
            return parent.getRootComponent();
        }
    },

    /**
     * Returns the index of this component in its parents children
     */
    getIndex : function () {
        if (this.parent) {
            var ar = this.parent.components;
            for (var i = 0; i < ar.length; ++i) {
                if (ar[i] === this) {
                    return i;
                }
            }
            return -1;
        } else {
            return 0;
        }
    },

    /**
     * Returns a colon delimited list of indices of each parent in the ancestry of this component followed by this component's index
     * The order is from root -> child -> child -> this
     */
    getIndexPath : function () {
        var ar = [];
        var traveller = this;
        while (traveller) {
            if (!traveller.getIndex) break;
            ar.push (traveller.getIndex ());
            traveller = traveller.parent;
        }
        ar.reverse ();
        return ar.join (":");
    },


    setComponentProp : function( key , val ) {
        var klip = this.getKlip();
        klip.dashboard.setDashboardProp(1, klip.id+":"+key, val, klip.id, klip.currentTab);
    },

    getComponentProp : function(key) {
        var klip = this.getKlip();
        return klip.dashboard.getDashboardProp(klip.id+":"+key, 1, klip.id, klip.currentTab);
    },


    getInternalPropNames : function() {
        return false;
    },


    /**
     * Validates and sets the display name
     *
     * @param displayName
     * @returns {boolean} - true, if a valid display name was set
     */
    setDisplayName : function(displayName) {
        if (displayName != this.displayName) {
            this.isRenamed = true;
        }

        this.displayName = displayName;

        return true;
    },

    /**
     * Returns true if this component has a results reference to a component within the same DST root
     * or if it has a formula reference to a results reference to a component within the same DST root
     * @returns {boolean|*}
     */
    hasDependenciesFromSameRoot : function() {
        var hasReferences = false;
        if (this.usePostDSTReferences()) {
            hasReferences = (Object.keys(this._dependenciesFromSameRoot).length > 0);
        }
        return hasReferences;
    },

    /**
     * Returns true if this component has a results reference to a another component
     * or if it has a formula reference to a results reference to a component
     * @returns {boolean|*}
     */
    isDependentComponent : function() {
        var hasReferences = false;
        if (this.usePostDSTReferences()) {
            hasReferences = (Object.keys(this._allDependencies).length > 0);
        }
        return hasReferences;
    },

	isDatePickerInternalComponent : function() {
    	switch (this.role) {
			case "display_format":
			case "output_format":
			case "calendar_format":
				return true;
		}
    	return false;
	},




    /**
     * when a klip is in the workspace it is inspected to see what components/elements should
     * be exposed for editing.  if a component can be edited in discreet parts, or manages sub-components,
     * it should return a model in the form:
     *
     * [
     *    {
     *       text:"Name of Editable Part" ,
     *       actionId:"id action to execute when the menu item is selected" ,
     *       actionArgs: {} ,
     *       item:[<child items in the same format>]
     *    }
     *    ...
     * ]
     *
     *
     */
    getEditorMenuModel : function() {
        if ( !this.visibleInWorkspace ) return false;

        var model = { text:this.displayName ? $("<div />").text(this.displayName).html() : "Unnamed", actionId:"select_component", actionArgs:this };

        if (this.components) {
            model.items = [];
            for (var i = 0; i < this.components.length; i++) {
                var childModel = this.components[i].getEditorMenuModel();
                if ( childModel ) model.items.push(childModel);
            }
        }

        return model;
    },


    getReferenceValueModel : function() {
        if (!this.visibleInWorkspace) return false;

        var model = { text:this.displayName ? $("<div />").text(this.displayName).html() : "Unnamed", reference:false };
        if (this.components) {
            model.items = [];
            for(var i in this.components) {
                var childModel = this.components[i].getReferenceValueModel();
                if ( childModel ) model.items.push(childModel);
            }
        }
        return model;
    },

    getReferenceName : function() {
        return this.displayName || "Unnamed";
    },


    /**
     * returns a schema that describes the properties for this component. is used
     * by the property editor
     *
     * schema = [
     *    { name:"Alignment" ,  format:"select" , options : ["left","right","center"] , constraints: ["required"] }
     *    { name:"Size" ,  format:"select" , options : ["1","2","3"] , constraints: ["required"] }
     * ]
     *
     *
     */
    getPropertyModel : function() {
        return [];
    },

    addDstProperties: function(model) {
        var dst = this.getAvailableDst();
        var propertyCtx = {
            disabled: !this.hasFormula()
        };
        if (dst.makeDstRoot) {
            this.addMakeDstRootProperty(model, propertyCtx);
        }
        if (dst.group) {
            this.addGroupProperty(model, propertyCtx);
        }
        if (dst.aggregate) {
            this.addAggregateProperties(model, propertyCtx);
        }
        if (!dst.aggregate && dst.aggregation) {
            this.addAggregationProperty(model, propertyCtx);
        }
        if (dst.sort) {
            this.addSortProperty(model, propertyCtx);
        }
        if (dst.filter) {
            this.addFilterProperties(model, propertyCtx);
        }

    },

    /**
     * Add the appearance properties to the model
     * - group: appearance
     *
     * @param model
     */
    addAppearanceProperties: function(model) {
        var factoryId = this.factory_id;
        var parentFactoryId = this.parent.factory_id;
        var alignmentOptions;
        var updatePrefixSuffixStyle;

        if (this.disableStyles) {
            return;
        }

        updatePrefixSuffixStyle = function() {
            var styleButton;

            if (typeof page !== "undefined" && typeof page.componentPropertyForm !== "undefined") {
                styleButton = page.componentPropertyForm.getWidget("fmt_arg_prefixStyle");

                if (styleButton) {
                    styleButton.setButtonSelected();
                }
            }

            if (typeof page !== "undefined" && typeof page.componentPropertyForm !== "undefined") {
                styleButton = page.componentPropertyForm.getWidget("fmt_arg_suffixStyle");

                if (styleButton) {
                    styleButton.setButtonSelected();
                }
            }
        };

        // Size
        model.push({
            type:"select",
            id:"size",
            label:"Font Size" ,
            group:"appearance",
            options: Props.Defaults.size,
            displayWhen: {
                fmt: Props.Defaults.alphanumericFormats
            },
            selectedValue: this.size
        });

        // Style
        model.push({
            type: "button_group",
            id: "font_style",
            group: "appearance",
            label: "Font Style",
            fieldMargin: "9px 0 5px",
            options: Props.Defaults.fontStyles,
            displayWhen: {
                fmt: Props.Defaults.alphanumericFormats
            },
            selectedValue: this.font_style,
            onChange: updatePrefixSuffixStyle
        });

        // Colour
        model.push({
            type:"color_picker" ,
            id:"font_colour",
            group:"appearance",
            fieldMargin: "9px 0 5px",
            displayWhen: {
                fmt: Props.Defaults.alphanumericFormats
            },
            valueType: "fgCss",
            defaultColour: Props.Defaults.defaultFontColour,
            selectedValue: this.font_colour,
            flow: { padding:"0" },
            onChange: updatePrefixSuffixStyle
        });

        // Align
        alignmentOptions = Props.Defaults.align.slice(factoryId === "table_col" ? 0 : 1);

        if (parentFactoryId != "mini_series" && parentFactoryId != "mini_bar" && parentFactoryId != "gauge") {
            model.push({
                type:"button_group_ex",
                id:"align",
                group:"appearance",
                fieldMargin: "0 0 9px",
                options: alignmentOptions,
                displayWhen: {
                    fmt: Props.Defaults.alignmentFormats
                },
                selectedValue: this.align
            });
        }

        // Wrap
        model.push({
            type:"checkboxes",
            id:"wrap",
            group:"appearance",
            displayWhen: {
                fmt: Props.Defaults.alphanumericFormats
            },
            options:[
                {value:"active",label:"Allow word wrap", checked:this.wrap}
            ]
        });
    },

    /**
     * Add prefix and suffix properties to the model
     * - group: content
     *
     * @param model
     */
    addPrefixSuffixProperties: function(model) {
        var _this = this;
        var defaultFontColour = Props.Defaults.defaultFontColour;

        if (this.disablePrefixSuffix) {
            return;
        }

        // Prefix
        model.push({
            type: "text",
            id: "fmt_arg_prefix",
            label: "Prefix",
            group: "content",
            displayWhen: {
                fmt: Props.Defaults.prefixSuffixFormats
            },
            value: this.fmtArgs.prefix
        });

        if (!this.disableStyles) {
            model.push({
                type: "overlay",
                id: "fmt_arg_prefixStyle",
                group: "content",
                buttonCss: "text_style",
                overlayModel: [
                    {
                        type: "button_group",
                        id: "fmt_arg_prefixFontStyle",
                        options: Props.Defaults.fontStyles
                    },
                    {
                        type:"color_picker",
                        id:"fmt_arg_prefixFontColour",
                        valueType: "fgCss",
                        defaultColour: defaultFontColour,
                        previewSwatchStyle: {
                            "vertical-align": "bottom"
                        }
                    }
                ],
                getSelectedValues: function() {
                    var selectedValues = {};
                    var fontStyle = _this.fmtArgs.prefixFontStyle;
                    var fontColour = _this.fmtArgs.prefixFontColour;

                    if (typeof fontStyle === "undefined" && typeof fontColour === "undefined") {
                        fontStyle = _this.font_style;
                        fontColour = _this.font_colour;
                    }

                    if (fontStyle) {
                        selectedValues["fmt_arg_prefixFontStyle"] = {
                            selectedValue: fontStyle
                        };
                    }

                    if (fontColour && fontColour !== defaultFontColour["fgCss"]) {
                        selectedValues["fmt_arg_prefixFontColour"] = {
                            selectedValue: fontColour
                        };
                    }

                    return selectedValues;
                },
                displayWhen: {
                    fmt: Props.Defaults.prefixSuffixFormats
                },
                flow: true
            });
        }

        // Suffix
        model.push({
            type: "text",
            id: "fmt_arg_suffix",
            label: "Suffix",
            group: "content",
            displayWhen: {
                fmt: Props.Defaults.prefixSuffixFormats
            },
            value: this.fmtArgs.suffix,
            breakFlow: true
        });

        if (!this.disableStyles) {
            model.push({
                type: "overlay",
                id: "fmt_arg_suffixStyle",
                group: "content",
                buttonCss: "text_style",
                overlayModel: [
                    {
                        type: "button_group",
                        id: "fmt_arg_suffixFontStyle",
                        options: Props.Defaults.fontStyles
                    },
                    {
                        type:"color_picker",
                        id:"fmt_arg_suffixFontColour",
                        valueType: "fgCss",
                        defaultColour: defaultFontColour,
                        previewSwatchStyle: {
                            "vertical-align": "bottom"
                        }
                    }
                ],
                getSelectedValues: function() {
                    var selectedValues = {};
                    var fontStyle = _this.fmtArgs.suffixFontStyle;
                    var fontColour = _this.fmtArgs.suffixFontColour;

                    if (typeof fontStyle === "undefined" && typeof fontColour === "undefined") {
                        fontStyle = _this.font_style;
                        fontColour = _this.font_colour;
                    }

                    if (fontStyle) {
                        selectedValues["fmt_arg_suffixFontStyle"] = {
                            selectedValue: fontStyle
                        };
                    }

                    if (fontColour && fontColour !== defaultFontColour["fgCss"]) {
                        selectedValues["fmt_arg_suffixFontColour"] = {
                            selectedValue: fontColour
                        };
                    }

                    return selectedValues;
                },
                displayWhen: {
                    fmt: Props.Defaults.prefixSuffixFormats
                },
                flow: true
            });
        }
    },

    addMakeDstRootProperty: function(model, propertyCtx) {
        var _this = this;
        var isRoot = this.isDstRoot;
        model.push({
            type: "checkboxes",
            id: "isDstRoot",
            group: "dst-root",
            ignoreUpdates: true,
            label: "Actions",
            help: {
                link: "klips/independent-data"
            },
            options: [
                {
                    value: true,
                    label: "Treat as independent data",
                    checked: isRoot
                }
            ],
            onChange: function() {
                page.invokeAction("make_dst_root", { cx:_this, newValue: !isRoot });
            },
            disabled: (propertyCtx && propertyCtx.disabled)
        });
    },

    addGroupProperty: function(model, propertyCtx) {
        var _this = this;
        var groupInfo = this.getGroupInfo();

        model.push({
            type: "checkboxes",
            id: "group",
            group: "dst-group",
            label: "Group",
            options: [
                {
                    value: true,
                    label: "Group repeating labels",
                    checked: groupInfo.isGroupedByThisComponent
                }
            ],
            onChange: function() {
                page.invokeAction("group_by_component", { cx:_this });
            },
            disabled: (propertyCtx && propertyCtx.disabled) || groupInfo.disableGroup
        });
    },

    addAggregateProperties: function(model, propertyCtx) {
        var _this = this;
        var aggregateInfo = this.getAggregateInfo();
        var dst = this.getAvailableDst();

        model.push({
            type: "checkboxes",
            id: "aggregate",
            group: "dst-group",
            label: "Aggregation",
            options: [
                {
                    value: true,
                    label: "Show as aggregated value",
                    checked: aggregateInfo.isAggregated
                }
            ],
            onChange: function() {
                page.invokeAction("aggregate_component", { cx:_this });
            },
            disabled: (propertyCtx && propertyCtx.disabled)
        });

        if (dst.aggregation) {
            model.push({
                type: "select",
                id: "aggregation",
                group: "dst-group",
                options: this.getAggregationsForProperty(["join"]),
                selectedValue: this.getAggregation(),
                displayWhen: {
                    aggregate: true
                },
                onChange: function(changeEvt) {
                    page.invokeAction("set_aggregation", { cx:_this, aggregation:changeEvt.value });
                },
                disabled: propertyCtx && propertyCtx.disabled
            })
        }
    },

    getAggregationsForProperty : function(excludedAggregations) {
        var result;
        var aggregations = this.getAvailableAggregations().slice();
        if(this.role=="region_id") {
          result = this.getNonDefaultAggregationsByRole(this.role);
        } else {
          result = $.grep(aggregations, function (n, i) {
            // if it's not in the exclusion list, return true and include in the result.
            return $.inArray(n.value, excludedAggregations) == -1;
          });

        }
        return result;
    },

    addAggregationProperty: function(model, propertyCtx) {
        var _this = this;
        model.push({
            type: "select",
            id: "aggregation",
            group: "dst-group",
            label: "Aggregation",
            options: this.getAggregationsForProperty(["join"]),
            selectedValue: this.getAggregation(),
            onChange: function(changeEvt) {
                page.invokeAction("set_aggregation", { cx:_this, aggregation:changeEvt.value });
            },
            disabled: propertyCtx && propertyCtx.disabled
        });
    },

    addSortProperty: function (model, propertyCtx) {
        var sortOptions = this.getAvailableSortOptions();
        var options = [];
        var sortInfo = this.getSortInfo();
        var appliedSort = (sortInfo.isSortedByThisComponent ? sortInfo.sortDirection : 0);
        var _this = this;

        if (sortOptions) {
            options = sortOptions.map(function (option) {
                return {value: option.sortDirection, label: option.text}
            });
        }

        model.push({
            type: "select",
            id: "dst-sort",
            group: "dst-sort",
            label: "Sort",
            options: options,
            selectedValue: appliedSort,
            onChange: function (changeEvt) {
                page.invokeAction("sort_by_component", {cx: _this, sortDirection: parseInt(changeEvt.value)});
            },
            disabled: propertyCtx && propertyCtx.disabled
        });
    },

    addDataSlotProperty: function(model, configProps) {
        var _this = this;
        var defaultProps = {
            type: "button",
            id: "add_data_slot",
            label: "Hidden Data",
            group: "data_slot",
            text: "Add Hidden Data",
            onClick: function () {
                page.invokeAction("add_component", {parent: _this, type: "data_slot", role: "data"}, {});
            },
            help: {
                link: "klips/hidden-data"
            }
        };
        $.extend(defaultProps,configProps);
        model.push(defaultProps);
    },

    addFilterProperties: function(model, propertyCtx) {
        var isFilteredByThisComponent = !!this.getFilterOrTopBottomOp();

        model.push({
            type: "button",
            id: "filter",
            group: "dst-filter",
            label: "Filter",
            text: (isFilteredByThisComponent ? "Edit" : "Add") + " Filter...",
            css: "dst-filter",
            onClick: function () {
                page.invokeAction("filter_by_component", {cx: this});
            }.bind(this),
            disabled: propertyCtx && propertyCtx.disabled
        });

        if (isFilteredByThisComponent) {
            model.push({
                type: "button",
                id: "remove_filter",
                group: "dst-filter",
                text: "Clear Filter",
                css: "dst-filter",
                onClick: function() {
                    page.invokeAction("remove_filter", { cx:this });
                }.bind(this),
                flow: true,
                disabled: propertyCtx && propertyCtx.disabled
            });
        }
    },

    /**
     * returns an array of property groups that determines the order of properties for this component
     * any property groups found when rendering the property model will be placed at the end of the property sheet
     * in the order that they are found
     *
     * schema = [ "fmt", "display" ]
     *
     */
    getPropertyModelGroups : function() {
        return [];
    },


    /**
     * when a component is displayed in the workspace it can execute
     * an additional initialization phase where it can expose editing
     * controls, change its appearance, add event listeners, etc.
     * @param workspace
     */
    initializeForWorkspace : function( workspace ) {
        // subclasses should implement as needed
    },


    displayWorkspaceControls : function( selected ) {

    },

    /**
     * returns a schema that describes the controls for this component. is used
     * by the property editor
     *
     * schema = [
     *    { name:"" ,
     *      controls:[[ {type:"add", actionId:"", actionArgs:""},
     *                  {type:"remove", actionId:"", actionArgs:""} ]] }
     * ]
     *
     *
     */
    getWorkspaceControlModel : function() {
        return [];
    },

    addDataSlotControl : function(model) {
        model.push({
            name: "Hidden Data",
            controls: [
                [{
                        type: "add",
                        actionId: "add_component",
                        actionArgs: {parent: this, type: "data_slot", role: "data"}
                },
                {type: "remove", disabled: true}]
            ]
        });
    },


    getContextMenuModel : function(ctx, menuConfig) {
        var model = [];
        var clipboardContents = $.jStorage.get("component_clipboard");

        this.addRenameContextMenuItem(model);

        if (ctx && ctx.isRoot) {
            model.push(
                { id:"separator-cut", separator:true },
                { id:"menuitem-cut", text:"Cut", actionId:"copy_cut_component", actionArgs:{cx:this, remove:true} },
                { id:"menuitem-copy", text:"Copy", actionId:"copy_cut_component", actionArgs:{cx:this} },
                { id:"menuitem-paste", text:"Paste", disabled:!clipboardContents, actionId:"paste_component", actionArgs:{cx:this} }
            );
        }

        this.addRemoveContextMenuItem(model);

        if (this.variableSubComponents) {
            this.variableSubComponents.forEach(function(subComponent) {
                var labelId = encodeForId(subComponent.label);

                model.push({ id:"separator-" + labelId, separator:true });

                this.addAddContextMenuItem(model, subComponent);
                this.addRemoveSubComponentContextMenuItem(model, subComponent);
            }.bind(this));
        }

        this.addMoveContextMenuItem(model);

        if (this.canHaveDataSlots) {
            this.addDataSlotContextMenuItem(model);
        }

        this.addDstContextMenuItems(ctx, menuConfig, model);

        return model;
    },

    addDataSlotContextMenuItem : function(model, customParent) {
        model.push({ id:"separator-add-data-slot", separator:true });
        model.push({
                id: "menuitem-add-data-slot",
                text: "Add Hidden Data",
                actionId: "add_component",
                actionArgs: {parent: customParent || this, type: "data_slot", role: "data"}
            }
        );
    },

    removeDataSlotContextMenuItem : function(model) {
        return _.filter(model, function(menuItem) {
            return (menuItem.id != "separator-add-data-slot" && menuItem.id != "menuitem-add-data-slot");
        })
    },

    addDstContextMenuItems : function (menuCtx, menuConfig, model) {
        var dst;
        var menuItemCtx = {
            disabled: !this.hasFormula()
        };
        var groupBySubComponent;

        if (menuCtx && menuCtx.fromFilterIcon) {
            //remove all other menu items and just show the filter options
            model.splice(0, model.length);
            this.addFilterContextMenuItems(model);
            return;
        }
        if (menuCtx && menuCtx.fromSortIcon && menuConfig) {
            //remove all other menu items and just show the sort directions
            model.splice(0, model.length);
            this.addSortContextMenuItem(model, true);
            menuConfig.showSelectionIcon = true;
            return;
        }

        dst = this.getAvailableDst();

        if (dst.group) {
            if (menuCtx && menuCtx.groupBySubComponent) {
                groupBySubComponent = menuCtx.groupBySubComponent;
                menuItemCtx.disabled = !groupBySubComponent.hasFormula();
                groupBySubComponent.addGroupContextMenuItem(model, menuItemCtx);
            } else {
                this.addGroupContextMenuItem(model, menuItemCtx);
            }
        }

        if (dst.aggregate) this.addAggregateContextMenuItem(model, menuItemCtx);

        if (dst.aggregation) {
            if (menuCtx && menuCtx.showSubComponentAggregation) {
                this.addSubComponentAggregationContextMenuItem(model);
            } else {
                this.addAggregationContextMenuItem(model, false, menuItemCtx);
            }
        }

        if (dst.sort) this.addSortContextMenuItem(model, false, menuItemCtx);

        if (dst.filter) {
            if (menuCtx && menuCtx.showSubComponentFilter) {
                this.addSubComponentFilterContextMenuItem(model);
            } else {
                this.addFilterContextMenuItems(model, false, menuItemCtx);
            }
        }
    },

    addRenameContextMenuItem : function (model) {
        model.push(
            { id:"menuitem-rename",
              text:"Rename...",
              actionId:"rename_component",
              actionArgs:{cx:this},
              treeOnly:  true },
            { id:"separator-rename", separator:true,treeOnly:  true }
        );
    },

    addMoveContextMenuItem : function (model) {
        var moveData = this.checkMovable(this);

        if (moveData.movable) {
            model.push(
                { id:"separator-move", separator:true },
                {
                    id: "menuitem-moveup",
                    text: "Move " + moveData.label + " Up",
                    disabled: !moveData.up,
                    actionId:"chart_move_series",
                    actionArgs: {
                        cx: this,
                        direction: "up"
                    }
                },
                {
                    id: "menuitem-movedown",
                    text: "Move " + moveData.label + " Down",
                    disabled: !moveData.down,
                    actionId: "chart_move_series",
                    actionArgs: {
                        cx: this,
                        direction: "down"
                    }
                }
            );
        }
    },

    isVCFeatureOn: function() {
        return KF.company.isVCFeatureOn();
    },

    getAvailableDst: function() {
        if (this.isVCFeatureOn()) {
            return this.getSupportedDst();
        } else {
            return {};
        }
    },

    getSupportedDst: function() {
        var dst;
        var supportedRoles  = ["region_id", "marker_name", "data_labels" ,"labels", "axis_x", "series", "tile_regions", "tile_markers"];

        if(this.role && this._dst && supportedRoles.indexOf(this.role)>-1){
            dst=this._dst;
            dst=this.updateSupportedDst(dst);

           return {
            group: dst.group,
            aggregation: dst.aggregation,
            aggregate: this.canAggregate,
            sort: this.canSort,
            filter: this.canFilter,
           makeDstRoot: this.canMakeDstRoot
          };
        } else{
            return {
                group: this.canGroup,
                aggregation: this.canSetAggregation,
                aggregate: this.canAggregate,
                sort: this.canSort,
                filter: this.canFilter,
                makeDstRoot: this.canMakeDstRoot
            };
        }

    },

    updateSupportedDst:function(dst){
        var isNumberColumn = (this.getVirtualColumnType() == DX.ModelType.NUMBER);
        var dstHelper = this.getDstHelper();

        dst.group = !isNumberColumn && !(dstHelper && dstHelper.areVCValuesAggregated(this.getVirtualColumnId()));
        dst.aggregation = !dst.group;

        return dst;
    },

    /**
     * @returns {Components formatted as Image URL or Hyperlink have
     *           the First(Default) Last as aggregation menu}
     */

    getAvailableAggregations : function (){
        var modelType= this.getVirtualColumnType();
        var componentFormat = this.fmt;
        var aggregations = {};

        // Component formatted as Hyperlink or Image would have
        // First(Default) and Last as their default aggregations.
        var overriddenAggregations = [
            {value: "first", label: "First", "default": true},
            {value: "last", label: "Last"}
        ];

        switch (componentFormat) {
            case "hyp":
            case "img":
                aggregations = overriddenAggregations;
                break;
            default:
                aggregations = Props.Defaults.dstAggregations[modelType];
        }

        return aggregations;

    },

    getAvailableSortOptions: function () {
        var modelType = this.getResolvedType();
        var SUBMENU_ITEMS_FOR_SORT_BY_TYPE = ContextMenu.getSortSubmenuItems();
        return SUBMENU_ITEMS_FOR_SORT_BY_TYPE[modelType];
    },

    addAddContextMenuItem: function(model, subComponent) {
        var labelId = encodeForId(subComponent.label);

        model.push({
            id: "menuitem-add_" + labelId,
            text: "Add " + subComponent.label,
            actionId: "add_component",
            actionArgs: {
                parent: this,
                type: subComponent.type,
                role: subComponent.role
            }
        });
    },

    addRemoveSubComponentContextMenuItem: function(model, subComponent) {
        var labelId = encodeForId(subComponent.label);
        var removeSubComponent = { id:"menuitem-remove_" + labelId, text:"Remove " + subComponent.label };

        var subComponents = subComponent.findByRole ?
            this.getChildrenByRole(subComponent.role) :
            this.getChildrenByType(subComponent.type);

        var i, cx;
        var subItems = [];

        if (subComponents.length > 1) {
            for (i = 0; i < subComponents.length; i++) {
                cx = subComponents[i];

                subItems.push({
                    id: "menuitem-" + labelId + "_" + encodeForId(cx.displayName),
                    text: cx.displayName,
                    actionId: "remove_component",
                    actionArgs: {
                        cx: cx
                    }
                });
            }

            removeSubComponent.submenu = {
                width:"150px",
                menuItems: subItems
            };
        } else {
            removeSubComponent.disabled = true;
        }

        model.push(removeSubComponent);
    },

    addGroupContextMenuItem: function(model, menuItemCtx) {
        var groupInfo = this.getGroupInfo();

        model.push(
            { id:"separator-group", separator:true },
            {
                id: "menuitem-group",
                text: (groupInfo.isGroupedByThisComponent ? "Ungroup" : "Group"),
                actionId: "group_by_component",
                actionArgs: {
                    cx: this
                },
                iconCss: (groupInfo.isGroupedByThisComponent ? "icon-ungroup" : "icon-group"),
                disabled: (menuItemCtx && menuItemCtx.disabled) || groupInfo.disableGroup
            }
        );
    },

    addAggregateContextMenuItem: function(model, menuItemCtx) {
        var aggregateInfo = this.getAggregateInfo();

        model.push(
            { id:"separator-component_aggregate", separator:true },
            {
                id: "menuitem-aggregate",
                text: "Show as Aggregated Value",
                actionId: "aggregate_component",
                actionArgs: {
                    cx: this
                },
                iconCss: (aggregateInfo.isAggregated ? "icon-check" : "icon-none"),
                disabled: (menuItemCtx && menuItemCtx.disabled)
            }
        );
    },

    addAggregationContextMenuItem: function (model, showComponentName, menuItemCtx) {
        var aggregationRules = this.getAggregationsForProperty(["join"]);
        var aggregation = this.getAggregation();
        var componentName = this.getReferenceName();
        var menuItemText = showComponentName ? componentName : "Aggregation";
        var previousMenuItem = model.length > 0 ? model[model.length - 1] : false;
        var aggregationDisabled = (menuItemCtx && menuItemCtx.disabled);
        var aggregateInfo;
        var menuItemId = "aggregation" + (showComponentName ? "-" + encodeForId(this.id) : "");

        var menuItems = [];

        aggregationRules.forEach(function (rule) {
            menuItems.push({
                id: "menuitem-" + menuItemId + "_" + rule.value,
                text: rule.label,
                actionId: "set_aggregation",
                actionArgs: {
                    cx: this,
                    aggregation: rule.value
                },
                isSelected: (rule.value == aggregation)
            });
        }.bind(this));

        if (previousMenuItem && previousMenuItem.id == "menuitem-aggregate") {
            aggregateInfo = this.getAggregateInfo();
            aggregationDisabled = aggregationDisabled || !aggregateInfo.isAggregated;
        } else {
            model.push({ id: "separator-" + menuItemId, separator: true });
        }

        model.push(
            {
                id: "menuitem-" + menuItemId,
                text: menuItemText,
                iconCss: !showComponentName ? "icon-aggregation" : "",
                disabled: aggregationDisabled,
                submenu: {
                    width: "200px",
                    showSelectionIcon: true,
                    menuItems: menuItems
                }
            }
        );
    },

    addSubComponentAggregationContextMenuItem: function(model) {
        var subComponents = this.components.filter(function (comp) {
            var dst = comp.getAvailableDst();

            return dst.aggregation;
        });

        var aggMenuItem = [];

        // Iterate through all subComponents
        subComponents.forEach(function (subComponent) {
            var hasFormula = subComponent.hasFormula();

            subComponent.addAggregationContextMenuItem(aggMenuItem, true, {
                disabled: !hasFormula
            });
        });

        if (aggMenuItem.length == 0) {
            return;
        }

        model.push(
            {id: "separator-aggregation", separator: true},
            {
                id: "menuitem-aggregation",
                text: "Aggregation",
                iconCss: "icon-aggregation",
                submenu: {
                    showSelectionIcon: true,
                    menuItems: aggMenuItem
                }
            }
        );
    },

    addSortContextMenuItem: function(model, isSortDetailMenu, menuItemCtx) {
        var modelType = this.getResolvedType();
        var sortInfo = this.getSortInfo();
        var sortDirection = (sortInfo.isSortedByThisComponent ? sortInfo.sortDirection : 0);

        var actionArgsForSort = [
            {cx: this, sortDirection: 0},
            {cx: this, sortDirection: 1},
            {cx: this, sortDirection: 2}
        ];

        var sortItems = this.addSortSubMenuItems(actionArgsForSort, sortDirection);

        var sortMenuIconCss = {};
        sortMenuIconCss[DX.ModelType.NUMBER] = "icon-sort-num-none";
        sortMenuIconCss[DX.ModelType.DATE] = "icon-sort-date-none";
        sortMenuIconCss[DX.ModelType.TEXT] = "icon-sort-text-none";

        if (isSortDetailMenu) {
             sortItems.forEach(function(currentValue) {
                model.push(currentValue);
            });
        } else {
            model.push(
                {id: "separator-component_sort", separator: true},
                {
                    id: "menuitem-sort",
                    text: "Sort",
                    submenu: {
                        width: "200px",
                        showSelectionIcon: true,
                        menuItems: sortItems
                    },
                    iconCss: sortMenuIconCss[modelType],
                    disabled: menuItemCtx && menuItemCtx.disabled
                }
            );
        }
    },

    addSortSubMenuItems:function(cxArgs, sortDirection) {
        var subMenuItemsForSort = this.getAvailableSortOptions();

        var i;
        var subMenuLength = subMenuItemsForSort.length;
        var subMenu = [];

        for (i = 0; i < subMenuLength; i++) {
            subMenuItemsForSort[i].actionId = "sort_by_component";
            if(cxArgs[i] && _.isNumber(cxArgs[i].sortDirection)) {
                subMenuItemsForSort[i].actionArgs = cxArgs[i];
                subMenuItemsForSort[i].isSelected = (sortDirection === cxArgs[i].sortDirection);
            }
            subMenu.push(subMenuItemsForSort[i]);
        }
        return subMenu;
    },

    addFilterContextMenuItems: function(model, showComponentName, menuItemCtx) {
        var isFilteredByThisComponent = !!this.getFilterOrTopBottomOp();

        var compName = this.getReferenceName();
        var menuItemText = showComponentName ? compName : "Filter";
        var menuItemId = "filter" + (showComponentName ? "-" + encodeForId(this.id) : "");

        model.push(
            { id: "separator-" + menuItemId, separator: true },
            {
                id: "menuitem-" + menuItemId,
                text: isFilteredByThisComponent ? "Edit " + menuItemText + "..." : menuItemText + "...",
                actionId: "filter_by_component",
                actionArgs: { cx: this },
                iconCss: !showComponentName ? "icon-filter" : "",
                disabled: menuItemCtx && menuItemCtx.disabled
            }
        );

        if (isFilteredByThisComponent) {
            model.push(
                {
                    id: "menuitem-remove_" + menuItemId,
                    text: "Clear Filter",
                    actionId: "remove_filter",
                    actionArgs: {
                        cx: this
                    },
                    iconCss: ""
                }
            );
        }
    },

    addSubComponentFilterContextMenuItem: function(model) {
        var subComponents = this.components.filter(function (comp) {
            var dst = comp.getAvailableDst();

            return dst.filter;
        });

        var filterMenuItem = [];

        // Iterate through all subComponents
        subComponents.forEach(function (subComponent) {
            var hasFormula = subComponent.hasFormula();

            subComponent.addFilterContextMenuItems(filterMenuItem, true, {
                disabled: !hasFormula
            });
        });

        if (filterMenuItem.length == 0) {
            return;
        }

        model.push(
            {id: "separator-filter", separator: true},
            {
                id: "menuitem-filter",
                text: "Filter",
                submenu: {
                    showSelectionIcon: true,
                    menuItems: filterMenuItem
                },
                iconCss: "icon-filter"
            }
        );
    },

    addRemoveContextMenuItem : function(model){
        if (this.checkDeletable({role:this.role})) {
            model.push(
                { id:"menuitem-remove", text:"Remove", actionId:"remove_component", actionArgs:{cx:this} }
            );
        }
    },


    /**
     * called by the workspace when this component is selected
     * @param selectionState
     */
    setSelectedInWorkspace : function( selectionState ) {
        if (this.el) {
            if ( selectionState ) this.el.addClass("selected-component");
            else this.el.removeClass("selected-component");
        }

        this.selected = selectionState;

        this.setSelectedRootInWorkspace(selectionState);
    },

    setSelectedRootInWorkspace : function( selectionState ) {
        if (!this.parent.isKlip && !this.parent.isPanel) {
            var root = this.getRootComponent();

            if ( selectionState ) {
                root.el.addClass("selected-component-root");
            } else {
                root.el.removeClass("selected-component-root");
            }
        }
    },


    /**
     * called when a a component is being removed/deleted.
     * component implementations should put cleanup code here, and
     * make sure to call their superclass' destroy method AFTER doing their
     * own cleanup.  Why not first?  Because Component.Base.destroy() will remove 'this.el'
     * from the DOM
     */
    destroy : function() {
        var len = this.components.length;
        while(--len > -1)
            this.components[len].destroy();

        KlipFactory.instance.destroyComponent(this);
        if (this.el) this.el.remove();
    },


    /**
     * hook called after a component has been created through the factory.
     */
    afterFactory : function() {
        this.applyConditions();
    },

    /**
     * called to see an upgrade is needed and notify the parent Klip
     *
     * each component will implement as necessary when there are changes made that
     * will break the component (won't display properly anymore), such as changing
     * the sub-component structure
     *
     * it should notify the parent Klip with an appropriate message for display
     * on the dashboard
     */
    checkForUpgrade : function() {

    },


    /**
     * recursively collects datasources from this component and its children
     */
    getDatasources : function( collectIn ){
        var i;
        if ( !collectIn ) collectIn = [];

        // if we have formulas, collect all datasources ids
        // ------------------------------------------------
        if ( this.formulas )
            for (i = 0; i < this.formulas.length; i++) {
                var f = this.formulas[i];
                for (var j = 0; j < f.datasources.length; j++) {
                    var ds = f.datasources[j];
                    if ( $.inArray( ds, collectIn ) == -1 )	collectIn.push( ds );
                }
            }
        // if we have components, iterate through them and call recursively
        // ----------------------------------------------------------------
        if ( this.components ) {
            for (i = 0; i < this.components.length; i++) {
                this.components[i].getDatasources(collectIn);
            }
        }
        return collectIn;
    },

    /**
     * Recursively collect all the formulas of this component
     * @param collectIn
     */
    getFormulas : function( collectIn ){
        var i;
        if ( !collectIn ) collectIn = [];

        if ( this.formulas ) {
            for( i in this.formulas ) {
                var f = this.formulas[i];
                collectIn.push (f);
            }
        }

        if ( this.components ) {
            for( i in this.components ) {
                this.components[i].getFormulas(collectIn);
            }
        }
        return collectIn;
    },

    getFormula : function() {
        //Only data components have formulas
        return null;
    },

    sendQueryRequestInsteadOfWatch : function() {
        return false;
    },

    usesVariable : function(n) {
        var allFormulas = this.getFormulas();

        for (var i = 0; i < allFormulas.length; i++) {
            if (allFormulas[i].usesVariable(n)) {
                return true;
            }
        }

        return false;
    },


    getChildrenByType : function(t) {
        return this.getChildrenByProperty("factory_id", t);
    },

    getChildrenByRole : function(role) {
        return this.getChildrenByProperty("role", role);
    },

    getChildrenByProperty : function(p, v) {
        return this.getChildrenByPropertyFilter(p, function(value) {
            return value == v;
        });
    },

    getChildrenByPropertyFilter : function(p, filter) {
        var cxLen = this.components.length;
        var results = [];
        for(var i = 0 ; i < cxLen; i++ ) {
            var cx = this.components[i];
            if (filter(cx[p])) results.push(cx);
        }
        return results;
    },


    getChildByType : function(t) {
        return this.getChildByProperty("factory_id",t);
    },

    getChildById : function(id) {
        return this.getChildByProperty("id",id);
    },

    getDescendantByVCId : function(virtualColumnId) {
        var desc = null;
        eachComponent(this, function(comp) {
            if (convertToVirtualColumnId(comp.id) == virtualColumnId) {
                desc = comp;
                return true;
            }
        });
        return desc;
    },

    getDescendantOrSelfByVCId : function(virtualColumnId) {
        if (convertToVirtualColumnId(this.id) === virtualColumnId) {
            return this;
        }
        return this.getDescendantByVCId(virtualColumnId);
    },

    getChildByRole : function(role) {
        return this.getChildByProperty("role",role);
    },

    getChildByProperty : function(p,v,recursive) {
        var cxLen = this.components.length;
        var i;
        var cx;
        if (!recursive) {
            for(i = 0 ; i < cxLen; i++ ) {
                cx = this.components[i];
                if ( cx[p] == v) return cx;
            }
        } else {
            // although both recurisve and non-recurive searches could be grouped into on block of code,
            // there would be additional overhead imposed on non-recursive searches (creating a stack).  since
            // most invocations are of the recursive type, it is probably best to keep different optimized implementations
            //
            var stack = [];
            for (i = 0; i < cxLen; i++) {
                stack.push(this.components[i]);
            }

            while (stack.length != 0) {
                cx = stack.pop();
                var cxChildren = cx.components;
                if (cx[p] == v) return cx;
                if (cxChildren && cxChildren.length > 0) {
                    for (var j = 0 ; j < cxChildren.length; j++ ) {
                        stack.push(cxChildren[j]);
                    }
                }
            }
            stack = null;
        }


        return false;
    },


    /**
     * tells this component what its width and height should be.  if a change is detected than onReize is triggered.
     * @param w
     * @param h
     */
    setDimensions : function(w, h) {
        var cxWidthMBP = this.el.outerWidth(true) - this.el.width();
        var widthNoMBP = w - cxWidthMBP;

        if ( h == undefined ) h = this.dimensions.h;

        var dimChanged = ( this.dimensions.w != widthNoMBP || this.dimensions.h != h );
        this.dimensions = { w:widthNoMBP, h:h };

        if ( dimChanged ) {
            this.onResize();
        }
    },

    /**
     * Set the width of a component in a layout grid
     * @param w
     */
    setWidthFromCell : function(w) {
        var cxWidthMBP = this.el.outerWidth(true) - this.el.width();
        this.dimensions.w = w - cxWidthMBP;

        if (!this.el) return;

        this.el.width(this.dimensions.w);

        this.invalidateAndQueue();
    },

    setHeight : function(h) {
        this.dimensions.h = h;
        this.onResize();
    },


    onResize : function() {
        if ( !this.el ) return;

        this.el.width(this.dimensions.w);

        if (this.dimensions.h) this.el.height(this.dimensions.h);
    },

    reRenderWhenVisible: function() {

    },


    /**
     * if a child component is update
     */
    onChildrenUpdated : function(childUpdated) {

    },

    /**
     * When this component has been added to a Klip or another component;
     * will have access to the klip object through the parent hierarchy
     */
    onKlipAssignment : function() {
        var ar = this.components;
        var fms = this.formulas;

        if (fms) {
            for (var count = 0; count < fms.length; count++) {
                if (fms[count] instanceof DX.Formula) {
                    fms[count].onKlipAssigned();
                }
            }
        }

        if (ar) {
            for (var i = 0; i < ar.length; ++i) {
                ar[i].onKlipAssignment();
            }
        }
    },

    /**
     * When this component has been removed from a Klip
     */
    onKlipRemoval : function() {
        var ar = this.components;
        if (ar) {
            for (var i = 0; i < ar.length; ++i) {
                ar[i].onKlipRemoval();
            }
        }
    },

    /**
     * helper function indicating whether a series component can be moved either up and/or down within it's siblings
     * @param seriesComponent The series component
     * @return Object with three boolean properties:
     *      movable: Indicates if component is eligible at all to be reordered (ie. whether it is a series component)
     *      up: Indicates if the component can be moved up (ie. that it is not the first of all it's siblings)
     *      down: Indicates if the component can be moved down (ie. that it is not the last of all it's siblings)
     */
    checkMovable : function(seriesComponent) {
        var retVal = {
            movable: false,
            up: true,
            down: true
        };

        var chartSeries;
        var seriesId;
        var firstElementId;
        var lastElementId;

        if (seriesComponent.isMovable) {
            chartSeries = seriesComponent.parent.getChildrenByType(seriesComponent.factory_id);
            retVal.label = seriesComponent.isMovable.label || "Series";

            if(chartSeries.length == 1){
                retVal.movable = true;
                retVal.up = false;
                retVal.down = false;
            } else {
                seriesId = seriesComponent.id;
                firstElementId = chartSeries[0].id;
                lastElementId = chartSeries[chartSeries.length - 1].id;

                if (seriesId === firstElementId) {
                    retVal.up = false;
                }
                if (seriesId === lastElementId) {
                    retVal.down = false;
                }

                retVal.movable = retVal.up || retVal.down;
            }
        }

        return retVal;
    },

    checkDeletable : function(ctx) {
        return this.isDeletable;
    },


    /**
     * check to see if this component is invalid.   if 'checkChildren' is true this method
     * will return true if any of the immediate child components are invalid
     * @param checkChildren
     * @return {Boolean} returns true if the component is invalid
     */
    checkInvalid : function(checkChildren) {
        if( this.isInvalid ) return true;
        if( checkChildren ){
            var cxlen = this.components.length;
            while(--cxlen > -1){
                if ( this.components[cxlen].isInvalid )
                    return true;
            }
        }
        return false;
    },

    /**
     * sets the isInvalid flag on this componentn
     * @param state
     * @param applyToChildren  also apply the state to all immediate children
     */
    setInvalid : function( state , applyToChildren ){
        this.isInvalid = state;
        if( applyToChildren ){
            var cxlen = this.components.length;
            while(--cxlen > -1){
                this.components[cxlen].isInvalid = state;
            }
        }
    },


    /**
     * shorthand for setInvalid(true)
     */
    invalidate : function(){
        this.setInvalid(true);
    },

    invalidateAndQueue : function() {
        this.invalidate();
        updateManager.queue(this);
    },


    onEvent : function(evt) {
        if ( evt.id == "cx-data-changed" ) {
            this.applyConditions();
        }
    },


    /**
     * publish an event to this component's Klip.  The event will then be sent to all components where they can
     * be handled via handleEvent().   This method exists to give a component an event-oriented way to communicate
     * with its Klip, component peers, siblings, etc.
     * @param {object} evt
     * @param {boolean} async
     */
    publishKlipEvent : function(evt,async) {
        var k = this.getKlip();
        if (!k) {
            // queue the event for delivery once assigned to a klip..?
        } else {
            k.handleEvent(evt,async);
        }
    },


    applyConditions : function(clearReactions) {
        if (clearReactions) this.reactions = false;

        CR.checkAllAndApply(this.conditions,this);
    },


    /**
     * components which can have 'reations' in the indicator system can declare which reactions
     * they support.
     * @return {Array} a list of reaction-ids supported
     */
    getSupportedReactions : function() {
        return false;
    },

    /**
     * adds a list of reaction object to this component which contain the following properties:
     *   - mask : an array of booleans which specifies where the reaction should be applied.  if a CX uses scalar data, this should be an array with
     *              one element.   if it uses vector data, then the array should contain N booleans
     *   - reaction : the reaction config object which specifies the type of reaction along with the appropriate configuration information
     */
    setReactions : function(rs) {
        if ( _.isEqual(rs, this.reactions)) return;

        this.invalidate();
        this.reactions = rs;
        updateManager.queue(this,{r:"set-reactions"});
    },


    /**
    * helper function which will traverse all reactions on this component and invoke this.handleReactions()
    * when it encounters mask indexes that are 'true'.  Since each component will want to handle reactions
    * in their own way, they will typically call this method during updates, passing in a object which will
    * store the reaction data.
    * @param ctx
    */
    collectReactions : function( ctx ) {
        if ( !this.reactions || this.reactions.length == 0 ) return;

        var collectedReactions = {},
            crReactions,
            hasReactions;

        // collect all the reactions for each index first
        _.each(this.reactions, function(crSet) {
            crReactions = crSet.reactions;
            hasReactions = crReactions && crReactions.length > 0;

            _.each(crSet.mask, function(mask, idx, list) {
                if (mask) {
                    if (hasReactions) {
                        if (!collectedReactions[idx]) collectedReactions[idx] = [];

                        _.each(crReactions, function(reaction) {
                            // if there's only one mask value for a reaction,
                            // it can be applied to all the data points
                            reaction.isScalar = (list.length == 1 ? true : undefined);

                            collectedReactions[idx].push(reaction);
                        });
                    }
                }
            });
        });

        // then handle them
        if (!_.isEmpty(collectedReactions)) {
            var _this = this;
            _.each(collectedReactions, function(reactions, idx) {
                _.each(reactions, function(r) {
                    if (r.isScalar) {
                        var rTargets = _this.getReactionTargets();
                        if (rTargets) {
                            // apply scalar reactions to all targets
                            for (var i = 0; i < rTargets.length; i++) {
                                _this.handleReaction(i, r, ctx);
                            }
                        } else {
                            _this.handleReaction(idx, r, ctx);
                        }
                    } else {
                        // other reactions only apply to a single index
                        _this.handleReaction(idx, r, ctx);
                    }
                });
            });
        }
    },

    /**
     * components which support reactions can override this method to provide an easy way to aggregate reaction
     * information.
     * @param idx - an index where the reaction should be applied
     * @param reaction - a reaction to apply
     * @param ctx - a context object in which to store information -- this is the same ctx object passed to collectReactions
     */
    handleReaction : function(idx, reaction, ctx) {

    },

    /**
     * components which support reactions can override this method to provide
     * the targets for a scalar reaction
     */
    getReactionTargets : function() {
        return (this.data ? this.getFilteredData(0) : false);
    },


    /**
     * if a component requires other script/css assets to be loaded it should return
     * an array of scripts.
     */
    getDependencies:function () {

    },


    /**
     *
     * @param enable
     */
    enableDrilldown : function(enable) {
        this.drilldownEnabled = enable;

        if (this.drilldownEnabled) {
            if (!this.drilldownConfig) this.drilldownConfig = [];
            if (!this.drilldownStack) this.drilldownStack = [];
        }
    },

    /**
     * Return the model of the columns that can be grouped and aggregated
     *
     * @return {Array}
     */
    getDrilldownModel : function() {
        return [];
    },

    /**
     * Return the drill down configuration
     *
     * @return {Boolean}
     */
    getDrilldownConfig : function() {
        return this.drilldownConfig;
    },

    /**
     * clean up the drilldownConfig to remove levels and display configurations
     * that use non-existant components
     *
     * @param ctx - context object
     *        ctx.cxRemoved - the component that was removed; if false, a complete clean will be done
     */
    cleanDrilldownConfig : function(ctx) {
        var cx;
        var cxRemoved = (ctx && ctx.cxRemoved ? ctx.cxRemoved : false);
        var i = 0;

        while (i < this.drilldownConfig.length) {
            var cfg = this.drilldownConfig[i];

            cx = true;
            if (cxRemoved) {
                if (cxRemoved.id == cfg.groupby) {
                    cx = false;
                }
            } else {
                if (cfg.groupby != "") {
                    cx = this.getChildById(cfg.groupby);
                }
            }

            if (!cx) {
                this.drilldownConfig.splice(i, 1);
                continue;
            }

            var displayCfgs = cfg.display;
            var j = 0;
            while (j < displayCfgs.length) {
                var dCfg = displayCfgs[j];

                cx = true;
                if (cxRemoved) {
                    if (cxRemoved.id == dCfg.id) {
                        cx = false;
                    }
                } else {
                    cx = this.getChildById(dCfg.id);
                }

                if (!cx) {
                    displayCfgs.splice(j, 1);
                    continue;
                }

                j++;
            }

            i++;
        }
    },

    invalidateDrilldown : function() {
        this.isInvalidDrilldown = true;
    },

    /**
     * Push a drill down level onto the stack (going deeper into the drill down levels)
     *
     * @param ctx
     */
    pushDrilldownLevel : function(ctx) {
        this.drilldownStack.push(ctx);
        this.invalidateDrilldown();
        updateManager.queue(this);
    },

    /**
     * Pop a drill down level from the stack (moving up the drill down levels)
     */
    popDrilldownLevel : function() {
        this.drilldownStack.pop();
        this.invalidateDrilldown();
        updateManager.queue(this);
    },

    getVirtualColumnType: function() {
        return FMT.toModelType(this.fmt);
    },

    getVirtualColumnId: function() {
        return convertToVirtualColumnId(this.id);
    },

    getVirtualColumnInfo: function() {
        var formulaIdx = 0;
        var fm, finalFormula, resolvedFormula;
        var fmtArgsCopy;

        if (!this.hasFormula()) return;

        fm = this.getFormula(formulaIdx);

        if (this.fmtArgs) {
            // Make a copy and clean the previous result metadata before constructing new DST Json spec.
            fmtArgsCopy = JSON.parse(JSON.stringify(this.fmtArgs));
            if (fmtArgsCopy.resultMetadata) {
                delete fmtArgsCopy.resultMetadata;
            }
        }

        // If we've resolved this formula previously, we will return the resolved one directly for performance reason.
        // The resolved formula will be cleared when setFormula is called.
        resolvedFormula = this.getResolvedFormula(formulaIdx);
        if (resolvedFormula) {
            finalFormula = resolvedFormula;
        } else {
            if (this.usePostDSTReferences()) {
                this._dependenciesFromSameRoot = {};
                this._allDependencies = {};
                finalFormula = DX.Formula.putVCReferencesAndReplaceFormulaReferences(fm.formula, this);
            } else {
                finalFormula = DX.Formula.replaceFormulaReferences(fm.formula, this.getKlip(), true);
            }
            // Cache the resolve formula for later use.
            this.setResolvedFormula(formulaIdx, finalFormula);
        }

        return {
            formula: finalFormula,
            id: this.getVirtualColumnId(),
            fmt: this.fmt,
            fmtArgs: fmtArgsCopy,
            autoFmt: this.isAutoFormatOn(),
            name : this.getReferenceName(),
            type: this.getVirtualColumnType()
        };
    },

    /**
     * Returns the current aggregation (either based on auto data format detection or user choice)
     * @returns {String}
     */
    getResolvedAggregate: function () {
        var currentAgg, defaultAgg;
        var dst = this.getDstContext();
        if (dst && dst.aggs) {
            currentAgg = dst.aggs[this.getVirtualColumnId()];
            if (currentAgg == "auto") {
                if (this.getAutoFmt()) {
                    defaultAgg = this.getAutoFmt().defaultAggregate;
                    return defaultAgg.toLowerCase();
                }
            } else {
                return currentAgg;
            }
        }
        return undefined;
    },

    /**
     * getResolvedType returns a type of numeric (2) for text columns which have been aggregated using Count or Count distinct due to a previous grouping or aggregation
     * Note: we need to maintain the underlying data type as is
     * @returns {number}
     */
    getResolvedType: function () {
        var sourceType = this.getVirtualColumnType();
        var currentConfig,resolvedType,resolvedAgg;
        var dstHelper = this.getDstHelper();

        resolvedType = sourceType;
        //Underlying type of text or date
        if (sourceType != DX.ModelType.NUMBER) {
            //checking if on dashboard since editor displays the values without drilldown being applied to it.
            if (!isWorkspace() && this.parent.drilldownEnabled && this.parent.drilldownStack && this.parent.drilldownStack.length > 0) {
                currentConfig = this.parent.drilldownConfig[this.parent.drilldownStack.length - 1];
                if (currentConfig && currentConfig.groupby == this.id) {
                    resolvedType = sourceType;
                } else {
                    resolvedAgg = this.getAutoFmt() && this.getAutoFmt().defaultAggregate.toLowerCase();
                    if (resolvedAgg && Props.Defaults.dstAggregationsMap[sourceType] && Props.Defaults.dstAggregationsMap[sourceType][resolvedAgg]) {
                        resolvedType = Props.Defaults.dstAggregationsMap[sourceType][resolvedAgg].resolvedType;
                    } else if(this.aggMethod && (this.aggMethod.toLowerCase() == "count" || this.aggMethod.toLowerCase() == "distinct")){
                        resolvedType = DX.ModelType.NUMBER;
                    }
                }
            }

            if (dstHelper && dstHelper.areVCValuesAggregated(this.getVirtualColumnId())) {
                //What is the current aggregation (either based on auto data format detection or user choice)
                resolvedAgg = this.getResolvedAggregate();
                if (resolvedAgg && Props.Defaults.dstAggregationsMap[sourceType] && Props.Defaults.dstAggregationsMap[sourceType][resolvedAgg]) {
                    resolvedType = Props.Defaults.dstAggregationsMap[sourceType][resolvedAgg].resolvedType;
                }
            }
        }
        return resolvedType;
    },

    processDSTContext : function() {
        PubSub.publish("dst-context-updated", { dstRootComponent: this.getDstRootComponent() });
        this.updateDstRootFormulas();
    },

    updateDstRootFormulas: function() {
        var klip = this.getKlip();
        var dstRootComponent = this.getDstRootComponent();
        var initialFormulas = dstRootComponent.getFormulas();
        var watchFormulas = initialFormulas;

        if (klip) {
            //Also get the formulas that refer to this DST root component, since they may have been
            //impacted by applying the DST to a formula change
            if (this.usePostDSTReferences()) {
              watchFormulas = DX.Formula.getFormulasToRewatch(initialFormulas);
            }

            DX.Manager.instance.watchFormulas([{kid: klip.id, fms: watchFormulas}]);
        }
    },

    getComponentsReferencingThisComponent: function() {

        var klip = this.getKlip();

        if (klip) { //Need this check, since the formulas can be evaluated before the component is added to the klip
            return klip.getComponentDependers(this);
        } else {
            return [];
        }
    },

    clearDependentFormulasCache: function(){
        this.dependentFormulas = null;
        if (this.components) {
            this.components.forEach(function (child) {
                child.clearDependentFormulasCache();
            });
        }
    },

    getDependentFormulas: function() {
        var _this = this;
        var processed = {};
        var dependers;
        var formula;

        if (this.dependentFormulas === null) {
            //From the depender map, get all components that depend on this component (i.e. have formulas that reference this component)
            dependers = this.getComponentsReferencingThisComponent();

            this.dependentFormulas = [];
            if (dependers) {
                //For each depender that has a formula (ex, table column) add that formula to the dependent formulas
                Object.keys(dependers).forEach(function (key) {
                    if (key != _this.id) {
                        if (dependers[key].formulas.length > 0) {
                            formula = dependers[key].getFormula();
                            if (formula && !processed[formula.id]) {
                                processed[formula.id] = true;
                                _this.dependentFormulas.push(formula);
                            }
                        }
                        //Add any dependent formulas on this component as well
                        dependers[key].getDependentFormulas().forEach(function (formula) {
                            if (!processed[formula.id]) {
                                processed[formula.id] = true;
                                _this.dependentFormulas.push(formula);
                            }
                        });
                    }
                });
            }

            //Now add the dependent formulas for any subcomponents
            if (this.components) {
                this.components.forEach(function (child) {
                    child.getDependentFormulas().forEach( function(formula) {
                        if (!processed[formula.id]) {
                            processed[formula.id] = true;
                            _this.dependentFormulas.push(formula);
                        }
                    });
                });
            }
        }

        return this.dependentFormulas;
    },

    /**
     * Note: The dst object will be destroyed when the component is destroyed. It should not be stored beyond the life of the component
     * @returns {DST}
     */
    getDstHelper : function() {
        return this._dst;
    },

    getDstContext : function() {
        var rootComponent = this.getDstRootComponent();
        return rootComponent && rootComponent.dstContext;
    },

    setDstContext : function(ctx) {

        var rootComponent = this.getDstRootComponent();
        if (rootComponent) {
            rootComponent.dstContext = ctx;

            //To support klips created pre-post-DST
            if (this.usePostDSTReferences() && !rootComponent.dstContext.id) {
                rootComponent.dstContext.id = rootComponent.getDstHelper().getId();
            }
        }
    },

    setDstBase: function(dstBase) {
        var dstContext = this.getDstHelper().getOrCreateDstContext();

        if (dstBase) {
            dstContext.base = dstBase;
        } else {
            if (dstContext.base) {
                delete dstContext.base;
            } else {
                // no base to set and no current base set
                return;
            }
        }

        this.processDSTContext();
    },

    setDstOperation: function(dstOperation, orderIndex, newOrderIndex) {
        return this._dst.setDstOperation(dstOperation, orderIndex, newOrderIndex);
    },

    getDstOperation: function(orderIndex) {
        return this._dst.getDstOperation(orderIndex);
    },

    findDstOperationByType: function(type) {
        return this._dst.findDstOperationByType(type);
    },

    getDstFiltersAfterIndex: function (index) {
        return this._dst.getDstFiltersAfterIndex(index);
    },

    getGroupOrAggregateOp: function() {
        return this._dst.getGroupOrAggregateOp();
    },

    getFilterOrTopBottomOp: function() {
        return this._dst.getFilterOrTopBottomOp();
    },

    findDstOperationByTypesAndDimensionId: function (types, dimensionId) {
        return this._dst.findDstOperationByTypesAndDimensionId(types, dimensionId);
    },

    findDstOperationByTypeAndDimensionId: function (type, dimensionId) {
        return this._dst.findDstOperationByTypeAndDimensionId(type, dimensionId);
    },

    /**
     *
     * @param sortOrder 0:No Sort, 1:ASC, 2:DESC
     * @returns {string} return the tooltip to be displayed
     */
    getTitleBySortOrder: function (sortOrder) {
        var sortSubmenuItems = this.getAvailableSortOptions();
        var visualCueTitle="";
        sortSubmenuItems.forEach(function (submenu) {
            if (submenu.sortDirection === sortOrder){
                visualCueTitle= submenu.text;
                return false;
            }
        });
        return visualCueTitle;
    },

    getDstOperationsAppliedToDimensionId: function (dimensionId) {
        return this._dst.getDstOperationsAppliedToDimensionId(dimensionId);
    },

    getGroupInfo: function() {
        return this._dst.getGroupInfo();
    },

    getAggregateInfo: function() {
        return this._dst.getAggregateInfo();
    },

    getSortInfo: function() {
        return this._dst.getSortInfo();
    },

    deleteDstOperation: function(orderIndex) {
        return this._dst.deleteDstOperation(orderIndex);
    },

    reorderDstOperations: function(newOrder) {
        return this._dst.reorderDstOperations(newOrder);
    },

    updateDstAggregations: function(dstAggregations) {
        return this._dst.updateDstAggregations(dstAggregations);
    },

    getDstAggregation: function(dimensionId) {
        return this._dst.getDstAggregation(dimensionId);
    },

    hasAggregatedParent: function(){
        return  false;
    },

    getAggregation: function(forDST) {
        var virtualColumnId = this.getVirtualColumnId();
        var aggregationRules = this.getAvailableAggregations();
        var defaultAggregationRule = findDefaultAggregationRule(aggregationRules);
        var aggregation = this.getDstAggregation(virtualColumnId);
        var nonDefaultAggregation;

        if (!aggregation) {
            if (this.isAutoFormatOn() && forDST) {
                nonDefaultAggregation= this.getNonDefaultAggregationByRole(this.role);
                // When auto format is on for the component and we are getting the aggregation type to be used in
                // DST (forDST=true), we will send the type "auto" to DPN so it can use proper aggregation for detected type.
                // Override aggregation with nonDefaultAggregation if any. ( e.g Map component )
                aggregation = nonDefaultAggregation ? nonDefaultAggregation: "auto";
            } else {
                aggregation = (this.fmt == "pct" ? "average" : defaultAggregationRule.value);
            }
        }
        if (this.hasMiniChartFmt()) {

            aggregation = "join";
        }
        if (!forDST && aggregation == "auto") {
            aggregation = defaultAggregationRule.value;
        }
        return aggregation;
    },

    getDstAggregations: function() {
        return this._dst.getDstAggregations();
    },

    getDstContextForEvaluation: function(formulaVariables, dstCache) {
        var root, dstToEval;
        //TODO: remove the isPostDST checks to enable caching for the old behavior too.
        if (this.usePostDSTReferences() && dstCache) {
            root = this.getDstRootComponent();
            if (dstCache[root.id]) {
                return dstCache[root.id];
            }
        }
        dstToEval = this._dst.getDstContextForEvaluation(formulaVariables);
        if (this.usePostDSTReferences() && dstCache) {
            root = this.getDstRootComponent();
            dstCache[root.id] = dstToEval;
        }
        return dstToEval;
    },

    getDstPreOperations: function() {
        if (this.needDSTFormatOperation() && !this.usePostDSTReferences()) {
            return [{type: "format"}];
        }
        return [];
    },

    generateDstVirtualColumns: function() {
        return this._dst.generateDstVirtualColumns();
    },


    getDstRootComponent: function() {
        var rootComponent = null;

        if (this.dstContext && this.dstContext.root){
            rootComponent = this;
        } else if (this.isDstRoot) {
            rootComponent = this;
        } else {
            if (this.parent && this.parent.getDstContext) {
                rootComponent = this.parent.getDstRootComponent();
            }
        }

        return rootComponent;
    },

    isInSameDSTAs: function( component ) {
        var dstRoot = this.getDstRootComponent();
        var otherDSTRoot = component.getDstRootComponent();

        return (dstRoot && otherDSTRoot && dstRoot.id === otherDSTRoot.id);
    },

    /**
     *
     * @returns {Array}
     */
    getDstPeerComponents : function() {
        var peerComponents = [];
        var dstRootComponent = this.getDstRootComponent();

        if (dstRootComponent) {
            if (dstRootComponent.hasFormula()) {
                peerComponents.push(dstRootComponent);
            }
            eachComponent(dstRootComponent, function (cx) {
                if (!cx.isDstRoot && cx.getDstRootComponent() == dstRootComponent) {
                    if (cx.hasFormula()) {
                        peerComponents.push(cx);
                    }
                }
            });
        }

        return peerComponents;
    },


    isUsingDataSet: function() {
        return false;
    },

    getDataSetId: function() {
        var rootComponent = this.getDstRootComponent();
        return rootComponent ?
            rootComponent._getDataSetId() :
            null;
    },

    _getDataSetId: function() {
        // TODO: Gord - This should ignore child elements that are their own DST root (e.g., "target value" label within a gauge).
        // TODO: Gord - This should ignore child elements with a DST root context.
        if (this.formulas) {
            for (var i = 0; i < this.formulas.length; i++) {
                var f = this.formulas[i];
                if (f.dataSetId) {
                    return f.dataSetId;
                }
            }
        }

        // If we have components, iterate through them and call recursively
        // ----------------------------------------------------------------
        if (this.components) {
            for (var j = 0; j < this.components.length; j++) {
                var dataSetId = this.components[j]._getDataSetId();
                if (dataSetId) {
                    return dataSetId;
                }
            }
        }

        return null;
    },

    hasDstActions : function (includePreOperations) {
        return this._dst.hasDstActions(includePreOperations);
    },

    hasMiniChartFmt : function () {
        if (this.fmt === "spk" || this.fmt === "spb" || this.fmt === "wlc" || this.fmt === "bch") {
            return true;
        }
    },

    /**
     *
     * @param selectedComponent - returns "true" if any of the sub-components has their own "dstContext".
     * @returns {boolean}
     */
    hasSubComponentsWithDstContext: function () {
        var hasDstContext = false;
        eachComponent(this, function (comp) {
            if (comp.dstContext && !$.isEmptyObject(comp.dstContext.ops)) {
                hasDstContext = true;
                return true;
            }
        });

        return hasDstContext;
    },

    dstOperationComparator: function(opA, opB) {
       return this._dst.dstOperationComparator(opA, opB);
    },

    getPeerComponentById: function(id) {
        var peers = this.parent.components;
        var i;
        var peer;
        if (peers && peers.length > 0) {
            for (i = 0; i < peers.length; i++) {
                peer = peers[i];
                if (peer && peer.id == id) {
                    return peer;
                }
            }
        }
        return null;
    },

    getPeerComponentsByRole: function(role) {
        var peers = this.parent.components;
        var i;
        var peer;
        var result = [];
        if (peers && peers.length > 0) {
            for (i = 0; i < peers.length; i++) {
                peer = peers[i];
                if (peer && peer.role == role) {
                    result.push(peer);
                }
            }
        }
        return result;
    },

    getComponentsByAxisReference: function(axisId) {
        var i,j;
        var cx;
        var components = [];
        var subcomponents;
        if (this.components) {
            for (i = 0; i < this.components.length; i++) {
                cx = this.components[i];
                if (cx && cx.axis && cx.axis == axisId) {
                    components.push(cx);
                } else if (cx.components && cx.components.length > 0) {
                    subcomponents = cx.getComponentsByAxisReference(axisId);
                    if (subcomponents && subcomponents.length > 0) {
                        for (j = 0; j < subcomponents.length; j++) {
                            components.push(subcomponents[j])
                        }
                    }
                }
            }
        }
        return components;
    },

    isAutoFormatOn: function () {
        return false;
    },

    isAutoFormatFeatureOn: function() {
        return this.isVCFeatureOn();
    },

    isAutoFormatMigrationDone: function() {
        return this.hasOwnProperty("autoFmt");
    },

    hasFormula: function() {
        return this.formulas && this.getFormula(0);
    },

    getKlipPublicId : function() {
        var klip = this.getKlip();
        return klip && klip.master;
    },

    getKlipInstanceId : function() {
        var klip = this.getKlip();
        return klip && klip.id;
    },

    getKlipComponentId: function() {
        return this.getKlipInstanceId() + "-" + this.id;
    },

    collectDstFilterVariables: function(){
        return this._dst.collectDstFilterVariables();
    },

    getDstVariablesFromFunction: function (predicate) {
        return this._dst.getDstVariablesFromFunction(predicate);
    },

    getDstVariableFromLiteral: function (predicate) {
        return this._dst.getDstVariableFromLiteral(predicate);
    },
    isNumerical: function(format) {
        if(FMT.toModelType(format) == DX.ModelType.NUMBER){
            return true;
        }

        return false;
    },

    setFormatAndValidateAggregation: function(fmt) {
        if (fmt) {
            var changed = this.fmt != fmt;
            this.setDataFormat(fmt);
            this.validateCurrentVirtualColumnAggregation(fmt);
            return changed;
        }
        return false;
    },

    validateCurrentVirtualColumnAggregation: function(fmt) {
        var dstAggrs = this.getDstAggregations();
        var currentAggr;
        var aggrInfo;
        if (dstAggrs && dstAggrs[this.getVirtualColumnId()]) {
            currentAggr = dstAggrs[this.getVirtualColumnId()];
            aggrInfo = this.getAggregationInfoByFormat(fmt, currentAggr);
            if (!aggrInfo) {
                delete dstAggrs[this.getVirtualColumnId()];
                dstAggrs[this.getVirtualColumnId()] = this.getAggregation(true);
            }
        }
    },

    getAggregationInfoByFormat: function(fmt, aggregation) {
        var modelType;
        var aggMap;
        if (fmt && aggregation) {
            modelType = FMT.toModelType(fmt);
            aggMap = Props.Defaults.dstAggregationsMap[modelType];
            if (aggMap) {
                return Props.Defaults.dstAggregationsMap[modelType][aggregation];
            }
        }
        return false;
    },

    getNonDefaultAggregationsByRole: function (role) {
        var aggregations;
        if (role) {
            aggregations = Props.Defaults.nonDefaultDstAggregationMap[role];
            if (aggregations && aggregations.value) {
                return aggregations.value;
            }
        }
        return null;
    },

    getNonDefaultAggregationByRole: function (role) {
        var defaultAggregation = undefined;
        var aggregations;
        if (role) {
            aggregations = this.getNonDefaultAggregationsByRole(role);
            if (aggregations) {
                aggregations.forEach(function (val) {
                    if (val.default) {
                        defaultAggregation = val.value;
                    }
                });
            }
        }
        return defaultAggregation;
    },

    setDataFormat: function(fmt) {
        // saas-9080. When switching from date format to anther format with autoFmt on, we will clear format properties.
        // We are only clearing format format arguments when switch from dat format to other format to avoid potential
        // incorrect parsing. For other type switching such as from num to cur, we main the fmtArgs such as precision.
        if (this.isAutoFormatOn() && FMT.isDateFormat(this.fmt) && !FMT.isDateFormat(fmt)) {
            this.fmtArgs = {};
        }
        if (KF.company.isNewDateFormatFeatureOn() && (fmt == FMT.type_dat || fmt == FMT.type_dat2)) {
            if (this.useNewDateFormat()) {
                this.fmt = FMT.type_dat2;
            } else {
                this.fmt = FMT.type_dat;
            }
        } else {
            this.fmt = fmt;
        }
    },

    usePostDSTReferences: function() {
        if (this.getKlip()) {
            return this.getKlip().isPostDST();
        }
        return false;
    },

    determineDateFormat : function() {
        if (!KF.company.isNewDateFormatFeatureOn()) {
            this.useLegacyDateFormat = true;
            if (this.fmt == FMT.type_dat) {
                this.fmt = FMT.type_dat;
            }
        } else {
            if (this.fmt == FMT.type_dat) {
                this.useLegacyDateFormat = true;
            } else {
                this.useLegacyDateFormat = false;
            }
        }
    },

    useNewDateFormat : function() {
        return !this.useLegacyDateFormat;
    },

    needDSTFormatOperation: function () {
        var i, peers;
        if (this.useNewDateFormat()) {
            peers = this.getDstPeerComponents();
            for (i = 0; i < peers.length; i++) {
                if (peers[i].fmt == FMT.type_dat2) {
                    return true;
                }
            }
        }
        return false;
    },

    needDSTFormatOperationPostDST : function() {
        return (this.useNewDateFormat() && this.fmt == FMT.type_dat2);
    },

    escapeDoubleQuotedString : function (s) {
        return (s + "").replace(/(\\|")/g, "\\$1");
    },

    getLabelSizeClass: function (size, isMiniChartSize) {
        if (isMiniChartSize) {
            return Props.Defaults.miniChartSizeMap [size] ? Props.Defaults.miniChartSizeMap[size] : size;
        } else {
            return Props.Defaults.labelSizeMap[size] ? Props.Defaults.labelSizeMap[size] : size;
        }
    },

    updateLabelSizes: function (config) {
        if (config && config.size) {
            this.size = this.migrateOldSizeToName(config.size, false);
        }
        // mini chart sizes
        if (config && config.fmtArgs && config.fmtArgs.chart_size) {
            this.fmtArgs.chart_size = this.migrateOldSizeToName(config.fmtArgs.chart_size, true);
        }
    },

    migrateOldSizeToName : function (oldSize, isMiniChartSize) {
        switch (oldSize){
            case 1: case "1":
            case 2: case "2":
            case 3: case "3":
            case 4: case "4":
            oldSize = this.getLabelSizeClass(oldSize, isMiniChartSize);
            break;
        }
        return oldSize;
    },

    getTimezoneOptions: function(){
        return window.timezoneOptions;
    },

    resetTimeZone: function(timezoneType){
        if(this.fmtArgs[timezoneType]){
            delete this.fmtArgs[timezoneType];
        }
    },

    useAccountTimeZone: function(timezoneType){
        return !(timezoneType in this.fmtArgs)
    },

    hasCustomStyleFeature : function () {
        return KF.company.hasFeature("theming");
    },

    getDashboard: function(){
        return dashboard;
    },

    persistChanges: function () {
        var klip = this.getKlip();
        klip.persistChanges();
    }
});
;/****** dst.js *******/ 

/* global CX:false, isWorkspace:false, page:false, convertToVirtualColumnId:false, KlipFactory:false */

function DST(cx) {
    this.cx = cx;
}

DST.DEFAULT_ID = "zzzz";

DST.OPERATION_TYPE = {
    FILTER : "filter",
    FORMAT : "format",
    FRAH: "frah",
    TRIM_N_ROWS: "remove_n_rows",
    GROUP : "group",
    RESOLVE : "resolve"
}

DST.FRAH_OPERATION = "frah";

DST.prototype.setDstOperation = function (dstOperation, orderIndex, newOrderIndex) {
    var dstContext = this.getOrCreateDstContext();

    if (!dstContext.ops) {
        dstContext.ops = [];
    }

    if (orderIndex !== undefined) {
        if (newOrderIndex !== undefined) {
            //Add new operation, then remove the old one
            dstContext.ops.splice(newOrderIndex, 0, dstOperation);
            orderIndex = orderIndex > newOrderIndex ? orderIndex + 1 : orderIndex;
            dstContext.ops.splice(orderIndex, 1);
        } else {
            dstContext.ops.splice(orderIndex, 1, dstOperation);
        }
    } else {
        if (newOrderIndex !== undefined) {
            dstContext.ops.splice(newOrderIndex, 0, dstOperation);
        } else {
            dstContext.ops.push(dstOperation);
        }
    }

    dstContext.ops.sort(this.dstOperationComparator);
    this.cx.processDSTContext();
};

DST.prototype.getDstOperation = function (orderIndex) {
    var dstContext = this.getOrCreateDstContext();

    if (!dstContext || !dstContext.ops || !dstContext.ops.length) {
        return;
    }

    return dstContext.ops[orderIndex];
};

DST.prototype.findDstOperationByType = function (type) {
    var dstContext = this.getOrCreateDstContext();
    var i, len, operation;
    var foundOperation = false;

    if (!dstContext || !dstContext.ops || !dstContext.ops.length) {
        return;
    }

    for (i = 0, len = dstContext.ops.length; i < len; i++) {
        operation = dstContext.ops[i];
        foundOperation = (operation.type == type);

        if (foundOperation) {
            break;
        }
    }

    if (foundOperation) {
        return {
            operation: operation,
            orderIndex: i
        };
    }
};

DST.prototype.getDstFiltersAfterIndex = function (index) {
    var i;
    var ops = [];
    var operation;
    var dstContext = this.getOrCreateDstContext();
    if (!dstContext || !dstContext.ops || !dstContext.ops.length) {
        return [];
    }

    for (i = index; i < dstContext.ops.length; i++) {
        operation = dstContext.ops[i];

        if (operation.type == "filter" || operation.type == "top_bottom") {
            ops.push({
                operation: operation,
                orderIndex: i
            });
        }
    }
    return ops;
};

DST.prototype.canReferToDstPeers = function() {
    var groupInfo, groupOrAggIndex, filterOp;
    if (this.cx.usePostDSTReferences()) {
        groupInfo = this.getGroupInfo();
        if (groupInfo && groupInfo.isGroupedByThisComponent) {
            return false;
        }
        groupOrAggIndex = this.getGroupOrAggregateOpIndex();
        filterOp = this.getFilterOrTopBottomOp();
        if (filterOp && (filterOp.orderIndex < groupOrAggIndex)) {
            return false;
        }
    }
    return true;
};

DST.prototype.getGroupOrAggregateOpIndex = function() {
    var groupOrAggIndex = -1;
    var groupInfo = this.getGroupInfo();
    var aggregateInfo = this.getAggregateInfo();
    if ((groupInfo && groupInfo.isGrouped) || (aggregateInfo && aggregateInfo.isAggregated)) {
        groupOrAggIndex = groupInfo.isGrouped ? groupInfo.operationData.orderIndex : aggregateInfo.operationData.orderIndex;
    }
    return groupOrAggIndex;
};

DST.prototype.getGroupOrAggregateOp = function() {
    var virtualColumnId = this.cx.getVirtualColumnId();
    return this.findDstOperationByTypesAndDimensionId([DST.OPERATION_TYPE.GROUP, "aggregate"], virtualColumnId);
};

DST.prototype.getFilterOrTopBottomOp = function() {
    var virtualColumnId = this.cx.getVirtualColumnId();
    return this.findDstOperationByTypesAndDimensionId(["filter", "top_bottom"], virtualColumnId);
};

DST.prototype.findDstOperationByTypesAndDimensionId = function (types, dimensionId) {
    var i, op;
    for (i=0; i < types.length; i++) {
        op = this.findDstOperationByTypeAndDimensionId(types[i], dimensionId);
        if (op) {
            return op;
        }
    }
};

DST.prototype.findDstOperationByTypeAndDimensionId = function (type, dimensionId) {
    var dstContext = this.getOrCreateDstContext();
    var i, len, operation;
    var foundOperation = false;

    if (!dstContext || !dstContext.ops || !dstContext.ops.length) return;

    for (i = 0, len = dstContext.ops.length; i < len; i++) {
        operation = dstContext.ops[i];

        if (operation && operation.type && operation.type == type) {
            foundOperation = (this.getDimensionIdFromOperation(operation) == dimensionId);
        }

        if (foundOperation) break;
    }

    if (foundOperation) {
        return {
            operation: operation,
            orderIndex: i
        };
    }
};

DST.prototype.getDimensionIdFromOperation = function(operation) {
    var type;
    var dimId = "";
    if(!operation) return null;

    type = operation.type;
    switch(type) {
        case "sort":
            dimId = operation.sortBy[0].dim;
            break;
        case "top_bottom":
            dimId = operation.orderBy[0].dim;
            break;
        case "group":
            dimId = operation.groupBy;
            break;
        case "filter" :
            dimId = operation.filterBy;
            break;
        case "aggregate":
            dimId = operation.aggregateBy;
            break;
        default:
            break;
    }
    return dimId;
};

DST.prototype.getDstOperationsAppliedToDimensionId = function (dimensionId) {
    var dstContext = this.getOrCreateDstContext();
    var i, len, operation, dimId;

    var dstOps = {
        grouped: false,
        filtered: false,
        aggregated: false,
        sorted: 0
    };

    if (!dstContext || !dstContext.ops || !dstContext.ops.length) return dstOps;

    for (i = 0, len = dstContext.ops.length; i < len; i++) {
        operation = dstContext.ops[i];

        switch(operation.type) {
            case "sort":
                dimId = operation.sortBy[0].dim;
                if (dimId === dimensionId) dstOps.sorted = operation.sortBy[0].dir;
                break;
            case "group":
                dimId = operation.groupBy;
                if (dimId === dimensionId) dstOps.grouped = true;
                break;
            case "filter":
                dimId = operation.filterBy;
                if (dimId === dimensionId) dstOps.filtered = true;
                break;
            case "top_bottom":
                dimId = operation.orderBy[0].dim;
                if (dimId === dimensionId) dstOps.filtered = true;
                break;
            case "aggregate":
                dimId = operation.aggregateBy;
                if (dimId === dimensionId) dstOps.aggregated = true;
                break;
            default:
                break;
        }
    }

    return dstOps;
};

DST.prototype.getDstOperationListAppliedToDimensionId = function (dimensionId) {
    var dstContext = this.getOrCreateDstContext();
    var i, len, operation, dimId;

    var dstOps = [];

    if (!dstContext || !dstContext.ops || !dstContext.ops.length) return dstOps;

    for (i = 0, len = dstContext.ops.length; i < len; i++) {
        operation = dstContext.ops[i];
        dimId = this.getDimensionIdFromOperation(operation);
        if (dimId === dimensionId) {
            dstOps.push({operation: operation, orderIndex:i});
        }
    }

    return dstOps;
};

DST.prototype.getGroupInfo = function() {
    var virtualColumnId = this.cx.getVirtualColumnId();
    var groupOperationData = this.cx.findDstOperationByType("group");
    var isGrouped = !!groupOperationData;
    var groupedBy = isGrouped ? groupOperationData.operation.groupBy : "";
    var isGroupedByThisComponent = (groupedBy == virtualColumnId);
    var disableGroup = this.cx.hasDependenciesFromSameRoot();

    return {
        operationData: groupOperationData,
        isGrouped: isGrouped,
        groupedBy: groupedBy,
        isGroupedByThisComponent: isGroupedByThisComponent,
        disableGroup: disableGroup
    }
};

DST.prototype.getAggregateInfo = function() {
    var aggregateOperationData = this.cx.findDstOperationByType("aggregate");
    var isAggregated = !!aggregateOperationData;

    return {
        operationData: aggregateOperationData,
        isAggregated: isAggregated
    };
};

DST.prototype.getSortInfo = function() {
    var virtualColumnId = this.cx.getVirtualColumnId();
    var sortOperationData = this.cx.findDstOperationByType("sort");
    var isSorted = !!sortOperationData;
    var sortedBy = isSorted ? sortOperationData.operation.sortBy[0].dim : "";
    var sortDirection = isSorted ? sortOperationData.operation.sortBy[0].dir : 0;
    var isSortedByThisComponent = (sortedBy == virtualColumnId);

    return {
        operationData: sortOperationData,
        isSorted: isSorted,
        sortedBy: sortedBy,
        sortDirection: sortDirection,
        isSortedByThisComponent: isSortedByThisComponent
    }
};

DST.prototype.deleteDstOperation = function(orderIndex) {
    var dstContext = this.getOrCreateDstContext();
    var deletedOperation;

    if (!dstContext || !dstContext.ops || !dstContext.ops.length) {
        return;
    }

    deletedOperation = dstContext.ops.splice(orderIndex, 1)[0];

    this.cx.processDSTContext();

    return deletedOperation;
};

DST.prototype.reorderDstOperations = function(newOrder) {
    var dstContext = this.getOrCreateDstContext();
    var newOperations = [];

    if (!dstContext || !dstContext.ops || !dstContext.ops.length) {
        return;
    }

    newOrder.forEach(function(index) {
        newOperations.push(dstContext.ops[index]);
    });

    dstContext.ops = newOperations;

    dstContext.ops.sort(this.dstOperationComparator);

    this.cx.processDSTContext();
};

DST.prototype.updateDstAggregations = function(dstAggregations) {
    var dstContext = this.getOrCreateDstContext();

    if (!dstContext.aggs) {
        dstContext.aggs = {};
    }

    dstContext.aggs = $.extend(dstContext.aggs, dstAggregations);

    this.cx.processDSTContext();
};

DST.prototype.getDstAggregation = function(dimensionId) {
    var aggs = this.cx.getDstAggregations();
    if (aggs) {
        return aggs[dimensionId];
    }
};

DST.prototype.getDstAggregations = function() {
    var dstContext = this.getOrCreateDstContext();

    if (dstContext && dstContext.aggs) {
        return dstContext.aggs;
    }
    return false;
};

DST.prototype.getResolveOperation = function(peers) {
    return {
            type: DST.OPERATION_TYPE.RESOLVE,
            dims: peers
        };
};

DST.prototype.getResolveAndFormatOperations = function(peers, allComps, unresolvedComps) {
    var i;
    var ops = [];
    var peer;
    for (i=0; i < peers.length; i++) {
        peer = peers[i];
        if (unresolvedComps[peer]) {
            ops.push({
                type: DST.OPERATION_TYPE.RESOLVE,
                dims: [peer]
            });
            if (allComps[peer].needDSTFormatOperationPostDST()) {
                ops.push({
                    type: DST.OPERATION_TYPE.FORMAT
                });
            }
            delete unresolvedComps[peer];
        }
    }

    return ops;
};

DST.prototype.squashAdjacentResolveOperations = function(operations) {
    var finalOps = [];
    var squashed = [];
    var _this = this;

    operations.forEach(function(op) {
        if (op.type != DST.OPERATION_TYPE.RESOLVE) {
            if (squashed.length) {
                finalOps.push(_this.getResolveOperation(squashed));
                squashed = [];
            }
            finalOps.push(op);

        } else {
            squashed = squashed.concat( op.dims );
        }
    });

    if (squashed.length) {
        finalOps.push(this.getResolveOperation(squashed));
    }
    return finalOps;
};

/*
Add resolve operations to DST operations as follows:
 Resolve the formulas for independent sub-components
 If no grouping or filtering was applied, resolve any sub-components that do not depend on sub-components which have filters or group actions
 While there are still user DST actions (other than sort):
 Apply the user DST action
 Determine the set of dependent sub-components which can be resolved & resolved them (ones which depended on the component with the last DST action)
 Apply sorting on the component
 */
DST.prototype._addResolveOps = function(dstOps) {
    var independent = [];
    var resultOps = [];
    var peerId;
    var unresolvedComps = {};
    var allComps = {};
    var cx;
    var peers = this.cx.getDstPeerComponents();
    var _this = this;

    peers.forEach(function(peer) {
        if (peer && peer.hasFormula()) {
            peerId = peer.getVirtualColumnId();
            allComps[peerId] = peer;
            unresolvedComps[peerId] = peer;
            if (Object.keys(peer._dependenciesFromSameRoot).length == 0) {
                independent.push(peerId);
            }
        }
    });

    //Resolve independent subcomponents before anything, to preserve alignment for filters.
    resultOps = resultOps.concat(_this.getResolveAndFormatOperations(independent, allComps, unresolvedComps));

    dstOps.forEach(function(op) {
        var dimOp = _this.getDimensionIdFromOperation(op);
        if (dimOp) {
            cx = allComps[dimOp];
            if (cx) {
                //Resolve the cx if not already resolved, and anything that it depends on that is also not currently resolved
                resultOps = resultOps.concat(_this.getResolveOperationAndItsDependencies(dimOp, allComps, unresolvedComps));

                if (op.type == "sort") {
                    //Resolve everything that is left before the sort
                    resultOps = resultOps.concat(_this.getResolveOperationsForAllRemaining(allComps, unresolvedComps));
                }

                resultOps.push(op);
            }
        } else if (op.type == DST.OPERATION_TYPE.FORMAT || op.type == DST.OPERATION_TYPE.FRAH) {
            //Format operation has no dimension
            resultOps.push(op);
        }
    });

    //Resolve everything that is left
    resultOps = resultOps.concat(this.getResolveOperationsForAllRemaining(allComps, unresolvedComps));

    resultOps = this.squashAdjacentResolveOperations(resultOps);

    if(Object.keys(unresolvedComps).length > 0) {
        console.log("invalid operations for resolve op on component " + this.cx.displayName); // eslint-disable-line no-console
        return resultOps;
    }

    return resultOps;
};

DST.prototype.getResolveOperationsForAllRemaining = function(allComps, unresolvedComps) {
    var i;
    var remainingDims;
    var resultOps = [];

    remainingDims = Object.keys(unresolvedComps);
    for (i = 0; i < remainingDims.length; i++) {
        resultOps = resultOps.concat(this.getResolveOperationAndItsDependencies(remainingDims[i], allComps, unresolvedComps));
    }

    return resultOps;
};

DST.prototype.getResolveOperationAndItsDependencies = function(dimId, allComps, unresolvedComps) {
    return this.getResolveOperationAndItsDependenciesRecursiveSafe(dimId, allComps, unresolvedComps, {})
};

DST.prototype.getResolveOperationAndItsDependenciesRecursiveSafe = function(dimId, allComps, unresolvedComps, currentProcessStack) {
    var cx, i;
    var dependencyKeys;
    var resultOps = [];

    if (currentProcessStack[dimId]) {
        console.log("invalid operations for resolve op on component " + this.cx.displayName); // eslint-disable-line no-console
    } else if (unresolvedComps[dimId]) {
        //First resolve all the components it depends on
        cx = allComps[dimId];

        if (cx._dependenciesFromSameRoot) {
            //Flag this id to make sure we don't hit it further down in the recursion
            currentProcessStack[dimId] = true;
            dependencyKeys = Object.keys(cx._dependenciesFromSameRoot);

            if (dependencyKeys.length) {
                for (i = 0; i < dependencyKeys.length; i++) {
                    resultOps = resultOps.concat(this.getResolveOperationAndItsDependenciesRecursiveSafe(dependencyKeys[i], allComps, unresolvedComps, currentProcessStack));
                }
            }
            currentProcessStack[dimId] = false;
        }

        //Resolve the current component
        resultOps = resultOps.concat(this.getResolveAndFormatOperations([dimId], allComps, unresolvedComps));
    }

    return resultOps;
};

DST.prototype.getDstContextForEvaluation = function(formulaVariables) {
    var dstContext;
    var preOperations;
    var operations;
    var virtualColumns;
    var generateVirtualColumns = false;
    var dstContextOps;

    if (!this.cx.isVCFeatureOn()) {
        return;
    }

    if (isWorkspace()) {
        // Only attempt to fix the aggregation type when in workspace.
        this.cx.validateCurrentVirtualColumnAggregation(this.cx.fmt);
    }

    if(formulaVariables === undefined) {
        formulaVariables = {};
    }

    dstContext = this.getOrCreateDstContext();
    preOperations = this.cx.getDstPreOperations();

    if (preOperations && preOperations.length > 0) {
        operations = preOperations;
        generateVirtualColumns = true;
    }

    if (this.cx.hasDstActions()) {
        if (!operations) operations = [];
        //Remove variable kf_all
        if (dstContext && dstContext.ops) {
            dstContextOps = this._sanitizeDstOps(formulaVariables, dstContext.ops);
            operations = operations.concat(dstContextOps);
        }
        generateVirtualColumns = true;
    }

    if (this.cx.usePostDSTReferences()) {
        generateVirtualColumns = true;
    }

    if (_.isEmpty(formulaVariables) && (!operations || operations.length === 0) && !generateVirtualColumns) {
        return;
    }

    if (generateVirtualColumns) {
        virtualColumns = this.cx.generateDstVirtualColumns();
    }

    if (this.cx.usePostDSTReferences()) {
        if (!operations) operations = [];
        operations = this._addResolveOps(operations);
    }

    dstContext.kid = this.cx.getKlip().id;

    return $.extend({}, dstContext, { ops: operations, virtualColumns: virtualColumns });
};

DST.prototype._removeVariableReferencesFromFunction = function (predicate, varsToRemove) {
    var i, arg, nestedFunctionVars, foundVarToRemove, currentVar, nestedArgs;
    var predicateCopy = $.extend({}, predicate);
    predicateCopy.args = [];

    for(i=0; i < predicate.args.length; i++) {
        arg = predicate.args[i];
        switch(arg.type) {
            case "op":
                if (arg.value && arg.value.var && !varsToRemove[arg.value.var]) {
                    predicateCopy.args.push($.extend({}, arg));
                } else if (arg.value && arg.value.var === undefined) {
                    predicateCopy.args.push($.extend({}, arg));
                }
                break;
            case "relativeDateRange":
                if(!arg.relativeAmountMap){
                    // in case of relative date "Unit to Date" filter the predicate will NOT have  "relativeAmountMap" and we want to keep the filter operation.
                    predicateCopy.args.push($.extend({}, arg));
                } else if(arg.relativeAmountMap && arg.relativeAmountMap.var && !varsToRemove[arg.relativeAmountMap.var]) {
                    predicateCopy.args.push($.extend({}, arg));
                } else if (arg.relativeAmountMap && arg.relativeAmountMap.var === undefined) {
                    predicateCopy.args.push($.extend({}, arg));
                }
                break;
            case "f":
                if (arg.f != "and" && arg.f != "or") {
                    //For and/or operations we can remove a single argument, but generally we need to remove the entire function
                    nestedFunctionVars = this.cx.getDstVariablesFromFunction(arg);
                    foundVarToRemove = false;
                    for (currentVar in nestedFunctionVars) {
                        if (nestedFunctionVars.hasOwnProperty(currentVar) && varsToRemove.hasOwnProperty(currentVar)) {
                            foundVarToRemove = true;
                            break;
                        }
                    }
                    if (!foundVarToRemove) {
                        predicateCopy.args.push($.extend({}, arg));
                    }
                } else {
                    //recursive call for and/or function to remove just the arguments that reference variables
                    nestedArgs = this._removeVariableReferencesFromFunction(arg, varsToRemove);
                    if (nestedArgs) {
                        predicateCopy.args.push(nestedArgs);
                    }
                }
                break;
            default:
                if (arg.var !== undefined) {
                    if (!varsToRemove[arg.var]) {
                        predicateCopy.args.push(arg);
                    }
                } else {
                    //Just a plain value
                    predicateCopy.args.push(arg);
                }
                break;
        }
    }
    return predicateCopy;
};

/**
 * This function gets rid of operators that uses a variable with value kf_all
 * This only affects the evaluation of the VC.
 * @param formulaVariables
 * @param operations
 * @returns {*}
 * @private
 */
DST.prototype._sanitizeDstOps = function(formulaVariables, operations) {
    var sanitizedOps = [];
    var keysToRemove = {};
    var i, operation;
    var hasAllValue = false;

    if(!formulaVariables || _.isEmpty(formulaVariables)
        || formulaVariables.length<1
        || !operations || operations.length<1) {
        return operations;
    }

    Object.keys(formulaVariables).forEach(function(key) {
        var value = formulaVariables[key];
        if (value && value === CX.KF_ALL_ID) {
            keysToRemove[key] = true;
            hasAllValue = true;
        }
    });

    if(hasAllValue) {
        //clone ops
        operations.forEach(function(op) {
            sanitizedOps.push($.extend(true, {}, op));
        });
        for (i=sanitizedOps.length-1; i >=0; i--) {
            operation = sanitizedOps[i];
            if(operation.type === "filter" && operation.variation === "condition") {
                operation.p = this._removeVariableReferencesFromFunction(operation.p, keysToRemove);
                if (operation.p.args.length == 0) {
                    //If all the arguments have been removed, remove the operation
                    sanitizedOps.splice(i,1);
                }
            }
        }
        return sanitizedOps;
    }
    return operations;
};

DST.prototype.generateDstVirtualColumns = function() {
    var dstRootComponent = this.cx.getDstRootComponent();
    var dstPeers = dstRootComponent.getDstPeerComponents();
    var virtualColumnInfo;

    return dstPeers.map(function(component) {
        virtualColumnInfo = component.getVirtualColumnInfo();
        delete virtualColumnInfo.name;

        return virtualColumnInfo;
    });
};

DST.prototype.hasDstActions = function (includePreOperations) {
    var dstContext, preOperations, hasOperations, hasPreOperations;

    if (!this.cx.isVCFeatureOn()) {
        return false;
    }
    if (!this.cx.getDstRootComponent()) {
        return false;
    }

    dstContext = this.getOrCreateDstContext();
    preOperations = this.cx.getDstPreOperations();
    hasOperations = (dstContext && dstContext.ops && dstContext.ops.length > 0);
    hasPreOperations = preOperations && preOperations.length > 0;

    return hasOperations || (includePreOperations ? hasPreOperations : false);
};

DST.prototype.dstOperationComparator = function(opA, opB) {
    // [saas-6939] sort is always the last operation
    if (opA.type === "sort" && opB.type !== "sort") {
        return 1;
    } else if (opA.type !== "sort" && opB.type === "sort") {
        return -1;
    } else {
        return 0;
    }
};

DST.prototype.collectDstFilterVariables = function(){
    var vars = {};
    var dstVariables = {};
    var dstRootComponent = this.cx.getDstRootComponent();
    var predicate = {};
    var filterOperations = [];
    var i;

    if (dstRootComponent.dstContext && !$.isEmptyObject(dstRootComponent.dstContext.ops)) {
        filterOperations = dstRootComponent.dstContext.ops.filter(function (op) {
            return op.type == "filter" && op.variation == "condition";
        });

        for (i = 0; i < filterOperations.length; i++) {
            if (filterOperations[i].p) {
                predicate = filterOperations[i].p;
                vars = this.cx.getDstVariablesFromFunction(predicate);
                $.extend(dstVariables, vars);
            }
        }
    }
    return dstVariables;

};

DST.prototype.getDstVariablesFromFunction = function (predicate) {
    var dstVars = {};
    var argVars = {};
    var _this = this;

    if (predicate.args) {
        predicate.args.forEach(function (arg) {
            argVars = {};
            switch (arg.type) {
                case "f":
                    argVars = _this.getDstVariablesFromFunction(arg);
                    break;
                case "op":
                    argVars = _this.getDstVariableFromLiteral(arg.value);
                    break;
                case "relativeDateRange":
                    argVars = _this.getDstVariableFromLiteral(arg.relativeAmountMap);
                    break;
                default:
                    argVars = _this.getDstVariableFromLiteral(arg);
                    break;
            }
            $.extend(dstVars, argVars);

        });
    }

    return dstVars;
};

DST.prototype.getDstVariableFromLiteral = function (predicate) {
    var dstVars = {};
    var varName, varValue;
    var klip = this.cx.getKlip();
    var uiVariables;
    if (predicate && predicate.var) {
        varName = predicate.var;
        if (isWorkspace()) {
            uiVariables = page.uiVariables;
            if(uiVariables[varName]) {
                varValue = uiVariables[varName].value;
            }
        } else {
            varValue = klip.getKlipProp(varName);
        }

        dstVars[varName] = varValue === undefined ? null : varValue;
    }
    return dstVars;
};


DST.prototype.normalizeDefaultColumnId = function(colId) {
    if(colId) {
        return colId.replace(/-/g, "");
    } else {
        return "";
    }
};

DST.prototype.generateFormulaForDatasetColumn = function (datasetId, columnId) {
    return datasetId + "#" + columnId + ";";
};

DST.prototype.getId = function() {
    var dstRoot;
    if (this.cx.usePostDSTReferences()) {
        dstRoot = this.cx.getDstRootComponent();
        if (dstRoot) {
            return "dst" + dstRoot.getVirtualColumnId();
        }
    }
    return DST.DEFAULT_ID;
};

DST.prototype.migratePostDST = function() {
    var dstContext;
    var dstRoot = this.cx.getDstRootComponent();
    if (dstRoot) {
        dstContext = this.cx.getDstContext();
        if (!dstContext) {
            dstContext = {};
        }
        dstContext.id = "dst" + dstRoot.getVirtualColumnId();
        this.cx.setDstContext(dstContext);
    }
};

DST.prototype.migrateSeparateRoots = function() {
    var root;
    if (this.cx) {
        if (this.cx.factory_id == "data_slot" || this.cx.factory_id == "html_data") {
            root = this.cx.getDstRootComponent();
            if (root && !root.getDstHelper().hasDstActionsOtherThanFormat() && !root.firstRowIsHeader) {
                this.cx.isDstRoot = true;
            }
        }
    }
};

DST.prototype.hasDstActionsOtherThanFormat = function () {
    var dstContext;
    var allOps;
    var hasOp = false;

    if (!this.cx.isVCFeatureOn()) {
        return false;
    }
    if (!this.cx.getDstRootComponent()) {
        return false;
    }

    dstContext = this.getOrCreateDstContext();

    if (dstContext) {
        allOps = dstContext.ops;
        if (allOps) {
            hasOp = allOps.some(function (op) {
                return (op.type !== DST.OPERATION_TYPE.FORMAT);
            });
        }
    }

    return hasOp;
};

DST.prototype.getOrCreateDstContext = function() {

    var dstContext = this.cx.getDstContext();
    if (!dstContext) {
        if (this.cx.usePostDSTReferences()) {
            dstContext = {id: this.getId()};
        } else {
            dstContext = {};
        }
    }

    this.cx.setDstContext(dstContext);
    return dstContext;
};

DST.prototype.getVCReferenceText = function() {
    return 	DST.getVCReferenceText(this.cx.getDstRootComponent().getDstHelper().getId(), this.cx.getVirtualColumnId());
};

DST.getVCReferenceText = function(dstId, compId) {
    return dstId + "#" + compId + ";"
};

//A virtual column can be aggregated if there is an aggregation DST operation, or if there is a grouping DST operation
//on another virtual column in the same dst, if the virtual column was resolved before the group operation.
DST.prototype.areVCValuesAggregated = function(vcId) {
    var i, operation, dst, groupOrAggregateOperation, groupBy, resolveOperationIdx, groupOrAggregateOperationIdx;

    dst = this.getDstContextForEvaluation(); //Make sure we get the DSTContext with the resolves embedded
    if (dst && dst.ops) {
        //TODO look into using isAggregated metadata flag from the server.
        //Not currently being used, as it is false for the case of a label showing the count of text values (here we return true for getResolvedType)

        groupOrAggregateOperation = null;
        groupOrAggregateOperationIdx = -1;
        resolveOperationIdx = -1;
        for (i = 0; i < dst.ops.length; i++) {
            operation = dst.ops[i];

            if (operation.type === "group" || operation.type === "aggregate") {
                groupOrAggregateOperation = operation;
                groupOrAggregateOperationIdx = i;
            } else if (operation.type === DST.OPERATION_TYPE.RESOLVE && operation.dims.indexOf(vcId) !== -1) {
                resolveOperationIdx = i;
            }
        }

        if (groupOrAggregateOperation) {
            if (groupOrAggregateOperation.type === "group") {
                groupBy = groupOrAggregateOperation.groupBy;
            }
            if ((groupOrAggregateOperation.type === "group" &&
                groupBy != vcId &&
                groupOrAggregateOperationIdx > resolveOperationIdx) || //The group has to appear after the resolve, or the vc is not grouped
                (groupOrAggregateOperation.type === "aggregate")) {
                return true;
            }
        }
    }

    return false;
};

DST.prototype.getDstFiltersOnComponentsWithReferences = function() {
    var result = [];
    var peerComponents = this.cx.getDstPeerComponents();
    peerComponents.forEach(_.bind(function(peer) {
        var filter;
        if (Object.keys(peer._dependenciesFromSameRoot).length) {
            filter = this.findDstOperationByTypesAndDimensionId(["filter", "top_bottom"], peer.getVirtualColumnId());
            if (filter) {
                result.push(filter);
            }
        }
    }, this));
    return result;
};

DST.createDummyDST = function(kid, formula) {
    var dst = {};
    var col = {};
    var dstId = "dst" + convertToVirtualColumnId(KlipFactory.instance.getNextId());
    var colId = convertToVirtualColumnId(KlipFactory.instance.getNextId());

    dst.id = dstId;
    dst.kid = kid;
    dst.ops = [];
    dst.virtualColumns = [];

    col.formula = formula;
    col.id = colId;
    col.fmt = "txt";
    col.autoFmt = true;
    col.type = 1;
    dst.virtualColumns.push(col);

    return dst;
};;/****** cx.data_component.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:true, DX:false, FMT:false, Props:false,  dashboard:false, page:false,  isWorkspace:false,
    bindValues:false, animateManager:false, updateManager:false */

Component.DataComponent = $.klass(Component.Base, {

    /**
     * a list of formulas that are assigned to this component, each component
     * can support 0.* forumlas that must defined in forumla definitions
     */
    formulas : false,

    /**
     * a list of resolved formulas for data component. This is for performance optimization purpose. When resolving
     * references, we only want to resolve it once until the original formula has changed.
     */
    resolvedFormulas : false,

    /**
     * current data.  each data element is the result of an evaluated forumla in the same
     * index position in the forumlas array.
     * @param config
     */
    data : false,

    /**
     * flag which tells the workspace to disable data-mapping controls for this component.
     * some components may be used in predefined ways/roles where their data is set automatically
     * by another (parent) component, and the user should not be allowed to override it.
     *
     * eg, the sparkline component has a child label which displays the last value in the sparkline.
     *   even though the label is exposed in the workspace for property editing, the user should
     *   not be allowed to map data to it since it always displays the last value.
     *
     */
    dataDisabled : false,

    /**
     * `true` if the component has received DPN data (during `#onFormulaUpdate`).
     */
    dpnDataReceived: false,

    hasErrorResult:false,

    autoFmt: null,

    displayAutoFormat: true,

    isFormatReset: false,

    initialize : function($super) {
        $super();
        this.formulas = [];
        this.resolvedFormulas = [];
        this.data = [];
        this.isFormatReset = false;
    },

    /**
     *
     * @param $super
     * @param config
     */
    configure : function($super, config) {
        $super(config);
        bindValues(this,config,["dataDisabled", "autoFmt"] );
        if (this.isAutoFormatFeatureOn() && this.autoFmt == null) {
            this.setAutoFormatDefault();
        }
    },


    serialize : function($super) {
        var i, f;
        var config = $super();

        config.formulas = [];
        for (i = 0; i < this.formulas.length; i++) {
            f = this.formulas[i];
            config.formulas.push( {txt:f.formula,src:f.src,ver:f.version} );
        }
        // Note DO NOT serialize the resolvedFormulas. We do not persist them.

        if (this.data && this.data.length && this.data[0] instanceof Array) {
            config.data = [this.data[0].slice(0,100)];
        } else {
            config.data = [];
        }

        if (this.autoFmt != null) {
            config.autoFmt = this.autoFmt;
        }

        bindValues(config,this,["dataDisabled"],{ dataDisabled:false } ,true);

        return config;
    },

    getReferenceValueModel : function($super) {
        var m = $super();

        m.reference = true;
        m.actionId = "vf_assign_reference";
        m.actionArgs = this;

        return m;
    },

    /**
     * provides a consistent way to access a data component's backing data structures.  Eg,
     * always returns an array even if the backing structure is undefined.
     *
     * @param seriesIdx
     * @param dataIndex
     * @param defaultVale NOT IMPLEMENTED
     */
    getData : function(seriesIdx,dataIndex,defaultVale) {
        var val;
        if ( !this.data || !this.isValueDefined(this.data[seriesIdx])) {
            if (this.isValueDefined(dataIndex)) {
                return false;
            } else {
                return [];
            }
        }

        if (this.isValueDefined(dataIndex)) {
            val = this.data[seriesIdx][dataIndex];
            if (!this.isValueDefined(val)) {
                return false;
            } else {
                return val;
            }
        }

        return this.data[seriesIdx];
    },

    hasData : function(seriesIdx,dataIndex) {

        if (!this.data || !this.isValueDefined(this.data[seriesIdx])) {
            return false;
        } else if (dataIndex !== undefined && !this.isValueDefined(this.data[seriesIdx][dataIndex])) {
            return false;
        }

        return true;
    },

    isAllFalsyData: function(data) {
        var isAllDataFalsy = data.every(function (point) {
            return !point;
        }.bind(this));

        return isAllDataFalsy;
    },

    isValueDefined : function(value) {
        return (value || value === 0);
    },

    /**
     * components that support filtering of their data should overide this method to return
     * the filtered data set
     * @param seriesIdx {not used atm}
     * @return should return an array of arrays (same structure as this.data)
     */
    getFilteredData : function(seriesIdx) {
        if ( seriesIdx == undefined ) return this.data;
        else return this.getData(seriesIdx);
    },


    getFormula : function(idx) {
        if ( !this.formulas ) {
            return undefined;
        } else if (!idx) {
            return this.formulas[0];
        }
        return this.formulas[idx];
    },

    setFormula: function(idx, fnTxt, fnSrc, formulaVersion) {
        if (this.formulas) {
            var fn = this.getFormula(idx);
            var changed;
            if (!fn) {
                fn = new DX.Formula(fnTxt, fnSrc, this, formulaVersion);
                DX.Manager.instance.registerFormula(fn);
                this.formulas[idx] = fn;
                changed = true;
            } else {
                changed = fn.setFormula(fnTxt, fnSrc);
            }
            if (changed && this.getKlip()) {
                this.getKlip().clearAllResolvedFormulas();
            }
        }

    },

    getResolvedFormula : function(idx) {
        if ( !this.resolvedFormulas ) {
            return undefined;
        } else if (!idx) {
            return this.resolvedFormulas[0];
        }
        return this.resolvedFormulas[idx];
    },

    setResolvedFormula : function(idx, resolvedFormula) {
        if ( this.resolvedFormulas && resolvedFormula && idx >= 0) {
            this.resolvedFormulas[idx] = resolvedFormula;
        }
    },

    drawDstHints: function() {

    },

    setData: function (data, msg) {
        var isSameData = _.isEqual(data, this.data[0]);
        var isSameMetadata = (msg && _.isEqual(msg.metadata, this.getMetadata()));

        if (msg) {
            // Set the result metadata as formatting arguments so formatter can take advantage of certain clues.
            this.setMetadata(msg.metadata);
        }

        // Only update format when we have non-empty formula result. For empty result, the previous format will be kept.
        if (msg && msg.metadata) {
            // If the formula result message have a auto format information, we will apply those format.
            this.updateFormat(msg.metadata);
        }

        if (isSameData && isSameMetadata) {
            if (isWorkspace()) {
                this.drawDstHints();
            }
            return;
        }

        var hasNewData = ( this.data && this.data.length > 0 && this.dpnDataReceived);

        if ( data != undefined && (_.isArray(data[0]) && data[0].length == 0) ) {
            data = [];
        }

        this.data[0] = data;
//		this.applyConditions();
        this.publishKlipEvent( {id:"cx-data-changed", cx:this}  , false);
        this.invalidate();

        updateManager.queue(this);

        if (!isWorkspace() && hasNewData && dashboard.config.showKlipUpdateAnimations && !this.disableAnimations) {
            var k = this.getKlip();
            if (k && k.isVisible) {
                setTimeout(function() {
                    animateManager.queue(k);
                }, 500);
            }
        }
    },

    updateFormat : function(metadata) {
        var changed;
        var autoFmt = metadata ? metadata.autoFmt : null;
        if (this.isAutoFormatOn() && autoFmt) {
            changed = this.setFormatAndValidateAggregation(autoFmt.fmt);
            // Move the auto detected format properties outside of resultMetadata and remove the original.
            // ResultMetadata may get removed between formula evaluation which will cause issue in dashboard.
            this.fmtArgs.autoFmt = autoFmt;
            // Remove the original. No need to keep duplicate information.
            delete metadata.autoFmt;
            // We only call updatePropertySheet when format changes. Otherwise we will end up with infinite loop.
            if (changed || this.isFormatReset) {
                this.isFormatReset = false;
                this.updatePropertySheet();
            }
        }
    },

    updatePropertySheet : function() {
        if (typeof page !== "undefined" && typeof page.updatePropertySheet == "function") {
            // Since we updated the format and format args, we will need to update the property sheet.
            // Only update if this is the active component, otherwise it will render over the properties of the active component.
            if (page.activeComponent === this) {
                page.updatePropertySheet(this.getPropertyModel(), this.getPropertyModelGroups(), page.componentPropertyForm);
            }
        }
    },

    clearFormula : function(idx) {
        this.formulas.splice(idx,1);
        this.setData([], { metadata:false });
    },


    onFormulaUpdate : function( fn, msg ) {
        this.setData(msg.v, msg);
        this.dpnDataReceived = true;
    },

    setBulletChartDefaults: function(config) {
        if (config.fmt == "bch" && !this.fmtArgs.rangeThemeColors) {
            this.fmtArgs.rangeThemeColors = ["cx-theme_ccc", "cx-theme_aaa", "cx-theme_888"];

            if (isWorkspace()) {
                page.updatePropertySheet(this.getPropertyModel(), this.getPropertyModelGroups(), page.componentPropertyForm);
            }
        }

        if (config["~propertyChanged"] && config.fmtArgs && this.fmtArgs.rangeThemeColors) {
            var _this = this;
            _.each(config.fmtArgs, function(color, prop) {
                if (prop.indexOf("range_color-") != -1) {
                    var index = prop.match(/\d+/);
                    _this.fmtArgs.rangeThemeColors[parseInt(index[0])] = color;
                    delete _this.fmtArgs[prop];
                }
            });
        }
    },

    setMetadata: function(metadata) {
        if (this.isVCFeatureOn()) {
            if (this.fmtArgs) {
                if (!metadata) {
                    delete this.fmtArgs["resultMetadata"];
                } else {
                    this.fmtArgs.resultMetadata = metadata;
                }
            }
            if (metadata && metadata.name && !this.isNameRenamed) {
                this.name = metadata.name;
            }
            if (isWorkspace() && metadata && metadata.debugReport) {
                /* eslint-disable no-console */
                console.log("Debug Report for Component: " + this.id);
                console.log(metadata.debugReport);
                /* eslint-enable no-console */
                delete metadata.debugReport;
            }
        }
    },

    getMetadata: function() {
        if (this.fmtArgs) {
            return this.fmtArgs.resultMetadata;
        }
    },

    getMetadataProperty: function(property) {
        if (this.fmtArgs && this.fmtArgs.resultMetadata) {
            return this.fmtArgs.resultMetadata[property];
        }
    },

    setAutoFormatDefault: function() {
        if (!this.hasFormula()) {
            this.autoFmt = true;
        }
    },

    isAutoFormatOn: function () {
        return this.isAutoFormatFeatureOn() && this.autoFmt;
    },

    isUsingDataSet: function(dataSetId) {
        return this.formulas && this.formulas[0] && this.formulas[0].dataSetId === dataSetId;
    },

    shouldSendAutoFormatToDpn: function(isWorkspace) {
        return isWorkspace && this.isAutoFormatOn();
    },

    /**
     * Get the effective format arg value for a given arg name. If there's a user set arg value, it takes precedence
     * over the automatically detected format arg value. And auto detected format arg value is only available when
     * auto detect flag is turned on for the component.
     */
    getEffectiveFmtArgs : function(argName, defaultResult) {
        var value;
        if (this.fmtArgs && this.fmtArgs[argName]) {
            return this.fmtArgs[argName];
        } else {
            value = this.getAutoDetectedFmtArgs()[argName];
            if ((value && !_.isEmpty(value)) || !defaultResult) {
                return value;
            } else {
                return defaultResult;
            }
        }
    },

    setEffectiveFmtArgs : function(argName, value) {
        if (this.fmtArgs) {
            this.fmtArgs[argName] = value;
        }
    },

    resetEffectiveFmtArgs : function(argName) {
        if (this.fmtArgs && this.fmtArgs[argName]) {
            delete this.fmtArgs[argName];
        }
    },

    getAutoFmt: function() {
        return this.fmtArgs && this.fmtArgs.autoFmt;
    },

    /**
     * Get the automatically detected format args when auto format is turned on for the component.
     */
    getAutoDetectedFmtArgs: function() {
        if (this.isAutoFormatFeatureOn() && this.getAutoFmt()) {
            return this.getAutoFmt().fmtArgs;
        } else {
            return {};
        }
    },

    resetFormat: function() {
        var changed = this.setFormatAndValidateAggregation("txt");
        this.fmtArgs = {};
        this.isFormatReset = true;
        if(changed){
            this.updatePropertySheet();
        }
    },

    /**
     * Merge the user set and auto detected fmtArgs when the auto detection checkbox is off so that
     * the dashboard can render properly with auto detection set to off.
     */
    mergeFormatArgs: function() {
        if (this.getAutoFmt()) {
            this.fmtArgs = this.getMergedFormatArgs();
        }
    },

    getMergedFormatArgs: function() {
        var merged = $.extend({}, this.getAutoDetectedFmtArgs(), this.fmtArgs);
        if (merged.autoFmt) {
            delete merged.autoFmt;
        }
        return merged;
    },

    /**
     * Get a property from the property model by its id.
     */
    getPropertyById : function(propertyModel, id) {
        var i;
        if (propertyModel) {
            for (i = 0; i < propertyModel.length; i++) {
                if (propertyModel[i] && propertyModel[i].id == id) {
                    return propertyModel[i];
                }
            }
        }
        return false;
    },

    /**
     * Find a property in the property model by its id and set the noRender flag to true (Stop rendering the property).
     */
    stopRenderProperty : function(propertyModel, id) {
        var property = this.getPropertyById(propertyModel, id);
        if (property) {
            property.noRender = true;
        }
    },

    updateFormula: function (reevaluateFormula) {
        var formula = this.getFormula(0);
        if (reevaluateFormula && formula && formula.formula) {
            // Submit an event with the same formula to trigger re-evaluation.
            PubSub.publish("formula-changed", [formula.formula, formula.src, true, this]);
        }
    },

    updateDSTFormula: function (reevaluateFormula) {
        var formula, dstToEval;

        if (!this.usePostDSTReferences()) {
            dstToEval = this.getDstContextForEvaluation();
        }

        if (this.usePostDSTReferences() || dstToEval) {
            formula = this.getFormula(0);
            if (reevaluateFormula && formula && formula.formula) {
                this.updateDstRootFormulas();
            }
        }
    },

    setDefaultDateInputFormat : function() {
        var epochDateFormat;
        if (!this.getEffectiveFmtArgs("dateInputFormat")) {
            epochDateFormat = FMT.parseEpochDate(this.getData(0));
            if (epochDateFormat) {
                this.fmtArgs.dateInputFormat = epochDateFormat;
            }
        }
    },

    setDefaultDateOutputFormat : function() {
        var defaultDateOutputFormat = Props.Defaults.dateFormatJava[Props.Defaults.dateFormatJavaDefaultIndex].value;
        if (this.fmt == FMT.type_dat2) {
            if (!this.fmtArgs) {
                this.fmtArgs = {
                    dateFormat: defaultDateOutputFormat,
                    dateFormatCustom: defaultDateOutputFormat
                };
            } else if (!this.fmtArgs.dateFormat) {
                this.fmtArgs.dateFormat = defaultDateOutputFormat;
                this.fmtArgs.dateFormatCustom = defaultDateOutputFormat;
            }
        }
    },

    setHasErrorResult : function (boolVal){
        this.hasErrorResult = !!boolVal;
    }
});
;/****** cx.formula.js *******/ 

/* global DX:false, isPreview:false, updateManager:false, getQueryParam:false */

var CxFormula = {};

/**
 * Submits a formula for execution.
 * @param options - {
 * 		cx: {},
 * 		formula: {},
 * 		vars: [],
 *		isWorkspace: true,
 * 		dashboard: {}
 * }
 */
CxFormula.submitFormula = function(options) {
	var componentToSubmit = options.cx;
	var formula = options.formula;
	var vars  = options.vars;
	var isWorkspace = options.isWorkspace;
	var dashboard = options.dashboard;
	var componentId = componentToSubmit.id;

	var dstContext;
	var formulaText = formula.formula;
	var query;

	if (isPreview(dashboard)) return;

	dstContext = componentToSubmit.getDstContextForEvaluation(vars);

	if (formula.getFormulaText) {
		formulaText = formula.getFormulaText();
	}

	query = {
		kid: componentToSubmit.getKlipInstanceId(),
		klip_pid: componentToSubmit.getKlipPublicId(),
		fm: formulaText,
		vars: vars,
		dst: dstContext,
		autoFmt: componentToSubmit.shouldSendAutoFormatToDpn(isWorkspace),
		useNewDateFormat: componentToSubmit.useNewDateFormat()
	};

	if (isWorkspace) {
		query.debugLevel = getQueryParam("debugLevel");
        $("#query-spinner").show();
        //Cap the results size within the editor
        query.maxResultsSize = DX.Manager.MAX_EDITOR_QUERY_RESULTS_SIZE;
	}

	DX.Manager.instance.query(query, function(result, msg) {
		if (isWorkspace) $("#query-spinner").hide();

		if (msg.error) {
			if (isWorkspace) {
				PubSub.publish("formula-evaluation-error", {
					component: componentToSubmit,
					message: msg
				});
			}
		} else {
			componentToSubmit.setData(result, msg);
		}

		componentToSubmit.invalidate();
		updateManager.queue(componentToSubmit);
    }, componentId);
};
;/****** cx.theme.js *******/ 

var CXTheme = {};

CXTheme.themes = {
	"default" : {
		cx_bg : "#ffffff",
		cx_chart_bg : "#ffffff",
		cx_text : "#4A4A4A",
		cx_text_highlight: "#000",
		cx_text_selected: "#ccc",
		cx_text_outline: "#ffffff",
		cx_red : "#eb4f54",
		cx_orange : "#f6921e",
		cx_green : "#70b27c",
		cx_blue : "#4994d0",
		cx_gauge_well : "#cccccc",
		cx_gauge_bar : "#8d8d8d",
		cx_gauge_stick : "#4a4a4a",
		cx_gauge_twig : "#4a4a4a",
		cx_gauge_zero : "#4a4a4a",
		cx_sparkline_area : "#D1E7FF",
		cx_mini_series_zero : "#4a4a4a",
		cx_chart_gridlines : "#e8e8e8",
		cx_chart_axis :"#dbdbdb",
		cx_chart_axis_labels : "#515151",
		cx_chart_axis_data_labels : "#383838",
		cx_chart_nav_active: "#767676",
		cx_chart_nav_inactive: "#d6d6d6",
		cx_chart_button_back_on: "#f2f2f2",
		cx_chart_button_back_hover: "#fafafa",
		cx_chart_button_border_on: "#c7c7c7",
		cx_chart_button_border_hover: "#bcbcbc",
		cx_chart_button_label_on: "#515151",
		series_colors : [ "cx-theme_blue_3", "cx-theme_green_3", "cx-theme_orange_3", "cx-theme_red_3", "cx-theme_violet_3", "cx-theme_brown_3", "#cc527b", "#33867f", "#ada434" ],
		cx_klip_tb_back_on : "#E5F3FC",
		cx_klip_tb_back_off : "#F8F8F9",
		cx_klip_tb_text_on : "#526470",
		cx_klip_tb_text_off : "#666666",
		cx_bullet_target : "#353535",
		cx_bullet_performance : "#747474",
		cx_bullet_range : ["#dedede","#c9c9c9","#b5b5b5"],
		cx_tile_map_color: "#cccccc",
		cx_tile_map_border: "#ffffff",
		cx_tile_map_scale_start: "#9ac4e5",
		cx_tile_map_scale_end: "#3e7eb1",
		cx_tile_map_selected: "#cccccc",

		// ColorPicker colours
		"cx-theme_blue_1":"#a9ccff",
		"cx-theme_blue_2":"#7cb1ff",
		"cx-theme_blue_3":"#5290e9",
		"cx-theme_blue_4":"#4876d7",
		"cx-theme_blue_5":"#3862bb",

		"cx-theme_green_1":"#c5f3d2",
		"cx-theme_green_2":"#99dba7",
		"cx-theme_green_3":"#71b37c",
		"cx-theme_green_4":"#52875a",
		"cx-theme_green_5":"#46754d",

		"cx-theme_yellow_green_1":"#e9fa61",
		"cx-theme_yellow_green_2":"#dcf044",
		"cx-theme_yellow_green_3":"#cee237",
		"cx-theme_yellow_green_4":"#a3bd28",
		"cx-theme_yellow_green_5":"#79911d",

		"cx-theme_yellow_1":"#fff470",
		"cx-theme_yellow_2":"#fbee58",
		"cx-theme_yellow_3":"#efd140",
		"cx-theme_yellow_4":"#d2a62f",
		"cx-theme_yellow_5":"#a87c22",

		"cx-theme_orange_1":"#ffcf68",
		"cx-theme_orange_2":"#fabf40",
		"cx-theme_orange_3":"#ec932f",
		"cx-theme_orange_4":"#cd6c22",
		"cx-theme_orange_5":"#a24f19",

		"cx-theme_red_1":"#fd91a1",
		"cx-theme_red_2":"#f66a77",
		"cx-theme_red_3":"#e14d57",
		"cx-theme_red_4":"#bb383f",
		"cx-theme_red_5":"#a93439",

		"cx-theme_violet_1":"#e9a0e7",
		"cx-theme_violet_2":"#c478c2",
		"cx-theme_violet_3":"#965994",
		"cx-theme_violet_4":"#804b7d",
		"cx-theme_violet_5":"#6e406a",

		"cx-theme_brown_1":"#cfbaa3",
		"cx-theme_brown_2":"#b89876",
		"cx-theme_brown_3":"#9d7952",
		"cx-theme_brown_4":"#896a49",
		"cx-theme_brown_5":"#7a5d3f",

		"cx-theme_warm_gray_1":"#dfd6c9",
		"cx-theme_warm_gray_2":"#beb3a8",
		"cx-theme_warm_gray_3":"#9a9289",
		"cx-theme_warm_gray_4":"#7e766e",
		"cx-theme_warm_gray_5":"#655e57",

		"cx-theme_000":"#000000",
		"cx-theme_222":"#222222",
		"cx-theme_444":"#444444",
		"cx-theme_666":"#666666",
		"cx-theme_888":"#888888",
		"cx-theme_aaa":"#aaaaaa",
		"cx-theme_ccc":"#cccccc",
		"cx-theme_eee":"#eeeeee",
		"cx-theme_fff":"#ffffff"
	},

	"dark" : {
		cx_bg : "#2A2A2A",
		cx_chart_bg : "#2A2A2A",
		cx_text : "#DADADA",
		cx_text_highlight: "#ffaec9",
		cx_text_selected: "#8000ff",
        cx_text_outline: "#2a2a2a",
		cx_red : "#c14f4f",
		cx_orange : "#d88f3c",
		cx_green : "#63986c",
		cx_blue : "#4994d0",
		cx_gauge_well : "#4f4f4f",
		cx_gauge_bar : "#9c9c9c",
		cx_gauge_stick : "white",
		cx_gauge_twig : "white",
		cx_gauge_zero : "white",
		cx_sparkline_area : "#334b67",
		cx_mini_series_zero : "#ffffff",
		cx_chart_gridlines : "#454545",
		cx_chart_axis :"#686868",
		cx_chart_axis_labels : "#a2a2a2",
		cx_chart_axis_data_labels : "#e8e8e8",
		cx_chart_nav_active: "#a5a5a5",
		cx_chart_nav_inactive: "#4b4b4b",
		cx_chart_button_back_on: "#3b3b3b",
		cx_chart_button_back_hover: "#454545",
		cx_chart_button_border_on: "#555555",
		cx_chart_button_border_hover: "#626262",
		cx_chart_button_label_on: "#c2c2c2",
		series_colors : [ "cx-theme_blue_3", "cx-theme_green_3", "cx-theme_orange_3", "cx-theme_red_3", "cx-theme_violet_3", "cx-theme_brown_3", "#cc527b", "#33867f", "#ada434" ],
		cx_klip_tb_back_on : "#43545f",
		cx_klip_tb_back_off : "#363636",
		cx_klip_tb_text_on : "#c5e2f6",
		cx_klip_tb_text_off : "#a5a5a5",
		cx_bullet_target : "#ffffff",
		cx_bullet_performance : "#bbbbbb",
		cx_bullet_range : ["#575757","#6a6a6a","#7b7b7b"],
		cx_tile_map_color: "#686868",
		cx_tile_map_border: "#2A2A2A",
		cx_tile_map_scale_start: "#9ac4e5",
		cx_tile_map_scale_end: "#3e7eb1",
		cx_tile_map_selected: "#cccccc",

		// ColorPicker colours
		"cx-theme_blue_1":"#3862bb",
		"cx-theme_blue_2":"#4876d7",
		"cx-theme_blue_3":"#5290e9",
		"cx-theme_blue_4":"#7cb1ff",
		"cx-theme_blue_5":"#a9ccff",

		"cx-theme_green_1":"#46754d",
		"cx-theme_green_2":"#52875a",
		"cx-theme_green_3":"#71b37c",
		"cx-theme_green_4":"#99dba7",
		"cx-theme_green_5":"#c5f3d2",

		"cx-theme_yellow_green_1":"#79911d",
		"cx-theme_yellow_green_2":"#a3bd28",
		"cx-theme_yellow_green_3":"#cee237",
		"cx-theme_yellow_green_4":"#dcf044",
		"cx-theme_yellow_green_5":"#e9fa61",

		"cx-theme_yellow_1":"#a87c22",
		"cx-theme_yellow_2":"#d2a62f",
		"cx-theme_yellow_3":"#efd140",
		"cx-theme_yellow_4":"#fbee58",
		"cx-theme_yellow_5":"#fff470",

		"cx-theme_orange_1":"#a24f19",
		"cx-theme_orange_2":"#cd6c22",
		"cx-theme_orange_3":"#ec932f",
		"cx-theme_orange_4":"#fabf40",
		"cx-theme_orange_5":"#ffcf68",

		"cx-theme_red_1":"#a93439",
		"cx-theme_red_2":"#bb383f",
		"cx-theme_red_3":"#e14d57",
		"cx-theme_red_4":"#f66a77",
		"cx-theme_red_5":"#fd91a1",

		"cx-theme_violet_1":"#6e406a",
		"cx-theme_violet_2":"#804b7d",
		"cx-theme_violet_3":"#965994",
		"cx-theme_violet_4":"#c478c2",
		"cx-theme_violet_5":"#e9a0e7",

		"cx-theme_brown_1":"#7a5d3f",
		"cx-theme_brown_2":"#896a49",
		"cx-theme_brown_3":"#9d7952",
		"cx-theme_brown_4":"#b89876",
		"cx-theme_brown_5":"#cfbaa3",

		"cx-theme_warm_gray_1":"#655e57",
		"cx-theme_warm_gray_2":"#7e766e",
		"cx-theme_warm_gray_3":"#9a9289",
		"cx-theme_warm_gray_4":"#beb3a8",
		"cx-theme_warm_gray_5":"#dfd6c9",

		"cx-theme_000":"#ffffff",
		"cx-theme_222":"#eeeeee",
		"cx-theme_444":"#cccccc",
		"cx-theme_666":"#aaaaaa",
		"cx-theme_888":"#888888",
		"cx-theme_aaa":"#666666",
		"cx-theme_ccc":"#444444",
		"cx-theme_eee":"#222222",
		"cx-theme_fff":"#000000"
	}
};

CXTheme.themeColourMigrationMap = {
	"#a9ccff":"cx-theme_blue_1",
	"#7cb1ff":"cx-theme_blue_2",
	"#5290e9":"cx-theme_blue_3",
	"#4876d7":"cx-theme_blue_4",
	"#3862bb":"cx-theme_blue_5",

	"#c5f3d2":"cx-theme_green_1",
	"#99dba7":"cx-theme_green_2",
	"#71b37c":"cx-theme_green_3",
	"#52875a":"cx-theme_green_4",
	"#46754d":"cx-theme_green_5",

	"#e9fa61":"cx-theme_yellow_green_1",
	"#dcf044":"cx-theme_yellow_green_2",
	"#cee237":"cx-theme_yellow_green_3",
	"#a3bd28":"cx-theme_yellow_green_4",
	"#79911d":"cx-theme_yellow_green_5",

	"#fff470":"cx-theme_yellow_1",
	"#fbee58":"cx-theme_yellow_2",
	"#efd140":"cx-theme_yellow_3",
	"#d2a62f":"cx-theme_yellow_4",
	"#a87c22":"cx-theme_yellow_5",

	"#ffcf68":"cx-theme_orange_1",
	"#fabf40":"cx-theme_orange_2",
	"#ec932f":"cx-theme_orange_3",
	"#cd6c22":"cx-theme_orange_4",
	"#a24f19":"cx-theme_orange_5",

	"#fd91a1":"cx-theme_red_1",
	"#f66a77":"cx-theme_red_2",
	"#e14d57":"cx-theme_red_3",
	"#bb383f":"cx-theme_red_4",
	"#a93439":"cx-theme_red_5",

	"#e9a0e7":"cx-theme_violet_1",
	"#c478c2":"cx-theme_violet_2",
	"#965994":"cx-theme_violet_3",
	"#804b7d":"cx-theme_violet_4",
	"#6e406a":"cx-theme_violet_5",

	"#cfbaa3":"cx-theme_brown_1",
	"#b89876":"cx-theme_brown_2",
	"#9d7952":"cx-theme_brown_3",
	"#896a49":"cx-theme_brown_4",
	"#7a5d3f":"cx-theme_brown_5",

	"#dfd6c9":"cx-theme_warm_gray_1",
	"#beb3a8":"cx-theme_warm_gray_2",
	"#9a9289":"cx-theme_warm_gray_3",
	"#7e766e":"cx-theme_warm_gray_4",
	"#655e57":"cx-theme_warm_gray_5",

	"#000000":"cx-theme_000",
	"#222222":"cx-theme_222",
	"#444444":"cx-theme_444",
	"#666666":"cx-theme_666",
	"#888888":"cx-theme_888",
	"#aaaaaa":"cx-theme_aaa",
	"#cccccc":"cx-theme_ccc",
	"#eeeeee":"cx-theme_eee",
	"#ffffff":"cx-theme_fff"
};

CXTheme.getCurrentThemeName = function() {
	var name = "light";
	if(CXTheme.current && CXTheme.current == CXTheme.themes["dark"]) {
		name = "dark";
	}
	return name;
};

CXTheme.setTheme = function(name){
	CXTheme.current = CXTheme.themes[name];
	if (!CXTheme.current) CXTheme.current = CXTheme.themes["default"];
};

CXTheme.setTheme("default");


CXTheme.getThemeColour = function(colourKey) {
	if (colourKey && colourKey.indexOf("cx-theme") != -1) {
		return CXTheme.current[colourKey];
	}

	return colourKey;
};

CXTheme.migrateThemeColour = function(colourHexCode) {
	var themeColor;

	if (colourHexCode && colourHexCode.indexOf("#") != -1) {
		themeColor = CXTheme.themeColourMigrationMap[colourHexCode.toLowerCase()];

		if (themeColor) return themeColor;
	}

	return colourHexCode;
};;/****** update_manager.js *******/ 

var updateManager = (function () { // eslint-disable-line no-unused-vars

	var q = [];
	var qlen = 0;
	var async = true;
	var paused = false;
	var debug = false;
	var batchData = {};

	setTimeout(processQueue, 0);

	function queue(cx, opts) {
		var existingBatchId;
		var currentBatchId;
		var i;
		if ( !async ) {
			try {
				cx.update();
			} catch (e) {
				handleUpdateException(cx, e);
			}
		}else{
			// is it already queued?
			for (i = 0; i < qlen; i++) {
				existingBatchId = q[i].opts && q[i].opts.batchId;
				currentBatchId = opts && opts.batchId;
				//If this component has been previously queued, but there is a callback and batchId
				//associated with the previous queued item or the current one, allow it to be queued again
				if (q[i].cx == cx && !existingBatchId && !currentBatchId) {
					return;
				}
			}

			if ( debug ) console.log("q-added",cx.displayName); // eslint-disable-line no-console

			q.push( {cx:cx, opts:opts} );
			if ( opts && opts.batchId ) {
				pushBatchData( opts.batchId, opts.batchCallback );
			}
			qlen++;
		}
	}

	function getBatchInfo( id ) {
		if ( !batchData[ id ] ) {
			batchData[id] = { items:[], cb:false, count:0 };
		}

		return batchData[id];
	}

	function pushBatchData( batchId, cb ) {
		var bi = getBatchInfo(batchId);

		bi.count++;
		bi.cb = cb;
	}

	function popBatchData( batchId ) {
		var bi = getBatchInfo(batchId);
		bi.count--;

		if ( bi.count == 0 ){
			if (bi.cb) bi.cb();
			delete bi.cb;
		}
	}


	function processQueue() {
		var qcopy, i, cx, opts, n, r, visible;
		if (!paused && qlen != 0) {
			qcopy = [];
			i = 0;
			for (i = 0; i < qlen; i++) {
				qcopy[i] = q[i];
			}

			q = [];
			qlen = 0;

			for (i = 0; i < qcopy.length; i++) {
				cx = qcopy[i].cx;
				opts = qcopy[i].opts;

				if ( debug ) {
					n = cx.displayName;
					r = "";
					visible = cx.isKlip ? cx.isVisible : cx.getKlip().isVisible;

					if ( opts ) r = opts.r;

					console.log("update", n, r, visible); // eslint-disable-line no-console
				}

				try {
					qcopy[i].cx.update();
				} catch (e) {
					handleUpdateException(qcopy[i].cx, e);
				}

				if (qcopy[i].opts && qcopy[i].opts.batchId) {
					popBatchData(qcopy[i].opts.batchId);
				}
			}
		}

		setTimeout(processQueue, 100);
	}

	function handleUpdateException(cx, e) {
		var klip = cx.getKlip();
		var message = "An error occurred while updating component/klip: " + cx.id + "/" + klip.id + " (" + cx.factory_id + ")";

		console.log(message); // eslint-disable-line no-console
		console.log(e); // eslint-disable-line no-console

		klip.addMessage({ type:"cx-update", id:cx.id, asset:cx }, "error");

		$.post("/error/ajax_logMessage", {msg:message});
	}

	function setAsync(b) {
		async = b;
	}

	function setPaused(b) {
		paused = b;
	}

	function isEmpty() {
		return q.length == 0;
	}

	function _queueLength() {
		return qlen;
	}

	function _emptyQueue() {
		q = [];
		qlen = 0;
	}

	return { "queue":queue, "setAsync":setAsync, "setPaused":setPaused, "isEmpty":isEmpty, "_queueLength" : _queueLength, "_emptyQueue" : _emptyQueue };

})();;/****** animate_manager.js *******/ 

/* global dashboard:false */

var animateManager = (function () { //eslint-disable-line no-unused-vars

    var q = [];
    var qlen = 0;
    var async = true;
    var paused = false;

    setTimeout(processQueue, 0);

    function queue(k) {
        var i;
        if (k) {
            if ( !async ){
                if (k.animate) {
                    dashboard.highlightKlipRefresh(k);
                }
            }else{
                for (i = 0; i < qlen; i++)
                    if (q[i] == k) return;

                q.push(k);
                qlen++;
            }
        }
    }

    function processQueue() {
        var qcopy, i;
        if (!paused && qlen != 0) {
            qcopy = [];
            i = 0;
            for (i = 0; i < qlen; i++)
                qcopy[i] = q[i];

            q = [];
            qlen = 0;

            for (i = 0; i < qcopy.length; i++) {
                if (qcopy[i].animate) {
                    dashboard.highlightKlipRefresh(qcopy[i]);
                }
            }
        }

        setTimeout(processQueue, 100);
    }

    function setAsync(b) {
        async = b;
    }

    function setPaused(b) {
        paused = b;
    }

    return { "queue":queue , "setAsync":setAsync, "setPaused":setPaused };

})();;/****** cx.proxy.js *******/ 

/**
 * Proxy components provide a way for the workspace to interact with complex
 * components in consistent way.  They do not render any dom.
 *
 * Table charts, for example
 */
/*global Component:false, bindValues:false, updateManager:false, KlipFactory:false */
Component.Proxy = $.klass(Component.DataComponent, {

  factory_id: "proxy",
  isProxy: true,
  role: false,
  selected: false,
  mixin: false,
  isDraggable: false,
  isDstRoot: false,

  /**
   * renderDom is overriden to not do anything at all! eg, it has no html dom so it does not call super
   * @param $super
   */
  renderDom: function ($super) {

  },

  onParentAssignment: function () {
    var compRole = this.role;
    var parentFactoryId = this.parent.factory_id;

    switch (compRole) {
      case "marker_lat":
      case "marker_lng":
        this.canSetAggregation = true;
        break;
      case "region_id":
        this.canGroup = true;
        this.canFilter = true;
        this.canSetAggregation = true;
        break;
      case "values":
      case "labels":
        if (parentFactoryId && parentFactoryId === "input") {
          this.canSort = true;
          this.canFilter = true;
          this.canGroup = true;
          this.canSetAggregation = true;
        }
        break;
      case "start":
      case "end":
        if (parentFactoryId == "range") {
          this.isDstRoot = true;
          this.canAggregate = true;
          this.canSetAggregation = true;
          this.canHaveDataSlots = true;
        }
        break;
      default:
        break;
    }
  },

  getWorkspaceControlModel: function ($super) {
    var model = $super();

    if (this.parent.factory_id == "range" || this.parent.factory_id
        == "table_col") {
      model = this.parent.getWorkspaceControlModel();
    }

    return model;
  },

  /**
   * overriden to prevent the default behaviour of adding a selection class to the component's
   * 'el'ement. Sets a flag which the parent will use in updating to select this component visually.
   * Update on deselection only if selecting another type of component to avoid a double update.
   */
  setSelectedInWorkspace: function ($super, selectionState, newComponent) {
    $super(selectionState);

//		if (selectionState || (!newComponent || newComponent.factory_id != this.factory_id))
//		{
//			this.update();
//		}
  },

  update: function ($super) {
    $super();
		if (this.parent) {
			updateManager.queue(this.parent, {r: "proxy-updated"});
		}
  },

  configure: function ($super, config) {
    $super(config);

    if (config.mixin) {
      this.mixin = KlipFactory.instance.newInstanceOf(config.mixin);
    }
    if (this.mixin) {
      bindValues(this, config, this.mixin.boundValues);
    }
		if (this.onConfigure) {
			this.onConfigure(config);
		}
    this.invalidate();
  },

  serialize: function ($super) {
    var cfg = $super();

    if (this.mixin) {
      cfg.mixin = this.mixin.factory_id;
      bindValues(cfg, this, this.mixin.boundValues);
    }

    return cfg;
  },

  invalidate: function ($super) {
    $super();
    if (this.parent) {
      this.parent.invalidate();
    }
  },

  getPropertyModel: function ($super) {
    var model = $super();
		if (this.mixin) {
			$.merge(model,
					_.bind(this.mixin.getPropertyModel, this)());
		}

    return model;
  },
  getSupportedDst: function ($super) {
    var dst = $super();
    var parentFactoryId;

    if (this.parent) {
      parentFactoryId = this.parent.factory_id;
    }

    if (parentFactoryId && parentFactoryId === "input" && this.factory_id
        != "data_slot") {
      dst = this.updateSupportedDst(dst);
    }

    return dst;
  },

  drawDstHints: function () {
    if (this.parent && this.parent.drawDstHints) {
      this.parent.drawDstHints(this);
    }
  },

  setData: function ($super, data, msg) {
    $super(data, msg);
    if (this.onData) {
      this.onData(data);
    }
  },

  getAvailableAggregations: function ($super) {
    var parentId = this.parent.factory_id;
    var defaultAggregations = $super();
    var aggs;

    var aggregations = defaultAggregations;
    var inputcontrolLabelAggregations = [{
      value: "first",
      label: "First",
      "default": true
    }, {value: "last", label: "Last"}];
    switch (parentId) {
      case "map_marker":
      case "input":
        aggs = this.getNonDefaultAggregationsByRole(this.role);
        if (aggs && this.role != "labels") {
          aggregations = aggs;
        } else if(this.role == "labels") {
          aggregations=inputcontrolLabelAggregations;
        }
        break;
      default:
        break;
    }
    return aggregations;
  },

  getHeader: function () {
    var header = this.name;
    var value;

    //If the header was renamed by the user then the user header should have priority over FRAH
    if (this.parent.firstRowIsHeader && !this.isVCFeatureOn()) {
      // For old first row header, if the data is empty, we leave the header unchanged.
      value = this.getData(0, 0);
      if (value) {
        header = value.toString();
      }
    }
    return header;
  },

  getHeaderName: function () {
    var header = this.displayName;

    if(this.customHeader && this.headerProxy && this.headerProxy.displayName) {
      header = this.headerProxy.displayName;
    }

    if(header) {
      return header;
    }

    return this.getHeader();
  }

});
;/****** cx.chart_common.js *******/ 

/* global Component:false */

/**
 * The common properties used in the charts
 * TODO: we can do further refactoring to extract the common method and properties for all charts
 * The goal is to reduce code duplication and chance of fixing an issue only for one component
 */
Component.ChartCommon = $.klass(Component.Base, {

	onResize : function($super) {
		$super();
		if ( !this.el ) return;

		this.clearPlot();
	},

	onEvent : function ($super, evt) {
		if (evt.id == "theme_changed") {
			this.clearPlot();
		}
	},

	invalidateAndQueue : function() {
		this.clearPlot();
	},

	getFontFamily : function() {
		return "Soleto, sans-serif";
	}


});
;/****** cx.chart_cartesian.js *******/ 

/* global Component:false, page:false, updateManager:false, Highcharts:false, isWorkspace:false, mixPanelTrack:false */

// Superclass used by cartesian chart types (i.e. charts with x- and y-axes).
Component.ChartCartesian = $.klass(Component.ChartCommon, {

    chartSizes : [0, 146, 221, 296, 396],
    height: 2,
    invertAxes: false,
    plotId: false,
    showLegend: true,
    mixPanelErrorCount: 0,
    zoom: "",

    update : function ($super) {
        $super();

        // Add an error handler to catch all the Highcharts errors.
        this.addHighchartsErrorHandler()
    },

    onRightClick : function(evt,compIndex) {
        var target;

        evt.preventDefault();
        evt.stopPropagation();

        if (compIndex) {
            target = this.components[compIndex];
        } else {
            target = this.getSelectionTarget(evt);
            if (!target) {
                target = this;
            }
        }
        page.invokeAction("select_component", target, evt);
        page.invokeAction("show_context_menu", { target:target, menuCtx:{ isRoot:(target == this) } }, evt);
    },

    getSelectionTarget : function(evt) {
        var target = false;
        var $target = $(evt.target);

        if ($target.is(".comp") || $target.is(".comp-drag-handle")) {
            target = this;
        } else {
            target = this.getChildById($(evt.target).attr("axis_id"));
        }

        return target;
    },

    clearPlot : function() {
        // things needed to trigger a replot
        this.$chart.empty();
        this.chart = null;
        this.invalidate();
        updateManager.queue(this, {r:"chart-clearPlot"});
    },

    initializeForWorkspace : function(workspace) {
        var klip;
        var noop = function(){};

        this.el.click($.bind(this.onClick || noop, this))
            .on("contextmenu", $.bind(this.onRightClick, this));

        klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(500);
        }
    },

    addHighchartsErrorHandler: function () {
        var _this = this;

        Highcharts.error = function (code) {
            switch (code) {
                case 12 :
                    _this.mixPanelErrorCount++;
                    console.log("The number of data points exceeded 1000. https://www.highcharts.com/errors/12"); // eslint-disable-line no-console
                    if (isWorkspace()) {
                        _this.handleHighchartsError();
                    } else {
                        _this.showErrorOnDashboard();
                    }
                    break;
            }
        }
    },

    handleHighchartsError: function () {
        var chartIndex,x,y;
        var chart;
        var imageURL = "/images/klips/error12-editor.svg";
        var IMAGE_WIDTH = 200;
        var IMAGE_HEIGHT = 50;

        for (chartIndex = 0; chartIndex < Highcharts.charts.length; chartIndex++) {
            if (Highcharts.charts[chartIndex]) {
                chart = Highcharts.charts[chartIndex];
                if (chart.renderTo && chart.renderTo.id && chart.renderTo.id.indexOf(this.plotId) !== -1) {
                    x = chart.plotLeft + (chart.plotWidth / 2) - (IMAGE_WIDTH / 2);
                    y = chart.plotTop + (chart.plotHeight / 2) - (IMAGE_HEIGHT / 2);

                    chart.renderer
                        .image(imageURL)
                        .attr({
                            id: "highcharts-error-12",
                            x: x,
                            y: y,
                            width: IMAGE_WIDTH
                        })
                        .add();
                }
            }
        }
    },

    showErrorOnDashboard: function () {
        var widgetName = KF.company.get("brand", "widgetName");

        var klip = this.getKlip();
        var message = "An error occurred while updating component/" + widgetName + ": " + this.id + "/" + klip.id + " (" + this.factory_id + "). Too many ticks drawn error from Highcharts";

        klip.addMessage({type: "cx-update-too-many-data-points", id: this.id, asset: this}, "error");

        $.post("/error/ajax_logMessage", {msg: message});

        if(typeof mixPanelPeopleIncrement == "function" && this.mixPanelErrorCount === 1) {
            mixPanelPeopleIncrement(MIXPANEL_VIZ_TOO_MUCH_DATA);
        }
    },

    setupClickHandling : function() {
        var _this = this;
        var y = 0;
        var addClickHandler, addRightClickHandler, onAxisRedraw, onSeriesRedraw;

        if (!this.chart) {
            return;
        }



        addClickHandler = function(axis, $element) {
            var chartIndex = 0;
            $element.attr("axis_id", axis.options.id);
            $element.attr("is_x_axis", axis.isXAxis);
            $element.off("click");
            $element.click(function (e) {
                var isXAxis = $(this).attr("is_x_axis");
                var axis;

                e.stopPropagation();
                if(isXAxis === "true") {
                    axis = _this.chart.xAxis[0];
                } else {
                    for(chartIndex = 0; chartIndex < _this.chart.yAxis.length; chartIndex++) {
                        if(_this.chart.yAxis[chartIndex].options.id == $(this).attr("axis_id")) {
                            axis = _this.chart.yAxis[chartIndex];
                            break;
                        }
                    }
                }

                page.invokeAction("select_component", _this.getChildById(axis.options.id));
            });
        };

        addRightClickHandler = function (series, $element) {
            $element.attr("series_id", series.options.id);
            $element.on("contextmenu", function (event) {
                var compIndex = $(this).attr("series_id").slice(-1);
                _this.onRightClick(event, compIndex)
            });
        };

        onAxisRedraw = function(axis) {
            // Click handling for the axis ticks
            var $axisTickEl;
            var $axisTitleEl;
            var tickPos;

            for (tickPos in axis.ticks) {
                if(axis.ticks[tickPos].label && $(axis.ticks[tickPos].label.element).length) {
                    $axisTickEl = $(axis.ticks[tickPos].label.element).children();
                    addClickHandler(axis, $axisTickEl);
                }
            }

            // Click handling for the axis titles
            if(axis.axisTitle) {
                $axisTitleEl = $(axis.axisTitle.element);
                addClickHandler(axis, $axisTitleEl);
            }
        };

        // Add click handling to all y-axes in the chart (bar/line charts may have more than one y-axis)
        for(y = 0; y < this.chart.yAxis.length; y++) {
            onAxisRedraw(this.chart.yAxis[y]);
            this.chart.yAxis[y].redraw(onAxisRedraw);
        }

        // Add click handling to the x-axis
        onAxisRedraw(this.chart.xAxis[0]);
        this.chart.xAxis[0].redraw(onAxisRedraw);

        onSeriesRedraw = function(series) {
            var legendItems,chartElements,scatterChart;
            if (series.legendGroup) {
                legendItems = $(series.legendGroup.element);
                addRightClickHandler(series, legendItems);
            }
            if (series.group) {
                chartElements = $(series.group.element);
                addRightClickHandler(series, chartElements)
            }

            if (series.markerGroup) {
                scatterChart = $(series.markerGroup.element);
                addRightClickHandler(series, scatterChart);
            }
        };

        this.chart.series.forEach(function(series) {
            onSeriesRedraw(series);
            series.redraw(onSeriesRedraw);
        });

    },

    drawSelectionBox : function(selectedComponent, index, chartHasRightAxis) {
        var boxX = 0;
        var boxY = 0;
        var boxHeight = 0;
        var boxWidth = 0;
        var axisWidth = 0;
        var extraPaddingLeft = 0;
        var extraPaddingRight = 0;
        var chartYAxis, chartXAxis;
        var legendObject, legendOriginX, legendOriginY;
        var j;

        // The selection boxes must have margins around it as defined below
        var BOX_MARGIN_COMPONENT_BORDER = 3;
        var BOX_MARGIN_AXES = 3;
        var BOX_MARGIN_SHORT_SIDES = 1;
        var VERTICAL_AXIS_TOP_PADDING = 10;
        var VERTICAL_AXIS_BOTTOM_PADDING = 20; // The box needs to grow by 10 pixels at the bottom + the 10 pixels added to the top
        var PADDING_FOR_ANGLED_X_AXIS_LABELS = 25;

        if(!selectedComponent) return;

        if(selectedComponent.role == "axis_y") {
            // Draw a box around the selected y-axis
            for(j = 0; j < this.chart.yAxis.length; j++) {
                if(this.chart.yAxis[j].options.id == selectedComponent.id) {
                    chartYAxis = this.chart.yAxis[j];
                    break;
                }
            }

            if(chartYAxis) {
                // A y-axis may be on the left-hand side (opposite == false) or right-hand side (opposite == true)
                if(this.invertAxes) {
                    // In an inverted chart left-hand y-axes are on the bottom, and right-hand y-axes are on the top.
                    if(selectedComponent.showLabel) {
                        boxX = chartYAxis.left + BOX_MARGIN_SHORT_SIDES;
                        boxY = chartYAxis.opposite ? chartYAxis.top - chartYAxis.transB + chartYAxis.offset + BOX_MARGIN_COMPONENT_BORDER : chartYAxis.top + chartYAxis.height + chartYAxis.offset + BOX_MARGIN_AXES;
                        boxHeight = chartYAxis.transB - BOX_MARGIN_COMPONENT_BORDER - BOX_MARGIN_AXES;
                        boxWidth = chartYAxis.len - BOX_MARGIN_SHORT_SIDES;
                    } else {
                        boxX = chartYAxis.labelGroup.element.getBBox().x;
                        boxY = chartYAxis.labelGroup.element.getBBox().y - (chartYAxis.opposite ? BOX_MARGIN_AXES : BOX_MARGIN_COMPONENT_BORDER);
                        boxHeight = chartYAxis.labelGroup.element.getBBox().height + BOX_MARGIN_COMPONENT_BORDER + BOX_MARGIN_AXES;
                        boxWidth = chartYAxis.labelGroup.element.getBBox().width;
                    }
                } else {
                    // If the axis label is shown, calculate the box position based on the axisGroup element.  If the label is not shown,
                    // the labelGroup element must be used instead (the axisGroup element in this case is only the axis line).
                    if(selectedComponent.showLabel) {
                        axisWidth = chartYAxis.axisGroup.element.getBBox().width;
                        boxX = chartYAxis.opposite ? chartYAxis.width + chartYAxis.left + chartYAxis.offset + BOX_MARGIN_AXES : chartYAxis.left - axisWidth + chartYAxis.offset;
                        boxY = chartYAxis.top - VERTICAL_AXIS_TOP_PADDING;
                        boxHeight = chartYAxis.height + VERTICAL_AXIS_BOTTOM_PADDING;
                        boxWidth = axisWidth - BOX_MARGIN_AXES;
                    } else {
                        boxX = chartYAxis.labelGroup.element.getBBox().x - (chartYAxis.opposite ? BOX_MARGIN_AXES : BOX_MARGIN_COMPONENT_BORDER);
                        boxY = chartYAxis.labelGroup.element.getBBox().y;
                        boxHeight = chartYAxis.labelGroup.element.getBBox().height;
                        boxWidth = chartYAxis.labelGroup.element.getBBox().width + BOX_MARGIN_COMPONENT_BORDER + BOX_MARGIN_AXES;
                    }
                }
            }
        } else if(selectedComponent.role == "axis_x") {
            // Draw a box around the x-axis.  All cartesian charts in Klipfolio can only have one x-axis.
            chartXAxis = this.chart.xAxis[0];

            // If the axis labels are angled, add extra padding to the selection box to accommodate them.
            if(selectedComponent.labelAngle == "-45") {
                extraPaddingLeft = PADDING_FOR_ANGLED_X_AXIS_LABELS;
            } else if(selectedComponent.labelAngle == "45") {
                extraPaddingLeft = 5;
                extraPaddingRight = chartHasRightAxis ? PADDING_FOR_ANGLED_X_AXIS_LABELS : 0;
            }

            if(this.invertAxes) {
                // In an inverted chart the x-axis is on the left-hand side
                boxX = BOX_MARGIN_COMPONENT_BORDER;
                boxY = chartXAxis.top - VERTICAL_AXIS_TOP_PADDING - extraPaddingRight;
                boxHeight = chartXAxis.height + VERTICAL_AXIS_BOTTOM_PADDING + extraPaddingLeft + extraPaddingRight;
                boxWidth = chartXAxis.left - BOX_MARGIN_COMPONENT_BORDER - BOX_MARGIN_AXES;
            } else {
                boxX = chartXAxis.left + BOX_MARGIN_SHORT_SIDES - extraPaddingLeft;
                boxY = this.chart.chartHeight - chartXAxis.bottom + BOX_MARGIN_AXES;
                boxHeight = chartXAxis.bottom - BOX_MARGIN_COMPONENT_BORDER - BOX_MARGIN_AXES;
                boxWidth = chartXAxis.len - BOX_MARGIN_SHORT_SIDES + extraPaddingLeft + extraPaddingRight;
            }
        } else {
            // A series is selected.  Draw the selection box around the corresponding legend item if the legend is displayed.
            if(this.chart.legend.allItems) {
                // Get the legend object - this is the Highcharts representation of the legend item box + text
                legendObject = this.chart.legend.allItems[index];

                // Determine the origin of the entire legend box
                legendOriginX = this.chart.legend.group.translateX;
                legendOriginY = this.chart.legend.group.translateY + this.chart.legend.initialItemY;

                // _legendItemPos contains the legend item's offset within the legend box.
                boxX = legendOriginX + legendObject._legendItemPos[0];
                boxY = legendOriginY + legendObject._legendItemPos[1];
                boxHeight = legendObject.legendItem.getBBox().height + 1;
                boxWidth = legendObject.legendItem.getBBox().width + 25;
            }
        }

        // See http://api.highcharts.com/highcharts#Renderer.rect
        this.chart.renderer.rect(boxX, boxY, boxWidth, boxHeight, 0).attr({fill: "rgba(255, 255, 255, 0.1)", stroke: "#15A7E2", "stroke-width": 1}).add();
    },

    createSVGImage : function (imageUrl, attrs, titleText) {
        var title, titleTextNode;
        var tagElement= document.createElementNS("http://www.w3.org/2000/svg", "image");
        var attr;

        for (attr in attrs) {
            tagElement.setAttribute(attr, attrs[attr]);
        }
        tagElement.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", imageUrl);

        if(titleText) {
            title = document.createElementNS("http://www.w3.org/2000/svg", "title");
            titleTextNode = document.createTextNode(titleText);
            title.appendChild(titleTextNode);
            tagElement.appendChild(title);
        }
        return tagElement;
    },

    findComponentBySeriesId: function (seriesId) {
        var componentId = seriesId.slice(seriesId.indexOf("-") + 1);
        return this.components[componentId];
    },

    drawDstHints : function() {
        var hintDimension = 14;
        var pixelPadding = 1;
        var filterHintIcon = "/images/workspace/dst-hints/dst-hint-filter.svg";
        var sortAscHintIcon = "/images/workspace/dst-hints/dst-hint-sort-asc.svg";
        var sortDescHintIcon = "/images/workspace/dst-hints/dst-hint-sort-desc.svg";

        if (!this.hasDstActions() || !this.chart) return;

        // Add hints to each series
        this.chart.series.forEach(function(series) {
            var legendItems, dimId, dstOps, visualCueTitle;
            var legendBox, xPos, yPos, sortHintIcon;

            var correspondingComponent = this.findComponentBySeriesId(series.options.id);

            if(!correspondingComponent) {
                return;
            }

            dimId = correspondingComponent.getVirtualColumnId();
            dstOps = this.getDstOperationsAppliedToDimensionId(dimId);

            if (series.legendGroup) {
                legendBox = series.legendGroup.element.getBBox(); //return object that defines the legend element's bounding box.

                // set the hint icon's position based on x and y coordinate of the legend item box.
                xPos = Math.round(legendBox.x + legendBox.width + 3*pixelPadding);
                yPos = Math.round(legendBox.y) + 2*pixelPadding;

                legendItems = $(series.legendGroup.element);

                if(dstOps.filtered) {
                    legendItems.append(this.createSVGImage(filterHintIcon, {x:xPos, y:yPos, width:hintDimension, height:hintDimension}, "Filter is applied"));
                    xPos = xPos + hintDimension;
                }

                if(dstOps.sorted!==0) {
                    sortHintIcon = sortAscHintIcon;
                    if(dstOps.sorted==2) {
                        sortHintIcon = sortDescHintIcon;
                    }
                    visualCueTitle = correspondingComponent.getTitleBySortOrder(dstOps.sorted);
                    legendItems.append(this.createSVGImage(sortHintIcon, {x:xPos, y:yPos, width:hintDimension, height:hintDimension}, "Sorted "+visualCueTitle));
                }
            }
        }.bind(this));

        this.chart.xAxis.forEach(function(xAxis) {
            var xAxisLabel, dimId, dstOps, visualCueTitle;
            var labelBox, xPos, yPos, verticalAxisPixelDistance = 5, transformation, sortHintIcon;

            var correspondingComponent = this.getChildById(xAxis.options.id);

            if(!correspondingComponent) {
                return;
            }

            dimId = correspondingComponent.getVirtualColumnId();
            dstOps = this.getDstOperationsAppliedToDimensionId(dimId);

            if(xAxis.axisTitle) {
                xAxisLabel = $(xAxis.axisTitle.element);
                labelBox = xAxis.axisTitle.element.getBBox();

                xPos = labelBox.x + labelBox.width + pixelPadding;
                yPos = labelBox.y + pixelPadding;

                // If the  axis are rotated
                if(xAxis.axisTitle.rotation === 270) {
                    xPos = labelBox.height - verticalAxisPixelDistance;
                    yPos = labelBox.y - labelBox.width/2 + 2*verticalAxisPixelDistance;
                }
                xPos = Math.round(xPos);
                yPos = Math.round(yPos);

                transformation = "translate(0, 0) rotate("+ xAxis.axisTitle.rotation +", "+xPos+", "+yPos+")";
                if(dstOps.filtered) {
                    xAxisLabel.parent().append(this.createSVGImage(filterHintIcon, {x:xPos, y:yPos, width:hintDimension, height:hintDimension, transform: transformation}, "Filter is applied"));
                    xPos = xPos + hintDimension;
                }

                if(dstOps.sorted!==0) {
                    sortHintIcon = sortAscHintIcon;
                    if(dstOps.sorted == 2) {
                        sortHintIcon = sortDescHintIcon;
                    }
                    visualCueTitle = correspondingComponent.getTitleBySortOrder(dstOps.sorted);
                    xAxisLabel.parent().append(this.createSVGImage(sortHintIcon, {x:xPos, y:yPos, width:hintDimension, height:hintDimension, transform: transformation}, "Sorted "+ visualCueTitle));
                }
            }

        }.bind(this));
    },

    // If hiding a series in response to clicking on the legend item would leave the chart empty, unhide all series
    checkAndSetSeriesVisibility: function(event) {
        var s = 0;
        var v = 0;
        var allowDefault = true;
        var chartSeries = event.target.chart.series;
        var showAllSeries;
        var optionsString;
        var componentId;

        if(event.target.chart) {
            showAllSeries = true;
            for(s = 0; s < chartSeries.length; s++) {
                if(chartSeries[s].visible && chartSeries[s] != event.target) {
                    showAllSeries = false;
                    break;
                }
            }

            if(showAllSeries) {
                for(v = 0; v < chartSeries.length; v++) {
                    optionsString = chartSeries[v].options.id;
                    componentId = optionsString.slice(optionsString.indexOf("-") + 1);

                    if(!this.components[componentId].seriesHidden) {
                        // Don't unhide series that hidden by default.
                        chartSeries[v].show();
                    }

                    if(chartSeries[v] == event.target && this.components[componentId].seriesHidden) {
                        // If the series that was clicked on should be hidden by default, hide it (otherwise it will
                        // stay visible due to preventing the default below).
                        chartSeries[v].hide();
                    }
                }

                // Return false to prevent the default, otherwise the selected series will still get hidden
                allowDefault = false;
            }
        }

        return allowDefault;
    },

    changeHeight : function() {
        if (!this.el) return;

        this.chartHeight = 0;
        if (this.height == 5) {
            // Custom chart size
            this.chartHeight = this.customHeight ? parseInt(this.customHeight) : this.chartSizes[2];
            if (this.chartHeight < 100) this.chartHeight = 100;
        } else {
            this.chartHeight = this.chartSizes[parseInt(this.height)];
        }

        this.setHeight(this.chartHeight);
    },

    /**
     * Determine the approximate number of lines the legend will occupy.
     * @param seriesNames legend entries
     * @param fontFamily
     * @param itemDistance space to reserve to the right of each legend entry
     * @param legendLeftPos The x position where the legend starts
     */
    determineNumberOfLegendLines : function(seriesNames, fontFamily, itemDistance, legendLeftPos) {
        var sn;
        var maxLegendWidthBuffer;
        var SWATCH_WIDTH = 21;	//default Highcharts symbolWidth is 16 and default symbolPadding is 5
        var PADDING = 8; //Default Highcharts legend padding
        var approxNumLegendLines = 1;
        var currentLineWidth = 0;
        var itemsOnLine = 0;

        /*
         Use the same calculation as Highcharts algorithm in renderItem, included below:
         Note initialItemX ends up being padding

         // if the item exceeds the width, start a new line
         if (horizontal && legend.itemX - initialItemX + itemWidth >
         (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
            legend.itemX = initialItemX;
            legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
            legend.lastLineHeight = 0; // reset for next line
         }*/
        var maxLegendWidth = this.el.width() - 2*PADDING - PADDING - legendLeftPos;

        // The hidden div will be used to determine the width in pixels of each series name when rendered.
        var $hiddenDiv = $("<div>").css({
            position: "absolute",
            visibility: "hidden",
            width: "auto",
            height: "auto",
            whiteSpace: "nowrap",
            fontFamily: fontFamily,
            fontSize: "13px"
        });
        $hiddenDiv.appendTo(this.el);

        for(sn = 0; sn < seriesNames.length; sn++) {
            itemsOnLine++;
            $hiddenDiv.empty();
            $hiddenDiv.html(seriesNames[sn]);
            currentLineWidth += itemDistance + SWATCH_WIDTH + $hiddenDiv.width();

            //Highcharts uses SVG widths which can be +/- 0.5 compared to the html width. Prefer allocating to much space to too little space
            maxLegendWidthBuffer = maxLegendWidth - itemsOnLine*0.5;
            if(currentLineWidth >= maxLegendWidth || ( sn == seriesNames.length-1 && currentLineWidth >= maxLegendWidthBuffer ) ) {
                // The current series name becomes the start of a new line in the legend
                approxNumLegendLines++;
                itemsOnLine = 0;
                currentLineWidth = itemDistance + SWATCH_WIDTH + $hiddenDiv.width();
            }
        }
        $hiddenDiv.remove();
        return approxNumLegendLines;
    }

});
;/****** cx.chart_single_series.js *******/ 

/* global Component:false, KlipFactory:false, page:false, updateManager:false */
// Superclass used by single-series chart types
Component.ChartSingleSeries = $.klass(Component.ChartCommon, {

    size: 2,
    height: 160,
    width: false,
    legendPad : 20,

    chartSizes : [ 120,160,230,310 ], //the heights for small, medium, large, x-large

    overrideColors: false,

    legend_labels: 2, //show labels in the legend - valid for pie and funnel chart
    legend_values: true,

    chart: null,
    plotId : false,
    canHaveDataSlots: true,

    selectionBoxCss: {fill: "rgba(255, 255, 255, 0.1)", stroke: "#15A7E2", "stroke-width": 1},

    renderDom : function($super) {
        $super();
        this.plotId = this.id + "-" + _.uniqueId();
        this.$plot = $("<div>").attr("id", this.plotId + "-plot").appendTo(this.el);
    },

    clearPlot : function() {
        // things needed to trigger a replot of
        this.$plot.empty();
        this.chart = null;
        this.invalidate();
        updateManager.queue(this, {r:"chart-clearPlot"});
    },

    initializeForWorkspace : function() {
        var klip = this.getKlip();

        this.el.on("contextmenu", $.bind(this.onRightClick, this));

        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(400);
        }
    },

    onRightClick: function(evt) {
        var target;

        evt.preventDefault();
        evt.stopPropagation();

        target = this.getSelectionTarget(evt);
        if (!target) return;

        page.invokeAction("select_component", target, evt);
        page.invokeAction("show_context_menu", { target:target, menuCtx:{ isRoot:(target == this) } }, evt);
    },

    getSelectionTarget: function(evt) {
        var $target = $(evt.target);
        var target, closestSubComponent;

        closestSubComponent = $target.closest("[sub_component_type]");
        target = this.getChildByType(closestSubComponent.attr("sub_component_type"));

        if (!target) {
            target = this;
        }

        return target;
    },

    setupClickHandling: function() {
        var values, labels;
        var chartIdSelector = "#" + this.id;

        if (!this.chart) return;

        values = this.chart.series[0];
        labels = this.chart.legend;

        if (values && values.group) {
            $(values.group.element).attr("sub_component_type", "series_data");
        }

        if (this.legend_labels == 1) {
            // The labels aren't instrumented in the Highcharts chart object, so access them the old-fashioned way with selectors.
            $(chartIdSelector + " .highcharts-data-labels").attr("sub_component_type", "series_labels");
        } else if (this.legend_labels == 2) {
            if (labels && labels.group) {
                $(labels.group.element).attr("sub_component_type", "series_labels");
            }
        }
    },

    drawSelectionBoxOrCircle : function() {
        // Single series charts only have two components, Values and Labels
        var selectedComponent;
        var c;

        for(c = 0; c < this.components.length; c++) {
            if(this.components[c].selected) selectedComponent = this.components[c];
        }
        if(!selectedComponent) return;

        if(selectedComponent.role == "data") {
            // The Values component is selected
            this.drawSelectionBoxAroundChart();
        } else if(selectedComponent.role == "labels") {
            // The Labels component is selected
            if(this.legend_labels == 1) {
                // The labels are displayed around the chart.
                this.drawSelectionBoxesAroundDataLabels();
            } else if(this.legend_labels == 2) {
                // The legend is displayed beside the chart (either on the right or bottom)
                this.drawSelectionBoxAroundLegend();
            }
        }
    },

    drawSelectionBoxAroundChart : function() {
        var CHART_MARGIN_PIE = 8;
        var CHART_MARGIN_FUNNEL = 2;
        var centerX, centerY, radius;
        var chartWidth, chartHeight, chartBoxTopLeftX, chartBoxTopLeftY;

        if(!this.chart) return;

        chartWidth = this.chart.plotSizeX;
        chartHeight = this.chart.plotSizeY;
        chartBoxTopLeftX = this.chart.plotLeft;
        chartBoxTopLeftY = this.chart.plotTop;

        if(this.factory_id == "chart_pie") {
            // Calculate the center and radius of the chart.  plotLeft and plotTop are the co-ordinates of the top-left corner of the pie chart bounding box.
            centerX = (chartBoxTopLeftX + (chartWidth + chartBoxTopLeftX)) / 2;
            centerY = (chartBoxTopLeftY + (chartHeight + chartBoxTopLeftY)) / 2;
            radius = (this.chartSize / 2) + CHART_MARGIN_PIE;

            this.chart.renderer.circle(centerX, centerY, radius).attr(this.selectionBoxCss).add();
        } else {
            this.chart.renderer.rect(chartBoxTopLeftX, 1, chartWidth, this.chartSize + CHART_MARGIN_FUNNEL, 0).attr(this.selectionBoxCss).add();
        }
    },

    drawSelectionBoxesAroundDataLabels : function() {
        var LABELS_AROUND_CHART_MARGIN = 2;
        var _this = this;
        var numberSeparator;
        var overallTransformX, overallTransformY;
        var dataLabelBoundingBox;
        var transform, transformX, transformY;
        var labelBoxX, labelBoxY, labelBoxWidth, labelBoxHeight;

        // Only draw boxes around the data labels for the currently selected Klip, otherwise boxes will be drawn for
        // every chart in a multi-component Klip.
        var chartIdSelector = "#" + this.id;

        // The labels aren't instrumented in the Highcharts chart object, so access them the old-fashioned way with selectors.
        var overallLabelTransform = $(chartIdSelector + " .highcharts-data-labels").attr("transform");

        overallLabelTransform = overallLabelTransform.slice(0, overallLabelTransform.indexOf(")") + 1);
        numberSeparator = overallLabelTransform.indexOf(",") != -1 ? "," : " ";

        overallTransformX = Number(overallLabelTransform.slice(overallLabelTransform.indexOf("(") + 1, overallLabelTransform.indexOf(numberSeparator)));
        overallTransformY = Number(overallLabelTransform.slice(overallLabelTransform.indexOf(numberSeparator) + 1, overallLabelTransform.indexOf(")")));

        $(".highcharts-data-labels > g").each(function() {
            if($(this).parents(chartIdSelector).length) {
                dataLabelBoundingBox = $(this)[0].getBBox();
                transform = $(this).attr("transform");

                transformX = Number(transform.slice(transform.indexOf("(") + 1, transform.indexOf(numberSeparator)));
                transformY = Number(transform.slice(transform.indexOf(numberSeparator) + 1, transform.indexOf(")")));

                labelBoxX = dataLabelBoundingBox.x + transformX + overallTransformX - LABELS_AROUND_CHART_MARGIN;
                labelBoxY = dataLabelBoundingBox.y + transformY + overallTransformY - LABELS_AROUND_CHART_MARGIN;
                labelBoxWidth = dataLabelBoundingBox.width + (LABELS_AROUND_CHART_MARGIN * 2);
                labelBoxHeight = dataLabelBoundingBox.height + (LABELS_AROUND_CHART_MARGIN * 2);

                _this.chart.renderer.rect(labelBoxX, labelBoxY, labelBoxWidth, labelBoxHeight, 0).attr(_this.selectionBoxCss).add();
            }
        });
    },

    drawSelectionBoxAroundLegend : function() {
        var LEGEND_MARGIN = 4;
        var legendBBox;
        var legendX, legendY, legendWidth, legendHeight, visibleLegendHeight;
        
        if(!this.chart) return;

        legendBBox = this.chart.legend.group.element.getBBox();
        legendX = legendBBox.x + this.chart.legend.group.translateX - LEGEND_MARGIN;
        legendY = legendBBox.y + this.chart.legend.group.translateY - LEGEND_MARGIN;
        legendWidth = legendBBox.width + (LEGEND_MARGIN * 2);
        legendHeight = legendBBox.height + (LEGEND_MARGIN * 2);
        visibleLegendHeight = this.chart.legend.group.getBBox().height;

        if(this.chart.legend.down) {
            // The down arrow for the legend scroll control is displayed, which means the bottom of the legend bounding box is outside
            // of the component area.  Instead adjust the box's height to be the height of the visible portion of the legend.
            legendHeight = visibleLegendHeight + LEGEND_MARGIN;
        }

        this.chart.renderer.rect(legendX, legendY, legendWidth, legendHeight, 0).attr(this.selectionBoxCss).add();
    },

    afterFactory : function() {
        if (!this.components || this.components.length == 0) {
            this.addComponent(KlipFactory.instance.createComponent({
                type:"series_data",
                role:"data",
                displayName:"Values"
            }));
            this.addComponent(KlipFactory.instance.createComponent({
                type:"series_labels",
                role:"labels"
            }));
        }

        this.getChildByRole("data").isDeletable = false;
        this.getChildByRole("labels").isDeletable = false;
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        this.addDataSlotControl(model);

        return model;
    }
});
;/****** cx.input_common.js *******/ 

/* global Component:false, Dashboard:false, bindValues:false, isWorkspace:false, isPreview:false, page:false, Props:false, dashboard:false */
// Superclass used by input components
Component.InputCommon = $.klass(Component.Base, {

    scope: 2,
    propName: "",
    defaultValue: "",

    controlHidden: "visible",
    higherScopedComponents: false,
    otherHiddenComponents: false,

    hideToken: false,
    pollHideToken: false,

    initialize: function($super) {
        $super();

        this.higherScopedComponents = [];
    },

    initializeForWorkspace: function() {
        var klip = this.getKlip();

        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(300);
        }
    },

    configure: function($super, config) {
        $super(config);

        if (config["~propertyChanged"]) {
            if ((config.propName && config.propName != this.propName) || (config.scope && config.scope != this.scope)) {
                if (this.scope == Dashboard.SCOPE_KLIP) {
                    this.deleteKlipProp();
                }
            }
        }

        bindValues(this, config, [
            "scope",
            "propName",
            "defaultValue",
            "controlHidden",
            "otherHiddenComponents"
        ]);

        if (config.scope) {
            // important to have as an int and not a string
            this.scope = parseInt(config.scope);
        }

        this.setVisible(this.controlHidden != "hidden");
    },

    serialize: function($super) {
        var cfg = $super();

        bindValues(cfg, this, [
            "scope",
            "propName",
            "defaultValue",
            "controlHidden",
            "otherHiddenComponents"
        ], {
            defaultValue: "",
            controlHidden: "visible",
            otherHiddenComponents: false
        }, true);

        return cfg;
    },

    onResize: function($super) {
        $super();

        this.invalidateAndQueue();
    },

    onEvent: function($super, evt) {
        var _this = this;
        var klip;

        $super(evt);

        switch (evt.id) {
            case "klip-show":
            case "added_to_tab":
                if (!isWorkspace()) {
                    if (evt.id == "added_to_tab" && !evt.isMobileWeb) break;

                    if (this.scope == Dashboard.SCOPE_KLIP && this.controlHidden == "scope-hidden") {
                        this.setVisible(true);
                        this._handleOtherHiddenComponents(false);

                        this.hideToken = PubSub.subscribe("scope-hide-input", function(evtName, args) {
                            if (_this._shouldHandleScopeHide(args.hide, args.variableName, args.dashboardId, args.klipComponentId)) {
                                _this._handleScopeHide(args.hide);
                            }
                        });

                        PubSub.publish("poll-scope-hide-input", { variableName: this.propName });
                    }

                    if (this.scope == Dashboard.SCOPE_TAB || this.scope == Dashboard.SCOPE_DASHBOARD) {
                        this.pollHideToken = PubSub.subscribe("poll-scope-hide-input", function(evtName, args) {
                            _this._handleScopeHidePoll(args.variableName);
                        });

                        klip = _this.getKlip();

                        PubSub.publish("scope-hide-input", {
                            klipComponentId: _this.getKlipComponentId(),
                            dashboardId: klip.currentTab,
                            variableName: this.propName,
                            hide: true
                        });
                    }
                }
                break;
            case "klip-hide":
                if (!isWorkspace()) {
                    PubSub.unsubscribe(this.hideToken);
                    PubSub.unsubscribe(this.pollHideToken);
                }
                break;
            default:
                break;
        }
    },

    _shouldHandleScopeHide: function(hide, variableName, dashboardId, higherScopedComponentId) {
        var klip = this.getKlip();
        var index;

        if (isPreview(klip.dashboard)) return false;
        if (variableName != this.propName || dashboardId != klip.currentTab) return false;

        index = this.higherScopedComponents.indexOf(higherScopedComponentId);

        if (hide) {
            if (index == -1) {
                this.higherScopedComponents.push(higherScopedComponentId);
            }
        } else {
            if (index >= 0) {
                this.higherScopedComponents.splice(index, 1);
            }

            if (this.higherScopedComponents.length > 0) {
                return false;
            }
        }

        return true;
    },

    _handleScopeHide: function(hide) {
        var klip = this.getKlip();

        // change visibility only if it's different from current visibility
        if (this.visible == hide) {
            this.setVisible(!hide);
            this._handleOtherHiddenComponents(hide);

            klip.setComponentPadding();
        }

        if (hide) {
            klip.dashboard.removeDashboardProp(Dashboard.SCOPE_KLIP, this.propName, klip.id, klip.currentTab);
            klip.dashboard.rewatchFormulas(klip, this.propName);
        }

        this.invalidateAndQueue();
    },

    _handleOtherHiddenComponents: function(hide) {
        var klip;

        if (this.otherHiddenComponents) {
            klip = this.getKlip();

            this.otherHiddenComponents.forEach(function(componentId) {
                var component = klip.getComponentById(componentId);

                if (component) {
                    component.setVisible(!hide);
                }
            });
        }
    },

    _handleScopeHidePoll: function(variableName) {
        var klip;

        if (variableName == this.propName) {
            klip = this.getKlip();

            PubSub.publish("scope-hide-input", {
                klipComponentId: this.getKlipComponentId(),
                dashboardId: klip.currentTab,
                variableName: this.propName,
                hide: true
            });
        }
    },

    onKlipRemoval: function($super) {
        $super();

        if (this.scope == Dashboard.SCOPE_KLIP) {
            this.deleteKlipProp();
        }
    },

    deleteKlipProp: function() {
        var klip = this.getKlip();

        $.post("/dashboard/ajax_deleteKlipProp", { prop: JSON.stringify({ scope:Dashboard.SCOPE_KLIP, name:this.propName, kid:klip.id }) });
    },

    getPropertyModel: function($super) {
        var model = $super();
        var variables;
        var variableKeys;
        var i;
        var createLink;
        var _this = this;
        var visibilityOptions;

        var options = [
            { value: "", label: "None Selected" }
        ];

        if (Component.Input.VarResolver) {
            variables = Component.Input.VarResolver();
            variableKeys = Object.keys(variables).sort(function(a, b) {
                return a.toUpperCase().localeCompare(b.toUpperCase());
            });

            for (i = 0; i < variableKeys.length; i++) {
                options.push({ value: variableKeys[i], label: variableKeys[i] });
            }
        }

        model.push({
            type: "select",
            id: "propName",
            label: "Use Variable",
            group: "vars",
            options: options,
            width: 140,
            selectedValue: this.propName,
            help: {
                link: "klips/use-variable"
            }
        });

        createLink = $("<a>").text("New Variable")
            .attr("href", "#")
            .attr({ actionId: "create_variable", actionArgs: true });

        model.push({
            type: "markup",
            id: "new_variable",
            group: "vars",
            el: createLink,
            flow: true
        });

        model.push({
            type: "select",
            id: "scope",
            label: "Scope",
            group: "vars",
            displayWhenNot: { propName: "" },
            options: Props.Defaults.scopes,
            selectedValue: this.scope,
            help: {
                link: "klips/property-scope"
            },
            onChange: function() {
                if (_this.scope !== Dashboard.SCOPE_KLIP && _this.controlHidden == "scope-hidden") {
                    _this.controlHidden = "visible";
                }

                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
            }
        });

        visibilityOptions = Props.Defaults.inputVisibility;
        if (this.scope !== Dashboard.SCOPE_KLIP) {
            visibilityOptions = visibilityOptions.slice(0, 2);
        }

        model.push({
            type: "select",
            id: "controlHidden",
            group: "visibility",
            label: "Visibility",
            help: {
                link: "klips/visibility"
            },
            options: visibilityOptions,
            selectedValue: this.controlHidden
        });

        return model;
    },

    toggleElVisibility: function() {
        if (this.el) {
            if (!this.visible) {
                if (!isWorkspace()) {
                    this.el.addClass("input-hidden-dashboard");
                } else {
                    this.el.addClass("input-hidden");
                }
            } else {
                this.el.removeClass("input-hidden");
                this.el.removeClass("input-hidden-dashboard");
            }
        }
    },

    update : function($super) {
        $super();

        // exit early optimizations
        // ------------------------
        if (!this.getKlip().isVisible) return;
        if (!this.checkInvalid(true)) return;

        if (this.controlHidden == "scope-hidden" && !this.visible) {
            // Height is ready can update panel cell heights
            // -----------------------------------------------
            if (this.fromPanelUpdate) {
                this.parent.handleComponentUpdated();
                this.fromPanelUpdate = false;
            }
            // -----------------------------------------------

            if (!isWorkspace() && this.parent.isKlip) {
                this.parent.handleComponentUpdated();
            }

            return;
        }

        if (this.controlHidden == "hidden") {
            this.toggleElVisibility();
        }

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        this._update();

        this.setInvalid(false, true);

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    /**
     * To be implemented by subclasses
     * @private
     */
    _update: function() {

    },

    updateProperty: function(value) {
        var _this = this;
        var klip;
        var scope;

        if (this.propName) {
            value = decodeURI(value);
            klip = this.getKlip();

            if (isWorkspace(klip.dashboard)) {
                if (page.uiVariables && page.uiVariables[this.propName] && page.uiVariables[this.propName].value != value) {
                    if (!page.uiVariables[this.propName].isUserProperty) {
                        $.post("/workspace/ajax_setUIVariable", { name: this.propName, value: value }, function() {
                            PubSub.publish("variables-changed", [
                                _this.propName,
                                [{ name: _this.propName, value: value }],
                                false,
                                true
                            ]);
                        });
                    } else {
                        PubSub.publish("variables-changed", [
                            this.propName,
                            [{ name: this.propName, value: value }],
                            false,
                            true
                        ]);
                    }
                }
            } else {
                scope = parseInt(this.scope);

                switch (scope) {
                    case Dashboard.SCOPE_KLIP:
                        klip.setKlipProp(this.propName, value);
                        break;
                    case Dashboard.SCOPE_TAB:
                        dashboard.setDashboardProp(scope, this.propName, value, klip.currentTab);
                        break;
                    case Dashboard.SCOPE_DASHBOARD:
                        dashboard.setDashboardProp(scope, this.propName, value);
                        break;
                }
            }
        }
    },

    destroy: function($super) {
        var klip;

        $super();

        if (!isWorkspace()) {
            PubSub.unsubscribe(this.hideToken);
            PubSub.unsubscribe(this.pollHideToken);

            klip = this.getKlip();

            PubSub.publish("scope-hide-input", {
                klipComponentId: this.getKlipComponentId(),
                dashboardId: klip.currentTab,
                variableName: this.propName,
                hide: false
            });
        }
    }
});;/****** cx.bally.js *******/ 

/* eslint-disable vars-on-top */
/* eslint-disable no-labels */
/* global Component:false, isWorkspace:false, dashboard:false, KlipFactory:false */

Component.BallyControl = $.klass(Component.Base, {

	displayName: "Bally Search Control",
	factory_id:"bally_ck",


	renderDom : function($super) {
		var _self = this;
		var dom = $super();
		var $seeAll, $clearField;
		this.$search = $("<input type='text'>");
		this.$button = $("<input type='button' class='rounded-button primary' value='Update Dashboard'/>");
		this.$cancel = $("<a href='' class='rounded-button secondary'>Cancel</a>");

		this.$button.click( _.bind(this.onSearch,this) );
		this.$cancel.click( function(evt){
			evt.preventDefault();
			_self.$panelSearch.hide();
			_self.$panelActive.show();

		});


		this.$panelActive = $("<div>").addClass("search-container")
			.append(
				$("<div>").addClass("search-topic label-size-2").click( _.bind(this.showSearch, this) )
			)
			.append(
				$("<div class='bally-date' style='font-size:18px;'></div>").addClass("label-size-1 quiet")
			)
			.append(
				$("<input type='button' value='Change Location'/>").addClass("change-loc").click( _.bind( this.showSearch, this) )
			);

		this.$search.autocomplete( {
			source: _.bind(this.onAutocomplete,this),
			focus : function(evt,ui){
				_self.setSearchField( ui.item.label );
			},
			change : function(evt,ui){
				_self.setSearchField( ui.item.label );
			},
			select : function(evt,ui){
				_self.setSearchField( ui.item.label );
				_self.setActiveSearch( ui.item.value.t,ui.item.value.v );
			}
		})
		.focus( function(){
				_self.hideTreeSelect();
		});


		$seeAll =  $("<div class='see-all'>See All</div>").click( _.bind(this.toggleTreeSelect,this) );

		$clearField = $("<div class='clear-field'></div>");

		this.$panelSearch = $("<div>").addClass("search-container")
			.append($seeAll)
			.append($clearField)
			.append($("<div>").append(this.$search).addClass("search-field-container"))
			.append($("<div class='clearfix'>"))
			.append($("<div>").append(this.$cancel).append(this.$button).addClass("search-submit-container"));

		this.$treeSelect = $("<div>")
			.css( { overflow:"auto", height:"200px", display:"none", position:"relative" , background:"white", border:"1px solid #888" } )
			.click( _.bind(this.onListSelect,this) );
//			.append( $("<div class='treelist'/>") )

		dom.append(this.$panelActive);
		dom.append(this.$panelSearch);
		dom.append(this.$treeSelect);
	},


	setSearchField : function(v) {

		var $s = this.$search;
		setTimeout(function(){

			$s.val(v);
		},0);
	},


	update : function($super) {
		$super();
		this.updateTree();
		var dateChild = this.getChildByRole("date");

		if ( dateChild ){
			var dateData = this.getChildByRole("date").getData(0);
			var prevDateData = this.getChildByRole("previous_date").getData(0);
			if ( dateData.length > 0 ) {
                this.el.find(".bally-date").html( dateData[0] );

                var tab = dashboard.getTabById(this.getKlip().currentTab);
                var klipLen = tab.klips.length;
                while(--klipLen > -1) {
                    var k = tab.klips[klipLen];
                    if (k.title.indexOf("Key Metrics (") == 0 || k.title.indexOf("Expenses (") == 0 ) {
                        k.title = k.title.substring(0, k.title.indexOf("(") ) + "(" + prevDateData[0] + ")";
                        k.update();
                    } else if (k.title.indexOf("Membership (") == 0 || k.title.indexOf("Mem. Dues (") == 0 ) {

						k.title = k.title.substring(0, k.title.lastIndexOf("(") ) + "(" + prevDateData[0] + ")";
						k.update();
					}
                }
            }

		}

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

	changeTabName : function(n) {
		var tid = this.getKlip().currentTab;
		$("#dashboard-tabs li[actionargs="+tid+"] div:first-child").text(n);
	},

	showSearch : function() {
		this.$panelSearch.show();
		this.$panelActive.hide();
		this.$search.val("");
		this.$search.focus();

	},

	onSearch  :function(evt) {
		var clubCode = this.$search.val();
		this.setActiveSearch("clubcode",clubCode);


	},

	setActiveSearch : function(t,q) {
		var newTabName, proxy_cc, cc_data, cclen, ccIdx, proxyClubName, dataClubName, clubName;
		this.searchType = t;
		newTabName = q;

		if ( t == "clubcode" ) {
			proxy_cc = this.getChildByRole("clubcode");
			cc_data = proxy_cc.getData(0);
			cclen = cc_data.length;
			ccIdx = -1;
			while(--cclen > -1 ) {
				if ( cc_data[cclen] == q ) {
					ccIdx = cclen;
					break;
				}
			}
			if (ccIdx != -1) {
				proxyClubName = this.getChildByRole("clubname");
				dataClubName = proxyClubName.getData(0);
				clubName = dataClubName[ccIdx];
				newTabName = clubName;
			}
			this.$search.val(q);

		}

        dashboard.setDashboardProp(2,"searchtype",t,this.getKlip().currentTab);
        dashboard.setDashboardProp(2,t,q,this.getKlip().currentTab);

		this.$panelActive.find(".search-topic").text(newTabName);
		this.changeTabName(newTabName);
		dashboard.updateTabWidths();


		this.$panelSearch.hide();
		this.$panelActive.show();

		this.updateKlipVisibility();

	},

	clearActive : function() {
		this.$panelSearch.show();
		this.$panelActive.hide();
	},


	afterFactory : function() {
		var proxys, i;
		if ( ! this.components || this.components.length == 0 ) {
			proxys = [
				{ role:"division",displayName:"Division"},
				{ role:"region",displayName:"District"},
				{ role:"district",displayName:"SGM"},
				{ role:"clubcode",displayName:"Club Code"},
				{ role:"clubname",displayName:"Club Name"},
				{ role:"date",displayName:"Date"},
				{ role:"previous_date",displayName:"Previous Month"}
			];

			for( i = 0; i < proxys.length; i++) {
				this.addComponent( KlipFactory.instance.createComponent({
					type:"proxy",
					role: proxys[i].role,
					displayName:proxys[i].displayName
				}));
			}
		}

		this.clearActive();
	},


	updateTree : function() {
		var division ={};

		var divisionData = this.getChildByRole("division").getData(0);
		var regionData = this.getChildByRole("region").getData(0);
		var districtData = this.getChildByRole("district").getData(0);
		var clubnameData = this.getChildByRole("clubname").getData(0);
		var clubcodeData = this.getChildByRole("clubcode").getData(0);

		var len = divisionData.length;

		// a little optimization to prevent firing when all required data is not yet assigned
		if ( regionData.length == 0 || districtData.length == 0 || clubnameData.length == 0  )
			return;

		for (var i = 0; i < len; i++) {
			var dt = districtData[i];
			var di = divisionData[i];
			var rg = regionData[i];
			var cn = clubnameData[i];

			if ( !division[di] ) division[di] = {};
			if ( !division[di][rg] ) division[di][rg] = {};
			if ( !division[di][rg][dt] ) division[di][rg][dt]= {};
			if ( !division[di][rg][dt][cn] ) division[di][rg][dt][cn] = { code : clubcodeData[i] };
		}

		this.treeModel = division;
	},


	hideTreeSelect : function() {
		this.$treeSelect.hide();
	},

	toggleTreeSelect : function() {
		if ( $(this.$treeSelect).is(":visible") ) {
			this.$treeSelect.hide();
			return;
		}

		this.$treeSelect.show();
		this.$treeSelect.appendTo( this.getKlip().el );

		var pos = this.$search.position();
		var top = pos.top + this.$search.offsetParent().position().top + this.$search.height() + 10;


		this.$treeSelect.css({ position:"absolute" , right:"10px", top:top+"px", width:"300px", zIndex:"10" }).empty();

		var defaultStyle = { paddingTop:"3px", borderBottom:"1px solid #e8e8e8", paddingBottom:"3px", cursor:"pointer"  };

		this.$treeSelect.append( $("<div>").html("National").css(defaultStyle).css({fontWeight:"bold",paddingLeft:"5px"}).attr("qtype","national") );

		for( var div_name in this.treeModel ) {
			var div = this.treeModel[div_name];
			this.$treeSelect.append( $("<div>").html(div_name).css(defaultStyle).css({paddingLeft:"5px"}).attr("qtype","div") );

			for( var region_name in div ) {
				var region = div[region_name];
				this.$treeSelect.append( $("<div>").html(region_name).css(defaultStyle).css({paddingLeft:"15px"}).attr("qtype","region") );
				for (var district_name in region ) {
					var district = region[district_name];
					this.$treeSelect.append( $("<div>").html(district_name).css(defaultStyle).css({paddingLeft:"30px"}).attr("qtype","district")  );
					for( var club_name in district) {
						var club = district[club_name];
						this.$treeSelect.append( $("<div>").html(club_name).css(defaultStyle).css({paddingLeft:"45px"}).attr("qtype","clubcode").attr("clubcode",club.code) );
					}
				}
			}
		}
	},


	onEvent : function($super,evt) {
		var self = this;

		if ( evt.id == "added_to_tab" ) {
			var st      = dashboard.getDashboardProp("searchtype",2,evt.tab.id);
            var sv      = dashboard.getDashboardProp(st,2,evt.tab.id);
            var tn      = dashboard.getDashboardProp("tabname",2,evt.tab.id);

            if ( !st) {
                st = "national";
                tn = "National";
                sv = "National";
            }

            this.changeTabName(tn);

            this.updateTree();

			setTimeout( function(){
				self.setActiveSearch(st,sv);
			},0);
		} else if ( evt.id == "parent_tab_selected" ) {
			setTimeout( function(){
				self.updateKlipVisibility();
			},0);
		}

	},


	onAutocomplete : function(v,r) {

		var dataClubname    = this.getChildByRole("clubname").getData(0);
		var dataRegionName  = this.getChildByRole("region").getData(0);
		var dataCode        = this.getChildByRole("clubcode").getData(0);
		var results         = [];
		var term            = v.term.toLowerCase();
		var i, j;
		var setData, setLabels, setLen, dupeMap, obj;

		var searchSets = [
			{t:"clubcode",labels:dataClubname, values:dataCode} ,
			{t:"region",labels:dataRegionName, values:dataRegionName}
		];
		for( i = 0 ; i < searchSets.length; i++ ) {
			setData = searchSets[i].values;
			setLabels = searchSets[i].labels;
			setLen = setData.length;
			dupeMap = {};
			for (j = 0; j < setLen ; j++) {
				obj = setLabels[j];
				if ( dupeMap[setData[j]] ) continue;
				if ( !obj ) continue;
				if ( obj.toLowerCase().indexOf(term) == 0 ){
					dupeMap[setData[j]] = 1;
					results.push( {value:{t: searchSets[i].t ,v:setData[j]} , label:obj}  );
				}
			}
		}

		r(results);
	},

	onListSelect : function(evt) {
		var $t = $(evt.target);
		var qtype = $t.attr("qtype");
		var sval = $t.text();

		switch(qtype) {
			case "div":
			case "region":
			case "district":
				// just use the default sval ...
				break;
			case "clubcode":
			case "club":
				sval = $t.attr("clubcode");
				break;
		}

		this.hideTreeSelect();
		this.setActiveSearch(qtype,sval);
	},

	/**
	 * displays/hides certain klips depending on the search state
	 */
	updateKlipVisibility : function() {
		// hide all POS Ratios klips
		var tab = dashboard.getTabById(this.getKlip().currentTab);
		var baseNames, whiteList, baseNamesLen, whiteListLen, searchType, klipLen, k, i, j, bn, exactMatch;


		if (!tab.klips || tab.klips.length == 0 ) return;
		baseNames = ["POS", "PT", "Retail","GE/TMA/Misc","Membership"];
		whiteList = ["POS Revenue", "PT Revenue", "Mem. Dues", "Retail Revenue"];
		baseNamesLen = baseNames.length;
		whiteListLen = whiteList.length;
		searchType = this.searchType;
		if ( searchType == "clubcode") searchType = "club";
		if ( searchType == "district") searchType = "sgm";
		if ( searchType == "region") searchType = "district";


		klipLen = tab.klips.length;

		kliploop:
		while( --klipLen > -1 ) {
			k = tab.klips[klipLen];

            for (j = 0 ; j < whiteListLen ; j++ ) if ( k.title.indexOf(whiteList[j]) == 0 ) continue kliploop;

			for( i = 0 ; i < baseNamesLen; i++){
				bn = baseNames[i];
				if ( k.title.indexOf(bn) ==  -1 ) continue;
				exactMatch =  bn + " ("+searchType+")";
				k.setVisible( k.title.toLowerCase().indexOf(exactMatch.toLowerCase()) == 0 );
				k.update();
                break;
			}
		}

	}
});
;/****** cx.chart_axis.js *******/ 

/* global Component:false, bindValues:false, page:false, isWorkspace:false, Props:false, FMT:false, safeText:false */

Component.ChartAxis = $.klass(Component.Proxy, {

	displayName: "Axis",
	factory_id:"chart_axis",

	label: "Untitled",
	labelAngle: "0",
	labelStagger: 1,
	labelDrop: 0,
	showLabel: false,
	tickInterval: 0,
	originLine: true,
	showAxis: true,
	showAxisScale: true,
	showLabelTicks: false,
	customRange: false,
	customOrigin: false,
	customInterval: 0,
	axisPosition: "left",
	min: "",
	max: "",
	origin: "",
	sort: 0,
	bubbleLabels: 0,
	customBubbleSize: false,
	bubbleSize: "30%",
    grid: false,

	canGroup: true,
	canSort: true,
	canFilter: true,

	disableFormatType: false,
	disableFormatGroup: false,

	initialize : function($super,config){
		$super(config);
		this.labelMixin = new Component.LabelComponent();
		this.disableStyles = true;

	},

	setSelectedInWorkspace : function($super, isSelected, newComponent) {
		$super(isSelected, newComponent);
		this.parent.invalidateAndQueue();
	},

    checkDeletable : function($super, ctx) {
        var isDeletable = $super();
		var siblings;

        if (isDeletable) {
            siblings = ctx && ctx.role ? this.parent.getChildrenByRole(ctx.role) : this.parent.getChildrenByType("chart_axis");
            isDeletable = (siblings.length > 1);
        }

        return isDeletable;
    },

	getSupportedDst: function($super) {
		var dst = $super();
		var isSeriesXAxis = (this.parent.factory_id == "chart_series" && this.axisType == Component.ChartAxis.TYPE_CATEGORY);

		dst.group = isSeriesXAxis && dst.group;
		dst.sort = isSeriesXAxis;
		dst.filter = isSeriesXAxis;

		return dst;
	},

    getWorkspaceControlModel : function($super) {
        var model = $super();

        if (this.parent.factory_id == "chart_series" && this.role == "axis_y") {
            model.push({
                name:"Series",
                controls: [
                    [
                        {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"series_data", role:"series"}},
                        {type:"remove", disabled:true}
                    ]
                ]
            });

            model.push({
                name:"Y-Axis",
                controls: [
                    [
                        {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"chart_axis", role:"axis_y", sibling:this}},
                        {type:"remove", disabled:!this.checkDeletable({role:"axis_y"}), actionId:"remove_component", actionArgs:{cx:this}}
                    ]
                ]
            });
        } else {
            model = this.parent.getWorkspaceControlModel(true);
        }

        return model;
    },

	getEditorMenuModel : function($super) {
		var m = $super();
		var type = this.getTypeLabel();
		var name = this.displayName;
		if (!name) name = "Untitled";

		if(KF.company.hasFeature("saas_11282")){
			name = safeText(name);
		}

		m.text = "<span parentIsAction='true' class='quiet'>" + type.replace(/ /g, "&nbsp;") + ":</span>&nbsp;" + name.replace(/ /g, "&nbsp;");
		m.tooltip =  this.getReferenceName();
		return m;
	},

	getReferenceValueModel : function($super) {
		var m = $super();
		m.text = this.getReferenceName().replace(/ /g, "&nbsp;");
		m.reference = !this.dataDisabled;
		return m;
	},

	getReferenceName : function() {
		var type = this.getTypeLabel();
		return type + ": " + (this.displayName || "Untitled");
	},

	getTypeLabel : function() {
		var type = "";
		switch(this.axisType) {
			case Component.ChartAxis.TYPE_CATEGORY:
				type = "X Axis";
				break;

			case Component.ChartAxis.TYPE_Z:
				type = "Z Axis";
				break;

			case Component.ChartAxis.TYPE_DATA:
			default:
				type = "Y Axis";
				break;
		}

		if(this.parent && this.parent.invertAxes) {
			type += " (inv)";
		}

		return type;
	},

	configure : function($super, config) {
		$super(config);
		bindValues(this, config, ["label","showLabel","labelAngle","labelStagger","labelDrop","customRange","customOrigin","origin","min","max","grid","originLine","showAxis","showAxisScale","showLabelTicks","tickInterval","customInterval","axisPosition","sort","bubbleLabels","customBubbleSize","bubbleSize"]);

		if (config.label && !this.isRenamed) {
			this.displayName = config.label;
            if (config["~propertyChanged"] && isWorkspace()) page.updateMenu();
		}

		if ( isWorkspace() && this.parent ) {
			this.parent.invalidate();
		}
	},


	serialize : function($super) {
		var cfg = $super();
		bindValues(cfg, this, ["label","showLabel","axisPosition","showAxis","customRange","min","max","customInterval","tickInterval","customOrigin","origin","originLine","showAxisScale","labelAngle","labelStagger","labelDrop","showLabelTicks","grid","sort","customBubbleSize","bubbleSize","bubbleLabels"],
            {
                showLabel:false,
                axisPosition: "left",
                showAxis: true,
                customRange: false,
                min: "",
                max: "",
                customInterval: 0,
                tickInterval: 0,
                customOrigin: false,
                origin: "",
                originLine: true,
                showAxisScale: true,
                labelAngle: "0",
                labelStagger: 1,
                labelDrop: 1,
                showLabelTicks: false,
                grid: false,
                sort: 0,
                customBubbleSize: false,
                bubbleSize: "30%",
                bubbleLabels: 0
            }, true);
		return cfg;
	},


	/**
	 * depending on the chart orientation, an axis can either be one of:
	 * - a 'data' axis that displays values (ie, a Y-axis),
	 * - a 'category' axis that displays the series category (eg, 'Monday','Tuesday',etc) (ie, an X-axis)
	 * - a silent Z-axis (used by the scatter/bubble chart component)
	 * @param t
	 */
	setAxisType : function(t) {
		var isScatter = (this.parent.factory_id == "chart_scatter");
		this.axisType = t;

		switch(t) {
			case Component.ChartAxis.TYPE_CATEGORY:
				this.dataDisabled = isScatter;
				this.allowedFormats = isScatter ? ["num","cur","pct","dur"] : ["txt","num","cur","pct","dat","dur"];
				break;
			case Component.ChartAxis.TYPE_Z:
				this.dataDisabled = true;
				this.allowedFormats = ["num","cur","pct","dur"];
				break;
			case Component.ChartAxis.TYPE_DATA:
			default:
				this.dataDisabled = true;
				this.allowedFormats = isScatter ? ["num","cur","pct","dur"] : ["num","cur","pct","dur"];
				this.isDeletable = !isScatter;
				break;
		}
	},


	getPropertyModelGroups: function($super) {
		var propertyGroups = $.merge($super(), this.labelMixin.getPropertyModelGroups());

		switch (this.axisType) {
			case Component.ChartAxis.TYPE_DATA:
				propertyGroups = propertyGroups.concat(["label", "position", "custom", "origin", "tick", "grid"]);
				break;
			case Component.ChartAxis.TYPE_CATEGORY:
				propertyGroups = propertyGroups.concat(["label", "line", "custom", "tick", "sort", "grid"]);
				break;
			case Component.ChartAxis.TYPE_Z:
				propertyGroups = propertyGroups.concat(["label", "bubble"]);
				break;
		}

		return propertyGroups;
	},
	
	updateFormat: function($super, metadata) {
		var fmtInfo = false;
		var i;
		var cx;
		var components;

		if (this.isAutoFormatOn() && isWorkspace()) {
			components = this.parent.getComponentsByAxisReference(this.id);
			if (components && components.length > 0) {
				for (i = 0; i < components.length; i++) {
					cx = components[i];
					if (cx && cx.formulas.length > 0) {
						if (!fmtInfo) {
							fmtInfo = {fmt: cx.fmt, fmtArgs: {currency: FMT.getFormatArg(cx, "currency")}};
						} else {
							fmtInfo = FMT.resolveFormats(fmtInfo, cx);
						}
					}
				}
				if (fmtInfo) {
					this.fmt = fmtInfo.fmt;
					if (!this.fmtArgs) {
						this.fmtArgs = {};
					}
					this.fmtArgs.currency = FMT.getFormatArg(fmtInfo, "currency");
				}
			} else {
				$super(metadata);
			}
		}
	},

	getPropertyModel  : function($super) {
		var model = $super();
		var _this = this;
		var isScatter = (this.parent.factory_id == "chart_scatter");
		var i;

		var labelModel = _.bind(this.labelMixin.getPropertyModel, this)();

		if (this.isAutoFormatFeatureOn() && this.disableFormatType) {
			this.stopRenderProperty(labelModel, "fmt");
			this.stopRenderProperty(labelModel, "autoFmt");
			this.stopRenderProperty(labelModel, "fmt_arg_currency");
		}

		if (this.isAutoFormatFeatureOn() && this.disableFormatGroup) {
			// If the disableFormatGroup is set to true, we will disable all properties in the "fmt" group.
			for(i = 0; i < labelModel.length; i++) {
				if(labelModel[i].group == "fmt") {
					// Prefix and suffix aren't supported for axis labels using date format
					labelModel[i].noRender = true;
				}
			}
		}

		for(i = 0; i < labelModel.length; i++) {
			if((labelModel[i].id == "fmt_arg_prefix") || (labelModel[i].id == "fmt_arg_suffix")) {
				// Prefix and suffix aren't supported for axis labels using date format
				labelModel[i].displayWhen = { fmt:["txt","num","cur","pct","dur"] };
			}
		}

		if(this.axisType == Component.ChartAxis.TYPE_DATA){///prop.id == 'fmt_arg_paren'
			labelModel = _.reject(labelModel, function(prop){
				return (prop.id == "size" || prop.id == "wrap" || prop.id == "font_style" || prop.id == "font_colour" || prop.id == "align" || prop.id.indexOf("spk") == 0||prop.id == "fmt_arg_paren" );
			});
		} else {
			labelModel = _.reject(labelModel, function(prop){
				return (prop.id == "size" || prop.id == "wrap" || prop.id == "font_style" || prop.id == "font_colour" || prop.id == "align" || prop.id.indexOf("spk") == 0 );
			});
		}

		model = $.merge(model, labelModel);

		model.push({
			type:"text" ,
			id:"label",
			label:"Axis Title",
			group:"label",
			value: this.label
		});

		if(this.axisType != Component.ChartAxis.TYPE_Z) {
			model.push({
				type:"checkboxes" ,
				id:"showLabel",
				options: [
					{value:true,label:"Show axis title", checked: this.showLabel}
				],
				label: "",
				group:"label",
				value: this.showLabel
			});
		}

		if ( this.axisType == Component.ChartAxis.TYPE_DATA ) {
			// Y-axis properties
			model.push({
				type: "button_group_ex",
				id: "axisPosition",
				label: "Axis Position",
				group: "position",
				options: Props.Defaults.axisPosition,
				selectedValue: this.axisPosition
			});

			model.push({
				type:"checkboxes",
				id:"showAxis",
				options: [
					{value:true,label:"Show axis line", checked: this.showAxis}
				],
				label:"Axis Line",
				group:"position",
				value: this.showAxis
			});

			model.push({
				type:"checkboxes",
				id:"customRange",
				options: [
					{value:true,label:"Use custom range", checked: this.customRange}
				],
				label:"Axis Range",
				group:"custom",
				value: this.customRange
			});

			model.push({
				type:"text" ,
				id:"min",
				group:"custom",
				label:"",
				minorLabels: [
					{label: "Min:", position: "left"}
				],
				width: 36,
				displayWhen: { customRange:true },
				value: this.min
			});

			model.push({
				type:"text" ,
				id:"max",
				group:"custom",
				label:"",
				minorLabels: [
					{label: "Max:", position: "left"}
				],
				width: 36,
				flow: true,
				displayWhen: { customRange:true },
				value: this.max
			});

			model.push({
				type:"checkboxes",
				id:"customInterval",
				options: [
							{value:true, label:"Use custom range interval", checked: this.customInterval}
						],
				label:"Axis Interval" ,
//				options: Props.Defaults.tickInterval,
				group:"custom",
				displayWhen: {fmt:["num","cur","pct","dur"]}
			});

			model.push({
				type:"text" ,
				id:"tickInterval",
				group:"custom",
				label:"",
				width: 36,
				displayWhen: { fmt:["num","cur","pct","dur"], customInterval:1 },
				value: this.tickInterval,
				validator: function(value){
					// If the user enters a small tick interval for a chart axis that has a large range,
					// this could result in an axis with too many ticks, which freezes the browser.  The
					// tick interval is rejected if it would result in more than 101 ticks on the axis.
					var axes = _this.role == "axis_y" ? _this.parent.chart.yAxis : _this.parent.chart.xAxis;
					var a;
					var axisRange, numTicks;

					for(a = 0; a < axes.length; a++) {
						if(axes[a].userOptions.id == _this.id) {
							axisRange = axes[a].max - axes[a].min;
							numTicks = axisRange / Number(value.replace(",", ""));
							if(numTicks > 101) {
								return false;
							}
						}
					}

					return true;
				}
			});

			if(!isScatter) {
				model.push({
					type:"checkboxes",
					id:"customOrigin",
					options: [
						{value:true,label:"Use custom origin", checked: this.customOrigin}
					],
					label:"Origin",
					group:"origin",
					value: this.customOrigin
				});

				model.push({
					type:"text" ,
					id:"origin",
					group:"origin",
					label:"",
					width: 36,
					displayWhen: { customOrigin:true },
					value: this.origin
				});
			}

			model.push({
				type:"checkboxes",
				id:"originLine",
				options: [
					{value:true,label:"Show origin line", checked: this.originLine}
				],
				label:"Origin Line",
				group:"origin",
				value: this.originLine
			});

			model.push({
				type:"checkboxes",
				id:"showAxisScale",
				options: [
					{value:true,label:"Show tick labels", checked: this.showAxisScale}
				],
				label:"Tick Label",
				group:"tick",
				value: this.showAxisScale
			});

			model.push({
				type: "button_group_ex",
				id: "labelAngle",
				label: "Label Angle",
				displayWhen: { showAxisScale:1 },
				group: "tick",
				options: Props.Defaults.labelRotateY,
				selectedValue: this.labelAngle
			});

			model.push({
				type:"checkboxes",
				id:"showLabelTicks",
				options: [
					{value:true,label:"Show tick marks on axis", checked: this.showLabelTicks}
				],
				label:"Tick Marks",
				group:"tick",
				value: this.showLabelTicks
			});

			model.push({
				type:"checkboxes",
				id:"grid",
				options: [
					{value:true,label:"Show grid lines", checked: this.grid}
				],
				label:"Grid Line",
				group:"grid",
				value: this.grid
			});

		} else if(this.axisType == Component.ChartAxis.TYPE_CATEGORY) {
			// X-axis properties
			model.push({
				type:"checkboxes",
				id:"showAxis",
				options: [
					{value:true,label:"Show axis line", checked: this.showAxis}
				],
				label:"Axis Line",
				group:"line",
				value: this.showAxis
			});

			if(isScatter) {
				model.push({
					type:"checkboxes",
					id:"customRange",
					options: [
						{value:true,label:"Use custom range", checked: this.customRange}
					],
					label:"Axis Range",
					group:"custom",
					value: this.customRange
				});

				model.push({
					type:"text" ,
					id:"min",
					group:"custom",
					label:"",
					minorLabels: [
						{label: "Min:", position: "left"}
					],
					width: 36,
					displayWhen: { customRange:true },
					value: this.min
				});

				model.push({
					type:"text" ,
					id:"max",
					group:"custom",
					label:"",
					minorLabels: [
						{label: "Max:", position: "left"}
					],
					width: 36,
					flow: true,
					displayWhen: { customRange:true },
					value: this.max
				});
			}

			model.push({
				type:"checkboxes",
				id:"customInterval",
				options: [
							{value:true, label:"Use custom range interval", checked: this.customInterval}
						],
				label:"Axis Interval" ,
				group:"custom",
//				options: Props.Defaults.tickInterval,
				displayWhen: {fmt:["num","cur","pct","dur"]}
			});

			model.push({
				type:"text" ,
				id:"tickInterval",
				group:"custom",
				label:"",
				displayWhen: { fmt:["num","cur","pct","dur"], customInterval:1 },
				width: 36,
				value: this.tickInterval
			});

			model.push({
				type:"checkboxes",
				id:"showAxisScale",
				options: [
					{value:true, label:"Show tick labels", checked: this.showAxisScale}
				],
				label:"Tick Label",
				group:"tick",
				value: this.showAxisScale
			});

			model.push({
				type: "button_group_ex",
				id: "labelStagger",
				label: "Label Rows",
				displayWhen: { showAxisScale:1 },
				group: "tick",
				options: Props.Defaults.labelStagger,
				selectedValue: this.labelStagger
			});

			model.push({
				type:"select",
				id:"labelDrop",
				label:"Label Drop" ,
				group:"tick",
				options: Props.Defaults.labelDrop,
				selectedValue: this.labelDrop,
                displayWhen: {fmt: ["txt", "dat","dat2" ,"dur"], showAxisScale: 1}
			});

			model.push({
				type: "button_group_ex",
				id: "labelAngle",
				label: "Label Angle",
				displayWhen: { showAxisScale:1 },
				group: "tick",
				options: Props.Defaults.labelRotateX,
				selectedValue: this.labelAngle
			});

			model.push({
				type:"checkboxes",
				id:"showLabelTicks",
				options: [
					{value:true,label:"Show tick marks on axis", checked: this.showLabelTicks}
				],
				label:"Tick Marks",
				group:"tick",
				value: this.showLabelTicks
			});

			model.push({
				type:"checkboxes",
				id:"grid",
				options: [
					{value:true,label:"Show grid lines", checked: this.grid}
				],
				label:"Grid Line",
				group:"grid",
				value: this.grid
			});
		} else if(this.axisType == Component.ChartAxis.TYPE_Z) {
			// Z-axis properties
			model.push({
				type:"checkboxes",
				id:"customBubbleSize",
				options: [
					{value:true, label:"Use custom bubble size", checked: this.customBubbleSize}
				],
                help: {
                    link: "klips/bubble-size"
                },
				label:"Bubble Size" ,
				group:"bubble",
				value: this.customBubbleSize,
                fieldMargin:"13px 0 0"
			});

			model.push({
				type:"text" ,
				id:"bubbleSize",
				group:"bubble",
				label:"Max diameter",
				displayWhen: { customBubbleSize:1 },
				width: 36,
				value: this.bubbleSize,
                minorLabels: [
                    {label: "% of chart height or px", position: "right", css:"quiet italics"}
                ],
                fieldMargin:"0 0 5px 0"
			});

			model.push({
				type:"markup" ,
				id:"minBubbleLabel",
				group:"bubble",
				displayWhen: { customBubbleSize:1 },
				el: $("<span class='bubble_min_size_label quiet'>The min bubble size is 8px. The max diameter size cannot be less than 8px.</span>"),
				fieldMargin:"0 0 10px 0"
			});

			model.push({
				type: "select",
				id: "bubbleLabels",
				label: "Bubble Labels",
				group: "bubble",
				options: Props.Defaults.axisBubbleLabels,
				selectedValue: this.bubbleLabels
			});
		}

		return model;
	}

});

Component.ChartAxis.TYPE_DATA = 1;
Component.ChartAxis.TYPE_CATEGORY = 2;
Component.ChartAxis.TYPE_Z = 3;
;/****** cx.chart_funnel.js *******/ 

/* global Component:false, CXTheme:false, FMT:false, Props:false, Highcharts:false, page:false, isWorkspace:false, bindValues:false, _sanitizeNumbers:false, _sanitizeStrings:false */
Component.ChartFunnel = $.klass(Component.ChartSingleSeries, {

	displayName: "Funnel Chart",
	factory_id:"chart_funnel",

	customHeight:"",

	conversion: 0,

    itemColors: [],

	//an estimation of approximately how many pixels each character in the legend will take up
	//It has to be an estimation because we need to know that before the chart is drawn, so it
	// can figure out where to place the legend (bottom/side), and how much space to leave everywhere
	pixelsPerChar: 6.5,
	maxPointNameWidth: 0,
	largestLabelLength: 0,


	initialize : function($super) {
		$super();

        this.itemColors = CXTheme.current.series_colors.slice(0);
	},

	/*
	 Creates the data model for funnel
	 */

	createModel : function(sData, sLabels){

		var _dataModel = [];
		var _dataModelLength = Math.min(sData.length,sLabels.length);
		var i;
		for (i = 0; i < _dataModelLength; ++i) {
			_dataModel[i] = {label:sLabels[i], data:sData[i], index: i};
		}
		return _dataModel;
	},

	conversionCalculator: function(_value, _label, index){
		var dataCx;
		var precision;
		var _percentage;

		// if the labels are disabled we don't calculate the percentage
		if(this.legend_labels != 0){
			dataCx = this.getChildByType("series_data");
			precision = 0;

			//As a % of the first stage

			if(this.conversion == 1) {
				_percentage = (_value * 100)/this.dataModel[0].y;

			//As % of previous stage && it's not the first data point (first stage would not have a % as there would be no denominator)
			} else if (this.conversion == 2  && index != 0) {
				_percentage = (_value * 100)/this.dataModel[index-1].y;
			}
			if(dataCx.fmtArgs && dataCx.fmtArgs.precision) {
				precision = Number(dataCx.fmtArgs.precision);
			}

			if (_percentage) {
				_label += " [" + Math.round(_percentage * Math.pow(10, precision)) / Math.pow(10, precision) + "%]";
			}

		}

		return _label;
	},

	getFormattedModel: function(){
		var pointName = "";
		var valFmt = "";
		var funnelData = [];
		var val;
		var i;

		var reactionContext = {seriesColours: {}};
		this.dataCx.collectReactions(reactionContext);

		for (i = 0; i < this.dataModel.length; i++) {
			val = FMT.convertToNumber(this.dataModel[i].data);
			if(val == null || val == Infinity || val == -Infinity) val = 0;

			if(this.legend_values) {
				valFmt = " (" + Component.LabelComponent.Render(this.getChildByType("series_data"),[val]).html() + ")";
			}
			pointName = Component.LabelComponent.Render(this.getChildByType("series_labels"), [this.dataModel[i].label]).html().replace(/&amp;/g, "&");
			if(pointName == "&nbsp;" || !pointName.length) pointName = val.toString();
			pointName +=  valFmt;

			if(reactionContext.seriesColours[this.dataModel[i].index]) {
				funnelData.push({name: pointName, y: val, color: reactionContext.seriesColours[this.dataModel[i].index]});
			} else {
				funnelData.push({name: pointName, y: val});
			}

			//calculate the maxPointNameWidth for the legend based on the largest label length
			this.largestLabelLength = Math.max(this.largestLabelLength, pointName.length);

		}
		return funnelData;
	},

	calculateSizes: function(_oSizeCalculations){
		// The labels are put on the right side by default, Calculate the available space.
		// for the chart to render in.

		var maxChartSize;

		// Add the size of the the label handle / distance
		_oSizeCalculations["maxLegendWidth"] = _oSizeCalculations["maxLegendWidth"] + _oSizeCalculations["_HANDLE_SIZE"];

		maxChartSize = _oSizeCalculations["componentWidth"] - _oSizeCalculations["maxLegendWidth"];


		// emptySpace + chartSize + labels + label handles + emptySpace
		_oSizeCalculations["emptySpace"] = (_oSizeCalculations["componentWidth"] - _oSizeCalculations["chartSize"] - _oSizeCalculations["maxLegendWidth"]) / 2;

		// Set the minimum padding if there is not room for padding given the chart size
		if (_oSizeCalculations["emptySpace"] < 0){
			_oSizeCalculations["emptySpace"] = 5;
		}


		if(maxChartSize < _oSizeCalculations["chartSize"]) {

			// The space left for the chart is less than what the user specified.  Shrink the chart to 100 px to fit
			// the labels in the Klip area.  If the labels would still display outside the Klip area after shrinking,
			// also set the maximum possible lable size
			if(maxChartSize < 100) {
				maxChartSize = 100;
				_oSizeCalculations["_HANDLE_SIZE"] = 10; // now that there is not enough room, we reduce the handle size as well

				_oSizeCalculations["maxTruncatedLabelWidth"]  = (_oSizeCalculations["componentWidth"] - maxChartSize - _oSizeCalculations["emptySpace"]*2 - _oSizeCalculations["_HANDLE_SIZE"]);

				// The labels will be truncated in the data label formatter callback below (otherwise the tooltips would also be truncated).

				// If we are going to turncate the labels the space for the labels should be set accordingly

				_oSizeCalculations["maxLegendWidth"] = _oSizeCalculations["maxTruncatedLabelWidth"] + _oSizeCalculations["_HANDLE_SIZE"];

			}
			_oSizeCalculations["chartSize"] = maxChartSize;
		}


		return _oSizeCalculations;
	},

	update : function($super) {
		var _this = this;
		var _HANDLE_SIZE = 50;
		var fontFamily;
		var sData, sLabels;
		var name;
		var funnelData;
		var maxLegendWidth = 0;
		var precision = 0;
		var chartHeightPadding = 5;
		var emptySpace =  0;
		var maxTruncatedLabelWidth = 0;
		var _oSizeCalculations;
		var showLegendOnBottom;
		var legendPosition;
		var numLines = 0;
		var n;
		var nameLength;
		var largerSize;
		var _rightMargin = null;
		var _leftMargin;
		var displayLegend;
		var chartHeight;
		var funnelColours;
		var c, colour;
		var plotTarget;

		$super();

        // exit early optimizations
        // ------------------------
        if ( !this.getKlip().isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

		if(this.chart) this.chart.destroy();

		// *******************
		// Data Manipulation
		// *******************
		fontFamily = this.getFontFamily();

		this.dataCx = this.getChildByType("series_data");

		sData = this.getChildByType("series_data").getData(0).slice(0); // slice (0) to clone array

		_sanitizeNumbers(sData);

		sLabels = this.getChildByType("series_labels").getData(0).slice(0); // slice (0) to clone array
		_sanitizeStrings(sLabels, sData.length);

		this.dataModel = this.createModel(sData, sLabels);

		funnelData = this.getFormattedModel();

		// Calculate the legend size based on the largest label and the properties that impact the label size
		this.maxPointNameWidth = this.largestLabelLength * this.pixelsPerChar;

		// start the legend size with the estimate size of the largest label
		maxLegendWidth = this.maxPointNameWidth;

		if(this.conversion != 0) {
			// Add padding for the percentage value
			if(this.dataCx.fmtArgs && this.dataCx.fmtArgs.precision) precision = Number(this.dataCx.fmtArgs.precision);
			maxLegendWidth += 40 + (this.pixelsPerChar * precision);
		}


		if (funnelData.length == 0) {
			name = "No data" + (this.legend_values ? " (1)" : "");
			funnelData = [{name: name, y: 1}];
			this.maxPointNameWidth = name.length * this.pixelsPerChar;
		}

		// *******************
		// Sizing Calculations
		// *******************

		// if custom size (i.e. 5) is selected get the custom size and set it unless it's smaller than min size
		if(this.size == 5) {
			// Custom chart size
			this.chartSize = this.customHeight ? parseInt(this.customHeight) : this.chartSizes[1];
			if(this.chartSize < 30) this.chartSize = 30;
		} else {
			// get the size form the sizes array defined earlier
			this.chartSize = this.chartSizes[parseInt(this.size) - 1];
		}


		_oSizeCalculations = {"maxLegendWidth": maxLegendWidth, "emptySpace": 0, "chartSize": this.chartSize, "componentWidth": this.el.width(), "maxTruncatedLabelWidth": 0, "_HANDLE_SIZE": _HANDLE_SIZE};

		//Calculate sizes when labels are set to be shown around the chart
		//TODO: Same approach (moving the logic to another function) could be used in the future to move the calculation out of the update method and write unit test for calculations
		if(this.legend_labels == 1) {
			_oSizeCalculations = this.calculateSizes(_oSizeCalculations);
			maxLegendWidth = _oSizeCalculations["maxLegendWidth"];
			emptySpace = _oSizeCalculations["emptySpace"];
			this.chartSize = _oSizeCalculations["chartSize"];
			maxTruncatedLabelWidth = _oSizeCalculations["maxTruncatedLabelWidth"];
			_HANDLE_SIZE = _oSizeCalculations["_HANDLE_SIZE"];
		}

		// if legend is set to display
		if(this.legend_labels == 2) {

			// Add 20 pixels to this.maxPointNameWidth to account for the colour rectangle
			maxLegendWidth = this.maxPointNameWidth + 20;

			// Pad the legend so that there are two roughly equal empty spaces in the Klip
			// (left edge -> funnel, legend -> right edge).  Allow 48 pixels padding between
			// the chart and the legend.
			emptySpace = (this.el.width() - this.chartSize - maxLegendWidth - 48) / 2;
			showLegendOnBottom = false;
			if(emptySpace < 0) {
				// If the legend is too wide to display in the Klip area, display the
				// legend on the bottom of the chart.
				emptySpace = 30;
				showLegendOnBottom = true;

				//if there is room to fit the legend without wrapping keep the legend in it's size
				//otherwise set it to be the same size as the chart
				maxLegendWidth = (maxLegendWidth > this.el.width()? this.chartSize: maxLegendWidth);
			}

			this.emptySpace = emptySpace;
			legendPosition = (emptySpace) * -1;

			if(showLegendOnBottom) {
				// Add height to the chart to account for the legend being placed on the bottom.
				// Assume the legend uses 21 pixels per line.  Add 10 pixels of padding between
				// the legend and the chart.
				for(n = 0; n < funnelData.length; n++) {
					nameLength = funnelData[n].name.length * this.pixelsPerChar;
					numLines += Math.ceil(nameLength / (maxLegendWidth - 15));
				}
				chartHeightPadding += (numLines * 21) + 10;

				//Use larger size between legend and chart to calculate the remaining space
				//So that we maximize the use of space
				largerSize = (maxLegendWidth > this.chartSize? maxLegendWidth: this.chartSize);
				emptySpace = (this.el.width() - largerSize) / 2;

				//if the chart does not fit, make it smaller and set the minimum of 5 for the margins / empty space
				if(emptySpace < 0){
					this.chartSize = this.chartSize + emptySpace*2 + 5;
					maxLegendWidth = maxLegendWidth + emptySpace*2;
					emptySpace = 5;
				}

			}
		}

		if(this.legend_labels == 0) {
			emptySpace = (this.el.width() - this.chartSize) / 2;

			//if the chart does not fit, make it smaller and set the minimum of 5 for the margins / empty space
			if(emptySpace < 0){
				this.chartSize = this.chartSize + emptySpace*2 + 5;
				emptySpace = 5;
			}
		}


		//right margin size is different depending on the type of label that is selected

		//if the labels are set to show around the chart
		//Since labels and chart are rendered as one unit
		//we add the space required to render the labels to the right margin
		if (this.legend_labels == 1 ) {
			_rightMargin = emptySpace + maxLegendWidth;
		//When labels are not shown right margin is equal to the remaining space
		//Remaining space component width - chart size
		} else if (this.legend_labels == 0){
			_rightMargin = emptySpace;
		} else if (this.legend_labels == 2 && showLegendOnBottom ){
			_rightMargin = emptySpace;
		}
		_leftMargin = emptySpace;


		displayLegend = this.legend_labels == 2;
		chartHeight = this.chartSize + chartHeightPadding;
		this.el.height(chartHeight);

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        funnelColours = this.overrideColors ? this.itemColors.slice(0) : CXTheme.current.series_colors.slice(0);

        // convert theme colours to actual colours
        for (c = 0; c < funnelColours.length; c++) {
            colour = funnelColours[c];

            funnelColours[c] = CXTheme.getThemeColour(colour);
        }

		// Don't draw the plot if the chart isn't currently visible
		plotTarget = this.plotId + "-plot";

		//update the data model after all the changes to be used later, if required
		this.dataModel = funnelData;

		if (($("#" + plotTarget).length != 0) && (this.el.width() > 0)) {
			// See http://api.highcharts.com/highcharts for API docs
			this.chart = new Highcharts.Chart({
				chart: {
                    backgroundColor: null,
                    borderColor: null,
                    events: {
                        click: function() {
                            if(isWorkspace()) {
                                page.invokeAction("select_component", _this);
                            }
                        }
                    },
                    height: chartHeight,
                    marginLeft: _leftMargin,
					marginRight: _rightMargin,
                    plotBackgroundColor: null,
                    plotShadow: false,
                    spacingTop: 14,
                    renderTo: plotTarget,
                    width: this.el.width()
				},
				colors: funnelColours,
				credits: {
					enabled: false
				},
				legend: {
					align: showLegendOnBottom ? "center" : "right",
					borderWidth: 0,
					itemHiddenStyle: {color: CXTheme.current.cx_text_selected},
					labelFormatter: function() {
						var label = this.name;
						label = _this.conversionCalculator(this.y, label, this.index);
						return label;
					},
					itemHoverStyle: {color: CXTheme.current.cx_text_highlight},
					itemStyle: {
						cursor: "pointer",
						color: CXTheme.current.cx_text,
						width: maxLegendWidth,
						fontFamily: fontFamily,
						fontSize: "13px",
						fontWeight: "normal"
					},
					layout: "vertical",
					navigation: {
						activeColor: CXTheme.current.cx_chart_nav_active,
						animation: true,
						arrowSize: 10,
						inactiveColor: CXTheme.current.cx_chart_nav_inactive,
						style: {
							color: CXTheme.current.cx_text,
							fontSize: "12px",
							fontFamily: fontFamily,
							letterSpacing: "4px"
						}
					},
					verticalAlign: showLegendOnBottom ? "bottom" : "middle",
					x: showLegendOnBottom ? null : legendPosition
				},
				plotOptions: {
					funnel: {
						allowPointSelect: isWorkspace() ? false: true,
						animation: !(this.getDashboard().isImaging() || isWorkspace()),
						borderColor: CXTheme.current.cx_bg,
						borderWidth: 0.8,
						cursor: "pointer",
						neckWidth: "10%",
						neckHeight: "0%",
						dataLabels: {
							overflow: "none",
							crop: false,
							color: CXTheme.current.cx_text,
							connectorColor: CXTheme.current.cx_text,
							distance: _HANDLE_SIZE,
							softConnector: false,
							enabled: (this.legend_labels == 1),
							formatter: function() {
								var label = this.point.name;
								var labelSize;
								var dataText = "";
								var dataSize;
								var availableLabelWidth;
								var numCharsAfterTruncation;

								if(_this.legend_labels == 1 && maxTruncatedLabelWidth > 0) {
									labelSize = label.length * _this.pixelsPerChar + 20;

									if(labelSize > maxTruncatedLabelWidth) {
										// Truncate the label to fit in the available Klip width
										if(_this.legend_values) {
											dataText = label.slice(label.lastIndexOf(" ("));
											label = label.slice(0, label.lastIndexOf(" ("));
										}
										dataSize = labelSize - (label.length * _this.pixelsPerChar + 20);
										availableLabelWidth = maxTruncatedLabelWidth - dataSize;
										numCharsAfterTruncation = Math.floor(availableLabelWidth / _this.pixelsPerChar) - 3; // Leave 3 spaces for the "..."
										label = label.slice(0, numCharsAfterTruncation) + "... " + dataText;
									}
								}

								label = _this.conversionCalculator(this.y, label, this.point.index);

								return label;
							},
							style: {
								fontFamily: fontFamily,
								fontSize:"13px",
                                fontWeight: "normal",
                                textOutline: "2px " + CXTheme.current.cx_text_outline + " 0.7"
							}
						},
						events: {
							click: function(event) {
								var element;

								if(isWorkspace()) {
									element = event.srcElement || event.originalTarget;
									if(element.nodeName == "tspan") {
										// When the legend is displayed around the chart, this handles when the legend item/data labels are clicked
										page.invokeAction("select_component", _this.getChildByType("series_labels"));
									} else {
										page.invokeAction("select_component", _this.getChildByType("series_data"));
									}
								}
							}
						},
						point: {
							events: {
								legendItemClick: function(){
									if(isWorkspace()) {
										page.invokeAction("select_component", _this.getChildByType("series_labels"));
										return false;
									}
								}
							}
						},
						shadow: false,
						showInLegend: displayLegend,
						height: this.chartSize
					}
				},
				series: [{
					data: funnelData,
					name: "series1",
					type: "funnel"
				}],
				title: {
					text: null
				},
				tooltip: {
					borderRadius: 4,
					borderWidth: 2,
					formatter: function() {
						var pointName = this.point.name;
						var wrappedPointName = "";
						var numLines = 0;
						var pointNameSplit;
						var i;
						var tooltipText;

						if(_this.legend_values) {
							pointName = pointName.slice(0, pointName.lastIndexOf(" ("));
						}

						pointNameSplit = pointName.split(" ");
						for(i = 0; i < pointNameSplit.length; i++) {
							wrappedPointName += pointNameSplit[i];
							if(i + 1 != pointNameSplit.length) {
								wrappedPointName += " ";
								if(wrappedPointName.length > (numLines * 40) + 30) {
									wrappedPointName += "</b><br/><b>";
									numLines++;
								}
							}
						}

						tooltipText = "<b>" + wrappedPointName + "</b><br/>" + Component.LabelComponent.Render(_this.getChildByType("series_data"),[this.y]).html();

						tooltipText = _this.conversionCalculator(this.y, tooltipText, this.point.index);

						return tooltipText;
					},
					style: {fontFamily: fontFamily, cursor:"default" }
				}
			});
		} else {
//			console.log("unable to update plot: " + plotTarget);
		}

		if(isWorkspace()) {
			this.setupClickHandling();
			this.drawSelectionBoxOrCircle();
		}

		this.setInvalid(false, true);

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
	},

	configure : function($super, config) {
		var prop;
		var index;

		$super(config);

        this.invalidate();

		bindValues(this, config, ["customHeight", "legend_labels","legend_values","conversion","overrideColors","itemColors"]);

        if (config["~propertyChanged"]) {
            for (prop in config) {
                if (config.hasOwnProperty(prop) && prop.indexOf("color-") != -1) {
                    index = prop.match(/\d+/);
                    this.itemColors[parseInt(index[0])] = config[prop];
                }
            }
        }
    },

	serialize : function($super) {

		var cfg = $super();

		bindValues(cfg, this, ["customHeight", "legend_labels","legend_values","conversion","overrideColors"],
            {
                legend_labels:2,
                legend_values:true,
				conversion:1, //default: s a % of the first stage
                overrideColors:false
            }, true);

        if (!_.isEqual(this.itemColors, CXTheme.current.series_colors)) cfg.itemColors = this.itemColors;

		return cfg;
	},

	getPropertyModel  : function($super) {
		var model = $super();
		var i, colorProp;
		var _this = this;

		model.push({
            type:"select",
            id:"size",
            label:"Size",
            group:"appearance",
            options: Props.Defaults.chartSize,
            selectedValue: this.size
        });

		model.push({
			type: "text",
			id: "customHeight",
			label: "",
			minorLabels: [
				{label: "Height:", position: "left"},
				{label: "px", position: "right",  css:"quiet italics"}
			],
			width: 36,
			group: "appearance",
			displayWhen: {size:5},
			value: this.customHeight
		});

        model.push({
            type: "checkboxes",
            id: "overrideColors",
            options: [
                {value:true, label:"Override the default colors", checked: this.overrideColors}
            ],
            label: "Stage Colors",
            group: "color"
        });

        for (i = 0; i < this.itemColors.length; i++) {
            colorProp = {
                type: "color_picker",
                id: "color-" + i,
                group: "color",
                valueType:"theme",
                selectedValue: this.itemColors[i],
                displayWhen: { overrideColors:true }
            };

            if (i % 7 != 0) {
                colorProp.flow = { padding:"3px" };
            } else {
                colorProp.breakFlow = true;
                colorProp.fieldMargin = "0";
            }

            model.push(colorProp);
        }

        model.push({
            type:"button",
            id:"add_item_color",
            group:"color",
            text:"+",
            fieldMargin:"0",
            breakFlow: true,
            onClick: function() {
                _this.itemColors.push("cx-theme_fff");
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(),page.componentPropertyForm);
                _this.update();
            },
            css:"add",
            displayWhen: { overrideColors:true }
        });

        model.push({
            type:"button",
            id:"remove_item_color",
            group:"color",
            text:"-",
            flow:{ padding:"0px" },
            onClick: function() {
                _this.itemColors.pop();
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                _this.update();
            },
            css:"remove",
            displayWhen: { overrideColors:true }
        });

		model.push({
			type: "select",
			id: "legend_labels",
			label: "Labels",
			group: "labels",
			options: Props.Defaults.funnelLegends,
			selectedValue: this.legend_labels
		});

		model.push({
			type: "checkboxes",
			id: "legend_values",
			options: [
				{value: "1", label: "Show values", checked: this.legend_values}
			],
			displayWhen: {legend_labels: ["1", "2"]},
            fieldMargin:"0",
			group: "labels"
		});

		model.push({
			type: "select",
			id: "conversion",
			label: "Conversion",
			group: "labels",
			options: Props.Defaults.funnelConversion,
			displayWhen: {legend_labels: ["1", "2"]},
			selectedValue: this.conversion
		});

		this.addDataSlotProperty(model);

		return model;
	}

});;/****** cx.chart_pie.js *******/ 

/* global Component:false, CXTheme:false, FMT:false, Props:false, Highcharts:false, page:false isWorkspace:false, bindValues:false, _sanitizeNumbers:false, _sanitizeStrings:false */
Component.ChartPie = $.klass(Component.ChartSingleSeries, {

	displayName: "Pie Chart",
	factory_id:"chart_pie",

	pie_style: "full",
    diameter: "",

	legend_items: 6,
	sort: 0,
	legend_percentages: false,

    sliceColors: [],


	initialize : function($super) {
		$super();

        this.sliceColors = CXTheme.current.series_colors.slice(0);
	},

	sortData : function(sData, sLabels){

		var sort_ar = [];
		var sort_len = Math.min(sData.length,sLabels.length);
		var i;
		for (i = 0; i < sort_len; ++i) {
			sort_ar[i] = {label:sLabels[i], data:sData[i], index: i};
		}

		switch (Number(this.sort)) {
			case 1: // alphabetical
				sort_ar.sort(function (a, b) {
					return (a.label < b.label?-1:(a.label == b.label?0:1));
				});
				break;
			case 2: // reverse alphabetical
				sort_ar.sort(function (a, b) {
					return (a.label > b.label?-1:(a.label == b.label?0:1));
				});
				break;
			case 3: // numeric
				sort_ar.sort(function (a, b) {
					return (a.data > b.data?-1:(a.data == b.data?0:1));
				});
				break;
			case 4:
				sort_ar.sort(function (a, b) {
					return (a.data < b.data?-1:(a.data == b.data?0:1));
				});
				break;
			case 0: // no sort
			default:
				break;
		}

		return sort_ar;

	},


	update : function($super) {
		var componentValue;
		var i;
		var otherSum = 0;
		var precision = 0;
		var valFmt = "";
		var pointName = "";
		var maxLegendWidth;
		var maxPointNameWidth = 0;
		var pieData = [];
		var fontFamily;
		var pixelsPerChar = 6.5;
		var dataCx;
		var sData;
		var sLabels;
		var reactionContext;
		var sort_ar;
		var sort_len;
		var val;
		var name;
		var maxChartSize;
		var maxTruncatedLabelWidth;
		var emptySpace;
		var showLegendOnBottom;
		var legendPosition;
		var numLines;
		var n;
		var c;
		var nameLength;
		var colour;
		var displayLegend;
		var chartHeight;
		var innerSize = 0;
		var pieColours;
		var plotTarget;
		// Add 34 pixels to the height to account for the pie slice hover highlight
		var chartHeightPadding = 34;
		var _this = this;

		$super();

        // exit early optimizations
        // ------------------------
        if ( !this.getKlip().isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

		if(this.chart) this.chart.destroy();

		// *******************
		// Data Manipulation
		// *******************
		fontFamily = this.getFontFamily();

		dataCx = this.getChildByType("series_data");
		sData = this.getChildByType("series_data").getData(0).slice(0); // slice (0) to clone array
		_sanitizeNumbers(sData);

		sLabels = this.getChildByType("series_labels").getData(0).slice(0); // slice (0) to clone array
		_sanitizeStrings(sLabels, sData.length);

		reactionContext = {seriesColours: {}};
		dataCx.collectReactions(reactionContext);

		sort_ar = this.sortData(sData, sLabels);
        sort_len = Math.min(sData.length,sLabels.length);

		for (i = 0; i < sort_len; i++) {
			val = FMT.convertToNumber(sort_ar[i].data);
			if(val == null || val == Infinity || val == -Infinity) val = 0;

			// NOTE - this.legend_items represents the new "Max Slices" property to maintain
			// backwards compatibility with previous Klip schemas.
			if (this.legend_items > 0 && (i+1) >= this.legend_items && this.legend_items != sort_len) {
				otherSum += val;
			} else {
				if(this.legend_values) {
					componentValue =Component.LabelComponent.Render(this.getChildByType("series_data"),[val]).html();
					valFmt = " (" + componentValue + ")";
				}
				pointName = Component.LabelComponent.Render(this.getChildByType("series_labels"), [sort_ar[i].label]).html().replace(/&amp;/g, "&");
				if(pointName == "&nbsp;" || !pointName.length) {
					if(componentValue) {
						pointName = componentValue;
					} else {
						pointName = val.toString();
					}
				}
				pointName +=  valFmt;

				if(reactionContext.seriesColours[sort_ar[i].index]) {
					pieData.push({name: pointName, y: val, color: reactionContext.seriesColours[sort_ar[i].index]});
				} else {
					pieData.push({name: pointName, y: val});
				}

				maxPointNameWidth = Math.max(maxPointNameWidth, pointName.length * pixelsPerChar);
			}
		}

		// Add 20 pixels to maxPointNameWidth to account for the colour rectangle
		maxLegendWidth = maxPointNameWidth + 20;
		if(this.legend_percentages) {
			// Add padding for the percentage value
			if(dataCx.fmtArgs && dataCx.fmtArgs.precision) precision = Number(dataCx.fmtArgs.precision);
			maxLegendWidth += 35 + (pixelsPerChar * precision);
		}

		if ( otherSum > 0 ) {
            if(this.legend_values) {
				valFmt =  " (" + Component.LabelComponent.Render(this.getChildByType("series_data"),[otherSum]).html() + ")";
            }
			pieData.push({name: ("Other" + valFmt), y: otherSum});
		}

		if (pieData.length == 0) {
			name = "No data" + (this.legend_values ? " (1)" : "");
			pieData = [{name: name, y: 1}];
			maxLegendWidth = name.length * pixelsPerChar + 20;
		}

		// *******************
		// Sizing Calculations
		// *******************
		if(this.size == 5) {
			// Custom chart size
			this.chartSize = this.diameter ? parseInt(this.diameter) : this.chartSizes[1];
			if(this.chartSize < 30) this.chartSize = 30;
		} else {
			this.chartSize = this.chartSizes[parseInt(this.size) - 1];
		}

		if(this.legend_labels == 1) {
			// Add height to the chart to account for the labels being placed around the chart
			chartHeightPadding += 14;

			// Assume at least one label will be placed on each side of the chart.  Calculate the space that is available
			// for the chart to render in.
			maxChartSize = this.el.width() - (maxPointNameWidth * 2);
			maxTruncatedLabelWidth = 0;
			if(maxChartSize < this.chartSize) {
				// The space left for the chart is less than what the user specified.  Shrink the chart to 50 px to fit
				// the labels in the Klip area.  If the labels would still display outside the Klip area after shrinking,
				// also truncate the data labels to a minimum of 10 characters.
				if(maxChartSize < 50) {
					maxChartSize = 50;
					maxTruncatedLabelWidth = (this.el.width() - maxChartSize) / 2;
					// The labels will be truncated in the data label formatter callback below (otherwise the tooltips would also be truncated).
				}
				this.chartSize = maxChartSize;
			}
		}
		if(this.legend_labels == 2) {
			// Pad the legend so that there are two roughly equal empty spaces in the Klip
			// (left edge -> pie, legend -> right edge).  Allow 48 pixels padding between
			// the chart and the legend.
			emptySpace = (this.el.width() - this.chartSize - maxLegendWidth - 48) / 2;
			showLegendOnBottom = false;
			if(emptySpace < 0) {
				// If the legend is too wide to display in the Klip area, display the
				// legend on the bottom of the chart.
				emptySpace = 30;
				showLegendOnBottom = true;
				maxLegendWidth = this.chartSize;
			}

			this.emptySpace = emptySpace;
			legendPosition = (emptySpace) * -1;

			if(showLegendOnBottom) {
				// Add height to the chart to account for the legend being placed on the bottom.
				// Assume the legend uses 21 pixels per line.  Add 10 pixels of padding between
				// the legend and the chart.
				numLines = 0;
				for(n = 0; n < pieData.length; n++) {
					nameLength = pieData[n].name.length * pixelsPerChar;
					numLines += Math.ceil(nameLength / (maxLegendWidth - 15));
				}
				chartHeightPadding += (numLines * 21) + 10;
			}
		}

		displayLegend = this.legend_labels == 2;
		chartHeight = this.chartSize + chartHeightPadding;
		this.el.height(chartHeight);

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

		if(this.pie_style == "circle") {
			innerSize = this.chartSize / 2;
		}

        pieColours = this.overrideColors ? this.sliceColors.slice(0) : CXTheme.current.series_colors.slice(0);

        // convert theme colours to actual colours
        for (c = 0; c < pieColours.length; c++) {
            colour = pieColours[c];

            pieColours[c] = CXTheme.getThemeColour(colour);
        }

		// Don't draw the plot if the chart isn't currently visible
		plotTarget = this.plotId + "-plot";
		if (($("#" + plotTarget).length != 0) && (this.el.width() > 0)) {
//			    See http://api.highcharts.com/highcharts for API docs
			this.chart = new Highcharts.Chart({
				chart: {
                    backgroundColor: null,
                    borderColor: null,
                    events: {
                        click: function() {
                            if(isWorkspace()) {
                                page.invokeAction("select_component", _this);
                            }
                        }
                    },
                    height: chartHeight,
                    marginLeft: (this.legend_labels == 2 && !showLegendOnBottom) ? emptySpace : null,
                    plotBackgroundColor: null,
                    plotShadow: false,
                    spacingTop: 14,
                    renderTo: plotTarget,
                    width: this.el.width()
				},
				colors: pieColours,
				credits: {
					enabled: false
				},
				legend: {
					align: showLegendOnBottom ? "center" : "right",
					borderWidth: 0,
					itemHiddenStyle: {color: CXTheme.current.cx_text_selected},
					labelFormatter: function() {
						var label = this.name;
						var precision = 0;
						if(_this.legend_percentages && _this.legend_labels != 0) {
							if(dataCx.fmtArgs && dataCx.fmtArgs.precision) precision = Number(dataCx.fmtArgs.precision);
							label += " [" + Math.round(this.percentage * Math.pow(10, precision)) / Math.pow(10, precision) + "%]";
						}

						return label;
					},
					itemHoverStyle: {color: CXTheme.current.cx_text_highlight},
					itemStyle: {
						cursor: "pointer",
						color: CXTheme.current.cx_text,
						width: maxLegendWidth,
						fontFamily: fontFamily,
						fontSize: "13px",
						fontWeight: "normal"
					},
					layout: "vertical",
					navigation: {
						activeColor: CXTheme.current.cx_chart_nav_active,
						animation: true,
						arrowSize: 10,
						inactiveColor: CXTheme.current.cx_chart_nav_inactive,
						style: {
							color: CXTheme.current.cx_text,
							fontSize: "12px",
							fontFamily: fontFamily,
							letterSpacing: "4px"
						}
					},
					verticalAlign: showLegendOnBottom ? "bottom" : "middle",
					x: showLegendOnBottom ? null : legendPosition
				},
				plotOptions: {
					pie: {
						allowPointSelect: isWorkspace() ? false: true,
						animation: !(this.getDashboard().isImaging() || isWorkspace()),
						borderColor: CXTheme.current.cx_bg,
						borderWidth: 0.8,
						cursor: "pointer",
						dataLabels: {
							color: CXTheme.current.cx_text,
							connectorColor: CXTheme.current.cx_text,
							distance: 15,
							enabled: (this.legend_labels == 1),
							formatter: function() {
								var label = this.point.name;
								var precision = 0;
								var labelSize;
								var dataText= "";
								var dataSize;
								var availableLabelWidth;
								var numCharsAfterTruncation;

								if(_this.legend_labels == 1 && maxTruncatedLabelWidth > 0) {
									labelSize = label.length * pixelsPerChar + 20;
									if(labelSize > maxTruncatedLabelWidth) {
										// Truncate the label to fit in the available Klip width
										if(_this.legend_values) {
											dataText = label.slice(label.lastIndexOf(" ("));
											label = label.slice(0, label.lastIndexOf(" ("));
										}
										dataSize = labelSize - (label.length * pixelsPerChar + 20);
										availableLabelWidth = maxTruncatedLabelWidth - dataSize;
										numCharsAfterTruncation = Math.floor(availableLabelWidth / pixelsPerChar) - 3; // Leave 3 spaces for the "..."
										label = label.slice(0, numCharsAfterTruncation) + "... " + dataText;
									}
								}

								if(_this.legend_percentages && _this.legend_labels != 0) {
									if(dataCx.fmtArgs && dataCx.fmtArgs.precision) precision = Number(dataCx.fmtArgs.precision);
									label += " [" + Math.round(this.percentage * Math.pow(10, precision)) / Math.pow(10, precision) + "%]";
								}

								return label;
							},
							style: {
								fontFamily: fontFamily,
								fontSize:"13px",
                                fontWeight: "normal",
                                textOutline: "2px " + CXTheme.current.cx_text_outline + " 0.7"
							}
						},
						events: {
							click: function(event) {
								var element;
								if(isWorkspace()) {
									element = event.srcElement || event.originalTarget;
									if(element.nodeName == "tspan") {
										// When the legend is displayed around the chart, this handles when the legend item/data labels are clicked
										page.invokeAction("select_component", _this.getChildByType("series_labels"));
									} else {
										page.invokeAction("select_component", _this.getChildByType("series_data"));
									}
								}
							}
						},
						innerSize: innerSize,
						point: {
							events: {
								legendItemClick: function(){
									if(isWorkspace()) {
										page.invokeAction("select_component", _this.getChildByType("series_labels"));
										return false;
									}
								}
							}
						},
						shadow: false,
						showInLegend: displayLegend,
						size: this.chartSize
					}
				},
				series: [{
					data: pieData,
					name: "series1",
					type: "pie"
				}],
				title: {
					text: null
				},
				tooltip: {
					borderRadius: 4,
					borderWidth: 2,
					formatter: function() {
						var pointName = this.point.name;
						var precision = 0;
						var wrappedPointName = "";
						var numLines = 0;
						var tooltipText;
						var pointNameSplit;
						var i;

						if(_this.legend_values) {
							pointName = pointName.slice(0, pointName.lastIndexOf(" ("));
						}

						pointNameSplit = pointName.split(" ");
						for(i = 0; i < pointNameSplit.length; i++) {
							wrappedPointName += pointNameSplit[i];
							if(i + 1 != pointNameSplit.length) {
								wrappedPointName += " ";
								if(wrappedPointName.length > (numLines * 40) + 30) {
									wrappedPointName += "</b><br/><b>";
									numLines++;
								}
							}
						}

						tooltipText = "<b>" + wrappedPointName + "</b><br/>" + Component.LabelComponent.Render(_this.getChildByType("series_data"),[this.y]).html();
						if(_this.legend_percentages && _this.legend_labels != 0) {
							if(dataCx.fmtArgs && dataCx.fmtArgs.precision) precision = Number(dataCx.fmtArgs.precision);
							tooltipText += " [" + Math.round(this.percentage * Math.pow(10, precision)) / Math.pow(10, precision) + "%]";
						}
						return tooltipText;
					},
					style: {fontFamily: fontFamily, cursor:"default" }
				}
			});
		} else {
//			console.log("unable to update plot: " + plotTarget);
		}

		if(isWorkspace()) {
			this.setupClickHandling();
			this.drawSelectionBoxOrCircle();
		}

        this.setInvalid(false, true);

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
	},

	configure : function($super, config) {
		var index;
		var prop;
		$super(config);

        this.invalidate();

		bindValues(this, config, ["legend_items","sort","pie_style","diameter","legend_labels","legend_values","legend_percentages","overrideColors","sliceColors"]);

        if (config["~propertyChanged"]) {
            for (prop in config) {
                if (config.hasOwnProperty(prop) && prop.indexOf("color-") != -1) {
                    index = prop.match(/\d+/);
                    this.sliceColors[parseInt(index[0])] = config[prop];
                }
            }
        }
    },

	serialize : function($super) {
		var cfg = $super();
		bindValues(cfg, this, ["pie_style","diameter","legend_items","sort","legend_labels","legend_values","legend_percentages","overrideColors"],
            {
                pie_style:"full",
                diameter:"",
                legend_items:6,
                sort:0,
                legend_labels:2,
                legend_values:true,
				legend_percentages:false,
                overrideColors:false
            }, true);

        if (!_.isEqual(this.sliceColors, CXTheme.current.series_colors)) cfg.sliceColors = this.sliceColors;

		return cfg;
	},

	getPropertyModel  : function($super) {
		var model = $super();
		var _this = this;
		var colorProp = {};
		var i;

		model.push({
			type: "button_group_ex",
			id: "pie_style",
			label: "Pie Style",
			group: "appearance",
			options: Props.Defaults.pieStyles,
			selectedValue: this.pie_style
		});

		model.push({
            type:"select",
            id:"size",
            label:"Size",
            group:"appearance",
            options: Props.Defaults.chartSize,
            selectedValue: this.size
        });

		model.push({
			type: "text",
			id: "diameter",
			label: "",
			minorLabels: [
				{label: "Diameter:", position: "left"},
				{label: "px", position: "right",  css:"quiet italics"}
			],
			width: 36,
			group: "appearance",
			displayWhen: {size:5},
			value: this.diameter
		});

		model.push({
			type:"text",
			id:"legend_items",
			group:"plot",
			label:"Max Slices",
			width: 36,
			value: this.legend_items
		});

        model.push({
            type: "checkboxes",
            id: "overrideColors",
            options: [
                {value:true, label:"Override the default colors", checked: this.overrideColors}
            ],
            label: "Slice Colors",
            group: "color"
        });

        for (i = 0; i < this.sliceColors.length; i++) {
            colorProp = {
                type: "color_picker",
                id: "color-" + i,
                group: "color",
                valueType:"theme",
                selectedValue: this.sliceColors[i],
                displayWhen: { overrideColors:true }
            };

            if (i % 7 != 0) {
                colorProp.flow = { padding:"3px" };
            } else {
                colorProp.breakFlow = true;
                colorProp.fieldMargin = "0";
            }

            model.push(colorProp);
        }

        model.push({
            type:"button",
            id:"add_slice_color",
            group:"color",
            text:"+",
            fieldMargin:"0",
            breakFlow: true,
            onClick: function() {
                _this.sliceColors.push("cx-theme_fff");
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(),page.componentPropertyForm);
                _this.update();
            },
            css:"add",
            displayWhen: { overrideColors:true }
        });

        model.push({
            type:"button",
            id:"remove_slice_color",
            group:"color",
            text:"-",
            flow:{ padding:"0px" },
            onClick: function() {
                _this.sliceColors.pop();
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                _this.update();
            },
            css:"remove",
            displayWhen: { overrideColors:true }
        });

		model.push({
			type: "select",
			id: "legend_labels",
			label: "Labels",
			group: "labels",
			options: Props.Defaults.pieLegends,
			selectedValue: this.legend_labels
		});

		model.push({
			type: "checkboxes",
			id: "legend_values",
			options: [
				{value: "1", label: "Show values", checked: this.legend_values}
			],
			displayWhen: {legend_labels: ["1", "2"]},
            fieldMargin:"0",
			group: "labels"
		});

		model.push({
			type: "checkboxes",
			id: "legend_percentages",
			options: [
				{value: "0", label: "Show percentage", checked: this.legend_percentages}
			],
			displayWhen: {legend_labels: ["1", "2"]},
            fieldMargin:"0",
			group: "labels"
		});

		this.addDataSlotProperty(model);

		return model;
	}

});;/****** cx.chart_scatter.js *******/ 

/* eslint-disable vars-on-top */
/* global CXTheme:false, Highcharts:false, FMT:false, Component: false, KlipFactory:false, isWorkspace:false, _sanitizeNumbers:false, page:false, bindValues:false, Props:false */

Component.ChartScatter = $.klass(Component.ChartCartesian, {

	displayName: "Scatter/Bubble Chart",
	factory_id:"chart_scatter",
	variableSubComponents: [
		{ label:"Series", type:"scatter_data", role:"scatter_data" }
	],

	pointSize: 6,
	renderer: "scatter",

	renderDom : function($super) {
		$super();

		// note, we reassign the div ID after this component is assigned to a klip to make its ID unique across
		// multiple instances
		// -----------------------------------------------------------------------------------------------------
		this.plotId = this.id + "-" + _.uniqueId();
		this.$chart = $("<div>").attr("id", "scatter-" + this.plotId).appendTo(this.el);

		this.changeHeight();
	},

	getWorkspaceControlModel : function($super) {
		var model = $super();

		model.push({
			name:"Scatter",
			controls: [
				[
					{type:"add", actionId:"add_component", actionArgs:{parent:this, type:"scatter_data", role:"scatter_data"}},
					{type:"remove", disabled:true}
				]
			]
		});

        return model;
    },

	addChildByType : function($super, config) {
		var newChild = $super(config);

		if (config.type == "scatter_data" && config.role == "scatter_data") {
			newChild = KlipFactory.instance.createComponent({
				type :"scatter_data",
				displayName :"New Scatter",
				dataDisabled: true,
				role:"scatter_data"
			});
		}

		this.addComponent(newChild, config.index);

		return newChild;
	},

	removeComponent : function($super, cx, nextActive, detach) {
		var i;
		var child;

		$super(cx, nextActive, detach);

		if (cx.role == "scatter_data") {
			// When a series is deleted, we need to get through the data components and update corresponding axis format.
			for (i = 0; i < cx.components.length; i++) {
				child = cx.components[i];
				if (child && child.factory_id == "series_data") {
					child.updateAxisFormat();
				}
			}
		}
	},

	update : function($super) {
		$super();

		// exit early optimizations
		// ------------------------
		if ( !this.getKlip().isVisible ) return;
		if ( !this.checkInvalid(true) ) return;

		// Height is ready can update panel cell heights
		// -----------------------------------------------
		if (this.fromPanelUpdate) {
			this.parent.handleComponentUpdated();
			this.fromPanelUpdate = false;
		}
		// -----------------------------------------------

		if ((jQuery.browser.msie && jQuery.browser.version < 9) || isWorkspace()) {
			// blow away the chart otherwise it overpaints
			this.$chart.empty();
		}

		if(isWorkspace()) this.getChildByRole("axis_z").visibleInWorkspace = (this.renderer == "bubble");

		// *******************
		// Data Manipulation
		// *******************
		var cxLen = this.components.length;
		var seriesNames = [];
		var jsonChartData = [];
		var categoryAxis = this.getChildByRole("axis_x");
		var dataAxis = this.getChildByRole("axis_y");
		var zAxis = this.getChildByRole("axis_z");
		var _this = this;
		var chartNeedsLeftPadding = false;
		var selectedComponent;
		var selectedComponentIndex = 0;
		var fontFamily = this.getFontFamily();
		var LEGEND_LEFT_POS = -25;
		var ITEM_DISTANCE = isWorkspace() ? 40 : 20; //Default highcharts itemDistance between legend entries is 20

		for (var i = 0; i < cxLen; i++) {
			var cx = this.components[i];
			if(cx.selected) {
				selectedComponent = cx;
				selectedComponentIndex = i;
			}

			if (cx.role != "scatter_data") continue;

			if(!selectedComponent) {
				// If one of the "Data" or "Labels" sub-components are selected, the selection box will be drawn
				// as if the parent "Series" component were selected.
				for(var c = 0; c < cx.components.length; c++) {
					if(cx.components[c].selected) {
						selectedComponent = cx;
						selectedComponentIndex = i;
					}
				}
			}

			if(isWorkspace()) cx.getChildByRole("data_z").visibleInWorkspace = (this.renderer == "bubble");

			seriesNames.push(cx.scatterName);

			// don't mess with the actual data
			var data = [];
			var componentX = cx.getChildByRole("data_x");
			var dataX = componentX.getData(0).slice(0);
			var componentY = cx.getChildByRole("data_y");
			var dataY = componentY.getData(0).slice(0);
			var componentZ = cx.getChildByRole("data_z");
			var dataZ = componentZ.getData(0).slice(0);
			var componentLabel = cx.getChildByRole("data_labels");
			var dataLabel = componentLabel.getData(0).slice(0);

			var axisX = _this.getChildByRole("axis_x");
			var axisY = _this.getChildByRole("axis_y");
			var axisZ = _this.getChildByRole("axis_z");


			componentX.axis = axisX.id;
			componentY.axis = axisY.id;
			componentZ.axis = axisZ.id;

			// The Y component's reactions override the X component's reactions.  The Z component's reactions override both.
			var reactionContext = {seriesColours: {}};
			componentX.collectReactions(reactionContext);
			componentY.collectReactions(reactionContext);
			componentZ.collectReactions(reactionContext);

			if(this.hideNullSeries && (cx.isAllFalsyData(dataX) || cx.isAllFalsyData(dataY))) {
				continue;
			}

			var dataLen = Math.max(dataX.length, dataY.length, dataZ.length);
			if(dataLen == 0) {
				if(!isWorkspace()) continue;
				dataX = [0];
				dataY = [0];
				dataZ = [5];
			}

			_sanitizeNumbers(dataX);
			_sanitizeNumbers(dataY);
			_sanitizeNumbers(dataZ);

			// set series color...
			// if in the workspace and cx is not the selected series, color it grey
			// ---------------------------------------------------------------------
			var color = cx.overrideColor ? CXTheme.getThemeColour(cx.color) : CXTheme.getThemeColour(CXTheme.current.series_colors[i % 9]);

			if( isWorkspace() && page.activeComponent.role &&
				( (page.activeComponent.role == "scatter_data" && page.activeComponent != cx) ||
				(page.activeComponent.role.indexOf("data") == 0 && page.activeComponent.parent != cx) )
			) {
				color = "#d6d6d6";
			}

			var totalX = 0;
			var totalY = 0;
			var totalXSquared = 0;
			var totalXY = 0;
			for(var s = 0; s < dataLen; s++) {
				// If one series is shorter than the other, fill in the shorter series with zeroes
				var pointX = 0;
				var pointY = 0;
				var pointZ = 0;
				var pointLabel = "";

				if(dataX.length > s) {
					if(dataX[s] != Infinity && dataX[s] != -Infinity) pointX = dataX[s];
				}

				if(dataY.length > s) {
					if(dataY[s] != Infinity && dataY[s] != -Infinity) pointY = dataY[s];
				}

				if(dataZ.length > s) {
					if(dataZ[s] != Infinity && dataZ[s] != -Infinity) pointZ = dataZ[s];
				}

				if(dataLabel.length > s) {
					pointLabel = dataLabel[s];
				}

				// Prevent data formatted as percentage from being too large - see saas-2267 and fmt.percentage.js.
				// See also the axis range and axis interval options.
				if(categoryAxis.fmt == "pct") {
					pointX = pointX / 100;
				}

				if(dataAxis.fmt == "pct") {
					pointY = pointY / 100;
				}

				if(cx.regLine) {
					// Prepare data for regression line calculation
					totalX += pointX;
					totalY += pointY;
					totalXSquared += pointX * pointX;
					totalXY += pointX * pointY;
				}

				var dataPointColour = (reactionContext.seriesColours[s] && color != "#d6d6d6") ? reactionContext.seriesColours[s] : color;

				// Convert the hex colour to rgb (needed to add translucency to the scatter points).
				// The colour is assumed to be in the format "#RRGGBB".
				var dataRed = parseInt(dataPointColour.substring(1,3), 16);
				var dataGreen = parseInt(dataPointColour.substring(3,5), 16);
				var dataBlue = parseInt(dataPointColour.substring(5,7), 16);
				var rgbaStr = "rgba(" + dataRed + "," + dataGreen + "," + dataBlue + ",0.5)";

				data.push({x: pointX, y: pointY, z: pointZ, name: pointLabel, color: rgbaStr, marker: {fillColor: rgbaStr, states: {hover: {fillColor: dataPointColour}}}});
			}

			if(cx.regLine && this.renderer == "scatter") {
				// Plot the regression line
				var regLineData = [];
				var b = ((dataLen * totalXY) - (totalX * totalY)) / ((dataLen * totalXSquared) - (totalX * totalX));
				var a = (totalY / dataLen) - ((b * totalX) / dataLen);
				for(var r = 0; r < dataLen; r++) {
					var regX = data[r].x;
					var regY = a + (b * data[r].x);
					regLineData.push([regX, regY]);
				}
				regLineData.sort(function(a, b){
					return(a[0] < b[0] ? -1 : (a[0] == b[0] ? 0 : 1));
				});

				// The id property is used by the scatter legendItemClick handler to hide the
				// corresponding regression line when a scatter series is hidden by the user.
				jsonChartData.push({
					color: color,
					data: regLineData,
					enableMouseTracking: false,
					id: "regression-" + i,
					lineWidth: 2,
					marker: {
						enabled: false
					},
					name: "Regression Line",
					shadow: false,
					showInLegend: false,
					states: {
						hover: {
							lineWidth: 0
						}
					},
					type: "line"
				});
			}

			// Convert the hex colour to rgb (needed to add translucency to the scatter points).
			// The colour is assumed to be in the format "#RRGGBB".
			var red = parseInt(color.substring(1,3), 16);
			var green = parseInt(color.substring(3,5), 16);
			var blue = parseInt(color.substring(5,7), 16);

			// The id property is used by the scatter click handlers to associate series with
			// their respective components.
			jsonChartData.push({
				color: "rgba(" + red + "," + green + "," + blue + ",0.5)",
				data: data,
				id: "scatter-" + i,
				marker: {
					lineColor: null,
					lineWidth: 0
				},
				name: cx.scatterName,
				type: this.renderer,
				visible: isWorkspace() || !cx.seriesHidden,
				xDataId: componentX.id,
				yDataId: componentY.id,
				zDataId: componentZ.id
			});
		}

		if(isWorkspace()) page.updateMenu();

		if(jsonChartData.length > 0) {
			// Default the format of all three axes to number
			if(!categoryAxis.fmt) categoryAxis.fmt = "num";
			if(!dataAxis.fmt) dataAxis.fmt = "num";
			if(!zAxis.fmt) zAxis.fmt = "num";
		}

		// *******************
		// Sizing Calculations
		// *******************
		var marginTop = 20;
		if(this.showLegend) {
			var approxNumLegendLines = this.determineNumberOfLegendLines(seriesNames, fontFamily, ITEM_DISTANCE, LEGEND_LEFT_POS);
			marginTop = 36 + (24 * (approxNumLegendLines - 1));
		}

		if(parseInt(dataAxis.labelAngle) == -90 && !_this.invertAxes && !dataAxis.showLabel) chartNeedsLeftPadding = true;

		// Bubble size
		var maxBubbleSize = this.bubbleSizeCalculation(zAxis);

		var zoomType = "";
		if(this.zoom == "x" || this.zoom == "y" || this.zoom == "xy") {
			zoomType = this.zoom;
		}

		try {
			var plotTarget = "scatter-" + this.plotId;
			if (($("#" + plotTarget).length != 0) && (this.el.width() > 0)) {
				this.chart = new Highcharts.Chart({
					chart: {
						backgroundColor: null,
						borderColor: CXTheme.current.cx_bg,
						events: {
							click: function(event) {
								if(isWorkspace()) {
									page.invokeAction("select_component", _this);
								}
							}
						},
						height: this.chartHeight,
						inverted: this.invertAxes,
						marginTop: marginTop,
						plotBackgroundColor: null,
						plotBorderWidth: null,
						plotShadow: false,
						renderTo: plotTarget,
						spacingLeft: chartNeedsLeftPadding ? 25 : 5,
						spacingRight: 5,
						spacingTop: 40,
						style: {cursor: "auto"},
						width: this.el.width(),
						zoomType: zoomType
					},
					credits: {
						enabled: false
					},
					legend: {
						align: "right",
						borderWidth: 0,
						enabled: this.showLegend,
						floating: true,
						itemMarginBottom: 5,
						itemDistance : ITEM_DISTANCE,
						itemStyle: {
							color: CXTheme.current.cx_text,
							fontFamily: fontFamily,
							fontSize: "13px",
							fontWeight: "normal"
						},
						layout: "horizontal",
						verticalAlign: "top",
						x: LEGEND_LEFT_POS,
						y: -40
					},
					plotOptions: {
						bubble: {
							dataLabels: {
								color: CXTheme.current.cx_chart_axis_data_labels,
								enabled: (zAxis.bubbleLabels > 0),
								format: null,
								formatter: function() {
									var label = "";
									if(zAxis.bubbleLabels == 1) {
										label = FMT.formatForHighcharts(zAxis.fmt, [this.point.z], zAxis.getFmtArgs());
									} else if(zAxis.bubbleLabels == 2) {
										label = this.series.name;
									}
									return label;
								},
								style: { fontFamily: fontFamily, fontSize:"11px", fontWeight: "normal", textShadow: "none", color: CXTheme.current.cx_text }
							},
							marker: {
								fillOpacity: "0.5"
							},
							maxSize: maxBubbleSize
						},
						scatter: {
							borderColor: CXTheme.current.cx_bg,
							marker: {
								radius: parseInt(this.pointSize),
								fillOpacity: "0.5"
							}
						},
						series: {
							animation: !(this.getDashboard().isImaging() || isWorkspace()),
							events:{
								click: function(event) {
									if(isWorkspace()) {
										var clickComponentId = this.options.id.slice(this.options.id.indexOf("-") + 1);
										page.invokeAction("select_component", _this.components[clickComponentId]);
										return false;
									}
								},
								legendItemClick: function(event) {
									if(isWorkspace()) {
										var legendComponentId = this.options.id.slice(this.options.id.indexOf("-") + 1);
										page.invokeAction("select_component", _this.components[legendComponentId]);
										return false;
									} else {
										for(var i = 0; i < _this.chart.series.length; i++) {
											var componentId = this.options.id.slice(this.options.id.indexOf("-") + 1);
											if((_this.chart.series[i].name == "Regression Line") && (_this.chart.series[i].options.id == "regression-" + componentId)) {
												// The legendItemClick event hasn't finished yet, so if the scatter plot is still
												// visible, hide the corresponding regression line (and vice versa).
												if(this.visible) {
													_this.chart.series[i].hide();
												} else {
													_this.chart.series[i].show();
												}
											}
										}

										return _this.checkAndSetSeriesVisibility(event);
									}
								}
							}
						}
					},
					series: jsonChartData,
					title: {
						text: null
					},
					tooltip: {
						borderRadius: 4,
						borderWidth: 2,
						formatter: function() {
							var xDataCx, yDataCx, zDataCx;
							var label = $.trim(this.series.name);
							if(this.key && this.key.length > 0) {
								var componentId = this.series.options.id.slice(this.series.options.id.indexOf("-") + 1);
								var scatterData = _this.components[componentId];
								var labelComponent = scatterData.getChildByRole("data_labels");
								label = FMT.formatForHighcharts(labelComponent.fmt, [this.key], labelComponent.getFmtArgs());
							}
							var message = "";
							if(label.length > 0) message += "<b>"+ label +"</b><br/>";

							// Only need to check one axis component to see if the migration has been done, since the migration will be done on entire Klip.
							if (_this.isAutoFormatFeatureOn() && componentX.isAutoFormatMigrationDone()) {
								xDataCx = _this.getChildByProperty("id", this.series.options.xDataId, true);
								yDataCx = _this.getChildByProperty("id", this.series.options.yDataId, true);

								message += categoryAxis.label + ": " + FMT.formatForHighcharts(xDataCx.fmt, [this.x], xDataCx.getFmtArgs()) + "<br/>";
								message += dataAxis.label + ": " + FMT.formatForHighcharts(yDataCx.fmt, [this.y], yDataCx.getFmtArgs());

								if(_this.renderer == "bubble") {
									zDataCx = _this.getChildByProperty("id", this.series.options.zDataId, true);
									message += "<br/>" + zAxis.label + ": " + FMT.formatForHighcharts(zDataCx.fmt, [this.point.z], zDataCx.getFmtArgs());
								}
							} else {
								message += categoryAxis.label + ": " + FMT.formatForHighcharts(categoryAxis.fmt, [this.x], categoryAxis.getFmtArgs()) + "<br/>";
								message += dataAxis.label + ": " + FMT.formatForHighcharts(dataAxis.fmt, [this.y], dataAxis.getFmtArgs());
								if(_this.renderer == "bubble") {
									message += "<br/>" + zAxis.label + ": " + FMT.formatForHighcharts(zAxis.fmt, [this.point.z], zAxis.getFmtArgs());
								}
							}
							return message;
						},
						style: { fontFamily: fontFamily  }
					},
					xAxis: {
						endOnTick: !categoryAxis.customRange,
						events: {
							setExtremes: function(evt){
								// If both min and max are undefined, the axis is being reset back to normal
								var prop = "";
								if(evt.min != "undefined" || evt.max != "undefined") prop = evt.min + ":" + evt.max;
								_this.setComponentProp(_this.getIndexPath() + ":zoom_x_min_max", prop);
							}
						},
						gridLineColor: CXTheme.current.cx_chart_gridlines,
						gridLineWidth: categoryAxis.grid ? ((jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1) : 0,
						id: categoryAxis.id,
						labels: {
							align: (categoryAxis.labelAngle != "0") ? ((categoryAxis.labelAngle != "45") ? "right" : "left") : "center",
							enabled: categoryAxis.showAxisScale,
							formatter: function(){
								return FMT.stripNbsp(FMT.formatForHighcharts(categoryAxis.fmt, [this.value], categoryAxis.getFmtArgs()));
							},
							maxStaggerLines: 3,
							overflow: parseInt(categoryAxis.labelAngle) != -90 ? "justify" : null,
							rotation: parseInt(categoryAxis.labelAngle),
							staggerLines: (categoryAxis.labelStagger == 0 ? null : categoryAxis.labelStagger),
							step: (categoryAxis.labelDrop == 0 ? null : categoryAxis.labelDrop),
							style: { fontFamily: fontFamily, fontSize:"12px", color: CXTheme.current.cx_chart_axis_labels, cursor: isWorkspace() ? "pointer" : "auto" },
							x: (parseInt(categoryAxis.labelAngle) == 45 ? -3 : (parseInt(categoryAxis.labelAngle) == 0 ? 0 : 3)),
							y: (categoryAxis.labelAngle != "0" ? 14 : 18)
						},
						lineColor: CXTheme.current.cx_chart_axis,
						lineWidth: categoryAxis.showAxis ? ((jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1) : 0,
						min: (categoryAxis.customRange && categoryAxis.min) ? (categoryAxis.fmt == "pct" ? parseFloat(categoryAxis.min) / 100 : parseFloat(categoryAxis.min)) : null,
						minPadding: 0,
						max: (categoryAxis.customRange && categoryAxis.max) ? (categoryAxis.fmt == "pct" ? parseFloat(categoryAxis.max) / 100 : parseFloat(categoryAxis.max)) : null,
						startOnTick: !categoryAxis.customRange,
						tickColor: CXTheme.current.cx_chart_axis,
						tickInterval: (categoryAxis.customInterval != 0 && categoryAxis.tickInterval) ? (categoryAxis.fmt == "pct" ? parseFloat(categoryAxis.tickInterval.replace(",", "")) / 100 : parseFloat(categoryAxis.tickInterval.replace(",", ""))) : null,
						tickLength: categoryAxis.showLabelTicks ? 5 : 0,
						tickWidth: (jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1,
						title: {
							style: {color: CXTheme.current.cx_text, fontWeight: "bold", fontFamily: fontFamily, fontSize: "13px", cursor: isWorkspace() ? "pointer" : "auto" },
							text: categoryAxis.showLabel ? categoryAxis.label : null,
							margin: 15
						}
					},
					yAxis: {
						endOnTick: !dataAxis.customRange,
						events: {
							setExtremes: function(evt) {
								// If both min and max are undefined, the axis is being reset back to normal
								var prop = "";
								if(evt.min != "undefined" || evt.max != "undefined") prop = evt.min + ":" + evt.max;
								_this.setComponentProp(_this.getIndexPath() + ":zoom_y_min_max", prop);
							}
						},
						gridLineColor: CXTheme.current.cx_chart_gridlines,
						gridLineWidth: dataAxis.grid ? ((jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1) : 0,
						id: dataAxis.id,
						labels: {
							enabled: dataAxis.showAxisScale,
							formatter: function(){
								return FMT.formatForHighcharts(dataAxis.fmt, [this.value], dataAxis.getFmtArgs());
							},
							overflow: "justify",
							rotation: parseInt(dataAxis.labelAngle),
							style: { fontFamily: fontFamily, fontSize:"12px", color: CXTheme.current.cx_chart_axis_labels, cursor: isWorkspace() ? "pointer" : "auto" },
							x: (dataAxis.axisPosition == "right" ? (parseInt(dataAxis.labelAngle == -45) ? 12 : 10) : (parseInt(dataAxis.labelAngle) == 45 ? -12 : -8)),
							y: (parseInt(dataAxis.labelAngle) == 0 ? 3 : (parseInt(dataAxis.labelAngle) == -90 ? (dataAxis.axisPosition == "right" ? 10 : -6) : 6))
						},
						lineColor: CXTheme.current.cx_chart_axis,
						lineWidth: dataAxis.showAxis ? ((jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1) : 0,
						min: (dataAxis.customRange && dataAxis.min) ? (dataAxis.fmt == "pct" ? parseFloat(dataAxis.min) / 100 : parseFloat(dataAxis.min)) : null,
						minPadding: null,
						max: (dataAxis.customRange && dataAxis.max) ? (dataAxis.fmt == "pct" ? parseFloat(dataAxis.max) / 100 : parseFloat(dataAxis.max)) : null,
						maxPadding: null,
						opposite: (dataAxis.axisPosition == "right"),
						plotLines: [{
							value: (dataAxis.customOrigin && dataAxis.origin) ? (dataAxis.fmt == "pct" ? parseFloat(dataAxis.origin) / 100 : parseFloat(dataAxis.origin)) : 0,
							width: dataAxis.originLine ? 1 : 0,
							color: CXTheme.current.cx_chart_axis
						}],
						startOnTick: !dataAxis.customRange,
						tickColor: CXTheme.current.cx_chart_axis,
						tickInterval: (dataAxis.customInterval != 0 && dataAxis.tickInterval) ? (dataAxis.fmt == "pct" ? parseFloat(dataAxis.tickInterval.replace(",", "")) / 100 : parseFloat(dataAxis.tickInterval.replace(",", ""))) : null,
						tickLength: dataAxis.showLabelTicks ? 5 : 0,
						tickWidth: 1,
						title: {
							offset: (parseInt(dataAxis.labelAngle) == -90) ? 30 : null,
							style: {color: CXTheme.current.cx_text, fontWeight: "bold", fontFamily: fontFamily, fontSize: "13px", cursor: isWorkspace() ? "pointer" : "auto" },
							text: dataAxis.showLabel ? dataAxis.label : null
						}
					}
				}, function(){
					// Callback function executed after the chart finishes loading/rendering.
					// Restore the user's zoom setting if it was previously set.  See setExtremes() callbacks in the axis objects.
					var showResetButton = false;
					var propX = _this.getComponentProp(_this.getIndexPath() + ":zoom_x_min_max");
					if(propX) {
						var propXSplit = propX.split(":");
						if(propXSplit[0] != "undefined" && propXSplit[1] != "undefined") {
							this.xAxis[0].setExtremes(propXSplit[0] == "undefined" ? null : Number(propXSplit[0]), propXSplit[1] == "undefined" ? null : Number(propXSplit[1]));
							showResetButton = true;
						}
					}

					var propY = _this.getComponentProp(_this.getIndexPath() + ":zoom_y_min_max");
					if(propY) {
						var propYSplit = propY.split(":");
						if(propYSplit[0] != "undefined" && propYSplit[1] != "undefined") {
							this.yAxis[0].setExtremes(propYSplit[0] == "undefined" ? null : Number(propYSplit[0]), propYSplit[1] == "undefined" ? null : Number(propYSplit[1]));
							showResetButton = true;
						}
					}
					if(showResetButton) this.showResetZoom();
				});
			}
		} catch(e) {
			this.handleHighchartsError(e);
		}

		if(isWorkspace()) {
			this.setupClickHandling();
			this.drawSelectionBox(selectedComponent, selectedComponentIndex, false);
			this.drawDstHints();
		}

		this.setInvalid(false, true);

		if (!isWorkspace() && this.parent.isKlip) {
			this.parent.handleComponentUpdated();
		}
	},

	bubbleSizeCalculation : function(zAxis) {
		var maxBubbleSize = "30%";
		if(this.renderer == "bubble" && zAxis.customBubbleSize != 0 && zAxis.bubbleSize) {
			var customSize = parseFloat(zAxis.bubbleSize);
			if(zAxis.bubbleSize.lastIndexOf("%") == (zAxis.bubbleSize.length - 1)) {
				// Limit percentage-based bubble sizes to between 0 and 100
				if(customSize > 100) customSize = 100;
				if(customSize < 10) customSize = 10;
				if(isNaN(customSize)) customSize = 30;

				maxBubbleSize = customSize + "%";
			} else {
				// If the user enters a pixel-based bubble size that is too large, the chart will be blank
				if((customSize / this.chartHeight) > 0.95) {
					maxBubbleSize = "100%";
				} else {
					if(customSize < 9) maxBubbleSize = 9;
					else if(isNaN(customSize)) maxBubbleSize = 30 + "%";
					else maxBubbleSize = customSize;

				}
			}
		}
		return maxBubbleSize;
	},

	configure : function($super, config) {
		$super(config);
		bindValues(this, config, ["height","customHeight","showLegend","pointSize","invertAxes","renderer","zoom", "hideNullSeries"]);

		if (config.renderer && !this.isRenamed) {

			if (config["~propertyChanged"] && isWorkspace()) page.updateMenu();
		}

		if (config.height || config.customHeight) {
			this.changeHeight();
		}

		this.invalidate();
	},

	serialize : function($super) {
		var cfg = $super();

		bindValues(cfg, this,["renderer","height","customHeight","showLegend","pointSize","invertAxes","zoom", "hideNullSeries"],
			{
				renderer: "scatter",
				height: 2,
				customHeight: "",
				showLegend: true,
				pointSize: 6,
				invertAxes: false,
				zoom: "",
				hideNullSeries: false
			}, true);
		return cfg;
	},


	afterFactory : function() {
		var requiredChildren = [
			{role:"scatter_data", props:{displayName:"Untitled",type:"scatter_data",dataDisabled:true}} ,
			{role:"axis_x", props:{displayName:"Untitled",type:"chart_axis", grid:false, show_label:false, showAxis: true, showLabelTicks: true}} ,
			{role:"axis_y", props:{displayName:"Untitled",type:"chart_axis", grid:true, show_label:false}},
			{role:"axis_z", props:{displayName:"Untitled",type:"chart_axis", grid:false, show_label:false}}
		];

		for (var i = 0; i < requiredChildren.length; i++) {
			var req = requiredChildren[i];
			if (!this.getChildByRole(req.role)) {
				var cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
				if ( req.props._data ) cx.setData( req.props._data );
				this.addComponent(cx);
			}
		}

		var x = this.getChildByRole("axis_x");
		x.setAxisType(Component.ChartAxis.TYPE_CATEGORY);
		x.isDeletable = false;

		var y = this.getChildByRole("axis_y");
		y.setAxisType(Component.ChartAxis.TYPE_DATA);
		y.isDeletable = false;

		var z = this.getChildByRole("axis_z");
		z.setAxisType(Component.ChartAxis.TYPE_Z);
		z.isDeletable = false;

		if (this.isAutoFormatFeatureOn()) {
			x.disableFormatType = true;
			y.disableFormatType = true;
			// For bubble chart, there's no point of having data format properties on Z axis. since Z axis is not visible.
			// For tooltip formatting for Z-axis, it will use the format from the data-Z component instead of axis.
			z.disableFormatGroup = true;
		}
	},

	getPropertyModel  : function($super) {
		var model = $super();

		model.push({
			type: "button_group_ex",
			id: "renderer",
			label: "Chart Type",
			group: "chartType",
			options: Props.Defaults.scatterRenderers,
			selectedValue: this.renderer,
			help: {
				link: "klips/scatter-type"
			}
		});

		model.push({
			type:"select",
			id:"height",
			group:"height",
			label:"Size",
			options: Props.Defaults.chartSize,
			selectedValue: this.height
		});

		model.push({
			type: "text",
			id: "customHeight",
			label: "",
			minorLabels: [
				{label: "Height:", position: "left"},
				{label: "px", position: "right", css:"quiet italics"}
			],
			width: 36,
			group: "height",
			displayWhen: {height:5},
			value: this.customHeight
		});

		model.push({
			type: "checkboxes",
			id: "showLegend",
			options: [
				{value:1,label:"Show legend", checked: this.showLegend}
			],
			label: "Legend",
			group: "legend",
			value: this.showLegend
		});

		model.push({
			type: "button_group_ex",
			id: "pointSize",
			label: "Point Size",
			group: "pointSize",
			options: Props.Defaults.pointSizes,
			selectedValue: this.pointSize,
			displayWhen: {renderer: "scatter"}
		});

		model.push({
			type: "checkboxes",
			id: "hideNullSeries",
			options: [
				{value:1, label: "Remove series that only have zeros, blanks or nulls", checked: this.hideNullSeries}
			],
			label: "Blank series",
			group: "axes",
			value: this.hideNullSeries
		});

//		model.push({
//			type: 'checkboxes',
//			id: 'invertAxes',
//			options: [
//				{value:1, label: "Switch position of X and Y axes", checked: this.invertAxes}
//			],
//			label: "Invert Axes",
//			group: "axes",
//			value: this.invertAxes
//		});

		model.push({
			type: "select",
			id: "zoom",
			group: "zoom",
			label: "Allow Zoom",
			help: {
				link: "klips/chart-zoom"
			},
			options: Props.Defaults.chartZoom,
			selectedValue: this.zoom
		});

		var _this = this;
		model.push({
			type:"button",
			id:"addScatter",
			label:"Data Series",
			group:"addComponents",
			text:"Add Scatter",
			onClick: function(evt) {
				page.invokeAction("add_component", {parent:_this, type:"scatter_data", role:"scatter_data"}, {});
			}
		});

		return model;
	},

	drawDstHints: function () {
		if (!this.chart) return;
		var hintDimension = 14, pixelPadding = 1;
		var filterHintIcon = "/images/workspace/dst-hints/dst-hint-filter.svg";
		var isFilterApplied;

		this.chart.series.forEach(function (series) {
			var legendItems, dimId;
			var legendBox, xPos, yPos;

			var correspondingComponent = this.findComponentBySeriesId(series.options.id);

			if (!correspondingComponent) {
				return;
			}

			dimId = correspondingComponent.getVirtualColumnId();
			isFilterApplied = correspondingComponent.findDstOperationByType("filter", dimId);

			if (series.legendGroup) {
				legendBox = series.legendGroup.element.getBBox();


				xPos = Math.round(legendBox.x + legendBox.width + 3 * pixelPadding);
				yPos = Math.round(legendBox.y) + 2 * pixelPadding;

				legendItems = $(series.legendGroup.element);

				if (isFilterApplied) {
					legendItems.append(this.createSVGImage(filterHintIcon, {
						x: xPos,
						y: yPos,
						width: hintDimension,
						height: hintDimension
					}, "Filter is applied"));
				}
			}

		}.bind(this));
	}

});
;/****** cx.chart_series.js *******/ 

/* global Component:false, CXTheme:false, FMT:false, Props:false, KlipFactory:false, page:false, isWorkspace:false, bindValues:false, Highcharts:false */
Component.ChartSeries = $.klass(Component.ChartCartesian, {

	displayName: "Bar/Line Chart",
	factory_id:"chart_series",
	variableSubComponents: [
		{ label:"Series", type:"series_data", role:"series" },
		{ label:"Y-Axis", type:"chart_axis", role:"axis_y", findByRole:true }
	],

    customHeight:"",
	showValues:false,
	stackBars: 0,
	stackLines: 0,
	stackAreas: 0,
	canHaveDataSlots: true,
    AXIS_LABEL_WIDTH:80,

	renderDom : function($super) {
		$super();

		// note, we reassign the div ID after this component is assigned to a klip to make its ID unique across
		// multiple instances
		// -----------------------------------------------------------------------------------------------------
		this.plotId = this.id + "-" + _.uniqueId();
		this.$chart = $("<div>").attr("id", "chart-" + this.plotId).appendTo(this.el);

        this.changeHeight();
	},

    getWorkspaceControlModel : function($super, fromChild) {
        var model = $super();

        model.push({
            name:"Series",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this, type:"series_data", role:"series"}},
                    {type:"remove", disabled:true}
                ]
            ]
        });

        model.push({
            name:"Y-Axis",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this, type:"chart_axis", role:"axis_y"}},
                    {type:"remove", disabled:true}
                ]
            ]
        });

		if (!fromChild) {
			this.addDataSlotControl(model);
		}

        return model;
    },
    addChildByType : function($super, config) {
        var newChild = $super(config);

        if (config.type == "chart_axis" && config.role == "axis_y") {
            newChild = KlipFactory.instance.createComponent({
                displayName: "New Y Axis",
                role: "axis_y",
                type: "chart_axis",
				axisPosition: "right"
            });

            // Insert the new y-axis after the last existing y-axis (ie, at the end of the list)
            newChild.setAxisType(Component.ChartAxis.TYPE_DATA);
			if (newChild.isAutoFormatFeatureOn()) {
				newChild.disableFormatType = true;
			}
        } else if (config.type == "series_data" && config.role == "series") {
            newChild = KlipFactory.instance.createComponent({
                type :"series_data",
                displayName :"New Series",
                role:"series",
                renderer: config.sibling ? config.sibling.renderer : "bar",
                chartStyle: config.sibling && config.sibling.renderer == "line" ? config.sibling.chartStyle : "points"
            });
        }

        this.addComponent(newChild, config.index);

        return newChild;
    },

    removeComponent : function($super, cx, nextActive, detach) {
		var newAxis, i, otherCx;

        $super(cx, nextActive, detach);

        if (cx.factory_id == "chart_axis" && cx.role == "axis_y" && nextActive) {
            newAxis = (nextActive.role == "axis_y" ? nextActive.id : null);
            for (i = 0; i < this.components.length; i++) {
                otherCx = this.components[i];
                // Update the associated y-axis of any series using the deleted axis
                if (otherCx.role == "series" && otherCx.axis == cx.id) {
                    otherCx.axis = newAxis;
                }
            }
        }

        if (cx.factory_id == "series_data" && cx.role == "series") {
			// When a series is deleted, we need to get through the data components and update corresponding axis format.
			cx.updateAxisFormat();
		}
    },

	update : function($super) {
		var cxLen,
			jsonChartData, jsonChartBarData, jsonChartLineData, jsonYAxisData,
			dataAxisMap,
			maxValue, minValue,
			categoryAxis,
			xAxisData,
			renderers,
			firstYAxisId,
			seriesNames,
			chartIsSparse, chartHasRightAxis, chartNeedsLeftPadding, chartNeedsRightPadding,
			rightAxisLabelRotation,
			selectedComponent, selectedComponentIndex,
			fontFamily,
			_this;

		var ITEM_DISTANCE, LEGEND_LEFT_POS, LABEL_PADDING;

		var yAxisCounter, y, axis;

		var xAxisLength, s, numToAdd, t, sortArray, a, dataToInsert, formattedDate, epoch;
		var counter, i, b, ridx, seriesMax, seriesMin, cx, data, dataLen, reactionContext;
		var useAxisNum, useAxisId, useAxis, sanitizedNum, dataPoints, numNullsToAdd, n;
		var color, colouredData, c, dataPointColour, sortedData, ss, chartType;
		var yy, w, checkAxis, checkNumTicks, xx, yAxesWidth, widestYAxisValue;
		var formattedValue, xAxisWidth, xAxisTickInterval, marginTop, spacingTop, approxNumLegendLines, yAxesHeight;
		var xAxisMax, xAxisMin, xLength, widestXAxisValue, numTicks, xAxisDataDensity;
		var xAxisProps, xAxisLabelAlign, zoomType, plotTarget, result;

		$super();

        // exit early optimizations
        // ------------------------
        if ( !this.getKlip().isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if(this.$chart) {
			// Remove the old chart so that we start fresh
			this.$chart.empty();
		}

		// *******************
		// Data Manipulation
		// *******************
		cxLen = this.components.length;
		jsonChartData = [];
		jsonChartBarData = [];
		jsonChartLineData = [];
		jsonYAxisData = [];
		dataAxisMap = [];
		maxValue = 0;
		minValue = 0;
		categoryAxis = this.getChildByRole("axis_x");
		xAxisData = categoryAxis.getData(0).slice(0);
		renderers = ["bar","line"];
		firstYAxisId = "";
		seriesNames = [];
		chartIsSparse = true;
		chartHasRightAxis = false;
		chartNeedsLeftPadding = false;
		chartNeedsRightPadding = false;
		rightAxisLabelRotation = 0;
		selectedComponent;
		selectedComponentIndex = 0;
		fontFamily = this.getFontFamily();
		_this = this;
		//Need to increase distance between legend entries to allow space for dst icons (sort & filter)
		ITEM_DISTANCE = isWorkspace() ? 40 : 20;
		LEGEND_LEFT_POS = -15;
		LABEL_PADDING = 60;

		// Gather information about the y-axes.
		yAxisCounter = 0;
		for(y = 0; y < cxLen; y++) {
			axis = this.components[y];
			if(axis.role.indexOf("axis") == 0 && axis.selected) {
				selectedComponent = axis;
				selectedComponentIndex = y;
			}
			if(axis.role != "axis_y") continue;

			dataAxisMap[axis.id] = yAxisCounter;
			if(yAxisCounter == 0) firstYAxisId = axis.id;
			yAxisCounter++;

			// Default to number format for all y-axes
			if(!axis.fmt) axis.fmt = "num";

			// Create a new variable scope for this iteration so that the label
			// formatter callback has the correct value of "y" for this series.
			(function(index) {
				var plotLines = [];

				var axisLabelX, axisLabelY;
				if(axis.originLine) {
					plotLines = [{
						value: (axis.customOrigin && axis.origin) ? (axis.fmt == "pct" ? parseFloat(axis.origin) / 100 : parseFloat(axis.origin)) : 0,
						width: 1,
						color: CXTheme.current.cx_chart_axis
					}];
				}

				axisLabelX = 0;
				axisLabelY = 0;
				switch(parseInt(axis.labelAngle)) {
					case 0:
					default:
						if(_this.invertAxes) {
							if (axis.axisPosition == "right") {
								axisLabelX = -4;
								axisLabelY = -12;
							} else {
								axisLabelX = -4;
								axisLabelY = 20;
							}
						} else {
							if (axis.axisPosition == "right") {
								axisLabelX = 8;
								axisLabelY = 3;
							} else {
								axisLabelX = -8;
								axisLabelY = 3;
							}
						}
						break;

					case 45:
						if(_this.invertAxes) {
							if(axis.axisPosition == "right") {
								axisLabelX = -8;
								axisLabelY = -12;
							} else {
								axisLabelX = 4;
								axisLabelY = 24;
							}
						} else {
							if(axis.axisPosition == "right") {
								axisLabelX = 8;
								axisLabelY = 6;
							} else                             {
								axisLabelX = -12;
								axisLabelY = 6;
							}
						}
						break;

					case -45:
						if(_this.invertAxes) {
							if(axis.axisPosition == "right") {
								axisLabelX = 10;
								axisLabelY = -16;
							} else                             {
								axisLabelX = -4;
								axisLabelY = 18;
							}
						} else {
							if(axis.axisPosition == "right") {
								axisLabelX = 10;
								axisLabelY = 4;
							} else                             {
								axisLabelX = -8;
								axisLabelY = 6;
							}
						}
						break;

					case -90:
						if(_this.invertAxes) {
							if(axis.axisPosition == "right") {
								axisLabelX = 4;
								axisLabelY = -20;
							} else {
								axisLabelX = 4;
								axisLabelY = 24;
							}
						} else {
							if(axis.axisPosition == "right") {
								axisLabelX = 14; axisLabelY = 16;
							} else                             {
								axisLabelX = -8; axisLabelY = -6;
							}
						}
						break;
				}

				if(axis.axisPosition == "right") {
					chartHasRightAxis = true;
					rightAxisLabelRotation = axis.labelAngle;
				}
				if(parseInt(axis.labelAngle) == -90 && !_this.invertAxes && axis.axisPosition == "left" && !axis.showLabel) chartNeedsLeftPadding = true;
				if(parseInt(axis.labelAngle) == -90 && !_this.invertAxes && axis.axisPosition == "right" && !axis.showLabel) chartNeedsRightPadding = true;

				jsonYAxisData.push({
					endOnTick: !axis.customRange,
					events: {
						setExtremes: function(evt) {
							var prop;

							// Only save the zoom information for the primary y-axis
							if(this.options.index == 0) {
								// If both min and max are undefined, the axis is being reset back to normal
								prop = "";
								if(evt.min != "undefined" || evt.max != "undefined") prop = evt.min + ":" + evt.max;
								_this.setComponentProp(_this.getIndexPath() + ":zoom_y_min_max", prop);
							}
						}
					},
					gridLineColor: CXTheme.current.cx_chart_gridlines,
					gridLineWidth: axis.grid ? ((jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1) : 0,
					id: axis.id,
					labels: {
						enabled: axis.showAxisScale,
						formatter: function(){
							return FMT.formatForHighcharts(_this.components[index].fmt, [this.value], _this.components[index].getFmtArgs());
						},
						overflow: "justify",
						rotation: parseInt(axis.labelAngle),
						style: { fontFamily: fontFamily, fontSize:"12px", color: CXTheme.current.cx_chart_axis_labels, cursor: isWorkspace() ? "pointer" : "auto" },
						x: axisLabelX,
						y: axisLabelY
					},
					lineColor: CXTheme.current.cx_chart_axis,
					lineWidth: axis.showAxis ? ((jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1) : 0,
					min: (axis.customRange && axis.min) ? (axis.fmt == "pct" ? parseFloat(axis.min) / 100 : parseFloat(axis.min)) : null,
					minPadding: null,
					max: (axis.customRange && axis.max) ? (axis.fmt == "pct" ? parseFloat(axis.max) / 100 : parseFloat(axis.max)) : null,
					maxPadding: null,
					opposite: (axis.axisPosition == "right"),
					plotLines: plotLines,
					startOnTick: !axis.customRange,
					tickColor: CXTheme.current.cx_chart_axis,
					tickLength: axis.showLabelTicks ? 5 : 0,
					tickWidth: 1,
					tickInterval: (axis.customInterval != 0 && axis.tickInterval) ? (axis.fmt == "pct" ? parseFloat(axis.tickInterval.replace(",", "")) / 100 : parseFloat(axis.tickInterval.replace(",", ""))) : null,
					title: {
						margin: (_this.invertAxes ? (axis.axisPosition == "right" ? (parseInt(axis.labelAngle) == 0 ? 8 : -2) : 20) : 12),
						offset: (parseInt(axis.labelAngle) == -90) ? 30 : null,
						style: {color: CXTheme.current.cx_text, fontWeight: "bold", fontFamily: fontFamily, fontSize: "13px", cursor: isWorkspace() ? "pointer" : "auto" },
						text: axis.showLabel ? axis.label : null
					}
				});
			})(y);
		}

		// Determine the length of the x-axis.  The x-axis will extend to match the length of the longest series of data.
		xAxisLength = xAxisData.length;
		for(s = 0; s < cxLen; s++) {
			if (this.components[s].role != "series") continue;
			xAxisLength = Math.max(xAxisLength, this.components[s].getData(0).slice(0).length);
		}

		// Pad the x-axis data if necessary, using consecutive numbers.
		// eg, if there should be 7 data points in the x-axis, and the data is
		// ["Apple,"Banana","Lemon","Strawberry","Watermelon"], then add [6,7] to the axis.
		if(xAxisLength > xAxisData.length) {
			numToAdd = xAxisLength - xAxisData.length;
			for(t = 0; t < numToAdd; t++) {
				xAxisData.push(xAxisData.length + 1);
			}
		}

		if(categoryAxis.sort != 0) {
			// Determine the sort order of the data points
			sortArray = [];
			for(a = 0; a < xAxisData.length; a++) {
				dataToInsert = xAxisData[a];
				if(categoryAxis.fmt == "dat") {
					formattedDate = FMT.format(categoryAxis.fmt, [xAxisData[a]], categoryAxis.getFmtArgs());
					if(formattedDate.indexOf("epoch") != -1) {
						epoch = formattedDate.slice(formattedDate.indexOf("=\"") + 2, formattedDate.lastIndexOf("\">"));
						if(epoch.length) {
							dataToInsert = Number(epoch);
						}
					}
				} else if(categoryAxis.fmt == "dur") {
					dataToInsert = Number(dataToInsert);
				}

				sortArray.push({index: a, value: dataToInsert});
			}

			switch (Number(categoryAxis.sort)) {
				case 1: // Ascending
					sortArray.sort(function (a, b) {
						return (a.value < b.value?-1:(a.value == b.value?0:1));
					});
					break;
				case 2: // Descending
					sortArray.sort(function (a, b) {
						return (a.value > b.value?-1:(a.value == b.value?0:1));
					});
					break;
				default:
					break;
			}

			for(b = 0; b < sortArray.length; b++) {
				xAxisData[b] = sortArray[b].value;
			}
		}

		// foreach renderer, go through each cx to group renderers together
		// ----------------------------------------------------------------
		counter = 0;
		for (ridx  in renderers) {
			for (i = 0; i < cxLen; i++) {
				seriesMax = 0;
				seriesMin = 0;
				cx = this.components[i];

				if (cx.role != "series") continue;
				if (cx.renderer != renderers[ridx] ) continue;

				// This is counting the order the series are rendered in, which is different than the order of the components
				// because all bar series are rendered first, then all line series.  The algorithm that draws the selection
				// boxes needs to know the series render order -- see drawSelectionBox() in cx.chart_cartesian.js.
				if(cx.selected) {
					selectedComponent = cx;
					selectedComponentIndex = counter;
				}
				if(!selectedComponent) {
					counter++;
				}

				reactionContext = {seriesColours: {}};
				cx.collectReactions(reactionContext);

				seriesNames.push(cx.seriesName);

				// don't mess with the actual data
				data = cx.getData(0).slice(0);
				dataLen = data.length;

				if(this.hideNullSeries && cx.isAllFalsyData(data)){
					continue;
				}

				if (dataLen == 0) {
					if(!isWorkspace()) continue;
					data = [0];
				} else {
					chartIsSparse = chartIsSparse && (dataLen <= 1);
				}

				// To maintain backwards compatibility, if the series has no y-axis associated with it, use the first (primary) axis.
				useAxisNum = cx.axis ? dataAxisMap[cx.axis] : 0; // used for the yAxis property below
				useAxisId = cx.axis ? cx.axis : firstYAxisId; // used by the dataLabel formatter
				useAxis = this.getChildById(useAxisId); // used by the threshold property below

				// _sanitizeNumbers() isn't used here because it changes blank/null values to 0.  If the
				// user wants to show gaps for blank values, the blank values must be converted to null
				// in the data array.
				while(--dataLen > -1) {
					if(data[dataLen] == Infinity || data[dataLen] == -Infinity) data[dataLen] = null;

					sanitizedNum = FMT.convertToNumber(data[dataLen]);

					if((sanitizedNum == undefined  || !sanitizedNum) && !cx.leaveGaps) {
						data[dataLen] = 0;
					} else {
						data[dataLen] = sanitizedNum;
					}

					if((useAxis.fmt == "pct") && (data[dataLen] != null)) {
						// Prevent data formatted as percentage from being too large - see saas-2267 and fmt.percentage.js.
						// See also the axis range, axis interval and custom origin options.
						data[dataLen] = data[dataLen] / 100;
					}

					seriesMax = Math.max( seriesMax, data[dataLen] );
					seriesMin = Math.min( seriesMin, data[dataLen] );
				}

				maxValue = Math.max(seriesMax, maxValue);
				minValue = Math.min(seriesMin, minValue);

				//Catch how many actual data points there are before padding
				dataPoints = data.length;

				if(xAxisData.length > data.length) {
					// Pad the data with nulls if there are not enough data points to fill the x-axis
					numNullsToAdd = xAxisData.length - data.length;
					for(n = 0; n < numNullsToAdd; n++) {
						data.push(null);
					}
				}

				// set series color...
				// if in the workspace and cx is not the selected series, color it grey
				// ---------------------------------------------------------------------
				color = cx.overrideColor ? CXTheme.getThemeColour(cx.color) : CXTheme.getThemeColour(CXTheme.current.series_colors[i % 9]);

				if ( isWorkspace() && page.activeComponent.role =="series" && page.activeComponent != cx ) {
					color = "#d6d6d6";
				}

				colouredData = [];
				for(c = 0; c < data.length; c++) {
					dataPointColour = (reactionContext.seriesColours[c] && color != "#d6d6d6") ? reactionContext.seriesColours[c] : color;
					if(cx.renderer == "bar") {
						colouredData.push({y: data[c], color: dataPointColour});
					} else {
						colouredData.push({y: data[c], marker: {fillColor: dataPointColour, states: {hover: {fillColor: dataPointColour}}}});
					}
				}

				sortedData = colouredData;
				if(categoryAxis.sort != 0) {
					// Re-order the data in this series to match the sorted x-axis
					sortedData = [];
					for(ss = 0; ss < data.length; ss++) {
						sortedData.push(colouredData[sortArray[ss].index]);
					}
				}

				chartType = "column";
				if(cx.renderer == "line") {
					if(cx.lineStyle == "curve") {
						if(cx.chartStyle.indexOf("area") != -1) {
							chartType = "areaspline";
						} else {
							chartType = "spline";
						}
					} else {
						if(cx.chartStyle.indexOf("area") != -1) {
							chartType = "area";
						} else {
							chartType = "line";
						}
					}
				}


				// Create a new variable scope for this iteration so that the dataLabel
				// formatter callback has the correct axis id.
				(function(id, expressPercentage) {
					// The id property is used by the series click handlers to associate series
					// with their respective components.

					var dataLabelYPosPoints = [0, 1, -1, -3, -5, -7];
					var dataLabelYPosLine = [0, 1, 0, -1, -3, -5, -7, -9, -10];
					var componentIndex = i;
					var seriesObject = {
						color: color,
						dashStyle: cx.lineDashes,
						data: sortedData,
						dataLabels: {
							align: _this.invertAxes && !(cx.renderer == "bar" && _this.stackBars != 0) ? "left" : "center",
							crop: true,
							enabled: cx.showValues,
							formatter: function(){
								var component, axis, useFmt, label;

								var series = _this.getChildById(this.series.options.seriesId);
								var useValue = this.y;

								if (_this.isAutoFormatFeatureOn() && series.isAutoFormatMigrationDone()) {
									component = cx.parent.components[componentIndex];
									useFmt = component.fmt;

									if(_this.stackBars == 2 && expressPercentage) {
										useFmt = "pct";
										useValue = this.percentage / 100;
									}

									label = FMT.formatForHighcharts(useFmt, [useValue], component.getFmtArgs());
								} else {
									axis = _this.getChildById(id);
									useFmt = axis.fmt;

									if(_this.stackBars == 2 && expressPercentage) {
										useFmt = "pct";
										useValue = this.percentage / 100;
									}

									label = FMT.formatForHighcharts(useFmt, [useValue], axis.getFmtArgs());
								}

								if(label == "&nbsp;" || label == "null") {
									label = "";
								}

								return label;
							},
							style: {
								fontFamily: fontFamily,
								fontSize:"11px",
								color: CXTheme.current.cx_text,
								textOutline: "2px " + CXTheme.current.cx_text_outline + " 0.7"
							},
							x: _this.invertAxes ? (cx.renderer == "bar" ? 2 : 5) : -2,
							y: _this.invertAxes ? (cx.renderer == "bar" ? -2 : 8) : (cx.chartStyle == "points_no_line" ? dataLabelYPosPoints[parseInt(cx.pointSize) / 2] : dataLabelYPosLine[parseInt(cx.lineWeight)])
						},
						id: "series-" + i,
						lineWidth: (cx.chartStyle == "points_no_line") ? 0 : parseInt(cx.lineWeight),
						marker: {
							enabled: (cx.chartStyle.indexOf("points") != -1 || dataPoints == 1),
							radius:  (cx.chartStyle == "points_no_line") ? parseInt(cx.pointSize) : parseInt(cx.lineWeight) + 1
						},
						name: cx.seriesName,
						seriesId: cx.id,
						step: (cx.lineStyle == "square"),
						stack: (cx.renderer == "bar" ? ((chartType.indexOf("area") != -1) ? "stack_areas" : "stack_bars") : "stack_lines"),
						states: {
							hover: {
								lineWidth: parseInt(cx.lineWeight) + 1
							}
						},
						threshold: _this._getSeriesThreshold(useAxis),
						type: chartType,
						visible: isWorkspace() || !cx.seriesHidden,
						yAxis: useAxisNum,
						zIndex: cx.renderer == "bar" ? 1 : 2
					};

					if(cx.renderer == "bar") {
						jsonChartBarData.push(seriesObject);
					} else {
						jsonChartLineData.push(seriesObject);
					}
				})(useAxisId, cx.expressPercentage);
			}
		}

		if(jsonChartBarData.length == 0 && jsonChartLineData.length == 0) return;

		// Default to text format for the x-axis
		if(!categoryAxis.fmt) categoryAxis.fmt = "txt";

		for(yy = 0; yy < cxLen; yy++) {
			checkAxis = this.components[yy];
			if(checkAxis.role != "axis_y") continue;

			checkNumTicks = (maxValue - minValue) / checkAxis.tickInterval;

			if(checkNumTicks > 101) {
				// If the specified tick interval would generate too many ticks for this y-axis,
				// ignore the specified tick interval and use the highcharts default instead.
				for(xx = 0; xx < jsonYAxisData.length; xx++) {
					if(jsonYAxisData[xx].id == checkAxis.id) {
						jsonYAxisData[xx].tickInterval = null;
					}
				}
			}
		}

		// *******************
		// Sizing Calculations
		// *******************

		yAxesWidth = 0;
		widestYAxisValue = (Math.abs(minValue) > maxValue) ? minValue : maxValue;
		for(w = 0; w < this.components.length; w++) {
			if(this.components[w].role != "axis_y") continue;

			// Assume 7 pixels per character
			formattedValue = FMT.format(this.components[w].fmt, [widestYAxisValue], this.components[w].getFmtArgs());
			yAxesWidth += formattedValue.length * 7;
		}

		// Assume 15 pixels on either side of the graph for padding
		xAxisWidth = this.el.width() - yAxesWidth - 30;

		// Determine the number of x-axis ticks to display if the user chose "Auto" for Tick Interval.
		// This isn't necessary if the X-axis is automatically generating its own data.
		xAxisTickInterval = null;
		if(categoryAxis.customInterval == 0 && xAxisData.length > 0 && categoryAxis.fmt != "txt" && !FMT.isDateFormat(categoryAxis.fmt)) {
			xAxisMax = 0;
			xAxisMin = 0;
			xLength = xAxisData.length;
			while(--xLength > -1) {
				xAxisMax = Math.max( xAxisMax, xAxisData[xLength] );
				xAxisMin = Math.min( xAxisMin, xAxisData[xLength] );
			}
			widestXAxisValue = (Math.abs(xAxisMin) > xAxisMax) ? xAxisMin : xAxisMax;
			widestXAxisValue = FMT.format(categoryAxis.fmt, [widestXAxisValue], categoryAxis.getFmtArgs());

			numTicks = Math.floor(xAxisWidth / (widestXAxisValue.length * 7));
			xAxisTickInterval = Math.ceil(xAxisData.length / numTicks);
		}

		// If the chart is narrow and has many data points, increase the spacing between each column
		xAxisDataDensity = xAxisWidth / xAxisData.length;

		marginTop = 20;
		spacingTop = 40;

		if(this.showLegend) {
			approxNumLegendLines = this.determineNumberOfLegendLines(seriesNames, fontFamily, ITEM_DISTANCE, LEGEND_LEFT_POS);
			marginTop = 36 + (24 * (approxNumLegendLines - 1));
			if(chartHasRightAxis && this.invertAxes) {
				// The right-hand y-axis moves to the top when the chart is inverted.  If the label angle is 0 the
				// labels are displayed horizontally, so allow 20 pixels between the axis and the legend.
				// If the labels are not rotated, use the y axis width calculation instead.
				yAxesHeight = 20;
				if(rightAxisLabelRotation != 0) {
					yAxesHeight = yAxesWidth;
				}
				marginTop += yAxesHeight;
			}
		} else {
			if(this.invertAxes && chartHasRightAxis) {
				// The right-hand y-axis moves to the top when the chart is inverted.  If the
				// legend isn't displayed, let Highcharts automatically adjust the padding.
				marginTop = null;
				spacingTop = 10;
			}
		}

		xAxisLabelAlign = "right";
		if(!this.invertAxes) {
			xAxisLabelAlign = (categoryAxis.labelAngle=== "auto") ? "right" : ((categoryAxis.labelAngle != "0") ? ((categoryAxis.labelAngle != "45") ? "right" : "left") : "center");
		}

		zoomType = "";
		if(this.zoom == "x" || this.zoom == "y" || this.zoom == "xy") {
			zoomType = this.zoom;
		}

		if(this.stackAreas != 0 || this.stackBars != 0 || this.stackLines != 0) {
			// Highcharts draws stacked bars in the order they appear in the legend from right-to-left (ie, the
			// right-most legend item is on the bottom of the stack).  We want the left-most legend item to be
			// on the bottom of the stack, so reverse the order of the series before plotting, then reverse the
			// legend so that the legend remains in the original order.
			jsonChartData = jsonChartLineData.reverse().concat(jsonChartBarData.reverse());
		} else {
			jsonChartData = jsonChartBarData.concat(jsonChartLineData);
		}

			plotTarget = "chart-" + this.plotId;
			result;

			xAxisProps = {
				categories: (chartIsSparse || xAxisData.length > 0) ? xAxisData : null,
				events: {
					setExtremes: function(evt){
						// If both min and max are undefined, the axis is being reset back to normal
						var prop = "";
						if(evt.min != "undefined" || evt.max != "undefined") prop = evt.min + ":" + evt.max;
						_this.setComponentProp(_this.getIndexPath() + ":zoom_x_min_max", prop);
					}
				},
				tickPositioner: this._tickPositioner.bind(this, chartIsSparse, xAxisData, categoryAxis),
				gridLineColor: CXTheme.current.cx_chart_gridlines,
				gridLineWidth: categoryAxis.grid ? ((jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1) : 0,
				id: categoryAxis.id,
				lineColor: CXTheme.current.cx_chart_axis,
				lineWidth: categoryAxis.showAxis ? ((jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1) : 0,
				minPadding: 0,
				tickColor: CXTheme.current.cx_chart_axis,
				tickInterval: (categoryAxis.customInterval != 0 && categoryAxis.tickInterval) ? parseFloat(categoryAxis.tickInterval.replace(",", "")) : xAxisTickInterval,
				tickLength: categoryAxis.showLabelTicks ? 5 : 0,
				tickWidth: (jQuery.browser.msie && jQuery.browser.version < 9) ? 2 : 1,
				title: {
					style: {color: CXTheme.current.cx_text, fontWeight: "bold", fontFamily: fontFamily, fontSize: "13px", cursor: isWorkspace() ? "pointer" : "auto"},
					text: categoryAxis.showLabel ? categoryAxis.label : null,
					margin:15
				}
			};

			/* This is a performance optimization:
			 * If labelDrop is set to auto (ie: === 0) we depend on the chart width to calculate the label step.
			 * Chart width will be 0 if the chart has not been rendered so this allows us to skip rendering labels
			 * if we know we have to render them again later (see the end of this function for the update to xAxis labels)
			 */
			if (categoryAxis.labelDrop !== 0 || this.invertAxes) {
				xAxisProps.labels = _this._getXAxisLabelProperties(xAxisLabelAlign, categoryAxis, fontFamily)
			}

			if (($("#" + plotTarget).length != 0) && (this.el.width() > 0)) {
				this.chart = new Highcharts.Chart({
					chart: {
						alignTicks: false,
						backgroundColor: null,
						borderColor: CXTheme.current.cx_bg,
						events: {
							click: function(event) {
								if(isWorkspace()) {
									page.invokeAction("select_component", _this);
								}
							}
						},
						height: this.chartHeight,
						inverted: this.invertAxes,
						marginTop: marginTop,
						plotBackgroundColor: null,
						plotBorderWidth: null,
						plotShadow: false,
						renderTo: plotTarget,
                        resetZoomButton: {
                            theme: {
                                fill: CXTheme.current.cx_chart_button_back_on,
                                stroke: CXTheme.current.cx_chart_button_border_on,
                                r: 0,
                                style: {
                                    color: CXTheme.current.cx_chart_button_label_on,
                                    cursor: "pointer"
                                },
                                states: {
                                    hover: {
                                        fill: CXTheme.current.cx_chart_button_back_hover,
                                        stroke: CXTheme.current.cx_chart_button_border_hover
                                    }
                                }
                            }

                        },
						spacingLeft: chartNeedsLeftPadding ? 25 : 5,
						spacingRight: (_this.invertAxes) ? 20 : (chartNeedsRightPadding ? 25 : 5),
						spacingTop: spacingTop,
						style: {cursor: "auto"},
						width: this.el.width(),
						zoomType: zoomType
					},
					credits: {
						enabled: false
					},
					legend: {
						align: "right",
						alignColumns: false,
						borderWidth: 0,
						enabled: this.showLegend,
						floating: true,
						itemMarginBottom: 5,
						itemDistance : ITEM_DISTANCE,
						itemStyle: {
							color: CXTheme.current.cx_text,
							fontFamily: fontFamily,
							//if el.width and LABEL_PADDING are almost the same size, don't include LABEL_PADDING
							width: this.el.width() > (LABEL_PADDING + 5) ? this.el.width() - LABEL_PADDING : this.el.width() ,
							fontSize: "13px",
							fontWeight: "normal"
						},
						layout: "horizontal",
						reversed: (this.stackAreas != 0 || this.stackBars != 0 || this.stackLines != 0),
						verticalAlign: "top",
						x: LEGEND_LEFT_POS,
						y: -40
					},
					plotOptions: {
						area: {
							stacking: (this.stackAreas != 0 ? (this.stackAreas == 2 ? "percent" : "normal") : null),
							shadow: false
						},
						areaspline: {
							shadow: false,
							stacking: (this.stackAreas != 0 ? (this.stackAreas == 2 ? "percent" : "normal") : null)
						},
						column: {
							borderWidth: 0,
							groupPadding: xAxisDataDensity > 9 ? 0.07 : 0.1,
							pointPadding: xAxisDataDensity > 9 ? 0.05 : 0.08,
							shadow: false,
							stacking: (this.stackBars != 0 ? (this.stackBars == 2 ? "percent" : "normal") : null)
						},
						line: {
							shadow: false,
							stacking: (this.stackLines != 0 ? (this.stackLines == 2 ? "percent" : "normal") : null)
						},
						series: {
							animation: !(this.getDashboard().isImaging() || isWorkspace()),
							borderColor: CXTheme.current.cx_bg,
							events: {
								click: function(event) {
									var componentId;

									if(isWorkspace()) {
										componentId = this.options.id.slice(this.options.id.indexOf("-") + 1);
										page.invokeAction("select_component", _this.components[componentId]);
										return false;
									}
								},
								legendItemClick: function(event) {
									var componentId;

									if(isWorkspace()) {
										componentId = this.options.id.slice(this.options.id.indexOf("-") + 1);
										page.invokeAction("select_component", _this.components[componentId]);
										return false;
									} else {
										return _this.checkAndSetSeriesVisibility(event);
									}
								}
							},
							fillOpacity: 0.2,
							pointStart: (xAxisData.length > 0 ? 0 : 1)
						},
						spline: {
							shadow: false,
							stacking: (this.stackLines != 0 ? (this.stackLines == 2 ? "percent" : "normal") : null)
						}
					},
					series: jsonChartData,
					title: {
						text: null
					},
					tooltip: {
						borderRadius: 4,
						borderWidth: 2,
						formatter: function() {
							var yAxis = _this.getChildById(this.series.yAxis.options.id);
							var series = _this.getChildById(this.series.options.seriesId);
							var seriesName = $.trim(this.series.name);
							var yAxisValue;
							var value, categoriesName;

                            seriesName = seriesName.replace("<","&lt;");
                            seriesName = seriesName.replace("<","&gt;");
							categoriesName = FMT.stripNbsp(FMT.formatForHighcharts(categoryAxis.fmt, [this.x], categoryAxis.getFmtArgs()));
							if (_this.isAutoFormatFeatureOn() && series.isAutoFormatMigrationDone()) {
								value = [this.y];
								if (series.fmt == "pct" && yAxis.fmt != "pct") {
									// HACK!!!! This bizzare but we need to compensate for the percentage scaling when y axis
									// is not formatted as percentage where as the series is formatted as percentage.
									value = [this.y / 100]
								}
								yAxisValue = FMT.stripNbsp(FMT.formatForHighcharts(series.fmt, value, series.getFmtArgs()));
							} else {
								yAxisValue = FMT.stripNbsp(FMT.formatForHighcharts(yAxis.fmt, [this.y], yAxis.getFmtArgs()));
							}
							return (seriesName.length > 0 ? "<b>"+ seriesName +"</b><br/>" : "") + categoriesName + " : " + yAxisValue;

						},
						style: { fontFamily: fontFamily, fontSize:"13px",  cursor:"default" }

					},
					xAxis: xAxisProps,
					yAxis: jsonYAxisData
				}, function() {
					// Callback function executed after the chart finishes loading/rendering.
					// Restore the user's zoom setting if it was previously set.  See setExtremes() callbacks in the axis objects.
					var showResetButton = false;
					var propX = _this.getComponentProp(_this.getIndexPath() + ":zoom_x_min_max");
					var propXSplit, propY, propYSplit;

					if(propX) {
						propXSplit = propX.split(":");
						if(propXSplit[0] != "undefined" && propXSplit[1] != "undefined") {
							this.xAxis[0].setExtremes(propXSplit[0] == "undefined" ? null : Number(propXSplit[0]), propXSplit[1] == "undefined" ? null : Number(propXSplit[1]));
							showResetButton = true;
						}
					}

					propY = _this.getComponentProp(_this.getIndexPath() + ":zoom_y_min_max");
					if(propY) {
						propYSplit = propY.split(":");
						if(propYSplit[0] != "undefined" && propYSplit[1] != "undefined") {
							this.yAxis[0].setExtremes(propYSplit[0] == "undefined" ? null : Number(propYSplit[0]), propYSplit[1] == "undefined" ? null : Number(propYSplit[1]));
							showResetButton = true;
						}
					}
					if(showResetButton) this.showResetZoom();
				});
			}

		if(this.chart && this.chart.series.length > 0 && (jQuery.browser.msie && jQuery.browser.version >= 10) && !isWorkspace()) {
			// Highcharts sometimes doesn't render bars properly in IE10.  Hiding and then showing a series redraws the chart correctly.
			this.chart.series[0].hide();
			this.chart.series[0].show();
		}

		if(isWorkspace()) {
			this.setupClickHandling();
			this.drawSelectionBox(selectedComponent, selectedComponentIndex, chartHasRightAxis);
			this.drawDstHints();
		}

		this.setInvalid(false, true);

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }

        // re-render the xAxis by calculating the new step size. re-draw the chart only if the Auto Label is set to auto.
		if(this.chart && this.chart.xAxis && !this.invertAxes && categoryAxis.labelDrop == "0") {
            this.chart.xAxis[0].update({
                labels: _this._getXAxisLabelProperties(xAxisLabelAlign, categoryAxis, fontFamily)
            }, true)
        }
    },

	configure: function($super, config) {
		$super(config);

		if(config.stack) {
			// This setting is from a previous schema where there was only one stack setting, for bars.
			// Migrate to the new setting if the old setting is true.
			config.stackBars = 1;
			config.stack = false;
		}

		bindValues(this, config, ["stack","height","showValues","customHeight","showLegend","stackBars","stackLines","stackAreas","invertAxes","zoom", "hideNullSeries"]);

        if(config.invertAxes && isWorkspace()) {
            page.updateMenu();
        }

        if (config.height || config.customHeight) {
            this.changeHeight();
        }

        this.invalidate();
    },

	serialize : function($super) {
		var cfg = $super();

		bindValues(cfg, this,["stack","height","customHeight","showValues","showLegend","stackBars","stackLines","stackAreas","invertAxes","zoom", "hideNullSeries"],
            {
                stack:false,
                height:2,
                customHeight:"",
                showValues:false,
                showLegend: true,
                stackBars: 0,
                stackLines: 0,
                stackAreas: 0,
                invertAxes: false,
                zoom: "",
				hideNullSeries: false
            }, true);
		return cfg;
	},


	afterFactory : function() {
		var requiredChildren = [
			{role:"series", props:{displayName:"Untitled",type:"series_data"}} ,
			{role:"axis_x", props:{displayName:"Untitled",type:"chart_axis", grid:false, show_label:false, showLabelTicks: true}} ,
			{role:"axis_y", props:{displayName:"Untitled",type:"chart_axis", grid:true, show_label:false}}
		];

		var i, req, cx, x, axisIds, y, s;

		for (i = 0; i < requiredChildren.length; i++) {
			req = requiredChildren[i];
			if (!this.getChildByRole(req.role)) {
				cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
				if ( req.props._data ) cx.setData( req.props._data );
				this.addComponent(cx);

			}
		}

		x = this.getChildByRole("axis_x");
        x.setAxisType(Component.ChartAxis.TYPE_CATEGORY);
        x.isDeletable = false;

        axisIds = [];
		for(y = 0; y < this.components.length; y++) {
			if(this.components[y].role == "axis_y") {
				this.components[y].setAxisType(Component.ChartAxis.TYPE_DATA);
				if (this.isAutoFormatFeatureOn()) {
					this.components[y].disableFormatType = true;
				}
				axisIds.push(this.components[y].id);
			}
		}

		for(s = 0; s < this.components.length; s++) {
			if(this.components[s].role == "series") {
				// Check to make sure the axis id associated with this series is still valid
				if(this.components[s].axis && $.inArray(this.components[s].axis, axisIds) == -1) {
					this.components[s].axis = axisIds[0];
				}
				this.components[s].isDeletable = true;
			}
		}
	},

	getPropertyModel  : function($super) {
		var model = $super();
		var _this;

		model.push({
			type:"select",
			id:"height",
			group:"height",
			label:"Size",
			options: Props.Defaults.chartSize,
			selectedValue: this.height
		});

		model.push({
			type: "text",
			id: "customHeight",
			label: "",
			minorLabels: [
				{label: "Height:", position: "left"},
				{label: "px", position: "right", css:"quiet italics"}
			],
			width: 36,
			group: "height",
			displayWhen: {height:5},
			value: this.customHeight
		});

		model.push({
			type: "checkboxes",
			id: "showLegend",
			options: [
				{value:1,label:"Show legend", checked: this.showLegend}
			],
			label: "Legend",
			group: "legend",
			value: this.showLegend
		});

		model.push({
			type: "select",
			id: "stackBars",
			group: "stack",
			label: "Stack Bars",
			options: Props.Defaults.chartStackBars,
			selectedValue: this.stackBars
		});

		model.push({
			type: "select",
			id: "stackLines",
			group: "stack",
			label: "Stack Lines",
			options: Props.Defaults.chartStackLines,
			selectedValue: this.stackLines
		});

		model.push({
			type: "select",
			id: "stackAreas",
			group: "stack",
			label: "Stack Areas",
			options: Props.Defaults.chartStackAreas,
			selectedValue: this.stackAreas
		});

		model.push({
			type: "select",
			id: "zoom",
			group: "zoom",
			label: "Allow Zoom",
			help: {
				link: "klips/chart-zoom"
			},
			options: Props.Defaults.chartZoom,
			selectedValue: this.zoom
		});

		model.push({
			type: "checkboxes",
			id: "hideNullSeries",
			options: [
				{value:1, label: "Remove series that only have zeros, blanks or nulls", checked: this.hideNullSeries}
			],
			label: "Blank series",
			group: "series",
			value: this.hideNullSeries
		});

		model.push({
			type: "checkboxes",
			id: "invertAxes",
			options: [
				{value:1, label: "Switch position of X and Y axes", checked: this.invertAxes}
			],
			label: "Invert Axes",
			group: "axes",
			value: this.invertAxes
		});

        _this = this;
        model.push({
            type:"button",
            id:"addSeries",
            label:"Data Series",
            group:"addComponents",
            text:"Add Series",
            onClick: function(evt) {
                page.invokeAction("add_component", {parent:_this, type:"series_data", role:"series"}, {});
            }
        });

        model.push({
            type:"button",
            id:"addAxis",
            label:"Axis",
            group:"addComponents",
            text:"Add Y-axis",
            onClick: function(evt) {
                page.invokeAction("add_component", {parent:_this, type:"chart_axis", role:"axis_y"}, {});
            }
        });

		this.addDataSlotProperty(model);

		return model;
	},

    _tickPositioner: function (chartIsSparse, xAxisData, categoryAxis) {
        var _xAxisData = xAxisData && (chartIsSparse || xAxisData.length > 0) ? xAxisData : [];
        var result = [];
        var i;
        if (categoryAxis.labelDrop == 0) return; // if "auto" is selected let highcharts do the label rendering.
        for(i = 0; i < _xAxisData.length; i++){
            result.push(i);
        }
        return result;
    },

    _getSeriesThreshold : function (useAxis) {
        var threshold = (useAxis.customOrigin && useAxis.origin) ? parseFloat(useAxis.origin) : 0;
        if (useAxis.fmt == "pct") {
            threshold /= 100;
        }
        return threshold;
    },

    _getXAxisLabelProperties: function (xAxisLabelAlign, categoryAxis, fontFamily) {
		var style = {
            useHTML:true,
            fontFamily: fontFamily,
            fontSize: "12px",
            color: CXTheme.current.cx_chart_axis_labels,
            cursor: isWorkspace() ? "pointer" : "auto"
		};

		var labelProps;

		// set label width only when Label Angle is horizontal , in other case let Highcharts calculate the width.
		if(categoryAxis.labelAngle == "0" && !KF.company.hasFeature("highcharts7")){
			style["width"] = this.AXIS_LABEL_WIDTH + "px"
		}

        labelProps = {
            align: xAxisLabelAlign,
            enabled: categoryAxis.showAxisScale,
            formatter: function () {
                var label = FMT.stripNbsp(FMT.formatForHighcharts(categoryAxis.fmt, [this.value], categoryAxis.getFmtArgs()));
                return "<div align='center' style='word-wrap: break-word;word-break: break-all ;width:80px'>" + label + "</div>";
            },
            maxStaggerLines: 3,
			overflow: parseInt(categoryAxis.labelAngle) != -90 ? "justify" : null,
			step: (categoryAxis.labelDrop == 0 && !this.invertAxes && categoryAxis.labelAngle !== "auto") ? this._getLabelStep() : categoryAxis.labelDrop,
            style: style,
            x: (this.invertAxes ? -8 : (parseInt(categoryAxis.labelAngle) == 45 ? -3 : ( (parseInt(categoryAxis.labelAngle) || categoryAxis.labelAngle==="auto" ) == 0 ? 0 : 3))),
            y: this.invertAxes ? 4 : ( (categoryAxis.labelAngle != "0" && categoryAxis.labelAngle !== "auto" ) ? 10 : 18)
        };

        if (categoryAxis.labelAngle === "auto") {
            labelProps["autoRotation"] = [-10, -20, -30, -40, -50, -60, -70, -80, -90];
            labelProps["rotation"] = null;
            labelProps["staggerLines"] = null;
            labelProps["step"] = null; // setting it to 'null' will let Highcharts calculate the step size automatically.
        } else {
            labelProps["autoRotation"] = null;
            labelProps["rotation"] = parseInt(categoryAxis.labelAngle);
            labelProps["staggerLines"] = (categoryAxis.labelStagger == 0 ? null : categoryAxis.labelStagger);
        }

        return labelProps;
    },

    /**
	 * Automatically calculate the step size based on the width of the container.
	 * 1. Find the width of the xAxis
     * 2. Get the width of the label, which in our case is 80px.
     * 3. Find the no of labels that can be displayed on the xAxis.
     * 4. Find the max no of labels by iterating through all the xAxis.
     * 5. To find the final step size by dividing value from step 4 / step 3.
     *
     * @returns {number}
     * @private
     */
    _getLabelStep: function () {
        var xAxisWidth = this._getXAxisWidth();
        var labelWidth = this.AXIS_LABEL_WIDTH;
        var labelsToDisplay = Math.floor(xAxisWidth / labelWidth);
        var noOfTicks = this._getMaxNoOfTicks();
        var step = Math.floor(noOfTicks / labelsToDisplay);

        return isNaN(step) ? 0 : step;
    },

    /**
	 * Iterate through all the xAxis' and find the max no of ticks (labels)'.
     * @returns {number}
     * @private
     */
	_getMaxNoOfTicks : function () {
		var i ,maxNoOfTicks =0 ;
		if(this.chart && this.chart.xAxis){
			for(i=0 ; i<this.chart.xAxis.length ; i ++){
				if(this.chart.xAxis[i].dataMax && this.chart.xAxis[i].dataMax > maxNoOfTicks ){
					maxNoOfTicks = this.chart.xAxis[i].dataMax;
				}
			}
		}
		return maxNoOfTicks;

    },

    /**
	 * returns the width of the xAxis.
     * @private
     */
	_getXAxisWidth : function () {
		if(this.chart && this.chart.xAxis && this.chart.xAxis.length>0){
			return this.chart.xAxis[0].len;
		}
    }

});
;/****** cx.dataslot.js *******/ 

/* global Component:false, page:false, isWorkspace:false, bindValues:false, KF:false, safeText:false */
/**
 * Data is a proxy component that does not do any direct rendering.  Instead
 * it provide UI access to additional data (eg for filtering) in a consistent way -- the workspace
 * is component-centric.
 */
Component.DataSlot = $.klass(Component.Proxy, {

    factory_id:"data_slot",
    role:"data",
    isDeletable:true,

    canSort: true,
    canFilter: true,
    canSetAggregation: true,
    canGroup: true,
    name:"Untitled",
    isDstRoot: false,
    canMakeDstRoot: true,
    defaultFilterBeforeAggregation: true,


    initialize : function($super, config) {
        $super(config);
        this.labelMixin = new Component.LabelComponent();
        this.disableStyles = true;
        this.disablePrefixSuffix = true;
        this.allowedFormats = ["txt","num","cur","pct","dat","dur"];
        this.fmt = "txt";
    },




    /**
     *
     * @param $super
     */
    getEditorMenuModel : function($super) {
        var m = $super();
        var name = this.displayName;
        if (!name) name = "Untitled";

        if(KF.company.hasFeature("saas_11282")){
            name = safeText(name);
        }

        m.text = "<span parentIsAction='true' class='quiet'>Data:</span>&nbsp;" + name.replace(/ /g, "&nbsp;");
        m.tooltip =  this.getReferenceName();
        return m;
    },

    /**
     *
     * @param $super
     */
    getReferenceValueModel : function($super) {
        var m = $super();

        m.text = this.getReferenceName().replace(/ /g, "&nbsp;");

        return m;
    },

    getReferenceName : function() {
        return "Data: " + (this.displayName || "Unnamed");
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();
        model.push({
            name: "Hidden Data",
            controls: [
                [
                    {type: "add", actionId: "add_component", actionArgs: {parent: this.parent, type: "data_slot", role: "data"} },
                    {type: "remove", disabled: false, actionId: "remove_component", actionArgs: {cx: this}}
                ]
            ]
        });
        return model;
    },

    getContextMenuModel : function(menuCtx, menuConfig) {
        var model = [];

        model.push(
            { id:"menuitem-rename", text:"Rename...", actionId:"rename_component", actionArgs:{cx:this} }
        );

        this.addRemoveContextMenuItem(model);

        this.addDataSlotContextMenuItem(model, this.parent);

        this.addDstContextMenuItems(menuCtx, menuConfig, model);

        return model;
    },

    getSupportedDst: function($super) {
        var dst = $super();
        var rootComponent = this.getDstRootComponent();
        var rootAggregationInfo = rootComponent && rootComponent.getAggregateInfo();
        dst = this.updateSupportedDst(dst);


        if(this.parent && this.parent.factory_id == "label" && rootAggregationInfo && rootAggregationInfo.isAggregated){
            dst.aggregation = true;
            dst.group = false;
        }

        if (this.isDstRoot) {
            dst.aggregation = false;
        }

      return dst;
  },

    getDstPreOperations: function($super) {
        var preOperations = [];

        if (this.isVCFeatureOn() && this.parent.firstRowIsHeader) {
            preOperations.push({
                type: "frah"
            });
        }

        preOperations = preOperations.concat($super());
        return preOperations;
    },

    hasAggregatedParent: function(){
        var componentParent=this.parent;
        var aggregateInfo;
        if(componentParent && !this.isDstRoot){
             aggregateInfo = componentParent.getAggregateInfo();
        }

        return  (aggregateInfo && aggregateInfo.isAggregated);
    },

    updateDisplayName : function() {
        if (this.isRenamed) return;

        this.displayName = this.getHeader();

        if (isWorkspace()) page.updateMenu();
    },

    configure : function($super, config) {
        $super(config);

        bindValues(this, config, $.merge(["name"], this.labelMixin.boundValues));

        this.invalidate();

        _.bind(this.labelMixin.configure, this)(config);

        if (config.name && !this.isRenamed) {
            this.updateDisplayName();
        }
    },

    /**
     *
     * @param $super
     */
    serialize : function($super) {
        var config = $super();

        _.bind(this.labelMixin.cleanFormatArgs, this)();

        bindValues(config, this, $.merge(["name"], this.labelMixin.boundValues), true);

        return config;
    },


    /**
     *
     * @param $super
     */
    getPropertyModelGroups : function($super) {
        var propertyGroups = $.merge($super(), this.labelMixin.getPropertyModelGroups());
        return ["header"].concat(propertyGroups);
    },

    /**
     *
     * @param $super
     */
    getPropertyModel : function($super) {
        var model = $super();

        model = $.merge(model, _.bind(this.labelMixin.getPropertyModel, this)());
        model.push({
            type:"text",
            id:"name",
            label:"Data Title",
            group:"header",
            value:this.name
        });

        this.addDstProperties(model);
        return model;
    },


    setData: function ($super, data, msg) {
        $super(data, msg);

        if (isWorkspace()) {
            this.updateDisplayName();
        }
    }

});

;/****** cx.gauge.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, KlipFactory:false, FMT:false, Props:false, CXTheme:false, dashboard:false, page:false, isWorkspace:false, updateManager:false, bindValues:false, TWEEN:false, animateTweens:false */

Component.Gauge = $.klass(Component.Base, {

    displayName: "Gauge",
    factory_id: "gauge",
    variableSubComponents: [
        { label:"Range", type:"range", role:"range" }
    ],
    gaugeStyle: "hg",
    size: 2,

    $canvas: false,
    canvas: false,
    radial: false,
    hasReaction: false,
    inUpdate: false,
    doUpdateFromChild: true,
    ranges: false,
    uniqueRangeNum: 1,

    dimensionsInvalid: true,
    currentTween: false,

    // default measurements for parts of gauge
    d_tickLength: 5,
    d_tickWidth: 3,
    d_targetLength: 10,
    d_targetWidth: 1,
    d_targetDash: 3,
    d_targetGap: 5,
    d_stickLength: 10,
    d_stickWidth: 10,
    minimumComponentWidth: 350,

    // keep track of overlaps to deal with component sizing
    targetMaxOverlap: false,
    targetMinOverlap: false,

    padding: 5,
    targetElHeight: 0,

    initialize : function($super) {
        $super();

        // horizontal gauge defaults
        this.horizontal = {
            d_length: 240,
            d_width: 34
        };

        // vertical gauge defaults
        this.vertical = {
            d_length: 240,
            d_width: 34
        };

        // arc gauge defaults
        this.arc = {
            d_outerRadius: 120,
            d_innerRadius: 50,
            radians: -26 * (Math.PI / 180)
        };

        // semicircle gauge defaults
        this.semicircle = {
            d_outerRadius: 120,
            d_innerRadius: 0,
            radians: 0
        };

        // circle gauge defaults
        this.circle = {
            d_outerRadius: 120,
            d_innerRadius: 50,
            radians: 26 * (Math.PI / 180)
        };

        this.tweenObj = { current: 0, opacity: 0 };
        this.onAnimationFrameBinded = _.bind(this.onAnimationFrame, this);
    },

    initializeForWorkspace : function() {
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(400);
        }
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Range",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this, type:"range", role:"range"}},
                    {type:"remove", disabled:true}
                ]
            ]
        });

        return model;
    },

    addChildByType : function($super, config) {
        var newChild = $super(config);

        if (config.type == "range" && config.role == "range") {
            var id = this.uniqueRangeNum++;

            newChild = KlipFactory.instance.createComponent({
                type :"range",
                displayName :"Range " + id,
                role:"range",
                rangeId: id
            });
        }

        this.addComponent(newChild, config.index);

        return newChild;
    },

    renderDom : function($super) {
        $super();

        // create the canvas
        if (!this.$canvas) {
            this.$canvas = $("<canvas>")
                .text("your browser does not support the canvas tag")
                .css({position: "relative"});
            this.el.prepend(this.$canvas);
            this.canvas = $(this.$canvas)[0];
            if ( window.G_vmlCanvasManager !== undefined ) window.G_vmlCanvasManager.initElement(this.canvas);
        }
    },

    onResize : function($super) {
        $super();

        // since update calls setHeight (ie onResize), make sure not in
        // update call to avoid infinite loop
        if (!this.inUpdate) {
            this.invalidateAndQueue();
        }
    },

    invalidateAndQueue : function() {
        this.invalidate();
        this.dimensionsInvalid = true;
        updateManager.queue(this);
    },

    onChildrenUpdated : function() {
        // update gauge when one of the child labels is updated
        if (this.doUpdateFromChild) {
            this.dimensionsInvalid = true;
            this.update();

            var target = this.getChildByRole("target");
            // condition 'target.data.length == 0' added to resize the canvas height when 'target value' is null
            if (target && (target.el.height() != this.targetElHeight) || target.data.length == 0) {
                this.positionCanvas();
            }
        }
    },

    update : function($super) {
        var resizeFactor = 1, componentWidth, componentSizePercentage;

        $super();

        // exit early optimizations
        // ------------------------
        if ( !this.getKlip().isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

        this.inUpdate = true;

        var ctx = this.canvas.getContext("2d");

        // add dashedLine method to canvas context
        if (ctx && ctx.lineTo && !ctx.dashedLine) {
            ctx.dashedLine = this.drawDashedLine;
        }

        // retrieve the values from the label components
        var target = this.getChildByRole("target");
        var current = this.getChildByRole("current");
        var min = this.getChildByRole("min");
        var max = this.getChildByRole("max");
        var ranges = this.getChildrenByType("range");

        var target_data = target.getData(0)[0];
        var current_data = current.getData(0)[0];
        var min_data = min.getData(0)[0];
        var max_data = max.getData(0)[0];

        var target_val = FMT.convertToNumber(target_data);

        var current_val = FMT.convertToNumber(current_data);
        if (current_val == Infinity) {
            current_val = 0;
        }

        var min_setVal = FMT.convertToNumber(min_data);
        var max_setVal = FMT.convertToNumber(max_data);

        var possibleMinValues = [current_val];
        if (target_val != null) possibleMinValues.push(target_val);
        if(min_data != null) possibleMinValues.push(min_setVal);

        var min_val =  Math.min.apply(null,possibleMinValues);

        var maxUndefined = (max_data === undefined);
        var max_val = (max_data != null ? Math.max(target_val, max_setVal, current_val) : Math.max(target_val, current_val));

        if (maxUndefined && max_val == target_val) {
            if (target_val > current_val && target_val > min_val) {
                // if a maximum wasn't set and target is higher than current,
                //  try to pick a max that will be just a little higher than target
                //  so that we don't have target at max, which doesn't look good
                var minToTargetDelta = target_val - min_val;
                var desiredTargetToMaxDelta = minToTargetDelta * 0.2;
                var allowedTargetToMaxDeltaVariance = desiredTargetToMaxDelta * 0.4;
                var rawMax = target_val + desiredTargetToMaxDelta;
                max_val = parseFloat((rawMax).toPrecision(1));
                if (Math.abs(max_val - rawMax) > allowedTargetToMaxDeltaVariance) {
                    max_val = parseFloat((rawMax).toPrecision(2));
                }
            }
        }

        // determine if all the values are the same
        this.sameValues = (current_val == target_val && target_val == min_val && min_val == max_val);

        this.ranges = [];
        for (var i = 0; i < ranges.length; i++) {
            var range = ranges[i];
            var colour = CXTheme.getThemeColour(range.color);
            var start_val = FMT.convertToNumber(range.getChildByRole("start").getData(0)[0]);
            var end_val = FMT.convertToNumber(range.getChildByRole("end").getData(0)[0]);

            if (start_val == null) start_val = 0;
            if (end_val == null) end_val = 0;

            this.ranges.push({start:start_val, end:end_val, colour:colour});
        }

        if ( this.dimensionsInvalid ) {
            // determine which radial gauge defaults to use
            this.setRadial();
            // reset the gauge measurements
            componentWidth = this.el.width();
            componentSizePercentage = Component.Gauge.Percentages[this.size];
            // If less than the minimum component width to look good, then reduce the size of the elements in the component..
            if( componentWidth > 0 && componentWidth < (componentSizePercentage * this.minimumComponentWidth/ 100)) {
                resizeFactor = componentWidth / (componentSizePercentage * this.minimumComponentWidth/ 100);
            }

            this.adjustSize(componentSizePercentage * resizeFactor / 100);

            // set the canvas size depending on the gauge type
            this.setCanvasSize();
            // set the height of the component and position the canvas accordingly
            this.positionCanvas();

            // get the main point that the gauges are centered around
            this.origin = this.getOrigin();

            this.dimensionsInvalid = false;
        }

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        // bundle the values needed to draw the gauge
        var values = {
            origin_x:this.origin.x,
            origin_y:this.origin.y,
            current:current_val,
            target:target_val,
            setMin:min_setVal,
            setMax:max_setVal,
            min:min_val,
            max:max_val,
            showCurrent:current.show_value,
            showTarget:target.show_value,
            showMin:min.show_value,
            showMax:max.show_value
        };


        var reactionContext = { barColor:false };
        this.collectReactions(reactionContext);

        // assign canvas cts and values to tweenObj so that onAnimationFrame has access to them
        this.tweenObj.ctx = ctx;
        this.tweenObj.values = values;
        // determine the colours for each gauge part
        this.tweenObj.colours = this.assignColours(values, reactionContext);


        // do not animate in the workspace, or if our current value is the same as the previous tweened value
        var animationRequired = !isWorkspace() && !dashboard.config.imagingMode && (this.tweenObj.current != current_val);

        if( !animationRequired ) {
            this.tweenObj.current = current_val; // force to current value
            this.tweenObj.opacity = 1; // force opacity to 1 since we start at 0
            this.doUpdateFromChild = true;
            this.onAnimationFrame();
        } else {
            this.isTweening = true;

            var _this = this;
            this.currentTween = new TWEEN.Tween(this.tweenObj)
            .easing(TWEEN.Easing.Exponential.Out)
            .to({ current:values.current, opacity:1 }, 1000)
            .onUpdate( this.onAnimationFrameBinded )
            .onComplete(function () {
                _this.isTweening = false;
                _this.doUpdateFromChild = true;
            })
            .start();

            animateTweens();
        }

        // clear invalid flag on children
        this.inUpdate = false;
        this.setInvalid(false,false);

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    drawDashedLine : function(x, y, x2, y2, dashArray) {
        if (!dashArray) dashArray = [10, 5];
        var dashCount = dashArray.length;
        var dx = (x2 - x);
        var dy = (y2 - y);
        var xSlope = (Math.abs(dx) > Math.abs(dy));
        var slope = (xSlope) ? dy / dx : dx / dy;

        this.moveTo(x, y);
        var distRemaining = Math.sqrt(dx * dx + dy * dy);
        var dashIndex = 0;
        while (distRemaining >= 0.1) {
            var dashLength = Math.min(distRemaining, dashArray[dashIndex % dashCount]);
            var step = Math.sqrt(dashLength * dashLength / (1 + slope * slope));
            if (xSlope) {
                if (dx < 0) step = -step;
                x += step;
                y += slope * step;
            } else {
                if (dy < 0) step = -step;
                x += slope * step;
                y += step;
            }
            this[(dashIndex % 2 == 0) ? "lineTo" : "moveTo"](x, y);
            distRemaining -= dashLength;
            dashIndex++;
        }
    },

    onAnimationFrame : function() {
        var ctx = this.tweenObj.ctx;
        var values = this.tweenObj.values;
        var colours = this.tweenObj.colours;


        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

        this.tweenObj.values.current = this.tweenObj.current;

        var labelPositions;
        // draw the gauge and retrieve the approximate positions for the labels
        if (this.gaugeStyle == "hg") {
            labelPositions = this.drawBar(ctx, values, colours, this.tweenObj.opacity, true);
        } else if (this.gaugeStyle == "vg") {
            labelPositions = this.drawBar(ctx, values, colours, this.tweenObj.opacity, false);
        } else {
            labelPositions = this.drawRadial(ctx, values, colours, this.tweenObj.opacity);
        }

        // position the labels around the gauge
        this.positionLabels(labelPositions);

    },

    /**
     * Position the current, target, min, and max labels, depending on the
     * type of gauge
     *
     * @param positions
     */
    positionLabels : function(positions) {
        var current = this.getChildByRole("current");
        var target = this.getChildByRole("target");
        var min = this.getChildByRole("min");
        var max = this.getChildByRole("max");

        var cLeft, cTop, tLeft, tTop, mnLeft, mnTop, mxLeft, mxTop;
        var exMinLeft, exMinTop, exMaxLeft, exMaxTop;

        var canvasLeft = parseInt(this.$canvas.css("left"));
        var canvasTop = parseInt(this.$canvas.css("top"));

        // show/hide label according to show_value
        if (current.show_value && !this.sameValues) current.el.show(); else current.el.hide();
        if (target.show_value && !this.sameValues) target.el.show(); else target.el.hide();
        if (min.show_value && !this.sameValues) min.el.show(); else  min.el.hide();
        if (max.show_value && !this.sameValues) max.el.show(); else max.el.hide();

        if (this.gaugeStyle == "hg") {
            cLeft = canvasLeft + positions.currentPos.x - current.el.outerWidth(true) / 2;
            cTop = canvasTop - current.el.outerHeight(true) + this.padding;
            tLeft = canvasLeft + positions.targetPos.x - target.el.outerWidth(true) / 2;
            tTop = canvasTop + positions.targetPos.y;
            mnLeft = canvasLeft + positions.minPos.x - this.padding;
            mnTop = canvasTop + positions.minPos.y;
            mxLeft = canvasLeft + positions.maxPos.x - max.el.outerWidth(true) + this.padding;
            mxTop = canvasTop + positions.maxPos.y;

            exMinLeft = canvasLeft + positions.exMin.x - this.padding;
            exMaxLeft = canvasLeft + positions.exMax.x + this.padding;

            // limit label movement at extremes of gauge
            if (current.show_value) {
                if (cLeft < exMinLeft) cLeft = exMinLeft;
                if (cLeft + current.el.outerWidth(true) > exMaxLeft) cLeft = exMaxLeft - current.el.outerWidth(true);
            }

            if (target.show_value) {
                if (tLeft < exMinLeft) tLeft = exMinLeft;
                if (tLeft + target.el.outerWidth(true) > exMaxLeft) tLeft = exMaxLeft - target.el.outerWidth(true);
            }

            if (max.show_value) {
                if (mxLeft < exMinLeft) tLeft = exMinLeft;
                if (mxLeft + max.el.outerWidth(true) > exMaxLeft) mxLeft = exMaxLeft - max.el.outerWidth(true);
            }

        } else if (this.gaugeStyle == "vg") {
            cLeft = canvasLeft + positions.currentPos.x + this.padding;
            cTop = canvasTop + positions.currentPos.y - current.el.outerHeight(true) / 2;
            tLeft = canvasLeft - target.el.outerWidth(true) + this.padding;
            tTop = canvasTop + positions.targetPos.y - target.el.outerHeight(true) / 2;
            mnLeft = canvasLeft - min.el.outerWidth(true) + this.padding;
            mnTop = canvasTop + positions.minPos.y - min.el.outerHeight(true) / 2;
            mxLeft = canvasLeft - max.el.outerWidth(true) + this.padding;
            mxTop = canvasTop + positions.maxPos.y - max.el.outerHeight(true) / 2;

            exMinTop = canvasTop + positions.exMin.y + this.padding * 2;
            exMaxTop = canvasTop + positions.exMax.y - this.padding * 2;

            // limit label movement at extremes of gauge
            if (current.show_value) {
                if (cTop < exMaxTop) cTop = exMaxTop;
                if (cTop + current.el.outerHeight(true) > exMinTop) cTop = exMinTop - current.el.outerHeight(true);
            }

            if (target.show_value) {
                if (tTop < exMaxTop) tTop = exMaxTop;
                if (tTop + target.el.outerHeight(true) > exMinTop) tTop = exMinTop - target.el.outerHeight(true);
            }

            if (max.show_value) {
                if (mxTop < exMaxTop) mxTop = exMaxTop;
                if (mxTop + max.el.outerWidth(true) > exMinTop) mxTop = exMinTop - max.el.outerWidth(true);
            }

            if (min.show_value) {
                if (mnTop + min.el.outerHeight(true) > exMinTop) mnTop = exMinTop - min.el.outerHeight(true);
            }
        } else {
            cLeft = canvasLeft + positions.currentPos.x - current.el.outerWidth(true) / 2;
            cTop = canvasTop + this.$canvas.outerHeight(true);
            tTop = canvasTop + positions.targetPos.y - target.el.outerHeight(true) / 2 + this.padding;

            var deg = Math.floor(positions.targetPos.degree);
            if (deg > 90) {
                tLeft = canvasLeft + positions.targetPos.x - 15;
            } else if (deg == 90) {
                tLeft = canvasLeft + positions.targetPos.x - target.el.outerWidth(true) / 2;
            } else { // deg < 90
                tLeft = canvasLeft + positions.targetPos.x - target.el.outerWidth(true) + 15;
            }

            mnLeft = canvasLeft + positions.minPos.x - min.el.outerWidth(true) - (this.size > 2 ? 2 : 1) * this.padding;
            mnTop = canvasTop + positions.minPos.y - min.el.outerHeight(true) / 2;
            mxLeft = canvasLeft + positions.maxPos.x + this.padding;
            mxTop = canvasTop + positions.maxPos.y - max.el.outerHeight(true) / 2;
        }

        // hide the min label if the target one overlaps it
        if (target.show_value && min.show_value) {
            if (this.labelOverlap(tLeft, tTop, tLeft + target.el.outerWidth(true), tTop + target.el.outerHeight(true),
                mnLeft, mnTop, mnLeft + min.el.outerWidth(), mnTop + min.el.outerHeight(true))) {
                min.el.hide();
                this.targetMinOverlap = true;
            } else {
                this.targetMinOverlap = false;
            }
        }

        // hide the max label if the target one overlaps it
        if (target.show_value && max.show_value) {
            if (this.labelOverlap(tLeft, tTop, tLeft + target.el.outerWidth(true), tTop + target.el.outerHeight(true),
                mxLeft, mxTop, mxLeft + max.el.outerWidth(true), mxTop + max.el.outerHeight(true))) {
                max.el.hide();
                this.targetMaxOverlap = true;
            } else {
                this.targetMaxOverlap = false;
            }
        }

        current.el.css({left: cLeft + "px", top: cTop + "px"});
        target.el.css({left: tLeft + "px", top: tTop + "px"});
        min.el.css({left: mnLeft + "px", top: mnTop + "px"});
        max.el.css({left: mxLeft + "px", top: mxTop + "px"});
    },

    /**
     * Determine if the given rectangles overlap
     *
     * @param a_x1 - first rect upper left x-coordinate
     * @param a_y1 - first rect upper left y-coordinate
     * @param a_x2 - first rect lower right x-coordinate
     * @param a_y2 - first rect lower right y-coordinate
     * @param b_x1 - second rect upper left x-coordinate
     * @param b_y1 - second rect upper left y-coordinate
     * @param b_x2 - second rect lower right x-coordinate
     * @param b_y2 - second rect lower right y-coordinate
     * @return {Boolean}
     */
    labelOverlap : function(a_x1, a_y1, a_x2, a_y2, b_x1, b_y1, b_x2, b_y2) {
        return (a_x1 < b_x2 && a_x2 > b_x1 && a_y1 < b_y2 && a_y2 > b_y1);
    },

    /**
     * Draw a bar gauge (horizontal or vertical)
     *
     * @param ctx - canvas context
     * @param v - values
     * @param c - colours
     * @param opacity
     * @param isHoriz - true if drawing horizontal gauge, false otherwise
     * @return {Object} - approximate label positions
     */
    drawBar : function(ctx, v, c, opacity, isHoriz) {
        // min tick
        if (v.setMin != null && v.showMin) {
            ctx.globalAlpha = 1;
            ctx.strokeStyle = c.minTick;
            ctx.lineWidth = this.tickWidth;

            var setMinPos = this.valueToPosition(isHoriz ? this.horizontal.length : this.vertical.length, v.setMin, v.min, v.max);

            ctx.beginPath();
            if (isHoriz) {
                ctx.moveTo(v.origin_x + setMinPos, v.origin_y);
                    ctx.lineTo(v.origin_x + setMinPos, v.origin_y + this.horizontal.width + this.tickLength);
            } else {
                ctx.moveTo(v.origin_x + this.vertical.width, v.origin_y + this.vertical.length - setMinPos);
                    ctx.lineTo(v.origin_x - this.tickLength, v.origin_y + this.vertical.length - setMinPos);
            }
            ctx.stroke();
        }

        // max tick
        var setMaxPos;
        if(v.setMax == null) {
            var nullMaxVal = Math.max(v.target, v.current, v.setMin);
            setMaxPos  = this.valueToPosition(isHoriz ? this.horizontal.length : this.vertical.length, nullMaxVal , v.min, nullMaxVal);
        } else if (v.showMax) {
            ctx.globalAlpha = 1;
            ctx.strokeStyle = c.maxTick;
            ctx.lineWidth = this.tickWidth;

            setMaxPos = this.valueToPosition(isHoriz ? this.horizontal.length : this.vertical.length, v.setMax, v.min, v.max);

            ctx.beginPath();
            if (isHoriz) {
                ctx.moveTo(v.origin_x + setMaxPos, v.origin_y);
                    ctx.lineTo(v.origin_x + setMaxPos, v.origin_y + this.horizontal.width + this.tickLength);
            } else {
                ctx.moveTo(v.origin_x + this.vertical.width, v.origin_y + this.vertical.length - setMaxPos);
                    ctx.lineTo(v.origin_x - this.tickLength, v.origin_y + this.vertical.length - setMaxPos);
            }
            ctx.stroke();
        }

        // well
        ctx.globalAlpha = 1;
        ctx.fillStyle = c.well;
        ctx.lineWidth = 1;

        if (isHoriz) {
            ctx.fillRect(v.origin_x, v.origin_y, this.horizontal.length, this.horizontal.width);
        } else {
            ctx.fillRect(v.origin_x, v.origin_y, this.vertical.width, this.vertical.length);
        }

        // ranges
        var _this = this;
        _.each(this.ranges, function(range) {
            if (range.start < range.end) {
                _this.drawBarRange(ctx, v, isHoriz, range.start, range.end, 1, range.colour);
            }
        });

        // bar
        if (v.current != null && (this.ranges.length == 0 || (this.ranges.length > 0 && this.hasReaction))) {
            this.drawBarRange(ctx, v, isHoriz, 0, v.current, opacity, c.bar);
        }

        // target
        if (v.target != null && v.showTarget) {
            ctx.globalAlpha = 1;
            ctx.strokeStyle = c.target;
            ctx.lineWidth = this.targetWidth;

            var targetPos = this.valueToPosition(isHoriz ? this.horizontal.length : this.vertical.length, v.target, v.min, v.max);

            ctx.beginPath();
            if (isHoriz) {
                ctx.dashedLine(
                    v.origin_x + targetPos,
                    v.origin_y,
                    v.origin_x + targetPos,
                    v.origin_y + this.horizontal.width + this.targetLength,
                    [this.targetDash, this.targetGap]
                );
            } else {
                ctx.dashedLine(
                    v.origin_x + this.vertical.width,
                    v.origin_y + this.vertical.length - targetPos,
                    v.origin_x - this.targetLength,
                    v.origin_y + this.vertical.length - targetPos,
                    [this.targetDash, this.targetGap]
                );
            }
            ctx.stroke();
        }

        // stick
        if (v.current != null) {
            ctx.globalAlpha = opacity;
            ctx.strokeStyle = c.stick;
            ctx.lineWidth = this.stickWidth;

            var currentPos = this.valueToPosition(isHoriz ? this.horizontal.length : this.vertical.length, Math.min(v.current, v.max), v.min, v.max);

            ctx.beginPath();
            if (isHoriz) {
                ctx.moveTo(v.origin_x + currentPos, v.origin_y + this.horizontal.width);
                ctx.lineTo(v.origin_x + currentPos, v.origin_y - this.stickLength);
            } else {
                ctx.moveTo(v.origin_x, v.origin_y + this.vertical.length - currentPos);
                ctx.lineTo(v.origin_x + this.vertical.width + this.stickLength, v.origin_y + this.vertical.length - currentPos);
            }
            ctx.stroke();
        }

        return {
            exMin: {x: isHoriz ? v.origin_x : v.origin_x - this.tickLength,
                y: isHoriz ? v.origin_y + this.horizontal.width + this.tickLength : v.origin_y + this.vertical.length},
            exMax: {x: isHoriz ? v.origin_x + this.horizontal.length : v.origin_x - this.tickLength,
                y: isHoriz ?  v.origin_y + this.horizontal.width + this.tickLength : v.origin_y},
            minPos: {x: isHoriz ? v.origin_x + setMinPos : v.origin_x - this.tickLength,
                y: isHoriz ? v.origin_y + this.horizontal.width + this.tickLength : v.origin_y + this.vertical.length - setMinPos},
            maxPos: {x: isHoriz ? v.origin_x + setMaxPos : v.origin_x - this.tickLength,
                y: isHoriz ?  v.origin_y + this.horizontal.width + this.tickLength : v.origin_y + this.vertical.length - setMaxPos},
            targetPos: {x: isHoriz ? v.origin_x + targetPos : v.origin_x - this.targetLength,
                y: isHoriz ? v.origin_y + this.horizontal.width + this.targetLength : v.origin_y + this.vertical.length - targetPos},
            currentPos: {x: isHoriz ? v.origin_x + currentPos : v.origin_x + this.vertical.width + this.stickLength,
                y: isHoriz ? v.origin_y - this.stickLength : v.origin_y + this.vertical.length - currentPos}
        };
    },

    /**
     * Draw a radial gauge
     *
     * @param ctx - canvas context
     * @param v - values
     * @param c - colours
     * @param opacity
     * @return {*} - approximate labels positions
     */
    drawRadial : function(ctx, v, c, opacity) {
        if (!this.radial) return null;

        // min tick
        if (v.setMin != null && v.showMin) {
            ctx.globalAlpha = 1;
            ctx.strokeStyle = c.minTick;
            ctx.lineWidth = this.tickWidth;

            var setMinRad = this.valueToRadians(this.radial.radians, v.setMin, v.min, v.max);

            ctx.beginPath();
            ctx.moveTo(v.origin_x + this.radial.innerRadius * Math.cos(Math.PI + this.radial.radians - setMinRad),
                v.origin_y - this.radial.innerRadius * Math.sin(Math.PI + this.radial.radians - setMinRad));
                ctx.lineTo(v.origin_x + (this.radial.outerRadius + this.tickLength) * Math.cos(Math.PI + this.radial.radians - setMinRad),
                    v.origin_y - (this.radial.outerRadius + this.tickLength) * Math.sin(Math.PI + this.radial.radians - setMinRad));
            ctx.stroke();
        }

        // max tick
        var setMaxRad;
        if(v.setMax == null) {
            var nullMaxVal = Math.max(v.target, v.current, v.setMin);
            setMaxRad = this.valueToRadians(this.radial.radians,nullMaxVal,v.min,nullMaxVal);
        } else if (v.showMax) {
            ctx.globalAlpha = 1;
            ctx.strokeStyle = c.maxTick;
            ctx.lineWidth = this.tickWidth;

            setMaxRad = this.valueToRadians(this.radial.radians, v.setMax, v.min, v.max);
            ctx.beginPath();
            ctx.moveTo(v.origin_x + this.radial.innerRadius * Math.cos(Math.PI + this.radial.radians - setMaxRad),
                v.origin_y - this.radial.innerRadius * Math.sin(Math.PI + this.radial.radians - setMaxRad));
                ctx.lineTo(v.origin_x + (this.radial.outerRadius + this.tickLength) * Math.cos(Math.PI + this.radial.radians - setMaxRad),
                    v.origin_y - (this.radial.outerRadius + this.tickLength) * Math.sin(Math.PI + this.radial.radians - setMaxRad));
            ctx.stroke();
        }

        // well
        this.drawRadialRange(ctx, {origin_x: v.origin_x, origin_y: v.origin_y, min:0, max:1}, 0, 1, 1, c.well);

        // ranges
        var _this = this;
        _.each(this.ranges, function(range) {
            if (range.start < range.end) {
                _this.drawRadialRange(ctx, v, range.start, range.end, 1, range.colour);
            }
        });

        // bar
        if (v.current != null && (this.ranges.length == 0 || (this.ranges.length > 0 && this.hasReaction))) {
            if (v.current < 0) {
                this.drawRadialRange(ctx, v, v.current, 0, opacity, c.bar);
            } else {
                this.drawRadialRange(ctx, v, 0, v.current, opacity, c.bar);
            }
        }

        // target
        if (v.target != null && v.showTarget) {
            ctx.globalAlpha = 1;
            ctx.strokeStyle = c.target;
            ctx.lineWidth = this.targetWidth;

            var targetRad = this.valueToRadians(this.radial.radians, v.target, v.min, v.max);

            ctx.beginPath();
            ctx.dashedLine(
                v.origin_x + this.radial.innerRadius * Math.cos(Math.PI + this.radial.radians - targetRad),
                v.origin_y - this.radial.innerRadius * Math.sin(Math.PI + this.radial.radians - targetRad),
                v.origin_x + (this.radial.outerRadius + this.targetLength) * Math.cos(Math.PI + this.radial.radians - targetRad),
                v.origin_y - (this.radial.outerRadius + this.targetLength) * Math.sin(Math.PI + this.radial.radians - targetRad),
                [this.targetDash, this.targetGap]
            );
            ctx.stroke();
        }

        // stick
        ctx.globalAlpha = 1;
        ctx.fillStyle = c.stick;
        ctx.strokeStyle = c.stick;
        ctx.lineWidth = 1;

        ctx.beginPath();
        ctx.arc(v.origin_x, v.origin_y, this.stickWidth, 0, Math.PI * 2);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();

        if (v.current != null) {
            ctx.globalAlpha = opacity;
            ctx.lineWidth = this.stickWidth;

            var currentRad = this.valueToRadians(this.radial.radians, Math.min(v.current, v.max), v.min, v.max);

            ctx.beginPath();
            ctx.moveTo(v.origin_x, v.origin_y);
            ctx.lineTo(v.origin_x + (this.radial.outerRadius + this.stickLength) * Math.cos(Math.PI + this.radial.radians - currentRad),
                v.origin_y - (this.radial.outerRadius + this.stickLength) * Math.sin(Math.PI + this.radial.radians - currentRad));
            ctx.stroke();
        }

        return {
            exMin: {x: v.origin_x + (this.radial.outerRadius + this.tickLength) * Math.cos(Math.PI + this.radial.radians),
                y: v.origin_y - (this.radial.outerRadius + this.tickLength) * Math.sin(Math.PI + this.radial.radians)},
            exMax: {x: v.origin_x + (this.radial.outerRadius + this.tickLength) * Math.cos(-this.radial.radians),
                y: v.origin_y - (this.radial.outerRadius + this.tickLength) * Math.sin(-this.radial.radians)},
            minPos: {x: v.origin_x + (this.radial.outerRadius + this.tickLength) * Math.cos(Math.PI + this.radial.radians - setMinRad),
                y: v.origin_y - (this.radial.outerRadius + this.tickLength) * Math.sin(Math.PI + this.radial.radians - setMinRad)},
            maxPos: {x: v.origin_x + (this.radial.outerRadius + this.tickLength) * Math.cos(Math.PI + this.radial.radians - setMaxRad),
                y: v.origin_y - (this.radial.outerRadius + this.tickLength) * Math.sin(Math.PI + this.radial.radians - setMaxRad)},
            targetPos: {x: v.origin_x + (this.radial.outerRadius + this.targetLength + 18) * Math.cos(Math.PI + this.radial.radians - targetRad),
                y: v.origin_y - (this.radial.outerRadius + this.targetLength + 18) * Math.sin(Math.PI + this.radial.radians - targetRad),
                degree: (targetRad - this.radial.radians) * 180 / Math.PI},
            currentPos: {x: v.origin_x,
                y: v.origin_y}
        };
    },

    /**
     * Draw range for bar gauge
     *
     * @param ctx
     * @param v
     * @param isHoriz
     * @param start
     * @param end
     * @param opacity
     * @param colour
     */
    drawBarRange : function(ctx, v, isHoriz, start, end, opacity, colour) {
        ctx.globalAlpha = opacity;
        ctx.fillStyle = colour;
        ctx.lineWidth = 0;

        var startPos = this.valueToPosition(isHoriz ? this.horizontal.length : this.vertical.length, Math.max(start, v.min), v.min, v.max);
        var endPos = this.valueToPosition(isHoriz ? this.horizontal.length : this.vertical.length, Math.min(end, v.max), v.min, v.max);

        if (isHoriz) {
            ctx.fillRect(v.origin_x + startPos, v.origin_y, endPos - startPos, this.horizontal.width);
        } else {
            ctx.fillRect(v.origin_x, v.origin_y + this.vertical.length - endPos, this.vertical.width, endPos - startPos);
        }
    },

    /**
     * Draw range for radial gauge
     *
     * @param ctx
     * @param v
     * @param start
     * @param end
     * @param opacity
     * @param colour
     */
    drawRadialRange : function(ctx, v, start, end, opacity, colour) {
        ctx.globalAlpha = opacity;
        ctx.fillStyle = colour;
        ctx.strokeStyle = colour;
        ctx.lineWidth = 1;

        var startRad = this.valueToRadians(this.radial.radians, Math.max(start, v.min), v.min, v.max);
        var endRad = this.valueToRadians(this.radial.radians, Math.min(end, v.max), v.min, v.max);

        ctx.beginPath();
        ctx.arc(v.origin_x, v.origin_y, this.radial.outerRadius, Math.PI - this.radial.radians + startRad, Math.PI - this.radial.radians + endRad);
        ctx.arc(v.origin_x, v.origin_y, this.radial.innerRadius, Math.PI - this.radial.radians + endRad, Math.PI - this.radial.radians + startRad, true);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
    },

    /**
     * Determine the position of the val along the given length
     *
     * @param length
     * @param val
     * @param min
     * @param max
     * @return {Number}
     */
    valueToPosition : function(length, val, min, max) {
        return length * ((val - min) / (max - min));
    },

    /**
     * Determine the radians for the val given the start radians
     *
     * @param sRad - start radians
     * @param val
     * @param min
     * @param max
     * @return {Number}
     */
    valueToRadians : function(sRad, val, min, max) {
        return (Math.PI + 2 * sRad) * ((val - min) / (max - min));
    },

    /**
     * Assign colours to each gauge part
     *
     * @param v - values
     * @param rCtx - aggregated indicator reactions
     * @return {Object}
     */
    assignColours : function(v, rCtx) {
        var minTick, maxTick, well, bar, target, stick;

        bar = rCtx.barColor ? rCtx.barColor : CXTheme.current.cx_gauge_bar;
        this.hasReaction = rCtx.barColor ? true : false;

        // ticks start as the same colour as the well
        minTick = CXTheme.current.cx_gauge_well;
        maxTick = CXTheme.current.cx_gauge_well;

        var i = 0;
        var minColoured = false;
        var maxColoured = false;
        // if necessary, they change to the range colours
        while (i < this.ranges.length && (!minColoured || !maxColoured)) {
            var range = this.ranges[i];

            if (!minColoured && this.inRange(v.setMin, range.start, range.end)) {
                minTick = range.colour;
                minColoured = true;
            }
            if (!maxColoured && this.inRange(v.setMax, range.start, range.end)) {
                maxTick = range.colour;
                maxColoured = true;
            }

            i++;
        }

        // finally, if there are no ranges or ranges with a reaction, they change to the bar colour
        if (this.inRange(v.setMin, Math.min(0, v.current), Math.max(0, v.current))
            && (this.ranges.length == 0 || (this.ranges.length > 0 && this.hasReaction))) {
            minTick = bar;
        }

        if (this.inRange(v.setMax, Math.min(0, v.current), Math.max(0, v.current))
            && (this.ranges.length == 0 || (this.ranges.length > 0 && this.hasReaction))) {
            maxTick = bar;
        }

        well = CXTheme.current.cx_gauge_well;
        target = CXTheme.current.cx_gauge_twig;
        stick = CXTheme.current.cx_gauge_stick;

        return {minTick:minTick, maxTick:maxTick, well:well, bar:bar, target:target, stick:stick};
    },

    /**
     * Determine if the given val is in [start, end]
     *
     * @param val
     * @param start
     * @param end
     * @return {Boolean}
     */
    inRange : function(val, start, end) {
        if (start >= end) return false;

        return (start <= val && val <= end);
    },

    /**
     * Determine which radial gauge defaults to use
     */
    setRadial : function() {
        switch (this.gaugeStyle) {
            case "ag": this.radial = this.arc; break;
            case "sg": this.radial = this.semicircle; break;
            case "cg": this.radial = this.circle; break;
        }
    },

    /**
     * Adjust the size of the gauge measurements
     *
     * @param pct - percentage
     * @return {Boolean}
     */
    adjustSize : function(pct) {
        this.tickLength = this.d_tickLength * pct;
        this.tickWidth = this.d_tickWidth * pct;
        this.targetLength = this.d_targetLength * pct;
        this.targetWidth = this.d_targetWidth;
        this.targetDash = this.d_targetDash;
        this.targetGap = this.d_targetGap;
        this.stickLength = this.d_stickLength * pct;
        this.stickWidth = this.d_stickWidth * pct;

        if (this.gaugeStyle == "hg") {
            this.horizontal.length = this.horizontal.d_length  * pct;
            this.horizontal.width = this.horizontal.d_width * pct;
        } else if (this.gaugeStyle == "vg") {
            this.vertical.length = this.vertical.d_length * pct;
            this.vertical.width = this.vertical.d_width * pct;
        } else {
            if (!this.radial) return false;

            this.radial.outerRadius = this.radial.d_outerRadius * pct;
            this.radial.innerRadius = this.radial.d_innerRadius * pct;
        }

        return true;
    },

    /**
     * Set the width and height of the canvas
     * - doing this also clears the canvas
     *
     * @return {Boolean}
     */
    setCanvasSize : function() {
        if (this.gaugeStyle == "hg") {
            this.canvas.width = this.el.width();
            this.canvas.height = this.horizontal.width + this.targetLength + this.stickLength + this.padding;

            // special case to make horizontal gauges scale to the width of the Klip
            this.horizontal.length = this.canvas.width - 2 * this.stickWidth;
        } else if (this.gaugeStyle == "vg") {
            this.canvas.width = this.vertical.width + this.targetLength + this.stickLength + 2 * this.padding;
            this.canvas.height = this.vertical.length + 2 * this.stickWidth + 2 * this.padding;
        } else {
            if (!this.radial) return false;

            this.canvas.width = 2 * this.radial.outerRadius + 2 * this.stickLength + 2 * this.padding;
            this.canvas.height = (this.radial.radians > 0 ? this.radial.outerRadius - this.radial.outerRadius * Math.sin(Math.PI + this.radial.radians) : this.radial.outerRadius)
                                + this.stickLength + this.stickWidth + 2 * this.padding;
        }

        return true;
    },

    /**
     * Sets the height of the component and positions the canvas depending on the type of gauge
     */
    positionCanvas : function() {
        if (!this.$canvas) return;

        var current = this.getChildByRole("current");
        var target = this.getChildByRole("target");
        var min = this.getChildByRole("min");
        var max = this.getChildByRole("max");

        var current_data = current.getData(0)[0];
        var target_data = target.getData(0)[0];
        var min_data = min.getData(0)[0];
        var max_data = max.getData(0)[0];

        // We should only account for height of labels if they are allowed to be shown AND have a value to display
        var showCurrent = (current_data != null && current.show_value);
        var showTarget = (target_data != null && target.show_value);
        var showMin = (min_data != null && min.show_value);
        var showMax = (max_data != null && max.show_value);

        var canvasLeft = Math.floor((this.el.width() - this.canvas.width) / 2);
        var canvasTop = 0;

        if (this.gaugeStyle == "hg") {
            var largestBottomElementHeight = 0;
            var height = this.canvas.height;

            if(showCurrent) {
                height += current.el.outerHeight(true) - this.padding;
            }

            // Target, min and max are all on the bottom of the gauge. add height for the largest of the 3.
            // Must take into account elements hidden due to overlaps
            if(showTarget) {
                if(target.el.outerHeight(true) > largestBottomElementHeight) {
                    largestBottomElementHeight = target.el.outerHeight(true);
                }
            }
            if(showMin && !this.targetMinOverlap) {
                if(min.el.outerHeight(true) > largestBottomElementHeight) {
                    largestBottomElementHeight = min.el.outerHeight(true);
                }
            }
            if(showMax && !this.targetMaxOverlap) {
                if(max.el.outerHeight(true) > largestBottomElementHeight) {
                    largestBottomElementHeight = max.el.outerHeight(true);
                }
            }

            height += largestBottomElementHeight;
            this.setHeight(height);

            canvasTop = (current_data != null && current.show_value ? current.el.outerHeight(true) - this.padding : 0);
        } else if (this.gaugeStyle == "vg") {
            this.setHeight(this.canvas.height);

            canvasLeft -= (current_data != null && current.show_value ? current.el.outerWidth(true) / 4 : 0);
            canvasTop = 0;
        } else {
            this.setHeight(this.canvas.height
                + (current_data != null && current.show_value ? current.el.outerHeight(true) : 0)
                + (target_data != null && target.show_value ? target.el.outerHeight(true) - this.padding : 0));

            canvasTop = (target_data != null && target.show_value ? target.el.outerHeight(true) - this.padding : 0);
        }

        this.targetElHeight = target.el.outerHeight(true);

        this.$canvas.css("left", canvasLeft + "px");
        this.$canvas.css("top", canvasTop + "px");
    },

    /**
     * Determine the origin for each gauge type
     * - top left corner for horizontal/vertical gauges
     * - center of circle for radial gauges
     *
     * @return {*}
     */
    getOrigin : function() {
        var x, y;

        if (this.gaugeStyle == "hg") {
            x = this.stickWidth;
            y = this.padding + this.stickLength;
        } else if (this.gaugeStyle == "vg") {
            x = this.padding + this.targetLength;
            y = this.padding + this.stickWidth;
        } else {
            if (!this.radial) return null;

            x = this.canvas.width / 2;
            y = this.radial.outerRadius + this.stickLength + this.padding;
        }

        return {x : x, y : y};
    },


    configure : function($super, config) {
        $super(config);
        bindValues(this,config,["gaugeStyle"]);

        this.invalidate();
        this.dimensionsInvalid = true;

        if (config["~propertyChanged"] && config.size) {
            var current = this.getChildByRole("current");
            var target = this.getChildByRole("target");
            var min = this.getChildByRole("min");
            var max = this.getChildByRole("max");

            if (current && current.size != Component.Gauge.FontSizes[0][config.size]) {
                current.size = Component.Gauge.FontSizes[0][config.size];
                this.doUpdateFromChild = false;
                current.update();
            }

            if (target && target.size != Component.Gauge.FontSizes[1][config.size]) {
                target.size = Component.Gauge.FontSizes[1][config.size];
                this.doUpdateFromChild = false;
                target.update();
            }

            if (min && min.size != Component.Gauge.FontSizes[2][config.size]) {
                min.size = Component.Gauge.FontSizes[2][config.size];
                this.doUpdateFromChild = false;
                min.update();
            }

            if (max && max.size != Component.Gauge.FontSizes[3][config.size]) {
                max.size = Component.Gauge.FontSizes[3][config.size];
                this.doUpdateFromChild = false;
                max.update();
            }
        }
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this, ["gaugeStyle"]);
        return cfg;
    },

    getPropertyModel : function($super) {
        var model = $super();
        // remove "Custom" from the chartSize
        var gaugeSizes = Props.Defaults.chartSize.slice(0, Props.Defaults.chartSize.length - 1);
        model.push({
            type:"select" ,
            id:"gaugeStyle",
            label:"Gauge Style" ,
            group:"gauge",
            options: Props.Defaults.gaugeStyle,
            selectedValue: this.gaugeStyle
        });

        model.push({
            type:"select" ,
            id:"size",
            label:"Size" ,
            group:"gauge",
            options: gaugeSizes,
            selectedValue: this.size
        });

        var _this = this;
        model.push({
            type:"button",
            id:"add_ranges",
            label:"Range",
            help: {
                link: "klips/gauge-range"
            },
            group:"ranges",
            text:"Add Color Range",
            onClick: function() {
                page.invokeAction("add_component", {parent:_this, type:"range", role:"range"}, {});
            }
        });
        model.push({
            type:"markup",
            id:"msg_range",
            group:"ranges",
            el:$("<span>Ranges are accessible from the component tree.</span>")
        });

        return model;
    },

    getSupportedReactions : function() {
        return { color: true };
    },

    /**
     *
     * @param idx
     * @param reaction
     * @param ctx - {
     *      barColor: false
     * }
     */
    handleReaction : function( idx, reaction, ctx ) {
        switch (reaction.type) {
            case "color":
                ctx.barColor = CXTheme.getThemeColour(reaction.value);
                break;
        }
    },


    onEvent : function($super, evt) {
        $super(evt);
        if(evt.id == "klip-hide") {
            // if we are animating,  stop the tween and mark the klip as invalid so that
            // it will be updated the next time it is made visible
            if ( this.isTweening ){
                this.invalidate();
                this.currentTween.stop();
            }
        } else if(evt.id == "theme_changed") {
            this.invalidate();
        }
    },


    afterFactory : function() {
        var i;
        var req, cx;
        var current, target, min, max, ranges;

        var requiredChildren = [
            {role: "current", props: {displayName: "Current Value", type: "label", autoFmt: true,
                                formulas: [{"txt":"50","src":{"t":"expr","v":false,"c":[{"t":"l","v":"50"}]}}],
                                data: [[50]], fmt:"num", size: Component.Gauge.FontSizes[0][this.size]}},
            {role: "target", props: {displayName: "Target Value", type: "label", autoFmt: true,
                                formulas: [{"txt":"80","src":{"t":"expr","v":false,"c":[{"t":"l","v":"80"}]}}],
                                data: [[80]], fmt:"num", size: Component.Gauge.FontSizes[1][this.size],
                                font_colour: "cx-color_888"}},
            {role: "min", props: {displayName: "Min Value", type: "label", autoFmt: true,
                                formulas: [{"txt":"0","src":{"t":"expr","v":false,"c":[{"t":"l","v":"0"}]}}],
                                data: [[0]], fmt:"num", size: Component.Gauge.FontSizes[2][this.size],
                                font_style: "", font_colour: "cx-color_888"}},
            {role: "max", props: {displayName: "Max Value", type: "label",
                                fmt:"num", size: Component.Gauge.FontSizes[3][this.size],
                                font_style: "", font_colour: "cx-color_888"}}
        ];

        this.setRadial();
        this.adjustSize(Component.Gauge.Percentages[this.size] / 100);

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);

            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                if ( req.props._data ) cx.setData( req.props._data );
                this.addComponent(cx);

                page.invokeAction("aggregate_component", { cx:cx, isAutomatic:true });
            }

            cx.isDeletable = false;
            cx.isDraggable = false;
        }

        current = this.getChildByRole("current");
        current.allowedFormats = ["txt","num","cur","pct","dat","dur"];
        if (current.fmt == "txt") current.fmt = "num";
        current.el.css("position", "absolute").appendTo(this.el);
        current.elLabel.css("padding", 1);

        target = this.getChildByRole("target");
        target.allowedFormats = ["txt","num","cur","pct","dat","dur"];
        if (target.fmt == "txt") target.fmt = "num";
        target.el.css("position", "absolute").appendTo(this.el);
        target.elLabel.css("padding", 1);

        min = this.getChildByRole("min");
        min.allowedFormats = ["txt","num","cur","pct","dat","dur"];
        if (min.fmt == "txt") min.fmt = "num";
        min.el.css("position", "absolute").appendTo(this.el);
        min.elLabel.css("padding", 1);

        max = this.getChildByRole("max");
        max.allowedFormats = ["txt","num","cur","pct","dat","dur"];
        if (max.fmt == "txt") max.fmt = "num";
        max.el.css("position", "absolute").appendTo(this.el);
        max.elLabel.css("padding", 1);

        ranges = this.getChildrenByType("range");
        for (i = 0; i < ranges.length; i++) {
            if (ranges[i].rangeId >= this.uniqueRangeNum) {
                this.uniqueRangeNum = ranges[i].rangeId + 1;
            }
        }
    }

});

/**
 * gauge percentages at different sizes (small -> x-large)
 */
Component.Gauge.Percentages = {
    1: 40,
    2: 80,
    3: 120,
    4: 160
};

/**
 * label font size at different sizes (xx-small -> xx-large)
 */
Component.Gauge.FontSizes = [
    {
        1: "medium",
        2: "x-large", // current
        3: "xx-large",
        4: "xx-large"
    },
    {
        1: "xx-small",
        2: "medium", // target
        3: "medium",
        4: "x-large"
    },
    {
        1: "xx-small",
        2: "xx-small", // min
        3: "medium",
        4: "medium"
    },
    {
        1: "xx-small",
        2: "xx-small", //max
        3: "medium",
        4: "medium"
    }
];;/****** cx.gauge_bar.js *******/ 

/* eslint-disable vars-on-top */
/* globals CXTheme:false, Component:false, FMT:false, isWorkspace:false, bindValues:false, updateManager: false,
    KlipFactory:false, Props:false, $g:false */

/**
 *  component which provides a horizontal bar gauge for a range and a value
 */

Component.GaugeBar = $.klass(Component.Base, {

    factory_id : "gauge_bar",
    displayName: "Bar Gauge",
    gaugeType: "s",
    canvas: false,

    paddingVert: 0,
    paddingHoriz: 10,
    textmargin: 4,
    $canvasdiv: false,

    defaultArgs : {
        width:"60px",
        height:"18px",
        lineWidth:2,
        lineColor:"darkblue",
        spotColor:"darkblue",
        fillColor:false,
        spotRadius:2.5,
        minSpotColor:false,
        maxSpotColor:false
    },

    args : {},


    red : CXTheme.current.cx_red,
    orange : CXTheme.current.cx_orange,
    green : CXTheme.current.cx_green,
    dark : CXTheme.current.cx_gauge_bar,

    well : {
        color: CXTheme.current.cx_gauge_well,
        yoffset: 10,
        height: 28,
        draw: function (ctx, canvas) {
            ctx.fillStyle = CXTheme.current.cx_gauge_well;
            ctx.fillRect(
                0,
                this.yoffset,
                canvas.width,
                this.height
            );
        }
    },

    bar : {
        color: CXTheme.current.cx_gauge_bar,
        yoffset: 10,
        height: 28,
        draw: function (ctx, canvas) {
            ctx.fillStyle = this.color;
            if (this.current < 0) {
                ctx.fillRect(
                    canvas.width * (1 - (this.vrange - (this.current-this.vmin))/this.vrange),
                    this.yoffset,
                    canvas.width * (1 - (this.vrange + this.vmin)/this.vrange) - canvas.width * (1 - (this.vrange - (this.current-this.vmin))/this.vrange),
                    this.height
                );
            } else {
                ctx.fillRect(
                    canvas.width * (1 - (this.vrange + this.vmin)/this.vrange),
                    this.yoffset,
                    canvas.width * this.current/this.vrange,
                    this.height
                );
            }
        }
    },

    stick : {
        color: CXTheme.current.cx_gauge_stick,
        width: 6,
        height: 38,
        draw: function (ctx, canvas) {
            ctx.fillStyle = CXTheme.current.cx_gauge_stick;
            ctx.fillRect(
                canvas.width * (1 - (this.vrange - (this.current-this.vmin))/this.vrange) - this.width / 2,
                0,
                this.width,
                this.height
            );
        }
    },

    twig : {
        color: CXTheme.current.cx_gauge_twig,
        width: 0.5,
        yoffset: 10,
        height: 36,
        draw: function (ctx, canvas) {
            ctx.fillStyle = CXTheme.current.cx_gauge_twig;
            ctx.fillRect(
                canvas.width * (1 - (this.vrange - (this.target-this.vmin))/this.vrange) - this.width / 2,
                this.yoffset,
                this.width,
                this.height
            );
        }
    },

    zero : {
        color: CXTheme.current.cx_gauge_zero,
        width: 0,
        yoffset: 38,
        height: 2,
        draw: function (ctx, canvas) {
            ctx.fillStyle = CXTheme.current.cx_gauge_zero;
            ctx.fillRect(
                canvas.width * (this.vrange + this.vmin)/this.vrange - 2,
                this.yoffset,
                this.width,
                this.height
            );
        }
    },

    initialize : function($super) {
        $super();
        this.args = this.defaultArgs;
		this.bar.color =  CXTheme.current.cx_gauge_bar;
    },

    renderDom : function ($super) {
        var dom = $super();
        this.$canvasdiv = $("<div>").appendTo (dom);
    },

	onResize : function($super) {
		$super();

        updateManager.queue(this);
	},

	update : function($super) {
        $super();

        if(!this.canvas) {
            this.canvas = $g().place(this.$canvasdiv);
            this.canvas.cx = this;
            this.canvas.add("well",this.well);
            this.canvas.add("bar",this.bar);
            this.canvas.add("stick",this.stick);
            this.canvas.add("twig",this.twig);
            this.canvas.add("zero",this.zero);
        }

        var target = this.getChildByRole("target");
        var current = this.getChildByRole("current");
        var min = this.getChildByRole("min");
        var max = this.getChildByRole("max");
        var target_data = target.getData(0)[0];
        var current_data = current.getData(0)[0];
        var min_data = min.getData(0)[0];
        var max_data = max.getData(0)[0];
        var target_val = FMT.convertToNumber(target_data);
        var current_val = FMT.convertToNumber(current_data);
        var min_val = (min_data != null?Math.min(target_val,FMT.convertToNumber(min_data),current_val):Math.min(target_val, current_val));
        var max_val = (max_data != null?Math.max(target_val,FMT.convertToNumber(max_data),current_val):Math.max(target_val, current_val));
        var range = Math.abs(max_val-min_val);

        if (this.gaugeType == "s") { // simple
            this.bar.color = CXTheme.current.cx_gauge_bar;
        } else if (this.gaugeType == "ou") { // over / under
            if (target_val < current_val) {
                this.bar.color = CXTheme.current.cx_green;
            } else if (target_val > current_val) {
                this.bar.color = CXTheme.current.cx_red;
            } else {
                this.bar.color = CXTheme.current.cx_gauge_bar;
            }
        } else if (this.gaugeType == "oo") { // over only
            if (target_val < current_val) {
                this.bar.color = CXTheme.current.cx_green;
            } else {
                this.bar.color = CXTheme.current.cx_gauge_bar;
            }
        } else if (this.gaugeType == "uo") { // under only
            if (target_val > current_val) {
                this.bar.color = CXTheme.current.cx_red;
            } else {
                this.bar.color = CXTheme.current.cx_gauge_bar;
            }
        }

        var current_pct = 1 - (range - (current_val-min_val))/range;
        var target_pct = 1 - (range - (target_val-min_val))/range;
        this.canvas.each (function (obj, idx) {
            obj.target = target_val;
            obj.current = current_val;
            obj.vmin = min_val;
            obj.vmax = max_val;
            obj.vrange = range;
        });


		// quickly hide the canvas to get the natural width of this component
		this.$canvasdiv.hide();
        var w = this.el.width ();
		this.$canvasdiv.show();


		var ch = this.well.yoffset + this.well.height + target.el.height();
		var cw = w - 2 * this.paddingHoriz;

		if (ch < 0 || cw < 0 ) return;

		this.canvas.size(cw, ch);
        this.$canvasdiv.css({"padding-left":this.paddingHoriz,"padding-top":this.paddingVert + current.el.height() + this.textmargin,"padding-bottom":this.paddingVert + this.textmargin,"padding-right":this.paddingHoriz});

        var current_left = ((w - 2 * this.paddingHoriz) * current_pct + this.paddingHoriz - current.el.width () / 2);
        if (current_left < 0) {
            current_left = 0;
        } else if ((current_left + current.el.width()) > w) {
            current_left = w - current.el.width();
        }

        current.el.css({
            left: current_left + "px",
            top: this.paddingVert,
            paddingVert: 0,
            paddingHoriz: 0
        });

        var target_left = ((w - 2 * this.paddingHoriz) * target_pct + this.paddingHoriz - target.el.width () / 2);
        if (target_left < 0) {
            target_left = 0;
        } else if ((target_left + target.el.width()) > w) {
            target_left = w - target.el.width();
        }

        target.el.css({
            left: target_left + "px",
            top: this.paddingVert + this.well.yoffset + this.well.height + current.el.height() + this.textmargin * 2,
            paddingVert: 0,
            paddingHoriz: 0,
            color: "#777"
        });

        this.canvas.draw();

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    afterFactory :  function() {
        if (! this.components || this.components.length == 0) {
            this.addComponent(KlipFactory.instance.createComponent({
                type:"label",
                role:"current",
                size:3,
                formulas:[{"txt":"50","src":{"t":"expr","v":false,"c":[{"t":"l","v":"50"}]}}],
				data:[[50]],
                updateParent:true,
                displayName:"Current Value"
            }));
            this.addComponent(KlipFactory.instance.createComponent({
                type:"label",
                role: "target",
                size:2,
				data:[[80]],
                formulas:[{"txt":"80","src":{"t":"expr","v":false,"c":[{"t":"l","v":"80"}]}}],
                updateParent:true,
                displayName:"Target Value"
            }));
            this.addComponent(KlipFactory.instance.createComponent({
                type:"proxy",
                role:"min",
                size:1,
                formulas:[{"txt":"0","src":{"t":"expr","v":false,"c":[{"t":"l","v":"0"}]}}],
                data:[[0]],
                updateParent:true,
                displayName:"Min Value"
            }));
            this.addComponent(KlipFactory.instance.createComponent({
                type:"proxy",
                role:"max",
                size:1,
                formulas:[{"txt":"100","src":{"t":"expr","v":false,"c":[{"t":"l","v":"100"}]}}],
                data:[[100]],
                updateParent:true,
                displayName:"Max Value"
            }));
        }
        var current = this.getChildByRole("current");
        current.allowedFormats = ["num","cur","pct"];
        current.el.css({display:"inline-block",position:"absolute",background:"transparent","white-space":"nowrap"});
        current.el.appendTo( this.el );

        var target = this.getChildByRole("target");
        target.allowedFormats = ["num","cur","pct"];
        target.el.css({display:"inline-block",position:"absolute",background:"transparent","white-space":"nowrap"});
        target.el.appendTo( this.el );

        var min = this.getChildByRole("min");
		if (!min) { // TODO: find a better way to migrate component schemas
			this.addComponent(KlipFactory.instance.createComponent({
				type:"proxy",
				role:"min",
				size:1,
				formulas:[{"txt":"0","src":{"t":"expr","v":false,"c":[{"t":"l","v":"0"}]}}],
				data:[[0]],
				updateParent:true,
				displayName:"Min Value"
			}));
			min = this.getChildByRole("min");
		}
        min.allowedFormats = ["num","cur","pct"];

        var max = this.getChildByRole("max");
        max.allowedFormats = ["num","cur","pct"];
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg,this,["gaugeType"]);
        return cfg;
    },
    configure : function($super, config, update) {
        $super(config);
        bindValues(this,config,["gaugeType"]);
    },
	getPropertyModel : function($super) {
		var model = $super();
        model.push({
            type:"select" ,
            id:"gaugeType",
            label:"Gauge Type" ,
            group:"gauge",
            options: Props.Defaults.gaugeType,
            selectedValue: this.gaugeType
        });

		return model;

	}
});;/****** cx.html_data.js *******/ 

/* global Component:false, isWorkspace:false, safeText:false, bindValues:false, page:false, name:true */

Component.HtmlData = $.klass(Component.Proxy, {

    displayName: "Data",
    factory_id: "html_data",
    dataId: "Series1",

    // DST Capabilities
    canSort: true,
    canFilter: true,
    canGroup: true,
    canSetAggregation: true,
    canMakeDstRoot: true,

    initialize : function($super, config) {
        $super(config);

        this.disableStyles = true;

        if ( isWorkspace() ) {
            this.mixin = new Component.LabelComponent();
            this.allowedFormats = [ "txt","num","cur","pct","dat","raw"];
        }
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Data",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"html_data", role:"data", sibling:this}},
                    {type:"remove", actionId:"remove_component", actionArgs:{cx:this}}
                ]
            ]
        });

        return model;
    },

    getEditorMenuModel : function($super) {
        var m = $super();

        var id = this.displayName;
        if (!id) id = "Unnamed";

        if(KF.company.hasFeature("saas_11282")){
            name = safeText(name);
        }

        m.text = "<span parentIsAction='true' class='quiet'>Data:</span>&nbsp;" + id.replace(/ /g, "&nbsp;");
        m.tooltip =  this.getReferenceName();

        return m;
    },

    getReferenceValueModel : function($super) {
        var m = $super();

        m.text = this.getReferenceName().replace(/ /g, "&nbsp;");

        return m;
    },

    getSupportedDst: function($super) {
        var dst = $super();

        dst=this.updateSupportedDst(dst);

        if (this.isDstRoot) {
            //There is nothing to aggregate by
            dst.aggregation = false;
        }

        return dst;
    },

    getReferenceName : function() {
        return "Data: " + (this.displayName || "Unnamed");
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg,this,["dataId"]);
        return cfg;
    },

    configure : function($super, config, update) {
        $super(config);
        bindValues(this,config,["dataId"]);

        if (config.dataId && !this.isRenamed) {
            this.displayName = config.dataId;
            if (config["~propertyChanged"] && isWorkspace()) page.updateMenu();
        }
    },

    getPropertyModel : function($super) {
        var model = $super();

        model.unshift ({
            type:"text",
            id:"dataId",
            label: "Data ID",
            group: "label",
            help: {
                link: "klips/data-id"
            },
            value: this.dataId
        });

        return model;
    }

});
;/****** cx.html_tpl.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, isWorkspace:false, bindValues: false */

Component.HtmlTpl = $.klass(Component.DataComponent, {

	displayName: "HTML Template",
	factory_id:"html_tpl",
	markerRE  : /{(\d+)}/g,


	renderDom : function($super) {
		$super();
	},


	update : function($super) {
		$super();

		this.el.empty();
		if ( this.noPad ) this.el.css("padding","0" );
		else this.el.css("padding","8px");

		this.el.html( this.executeTemplate() );

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

	executeTemplate : function() {
		if ( !this.tpl ) return "";

		var match;
		var data = this.getData(0);
		var html = this.tpl+"";

		while( match = this.markerRE.exec(html) ) { // eslint-disable-line no-cond-assign
			var idx = match[1];
			var dataPoint = data[ parseInt(idx) ];

			if (!dataPoint) dataPoint = "";
			html = html.replace( match[0], dataPoint);
		}

		return html;
	},

	serialize : function($super) {
		var cfg = $super();
		bindValues(cfg,this,["tpl","noPad"]);
		return cfg;
	},

	getPropertyModel : function($super) {
		var model = $super();
		model.push( { type:"textarea" , id:"tpl", label:"HTML Template" , useMono:true, group:"label", height:"200px", width:"100%", value:this.tpl} );
		model.push({
			type:"checkboxes",
			label:"Remove Padding",
			id:"noPad",
			options:[
				{
					value:true,
					label:"",
					checked:this.noPad
				}
			]
		});

		return model;
	},

	configure : function($super, config, update) {
		$super(config);
		bindValues(this,config,["tpl","noPad"]);
	}
});;/****** cx.html_tpl2.js *******/ 

/* global Component:false, updateManager:false, isWorkspace:false, newRelicNoticeError:false,
    FMT:false, safeText:false, dashboard:false, bindValues:false, Props:false, page:false,
    KlipFactory:false, logEvent: false */
Component.HtmlTpl2 = $.klass(Component.Base, {

    displayName: "HTML Template",
    factory_id:"html_tpl2",
    variableSubComponents: [
        { label:"Data", type:"html_data", role:"data" }
    ],

    height: "auto",
    customHeightVal: "",
    overflow: "as",
    dataModelType: "obj",
    uniqueDataNum: 1,

    tpl: "",
    script_display: "",
    script_tpl: "",

    renderDom : function($super) {
        var dom = $super();

        this.$template = $("<div>").css({width:"100%"}).appendTo(dom);

        this.changeHeight();
        this.changeOverflow();
    },

    onResize : function($super) {
        $super();

        this.invalidateAndQueue();
    },

    update : function($super) {
        var tmp;

        $super();

        if ( !this.dependenciesLoaded ) {
            updateManager.queue(this);
            return;
        }

        // exit early optimizations
        // ------------------------
        if ( !this.getKlip().isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

        this.dataModel = this.groupData();
        this.changeOverflow();

        if (!this.tpl) {
            this.el.css("min-height", "15px");
        } else {
            this.el.css("min-height", "");
        }

        try {
            tmp = $.template(null, this.tpl);
            $.tmpl(tmp, this.dataModel).appendTo(this.$template.empty());
        } catch (err) {
            // user is probably not finished inputting template
        }

        this.executeInlineScript();

        // clear invalid flag on children
        this.setInvalid(false,true);

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    executeInlineScript : function() {
        var klip;

        if (this.script_display) {
            try {
                //log when klip makes google maps api request
                if ((this.script_display).search("https://maps.google.com/maps/api/js")>=0){
                    logEvent("klip", "google_maps_api_request", "Google Maps api script included in HTML Component");
                }
                eval(this.script_display);
            } catch (err) {
                console.warn("Customer JS error in HTML Component: Error evaluating: " + this.script_tpl); // eslint-disable-line no-console
                console.warn("Customer JS error in HTML Component: " + err); // eslint-disable-line no-console

                klip = this.getKlip();

                newRelicNoticeError("Customer JS error in HTML Component: [klipId: " + klip.master + ", userId: " + KF.user.get("publicId") + ", companyId: " + KF.company.get("publicId") + "]");

                klip.addMessage({ type:"bad-script", id:this.id, asset:this }, "error");
            }
        }
    },

    groupData : function() {
        var i;
        var data = this.getChildrenByType("html_data");
        var tplModel = {};
        var dataArr;
        var fmtDataArr;
        var maxLength;
        var currLength;
        var dataObj;
        var j;
        var fmtData;

        if (data.length > 0) {
            if (this.dataModelType == "obj") {
                dataArr = [];
                fmtDataArr = [];

                maxLength = data[0].getData(0).length;
                fmtDataArr.push(this.formatData(data[0]));

                for (i = 1; i < data.length; i++) {
                    currLength = data[i].getData(0).length;
                    if (currLength > maxLength) maxLength = currLength;

                    fmtDataArr.push(this.formatData(data[i]));
                }

                for (i = 0; i < maxLength; i++) {
                    dataObj = {};
                    for (j = 0; j < data.length; j++) {
                        dataObj[data[j].dataId] = fmtDataArr[j][i];
                    }
                    dataArr.push(dataObj);
                }

                tplModel["data"] = dataArr;
            } else {
                for (i = 0; i < data.length; i++) {
                    fmtData = this.formatData(data[i]);

                    tplModel[data[i].dataId] = fmtData;
                }
            }
        }

        return tplModel;
    },

    formatData : function(cx) {
        var data = cx.getData(0);
        var fmtData = [];
        var tempData;
        var m;
        var formatted;

        try {
            tempData = [];
            for (m = 0; m < data.length; m++) {
//	            console.log(cx.fmt)
                tempData[0] = data[m];
                if (cx.fmt) {
                    formatted = FMT.format(cx.fmt, tempData, cx.getFmtArgs());

                    if (cx.fmt == "dat") {
                        formatted = formatted.replace(/<\/?span[^>]*>/g, "");
                    }

                    fmtData.push(formatted);
                } else {
                    fmtData.push(safeText("" + tempData));
//                    fmtData.push(tempData);
                }
            }
        } catch(e) {
            console.log(e); // eslint-disable-line no-console
        }

        return fmtData;
    },

    changeHeight : function() {
        var newHeight;

        if (this.$template) {
            if (this.height == "auto") {
                this.$template.css("height", "auto");
            } else if (this.height == "cust") {
                this.$template.height(this.customHeightVal);
                this.setHeight(this.customHeightVal);
            } else {
                newHeight = Component.HtmlTpl.heights[this.height-1];

                this.$template.height(newHeight);
                this.setHeight(newHeight);
            }
        }
    },

    changeOverflow : function() {
        if (this.$template) {
            switch (this.overflow) {
                case "ns": this.$template.css("overflow", "hidden"); break;
                case "as": this.$template.css("overflow", "auto"); break;
                case "hvs": this.$template.css("overflow", "scroll"); break;
            }
        }
    },

    getDependencies : function() {
        return [dashboard.getRootPath() + "js/jquery.tmpl.min.js"];
    },

    serialize : function($super) {
        var cfg = $super();

        this.script_tpl = encodeURI(this.script_display);

        bindValues(cfg, this, ["tpl","script_tpl", "height", "customHeightVal", "overflow", "dataModelType"],
            {
                script_tpl:"",
                height:"auto",
                customHeightVal:"",
                overflow:"as",
                dataModelType:"obj"
            }, true);

        return cfg;
    },

    getPropertyModel : function($super) {
        var model = $super();
        var hideScript;
        var createLink;
        var _this = this;

        model.push({
            type:"textarea",
            id:"tpl",
            label:"HTML Template",
            help: {
                link: "klips/klip-components/html-template-klip",
                position: "label"
            },
            useMono:true,
            twoCol:true,
            group:"code",
            height:"200px",
            width:"100%",
            value:this.tpl,
            validator: function(value){
                if(value && value.indexOf("<script") !=-1 ){
                    require(["js_root/utilities/utils.dialog"], function(dialogUtils) {
                        dialogUtils.alert("Use the Script text area (below the HTML Template text area) to enter JavaScript for this component.");
                    });
                    value = value.substring(0,value.indexOf("<script"));
                    this.setValue(value, true);
                }
                return true;
            }
        });

        hideScript = (this.script_tpl == undefined || this.script_tpl == "");
        createLink = $("<a>").text("Use JavaScript with this HTML Template")
            .attr("href","#");

        model.push({
            type:"markup",
            id:"script_link",
            label:"Script",
            twoCol:true,
            group:"label",
            onClick:function(evt,widget){
                widget.hidden = true;
                widget.update();
                //widgets[2] should be script_tpl
                widget.form.widgets[2].hidden = false;
                widget.form.widgets[2].update();
            },
            el:createLink,
            hidden:!hideScript,
            value:this.script_link
        });
        //displayed script field, is not saved
        model.push({
            type:"textarea",
            id:"script_display",
            label:"Script",
            help: {
                link: "klips/klip-components/html-template-klip",
                position: "label"
            },
            useMono:true,
            twoCol:true,
            ignoreUpdates:true,
            group:"label",
            height:"100px",
            width:"100%",
            hidden:hideScript,
            value:this.script_display
        });
        //hidden script prop to serialize encoded script
        model.push({
            type:"textarea",
            id:"script_tpl",
            label:"Script",
            useMono:true,
            twoCol:true,
            ignoreUpdates:true,
            group:"label",
            hidden:true,
            value:this.script_tpl
        });

        model.push({
            type:"select",
            id:"height",
            label:"Height",
            group:"label",
            options: [{value:"auto", label:"Auto"}].concat(Props.Defaults.size, [{value:"cust", label:"Custom..."}]),
            selectedValue: this.height
        });
        model.push({
            type:"text" ,
            id:"customHeightVal",
            label:"",
            group:"label",
            minorLabels: [
                {label: "px", position: "right", css:"quiet italics"}
            ],
            width:38,
            displayWhen: { height:"cust" },
            value: this.customHeightVal
        });

        model.push({
            type:"select",
            id:"overflow",
            label:"Overflow",
            group:"label",
            options: Props.Defaults.overflow,
            selectedValue: this.overflow,
            help: {
                link: "klips/html-overflow"
            }
        });

        model.push({
            type: "select",
            id:"dataModelType",
            label:"Data Model" ,
            group:"data",
            options:[
                { value:"obj", label:"Use object model" },
                { value:"arr", label:"Use array model" }
            ],
            selectedValue: this.dataModelType,
            help: {
                link: "klips/html-model"
            }
        });

        model.push({
            type:"button",
            id:"addData",
            label:"Data Series",
            group:"addComponents",
            text:"Add Data",
            onClick: function() {
                page.invokeAction("add_component", {parent:_this, type:"html_data", role:"data"}, {});
            }
        });

        return model;
    },

    configure : function($super, config) {
        $super(config);

        bindValues(this,config,["tpl", "script_tpl","script_display", "height", "customHeightVal", "overflow", "dataModelType"]);

        if (config.script_tpl) this.script_display = decodeURI(this.script_tpl);
        this.invalidate();

        if (config.height || config.customHeightVal) {
            this.changeHeight();
        }

        // ensures user knows if changing data models was a bad idea
        if (config.dataModelType && this.$template && this.tpl) {
//            this.dataModel = this.groupData();
            this.$template.empty().html("<span style='font-weight:bold;'>The data is no longer referenced correctly.<span>");
        }
    },

    initializeForWorkspace : function() {
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(500);
        }
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Data",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this, type:"html_data", role:"data", isDstRoot:true}},
                    {type:"remove", disabled:true}
                ]
            ]
        });

        return model;
    },

    addChildByType : function($super, config) {
        var newChild = $super(config);
        var id;

        if (config.type == "html_data" && config.role == "data") {
            id = this.uniqueDataNum++;

            newChild = KlipFactory.instance.createComponent({
                type :"html_data",
                displayName :"Series" + id,
                role:"data",
                dataId: "Series" + id,
                isDstRoot: true
            });
        }

        this.addComponent(newChild, config.index);

        return newChild;
    },

    afterFactory : function() {
        var i;
        var requiredChildren = [
            {role:"data", props:{displayName:"Series" + this.uniqueDataNum, type:"html_data", isDstRoot: true, dataId:"Series" + this.uniqueDataNum}}
        ];
        var req;
        var cx;
        var data;
        var dataId;
        var dataNum;

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);
            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                if ( req.props._data ) cx.setData( req.props._data );
                this.addComponent(cx);
            }
        }

        data = this.getChildrenByType("html_data");
        for (i = 0; i < data.length; i++) {
            dataId = data[i].dataId;
            if (dataId.indexOf("Series") != -1) {
                dataNum = parseInt(dataId.replace("Series", ""));

                if (!_.isNaN(dataNum) && dataNum >= this.uniqueDataNum) {
                    this.uniqueDataNum = dataNum + 1;
                }
            }
        }

        if ( isWorkspace() ) {
            page.updateMenu();
        }
    }
});

/**
 * HTML template heights at different sizes (small -> x-large)
 */
Component.HtmlTpl.heights = [40, 160, 320, 640];
;/****** cx.iframe.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, isWorkspace:false, dashboard:false, Props:false, bindValues:false */

Component.Iframe = $.klass(Component.DataComponent, {

    displayName: "Inline Frame",
    factory_id:"iframe",

    $div: false,
    $iframe: false,
    $error: false,

    height: 2,
    customHeightVal: "",
    overflow: "as",
    refreshRate: 0,

    renderDom : function($super) {
        var dom = $super();
        this.$div = $("<div>").css("position", "relative");
        this.$iframe = $("<iframe frameborder='0'>").appendTo(this.$div);
        this.$error = ($("<div class='iframe-error-message'>Page not found. Check your URL and try again.</div>")).hide();

        if (isWorkspace()) {
            this.$eventTrap = $("<div>").css({position:"absolute", top:0, left:0, width:"100%", height:"100%", display:"none"}).appendTo(this.$div);
        }

        dom.append(this.$div).append(this.error);

        this.changeHeight();
        this.changeOverflow();
    },

    onResize : function($super) {
        $super();

        this.invalidateAndQueue();
    },

    initializeForWorkspace : function(workspace) {
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(500);
        }

        var data = this.getData(0);
        var url = data[0];
        var formula = this.getFormula(0);

        if (!url && !formula) {
            this.setFormula(0, "\"" + dashboard.getRootPath() + "images/components/iframe/index.html\"", {"t": "expr", "v": false, "c": [{"t": "l",	"v": dashboard.getRootPath() + "images/components/iframe/index.html"}]});
            this.setData([dashboard.getRootPath() + "images/components/iframe/index.html"]);
        }

        var _this = this;
        PubSub.subscribe("dragging-element", function(evtName, args) {
            _this.toggleEventTrap(args);
        });
    },

    toggleEventTrap : function(show) {
        if (!this.$eventTrap) return;

        if (show) {
            this.$eventTrap.show();
        } else {
            this.$eventTrap.hide();
        }
    },

    update : function($super) {
        $super();

        // exit early optimizations
        // ------------------------
        if ( !this.getKlip().isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        this.$iframe.width(this.$div.width());
        this.$iframe.height(this.$div.height());

        var data = this.getData(0);
        var url = data[0];

        if (url && ((url.indexOf("http://") != 0) && (url.indexOf("https://") != 0) && (url != dashboard.getRootPath() + "images/components/iframe/index.html"))) {
            this.$iframe.attr("src", "");
            this.$iframe.hide();
            this.$error.show();
        } else {
            this.$error.hide();
            this.$iframe.show();
        }

        var currentUrl = this.$iframe.attr("src");

        if ( url && url != currentUrl ) {
            this.$iframe.attr("src", url);
        }

        this.setInvalid(false,true);

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    changeHeight : function() {
        if (!this.$div) return;

        if (this.height == "cust") {
            this.$div.height(this.customHeightVal);
            this.setHeight(this.customHeightVal);
        } else {
            var newHeight = Component.Iframe.heights[this.height-1];

            this.$div.height(newHeight);
            this.setHeight(newHeight);
        }
    },

    changeOverflow : function() {
        if (!this.$iframe) return;

        switch (this.overflow) {
            case "ns":
                this.$iframe.attr("scrolling", "no");
                break;

            case "as":
            default:
                this.$iframe.attr("scrolling", "auto");
                break;
        }

        // Chrome does not update the visibility of the scrollbars unless the src attribute is updated
        var data = this.getData(0);
        var url = data[0];
        if ( url )	this.$iframe.attr("src",url);
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg,this,["height", "customHeightVal", "overflow", "refreshRate"],
            {
                height:2,
                customHeightVal:"",
                overflow:"as",
                refreshRate:0
            }, true);
        return cfg;
    },

    getPropertyModel : function($super) {
        var model = $super();

        model.push({
            type:"select",
            id:"height",
            label:"Height",
            group:"label",
            options: Props.Defaults.chartSize.slice(0, Props.Defaults.chartSize.length - 1).concat([{value:"cust",label:"Custom..."}]),
            selectedValue: this.height
        });
        model.push({
            type:"text" ,
            id:"customHeightVal",
            label:"",
            group:"label",
            minorLabels: [
                {label: "px", position: "right", css:"quiet italics"}
            ],
            displayWhen: { height:"cust" },
            width: 38,
            value: this.customHeightVal
        });

        model.push({
            type:"select",
            id:"overflow",
            label:"Overflow",
            group:"label",
            options: Props.Defaults.overflow,
            selectedValue: this.overflow,
            help: {
                link: "klips/html-overflow"
            }
        });

        model.push({
            type:"select",
            id:"refreshRate",
            label:"Refresh",
            group:"label",
            options: Props.Defaults.refreshRate,
            selectedValue: this.refreshRate
        });
        return model;
    },

    configure : function($super, config, update) {
        $super(config);
        bindValues(this,config,["height", "customHeightVal", "overflow", "refreshRate"]);

        this.invalidate();

        if (config.height || config.customHeightVal) {
            this.changeHeight();
        }

        if (config.overflow) {
            this.changeOverflow();
        }

        if (config.refreshRate > 0 ){
           clearInterval(this.intervalId);
           this.intervalId = setInterval(_.bind(this.refreshIframe,this) , this.refreshRate);
        }

    },

    refreshIframe : function () {
        this.$iframe.unload();
        var src = this.$iframe.attr("src");
        this.$iframe.attr("src",src);
    },

    destroy : function ($super){
        $super();
        clearInterval(this.intervalId);
    }

});

/**
 * iframe heights at different sizes (small -> x-large)
 */
Component.Iframe.heights = [40, 160, 320, 640];;/****** cx.image.js *******/ 

/* global Component:false, dashboard:false, Props:false, isWorkspace:false, bindValues:false */
Component.Image = $.klass(Component.DataComponent, {

	displayName: "Image",
	factory_id:"image",

    $div: false,
    $image: false,

    height: "auto",
    customHeightVal: "",
    scale: "contain",
    resizeWidth: "",
    resizeHeight: "",
    horizAlign:0,
    vertAlign:1,
    refreshRate : 0,
    enableClick: true,
    IMAGING_TIMEOUT: 10000,


	renderDom : function($super) {
		var dom = $super();
        this.$div = $("<div style='overflow:hidden; position:relative;'>").addClass("image_container");

        this.$image = $("<img style='display:block; position:absolute;'>").appendTo(this.$div);

        dom.append(this.$div);

        this.changeHeight();
    },

	initializeForWorkspace : function() {
        var data, url, formula;
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(500);
        }

        data = this.getData(0);
        url = data[0];
        formula = this.getFormula(0);

        if (!url && !formula) {
            this.setFormula(0, "\"" + dashboard.getRootPath() + "images/components/image-placeholder.png\"", {"t": "expr", "v": false, "c": [{"t": "l",	"v": dashboard.getRootPath() + "images/components/image-placeholder.png"}]});
            this.setData([dashboard.getRootPath() + "images/components/image-placeholder.png"]);
        }
    },

	onResize : function($super) {
		$super();

        this.invalidateAndQueue();
	},

	update : function($super) {
        var data, url, klipId, img, _this;
        var componentId = this.id;
        var busyState = false;
        var klip = this.getKlip();
        var hasCustomStyleFeature = this.hasCustomStyleFeature();
        $super();

        // exit early optimizations
        // ------------------------
        if ( !klip.isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

        data = this.getData(0);
		url = data[0];

        if(url) {
			this.$image.attr("src", url);

            img = new Image();
			img.src = url;

            _this = this;

            klipId = klip.id;
            if(dashboard.config && dashboard.config.imagingMode && klipId) {
                PubSub.publish("component_rendering", {status:true, klipId: klipId, componentId: componentId});
                busyState = true;
                setTimeout(function() {
                    PubSub.publish("component_rendering", {status:false, klipId: klipId, componentId: componentId});
                },this.IMAGING_TIMEOUT);
            }

            img.onload = function() {
                //wait for new image to load so we can get proper sizing

                _this.imgWidth = this.width;
                _this.imgHeight = this.height;

                _this.setImageDimensions();

                // Height is ready, can update parent height
                // - always update the image's parent to overcome
                //   timing issues of images in grids
                // -----------------------------------------------
                _this.parent.handleComponentUpdated();
                _this.fromPanelUpdate = false;
                // -----------------------------------------------

                if(dashboard.config && dashboard.config.imagingMode  && busyState && klipId) {
                    PubSub.publish("component_rendering", {status:false, klipId: klipId, componentId: componentId});
                    busyState = false;
                }
            };
		} else {
            if (this.imgWidth && this.imgHeight) {
                this.$image.attr("src", dashboard.getRootPath() + "images/components/image-missing.png");
            } else {
                this.$image.attr("src", dashboard.getRootPath() + "images/components/image-broken.png");
                this.$image.attr("height", 13);
                this.$image.attr("width", 13);

                // Height is ready can update panel cell heights
                // -----------------------------------------------
                if (this.fromPanelUpdate) {
                    this.parent.handleComponentUpdated();
                    this.fromPanelUpdate = false;
                }
                // -----------------------------------------------

                if (!isWorkspace() && this.parent.isKlip) {
                    this.parent.handleComponentUpdated();
                }
            }
        }

        if(this.customCSSClass && hasCustomStyleFeature){
            this.$image.addClass(this.customCSSClass);
        }

        this.setInvalid(false, true);
    },

    setImageDimensions : function() {
        var rWidth, rHeight;
        var frameWidth = this.el.width();
        var frameHeight = this.el.height();
        var frameRatio = frameWidth / frameHeight;

        var imgRatio = this.imgWidth / this.imgHeight;

        var constrained = true;
        var scaledWidth, scaledHeight;

        if (this.height == "auto" && (this.scale == "contain" || this.scale == "cover")) {
            scaledWidth = frameWidth;
            scaledHeight = frameWidth / imgRatio;
        } else if ((this.scale == "contain" && imgRatio < frameRatio) || (this.scale == "cover" && imgRatio >= frameRatio)) {
            scaledWidth = frameHeight * imgRatio;
            scaledHeight = frameHeight;
        } else if ((this.scale == "contain" && imgRatio >= frameRatio) || (this.scale == "cover" && imgRatio < frameRatio)) {
            scaledWidth = frameWidth;
            scaledHeight = frameWidth / imgRatio;
        } else if (this.scale == "resize") {
            rWidth = parseFloat(this.resizeWidth);
            rHeight = parseFloat(this.resizeHeight);

            if (!_.isNaN(rWidth) && _.isNaN(rHeight)) {
                scaledWidth = rWidth;
                scaledHeight = rWidth / imgRatio;
            } else if (_.isNaN(rWidth) && !_.isNaN(rHeight)) {
                scaledWidth = rHeight * imgRatio;
                scaledHeight = rHeight;
            } else {
                scaledWidth = rWidth;
                scaledHeight = rHeight;
            }
        } else {
            scaledWidth = this.imgWidth;
            scaledHeight = this.imgHeight;
            constrained = false;
        }

        // No upscaling with "contain" only
        // --------------------------------
        if (this.scale == "contain") {
            scaledWidth = Math.min(scaledWidth, this.imgWidth);
            scaledHeight = Math.min(scaledHeight, this.imgHeight);
        }

        scaledWidth = Math.floor(scaledWidth);
        scaledHeight = Math.floor(scaledHeight);


        this.$image.attr("width", scaledWidth + "");
        this.$image.attr("height", scaledHeight + "");

        if (this.height == "auto") {
            this.$div.height(scaledHeight);
            frameHeight = scaledHeight;
        }


        // image is not as large as it could be
        if (constrained && this.enableClick) {
            this.$image.addClass("constrained-size");
        }

        // align image horizontally
        switch (this.horizAlign) {
            case 0: this.$image.css("right", ""); break;
            case 1: this.$image.css("right", ((frameWidth - scaledWidth) / 2) + "px"); break;
            case 2: this.$image.css("right", "0"); break;
        }

        // align image vertically
        switch (this.vertAlign) {
            case 0: this.$image.css("bottom", ""); break;
            case 1: this.$image.css("bottom", ((frameHeight - scaledHeight) / 2) + "px"); break;
            case 2: this.$image.css("bottom", "0"); break;
        }
    },

    changeHeight : function() {
        var customVal, newHeight;
        if (!this.$div) return;

        if (this.height == "auto") {
            this.$div.css("height", "auto");
        } else if (this.height == "cust") {
            customVal = parseFloat(this.customHeightVal);

            if (!_.isNaN(customVal)) {
                this.$div.height(customVal);
                this.setHeight(customVal);
            }
        } else {
            newHeight = Component.Image.heights[this.height-1];

            this.$div.height(newHeight);
            this.setHeight(newHeight);
        }
    },

	serialize : function($super) {
		var cfg = $super();
        bindValues(cfg, this, ["height", "customHeightVal", "scale", "resizeWidth", "resizeHeight", "horizAlign", "vertAlign", "refreshRate","enableClick","customCSSClass"],
            {
                height:"auto",
                customHeightVal:"",
                scale:"contain",
                resizeWidth:"",
                resizeHeight:"",
                horizAlign:0,
                vertAlign:1,
                refreshRate:0,
                enableClick:true
            }, true);
		return cfg;
	},

	getPropertyModel : function($super) {
		var model = $super();

        var imageSizes = [{value:"auto", label:"Auto"}].concat(Props.Defaults.chartSize.slice(0, Props.Defaults.chartSize.length - 1)).concat([{value:"cust",label:"Custom..."}]);

        model.push({
            type:"select",
            id:"height",
            label:"Component Height",
            help:{ link:"klips/image-height" },
            group:"size",
            width:"175px",
            options: imageSizes,
            selectedValue: this.height
        });

        model.push({
            type:"text",
            id:"customHeightVal",
            group:"size",
            displayWhen: { height:"cust" },
            width:"50px",
            minorLabels: [
                { label:"px", position:"right", css:"quiet italics" }
            ],
            value: this.customHeightVal,
            flow:true
        });


        model.push({
            type:"select" ,
            id:"scale",
            label:"Image Resize" ,
            help:{ link:"klips/image-resize" },
            group:"image",
            width:"175px",
            options: Props.Defaults.imageScale,
            selectedValue: this.scale
        });

        model.push({
            type:"text",
            id:"resizeWidth",
            group:"image",
            minorLabels: [
                { label:"Width:", position:"left", width:"50px" },
                { label:"px", position:"right", css:"quiet italics" }
            ],
            displayWhen: { scale:"resize" },
            width:"45px",
            value:this.resizeWidth
        });

        model.push({
            type:"text",
            id:"resizeHeight",
            group:"image",
            minorLabels: [
                { label:"Height:", position:"left", width:"50px" },
                { label:"px", position:"right", css:"quiet italics" }
            ],
            displayWhen: { scale:"resize" },
            value:this.resizeHeight,
            width:"45px"
        });

        model.push({
            type:"button_group_ex",
            id:"horizAlign",
            label:"Align",
            help:{ link:"klips/image-align" },
            group:"alignment",
            options: Props.Defaults.horizontalAlign,
            selectedValue: this.horizAlign
        });

        model.push({
            type:"button_group_ex",
            id:"vertAlign",
            group:"alignment",
            options: Props.Defaults.verticalAlign,
            selectedValue: this.vertAlign
        });

        model.push({
            type:"select",
            id:"refreshRate",
            label:"Auto-Update",
            help:{ link:"klips/image-auto-update" },
            group:"refresh",
            width:"175px",
            options: Props.Defaults.refreshRate,
            selectedValue: this.refreshRate
        });

        model.push({
            type:"checkboxes",
            id:"enableClick",
            label:"On Click",
            options:[
                {
                    value:true,
                    label:"Display image preview",
                    checked:this.enableClick
                }
            ]
        });

		return model;
	},

	configure : function($super, config) {
        var _this;
		$super(config);

        bindValues(this,config,["height", "customHeightVal", "scale", "resizeWidth", "resizeHeight", "horizAlign", "vertAlign", "refreshRate", "enableClick","customCSSClass"]);

        this.invalidate();

        // ------------------------------------------
        // Handle deprecated 'scale' property values
        if (config.scale == "fit") {
            this.scale = "contain";
        } else if (config.scale == "fill") {
            this.scale = "cover";
        }

        // Handle deprecate 'max_height' property
        if (config.max_height) {
            config.height = "cust";
            this.height = config.height;
            config.customHeightVal = config.max_height;
            this.customHeightVal = config.customHeightVal;
        }
        // ------------------------------------------


        if (config.height || config.customHeightVal) {
            this.changeHeight();
        }

        if (config.horizAlign) this.horizAlign = parseInt(config.horizAlign);
        if (config.vertAlign) this.vertAlign = parseInt(config.vertAlign);
      
        if (config.refreshRate > 0) {
            clearInterval(this.intervalId);
            _this = this;

            this.intervalId = setInterval(function() {

                var src = _this.$image.attr("src");
                var random = Math.floor((Math.random() * 100000) + 1);
                if (src.indexOf("?") != -1) {
                    if (src.indexOf("&klipver=") != -1) {
                        src = src.substring(0, src.indexOf("&klipver="));
                        src = src + "&klipver=" + random;
                    } else if (src.indexOf("?klipver=") != -1) {
                        src = src.substring(0, src.indexOf("?klipver="));
                        src = src + "?klipver=" + random;
                    } else {
                        src = src + "&klipver=" + random;
                    }
                } else {
                    src = src + "?klipver=" + random;
                }

                _this.$image.unload();
                _this.$image.attr("src", src);

            }, /* Bugfix saas-6118:  Defaults are specified in seconds instead of minutes and changing the defaults wouldn't affect the already existing Klips*/
                (config.refreshRate*60));
        } else {
            clearInterval(this.intervalId);
        }

	},

    destroy : function ($super){
        $super();
        clearInterval(this.intervalId);
    }

});

/**
 * image heights at different sizes (small -> x-large)
 */
Component.Image.heights = [40, 160, 320, 640];;/****** cx.input.js *******/ 

/* global Component:false, DX:false, Props:false, KlipFactory:false, page:false, isWorkspace:false, bindValues:false,
    KF: false, dashboard: false, dateFormatConverter:false, Dashboard:false, safeText:false, CX:false */

Component.Input = $.klass(Component.InputCommon, {

    displayName: "User Input Control",
    factory_id: "input",

    inputType: 1,
    inputLabel: "",
    controlWidth: false,
    width: "",
    $label: false,
    $control: false,

    controlFormat: "dd/MM/yyyy",
    controlFormatCustom: "",
    outputFormat: "epoch",
    outputFormatCustom: "",
    timezone: "GMT+00:00",
    dateValue: null,

    isDstRoot: true,
	canHaveDataSlots: true,

    includeAllData: false,
    allDataId: CX.KF_ALL_ID,
    allDataLabel : "All",

    submitAsGroup: false,
    submitEvent: true,
	useButton: "",
	listenAllChanges: true,
    requestVariable: false,
    requestDisplayAndCalendar: false,
    receivedData: {
        display_format: false,
        calendar_format: false
    },
    renderDom : function($super) {
        var dom = $super();

        dom.delegate("select", "change", _.bind(this.onInputChange,this));
        dom.delegate("input[type='text']", "change keyup",  _.throttle(_.bind(this.onInputChange, this), 200));

        this.$label = $("<span>").addClass("cx-input-label").appendTo(dom).hide();

        this.renderControl();
    },

    renderControl : function() {
        if (this.$control) this.$control.detach();

        if (this.getDashboard().isImaging()) {
            this.$control = $("<span>").addClass("cx-input-value");
        } else {
            switch(this.inputType) {
                case Component.Input.TYPE_DROPDOWN:
                    this.$control = $("<select>");
                    break;
                case Component.Input.TYPE_TEXT:
                    this.$control = $("<input type='text'>");
                    break;
                case Component.Input.TYPE_DATEPICKER:
                {
                    this.$control = $("<input type='text'>")
                        .datepicker({
                            changeMonth:true,
                            changeYear:true,
                            autoSize:true
                        });
                    break;
                }
            }
        }

        this.el.append(this.$control);
    },

    onEvent : function ($super, evt) {
        $super(evt);

        switch (evt.id) {
            case "parent_tab_selected":
                this.invalidate();
                break;
            default:
                break;
        }
    },

    setSubmitEvent: function(val){
        this.submitEvent = val;
    },

    setRequestDisplayAndCalendar: function(val){
        this.requestDisplayAndCalendar = val;
    },

    resetDisplayAndCalendarRequest : function(){
        this.requestDisplayAndCalendar = false;
        this.receivedRequestData("display_format", false);
        this.receivedRequestData("calendar_format", false);
    },

    setRequestVariable : function (val){
        this.requestVariable = val;
    },

    receivedRequestData : function (role, val){
        this.receivedData[role] = val;
    },

    _update: function() {
        var useFullWidth = false;
        var $parent;
        var widthVal;
        var width;
        var values;
        var labels;
        var triggerChange = false;
        var valIndex;
        var originalSize;
        var strValues;
        var i;
        var v;
        var l;
        var $opt;
        var isNotEmptyValue = true;
        var valueLabelsFromInput = {};
        var pval = this._getSelectedValue();

        if (dashboard.isImaging()) {
            if (this.inputType == Component.Input.TYPE_DROPDOWN) {
                valueLabelsFromInput = this._valuesLabelPairForInput();
                values = valueLabelsFromInput.values;
                labels = valueLabelsFromInput.labels;
                isNotEmptyValue = valueLabelsFromInput.isNotEmptyValue;

                if (values && values[0] && isNotEmptyValue) {
                    valIndex = _.indexOf(values, pval);
                    // in case the value is an integer and got lost in translation at some point (converted into a string when got parsed).
                    if (valIndex == -1) valIndex = _.indexOf(values, parseInt(pval, 10));
                    // if a default value is defined.
                    if (valIndex == -1) valIndex = _.indexOf(values, this.defaultValue);
                    // fall back is to select the first value
                    if (valIndex == -1) valIndex = 0;

                    this.$control.html(safeText((!_.isArray(labels[valIndex]) && labels[valIndex]) || values[valIndex]));
                } else {
                    this.$control.html(safeText(pval));
                }
            } else if(this.inputType == Component.Input.TYPE_DATEPICKER) {
               this.formatAndSetDateControlValue(pval);
            } else {
                this.$control.html(safeText(pval));
            }
        } else {
            if (this.inputType == Component.Input.TYPE_DROPDOWN) {
                originalSize = this.$control.children("option").size();
                this.$control.empty();
                valueLabelsFromInput = this._valuesLabelPairForInput();
                values = valueLabelsFromInput.values;
                labels = valueLabelsFromInput.labels;
                isNotEmptyValue = valueLabelsFromInput.isNotEmptyValue;

                if (values && isNotEmptyValue) {
                    strValues = _.map(values, function(val) {
                        return val.toString();
                    });
                    if ($.inArray(pval, strValues) == -1) {
                        // The currently selected value (pval) was not found in the list of
                        // dropdown options, so reset pval to the component's default value
                        // if available, or the first available value.
                        pval = this.defaultValue ? this.defaultValue : values[0];
                    }

                    for (i = 0 ; i < values.length; i++) {
                        v = values[i];
                        l = ( !_.isArray(labels[i]) && labels[i]) || values[i];
                        $opt = $("<option value=\""+ encodeURI(v)+"\">"+safeText(l)+"</option>");

                        this.$control.append($opt);
                    }
                }
            }

            triggerChange = this.setControlValue(pval);

            // Don't trigger the change if the input control is in preview or tied to a submit button (see saas-4552)
            if (dashboard.isPreview() || (parseInt(this.submitAsGroup) && originalSize > 0)) {
                triggerChange = false;
            }
        }

        if (this.inputLabel && this.inputLabel.length != 0) {
            this.$label.text(this.inputLabel + ":").css("display", "");
        } else {
            this.$label.hide();
        }

        $parent = this.$control.parent();
        // if cx-input is not the immediate parent, then the control has been wrapped with jQuery mobile elements;
        // need control to take up the whole space
        if (!$parent.hasClass("cx-input")) {
            useFullWidth = true;
        }

        if (useFullWidth) {
            this.$control.css("width", "100%");
        } else {
            if (this.controlWidth && this.width) {
                widthVal = this.width;
                if (this.width.indexOf("%") === -1) {
                    widthVal += "px";
                }

                this.$control.css("width", widthVal);
            } else {
                width = this.el.width();
                if (this.inputLabel && this.inputLabel.length !== 0) {
                    width -= this.$label.outerWidth(true);
                }
                width -= this.$control.outerWidth(true) - this.$control.width();

                this.$control.width(width);
            }
        }

        if (triggerChange) {
            this.onInputChange( { currentTarget : this.$control });
        }else if(parseInt(this.submitAsGroup)){ //the input is tied to a submit button, this means it needs to update explicit listeners without changing the submitted values
            this.updateExplicitListeners(pval); //make sure that any update on this component is cascaded down to any other relevant component
        }

    },

    _updateDatepicker: function(){
        var dateFormat = dateFormatConverter.convert(this.controlFormat == "custom" ? this.controlFormatCustom : this.controlFormat);
        this.$control.datepicker("option", "dateFormat", dateFormat ? dateFormat : "");
    },

    _getSelectedValue: function() {
        var variables;
        var value;
        var klip = this.getKlip();
        var dashboard = klip.dashboard;

        if (isWorkspace()) {
            variables = Component.Input.VarResolver();

            if (variables[this.propName]) {
                value = variables[this.propName].value;
            }
        } else {
            switch (this.scope) {
                case Dashboard.SCOPE_KLIP:
                    value = klip.getKlipProp(this.propName, true);
                    break;
                case Dashboard.SCOPE_TAB:
                    value = dashboard.getDashboardProp(this.propName, Dashboard.SCOPE_TAB, klip.currentTab);
                    break;
                case Dashboard.SCOPE_DASHBOARD:
                    value = dashboard.getDashboardProp(this.propName, Dashboard.SCOPE_DASHBOARD);
                    break;
            }

            if (dashboard.isImaging() && !value) {
                // Note: you can image a klip separate from the dashboard so if the input is
                // "scope-hidden", its value could be at a higher scope
                value = klip.getKlipProp(this.propName, this.controlHidden != "scope-hidden");
            }
        }

        if (!value && this.defaultValue) {
            value = this.defaultValue;
        }

        return value;
    },

    _valuesLabelPairForInput : function() {
        var values;
        var labels;
        var isNotEmptyValue;

        //Don't modify the data directly, clone it by calling slice(0)
        values = this.getChildByRole("values").getData(0).slice(0);
        labels = this.getChildByRole("labels").getData(0).slice(0);
        isNotEmptyValue = values && values.length > 0;
        if(this.includeAllData && isNotEmptyValue) {
            if (values && values[0] !== this.allDataId) {
                values.unshift(this.allDataId);
                labels.unshift(this.allDataLabel);
            } else {
                labels[0] = this.allDataLabel;
            }
        } else {
            if(values && isNotEmptyValue && values[0] === this.allDataId) {
                values.shift();
                labels.shift();
            }
        }

        if(!values) values = [];
        if(!labels) labels = [];

        return {values : values, labels : labels, isNotEmptyValue: isNotEmptyValue};
    },

    _constructValueLabelDisplayValues : function(valueLabelPairs) {
        var values = valueLabelPairs.values;
        var labels = valueLabelPairs.labels;
        var valuesLength = values.length;
        var valuePairs = [{value:"", label:""}];
        var i;

        for(i=0; i<valuesLength; i++) {
            if(!labels[i]) {
                labels[i] = values[i];
            }
            valuePairs.push({value: values[i], label:labels[i]});
        }

        return valuePairs;
    },

    setControlValue : function(pval) {
        var triggerChange = false;
        var _this = this;
        var date;

        switch(this.inputType) {
            case Component.Input.TYPE_DROPDOWN:
            {
                _.each(this.$control.children("option"), function(opt) {
                    var val = $(opt).attr("value");

                    if (val == encodeURI(pval)) {
                        $(opt).attr("selected", "selected");
                        _this.el.find(".ui-select span").html($(opt).text());   // show selection in mobile version
                        triggerChange = true;
                    }
                });

                break;
            }
            case Component.Input.TYPE_TEXT:
            {
                this.$control.val(pval);
                triggerChange = true;

                break;
            }
            case Component.Input.TYPE_DATEPICKER:
            {
                date = this.dateValue;
                if(!date){
                    date = pval;
                }

                this.formatAndSetDateControlValue(date)
                break;
            }
        }

        return triggerChange;
    },

    formatAndSetDateControlValue: function(pval){
      var date, inputFormat, inputFormatCustom;
      var cxDisplay = this.getChildByRole("display_format");
      var cxCalendar = this.getChildByRole("calendar_format");
      var cxOutput = this.getChildByRole("output_format");

      if(!pval){ // There is no variable associated to the input control
          pval = cxOutput.getData(0).slice(0)[0];
      }

      date = "\""+pval+"\"";
      inputFormat = cxOutput.getEffectiveFmtArgs("dateFormat");
      inputFormatCustom = cxOutput.getEffectiveFmtArgs("dateFormatCustom");

      cxDisplay.setEffectiveFmtArgs("dateInputFormat", inputFormat);
      cxDisplay.setEffectiveFmtArgs("dateInputFormatCustom", inputFormatCustom);
      cxCalendar.setEffectiveFmtArgs("dateInputFormat", inputFormat);
      cxCalendar.setEffectiveFmtArgs("dateInputFormatCustom", inputFormatCustom);

      this.setRequestDisplayAndCalendar(true);

      this.setProxyFormulas(date, [cxDisplay, cxCalendar]);

      return false;
    },

    setDateControlValue : function(){
        var date, dateString, dateVal;
        var cxDisplay, cxCalendar;

        cxDisplay = this.getChildByRole("display_format");
        cxCalendar = this.getChildByRole("calendar_format");

        try {
            dateVal = cxCalendar.getData(0).slice(0)[0];
            date = $.datepicker.parseDate(Component.Input.DATE_FORMAT_FOR_JS, dateVal);
        } catch(e) {

            date= false;
        }

        if(date) {
            dateString = cxDisplay.getData(0).slice(0)[0];
            if (this.getDashboard().isImaging()) {
                this.$control.html(safeText(dateString));
            } else {
                this.$control.datepicker("option", "dateFormat", "'"+dateString+"'");
                this.$control.datepicker("option", "defaultDate", new Date(date));
                this.$control.datepicker("setDate", new Date(date));
            }
        }
        return false;
    },

    /**
     * Use Java TimeZone normalized custom ID as template for
     * timezone
     *
     * @param timezone
     * @returns {number}
     */
    getOffsetInMilliseconds : function(timezone) {
        var tz = timezone;
        var parts = [];

        if (tz.indexOf("GMT") != -1) {
            tz = tz.replace("GMT", "");
        }

        if (tz == "") return 0;

        if (tz.indexOf(":") != -1) {
            parts = tz.split(":");
        } else {
            parts.push(tz.substring(0, tz.length - 2));
            parts.push(tz.substring(tz.length - 2));
        }

        return (parseInt(parts[0]) * 60 + parseInt(parts[1])) * 60 * 1000;
    },

    onInputChange : function(evt) {
		// If this input control is bound to a submit button and the input change event is not from a submit button
		// (ie the input control was changed by the user), find all other input controls in the Klip that are listening
        // to all changes and update their formulas.  Otherwise set the dashboard property as normal (either the input
        // control isn't bound to a submit button or the control's submit button was clicked).
        var val;

        if(evt.type && evt.type != "submitButtonClicked"){
            this.setSubmitEvent(false);
        }

        if (this.inputType == Component.Input.TYPE_DATEPICKER) {
            this.getDatepickerOutputValue();
        }else {
            val = this.$control.val();
        }

        if (val !== null && val !== undefined) {
            this.updateVariableChange(val);
        }

    },

    updateVariableChange: function(val){
        if (parseInt(this.submitAsGroup) && !this.submitEvent) {
            this.updateExplicitListeners(val);
        } else {
            this.updateProperty(val);
        }

        if(!this.submitEvent) {
            this.setSubmitEvent(true);
        }
    },

    getDatepickerOutputValue: function(){
      var date;
      var cxOutput;

      try {
          date = "\"" + $.datepicker.formatDate(Component.Input.DATE_FORMAT_FOR_JS, this.$control.datepicker("getDate")) + "\"";
      } catch (e){

          date = false;
      }
      // yyyy-mm-dd

      if(date) {
          this.setRequestVariable(true);
          cxOutput = this.getChildByRole("output_format");
          this.setProxyFormulas(date, [cxOutput]);
      }

      return null;

    },

	updateExplicitListeners: function (val) {
		// Update all input components in this Klip that use this input component's variable and have
		// "Listen to all changes" checked.  This allows secondary inputs to respond to a primary input changing
		// (eg, Input 1 selects the Country and then Input 2 updates and allows selecting the Province/State).
		var klip = this.getKlip();
		var formulasToRewatch = [];
        var rewatch = [];
        var _this = this;

        // Go through each input in the Klip that is set to submit as group and collect its variable and value.
		// Each input may have a new value that has not yet been submitted and would therefore be wiped out when the
		// formula is serialized below (this becomes a problem when an input has a formula that uses more than one
		// variable that is set to submit as group).  See saas-4573.
		var variablesToCheck = {};
		variablesToCheck[_this.propName] = val;
		klip.eachComponent(function(cx){
			if (cx.factory_id == "input" && cx != _this && cx.submitAsGroup) {
                variablesToCheck[cx.propName] = cx.$control.val();
                if(cx.inputType == Component.Input.TYPE_DATEPICKER){
                  variablesToCheck[cx.propName] = cx.getChildByRole("output_format").getData(0).slice(0)[0];
                }

			}
		});

		klip.eachComponent(function(cx) {
            var formulas;
            var f;

			if ((cx.factory_id == "input" && cx != _this && cx.submitAsGroup && cx.listenAllChanges) ||  cx.factory_id == "input_button" ){
				// Check all formulas in this component to see if they use this input's variable.  If so, serialize
				// the formula, manually update variables from the list collected above, then watch the formula.
				formulas = cx.getFormulas();
				for (f = 0; f < formulas.length; f++) {
					if (formulas[f].usesVariable(_this.propName)) {
						formulasToRewatch.push(formulas[f]);
					}
				}
			}
		});

		if (formulasToRewatch.length > 0) {
			rewatch.push({ kid: klip.id, fms: formulasToRewatch });
			DX.Manager.instance.watchFormulas( rewatch, false, variablesToCheck );
		}
	},

	serialize : function($super) {
        var cfg = $super();
        var cfgProps = [
            "inputType",
            "inputLabel",
            "controlWidth",
            "width",
            "includeAllData",
            "allDataLabel",
            "submitAsGroup",
            "useButton",
            "listenAllChanges"
        ];
        var cfgDefaults = {
            inputLabel: "",
            controlWidth: false,
            width: "",
            includeAllData: false,
            allDataLabel: "All",
            submitAsGroup: false,
            useButton: "",
            listenAllChanges: true
        };

        bindValues(cfg, this, cfgProps, cfgDefaults, true);


        return cfg;
    },

    configure : function($super, config) {

        $super(config);
        bindValues(this,config,[
            "inputType",
            "inputLabel",
            "includeAllData",
            "allDataLabel",
            "controlWidth",
            "width",
            "controlFormat",
            "controlFormatCustom",
            "outputFormat",
            "outputFormatCustom",
            "timezone",
            "submitAsGroup",
            "useButton",
            "listenAllChanges"
        ]);


        if (config.inputType) {
            this.inputType = parseInt(config.inputType);    // important to have as an int and not a string

            this.canHaveDataSlots = true;
            if(this.inputType !== Component.Input.TYPE_DROPDOWN) {
                this.canHaveDataSlots = false; // to remove "Add Hidden Data" from the context-menu
            }

            if (config["~propertyChanged"]) {
                this.createProxys();
                this.renderControl();
                // add or remove the "Data" from the Control Palette.
                page.controlPalette.updateItems(this.getWorkspaceControlModel());
            }



            if (isWorkspace()) page.updateMenu();

        }

        if (config["~propertyChanged"] && (config.controlFormat || config.controlFormatCustom)) {
            this._updateDatepicker();
        }

        if (config.timezone) {
            this.timezone = config.timezone.toUpperCase();
        }

        if(config.allDataLabel) {
            this.allDataLabel = config.allDataLabel;
        }

        if(config.includeAllData !== undefined) {
            this.includeAllData = config.includeAllData;
        }

        this.invalidate();
    },

	/**
	 *
	 * @param $super
	 */
	getPropertyModelGroups : function($super) {
		return $super().concat(["prop", "vars", "submitGroup", "visibility", "data_slot"]);
	},


	getPropertyModel : function($super) {
        var model = $super();
        var klip;
        var buttonList;
        var propName;
        var valueLabelPairs;
        var _this = this;
        var values = {};
        var configProps = {
            displayWhen: {inputType: 1}
        };

        var resetDefaultValue = function(){
          _this.defaultValue = "";
          page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
        };

        var updateDefaultValueDropDown = function($widgetScope) {
            var defaultValueWidget;

            if(_this.inputType !== Component.Input.TYPE_DROPDOWN) {
                return;
            }

            valueLabelPairs = _this._valuesLabelPairForInput();
            values = _this._constructValueLabelDisplayValues(valueLabelPairs);
            if($widgetScope) {
                defaultValueWidget = $widgetScope.findWidget("defaultValue");
                defaultValueWidget.options = values;
                defaultValueWidget.update();
            }
        };

        updateDefaultValueDropDown();

        model.push({
            type:"text",
            id:"inputLabel",
            label:"Control Label",
            group:"prop",
            value:this.inputLabel
        });

        model.push({
            type:"select",
            id:"inputType",
            label:"Control Type",
            help: {
                    link: "klips/control-type"
                },
            group:"prop",
            options:Props.Defaults.inputType,
            selectedValue:this.inputType,
            onChange: function () {
                resetDefaultValue();
                updateDefaultValueDropDown(this);
            }
        });

        model.push({
            type: "checkboxes",
            id: "includeAllData",
            label: "All Data",
            group: "vars",
            component: this,
            fieldMargin:"0 0 -8px 0",
            displayWhen : { inputType:1 },
            options: [
                {value: true, label: "Include \"All\" data option", checked: this.includeAllData}
            ],
            onChange: function () {
                updateDefaultValueDropDown(this);
            }
        });

        model.push({
            type: "text",
            id: "allDataLabel",
            group: "vars",
            displayWhen: { includeAllData: true, inputType : 1},
            value: this.allDataLabel,
            onChange: function () {
                updateDefaultValueDropDown(this);
            }
        });

        model.push({
            type:"select",
            id:"defaultValue",
            label:"Default Value",
            group:"vars",
            displayWhen : {inputType : 1},
            displayWhenNot: { propName: ""},
            options:values,
            selectedValue:this.defaultValue
        });

        model.push({
            type: "text",
            id: "defaultValue",
            label: "Default Value",
            group: "vars",
            displayWhenNot: { propName: "", inputType : 1},
            value: this.defaultValue
        });

        model.push({
            type: "select",
            id: "display_fmt_dateFormat",
            label: "Display Format",
            group: "prop",
            displayWhen: {inputType: 4},
            component: this.getChildByRole("display_format"),
            options: Props.Defaults.dateFormatJava,
            selectedValue: this.getChildByRole("display_format") && this.getChildByRole("display_format").getEffectiveFmtArgs("dateFormat"),
            onChange: function (event) {
                this.component.setEffectiveFmtArgs("dateFormat", event.value);
                if (event.value != "custom") {
                    this.component.setEffectiveFmtArgs("dateFormatCustom", event.value);
                    page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                }
            },
            help: {
                link: "klips/control-date-picker"
            }
        });

        model.push({
            type: "text",
            id: "display_fmt_dateFormatCustom",
            group: "prop",
            component: this.getChildByRole("display_format"),
            displayWhen: {inputType: 4},
            displayWhenNot: {display_fmt_dateFormat: "epoch"},
            enableWhen: {display_fmt_dateFormat: "custom"},
            value: this.getChildByRole("display_format") && this.getChildByRole("display_format").getEffectiveFmtArgs("dateFormatCustom"),
            debounceEnabled: true,
            onChange: function (event) {
                this.component.setEffectiveFmtArgs("dateFormatCustom", event.value);
            }
        });

        model.push({
            type: "select",
            id: "output_fmt_dateFormat",
            label: "Output Format",
            group: "prop",
            displayWhen: {inputType: 4},
            component: this.getChildByRole("output_format"),
            options: Props.Defaults.javaDateFormatOutput,
            selectedValue: this.getChildByRole("output_format") && this.getChildByRole("output_format").getEffectiveFmtArgs("dateFormat"),
            onChange: function (event) {
                this.component.setEffectiveFmtArgs("dateFormat", event.value);
                if (event.value != "custom") {
                    this.component.setEffectiveFmtArgs("dateFormatCustom", event.value);
                    _this.onInputChange(event);
                    page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                }
            },
            help: {
                link: "klips/control-date-picker"
            }
        });

        model.push({
            type: "text",
            id: "output_fmt_dateFormatCustom",
            group: "prop",
            component: this.getChildByRole("output_format"),
            displayWhen: {inputType: 4},
            displayWhenNot: {output_fmt_dateFormat: "epoch"},
            enableWhen: {output_fmt_dateFormat: "custom"},
            value: this.getChildByRole("output_format") && this.getChildByRole("output_format").getEffectiveFmtArgs("dateFormatCustom"),
            debounceEnabled: true,
            onChange: function (event) {
                this.component.setEffectiveFmtArgs("dateFormatCustom", event.value);
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                _this.onInputChange(event);
            }
        });

        model.push({
            type: "checkboxes",
            id: "fmt_arg_useEOD",
            group: "prop",
            component: this.getChildByRole("output_format"),
            displayWhen: {inputType: 4, output_fmt_dateFormat: ["epoch","custom"], output_fmt_dateFormatCustom:["epoch"]},
            options: [
                {
                    value: true,
                    label: "Use end of day value",
                    checked: this.getChildByRole("output_format") && (this.getChildByRole("output_format").getEffectiveFmtArgs("useEOD", "false") == "true")
                }
            ],
            fieldMargin: "-6px 0 0",
            onChange: function(arg){
                if (arg && arg.value == true){
                    this.component.setEffectiveFmtArgs("useEOD", "true");
                }else{
                    this.component.resetEffectiveFmtArgs("useEOD");
                }
                _this.onInputChange(arg);
            }
        });

        model.push({
            type: "checkboxes",
            id: "account_timezone",
            group: "prop",
            component: this,
            displayWhen: {inputType: 4, output_fmt_dateFormat:["epoch", "custom"]},
            displayWhenContains:{output_fmt_dateFormatCustom:["epoch", "Z", "z"]},
            fieldMargin: "-3px 0 -8px 0",
            options: [
                {
                    value: true,
                    label: "Use default time zone",
                    checked: this.getChildByRole("output_format") && this.getChildByRole("output_format").useAccountTimeZone(Props.Defaults.timezoneArgMap.OUTPUT_TIMEZONE)
                }
            ],
            toggleTimezoneWidget: function () {
                // This checkbox will also disable/enabled the 'Time Zone' dropdown.
                // We will also trigger the formula to be re-evaluated to get the new time zone.
                var widget = this.findWidget("timezone");
                if (widget) {
                    if (this.component.getChildByRole("output_format") && this.component.getChildByRole("output_format").useAccountTimeZone(Props.Defaults.timezoneArgMap.OUTPUT_TIMEZONE)) {
                        widget.disable();
                    } else {
                        widget.enable();
                    }
                }
            },
            onChange: function (arg) {
                if (arg && arg.value == true) {
                    // When enabling the auto format detection, we will clear out the user set values.
                    this.component.components.forEach ( function (p) {
                        p.resetTimeZone(Props.Defaults.timezoneArgMap.INPUT_TIMEZONE);
                        p.resetTimeZone(Props.Defaults.timezoneArgMap.OUTPUT_TIMEZONE);
                    });
                } else {
                    this.component.components.forEach ( function (p) {
                        p.setEffectiveFmtArgs("inputTimezone", KF.company.getTimeZone());
                        p.setEffectiveFmtArgs("outputTimezone", KF.company.getTimeZone());
                    });
                }
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                this.toggleTimezoneWidget();
                _this.onInputChange(event);
            }

        });

        model.push({
            type: "select",
            id: "timezone",
            group: "prop",
            component: this,
            displayWhen: {inputType: 4, output_fmt_dateFormat:["epoch", "custom"]},
            displayWhenContains:{output_fmt_dateFormatCustom:["epoch", "Z", "z"]},
            enableWhen: {account_timezone: false},
            options: this.getTimezoneOptionList(),
            selectedValue: this.getChildByRole("output_format") && this.getChildByRole("output_format").getEffectiveFmtArgs("outputTimezone", KF.company.getTimeZone()),
            width: 250,
            help: {
                link: "klips/control-timezone"
            },
            onChange: function (evt) {
                this.component.components.forEach ( function (p) {
                    p.setEffectiveFmtArgs("inputTimezone", evt.value);
                    p.setEffectiveFmtArgs("outputTimezone", evt.value);
                });

                _this.onInputChange(event);
            }
        });

        model.push({
            type:"checkboxes",
            id:"controlWidth",
            label:"Control Width",
            group:"prop",
            options: [
                {value:true, label:"Specify a width", checked:this.controlWidth}
            ]
        });

        model.push({
            type: "text",
            id: "width",
            group: "prop",
            value: this.width,
            width:40,
            displayWhen: {controlWidth:true},
            minorLabels: [
                { label:"px or % of container width", position:"right", css:"quiet italics" }
            ]
        });

        model.push({
            type:"select",
            id:"submitAsGroup",
            label:"Set Value",
            group:"submitGroup",
            options: Props.Defaults.submitValue,
            selectedValue: this.submitAsGroup,
            help: {
                link: "klips/input-set-value"
            }
        });

        // Make a list of all buttons available in the Klip
        klip = this.getKlip();
        buttonList = [{value:"", label:"None Selected"}];
        klip.eachComponent(function(cx){
            if(cx.factory_id == "input_button") {
                propName = cx.propName;
                buttonList.push({value: cx.id + "****" + propName, label: cx.displayName + (propName == "" ? "" : " (" + propName + ")")});
            }
        });

        model.push({
            type:"select",
            id:"useButton",
            label:"Use Button",
            group:"submitGroup",
            options: buttonList,
            selectedValue: this.useButton,
            displayWhen: {submitAsGroup:1}
        });

        model.push({
            type:"checkboxes",
            id:"listenAllChanges",
            label:"Listen",
            group:"submitGroup",
            options: [
                {value:true, label:"Instantly react to variable updates", checked:this.listenAllChanges}
            ],
            displayWhen: {inputType:1, submitAsGroup:1}
        });

        this.addDataSlotProperty(model, configProps);

        return model;
    },

    createProxys : function() {
        var requiredChildren = [];
        var i;
        var req;
        var cx;

        switch(this.inputType) {
            case Component.Input.TYPE_DROPDOWN:
            {
                requiredChildren = [
                    {role:"values", props:{displayName:"Values", type:"proxy"}},
                    {role:"labels", props:{displayName:"Labels", type:"proxy"}}
                ];
                break;
            }
            case Component.Input.TYPE_DATEPICKER:
            {
                requiredChildren = [
                    {role:"display_format", props:{displayName:"Display Format", type:"calendar_display_format", autoFmt:false, fmt:"dat2",
                        fmtArgs:{dateInputFormat:Component.Input.DATE_FORMAT_TO_DPN, dateFormat:"MM/dd/yyyy", dateFormatCustom:"MM/dd/yyyy"}
                    }},
                    {role:"output_format", props:{displayName:"Output Format", type:"output_format", autoFmt:false, fmt:"dat2",
                        fmtArgs:{dateInputFormat:Component.Input.DATE_FORMAT_TO_DPN, dateFormat:"epoch", dateFormatCustom:"epoch"}
                    }},
                    {role:"calendar_format", props:{displayName:"Calendar Format", type:"calendar_display_format", autoFmt:false, fmt:"dat2",
                        fmtArgs:{dateInputFormat:Component.Input.DATE_FORMAT_TO_DPN, dateFormat:Component.Input.DATE_FORMAT_TO_DPN, dateFormatCustom:Component.Input.DATE_FORMAT_TO_DPN}
                    }}
                ];
                break;
            }
            case Component.Input.TYPE_TEXT:
            {
                break;
            }
        }

        this.removeProxyComponents(requiredChildren);

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);
            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                if ( req.props._data ) cx.setData( req.props._data );
                if (this.isAutoFormatFeatureOn()) {
                    cx.autoFmt = false;
                }
                this.addComponent(cx);
            }

            cx.isDeletable = false;
        }
    },

    removeProxyComponents: function(requiredChildren){
        var requiredRoles = _.map(requiredChildren, "role");
        var componentsToRemove;

        if(this.components) {

            componentsToRemove = this.components.filter(function (cx) {
                return requiredRoles.indexOf(cx.role) < 0;
            });

            componentsToRemove.forEach(function (cx) {
                if(this.canHaveDataSlots && cx.factory_id == "data_slot") return; // do not remove hidden data slots if user has added any
                this.removeComponent(cx);
            }.bind(this));
        }
    },

    setProxyFormulas: function(date, proxies){
            var klip = this.getKlip();
            var formulas = [];
            proxies.forEach( function(proxy) {
                proxy.setFormula(0, date);
                formulas.push(proxy.getFormula());
            });
            DX.Manager.instance.watchFormulas([{kid: klip.id, fms: formulas}], false);
    },

    afterFactory : function() {
         this.createProxys();

        if(this.hasOldDatepicker()){
            this.migrateOldDatepicker();
        }
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        if (this.inputType == Component.Input.TYPE_DROPDOWN) {
            this.addDataSlotControl(model);
        }

        return model;
    },

    hasOldDatepicker: function(){
        return ((this.inputType === Component.Input.TYPE_DATEPICKER) && (this.hasOwnProperty("outputFormat") || this.hasOwnProperty("controlFormat") || this.hasOwnProperty("timezone")));
    },

    mapGMTOffsetToRegionalTimezone : function(timezone){
        var timezoneRegex = /GMT(\+|-)\d{2}:\d{2}/;
        var timezoneWithoutMinutes = /:00/;
        var tzIndex;

        if(timezoneRegex.test(timezone)){
            timezone = timezone.replace(/(\+|-)0/, "$1");
            if(timezoneWithoutMinutes.test(timezone)){
                timezone = timezone.replace(timezoneWithoutMinutes,"");
            } else {
                tzIndex = this.getTimezoneOptions().findIndex(function(o){
                    return  o.label.indexOf(timezone) > -1;
                });

                if(tzIndex && tzIndex > -1){
                    timezone = this.getTimezoneOptions()[tzIndex].value;
                }
            }
        }

        this.components.forEach( function (p) {
            p.setEffectiveFmtArgs("inputTimezone", timezone);
            p.setEffectiveFmtArgs("outputTimezone", timezone);
        });
    },

    getTimezoneOptionList : function () {
        var timezone = this.getChildByRole("output_format") && this.getChildByRole("output_format").getEffectiveFmtArgs("outputTimezone");

        if(timezone && !this.hasTimezoneOption(timezone)){
            return $.extend({
                value: timezone,
                label:timezone
            }, this.getTimezoneOptions());
        } else {
            return this.getTimezoneOptions();
        }
    },

    hasTimezoneOption : function (timezone){
        var timezoneOptions = this.getTimezoneOptions().map(function(o){
            return o.value;
        });

        return timezoneOptions.indexOf(timezone) > -1;
    },

    migrateOldDatepicker: function(){
        var displayFormatProxy;
        var outputFormatProxy;

        displayFormatProxy = this.getChildByRole("display_format");
        outputFormatProxy = this.getChildByRole("output_format");

        if(this.controlFormat){
            displayFormatProxy.setEffectiveFmtArgs("dateFormat", this.controlFormat);

            if(this.controlFormatCustom){
                displayFormatProxy.setEffectiveFmtArgs("dateFormatCustom", this.controlFormatCustom);
            }
        }

        if(this.outputFormat){
            outputFormatProxy.setEffectiveFmtArgs("dateFormat", this.outputFormat);

            if(this.outputFormatCustom){
                outputFormatProxy.setEffectiveFmtArgs("dateFormatCustom", this.outputFormatCustom);
            }
        }

        if(this.timezone){
            //datepicker is not doing a conversion from input to output timezone. Therefore, timezone is equal for both input and output timezone.
            this.mapGMTOffsetToRegionalTimezone(this.timezone);
        }
    }

});


/**
 *  function assigned by environment that should return a list of UI variables that can be used by the input
 */
Component.Input.VarResolver = false;

Component.Input.TYPE_DROPDOWN = 1;
Component.Input.TYPE_TEXT = 2;
Component.Input.TYPE_CHECKBOX = 3;
Component.Input.TYPE_DATEPICKER = 4;
Component.Input.DATE_FORMAT_TO_DPN = "MMMM d, yyyy"; //Do not include time until we start using a date-time picker
Component.Input.DATE_FORMAT_FOR_JS = "MM d, yy";
;/****** cx.input_button.js *******/ 

/* global Component:false, CXTheme:false, isWorkspace:false, bindValues:false, KlipFactory:false */
Component.InputButton = $.klass(Component.InputCommon, {

    displayName: "Button",
    factory_id: "input_button",

    $button: false,
	customWidth: 0,
	buttonWidth: "",
	overrideColors: false,
	backgroundColor: "cx-theme_eee",
	borderColor: "cx-theme_aaa",
	textColor: "cx-theme_222",

    renderDom : function($super) {
        var dom = $super();

        dom.on("click", "button", _.bind(this.onSubmitButtonClick, this))
			.on("mousedown", "button", _.bind(this.onSubmitButtonDown, this))
            .on("mouseup", "button", _.bind(this.onSubmitButtonUp, this));

        this.$button = $("<button type='button' style='width:100%;'>").appendTo(dom);
    },

    onEvent : function ($super, evt) {
        $super(evt);

        switch (evt.id) {
            case "theme_changed":
                this.invalidate();
                break;
            default:
                break;
        }
    },

    _update: function() {
        var value;
        var label;
        var width = "100%";
        var currentVal;
        var triggerChange = false;
        var klip = this.getKlip();
        var hasCustomStyleFeature = this.hasCustomStyleFeature();

        value = this.getChildByRole("value").getData(0)[0];
        label = this.getChildByRole("label").getData(0)[0];


        if (this.customCSSClass && hasCustomStyleFeature) {
            this.$button.addClass(this.customCSSClass);
        }

        if (this.customWidth && this.buttonWidth) {
            width = this.buttonWidth + (this.buttonWidth.indexOf("%") == -1 ? "px" : "");
        }

        this.$button.css({
            width: width,
            "background-color": CXTheme.getThemeColour(this.overrideColors ? this.backgroundColor : "cx-theme_eee"),
            border: "1px solid " + CXTheme.getThemeColour(this.overrideColors ? this.borderColor : "cx-theme_aaa"),
            color: CXTheme.getThemeColour(this.overrideColors ? this.textColor : "cx-theme_222")
        });

        this.$button.text(label);

        currentVal = this.$button.attr("name");
        this.$button.attr("name", value);

        if (klip && !klip.dashboard.isImaging() && !klip.dashboard.isPreview()) {
            triggerChange = (value && !currentVal);
        }

        if (triggerChange) {
            this.onSubmitButtonClick({ currentTarget: this.$button });
        }
    },

	onSubmitButtonDown: function (evt) {
        var backgroundColour;
        var redColour;
        var greenColour;
        var blueColour;

		evt.preventDefault();
		if (!isWorkspace()) {
			// Set the background colour to 20% darker when the button is pressed
			backgroundColour = CXTheme.getThemeColour(this.overrideColors ? this.backgroundColor : "cx-theme_eee");

			redColour = parseInt(backgroundColour.substring(1, 3), 16);
			greenColour = parseInt(backgroundColour.substring(3, 5), 16);
			blueColour = parseInt(backgroundColour.substring(5, 7), 16);

			this.$button.css("background-color", "#" + Math.round(redColour * 0.8).toString(16) + Math.round(greenColour * 0.8).toString(16) + Math.round(blueColour * 0.8).toString(16));
		}
	},

	onSubmitButtonUp: function () {
		if (!isWorkspace()) {
            this.$button.css("background-color", CXTheme.getThemeColour(this.overrideColors ? this.backgroundColor : "cx-theme_eee"));
        }
	},

	onSubmitButtonClick: function (evt) {
        var val;

		if (!isWorkspace()) {
			val = $(evt.currentTarget).attr("name");

            this.updateProperty(val);
			this.notifySubmitListeners();
		}
	},

	// Get all inputs that are bound to this button's variable and trigger their onInputChange events
	notifySubmitListeners: function() {
		var buttonEvent = document.createEvent("Event");
        var klip = this.getKlip();
        var _this = this;

		buttonEvent.initEvent("submitButtonClicked", true, true);

		klip.eachComponent(function(cx) {
			if (cx.factory_id == "input" && cx.submitAsGroup && cx.useButton == _this.id + "****" + _this.propName) {
				cx.onInputChange(buttonEvent);
			}
		});
	},

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this,
            [
                "customWidth",
                "buttonWidth",
                "overrideColors",
                "backgroundColor",
                "borderColor",
                "textColor",
                "customCSSClass"
            ],
            {
                customWidth:0,
                buttonWidth:"",
                overrideColors:false,
                backgroundColor:"cx-theme_eee",
                borderColor:"cx-theme_aaa",
                textColor:"cx-theme_222"
            },
            true
        );

        return cfg;
    },

    configure : function($super, config)  {
        $super(config);

        bindValues(this, config, [
            "customWidth",
            "buttonWidth",
            "overrideColors",
            "backgroundColor",
            "borderColor",
            "textColor",
            "customCSSClass"
        ]);

        this.invalidate();
    },

    getPropertyModelGroups: function($super) {
        return $super().concat(["button", "vars", "visibility"]);
    },

    getPropertyModel : function($super) {
        var model = $super();

        model.push({
            type: "text",
            id: "defaultValue",
            label: "Default Value",
            group: "vars",
            displayWhenNot: { propName: "" },
            value: this.defaultValue
        });
        
        model.push({
            type:"checkboxes",
            id:"customWidth",
            label:"Button Width",
            group:"button",
            options: [
                {value:true, label:"Specify a width", checked: this.customWidth}
            ]
        });

        model.push({
            type:"text" ,
            id:"buttonWidth",
            label:"",
            group:"button",
            minorLabels: [
                { label:"px or % of container width", position:"right", css:"quiet italics" }
            ],
            width: 40,
            displayWhen: { customWidth:1 },
            value: this.buttonWidth
        });

        model.push({
            type: "checkboxes",
            id: "overrideColors",
            label: "Button Colors",
            group: "button",
            options: [
                {value:true, label:"Override the default colors", checked: this.overrideColors}
            ]
        });

        model.push({
            type: "color_picker",
            id: "backgroundColor",
            group: "button",
            minorLabels: [
                { label:"Background", position:"right" }
            ],
            valueType:"theme",
            selectedValue: this.backgroundColor,
            displayWhen: { overrideColors:true }
        });

        model.push({
            type: "color_picker",
            id: "borderColor",
            group: "button",
            minorLabels: [
                { label:"Border", position:"right" }
            ],
            flow: true,
            valueType:"theme",
            selectedValue: this.borderColor,
            displayWhen: { overrideColors:true }
        });

        model.push({
            type: "color_picker",
            id: "textColor",
            group: "button",
            minorLabels: [
                { label:"Text", position:"right" }
            ],
            valueType:"theme",
            selectedValue: this.textColor,
            displayWhen: { overrideColors:true }
        });

        return model;
    },

    afterFactory : function() {
        var requiredChildren = [
            {role:"value", props:{displayName:"Value", type:"proxy"}},
            {role:"label", props:{displayName:"Label", type:"proxy", data:[["Submit"]], formulas:[{"txt":"\"Submit\"","src":{"t":"expr","v":false,"c":[{"t":"l","v":"Submit"}]}}]}}
        ];
        var i;
        var req;
        var cx;

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);
            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                if ( req.props._data ) cx.setData( req.props._data );
                this.addComponent(cx);
            }

            cx.isDeletable = false;
        }
    }

});

;/****** cx.label.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, isWorkspace:false, bindValues:false, CXTheme:false,Props:false, page:false, FMT:false,
    safeText:false, findDefaultAggregationRule:false,dashboard:false, DX:false */

Component.LabelComponent = $.klass(Component.DataComponent, {

    displayName: "Label",
    factory_id:"label",
    elLabel : false ,
    value : false,
    boundValues : ["align","font_colour","font_style","wrap","show_value","customCSSClass"],

    /** fixed string that appears after the value */
    suffix: false,
    /** fixed string that appears before the value */
    prefix: false,
    percentage: false,

    align: "l",
    font_style: "bold",
    font_colour: "cx-color_444",
    wrap: false,
    show_value: true,

    canAggregate: true,
    canSetAggregation: true,

    initialize: function($super) {
        $super();
    },

    onParentAssignment: function () {
        var parentFactoryId = this.parent.factory_id;

        switch (parentFactoryId) {
            case "gauge":
            case "simple_value":
            case "panel_grid": //simple label inside layout grid
                this.defaultFilterBeforeAggregation = true;
                this.canHaveDataSlots = true;
                this.canFilter = true;
                break;

            case "mini_series":
                this.canHaveDataSlots = true;
                break;

            default:
                break;
        }
    },

    onKlipAssignment : function($super) {

        $super();

        if (!this.parent.factory_id) {
            this.canHaveDataSlots = true; //simple label
            this.canFilter = true;
            this.defaultFilterBeforeAggregation = true;
        }
    },

    getSupportedDst: function($super) {
        var dst = $super();
        var peerComponents = this.getDstPeerComponents();

        if (this.hasMiniChartFmt()) {
            dst.aggregation = false;
        }

        if(this.role && this.role === "last-value" && this.dataDisabled) {
            dst.aggregation = false;
            dst.aggregate = false;
        }

        peerComponents.forEach(function (component) {
            if(component.factory_id == "data_slot" && component.getGroupInfo().isGrouped){
                dst.aggregate = false;
            }
        });


        return dst;
    },

    renderDom : function($super) {
        var dom = $super();
        this.elLabel = $("<div>");
        dom.append(this.elLabel);
    },

    onResize : function($super) {
        $super();

        this.invalidateAndQueue();
    },

    onEvent : function ($super, evt) {
        $super(evt);

        if (evt.id == "theme_changed") {
//            this.invalidate();
        }
    },

    update : function($super) {
        $super();

        var data = this.getData(0);

        if (this.parent.isKlip || this.parent.isPanel) {
            if (data.length == 0) {
                this.el.css("min-height", "15px");
            } else {
                this.el.css("min-height", "");
            }
        }

        // handle label reactions
        // ----------------------
        var reactionContext = { colors:false, icons:[], styles:[], text:false };
        this.collectReactions(reactionContext);

        Component.LabelComponent.Render(this, data, {
            $target: this.elLabel,
            mergeArgs: { width:this.elLabel.width() },
            rCtx: reactionContext
        });

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },


    serialize : function($super) {
        var cfg = $super();
        this.cleanFormatArgs();

        bindValues(cfg, this, this.boundValues, {
            align:"l",
            font_colour:"cx-color_444",
            font_style:"bold",
            wrap:false,
            show_value:true
        }, true);
        return cfg;
    },

    cleanFormatArgs : function() {
        if (this.fmtArgs) {
            if (_.indexOf(["spk", "spb", "dsc", "hrz"], this.fmt) == -1) {
                delete this.fmtArgs["chartRangeOption"];
                delete this.fmtArgs["minCustomVal"];
                delete this.fmtArgs["maxCustomVal"];
            } else {
                if (this.fmtArgs.chartRangeOption == "auto") delete this.fmtArgs["chartRangeOption"];
                if (this.fmtArgs.minCustomVal == "") delete this.fmtArgs["minCustomVal"];
                if (this.fmtArgs.maxCustomVal == "") delete this.fmtArgs["maxCustomVal"];
            }

            if (_.indexOf(["spb", "hrz"], this.fmt) == -1) {
                delete this.fmtArgs["posBarColor"];
                delete this.fmtArgs["negBarColor"];
            } else {
                if (this.fmtArgs.posBarColor == "cx-theme_blue_3") delete this.fmtArgs["posBarColor"];
                if (this.fmtArgs.negBarColor == "cx-theme_neg_3") delete this.fmtArgs["negBarColor"];
            }

            if (this.fmt != "spk") {
                delete this.fmtArgs["chartLineFormat"];
                delete this.fmtArgs["includeHighLowPoints"];
                delete this.fmtArgs["lineColor"];
                delete this.fmtArgs["spotColor"];
                delete this.fmtArgs["fillColor"];
                delete this.fmtArgs["minSpotColor"];
                delete this.fmtArgs["maxSpotColor"];
            } else {
                if (this.fmtArgs.chartLineFormat == "la") delete this.fmtArgs["chartLineFormat"];
                if (this.fmtArgs.includeHighLowPoints === false) delete this.fmtArgs["includeHighLowPoints"];
                if (this.fmtArgs.lineColor == "cx-theme_blue_3") delete this.fmtArgs["lineColor"];
                if (this.fmtArgs.spotColor == "cx-theme_blue_3") delete this.fmtArgs["spotColor"];
                if (this.fmtArgs.fillColor == CXTheme.current.cx_sparkline_area) delete this.fmtArgs["fillColor"];
                if (this.fmtArgs.minSpotColor == "cx-theme_red_3") delete this.fmtArgs["minSpotColor"];
                if (this.fmtArgs.maxSpotColor == "cx-theme_red_3") delete this.fmtArgs["maxSpotColor"];
            }

            if (this.fmt != "spb") {
                delete this.fmtArgs["zeroBarColor"];
            } else {
                if (this.fmtArgs.zeroBarColor == "cx-theme_aaa") delete this.fmtArgs["zeroBarColor"];
            }

            if (this.fmt != "wlc") {
                delete this.fmtArgs["winThreshold"];
                delete this.fmtArgs["winColor"];
                delete this.fmtArgs["drawColor"];
                delete this.fmtArgs["lossColor"];
            } else {
                if (this.fmtArgs.winThreshold == "") delete this.fmtArgs["winThreshold"];
                if (this.fmtArgs.winColor == "cx-theme_blue_3") delete this.fmtArgs["winColor"];
                if (this.fmtArgs.drawColor == "cx-theme_aaa") delete this.fmtArgs["drawColor"];
                if (this.fmtArgs.lossColor == "cx-theme_red_3") delete this.fmtArgs["lossColor"];
            }

            if (this.fmt != "dsc") {
                delete this.fmtArgs["barHeight"];
                delete this.fmtArgs["customBarHeight"];
                delete this.fmtArgs["discreteThreshold"];
                delete this.fmtArgs["discreteBarColor"];
                delete this.fmtArgs["discreteThresholdColor"];
            } else {
                if (this.fmtArgs.barHeight == "auto") delete this.fmtArgs["barHeight"];
                if (this.fmtArgs.customBarHeight == "") delete this.fmtArgs["customBarHeight"];
                if (this.fmtArgs.discreteThreshold == "") delete this.fmtArgs["discreteThreshold"];
                if (this.fmtArgs.discreteBarColor == "cx-theme_blue_3") delete this.fmtArgs["discreteBarColor"];
                if (this.fmtArgs.discreteThresholdColor == "cx-theme_red_3") delete this.fmtArgs["discreteThresholdColor"];
            }

            if (this.fmt != "bch") {
                delete this.fmtArgs["targetColor"];
                delete this.fmtArgs["performanceColor"];
                delete this.fmtArgs["rangeThemeColors"];
                delete this.fmtArgs["rangeColors"];
            } else {
                if (this.fmtArgs.targetColor == "cx-theme_444") delete this.fmtArgs["targetColor"];
                if (this.fmtArgs.performanceColor == "cx-theme_666") delete this.fmtArgs["performanceColor"];
            }

            if (this.fmt != "hrz") {
                delete this.fmtArgs["hideRange"];
                delete this.fmtArgs["horizRangeColor"];
            } else {
                if (this.fmtArgs.hideRange === false) delete this.fmtArgs["hideRange"];
                if (this.fmtArgs.horizRangeColor == "cx-theme_ccc") delete this.fmtArgs["horizRangeColor"];
            }
        }
    },

    getContextMenuModel : function($super, ctx, menuConfig) {
        var model = $super(ctx, menuConfig);

        if (this.role && this.role === "last-value") {
            if (this.dataDisabled) {
                model = this.removeDataSlotContextMenuItem(model);
            }
        }
        return model;
    },

    setData: function ($super, data, msg) {
        $super(data, msg);
//		if (isWorkspace())
//		{
//			var f = this.getFormula(0);
//			// test to see if this is a single expression containing a single data pointer (default click on data visualizer for a blank formula)
//			if (f && f.src.t == "expr" && f.src.c && f.src.c.length == 1 && (f.src.c[0].t == "d" || f.src.c[0].t == "l"))
//			{
//				// grab the first data
//				var d = this.getData(0);
//				if (d && d.length)
//				{
//					var fmt = FMT.identifyFormat(d[0]);
//					if (fmt == 'txt')
//					{
//						fmt = FMT.identifyFormat(d[1]);
//					}
//
//                    if (this.fmt != fmt && (!this.allowedFormats || _.indexOf(this.allowedFormats, fmt) != -1)) {
//                        this.configure({fmt: fmt});
//
//                        if (page.activeComponent == this) {
//                            page.updatePropertySheet(
//                                this.getPropertyModel(),
//                                this.getPropertyModelGroups(),
//                                page.componentPropertyForm
//                            );
//                        }
//                    }
//				}
//			}
//		}
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        if (this.parent.factory_id == "gauge" || this.parent.factory_id == "mini_series") {
            model = this.parent.getWorkspaceControlModel();
        }

        if (this.role && this.role === "last-value") {
            if (!this.dataDisabled) {
                this.addDataSlotControl(model);
            }
        } else if (this.canHaveDataSlots) {
            this.addDataSlotControl(model);
        }
        return model;
    },

    getPropertyModelGroups : function($super) {
        return $.merge($super(), [
            "fmt",
            "dst-root",
            "dst-group",
            "dst-sort",
            "dst-filter",
            "appearance",
            "content",
            "data_slot"
        ]);
    },

    getPropertyModel : function($super) {
        var i, j;
        var model = $super();
        var _this = this;
        var parentFactoryId = this.parent.factory_id;
        var factoryId = this.factory_id;
        var fmt_ar = [];
        var previousFormat=this.fmt;
        if (parentFactoryId == "gauge") {
            model.push({
                type: "checkboxes",
                id: "show_value",
                label: "Display",
                group: "gauge",
                options: [
                    {
                        value: true,
                        label: "Show value on gauge",
                        checked: this.show_value
                    }
                ]
            });
        } else if (parentFactoryId == "mini_bar") {
            model.push({
                type:"checkboxes",
                id:"show_value",
                label:"Display",
                group:"mini_bar",
                options: [
                    { value:true, label:"Show performance label", checked: this.show_value }
                ]
            });
        }

        if (this.allowedFormats) {
            for (i = 0; i < this.allowedFormats.length; ++i) {
                for (j = 0; j < Props.Defaults.formats.length; ++j) {
                    if (this.allowedFormats[i] == Props.Defaults.formats[j].value) {
                        // Need to push a copy of the property so when we modify it, it doesn't affect the original.
                        fmt_ar.push($.extend({},Props.Defaults.formats[j]));
                        break;
                    }
                }
            }
        } else {
            for (i = 0; i < Props.Defaults.formats.length; ++i) {
                // Need to push a copy of the property so when we modify it, it doesn't affect the original.
                fmt_ar.push($.extend({}, Props.Defaults.formats[i]));
            }
        }

        if(this.fmt === "raw") {
            fmt_ar.push({value:"raw", label:"Raw"});
        }

        if (this.useNewDateFormat()) {
            // If this component needs to use new dat2 format, we need to update the supported format list and switch from dat to dat2.
            for (i = 0; i < fmt_ar.length; ++i) {
                if (fmt_ar[i].value == FMT.type_dat) {
                    fmt_ar[i].value = FMT.type_dat2;
                }
            }
        }

        if (this.getVirtualColumnType() == DX.ModelType.DATE && !this.isAutoFormatOn()) {
            // We will determine the default date input format if the data type is "dat" and auto detection is NOT on.
            this.setDefaultDateInputFormat();
        }

        // --- Formats (group: fmt)
        if (this.isAutoFormatFeatureOn() && this.displayAutoFormat) {

            model.push({
                type: "checkboxes",
                id: "autoFmt",
                label: "Format as",
                group: "fmt",
                component: this,
                fieldMargin:"0 0 -8px 0",
                options: [
                    {value: true, label: "Automatically set format", checked: this.isAutoFormatOn()}
                ],
                toggleFormatTypeWidget: function (reevaluateFormula) {
                    // This checkbox will also disable/enabled the 'Format as' dropdown. When auto format is enabled,
                    // We will also trigger the formula to be re-evaluated to get the new auto detected format.
                    var widget = this.findWidget("fmt");
                    if (widget) {
                        if (this.component.isAutoFormatOn()) {
                          if(_this.isNumerical(_this.fmt) && _this.getGroupInfo() && _this.getGroupInfo().isGroupedByThisComponent){
                            _this.setFormatAndValidateAggregation("txt");
                          }
                            _this.updateFormula(reevaluateFormula);
                            widget.disable();
                        } else {
                            widget.enable();
                        }
                    }
                },
                onChange: function (arg) {
                    if (arg && arg.value == true) {
                        // When enabling the auto format detection, we will clear out the user set values.
                        this.component.resetFormat();
                    } else  {
                        // When auto format detection is off, we need to merge the user set args with detect args.
                        this.component.mergeFormatArgs();
                    }
                    this.toggleFormatTypeWidget(true);

                    //Store the current format for the next change.
                    this.component.previouslySelectedFmt = this.component.fmt;
                },
                update: function () {
                    this.toggleFormatTypeWidget(false);
                    if (this.hidden) this.$field.hide();
                    else this.$field.show();
                }
            });
        }
        // Format As
        model.push({
            id: "fmt",
            group: "fmt",
            type: "select",
            label: this.isAutoFormatFeatureOn() && this.displayAutoFormat ? "" : "Format as",
            options: fmt_ar,
            selectedValue: this.fmt,
            help: {
                link: "klips/format-as"
            },

            onChange: function (event) {

                if(_this.isVCFeatureOn()){

                    if(_this.isNumerical(_this.fmt) && _this.getGroupInfo() && _this.getGroupInfo().isGroupedByThisComponent) {
                        Component.LabelComponent.notifyGroupOnNumber(_this,previousFormat);
                    } else {
                        var virtualColumnId = _this.getVirtualColumnId();
                        var hasMiniChartFmt = _this.hasMiniChartFmt();
                        var aggregationRules = _this.getAvailableAggregations();
                        var defaultAggregationRule = findDefaultAggregationRule(aggregationRules);
                        var aggregation = _this.getDstAggregation(virtualColumnId);
                        var defaultAggregation = hasMiniChartFmt ? "join"
                            : defaultAggregationRule.value;
                        var needUpdateDSTFormula = _this.useNewDateFormat()
                            && (_this.fmt == FMT.type_dat2 || event.value
                            == FMT.type_dat2);
                        // Set the new data format and also validate the current DST aggregation.
                        _this.setFormatAndValidateAggregation(event.value);
                        _this.setDefaultDateOutputFormat();
                        page.updatePropertySheet(_this.getPropertyModel(),
                            _this.getPropertyModelGroups(),
                            page.componentPropertyForm);
                        if (aggregation && aggregation
                            != defaultAggregation) {
                            page.invokeAction("set_aggregation", {
                                cx: _this,
                                aggregation: defaultAggregation
                            });
                        }
                        if (needUpdateDSTFormula) {
                            _this.updateDSTFormula(true);
                        } else if (_this.usePostDSTReferences() && (FMT.toModelType(_this.previouslySelectedFmt) !== FMT.toModelType(event.value))) {
                            //Only watch requests accept fmt value changes, so to avoid confusion for pre-post-DST where the format change will be respected
                            //only when a DST action is present, only update the results if post-DST is enabled (since all requests are then watch requests)
                            _this.updateFormula(true);
                        }
                    }

                    // saas-9080 When switching from dat2 format to other formats manually, we need to re-evaluate the formula.
                    if (this.getSelectedFormat(event) === FMT.type_dat2) {
                        _this.updateFormula(true);
                    }
                    //Store the format value as the previous value for the next pass.
                    _this.previouslySelectedFmt = _this.fmt;
                }
            },

            getSelectedFormat: function(event) {
                // Get the currently selected value from the widget, i.e. the previous value before change.
                if (event && event.widget) {
                    return event.widget.selectedValue;
                }
                return "";
            }
        });
        // Decimals (for all numeric formats)
        model.push({
            type: "select",
            id: "fmt_arg_precision",
            label: "Decimal Places",
            group: "fmt",
            displayWhen: {fmt: ["num", "cur", "pct", "dur"]},
            options: Props.Defaults.numberFormat,
            selectedValue: this.getEffectiveFmtArgs("precision")
        });
        // Separator (for number, currency and percent)
        model.push({
            type: "select",
            id: "fmt_arg_separator",
            label: "Separators",
            group: "fmt",
            displayWhen: {fmt: ["num", "cur", "pct"]},
            options: Props.Defaults.separator,
            selectedValue: this.fmtArgs.separator
        });
        // Symbol (for currency)
        model.push({
            type: "select",
            id: "fmt_arg_currency",
            label: "Symbol",
            group: "fmt",
            displayWhen: {fmt: "cur"},
            options: Props.Defaults.currencyFormat,
            selectedValue: this.fmtArgs.currency
        });
        // Negative Format (for currency)
        model.push({
            type: "select",
            id: "fmt_arg_paren",
            label: "Negative Marker",
            group: "fmt",
            displayWhen: {fmt: "cur"},
            options: Props.Defaults.negativeFormat,
            selectedValue: this.fmtArgs.paren
        });
        if (!this.useNewDateFormat()) {
            // These set of model properties are related to the old dat format.
            //Input format for date
            model.push({
                type: "select",
                id: "fmt_arg_dateInputFormat",
                label: "Input Format",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat},
                options: [
                    {value: "default", label: "Automatically Detect"},
                    {value: "yyyyMMdd", label: "yyyyMMdd (Google Analytics)"},
                    {value: "custom", label: "Custom..."}
                ],
                selectedValue: this.getEffectiveFmtArgs("dateInputFormat"),
                help: {
                    link: "formula-bar/date_format"
                }
            });
            // custom input format for Date
            model.push({
                type: "text",
                id: "fmt_arg_dateInputFormatCustom",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat, fmt_arg_dateInputFormat: FMT.DATE_INPUT_FORMAT_CUSTOM},
                value: this.getEffectiveFmtArgs("dateInputFormatCustom"),
                debounceEnabled: true,
                updateOnEnterOrDefocus: true,
                updateConditionVisibility:true
            });
            //Output format for date
            model.push({
                type: "select",
                id: "fmt_arg_dateFormat",
                label: "Display Format",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat},
                options: Props.Defaults.dateFormat,
                selectedValue: this.getEffectiveFmtArgs("dateFormat", Props.Defaults.dateFormat[Props.Defaults.dateFormatDefaultIndex].value),
                onChange: function (event) {
                    if (event.value !== "custom") {
                        //Force the update of the custom field value
                        _this.setEffectiveFmtArgs("dateFormatCustom", event.value);
                        page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                    }
                },
                help: {
                    link: "formula-bar/date_format"
                }
            });
            // custom format for Date
            model.push({
                type: "text",
                id: "fmt_arg_dateFormatCustom",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat},
                enableWhen: {fmt: FMT.type_dat, fmt_arg_dateFormat: "custom"},
                value: this.getEffectiveFmtArgs("dateFormatCustom", Props.Defaults.dateFormat[Props.Defaults.dateFormatDefaultIndex].value),
                debounceEnabled: true,
                cx: _this,
                updateOnEnterOrDefocus: true
            });
        } else {
            // These set of model properties are related to the new dat2 format.
            model.push({
                type: "select",
                id: "fmt_arg_dateInputFormat",
                label: "Input Format",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat2},
                options: Props.Defaults.dateInputFormats,
                selectedValue: this.getEffectiveFmtArgs("dateInputFormat"),
                help: {
                    link: "formula-bar/date_format"
                },
                onChange: function (evt) {
                    _this.updateDSTFormula(true);
                }
            });
            // custom input format for Date
            model.push({
                type: "text",
                id: "fmt_arg_dateInputFormatCustom",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat2, fmt_arg_dateInputFormat: FMT.DATE_INPUT_FORMAT_CUSTOM},
                value: this.getEffectiveFmtArgs("dateInputFormatCustom"),
                debounceEnabled:true,
                updateOnEnterOrDefocus:true,
                updateConditionVisibility:true,
                onChange: function (evt) {
                    _this.updateDSTFormula(true);
                }
            });

            // custom timezone for input date
            model.push({
                type: "checkboxes",
                id: "account_input_timezone",
                group: "fmt",
                component: this,
                displayWhen: {fmt: FMT.type_dat2},
                displayWhenNot: {fmt_arg_dateInputFormat: ["epoch-ms", "epoch"]},
                displayWhenNotContains: {fmt_arg_dateInputFormatCustom: ["Z", "z"]},
                fieldMargin:"0 0 -8px 0",
                options: [
                    {value: true, label: "Use default time zone", checked: this.useAccountTimeZone(Props.Defaults.timezoneArgMap.INPUT_TIMEZONE)}
                ],
                toggleInputTimezoneWidget: function (reevaluateFormula) {
                    // This checkbox will also disable/enabled the 'Input Time Zone' dropdown
                    // We will also trigger the formula to be re-evaluated to get the new input time zone.
                    var widget = this.findWidget("fmt_arg_inputTimezone");
                    if (widget) {
                        if (this.component.useAccountTimeZone(Props.Defaults.timezoneArgMap.INPUT_TIMEZONE)) {
                            this.component.updateDSTFormula(true);
                            widget.disable();
                        } else {
                            widget.enable();
                        }
                    }
                },
                onChange: function (arg) {
                    if (arg && arg.value == true) {
                        // When enabling the auto format detection, we will clear out the user set values.
                        this.component.resetTimeZone(Props.Defaults.timezoneArgMap.INPUT_TIMEZONE);
                    }else {
                        this.component.setEffectiveFmtArgs("inputTimezone", KF.company.getTimeZone());
                    }
                    page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                    this.toggleInputTimezoneWidget(true);
                },
                update: function () {
                    this.toggleInputTimezoneWidget(false);
                    if (this.hidden) this.$field.hide();
                    else this.$field.show();
                }
            });

            model.push({
                type: "select",
                id: "fmt_arg_inputTimezone",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat2},
                displayWhenNot: {fmt_arg_dateInputFormat: ["epoch-ms", "epoch"]},
                displayWhenNotContains: {fmt_arg_dateInputFormatCustom: ["Z", "z"]},
                enableWhen:{account_input_timezone:false},
                options: this.getTimezoneOptions(),
                selectedValue: this.getEffectiveFmtArgs("inputTimezone", KF.company.getTimeZone()),
                width: 250,
                help:{
                    link:"klips/control-timezone"
                },
                onChange: function (evt) {
                    _this.updateDSTFormula(true);
                }
            });

            //Output format for date
            model.push({
                type: "select",
                id: "fmt_arg_dateFormat",
                label: "Display Format",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat2},
                options: Props.Defaults.dateFormatJava,
                help: {
                    link: "formula-bar/date_format"
                },
                selectedValue: this.getEffectiveFmtArgs("dateFormat", Props.Defaults.dateFormatJava[Props.Defaults.dateFormatJavaDefaultIndex].value),
                onChange: function (event) {
                    if (event.value !== "custom") {
                        //Force the update of the custom field value
                        _this.setEffectiveFmtArgs("dateFormatCustom", event.value);
                        page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                    }
                    _this.updateDSTFormula(true);
                }
            });
            // custom format for Date
            model.push({
                type: "text",
                id: "fmt_arg_dateFormatCustom",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat2},
                enableWhen: {fmt: FMT.type_dat2, fmt_arg_dateFormat: "custom"},
                value: this.getEffectiveFmtArgs("dateFormatCustom",Props.Defaults.dateFormatJava[Props.Defaults.dateFormatJavaDefaultIndex].value),
                debounceEnabled:true,
                cx: _this,
                updateOnEnterOrDefocus:true,
                onChange: function (evt) {
                    _this.updateDSTFormula(true);
                }
            });

            // custom timezone for output date
            model.push({
                type: "checkboxes",
                id: "account_output_timezone",
                group: "fmt",
                component: this,
                displayWhen: {fmt: FMT.type_dat2},
                fieldMargin:"0 0 -8px 0",
                options: [
                    {value: true, label: "Use default time zone", checked: this.useAccountTimeZone(Props.Defaults.timezoneArgMap.OUTPUT_TIMEZONE)}
                ],
                toggleOutputTimezoneWidget: function (reevaluateFormula) {
                    // This checkbox will also disable/enabled the 'Output Time Zone' dropdown
                    // We will also trigger the formula to be re-evaluated to get the new output time zone.
                    var widget = this.findWidget("fmt_arg_outputTimezone");
                    if (widget) {
                        if (this.component.useAccountTimeZone(Props.Defaults.timezoneArgMap.OUTPUT_TIMEZONE)) {
                            this.component.updateFormula(reevaluateFormula);
                            widget.disable();
                        } else {
                            widget.enable();
                        }
                    }
                },
                onChange: function (arg) {
                    if (arg && arg.value == true) {
                        this.component.resetTimeZone(Props.Defaults.timezoneArgMap.OUTPUT_TIMEZONE);
                    }else {
                        this.component.setEffectiveFmtArgs("outputTimezone", KF.company.getTimeZone());
                    }
                    page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                    this.toggleOutputTimezoneWidget(true);
                },
                update: function () {
                    this.toggleOutputTimezoneWidget(false);
                    if (this.hidden) this.$field.hide();
                    else this.$field.show();
                }
            });

            model.push({
                type: "select",
                id: "fmt_arg_outputTimezone",
                group: "fmt",
                displayWhen: {fmt: FMT.type_dat2},
                enableWhen:{account_output_timezone:false},
                options: this.getTimezoneOptions(),
                selectedValue: this.getEffectiveFmtArgs("outputTimezone", KF.company.getTimeZone()),
                width: 250,
                help:{
                    link:"klips/control-timezone"
                },
                onChange: function (evt) {
                    _this.updateDSTFormula(true);
                }
            });
        }

        //Format for duration
        model.push({
            type: "select",
            id: "fmt_arg_durationFormat",
            label: "Duration Format",
            group: "fmt",
            displayWhen: {fmt: "dur"},
            options: Props.Defaults.durationFormat,
            selectedValue: this.fmtArgs.durationFormat
        });

        //Format for mini charts
        // Chart Size (for mini-charts)
        model.push({
            type: "select",
            id:"fmt_arg_chart_size",
            label:"Chart Height" ,
            group:"fmt",
            displayWhen: { fmt: ["spk","spb","wlc","dsc","bch","hrz"] },
            options: Props.Defaults.size,
            selectedValue:this.fmtArgs.chart_size
        });

        // Line Options
        model.push({
            type:"button_group_ex",
            id:"fmt_arg_chartLineFormat",
            label:"Style",
            group:"fmt",
            displayWhen: { fmt:"spk" },
            options: Props.Defaults.miniLineFormats,
            selectedValue: this.fmtArgs.chartLineFormat ? this.fmtArgs.chartLineFormat : "la"
        });

        model.push({
            type:"checkboxes",
            id:"fmt_arg_includeHighLowPoints",
            label:"High/Low",
            fieldMargin:"0",
            group:"fmt",
            displayWhen: { fmt:"spk" },
            options: [
                {value:true, label:"Highlight highest & lowest points", checked:this.fmtArgs.includeHighLowPoints}
            ]
        });

        // Tristate options
        model.push({
            type:"text" ,
            id:"fmt_arg_winThreshold",
            label:"Threshold" ,
            group:"fmt",
            displayWhen: { fmt:"wlc" },
            width:"50px",
            value: this.fmtArgs.winThreshold
        });

        // Discrete options
        model.push({
            type:"select",
            id:"fmt_arg_barHeight",
            label:"Bar Height",
            group:"fmt",
            displayWhen: { fmt:"dsc" },
            options: [{value:"auto", label:"Automatic"}, {value:"cust", label:"Custom..."}],
            selectedValue: this.fmtArgs.barHeight
        });

        model.push({
            type:"text",
            id:"fmt_arg_customBarHeight",
            minorLabels: [ {label:"%", position:"right", css:"quiet"} ],
            group:"fmt",
            displayWhen: { fmt:"dsc", fmt_arg_barHeight:"cust" },
            width:"50px",
            value: this.fmtArgs.customBarHeight,
            flow:true
        });

        model.push({
            type:"text",
            id:"fmt_arg_discreteThreshold",
            label:"Threshold",
            group:"fmt",
            displayWhen: { fmt:"dsc" },
            width: "50px",
            value: this.fmtArgs.discreteThreshold
        });

        // Range
        model.push({
            type:"select",
            id:"fmt_arg_chartRangeOption",
            label:"Range",
            group:"fmt",
            displayWhen: { fmt:["spk", "spb", "dsc", "hrz"] },
            options: [{value:"auto", label:"Automatic"}, {value:"cust", label:"Custom..."}],
            selectedValue: this.fmtArgs.chartRangeOption,
            help: {
                link: "klips/range"
            }
        });

        model.push({
            type:"text",
            id:"fmt_arg_minCustomVal",
            minorLabels: [ {label:"to", position:"right"} ],
            group:"fmt",
            displayWhen: { fmt:["spk", "spb", "dsc", "hrz"], fmt_arg_chartRangeOption:"cust" },
            width:"40px",
            value: this.fmtArgs.minCustomVal,
            flow:true
        });

        model.push({
            type:"text",
            id:"fmt_arg_maxCustomVal",
            group:"fmt",
            displayWhen: { fmt:["spk", "spb", "dsc", "hrz"], fmt_arg_chartRangeOption:"cust" },
            width:"40px",
            value: this.fmtArgs.maxCustomVal,
            flow:true
        });

        model.push({
            type:"checkboxes",
            id:"fmt_arg_hideRange",
            group:"fmt",
            displayWhen: { fmt:"hrz" },
            options: [
                {value:true, label:"Hide range background", checked:this.fmtArgs.hideRange}
            ]
        });

        // Line colours
        model.push({
            type:"color_picker",
            id:"fmt_arg_lineColor",
            group:"fmt",
            label:"Colors",
            minorLabels: [ {label:"Line", position:"right"} ],
            displayWhen: { fmt:"spk" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.fmtArgs.lineColor,
            breakFlow:true
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_spotColor",
            group:"fmt",
            minorLabels: [ {label:"End point", position:"right"} ],
            displayWhen: { fmt:"spk" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.fmtArgs.spotColor,
            flow: {padding:"75px"}
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_fillColor",
            group:"fmt",
            fieldMargin: "0",
            minorLabels: [ {label:"Area", position:"right"} ],
            displayWhen: { fmt:"spk", fmt_arg_chartLineFormat:"la" },
            valueType:"theme",
            defaultColour: {rgb:CXTheme.current.cx_sparkline_area},
            selectedValue:this.fmtArgs.fillColor
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_minSpotColor",
            group:"fmt",
            minorLabels: [ {label:"Low point(s)", position:"right"} ],
            displayWhen: { fmt:"spk", fmt_arg_includeHighLowPoints:true },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.fmtArgs.minSpotColor
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_maxSpotColor",
            group:"fmt",
            minorLabels: [ {label:"High point(s)", position:"right"} ],
            displayWhen: { fmt:"spk", fmt_arg_includeHighLowPoints:true },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.fmtArgs.maxSpotColor,
            flow: {padding:"20px"}
        });

        // Bar colours
        model.push({
            type:"color_picker",
            id:"fmt_arg_posBarColor",
            group:"fmt",
            fieldMargin: "0",
            label:"Colors",
            minorLabels: [ {label:"Positive", position:"right"} ],
            displayWhen: { fmt:["spb", "hrz"] },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.fmtArgs.posBarColor
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_zeroBarColor",
            group:"fmt",
            fieldMargin: "0",
            minorLabels: [ {label:"Zero", position:"right"} ],
            displayWhen: { fmt:"spb" },
            valueType:"theme",
            defaultColour: {rgb:"#aaaaaa", theme:"cx-theme_aaa"},
            selectedValue:this.fmtArgs.zeroBarColor
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_negBarColor",
            group:"fmt",
            fieldMargin: "0",
            minorLabels: [ {label:"Negative", position:"right"} ],
            displayWhen: { fmt:["spb", "hrz"] },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.fmtArgs.negBarColor
        });

        // Tristate colours
        model.push({
            type:"color_picker",
            id:"fmt_arg_winColor",
            group:"fmt",
            fieldMargin: "0",
            label:"Colors",
            minorLabels: [ {label:"Win", position:"right"} ],
            displayWhen: { fmt:"wlc" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.fmtArgs.winColor
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_drawColor",
            group:"fmt",
            fieldMargin: "0",
            minorLabels: [ {label:"Draw", position:"right"} ],
            displayWhen: { fmt:"wlc" },
            valueType:"theme",
            defaultColour: {rgb:"#aaaaaa", theme:"cx-theme_aaa"},
            selectedValue:this.fmtArgs.drawColor
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_lossColor",
            group:"fmt",
            fieldMargin: "0",
            minorLabels: [ {label:"Loss", position:"right"} ],
            displayWhen: { fmt:"wlc" },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.fmtArgs.lossColor
        });

        // Discrete colours
        model.push({
            type:"color_picker",
            id:"fmt_arg_discreteBarColor",
            group:"fmt",
            label:"Colors",
            minorLabels: [ {label:"Bar", position:"right"} ],
            displayWhen: { fmt:"dsc" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.fmtArgs.discreteBarColor
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_discreteThresholdColor",
            group:"fmt",
            minorLabels: [ {label:"Threshold", position:"right"} ],
            displayWhen: { fmt:"dsc" },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.fmtArgs.discreteThresholdColor
        });

        // Bullet colours
        model.push({
            type:"color_picker",
            id:"fmt_arg_targetColor",
            group:"fmt",
            label:"Colors",
            minorLabels: [ {label:"Target", position:"right"} ],
            displayWhen: { fmt:"bch" },
            fieldMargin:"9px 0 0",
            valueType:"theme",
            defaultColour: {rgb:"#444444", theme:"cx-theme_444"},
            selectedValue:this.fmtArgs.targetColor
        });

        model.push({
            type:"color_picker",
            id:"fmt_arg_performanceColor",
            group:"fmt",
            minorLabels: [ {label:"Performance", position:"right"} ],
            displayWhen: { fmt:"bch" },
            fieldMargin:"0",
            valueType:"theme",
            defaultColour: {rgb:"#666666", theme:"cx-theme_666"},
            selectedValue:this.fmtArgs.performanceColor
        });

        model.push({
            type:"markup",
            id:"msg_range_color",
            group:"fmt",
            label:"Ranges",
            displayWhen: { fmt:"bch" },
            fieldMargin:"0",
            el:$("<span class='strong' style='color:#333333;font-size:1.1em;'>Comparative background measures:</span>"),
            help: {
                link: "klips/background-ranges"
            }
        });

        if (this.fmtArgs.rangeThemeColors) {
            for (i = 0; i < this.fmtArgs.rangeThemeColors.length; i++) {
                var colorProp = {
                    type: "color_picker",
                    id: "fmt_arg_range_color-" + i,
                    group: "fmt",
                    displayWhen: { fmt:"bch" },
                    fieldMargin:"0",
                    valueType:"theme",
                    selectedValue: this.fmtArgs.rangeThemeColors[i]
                };

                if (i % 7 != 0) {
                    colorProp.flow = { padding:"3px" };
                } else {
                    colorProp.breakFlow = true;
                    colorProp.fieldMargin = "0";
                }

                model.push(colorProp);
            }
        }

        model.push({
            type:"button",
            id:"add_range_color",
            group:"fmt",
            displayWhen: { fmt:"bch" },
            text:"+",
            fieldMargin:"0",
            breakFlow: true,
            onClick: function(evt) {
                _this.fmtArgs.rangeThemeColors.push("cx-theme_fff");
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(),page.componentPropertyForm);
                _this.update();
            },
            css:"add"
        });

        model.push({
            type:"button",
            id:"remove_range_color",
            group:"fmt",
            displayWhen: { fmt:"bch" },
            text:"-",
            flow:{ padding:"0px" },
            onClick: function(evt) {
                _this.fmtArgs.rangeThemeColors.pop();
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                _this.update();
            },
            css:"remove"
        });

        // Horizontal colours
        model.push({
            type:"color_picker",
            id:"fmt_arg_horizRangeColor",
            group:"fmt",
            fieldMargin: "0",
            minorLabels: [ {label:"Range", position:"right"} ],
            displayWhen: { fmt:"hrz" },
            valueType:"theme",
            defaultColour: {rgb:"#cccccc", theme:"cx-theme_ccc"},
            selectedValue:this.fmtArgs.horizRangeColor
        });


        //Format options for img
        model.push({
            type: "select",
            id:"fmt_arg_scale",
            label:"Image Size" ,
            group:"fmt",
            displayWhen: { fmt:"img" },
            options:[
                {value:"dns",label:"Keep original size"},
                {value:"fd",label:"Force size to..."},
                {value:"md",label:"Scale down if larger than..."}
            ],
            selectedValue:this.fmtArgs.scale
        });
        model.push({
            type: "text",
            id:"fmt_arg_imgFixWidth",
            label:"Image Width" ,
            group:"fmt",
            displayWhen: { fmt:"img", fmt_arg_scale:"fd" },
            value:this.fmtArgs.imgFixWidth
        });
        model.push({
            type: "text",
            id:"fmt_arg_imgFixHeight",
            label:"Image Height" ,
            group:"fmt",
            displayWhen: { fmt:"img", fmt_arg_scale:"fd" },
            value:this.fmtArgs.imgFixHeight
        });
        model.push({
            type: "text",
            id:"fmt_arg_imgMaxWidth",
            label:"Max Width" ,
            group:"fmt",
            displayWhen: { fmt:"img", fmt_arg_scale:"md" },
            value:this.fmtArgs.imgMaxWidth
        });
        model.push({
            type: "text",
            id:"fmt_arg_imgMaxHeight",
            label:"Max Height" ,
            group:"fmt",
            displayWhen: { fmt:"img", fmt_arg_scale:"md" },
            value:this.fmtArgs.imgMaxHeight
        });


        //Format options for hyperlink
        model.push({
            type: "text",
            id:"fmt_arg_hypFixedText",
            label:"Show Label" ,
            group:"fmt",
            displayWhen: { fmt:"hyp" },
            value:this.fmtArgs.hypFixedText
        });
        model.push({
            type: "checkboxes",
            id:"fmt_arg_hypSameTab",
            label:"Open Link" ,
            group:"fmt",
            displayWhen: { fmt:"hyp" },
            options:[
                {
                    value:"active",
                    label:"Open link in current browser tab",
                    checked:this.fmtArgs.hypSameTab
                }
            ]
        });

        if (
            !parentFactoryId || // just a label
            parentFactoryId == "panel_grid" || //label inside layout grid
            parentFactoryId == "simple_value" ||
            parentFactoryId == "gauge" ||
            factoryId == "table_col" ||
            factoryId == "series_data" ||
            factoryId == "series_labels" ||
            factoryId == "chart_axis" ||
            factoryId == "html_data" ||
            factoryId == "map_tooltip_prop" ||
            factoryId == "mini_data"
        ) {
            this.addDstProperties(model);
        }

        this.addAppearanceProperties(model);
        this.addPrefixSuffixProperties(model);

        if (this.role && this.role === "last-value") {
            if (!this.dataDisabled) {
                this.addDataSlotProperty(model);
            }
        } else if (this.canHaveDataSlots) {
            this.addDataSlotProperty(model);
        }

        return model;
    },

    configure : function($super, config, update) {

        $super(config);

        bindValues(this, config, this.boundValues);

        this.updateLabelSizes(config);
        if (config["~propertyChanged"] && (this.parent.factory_id == "mini_series" || this.parent.factory_id == "mini_bar" || this.parent.factory_id == "gauge")) {
            this.invalidate();
        }

        this.setBulletChartDefaults(config);
    },

    getSupportedReactions: function($super) {
        return { color: true, icon: true, style: true, text: true };
    },

    /**
     *
     * @param idx
     * @param reaction
     * @param ctx - {
     *      color: false,
     *      icons: [],
     *      styles: [],
     *      text: false
     * }
     */
    handleReaction : function( idx, reaction, ctx ) {
        switch (reaction.type) {
            case "color":
                ctx.color = CXTheme.getThemeColour(reaction.value);
                break;
            case "icon":
                ctx.icons.push(reaction);
                break;
            case "style":
                ctx.styles.push(reaction.value);
                break;
            case "text":
                ctx.text = reaction.value;
                break;
        }
    },

    getAvailableAggregations: function($super) {
        var aggregations = $super();
        var parentId = this.parent.factory_id;
        var modelType = this.getVirtualColumnType();

        switch (parentId) {
            case "gauge":
                if (modelType != DX.ModelType.NUMBER) {
                    aggregations = Props.Defaults.numericTextAggregations;
                }
                break;
            default:
                break;
        }

        return aggregations;
    }
});

Component.LabelComponent.notifyGroupOnNumber = function (cx,prevFormat){
    var encodedName = _.escape(cx.displayName);
    require(["js_root/utilities/utils.dialog"], function (dialogUtils) {
        dialogUtils.confirm("You're attempting to change the data format to one which does not support grouping. The component will have to be ungrouped before the new data format can be applied.", {
            title: "Do you want to ungroup?",
            okButtonText: "Ungroup",
            okButtonId: "ungroup",
            showShadow: true,
            closeOnShadowClick: true,
            objectName: encodedName
        }).then(function() {
            cx.deleteDstOperation(cx.findDstOperationByType("group"));
        }.bind(this), function(){
            cx.setFormatAndValidateAggregation(prevFormat);
            cx.updateFormula(true);
            page.updatePropertySheet(cx.getPropertyModel(), cx.getPropertyModelGroups(), page.componentPropertyForm);
            }.bind(this))
    }.bind(this));
};
/**
 * generates (or renders into) a <div> a label based on the provided CX's properties
 *
 * @param cx - model containing label properties
 * @param data - data to display in the label
 * @param config - {
 *      $target: false,     // targeted label element
 *      index: 0,           // the index postion of this label when in the context of a series of labels for which a single reaction should be applied.
 *                          // eg, with a table column, index would be the column row
 *      mergeArgs: {},      // other format arguments such as width
 *      rCtx: {}
 * }
 * @returns {*}
 * @constructor
 */
Component.LabelComponent.Render = function(cx, data, config) {
    var $el = (config && config.$target ? config.$target : false);
    var fontStyles;
    var hasCustomStyleFeature = cx.hasCustomStyleFeature();

    if (!$el) {
        $el = $("<div>");
    } else {
        $el.empty();
    }

    if (!cx.fmt) cx.fmt = "txt";

    // extract reactions
    var rColour = (config && config.rCtx && config.rCtx.color ? config.rCtx.color : false);
    var rIcons = (config && config.rCtx && config.rCtx.icons ? config.rCtx.icons : []);
    var rStyles = (config && config.rCtx && config.rCtx.styles ? config.rCtx.styles : []);
    var rText = (config && config.rCtx && config.rCtx.text ? config.rCtx.text : false);


    // Format and display data
    // ----------------------------------------------
    if (typeof(data) == "string") {
        $el.html(data);
    } else {
        try {
            var newData = [];
            var tempData = [];
            var args;
            for (var m = 0; m < data.length; m++) {
                tempData[0] = data[m];
                if (cx.fmt) {
                    if (config && config.mergeArgs) {
                        args = $.extend({}, cx.getFmtArgs(), config.mergeArgs);
                    } else {
                        args = cx.getFmtArgs();
                    }

                    if (rColour) args.reactionColour = rColour;
                    if (rIcons.length > 0) args.iconDisplayed = true;

                    newData.push(FMT.format(cx.fmt, tempData, args));
                } else {
                    newData.push(safeText("" + tempData));
                }
            }

            if (newData.length == 1) {
                $el.append(newData[0]);
            } else {
                $el.append(newData.join(" "));
            }
        } catch(e) {
            // console.log(e);
        }
    }


    // Clean up style then add classes for properties
    // ----------------------------------------------
        $el.attr("class", "").css("color", "");

        if (cx.customCSSClass && hasCustomStyleFeature) {
            $el.addClass(cx.customCSSClass);
        }

        $el.addClass("label-inner align-" + cx.align + " size-" + (cx.fmtArgs.chart_size ? cx.fmtArgs.chart_size : cx.size))
            .css("white-space", (cx.wrap ? "normal" : "nowrap"));

        if (cx.font_style) {
            fontStyles = cx.font_style.split(",");

            fontStyles.forEach(function (fontStyle) {
                $el.addClass("label-style-" + fontStyle);
            });
        }

        if (cx.font_colour) {
            if (cx.font_colour.indexOf("#") == -1) {
                $el.addClass(cx.font_colour);
            } else {
                $el.css("color", cx.font_colour);
            }
        }

        if (!isWorkspace() && cx.grouped) {
            $el.addClass("grouped")
                .data("drilldown-data", data[0]);
        }



    // Handle reactions
    // ----------------------------------------------
    // - apply the reaction colour depending on the format
    // - apply the reaction styles depending on the format
    switch(cx.fmt) {
        case "txt":
        case "num":
        case "cur":
        case "pct":
        case "dat":
        case "dat2":
        case "hyp":
        case "dur":
        case "raw":
        {
            if (rColour && rColour.length > 0) {
                $el.css("color", rColour);
                $el.find(".label-affix").removeClass(function (index,className) {
                    return (className.match (/(^|)cx-color\S+/g) || []).join(" ");
                }).css("color",rColour);
            }

            if (rStyles.length > 0) {
                // TODO regular and bolditalic are deprecated
                $el.removeClass("label-style-regular label-style-italic label-style-bold label-style-bolditalic");
                $el.find(".label-affix").removeClass("label-style-bold label-style-no_bold label-style-italic label-style-no_italic");

                for (var i = 0; i < rStyles.length; i++) {
                    $el.addClass("cr-style-" + rStyles[i]);
                }
            }

            break;
        }

        case "img":
        case "spk":
        case "spb":
        case "wlc":
        case "dsc":
        case "bch":
        case "hrz":
        default:
            // The above formats either handle the reaction colour themselves, or don't support reaction colours
            break;
    }

    // - apply the reaction text
    if (rText && rText.length > 0) {
        $el.empty().text(rText);
    }

    // - apply the reaction icons
    for (var k = 0; k < rIcons.length; k++) {
        var size;

        // fmtArgs.chart_size goes from 0 (xx-small) to 6 (xx-large), rather than 1-7 like the font sizes.
        // 1 is being added to the chart size so they line up
        switch (cx.size) {
            case "xx-small": size = "14"; break;
            case "x-small" : size = "18"; break;
            case "small"   : size = "22"; break;
            case "medium"  : size = "26"; break;
            case "large"   : size = "38"; break;
            case "x-large" : size = "48"; break;
            case "xx-large": size = "66"; break;
            default: size = "14"; break;
        }

        var pos = rIcons[k].pos || "left";
        var indHtml = "<img class='cx-label-indicator indicator-" + pos + "' height='"+ size +"' width='"+ size +"' src='" + dashboard.getRootPath() + "images/resources/indicators/hd/" + rIcons[k].value + ".png'>";

        switch (pos) {
            case "right":
                $el.append(indHtml);
                break;
            case "left":
                $el.prepend(indHtml);
                break;
            case "replace":
                $el.empty().append(indHtml);
                break;
        }
    }

    return $el;

};
;/****** cx.map_link.js *******/ 

/*global Component:false,bindValues:false*/
Component.MapLink = $.klass(Component.Proxy, {

    displayName:"Link",
    factory_id:"map_link",
    isDeletable: false,

    xrefInParent: false,
    canSetAggregation:true,

    configure : function($super, config) {
        $super(config);
        bindValues(this, config, ["xrefInParent"]);
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this, ["xrefInParent"],
            {
                xrefInParent:false
            }, true);
        return cfg;
    },

    getPropertyModel : function($super) {
        var model = $super();

        model.push({
            type:"checkboxes",
            id:"xrefInParent",
            group:"xref",
            label:"Browser",
            options:[
                {
                    value:true,
                    label:"Open links in current tab/page/window",
                    checked:this.xrefInParent
                }
            ]
        });

        this.addDstProperties(model);

        return model;
    },

    getAvailableAggregations: function () {
        return [
            {value: "first", label: "First", "default": true},
            {value: "last", label: "Last"}
        ];
    }
});;/****** cx.map_marker.js *******/ 

/*global Component:false,bindValues:false, page:false, KlipFactory:false,isWorkspace:false*/
Component.MapMarker = $.klass(Component.Base, {

	displayName: "Markers",
	factory_id: "map_marker",
    isDeletable: false,
    isDraggable: false,

    xrefEnabled: false,
    canGroup : true,
    canSetAggregation : true,
    canFilter : true,
    canHaveDataSlots: true,

    /**
     * nothing should be rendered for this component so override the base method
     * @param $super
     */
	renderDom : function($super) {

	},

	getPropertyModel : function($super) {
		var model = $super();

        model.push({
            type:"checkboxes",
            id:"xrefEnabled",
            group:"xref",
            label:"Marker Links",
            options:[
                {
                    value:true,
                    label:"Enable marker hyperlinks",
                    checked:this.xrefEnabled
                }
            ]
        });
        this.addDataSlotProperty(model);

		return model;
	},

    getContextMenuModel : function(menuCtx, menuConfig) {

        var model = [];

        if (!menuCtx) {
            menuCtx = {};
        }

        this.addRenameContextMenuItem(model);

        this.addDataSlotContextMenuItem(model);

		menuCtx.groupBySubComponent = this.getChildByRole("marker_name");
        menuCtx.showSubComponentAggregation = true;
        menuCtx.showSubComponentFilter = true;

        this.addDstContextMenuItems(menuCtx,menuConfig,model);

        return model;
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this, ["xrefEnabled"],
            {
                xrefEnabled:false
            }, true);
        return cfg;
    },

	configure : function($super, config, update) {
		$super(config);
		bindValues(this, config, ["xrefEnabled"]);

        if ( isWorkspace() && this.xrefEnabled != undefined  ) {
            this.getChildByRole("marker_xref").visibleInWorkspace = this.xrefEnabled;
            page.updateMenu();
        }
    },

    afterFactory : function($super) {
        var requiredChildren = [
            {role:"marker_lat", props:{displayName:"Latitude", type:"proxy"}},
            {role:"marker_lng", props:{displayName:"Longitude", type:"proxy"}},
            {role:"marker_name", props:{displayName:"Name", type:"map_tooltip_prop", valueType: "name"}},
            {role:"marker_color", props:{displayName:"Color", type:"map_tooltip_prop", valueType:"color"}},
            {role:"marker_size", props:{displayName:"Size", type:"map_tooltip_prop", valueType:"size"}},
            {role:"marker_desc", props:{displayName:"Description", type:"map_tooltip_prop",valueType:"description"}},
            {role:"marker_xref", props:{displayName:"Link", type:"map_link"}}
        ];
        var i;
        var req;
        var cx;

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);
            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                if ( req.props._data ) cx.setData( req.props._data );
                this.addComponent(cx);
            }

            if (cx.factory_id == "proxy") {
                cx.isDeletable = false;
            }

            if ( isWorkspace() ) {
                if (req.role == "marker_name" || req.role == "marker_desc") {
                    cx.allowedFormats = ["txt", "num", "cur", "pct", "dat", "dur"];
                } else if (req.role == "marker_color" || req.role == "marker_size") {
                    cx.allowedFormats = ["txt", "num", "cur", "pct", "dat", "dur"];
                    cx.fmt = "num";
                } else if (req.role == "marker_xref") {
                    cx.visibleInWorkspace = this.xrefEnabled;
                }
            }
        }

        if ( isWorkspace() ) {
            page.updateMenu();
        }
    },

    /**
     * returns a schema that describes the controls for this component. is used
     * by the property editor
     *
     * schema = [
     *    { name:"" ,
     *      controls:[[ {type:"add", actionId:"", actionArgs:""},
     *                  {type:"remove", actionId:"", actionArgs:""} ]] }
     * ]
     *
     *
     */
    getWorkspaceControlModel : function($super) {
        var model = $super();
        this.addDataSlotControl(model);
        return model;
    }
});
;/****** cx.map_region.js *******/ 

/*global Component:false,bindValues:false, page:false, KlipFactory:false,isWorkspace:false*/
Component.MapRegion = $.klass(Component.Base, {

	displayName: "Regions",
	factory_id: "map_region",
    isDeletable: false,
    isDraggable: false,

    xrefEnabled: false,
    canGroup : true,
    canSetAggregation : true,
    canFilter : true,
    canHaveDataSlots: true,

    /**
     * nothing should be rendered for this component so override the base method
     * @param $super
     */
	renderDom : function($super) {

	},

	getPropertyModel : function($super) {
		var model = $super();

        model.push({
            type:"checkboxes",
            id:"xrefEnabled",
            group:"xref",
            label:"Region Links",
            options:[
                {
                    value:true,
                    label:"Enable region hyperlinks",
                    checked:this.xrefEnabled
                }
            ]
        });

        this.addDataSlotProperty(model);

		return model;
	},

    getContextMenuModel : function(menuCtx, menuConfig){

        var model = [];

        if (!menuCtx) {
            menuCtx = {};
        }

        this.addRenameContextMenuItem(model);

        this.addDataSlotContextMenuItem(model);

        menuCtx.groupBySubComponent = this.getChildByRole("region_id");
        menuCtx.showSubComponentAggregation = true;
        menuCtx.showSubComponentFilter = true;

        this.addDstContextMenuItems(menuCtx, menuConfig, model);

        return model;
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this, ["xrefEnabled"],
            {
                xrefEnabled:false
            }, true);
        return cfg;
    },

	configure : function($super, config, update) {
		$super(config);
		bindValues(this, config, ["xrefEnabled"]);

        if ( isWorkspace() && this.xrefEnabled != undefined  ) {
            this.getChildByRole("region_xref").visibleInWorkspace = this.xrefEnabled;
            page.updateMenu();
        }
    },

    afterFactory : function($super) {
        var requiredChildren = [
            {role:"region_id", props:{displayName:"ID", type:"proxy"}},
            {role:"region_color", props:{displayName:"Color", type:"map_tooltip_prop", valueType:"color"}},
            {role:"region_desc", props:{displayName:"Description", type:"map_tooltip_prop", valueType:"description"}},
            {role:"region_xref", props:{displayName:"Link", type:"map_link"}}
        ];
        var i;
        var req;
        var cx;

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);
            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                if ( req.props._data ) cx.setData( req.props._data );
                this.addComponent(cx);
            }

            if (cx.factory_id == "proxy") {
                cx.isDeletable = false;
            }

            if ( isWorkspace() ) {
                if (req.role == "region_desc") {
                    cx.allowedFormats = ["txt", "num", "cur", "pct", "dat", "dur"];
                } else if (req.role == "region_color") {
                    cx.allowedFormats = ["txt", "num", "cur", "pct", "dat", "dur"];
                    cx.fmt = "num";
                } else if (req.role == "region_xref") {
                    cx.visibleInWorkspace = this.xrefEnabled;
                }
            }
        }

        if ( isWorkspace() ) {
            page.updateMenu();
        }
    },

    /**
     * returns a schema that describes the controls for this component. is used
     * by the property editor
     *
     * schema = [
     *    { name:"" ,
     *      controls:[[ {type:"add", actionId:"", actionArgs:""},
     *                  {type:"remove", actionId:"", actionArgs:""} ]] }
     * ]
     *
     *
     */
    getWorkspaceControlModel : function($super) {
        var model = $super();
        this.addDataSlotControl(model);
        return model;
    }

});
;/****** cx.map_tooltip_property.js *******/ 

/*global Component:false,bindValues:false, isWorkspace:false, DX:false, Props:false */
Component.MapTooltipProperty = $.klass(Component.Proxy, {

    displayName:"Tooltip Property",
    factory_id:"map_tooltip_prop",
    isDeletable: false,

    valueType:"",    // color, size

    colorsEnabled: true,
    startColor: "cx-theme_blue_1",
    endColor: "cx-theme_blue_5",

    sizesEnabled: true,
    startSize: 5,
    endSize: 15,

    showValue: true,

    initialize : function($super, config) {
        $super(config);

        this.disableStyles = true;

        if ( isWorkspace() ) {
            this.labelMixin = new Component.LabelComponent();
        }
    },

    onParentAssignment: function() {
        var compRole = this.role;
            
        switch (compRole){
            case "region_desc":
            case "region_color":
            case "marker_color":
            case "marker_size":
            case "marker_desc":
                this.canSetAggregation = true;
                this.canFilter = true;
                break;
            case "marker_name" :
                this.canGroup = true;
                this.canSetAggregation = true;
                this.canFilter = true;
                break;
            default :
                break;
        }
    },

    configure : function($super, config) {
        $super(config);
        bindValues(this, config, ["valueType","showValue","colorsEnabled","startColor","endColor","sizesEnabled","startSize","endSize"]);

        if (config.startSize) this.startSize = parseInt(config.startSize);
        if (config.endSize) this.endSize = parseInt(config.endSize);
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this, ["valueType","showValue","colorsEnabled","startColor","endColor","sizesEnabled","startSize","endSize"],
            {
                valueType:"",
                showValue:true,
                colorsEnabled:true,
                startColor: "cx-theme_blue_1",
                endColor: "cx-theme_blue_5",
                sizesEnabled:true,
                startSize: 5,
                endSize: 15
            }, true);
        return cfg;
    },

    getPropertyModel : function($super) {
        var model = $super();

        if (this.valueType == "color") {
            model.push({
                type:"checkboxes",
                id:"colorsEnabled",
                group:"color",
                label:"Colorize",
                fieldMargin:"9px 0 0 0",
                options:[
                    {
                        value:true,
                        label:"Express values as colors in range",
                        checked:this.colorsEnabled
                    }
                ]
            });

            model.push({
                id:"startColor",
                group:"color",
                type:"color_picker",
                minorLabels: [
                    { label:"to", position:"right" }
                ],
                displayWhen: { colorsEnabled:true },
                fieldMargin:"0 0 9px 0",
                defaultColour: { rgb:"#a9ccff", theme:"cx-theme_blue_1" },
                valueType:"theme",
                selectedValue: this.startColor
            });

            model.push({
                id:"endColor",
                group:"color",
                type:"color_picker",
                displayWhen: { colorsEnabled:true },
                fieldMargin:"0 0 9px 10px",
                defaultColour: { rgb:"#3862bb", theme:"cx-theme_blue_5" },
                valueType:"theme",
                selectedValue: this.endColor,
                flow:true
            });

        }

        if (this.valueType == "size") {
            model.push({
                type:"checkboxes",
                id:"sizesEnabled",
                group:"size",
                label:"Resize",
                fieldMargin:"9px 0 0 0",
                options:[
                    {
                        value:true,
                        label:"Express values as marker size in range",
                        checked:this.sizesEnabled
                    }
                ]
            });

            model.push({
                id:"startSize",
                group:"size",
                type:"text",
                minorLabels: [
                    { label:"to", position:"right" }
                ],
                displayWhen: { sizesEnabled:true },
                width:"38px",
                fieldMargin:"0 0 9px 0",
                isNumeric:true,
                min:0,
                value: this.startSize
            });

            model.push({
                id:"endSize",
                group:"size",
                type:"text",
                minorLabels: [
                    { label:"px", position:"right", css:"quiet italics" }
                ],
                displayWhen: { sizesEnabled:true },
                width:"38px",
                fieldMargin:"0 0 9px 10px",
                isNumeric:true,
                min:0,
                value: this.endSize,
                flow:true
            });

        }

        model.push({
            type:"checkboxes",
            id:"showValue",
            group:"tooltips",
            label:"Tooltips",
            options:[
                {
                    value:true,
                    label:"Show value in tooltips",
                    checked:this.showValue
                }
            ]
        });

        model = $.merge(model, _.bind(this.labelMixin.getPropertyModel, this)());
        return model;
    },

    getPropertyModelGroups : function() {
        return [
            "fmt",
            "dst-group",
            "dst-sort",
            "dst-filter",
            "color",
            "size",
            "tooltips",
            "content"
        ];
    },

    // Overridden function to have custom aggregation rule for Markers sub-components
    getAvailableAggregations: function ($super) {
        var modelType = this.getVirtualColumnType();
        var aggregations = $super();
        var parentId = this.parent.factory_id;

        var numericTextDateAggregations = Props.Defaults.numericTextAggregations;

        var textDateAggregations = [
            {value: "count", label: "Count"},
            {value: "countdistinct", label: "Count Distinct"},
            {value: "first", label: "First", "default": true},
            {value: "last", label: "Last"}
        ];

        if (modelType != DX.ModelType.NUMBER) {
            switch (parentId) {
                case "map_marker":
                    switch (this.role) {
                        case "marker_size":
                        case "marker_color":
                            aggregations = numericTextDateAggregations;
                            break;
                        case "marker_desc":
                        case "marker_name":
                            aggregations = textDateAggregations;
                            break;
                        default:
                            break;
                    }
                    break;

                case "map_region":
                    switch (this.role) {
                        case "region_color":
                            aggregations = numericTextDateAggregations;
                            break;
                        case "region_desc":
                            aggregations = textDateAggregations;
                            break;
                        default:
                            break;
                    }
                    break;
                default:
                    break;
            }
        }

        return aggregations;
    }

});;/****** cx.metric_view.js *******/ 

/* global Component:false, bindValues:false, CXTheme:false */

Component.MetricView = $.klass(Component.Base, {

	displayName: "Metric View",
	factory_id:"metric_view",

    $div: false,

    height: "300px",
    visOptions: {},
    metricId: "",
	visOptionsPopup: null,
	aboutMetricDialog: null,

	renderDom : function($super) {
		var dom = $super();
        this.$div = $("<div style='overflow:hidden; position:relative; height: 100%'>").addClass("metric_view_container");
		this.$propsDiv = $("<div>");
		this.$aboutDiv = $("<div>");

        dom.append(this.$div);
		dom.append(this.$propsDiv);
		dom.append(this.$aboutDiv);

        this.changeHeight();

        this.renderVis();
    },

	renderVis : function() {
		if (this.metricId) {
			require(["js_root/build/vendor.packed"], function (Vendor) { //Ensure vendor bundle is loaded first
				require(["js_root/build/metrics_lib.packed"], function (MetricsLib) {
					var theme = this._isDarkTheme() ? MetricsLib.themes.DARK : MetricsLib.themes.LIGHT;
					MetricsLib.renderVis(this.$div[0], this.metricId, this._getVisOptions(), !this._shouldAnimate(), theme, this.el.width(), this.el.height());
					if (!this.visOptionsPopup) {
						this.visOptionsPopup = MetricsLib.createVisPropsPopup(this.$propsDiv[0], this.metricId, this.getTitle(), this._onTitleChange.bind(this), this._onVisOptionsChange.bind(this), this.$div[0], theme, this._onPopupClose.bind(this));
					}
					if (!this.aboutMetricDialog) {
						const isMobile = window.matchMedia("(max-width: 650px)").matches;
						this.aboutMetricDialog = MetricsLib.createAboutDialog(this.$aboutDiv[0], this.metricId, theme, isMobile, this.getTitle());
					}
				}.bind(this));
			}.bind(this));
		}
	},

	getTitle : function () {
		var klip = this.getKlip();
		if (klip) {
			return klip.title;
		}
	},

	configureVisOptions : function() {
		if (this.visOptionsPopup) {
			this.visOptionsPopup.openPropsPanel(this._getVisOptions());
		}
	},

	aboutMetric : function() {
		if (this.aboutMetricDialog) {
			this.aboutMetricDialog.openAboutDialog(this.getTitle());
		}
	},

	_onVisOptionsChange : function(visOptions) {
		this.setOptions(visOptions);
		this.renderVis();
	},

	_onTitleChange : function(title) {
		var klip = this.getKlip();
		if (klip) {
			klip.setTitle(title);
		}
	},

	_onPopupClose: function() {
		this.persistChanges();
	},

    _getVisOptions : function() {
		if (this.visOptions && this.visOptions !== "undefined" && Object.keys(this.visOptions).length) {
			return this.visOptions;
		}

		return undefined;
    },

	_shouldAnimate : function() {
		return !this.getDashboard().isImaging();
	},

	_isDarkTheme : function(metricsLib) {
		return (CXTheme.getCurrentThemeName() === "dark");
	},

	initializeForWorkspace : function() {
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(500);
        }
    },

	onResize : function($super) {
		$super();
        this.invalidateAndQueue();
        this.renderVis();
	},

	update : function($super) {
        var klip = this.getKlip();
        $super();

        // exit early optimizations
        // ------------------------
        if ( !klip.isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

        this.setInvalid(false, true);
    },

    changeHeight : function() {
		var newHeight;
        if (!this.$div) return;

        if (this.height) {
            this.setHeight(this.height);
        } else {
            newHeight = Component.MetricView.heights[this.height-1];
            this.setHeight(newHeight);
        }
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this,["height", "visOptions", "metricId"],
            {
                height: "auto",
                visOptions: {},
                metricId: ""
            }, true);
        return cfg;
    },

    configure : function($super, config) {
        $super(config);
        bindValues(this, config,["height", "visOptions", "metricId"]);

		if (config.visOptions) {
			this.setOptions(config.visOptions);
		}

        this.invalidate();
    },

    setOptions: function (visOptions) {
		this.visOptions = (visOptions !== "undefined" && visOptions.length) ? JSON.parse(visOptions) : visOptions;
    },

    destroy : function ($super) {
        $super();
        clearInterval(this.intervalId);
    },

	getPropertyModel  : function($super) {
		var model = $super();

		model.push({
			type:"text",
			id:"metricId",
			label:"Metric ID",
			value: this.metricId,
			group:"default",
			isNumeric:false,
			readOnly:false,
			min:1,
			width: 300
		});

		model.push({
			type:"textarea",
			id:"visOptions",
			label:"Vis Options",
			value: JSON.stringify(this.visOptions),
			group:"default",
			height:"150px",
			width:"100%"
		});

		return model;
	}
});

Component.MetricView.heights = [40, 160, 320, 640];
;/****** cx.mini_bar.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, isWorkspace:false, page:false, CXTheme:false, FMT:false, _sanitizeNumbers:false, G_vmlCanvasManager:false
    bindValues:false, Props:false, KlipFactory:false, dashboard:false */

Component.MiniBar = $.klass(Component.Base, {

    displayName: "Bullet Chart",
    factory_id:"mini_bar",

    $sparkline : false,
    $textIndicator: false,
    size: "large",
    customSize:"",

    chartType:"bullet",

    bulletTarget:"10",
    bulletRanges:"12,9,7",
    targetColor:"cx-theme_444",
    performanceColor:"cx-theme_666",
    rangeColors:[],

    chartRangeOption:"auto",
    minCustomVal:"",
    maxCustomVal:"",
    hideRange:false,
    posBarColor:"cx-theme_blue_3",
    negBarColor:"cx-theme_red_3",
    horizRangeColor:"cx-theme_ccc",

    iconWidth: 0,
    padding: 10,

    defaultArgs : {
        tooltipOffsetX:20,
        tooltipOffsetY:20,
        tooltipFormat: "{{fieldkey:fieldNames}}: {{value}}",
        tooltipValueLookups: {
            fieldNames: { p:"Performance", r:"Range", t:"Target" }
        }
    },

    sizes : {
        "xx-small" :    { height:14, indSize:14, zeroWidth:2, padding:"7px 10px 8px 10px" },
        "x-small"  :    { height:17, indSize:18, zeroWidth:3, padding:"8px 10px 8px 10px" },
        "small"    :    { height:20, indSize:22, zeroWidth:5, padding:"13px 10px 10px 10px" },
        "medium"   :    { height:24, indSize:26, zeroWidth:5, padding:"19px 10px 14px 10px" },
        "large"    :    { height:32, indSize:38, zeroWidth:5, padding:"19px 10px 14px 10px" },
        "x-large"  :    { height:44, indSize:48, zeroWidth:5, padding:"19px 10px 14px 10px" },
        "xx-large" :    { height:60, indSize:66, zeroWidth:5, padding:"19px 10px 14px 10px" },
        cust : { height:38, indSize:50, zeroWidth:5, padding:"13px 10px 10px 10px" }
    },


    initialize : function($super) {
        $super();
        this.labelMixin = new Component.LabelComponent();
        this.disableStyles = true;
        this.allowedFormats = ["num","cur","pct","dur"];
        this.fmt = "num";

        this.rangeColors = ["cx-theme_eee", "cx-theme_ccc", "cx-theme_aaa"];
    },

    initializeForWorkspace : function($super, workspace) {
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(500);
        }
    },

    renderDom : function($super) {
        var dom = $super();
        this.$sparkline = $("<div>").css({display:"inline-block", "vertical-align":"top"});
        this.$textIndicator = $("<div>").hide();

        dom.append(this.$sparkline).append(this.$textIndicator);

        var _this = this;
        this.$sparkline.bind("sparklineClick", function(evt){
            if(isWorkspace()) {
                page.invokeAction("select_component", _this);
                return false;
            }
        });
    },

    onResize : function($super) {
        $super();

        this.invalidateAndQueue();
    },

    onEvent : function ($super, evt) {
        $super(evt);

        if (evt.id == "theme_changed") {
            this.invalidate();
        }
    },

    onChildrenUpdated : function($super) {
        this.update();
    },

    /**
     * gets the preferred widths/heights of managed components
     */
    updateComponentBounds : function() {
        this.chartHeight = (this.size == "cust" && this.customSize != "" ? this.customSize : this.sizes[this.size].height);
        this.canvasHeight = (this.size == "cust" && this.customSize != "" ? this.customSize : this.sizes[this.size].height);

        if (!this.displayPerformance) {
            this.chartWidth = this.el.width() - this.padding * 2 - this.iconWidth;
        } else {
            this.performance.el.css("width", "auto");

            var lastValWidth = this.performance.el.outerWidth(true);
            var lastValHeight = this.performance.el.outerHeight();

            if (this.chartHeight < lastValHeight) {
                this.chartHeight = lastValHeight;
            }

            var availWidth = this.el.width() - this.padding * 3 - this.iconWidth;
            if (lastValWidth > availWidth * 0.8) {
                var lastPadding = lastValWidth - this.performance.el.width();

                this.performance.el.width(availWidth * 0.8 - lastPadding);
                this.performance.elLabel.addClass("ellipsis");

                lastValWidth = this.performance.el.outerWidth(true);
            } else {
                this.performance.elLabel.removeClass("ellipsis");
            }

            this.chartWidth = this.el.width() - lastValWidth - this.padding * 3 - this.iconWidth;
        }
    },

    update : function($super) {
        $super();

        // exit early optimizations
        // ------------------------
        if ( !this.getKlip().isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

        // handle chart reactions
        // ----------------------
        var reactionContext = { icons:[], bgColor:false, text:false };
        this.collectReactions(reactionContext);

        var compReplaced = this.applyIconReactions(reactionContext.icons);

        // apply background color reaction
        if (reactionContext.bgColor) {
            if (reactionContext.bgColor.indexOf("#") != -1) {
                this.el.css("background-color", reactionContext.bgColor);
            } else {
                this.el.addClass(reactionContext.bgColor);
            }
        }

        // apply text reaction
        if (reactionContext.text || compReplaced) {
            this.displayLastValue = false;
            this.$sparkline.hide();
            this.performance.el.hide();

            if (!compReplaced) {
                this.$textIndicator.removeClass()
                    .addClass("text-indicator size-"+ (this.size == "cust" ? "x-large" : this.size))
                    .css("display", "inline-block")
                    .text(reactionContext.text);
            } else {
                this.$textIndicator.hide();
            }
        } else {
            this.$sparkline.show();
            this.$textIndicator.hide();

            var performance_data = this.performance.getData(0);
            this.displayPerformance = (this.performance.show_value && performance_data && performance_data.length > 0);

            if (this.displayPerformance) {
                this.performance.el.show();
                this.performance.elLabel.css("padding", "0");
            } else {
                this.performance.el.hide();
            }

            this.updateComponentBounds();

            var chartPadding = this.sizes[this.size].padding;

            this.$sparkline.empty().css("padding", chartPadding);
            this.$sparkline.height(this.chartHeight);
            this.$sparkline.width(this.chartWidth);

            this.renderBar(performance_data);

            // position chart and label
            // ------------------------
            var topSparkPad = 0;
            if (this.displayPerformance) {
                var performanceHeight = this.performance.el.outerHeight();

                this.performance.el.css("margin-top", (this.el.height() - performanceHeight) / 2);

                topSparkPad = performanceHeight / 2 - this.$sparkline.find("canvas").outerHeight(true) / 2;
            }

            this.$sparkline.find("canvas").css("padding-top", topSparkPad + "px");
        }

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        // position icons
        var _this = this;
        _.each(this.el.find(".icon-indicator"), function(icon) {
            var height = $(icon).outerHeight();
            $(icon).css("margin-top", (_this.el.height() - height) / 2);
        });

        this.setInvalid(false,true);

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    renderBar : function(performanceData) {
        // handle reactions
        // ----------------
        var targetReaction = false;
        var performanceReaction = false;
        var barReaction = false;
        var rangeReaction = false;
        _.each(this.reactions, function(r) {
            if (_.max(r.mask)) {
                _.each(r.reactions, function(re) {
                    var val = re.value;

                    switch (re.type) {
                        case "target-color":
                            targetReaction = CXTheme.getThemeColour(val);
                            break;
                        case "performance-color":
                            performanceReaction = CXTheme.getThemeColour(val);
                            break;
                        case "bar-color":
                            barReaction = CXTheme.getThemeColour(val);
                            break;
                        case "range-color":
                            rangeReaction = CXTheme.getThemeColour(val);
                            break;
                    }
                });
            }
        });


        var performance = FMT.convertToNumber(performanceData[0]);

        if (this.chartType == "bullet") {
            var target = FMT.convertToNumber(this.bulletTarget != "" ? this.bulletTarget : performance * 1.3);

            var ranges = [];
            if (this.bulletRanges != "") {
                ranges = this.bulletRanges.split(",");
                _sanitizeNumbers(ranges);

                ranges.sort(function(a,b) {
                    return b-a;
                });
            }

            var data = [target, performance].concat(ranges);

            var args = $.extend({}, this.defaultArgs);

            args.type = this.chartType;
            args.width = this.chartWidth;
            args.height = this.canvasHeight;

            args.targetColor = targetReaction ? targetReaction : CXTheme.getThemeColour(this.targetColor);
            args.performanceColor = performanceReaction ? performanceReaction : CXTheme.getThemeColour(this.performanceColor);

            // convert theme colours to actual colours
            args.rangeColors = this.rangeColors.slice(0);
            for (var i = 0; i < args.rangeColors.length; i++) {
                var colour = args.rangeColors[i];

                args.rangeColors[i] = CXTheme.getThemeColour(colour);
            }

            var _this = this;
            args.numberFormatter = function(val) {
                return FMT.format(_this.fmt, [val], _this.getFmtArgs());
            };

            // HACK: This is a hack to fix very wide tooltips when display:flex on body
            var $dashboard = $(".c-dashboard");
            if (KF.company.hasFeature("one_product_ui") && $dashboard.length > 0) {
                args.tooltipContainer = $dashboard;
            }

            this.$sparkline.sparkline(data, args);
        } else if (this.chartType == "horiz") {
            var $canvas = $("<canvas>")
                .text("your browser does not support the canvas tag")
                .css({
                    display: "inline-block",
                    width: this.chartWidth + "px",
                    height: this.canvasHeight + "px",
                    "vertical-align": "top"
                });
            this.$sparkline.append($canvas);
            var canvas = $($canvas)[0];
            if ( window.G_vmlCanvasManager != undefined ) G_vmlCanvasManager.initElement(canvas);

            canvas.width = this.chartWidth;
            canvas.height = this.canvasHeight;

            var ctx = canvas.getContext("2d");

            var min = (performance < 0 ? performance * 1.3 : 0);
            if (this.chartRangeOption == "cust" && this.minCustomVal != "") min = FMT.convertToNumber(this.minCustomVal);

            var max = (performance < 0 ? 0 : performance * 1.3);
            if (this.chartRangeOption == "cust" && this.maxCustomVal != "") max = FMT.convertToNumber(this.maxCustomVal);

            var zeroWidth = (this.sizes[this.size] ? this.sizes[this.size].zeroWidth : 5);
            var origin = {x:0, y:zeroWidth};

            // range
            if (!this.hideRange) {
                ctx.globalAlpha = 1;
                ctx.fillStyle = rangeReaction ? rangeReaction : CXTheme.getThemeColour(this.horizRangeColor);
                ctx.lineWidth = 1;

                ctx.fillRect(origin.x, origin.y, this.chartWidth, this.canvasHeight - 2 * zeroWidth);
            }

            // performance
            if (performance != null) {
                ctx.globalAlpha = 1;
                ctx.fillStyle = barReaction ? barReaction : CXTheme.getThemeColour(performance < 0 ? this.negBarColor : this.posBarColor);
                ctx.lineWidth = 1;

                var startPos = this.valueToPosition(this.chartWidth , Math.max(0, min), min, max);
                var endPos = this.valueToPosition(this.chartWidth, Math.min(performance, max), min, max);

                ctx.fillRect(origin.x + startPos, origin.y, endPos - startPos, this.canvasHeight - 2 * zeroWidth);
            }

            // zero bar
            if (performance < 0 || min < 0) {
                ctx.globalAlpha = 1;
                ctx.strokeStyle = CXTheme.current.cx_mini_series_zero;
                ctx.lineWidth = zeroWidth;

                var zeroPos = this.valueToPosition(this.chartWidth, 0, min, max);
                if (zeroPos == this.chartWidth) zeroPos -= zeroWidth / 2;

                ctx.beginPath();
                ctx.moveTo(origin.x + zeroPos, 0);
                ctx.lineTo(origin.x + zeroPos, this.canvasHeight);
                ctx.stroke();
            }
        }
    },

    /**
     * Determine the position of the val along the given length
     *
     * @param length
     * @param val
     * @param min
     * @param max
     * @return {Number}
     */
    valueToPosition : function(length, val, min, max) {
        return length * ((val - min) / (max - min));
    },

    applyIconReactions : function(icons) {
        this.el.find(".icon-indicator").remove();
        this.iconWidth = 0;

        var replaced = false;
        var i = 0;
        // apply the reaction icons
        while (!replaced && i < icons.length) {
            var size = this.sizes[this.size].indSize;

            var pos = icons[i].pos || "left";
            var indHtml = "<img class='icon-indicator indicator-" + pos + "' height='"+ size +"' width='"+ size +"' src='" + dashboard.getRootPath() + "images/resources/indicators/hd/" + icons[i].value + ".png'>";
            if (pos == "right") {
                this.el.append(indHtml);
            } else if (pos == "left") {
                this.el.prepend(indHtml);
            } else if (pos == "replace") {
                replaced = true;
                this.el.find(".icon-indicator").remove();
                this.iconWidth = 0;
                this.el.append(indHtml);
            }

            this.iconWidth += size + this.padding;

            i++;
        }

        return replaced;
    },


    configure : function($super, config) {
        $super(config);
        bindValues(this, config, ["size","customSize","chartType","bulletTarget","bulletRanges","targetColor","performanceColor","rangeColors","chartRangeOption","minCustomVal","maxCustomVal","hideRange","posBarColor","negBarColor","horizRangeColor"]);

        this.invalidate();

        if (config["~propertyChanged"]) {
            for (var prop in config) {
                if (prop.indexOf("range_color-") != -1) {
                    var index = prop.match(/\d+/);
                    this.rangeColors[parseInt(index[0])] = config[prop];
                }
            }
        }
    },

    serialize : function($super) {
        if (this.chartType != "bullet") {
            this.targetColor = "cx-theme_444";
            this.performanceColor = "cx-theme_666";
        }

        if (this.chartType != "horiz") {
            this.chartRangeOption = "auto";
            this.minCustomVal = "";
            this.maxCustomVal = "";
            this.hideRange = false;
            this.posBarColor = "cx-theme_blue_3";
            this.negBarColor = "cx-theme_red_3";
            this.horizRangeColor = "cx-theme_ccc";
        }

        var config = $super();
        bindValues(config, this, ["size","customSize","chartType","bulletTarget","bulletRanges","targetColor","performanceColor","rangeColors","chartRangeOption","minCustomVal","maxCustomVal","hideRange","posBarColor","negBarColor","horizRangeColor"],
            {
                size:3,
                customSize:"",
                chartType:"bullet",
                bulletTarget:"",
                bulletRanges:"",
                targetColor:"cx-theme_444",
                performanceColor:"cx-theme_666",
                chartRangeOption:"auto",
                minCustomVal:"",
                maxCustomVal:"",
                hideRange:false,
                posBarColor:"cx-theme_blue_3",
                negBarColor:"cx-theme_red_3",
                horizRangeColor:"cx-theme_ccc"
            }, true);

        return config;
    },


    getPropertyModel : function($super) {
        var model = $super();
        var labelModel = _.bind(this.labelMixin.getPropertyModel, this)();

        model.push({
            type:"select",
            id:"size",
            label:"Height",
            group:"size",
            options: Props.Defaults.size.concat({value:"cust", label:"Custom..."}),
            selectedValue: this.size
        });

        model.push({
            type:"text",
            id:"customSize",
            group:"size",
            displayWhen: { size:"cust" },
            width:"50px",
            value: this.customSize,
            flow:true
        });

        model.push({
            type:"button_group_ex" ,
            id:"chartType",
            label:"Type" ,
            group:"type",
            options: Props.Defaults.miniChartTypes.slice(4),
            selectedValue: this.chartType,
            help: {
                link: "klips/mini-properties"
            }
        });


        // Bullet options
        model.push({
            type:"text",
            id:"bulletTarget",
            label:"Target",
            group:"bullet_options",
            displayWhen: { chartType:"bullet" },
            value:this.bulletTarget
        });

        model.push({
            type:"text",
            id:"bulletRanges",
            label:"Ranges",
            group:"bullet_options",
            displayWhen: { chartType:"bullet" },
            value:this.bulletRanges
        });

        // Horizontal options
        model.push({
            type:"select",
            id:"chartRangeOption",
            label:"Range",
            group:"horiz_options",
            displayWhen: { chartType:"horiz" },
            options: [{value:"auto", label:"Automatic"}, {value:"cust", label:"Custom..."}],
            selectedValue: this.chartRangeOption
        });

        model.push({
            type:"text",
            id:"minCustomVal",
            minorLabels: [ {label:"to", position:"right"} ],
            group:"horiz_options",
            displayWhen: { chartType:"horiz", chartRangeOption:"cust" },
            width:"40px",
            value: this.minCustomVal,
            flow:true
        });

        model.push({
            type:"text",
            id:"maxCustomVal",
            group:"horiz_options",
            displayWhen: { chartType:"horiz", chartRangeOption:"cust" },
            width:"40px",
            value: this.maxCustomVal,
            flow:true
        });

        model.push({
            type:"checkboxes",
            id:"hideRange",
            group:"horiz_options",
            displayWhen: { chartType:"horiz" },
            options: [
                {value:true,label:"Hide range background", checked: this.hideRange}
            ]
        });


        // Bullet colours
        model.push({
            type:"color_picker",
            id:"targetColor",
            group:"bullet_colour",
            label:"Colors",
            minorLabels: [ {label:"Target", position:"right"} ],
            displayWhen: { chartType:"bullet" },
            valueType:"theme",
            defaultColour: {rgb:"#444444", theme:"cx-theme_444"},
            selectedValue:this.targetColor
        });

        model.push({
            type:"color_picker",
            id:"performanceColor",
            group:"bullet_colour",
            minorLabels: [ {label:"Performance", position:"right"} ],
            displayWhen: { chartType:"bullet" },
            valueType:"theme",
            defaultColour: {rgb:"#666666", theme:"cx-theme_666"},
            selectedValue:this.performanceColor
        });

        model.push({
            type:"markup",
            id:"msg_range_color",
            group:"bullet_colour",
            displayWhen: { chartType:"bullet" },
            el:$("<span style='color:#333333;font-size:1.1em;'>Background ranges</span>")
        });

        for (var i = 0; i < this.rangeColors.length; i++) {
            var colorProp = {
                type: "color_picker",
                id: "range_color-" + i,
                group: "bullet_colour",
                displayWhen: { chartType:"bullet" },
                valueType:"theme",
                selectedValue: this.rangeColors[i]
            };

            if (i % 7 != 0) {
                colorProp.flow = { padding:"3px" };
            } else {
                colorProp.breakFlow = true;
                colorProp.fieldMargin = "0";
            }

            model.push(colorProp);
        }

        var _this = this;
        model.push({
            type:"button",
            id:"add_range_color",
            group:"bullet_colour",
            displayWhen: { chartType:"bullet" },
            text:"+",
            fieldMargin:"0",
            breakFlow: true,
            onClick: function(evt) {
                _this.rangeColors.push("cx-theme_fff");
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(),page.componentPropertyForm);
                _this.update();
            },
            css:"add"
        });

        model.push({
            type:"button",
            id:"remove_range_color",
            group:"bullet_colour",
            displayWhen: { chartType:"bullet" },
            text:"-",
            flow:{ padding:"0px" },
            onClick: function(evt) {
                _this.rangeColors.pop();
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                _this.update();
            },
            css:"remove"
        });

        // Horizontal colours
        model.push({
            type:"color_picker",
            id:"posBarColor",
            group:"horiz_colour",
            label:"Colors",
            minorLabels: [ {label:"Positive", position:"right"} ],
            displayWhen: { chartType:"horiz" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.posBarColor
        });

        model.push({
            type:"color_picker",
            id:"negBarColor",
            group:"horiz_colour",
            minorLabels: [ {label:"Negative", position:"right"} ],
            displayWhen: { chartType:"horiz" },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.negBarColor
        });

        model.push({
            type:"color_picker",
            id:"horizRangeColor",
            group:"horiz_colour",
            minorLabels: [ {label:"Range", position:"right"} ],
            displayWhen: { chartType:"horiz" },
            valueType:"theme",
            defaultColour: {rgb:"#cccccc", theme:"cx-theme_ccc"},
            selectedValue:this.horizRangeColor
        });


        _.each(labelModel, function(prop) {
            if (prop.id == "fmt") {
                prop.label = "Tooltip Format";
            }

            if (!prop.displayWhen) prop.displayWhen = {};
            prop.displayWhen["chartType"] = "bullet";
        });

        model = $.merge(model, labelModel);

        return model;
    },


    getSupportedReactions : function($super) {
        var reactions = {};

        if (this.chartType == "bullet") {
            reactions = { "target-color":true, "performance-color":true };
        } else if (this.chartType == "horiz") {
            reactions = { "bar-color":true, "range-color":true };
        }

        reactions["icon"] = true;
        reactions["background-color"] = true;
        reactions["text"] = true;

        return reactions;
    },

    /**
     *
     * @param idx
     * @param reaction
     * @param ctx - {
     *      icons: [],
     *      bgColor: false,
     *      text: false
     * }
     */
    handleReaction : function( idx, reaction, ctx ) {
        switch (reaction.type) {
            case "icon":
                ctx.icons.push(reaction);
                break;
            case "background-color":
                ctx.bgColor = reaction.value;
                break;
            case "text":
                ctx.text = reaction.value;
                break;
        }
    },


    afterFactory : function() {
        var requiredChildren = [
            {role:"performance", props:{displayName:"Performance",type:"label",size:3,show_value:true,
                                formulas: [{"txt":"0","src":{"t":"expr","v":false,"c":[{"t":"l","v":"0"}]}}],
                                data: [[0]], fmt:"num"}}
        ];

        for (var i = 0; i < requiredChildren.length; i++) {
            var req = requiredChildren[i];
            if (!this.getChildByRole(req.role)) {
                var cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                this.addComponent(cx);
            }
        }

        this.performance = this.getChildByRole("performance");
        this.performance.allowedFormats = ["num","cur","pct","dur"];
        if (this.performance.fmt == "txt") this.performance.fmt = "num";
        this.performance.isDeletable = false;
        this.performance.isDraggable = false;
        this.performance.el.css({display:"inline-block"}).appendTo(this.el);
    }

});
;/****** cx.mini_data.js *******/ 

/*global Component:false, isWorkspace:false, CXTheme:false, bindValues:false, Props:false, page:false, DX:false */
Component.MiniData = $.klass(Component.Proxy, {

    displayName: "Series",
    factory_id:"mini_data",
    isMovable: true,
    seriesId: 1,

    chartType: "line",

    chartRangeOption: "auto",
    minCustomVal: "",
    maxCustomVal: "",

    lineFormat: "la",
    lineWeight: "auto",
    pointSize: "auto",
    includeHighLowPoints: false,
    lineColor: "cx-theme_blue_3",
    spotColor: "cx-theme_blue_3",
    fillColor: CXTheme.current.cx_sparkline_area,
    minSpotColor: "cx-theme_red_3",
    maxSpotColor: "cx-theme_red_3",

    posBarColor: "cx-theme_blue_3",
    zeroBarColor: "cx-theme_aaa",
    negBarColor: "cx-theme_red_3",

    winThreshold: "",
    winColor: "cx-theme_blue_3",
    drawColor: "cx-theme_aaa",
    lossColor: "cx-theme_red_3",

    barHeight: "auto",
    customBarHeight: "",
    discreteThreshold: "",
    discreteBarColor: "cx-theme_blue_3",
    discreteThresholdColor: "cx-theme_red_3",
    canHaveDataSlots: true,

    initialize : function($super, config) {
        $super(config);

        this.disableStyles = true;

        if ( isWorkspace() ) {
            this.labelMixin = new Component.LabelComponent();
            this.allowedFormats = ["txt","num","cur","pct","dat","dur"];
            this.fmt = "num";
        }
    },

    checkDeletable : function($super, ctx) {
        var isDeletable = $super();
        var siblings;

        if (isDeletable) {
            siblings = this.parent.getChildrenByType("mini_data");
            isDeletable = (siblings.length > 1);
        }

        return isDeletable;
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Series",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"mini_data", role:"series", sibling:this}},
                    {type:"remove", disabled:!this.checkDeletable(), actionId:"remove_component", actionArgs:{cx:this}}
                ]
            ]
        });

        this.addDataSlotControl(model);

        return model;
    },

    getSupportedReactions: function($super) {
        var reactions = false;

        switch (this.chartType) {
            case "line":
                reactions = { points:true, "point-color":true, "line-color":true, "end-color":true };

                if (this.lineFormat == "la") reactions["area-color"] = true;

                if (this.includeHighLowPoints) {
                    reactions["low-color"] = true;
                    reactions["high-color"] = true;
                }

                break;
            case "bar":
            case "tristate":
                reactions = { "bar-color":true };
                break;
            case "discrete":
                break;
        }

        return reactions;
    },

    /**
     *
     * @param idx
     * @param reaction
     * @param ctx - {
     *      barColours: {},
     *      pointColours: {}
     * }
     */
    handleReaction : function( idx, reaction, ctx ) {
        switch (this.chartType) {
            case "line":
                switch (reaction.type) {
                    case "points":
                        ctx.pointColours[ idx ] = CXTheme.getThemeColour(this.spotColor);
                        break;
                    case "point-color":
                        ctx.pointColours[ idx ] = CXTheme.getThemeColour(reaction.value);
                        break;
                }
                break;
            case "bar":
            case "tristate":
                switch (reaction.type) {
                    case "bar-color":
                        ctx.barColours[ idx ] = CXTheme.getThemeColour(reaction.value);
                        break;
                }
                break;
            case "discrete":
                break;
        }
    },

    serialize : function($super) {
        var cfg = $super();

        if (this.chartType != "line") {
            this.lineFormat = "la";
            this.lineWeight = "auto";
            this.pointSize = "auto";
            this.includeHighLowPoints = false;
            this.lineColor = "cx-theme_blue_3";
            this.spotColor = "cx-theme_blue_3";
            this.fillColor = CXTheme.current.cx_sparkline_area;
            this.minSpotColor = "cx-theme_red_3";
            this.maxSpotColor = "cx-theme_red_3";
        }

        if (this.chartType != "bar") {
            this.posBarColor = "cx-theme_blue_3";
            this.zeroBarColor = "cx-theme_aaa";
            this.negBarColor = "cx-theme_red_3";
        }

        if (this.chartType != "tristate") {
            this.winThreshold = "";
            this.winColor = "cx-theme_blue_3";
            this.drawColor = "cx-theme_aaa";
            this.lossColor = "cx-theme_red_3";
        } else {
            this.chartRangeOption = "auto";
            this.minCustomVal = "";
            this.maxCustomVal = "";
        }

        if (this.chartType != "discrete") {
            this.barHeight = "auto";
            this.customBarHeight = "";
            this.discreteThreshold = "";
            this.discreteBarColor = "cx-theme_blue_3";
            this.discreteThresholdColor = "cx-theme_red_3";
        }

        bindValues(cfg, this, ["seriesId","chartRangeOption","minCustomVal","maxCustomVal","chartType","lineFormat","lineWeight","pointSize","includeHighLowPoints","lineColor","spotColor","fillColor","minSpotColor","maxSpotColor","posBarColor","zeroBarColor","negBarColor","winColor","drawColor","lossColor","winThreshold","barHeight","customBarHeight","discreteThreshold","discreteBarColor","discreteThresholdColor"],
            {
                chartType:"line",
                chartRangeOption:"auto",
                minCustomVal:"",
                maxCustomVal:"",
                lineFormat:"la",
                lineWeight:"auto",
                pointSize:"auto",
                includeHighLowPoints:false,
                lineColor:"cx-theme_blue_3",
                spotColor:"cx-theme_blue_3",
                fillColor:CXTheme.current.cx_sparkline_area,
                minSpotColor:"cx-theme_red_3",
                maxSpotColor:"cx-theme_red_3",
                posBarColor:"cx-theme_blue_3",
                zeroBarColor:"cx-theme_aaa",
                negBarColor:"cx-theme_red_3",
                winThreshold:"",
                winColor:"cx-theme_blue_3",
                drawColor:"cx-theme_aaa",
                lossColor:"cx-theme_red_3",
                barHeight:"auto",
                customBarHeight:"",
                discreteThreshold:"",
                discreteBarColor:"cx-theme_blue_3",
                discreteThresholdColor:"cx-theme_red_3"
            }, true);
        return cfg;
    },

    configure : function($super, config, update) {
        var displayTabs = ["data", "properties"];

        $super(config);
        bindValues(this,config,["seriesId","chartRangeOption","minCustomVal","maxCustomVal","chartType","lineFormat","lineWeight","pointSize","includeHighLowPoints","lineColor","spotColor","fillColor","minSpotColor","maxSpotColor","posBarColor","zeroBarColor","negBarColor","winColor","drawColor","lossColor","winThreshold","barHeight","customBarHeight","discreteThreshold","discreteBarColor","discreteThresholdColor"]);

        if (config.seriesId && !this.isRenamed) {
            this.displayName = "Series " + config.seriesId;
            if (config["~propertyChanged"] && isWorkspace()) page.updateMenu();
        }

        if (config["~propertyChanged"] && config.chartType) {
            if (this.getSupportedReactions()) displayTabs.push("conditions");

            page.displayWorkspaceTabs(displayTabs, page.selectedTab);
        }
    },

    getPropertyModel : function($super) {
        var model = $super();
        var labelModel = _.bind(this.labelMixin.getPropertyModel, this)();
        var pointSizes = [];

        model.push({
            type:"button_group_ex" ,
            id:"chartType",
            label:"Type" ,
            group:"type",
            options: Props.Defaults.miniChartTypes.slice(0, 3),
            selectedValue: this.chartType,
            help: {
                link: "klips/mini-properties"
            }
        });


        // Line options
        model.push({
            type:"button_group_ex",
            id:"lineFormat",
            label:"Style",
            group:"line_options",
            displayWhen: { chartType:"line" },
            options: Props.Defaults.miniLineFormats,
            selectedValue: this.lineFormat
        });

        model.push({
            type:"button_group_ex",
            id:"lineWeight",
            label:"Line Weight",
            group:"line_options",
            displayWhen: { chartType:"line" },
            options: [{value:"auto", label:"", css:"mini_lineWeight_auto"}].concat(Props.Defaults.lineWeights),
            selectedValue: this.lineWeight,
            help: {
                link: "klips/mini-line-weight"
            }
        });

        switch (parseInt(this.parent.size)) {
            case 1: pointSizes = Props.Defaults.pointSizes.slice(0, 1); break;
            case 2: pointSizes = Props.Defaults.pointSizes.slice(0, 2); break;
            case 3: pointSizes = Props.Defaults.pointSizes.slice(0, 4); break;
            default: pointSizes = Props.Defaults.pointSizes;
        }

        model.push({
            type:"button_group_ex",
            id:"pointSize",
            label:"Point Size",
            group:"line_options",
            displayWhen: { chartType:"line" },
            options: [{value:"auto", label:"", css:"mini_pointSize_auto"}].concat(pointSizes),
            selectedValue: this.pointSize,
            help: {
                link: "klips/mini-point-size"
            }
        });

        model.push({
            type:"checkboxes",
            id:"includeHighLowPoints",
            label:"High/Low",
            group:"line_options",
            displayWhen: { chartType:"line" },
            options: [
                {value:true, label:"Highlight highest & lowest points", checked: this.includeHighLowPoints}
            ]
        });

        // Tristate options
        model.push({
            type:"text" ,
            id:"winThreshold",
            label:"Threshold" ,
            group:"tri_options",
            displayWhen: { chartType:"tristate" },
            width:"50px",
            value: this.winThreshold
        });

        // Discrete options
        model.push({
            type:"select",
            id:"barHeight",
            label:"Bar Height",
            group:"discrete_options",
            displayWhen: { chartType:"discrete" },
            options: [{value:"auto", label:"Automatic"}, {value:"cust", label:"Custom..."}],
            selectedValue: this.barHeight
        });

        model.push({
            type:"text",
            id:"customBarHeight",
            minorLabels: [ {label:"%", position:"right", css:"quiet"} ],
            group:"discrete_options",
            displayWhen: { chartType:"discrete", barHeight:"cust" },
            width:"50px",
            value: this.customBarHeight,
            flow:true
        });

        model.push({
            type:"text",
            id:"discreteThreshold",
            label:"Threshold",
            group:"discrete_options",
            displayWhen: { chartType:"discrete" },
            width: "50px",
            value: this.discreteThreshold
        });


        // Range
        model.push({
            type:"select",
            id:"chartRangeOption",
            label:"Range",
            group:"range",
            displayWhen: { chartType:["line", "bar", "discrete"] },
            options: [{value:"auto", label:"Automatic"}, {value:"cust", label:"Custom..."}],
            selectedValue: this.chartRangeOption,
            help: {
                link: "klips/range"
            }
        });

        model.push({
            type:"text",
            id:"minCustomVal",
            minorLabels: [ {label:"to", position:"right"} ],
            group:"range",
            displayWhen: { chartType:["line", "bar", "discrete"], chartRangeOption:"cust" },
            width:"40px",
            value: this.minCustomVal,
            flow:true
        });

        model.push({
            type:"text",
            id:"maxCustomVal",
            group:"range",
            displayWhen: { chartType:["line", "bar", "discrete"], chartRangeOption:"cust" },
            width:"40px",
            value: this.maxCustomVal,
            flow:true
        });


        // Line colours
        model.push({
            type:"color_picker",
            id:"lineColor",
            group:"line_colour",
            label:"Colors",
            fieldMargin:"9px 0 0",
            minorLabels: [ {label:"Line", position:"right"} ],
            displayWhen: { chartType:"line" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.lineColor
        });

        model.push({
            type:"color_picker",
            id:"spotColor",
            group:"line_colour",
            minorLabels: [ {label:"End point", position:"right"} ],
            displayWhen: { chartType:"line" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.spotColor,
            flow: {padding:"75px"}
        });

        model.push({
            type:"color_picker",
            id:"fillColor",
            group:"line_colour",
            fieldMargin:"0",
            minorLabels: [ {label:"Area", position:"right"} ],
            displayWhen: { chartType:"line", lineFormat:"la" },
            valueType:"theme",
            defaultColour: {rgb:CXTheme.current.cx_sparkline_area},
            selectedValue:this.fillColor
        });

        model.push({
            type:"color_picker",
            id:"minSpotColor",
            group:"line_colour",
            minorLabels: [ {label:"Low point(s)", position:"right"} ],
            displayWhen: { chartType:"line", includeHighLowPoints:true },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.minSpotColor
        });

        model.push({
            type:"color_picker",
            id:"maxSpotColor",
            group:"line_colour",
            minorLabels: [ {label:"High point(s)", position:"right"} ],
            displayWhen: { chartType:"line", includeHighLowPoints:true  },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.maxSpotColor,
            flow: {padding:"20px"}
        });

        // Bar colours
        model.push({
            type:"color_picker",
            id:"posBarColor",
            group:"bar_colour",
            label:"Colors",
            fieldMargin:"9px 0 0 0",
            minorLabels: [ {label:"Positive", position:"right"} ],
            displayWhen: { chartType:"bar" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.posBarColor
        });

        model.push({
            type:"color_picker",
            id:"zeroBarColor",
            group:"bar_colour",
            fieldMargin:"0",
            minorLabels: [ {label:"Zero", position:"right"} ],
            displayWhen: { chartType:"bar" },
            valueType:"theme",
            defaultColour: {rgb:"#aaaaaa", theme:"cx-theme_aaa"},
            selectedValue:this.zeroBarColor
        });

        model.push({
            type:"color_picker",
            id:"negBarColor",
            group:"bar_colour",
            fieldMargin:"0",
            minorLabels: [ {label:"Negative", position:"right"} ],
            displayWhen: { chartType:"bar" },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.negBarColor
        });

        // Tristate colours
        model.push({
            type:"color_picker",
            id:"winColor",
            group:"tri_colour",
            label:"Colors",
            fieldMargin:"9px 0 0 0",
            minorLabels: [ {label:"Win", position:"right"} ],
            displayWhen: { chartType:"tristate" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.winColor
        });

        model.push({
            type:"color_picker",
            id:"drawColor",
            group:"tri_colour",
            fieldMargin:"0",
            minorLabels: [ {label:"Draw", position:"right"} ],
            displayWhen: { chartType:"tristate" },
            valueType:"theme",
            defaultColour: {rgb:"#aaaaaa", theme:"cx-theme_aaa"},
            selectedValue:this.drawColor
        });

        model.push({
            type:"color_picker",
            id:"lossColor",
            group:"tri_colour",
            fieldMargin:"0",
            minorLabels: [ {label:"Loss", position:"right"} ],
            displayWhen: { chartType:"tristate" },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.lossColor
        });

        // Discrete colours
        model.push({
            type:"color_picker",
            id:"discreteBarColor",
            group:"discrete_colour",
            label:"Colors",
            minorLabels: [ {label:"Bar", position:"right"} ],
            displayWhen: { chartType:"discrete" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.discreteBarColor
        });

        model.push({
            type:"color_picker",
            id:"discreteThresholdColor",
            group:"discrete_colour",
            minorLabels: [ {label:"Threshold", position:"right"} ],
            displayWhen: { chartType:"discrete" },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.discreteThresholdColor
        });


        _.each(labelModel, function(prop) {
            if (prop.id == "fmt") {
                prop.label = "Tooltip Format";
            }
        });

        model = $.merge(model, labelModel);

        return model;
    },

    clearFormula : function($super,idx) {
        $super();
        this.setData([0,0]);
    },

    getAvailableAggregations: function($super) {
        var aggregations = $super();
        var parentId = this.parent.factory_id;
        var modelType = this.getVirtualColumnType();

        switch (parentId) {
            case "mini_series":
                if (modelType != DX.ModelType.NUMBER) {
                    aggregations = Props.Defaults.numericTextAggregations;
                }
                break;
            default:
                break;
        }

        return aggregations;
    }

});
;/****** cx.mini_series.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, KlipFactory:false, isWorkspace:false, page:false, updateManager:false, _sanitizeNumbers:false,
    CXTheme:false, FMT:false, safeText:false, dashboard:false, bindValues:false, Props:false */

Component.MiniSeries = $.klass(Component.Base, {

    displayName: "Sparkline",
    factory_id:"mini_series",
    variableSubComponents: [
        { label:"Series", type:"mini_data", role:"series" }
    ],

    $sparkline : false,
    $textIndicator: false,
    size: "x-large",
    customSize:"",
    uniqueSeriesNum: 1,
    lastValueSeries: 1,

    iconWidth: 0,

    defaultArgs : {
        tooltipOffsetX:20,
        tooltipOffsetY:20
    },

    sizes : {
        "xx-small" :    { height:16, indSize:14, indMargin:1,  lineWidth:1, spotRadius:1.5,  barSpacing:1 },
        "x-small"  :    { height:20, indSize:15, indMargin:1.4,  lineWidth:1.4, spotRadius:2.0,  barSpacing:1 },
        "small"    :    { height:24, indSize:19, indMargin:1.8,  lineWidth:1.8, spotRadius:2.2,  barSpacing:1 },
        "medium"   :    { height:31, indSize:26, indMargin:2, lineWidth:2, spotRadius:2.5, barSpacing:1 },
        "large"    :    { height:40, indSize:33, indMargin:3,  lineWidth:2.5, spotRadius:3.0,  barSpacing:2 },
        "x-large"  :    { height:55, indSize:48, indMargin:4, lineWidth:3, spotRadius:4.0, barSpacing:2 },
        "xx-large" :    { height:78, indSize:66, indMargin:6, lineWidth:4, spotRadius:5.5, barSpacing:2 },
        cust : { height:62, indSize:48, indMargin:6, lineWidth:3, spotRadius:4.0,  barSpacing:2 }
    },


    initialize : function($super) {
        $super();
    },

    initializeForWorkspace : function() {
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(400);
        }
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Series",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this, type:"mini_data", role:"series"}},
                    {type:"remove", disabled:true}
                ]
            ]
        });

        return model;
    },

    addChildByType : function($super, config) {
        var newChild = $super(config);

        if (config.type == "mini_data" && config.role == "series") {
            var id = this.uniqueSeriesNum++;

            newChild = KlipFactory.instance.createComponent({
                type :"mini_data",
                displayName :"Series " + id,
                role:"series",
                seriesId: id
            });
        }

        this.addComponent(newChild, config.index);

        return newChild;
    },

    renderDom : function($super) {
        var dom = $super();
        this.$sparkline = $("<div>").css({display:"inline-block", "vertical-align":"top"});
        this.$textIndicator = $("<div class='text-indicator'>").hide();

        dom.append(this.$sparkline).append(this.$textIndicator);

        var _this = this;
        this.$sparkline.bind("sparklineClick", function(){
            if(isWorkspace()) {
                page.invokeAction("select_component", _this);
                return false;
            }
        });
    },

    onResize : function($super) {
        $super();

        this.invalidateAndQueue();
    },

    invalidateAndQueue : function() {
        // hide the sparkline in case resizing smaller
        // do not want the label to go below the sparkline
        this.$sparkline.hide();

        this.invalidate();
        updateManager.queue(this);
    },

    onEvent : function ($super, evt) {
        $super(evt);

        if (evt.id == "theme_changed") {
            this.invalidate();
        }
    },

    onChildrenUpdated : function() {
        this.update();
    },

    /**
     * gets the preferred widths/heights of managed components
     */
    updateComponentBounds : function() {
        var componentWidth, lastValWidth, lastValHeight, availWidth, lastPadding;

        if(!this.el.is(":visible") || this.el.width() <= 0) {
            return;
        }

        componentWidth = this.el.width();

        this.chartHeight = (this.size == "cust" && this.customSize != "" ? this.customSize : this.sizes[this.size].height);

        if (!this.displayLastValue) {
            this.chartWidth = componentWidth - this.iconWidth;
        } else {
//            this.lastValue.el.css("width", "auto");

            lastValWidth = this.lastValue.el.outerWidth(true);
            lastValHeight = this.lastValue.el.outerHeight(true);

            if (this.chartHeight < lastValHeight) {
                this.chartHeight = lastValHeight;
            }

            availWidth = componentWidth - this.iconWidth - 1; /* mystery 1 for FF and IE */
            if (lastValWidth > availWidth * 0.8) {
                lastPadding = lastValWidth - this.lastValue.el.width();

                this.lastValue.el.width(Math.floor(availWidth * 0.8) - lastPadding);
                this.lastValue.elLabel.addClass("ellipsis");

                lastValWidth = this.lastValue.el.outerWidth(true);
            } else {
                this.lastValue.elLabel.removeClass("ellipsis");
            }

            this.chartWidth = componentWidth - lastValWidth - this.iconWidth - 1; /* mystery 1 for FF and IE */
        }

        this.$sparkline.height(this.chartHeight);
        this.$sparkline.width(this.chartWidth);
    },

    update : function($super) {
        $super();

        // exit early optimizations
        // ------------------------
        if ( !this.getKlip().isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

        var _this = this;

        // handle chart reactions
        // ----------------------
        var reactionContext = { icons:[], bgColor:false, text:false };
        this.collectReactions(reactionContext);

        var compReplaced = this.applyIconReactions(reactionContext.icons);

        // clear previous background colors
        this.el.css("background-color", "");
        this.el.removeClass(function(index, classNames) {
            var currentClasses = classNames.split(" "); // change the list into an array
            var classesToRemove = []; // array of classes which are to be removed

            $.each(currentClasses, function (index, className) {
                // if the className begins with  add it to the classesToRemove array
                if (/cx-bgcolor_.*/.test(className)) {
                    classesToRemove.push(className);
                }
            });

            // turn the array back into a string
            return classesToRemove.join(" ");
        });

        // apply background color reaction
        if (reactionContext.bgColor) {
            if (reactionContext.bgColor.indexOf("#") != -1) {
                this.el.css("background-color", reactionContext.bgColor);
            } else {
                this.el.addClass(reactionContext.bgColor);
            }
        }

        // apply text reaction
        if (reactionContext.text || compReplaced) {
            this.displayLastValue = false;
            this.$sparkline.hide();
            this.lastValue.el.hide();

            if (!compReplaced) {
                this.$textIndicator.removeClass()
                    .addClass("text-indicator size-" + (this.size == "cust" ? "x-large" : this.size))
                    .css("display", "inline-block")
                    .text(reactionContext.text);
            } else {
                this.$textIndicator.hide();
            }
        } else {
            this.$sparkline.show();
            this.$textIndicator.hide();

            var dataSeries = this.getChildrenByType("mini_data");
            if (dataSeries.length == 0) {
                this.lastValueSeries = "hidden";
            }

            var lastValData = this.lastValue.getData(0);

            this.displayLastValue = true;
            if (this.lastValueSeries == "hidden") {
                this.displayLastValue = false;
            } else if (this.lastValueSeries == "cust") {
                this.displayLastValue = (lastValData && lastValData.length > 0);
            }

            if (this.displayLastValue) {
                this.lastValue.el.show();

                if (this.lastValueSeries != "cust") {
                    for (var i = 0, len = dataSeries.length; i < len; i++) {
                        // set the data in the last value label
                        if (dataSeries[i].seriesId == this.lastValueSeries) {
                            var data = dataSeries[i].getData(0).slice(0);

                            // can't have a last value if the series doesn't have any data to start with
                            if (data.length > 0) {
                                _sanitizeNumbers(data);

                                var lastVal = ((lastValData && lastValData.length > 0) ? lastValData[0] : false),
                                    newVal = data[data.length - 1];

                                if (lastVal !== newVal) {
                                    if (newVal === undefined) {
                                        newVal = "";
                                    }

                                    this.lastValue.setFormula(0, newVal.toString(), {"t":"expr","v":false,"c":[{"t":"l","v":newVal.toString()}]});
                                    this.lastValue.setData([ newVal ]);
                                    this.lastValue.update();
                                }
                            }

                            break;
                        }
                    }
                }
            } else {
                this.lastValue.el.hide();
            }

            this.updateComponentBounds();

            // render the data series
            // ----------------------
            _.each(dataSeries, function(series, i) {
                _this.renderSeries(series, i > 0);
            });
        }

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        // position icons
        _.each(this.el.find(".icon-indicator"), function(icon) {
            $(icon).css("margin-top", (_this.sizes[_this.size].indMargin));
        });

        this.setInvalid(false,true);

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    renderSeries : function(cx, composite) {
        var j;
        var data = cx.getData(0).slice(0);
        _sanitizeNumbers(data);

        var args = $.extend({}, this.defaultArgs);

        args.type = cx.chartType;
        args.width = "100%";
        args.height = (this.size == "cust" && this.customSize != "" ? this.customSize : this.sizes[this.size].height);

        if (composite) args.composite = true;

        switch (args.type) {
            case "line":
            {
                args.lineWidth = (cx.lineWeight == "auto" ? this.sizes[this.size].lineWidth : cx.lineWeight);
                args.spotRadius = (cx.pointSize == "auto" ? this.sizes[this.size].spotRadius : cx.pointSize);

                args.lineColor = CXTheme.getThemeColour(cx.lineColor);
                args.spotColor = CXTheme.getThemeColour(cx.spotColor);
                args.fillColor = (cx.lineFormat == "la" ? CXTheme.getThemeColour(cx.fillColor) : false);
                args.minSpotColor = (cx.includeHighLowPoints ? CXTheme.getThemeColour(cx.minSpotColor) : false);
                args.maxSpotColor = (cx.includeHighLowPoints ? CXTheme.getThemeColour(cx.maxSpotColor) : false);

                if (args.fillColor && (cx.fillColor.toLowerCase() == CXTheme.themes["default"].cx_sparkline_area.toLowerCase() || cx.fillColor.toLowerCase() == CXTheme.themes["dark"].cx_sparkline_area.toLowerCase())) {
                    args.fillColor = CXTheme.current.cx_sparkline_area;
                }

                args.highlightSpotColor = args.spotColor;
                args.highlightLineColor = "#d7d7d7";

                break;
            }
            case "bar":
            {
                args.barSpacing = this.sizes[this.size].barSpacing;
                args.barWidth = (this.chartWidth - args.barSpacing * data.length) / data.length;

                args.barColor = CXTheme.getThemeColour(cx.posBarColor);
                args.negBarColor = CXTheme.getThemeColour(cx.negBarColor);
                args.zeroColor = CXTheme.getThemeColour(cx.zeroBarColor);
                args.highlightLighten = 1.17;

                break;
            }
            case "tristate":
            {
                args.barSpacing = this.sizes[this.size].barSpacing;
                args.barWidth = (this.chartWidth - args.barSpacing * data.length) / data.length;

                args.posBarColor = CXTheme.getThemeColour(cx.winColor);
                args.negBarColor = CXTheme.getThemeColour(cx.lossColor);
                args.zeroBarColor = CXTheme.getThemeColour(cx.drawColor);
                args.highlightLighten = 1.17;

                var tv = parseFloat(cx.winThreshold && cx.winThreshold != "" ? cx.winThreshold : 0);
                for (var $x = 0; $x < data.length; $x++) {
                    if (data[$x] == tv) {
                        data[$x] = 0;
                    } else if (data[$x] > tv) {
                        data[$x] = 1;
                    } else {
                        data[$x] = -1;
                    }
                }

                break;
            }
            case "discrete":
            {
                args.lineColor = CXTheme.getThemeColour(cx.discreteBarColor);

                if (cx.barHeight == "cust" && cx.customBarHeight != "") {
                    args.lineHeight = args.height * (cx.customBarHeight / 100);
                }

                if (cx.discreteThreshold && cx.discreteThreshold != "") {
                    args.thresholdValue = parseFloat(cx.discreteThreshold);
                    args.thresholdColor = CXTheme.getThemeColour(cx.discreteThresholdColor);
                }

                break;
            }
        }

        if (cx.chartRangeOption == "cust" && cx.minCustomVal != "") args.chartRangeMin = FMT.convertToNumber(cx.minCustomVal);
        if (cx.chartRangeOption == "cust" && cx.maxCustomVal != "") args.chartRangeMax = FMT.convertToNumber(cx.maxCustomVal);

        var numFmtArgs = _.clone(cx.getFmtArgs());
        if (cx.fmtArgs.prefix) {
            args.tooltipPrefix = safeText(cx.fmtArgs.prefix);
            delete numFmtArgs.prefix;
        }
        if (cx.fmtArgs.suffix) {
            args.tooltipSuffix = safeText(cx.fmtArgs.suffix);
            delete numFmtArgs.suffix;
        }

        switch (args.type) {
            case "line":
            case "bar":
                args.numberFormatter = function(val) {
                    return FMT.format(cx.fmt, [val], numFmtArgs);
                };
                break;
            case "tristate":
                args.tooltipFormat = "<span style=\"color: {{color}}\">&#9679;</span> {{prefix}}{{value:results}}{{suffix}}";
                args.tooltipValueLookups = {
                    results: $.range_map({ ":-1":"Loss", "0":"Draw", "1:":"Win" })
                };
                break;
        }


        // handle series reactions
        // -----------------------
        var dsReactionCtx = {barColours: {}, pointColours: {}};
        cx.collectReactions(dsReactionCtx);

        if (cx.chartType == "line") {
            if (!_.isEmpty(dsReactionCtx.pointColours)) {
                args.valueSpots = {};

                for (j = 0; j < data.length; j++) {
                    if (dsReactionCtx.pointColours[j]) {
                        args.valueSpots[data[j]] = dsReactionCtx.pointColours[j];

                        if (j == data.length - 1) args.spotColor = false;
                    }
                }
            }

            _.each(cx.reactions, function(r) {
                if (_.max(r.mask)) {
                    _.each(r.reactions, function(re) {
                        var val = re.value;

                        switch (re.type) {
                            case "line-color":
                                args.lineColor = CXTheme.getThemeColour(val);
                                break;
                            case "end-color":
                                args.spotColor = CXTheme.getThemeColour(val);
                                break;
                            case "area-color":
                                args.fillColor = (cx.lineFormat == "la" ? CXTheme.getThemeColour(val) : false);
                                break;
                            case "low-color":
                                args.minSpotColor = (cx.includeHighLowPoints ? CXTheme.getThemeColour(val) : false);
                                break;
                            case "high-color":
                                args.maxSpotColor = (cx.includeHighLowPoints ? CXTheme.getThemeColour(val) : false);
                                break;
                        }
                    });
                }
            });
        } else if (cx.chartType == "bar" || cx.chartType == "tristate") {
            if (!_.isEmpty(dsReactionCtx.barColours)) {
                args.colorMap = [];

                for (j = 0; j < data.length; j++) {
                    if (dsReactionCtx.barColours[j]) {
                        args.colorMap.push(dsReactionCtx.barColours[j]);
                    } else {
                        if (data[j] == 0) {
                            args.colorMap.push(cx.chartType == "bar" ? args.zeroColor : args.zeroBarColor);
                        } else if (data[j] > 0) {
                            args.colorMap.push(cx.chartType == "bar" ? args.barColor : args.posBarColor);
                        } else {
                            args.colorMap.push(args.negBarColor);
                        }
                    }
                }
            }
        }

        // HACK: This is a hack to fix very wide tooltips when display:flex on body
        var $dashboard = $(".c-dashboard");
        if (KF.company.hasFeature("one_product_ui") && $dashboard.length > 0) {
            args.tooltipContainer = $dashboard;
        }

        this.$sparkline.sparkline(data, args);
    },

    applyIconReactions : function(icons) {
        this.el.find(".icon-indicator").remove();
        this.iconWidth = 0;

        var replaced = false;
        var i = 0;
        // apply the reaction icons
        while (!replaced && i < icons.length) {
            var size = this.sizes[this.size].indSize;

            var pos = icons[i].pos || "left";
            var indHtml = $("<img class='icon-indicator indicator-" + pos + "' height='"+ size +"' width='"+ size +"' src='" + dashboard.getRootPath() + "images/resources/indicators/hd/" + icons[i].value + ".png'>");
            if (pos == "right") {
                this.lastValue.el.before(indHtml.css("float", "right"));
            } else if (pos == "left") {
                this.el.prepend(indHtml);
            } else if (pos == "replace") {
                replaced = true;
                this.el.find(".icon-indicator").remove();
                this.iconWidth = 0;
                this.el.append(indHtml);
            }

            this.iconWidth += size;

            i++;
        }

        return replaced;
    },


    configure : function($super, config) {
        $super(config);
        bindValues(this, config, ["customSize","lastValueSeries"]);
        this.updateLabelSizes(config);
        this.invalidate();

        if (this.lastValue && config.lastValueSeries) {
            this.lastValue.dataDisabled = (config.lastValueSeries != "cust");
        }
    },

    serialize : function($super) {
        var config = $super();
        bindValues(config, this, ["customSize","lastValueSeries"],
            {
                customSize:""
            }, true);

        return config;
    },

    getPropertyModel : function($super) {
        var model = $super();
        var _this = this;
        var dataSeries;
        var lastValueOptions;
        var lastValMsg;
        var i;

        model.push({
            type:"select",
            id:"size",
            label:"Height",
            group:"size",
            options: Props.Defaults.size.concat({value:"cust", label:"Custom..."}),
            selectedValue: this.size
        });

        model.push({
            type:"text",
            id:"customSize",
            group:"size",
            displayWhen: { size:"cust" },
            width:"50px",
            value: this.customSize,
            flow:true
        });


        dataSeries = this.getChildrenByType("mini_data");

        lastValueOptions = [{ value:"hidden", label:"Hidden" }];
        for (i = 0; i < dataSeries.length; i++) {
            lastValueOptions.push({ value:dataSeries[i].seriesId, label:dataSeries[i].displayName });
        }
        lastValueOptions.push({ value:"cust", label:"Custom..." });

        model.push({
            type:"select",
            id:"lastValueSeries",
            label:"Last Value",
            group:"lastValue",
            options: lastValueOptions,
            selectedValue: this.lastValueSeries,
            onChange: function() {
                var lastChild = _this.getChildByRole("last-value");
                if(_this.lastValueSeries !== "cust" && lastChild.dstContext) {
                    delete lastChild.dstContext;
                    page.updateMenu();
                }
            },
            help: {
                link: "klips/last-value"
            }
        });

        lastValMsg = $("<span>");
        $("<a>").text("Assign data")
            .attr("href","#")
            .click(function(){
                page.invokeAction("select_component", _this.lastValue);
                page.selectWorkspaceTab("data");
            })
            .appendTo(lastValMsg);
        lastValMsg.append(" to Last Value");

        model.push({
            type:"markup",
            id:"msg_custLastValue",
            group:"lastValue",
            displayWhen: { lastValueSeries:"cust" },
            el:lastValMsg
        });

        model.push({
            type:"button",
            id:"addSeries",
            label:"Data Series",
            group:"addComponents",
            text:"Add Series",
            onClick: function() {
                page.invokeAction("add_component", {parent:_this, type:"mini_data", role:"series"}, {});
            }
        });

        return model;
    },


    getSupportedReactions : function() {
        return { icon:true, "background-color":true, text:true };
    },

    /**
     *
     * @param idx
     * @param reaction
     * @param ctx - {
     *      icons: [],
     *      bgColor: false,
     *      text: false
     * }
     */
    handleReaction : function( idx, reaction, ctx ) {
        switch (reaction.type) {
            case "icon":
                ctx.icons.push(reaction);
                break;
            case "background-color":
                ctx.bgColor = reaction.value;
                break;
            case "text":
                ctx.text = reaction.value;
                break;
        }
    },


    afterFactory : function() {
        var i;
        var requiredChildren = [
            {role:"series", props:{displayName:"Series " + this.uniqueSeriesNum,type:"mini_data",seriesId:this.uniqueSeriesNum}},
            {role:"last-value", props:{displayName:"Last Value",type:"label",dataDisabled:true, autoFmt:true,
                                formulas: [{"txt":"0","src":{"t":"expr","v":false,"c":[{"t":"l","v":"0"}]}}],
                                data: [[0]], fmt:"num", size:3}}
        ];

        for (i = 0; i < requiredChildren.length; i++) {
            var req = requiredChildren[i];
            if (!this.getChildByRole(req.role)) {
                var cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                this.addComponent(cx);
            }
        }

        this.lastValue = this.getChildByRole("last-value");
        this.lastValue.allowedFormats = ["txt","num","cur","pct","dat","dur"];
        this.lastValue.isDeletable = false;
        this.lastValue.isDraggable = false;
        this.lastValue.el.css({"float":"right", clear:"none"}).appendTo(this.el);
        this.lastValue.elLabel.css("padding", "0");

        var series = this.getChildrenByType("mini_data");
        for (i = 0; i < series.length; i++) {
            if (series[i].seriesId >= this.uniqueSeriesNum) {
                this.uniqueSeriesNum = series[i].seriesId + 1;
            }
        }
    }

});
;/****** cx.news_reader.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, updateManager:false, dashboard:false, KlipFactory:false, bindValues:false, isWorkspace:false, safeText:false */

Component.NewsReader = $.klass(Component.Base, {

    displayName: "News",
    factory_id:"news_reader",
    itemsPerPage:3,

    defaultArgs : {
        width:"60px",
        height:"18px",
        lineWidth:2,
        lineColor:"darkblue",
        spotColor:"darkblue",
        fillColor:false,
        spotRadius:2.5,
        minSpotColor:false,
        maxSpotColor:false
    },

    initialize : function ($super,config) {
        $super(config);
        this.currentPage = 0;
    },

    initializeForWorkspace : function(workspace) {
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(400);
        }
    },

    renderDom : function($super) {
        var dom = $super();
        dom.delegate(".selectable", "mouseover", _.bind(this.onItemMouseOver, this) );
        dom.delegate(".selectable", "mouseout", _.bind(this.onItemMouseOut, this) );
    },

    onResize : function($super) {
        $super();

        updateManager.queue(this);
    },

	// disable html security risk
	setHtml : function(n) {
		if (KF.company.hasFeature("saas_15885")){
			return safeText(n);
		}else{
			return n;
		}
	},


    update : function($super) {
        $super();

        this.el.children().not(".comp-drag-handle").remove();

        var headlines = this.getChildByProperty("role","headline");
        var abstracts = this.getChildByProperty("role","abstract");
        var hrefs = this.getChildByProperty("role","href");
        var images = this.getChildByProperty("role","img");
        var authors = this.getChildByProperty("role","author");
        var date = this.getChildByProperty("role","date");

        var data_hl = headlines.getData(0);
        var data_abstracts = abstracts.getData(0);
        var data_hrefs = hrefs.getData(0);
        var data_images = images.getData(0);
        var data_author = authors.getData(0);
        var data_date = date.getData(0);

        var len = Math.max(data_abstracts.length,data_hl.length);

        var $table = $("<table>").css("width", this.el.width() - 20);  // Padding

        for( var i = 0 ; i < len; i++) {
            var $item = $("<tr>").addClass("item").attr("valign","top");
            var $text = $("<td>").addClass("text");

            if ( data_images[i] ) {
                $item.append($("<td>").append(
                    $("<img>").addClass("image").attr("src", data_images[i]).css("height", "auto")
                    ));
            }

            if ( data_hl[i] ){
                $text.append( $("<div>").addClass("headline").html(this.setHtml(data_hl[i])) );
            }

            // "abstract" is correspond to "content" in News and should be treated as HTML
            if ( data_abstracts[i] ) {
                $text.append( $("<div>").addClass("abstract").html(data_abstracts[i]) );
            }
            if ( data_hrefs[i] ) {
                $text.attr("item-link",data_hrefs[i] );
                $text.addClass("selectable") ;
                $text.click( _.bind(this.onItemClick,this,data_hrefs[i]));
            }
            if ( data_author[i] ){
                $text.append( $("<div>").addClass("author").html(this.setHtml(data_author[i])) );
            }

            if ( data_date[i] ){
                $text.append( $("<div>").addClass("date").html(this.setHtml(data_date[i])) );
            }

            $item.append($text);
//			$item.append( $("<div>").addClass("clearfix") );

            $table.append($item);
        }
        this.el.append($table);

        if ( dashboard.config.mobile ) return;

        if (len > 1) {
            this.$pager = $("<div>")
                .addClass("pager")
                .append(
                    $("<div>").addClass("pager-prev").click( _.bind(this.cyclePage,this,-1) )
                )
                .append(
                    $("<span>").addClass("pager-current")
                )
                .append(
                    $("<span>").text("of")
                )
                .append(
                    $("<span>").addClass("pager-max")
                )
                .append(
                    $("<div>").addClass("pager-next").click( _.bind(this.cyclePage,this,1) )
                ).disableSelection();


            this.el.append(this.$pager);
        }
        this.setPage(1);

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------
    },

    setPage : function(n) {
        var items = this.el.find(".item");
        var start = (n * this.itemsPerPage) - this.itemsPerPage;
        var end = start + parseInt(this.itemsPerPage);
        this.pageCount = Math.ceil(items.length / this.itemsPerPage);


        for( var i = 0 ; i < items.length ; i++) {
            if ( i < start || i >= end ) items.eq(i).hide();
            else items.eq(i).show();
        }

        this.currentPage = n;
        if (items.length > 1) {
            this.$pager.find(".pager-current").text(n);
            this.$pager.find(".pager-max").text(this.pageCount);
        }

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },


    onItemClick : function(url,evt) {
        window.open( url );
    },

    onItemMouseOver : function(evt) {
        $(evt.currentTarget).children(".headline").addClass("hover");
    },

    onItemMouseOut : function(evt) {
        $(evt.currentTarget).children(".headline").removeClass("hover");
    },

    cyclePage : function(dir) {
        var nextPage = this.currentPage + dir;
        if ( nextPage < 1 || nextPage > this.pageCount ) return;
        this.setPage(nextPage);

    },

    /**
     * after a new news_reader is created, check to see if we have the required child proxy components.
     * if not (which would be the case on initial creation), create and add them.
     *
     */
    afterFactory : function() {
        var i;
        if ( ! this.components || this.components.length == 0 ) {
            var proxys = {"headline":"Headline","abstract":"Content","href":"Link","img":"Image","author":"Author",date:"Date"} ;
            for(i in proxys) {
                var cfg = { type:"proxy", displayName:proxys[i], role:i };
                this.addComponent( KlipFactory.instance.createComponent(cfg) );
            }
        }

        for (i = 0; i < this.components.length; i++) {
            this.components[i].isDeletable = false;
        }
    },


    getPropertyModel : function($super) {
        var model = $super();
        model.push({
            type:"text" ,
            id:"itemsPerPage",
            label:"Items per Page" ,
            group:"news",
            value: this.itemsPerPage
        });
        return model;
    },


    configure : function($super,config) {
        $super(config);
        bindValues(this,config,["itemsPerPage"]);
    },

    serialize : function($super) {
        var config = $super();
        bindValues(config,this,["itemsPerPage"],
            {
                itemsPerPage:3
            }, true);
        return config;
    }


})


;/****** cx.panel.js *******/ 

/* global Component:false, isWorkspace:false, page:false, bindValues:false, encodeForId:false,
    Props:false, global_dialogUtils:false, global_contentUtils:false, updateManager:false */

/**
 * A component which has a layout manager and supports the addition of managed
 * components
 */
Component.Panel = $.klass(Component.Base, {

    factory_id : "panel",
    displayName : "Panel",

    initialize : function($super) {
        this.debouncedLayout = _.debounce( this.doLayout , 50 );
        this.handleComponentUpdated = _.debounce(_.bind(this.handleComponentUpdated, this), 200);

        this.isPanel = true;
        $super();
    },

    renderDom: function($super) {
        return $super();
    },

    configure : function($super, config) {
        $super(config);
    },

    serialize : function($super) {
        return $super();
    },


    update : function($super) {
        $super();

        this.doLayout();
    },

    onResize : function($super) {
        $super();

        this.invalidateAndQueue();
    },

    onEvent : function ($super, evt) {
        if (evt.id == "theme_changed") {
            this.invalidate();
        }
    },

    configureLayout : function(config,cx) {

    },


    getPropertyModel : function() {
        var model = [];
        if ( this.layout )
            $.merge( model, this.layout.getPropertyModel() );

        return model;
    },

    doLayout : function() {

    },

    handleComponentUpdated : function() {

    },

    getLayoutConstraintsModel : function(cx) {

    },

    /**
     * called by the workspace when the 'Position' properties panel is updated AND the workspace has an active layout
     * to forward events to.
     * @param evt
     */
    onLayoutPropertiesChange : function(evt) {

    },

    /**
     *
     * @param $super
     * @param work
     */
    initializeForWorkspace : function($super, work) {
        _.each(this.components,function(cx){
            cx.initializeForWorkspace(work);
        });
    }
});


Component.GridPanel = $.klass( Component.Panel , {

    factory_id : "panel_grid",
    displayName : "Layout Grid",
    variableSubComponents: [
        { label:"Column", length:"cols", dimension:"x" },
        { label:"Row", length:"rows", dimension:"y" }
    ],
    isDstRoot: false,

    rows : 2 ,
    cols : 3 ,
    widths : false,
    rowHeights : false,
    cellData : false,
    cellMatrix: false,
    $container : false,

    containerClassName : false,

    defaultCellPadding : 5,
    defaultCellHeight : 56,

    /**
     * Flag used to only update layout cell height on first pass while klip is still busy loading
     */
    initialLayoutCellHeightCalculated: false,

    initialize : function($super){
        $super();

        this.isPanel = true;
        this.widths = [];
        this.rowHeights = [];
        this.cellData = [];

        if (!isWorkspace()) {
            this.defaultCellHeight = 0;
        }
    },

    configure : function($super,config) {
        var _this = this;
        var diff = 0;
        var dimension;
        var rowsOrColumnsToRemove = [];
        var componentsToRemove = [];
        var i;
        var originCell;

        $super(config);

        if (config.cols) config.cols = parseInt(config.cols);
        if (config.rows) config.rows = parseInt(config.rows);

        if (config["~propertyChanged"] && (config.cols || config.rows)) {
            if (config.cols && config.cols < this.cols) {
                diff = this.cols - config.cols;
                dimension = "x";
            } else if (config.rows && config.rows < this.rows) {
                diff = this.rows - config.rows;
                dimension = "y";
            }

            if (diff) {
                for (i = 1; i <= diff; i++) {
                    originCell = (dimension == "x" ? {x:this.cols - i, y:0} : {x:0, y:this.rows - i});

                    rowsOrColumnsToRemove.push({
                        originCell: originCell,
                        dimension: dimension
                    });
                }

                rowsOrColumnsToRemove.forEach(function(rowOrColumn) {
                    componentsToRemove = componentsToRemove.concat(_this.getComponentsToRemove(rowOrColumn.originCell, rowOrColumn.dimension));
                });

                if (componentsToRemove.length > 0) {
                    global_dialogUtils.confirm(global_contentUtils.buildWarningIconAndMessage("One or more components will be removed from your " + KF.company.get("brand", "widgetName") + ". Continue?"), {
                        title: "Remove Component",
                        okButtonText: "Remove"
                    }).then(function() {
                        componentsToRemove.forEach(function(component) {
                            _this.removeComponent(component);
                        });

                        rowsOrColumnsToRemove.forEach(function(rowOrColumn) {
                            _this.removeRowOrColumn(rowOrColumn.originCell, rowOrColumn.dimension);
                        });

                        if (isWorkspace()) page.updateMenu();

                        _this.configure({
                            rows: config.rows,
                            cols: config.cols
                        });
                        updateManager.queue(_this);
                    }, function() {
                        page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                    });

                    return;
                } else {
                    rowsOrColumnsToRemove.forEach(function(rowOrColumn) {
                        _this.removeRowOrColumn(rowOrColumn.originCell, rowOrColumn.dimension);
                    });
                }
            }
        }

        bindValues(this, config, [
            "rowHeights",
            "widths",
            "cellData",
            "rows",
            "cols",
            "containerClassName"
        ]);

        if (config["~controlUsed"]) {
            this.controlUsed = true;
            page.controlPalette.updateItems(this.getWorkspaceControlModel());
            page.updatePropertySheet(this.getPropertyModel(), this.getPropertyModelGroups(), page.componentPropertyForm);
        }

        this.invalidate();
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this,["rows","cols","containerClassName"], {"containerClassName":false}, true);

        if (!_.isEmpty(this.rowHeights)) cfg.rowHeights = this.rowHeights;
        if (!_.isEmpty(this.widths)) cfg.widths = this.widths;
        if (!_.isEmpty(this.cellData)) cfg.cellData = this.cellData;

        return cfg;

    },


    setRows : function(n) {

    },

    setCols : function(n) {

    },

    setCellConstraints : function( config ) {

    },

    getCellConstraints : function(x,y) {
        var i;
        var cd;

        for( i = 0 ; i < this.cellData.length; i++ ){
            cd = this.cellData[i];
            if ( cd.x == x && cd.y == y ) return cd;
        }
        return false;
    },


    onChildrenUpdated : function($super, childUpdated) {
        // remove the height from the row the child component is in so it will be resized properly
        var row = childUpdated.layoutConfig ? childUpdated.layoutConfig.y : false;
        if (row !== false) {
            this.clearRowHeights(row);
        }

        this.handleComponentUpdated();
    },

    update : function($super) {
        $super();

//		var cxLen = this.components.length;
//        while (--cxLen > -1) {
//            updateManager.queue( this.components[cxLen] );
//        }
    },

    doLayout : function() {
        var i;
        var j;
        var k;
        var cx;
        var x;
        var y;
        var $tr;
        var $layoutCell;
        var $td;
        var matrixInfo;
        var cd;
        var cellPadding;
        var cellPaddingStr;
        var cell;

        if ( !this.getKlip().isVisible ) return;

        /**
         * When panel is resized/moved we will lower the throttle time so that height is updated more accurately
         */
        if(KF.company.hasFeature("safari_klip_throttle") && $.browser.safari) {
            if (!this.getKlip().isBusy && this.getKlip().isBusy != undefined) { // only when klip is done loading
                this.updateLayoutCellHeights = _.throttle(_.bind(this.updateLayoutCellHeightsNoThrottle, this), 1000, {leading: true, trailing: false});
            }
        }

        if (this.isInvalid ) {
            if ( this.$container){
                for(i = 0 ; i < this.components.length; i++)
                    this.components[i].el.detach();
            }
        }


        // hide all components -- they'll be redisplayed at the end when they are sized
        // ----------------------------------------------------------------------------
        for (i = 0 ; i < this.components.length ; i ++){
            this.components[i].el.hide();
        }


        if (!this.$container || this.isInvalid ){

            // build the cell matrix which keeps track of which cells are spanned, and
            // which cells are the 'origin' cells.  Eg, if 0,0 has a colspan of 2
            // ------------------------------------------------------------------------
            this.cellMatrix = new Array(this.rows);
            for ( y = 0 ; y < this.rows ; y++ ){
                this.cellMatrix[y] = [];
                for( x = 0 ; x < this.cols ; x++)
                    this.cellMatrix[y].push( {} );

            }

            this.$container = $("<table>").addClass("layout-grid");
            this.$widthRow = $("<tr>");

            if(this.containerClassName) {
                this.$container.addClass(this.containerClassName);
            }

            // render table cells
            // ------------------
            for(i = 0 ; i < this.rows; i++ ){
                // before we render the first row, create empty tds that will hold width information
                // ---------------------------------------------------------------------------------
                if( i == 0 ){
                    for( j = 0 ; j < this.cols; j++ ){
                        this.$widthRow.append( $("<td>") );
                    }
                    this.$container.append( this.$widthRow );
                }

                // render the row and its tds
                // --------------------------
                $tr = $("<tr>");
                for( j = 0 ; j < this.cols; j++ ){
                    $layoutCell = $("<div class='layout-cell'>");
                    $td = $("<td>").append($layoutCell);

                    if (isWorkspace()) {
                        $td.addClass("drop-target").attr("drop-region", "layout");
                    }

                    if ( this.activeCell && this.activeCell.x == j && this.activeCell.y == i ){
                        $td.addClass("layout-active-cell");
                    }

                    matrixInfo = this.getMatrixCell(j, i);

                    if (matrixInfo && matrixInfo.spanned) continue;

                    cd = this.getCellData(j,i,false);
                    cellPadding  = this.defaultCellPadding;
                    cellPaddingStr = cellPadding + "px";
                    if (cd) {
                        if (cd.w && cd.w > 1) {
                            $td.attr("colspan", cd.w);
                            matrixInfo.w = cd.w;
                        }
                        if (cd.h && cd.h > 1) {
                            $td.attr("rowspan", cd.h);
                            matrixInfo.h = cd.h;
                        }

                        this.spanMatrix(j, i, _.clone(cd));

                        if (cd.cellPadding !== undefined) {
                            cellPadding = cd.cellPadding;
                            matrixInfo.cellPadding = cd.cellPadding;

                            if (cellPadding instanceof Array) {
                                cellPaddingStr = "";
                                for (k = 0; k < cellPadding.length; k++) {
                                    cellPaddingStr += cellPadding[k] + "px";

                                    if (k < cellPadding.length - 1) {
                                        cellPaddingStr += " ";
                                    }
                                }
                            } else {
                                cellPaddingStr = cellPadding + "px";
                            }
                        }

                        if (cd.alignment) {
                            $layoutCell.append($("<div class='floater'>").addClass(cd.alignment == 1 ? "middle" : "bottom"));
                            matrixInfo.alignment = cd.alignment;
                        }
                    }

                    $layoutCell.css("margin", cellPaddingStr);

                    matrixInfo.$el = $td;
                    matrixInfo.x = j;
                    matrixInfo.y = i;

                    $td.data("matrixInfo", matrixInfo);
                    $tr.append($td);
                }
                this.$container.append($tr);
            }


            // insert container into dom so that height/width ops work
            // -------------------------------------------------------
            this.el.children().not(".comp-drag-handle").remove();
            this.el.append(this.$container);

            // if there are components, sort by their XY position and then place them
            // into their layout cells
            // ----------------------------------------------------------------------
            if (this.components && this.components.length > 0) {
                this.components.sort( this.sortComponentXY );

                for(i = 0 ; i < this.components.length ; i++){
                    cx = this.components[i];
                    // no layout config?  no placement in grid!
                    if ( !cx.layoutConfig ) continue;
                    cell = this.getMatrixCell( cx.layoutConfig.x , cx.layoutConfig.y  );

                    // don't try to place components into cells that are spanned into and have origins
                    if ( cell && !cell.origin ){
                        cell.$el.children().first().append(cx.el);
                    }
                }

                if (isWorkspace()) page.updateMenu();    // order of components
            }
        } // finish rendering of layout


        // calculate column widths
        // -----------------------
        this.updateColumnWidths();

        if (this.components.length > 0) {
            // update component widths
            // -----------------------
            for (i = 0; i < this.components.length; i++) {
                cx = this.components[i];
                cx.fromPanelUpdate = true;
                cx.setWidthFromCell(cx.el.parent().width());

                if (cx.visible || isWorkspace()) {
                    cx.el.show();
                }
            }

            // set the heights of the empty rows
            this.updateLayoutCellHeights(true);
        } else {
            // update layout-cell heights
            // -----------------------------
            this.updateLayoutCellHeights();

            // Height is ready can update panel cell heights
            // -----------------------------------------------
            if (this.parent.isPanel) {
                this.parent.handleComponentUpdated();
                this.fromPanelUpdate = false;
            }
            // -----------------------------------------------

            if (!isWorkspace() && this.parent.isKlip) {
                this.parent.handleComponentUpdated();
            }
        }

        // update active cell if necessary
        // -------------------------------
        if (this.controlUsed && this.activeCell) {
            matrixInfo = this.getMatrixCell((this.activeCell.x >= this.cols ? this.cols - 1 : this.activeCell.x), (this.activeCell.y >= this.rows ? this.rows - 1 : this.activeCell.y), true);

            this.selectCell(matrixInfo);

            this.controlUsed = false;
        }

        this.setInvalid(false);
    },

    handleComponentUpdated : function() {
        var _this = this;

        if (this.getKlip().isVisible) {
            // put height updates in timeout so components don't need to wait
            // if they need to finish updating
            setTimeout(function() {
                _this.updateLayoutCellHeights();

                // Height is ready can update panel cell heights
                // -----------------------------------------------
                if (_this.parent.isPanel) {
                    _this.parent.handleComponentUpdated();
                    _this.fromPanelUpdate = false;
                }
                // -----------------------------------------------

                if (!isWorkspace() && _this.parent.isKlip) {
                    _this.parent.handleComponentUpdated();
                }
            }, 200);
        }
    },


    calculateColumnWidths : function() {
        var cw = this.el.width();
        var rw;
        var widthInfo = [];
        var i;
        var wObj;
        var colWidth;
        var pw;
        var openCols;
        var openWidth;

        if (isWorkspace()) cw -= this.cols + 1; // Dashed borders

        rw = cw; // remaining width

        // compute remaining width by subtracting fixed column widths
        for (i = 0; i < this.widths.length; i++) {
            wObj = this.widths[i];
            colWidth = wObj.w;

            if (_.isNumber(colWidth)) {
                rw -= colWidth;
            } else {
                if (colWidth.indexOf("%") != -1){
                    pw = parseFloat(colWidth.substring(0, colWidth.length - 1));
                    if (pw != 0) pw = pw / 100;
                    colWidth = cw * pw; // percent width as pixel

                    rw -= colWidth;
                }
            }

            widthInfo[ wObj.x ] = parseFloat(colWidth);
        }

        openCols = this.cols - this.widths.length;
        if (openCols != 0) {
            openWidth = rw / openCols;
            if (openWidth < 0) openWidth = 0;

            for (i = 0; i < this.cols; i++) {
                if (widthInfo[i]) continue;
                widthInfo[i] = parseFloat(openWidth);
            }
        }

        this.totalWidth = _.reduce(widthInfo, function(memo, num) {
            return memo + num;
        }, 0);
        // TODO hack: why Firefox is one pixel off after jQuery upgrade
        if (isWorkspace()) this.totalWidth += this.cols + ($.browser.mozilla ? 0 : 1);

        return widthInfo;
    },

    updateColumnWidths : function() {
        var calcWidths = this.calculateColumnWidths();
        var i;
        var td;

        this.$container.width(this.totalWidth);

        for (i = 0; i < this.cols; i++) {
            td = this.$widthRow.children().eq(i);
            td.width(calcWidths[i]);
        }

        this.$container.hide();
        this.$container.show();
    },

    /**
     * Placeholder function to store non-throttled version of updateLayoutCellHeights
     */
    updateLayoutCellHeightsNoThrottle: function() {},

    updateLayoutCellHeights : function(emptyOnly) {
        var i;
        var cx;
        var verticalSeps = [];
        var _this = this;
        var $layoutCells;
        var setHeightCells = [];
        var emptyRowCells = [];
        var otherCells = [];
        var rowSpanned = [];
        var matrixInfo;
        var heightCfg;
        var updatedSeparatorHeights = [];

        if (!this.$container) return;

        /**
         * Prevents updating of height when klip content is not finished loading
         */
        if(KF.company.hasFeature("safari_klip_throttle") && $.browser.safari) {
            if ((this.getKlip().isBusy || this.getKlip().isBusy == undefined) && this.initialLayoutCellHeightCalculated) {
                return; // skip updating of height on additional iterations until klip is done loading
            } else {
                if(!this.initialLayoutCellHeightCalculated) {
                    // Updated height on first iteration
                    this.initialLayoutCellHeightCalculated = true;
                    // Save reference to non throttled version of function
                    this.updateLayoutCellHeightsNoThrottle = this.updateLayoutCellHeights;
                    // Throttle function with really large time to prevent it from being called again until resizing/moving panel
                    this.updateLayoutCellHeights = _.throttle(_.bind(this.updateLayoutCellHeights, this), 100000, {leading: true, trailing: false});
                }
            }
        }

        // remove height from vertical separator so cells are resized properly
        for (i = 0 ; i < this.components.length ; i ++){
            cx = this.components[i];
            if (cx.factory_id == "separator" && cx.orientation == "v") {
                cx.el.css("height", "");
                verticalSeps.push(cx);
            }
        }

        $layoutCells = this.$container.children().children("tr").children("td").children("div.layout-cell");



        $layoutCells.each(function() {
            if ($(this).parent().attr("rowspan") !== undefined) {
                rowSpanned.push(this);
            } else {
                matrixInfo = _this.getCellMatrixInfo($(this).parent());
                heightCfg = _this.getHeightConfig(matrixInfo.y, false);

                if (heightCfg) {
                    setHeightCells.push(this);
                } else if (_this.isRowEmpty(matrixInfo.y)) {
                    emptyRowCells.push(this);
                } else {
                    otherCells.push(this);
                }
            }
        });

        if (emptyOnly) {
            _.each(emptyRowCells, function(cell) {
                _this.setLayoutCellHeight($(cell));
            });
        } else {
            _.each(setHeightCells, function(cell) {
                _this.setLayoutCellHeight($(cell));
            });

            _.each(emptyRowCells, function(cell) {
                _this.setLayoutCellHeight($(cell));
            });

            _.each(otherCells, function(cell) {
                _this.setLayoutCellHeight($(cell));
            });

            _.each(rowSpanned, function(cell) {
                _this.setLayoutCellHeight($(cell), true);
            });
        }

        // give height back to vertical separator
        for (i = 0; i < verticalSeps.length; i++) {
            cx = verticalSeps[i];

            /**
             * Directly setting height in loop causes inaccurate height retrieval on next iteration.
             * Instead we store all values in array and set height afterwards. Also we retrieve height
             * from table cell instead of direct parent since parent height incorrectly returns 0.
             */
            if(KF.company.hasFeature("safari_klip_throttle") && $.browser.safari) {
                updatedSeparatorHeights.push(cx.el.closest("td").height());
            } else {
                cx.el.height(cx.el.parent().height() - (cx.el.outerHeight(true) - cx.el.height()));
            }
        }

        /* Fixes safari bug: apply the calculated heights. Must do this here since setting the height in the loop above
           messes with the value of the calculated height in the next iteration
        */
        if(KF.company.hasFeature("safari_klip_throttle") && $.browser.safari) {
            for (i = 0; i < verticalSeps.length; i++) {
                cx = verticalSeps[i]
                if (updatedSeparatorHeights[i] != 0) { // do not set back height if 0
                    cx.el.height(updatedSeparatorHeights[i]);
                }
            }
        }
    },

    setLayoutCellHeight : function($cell, rowSpanned) {
        var matrixInfo = this.getCellMatrixInfo($cell.parent());
        var cx = this.getComponentAtLayoutXY(matrixInfo.x, matrixInfo.y);

        var cellMBP = $cell.outerHeight(true) - $cell.height();

        var heightCfg = this.getHeightConfig(matrixInfo.y, false);
        var setRowHeight = heightCfg ? heightCfg.h - cellMBP : false;

        var $td = $cell.parent();
        var parentHeight = $td.height();

        var cellHeight = false;
        var tdHeight = false;
        var newHeight;
        var $floater;
        var marginBottom;

        if (rowSpanned || setRowHeight === false) {
            newHeight = parentHeight - cellMBP;

            if (!rowSpanned && this.isRowEmpty(matrixInfo.y)) {
                cellHeight = (parentHeight > this.defaultCellHeight ? newHeight: this.defaultCellHeight - cellMBP);
                tdHeight = (parentHeight > this.defaultCellHeight ? newHeight + cellMBP: this.defaultCellHeight);
            } else {
                // TODO hack: solve one pixel off in Chrome
                if (cx && newHeight < cx.el.outerHeight(true)) newHeight = cx.el.outerHeight(true);

                cellHeight = (newHeight <= 0 ? this.defaultCellHeight - cellMBP : newHeight);
                tdHeight = (newHeight <= 0 ? this.defaultCellHeight : newHeight + cellMBP);
            }
        } else {
            if (setRowHeight < 0) setRowHeight = 0;

            cellHeight = setRowHeight;
            tdHeight = setRowHeight + cellMBP;
        }

        $cell.height(cellHeight);
        $td.height(tdHeight);

        if (cx) {
            $floater = $cell.children(".floater");
            if ($floater.length > 0) {
                marginBottom = 0;
                if ($floater.hasClass("middle")) {
                    marginBottom = - Math.ceil(cx.el.outerHeight() / 2);
                } else if ($floater.hasClass("bottom")) {
                    marginBottom = - Math.ceil(cx.el.outerHeight());
                }
                $floater.css("margin-bottom", marginBottom + "px");
            }
        }
    },

    clearRowHeights : function(row) {
        var $layoutCells;

        if (!this.$container) return;

        $layoutCells = this.$container.children().children("tr:eq(" + (row + 1) + ")").children("td").children("div.layout-cell");

        $layoutCells.each(function() {
            $(this).css("height", "");
            $(this).parent().css("height", "");
        });
    },

    /**
     *
     * @param x
     * @param y
     * @return {*}
     * @param create
     */
    getCellData: function(x, y, create) {
        var c;
        var len = this.cellData.length;
        while (--len > -1) {
            c = this.cellData[len];
            if (c.x == x && c.y == y) return c;
        }

        // nothing found...create one on demand
        if (create) {
            c = { x:x, y:y };
            this.cellData.push(c);
            return c;
        }
    },

    initializeForWorkspace : function($super, workspace) {
        $super(workspace);

        this.el.click($.bind(this.onClick, this))
            .on("contextmenu", $.bind(this.onRightClick, this));
    },

    onClick : function(evt) {
        var matrixInfo;

        page.cxContextMenu.hide();

        matrixInfo = this.getSelectionTarget(evt);
        if (!matrixInfo) return;

        this.selectCell(matrixInfo);

        evt.stopPropagation();
    },

    onRightClick : function(evt) {
        var matrixInfo = this.getSelectionTarget(evt);
        if (!matrixInfo) return;

        this.selectCell(matrixInfo);
        this.showCellContextMenu(matrixInfo, evt);

        evt.preventDefault();
        evt.stopPropagation();
    },

    getSelectionTarget : function(evt) {
        var target = false;
        var $target = $(evt.target);
        var td;

        if ( !$target.is(".layout-cell") && !$target.is("td") && !$target.is(".floater") ) return target;

        td =  $target.closest("td");

        return td.data("matrixInfo");
    },


    selectCell : function(cell) {
        var cx = this.getComponentAtLayoutXY(cell.x, cell.y);
        var model;

        if (page.klipIsSelected) page.selectActiveKlip(false);

        // there is a component in the cell...select the Position tab
        // ----------------------------------------------------------
        if (cx) {
            page.setActiveComponent(cx);

            if (cx.layoutConfig) {
                this.setActiveCell(cx.layoutConfig.x, cx.layoutConfig.y);
                page.setActiveLayout(this);
            }

            page.selectWorkspaceTab("layout");
            page.selectMenuItem("component-" + cx.id);
        } else {
            // if the cell is empty, update the 'Position' property form, binding in the values for the current
            // cell
            model = this.getLayoutConstraintsModel( { cell: cell } );

            page.setActiveComponent(false);
            page.setActiveLayout(this);
            this.setActiveCell(cell.x, cell.y);

            page.displayWorkspaceTabs(["layout"], "layout");
            page.updatePropertySheet(model.props, model.groups, page.layoutPropertyForm);
            page.selectMenuItem(false);
        }
    },

    showCellContextMenu : function(cell, evt) {
        var cx = this.getComponentAtLayoutXY(cell.x, cell.y);

        var menuModel = this.getLayoutContextMenuModel(cell, cx);

        page.cxContextMenu.setMenuItems(menuModel);
        page.cxContextMenu.show(evt, {corner:"left top"});
    },


    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Column",
            controls: [
                [
                    {type:"add", actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"x", value:1}},
                    {type:"remove", disabled:(this.cols <= 1), actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"x", value:-1}}
                ]
            ]
        });

        model.push({
            name:"Row",
            controls: [
                [
                    {type:"add", actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"y", value:1}},
                    {type:"remove", disabled:(this.rows <= 1), actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"y", value:-1}}
                ]
            ]
        });

        return model;
    },

    addAddContextMenuItem: function(model, subComponent) {
        var labelId = encodeForId(subComponent.label);

        model.push({
            id: "menuitem-add_" + labelId,
            text: "Add " + subComponent.label,
            actionId: "panel.change_dimension",
            actionArgs: {
                cx: this,
                dimension: subComponent.dimension,
                value: 1
            }
        });
    },

    addRemoveSubComponentContextMenuItem: function(model, subComponent) {
        var labelId = encodeForId(subComponent.label);
        var subItems = [];
        var i;
        var len = this[subComponent.length];

        var removeSubComponent = { id:"menuitem-remove_" + labelId, text:"Remove " + subComponent.label };

        if (len > 1) {
            for (i = 0; i < len; i++) {
                subItems.push({
                    id: "menuitem-" + labelId + "_" + i,
                    text: subComponent.label + " " + (i + 1),
                    actionId: "panel.change_dimension",
                    actionArgs: {
                        cx: this,
                        dimension: subComponent.dimension,
                        value: -1,
                        origin: {
                            x: subComponent.dimension == "x" ? i : 0,
                            y: subComponent.dimension == "x" ? 0 : i
                        }
                    }
                });
            }

            removeSubComponent.submenu = {
                width:"150px",
                menuItems: subItems
            };
        } else {
            removeSubComponent.disabled = true;
        }

        model.push(removeSubComponent);
    },

    getLayoutContextMenuModel : function(cell, cx) {
        var model = [];

        var clipboardContents = $.jStorage.get("component_clipboard");

        model.push(
            { id:"menuitem-cut", text:"Cut", disabled:!cx, actionId:"copy_cut_component", actionArgs:{cx:cx, remove:true} },
            { id:"menuitem-copy", text:"Copy", disabled:!cx, actionId:"copy_cut_component", actionArgs:{cx:cx} },
            { id:"menuitem-paste", text: "Paste", disabled:!clipboardContents, actionId:"paste_component", actionArgs:{parent:this, cell:cell, cx:cx} },
            { id:"menuitem-remove", text:"Clear", disabled:!cx, actionId:"remove_component", actionArgs:{cx:cx} },
            { id:"separator-column", separator:true },
            { id:"menuitem-insert_left", text:"Insert column left", actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"x", value:1, before:true} },
            { id:"menuitem-insert_right", text:"Insert column right", actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"x", value:1} },
            { id:"menuitem-delete_column", text:"Delete column", disabled:(this.cols <= 1), actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"x", value:-1} },
            { id:"separator-row", separator:true },
            { id:"menuitem-insert_above", text:"Insert row above", actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"y", value:1, before:true} },
            { id:"menuitem-insert_below", text:"Insert row below", actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"y", value:1} },
            { id:"menuitem-delete_row", text:"Delete row", disabled:(this.rows <= 1), actionId:"panel.change_dimension", actionArgs:{cx:this, dimension:"y", value:-1} }
        );

        return model;
    },

    /**
     * sets the cell currently active in the workspace.  the active cell is the one that will receive
     * configuration events.  the active cell can be cleared by setting the first argument to false
     *
     * @param x {int|bool} if false clears the active cell
     * @param y
     */
    setActiveCell : function(x,y) {
        var cell;
        var cx;

        if (this.activeCell) {
            cell = this.getMatrixCell(this.activeCell.x, this.activeCell.y);
            cx = this.getComponentAtLayoutXY(this.activeCell.x, this.activeCell.y);

            if (cell.$el) cell.$el.removeClass("layout-active-cell");
            this.activeCell = undefined;
        }

        if (x === false) {
            page.controlPalette.updateItems(cx ? cx.getWorkspaceControlModel() : []);
            return;
        }

        cell = this.getMatrixCell(x, y);

        //cell may not be rendered if another row spans it
        if (cell.$el) {
            cell.$el.addClass("layout-active-cell");
        }

        this.activeCell = cell;

        page.controlPalette.updateItems(this.getWorkspaceControlModel());
    },


    getComponentsToRemove: function(originCell, dimension) {
        var componentsToRemove = [];
        var i, x, y, cx;
        var length = (dimension === "x" ? this.rows : this.cols);

        for (i = 0; i < length; i++) {
            x = (dimension === "x" ? originCell[dimension] : i);
            y = (dimension === "x" ? i : originCell[dimension]);

            cx = this.getComponentAtLayoutXY(x, y);

            if (cx) {
                componentsToRemove.push(cx);
            }
        }

        return componentsToRemove;
    },

    /**
     * Remove a row or column from grid including any components, spans,
     * widths or heights related to that row/col
     *
     * @param originCell - remove row/col that this cell belongs to
     * @param dimension - "x" remove column, "y" remove row
     */
    removeRowOrColumn : function(originCell, dimension) {
        var i, x, y, cd;
        var length = (dimension == "x" ? this.rows : this.cols);
        var originDim;
        var matrixInfo;

        // remove span data from row/col
        // -----------------------------
        originDim = (dimension == "x" ? originCell.x : originCell.y);
        i = 0;
        while (i < this.cellData.length) {
            cd = this.cellData[i];
            if (cd[dimension] == originDim) {
                this.cellData.splice(i, 1);
                i--;
            }

            i++;
        }

        // reduce span size if cell was spanned
        // ------------------------------------
        for (i = 0; i < length; i++) {
            x = (dimension == "x" ? originCell[dimension] : i);
            y = (dimension == "x" ? i : originCell[dimension]);

            matrixInfo = this.getMatrixCell(x, y, true);

            if (matrixInfo.x != x || matrixInfo.y != y) {
                cd = this.getCellData(matrixInfo.x, matrixInfo.y);

                if (cd && (cd.w || cd.h)) {
                    if (dimension == "x") {
                        if (cd.w) cd.w -= 1;
                    } else {
                        if (cd.h) cd.h -= 1;
                    }
                }
            }
        }

        // remove specified width/height for col/row
        // -----------------------------------------
        if (dimension == "x") {
            this.removeWidthConfig(originCell.x);
        } else {
            this.removeHeightConfig(originCell.y);
        }
    },

    /**
     * Shift the components and spans left/right or up/down depending on
     * if a row or column was added or removed
     *
     * @param originCell - make shifts relative to this cell
     * @param dimension - "x" for shifting columns, "y" for shifting rows
     * @param value - 1 = row/col added, -1 = row/col removed
     * @param includeOrigin - true if should also shift the row/col selected
     */
    shiftDimension : function(originCell, dimension, value, includeOrigin) {
        var i;
        var j;
        var cx;
        var originDim;
        var cd;
        var w;
        var h;

        // shift components from other rows or cols
        // ----------------------------------------
        var colInit = (dimension == "x" ? originCell.x + (includeOrigin ? 0 : 1) : 0);
        var rowInit = (dimension == "x" ? 0 : originCell.y + (includeOrigin ? 0 : 1));

        var shiftedComps = [];
        for (i = colInit; i < this.cols; i++) {
            for (j = rowInit; j < this.rows; j++) {
                cx = this.getComponentAtLayoutXY(i, j);

                if (cx) {
                    cx.layoutConfig[dimension] += value;
                    cx.layoutConfig.shifted = true;
                    shiftedComps.push(cx);
                }
            }
        }

        for (i = 0; i < shiftedComps.length; i++) {
            shiftedComps[i].layoutConfig.shifted = undefined;
        }

        // shift spans/properties from other rows or cols
        // ----------------------------------------------
        originDim = (dimension == "x" ? originCell.x : originCell.y) + (includeOrigin ? -1 : 0);
        for (i = 0; i < this.cellData.length; i++) {
            cd = this.cellData[i];
            if (cd[dimension] > originDim) {
                cd[dimension] += value;
            }
        }

        // shift widths/heights from other rows or cols
        // --------------------------------------------
        if (dimension == "x") {
            for (i = 0; i < this.widths.length; i++) {
                w = this.widths[i];
                if (w[dimension] > originDim) {
                    w[dimension] += value;
                }
            }
        } else {
            for (i = 0; i < this.rowHeights.length; i++) {
                h = this.rowHeights[i];
                if (h[dimension] > originDim) {
                    h[dimension] += value;
                }
            }
        }
    },


    /**
     * fetchs the widthConfig information for the given column.
     * @param colx
     * @param create lazily create a config object if none is found
     * @return {*}
     */
    getWidthConfig : function(colx,create) {
        var i;
        var wo;

        for( i = 0 ; i < this.widths.length; i++) {
            if ( this.widths[i].x == colx ) {
                return this.widths[i];
            }
        }


        if ( create ){
            wo = {x:colx};
            this.widths.push(wo);
            return wo;
        }
    },

    removeWidthConfig : function(colX) {
        var i = 0;
        while (i < this.widths.length) {
            if (this.widths[i].x == colX) {
                this.widths.splice(i, 1);
                i--;
            }

            i++;
        }
    },

    /**
     * fetchs the heightConfig information for the given row.
     * @param rowY
     * @param create lazily create a config object if none is found
     * @return {*}
     *   - y : the row this config applies to
     *   - h : the height
     */
    getHeightConfig : function(rowY,create) {
        var i;
        var ho;

        for(i = 0 ; i < this.rowHeights.length; i++) {
            if ( this.rowHeights[i].y == rowY ) {
                return this.rowHeights[i];
            }
        }

        if ( create ){
            ho = {y:rowY};
            this.rowHeights.push(ho);
            return ho;
        }
    },

    removeHeightConfig : function(rowY) {
        var i = 0;
        while (i < this.rowHeights.length) {
            if (this.rowHeights[i].y == rowY) {
                this.rowHeights.splice(i, 1);
                i--;
            }

            i++;
        }
    },

    /**
     * matrix cells are dynamically computed after each update and contain information to help
     * in resolving x,y layout cells to actual DOM TDs.  Matrix cells should contain:
     * {
     *   spanned: <bool>,
     *   origin: {x,y},
     *   $el: $jqueryElement,
     *   x: <int>,
     *   y: <int>
     * }
     * @param x
     * @param y
     * @param resolveToOrigin
     */
    getMatrixCell : function(x,y,resolveToOrigin) {
        var cell;

        if ( !this.cellMatrix || x == undefined || y == undefined || x >= this.cols || y >= this.rows || x < 0 || y < 0  ) return false;

        cell = this.cellMatrix[y][x];
        if ( resolveToOrigin && cell && cell.origin ){
            cell = this.getMatrixCell( cell.origin.x, cell.origin.y );
        }
        return cell;
    },


    /**
     * returns information containing the the x/y and span widths of the given TD cell.
     * @param $td
     * @return {*}
     */
    getCellMatrixInfo : function($td) {
        return $td.data("matrixInfo");
    },

    /**
     * apply spanning information to the cellMatrix
     * cfg.w = colspan
     * cfg.h = rowspan
     */
    spanMatrix : function(x, y, cfg) {
        var w;
        var h;
        var i;
        var j;

        // handle colspan and rowspan
        // --------------------------
        if (cfg.w || cfg.h) {
            w = cfg.w ? cfg.w : 1;
            h = cfg.h ? cfg.h : 1;

            for (i = 0; i < w; i++) {
                for (j = 0; j < h; j++) {
                    if (i != 0 || j != 0) {
                        if (this.cellMatrix[y+j] !== undefined) {
                            this.cellMatrix[y+j][x+i] = { spanned:true, origin:{x:x,y:y} };
                        }
                    }
                }
            }
        }
    },


    /**
     * validator that checks to see if spanning is allowed from a cell.  spanning is allowed if:
     *
     *   - amount is > 0
     *   - spanning does not exceed the bounds of the grid
     *   - spanning does not overlap a previously defined spanned area
     *
     * @param x position of cell to start span from
     * @param y position of cell to start span from
     * @param dir
     * @param spanIncreaseAmount  number of cells to span
     * @return {boolean}
     */
    spanAllowed : function(x,y,dir,spanIncreaseAmount ) {
        var cell;
        var maxCells;
        var valueToUse;
        var currentPos;
        var currentCell;
        var i;

        if( x == undefined || y == undefined ) return false;

        spanIncreaseAmount = parseInt(spanIncreaseAmount);

        // amount must be > 0
        if ( spanIncreaseAmount < 0 ) return false;

        cell = this.getMatrixCell(x,y);

        // if the cell has an origin, we can't apply spanning -- spanning can only be applied to a origin cells
        if ( cell.origin ) return false;


        switch(dir) {
            case "x":
                maxCells = this.cols;
                valueToUse = x;
                break;
            case "y":
                valueToUse = y;
                maxCells = this.rows;
        }

        // return false if there aren't enough cells to span to
        if ( valueToUse + spanIncreaseAmount > maxCells ) return false;

        // in order to span, there must be enough unspanned cells in the direction
        // to travese -- eg, we can't overlap other spanned regions
        // ------------------------------------------------------------------------
        currentPos = { x : x, y : y };
        for( i = valueToUse; i < (valueToUse + spanIncreaseAmount); i++ ) {
            currentPos[dir]++;
            currentCell = this.getMatrixCell( currentPos.x, currentPos.y );

            // current cell has an origin and it is not the same as the initial cell, return false;
            // ------------------------------------------------------------------------------------
            if ( currentCell.origin && ( currentCell.origin.x != x || currentCell.origin.y != y )  ){
                return false;
            }
            // current cell has spanning info, return false
            // --------------------------------------------
            if ( currentCell.w || currentCell.h ){
                return false;
            }
        }

        return true;
    },

    /**
     * Returns the maximum spanning allowed for a cell.  spanning is allowed if:
     *
     *   - amount is > 0
     *   - spanning does not exceed the bounds of the grid
     *   - spanning does not overlap a previously defined spanned area
     *
     * @param x position of cell to start span from
     * @param y position of cell to start span from
     * @param dir - "x" or "y"
     * @return {*}
     */
    getMaxSpanAllowed : function(x, y, dir) {
        var cell;
        var maxCells;
        var valueToUse;
        var otherDir;
        var otherValue;
        var otherSpan;
        var maxSpan = 1;
        var currentPos;
        var currentCell;
        var i;
        var noSpan;
        var j;

        if( x == undefined || y == undefined ) return false;

        cell = this.getMatrixCell(x,y);

        switch(dir) {
            case "x":
                maxCells = this.cols;
                valueToUse = x;
                otherDir = "y";
                otherValue = y;
                otherSpan = cell.h ? cell.h : 1;
                break;
            case "y":
                maxCells = this.rows;
                valueToUse = y;
                otherDir = "x";
                otherValue = x;
                otherSpan = cell.w ? cell.w : 1;
                break;
        }

        currentPos = { x : x, y : y };
        for (i = valueToUse; i < maxCells - 1; i++) {
            currentPos[dir]++;
            currentPos[otherDir] = otherValue;

            noSpan = true;
            for (j = 0; j < otherSpan; j++) {
                currentCell = this.getMatrixCell(currentPos.x, currentPos.y);

                // current cell has an origin and it is not the same as the initial cell, break
                // ------------------------------------------------------------------------------------
                if (currentCell.origin && (currentCell.origin.x != x || currentCell.origin.y != y)) {
                    noSpan = false;
                    break;
                }
                // current cell has spanning info, break
                // --------------------------------------------
                if ((currentCell.w && currentCell.w != 1) || (currentCell.h && currentCell.h != 1)) {
                    noSpan = false;
                    break;
                }

                currentPos[otherDir]++;
            }

            if (noSpan) {
                maxSpan++;
            } else {
                break;
            }
        }

        return maxSpan;
    },

    /**
     * sort predicate function to order components from top-left to bottom-right
     * @param a
     * @param b
     * @return {*}
     */
    sortComponentXY : function(a,b) {
        if (!a.layoutConfig) return 1;
        if (!b.layoutConfig) return -1;
        if(a.layoutConfig.y != b.layoutConfig.y) return a.layoutConfig.y - b.layoutConfig.y;
        return a.layoutConfig.x - b.layoutConfig.x;
    },


    /**
     * returns the component assigned to the given x,y layout constraint (as opposed to the matrix cell).
     * @param x
     * @param y
     * @return {*}
     */
    getComponentAtLayoutXY : function(x,y) {
        var i;
        var cx;

        for(i = 0 ; i < this.components.length; i++ ){
            cx = this.components[i];
            if ( !cx.layoutConfig.shifted && cx.layoutConfig && cx.layoutConfig.x == x && cx.layoutConfig.y == y ) return cx;
        }
        return false;
    },

    isRowEmpty : function(y) {
        var i;
        var cx;
        var cd;

        for (i = 0; i < this.components.length; i++) {
            cx = this.components[i];
            if (cx.layoutConfig && cx.layoutConfig.y == y) {
                cd = this.getCellData(cx.layoutConfig.x, cx.layoutConfig.y, false);
                if (!cd || !cd.h) return false;
            }
        }
        return true;
    },

    /**
     * panels are responsible for creating the property model for their child components.
     * generate a constraints model for the given component (which should be a current child).
     */
    getLayoutConstraintsModel : function(config) {
        var model = { props:[], groups:[] };
        var cx = config.cx;
        var defaultCd;
        var cd;
        var spanData;
        var colWidth;
        var rowHeight;

        if ( cx ) {
            defaultCd = { x: cx.layoutConfig.x, y: cx.layoutConfig.y };
            cd = _.extend(defaultCd, this.getCellData( cx.layoutConfig.x, cx.layoutConfig.y, false ));
        } else {
            cd = config.cell;
        }

        spanData = {
            w: cd.w || 1,
            h: cd.h || 1,
            useSamePadding: !(cd.cellPadding instanceof Array),
            padding: !(cd.cellPadding instanceof Array) ? (cd.cellPadding !== undefined ? cd.cellPadding : this.defaultCellPadding) : cd.cellPadding[0],
            paddingTop: !(cd.cellPadding instanceof Array) ? cd.cellPadding : cd.cellPadding[0],
            paddingRight: !(cd.cellPadding instanceof Array) ? cd.cellPadding : cd.cellPadding[1],
            paddingBottom: !(cd.cellPadding instanceof Array) ? cd.cellPadding : cd.cellPadding[2],
            paddingLeft: !(cd.cellPadding instanceof Array) ? cd.cellPadding : cd.cellPadding[3],
            align: cd.alignment || 0
        };

        model.groups = ["span", "cell-dimensions", "padding", "alignment"];

        model.props.push({
            type:"text",
            id:"colSpan",
            label:"Column Span",
            group:"span",
            help:{ link:"klips/cell-span" },
            width:38,
            value: spanData.w,
            isNumeric:true,
            readOnly:false,
            min:1,
            max: this.getMaxSpanAllowed(cd.x, cd.y, "x")
        });

        model.props.push({
            type:"text",
            id:"rowSpan",
            label:"Row Span",
            group:"span",
            help:{ link:"klips/cell-span" },
            width:38,
            value: spanData.h,
            isNumeric:true,
            readOnly:false,
            min:1,
            max: this.getMaxSpanAllowed(cd.x, cd.y, "y")
        });


        colWidth = this.getWidthConfig(cd.x);
        if (colWidth) colWidth = colWidth.w;

        model.props.push({
            type:"select",
            id:"specifyWidth",
            label:"Cell Width",
            group:"cell-dimensions",
            width:"125px",
            fieldMargin:"9px 0 0 0",
            displayWhen: {colSpan:1},
            options: [{value:"auto", label:"Auto"}, {value:"cust", label:"Custom..."}],
            selectedValue: (colWidth === undefined ? "auto" : "cust")
        });

        model.props.push({
            type: "text",
            id: "width",
            group: "cell-dimensions",
            value: colWidth,
            width:36,
            displayWhen: {colSpan:1, specifyWidth:"cust"},
            minorLabels: [
                { label:"px or % of " + KF.company.get("brand", "widgetName") + " width", position:"right", css:"quiet italics" }
            ],
            flow:true
        });


        rowHeight = this.getHeightConfig(cd.y);
        if ( rowHeight ) rowHeight = rowHeight.h;

        model.props.push({
            type:"select",
            id:"specifyHeight",
            label:"Cell Height",
            group:"cell-dimensions",
            width:"125px",
            displayWhen: {rowSpan:1},
            options: [{value:"auto", label:"Auto"}, {value:"cust", label:"Custom..."}],
            selectedValue: (rowHeight === undefined ? "auto" : "cust"),
            breakFlow:true
        });

        model.props.push({
            type:"text",
            id:"height",
            value: rowHeight,
            group:"cell-dimensions",
            width:36,
            displayWhen: {rowSpan:1, specifyHeight:"cust"},
            minorLabels: [
                { label:"px", position:"right", css:"quiet italics" }
            ],
            flow:true
        });

        model.props.push({
            type:"checkboxes",
            id:"useSamePadding",
            label:"Cell Padding",
            help:{ link:"klips/cell-padding" },
            fieldMargin:"9px 0 0 0",
            group:"padding",
            options: [
                {value:true, label:"Same for all sides", checked:spanData.useSamePadding}
            ]
        });

        model.props.push({
            type:"text",
            id:"cellPadding",
            fieldMargin:"0 0 9px 0",
            group:"padding",
            minorLabels: [
                {label: "px", position: "right", css:"quiet italics"}
            ],
            displayWhen: {useSamePadding:true},
            width:38,
            value: spanData.padding,
            isNumeric:true,
            min:0
        });

        model.props.push({
            type:"text",
            id:"cellPaddingTop",
            group:"padding",
            minorLabels: [
                { label:"Top:", position:"left", width:"30px" }
            ],
            displayWhen: {useSamePadding:false},
            width:38,
            fieldMargin:"0 0 9px 0",
            value: spanData.paddingTop,
            isNumeric:true,
            min:0
        });

        model.props.push({
            type:"text",
            id:"cellPaddingBottom",
            group:"padding",
            minorLabels: [
                { label:"Bottom:", position:"left", width:"53px" }
            ],
            displayWhen: {useSamePadding:false},
            width:38,
            fieldMargin:"0 0 9px 0",
            value: spanData.paddingBottom,
            isNumeric:true,
            min:0,
            flow:{padding:"40px"}
        });

        model.props.push({
            type:"text",
            id:"cellPaddingLeft",
            group:"padding",
            minorLabels: [
                { label:"Left:", position:"left", width:"30px" }
            ],
            displayWhen: {useSamePadding:false},
            width:38,
            fieldMargin:"0 0 9px 0",
            value: spanData.paddingLeft,
            isNumeric:true,
            min:0,
            breakFlow:true
        });

        model.props.push({
            type:"text",
            id:"cellPaddingRight",
            group:"padding",
            minorLabels: [
                { label:"Right:", position:"left", width:"53px" }
            ],
            displayWhen: {useSamePadding:false},
            width:38,
            fieldMargin:"0 0 9px 0",
            value: spanData.paddingRight,
            isNumeric:true,
            min:0,
            flow:{padding:"40px"}
        });


        model.props.push({
            type:"button_group_ex",
            id:"alignment",
            label:"Alignment",
            help:{ link:"klips/cell-alignment" },
            group:"alignment",
            options: Props.Defaults.verticalAlign,
            selectedValue: spanData.align
        });

        return model;
    },


    getPropertyModel : function($super) {
        var model = $super();

        model.push({
            type:"text",
            id:"cols",
            label:"Columns",
            value: this.cols,
            group:"grid-dimensions",
            isNumeric:true,
            readOnly:false,
            min:1,
            max:50,
            width:38
        });

        model.push({
            type:"text",
            id:"rows",
            label:"Rows",
            value: this.rows,
            group:"grid-dimensions",
            isNumeric:true,
            readOnly:false,
            min:1,
            max:50,
            width:38
        });


        return model;
    },


    getPropertyModelGroups : function() {
        return ["grid-dimensions"];
    },


    /**
     * called when Position panel fields change and this layout is the activeLayout.
     * @param $super
     * @param evt
     * @param cx
     */
    onLayoutPropertiesChange: function($super,evt,cx) {
        var updatePropertySheet, model;
        var matrixCell = this.activeCell;
        var cd;
        var val;
        var update;
        var custWidth;
        var num;
        var wObj;
        var custHeight;
        var hObj;
        var useSamePadding;
        var paddingVal;

        if ( cx ) {
            matrixCell = this.getMatrixCell( cx.layoutConfig.x, cx.layoutConfig.y );
        }

        if (!matrixCell) return;

        cd = this.getCellData(matrixCell.x, matrixCell.y, true);
        val = parseFloat( evt.value );
        update = false;

        switch(evt.name) {
            case "colSpan":
            {
                if (val == 1 && cd.w === undefined) break;

                if (val != cd.w) {
                    updatePropertySheet = (val == 1 || cd.w === undefined);

                    cd.w = ( val ==  1 ? undefined : val );
                    update = true;

                    if (updatePropertySheet) {
                        model = this.getLayoutConstraintsModel( { cell: cd } );
                        page.updatePropertySheet(model.props, model.groups, page.layoutPropertyForm);
                    }
                }

                break;
            }
            case "rowSpan":
            {
                if (val == 1 && cd.h === undefined) break;

                if (val != cd.h) {
                    updatePropertySheet = (val == 1 || cd.h === undefined);

                    cd.h = ( val == 1 ? undefined : val );
                    update = true;

                    if (updatePropertySheet) {
                        model = this.getLayoutConstraintsModel( { cell: cd } );
                        page.updatePropertySheet(model.props, model.groups, page.layoutPropertyForm);
                    }
                }

                break;
            }
            case "specifyWidth":
            {
                if (evt.value == "auto") {
                    this.removeWidthConfig(matrixCell.x);
                } else if (evt.value == "cust") {
                    custWidth = $("#f-width").find("input").val();

                    this.onLayoutPropertiesChange({name:"width", value:custWidth, dropdownChanged:true});
                }

                update = true;
                break;
            }
            case "width":
            {
                num = val;

                if (evt.value.indexOf("%") == evt.value.length - 1) {
                    num = parseFloat(evt.value.substring(0, evt.value.length - 1));
                    val = evt.value;
                }

                if (!_.isNumber(num) || _.isNaN(num)) {
                    this.removeWidthConfig(matrixCell.x);
                } else {
                    wObj = this.getWidthConfig(matrixCell.x, true);
                    wObj.w = val;
                }

                update = true;
                break;
            }
            case "specifyHeight":
            {
                if (evt.value == "auto") {
                    this.removeHeightConfig(matrixCell.y);
                } else if (evt.value == "cust") {
                    custHeight = $("#f-height").find("input").val();

                    this.onLayoutPropertiesChange({name:"height", value:custHeight, dropdownChanged:true});
                }

                update = true;
                break;
            }
            case "height":
            {
                if (!_.isNumber(val) || _.isNaN(val)) {
                    this.removeHeightConfig(matrixCell.y);
                } else {
                    hObj = this.getHeightConfig(matrixCell.y,true);
                    hObj.h = val;
                }

                update = true;
                break;
            }
            case "useSamePadding":
            {
                useSamePadding = evt.value;

                if (!useSamePadding) {
                    paddingVal = (cd.cellPadding !== undefined) ? cd.cellPadding : this.defaultCellPadding;

                    cd.cellPadding = [paddingVal /* top */, paddingVal /* right */, paddingVal /* bottom */, paddingVal /* left */];
                } else {
                    cd.cellPadding = cd.cellPadding[0] == this.defaultCellPadding ? undefined : cd.cellPadding[0];
                }

                model = this.getLayoutConstraintsModel( { cell: cd } );
                page.updatePropertySheet(model.props, model.groups, page.layoutPropertyForm);

                update = true;
                break;
            }
            case "cellPadding":
            {
                update = cd.cellPadding != val;

                cd.cellPadding = ( val == this.defaultCellPadding ? undefined : val );

                break;
            }
            case "cellPaddingTop":
            {
                update = cd.cellPadding[0] != val;

                cd.cellPadding[0] = val;

                break;
            }
            case "cellPaddingRight":
            {
                update = cd.cellPadding[1] != val;

                cd.cellPadding[1] = val;

                break;
            }
            case "cellPaddingBottom":
            {
                update = cd.cellPadding[2] != val;

                cd.cellPadding[2] = val;

                break;
            }
            case "cellPaddingLeft":
            {
                update = cd.cellPadding[3] != val;

                cd.cellPadding[3] = val;

                break;
            }
            case "alignment":
                cd.alignment = ( val == 0 ? undefined : val );
                update = true;
                break;
        }

        if (update) {
            this.invalidateAndQueue();
        }
    }

});;/****** cx.picto_data.js *******/ 

/*global Component:false, CXTheme:false, bindValues:false, page:false, Props:false */

Component.PictoData = $.klass(Component.Proxy, {

    displayName: "Image Set",
    factory_id:"picto_data",
    isMovable: { label:"Image Set" },
    valueId: 1,

    valueType:"num",
    stepValue:1,
    steps:1,
    cropping:"crop",
    rounding:false,
    imageType:"man",
    useSamePadding:true,
    padding:2,
    colorFilter:"none",
    valueColor:"cx-theme_blue_3",
    baseColor:"cx-theme_ccc",

    isDstRoot: true,
    canAggregate: true,
    canSetAggregation: true,
    canHaveDataSlots: true,

    fmt: "num",

    checkDeletable : function($super, ctx) {
        var isDeletable = $super();
        var siblings;

        if (isDeletable) {
            siblings = this.parent.getChildrenByType("picto_data");
            isDeletable = (siblings.length > 1);
        }

        return isDeletable;
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Image Set",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"picto_data", role:"value", sibling:this}},
                    {type:"remove", disabled:!this.checkDeletable(), actionId:"remove_component", actionArgs:{cx:this}}
                ]
            ]
        });

        this.addDataSlotControl(model);

        return model;
    },

    onKlipAssignment : function($super) {
        $super();

        if (this.imageType == "custom" && this.customImageAsset) {
            this.getCustomImageUrl();
        }
    },

    configure : function($super, config, update) {
        $super(config);
        bindValues(this,config,[
            "valueId",
            "valueType",
            "stepValue",
            "steps",
            "cropping",
            "rounding",
            "imageType",
            "customImageAsset",
            "useSamePadding",
            "padding",
            "paddingTop",
            "paddingRight",
            "paddingBottom",
            "paddingLeft",
            "colorFilter",
            "valueColor",
            "baseColor"
        ]);

        if (config.stepValue !== undefined) {
            this.stepValue = parseInt(config.stepValue);

            if (_.isNaN(this.stepValue) || this.stepValue < 1) this.stepValue = 1;
        }

        if (config.steps !== undefined) {
            this.steps = parseInt(config.steps);

            if (_.isNaN(this.steps) || this.steps < 1) this.steps = 1;
        }

        if (config.useSamePadding !== undefined) {
            if (config["~propertyChanged"]) {
                if (!config.useSamePadding) {
                    this.paddingTop = this.padding;
                    this.paddingRight = this.padding;
                    this.paddingBottom = this.padding;
                    this.paddingLeft = this.padding;

                    this.padding = [this.paddingTop, this.paddingRight, this.paddingBottom, this.paddingLeft];
                } else {
                    this.padding = this.paddingTop;
                }

                page.updatePropertySheet(this.getPropertyModel(), this.getPropertyModelGroups(), page.componentPropertyForm);
            } else {
                if (!config.useSamePadding) {
                    this.paddingTop = this.padding[0];
                    this.paddingRight = this.padding[1];
                    this.paddingBottom = this.padding[2];
                    this.paddingLeft = this.padding[3];

                } else {
                    this.paddingTop = this.padding;
                    this.paddingRight = this.padding;
                    this.paddingBottom = this.padding;
                    this.paddingLeft = this.padding;
                }
            }
        }

        if (config["~propertyChanged"] && config.padding !== undefined) {
            this.padding = parseInt(config.padding);
        }

        if (config["~propertyChanged"] && (config.paddingTop || config.paddingRight || config.paddingBottom || config.paddingLeft)) {
            if (config.paddingTop) this.padding[0] = parseInt(config.paddingTop);
            if (config.paddingRight) this.padding[1] = parseInt(config.paddingRight);
            if (config.paddingBottom) this.padding[2] = parseInt(config.paddingBottom);
            if (config.paddingLeft) this.padding[3] = parseInt(config.paddingLeft);
        }

        if ( (this.imageType == "custom") && this.customImageAsset && ( config.imageType  || config.customImageAsset ) ) {
            this.getCustomImageUrl();
        }
    },

    getCustomImageUrl : function() {
        var klip = this.getKlip();
        var _this = this;

        if (!klip) return;

        klip.dashboard.getAssetInfo(
            this.customImageAsset,
            function(data) {
                _this.customImageUrl = data.perm_url;
                _this.invalidateAndQueue();
            }
        );
    },

    serialize : function($super) {
        var cfg = $super();

        if (this.valueType == "num") {
            this.steps = 1;
        }

        if (this.valueType == "pct") {
            this.stepValue = 1;
        }

        bindValues(cfg, this, ["valueId","valueType","stepValue","steps","cropping","rounding","imageType","customImageAsset","useSamePadding","padding","colorFilter","valueColor","baseColor"],
            {
                valueType:"num",
                stepValue:1,
                steps:1,
                cropping:"crop",
                rounding:false,
                useSamePadding:true,
                padding:2,
                colorFilter:"none",
                valueColor:"cx-theme_blue_3",
                baseColor:"cx-theme_ccc"
            }, true);
        return cfg;
    },

    getPropertyModel : function($super) {
        var model = $super();

        model.push({
            type:"select",
            id:"valueType",
            group:"format",
            label:"Type",
            options: [{value:"num", label:"Quantity"}, {value:"pct", label:"Percentage"}],
            selectedValue: this.valueType,
            help: { link:"klips/pictograph/image-set" }
        });

        model.push({
            type:"text",
            id:"stepValue",
            group:"format",
            displayWhen: { valueType:"num" },
            label:"Increment",
            width:"50px",
            isNumeric:true,
            min:1,
            value: this.stepValue
        });

        model.push({
            type:"text",
            id:"steps",
            group:"format",
            displayWhen: { valueType:"pct" },
            label:"Steps",
            width:"50px",
            isNumeric:true,
            min:1,
            value: this.steps
        });

        this.addDstProperties(model);

        model.push({
            type:"select",
            id:"imageType",
            group:"image",
            label:"Image",
            options: Props.Defaults.pictoImages,
            selectedValue: this.imageType,
            help: { link:"klips/pictograph/image-set" }
        });

        model.push({
            type:"asset_select" ,
            id:"customImageAsset",
            group:"image",
            displayWhen: { imageType:"custom" },
            buttonText:"Select Image...",
            assetTitle:"Upload an image file.",
            assetContext:"picto-image",
            assetType:"picto-image",
            selectedValue: this.customImageAsset,
            fieldMargin:"0 0 12px 0"
        });

        model.push({
            type:"select",
            id:"colorFilter",
            group:"image",
            label:"Colorize",
            options: [{value:"none", label:"None"}, {value:"color", label:"Solid color..."}, {value:"scale", label:"Color overlay..."}],
            selectedValue: this.colorFilter,
            fieldMargin:"0 0 9px 0"
        });

        model.push({
            type:"color_picker",
            id:"valueColor",
            group:"image",
            displayWhen: { colorFilter:["color","scale"] },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.valueColor,
            flow:true
        });

        model.push({
            type:"color_picker",
            id:"baseColor",
            group:"image",
            label:"Base Color",
            valueType:"theme",
            defaultColour: {rgb:"#cccccc", theme:"cx-theme_ccc"},
            selectedValue:this.baseColor,
            fieldMargin:"0 0 9px 0"
        });


        model.push({
            type:"checkboxes",
            id:"useSamePadding",
            label:"Padding",
            group:"padding",
            fieldMargin:"9px 0 0 0",
            options:[
                {
                    value:true,
                    label:"Same for all sides",
                    checked:this.useSamePadding
                }
            ]
        });

        model.push({
            type:"text",
            id:"padding",
            group:"padding",
            minorLabels: [
                {label: "px", position: "right", css:"quiet italics"}
            ],
            width:50,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:true},
            value: this.padding,
            isNumeric:true,
            min:0
        });

        model.push({
            type:"text",
            id:"paddingTop",
            group:"padding",
            minorLabels: [
                { label:"Top:", position:"left", width:"30px" }
            ],
            width:50,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:false},
            value: this.paddingTop,
            isNumeric:true,
            min:0
        });

        model.push({
            type:"text",
            id:"paddingBottom",
            group:"padding",
            minorLabels: [
                { label:"Bottom:", position:"left", width:"53px" }
            ],
            width:50,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:false},
            value: this.paddingBottom,
            isNumeric:true,
            min:0,
            flow:{padding:"40px"}
        });

        model.push({
            type:"text",
            id:"paddingLeft",
            group:"padding",
            minorLabels: [
                { label:"Left:", position:"left", width:"30px" }
            ],
            width:50,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:false},
            value: this.paddingLeft,
            isNumeric:true,
            min:0,
            breakFlow:true
        });

        model.push({
            type:"text",
            id:"paddingRight",
            group:"padding",
            minorLabels: [
                { label:"Right:", position:"left", width:"53px" }
            ],
            width:50,
            fieldMargin:"0 0 9px 0",
            displayWhen: {useSamePadding:false},
            value: this.paddingRight,
            isNumeric:true,
            min:0,
            flow:{padding:"40px"}
        });


        model.push({
            type:"select",
            id:"cropping",
            group:"cropping",
            label:"Cropping",
            displayWhen:{ valueType:"num" },
            options: [
                { value:"crop", label:"Crop image" },
                { value:"fill", label:"Crop image and show base color" },
                { value:"round", label:"Don't crop (round values)" }
            ],
            selectedValue: this.cropping,
            help: { link:"klips/pictograph/image-set" }
        });

        model.push({
            type:"checkboxes",
            id:"rounding",
            fieldMargin:"11px 0 0 0",
            group:"rounding",
            label:"Rounding",
            displayWhen:{ valueType:"pct" },
            options:[
                {
                    value:true,
                    label:"Round values to prevent cropping",
                    checked:this.rounding
                }
            ],
            help: { link:"klips/pictograph/image-set" }
        });

        this.addDataSlotProperty(model);

        return model;
    },

    getSupportedReactions: function($super) {
        return { color:true };
    },

    /**
     *
     * @param idx
     * @param reaction
     * @param ctx - {
     *      reactionColor: ""
     * }
     */
    handleReaction : function( idx, reaction, ctx ) {
        switch (reaction.type) {
            case "color":
                ctx.reactionColor = CXTheme.getThemeColour(reaction.value);
                break;
        }
    }

});
;/****** cx.pictograph.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, page:false, KlipFactory:false, isWorkspace:false, G_vmlCanvasManager:false, _sanitizeNumbers:false,
    hexToRGB:false, CXTheme:false, bindValues:false, Props:false */

Component.Pictograph = $.klass(Component.Base, {

	displayName: "Pictograph",
	factory_id:"pictograph",
    variableSubComponents: [
        { label:"Image Set", type:"picto_data", role:"value" }
    ],
    uniqueValueNum: 1,
    imageLimit: 200,

    $pictograph: false,
    canvas: false,
    scaleRatio: 2,
    stepScale : 0.5,

    size: "medium",
    customSize:"",
    wrap:false,

    chartHeight: false,
    unscaledHeight: false,
    unscaledWidth: false,

    iconWidth: 0,

    sizes : {
        "xx-small" :    { height:17, indSize:14, indMargin:2 },
        "x-small" :    { height:22, indSize:18, indMargin:3 },
        "small" :    { height:26, indSize:22, indMargin:3 },
        "medium" :    { height:32, indSize:26, indMargin:3 },
        "large" :    { height:44, indSize:38, indMargin:6 },
        "x-large" :    { height:58, indSize:48, indMargin:6 },
        "xx-large" :    { height:82, indSize:66, indMargin:6 },
        cust : { height:58, indSize:48, indMargin:6 }
    },

	initializeForWorkspace : function($super, workspace) {
        this.el.click(_.bind(this.onClick, this));
        this.el.contextmenu(_.bind(this.onContextMenu, this));

        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(400);
        }
	},

    onClick : function(evt) {
        evt.stopPropagation();

        var target = this.getSelectionTarget(evt);
        if (!target) return;

        page.invokeAction("select_component", target, evt);
    },

    onContextMenu : function (evt) {
        evt.preventDefault();
        evt.stopPropagation();

        var target = this.getSelectionTarget(evt);
        if (!target) return;

        page.invokeAction("show_context_menu", { target:target, menuCtx:{ isRoot:(target == this) } }, evt);
    },

    getSelectionTarget : function(evt) {
        var $target = $(evt.target);

        if ($target.is("canvas")) {
            var rect = this.canvas.getBoundingClientRect(),
                x = evt.clientX - rect.left,
                y = evt.clientY - rect.top,
                i, len;

            // determine which image was clicked and select the corresponding component
            for (i = 0, len = this.imageAreas.length; i < len; i++) {
                var imageArea = this.imageAreas[i];
                if ((imageArea.top <= y && y <= imageArea.top + imageArea.height)
                    && (imageArea.left <= x && x <= imageArea.left + imageArea.width)) {
                    return imageArea.valueCx;
                }
            }
        }

        return this;
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Image Set",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this, type:"picto_data", role:"value"}},
                    {type:"remove", disabled:true}
                ]
            ]
        });

        return model;
    },

    addChildByType : function($super, config) {
        var newChild = $super(config);

        if (config.type == "picto_data" && config.role == "value") {
            var id = this.uniqueValueNum++;

            newChild = KlipFactory.instance.createComponent({
                type:"picto_data",
                displayName:"Image Set " + id,
                role:"value",
                valueId: id,
                formulas: [{"txt":"5","src":{"t":"expr","v":false,"c":[{"t":"l","v":"5"}]}}],
                data: [[5]]
            });
        }

        this.addComponent(newChild, config.index);

        if (config.type == "picto_data" && config.role == "value") {
            // evaluate formulas so data appears
            page.evaluateFormulas(newChild);
        }

        return newChild;
    },

	renderDom : function($super) {
		var dom = $super();

        this.$pictograph = $("<div>").css({display:"inline-block", "vertical-align":"top"});

        dom.append(this.$pictograph);
    },

	onResize : function($super) {
		$super();

        this.invalidateAndQueue();
	},

    onEvent : function ($super, evt) {
        $super(evt);

        if (evt.id == "theme_changed") {
            this.invalidate();
        }
    },

	onChildrenUpdated : function($super) {
        this.update();
	},

	update : function($super) {
        $super();

        var klip = this.getKlip();

        // exit early optimizations
        // ------------------------
        if ( !klip.isVisible ) return;
        if ( !this.checkInvalid(true) ) return;

        if (isWorkspace()) this.imageAreas = [];

        this.el.css("white-space", (!this.wrap ? "nowrap" : ""));

        this.$pictograph.show().empty();

        var $canvas = $("<canvas>")
            .text("your browser does not support the canvas tag")
            .css({
                display: "inline-block",
                "vertical-align": "top"
            });
        this.$pictograph.append($canvas);
        this.canvas = $($canvas)[0];
        if ( window.G_vmlCanvasManager != undefined ) G_vmlCanvasManager.initElement(this.canvas);

        this.chartHeight = (this.size == "cust" && this.customSize != "" ? this.customSize : this.sizes[this.size].height);

        this.canvas.width = 0;
        this.canvas.height = this.unscaledHeight || this.chartHeight;

        var dataValues = this.getChildrenByType("picto_data");
        var imgConfigs = [];

        // setup config map for each image set
        // -----------------------------------
        for (var i = 0; i < dataValues.length; i++) {
            var valueCx = dataValues[i];
            var config = {};

            var data = valueCx.getData(0).slice(0);
            _sanitizeNumbers(data);

            if (data.length > 0) {
                var val = data[0];

                var allFullImages = true;
                var numImages;
                var remainder = 0;
                var fillImage = (valueCx.valueType == "pct" || valueCx.cropping == "fill");

                // calculate the number of images in each set
                if (valueCx.valueType == "num") {
                    val = val / valueCx.stepValue;

                    if (valueCx.cropping != "round") {
                        remainder = val % 1;
                        val = Math.floor(val);
                    } else {
                        val = Math.round(val);
                    }

                    allFullImages = (!remainder || (remainder && fillImage));
                    numImages = val + (remainder ? 1 : 0);
                } else if (valueCx.valueType == "pct") {
                    numImages = valueCx.steps;

                    if (!valueCx.rounding) {
                        var expandedVal = numImages * val;
                        remainder = expandedVal % 1;
                        val = Math.floor(expandedVal);
                    } else {
                        val = Math.round(numImages * val);
                    }
                }

                var reactionCtx = {reactionColor:""};
                valueCx.collectReactions(reactionCtx);

                config = {
                    valueCx: valueCx,
                    allFullImages: allFullImages,
                    numImages: numImages,
                    wholeNum: val,
                    remainder: remainder,
                    fillImage: fillImage,
                    colorFilter: valueCx.colorFilter,
                    valueColor: valueCx.valueColor,
                    baseColor: valueCx.baseColor,
                    reactionColor: reactionCtx.reactionColor
                };

                if (_.isArray(valueCx.padding)) {
                    config.paddingTop = valueCx.padding[0];
                    config.paddingRight = valueCx.padding[1];
                    config.paddingBottom = valueCx.padding[2];
                    config.paddingLeft = valueCx.padding[3];
                } else {
                    config.paddingTop = valueCx.padding || 0;
                    config.paddingRight = valueCx.padding || 0;
                    config.paddingBottom = valueCx.padding || 0;
                    config.paddingLeft = valueCx.padding || 0;
                }

                if (valueCx.imageType != "custom") {
                    config.src = klip.dashboard.getRootPath() + "images/components/pictograph/"+ valueCx.imageType +".png";
                } else {
                    if (valueCx.customImageUrl) {
                        config.src = klip.dashboard.getLocationOrigin() + valueCx.customImageUrl;
                    }
                }

                if (config.src) {
                    imgConfigs.push(config);
                }
            }
        }

        // render the data value after images have loaded
        // ----------------------------------------------
        if (imgConfigs.length > 0) {
            var _this = this;
            this.loadImages(imgConfigs, function(imgConfigs) {
                var ctx = _this.canvas.getContext("2d");
                var elWidth = _this.el.width();

                // determine the destination size of each image
                for (var i = 0; i < imgConfigs.length; i++) {

                    var imgConfig = imgConfigs[i];

                    var srcWidth = imgConfig["image"].width;
                    var srcHeight = imgConfig["image"].height;

                    // subtract top/bottom padding to keep image proportions
                    var destHeight = _this.chartHeight - (imgConfig.paddingTop + imgConfig.paddingBottom);
                    if (destHeight < 0) destHeight = 0;

                    var destWidth = Math.ceil((srcWidth * destHeight) / srcHeight);
                    if (destWidth < 0) destWidth = 0;
                    if (destWidth + (imgConfig.paddingLeft + imgConfig.paddingRight) > elWidth) {
                        destWidth = elWidth - (imgConfig.paddingLeft + imgConfig.paddingRight);
                        destHeight = Math.ceil((destWidth * srcHeight) / srcWidth);

                        _this.chartHeight = destHeight + (imgConfig.paddingTop + imgConfig.paddingBottom);
                    }

                    var srcRemainderWidth = Math.round(srcWidth * imgConfig.remainder);
                    var destRemainderWidth = Math.round(destWidth * imgConfig.remainder);

                    $.extend(imgConfig, {
                        srcWidth: srcWidth,
                        srcHeight: srcHeight,
                        destWidth: destWidth,
                        destHeight: destHeight,
                        srcRemainderWidth: srcRemainderWidth,
                        destRemainderWidth: destRemainderWidth
                    });
                }

                // size canvas
                _this.setCanvasWidth(imgConfigs);
                _this.canvas.height = _this.chartHeight;

                // scale canvas to make images look better
                _this.unscaledWidth = _this.canvas.width;
                _this.unscaledHeight = _this.canvas.height;
                _this.scaleCanvas(_this.unscaledWidth, _this.unscaledHeight, ctx);

                // render images
                _this.renderImages(imgConfigs, ctx);

                // shrink the image
                if (!_this.wrap && _this.unscaledWidth > elWidth) {
                    var newHeight = Math.ceil((elWidth * _this.unscaledHeight) / _this.unscaledWidth);

                    var smallCanvas = document.createElement("canvas");
                    var smallCtx = smallCanvas.getContext("2d");
                    smallCanvas.width = elWidth;
                    smallCanvas.height = newHeight;

                    var scaledData = _this.scaleDown(_this.canvas, elWidth / _this.canvas.width);
                    smallCtx.scale(scaledData.remainingScale, scaledData.remainingScale);
                    smallCtx.drawImage(scaledData.image, 0, 0);

                    _this.$pictograph.empty()
                        .append($(smallCanvas)
                            .text("your browser does not support the canvas tag")
                            .css({
                                display: "inline-block",
                                "vertical-align": "top"
                            }));
                }

                // Height is ready can update panel cell heights
                // -----------------------------------------------
                if (_this.fromPanelUpdate) {
                    _this.parent.handleComponentUpdated();
                    _this.fromPanelUpdate = false;
                }
                // -----------------------------------------------

                if (!isWorkspace() && _this.parent.isKlip) {
                    _this.parent.handleComponentUpdated();
                }
            });
        } else {
            // Height is ready can update panel cell heights
            // -----------------------------------------------
            if (this.fromPanelUpdate) {
                this.parent.handleComponentUpdated();
                this.fromPanelUpdate = false;
            }
            // -----------------------------------------------

            if (!isWorkspace() && this.parent.isKlip) {
                this.parent.handleComponentUpdated();
            }
        }

        this.setInvalid(false,true);
    },

    /**
     * Load all the images, then call the callback function
     *
     * @param imgConfigs - image config objects with src url
     * @param callback - function to call once all images have been loaded
     */
    loadImages : function(imgConfigs, callback) {
        var imgCount = imgConfigs.length;
        var loadedImages = 0;

        for (var i = 0; i < imgConfigs.length; i++) {
            var imgConfig = imgConfigs[i];
            var imageObj = new Image();
            imageObj.src = imgConfig.src;

            imageObj.onload = function() {
                loadedImages++;

                if (loadedImages == imgCount) {
                    callback(imgConfigs);
                }
            };

            imgConfig["image"] = imageObj;
        }
    },

    setCanvasWidth : function(imgConfigs) {
        var chartWidth = 0,
            imgConfig,
            totalNumImages = 0,
            excessImages;

        for (var i = 0; i < imgConfigs.length; i++) {
            imgConfig = imgConfigs[i];

            // determine if the number of images exceeds the limit
            excessImages = (totalNumImages + imgConfig.numImages) - this.imageLimit;
            if (excessImages > 0) {
                // add the images the fit within the limit
                chartWidth += (imgConfig.paddingLeft + imgConfig.destWidth + imgConfig.paddingRight) * (imgConfig.allFullImages ? imgConfig.numImages - excessImages : imgConfig.wholeNum - (excessImages + 1));
            } else {
                totalNumImages += imgConfig.numImages;

                // width = (whole images + remainder image) with padding
                chartWidth += (imgConfig.paddingLeft + imgConfig.destWidth + imgConfig.paddingRight) * (imgConfig.allFullImages ? imgConfig.numImages : imgConfig.wholeNum)
                    + (!imgConfig.allFullImages ? imgConfig.paddingLeft + imgConfig.destRemainderWidth : 0);
            }

            if (this.wrap && chartWidth > this.el.width()) {
                chartWidth = this.el.width();
                break;
            }

            if (excessImages > 0) break;
        }

        this.canvas.width = chartWidth;
    },

    scaleCanvas : function(oldWidth, oldHeight, ctx) {
        this.canvas.width = oldWidth * this.scaleRatio;
        this.canvas.height = oldHeight * this.scaleRatio;

        this.canvas.style.width = oldWidth + "px";
        this.canvas.style.height = oldHeight + "px";

        // now scale the context to counter
        // the fact that we've manually scaled
        // our canvas element
        ctx.scale(this.scaleRatio, this.scaleRatio);
    },

    renderImages : function(imgConfigs, ctx) {
        var srcWidth, srcHeight, destWidth, destHeight,
            destX = 0, scaleDestX = 0,
            destY = 0, scaleDestY = 0, destYPadding, scaleDestYPadding,
            imgConfig,
            totalNumImages = 0;

        try {
            for (var i = 0; i < imgConfigs.length; i++) {
                imgConfig = imgConfigs[i];

                srcWidth = imgConfig.srcWidth;
                destWidth = imgConfig.destWidth;
                srcHeight = imgConfig.srcHeight;
                destHeight = imgConfig.destHeight;

                destYPadding = destY + imgConfig.paddingTop;
                scaleDestYPadding = scaleDestY + (imgConfig.paddingTop * this.scaleRatio);

                for (var j = 0; j < imgConfig.numImages; j++) {
                    totalNumImages++;
                    // do not render more that the limit
                    if (totalNumImages > this.imageLimit) {
                        if (isWorkspace()) {
                            page.showMessage("Image set limited to " + this.imageLimit + " images. Change <span class='strong'>Value per Image</span> property to communicate your data with fewer images. <span class='underline'>Got it</span>.");
                        }
                        return;
                    }

                    var isRemainder = !!(j == imgConfig.wholeNum && imgConfig.remainder);
                    var fillRemainder = (isRemainder && imgConfig.fillImage);

                    var destImageWidth = (isRemainder ? imgConfig.destRemainderWidth : destWidth);
                    var destImageHeight = destHeight;

                    var destFillWidth = (fillRemainder ? destWidth - destImageWidth : 0);
                    var destFillHeight = destHeight;

                    var destTotalWidth = imgConfig.paddingLeft + (!isRemainder || fillRemainder ? destWidth + imgConfig.paddingRight : imgConfig.destRemainderWidth);

                    // start a new row
                    if (destX + destTotalWidth > this.unscaledWidth) {
                        var allPixels = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);

                        var rowHeight = this.chartHeight;
                        this.unscaledHeight += rowHeight;
                        this.scaleCanvas(this.unscaledWidth, this.unscaledHeight, ctx);

                        ctx.putImageData(allPixels, 0, 0);

                        destX = 0;
                        scaleDestX = 0;
                        destY += rowHeight;
                        scaleDestY += rowHeight * this.scaleRatio;

                        destYPadding = destY + imgConfig.paddingTop;
                        scaleDestYPadding = scaleDestY + (imgConfig.paddingTop * this.scaleRatio);
                    }

                    if (isWorkspace()) {
                        // save click regions of images
                        this.imageAreas.push({
                            valueCx: imgConfig.valueCx,
                            left: destX,
                            top: destY,
                            width: destTotalWidth,
                            height: destHeight
                        });
                    }

                    // leave space for nonexistent images
                    if (destImageWidth <= 0 || destImageHeight <= 0) {
                        destX += destTotalWidth;
                        scaleDestX += destTotalWidth * this.scaleRatio;
                        continue;
                    }


                    destX += imgConfig.paddingLeft;
                    scaleDestX += imgConfig.paddingLeft * this.scaleRatio;

                    if (destImageWidth > 0 && destImageHeight > 0) {
                        if (isRemainder) {
                            ctx.save();
                            ctx.beginPath();
                            // create a rectangle where the image will go and clip it
                            ctx.rect(destX, destYPadding, destImageWidth, destImageHeight);
                            ctx.clip();
                        }

                        // draw the image; only the part in the clipped area will appear
                        ctx.drawImage(imgConfig.image, 0, 0, srcWidth, srcHeight, destX, destYPadding, destWidth, destHeight);

                        if (isRemainder) {
                            ctx.restore();
                        }

                        // color image with value color
                        if (j < imgConfig.wholeNum || (j == imgConfig.wholeNum && imgConfig.remainder)) {
                            if (imgConfig.reactionColor != "") {
                                // color from indicators
                                this.colorPixels(ctx, scaleDestX, scaleDestYPadding, destImageWidth, destImageHeight, imgConfig.reactionColor, "color");
                            } else {
                                // value images
                                if (imgConfig.colorFilter != "none") {
                                    this.colorPixels(ctx, scaleDestX, scaleDestYPadding, destImageWidth, destImageHeight, imgConfig.valueColor, imgConfig.colorFilter);
                                }
                            }
                        } else {
                            // background images
                            this.colorPixels(ctx, scaleDestX, scaleDestYPadding, destImageWidth, destImageHeight, imgConfig.baseColor, "color");
                        }

                        destX += destImageWidth;
                        scaleDestX += destImageWidth * this.scaleRatio;
                    }

                    // fill in the partial image with the base color
                    if (fillRemainder && destFillWidth > 0 && destFillHeight > 0) {
                        ctx.save();
                        ctx.beginPath();
                        ctx.rect(destX, destYPadding, destFillWidth, destFillHeight);
                        ctx.clip();
                        ctx.drawImage(imgConfig.image, 0, 0, srcWidth, srcHeight, destX - destImageWidth, destYPadding, destWidth, destHeight);
                        ctx.restore();

                        this.colorPixels(ctx, scaleDestX, scaleDestYPadding, destFillWidth, destFillHeight, imgConfig.baseColor, "color");

                        destX += destFillWidth;
                        scaleDestX += destFillWidth * this.scaleRatio;
                    }

                    if (!isRemainder || fillRemainder) {
                        destX += imgConfig.paddingRight;
                        scaleDestX += imgConfig.paddingRight * this.scaleRatio;
                    }
                }
            }

            if (isWorkspace() && totalNumImages < this.imageLimit) {
                page.hideMessage();
            }
        } catch (e) {
            // console.log("Error rendering pictograph images", e);
            this.$pictograph.hide();
        }
    },

    colorPixels : function(ctx, x, y, width, height, color, filter) {
        var pixels = ctx.getImageData(x, y, width * this.scaleRatio, height * this.scaleRatio);

        if (filter == "color") {
            this.changeColor(hexToRGB(CXTheme.getThemeColour(color)), pixels);
        } else if (filter == "scale") {
            this.scaleColor(hexToRGB(CXTheme.getThemeColour(color)), pixels);
        }

        ctx.putImageData(pixels, x, y);
    },

    changeColor : function(color, pixels) {
        for (var i = 0; i < pixels.data.length; i += 4) {
            if (pixels.data[i + 3] > 0) {   // not a transparent pixel
                pixels.data[i] = color.r;
                pixels.data[i + 1] = color.g;
                pixels.data[i + 2] = color.b;
            }
        }
    },

    scaleColor : function(color, pixels) {
        for (var i = 0; i < pixels.data.length; i += 4) {
            if (pixels.data[i + 3] > 0) {   // not a transparent pixel
                pixels.data[i] = pixels.data[i] / 255 * color.r;
                pixels.data[i + 1] = pixels.data[i + 1] / 255 * color.g;
                pixels.data[i + 2] = pixels.data[i + 2] / 255 * color.b;
            }
        }
    },

    scaleDown : function(image, targetScale) {
        var currentScale = 1;

        while(currentScale * this.stepScale > targetScale) {
            currentScale *= this.stepScale;
            image = this.stepDown(image);
        }

        return {
            image:image,
            remainingScale: targetScale / currentScale
        };
    },

    stepDown : function(image) {
        var temp = {};
        temp.canvas = document.createElement("canvas");
        temp.ctx = temp.canvas.getContext("2d");

        temp.canvas.width = (image.width * this.stepScale) + 1;
        temp.canvas.height = (image.height * this.stepScale) + 1;
        temp.ctx.scale(this.stepScale, this.stepScale);
        temp.ctx.drawImage(image, 0, 0);

        return temp.canvas;
    },


    configure : function($super, config) {
		$super(config);
		bindValues(this, config, ["size","customSize","wrap"]);
        this.updateLabelSizes(config);

        this.invalidate();
    },

	serialize : function($super) {
		var config = $super();
		bindValues(config, this, ["size","customSize","wrap"],
            {
                size:2,
                customSize:"",
                wrap:false
            }, true);

		return config;
	},


	getPropertyModel : function($super) {
		var model = $super();

		model.push({
			type:"select",
			id:"size",
			label:"Image Height",
			group:"style",
			options: Props.Defaults.size.concat({value:"cust", label:"Custom..."}),
			selectedValue: this.size,
            help: {
                link:"klips/pictograph/properties"
            }
		});

        model.push({
            type:"text",
            id:"customSize",
            group:"style",
            displayWhen: { size:"cust" },
            minorLabels: [
                {label: "px", position: "right", css:"quiet italics"}
            ],
            width:"50px",
            value: this.customSize,
            flow:true
        });


        model.push({
            type:"checkboxes",
            id:"wrap",
            label:"Wrap",
            group:"style",
            fieldMargin:"4px 0 12px 0",
            options:[
                {
                    value:true,
                    label:"Let images wrap onto multiple lines",
                    checked:this.wrap
                }
            ]
        });

        var dataValues = this.getChildrenByType("picto_data");

        var showValueOptions = [{ value:"hidden", label:"Hidden" }];
        for (var i = 0; i < dataValues.length; i++) {
            showValueOptions.push({ value:dataValues[i].valueId, label:dataValues[i].displayName });
        }

        var _this = this;
        model.push({
            type:"button",
            id:"addValue",
            label:"Image",
            group:"addComponents",
            text:"Add Image Set",
            onClick: function(evt) {
                page.invokeAction("add_component", {parent:_this, type:"picto_data", role:"value"}, {});
            }
        });

		return model;
	},


	afterFactory : function() {
		var i;
        var requiredChildren = [
            {role:"value", props:{displayName:"Image Set",type:"picto_data",valueId:this.uniqueValueNum,
                            formulas: [{"txt":"5","src":{"t":"expr","v":false,"c":[{"t":"l","v":"5"}]}}],
                            data: [[5]]}}
        ];

        for (i = 0; i < requiredChildren.length; i++) {
            var req = requiredChildren[i];
            if (!this.getChildByRole(req.role)) {
                var cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                this.addComponent(cx);
            }
        }

        var values = this.getChildrenByType("picto_data");
        for (i = 0; i < values.length; i++) {
            if (values[i].valueId >= this.uniqueValueNum) {
                this.uniqueValueNum = values[i].valueId + 1;
            }
        }
    }

});
;/****** cx.proxy_calendar_display_format.js *******/ 

/*global Component:false */

Component.ProxyCalendarDisplayFormat = $.klass(Component.Proxy, {
    factory_id: "calendar_display_format",
    role: false,
    hideInWorkspace: true,

    /**
     * renderDom is overriden to not do anything at all! eg, it has no html dom so it does not call super
     * @param $super
     */
    renderDom: function ($super) {

    },

    getEditorMenuModel: function($super) {
        return null;
    },

    setData: function ($super, data, msg) {
        $super(data, msg);
        if(this.parent.requestDisplayAndCalendar){
            this.parent.receivedRequestData(this.role, true);
        }

        if(this.parent.receivedData["display_format"] && this.parent.receivedData["calendar_format"]){
            this.parent.setDateControlValue();
            this.parent.resetDisplayAndCalendarRequest();
        }
    }
});;/****** cx.proxy_output_format.js *******/ 

/* global Component:false */

Component.ProxyOutputFormat = $.klass(Component.Proxy, {
    factory_id: "output_format",
    role: false,
    hideInWorkspace: true,

    /**
     * renderDom is overriden to not do anything at all! eg, it has no html dom so it does not call super
     * @param $super
     */
    renderDom: function ($super) {

    },

    getEditorMenuModel: function($super){
        return null;
    },

    setData: function ($super, data, msg) {
        var date;

        $super(data, msg);

        if(this.parent.requestVariable){
            date = this.getData(0).slice(0);
            this.parent.dateValue = date;
            this.parent.setRequestVariable(false);
            this.parent.updateVariableChange(date);
        }
    }
});;/****** cx.range.js *******/ 

/* globals Component:false, KlipFactory:false, bindValues:false, isWorkspace:false, page:false */
Component.Range = $.klass(Component.Proxy, {

	displayName: "Range",
	factory_id: "range",
    rangeId: 1,
    color: "cx-theme_blue_3",
    isDraggable: false,
    dataDisabled: true,

    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Range",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"range", role:"range", sibling:this}},
                    {type:"remove", actionId:"remove_component", actionArgs:{cx:this}}
                ]
            ]
        });

        return model;
    },

	getPropertyModel : function($super) {
		var model = $super();

		model.push({
            type:"color_picker",
            id:"color",
			help: { link: "klips/gauge-range" },
            label:"Range Color",
            group:"colour",
			defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            valueType:"theme",
            selectedValue: this.color
        });

		model.push({
			type:"markup",
			id:"msg_range_indicators",
			group:"colour",
			el:$("<span class='strong'>Note: </span><span>Gauge indicator colors override ranges.</span>")
		});

		return model;
	},

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this, ["rangeId", "color"],
            {
                color:"cx-theme_blue_3"
            }, true);
        return cfg;
    },

	configure : function($super, config) {
		$super(config);
		bindValues(this, config, ["rangeId", "color"]);

        if (config.rangeId && !this.isRenamed) {
            this.displayName = "Range " + config.rangeId;
            if (config["~propertyChanged"] && isWorkspace()) page.updateMenu();
        }
    },

    afterFactory : function() {
        var i, req, cx;

        var requiredChildren = [
            {role:"start", props:{displayName:"Start " + this.rangeId, type:"proxy", fmt:"num", autoFmt:false }},
            {role:"end", props:{displayName:"End " + this.rangeId, type:"proxy", fmt:"num", autoFmt:false}}
        ];

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);

            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                if ( req.props._data ) cx.setData( req.props._data );
                this.addComponent(cx);

                page.invokeAction("aggregate_component", { cx:cx, isAutomatic:true });
            }

            cx.isDeletable = false;
        }
    }
});
;/****** cx.scatter_data.js *******/ 

/*global Component:true, KlipFactory:false, bindValues:false, isWorkspace:false, page:false, safeText:false */

Component.ScatterData = $.klass(Component.Proxy, {

    displayName: "Series",
    factory_id:"scatter_data",
    isDeletable: true,
    isMovable: true,

    scatterName : "Untitled",
    regLine: false,
    overrideColor: false,
    color : "cx-theme_orange_3",
    seriesHidden: false,

    isDstRoot: true,
    canGroup : true,
    canSetAggregation : true,
    canFilter : true,
    canHaveDataSlots: true,

    checkDeletable : function($super) {
        var isDeletable = $super();
        var siblings;

        if (isDeletable) {
            siblings = this.parent.getChildrenByType("scatter_data");
            isDeletable = (siblings.length > 1);
        }

        return isDeletable;
    },

    getWorkspaceControlModel : function($super, fromChild) {
        var model = $super();

        model.push({
            name:"Scatter",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"scatter_data", role:"scatter_data", sibling:this}},
                    {type:"remove", disabled:!this.checkDeletable(), actionId:"remove_component", actionArgs:{cx:this}}
                ]
            ]
        });

        if (!fromChild) {
            this.addDataSlotControl(model);
        }

        return model;
    },

    setSelectedInWorkspace : function($super, selected, newComponent) {
        $super(selected, newComponent);

        this.parent.invalidateAndQueue();
    },

    getEditorMenuModel : function($super) {
        var m = $super();

        var name = this.displayName;
        if (!name) name = "Untitled";

        if(KF.company.hasFeature("saas_11282")){
            name = safeText(name);
        }

        m.text = "<span parentIsAction='true' class='quiet'>Series:</span>&nbsp;" + name.replace(/ /g, "&nbsp;");
        m.tooltip =  this.getReferenceName();
        return m;
    },

    getReferenceValueModel : function($super) {
        var m = $super();
        m.text = this.getReferenceName().replace(/ /g, "&nbsp;");
        m.reference = false;
        return m;
    },

    getReferenceName : function() {
        return "Series: " + (this.displayName || "Untitled");
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg,this,["scatterName","regLine","overrideColor","color","seriesHidden"],
            {
                regLine: false,
                overrideColor: false,
                color: "cx-theme_orange_3",
                seriesHidden: false
            }, true);
        return cfg;
    },

    configure : function($super, config) {
        $super(config);
        bindValues(this,config,["regLine","scatterName","overrideColor","color","seriesHidden"]);

        if (config.scatterName && !this.isRenamed) {
            this.displayName = config.scatterName;
            if (config["~propertyChanged"] && isWorkspace()) page.updateMenu();
        }

        this.invalidate();
    },

    afterFactory : function() {
        var requiredChildren = [
            {role:"data_x", props:{seriesName:"X",type:"series_data",isDeletable:false}},
            {role:"data_y", props:{seriesName:"Y",type:"series_data",isDeletable:false}},
            {role:"data_z", props:{seriesName:"Z",type:"series_data",isDeletable:false}},
            {role:"data_labels", props:{type:"series_labels",isDeletable:false}}
        ];
        var i;
        var req;
        var cx;

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);
            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                if ( req.props._data ) cx.setData( req.props._data );
                this.addComponent(cx);
            }

            cx.isDeletable = false;
        }
    },

    getPropertyModel : function($super) {
        var model = $super();

        model.push ({
            type:"text",
            id:"scatterName",
            group: "scatterLabel",
            label: "Series Label",
            value: this.scatterName
        });

        if(this.parent.renderer == "scatter") {
            model.push({
                type:"checkboxes" ,
                id:"regLine",
                options: [
                    {value:true,label:"Draw a regression line", checked: this.regLine}
                ],
                label: "Regression",
                group: "regression",
                value: this.regLine
            });
        }

        model.push({
            type: "checkboxes",
            id: "overrideColor",
            options: [
                {value:true, label:"Override the default color", checked: this.overrideColor}
            ],
            label: "Series Color",
            group: "color"
        });

        model.push({
            type: "color_picker",
            id: "color",
            group: "color",
            valueType:"theme",
            selectedValue: this.color,
            displayWhen: { overrideColor:true }
        });

        model.push({
            type: "checkboxes",
            id: "seriesHidden",
            options: [
                {value:true, label:"Hide this series by default", checked: this.seriesHidden}
            ],
            label: "Visibility",
            group: "visibility"
        });

        this.addDataSlotProperty(model);

        return model;
    },

    getContextMenuModel: function (menuCtx, menuConfig) {

        var model = [];

        if (!menuCtx) {
            menuCtx = {};
        }

        this.addRenameContextMenuItem(model);

        this.addRemoveContextMenuItem(model);

        this.addMoveContextMenuItem(model);

        this.addDataSlotContextMenuItem(model);

        menuCtx.groupBySubComponent = this.getChildByRole("data_labels");
        menuCtx.showSubComponentAggregation = true;
        menuCtx.showSubComponentFilter = true;

        this.addDstContextMenuItems(menuCtx, menuConfig, model);

        return model;
    }

});
;/****** cx.separator.js *******/ 

/* global Component:false, updateManager:false, isWorkspace:false, bindValues:false, Props:false */

Component.Separator = $.klass(Component.Base, {

    displayName: "Separator",
    factory_id: "separator",
    isDstRoot: false,

    $separator: false,

    orientation: "h",
    line_style: "Solid",
    line_weight: 1,
    line_colour: "cx-border_ccc",

    initialize : function($super) {
        $super();
    },

    renderDom : function($super) {
        var dom = $super();
        this.$separator = $("<div>");
        dom.append(this.$separator);
    },

    onResize : function($super) {
        $super();

        updateManager.queue(this);
    },

    update : function($super) {
        $super();

        if (!this.parent.isKlip) {
            if (this.orientation == "v") {
                this.el.height(this.el.parent().height() - (this.el.outerHeight(true) - this.el.height()));
            } else {
                this.el.css("height", "");
            }
        }

        this.$separator.removeClass()
            .addClass("separator")
            .addClass(this.orientation == "v" ? "vert" : "horiz")
            .addClass(this.line_style == "ShortDot" ? "dotted" : (this.line_style == "Dash" ? "dashed" : "solid"))
            .addClass("line-" + this.line_weight);

        if (this.line_colour.indexOf("#") != -1) {
            this.$separator.css("border-color", this.line_colour);
        } else {
            this.$separator
                .css("border-color", "")
                .addClass(this.line_colour);
        }

        if (isWorkspace()) {
            this.$separator.css("min-height", "20px");
            this.$separator.css("top", this.orientation == "h" ? "10px" : "0");
        }

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    configure : function($super, config) {
        $super(config);
        bindValues(this, config, ["orientation","line_style","line_weight","line_colour"]);
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this, ["orientation","line_style","line_weight","line_colour"],
            {
                orientation:"h",
                line_style:"Solid",
                line_weight:1,
                line_colour:"cx-border_ccc"
            }, true);
        return cfg;
    },

    getPropertyModel  : function($super) {
        var model = $super();

        model.push({
            type: "button_group_ex",
            id: "orientation",
            label: "Orientation",
            group: "orientation",
            options: Props.Defaults.lineOrientation,
            selectedValue: this.orientation
        });

        model.push({
            type: "button_group_ex",
            id: "line_style",
            label: "Line Style",
            group: "style",
            options: [Props.Defaults.lineDashes[0], Props.Defaults.lineDashes[2], Props.Defaults.lineDashes[3]],
            selectedValue: this.line_style
        });

        model.push({
            type: "button_group_ex",
            id: "line_weight",
            label: "Line Weight",
            group: "style",
            options: Props.Defaults.lineWeights,
            selectedValue: this.line_weight
        });

        model.push({
            type:"color_picker" ,
            id:"line_colour",
            group:"style",
            label:"Line Color",
            valueType: "borderCss",
            defaultColour: {rgb:"#aaaaaa", borderCss:"cx-border_aaa"},
            selectedValue: this.line_colour
        });

        return model;
    }
});
;/****** cx.series_data.js *******/ 

/* global Component:false, CXTheme:false, Props:false, page:false, isWorkspace:false, bindValues:false,
    DX:false, safeText:false */

Component.SeriesData = $.klass(Component.Proxy, {

    displayName: "Series",
    factory_id:"series_data",
    seriesName : "Untitled",

    renderer: "bar",
    chartStyle: "points",
    lineStyle: "angle",
    lineWeight: 3,
    lineDashes: "Solid",
    pointSize: 6,
    showValues: false,
    expressPercentage: false,
    leaveGaps: false,
    axis: null,
    overrideColor : false,
    color : "cx-theme_orange_3",
    seriesHidden: false,


    initialize : function($super, config) {
        $super(config);

        this.disableStyles = true;

        if ( isWorkspace() ) {
            this.labelMixin = new Component.LabelComponent();
            this.allowedFormats = ["txt","num","cur","pct","dat","dur"];
            this.fmt = "num";
        }
    },

    onParentAssignment: function() {
        var parentId = this.parent.factory_id;

        switch (parentId) {
            case "chart_series":
                this.isMovable = true;
                this.canSetAggregation = true;
                this.canSort = true;
                this.canFilter = true;
                break;
            case "chart_pie":
            case "chart_funnel":
                this.isMovable = false;
                this.canSetAggregation = true;
                this.canSort = true;
                this.canFilter = true;
                break;
            case "scatter_data":
                this.isMovable = false;
                this.canSetAggregation = true;
                this.canFilter = true;
                break;
            default:
                break;
        }
    },

    setSelectedInWorkspace : function($super, selected, newComponent) {
        var parent;

        $super(selected, newComponent);

        parent = this.parent;
        if (this.parent.factory_id == "scatter_data") parent = this.parent.parent;

        parent.invalidateAndQueue();
    },

    checkDeletable : function($super) {
        var isDeletable = $super();
        var siblings;

        if (isDeletable) {
            siblings = this.parent.getChildrenByType("series_data");
            isDeletable = (siblings.length > 1);
        }

        return isDeletable;
    },

    getSupportedDst: function($super) {
        var dst = $super();
        var parent = this.parent;

        if (parent.factory_id == "scatter_data" && this.role == "data_z") {
            parent = parent.parent;

            if (parent.renderer == "scatter") {
                dst.aggregation = false;
                dst.filter = false;
            }
        } else if(parent.factory_id == "chart_series" && this.role == "series") {
            //Series only works with numeric data, therefore group should always be disallowed
            dst.group = this.canGroup;
        }

        return dst;
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        if (this.parent.factory_id == "chart_series") {
            //Can't call parent because we want to set the sibling for series
            model.push({
                name:"Series",
                controls: [
                    [
                        {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"series_data", role:"series", sibling:this}},
                        {type:"remove", disabled:!this.checkDeletable(), actionId:"remove_component", actionArgs:{cx:this}}
                    ]
                ]
            });

            model.push({
                name:"Y-Axis",
                controls: [
                    [
                        {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"chart_axis", role:"axis_y"}},
                        {type:"remove", disabled:true}
                    ]
                ]
            });
        } else if (this.parent.factory_id == "scatter_data") {
            model = this.parent.getWorkspaceControlModel(true);
        }

        return model;
    },

    getEditorMenuModel : function($super) {
        var m = $super();
        var type = "Series";
        var name;

        if (this.parent.factory_id == "chart_pie" || this.parent.factory_id == "chart_funnel") return m;

        if (this.parent.factory_id == "scatter_data") type = "Data";

        name = this.displayName;
        if (!name) name = "Untitled";

        if(KF.company.hasFeature("saas_11282")) {
            name = safeText(name);
        }

        m.text = "<span parentIsAction='true' class='quiet'>" + type + ":</span>&nbsp;" + name.replace(/ /g, "&nbsp;");
        m.tooltip =  this.getReferenceName();

        return m;
    },

    getReferenceValueModel : function($super) {
        var m = $super();

        if (this.parent.factory_id == "chart_pie" || this.parent.factory_id == "chart_funnel") return m;

        m.text = this.getReferenceName().replace(/ /g, "&nbsp;");

        return m;
    },

    getReferenceName : function() {
        var type = "Series";
        if (this.parent.factory_id == "scatter_data") type = "Data";

        return type + ": " + (this.displayName || "Untitled");
    },

    getSupportedReactions: function() {
        return { color: true };
    },

    /**
     *
     * @param idx
     * @param reaction
     * @param ctx - {
     * 		seriesColours: {}
     * }
     */
    handleReaction : function( idx, reaction, ctx ) {
        switch (reaction.type) {
            case "color":
                ctx.seriesColours[ idx ] = CXTheme.getThemeColour(reaction.value);
                break;
        }
    },

    serialize : function($super) {
        var cfg = $super();

        if (this.parent.factory_id != "chart_pie" && this.parent.factory_id != "chart_funnel") cfg.seriesName = this.seriesName;

        bindValues(cfg,this,["renderer","chartStyle","lineStyle","lineWeight","lineDashes","pointSize","showValues","expressPercentage","leaveGaps","axis","overrideColor","color","seriesHidden"],
            {
                renderer:"bar",
                chartStyle:"points",
                lineStyle:"angle",
                lineWeight:3,
                lineDashes:"Solid",
                pointSize:6,
                showValues:false,
                expressPercentage:false,
                leaveGaps:false,
                axis:null,
                overrideColor:false,
                color:"cx-theme_orange_3",
                seriesHidden: false
            }, true);
        return cfg;
    },

    configure : function($super, config) {
        $super(config);
        bindValues(this,config,["seriesName","renderer","showValues","expressPercentage","chartStyle","lineStyle","lineWeight","lineDashes","leaveGaps","axis","overrideColor","color","pointSize","seriesHidden"]);

        if (config.seriesName && !this.isRenamed) {
            this.displayName = config.seriesName;
            this.displayName = this.displayName.replace("<","&lt;");
            this.displayName = this.displayName.replace(">","&gt;");
            if (config["~propertyChanged"] && isWorkspace()) page.updateMenu();
        }

        this.updateAxisFormat();

        this.invalidate();
    },

    setData: function ($super, data, msg) {
        $super(data, msg);
        this.updateAxisFormat();
    },

    getPropertyModelGroups: function($super) {
        var propertyGroups = $super();

        if (this.parent.factory_id == "chart_pie" || this.parent.factory_id == "chart_funnel") {
            propertyGroups = $.merge(propertyGroups, this.labelMixin.getPropertyModelGroups());
        }

        return propertyGroups;
    },

    updateAxisFormat: function() {
        var components;
        var i;
        var cxAxis;

        if (this.isAutoFormatFeatureOn()) {
            // For a given series data component, we will find the referenced axis component and trigger a format update
            // on the referenced component (axis_y) as well as all other component of the same role.
            cxAxis = this.findReferencedAxis();
            if (cxAxis) {
                components = cxAxis.getPeerComponentsByRole(cxAxis.role);
                if (components) {
                    for (i = 0; i < components.length; i++) {
                        components[i].updateFormat();
                    }
                }
            }
        }
    },

    findReferencedAxis: function() {
        var axis;
        if (this.axis) {
            axis = this.getPeerComponentById(this.axis);
            if (!axis && this.parent) {
                axis = this.parent.getPeerComponentById(this.axis);
            }
        }
        return axis;
    },

    getPropertyModel : function($super) {
        var model = $super();
        var labelModel = _.bind(this.labelMixin.getPropertyModel, this)();
        var axisList = [];
        var component;
        var c;

        if (this.parent.factory_id == "chart_pie" || this.parent.factory_id == "chart_funnel") {
            // delete size, wrap
            labelModel = _.reject(labelModel, function(prop){
                return (prop.id == "size" || prop.id == "wrap" || prop.id == "font_style" || prop.id == "font_colour" || prop.id == "align" );
            });

            model = $.merge(model, labelModel);

            return model;
        }

        if (this.isAutoFormatFeatureOn()) {
            // When auto format feature is on, we will import formatting related properties from label component, but
            // remove the properties that's not relevant to the series data.
            labelModel = _.reject(labelModel, function(prop){
                // We will reject any property that's not from the "fmt" group.
                return prop.group != "fmt";
            });
            model = $.merge(model, labelModel);
        }

        this.addPrefixSuffixProperties(model);

        if (this.parent.factory_id == "scatter_data") {
            if (this.isAutoFormatFeatureOn()) {
                this.addDstProperties(model);
            }
            return model;
        }

        // Parent: chart_series
        // --------------------

        model.push({
            type: "button_group_ex",
            id: "renderer",
            label: "Chart Type",
            group: "chartType",
            options: Props.Defaults.seriesRenderers,
            selectedValue: this.renderer
        });

        model.push ({
            type:"text",
            id:"seriesName",
            group: "seriesLabel",
            label: "Series Label",
            value: this.seriesName
        });

        this.addDstProperties(model);

        model.push({
            type: "button_group_ex",
            id: "chartStyle",
            label: "Chart Style",
            group: "chartStyle",
            options: Props.Defaults.chartStyles,
            displayWhen: { renderer:"line" },
            selectedValue: this.chartStyle
        });

        model.push({
            type: "button_group_ex",
            id: "lineStyle",
            label: "Line Style",
            group: "lineProps",
            options: Props.Defaults.lineStyles,
            displayWhen: { renderer:"line", chartStyle:["line","line_area","points","points_area"] },
            selectedValue: this.lineStyle
        });

        model.push({
            type: "button_group_ex",
            id: "lineWeight",
            label: "Line Weight",
            group: "lineProps",
            options: Props.Defaults.lineWeights,
            displayWhen: { renderer:"line", chartStyle:["line","line_area","points","points_area"] },
            selectedValue: this.lineWeight
        });

        model.push({
            type: "button_group_ex",
            id: "lineDashes",
            label: "Line Dashes",
            group: "lineProps",
            options: Props.Defaults.lineDashes,
            displayWhen: { renderer:"line", chartStyle:["line","line_area","points","points_area"] },
            selectedValue: this.lineDashes
        });

        model.push({
            type: "button_group_ex",
            id: "pointSize",
            label: "Point Size",
            group: "pointProps",
            options: Props.Defaults.pointSizes,
            displayWhen: { renderer:"line", chartStyle:"points_no_line" },
            selectedValue: this.pointSize
        });

        model.push({
            type:"checkboxes" ,
            id:"showValues",
            options: [
                {value:true,label:"Show values on chart", checked: this.showValues}
            ],
            label: "Show Values",
            group: "values",
            value: this.showValues
        });

        if(this.parent.factory_id == "chart_series" && this.parent.stackBars == 2) {
            // Only show this option if this series' renderer is bar and the chart is set to stack bars as percentage
            model.push({
                type:"checkboxes" ,
                id:"expressPercentage",
                options: [
                    {value:true,label:"Express as percentage", checked: this.expressPercentage}
                ],
                label: "",
                group: "values",
                displayWhen: {renderer: "bar", showValues: true},
                value: this.expressPercentage
            });
        }

        model.push({
            type:"checkboxes" ,
            id:"leaveGaps",
            options: [
                {value:true,label:"Leave gaps for blank values", checked: this.leaveGaps}
            ],
            label: "Blanks",
            group: "values",
            value: this.leaveGaps
        });

        if( this.parent.factory_id == "chart_series" ) {
            // Make a list of the parent chart's y-axes for the "Use Axis" control
            for(c = 0; c < this.parent.components.length; c++) {
                component = this.parent.components[c];
                if(component.role != "axis_y") continue;

                axisList.push({value: component.id, label: component.displayName});
            }

            // Default to using the primary y-axis
            if(!this.axis && axisList.length) this.axis = axisList[0].value;

            model.push({
                type: "select",
                id: "axis",
                label:"Use Axis",
                group:"axis",
                options: axisList,
                selectedValue: this.axis
            });
        }

        model.push({
            type: "checkboxes",
            id: "overrideColor",
            options: [
                {value:true, label:"Override the default color", checked: this.overrideColor}
            ],
            label: "Series Color",
            group: "color"
        });

        model.push({
            type: "color_picker",
            id: "color",
            group: "color",
            valueType:"theme",
            selectedValue: this.color,
            displayWhen: { overrideColor:true }
        });

        model.push({
            type: "checkboxes",
            id: "seriesHidden",
            options: [
                {value:true, label:"Hide this series by default", checked: this.seriesHidden}
            ],
            label: "Visibility",
            group: "visibility"
        });

        return model;
    },

    getAvailableAggregations: function($super) {
        var aggregations = $super();
        var parentId = this.parent.factory_id;
        var modelType = this.getVirtualColumnType();

        switch (parentId) {
            case "chart_series":
            case "chart_pie":
            case "chart_funnel":
            case "scatter_data":
                if (modelType != DX.ModelType.NUMBER) {
                    aggregations = Props.Defaults.numericTextAggregations;
                }
            break;
                default:
            break;
        }

        return aggregations;
    }

});
;/****** cx.series_labels.js *******/ 

/* global Component:false, isWorkspace:false, bindValues:false */

Component.SeriesLabels = $.klass(Component.Proxy, {

    displayName: "Labels",
    factory_id:"series_labels",

    initialize : function($super, config) {
        $super(config);

        this.disableStyles = true;

        if ( isWorkspace() ) {
            this.labelMixin = new Component.LabelComponent();
            this.allowedFormats = ["txt","num","cur","pct","dat"];
            this.fmt = "txt";
        }
    },

    onParentAssignment: function() {
        var parentId = this.parent.factory_id;

        switch (parentId) {
            case "chart_pie":
            case "chart_funnel":
                this.canGroup = true;
                this.canSort = true;
                this.canFilter = true;
                break;
            case "scatter_data":
                this.canGroup = true;
                this.canFilter = true;
                break;

            default:
                break;
        }
    },

    setSelectedInWorkspace : function($super, selected, newComponent) {
        $super(selected, newComponent);

        this.parent.invalidateAndQueue();
    },

    serialize : function($super) {
        return $super();
    },

    configure : function($super, config) {
        $super(config);
        bindValues(this,config,[]);
    },

    getPropertyModelGroups: function($super) {
        return $.merge($super(), this.labelMixin.getPropertyModelGroups());
    },

    getPropertyModel : function($super) {
        var model = $super();
        var labelModel = _.bind(this.labelMixin.getPropertyModel, this)();

        if (this.parent.factory_id == "chart_pie" ||
            this.parent.factory_id == "scatter_data" ||
            this.parent.factory_id == "chart_funnel") {
            // delete size, wrap
            labelModel = _.reject(labelModel, function(prop){
                return (prop.id == "size" || prop.id == "wrap" || prop.id == "font_style" || prop.id == "font_colour" || prop.id == "align" );
            });

            model = $.merge(model, labelModel);

            return model;
        }

        return model;
    }

});;/****** cx.simple_list.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, isWorkspace:false, FMT:false, KlipFactory:false, bindValues:false, Props:false, page:false */

/**
 *  component which provides two labels in a vertical layout
 */

Component.SimpleList = $.klass(Component.Panel, {

    factory_id : "simple_list",
    displayName: "List",
	tableEl : false,

    maxRows : 8,
    autoColon : true,

    initialize : function($super, cfg) {
        $super(cfg);
//        this.setLayout(new VBoxLayout());
    },


    renderDom : function ($super) {
        $super();
    },


	update : function($super) {
		$super();

        this.el.empty();

        var labels = this.getChildByRole("labels");
        var values = this.getChildByRole("values");

		var labels_data = labels.getData(0);
		var values_data = values.getData(0);

        var labels_len = labels_data.length;
        var values_len = values_data.length;

        var rows = Math.min(Math.max(labels_len, values_len),this.maxRows);

        this.tableEl = $("<table style='margin: 4px; width:100%'></table>");

		for( var i = 0; i < rows; i++) {
            var $item = $("<tr>");

            var $label = $("<td style='width:50%; font-weight:normal; border:none'></td>");
            if (i < labels_len) {
                this.formatCell (labels_data[i], $label, labels, 0);
            }
            var $value = $("<td style='width:50%; border:none'></td>");
            if (i < values_len) {
                this.formatCell (values_data[i], $value, values, 1);
            }

            $item.append($label).append($value);
            this.tableEl.append($item);
		}
		this.setColumnSelection();

        this.el.append(this.tableEl);

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

	setColumnSelection : function () {
		var len = this.components.length;
		for( var i = 0 ; i < len; i++ ) {
			var cx = this.components[i];
			if ( cx.factory_id != "simple_list_col" ) continue;

			if (cx.selected) {
				var selector = "td:nth-child(" + (i + 1) + ")";
				this.tableEl.find(selector).addClass("selected-component");
			}
		}
	},

    formatCell: function (text, cell, label_component, column_index) {
        if (label_component.fmt) text = FMT.format(label_component.fmt,text,label_component.getFmtArgs());

        text =
          (label_component.prefix ? label_component.prefix + " " : "") +
             text +
             (label_component.suffix ? " " + label_component.suffix : "");

        if (!text || text.length == 0 ) text = "&nbsp;";

        if (column_index == 0 && this.autoColon) {
            text += ":";
        }

        cell.html(text);
        cell.addClass("size-" + label_component.size);
        if (label_component.wrap) {
            cell.css("white-space","normal");
        } else {
            cell.css("white-space","nowrap");
        }
    },

    getPropertyModel : function($super) {
        var model = $super();
        model.push({
            type:"select" ,
            id:"maxRows",
            label:"Height" ,
            group:"list",
            options: [
                { value:1, label:"1 row" },
                { value:3, label:"3 rows" },
                { value:5, label:"5 rows" },
                { value:8, label:"8 rows" },
                { value:10, label:"10 rows" },
                { value:15, label:"15 rows" }
            ],
            selectedValue: this.maxRows
        });
        model.push({
            type:"checkboxes",
            id:"autoColon",
            label:"",
            options:[
                {value:true,label:"Use a colon to separate labels", checked:this.autoColon}
            ]
        });


        return model;
    },


    afterFactory :  function() {
        if (! this.components || this.components.length == 0) {
            this.addComponent(KlipFactory.instance.createComponent({
                type:"simple_list_col",
                role: "labels",
                size: 1,
                displayName:"Labels",
                updateParent:true
            }));
            this.addComponent(KlipFactory.instance.createComponent({
                type:"simple_list_col",
                role:"values",
                size: 1,
                displayName:"Values",
                updateParent:true
            }));
        }
    },

    configure : function($super, config, update) {
        $super(config);
        bindValues(this,config,["maxRows","autoColon"]);
    },

    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg,this,["maxRows","autoColon"],{maxRows:8,autoColon:true},true);
        return cfg;
    },


	initializeForWorkspace : function(workspace) {
		// Bind click handler for selecting columns
		this.el.click( $.bind(this.onMouseClick,this) );
	},

	onMouseClick : function(evt) {

		// Find the td that was clicked
		var thisTd = $(evt.target).closest("td");
		// Find the index of that td in its tr parent
		var colIdx =  $(thisTd).parent().children().index(thisTd);

		// Find the selected component column index
		var role = "labels";
		if (colIdx==1) role = "values";
		var col = this.getChildByRole(role);

		page.invokeAction("select_component", col, evt);
	}
})
;

/**
 * SimpleListColumns are proxy components that do not do any direct rendering.  Instead
 * they provide UI access to the SimpleList columns which are an abstraction of its parent table columns.
 */
Component.SimpleListColumn = $.klass(Component.Proxy, {

	factory_id:"simple_list_col",
	suffix: false,
	prefix: false,
	wrap: true,

	configure : function($super, config, update) {
		$super(config);
		bindValues(this,config,["fmt","prefix","suffix","wrap"]);
	},

	serialize : function($super) {
		var cfg = $super();
		bindValues(cfg,this,["fmt","prefix","suffix","wrap"]);
		return cfg;
	},

	getPropertyModel : function($super) {
		var model = $super();
		var fmtOptions = Props.Defaults.formats;

		if(this.fmt === "raw") {
			fmtOptions.push({value:"raw", label:"Raw"});
		}
		
		model.push( { type:"select" , id:"fmt", label:"Type" , group:"label", options: fmtOptions, selectedValue: this.fmt } ) ;

		model.push( { type:"text" , id:"prefix", label:"Prefix" , group:"label" , value:this.prefix} );
		model.push( { type:"text" , id:"suffix", label:"Suffix" , group:"label", value:this.suffix} );
		model.push( { type:"select" , id:"size", label:"Size" , group:"appearance", options: Props.Defaults.size, selectedValue: this.size } );
        model.push({
            type:"checkboxes",
            id:"wrap",
            label:"",
            options:[
                {value:false,label:"Allow word wrap", checked:this.wrap}
            ]
        });
		return model;
	}


});
;/****** cx.simple_value.js *******/ 

/* global Component:false, isWorkspace:false, bindValues:false, KlipFactory:false */

 /**
 *  component which provides two labels in a vertical layout
 */

Component.SimpleValue = $.klass( Component.Base, {

    factory_id : "simple_value",
    displayName: "Value Pair",
    separation: "0",

    initialize : function($super,cfg) {
        $super(cfg);
//		this.setLayout(new VBoxLayout());
    },

    initializeForWorkspace : function(workspace) {
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(400);
        }
    },

    renderDom : function ($super) {
        $super();
    },

    onResize : function($super) {
        $super();

        this.invalidateAndQueue();
    },

    onEvent : function ($super, evt) {
        $super(evt);

        if (evt.id == "theme_changed") {
            this.invalidateAndQueue();
        }
    },

    invalidateAndQueue : function() {
        this.primary.invalidateAndQueue();
        this.secondary.invalidateAndQueue();
    },

    onChildrenUpdated : function($super) {
        this.update();
    },

    update : function($super) {
        var primaryData, secondaryData;

        $super();

        primaryData = this.primary.getData(0);
        secondaryData = this.secondary.getData(0);

        if (primaryData.length == 0 && secondaryData.length == 0) {
            this.el.css("min-height", "15px");
        } else {
            this.el.css("min-height", "");
        }

        this.secondary.el.css("margin-top", this.separation + "px");

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    configure : function($super, config) {
        $super(config);

        bindValues(this, config, ["separation","customCSSClass"]);

        this.invalidate();
    },

    serialize : function($super) {
        var cfg = $super();

        bindValues(cfg, this, ["separation","customCSSClass"],
            {
                separation:"0"
            }, true);

        return cfg;
    },

    getPropertyModel : function($super) {
        var model = $super();

        model.push({
            type:"text",
            id:"separation",
            label:"Pair Separation",
            value: this.separation,
            group:"separation",
            minorLabels: [
                {label: "px", position: "right", css:"quiet italics"}
            ],
            isNumeric:true,
            width:38
        });

        return model;
    },

    afterFactory :  function() {
        var requiredChildren = [
            {role:"primary", props:{displayName:"Primary Value", type:"label", size:2, autoFmt:true,
                formulas: [{"txt":"\"My Value\"","src":{"t":"expr","v":false,"c":[{"t":"l","v":"My Value"}]}}],
                data: [["Value"]]}},
            {role:"secondary", props:{displayName:"Secondary Value", type:"label", size:1, autoFmt:true,
                formulas: [{"txt":"\"My Value\"","src":{"t":"expr","v":false,"c":[{"t":"l","v":"My Value"}]}}],
                data: [["Value"]]}}
        ];
        var i;
        var req;
        var cx;

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);
            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                if ( req.props._data ) cx.setData( req.props._data );
                this.addComponent(cx);
            }

            cx.isDeletable = false;
            cx.isDraggable = false;
        }

        this.primary = this.getChildByProperty("role", "primary");
        this.secondary = this.getChildByProperty("role", "secondary");

        this.primary.el.appendTo( this.el );
        this.secondary.el.appendTo( this.el );
    }

});;/****** cx.sparkline.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, CXTheme:false, updateManager:false, _sanitizeNumbers:false, isWorkspace:false,
    bindValues:false, Props:false, KlipFactory:false, page:false */

Component.Sparkline = $.klass(Component.DataComponent, {

    displayName: "Mini Chart",
    factory_id:"sparkline",

    $sparkline : false ,
    winThreshold: 0,

    defaultArgs : {
        highlightSpotColor:"#5290e9",
        highlightLineColor:"#d7d7d7",
        tooltipOffsetX:20,
        tooltipOffsetY:20
//		width:"60px",
//		height:"18px",
//		lineWidth:2,
//		lineColor:'#4994D0',
//		spotColor:'#4994D0',
//		rangeColors: ["#e2e2e2","#cacaca","#b6b6b6"],
//		targetColor: "#252525",
//		performanceColor: "#898989",
//		fillColor:false,
//		spotRadius:2.5,
//		minSpotColor:false,
//		type:'line',
//		maxSpotColor:false
    },
//
//	args : {},


    sizes : {
        1 : { width:60, height:10, lineWidth:1, spotRadius:1.5, padding:"7px 0 8px 5px",  barSpacing:1 },
        2 : { width:130, height:21, lineWidth:2, spotRadius:2.5, padding:"8px 0 8px 8px",  barSpacing:1},
        3 : { width:190, height:38, lineWidth:3, spotRadius:4.0, padding:"13px 0 8px 10px",  barSpacing:2},
        4 : { width:260, height:48, lineWidth:4, spotRadius:5.5, padding:"19px 0 8px 16px", barSpacing:2 },
        5 : { width:320, height:90, lineWidth:5, spotRadius:6.5, padding:"4px 0 8px 0", barSpacing:3 }
    },


    initialize : function($super) {
        $super();
        this.args = $.extend({}, this.defaultArgs);
        this.displayLastValue = true;

        this.lineFormat = "la";
        this.size = 3;
        this.chartType = "line";
        this.lineColor = "cx-theme_blue_3";
        this.spotColor = "cx-theme_blue_3";
        this.minSpotColor = "cx-theme_blue_3";
        this.maxSpotColor = "cx-theme_blue_3";
        this.fillColor = CXTheme.current.cx_sparkline_area;
        this.barColor = "cx-theme_blue_3";
        this.negBarColor = "cx-theme_red_3";
        this.winColor = "cx-theme_blue_3";
        this.lossColor = "cx-theme_red_3";
        this.drawColor = "cx-theme_aaa";
    },

    initializeForWorkspace:function($super,workspace){
        var ourKlip = this.getKlip();
        if ( !ourKlip.workspace || ! ourKlip.workspace.dimensions ){
            this.setDimensions(400);
        }
    },


    renderDom : function($super) {
        var dom = $super();
        this.$sparkline = $("<div>").addClass("chart");
        dom.append(this.$sparkline);
    },


    onResize : function($super) {
        $super();

        updateManager.queue(this);
    },

    onEvent : function ($super, evt) {
        $super(evt);

        if (evt.id == "theme_changed") {
//            this.update();
        }
    },

    /**
     * gets the preferred widths/heights of managed components and repositions them
     */
    updateComponentBounds : function() {
        this.chartHeight = this.sizes[this.size].height;

        if (! this.displayLastValue ) {
            this.chartWidth = this.dimensions.w - 40;
        } else {
            var lastValHeight = this.lastValue.el.height();

            if ( this.chartHeight < lastValHeight ){
                this.chartHeight = lastValHeight;
                this.topSparkPad = lastValHeight/2 -((this.size-1)*10);
            }
            var lastValWidth =  this.lastValue.el.width();
            var newWidth = this.dimensions.w - lastValWidth  - 40;
            if ( newWidth != this.chartWidth ) updateManager.queue(this);
            this.chartWidth = newWidth;
            this.lastValue.el.css("top",(this.el.height() - lastValHeight) / 2);

        }
    },


    onChildrenUpdated : function($super) {
        this.updateComponentBounds();
    },

    update : function($super) {
        $super();

        var data = new Array();
        data = this.getData(0).slice(0);
        _sanitizeNumbers(data);

        this.lastValue.setVisible(this.displayLastValue);

        if (this.displayLastValue) {
            var lastValCx = this.lastValue;

            var lastVal = data[ data.length - 1];
            if(lastVal ==undefined) {
                lastVal ="";
            }
            lastValCx.setData([ lastVal ]);
            lastValCx.el.css("lineHeight", (this.args.height + 5) + "px");

            updateManager.queue(lastValCx);
//			lastValCx.update();
        }

        // call to recalc this.chartWidth
        // -----------------------------
        this.updateComponentBounds();


        this.args.width = this.chartWidth;
        this.args.height = this.sizes[this.size].height;//this.chartHeight;
        this.args.lineWidth = this.sizes[this.size].lineWidth;
        this.args.spotRadius = this.sizes[this.size].spotRadius;
        this.args.padding = this.sizes[this.size].padding;
        this.args.barSpacing = this.sizes[this.size].barSpacing;//  /(Math.floor(this.getData(0).length/20)+1)
        this.args.barWidth = (this.chartWidth-this.args.barSpacing*this.getData(0).length)/this.getData(0).length;
        this.args.type = this.chartType;
        this.args.lineFormat = this.lineFormat;
        this.args.lineColor = CXTheme.getThemeColour(this.lineColor);
        this.args.spotColor = CXTheme.getThemeColour(this.spotColor);

        this.updateLineColours();

        if (this.args.fill && (this.fillColor.toLowerCase() == CXTheme.themes["default"].cx_sparkline_area.toLowerCase() || this.fillColor.toLowerCase() == CXTheme.themes["dark"].cx_sparkline_area.toLowerCase())) {
            this.args.fillColor = CXTheme.current.cx_sparkline_area;
        }

        if (this.args.type == "bar") {
            this.args.barColor = CXTheme.getThemeColour(this.barColor);
            this.args.negBarColor = CXTheme.getThemeColour(this.negBarColor);
            this.args.highlightLighten = 1.17;
        }

        if (this.args.type == "tristate") {
            this.args.posBarColor = CXTheme.getThemeColour(this.winColor);
            this.args.negBarColor = CXTheme.getThemeColour(this.lossColor);
            this.args.zeroBarColor = CXTheme.getThemeColour(this.drawColor);
            this.args.highlightLighten = 1.17;
            
            var tv = parseFloat(this.winThreshold);
            for (var $x = 0; $x < data.length; $x++) {
                if (data[$x] == tv) {
                    data[$x] = 0;
                } else if (data[$x] > tv) {
                    data[$x] = 1;
                } else {
                    data[$x] = -1;
                }
            }
        }


        //clear previous min/max restrictions
        this.args.chartRangeMin = undefined;
        this.args.chartRangeMax = undefined;
        if(this.chartRangeMinOption == "cust" && this.minCustomVal){
            this.args.chartRangeMin =  this.minCustomVal;
        }
        if(this.chartRangeMaxOption == "cust" && this.maxCustomVal){
            this.args.chartRangeMax =  this.maxCustomVal;
        }

        this.$sparkline.css("padding", this.args.padding);


        if ( this.chartHeight )
            this.$sparkline.height(this.chartHeight);
        if ( this.chartWidth )
            this.$sparkline.width(this.chartWidth);

        // HACK: This is a hack to fix very wide tooltips when display:flex on body
        var $dashboard = $(".c-dashboard");
        if (KF.company.hasFeature("one_product_ui") && $dashboard.length > 0) {
            this.args.tooltipContainer = $dashboard;
        }

        this.$sparkline.empty().sparkline(data, this.args);
        this.$sparkline.find("canvas").css("padding-top",this.topSparkPad+"px");

        var _this = this;
        this.$sparkline.bind("sparklineClick", function(evt){
            if(isWorkspace()) {
                page.invokeAction("select_component", _this);
                return false;
            }
        });

        // update at the end to reposition last-value in case height has changed
        // ---------------------------------------------------------------------
        this.updateComponentBounds();


        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },

    updateLineColours : function() {
        switch (this.lineFormat) {
            case "l":
                this.args.fill = false;
                this.args.fillColor = false;
                this.args.minSpotColor = false;
                this.args.maxSpotColor = false;
                break;
            case "lp":
                this.args.fill = false;
                this.args.fillColor = false;
                this.args.minSpotColor = CXTheme.getThemeColour(this.minSpotColor);
                this.args.maxSpotColor = CXTheme.getThemeColour(this.maxSpotColor);
                break;
            default:
            case "la":
                this.args.fill = true;
                this.args.fillColor = CXTheme.getThemeColour(this.fillColor);
                this.args.minSpotColor = false;
                this.args.maxSpotColor = false;
                break;
            case "lpa":
                this.args.fill = true;
                this.args.fillColor = CXTheme.getThemeColour(this.fillColor);
                this.args.minSpotColor = CXTheme.getThemeColour(this.minSpotColor);
                this.args.maxSpotColor = CXTheme.getThemeColour(this.maxSpotColor);
                break;
        }
    },


    configure : function($super, config) {
        $super(config);
        bindValues(this, config, ["lineFormat","displayLastValue","size","chartType","winThreshold","chartRangeMinOption","minCustomVal","chartRangeMaxOption","maxCustomVal","lineColor","spotColor","minSpotColor","maxSpotColor","fillColor","barColor","negBarColor","winColor","lossColor","drawColor"]);

        if (config.hasOwnProperty("displayLastValue") && config.displayLastValue == undefined) {
            this.displayLastValue = false;
        }
    },


    serialize : function($super) {
        var config = $super();
        bindValues(config, this, ["lineFormat","displayLastValue","size","chartType","winThreshold","chartRangeMinOption","minCustomVal","chartRangeMaxOption","maxCustomVal","lineColor","spotColor","minSpotColor","maxSpotColor","fillColor","barColor","negBarColor","winColor","lossColor","drawColor"],
            {
                lineFormat:"la",
                size:3,
                chartType:"line",
                lineColor:"cx-theme_blue_3",
                spotColor:"cx-theme_blue_3",
                minSpotColor:"cx-theme_blue_3",
                maxSpotColor:"cx-theme_blue_3",
                fillColor:CXTheme.current.cx_sparkline_area,
                barColor:"cx-theme_blue_3",
                negBarColor:"cx-theme_red_3",
                winColor:"cx-theme_blue_3",
                lossColor:"cx-theme_red_3",
                drawColor:"cx-theme_aaa"
            }, true);

        return config;
    },


    getPropertyModel : function($super) {

        var model = $super();
        model.push({
            type:"select" ,
            id:"chartType",
            label:"Chart Type" ,
            group:"sparkline",
            options: [{ value:"line",label:"Sparkline" },{value:"bar",label:"Bar"},{value:"tristate",label:"Win/loss"}] ,
            selectedValue: this.chartType,
            help: {
                link: "klips/mini-properties"
            }
        });

        model.push({
            type:"select" ,
            id:"lineFormat",
            label:"Line Format" ,
            group:"sparkline",
            displayWhen: { chartType:"line" },
            options: Props.Defaults.lineFormat,
            selectedValue: this.lineFormat
        });

        model.push({
            type:"text" ,
            id:"winThreshold",
            label:"Threshold" ,
            group:"sparkline",
            displayWhen: { chartType:"tristate" },
            value: this.winThreshold
        });

        model.push({
            type:"select" ,
            id:"size",
            label:"Size" ,
            group:"sparkline",
            options: Props.Defaults.size,
            selectedValue: this.size
        });

        model.push({
            type:"select" ,
            id:"chartRangeMinOption",
            label:"Range Min" ,
            group:"sparkline",
            displayWhen: { chartType:["line","bar"] },
            options: [{ value:"default",label:"Use lowest value" },{value:"cust",label:"Custom..."}],
            selectedValue: this.chartRangeMinOption
        });
        model.push({
            type:"text" ,
            id:"minCustomVal",
            group:"sparkline",
            displayWhen: { chartRangeMinOption:"cust" },
            value: this.minCustomVal
        });

        model.push({
            type:"select" ,
            id:"chartRangeMaxOption",
            label:"Range Max" ,
            group:"sparkline",
            displayWhen: { chartType:["line","bar"] },
            options: [{ value:"default",label:"Use highest value" },{value:"cust",label:"Custom..."}],
            selectedValue: this.chartRangeMaxOption
        });
        model.push({
            type:"text" ,
            id:"maxCustomVal",
            group:"sparkline",
            displayWhen: { chartRangeMaxOption:"cust" },
            value: this.maxCustomVal
        });

        model.push({
            type:"checkboxes" ,
            id:"displayLastValue",
            options: [
                {value:"1",label:"Display last value", checked: this.displayLastValue}
            ],
            group:"sparkline"
        });


        // Line colours
        model.push({
            type:"color_picker",
            id:"lineColor",
            group:"line_colour",
            label:"Line Color",
            displayWhen: { chartType:"line" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.lineColor
        });

        model.push({
            type:"color_picker",
            id:"fillColor",
            group:"line_colour",
            label:"Area Color",
            displayWhen: { chartType:"line", lineFormat:["la", "lpa"] },
            valueType:"theme",
            defaultColour: {rgb:CXTheme.current.cx_sparkline_area},
            selectedValue:this.fillColor
        });


        model.push({
            type:"color_picker",
            id:"spotColor",
            label:"End Point Color",
            group:"line_colour",
            displayWhen: { chartType:"line" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.spotColor
        });

        model.push({
            type:"color_picker",
            id:"minSpotColor",
            label:"Low/High Colors",
            group:"line_colour",
            minorLabels: [
                {label:"Lowest Point", position:"right"}
            ],
            displayWhen: { chartType:"line", lineFormat:["lp", "lpa"] },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.minSpotColor
        });

        model.push({
            type:"color_picker",
            id:"maxSpotColor",
            group:"line_colour",
            minorLabels: [
                {label:"Highest Point", position:"right"}
            ],
            displayWhen: { chartType:"line", lineFormat:["lp", "lpa"] },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.maxSpotColor,
            flow: {padding:"20px"}
        });

        // Bar colours
        model.push({
            type:"color_picker",
            id:"barColor",
            group:"bar_colour",
            label:"Bar Color",
            displayWhen: { chartType:"bar" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.barColor
        });

        model.push({
            type:"color_picker",
            id:"negBarColor",
            group:"bar_colour",
            label:"Neg Bar Color",
            displayWhen: { chartType:"bar" },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.negBarColor
        });

        // Tristate colours
        model.push({
            type:"color_picker",
            id:"winColor",
            group:"tri_colour",
            label:"Win Color",
            displayWhen: { chartType:"tristate" },
            valueType:"theme",
            defaultColour: {rgb:"#5290e9", theme:"cx-theme_blue_3"},
            selectedValue:this.winColor
        });

        model.push({
            type:"color_picker",
            id:"lossColor",
            group:"tri_colour",
            label:"Loss Color",
            displayWhen: { chartType:"tristate" },
            valueType:"theme",
            defaultColour: {rgb:"#e14d57", theme:"cx-theme_red_3"},
            selectedValue:this.lossColor
        });

        model.push({
            type:"color_picker",
            id:"drawColor",
            group:"tri_colour",
            label:"Draw Color",
            displayWhen: { chartType:"tristate" },
            valueType:"theme",
            defaultColour: {rgb:"#aaaaaa", theme:"cx-theme_aaa"},
            selectedValue:this.drawColor
        });

        return model;
    },


    afterFactory : function() {
        if (!this.getChildByRole("last-value")) {
            this.addComponent(KlipFactory.instance.createComponent({
                type:"label",
                displayName:"Last Value",
                size:3,
                dataDisabled:true,
                role:"last-value"
            }));
        }

        this.lastValue = this.getChildByRole("last-value") ;
        this.lastValue.isDeletable = false;
        this.lastValue.isDraggable = false;
        this.lastValue.el.css("position","absolute").css("right","10px").appendTo(this.el).hide();
        this.chartWidth = 100;

        this.el.append($("<div>").addClass("clearfix"));
    },

    clearFormula : function($super,idx) {
        $super();
        this.setData([0,0]);
    }
});
;/****** cx.table.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, dashboard:false, isWorkspace:false, updateManager:false, page:false, bindValues:false,
    KlipFactory:false, dataFilters:false, global_dialogUtils:false, global_contentUtils:false, DX:false,
    safeText:false, Props:false */

Component.Table = $.klass(Component.Base, {

    factory_id : "table",
    displayName:"Table",
    variableSubComponents: [
        { label:"Column", type:"table_col", role:"tcol" }
    ],

    tableEl : false,
    tbodyEl : false,
    theadEl : false,
    resultsEl : false,
    ddPathEl : false,

    tableRowsType: "upto",
    tableRows:8,
    maxRows:8,
    showHeader : true,
    firstRowIsHeader : false,
//    showResultsRow : false,
    showBorder : true,

    colCount: 0,
    rowCount: 0,
    sortData : null,
    sortApplied : false,
    scrollbarWidth : 15,

    drilldownSupported : true,

    canHaveDataSlots: true,

    initialize : function($super) {
        $super();
    },

    renderDom : function($super) {
        $super();

        if (dashboard.config.mobile) {
            this.scrollbarWidth = 0;
        }

        this.tableContainer = $("<div>").addClass("table-container");
        this.tableEl = $("<table><thead style='display:none' ><tr/></thead><tbody></tbody></table>").addClass("cx-table").addClass("overview");
        this.theadEl = $("<table><tbody><tr/></tbody></table>").addClass("cx-table").addClass("cx-table-headers");
        this.resultsEl = $("<table><tbody><tr/></tbody></table>").addClass("cx-table").addClass("cx-table-results");

        this.theadEl.delegate("th","click", _.bind(this.onHeaderClick,this) );
        this.tableEl.delegate(".grouped", "click", _.bind(this.onCellClick, this));

        this.ddPathEl = $("<div class='drilldown-path'>")
            .append($("<span class='ddpath-back'>Back</span>").prepend($("<span class='ddpath-back-icon'></span>")))
            .append($("<ul class='ddpath-breadcrumbs'></ul>"))
            .delegate(".ddpath-back, li.level", "click", _.bind(this.onPathClick, this));
    },


    /**
     * do as little work as possible
     * @param $super
     */
    update : function($super) {
        $super();
        if ( !this.cols ) return;
        if ( !this.getKlip().isVisible ) return;


        var invalidCols = false;
        for( var i = 0 ; i < this.cols.length ; i++)
            if ( this.cols[i].isInvalid ){
                invalidCols = true;
                break;
            }


        // if nothing is invalid -- return..
        if ( !this.isInvalid && !invalidCols )
            return;

        // if there are invalid columns, it is *possible* that new table markup might be required.
        // ---------------------------------------------------------------------------------------
        if ( this.isInvalid || invalidCols ){
            this.reindexColumns();
            this.updateFilteredData();

            if (!isWorkspace()) {
                if (this.drilldownEnabled) {
                    this.applyDrilldownSort(this.drilldownStack.length - 1);
                } else {
                    this.updateSortIndexes();
                }
            }

            this.updateTableMarkup();
        }


        // once updateTableMarkup is called it is safe to call updateTableData which will re-render
        // the cells of any columns which are invalid
        // -----------------------------------------------------------------------------------------
        if ( invalidCols ) {
            this.updateTableData();
        }


        if( isWorkspace() ) {
            if (!this.el.hasClass("selected-component")) {
                if (this.selectedCol) {
                    this.setSelectedColumn();
                } else if (this.selectedResultProxy) {
                    this.selectedResultProxy.setSelectedInWorkspace(true);
                } else if (this.selectedHeaderProxy) {
                    this.selectedHeaderProxy.setSelectedInWorkspace(true);
                }
            }
            this.theadEl.toggle( this.showHeader );
        }


        if (!isWorkspace() && this.drilldownEnabled) {
            if (this.showBorder) {
                this.ddPathEl.addClass("border-top-1 border-left-1 border-right-1").removeClass("border-bottom-1");
            } else {
                this.ddPathEl.removeClass("border-top-1 border-left-1 border-right-1").addClass("border-bottom-1");
            }

            this.updateDrilldownPath();
        }

        // always update column widths
        this.updateColumnWidths();
        if ( invalidCols || this.isInvalid ){
            this.updateScrollPane();
        }
//		this.updateColumnWidths();

        this.isInvalid = false;

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
    },


    /**
     * invalidate this component and all of its columns
     */
    invalidateCols : function() {
        this.invalidate();
        if ( !this.cols ) return;
        for(var i = 0 ; i < this.cols.length ; i++){
            this.cols[i].invalidate();
        }
    },


    /**
     *
     */
    updateColumnWidths : function() {
        var cols = this.cols;
        var len = cols.length;
        var i, c;

        var availWidth = this.el.width();
        var tableWidth = availWidth;

        var isScrollbarVisible = this.maxRows && (this.rowCount > this.maxRows);
        if (isScrollbarVisible) {   // if a scroll bar will be used
            availWidth -= this.scrollbarWidth;
        }


        // remove borders and scrollbar from available width
        // ----------------------------------------------------------------------
        var borderWidth = 0;
        for (i = len - 1; i >= 0; i--) {
            c = cols[i];

            if ((c.visible && !c.groupHidden) || isWorkspace()) {
                borderWidth += (c.borderLeft == "auto" ? ((this.showBorder || c != this.firstVisibleCol) ? 1 : 0) : parseInt(c.borderLeft));
                borderWidth += (c.borderRight == "auto" ? ((this.showBorder && c == this.lastVisibleCol) ? 1 : 0) : parseInt(c.borderRight));
            }
        }
        availWidth -= borderWidth;


        // determine the floating width by calculating total implicit fixed width
        // ----------------------------------------------------------------------
        var fixedTotal = 0;
        var floatColCount = 0;
        for (i = 0; i < len; i++) {
            c = cols[i];

            if ( (!c.visible || c.groupHidden) && !isWorkspace() ) {
                c.setCalculatedWidth(0);
            } else {
                if ( c.fw && c.w != undefined && c.w.length > 0 ) {
                    if ( c.w.indexOf("%") != -1 ) {
                        var pw = parseFloat(c.w.substring(0, c.w.length - 1));
                        if (pw != 0) pw = pw / 100;
                        var pwAsPx = availWidth * pw;
                        fixedTotal += pwAsPx;
                        c.setCalculatedWidth(pwAsPx);
                    } else {
                        fixedTotal += parseFloat(c.w);
                        c.setCalculatedWidth(parseFloat(c.w));
                    }
                } else {
                    floatColCount++;
                }
            }
        }

        if (floatColCount == 0) {
            tableWidth = fixedTotal + borderWidth;

            if (isScrollbarVisible) {
                tableWidth += this.scrollbarWidth;
            }
        } else {
            availWidth -= fixedTotal;
            var floatingWidth = Math.floor(availWidth / floatColCount);
            var diff = availWidth - (floatingWidth * floatColCount);

            // set the computed width (cw) on each column that does not have an implicit fixed width (fw)
            // ------------------------------------------------------------------------------------------
            for (i = 0; i < len; i++) {
                c = cols[i];
                if ( ((c.visible && !c.groupHidden) || isWorkspace()) && !(c.fw && c.w) ) {
                    c.setCalculatedWidth(floatingWidth + (diff-- > 0 ? 1 : 0));
                }
            }
        }

        this.tableContainer.width( tableWidth );
        this.tableEl.width( tableWidth );
        this.theadEl.width( tableWidth );
        this.resultsEl.width( tableWidth );


        // once we know the widths of all the columns, go through each table (head,body,results)
        // and set the widths of each column.
        // ---------------------------------------------------------------------------------------
        var _this = this;
        if ( this.showHeader ) {
            this.theadEl.find("th").each(function(idx) {
                $(this).width(cols[idx].cw + (isScrollbarVisible && cols[idx] == _this.lastVisibleCol ? _this.scrollbarWidth : 0));
            });
        }

        // for the body table we only need to set the width of the first row
        // --------------------------------------------------------
        this.tableEl.find("tbody tr:first-child td").each(function(idx) {
            $(this).width(cols[idx].cw + (isScrollbarVisible && cols[idx] == _this.lastVisibleCol ? _this.scrollbarWidth : 0));
        });

        if ( this.showResultsRow ) {
            this.resultsEl.find("td").each(function(idx) {
                $(this).width(cols[idx].cw + (isScrollbarVisible && cols[idx] == _this.lastVisibleCol ? _this.scrollbarWidth : 0));
            });
        }

        // hack to make the header column widths correct in Chrome
        this.theadEl.detach();
        if (this.el.children(".drilldown-path").length > 0) {
            this.theadEl.insertAfter(this.ddPathEl);
        } else {
            this.theadEl.prependTo(this.el);
        }
    },


    enableScrollpane : function( enable, h ) {
        // if disabling and we have a scrollPane element, destory it.
        if (!enable && this.$scrollPane ) {
            this.theadEl.after(this.tableEl);
            this.el.removeClass("has-scrollbar");
            this.$scrollPane.remove();
            this.$scrollPane = false;
        } else if( enable && !this.$scrollPane ) {
            // if enable, and we have no scrollPane el, create one..

            this.el.addClass("has-scrollbar");
            this.$scrollPane = $("<div>").addClass("cx-scroll");
            if (!dashboard.config.mobile) {
                this.$scrollBar = $("<div class=\"scrollbar\"><div class=\"track\"><div class=\"thumb\"><div class=\"end\"></div></div></div></div>");
                this.$scrollPane.prepend(this.$scrollBar);
            }
            this.$scrollPaneViewport = $("<div>").addClass("viewport");
            this.$scrollPane.append(this.$scrollPaneViewport);
            this.$scrollPaneViewport.append(this.tableEl);
            this.$scrollPaneViewport.height(h);
            this.theadEl.after(this.tableContainer.html(this.$scrollPane));
            // Using native scrollbars instead of tinyscrollbar on mobile because the
            // latter does not allow the user to scroll by dragging the table's content
            if (!dashboard.config.mobile) {
                this.$scrollPane.tinyscrollbar();
                this.$scrollPaneViewport.css("overflow","hidden");
            }
            if (this.showBorder || this.showHeader) this.$scrollPane.addClass("border-top-1");
            if (this.showBorder || this.showResultsRow) this.$scrollPane.addClass("border-bottom-1");

            if (this.$scrollBar && (this.lastVisibleCol.borderRight != "auto")) {
                this.$scrollBar.css("right", this.lastVisibleCol.borderRight + "px");
            }

            // re-inserting the results table helps with updating the padding (not sure why it is required)
            // -------------------------------------------------------------
            this.el.append(this.resultsEl);
        }
    },


    updateScrollPane : function() {
        var scrollRequired = ( this.maxRows &&  (this.rowCount > this.maxRows ) );

        if ( scrollRequired ) {
            if ( this.$scrollPane ) return; // nothing to do..
            var row = this.tableEl.find("tbody tr:eq(0)");
            var height = Math.min(row.height() * this.maxRows,this.tableEl.height());//account for uneven rows
            if (dashboard.config.mobile) {
                height += row.height() / 2; // add half a row to the height to let user know that there's more content
            }
            this.enableScrollpane(true,height);

        } else {
            this.enableScrollpane(false);
        }

    },


    /**
     * responsible for updating the TABLE & TD markup, ensuring that there are enough cells for
     * columns to render their contents into.  The intent is that the effort of DOM creation only happens when
     * there is a change in the dimensions of the data.
     */
    updateTableMarkup : function() {
        this.enableScrollpane(false);

        var colCount = this.cols.length;
        var i;

        var rowMax = 0;
        this.showResultsRow = false;
        this.firstVisibleCol = false;
        this.lastVisibleCol = false;
        for (i = 0; i < colCount; i++) {
            var c = this.cols[i];
            if ((c.visible && !c.groupHidden) || isWorkspace()) {
                if (this.firstVisibleCol === false) this.firstVisibleCol = c;
                this.lastVisibleCol = c;

                rowMax = Math.max( c.filteredData.length, rowMax );
                if (c.resultFmt && c.resultFmt != "empty") this.showResultsRow = true;
            }
        }
        if (isWorkspace() && rowMax == 0) rowMax = 4;

        var rowCap = Component.Table.MAX_ROWS;

        if (!isWorkspace()) {
            // maxData is a schema level override - if it is provided it must be used
            if (this.maxData) {
                rowCap = this.maxData;
            } else if (dashboard.config.imagingMode) {
                // If the number of visible rows is less than the global cap, use it (huge performance boost to only render visible rows and not render rows hidden by scrollbar)
                if (this.maxRows < Component.Table.MAX_ROWS) {
                    rowCap = this.maxRows;
                }
            }
        }

        rowMax = Math.min(rowCap,rowMax); // cap the number of rows to 400

        if ( rowMax == 0 ) rowMax = 1;
        if (!this.isVCFeatureOn() && this.firstRowIsHeader) {
            rowMax--;
        }

        if (this.tableRowsType == "exactly" && rowMax < this.maxRows && this.maxRows < rowCap) {
            rowMax = this.maxRows;
        }

        this.resultsEl.toggle( this.showResultsRow );
        this.theadEl.toggle( this.showHeader );


        /// --------------------------------------------------------------
        // EXIT EARLY:
        // if there is no change in the number of cols or rows, do nothing
        // ---------------------------------------------------------------
        if ( colCount == this.colCount && rowMax == this.rowCount)
            return;


        // add/remove COLS if there is a difference in the number of columns
        // ------------------------------------------------------------------
        var colDiff = Math.abs(colCount - this.colCount);
        var rowDiff = Math.abs(rowMax - this.rowCount);

        if( rowDiff == 0 && colDiff == 0 ){
            // re-appending elements seems to be necessary to make cols line up
            this.el.append(this.theadEl).append(this.tableEl).append(this.resultsEl);
            return;
        }

        // markup change is required .. start by detaching dom elements to make updates more efficient
        // --------------------------------------------------------------------------------------------
        this.tableEl.detach();
        this.theadEl.detach();
        this.resultsEl.detach();

        if ( colDiff != 0 ) {
            if ( colCount > this.colCount ) {
                // add header & result cells
                // --------------------------
                var n = colDiff;
                var $headTr = this.theadEl.find("tr");
                var $resultsTr = this.resultsEl.find("tr");
                var $bodyHeadTr = $(this.tableEl[0].firstChild.firstChild);

                while(n-- > 0 ){
                    $resultsTr.append("<td><div class='cell'></div></td>");
                    $headTr.append("<th><div class='cell'></div></th>");
                    $bodyHeadTr.append( $("<th></th>"));
                }
                // add row cells
                // -------------
                $.each(this.tableEl.find("tbody tr"),function(){
                    n = colDiff;
                    while(n-- > 0 ) $(this).append("<td><div class='cell'>&nbsp;</div></td>");
                });
            } else if ( colCount < this.colCount ) {
                while(colDiff-- > 0){
                    this.tableEl.find("tr td:first-child").remove() ;
                    this.theadEl.find("tr th:first-child").remove();
                    this.resultsEl.find("tr td:first-child").remove();
                }
            }

            this.colCount = colCount;
            this.invalidateCols();
        }


        // add/remove ROWS if there is a difference
        // -----------------------------------------

        if ( rowMax > this.rowCount ){
            while(rowDiff-- > 0 ){
                var $r = $("<tr>");
                for (i = 0; i < colCount; i++) {
                    $r.append( $("<td><div class='cell'><div class='label-inner size-xx-small cx-color_444' style='white-space: nowrap; '>&nbsp;</div></div></td>") );
                }
                this.tableEl.append($r);
            }
        } else if(rowMax < this.rowCount ) {
            while(rowDiff-- > 0){
                this.tableEl.find("tbody tr:eq(0)").remove();
            }
        }

        // update each columns cached td set
        // ---------------------------------
        for( i = 0 ; i < colCount ; i++) {
            this.cols[i].cellsReady = true;
        }

        this.rowCount = rowMax;


        // ------------------------------------------------------------------------------
        // re-attach table elements
        // detaching seems to trigger a reflow which enables cell (hidden) overflow
        // ---------------------------------------------------------------------------
        this.el.append(this.theadEl).append(this.tableEl).append(this.resultsEl);
        this.invalidateCols(); // by this point cells have been added/removed, so all bets are off -- repaint all cols
        this.updateSortIndexes();
    },

    /**
     * go through all the columns and update the ones that are invalid
     */
    updateTableData : function() {
        var clen = this.cols.length;
        for( var i = 0 ; i < clen ; i++ ) {
            var c = this.cols[i];
            var sorted = false;
            if(this.sortData && this.sortData[1] != 0) sorted = true;
            if ( !c.isInvalid && !sorted ) continue;

//            this.tableEl.hide();  // hiding the table during cell updates improves preformance a lot..
            this.updateColumn(c);
        }
//        this.tableEl.show();
    },

    updateDrilldownPath : function() {
        if (this.drilldownStack.length > 1) {
            var $breadcrumbs = this.ddPathEl.find("ul.ddpath-breadcrumbs").empty();

            for (var i = 0; i < this.drilldownStack.length; i++) {
                var level = this.drilldownStack[i];

                if (i > 0) $breadcrumbs.append($("<li>/</li>"));

                var levelText = level.cdId == undefined ? "All" :
                    (level.cdId == "" ? "Other" :
                        (level.displayText ? level.displayText :
                            level.cdId));

                var $pathLvl = $("<li class='level'>").text(levelText).data("levelIdx", i);

                if (i == this.drilldownStack.length - 1) {
                    $pathLvl.removeClass("level").addClass("last-level");
                }

                $breadcrumbs.append($pathLvl);
            }

            this.el.prepend(this.ddPathEl);
        } else {
            this.ddPathEl.detach();
        }
    },


    /**
     *
     * @param $super
     */
    onResize : function($super) {
        $super();
        this.updateColumnWidths();

        var invalidCols = false;
        for( var i = 0 ; i < this.cols.length ; i++ ){
            var col = this.cols[i];
            if ( col.fmt == "spk" || col.fmt == "bch" || col.fmt == "spb" || col.fmt == "dsc" || col.fmt == "wlc" || col.fmt == "hrz" ) {
                col.invalidate();
                invalidCols = true;
            }
        }

        if(invalidCols) updateManager.queue(this);
    },


    /**
     *
     * @param $super
     * @param config
     */
    configure : function($super, config) {
        var _this = this;

        $super(config);

        var i;

        this.invalidate();

        if (config["~propertyChanged"] && config.numColumns) {
            var diff = config.numColumns - this.numColumns;
            if (diff > 0) {
                for (i = 1; i <= diff; i++) {
                    this.addColumn({name:"New Column"});
                }
            } else if (diff < 0) {
                var colsToRemove = [];
                var hasFormula = false;

                diff *= -1;
                for (i = 1; i <= diff; i++) {
                    var colToRemove = this.cols[this.cols.length - i];
                    colsToRemove.push(colToRemove);

                    if (colToRemove) {
                        var cxFormula = colToRemove.getFormula(0);

                        if (cxFormula) {
                            hasFormula = true;
                        }
                    }
                }

                if (hasFormula) {
                    global_dialogUtils.confirm(global_contentUtils.buildWarningIconAndMessage("Are you sure you want to delete one or more columns with data?"), {
                        title: "Delete Column",
                        okButtonText: "Delete"
                    }).then(function() {
                        colsToRemove.forEach(function(colToRemove) {
                            _this.removeComponent(colToRemove);
                        });

                        if (isWorkspace()) page.updateMenu();

                        _this.invalidateCols();
                        updateManager.queue(_this);
                    }, function() {
                        page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
                    });

                    return;
                } else {
                    colsToRemove.forEach(function(colToRemove) {
                        _this.removeComponent(colToRemove);
                    });
                }
            }

            if (isWorkspace()) page.updateMenu();
        }

        if (!isWorkspace() && this.drilldownEnabled) {
            this.drilldownSort = [];
            for (i = 0; i < this.drilldownConfig.length; i++) {
                this.drilldownSort.push(null);
            }
        }

        bindValues(this, config, ["firstRowIsHeader","tableRowsType","maxRows","maxData","showHeader",/*"showResultsRow",*/"showBorder"]);

        this.hasVCSupport = (DX.Manager.instance.dpnType == DX.Manager.DpnType.CLUSTER);

        if (config["~propertyChanged"] && config.firstRowIsHeader !== undefined) {
            if (this.isVCFeatureOn()) {
                if (isWorkspace()) {
                    this.updateDstRootFormulas();
                }
            } else {
                if (this.cols) {
                    for (i = 0; i < this.cols.length; i++) {
                        this.cols[i].updateDisplayName();
                    }

                    if (isWorkspace()) page.updateMenu();
                }
            }
        }

        if (config.drilldownConfig) {
            if (this.drilldownEnabled) {
                this.cleanDrilldownConfig();
            }
        }

        if (config.maxRows) {
            this.tableRows = config.maxRows;
        } else if (config.tableRows) {
            this.tableRows = config.tableRows;
            this.maxRows = config.tableRows;
        }

        if (config.tableRowsType == "all") {
            this.maxRows = 100000;
        } else if (config.tableRowsType == "upto" || config.tableRowsType == "exactly") {
            this.maxRows = this.tableRows;
        }

        if (config["~propertyChanged"]) {
            this.invalidateCols();
        }
    },

    /**
     *
     * @param $super
     */
    serialize : function($super) {
        var cfg = $super();
        bindValues(cfg, this, ["tableRowsType","maxRows","maxData","showHeader","firstRowIsHeader",/*'showResultsRow',*/"showBorder"],
            {
                tableRowsType:"upto",
                maxRows:8,
                showHeader:true,
                firstRowIsHeader:false,
//                showResultsRow:false,
                showBorder:true
            }, true);
        return cfg;
    },


    /**
     * creates a new column, and underlying component based on the model provided
     * @param model object
     *	- name
     *  - align
     *  - format
     *  - formatArgs
     *  - resultFn
     * @param index
     */
    addColumn : function(model, index) {
        // update the model to have default properties, if they weren't specified
        model = $.extend({ align:"left", data:[] }, model);

        // create a component DSL to create a table column, then send it to the factory
        // ---------------------------------------------------------------------------

        // if no index is specified, set index to the end position
        if ( !index && index != 0 ) index = this.cols.length;

        var cxConfig = $.extend({type:"table_col",index: (index - 0), data:[] }, model);
        var colCx = KlipFactory.instance.createComponent(cxConfig);

        colCx.parent = this;


        this.addComponent(colCx, index);
        this.reindexColumns();

        return colCx;
    },


    getColumn : function(idx) {
        var len = this.components.length;
        var i;
        var colPos = -1;
        for (i = 0; i < len; i++) {
            var cx = this.components[i];
            if (cx.factory_id == "table_col") {
                colPos++;
                if (colPos == idx) return cx;
            }
        }
    },


    getNextColumn : function(col) {
        var currentIndex = col.index;
        var i;

        for (i = currentIndex + 1; i < this.components.length; i++) {
            if (this.components[i].factory_id == "table_col") return this.components[i];
        }
        for (i = currentIndex - 1; i >= 0; i--) {
            if (this.components[i].factory_id == "table_col") return this.components[i];
        }
        return null;
    },


    reindexColumns : function() {
        this.cols = this.getChildrenByType("table_col");
        this.numColumns = this.cols.length;

        for (var i = 0; i < this.numColumns; i++) {
            this.cols[i].index = i;
        }
    },


    onEvent : function ($super, evt) {
        if (evt.id == "theme_changed") {
            this.invalidateCols ();
        } else if( evt.id == "parent_tab_selected") {
            // during tab switch we need to redo some rendering that may not been done properly when the
            // klip wasn't visible
            this.invalidate();
            updateManager.queue(this);
        } else if ( evt.id == "added_to_tab" || evt.id == "added_to_dashboard" ) {
            this.applyComponentProps();
        }
    },


    onHeaderClick : function(evt) {
        var idx = $(evt.currentTarget).index();

        if (!isWorkspace()) {
            var col = this.cols[idx];
            var sortDir = col.sortDir + 1;
            if (sortDir > Component.Table.SORT_DESC) sortDir = Component.Table.SORT_DEFAULT;

            this.sortData = [idx, sortDir];

            if (this.drilldownEnabled) {
                var ddSortKey = this.getIndexPath() + ":drilldown-sort";

                this.drilldownSort[this.drilldownStack.length - 1] = this.sortData;

                this.setComponentProp(ddSortKey, JSON.stringify(this.drilldownSort));
                this.sorted = (sortDir != Component.Table.SORT_DEFAULT);
            } else {
                var sortPropKey = this.getIndexPath() + ":tablesort";

                if (sortDir == Component.Table.SORT_DEFAULT) {
                    this.setComponentProp(sortPropKey, false);
                    this.sorted = false;
                } else {
                    this.setComponentProp(sortPropKey, this.sortData);
                    this.sorted = true;
                }
            }

            this.sortByColumn(col, sortDir, true);
        }
    },

    onCellClick : function(evt) {
        var $target = $(evt.target);
        var value = $target.data("drilldown-data");

        if (value == "Other") value = ""; // TODO hack. fix!

        this.pushDrilldownLevel( { cdId : value, displayText: $target.text() } );

        this.setComponentProp(this.getIndexPath() + ":drilldown", JSON.stringify(this.drilldownStack));
    },

    onPathClick : function(evt) {
        var $target = $(evt.target);

        if ($target.hasClass("ddpath-back") || $target.parent().hasClass("ddpath-back")) {
            this.popDrilldownLevel();
        } else if ($target.is("li")) {
            var levelIdx = $target.data("levelIdx");

            while (this.drilldownStack.length > levelIdx + 1) {
                this.popDrilldownLevel();
            }
        }

        this.setComponentProp(this.getIndexPath() + ":drilldown", JSON.stringify(this.drilldownStack));
    },

    applyDrilldownSort : function(levelIdx) {
        this.sortData = (levelIdx != undefined && levelIdx < this.drilldownSort.length) ? this.drilldownSort[levelIdx] : null;

        if (this.sortData != null) {
            this.sorted = (this.sortData[1] != Component.Table.SORT_DEFAULT);

            var col = this.getColumn(this.sortData[0]);
            this.sortByColumn(col, this.sortData[1], false);
        } else {
            this.sorted = false;
            this.sortByColumn(false, Component.Table.SORT_DEFAULT, false);
        }
    },


    sortByColumn : function(cx, dir, doUpdate) {
        // reset the sort order of all columns
        // -----------------------------------
        for (var i = 0; i < this.cols.length; i++) {
            this.cols[i].sortDir =  Component.Table.SORT_DEFAULT;
        }

        if (dir ==  0) {
            this.sortedData = null;
        } else {
            if (this.rowCount != 0) {
                this.sortedData = cx.sortWithIndexes(dir, this.rowCount);
            }

            cx.sortDir = dir;
        }

        if (doUpdate && this.rowCount > 0) {
            this.invalidateCols();
            updateManager.queue(this);
        }
    },

    setSelectedColumn : function (cx ) {
        var idx;
        if (!cx && !this.selectedCol ) return;
        if (!cx ) cx = this.selectedCol;

        if (this.selectedCol && cx != this.selectedCol ){
            idx = this.selectedCol.index;
            this.theadEl.find("th:eq("+idx+") ").removeClass("selected-component");
            this.resultsEl.find("td:eq("+idx+") ").removeClass("selected-component");
            this.tableEl.find("td:nth-child("+(idx+1)+") ").removeClass("selected-component");
        }

        idx = cx.index;
        this.theadEl.find("th:eq("+idx+") ").addClass("selected-component");
        this.resultsEl.find("td:eq("+idx+") ").addClass("selected-component");
        this.tableEl.find("td:nth-child("+(idx+1)+") ").addClass("selected-component");

        this.selectedCol = cx;

    },

    drawDstHints : function(cx) {

        if (!cx || !cx.hasDstActions()) return;

        var virtualColumnId = cx.getVirtualColumnId();

        var dstOperationData = cx.getDstOperationsAppliedToDimensionId(virtualColumnId);

        var filterHint, sortHint,visualCueTitle;
        var $dstFilterHint, $dstSortHint;
        var sortDirectionHintClass;

        var headerElement = this.theadEl.find("th:eq("+cx.index+") ");

        //Remove visual cues if not needed
        var dstCueContainer = headerElement.find(".dst-cue-container");
        var cellElement = headerElement.find(".cell");
        if(dstOperationData.sorted=== 0 && !dstOperationData.filtered) {
            if(dstCueContainer.length > 0) {
                dstCueContainer.remove();
            }
            cellElement.css({paddingRight: "0px", position: "static"});
            return;
        }
        //add spacing for visual cues. Cues are positioned absolutely so that they always show up even if the text
        //doesn't fit
        var padding = (dstOperationData.sorted!==0 && dstOperationData.filtered) ? "30" : "15";
        cellElement.css({paddingRight: padding + "px", position: "relative"});


        if(dstCueContainer.length < 1) {
            dstCueContainer = $("<div class='dst-cue-container'></div>");
            cellElement.append(dstCueContainer);
        }

        filterHint = headerElement.find(".dst-hint-icon-filter");
        if(filterHint.length>0 && !dstOperationData.filtered) headerElement.find(".dst-hint-icon-filter").remove();

        if(filterHint.length == 0 && dstOperationData.filtered){
            $dstFilterHint = $("<span title='Filter is applied' class='dst-hint-icon-column dst-hint-icon-filter'></span>");
            dstCueContainer.append($dstFilterHint)
        }

        sortHint = headerElement.find(".dst-hint-sort-icon");
        if(sortHint.length > 0 && dstOperationData.sorted===0) headerElement.find(".dst-hint-sort-icon").remove();

        if(sortHint.length == 0 && dstOperationData.sorted!==0){
            sortDirectionHintClass = "dst-hint-icon-sort-asc";
            if (dstOperationData.sorted == 2) {
                sortDirectionHintClass = "dst-hint-icon-sort-desc";
            }
            visualCueTitle = cx.getTitleBySortOrder(dstOperationData.sorted);
            $dstSortHint = $("<span title='Sorted "+visualCueTitle+"' class='dst-hint-icon-column "+sortDirectionHintClass+" dst-hint-sort-icon'></span>");
            dstCueContainer.append($dstSortHint);
        }

    },

    deselectColumn : function(idx){
        this.theadEl.find("th:eq("+idx+") ").removeClass("selected-component");
        this.resultsEl.find("td:eq("+idx+") ").removeClass("selected-component");
        this.tableEl.find("td:nth-child("+(idx+1)+") ").removeClass("selected-component");

        this.selectedCol = false;
    },


    /**
     * since we create cells in different parts of the component, provide
     * a common way to style cells based on their column model
     * @param cell
     * @param columnModel
     * @param cellValue
     * @param cellType
     * @param config
     */
    styleCell : function(cell, columnModel, cellValue, cellType, config) {
        var proxyCx;
        var td;
        var isGrouped;
        var width;
        var bgColor;
        if ( !cell || cell.length == 0 ) return;

        // clear the css otherwise rules can stack in the workspace
        // --------------------------------------------------------
        td = cell.parent();
        cell.attr("class","cell").css("display", "block");
        td.attr("class","");


        if (columnModel == this.lastVisibleCol) td.addClass("lastVisibleCol");

        if (columnModel.align) cell.addClass("align-" + columnModel.align);
        if (config.fmt) cell.addClass("fmt-" + config.fmt);

        if (config.borderTop) td.addClass("border-top-" + config.borderTop);
        if (config.borderLeft) td.addClass("border-left-" + config.borderLeft);
        if (config.borderRight) td.addClass("border-right-" + config.borderRight);
        if (config.borderBottom) td.addClass("border-bottom-" + config.borderBottom);

        if (!columnModel.visible) {
            if (isWorkspace()) {
                cell.addClass("col-hidden");
            } else {
                td.addClass("border-hidden");
                cell.hide();
            }
        }

        if (columnModel.groupHidden) {
            if (isWorkspace()) {
                cell.addClass("col-group-hidden");
            } else {
                td.addClass("border-hidden");
                cell.hide();
            }
        }

        // apply background colours to the cells
        // -------------------------------------
        td.css("background-color", "");
        if (config.rCtx) {
            bgColor = config.rCtx.bgColor;

            if (bgColor) {
                if (bgColor.indexOf("#") != -1) {
                    td.css("background-color", bgColor);
                } else {
                    td.addClass(bgColor);
                }
            }
        }


        // only apply formats to cell, and not to headers
        // ----------------------------------------------


        if (cellValue == undefined || $.trim(cellValue).length == 0) {
            cell.html("<div class='label-inner size-xx-small cx-color_444' style='white-space: nowrap; '>&nbsp;</div>");
            return;
        }


        width = Math.max(0, columnModel.cw - 16 /* cell padding */);

        switch(cellType) {
            case Component.Table.CELL_RESULT:
            case Component.Table.CELL_CUSTOM_HEADER:
                if(cellType === Component.Table.CELL_CUSTOM_HEADER) {
                    proxyCx = columnModel.headerProxy;
                } else {
                    proxyCx = columnModel.resultProxy;
                }

                if (proxyCx.isCustomRow()) {
                    if(cellType === Component.Table.CELL_CUSTOM_HEADER) {
                        cell.removeClass("non-custom-header-style");
                    }
                    cell.empty().append(Component.LabelComponent.Render(proxyCx, [cellValue], {
                        mergeArgs: { width:width },
                        rCtx: config.rCtx
                    }));
                } else {
                    // use label renderer with no 'index' so that reactions from col are not used for result
                    isGrouped = columnModel.grouped;
                    columnModel.grouped = false;    // so results rows don't look like they are clickable
                    cell.empty().append(Component.LabelComponent.Render($.extend({}, columnModel, {"font_style":"bold"}), [cellValue]));
                    if(cellType === Component.Table.CELL_CUSTOM_HEADER) {
                        cell.addClass("non-custom-header-style");
                    }
                    columnModel.grouped = isGrouped;
                }

                if(isWorkspace()) {
                    cell.addClass("workspace-header-style");
                }

                break;
            case Component.Table.CELL_DEFAULT:
                cell.empty().append(Component.LabelComponent.Render(columnModel, [cellValue], {
                    mergeArgs: { width:width },
                    rCtx: config.rCtx
                }));
                break;

            case Component.Table.CELL_HEADER:
                cell.addClass("non-custom-header-style");

                if(isWorkspace()) {
                    cell.addClass("workspace-header-style");
                }

                if(KF.company.hasFeature("saas_11282") && (config.fmt !== "raw")){
                    cellValue = safeText(cellValue);
                }

                cell.html( cellValue );
                break;

        }


    },


    /**
     * when a table is displayed in a workspace, create a 'add column' widget that the
     * user can click on to add a column
     */
    initializeForWorkspace : function() {
        this.el.click($.bind(this.onClick, this))
            .on("contextmenu", $.bind(this.onRightClick, this));

        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(this.cols.length > 4 ? this.cols.length * 100 : 400);
        }

//		this.invalidateCols()
//		updateManager.queue(this)

        var _this = this;
        PubSub.subscribe("cx-removed", function(evtid, args) {
            if (_this.drilldownEnabled && args.factory_id == "table_col") _this.cleanDrilldownConfig({cxRemoved : args});
        });

        PubSub.subscribe("ddLevel-removed", function() {
            var klip = _this.getKlip();
            var key = _this.getIndexPath() + ":drilldown";

            $.post("/dashboard/ajax_clearKlipProp", { klipId:klip.id, key:key });
        });
    },

    onClick : function(evt) {
        evt.stopPropagation();

        var target = this.getSelectionTarget(evt);
        if (!target) return;

        page.invokeAction("select_component", target, evt);
    },

    onRightClick : function(evt) {
        evt.preventDefault();
        evt.stopPropagation();

        var target = this.getSelectionTarget(evt);
        if (!target) return;

        if (target.parent.factory_id == "table_col") {
            target = target.parent;
        }

        page.invokeAction("select_component", target, evt);
        page.invokeAction("show_context_menu", { target:target, menuCtx:{ isRoot:(target == this) } }, evt);
    },

    getSelectionTarget : function(evt) {
        var target = false;
        var $target = $(evt.target);

        if ($target.is(".comp") || $target.is(".comp-drag-handle")) {
            target = this;
        } else {
            var thisTd = $target.closest("td,th").get(0);

            if (!$(thisTd).parents(".klip").length) return target;

            var tr = $target.closest("tr");
            var _this = this;
            _.each(tr.find("td,th"), function(td, idx) {
                if (thisTd == td) {
                    var col = _this.getColumn(idx);
                    if(col.resultProxy.visibleInWorkspace && tr.closest("table").hasClass("cx-table-results")) {
                        target = col.resultProxy;
                    } else if (col.headerProxy.visibleInWorkspace && tr.closest("table").hasClass("cx-table-headers")) {
                        target = col.headerProxy;
                    } else {
                        target = col;
                    }
                }
            });
        }

        return target;
    },

    getDstPreOperations: function($super) {
        var preOperations = [];

        if (this.isVCFeatureOn() && this.firstRowIsHeader) {
            preOperations.push({
                type: "frah"
            });
        }

        preOperations = preOperations.concat($super());
        return preOperations;
    },
    /**
     * called by the workspace when the component has been selected in the workspace
     * @param $super
     * @param selected
     */
    setSelectedInWorkspace : function($super, selected) {
        if (!selected) {
            if (this.drilldownEnabled) {
                this.popDrilldownLevel();
            }
        }

        $super(selected);
    },

    getWorkspaceControlModel : function($super) {
        var model = $super();

        model.push({
            name:"Column",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this, type:"table_col", role:"tcol"}},
                    {type:"remove", disabled:true}
                ]
            ]
        });

        this.addDataSlotControl(model);

        return model;
    },

    addChildByType : function($super, config) {
        var newChild = $super(config);

        if (config.type == "table_col") {
            if (config.sibling) {
                newChild = this.addColumn( { name:"New Column" }, config.index );
            } else {
                newChild = this.addColumn( { name:"New Column" } );
            }
        }

        return newChild;
    },

    removeComponent : function($super, cx, nextActive, detach) {
        $super(cx, nextActive, detach);

        if (cx.factory_id == "table_col") {
            this.invalidateCols();
            this.reindexColumns();
        }
    },


    /**
     *
     * @param $super
     */
    getPropertyModel : function($super) {
        var model = $super();

        //Don't show FRAH if no datasource or only models
        var showFRAH = false;
        if (this.getKlip() && this.getKlip().hasDatasources()) {
            showFRAH = true;
        }

        model.push({
            type:"text",
            id:"numColumns",
            label:"Table Columns",
            value: this.numColumns,
            group:"table-columns",
            isNumeric:true,
            readOnly:true,
            min:1,
            width:38
        });

        model.push({
            type:"select" ,
            id:"tableRowsType",
            label:"Table Rows" ,
            group:"table",
            options: [
                { value:"upto", label:"Show at most:" },
                { value:"exactly", label:"Show exactly:" },
                { value:"all", label:"Show all rows" }
            ],
            width:"150px",
            selectedValue: this.tableRowsType
        });

        model.push({
            type:"text",
            id:"tableRows",
            group:"table",
            displayWhen: { tableRowsType:["upto", "exactly"] },
            minorLabels: [
                { label:"rows", position:"right",  css:"quiet italics"}
            ],
            value: this.tableRows,
            isNumeric:true,
            min:1,
            width:38,
            flow:true
        });


        model.push({
            type:"checkboxes",
            id:"showHeader",
            label:"Column Headers",
            group:"table-column-headers",
            options:[{value:true,label:"Show all column headers",checked:this.showHeader}]
        });

        if (showFRAH) {
          model.push({
            type:"checkboxes",
            id:"firstRowIsHeader",
            group:"table-column-headers",
            options:[
              {value:true,label:"Use first values as column headers", checked:this.firstRowIsHeader}
            ],
            help: {
              link: "klips/use-first-value"
            },
            fieldMargin:"0"
          });
        }



//        model.push({
//            type:"checkboxes",
//            id:"showResultsRow",
//            label:"Results Row",
//            group:"table-results-row",
//            options: [
//                { value:true, label:"Show a results (totals) row", checked:this.showResultsRow }
//            ]
//        });


        model.push({
            type:"checkboxes",
            id:"showBorder",
            label:"Table Border",
            group:"table-border",
            options:[
                {
                    value:true,
                    label:"Show a border around this table",
                    checked:this.showBorder
                }
            ]
        });

        this.addDataSlotProperty(model);

        return model;
    },


    applyComponentProps : function() {
        try {
            var tableSortProp = this.getComponentProp(this.getIndexPath() + ":tablesort");

            if (typeof tableSortProp == "string") {
                this.sortData = JSON.parse(this.getComponentProp(this.getIndexPath() + ":tablesort"));
            } else {
                this.sortData = tableSortProp;
            }

            var col = this.getColumn(this.sortData[0]);
            this.sortByColumn(col, this.sortData[1], true);
        } catch (e) {
            this.sortData = null;
        }

        if (!isWorkspace() && this.drilldownEnabled) {
            try {
                var ddStack = JSON.parse(this.getComponentProp(this.getIndexPath() + ":drilldown"));

                if (ddStack && ddStack.length > 0) {
                    var invalidStack = false;
                    for (var i = 0; i < ddStack.length; i++) {
                        if (i < this.drilldownConfig.length) {
                            this.pushDrilldownLevel(ddStack[i]);
                        } else {
                            invalidStack = true;
                            break;
                        }
                    }

                    if (invalidStack) {
                        this.setComponentProp(this.getIndexPath() + ":drilldown", JSON.stringify(this.drilldownStack));
                    }
                } else {
                    this.pushDrilldownLevel({isTop:true});
                }
            } catch (e) {
                this.pushDrilldownLevel({isTop:true});
            }

            try {
                this.drilldownSort = JSON.parse(this.getComponentProp(this.getIndexPath() + ":drilldown-sort"));
                this.applyDrilldownSort(this.drilldownStack.length - 1);
            } catch (e) {
                this.applyDrilldownSort();
            }
        }
    },


    /**
     * updates the cells in a column.
     * @param c
     */
    updateColumn : function(c) {
        var data = c.filteredData;
        var alignFmt;

        if (!c.cellsReady) return; // use the new flag instead of the old cache
        var cells  = this.getColumnCells(c.index);

        var header = c.getHeader();
        if (!this.isVCFeatureOn() && this.firstRowIsHeader) {
            data = data.slice(1);
        }

        var originalFormat = c.fmt;
        var originalPrecision = c.fmtArgs.precision;
        var borderLeft = c.borderLeft == "auto" ? ((this.showBorder || c != this.firstVisibleCol) ? 1 : undefined) : c.borderLeft;
        var borderRight = c.borderRight == "auto" ? ((this.showBorder && c == this.lastVisibleCol) ? 1 : undefined) : c.borderRight;

        alignFmt = c.getResolvedType() === DX.ModelType.NUMBER ? "num" : originalFormat;

        if ( this.showHeader ) {
            var $cell = $(cells.head.firstChild);
            var hdata = c.headerProxy.getData(0);
            var headersRCtx = { color:true, icons:[], bgColor:false, styles:[], text:true };
            var $th = $(cells.head);
            var headerData = (c.customHeader === true && hdata[0]) ? hdata[0] : header;
            var cellType = (c.customHeader === true) ? Component.Table.CELL_CUSTOM_HEADER : Component.Table.CELL_HEADER;

            c.headerProxy.collectReactions(headersRCtx);
            c.headerProxy.name = headerData;
            c.headerProxy.displayName = c.getHeaderName();

            this.styleCell($cell, c, headerData, cellType, {
                rCtx: headersRCtx,
                fmt: alignFmt,
                borderTop: this.showBorder ? 1 : undefined,
                borderLeft: borderLeft,
                borderRight: borderRight
            });

            if (isWorkspace()) {
                this.drawDstHints(c);
            }

            switch (c.sortDir) {
                case Component.Table.SORT_ASC:
                    $th.addClass("headerSortDown");
                    break;
                case Component.Table.SORT_DESC:
                    $th.addClass("headerSortUp");
                    break;
                default:
                    $th.removeClass("headerSortDown");
                    $th.removeClass("headerSortUp");
                    break;
            }
        }


        if(this.showResultsRow){
            this.createResultsRowCell(cells, c,alignFmt,borderLeft,borderRight);
        }

        // temporarily change column format to numeric if drilldown aggregation is being
        // displayed.  revert it back to originalFormat after cell rendering is complete
        if (c.aggMethod && (c.aggMethod == "count" || c.aggMethod == "distinct")) {
            c.fmt = "num";
            c.fmtArgs.precision = 0; //Count is always a whole number
        }

        var reactionContext = { colors:{}, icons:{}, bgColors:{}, styles:{}, text:{} };
        c.collectReactions(reactionContext);

        var scrollRequired = (this.maxRows && (this.rowCount > this.maxRows));

        for (var i = 0; i < this.rowCount; i++) {
            var rowIdx = (this.sortedData ? (i < this.sortedData.length ? this.sortedData[i][0] : i) : i);
            var cellNode = cells.body[i].firstChild;

            var row = ((!this.isVCFeatureOn() && this.firstRowIsHeader) ? rowIdx + 1 : rowIdx);
            var labelRCtx = {
                color: reactionContext.colors[row] ? reactionContext.colors[row] : false,
                icons: reactionContext.icons[row] ? reactionContext.icons[row] : [],
                bgColor: reactionContext.bgColors[row] ? reactionContext.bgColors[row] : false,
                styles: reactionContext.styles[row] ? reactionContext.styles[row] : [],
                text: reactionContext.text[row] ? reactionContext.text[row] : false
            };

            this.styleCell($(cellNode), c, data[rowIdx], Component.Table.CELL_DEFAULT, {
                rCtx: labelRCtx,
                fmt: alignFmt,
                borderTop: i == 0 && (!scrollRequired && (this.showBorder || this.showHeader)) ? 1 : undefined,
                borderLeft: borderLeft,
                borderRight: borderRight,
                borderBottom: i == this.rowCount - 1 && (!scrollRequired && (this.showBorder || this.showResultsRow)) ? 1 : undefined
            });
        }

        //Must be returned back to original format for lower level columns holding original/unfiltered data.
        if (c.aggMethod && (c.aggMethod == "count" || c.aggMethod == "distinct")) {
            c.fmt = originalFormat;
            c.fmtArgs.precision = originalPrecision;
        }

        c.isInvalid = false;
    },


    createResultsRowCell: function(cells, c,alignFmt,borderLeft,borderRight){
        var i;
        var $rcell = $(cells.foot.firstChild);
        var rdata;
        var validData;
        var originalFormat = c.fmt;
        var originalPrecision = c.fmtArgs.precision;
        var possibleResultFmts = Props.Defaults.resultFormats[c.getResolvedType()];

        // temporarily change column format to numeric if result row format is count or countdistinct.
        // revert it back to originalFormat after cell rendering is complete
        if (c.resultFmt == "count" || c.resultFmt == "countdistinct") {
            c.fmt = "num";
            c.fmtArgs.precision = 0; //Count is always a whole number
        }

        //if previous result row option is valid for new column data type, data is valid
        validData = false;
        for(i = 0; i<possibleResultFmts.length; i++){
            if(possibleResultFmts[i].value === c.resultFmt){
                validData = true;
                break;
            }
        }

        if (c.hasErrorResult || !validData) { // if error or invalid data given data type, we want to return a blank square
            rdata = [""];
        } else {
            rdata = c.resultProxy.getData(0);
        }
        var resultsRCtx = {color: false, icons: [], bgColor: false, styles: [], text: false};
        c.resultProxy.collectReactions(resultsRCtx);

        this.styleCell($rcell, c, rdata[0], Component.Table.CELL_RESULT, {
            rCtx: resultsRCtx,
            fmt: alignFmt,
            borderLeft: borderLeft,
            borderRight: borderRight,
            borderBottom: this.showBorder ? 1 : undefined
        });

        if (c.resultFmt == "count" || c.resultFmt == "countdistinct") {
            c.fmt = originalFormat;
            c.fmtArgs.precision = originalPrecision;
        }
    },

    getColumnCells : function(idx){
        var cells = { head:false, body:[], foot:false } ;

        //get body cells
        //--------------
        var tbodyRows = this.tableEl[0].children[1].children;
        for( var i = 0 ; i < tbodyRows.length; i++ ){
            cells.body.push(tbodyRows[i].children[idx]);
        }

        cells.head = this.theadEl[0].firstChild.firstChild.children[idx];
        cells.foot = this.resultsEl[0].firstChild.firstChild.children[idx];

        return cells;
    },


    updateSortIndexes : function() {

        for(var i = 0 ; i < this.cols.length ; i++ ){
            if( this.cols[i].sortDir != Component.Table.SORT_DEFAULT ){
                this.sortedData = this.cols[i].sortWithIndexes(this.cols[i].sortDir,this.rowCount);
            }
        }
    },


    afterFactory : function($super) {
        $super();

        this.cols = this.getChildrenByType("table_col");
        this.numColumns = this.cols.length;

        this.invalidateCols();
    },


    getInternalPropNames : function() {
        var klip = this.getKlip();
        var indexPath = this.getIndexPath();
        var prefix = klip.id + ":" + indexPath;

        var names = [prefix + ":tablesort"];

        if (this.drilldownEnabled) {
            names.push(prefix + ":drilldown", prefix + ":drilldown-sort");
        }

        return names;
    },


    getDrilldownModel : function($super) {
        var model = $super();

        for (var i = 0; i < this.cols.length; i++) {
            var col = this.cols[i];

            model.push({
                name: col.displayName,
                type: col.getDrilldownType(),
                id: col.id
            });
        }

        return model;
    },

    cleanDrilldownConfig : function($super, ctx) {
        if (!ctx || ctx.cxRemoved) {
            $super(ctx && ctx.cxRemoved ? ctx.cxRemoved : undefined);
        }

        if (ctx && ctx.changedType) {
            var colType = ctx.col.getDrilldownType();

            var i = 0;
            while (i < this.drilldownConfig.length) {
                var cfg = this.drilldownConfig[i];

                if (colType == "measure" && ctx.col.id == cfg.groupby) {
                    this.drilldownConfig.splice(i, 1);
                    continue;
                } else if (colType == "field") {
                    var displayCfgs = cfg.display;
                    var j = 0;
                    while (j < displayCfgs.length) {
                        var dCfg = displayCfgs[j];

                        if (ctx.col.id == dCfg.id) {
                            var remove = false;

                            if (cfg.groupby == "") {
                                if (dCfg.display != "show") {
                                    remove = true;
                                }
                            } else {
                                if (dCfg.display != "count" && dCfg.display != "distinct") {
                                    remove = true;
                                }
                            }

                            if (remove) {
                                displayCfgs.splice(j, 1);
                                continue;
                            }
                        }

                        j++;
                    }
                }

                i++;
            }
        }
    },

    invalidateDrilldown : function($super) {
        $super();

        this.invalidateCols();
        updateManager.queue(this);
    },

    updateFilteredData : function() {
        var drilldownReady = this.drilldownEnabled && this.drilldownStack.length > 0 && this.drilldownConfig.length > 0;
        var _this = this;
        var j, k;
        var displayCfg;
        var col;

        // reset the filtered data each time
        _.each(this.cols, function(c) {
            c.aggMethod = false;
            c.groupHidden = drilldownReady;
            c.grouped = false;
            c.filteredData = c.getData(0);

            if (c.fmt == "hrz") c.setAutoRangeValues();

            // remove the header so it doesn't affect the grouping
            if (drilldownReady && !_this.isVCFeatureOn() && _this.firstRowIsHeader) {
                c.filteredData = c.filteredData.slice(1);
            }
        });

        if (drilldownReady) {
            // should always be the same length and the value in the path must be
            // in the corresponding column of data
            var groupData = [];
            var groupPath = [];

            for (var i = 0; i < this.drilldownStack.length; i++) {
                var level = this.drilldownStack[i];
                var levelIdx = level.levelIdx ? level.levelIdx : i;
                var levelCfg = this.drilldownConfig[levelIdx];

                if (!levelCfg) continue;

                var groupCol = this.getChildById(levelCfg.groupby);
                // the top drilldown level doesn't have a value associated with it
                if (level.cdId != undefined) groupPath.push(level.cdId);

                // add to the groupData until the last level of stack is reached
                if (i < this.drilldownStack.length - 1) {
                    groupData.push(groupCol.filteredData);
                } else {
                    if (levelCfg.groupby == "") {
                        // select the end-of-path data for the visible columns
                        for (j = 0; j < levelCfg.display.length; j++) {
                            displayCfg = levelCfg.display[j];
                            col = this.getChildById(displayCfg.id);

                            col.groupHidden = false;
                            col.filteredData = dataFilters.select(col.filteredData, groupData, groupPath);

                            if (col.fmt == "hrz") col.setAutoRangeValues();
                        }
                    } else {
                        // clone the current filteredData since the filteredData will be changed
                        var groupColData = _.clone(groupCol.filteredData);

                        // haven't reached the last level of drilldown
                        if (levelIdx < this.drilldownConfig.length - 1) groupCol.grouped = true;
                        groupCol.groupHidden = false;

                        // find the groups at this level
                        var groups = dataFilters.group(groupCol.filteredData, groupData, groupPath);

                        // more the "empty"/other group to the end of the list
                        var otherIdx = _.indexOf(groups, "");
                        if (otherIdx != -1) {
                            groups.splice(otherIdx, 1);
                            groups.push("");
                            otherIdx = groups.length - 1;
                        }

                        groupCol.filteredData = groups;

                        // aggregate the remaining visible columns according to the group path and the groups
                        for (j = 0; j < levelCfg.display.length; j++) {
                            displayCfg = levelCfg.display[j];
                            col = this.getChildById(displayCfg.id);

                            col.aggMethod = displayCfg.display;
                            col.groupHidden = false;

                            if (col.filteredData.length) {

                                if (_.indexOf(["spk", "spb", "wlc", "dsc", "bch"/*, "hrz"*/], col.fmt) != -1) {
                                    var splitData = [];
                                    for (k = 0; k < col.filteredData.length; k++) {
                                        splitData.push(col.filteredData[k].split(","));
                                    }

                                    var splitFilteredData = dataFilters[displayCfg.display](splitData, groupColData, groupData, groupPath, groups, true);

                                    col.filteredData = [];
                                    for (k = 0; k < splitFilteredData.length; k++) {
                                        var sfd = splitFilteredData[k];

                                        col.filteredData.push(_.isArray(sfd) ? sfd.join() : sfd);
                                    }
                                } else {
                                    col.filteredData = dataFilters[displayCfg.display](col.filteredData, groupColData, groupData, groupPath, groups);
                                }

                                if (col.fmt == "hrz") col.setAutoRangeValues();

                            }else{ //there was nothing to count, leave column blank <-- error or empty formula
                                col.filteredData = [];
                                for(k=0;k < groups.length; k++){
                                    col.filteredData.push("");
                                }
                            }
                        }

                        // change "" to "Other"
                        if (otherIdx != -1) groupCol.filteredData[otherIdx] = "Other";
                    }

                    // remove data from hidden columns
                    _.each(this.cols, function(c) {
                        if (c.groupHidden) {
                            c.filteredData = [];
                        }
                    });
                }
            }
        }

        _.each(this.cols, function(c) {
            if (c.hasData(0)) {
                // re-add the header
                if (drilldownReady && !_this.isVCFeatureOn() && _this.firstRowIsHeader) {
                    c.filteredData.unshift(c.getData(0)[0]);
                }

                c.applyConditions();
                c.updateProxyComponents();

                _this.publishKlipEvent({id: "cx-data-changed", cx: c}, false);
            }
        });
    }

});


Component.Table.CELL_DEFAULT = 1;
Component.Table.CELL_HEADER = 2;
Component.Table.CELL_RESULT = 3;
Component.Table.CELL_CUSTOM_HEADER = 4;

Component.Table.SORT_DEFAULT = 0;
Component.Table.SORT_ASC = 1;
Component.Table.SORT_DESC = 2;
Component.Table.MAX_ROWS = 200;
;/****** cx.table_column.js *******/ 

/* global Component:false, CXTheme:false, DX:false, FMT:false, Props:false, KlipFactory:false, page:false,
    isWorkspace:false, sortComparators:false, bindValues:false, KF:false, safeText:false */
/**
 * TableColumns are proxy components that do not do any direct rendering.  Instead
 * they provide UI access to a column model in a consistent way -- the workspace
 * is component-centric.
 */
Component.TableColumn = $.klass(Component.Proxy, {

    factory_id:"table_col",
    role:"tcol",
    isDeletable:true,

    canGroup: true,
    canSetAggregation: true,
    canSort: true,
    canFilter: true,

    name:false,
    index:-1,
    borderLeft:"auto",
    borderRight:"auto",
    fw:false,
    w:"",
    align:"0",
    font_style: "",
    resultFmt: "empty",
    colHidden: false,
    customHeader: false,

    sortDir: 0,

    initialize: function($super, config) {
        $super(config);
        this.isInvalid = true;
        this.labelMixin = new Component.LabelComponent();
        this.size = "xx-small";
        this.fmt = "txt";
        this.visible = true;
    },


    /**
     *
     * @param $super
     */
    getEditorMenuModel: function($super) {
        var m;
        var name = this.displayName;
        if (!name) {
            name = "Unnamed";
        }

        if(KF.company.hasFeature("saas_11282")){
            name = safeText(name);
        }

        m = $super();
        m.text = "<span parentIsAction='true' class='quiet'>Column:</span>&nbsp;" + name.replace(/ /g, "&nbsp;");
        m.tooltip =  this.getReferenceName();
        return m;
    },

    /**
     *
     * @param $super
     */
    getReferenceValueModel: function($super) {
        var m = $super();

        m.text = this.getReferenceName().replace(/ /g, "&nbsp;");

        return m;
    },

    getReferenceName: function() {
		return "Column: " + (this.displayName || "Unnamed");
	},

    checkDeletable: function($super) {
        var isDeletable = $super();

        return isDeletable && (this.parent.cols.length > 1);
    },

    getWorkspaceControlModel: function($super) {
        var model = $super();

        model.push({
            name:"Column",
            controls: [
                [
                    {type:"add", actionId:"add_component", actionArgs:{parent:this.parent, type:"table_col", role:"tcol", sibling:this}},
                    {type:"remove", disabled:!this.checkDeletable(), actionId:"remove_component", actionArgs:{cx:this}}
                ],
                [
                    {type:"move_left", disabled:(this.index == 0), actionId:"table_move_column_left", actionArgs:this.parent},
                    {type:"move_right", disabled:(this.index == this.parent.cols.length - 1), actionId:"table_move_column_right", actionArgs:this.parent}
                ]
            ]
        });
        return model;
    },

    getContextMenuModel : function(menuCtx, menuConfig) {
        var model = [];
        this.addRenameContextMenuItem(model);
        model.push(
            { id:"separator-column_insert", separator:true },
            { id:"menuitem-insert_left", text:"Insert Column Left", actionId:"add_component", actionArgs:{parent:this.parent, type:"table_col", role:"tcol", sibling:this, before:true} },
            { id:"menuitem-insert_right", text:"Insert Column Right", actionId:"add_component", actionArgs:{parent:this.parent, type:"table_col", role:"tcol", sibling:this} },
            { id:"menuitem-delete_column", text:"Delete Column", disabled:!this.checkDeletable(), actionId:"remove_component", actionArgs:{cx:this} },
            { id:"separator-column_move", separator:true },
            { id:"menuitem-move_left", text:"Move Column Left", disabled:(this.index == 0), actionId:"table_move_column_left", actionArgs:this.parent },
            { id:"menuitem-move_right", text:"Move Column Right", disabled:(this.index == this.parent.cols.length - 1), actionId:"table_move_column_right", actionArgs:this.parent }
        );

        this.addDstContextMenuItems(menuCtx, menuConfig, model);

        return model;
    },

    getSupportedDst: function($super) {
        var dst = $super();

        if (this.hasMiniChartFmt()) {
            dst.group = false;
            dst.aggregation = false;
            dst.sort = false;
            dst.filter = false;
        } else {
            dst=this.updateSupportedDst(dst);
        }

        return dst;
    },

    getDstPreOperations: function($super) {
        var preOperations = [];

        if (this.isVCFeatureOn() && this.isFirstRowHeader()) {
            preOperations.push({
                type: "frah"
            });
        }

        preOperations = preOperations.concat($super());
        return preOperations;
    },

    /**
     * Update workspace controls on the parent
     * @param $super
     * @param selected
     * @param newComponent
     */
    setSelectedInWorkspace: function($super, selected, newComponent) {
        if (selected) {
            this.parent.setSelectedColumn(this);
        } else {
            this.parent.deselectColumn(this.index);
        }

        $super(selected, newComponent);
    },


    getDrilldownType : function() {
        var measureFmts = ["num", "cur", "pct", "dur", "spk", "spb", "wlc", "dsc", "bch", "hrz"];

        return (_.indexOf(measureFmts, this.fmt) != -1 ? "measure" : "field");
    },

    setVisible: function(v) {
        this.visible = v;
    },

    setCalculatedWidth: function(w) {
        if ( w == this.cw ) return;
        this.cw = w;
        switch(this.fmt){
            case "spk": case "bch": case "spb": case "wlc": case "dsc": case "hrz":
            this.invalidate();
            break;
        }
    },

    isFirstRowHeader: function() {
      return this.parent.firstRowIsHeader;
    },

    updateDisplayName: function() {
        var header;

        if (this.isRenamed) return;

        header = this.getHeader();

        if (this.displayName != header) {
            this.displayName = this.getHeader();

            if (isWorkspace()) {
                page.updateMenu();
            }
        }
    },

    setDisplayName: function($super, displayName) {
        var renamed = $super(displayName);

        if (renamed) {
            if (!this.resultProxy.isRenamed) {
                this.resultProxy.displayName = this.displayName;
            }
            if (!this.headerProxy.isRenamed) {
                this.headerProxy.displayName = this.displayName;
            }
        }

        return renamed;
    },

    setAutoRangeValues: function() {
        var min, max, data, i, val;
        if (!this.filteredData) return;

        min = Infinity;
        max = -Infinity;
        data = this.filteredData.slice(0);

        for (i = 0; i < data.length; i++) {
            val = FMT.convertToNumber(data[i]);

            if (val < min) min = val;
            if (val > max) max = val;
        }

        this.fmtArgs.minVal = min;
        this.fmtArgs.maxVal = max;
    },

    sortWithIndexes: function(dir, tableRowCount) {
        var data;
        var dataToSort;
        var indexedData = [];
        var i, d, sortFn, parsedDate;
        if (typeof this.filteredData == "undefined") return [];

        data = this.filteredData;
        dataToSort = data;
        if (!this.isVCFeatureOn() && this.isFirstRowHeader()) dataToSort = data.slice(1);



        // create an array that contains the underlying data to be sorted and its
        // original index.  This data will be then sent for sorting
        // ----------------------------------------------------------------------
        for (i = 0; i < dataToSort.length; i++) {
            d = dataToSort[i];
            switch (this.fmt) {
                case "dat":
                case "dat2":
                    parsedDate = FMT.parseDate(d);
                    d = parsedDate != FMT.INVALID_DATE ? parsedDate.getTime() : null;
                    break;
                case "dur":
                    d = Number(d);
                    break;
                default:
            }
            indexedData.push([i, d]);
        }

        if ( dataToSort.length < tableRowCount ) {
            for (i = dataToSort.length; i < tableRowCount; i++) {
                indexedData.push([i,null]);
            }
        }

        switch (this.fmt) {
            case "dat":
            case "dat2":
            case "num":
            case "cur":
            case "pct":
            case "dur":
            case "hrz":
                sortFn = sortComparators.numericSort;
                break;

            default:
                if(this.getResolvedType() === DX.ModelType.NUMBER) {
                    sortFn = sortComparators.numericSort;
                } else {
                    sortFn = sortComparators.stringSort;
                }
        }

        indexedData.sort( sortFn );
        if ( dir == Component.Table.SORT_DESC ) indexedData.reverse();

        return indexedData;
    },

    configure: function($super, config) {
        var oldDDType = this.getDrilldownType();
        var isVisible;
        var alreadyVisible;
        var newDDType;

        $super(config);
        bindValues(this, config, $.merge(["name","index","borderLeft","borderRight","fw","w","colHidden","resultFmt", "customHeader","customCSSClass"], this.labelMixin.boundValues));
        this.invalidate();

        this.setVisible(!this.colHidden);

        _.bind(this.labelMixin.configure, this)(config);

        // ----------------------------------------
        // Handle deprecated 'border' property
        if (config.border && config.borderLeft === undefined && config.borderRight === undefined) {
            switch (config.border) {
                case "left-heavy":
                    this.borderLeft = "2";
                    break;
                case "left-none":
                    this.borderLeft = "0";
                    break;
                case "right-heavy":
                    this.borderRight = "2";
                    break;
                case "right-none":
                    this.borderRight = "0";
                    break;
            }
        }
        // ----------------------------------------

        if (config.name && !this.isRenamed) {
            this.updateDisplayName();
        }

        if ( isWorkspace() && config.resultFmt ) {
            if (this.parent && !this.parent.showResultsRow) {
                this.parent.invalidateCols();
            }

            if ( this._previousFmt == undefined ) this._previousFmt = config.resultFmt;

            if (this.resultProxy) {
                this.resultProxy.updateProxy();
                isVisible = (this.resultFmt == "custom");
                alreadyVisible = this.resultProxy.visibleInWorkspace;

                this.resultProxy.visibleInWorkspace = isVisible;

                if (isVisible != alreadyVisible) {
                    page.updateMenu();
                }
            }
        }

        if ( isWorkspace() && config.customHeader === true ) {

            if (this.headerProxy) {
                this.headerProxy.updateProxy();
                isVisible = (this.customHeader === true);
                alreadyVisible = this.headerProxy.visibleInWorkspace;

                this.headerProxy.visibleInWorkspace = isVisible;

                if (isVisible != alreadyVisible) {
                    page.updateMenu();
                }
            }
        }

        if (config["~propertyChanged"] && config.fmt) {
            newDDType = this.getDrilldownType();

            if (newDDType != oldDDType) {
                if (this.parent) this.parent.cleanDrilldownConfig({changedType: true, col:this});
            }
        }
    },

    /**
     *
     * @param $super
     */
    serialize: function($super) {
        var config = $super();
        var subComponents = [];

        _.bind(this.labelMixin.cleanFormatArgs, this)();

        bindValues(config, this, $.merge(["name","index","borderLeft","borderRight","fw","w","colHidden","resultFmt", "customHeader","customCSSClass"], this.labelMixin.boundValues),
            {
                borderLeft:"auto",
                borderRight:"auto",
                fw:false,
                w:"",
                align:"0",
                font_style: "",
                resultFmt:"empty",
                colHidden:false
            }, true);


        // remove components that are not used to optimize the klip schema
        if(config.components && config.components.length > 0) {
            config.components.forEach(function(comp) {
                if(this.customHeader && comp.role === "customHeader") {
                    subComponents.push(comp);
                } else if(this.resultFmt !== "empty" && comp.role === "result") {
                    subComponents.push(comp);
                }
            }.bind(this));
        }

        config.components = subComponents;

        return config;
    },


    /**
     *
     * @param $super
     */
    getPropertyModelGroups: function($super) {
        return $.merge(["header_label", "header_general","c_width"], $.merge($super(), $.merge(this.labelMixin.getPropertyModelGroups(),["row-results","visibility"])));
    },

    /**
     *
     * @param $super
     */
    getPropertyModel: function($super) {
        var model = $super();
        var _this = this;
        var modelType = this.getResolvedType();
        var resultFormats = Props.Defaults.resultFormats[modelType].slice(0);
        var labelMsg, rsltMsg;

        model.push({
            type:"checkboxes",
            id:"customHeader",
            label:"Column Header",
            group:"header_label",
            help: {
                link: "klips/custom-header"
            },
            options:[
                {
                    value:true,
                    label:"Use custom header",
                    checked:this.customHeader
                }
            ],
            onChange: function() {
                var widget = this.findWidget("name");
                _this.headerProxy.visibleInWorkspace = _this.customHeader;
                if (widget) {
                    if(!_this.customHeader) {
                        widget.enable();
                    } else {
                        widget.disable();
                    }
                    page.updateMenu();
                }
            }
        });

        model.push({
            type:"text",
            id:"name",
            group:"header_label",
            displayWhen: {customHeader: false},
            value:this.name,
            onChange: function() {
                _this.isNameRenamed = true;
                if(_this.displayName) {
                    _this.setDisplayName(_this.displayName);
                    page.updateMenu();
                }
            }
        });

        labelMsg = $("<span>");
        $("<a>")
            .text("Header")
            .attr("href","#")
            .click(function(){
                page.invokeAction("select_component", _this.headerProxy);
            })
            .appendTo(labelMsg);
        labelMsg.append(" component added to the component tree.");

        model.push({
            id:"msg_custHeaders",
            type:"markup",
            el:labelMsg,
            group:"header_label",
            displayWhen:{customHeader:true}
        });

        model.push({
            type: "button_group_ex",
            id: "borderLeft",
            group: "header_general",
            label: "Column Border",
            minorLabels: [
                { label:"Left:", position:"left", width:"40px" }
            ],
            options: Props.Defaults.borderLeft,
            selectedValue: this.borderLeft
        });

        model.push({
            type: "button_group_ex",
            id: "borderRight",
            group: "header_general",
            minorLabels: [
                { label:"Right:", position:"left", width:"40px" }
            ],
            options: Props.Defaults.borderRight,
            selectedValue: this.borderRight
        });

        model.push({
            type:"checkboxes",
            id:"fw",
            group:"c_width",
            label:"Column Width",

            options:[
                {
                    value:"fixed",
                    label:"Fix column to a specific width...",
                    checked:this.fw
                }
            ]
        });

        model.push({
            type:"text",
            displayWhen: {fw:"fixed"},
            id:"w",
            group:"c_width",
            minorLabels: [
                {label: "px or % of table width", position: "right", css:"quiet italics"}
            ],
            label:"Width",
            value:this.w,
            width:38,
            help: {
                link: "klips/column-width"
            }
        });

        if (this.hasMiniChartFmt()) {
            resultFormats = [];
            resultFormats.push(
                {value: "empty", label: "Empty", "default": true},
                {value: "count", label: "Count"},
                {value: "countdistinct", label: "Count Distinct"},
                {value: "custom", label: "Custom..."});
        }

        model.push({
            type:"select",
            id:"resultFmt",
            group:"row-results",
            label:"Results Row",
            selectedValue:this.resultFmt,
            displayWhen: {showResult:true},
            options: resultFormats,
            onChange: function(changeEvent) {
                page.invokeAction("add_results_row", { cx:this.resultProxy, resultFmt: changeEvent.value, workspace: page });
            }.bind(this),
            help: {
                link: "klips/results-row"
            }

        });


        //make jquery el to appear when using a custom result row
        rsltMsg = $("<span>");
        $("<a>")
            .text("Results")
            .attr("href","#")
            .click(function(){
                page.invokeAction("select_component", _this.resultProxy);
            })
            .appendTo(rsltMsg);
        rsltMsg.append(" component added to the component tree.");

        model.push({
            id:"msg_custResults",
            type:"markup",
            el:rsltMsg,
            group:"row-results",
            displayWhen:{resultFmt:"custom"}
        });


        model.push({
            type:"checkboxes",
            id:"colHidden",
            group:"visibility",
            label:"Visibility",
            help: {
                link: "klips/visibility"
            },

            options:[
                {
                    value:"hidden",
                    label:"Hide this column",
                    checked:this.colHidden
                }
            ]
        });

        model = $.merge(model, _.bind(this.labelMixin.getPropertyModel, this)());
        return model;
    },


    setData: function ($super, data, msg) {
        $super(data, msg);

        if (isWorkspace()) {
            this.updateDisplayName();
        }
    },

    getFilteredData: function(seriesIdx) {
        return (seriesIdx == undefined ? [this.filteredData] : this.filteredData);
    },


    getSupportedReactions: function() {
        var miniFmts = ["spk", "spb", "wlc", "dsc", "bch", "hrz"];

        if (_.indexOf(miniFmts, this.fmt) != -1) {
            return { color: true, icon: true, "background-color":true, text:true };
        } else {
            return { color: true, icon: true, "background-color":true, style:true, text:true };
        }
    },

    updateProxyComponents: function() {
        if (this.resultProxy) {
            this.resultProxy.updateProxy();
        }

        if (this.headerProxy) {
            this.headerProxy.updateProxy();
        }
    },

    /**
     *
     * @param idx
     * @param reaction
     * @param ctx - {
     *      colors: {},
     *      icons: {},
     *      bgColors: {},
     *      styles: {},
     *      text: {}
     * }
     */
    handleReaction: function( idx, reaction, ctx ) {
        switch (reaction.type) {
            case "color":
                ctx.colors[ idx ] = CXTheme.getThemeColour(reaction.value);
                break;
            case "icon":
                if (!ctx.icons[idx]) ctx.icons[idx] = [];
                ctx.icons[ idx ].push(reaction);
                break;
            case "background-color":
                ctx.bgColors[ idx ] = CXTheme.getThemeColour(reaction.value);
                break;
            case "style":
                if (!ctx.styles[idx]) ctx.styles[idx] = [];
                ctx.styles[ idx ].push(reaction.value);
                break;
            case "text":
                ctx.text[ idx ] = reaction.value;
                break;
        }
    },


    /**
     * ensure that a column has a 'result' proxy component, and attach an onData listener to it
     * so that when results data updates we update the column.
     * @param $super
     */
    afterFactory: function($super) {
        var requiredChildren, i, req, cx, oldConfig, newResults;

        $super();
        requiredChildren = [
            {role:"customHeader", props:{displayName:this.displayName, type:"table_headers", name: this.name, size: "xx-small", font_style: "", autoFmt:false}},
            {role:"result", props:{displayName:this.displayName, type:"table_results", name: this.name, autoFmt:false}}
        ];

        for (i = 0; i < requiredChildren.length; i++) {
            req = requiredChildren[i];
            cx = this.getChildByRole(req.role);
            if (!cx) {
                cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
                this.addComponent(cx);
            } else {
                // migrate old results row component from type 'proxy' to type 'table_results'
                if (req.role == "result" && cx.factory_id == "proxy") {
                    oldConfig = cx.serialize();
                    newResults = KlipFactory.instance.createComponent($.extend(oldConfig, {type:"table_results"}));

                    this.removeComponent(cx);
                    this.addComponent(newResults);
                }
            }
        }

        this.resultProxy = this.getChildByRole("result");
        this.resultProxy.visibleInWorkspace = (this.resultFmt == "custom");

        this.headerProxy = this.getChildByRole("customHeader");
        this.headerProxy.visibleInWorkspace = (this.customHeader === true);
    }
});
;/****** cx.table_custom_cell.js *******/ 

/* global Component:false, CXTheme:false, KF: false, updateManager:false */

Component.TableCustomCell = $.klass(Component.Proxy, {

    factory_id: "table_custom_cell",
    role: "custom_cell",
    isDstRoot: true,
    cacheDisabled: true,
    disableAnimations: true,

    type:"",

    size:  "xx-small",
    align: "l",
    font_style: "bold",

    parentTable : false,
    currentValue: false,
    previousValue: false,

    displayAutoFormat : false,

    initialize : function($super, config) {
        $super(config);

        this.mixin = new Component.LabelComponent();
    },

    getReferenceName : function() {
        return this.type + ": " + (this.displayName || "Untitled");
    },

    configure : function($super, config) {
        $super(config);

        _.bind(this.mixin.configure, this)(config);

        if (this.parent) this.parent.invalidate();
        if (this.parentTable) updateManager.queue(this.parentTable);
        this.updateLabelSizes(config);

        this.setBulletChartDefaults(config);

        this.autoFmt = null;
    },

    setData : function($super, data, msg) {
        $super(data, msg);

        if (this.parent) this.parent.invalidate();
    },

    getSupportedReactions : function($super) {
        return { color: true, icon: true, "background-color":true, style:true, text:true };
    },

    /**
     *
     * @param idx
     * @param reaction
     * @param ctx - {
     *      color: false,
     *      icons: [],
     *      bgColor: false,
     *      styles: [],
     *      text: false
     * }
     */
    handleReaction : function( idx, reaction, ctx ) {
        switch (reaction.type) {
            case "color":
                ctx.color = CXTheme.getThemeColour(reaction.value);
                break;
            case "icon":
                ctx.icons.push(reaction);
                break;
            case "background-color":
                ctx.bgColor = CXTheme.getThemeColour(reaction.value);
                break;
            case "style":
                ctx.styles.push(reaction.value);
                break;
            case "text":
                ctx.text = reaction.value;
                break;
        }
    }

});
;/****** cx.table_headers.js *******/ 

/* global Component:false, KF: false, isPreview:false, safeText:false */

Component.TableHeaders = $.klass(Component.TableCustomCell, {

    factory_id: "table_headers",
    role: "theaders",
    isDeletable: false,

    size:  "xx-small",
    align: "l",
    font_style: "bold",

    type: "Header",

    parentTable : false,

    initialize : function($super, config) {
        var _this = this;
        $super(config);

        PubSub.subscribe("cx-added", function(evtName, args) {
            if (isPreview(_this.getKlip().dashboard)) return;

            if (args == _this.parent) {
                // the table the column is assigned to
                _this.parentTable = _this.parent.parent;

                // clear formula for non-custom, non-empty formats so results can be queried again
                if (_this.parent.customHeader === false) {
                    _this.clearFormula(0);
                }
            }
        });
    },

    /**
     *
     * @param $super
     */
    getEditorMenuModel : function($super) {
        var m = $super();
        var name = this.displayName;
        if (!name) name = "Untitled";

        if(KF.company.hasFeature("saas_11282")){
            name = safeText(name);
        }

        m.text = "<span parentIsAction='true' class='quiet'>Header:</span>&nbsp;" + name.replace(/ /g, "&nbsp;");
        return m;
    },

    getFormulas : function($super, collectIn) {
        // do not need to collect the formulas in the results row if the format is not
        // custom because it will be an aggregation of the parent column values and only
        // change when the column changes; the data will be returned with direct queries
        // to DPN and there will not be any variables

        if (this.parent && this.parent.customHeader === true) {
            return $super(collectIn);
        } else {
            return collectIn;
        }
    },

    isCustomRow : function() {
        return this.parent && this.parent.customHeader === true;
    },

    setSelectedInWorkspace : function($super, selectionState, newComponent) {
        var headerELement = this.parentTable.theadEl.find("th:eq(" + this.parent.index + ")");
        var headerStyleElement;
        if(headerELement && headerELement.children()) {
            headerStyleElement = headerELement.children().first();
            if(!headerStyleElement) {
                return;
            }

            if (selectionState) {
                headerStyleElement.addClass("selected-component-header");
                this.parentTable.selectedHeaderProxy = this;
            } else {
                headerStyleElement.removeClass("selected-component-header");
                this.parentTable.selectedHeaderProxy = false;
            }
        }
    },

    updateProxy : function() {
        var proxyFormulaObj = this.getFormula();
        var defaultFormula = (this.name) ? "\""+this.name+"\"" : "\"\"";
        var proxyFormula = ( proxyFormulaObj && proxyFormulaObj.formula ) ?
                    proxyFormulaObj.formula : defaultFormula;

        if(!this.isCustomRow()) {
            return;
        }

        this.currentValue = proxyFormula;
        if(this.currentValue === this.previousValue) {
            return;
        }

        this.setFormula(0, proxyFormula);

        this.previousValue = this.currentValue;
    }
});
;/****** cx.table_results.js *******/ 

/* global Component:false, CxFormula:false, page:false, isWorkspace:false, isPreview:false, KF: false, Props: false,
    safeText:false */

Component.TableResults = $.klass(Component.TableCustomCell, {

    factory_id: "table_results",
    role: "results",
    isDeletable: false,

    size:  "xx-small",
    align: "l",
    font_style: "bold",

    type: "Result",

    parentTable : false,

    initialize : function($super, config) {
        var _this = this;
        $super(config);

        PubSub.subscribe("cx-added", function(evtName, args) {
            if (isPreview(_this.getKlip().dashboard)) return;

            if (args ===_this.parent) {
                // the table the column is assigned to
                _this.parentTable = _this.parent.parent;

                // clear formula for non-custom, non-empty formats so results can be queried again
                if (_this.sendQueryRequestInsteadOfWatch() && _this.parent.resultFmt !== Props.Defaults.Agg.EMPTY) {
                    _this.clearFormula(0);
                }
            }
        });
    },

    /**
     *
     * @param $super
     */
    getEditorMenuModel : function($super) {
        var m = $super();
        var name = this.displayName;
        if (!name) name = "Untitled";

        if(KF.company.hasFeature("saas_11282")){
            name = safeText(name);
        }

        m.text = "<span parentIsAction='true' class='quiet'>Result:</span>&nbsp;" + name.replace(/ /g, "&nbsp;");
        return m;
    },

    getFormulas : function($super, collectIn) {
        // do not need to collect the formulas in the results row if the format is not
        // custom because it will be an aggregation of the parent column values and only
        // change when the column changes; the data will be returned with direct queries
        // to DPN and there will not be any variables

        if (this.parent && !this.sendQueryRequestInsteadOfWatch()) {
            return $super(collectIn);
        } else {
            return collectIn;
        }
    },

    isCustomRow : function() {
        return this.parent && this.parent.resultFmt == Props.Defaults.Agg.CUSTOM;
    },

    sendQueryRequestInsteadOfWatch : function() {
      //We can't use result references to calculate result row values if drill down is enabled,
      //since drill down is done client side.
      if (this.parentTable && this.parentTable.drilldownEnabled && !this.isCustomRow()) {
        return true;
      }

      if (KF.company.isVCFeatureOn() && this.getKlip() && this.getKlip().getAppliedMigrations()["result_rows2"]) {
          return false;
      }
      return (!this.isCustomRow());
    },

    setSelectedInWorkspace : function($super, selectionState, newComponent) {
        if (selectionState) {
            this.parentTable.resultsEl.find("td:eq(" + this.parent.index + ")").addClass("selected-component");
            this.parentTable.selectedResultProxy = this;
        } else {
            this.parentTable.resultsEl.find("td:eq(" + this.parent.index + ")").removeClass("selected-component");
            this.parentTable.selectedResultProxy = false;
        }
    },

    updateProxy : function() {
      var newFormula = "";

      // if there is no forumla assigned to the column, and the format is set to custom, we'll continue
      // with updating the custom proxy, however we need to exclude the collection of formula information
      // ------------------------------------------------------------------------------------------------
        if (isWorkspace() && this._lastSentQ && !this.sendQueryRequestInsteadOfWatch()) {
          //This is to handle the case in the editor when the user had shown summarized data using drill down, then disables drill down
          if (!this.isCustomRow() && this.parent.resultFmt !== Props.Defaults.Agg.EMPTY) {
            newFormula = this.createPostDstFormula(this.parent.resultFmt);
            page.setComponentFormula(this, newFormula);
          }
        }
        if (!this.sendQueryRequestInsteadOfWatch()) {
          this._lastSentQ = false;
          return;
        } else {
          this._lastSentQ = true;
          newFormula = this.createArrayFormula();
        }


      if (!newFormula) {
        return;
      }

        // keep track of the previous format so we can compare in future calls
        this.parent._previousFmt = this.parent.resultFmt;



        if (isWorkspace()) {
          page.setComponentFormula(this, newFormula);
        } else {
          this.setFormula(0, newFormula);
          CxFormula.submitFormula({
            cx: this,
            formula: {formula: newFormula},
            dashboard: this.getKlip().dashboard
          });
        }

    },

  migrateResultRowsToUseResultReferences: function () {
    var newFormula;
    if (!this.isCustomRow() && this.parent.resultFmt !== "empty") {
      newFormula = this.createPostDstFormula(this.parent.resultFmt);

      this.setFormula(0, newFormula);

      //Previously, the formula was not returned with this component, so would not get migrated to new format if this klip
      //was in the process of being migrated from old formula bar mode
      if (this.getFormula(0)) {
        this.getFormula(0).convertToTextOnlyFormat();
      }

    }
  },

  createPostDstFormula: function (agg) {
    var newFormula, componentResultRef;

    componentResultRef = "&'" + this.parent.id + "'";
    if (agg === Props.Defaults.Agg.COUNTDISTINCT) {
      newFormula = "count(group" + "(" + componentResultRef + "))";
    } else {
      newFormula = agg + "(" + componentResultRef + ")";
    }
    return newFormula;

  },

  createArrayFormula: function () {
    var newFormula;
    var proxyFormula = "";
    var proxyFormulaObj;
    //Use dst_array function which does not split a single value on commas, if we're not on a Pdpn
    //See saas-8998
    var colFormula = KF.company.isVCFeatureOn() ? "dst_array(" : "array(";

    this.parent.filteredData.forEach(function (d, i) {
      if (!this.isVCFeatureOn() && this.parent.parent.firstRowIsHeader && i == 0) return;

      if (i > ((!this.isVCFeatureOn() && this.parent.parent.firstRowIsHeader) ? 1 : 0)) colFormula += ",";

      colFormula += "\"" + this.escapeDoubleQuotedString(d) + "\"";
    }, this);

    if ("(" == colFormula.slice(-1)) {
      // Make sure the column formula is valid when the column is empty.  When the column is empty the results
      // row should show zero.  For all formulas except "average", empty data will generate the desired result.
      // For "average", "0" needs to be specified so that the resulting average is zero.
      colFormula += this.parent.resultFmt == "average" ? "\"0\"" : "\"\"";
    }
    colFormula += ")";

    proxyFormulaObj = this.getFormula();
    proxyFormula =
      ( proxyFormulaObj && proxyFormulaObj.formula ) ?
        proxyFormulaObj.formula :
        "\"\"";


    switch (this.parent.resultFmt) {
      case Props.Defaults.Agg.SUM:
      case Props.Defaults.Agg.AVERAGE:
      case Props.Defaults.Agg.MEDIAN:
      case Props.Defaults.Agg.MIN:
      case Props.Defaults.Agg.MAX:
      case Props.Defaults.Agg.FIRST:
      case Props.Defaults.Agg.LAST:
      case Props.Defaults.Agg.COUNT:
        newFormula = this.parent.resultFmt + "(" + colFormula + ")";
        break;
      case Props.Defaults.Agg.COUNTDISTINCT:
        newFormula = "count(group" + "(" + colFormula + "))";
        break;
      case Props.Defaults.Agg.CUSTOM:
        // if we are switching to custom for the first time, clear the formula
        if (this.parent._previousFmt !== undefined && this.parent._previousFmt !== this.parent.resultFmt) {
          newFormula = "\"\"";
        } else { // otherwise set the newFormula to the current formula -- this makes it tie in to the logic further down
          newFormula = proxyFormula;
        }
        break;

      case Props.Defaults.Agg.EMPTY:
      default:
        this.setData([]);
        newFormula = "\"\"";
        break;
    }

    if (newFormula === proxyFormula) return null;
    return newFormula;
  }
});
;/****** cx.tile_map.js *******/ 

/* eslint-disable vars-on-top */
/* global Component:false, isWorkspace:false, dashboard:false, bindValues:false, FMT:false, page:false, updateManager:false, FMT:false, CXTheme:false, _sanitizeNumbers:false, Props:false, KlipFactory:false*/

/**
 * Displays a map based on tiles.  Is not intended to be a map that is accurate in a geo-spacial sense.
 *
 * Tiles are sourced from a javascript format.
 *
 */
Component.TileMap = $.klass(Component.Base, {

	displayName: "Map",
	factory_id:"tile_map",

    mapId: false,
    customMapAsset: false,
    noMarkerSupport: false,

    size: 2,
    zoomEnabled : false,
    focusType : "df",
    focusRegions : "",
    focusX : "",
    focusY : "",
    focusScale : "",

	initialize : function($super) {
		$super();
	},

	renderDom : function($super) {
		var dom = $super();

        this.$div = $("<div>").css("position", "relative");

        if (isWorkspace()) {
            this.$eventTrap = $("<div>").css({position:"absolute", top:0, left:0, width:"100%", height:"100%", display:"none"}).appendTo(this.$div);
        }

        dom.append(this.$div);

        this.changeHeight();
	},

	onResize : function($super) {
		$super();

        this.invalidateAndQueue();
	},

    reRenderWhenVisible: function() {
        this.invalidateAndQueue();
    },

    onChildrenUpdated : function($super) {
        $super();

        this.invalidateAndQueue();
    },

	onKlipAssignment : function($super) {
		$super();

		if (this.mapId == "custom" && this.customMapAsset) {
			this.getCustomMapUrl();
		}

        this.checkForUpgrade();
    },


    configure : function($super,config) {
        $super(config);
        bindValues(this, config, ["size","customMapAsset","zoomEnabled","focusType","focusRegions","focusX","focusY","focusScale"]);

        if (config.size) {
            this.changeHeight();
        }

        if ( config.mapId ) {

            var diffMaps = (this.mapId != config.mapId);
            this.mapId = config.mapId;

            // for canned map files, just use a fixed filename for the map
            // -----------------------------------------------------------
            if (config.mapId != "custom" && diffMaps) {
                this.changeMap(dashboard.getRootPath() + "js/jvectormap/maps/jquery.vmap." + config.mapId + ".js");
            }
        }


        if ((this.mapId == "custom") && this.customMapAsset && ( config.mapId || config.customMapAsset )) {
            this.getCustomMapUrl();
        }

        this.invalidate();
    },

	getCustomMapUrl : function() {
        this.mapLoaded = false; // if we wait for changeMap to do it, it will be too late

        var klip = this.getKlip();
		if (!klip) return;

		var _this = this;

		klip.dashboard.getAssetInfo(
			this.customMapAsset,
			function(data) {
				_this.changeMap(klip.dashboard.getLocationOrigin() + data.perm_url);
			}
		);
	},


	initializeForWorkspace : function($super) {
        var klip = this.getKlip();
        if (!klip.workspace || !klip.workspace.dimensions) {
            klip.setDimensions(Component.TileMap.Sizes[this.size-1] * 1.6);
        }

        this.el.click($.bind(this.onClick, this))
            .on("contextmenu", $.bind(this.onRightClick, this));

        var _this = this;
        PubSub.subscribe("dragging-element", function(evtName, args) {
            _this.toggleEventTrap(args);
        });
	},

    toggleEventTrap : function(show) {
        if (!this.$eventTrap) return;

        if (show) {
            this.$eventTrap.show();
        } else {
            this.$eventTrap.hide();
        }
    },


    serialize : function($super) {
        var config = $super();
        bindValues(config, this, ["mapId","customMapAsset","zoomEnabled","focusType","focusRegions","focusX","focusY","focusScale"] ,
        {
            customMapAsset: false,
            zoomEnabled : false,
            focusType: "df",
            focusRegions: "",
            focusX: "",
            focusY: "",
            focusScale: ""
        },true);
        return config;
    },


    /**
     * Returns the map definition stored in the jvectormap object
     */
    getMap : function() {
        return $.fn.vectorMap("getMap", this.mapId == "custom" ? this.id : this.mapId );
    },

    /**
     * Add a map definition to the jvectormap object
     */
    addMap: function(mapConfig) {
        $.fn.vectorMap("addMap", mapConfig.id, mapConfig.definition);
    },

	/**
	 * find the region code in an array and return its index
	 * @param rcode
	 * @param rarray
	 * @return {Number}
	 */
	getRegionIdx : function( rcode, rarray ){
		var codeIdx = -1;
		for (var i = 0; i < rarray.length; i++){
			if (rarray[i].toUpperCase() == rcode.toUpperCase()){
				codeIdx = i;
				break;
			}
		}
		return codeIdx;
	},


	/**
	 * returns the region code in the same case that it is stored in the map file.
	 * @param rcode
	 */
	getRegionICase : function(rcode) {
        if (rcode === undefined) return;

        if ($.fn.vectorMap) {
            var map = this.getMap();
            for (var tid in map.paths) {
                if (map.paths.hasOwnProperty(tid)) {
                    if (tid.toUpperCase() === rcode.toUpperCase()) {
                        return tid;
                    }
                }
            }
		}
	},


    /**
     * handler for showing region labels
     * @param evt
     * @param label
     * @param code
     */
    onRegionLabelShow : function(evt, label, code) {
        var name = this.$map.vectorMap("get", "regionName", code);

        var regionCx = this.getChildByRole("tile_regions");
        var rIds = regionCx.getChildByRole("region_id").getData(0);
        var rcCx = regionCx.getChildByRole("region_color");
        var rdCx = regionCx.getChildByRole("region_desc");

        var html = "<strong>"+name+"</strong>";

        // find idx:  if we have data points for this region, append them to the label
        // ---------------------------------------------------------------------------
        var codeIdx = this.getRegionIdx(code, rIds);
        if (codeIdx != -1) {
            // append value, if it exists
            // --------------------------
            if (rcCx.showValue) {
                var rcData = rcCx.getData(0);
                var color = rcData[codeIdx];

                if (color !== undefined) {
                    color = FMT.format(rcCx.fmt, [color], rcCx.getFmtArgs());
                    html += "<div style='margin-top:2px'>" + color + "</div>";
                }
            }

            // append description, if it exists
            // --------------------------------
            if (rdCx.showValue) {
                var rdData = rdCx.getData(0);
                var desc = rdData[codeIdx];

                if (desc !== undefined) {
                    desc = FMT.format(rdCx.fmt, [desc], rdCx.getFmtArgs());
                    html += "<div style='margin-top:12px'>" + desc + "</div>";
                }
            }
        }

        label.html(html);
    },

	/**
	 * click handler for region tiles -- triggered by map
	 * @param evt
	 * @param code
	 * @param map
	 */
    onRegionClick : function(evt,code,map) {
        var regionCx = this.getChildByRole("tile_regions");

        if ( !regionCx.xrefEnabled ) return;

        map.label.hide();

        var rdata = regionCx.getChildByRole("region_id").getData(0);
        var rxCx = regionCx.getChildByRole("region_xref");
        var xdata = rxCx.getData(0);

        var codeIdx = this.getRegionIdx(code, rdata);
        var xref = xdata[codeIdx];

        if (xref == undefined) return;

        if (rxCx.xrefInParent) {
            document.location = xref;
        } else {
            window.open(xref, "map-" + this.id);
        }
    },

    onRightClick : function(evt) {
        evt.preventDefault();
        evt.stopPropagation();
        var $target= $(evt.target).get(0);
        var target = false;

        if($target.nodeName=="path") {
            target = this.getChildByRole("tile_regions");
        } else if($target.nodeName=="circle") {
            target = this.getChildByRole("tile_markers");
        } else {
            target = this;
        }

         page.invokeAction("select_component", target, evt);
         page.invokeAction("show_context_menu", { target:target, menuCtx:{ isRoot:(target == this) } }, evt);
    },

    /**
     * handler for showing marker labels
     * @param evt
     * @param label
     * @param markerIdx
     */
    onMarkerLabelShow : function(evt, label, markerIdx) {
        var markerCx = this.getChildByRole("tile_markers");
        var mnCx = markerCx.getChildByRole("marker_name");
        var mcCx = markerCx.getChildByRole("marker_color");
        var msCx = markerCx.getChildByRole("marker_size");
        var mdCx = markerCx.getChildByRole("marker_desc");

        var html = "";

        if (mnCx.showValue) {
            var mnData = mnCx.getData(0);
            var name = mnData[markerIdx];

            if (name !== undefined) {
                name = FMT.format(mnCx.fmt, [name], mnCx.getFmtArgs());
                html += "<strong>"+name+"</strong>";
            }
        }

        if (mcCx.showValue) {
            var mcData = mcCx.getData(0);
            var color = mcData[markerIdx];

            if (color !== undefined) {
                color = FMT.format(mcCx.fmt, [color], mcCx.getFmtArgs());
                html += "<div style='margin-top:2px'>" + color + "</div>";
            }
        }

        if (msCx.showValue) {
            var msData = msCx.getData(0);
            var size = msData[markerIdx];

            if (size !== undefined) {
                size = FMT.format(msCx.fmt, [size], msCx.getFmtArgs());
                html += "<div style='margin-top:2px'>" + size + "</div>";
            }
        }

        if (mdCx.showValue) {
            var mdData = mdCx.getData(0);
            var desc = mdData[markerIdx];

            if (desc !== undefined) {
                desc = FMT.format(mdCx.fmt, [desc], mdCx.getFmtArgs());
                html += "<div style='margin-top:12px'>" + desc + "</div>";
            }
        }

        if (html == "") html = "<strong>Unnamed</strong>";

        label.html(html);
    },

    /**
     * click handler for markers
     * @param evt
     * @param markerIdx
     * @param map
     */
    onMarkerClick : function(evt, markerIdx, map) {
        var markerCx = this.getChildByRole("tile_markers");

        if (!markerCx.xrefEnabled) return;

        map.label.hide();

        var mxCx = markerCx.getChildByRole("marker_xref");
        var xdata = mxCx.getData(0);
        var xref = xdata[markerIdx];

        if (xref == undefined) return;

        if (mxCx.xrefInParent) {
            document.location = xref;
        } else {
            window.open(xref, "map-" + this.id);
        }
    },


    onEvent : function ($super, evt) {
        $super(evt);

        if (evt.id == "theme_changed") {
            this.invalidate();
        }
    },

	update : function($super) {
		$super();

		if ( !this.getKlip().isVisible ) return;

		// do nothing if are not in an invalid state,
		//   or if our dependencies aren't ready yet
		// ------------------------------------------
		if ( !this.checkInvalid(true) ) return;
		if ( !this.dependenciesLoaded || !this.mapLoaded  ) {
			updateManager.queue(this);
			return;
		}

        // return if no custom map is provided
        if ( this.mapId == "custom" && !this.customMapAsset ) return;

        // Height is ready can update panel cell heights
        // -----------------------------------------------
        if (this.fromPanelUpdate) {
            this.parent.handleComponentUpdated();
            this.fromPanelUpdate = false;
        }
        // -----------------------------------------------

        if ( this.$map ) {
            this.$map.vectorMap("destroy");   // call destroy to remove label
            this.$map.remove();
        }

		// display message for users who use IE 8 and are trying to display a custom map
		// ------------------------------------------------------------------------------
//		if (
//			this.mapId == 'custom' &&
//			$.browser.msie &&
//			$.browser.version.slice(0,1) == "8"
//			){
//			this.el.append( $("<div class='quiet small' style='padding:10px'>Custom maps are not supported in IE 8</div>"))
//			return
//		}


        // use a child div instead of this.el, otherwise jqvmap does not clean up custom
        // event handlers
        // ----------------------------------------------------------------------------
        this.$map = $("<div>")
            .addClass("tile_map_container")
            .css({width:"100%", height:"100%"})
            .prependTo(this.$div);


		// collect data from proxies
		// -------------------------
        var regionCx = this.getChildByRole("tile_regions");
        var rIds = regionCx.getChildByRole("region_id").getData(0);
        var rcCx = regionCx.getChildByRole("region_color");
        var rcv = rcCx.getData(0);

        // build map data -- an object where values are keyed against tile codes { "us":100, "ca":200 }
        // --------------------------------------------------------------------------------------------
        var data = {};
        var i, len, region;
		for(i = 0, len = rIds.length; i < len ; i++){
            region = this.getRegionICase(rIds[i]);
            if (region) {
                data[region] = FMT.convertToNumber(rcv[i]);
            }
		}

        var scaleColors = [CXTheme.getThemeColour(rcCx.startColor)];
        if (rcCx.startColor != rcCx.endColor) {
            scaleColors.push(CXTheme.getThemeColour(rcCx.endColor));
        }

        // collection reaction information into our context object
        // -------------------------------------------------------
        var reactionContext = {tileReactionColors:{} , tileIds:rIds, rawData : data };
        this.collectReactions( reactionContext );

        // determine initial focal point
        // -----------------------------
        var focusOn;
        if (this.focusType == "rg") {
            if (this.focusRegions != "") {
                focusOn = [];

                var regions = this.focusRegions.split(",");
                for (i = 0, len = regions.length; i < len; i++) {
                    region = this.getRegionICase(regions[i].replace(/^\s+|\s+$/g, ""));

                    if (region) {
                        focusOn.push(region);
                    }
                }

                if (focusOn.length == 0) focusOn = undefined;
            }
        } else if (this.focusType == "xy") {
            if (this.focusScale == "") this.focusScale = 1;

            focusOn = {};

            focusOn.x = parseFloat(this.focusX);
            focusOn.y = parseFloat(this.focusY);
            focusOn.scale = parseFloat(this.focusScale);
        }

        // handle markers
        // --------------
        var markerCx = this.getChildByRole("tile_markers");
        var mlat = markerCx.getChildByRole("marker_lat").getData(0);
        var mlng = markerCx.getChildByRole("marker_lng").getData(0);
        var mnCx = markerCx.getChildByRole("marker_name");
        var mn = mnCx.getData(0);

        var mcCx = markerCx.getChildByRole("marker_color");
        var mcv = mcCx.getData(0);
        _sanitizeNumbers(mcv);

        var msCx = markerCx.getChildByRole("marker_size");
        var msv = msCx.getData(0);
        _sanitizeNumbers(msv);

        var markers,
            markerColourData,
            markerRadiusData,
            markerColors,
            markerSizes;
        if (!this.noMarkerSupport) {
            if (mlat && mlat.length > 0 && mlng && mlng.length > 0) {
                markers = [];

                for (i = 0, len = mlat.length; i < len; i++) {
                    var marker = {};

                    if (i < mlng.length) {
                        marker.latLng = [FMT.convertToNumber(mlat[i]), FMT.convertToNumber(mlng[i])];

                        if (mn && i < mn.length) {
                            marker.name = FMT.format(mnCx.fmt, [mn[i]], mnCx.getFmtArgs());
                        }

                        markers.push(marker);
                    }
                }
            }

            markerColourData = (mcv && mcv.length > 0) ? mcv : undefined;
            markerRadiusData = (msv && msv.length > 0) ? msv : undefined;

            markerColors = [CXTheme.getThemeColour(mcCx.startColor)];
            if (mcCx.startColor != mcCx.endColor) {
                markerColors.push(CXTheme.getThemeColour(mcCx.endColor));
            }

            markerSizes = [msCx.startSize];
            if (msCx.startSize != msCx.endSize) {
                markerSizes.push(msCx.endSize);
            }
        }


        var mapConfig = {
            map: this.mapId == "custom" ? this.id : this.mapId,
            backgroundColor: null,
            zoomButtons: this.zoomEnabled,
            zoomOnScroll: this.zoomEnabled,
            regionStyle: {
                initial: {
                    fill: CXTheme.current.cx_tile_map_color,
                    stroke: CXTheme.current.cx_tile_map_border,
                    "stroke-width": Component.TileMap.DisplaySettings[this.size - 1].b,
                    "stroke-opacity": Component.TileMap.DisplaySettings[this.size - 1].o
                },
                hover: {
                    "fill-opacity": 0.7
                },
                selected: {
                    fill: CXTheme.current.cx_tile_map_selected
                }
            },
            markers: markers,
            series: {
                markers: [
                    {
                        attribute: "fill",
                        scale: markerColors,
                        values: mcCx.colorsEnabled ? markerColourData : []
                    },
                    {
                        attribute: "r",
                        scale: markerSizes,
                        values: msCx.sizesEnabled ? markerRadiusData : []
                    }
                ],
                regions: [
                    {
                        values: rcCx.colorsEnabled ? data : [],
                        attribute: "fill",
                        scale: scaleColors,
                        normalizeFunction:"linear"
                    },
                    {
                        attribute:"fill"
                    }
                ]
            },
            focusOn: focusOn,
            onRegionLabelShow: _.bind(this.onRegionLabelShow, this),
            onRegionClick: _.bind(this.onRegionClick, this),
            onMarkerLabelShow: _.bind(this.onMarkerLabelShow, this),
            onMarkerClick: _.bind(this.onMarkerClick, this)
        };

		try {
            this.$map.vectorMap(mapConfig);

            // apply reaction colors
            var map = this.$map.vectorMap("get", "mapObject");
            map.series.regions[1].setValues(reactionContext.tileReactionColors);
		} catch(e) {
			console.log("error creating map", e); // eslint-disable-line no-console
		}

		this.setInvalid(false,true);

        if (!isWorkspace() && this.parent.isKlip) {
            this.parent.handleComponentUpdated();
        }
	},

    changeHeight : function() {
        if (!this.$div) return;

        var newHeight = Component.TileMap.Sizes[this.size-1];

        this.$div.height(newHeight);
        this.setHeight(newHeight);
    },

	changeMap : function( mapPath ) {
        this.mapLoaded = false;

        if (!this.dependenciesLoaded) return;

        mapPath = mapPath + "?mapid=" + this.id;
		var _this = this;

		require([mapPath], function(mapConfig) {
			_this.addMap(mapConfig);

            _this.mapLoaded = true;

            var map = _this.getMap();
            var diffSupport = ((map.noMarkerSupport && !_this.noMarkerSupport) || (!map.noMarkerSupport && _this.noMarkerSupport));
            _this.noMarkerSupport = map.noMarkerSupport ? true : false;

            if (isWorkspace() && diffSupport) {
                page.updatePropertySheet(_this.getPropertyModel(), _this.getPropertyModelGroups(), page.componentPropertyForm);
            }

            updateManager.queue(_this);
		});
	},

	getDependencies : function() {
		return [dashboard.getRootPath() + "js/jvectormap/jquery-jvectormap-1.2.2.js"];
	},


	getPropertyModel : function($super) {
		var model = $super();
        // remove "Custom" from the chartSize
        var mapSizes = Props.Defaults.chartSize.slice(0, Props.Defaults.chartSize.length - 1);

		model.push({
			id:"mapId",
			group:"map",
			type:"select",
			label:"Map",
			options: Component.TileMap.DefaultMaps,
			selectedValue:this.mapId,
			help: {
                link: "klips/map-general"
            }
		});

		model.push({
			type:"asset_select" ,
			id:"customMapAsset",
			group:"map",
			label:"Custom Map",
            buttonText:"Select Map File",
            assetTitle:"Upload a map file.",
			assetContext:"map-data",
			assetType:"map-data",
			displayWhen: { mapId:"custom" },
			selectedValue: this.customMapAsset
		});

        if (this.noMarkerSupport) {
            model.push({
                type:"markup",
                id:"msg_no_marker_support",
                group:"map",
                el:$("<span>Note: This map does not support markers.</span>")
            });
        }


		model.push({
			id:"size",
			group:"size",
			type:"select",
			label:"Size",
			options: mapSizes,
			selectedValue:this.size
		});


		model.push({
			type:"checkboxes",
			id:"zoomEnabled",
			group:"size",
			label:"",
			options:[
				{
					value:1,
					label:"Show zoom controls",
					checked:this.zoomEnabled
				}
			]
		});

        model.push({
            type:"select",
            id:"focusType",
            group:"focus",
            label:"Focus On",
            options: [
                {value:"df", label:"Default"},
                {value:"rg", label:"Regions"},
                {value:"xy", label:"X- and Y-Coordinates"}
            ],
            help: {
                link: "klips/map-focuson"
            },
            selectedValue:this.focusType
        });

        model.push({
            type:"text",
            id:"focusRegions",
            group:"focus",
            displayWhen: { focusType:"rg" },
            value:this.focusRegions
        });

        model.push({
            type:"text",
            id:"focusScale",
            group:"focus",
            minorLabels: [
                {label:"Scale:", position:"left"},
                {label:"(1-8)", position:"right", css:"quiet italics"}
            ],
            displayWhen: { focusType:["xy"] },
            width:"36px",
            value:this.focusScale
        });

        model.push({
            type:"text",
            id:"focusX",
            group:"focus",
            minorLabels: [
                {label:"X:", position:"left"},
                {label:"(0-1)", position:"right", css:"quiet italics"}
            ],
            displayWhen: { focusType:"xy" },
            width:"36px",
            value:this.focusX
        });

        model.push({
            type:"text",
            id:"focusY",
            group:"focus",
            minorLabels: [
                {label:"Y:", position:"left"},
                {label:"(0-1)", position:"right", css:"quiet italics"}
            ],
            displayWhen: { focusType:"xy" },
            width:"36px",
            value:this.focusY
        });

        return model;
	},


    getSupportedReactions : function() {
        return { color: true };
    },

	/**
	 *
	 * @param idx
	 * @param reaction
	 * @param ctx - {
	 * 		tileReactionColors: {},
	 * 		tileIds: [],
	 * 		rawData: {}
	 * }
	 */
	handleReaction : function( idx, reaction, ctx ) {
		switch (reaction.type) {
			case "color":
				var tileId = this.getRegionICase(ctx.tileIds[idx]);
				if (!tileId) return;
				ctx.tileReactionColors[ tileId ] = CXTheme.getThemeColour(reaction.value);
				delete ctx.rawData[tileId]; // remove it from the raw data so that color is used instead of value
				break;
		}
	},

	getReactionTargets : function() {
		var regionCx = this.getChildByRole("tile_regions");

        return regionCx.getChildByRole("region_id").getFilteredData(0);
	},


	afterFactory : function($super) {
        if (!this.mapId) {
			this.mapId = "world_en";
		}

        if (this.mapId == "custom") {
            this.getCustomMapUrl();
        } else {
            this.changeMap(dashboard.getRootPath() + "js/jvectormap/maps/jquery.vmap." + this.mapId + ".js");
        }

		var requiredChildren = [
            {role:"tile_regions", props:{displayName:"Regions", type:"map_region"}},
            {role:"tile_markers", props:{displayName:"Markers", type:"map_marker"}}
		];

		for (var i = 0; i < requiredChildren.length; i++) {
			var req = requiredChildren[i];
			var cx = this.getChildByRole(req.role);
			if (!cx) {
				cx = KlipFactory.instance.createComponent($.extend(req.props, {role:req.role}));
				if ( req.props._data ) cx.setData( req.props._data );
				this.addComponent(cx);
			}
		}

		if (isWorkspace()) {
			page.updateMenu();
		}
    },

    checkForUpgrade : function() {
        if (isWorkspace()) return;

        var k = this.getKlip();
        if (!k) return;

        // ----------------------------------------------------------
        // [saas-3233]
        // Original sub-components moved into Regions sub-component
        // ----------------------------------------------------------
        var ids = this.getChildByRole("tile_ids");
        var values = this.getChildByRole("tile_values");
        var desc = this.getChildByRole("tile_desc");
        var xref = this.getChildByRole("tile_xref");

        if (ids || values || desc || xref) {
            k.addMessage({ type:"cx-upgrade", id:this.id }, "upgrade");
        }
        // ----------------------------------------------------------
    }

});

/**
 * map heights at different sizes (small -> x-large)
 */
Component.TileMap.Sizes = [120,240,400,600];

/**
 * config objects for each size that contain (b)order size information, and border (o)pacity
 */
Component.TileMap.DisplaySettings = [
	{b:4, o:0.75},
	{b:2, o:0.75},
	{b:1, o:1},
	{b:1, o:1}

];

Component.TileMap.DefaultMaps = [
    {value:"world_en", label:"World"} ,
    {value:"world_by_region_en", label:"World by Region"} ,
	{value:"europe_en", label:"Europe"},
    {value:"africa_en", label:"Africa"},
	{value:"apac_en", label:"APAC"},
    {value:"canada_en", label:"Canada"} ,
    {value:"germany_en", label:"Germany"} ,
    {value:"in_en", label:"India"} ,
    {value:"mexico_es", label:"Mexico"} ,
    {value:"netherlands_nl", label:"Netherlands"} ,
    {value:"usa_en", label:"USA"} ,
	{value:"custom",label:"Custom..."}
];
;