/*!
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
//VERSION 13.06.05

var pvc = (function(def, pv) {


/*global pvc:true */
var pvc = def.globalSpace('pvc', {
    // 0 - off
    // 1 - errors 
    // 2 - errors, warnings
    // 3 - errors, warnings, info
    // 4 - verbose
    // 5 - trash
    // ...
    debug: 0
});

// Check URL debug and debugLevel
(function() {
    /*global window:true*/
    if((typeof window.location) !== 'undefined') {
        var url = window.location.href;
        if(url && (/\bdebug=true\b/).test(url)) {
            var m = /\bdebugLevel=(\d+)/.exec(url);
            pvc.debug = m ? (+m[1]) : 3;
        }
    }
}());

var pv_Mark = pv.Mark;

// goldenRatio proportion
// ~61.8% ~ 38.2%
//pvc.goldenRatio = (1 + Math.sqrt(5)) / 2;

pvc.invisibleFill = 'rgba(127,127,127,0.00001)';

pvc.logSeparator = "------------------------------------------";

var pvc_arraySlice = Array.prototype.slice;

pvc.setDebug = function(level) {
    level = +level;
    pvc.debug = isNaN(level) ? 0 : level;
    
    pvc_syncLog();
    pvc_syncTipsyLog();
    
    return pvc.debug;
};

/*global console:true*/

function pvc_syncLog() {
    if (pvc.debug && typeof console !== "undefined") {
        ['log', 'info', ['trace', 'debug'], 'error', 'warn', ['group', 'groupCollapsed'], 'groupEnd']
        .forEach(function(ps) {
            ps = ps instanceof Array ? ps : [ps, ps];
            
            pvc_installLog(pvc, ps[0],  ps[1],  '[pvChart]');
        });
    } else {
        if(pvc.debug > 1) { pvc.debug = 1; }
        
        ['log', 'info', 'trace', 'warn', 'group', 'groupEnd']
        .forEach(function(p) { pvc[p] = def.noop; });

        var _errorPrefix = "[pvChart ERROR]: ";
        
        pvc.error = function(e) {
            if(e && typeof e === 'object' && e.message) { e = e.message; }

            e = '' + def.nullyTo(e, '');
            if(e.indexOf(_errorPrefix) < 0) { e = _errorPrefix + e; }
            
            throw new Error(e);
        };
    }
    
    pvc.logError = pvc.error;
    
    // Redirect protovis error handler
    pv.error = pvc.error;
}

function pvc_syncTipsyLog() {
    var tip = pv.Behavior.tipsy;
    if(tip && tip.setDebug) {
        tip.setDebug(pvc.debug);
        tip.log = pvc.log;
    }
}

function pvc_installLog(o, pto, pfrom, prompt) {
    if(!pfrom) { pfrom = pto; }
    var c = console;
    var m = c[pfrom] || c.log;
    var fun;
    if(m) {
        var mask = prompt + ": %s";
        if(!def.fun.is(m)) {
            // For IE these are not functions...but simply objects
            // Bind is not available or may be a polyfill that won't work...
            
            var apply = Function.prototype.apply;
            fun = function() {
                apply.call(m, c, def.array.append([mask], arguments));
            };
        } else {
            // Calls to fun are like direct calls to m...
            // and capture file and line numbers correctly!
            fun = m.bind(c, mask);
        }
    }
    
    o[pto] = fun;
}

pvc.setDebug(pvc.debug);

/**
 * Gets or sets the default CCC compatibility mode. 
 * <p>
 * Use <tt>Infinity</tt> for the <i>latest</i> version.
 * Use <tt>1</tt> for CCC version 1.
 * </p>
 * 
 * @param {number} [compatVersion] The new compatibility version.    
 */
pvc.defaultCompatVersion = function(compatVersion) {
    var defaults = pvc.BaseChart.prototype.defaults;
    if(compatVersion != null) {
        return defaults.compatVersion = compatVersion;
    } 
    
    return defaults.compatVersion;
};

pvc.cloneMatrix = function(m) {
    return m.map(function(d) { return d.slice(); });
};

pvc.stringify = function(t, keyArgs) {
    var maxLevel = def.get(keyArgs, 'maxLevel') || 5;
    
    var out = [];
    pvc.stringifyRecursive(out, t, maxLevel, keyArgs);
    return out.join('');
};

pvc.stringifyRecursive = function(out, t, remLevels, keyArgs) {
    if(remLevels > 0) {
        remLevels--;
        switch(typeof t) {
            case 'undefined': return out.push('undefined');
            case 'object':
                if(!t) {
                    out.push('null');
                    return true;
                }
                
                if(def.fun.is(t.stringify)) {
                    return t.stringify(out, remLevels, keyArgs);
                }
                
                if(t instanceof Array) {
                    out.push('[');
                    t.forEach(function(item, index) {
                        if(index) { out.push(', '); }
                        if(!pvc.stringifyRecursive(out, item, remLevels, keyArgs)) {
                            out.pop();
                        }
                    });
                    out.push(']');
                } else {
                    var ownOnly = def.get(keyArgs, 'ownOnly', true);
                    if(t === def.global) {
                        out.push('<window>');
                        return true;
                    }

                    if(def.fun.is(t.cloneNode)) {
                        // DOM object
                        out.push('<dom #' + (t.id || t.name || '?') + '>');
                        return true;
                    }

                    if(remLevels > 1 && t.constructor !== Object) {
                        remLevels = 1;
                        ownOnly = true;
                    }
                    
                    out.push('{');
                    var first = true;
                    for(var p in t) {
                        if(!ownOnly || def.hasOwnProp.call(t, p)) {
                            if(!first) { out.push(', '); }
                            out.push(p + ': ');
                            if(!pvc.stringifyRecursive(out, t[p], remLevels, keyArgs)) {
                                out.pop();
                                if(!first) { out.pop(); }
                            } else if(first) {
                                first = false;
                            }
                        }
                    }
                    
                    if(first) {
                        var s = '' + t;
                        if(s !== '[object Object]') { // not very useful
                            out.push('{'+ s + '}');
                        }
                    }
                    
                    out.push('}');
                }
//                    else {
//                        out.push(JSON.stringify("'new ...'"));
//                    }
                return true;
            
            case 'number':
                out.push(''+(Math.round(100000 * t) / 100000)); // 6 dec places max
                return true;

            case 'boolean': 
                out.push(''+t);
                return true;
                
            case 'string': 
                out.push(JSON.stringify(t));
                return true;
                
            case 'function':
                if(def.get(keyArgs, 'funs', false)) {
                    out.push(JSON.stringify(t.toString().substr(0, 13) + '...'));
                    return true;
                }
                
                return false;
        }
        
        out.push("'new ???'");
        return true;
    }
};

pvc.orientation = {
    vertical:   'vertical',
    horizontal: 'horizontal'
};

/** 
 * To tag pv properties set by extension points
 * @type string 
 * @see pvc.BaseChart#extend
 */
pvc.extensionTag = 'extension';

/**
 * Extends a type created with {@link def.type}
 * with the properties in {@link exts}, 
 * possibly constrained to the properties of specified names.
 * <p>
 * The properties whose values are not functions
 * are converted to constant functions that return the original value.
 * </p>
 * @param {function} type
 *      The type to extend.
 * @param {object} [exts] 
 *      The extension object whose properties will extend the type.
 * @param {string[]} [names]
 *      The allowed property names. 
 */
pvc.extendType = function(type, exts, names) {
    if(exts) {
        var exts2;
        var sceneVars = type.prototype._vars;
        var addExtension = function(ext, n) {
            if(ext !== undefined) {
                if(!exts2) { exts2 = {}; }
                if(sceneVars && sceneVars[n]) {
                    n = '_' + n + 'EvalCore';
                }
                
                exts2[n] = def.fun.to(ext);
            }
        };
        
        if(names) {
            names.forEach(function(n) { addExtension(exts[n], n); });
        } else {
            def.each(addExtension);
        }
        
        if(exts2) { type.add(exts2); }
    }
};

pv.Color.prototype.stringify = function(out, remLevels, keyArgs){
    return pvc.stringifyRecursive(out, this.key, remLevels, keyArgs);
};

pv_Mark.prototype.hasDelegateValue = function(name, tag) {
    var p = this.$propertiesMap[name];
    if(p){
        return (!tag || p.tag === tag);
    }
    
    // This mimics the way #bind works
    if(this.proto){
        return this.proto.hasDelegateValue(name, tag);
    }
    
    return false;
};

/**
 * The default color scheme used by charts.
 * <p>
 * Charts use the color scheme specified in the chart options 
 * {@link pvc.BaseChart#options.colors}
 * and 
 * {@link pvc.BaseChart#options.color2AxisColorss}, 
 * for the main and second axis series, respectively, 
 * or, when any is unspecified, 
 * the default color scheme.
 * </p>
 * <p>
 * When null, the color scheme {@link pv.Colors.category10} is implied. 
 * To obtain the default color scheme call {@link pvc.createColorScheme}
 * with no arguments. 
 * </p>
 * <p>
 * To be generically useful, 
 * a color scheme should contain at least 10 colors.
 * </p>
 * <p>
 * A color scheme is a function that creates a {@link pv.Scale} color scale function
 * each time it is called. 
 * It sets as its domain the specified arguments and as range 
 * the pre-spcecified colors of the color scheme.
 * </p>
 * 
 * @readonly
 * @type function
 */
pvc.defaultColorScheme = null;

pvc.brighterColorTransform = function(color){
    return (color.rgb ? color : pv.color(color)).brighter(0.6);
};

/**
 * Sets the colors of the default color scheme used by charts 
 * to a specified color array.
 * <p>
 * If null is specified, the default color scheme is reset to its original value.
 * </p>
 * 
 * @param {string|pv.Color|string[]|pv.Color[]|pv.Scale|function} [colors=null] Something convertible to a color scheme by {@link pvc.colorScheme}.
 * @return {null|pv.Scale} A color scale function or null.
 */
pvc.setDefaultColorScheme = function(colors){
    return pvc.defaultColorScheme = pvc.colorScheme(colors);
};

pvc.defaultColor = pv.Colors.category10()('?');

/**
 * Creates a color scheme if the specified argument is not one already.
 * 
 * <p>
 * A color scheme function is a factory of protovis color scales.
 * Given the domain values, returns a protovis color scale.
 * The arguments of the function are suitable for passing
 * to a protovis scale's <tt>domain</tt> method.
 * </p>
 * 
 * @param {string|pv.Color|string[]|pv.Color[]|pv.Scale|function} [colors=null] A value convertible to a color scheme: 
 * a color string, 
 * a color object, 
 * an array of color strings or objects, 
 * a protovis color scale function,
 * a color scale factory function (i.e. a color scheme), 
 * or null.
 * 
 * @returns {null|function} A color scheme function or null.
 */
pvc.colorScheme = function(colors){
    if(colors == null) { return null; }
    
    if(typeof colors === 'function') {
        // Assume already a color scheme (a color scale factory)
        if(!colors.hasOwnProperty('range')) { return colors; }
            
        // A protovis color scale
        // Obtain its range colors array and discard the scale function.
        colors = colors.range();
    } else {
        colors = def.array.as(colors);
    }
    
    if(!colors.length) { return null; }
    
    return function() {
        var scale = pv.colors(colors); // creates a color scale with a defined range
        scale.domain.apply(scale, arguments); // defines the domain of the color scale
        return scale;
    };
},

/**
 * Creates a color scheme based on the specified colors.
 * When no colors are specified, the default color scheme is returned.
 * 
 * @see pvc.defaultColorScheme 
 * @param {string|pv.Color|string[]|pv.Color[]|pv.Scale|function} [colors=null] Something convertible to a color scheme by {@link pvc.colorScheme}.
 * @type function
 */
pvc.createColorScheme = function(colors){
    return pvc.colorScheme(colors) ||
           pvc.defaultColorScheme  ||
           pv.Colors.category10;
};

// Convert to Grayscale using YCbCr luminance conv.
pvc.toGrayScale = function(color, alpha, maxGrayLevel, minGrayLevel){
    color = pv.color(color);
    
    var avg = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
    // Don't let the color get near white, or it becomes unperceptible in most monitors
    if(maxGrayLevel === undefined) {
        maxGrayLevel = 200;
    } else if(maxGrayLevel == null){
        maxGrayLevel = 255; // no effect
    }
    
    if(minGrayLevel === undefined){
        minGrayLevel = 30;
    } else if(minGrayLevel == null){
        minGrayLevel = 0; // no effect
    }
    
    var delta = (maxGrayLevel - minGrayLevel);
    if(delta <= 0){
        avg = maxGrayLevel;
    } else {
        // Compress
        avg = minGrayLevel + (avg / 255) * delta;
    }
    
    if(alpha == null){
        alpha = color.opacity;
    } else if(alpha < 0){
        alpha = (-alpha) * color.opacity;
    }
    
    avg = Math.round(avg);
    
    return pv.rgb(avg, avg, avg, alpha);
};

// TODO: change the name of this
pvc.removeTipsyLegends = function(){
    try {
        $('.tipsy').remove();
    } catch(e) {
        // Do nothing
    }
};

pvc.createDateComparer = function(parser, key){
    if(!key){
        key = pv.identity;
    }
    
    return function(a, b){
        return parser.parse(key(a)) - parser.parse(key(b));
    };
};

pvc.time = {
    intervals: {
        'y':   31536e6,
        
        'm':   2592e6,
        'd30': 2592e6,
        
        'w':   6048e5,
        'd7':  6048e5,
        
        'd':   864e5,
        'h':   36e5,
        'M':   6e4,
        's':   1e3,
        'ms':  1
    },
    
    withoutTime: function(t){
        return new Date(t.getFullYear(), t.getMonth(), t.getDate());
    },
    
    weekday: {
        previousOrSelf: function(t, toWd){
            var wd  = t.getDay();
            var difDays = wd - toWd;
            if(difDays){
                // Round to the previous wanted week day
                var previousOffset = difDays < 0 ? (7 + difDays) : difDays;
                t = new Date(t - previousOffset * pvc.time.intervals.d);
            }
            return t;
        },
        
        nextOrSelf: function(t, toWd){
            var wd  = t.getDay();
            var difDays = wd - toWd;
            if(difDays){
                // Round to the next wanted week day
                var nextOffset = difDays > 0 ? (7 - difDays) : -difDays;
                t = new Date(t + nextOffset * pvc.time.intervals.d);
            }
            return t;
        },
        
        closestOrSelf: function(t, toWd){
            var wd = t.getDay(); // 0 - Sunday, ..., 6 - Friday
            var difDays = wd - toWd;
            if(difDays){
                var D = pvc.time.intervals.d;
                var sign = difDays > 0 ? 1 : -1;
                difDays = Math.abs(difDays);
                if(difDays >= 4){
                    t = new Date(t.getTime() + sign * (7 - difDays) * D);
                } else {
                    t = new Date(t.getTime() - sign * difDays * D);
                }
            }
            return t;
        }
    }
};

pv.Format.createParser = function(pvFormat) {
    
    function parse(value) { return pvFormat.parse(value); }
    
    return parse;
};

pv.Format.createFormatter = function(pvFormat) {
    
    function format(value) { return value != null ? pvFormat.format(value) : ""; }
    
    return format;
};

pvc.buildTitleFromName = function(name) {
    // TODO: i18n
    return def.firstUpperCase(name).replace(/([a-z\d])([A-Z])/, "$1 $2");
};

pvc.buildIndexedId = function(prefix, index) {
    if(index > 0) { return prefix + "" + (index + 1); } // base2, ortho3,..., legend2
    
    return prefix; // base, ortho, legend
};

/**
 * Splits an indexed id into its prefix and index.
 * 
 * @param {string} indexedId The indexed id.
 * 
 * @type Array
 */
pvc.splitIndexedId = function(indexedId){
    var match = /^(.*?)(\d*)$/.exec(indexedId);
    var index = null;
    
    if(match[2]) {
        index = Number(match[2]);
        if(index <= 1) {
            index = 1;
        } else {
            index--;
        }
    }
    
    return [match[1], index];
};

function pvc_unwrapExtensionOne(id, prefix){
    if(id){
        if(def.object.is(id)){
            return id.abs;
        }
        
        return prefix ? (prefix + def.firstUpperCase(id)) : id;
    }
    
    return prefix;
}

var pvc_oneNullArray = [null];

pvc.makeExtensionAbsId = function(id, prefix) {
    if(!id) { return prefix; }
    
    return def
       .query(prefix || pvc_oneNullArray)
       .selectMany(function(oneprefix) {
           return def
               .query(id)
               .select(function(oneid) { return pvc_unwrapExtensionOne(oneid, oneprefix); });
       })
       .where(def.truthy)
       .array()
       ;
};

pvc.makeEnumParser = function(enumName, keys, dk) {
    var keySet = {};
    keys.forEach(function(k){ if(k) { keySet[k.toLowerCase()] = k; }});
    if(dk) { dk = dk.toLowerCase(); }
    
    return function(k) {
        if(k) { k = (''+k).toLowerCase(); }
        
        if(!def.hasOwn(keySet, k)) {
            if(k && pvc.debug >= 2) {
                pvc.log("[Warning] Invalid '" + enumName + "' value: '" + k + "'. Assuming '" + dk + "'.");
            }
        
            k = dk;
        }
        
        return k;
    };
};

pvc.parseDistinctIndexArray = function(value, min, max){
    value = def.array.as(value);
    if(value == null) { return null; }
    
    if(min == null) { min = 0; }
    
    if(max == null) { max = Infinity; }
    
    var a = def
        .query(value)
        .select(function(index) { return +index; }) // to number
        .where(function(index) { return !isNaN(index) && index >= min && index <= max; })
        .distinct()
        .array();
    
    return a.length ? a : null;
};

pvc.parseLegendClickMode = 
    pvc.makeEnumParser('legendClickMode', ['toggleSelected', 'toggleVisible', 'none'], 'toggleVisible');

pvc.parseTooltipAutoContent = 
    pvc.makeEnumParser('tooltipAutoContent', ['summary', 'value'], 'value');

pvc.parseSelectionMode =
    pvc.makeEnumParser('selectionMode', ['rubberBand', 'focusWindow'], 'rubberBand');
   
    pvc.parseClearSelectionMode =
        pvc.makeEnumParser('clearSelectionMode', ['emptySpaceClick', 'manual'], 'emptySpaceClick');

pvc.parseShape = 
    pvc.makeEnumParser('shape', ['square', 'circle', 'diamond', 'triangle', 'cross', 'bar'], null);

pvc.parseTreemapColorMode = 
    pvc.makeEnumParser('colorMode', ['byParent', 'bySelf'], 'byParent');

pvc.parseTreemapLayoutMode = 
    pvc.makeEnumParser('layoutMode', ['squarify', 'slice-and-dice', 'slice', 'dice'], 'squarify');

pvc.parseContinuousColorScaleType = function(scaleType) {
    if(scaleType) {
        scaleType = (''+scaleType).toLowerCase();
        switch(scaleType) {
            case 'linear':
            case 'normal':
            case 'discrete':
                break;
            
            default:
                if(pvc.debug >= 2){
                    pvc.log("[Warning] Invalid 'ScaleType' option value: '" + scaleType + "'.");
                }
            
            scaleType = null;
                break;
        }
    }
    
    return scaleType;  
};

pvc.parseDomainScope = function(scope, orientation){
    if(scope){
        scope = (''+scope).toLowerCase();
        switch(scope){
            case 'cell':
            case 'global':
                break;
            
            case 'section': // row (for y) or col (for x), depending on the associated orientation
                if(!orientation){
                    throw def.error.argumentRequired('orientation');
                }
                
                scope = orientation === 'y' ? 'row' : 'column';
                break;
                
            case 'column':
            case 'row':
                if(orientation && orientation !== (scope === 'row' ? 'y' : 'x')){
                    scope = 'section';
                    
                    if(pvc.debug >= 2){
                        pvc.log("[Warning] Invalid 'DomainScope' option value: '" + scope + "' for the orientation: '" + orientation + "'.");
                    }
                }
                break;
            
            default:
                if(pvc.debug >= 2){
                    pvc.log("[Warning] Invalid 'DomainScope' option value: '" + scope + "'.");
                }
            
                scope = null;
                break;
        }
    }
    
    return scope;
};

pvc.parseDomainRoundingMode = function(mode){
    if(mode){
        mode = (''+mode).toLowerCase();
        switch(mode){
            case 'none':
            case 'nice':
            case 'tick':
                break;
                
            default:
                if(pvc.debug >= 2){
                    pvc.log("[Warning] Invalid 'DomainRoundMode' value: '" + mode + "'.");
                }
            
                mode = null;
                break;
        }
    }
    
    return mode;
};

pvc.parseOverlappedLabelsMode = function(mode){
    if(mode){
        mode = (''+mode).toLowerCase();
        switch(mode){
            case 'leave':
            case 'hide':
            case 'rotatethenhide':
                break;
            
            default:
                if(pvc.debug >= 2){
                    pvc.log("[Warning] Invalid 'OverlappedLabelsMode' option value: '" + mode + "'.");
                }
            
                mode = null;
                break;
        }
    }
    
    return mode;
};

pvc.castNumber = function(value) {
    if(value != null) {
        value = +value; // to number
        if(isNaN(value)) {
            value = null;
        }
    }
    
    return value;
};

pvc.parseWaterDirection = function(value) {
    if(value){
        value = (''+value).toLowerCase();
        switch(value){
            case 'up':
            case 'down':
                return value;
        }
        
        if(pvc.debug >= 2){
            pvc.log("[Warning] Invalid 'WaterDirection' value: '" + value + "'.");
        }
    }
};

pvc.parseTrendType = function(value) {
    if(value){
        value = (''+value).toLowerCase();
        if(value === 'none'){
            return value;
        }
        
        if(pvc.trends.has(value)){
            return value;
        }
        
        if(pvc.debug >= 2){
            pvc.log("[Warning] Invalid 'TrendType' value: '" + value + "'.");
        }
    }
};

pvc.parseNullInterpolationMode = function(value) {
    if(value){
        value = (''+value).toLowerCase();
        switch(value){
            case 'none':
            case 'linear':
            case 'zero':
                return value;
        }
        
        if(pvc.debug >= 2){
            pvc.log("[Warning] Invalid 'NullInterpolationMode' value: '" + value + "'.");
        }
    }
};

pvc.parseAlign = function(side, align){
    if(align){ align = (''+align).toLowerCase(); }
    var align2, isInvalid;
    if(side === 'left' || side === 'right'){
        align2 = align && pvc.BasePanel.verticalAlign[align];
        if(!align2){
            align2 = 'middle';
            isInvalid = !!align;
        }
    } else {
        align2 = align && pvc.BasePanel.horizontalAlign[align];
        if(!align2){
            align2 = 'center';
            isInvalid = !!align;
        }
    }
    
    if(isInvalid && pvc.debug >= 2){
        pvc.log(def.format("Invalid alignment value '{0}'. Assuming '{1}'.", [align, align2]));
    }
    
    return align2;
};

// suitable for protovis.anchor(..) of all but the Wedge mark... 
pvc.parseAnchor = function(anchor){
    if(anchor){
        anchor = (''+anchor).toLowerCase();
        switch(anchor){
            case 'top':
            case 'left':
            case 'center':
            case 'bottom':
            case 'right':
                return anchor;
        }
        
        if(pvc.debug >= 2){
            pvc.log(def.format("Invalid anchor value '{0}'.", [anchor]));
        }
    }
};

pvc.parseAnchorWedge = function(anchor){
    if(anchor){
        anchor = (''+anchor).toLowerCase();
        switch(anchor){
            case 'outer':
            case 'inner':
            case 'center':
            case 'start':
            case 'end':
                return anchor;
        }
        
        if(pvc.debug >= 2){
            pvc.log(def.format("Invalid wedge anchor value '{0}'.", [anchor]));
        }
    }
};

pvc.unionExtents = function(result, range){
    if(!result) {
        if(!range){
            return null;
        }

        result = {min: range.min, max: range.max};
    } else if(range){
        if(range.min < result.min){
            result.min = range.min;
        }

        if(range.max > result.max){
            result.max = range.max;
        }
    }

    return result;
};

/**
 * Creates a margins/sides object.
 * @constructor
 * @param {string|number|object} sides May be a css-like shorthand margin string.
 * 
 * <ol>
 *   <li> "1" - {all: '1'}</li>
 *   <li> "1 2" - {top: '1', left: '2', right: '2', bottom: '1'}</li>
 *   <li> "1 2 3" - {top: '1', left: '2', right: '2', bottom: '3'}</li>
 *   <li> "1 2 3 4" - {top: '1', right: '2', bottom: '3', left: '4'}</li>
 * </ol>
 */
var pvc_Sides = pvc.Sides = function(sides) {
    if(sides != null) { this.setSides(sides); }
};

pvc_Sides.hnames = 'left right'.split(' ');
pvc_Sides.vnames = 'top bottom'.split(' ');
pvc_Sides.names = 'left right top bottom'.split(' ');
pvc_Sides.namesSet = pv.dict(pvc_Sides.names, def.retTrue);

pvc.parsePosition = function(side, defaultSide){
    if(side){ 
        side = (''+side).toLowerCase();
        
        if(!def.hasOwn(pvc_Sides.namesSet, side)){
            var newSide = defaultSide || 'left';
            
            if(pvc.debug >= 2){
                pvc.log(def.format("Invalid position value '{0}. Assuming '{1}'.", [side, newSide]));
            }
            
            side = newSide;
        }
    }
    
    return side || defaultSide || 'left';
};

pvc_Sides.as = function(v){
    if(v != null && !(v instanceof pvc_Sides)){
        v = new pvc_Sides().setSides(v);
    }
    
    return v;
};

pvc_Sides.prototype.stringify = function(out, remLevels, keyArgs){
    return pvc.stringifyRecursive(out, def.copyOwn(this), remLevels, keyArgs);
};

pvc_Sides.prototype.setSides = function(sides){
    if(typeof sides === 'string'){
        var comps = sides.split(/\s+/).map(function(comp){
            return pvc_PercentValue.parse(comp);
        });
        
        switch(comps.length){
            case 1:
                this.set('all', comps[0]);
                return this;
                
            case 2:
                this.set('top',    comps[0]);
                this.set('left',   comps[1]);
                this.set('right',  comps[1]);
                this.set('bottom', comps[0]);
                return this;
                
            case 3:
                this.set('top',    comps[0]);
                this.set('left',   comps[1]);
                this.set('right',  comps[1]);
                this.set('bottom', comps[2]);
                return this;
                
            case 4:
                this.set('top',    comps[0]);
                this.set('right',  comps[1]);
                this.set('bottom', comps[2]);
                this.set('left',   comps[3]);
                return this;
                
            case 0:
                return this;
        }
    } else if(typeof sides === 'number') {
        this.set('all', sides);
        return this;
    } else if (typeof sides === 'object') {
        if(sides instanceof pvc_PercentValue){
            this.set('all', sides);
        } else {
            this.set('all', sides.all);
            for(var p in sides){
                if(p !== 'all' && pvc_Sides.namesSet.hasOwnProperty(p)){
                    this.set(p, sides[p]);
                }
            }
        }
        
        return this;
    }
    
    if(pvc.debug) {
        pvc.log("Invalid 'sides' value: " + pvc.stringify(sides));
    }
    
    return this;
};

pvc_Sides.prototype.set = function(prop, value){
    value = pvc_PercentValue.parse(value);
    if(value != null){
        if(prop === 'all'){
            // expand
            pvc_Sides.names.forEach(function(p){
                this[p] = value;
            }, this);
            
        } else if(def.hasOwn(pvc_Sides.namesSet, prop)){
            this[prop] = value;
        }
    }
};

pvc_Sides.prototype.resolve = function(width, height){
    if(typeof width === 'object'){
        height = width.height;
        width  = width.width;
    }
    
    var sides = {};
    
    pvc_Sides.names.forEach(function(side){
        var value  = 0;
        var sideValue = this[side];
        if(sideValue != null){
            if(typeof(sideValue) === 'number'){
                value = sideValue;
            } else {
                value = sideValue.resolve((side === 'left' || side === 'right') ? width : height);
            }
        }
        
        sides[side] = value;
    }, this);
    
    return pvc_Sides.updateSize(sides);
};

pvc_Sides.updateSize = function(sides){
    sides.width  = (sides.left   || 0) + (sides.right || 0);
    sides.height = (sides.bottom || 0) + (sides.top   || 0);
    
    return sides;
};

pvc_Sides.resolvedMax = function(a, b){
    var sides = {};
    
    pvc_Sides.names.forEach(function(side){
        sides[side] = Math.max(a[side] || 0, b[side] || 0);
    });
    
    return sides;
};

pvc_Sides.inflate = function(sides, by){
    var sidesOut = {};
    
    pvc_Sides.names.forEach(function(side){
        sidesOut[side] = (sides[side] || 0) + by;
    });
    
    return pvc_Sides.updateSize(sidesOut);
};

// -------------

var pvc_PercentValue = pvc.PercentValue = function(pct){
    this.percent = pct;
};

pvc_PercentValue.prototype.resolve = function(total){
    return this.percent * total;
};

pvc_PercentValue.parse = function(value){
    if(value != null && value !== ''){
        switch(typeof value){
            case 'number': return value;
            case 'string':
                var match = value.match(/^(.+?)\s*(%)?$/);
                if(match){
                    var n = +match[1];
                    if(!isNaN(n)){
                        if(match[2]){
                            if(n >= 0){
                                return new pvc_PercentValue(n / 100);
                            }
                        } else {
                            return n;
                        }
                    }
                }
                break;
                
            case 'object':
                if(value instanceof pvc_PercentValue){
                    return value;
                }
                break;
        }
        
        if(pvc.debug){
            pvc.log(def.format("Invalid margins component '{0}'", [''+value]));
        }
    }
};

pvc_PercentValue.resolve = function(value, total){
    return (value instanceof pvc_PercentValue) ? value.resolve(total) : value;
};

/* Z-Order */

// Backup original methods
var pvc_markRenderCore = pv_Mark.prototype.renderCore,
    pvc_markZOrder = pv_Mark.prototype.zOrder;

pv_Mark.prototype.zOrder = function(zOrder) {
    var borderPanel = this.borderPanel;
    if(borderPanel && borderPanel !== this){
        return pvc_markZOrder.call(borderPanel, zOrder);
    }
    
    return pvc_markZOrder.call(this, zOrder);
};

/* Render id */
pv_Mark.prototype.renderCore = function() {
    /* Assign a new render id to the root mark */
    var root = this.root;
    
    root._renderId = (root._renderId || 0) + 1;
    
    if(pvc.debug >= 25) { pvc.log("BEGIN RENDER " + root._renderId); }
    
    /* Render */
    pvc_markRenderCore.call(this);
    
    if(pvc.debug >= 25) { pvc.log("END RENDER " + root._renderId); }
};

pv_Mark.prototype.renderId = function() { return this.root._renderId; };

/* PROPERTIES */
pv_Mark.prototype.wrapper = function(wrapper) {
    this._wrapper = wrapper;
    return this;
};

pv_Mark.prototype.wrap = function(f, m) {
    if(f && def.fun.is(f) && this._wrapper && !f._cccWrapped) {
        f = this._wrapper(f, m);

        f._cccWrapped = true;
    }

    return f;
};

pv_Mark.prototype.lock = function(prop, value){
    if(value !== undefined) {
        this[prop](value);
    }

    (this._locked || (this._locked = {}))[prop] = true;

    return this;
};

pv_Mark.prototype.isIntercepted = function(prop) {
    return this._intercepted && this._intercepted[prop];
};

pv_Mark.prototype.isLocked = function(prop){
    return this._locked && this._locked[prop];
};

pv_Mark.prototype.ensureEvents = function(defEvs) {
    // labels and other marks don't receive events by default
    var events = this.propertyValue('events', /*inherit*/ true);
    if(!events || events === 'none') {
        this.events(defEvs || 'all');
    }
    return this;
};

/* ANCHORS */
/**
 * name = left | right | top | bottom
 */
pv_Mark.prototype.addMargin = function(name, margin) {
    if(margin !== 0) {
        var staticValue = def.nullyTo(this.propertyValue(name), 0),
            fMeasure    = pv.functor(staticValue);
        
        this[name](function() {
            return margin + fMeasure.apply(this, pvc_arraySlice.call(arguments));
        });
    }
    
    return this;
};

/**
 * margins = {
 *      all:
 *      left:
 *      right:
 *      top:
 *      bottom:
 * }
 */
pv_Mark.prototype.addMargins = function(margins) {
    var all = def.get(margins, 'all', 0);
    
    this.addMargin('left',   def.get(margins, 'left',   all));
    this.addMargin('right',  def.get(margins, 'right',  all));
    this.addMargin('top',    def.get(margins, 'top',    all));
    this.addMargin('bottom', def.get(margins, 'bottom', all));
    
    return this;
};

/* SCENE */
pv_Mark.prototype.eachInstanceWithData = function(fun, ctx) {
    this.eachInstance(function(scenes, index, t) {
        if(scenes.mark.sign && scenes[index].data) {
            fun.call(ctx, scenes, index, t);
        }
    });
};

pv_Mark.prototype.eachSceneWithDataOnRect = function(rect, fun, ctx, selectionMode) {
    var me   = this;
    var sign = me.sign;
    if(sign && !sign.selectable()) { return; } // TODO: shouldn't it be selectableByRubberband?
            
    // center, partial and total (not implemented)
    if(selectionMode == null) {
        selectionMode = me.rubberBandSelectionMode || 'partial';
    }
    
    var useCenter = (selectionMode === 'center');
    
    me.eachInstanceWithData(function(scenes, index, toScreen) {
        // Apply size reduction to tolerate user unprecise selections
        var shape = me.getShape(scenes, index, /*inset margin each side*/0.15);
        
        shape = (useCenter ? shape.center() : shape).apply(toScreen);
        
        processShape(shape, scenes[index], index);
    });
    
    function processShape(shape, instance, index) {
        if (shape.intersectsRect(rect)) {
            var cccScene = instance.data; // exists for sure (ensured by eachInstanceWithData)
            if(cccScene && cccScene.datum) { fun.call(ctx, cccScene); }
        }
    }
};

pv_Mark.prototype.eachDatumOnRect = function(rect, fun, ctx, selectionMode) {
    var me   = this;
    var sign = me.sign;
    if(sign && !sign.selectable()) { return; }
            
    // center, partial and total (not implemented)
    if(selectionMode == null) {
        selectionMode = me.rubberBandSelectionMode || 'partial';
    }
    
    var useCenter = (selectionMode === 'center');
    
    me.eachInstanceWithData(function(scenes, index, toScreen) {
        // Apply size reduction to tolerate user unprecise selections
        var shape = me.getShape(scenes, index, /*inset margin each side*/0.15);
        
        shape = (useCenter ? shape.center() : shape).apply(toScreen);
        
        processShape(shape, scenes[index], index);
    });
    
    function processShape(shape, instance, index) {
        if (shape.intersectsRect(rect)) {
            var cccScene = instance.data; // exists for sure (ensured by eachInstanceWithData)
            if(cccScene && cccScene.datum) {
                cccScene
                    .datums()
                    .each(function(datum) { if(!datum.isNull) { fun.call(ctx, datum); } });
            }
        }
    }
};

/* BOUNDS */
pv.Transform.prototype.transformHPosition = function(left){
    return this.x + (this.k * left);
};

pv.Transform.prototype.transformVPosition = function(top){
    return this.y + (this.k * top);
};

// width / height
pv.Transform.prototype.transformLength = function(length){
    return this.k * length;
};

// --------------------

var pvc_Size = def.type('pvc.Size')
.init(function(width, height){
    if(arguments.length === 1){
        if(width != null){
            this.setSize(width);
        }
    } else {
        if(width != null){
            this.width  = width;
        }
        
        if(height != null){
            this.height = height;
        }
    }
})
.add({
    stringify: function(out, remLevels, keyArgs){
        return pvc.stringifyRecursive(out, def.copyOwn(this), remLevels, keyArgs);
    },
    
    setSize: function(size, keyArgs){
        if(typeof size === 'string'){
            var comps = size.split(/\s+/).map(function(comp){
                return pvc_PercentValue.parse(comp);
            });
            
            switch(comps.length){
                case 1: 
                    this.set(def.get(keyArgs, 'singleProp', 'all'), comps[0]);
                    return this;
                    
                case 2:
                    this.set('width',  comps[0]);
                    this.set('height', comps[1]);
                    return this;
                    
                case 0:
                    return this;
            }
        } else if(typeof size === 'number') {
            this.set(def.get(keyArgs, 'singleProp', 'all'), size);
            return this;
        } else if (typeof size === 'object') {
            if(size instanceof pvc_PercentValue){
                this.set(def.get(keyArgs, 'singleProp', 'all'), size);
            } else {
                
                this.set('all', size.all);
                for(var p in size){
                    if(p !== 'all'){
                        this.set(p, size[p]);
                    }
                }
            }
            return this;
        }
        
        if(pvc.debug) {
            pvc.log("Invalid 'size' value: " + pvc.stringify(size));
        }
        
        return this;
    },
    
    set: function(prop, value){
        if(value != null && (prop === 'all' || def.hasOwn(pvc_Size.namesSet, prop))){
            value = pvc_PercentValue.parse(value);
            if(value != null){
                if(prop === 'all'){
                    // expand
                    pvc_Size.names.forEach(function(p){
                        this[p] = value;
                    }, this);
                    
                } else {
                    this[prop] = value;
                }
            }
        }
        
        return this;
    },
    
    clone: function(){
        return new pvc_Size(this.width, this.height);
    },
    
    intersect: function(size){
        return new pvc_Size(
               Math.min(this.width,  size.width), 
               Math.min(this.height, size.height));
    },
    
    resolve: function(refSize){
        var size = {};
        
        pvc_Size.names.forEach(function(length){
            var lengthValue = this[length];
            if(lengthValue != null){
                if(typeof(lengthValue) === 'number'){
                    size[length] = lengthValue;
                } else if(refSize){
                    var refLength = refSize[length];
                    if(refLength != null){
                        size[length] = lengthValue.resolve(refLength);
                    }
                }
            }
        }, this);
        
        return size;
    }
});

pvc_Size.names = ['width', 'height'];
pvc_Size.namesSet = pv.dict(pvc_Size.names, def.retTrue);

pvc_Size.toOrtho = function(value, anchor){
    if(value != null){
        // Single size (a number or a string with only one number)
        // should be interpreted as meaning the orthogonal length.
        var a_ol;
        if(anchor){
            a_ol = pvc.BasePanel.orthogonalLength[anchor];
        }
        
        value = pvc_Size.to(value, {singleProp: a_ol});
        
        if(anchor){
            delete value[pvc.BasePanel.oppositeLength[a_ol]];
        }
    }
    
    return value;
};

pvc_Size.to = function(v, keyArgs){
    if(v != null && !(v instanceof pvc_Size)){
        v = new pvc_Size().setSize(v, keyArgs);
    }
    
    return v;
};

// --------------------

var pvc_Offset = 
def
.type('pvc.Offset')
.init(function(x, y){
    if(arguments.length === 1){
        if(x != null){
            this.setOffset(x);
        }
    } else {
        if(x != null){
            this.x = x;
        }
        
        if(y != null){
            this.y = y;
        }
    }
})
.add({
    stringify: function(out, remLevels, keyArgs){
        return pvc.stringifyRecursive(out, def.copyOwn(this), remLevels, keyArgs);
    },
    
    setOffset: function(offset, keyArgs){
        if(typeof offset === 'string'){
            var comps = offset.split(/\s+/).map(function(comp){
                return pvc_PercentValue.parse(comp);
            });
            
            switch(comps.length){
                case 1: 
                    this.set(def.get(keyArgs, 'singleProp', 'all'), comps[0]);
                    return this;
                    
                case 2:
                    this.set('x', comps[0]);
                    this.set('y', comps[1]);
                    return this;
                    
                case 0:
                    return this;
            }
        } else if(typeof offset === 'number') {
            this.set(def.get(keyArgs, 'singleProp', 'all'), offset);
            return this;
        } else if (typeof offset === 'object') {
            this.set('all', offset.all);
            for(var p in offset){
                if(p !== 'all'){
                    this.set(p, offset[p]);
                }
            }
            return this;
        }
        
        if(pvc.debug) {
            pvc.log("Invalid 'offset' value: " + pvc.stringify(offset));
        }
        return this;
    },
    
    set: function(prop, value){
        if(value != null && def.hasOwn(pvc_Offset.namesSet, prop)){
            value = pvc_PercentValue.parse(value);
            if(value != null){
                if(prop === 'all'){
                    // expand
                    pvc_Offset.names.forEach(function(p){
                        this[p] = value;
                    }, this);
                    
                } else {
                    this[prop] = value;
                }
            }
        }
    },
    
    resolve: function(refSize){
        var offset = {};
        
        pvc_Size.names.forEach(function(length){
            var offsetProp  = pvc_Offset.namesSizeToOffset[length];
            var offsetValue = this[offsetProp];
            if(offsetValue != null){
                if(typeof(offsetValue) === 'number'){
                    offset[offsetProp] = offsetValue;
                } else if(refSize){
                    var refLength = refSize[length];
                    if(refLength != null){
                        offset[offsetProp] = offsetValue.resolve(refLength);
                    }
                }
            }
        }, this);
        
        return offset;
    }
});

pvc_Offset
.addStatic({ names: ['x', 'y'] })
.addStatic({ 
    namesSet: pv.dict(pvc_Offset.names, def.retTrue),
    namesSizeToOffset: {width: 'x', height: 'y'},
    namesSidesToOffset: {left: 'x', right: 'x', top: 'y', bottom: 'y'},
    as: function(v) {
        if(v != null && !(v instanceof pvc_Offset)) {
            v = new pvc_Offset().setOffset(v);
        }
        
        return v;
    }
});

/**
 * Implements support for svg detection
 */
(function($) {
    /*global document:true */
    jQuery.support.svg = jQuery.support.svg || 
        document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1");
}(/*global jQuery:true */jQuery));


pvc.text = {
    getFitInfo: function(w, h, text, font, diagMargin){
        if(text === '') {
            return {h: true, v: true, d: true};
        }
        
        var len = pv.Text.measure(text, font).width;
        return {
            h: len <= w,
            v: len <= h,
            d: len <= Math.sqrt(w*w + h*h) - diagMargin
        };
    },

    trimToWidthB: function(len, text, font, trimTerminator, before){
        len -= pv.Text.measure(trimTerminator, font).width;
        
        return pvc.text.trimToWidth(len, text, font, trimTerminator, before);
    },
    
    trimToWidth: function(len, text, font, trimTerminator, before){
        if(text === '') {
            return text;
        }
  
        var textLen = pv.Text.measure(text, font).width;
        if(textLen <= len){
            return text;
        }
    
        if(textLen > len * 1.5){ //cutoff for using other algorithm
            return pvc.text.trimToWidthBin(len, text, font, trimTerminator, before);
        }
    
        while(textLen > len){
            text = before ? text.slice(1) : text.slice(0, text.length -1);
            textLen = pv.Text.measure(text, font).width;
        }
    
        return before ? (trimTerminator + text) : (text + trimTerminator);
    },
    
    trimToWidthBin: function(len, text, font, trimTerminator, before){

        var ilen = text.length,
            high = ilen - 2,
            low = 0,
            mid,
            textLen;

        while(low <= high && high > 0){

            mid = Math.ceil((low + high)/2);
            
            var textMid = before ? text.slice(ilen - mid) : text.slice(0, mid);
            textLen = pv.Text.measure(textMid, font).width;
            if(textLen > len){
                high = mid - 1;
            } else if(pv.Text.measure(before ? text.slice(ilen - mid - 1) : text.slice(0, mid + 1), font).width < len){
                low = mid + 1;
            } else {
                return before ? (trimTerminator + textMid) : (textMid + trimTerminator);
            }
    }
    
        return before ? (trimTerminator + text.slice(ilen - high)) : (text.slice(0, high) + trimTerminator);
    },
    
    justify: function(text, lineWidth, font){
        var lines = [];
        
        if(lineWidth < pv.Text.measure('a', font).width){
            // Not even one letter fits...
            return lines;
        } 
        
        var words = (text || '').split(/\s+/);
        
        var line = "";
        while(words.length){
            var word = words.shift();
            if(word){
                var nextLine = line ? (line + " " + word) : word;
                if(pv.Text.measure(nextLine, font).width > lineWidth){
                    // The word by itself may overflow the line width
                    
                    // Start new line
                    if(line){
                        lines.push(line);
                    }
                    
                    line = word;
                } else {
                    line = nextLine; 
                }
            }
        }
        
        if(line){
            lines.push(line);
        }
        
        return lines;
    },
    
    /* Returns a label's BBox relative to its anchor point */
    getLabelBBox: function(textWidth, textHeight, align, baseline, angle, margin){
            
        var polygon = pv.Label.getPolygon(textWidth, textHeight, align, baseline, angle, margin);
        
        var bbox             = polygon.bbox();
        bbox.source          = polygon;
        bbox.sourceAngle     = angle;
        bbox.sourceAlign     = align;
        bbox.sourceTextWidth = textWidth;
        
        return bbox;
    }
};


// Colors utility

pvc.color = {
    scale:  pvc_colorScale,
    scales: pvc_colorScales,
    toGray: pvc.toGrayScale,
    isGray: pvc_colorIsGray
};

// --------------------------
// exported

function pvc_colorIsGray(color){
    color = pv.color(color);
    var r = color.r;
    var g = color.g;
    var b = color.b;
    var avg = (r + g + b) / 3;
    var tol = 2;
    return Math.abs(r - avg) <= tol &&
           Math.abs(g - avg) <= tol &&
           Math.abs(b - avg) <= tol;
}

/**
 * Creates color scales of a specified type for datums grouped by a category.
 * 
 * @name pvc.color.scales
 * @function
 * @param {object} keyArgs Keyword arguments.
 * See {@link pvc.color.scale} for available arguments.
 * 
 * @param {def.Query} keyArgs.data
 * A {@link pvc.data.Data} that is the result of grouping datums along what are here called "category" dimensions.
 * <p>
 * One (possibly equal) color scale is returned per leaf data, indexed by the leaf's absolute key (see {@link pvc.data.Data#absKey}).  
 * </p>
 * @param {boolean} [keyArgs.normPerBaseCategory=false] Indicates that a different color scale should be computed per distinct data category.
 * 
 * @type function 
 */
function pvc_colorScales(keyArgs){
    /*jshint expr:true */
    keyArgs || def.fail.argumentRequired('keyArgs');
    
    var type = keyArgs.type || def.fail.argumentRequired('keyArgs.type');
    
    switch (type) {
        case 'linear':   return new pvc.color.LinearScalesBuild(keyArgs).buildMap();
        case 'discrete': return new pvc.color.DiscreteScalesBuild(keyArgs).buildMap();
        case 'normal':   return new pvc.color.NormalScalesBuild(keyArgs).buildMap(); // TODO
    }
    
    throw def.error.argumentInvalid('scaleType', "Unexistent scale type '{0}'.", [type]);
}

/**
 * Creates a color scale of a specified type.
 * 
 * @name pvc.color.scale
 * @function
 * @param {object} keyArgs Keyword arguments.
 * See {@link pvc.color.scales} for available arguments.
 * 
 * @param {def.Query} keyArgs.data A {@link pvc.data.Data} instance that 
 * may be used to obtain the domain of the color scale.
 * 
 * @param {string} keyArgs.type The type of color scale.
 * <p>
 * Valid values are 'linear', 'discrete' and 'normal' (normal probability distribution).
 * </p>
 * @param {string|pv.color} [keyArgs.colorMin] The minimum color.
 * @param {string|pv.color} [keyArgs.colorMax] The maximum color.
 * @param {string|pv.color} [keyArgs.colorMissing] The color shown for null values.
 * @param {(string|pv.color)[]} [keyArgs.colors] Array of colors.
 * <p>
 * This argument is ignored if both minimum and maximum colors are specified.
 * Otherwise, if only one of minimum or maximum is specified, it is prepended or appended to
 * the color range array, respectively.
 * </p>
 * <p>
 * When unspecified, the color range is assumed to be 'red', 'yellow' and 'green'. 
 * </p>
 * @param {string} keyArgs.colorDimension The name of the data dimension that is the <b>domain</b> of the color scale.
 * @param {object[]} [keyArgs.colorDomain] An array of domain values to match colors in the color range.
 * 
 * @type function 
 */
function pvc_colorScale(keyArgs){
    /*jshint expr:true */
    keyArgs || def.fail.argumentRequired('keyArgs');
    
    var type = keyArgs.type || def.fail.argumentRequired('keyArgs.type');
    
    switch (type) {
        case 'linear':   return new pvc.color.LinearScalesBuild(keyArgs).build();
        case 'discrete': return new pvc.color.DiscreteScalesBuild(keyArgs).build();
        case 'normal':   return new pvc.color.NormalScalesBuild(keyArgs).build();
    }
    
    throw def.error.argumentInvalid('scaleType', "Unexistent scale type '{0}'.", [type]);
}

// --------------------------
// private

/**
 * @class Represents one creation/build of a set of scale functions.
 * @abstract
 */
def
.type('pvc.color.ScalesBuild')
.init(function(keyArgs){
    this.keyArgs        = keyArgs;
    this.data           = keyArgs.data || def.fail.argumentRequired('keyArgs.data');
    this.domainDimName  = keyArgs.colorDimension || def.fail.argumentRequired('keyArgs.colorDimension');
    this.domainDim      = this.data.dimensions(this.domainDimName);
   
    var dimType = this.domainDim.type;
    if(!dimType.isComparable) {
        this.domainComparer = null;
        pvc.log("Color value dimension should be comparable. Generated color scale may be invalid.");
    } else {
        this.domainComparer = function(a, b){ return dimType.compare(a, b); };
    }
   
    this.nullRangeValue = keyArgs.colorMissing ? pv.color(keyArgs.colorMissing) : pv.Color.transparent;
   
    this.domainRangeCountDif = 0;
})
.add(/** @lends pvc.color.ScalesBuild# */{
   /**
    * Builds one scale function.
    * 
    * @type pv.Scale
    */
    build: function(){
        this.range = this._getRange();
        this.desiredDomainCount = this.range.length + this.domainRangeCountDif;
       
        var domain = this._getDomain();
        return this._createScale(domain);
    },
   
    /**
     * Builds a map from category keys to scale functions.
     * 
     * @type object
     */
    buildMap: function(){
        this.range = this._getRange();
        this.desiredDomainCount = this.range.length + this.domainRangeCountDif;
        
        var createCategoryScale;
        
        /* Compute a scale-function per data category? */
        if(this.keyArgs.normPerBaseCategory){
            /* Ignore args' domain and calculate from data of each category */
            createCategoryScale = function(leafData){
                // Create a domain from leafData
                var domain = this._ensureDomain(null, false, leafData);
                return this._createScale(domain);
            };
        } else {
            var domain = this._getDomain(),
                scale  = this._createScale(domain);
           
            createCategoryScale = def.fun.constant(scale);
        }
       
        return this._createCategoryScalesMap(createCategoryScale); 
    },
   
    _createScale: def.method({isAbstract: true}),
   
    _createCategoryScalesMap: function(createCategoryScale){
        return this.data.children()
            .object({
                name:    function(leafData){ return leafData.absKey; },
                value:   createCategoryScale,
                context: this
            });
    },
   
    _getRange: function(){
        var keyArgs = this.keyArgs,
            range = keyArgs.colors || ['red', 'yellow', 'green'];
   
        if(keyArgs.colorMin != null && keyArgs.colorMax != null){
           
            range = [keyArgs.colorMin, keyArgs.colorMax];
           
        } else if (keyArgs.colorMin != null){
           
            range.unshift(keyArgs.colorMin);
           
        } else if (keyArgs.colorMax != null){
           
            range.push(keyArgs.colorMax);
        }
   
        return range.map(function(c) { return pv.color(c); });
    },
   
    _getDataExtent: function(data){
       
        var extent = data.dimensions(this.domainDimName).extent({visible: true});
        if(!extent) { // No atoms...
            return null;
        }
       
        var min = extent.min.value,
            max = extent.max.value;
        
        if(max == min){
            if(max >= 1){
                min = max - 1;
            } else {
                max = min + 1;
            }
        }
       
        return {min: min, max: max};
    },
   
    _getDomain: function() {
        var domain = this.keyArgs.colorDomain;
        if(domain != null){
            if(this.domainComparer) {
                domain.sort(this.domainComparer);
            }
           
            if(domain.length > this.desiredDomainCount){ 
                // More domain points than needed for supplied range
                domain = domain.slice(0, this.desiredDomainCount);
            }
        } else {
            // This ends up being padded...in ensureDomain
            domain = [];
        }
       
        return this._ensureDomain(domain, true, this.data);
    },
   
    _ensureDomain: function(domain, doDomainPadding, data) {
        var extent;
       
        if(domain && doDomainPadding){
            /* 
             * If domain does not have as many values as there are colors (taking domainRangeCountDif into account),
             * it is *completed* with the extent calculated from data.
             * (NOTE: getArgsDomain already truncates the domain to number of colors)
             */
            var domainPointsMissing = this.desiredDomainCount - domain.length;
            if(domainPointsMissing > 0){ 
                extent = this._getDataExtent(data);
                if(extent){
                    // Assume domain is sorted
                    switch(domainPointsMissing){  // + 1 in discrete ?????
                        case 1:
                            if(this.domainComparer) {
                                def.array.insert(domain, extent.max, this.domainComparer);
                            } else {
                                domain.push(extent.max);
                            }
                            break;

                        case 2:
                            if(this.domainComparer) {
                                def.array.insert(domain, extent.min, this.domainComparer);
                                def.array.insert(domain, extent.max, this.domainComparer);
                            } else {
                                domain.unshift(extent.min);
                                domain.push(extent.max);
                            }
                            break;

                        default:
                            /* Ignore args domain altogether */
                            if(pvc.debug >= 2){
                                    pvc.log("Ignoring option 'colorDomain' due to unsupported length." +
                                            def.format(" Should have '{0}', but instead has '{1}'.", [this.desiredDomainCount, domain.length]));
                            }
                            domain = null;
                    }
                }
           }
       }
       
       if(!domain) {
           /*jshint expr:true */
               extent || (extent = this._getDataExtent(data));
               if(extent){
                   var min = extent.min,
                       max = extent.max;
                   var step = (max - min) / (this.desiredDomainCount - 1);
                   domain = pv.range(min, max + step, step);
               }
           }
           
           return domain;
       }
   });
        
    
def
.type('pvc.color.LinearScalesBuild', pvc.color.ScalesBuild)
.add(/** @lends pvc.color.LinearScalesBuild# */{
    
    _createScale: function(domain){
        var scale = pv.Scale.linear();

        if(domain){
            scale.domain.apply(scale, domain);
        }
        
        scale.range.apply(scale, this.range);
        
        return scale;
    }
});

def
.type('pvc.color.DiscreteScalesBuild', pvc.color.ScalesBuild)
.init(function(keyArgs){
    this.base(keyArgs);
    
    this.domainRangeCountDif = 1;
})
.add(/** @lends pvc.color.DiscreteScalesBuild# */{
    
    /*
     * Dmin   DMax    C
     * --------------------
     * -      <=d0    c0
     * >d0    <=d1    c1
     * >d1    <=d2    c2
     * ..
     * >dN-3  <=dN-2  cN-2
     * 
     * >dN-2  -       cN-1
     */
    //d0--cR0--d1--cR1--d2
    _createScale: function(domain){
        var Dl = domain.length - 1,
            range = this.range,
            nullRangeValue = this.nullRangeValue,
            Rl = range.length - 1;
        
        function scale(val){
            if(val == null) {
                return nullRangeValue;
            }
            
            for(var i = 0 ; i < Dl ; i++){  // i <= D - 2  => domain[D-1]
                if(val <= domain[i + 1]){
                    return range[i];
                }
            }
            
            // > domain[Dl]
            return range[Rl];
        }
        
        // TODO: Not a real scale; 
        // some methods won't work on the result of by, by1 and transform.
        // Give it a bit of protovis looks
        def.copy(scale, pv.Scale.common);
        
        scale.domain = function(){ return domain; };
        scale.range  = function(){ return range;  };
        
        return scale;
    }
});

/* TODO */ 
  
/***********
 * compute an array of fill-functions. Each column out of "colAbsValues" 
 * gets it's own scale function assigned to compute the color
 * for a value. Currently supported scales are:
 *    -  linear (from min to max
 *    -  normal distributed from   -numSD*sd to  numSD*sd 
 *         (where sd is the standard deviation)
 ********/
/*
     getNormalColorScale: function (data, colAbsValues, origData){
    var fillColorScaleByColKey;
    var options = this.chart.options;
    if (options.normPerBaseCategory) {
      // compute the mean and standard-deviation for each column
      var myself = this;
      
      var mean = pv.dict(colAbsValues, function(f){
        return pv.mean(data, function(d){
          return myself.getValue(d[f]);
        })
      });
      
      var sd = pv.dict(colAbsValues, function(f){
        return pv.deviation(data, function(d){
          myself.getValue(d[f]);
        })
      });
      
      //  compute a scale-function for each column (each key)
      fillColorScaleByColKey = pv.dict(colAbsValues, function(f){
        return pv.Scale.linear()
          .domain(-options.numSD * sd[f] + mean[f],
                  options.numSD * sd[f] + mean[f])
          .range(options.colorMin, options.colorMax);
      });
      
    } else {   // normalize over the whole array
      
      var mean = 0.0, sd = 0.0, count = 0;
      for (var i=0; i<origData.length; i++)
        for(var j=0; j<origData[i].length; j++)
          if (origData[i][j] != null){
            mean += origData[i][j];
            count++;
          }
      mean /= count;
      for (var i=0; i<origData.length; i++){
        for(var j=0; j<origData[i].length; j++){
          if (origData[i][j] != null){
            var variance = origData[i][j] - mean;
            sd += variance*variance;
          }
        }
      }
      
      sd /= count;
      sd = Math.sqrt(sd);
      
      var scale = pv.Scale.linear()
        .domain(-options.numSD * sd + mean,
                options.numSD * sd + mean)
        .range(options.colorMin, options.colorMax);
      
      fillColorScaleByColKey = pv.dict(colAbsValues, function(f){
        return scale;
      });
    }

    return fillColorScaleByColKey;  // run an array of values to compute the colors per column
}      
     */

/* 
 *          r0   ]   r1 ]    r2   ]           rD-2  ] (rD-1)
 * ... --+-------+------+---------+-- ... -+--------+------->
 *       d0      d1     d2        d3       dD-2    dD-1   (linear)
 * 
 * 
 * Mode 1 - Domain divider points
 * 
 * User specifies:
 * # D domain divider points
 * # R = D+1 range points
 * 
 * ////////////////////////////
 * D=0, R=1
 *
 *   r0
 *   ...
 *
 *
 * ////////////////////////////
 * D=1, R=2
 *
 *   r0  ]  r1
 * ... --+-- ...
 *       d0
 *
 *
 * ////////////////////////////
 * D=2, R=3
 *
 *   r0  ]  r1  ]  r2
 * ... --+------+-- ...
 *       d0     d1
 *
 *
 * ////////////////////////////
 * D=3, R=4
 * 
 *   r0  ]  r1  ]  r2  ]  r3
 * ... --+------+------+-- ...
 *       d0     d1     d2
 * 
 * ...
 * 
 * Mode 2 - Domain dividers determination from data extent
 * 
 * //////////////////////////// (inf. = sup.)
 * Special case
 * Only one color is used (the first one, for example)
 * 
 *   r0
 *   
 * //////////////////////////// (inf. < sup.)
 * C=1  => constant color
 * 
 *       r0
 *   +--------+
 *   I        S
 * 
 * ////////////////////////////
 * C=2  =>  N=1 (1 divider point)
 * 
 * B = (S-I)/2
 * 
 *       C0   ]   C1
 *   +--------+--------+
 *   I        d0        S
 *       B         B
 * 
 * ////////////////////////////
 * C=3  =>  N=2 (2 divider points)
 * 
 * B = (S-I)/3
 * 
 *      C0    ]   C1   ]   C2
 *   +--------+--------+--------+
 *   I        d0       d1       S
 *       B        B        B
 *
 * ...
 * 
 */

def.space('pvc.trends', function(trends){
    var _trends = {};
    
    def.set(trends, 
        'define', function(type, trendSpec){
            /*jshint expr:true*/
            
            type      || def.fail.argumentRequired('type');
            trendSpec || def.fail.argumentRequired('trendSpec');
            def.object.is(trendSpec) || def.fail.argumentInvalid('trendSpec', "Must be a trend specification object.");
            
            type = (''+type).toLowerCase();
            
            if(pvc.debug >= 2 && def.hasOwn(_trends, type)){
                pvc.log(def.format("[WARNING] A trend type with the name '{0}' is already defined.", [type]));
            }
            
            var label = trendSpec.label || def.fail.argumentRequired('trendSpec.label');
            var model = trendSpec.model || def.fail.argumentRequired('trendSpec.model');
            def.fun.is(model) || def.fail.argumentInvalid('trendSpec.mode', "Must be a function.");
            
            var trendInfo = {
               dataPartAtom: {v: 'trend', f: label},
               type:  type,
               label: label,
               model: model
            };
            
            _trends[type] = trendInfo;
        },
        
        'get', function(type){
            /*jshint expr:true*/
            type || def.fail.argumentRequired('type');
            return def.getOwn(_trends, type) ||
                def.fail.operationInvalid("Undefined trend type '{0}'.", [type]);
        },
        
        'has', function(type){
            return def.hasOwn(_trends, type);
        },
        
        'types', function(){
            return def.ownKeys(_trends);
        });
    
    
    trends.define('linear', {
        label: 'Linear trend',
        model: function(options){
            var rows = def.get(options, 'rows'); 
            var funX = def.get(options, 'x');
            var funY = def.get(options, 'y');
            
            var i = 0;
            var N = 0;
            var sumX  = 0;
            var sumY  = 0;
            var sumXY = 0;
            var sumXX = 0;
            var parseNum = function(value){
                return value != null ? (+value) : NaN;  // to Number works for dates as well
            };
            
            while(rows.next()){
                var row = rows.item;
                
                // Ignore null && NaN values
                
                var x = funX ? parseNum(funX(row)) : i; // use the index itself for discrete stuff
                if(!isNaN(x)){
                    var y = parseNum(funY(row));
                    if(!isNaN(y)){
                        N++;
                        
                        sumX  += x;
                        sumY  += y;
                        sumXY += x * y;
                        sumXX += x * x;
                    }
                }
                
                i++; // Discrete nulls must still increment the index
            }
            
            // y = alpha + beta * x
            var alpha, beta;
            if(N >= 2){
                var avgX  = sumX  / N;
                var avgY  = sumY  / N;
                var avgXY = sumXY / N;
                var avgXX = sumXX / N;
            
                // When N === 1 => den = 0
                var den = (avgXX - avgX * avgX);
                if(den === 0){
                    beta = 0;
                } else {
                    beta = (avgXY - (avgX * avgY)) / den;
                }
                
                alpha = avgY - beta * avgX;
                
                return {
                    alpha: alpha,
                    beta:  beta,
                    
                    reset: def.noop,
                    
                    // y = alpha + beta * x
                    sample: function(x/*, y, i*/){
                        return alpha + beta * (+x);
                    }
                };
            }
        }
    });
    
    // Source: http://en.wikipedia.org/wiki/Moving_average
    trends.define('moving-average', {
        label: 'Moving average',
        model: function(options){
            var W = Math.max(+(def.get(options, 'periods') || 3), 2);
              
            var sum = 0; // Current sum of values in avgValues
            var avgValues = []; // Values in the average window
            
            return {
                reset: function(){
                    sum = 0;
                    avgValues.length = 0;
                },
                
                sample: function(x, y, i){
                    // Only y is relevant for this trend type
                    var L = W;
                    if(y != null){
                        avgValues.unshift(y);
                        sum += y;
                        
                        L = avgValues.length;
                        if(L > W){
                            sum -= avgValues.pop();
                            L = W;
                        }
                    }
                    
                    return sum / L;
                }
            };
        }
    });
    
    // Source: http://en.wikipedia.org/wiki/Moving_average
    trends.define('weighted-moving-average', {
        label: 'Weighted Moving average',
        model: function(options){
            var W = Math.max(+(def.get(options, 'periods') || 3), 2);
            
            // Current sum of values in the window
            var sum = 0; // Current sum of values in avgValues
            
            // Current numerator
            var numer = 0;
            
            var avgValues = []; // Values in the average window
            var L = 0;
            
            // Constant Denominator (from L = W onward it is constant)
            // W +  (W - 1) + ... + 2 + 1
            // = W * (W + 1) / 2;
            var denom = 0;
            
            return {
                reset: function(){
                    sum = numer = denom = L = 0;
                    avgValues.length = 0;
                },
                
                sample: function(x, y/*, i*/){
                    // Only y is relevant for this trend type
                    if(y != null){
                        
                        if(L < W){
                            // Still filling the avgValues array
                            
                            avgValues.push(y);
                            L++;
                            denom += L;
                            numer += L * y;
                            sum   += y;
                        } else {
                            // denom is now equal to: W * (W + 1) / 2; 
                            numer += (L * y) - sum;
                            sum   += y - avgValues[0]; // newest - oldest
                            
                            // Shift avgValues left
                            for(var j = 1 ; j < W ; j++){
                                avgValues[j - 1] = avgValues[j];
                            }
                            avgValues[W - 1] = y;
                        }
                    }
                    
                    return numer / denom;
                }
            };
        }
    });
});

// Options management utility

/**
 * Creates an options manager given an options specification object,
 * and, optionally, a corresponding context object.
 * 
 * @name pvc.options
 * @class An options manager.
 * @example
 * <pre>
 * var foo = {};
 * 
 * foo.options = pvc.options({
 *         Name: {
 *             alias: 'legendName',
 *             cast:  String,
 *             value: 'John Doe',
 *             resolve: function(context){
 *                 this.setDefault();
 *             }
 *         }
 *     }, foo);
 *     
 * foo.options.specify({
 *    'legendName': "Fritz"
 * });
 * 
 * foo.options('Name2'); // -> "Fritz"
 * </pre>
 * 
 * @constructor
 * @param {object} specs An object whose properties, owned or inherited,
 * have the name of an option to define, and whose values are option
 * specification objects, each having the following <i>optional</i> properties:
 * <ul>
 * <li>resolve - 
 * a method that allows to apply custom value resolution logic for an option.
 * 
 * It is called 
 * on the {@link pvc.options.Info} instance with the 
 * previously specified context object as argument. 
 * </li>
 * <li>cast  - a cast function, called to normalize the value of an option</li>
 * <li>value - the default value of the property, considered already cast</li>
 * </li>
 * </ul>
 * 
 * @param {object} [context=null] Optional context object on which to call
 * the 'resolve' function specified in {@link specs}.
 * 
 * @type function
 */
function pvc_options(specs, context) {
    /*jshint expr:true */
    specs || def.fail.argumentRequired('specs');
    
    var _infos = {};
    
    def.each(specs, function(spec, name) {
        var info = new pvc_OptionInfo(name, option, context, spec);
        _infos[info.name] = info;
    });
    
    /** @private */
    function resolve(name) {
        var info = def.getOwn(_infos, name) || 
                   def.fail.operationInvalid("Undefined option '{0}'", [name]);
        
        return info.resolve();
    }
    
    /**
     * Obtains the value of an option given its name.
     * <p>
     * If a value for the option hasn't been provided
     * a default value is returned, 
     * from the option specification.
     * </p>
     * @name pvc.options#option
     * @function
     * @param {string} name The name of the option.
     * @param {booleam} [noDefault=false] Prevents returning a default value.
     * If a value for the option hasn't been provided, undefined is returned.
     * 
     *  @type any
     */
    function option(name, noDefault) {
        var info = resolve(name);
        return noDefault && !info.isSpecified ? undefined : info.value;
    }
    
    /**
     * Indicates if a value for a given option has been specified.
     * @name pvc.options#isSpecified
     * @function
     * @param {string} name The name of the option.
     * @type boolean
     */
    function isSpecified(name) { return resolve(name).isSpecified; }
    
    /**
     * Obtains the value of an option given its name,
     * but only if it has been specified (not defaulted).
     * <p>
     * This is a convenience method for calling {@link #option}
     * with the <tt>noDefault</tt> argument with the value <tt>true</tt>.
     * </p>
     * 
     * @name pvc.options#specified
     * @function
     * @param {string} name The name of the option.
     * 
     * @type any
     */
    function specified(name) { return option(name, /*noDefault*/ true); }
    
    /**
     * Indicates if an option with the given name is defined.
     * @name pvc.options#isDefined
     * @function
     * @param {string} name The name of the option.
     * @type boolean
     */
    function isDefined(name) { return def.hasOwn(_infos, name); }
    
    /**
     * Specifies options' values given an object
     * with properties as option names
     * and values as option values.
     * <p>
     * Only properties whose name is the name of a defined option 
     * are taken into account.
     * </p>
     * <p>
     * Every property, own or inherited, is considered, 
     * as long as its value is not <c>undefined</c>.
     * </p>
     * @name pvc.options#specify
     * @function
     * @param {object} [opts] An object with option values
     * @returns {function} The options manager. 
     */
    function specify(opts) { return set(opts, false); }
    
    /**
     * Sets options' default values.
     * @name pvc.options#defaults
     * @function
     * @param {object} [opts] An object with option default values
     * @returns {function} The options manager.
     * @see #specify
     */
    function defaults(opts) { return set(opts, true); }
    
    /**
     * Obtains the default value of an option, given its name.
     * <p>
     * If a property has no default value, <c>undefined</c> is returned.
     * </p>
     * @name pvc.options#defaultValue
     * @function
     * @param {string} name The name of the option.
     */
    function getDefaultValue(name) { return resolve(name)._defaultValue; }
    
    /** @private */
    function set(opts, isDefault) {
        for(var name in opts) {
            var info = def.getOwn(_infos, name);
            if(info) {
                var value = opts[name];
                if(value !== undefined) { info.set(value, isDefault); }
            }
        }
        
        return option;
    }
    
    // ------------
    
    option.option = option;
    option.specified   = specified; 
    option.isSpecified = isSpecified;
    option.isDefined   = isDefined;
    
    option.defaultValue = getDefaultValue;
    
    option.specify  = specify;
    option.defaults = defaults;
    
    return option;
}

// ------------
 
// Creates a resolve method, 
// that combines a list of resolvers. 
// The resolve stops when the first resolver returns the value <c>true</c>,
// returning <c>true</c> as well.
function options_resolvers(list) {
    return function(optionInfo) {
        for(var i = 0, L = list.length ; i < L ; i++) {
            var m = list[i];
            
            if(def.string.is(m)) { m = this[m]; } 
            
            if(m.call(this, optionInfo) === true) { return true; }
        }
    };
}

function options_constantResolver(value, op) {
    return function(optionInfo) {
        optionInfo.specify(value);
        return true;
    };
}

function options_specifyResolver(fun, op) {
    return function(optionInfo) {
        var value = fun.call(this, optionInfo);
        if(value !== undefined) {
            optionInfo.specify(value);
            return true;
        }
    };
}

function options_defaultResolver(fun) {
    return function(optionInfo) {
        var value = fun.call(this, optionInfo);
        if(value !== undefined) {
            optionInfo.defaultValue(value);
            return true;
        }
    };
}

pvc_options.resolvers    = options_resolvers;
pvc_options.constant     = options_constantResolver;
pvc_options.specify      = options_specifyResolver;
pvc_options.defaultValue = options_defaultResolver;

// ------------

pvc.options = pvc_options;

// ------------

/**
 * @name pvc.options.OptionInfo
 * @class An option in an options manager.
 * @private
 */
var pvc_OptionInfo = 
def
.type() // Anonymous type
.init(function(name, option, context, spec){
    this.name = name;
    this._context = context;
    this.option = option;
    
    this._cast = def.get(spec, 'cast');
    
    // Assumed already cast
    // May be undefined
    var value = def.get(spec, 'value');
    if(value !== undefined) { this._defaultValue = this.value = value; }
    
    var resolve = def.get(spec, 'resolve'); // function or string
    if(resolve) { this._resolve = resolve; } 
    else        { this.isResolved = true; }

    var getDefault = def.get(spec, 'getDefault'); // function or string
    if(getDefault) { this._getDefault = getDefault; }
    
    var data = def.get(spec, 'data');
    if(data != null) { this.data = data; }
    
    // --------
    // Can be used by resolvers...
    this.alias = def.array.to(def.get(spec, 'alias'));
})
.add( /** @lends pvc.options.OptionInfo#  */{
    isSpecified: false,
    isResolved:  false,
    value:   undefined,
    
    /** @private */
    _defaultValue: undefined,
    
    /**
     * Resolves an option if it is not yet resolved.
     * @type pvc.options.Info
     */
    resolve: function() {
        if(!this.isResolved) {
            // In case of re-entry, the initial default value is obtained.
            this.isResolved = true;
            
            // Must call 'set', 'specify' or 'defaultValue'
            // Otherwise, the current default value becomes _the_ value.
            this._getFunProp('_resolve').call(this._context, this);
            
            // Handle the case where none of the above referred methods is called.
            if(this.value == null) {
                var value = this._dynDefault();
                if(value != null) {
                    delete this.isSpecified;
                    this.value = this._defaultValue = value;
                }
            }
        }
        
        return this;
    },
    
    /**
     * Specifies the value of the option.
     * 
     * @param {any} value the option value.
     * @type pvc.options.Info
     */
    specify: function(value) { return this.set(value, false); },
    
    /**
     * Gets, and optionally sets, the default value.
     * @param {any} [value=undefined] the option default value.
     * @type any
     */
    defaultValue: function(defaultValue) {
        if(defaultValue !== undefined) { this.set(defaultValue, true); }
        
        return this._defaultValue;
    },
    
    cast: function(value) {
        if(value != null) {
            var cast = this._getFunProp('_cast');
            if(cast) { value = cast.call(this._context, value, this); }
        }
        return value;
    },
    
    /**
     * Sets the option's value or default value.
     * 
     * @param {any} [value=undefined] the option value or default value.
     * @param {boolean} [isDefault=false] indicates if the operation sets the default value.
     * 
     * @type pvc.options.Info
     */
    set: function(value, isDefault) {
        if(value != null) { value = this.cast(value); }
        
        if(value == null) {
            value = this._dynDefault();
            if(value != null) { isDefault = true; }
        }
        
        if(!isDefault) {
            this.isSpecified = true;
            this.isResolved  = true;
            this.value = value;
        } else {
            delete this.isSpecified; // J.I.C. 'defaultValue' is called after a 'specify'
            
            this._defaultValue = value;
            
            // Don't touch an already specified value
            if(!this.isSpecified) { this.value = value; }
        }
        
        return this;
    },
    
    _dynDefault: function() {
        var get = this._getFunProp('_getDefault');
        return get && this.cast(get.call(this._context, this));
    },

    _getFunProp: function(name) {
        var fun = this[name];
        if(fun) {
            var context = this._context;
            if(context && def.string.is(fun)) { fun = context[fun]; }
        }
        return fun;
    }
});

/**
 * Namespace with data related classes.
 * @name pvc.data
 * @namespace
 */

/**
 * @name NoDataException
 * @class An error thrown when a chart has no data.
 */
def.global.NoDataException = function(){};


pvc.data = {
    visibleKeyArgs: {visible: true}
};

/**
 * Disposes a list of child objects.
 * 
 * @name pvc.data._disposeChildList
 * 
 * @param {Array} list The list with children to dispose.
 * @param {string} [parentProp] The child's parent property to reset.
 * 
 * @static
 * @private
 */
function data_disposeChildList(list, parentProp) {
    if(list){
        list.forEach(function(child){
            if(parentProp) {
                child[parentProp] = null; // HACK: to avoid child removing itself from its parent (this)
            }
            
            child.dispose(); 
        });
        
        list.length = 0;
    }
}

/**
 * Adds a child object.
 * 
 * @name pvc.data._addColChild
 * 
 * @param {object} parent The parent.
 * @param {string} childrenProp A parent's children array property.
 * @param {object} child The child to add.
 * @param {string} parentProp The child's parent property to set.
 * @param {number} [index=null] The index at which to insert the child.
 * 
 * @static
 * @private
 */
function data_addColChild(parent, childrenProp, child, parentProp, index) {
    // <Debug>
    /*jshint expr:true */
    (child && !child[parentProp]) || def.assert("Must not have a '" + parentProp + "'.");
    // </Debug>
    
    child[parentProp] = parent;
    
    var col = (parent[childrenProp] || (parent[childrenProp] = []));
    if(index == null || index >= col.length){
        col.push(child);
    } else {
        col.splice(index, 0, child);
    }
}

/**
 * Removes a child object.
 * 
 * @name pvc.data._removeColChild
 * 
 * @param {object} parent The parent.
 * @param {string} childrenProp A parent's children array property.
 * @param {object} child The child to remove.
 * @param {string} parentProp The child's parent property to reset.
 * 
 * @static
 * @private
 */
function data_removeColChild(parent, childrenProp, child, parentProp) {
    // <Debug>
    /*jshint expr:true */
    (child && (!child[parentProp] || child[parentProp] === parent)) || def.assert("Not a child");
    // </Debug>
    
    var children = parent[childrenProp];
    if(children) {
        var index = children.indexOf(child);
        if(index >= 0){
            def.array.removeAt(children, index);
        }
    }
    
    child[parentProp] = null;
}

/**
 * Initializes a dimension type
 * 
 * @name pvc.data.DimensionType
 * 
 * @class A dimension type describes a dimension of a complex type.
 * <p>
 * Most of the held information is of 
 * intrinsic characteristics of the dimensions values.
 * Yet, it also holds information 
 * related to a specific data translation usage.
 * </p>
 *
 * @property {pvc.data.ComplexType} complexType
 * The complex type that this dimension type belongs to.
 * 
 * @property {string} name
 * The name of this dimension type.
 * The name of a dimension type is unique on its complex type.
 * 
 * @property {string} label
 * The label of this dimension type.
 * The label <i>should</i> be unique on its complex type.
 * 
 * @property {string} group The group that the dimension type belongs to.
 * <p>
 * The group name is taken to be the name of the dimension
 * without any suffix numbers. 
 * So, if the name of a dimension type is 'series2',
 * then its default group is 'series'.
 * </p>
 *
 * @property {number} groupLevel The index within the group that the dimension type belongs to.
 *
 * @property {Function} valueType
 * The type of the value of atoms belonging to dimensions of this type.
 * It is a function that casts values to the represented type.
 * 
 * The values null and undefined are never converted by this function.
 * 
 * The function must be idempotent.
 *
 * @property {string} valueTypeName A description of the value type.
 * 
 * @property {boolean} isDiscrete
 * Indicates if the values of this dimension are 
 * to be considered discrete,
 * as opposed to continuous,
 * even if the value type is continuous.
 *
 * @property {boolean} isDiscreteValueType
 * Indicates if the value type of the values of this dimension are discrete,
 * as opposed to continuous.
 *
 * @property {boolean} isComparable
 * Indicates if the values of this dimension can be compared.
 * 
 * @property {boolean} isHidden Indicates if the dimension is
 * hidden from the user, in places like a tooltip, for example, or in the legend.
 * 
 * @property {def.Map} playedVisualRoles
 * A map of {@link pvc.visual.Role} indexed by visual role name, of the visual roles currently being played by this dimension type.
 * 
 * @constructor
 *
 * @param {pvc.data.ComplexType} complexType The complex type that this dimension belongs to.
 * @param {string} name The name of the dimension type.
 *
 * @param {object} [keyArgs] Keyword arguments.
 * @param {string} [keyArgs.label] The label of this dimension type.
 * Defaults to the name of the dimension type.
 * @param {function} [keyArgs.valueType=null] The type of the values of this dimension type.
 * <p>
 * The supported value types are: <i>null</i> (which really means <i>any</i>), {@link Boolean}, {@link Number}, {@link String}, {@link Date} and {@link Object}.
 * </p>
 * @param {boolean} [keyArgs.isHidden=false] Indicates if the dimension should
 * be hidden from the user, in places like a tooltip, for example, or in the legend.
 * @param {boolean} [keyArgs.isDiscrete]
 * Indicates if the dimension
 * is considered discrete.
 * The default value depends on the value of {@link valueType};
 * it is true unless the {@link valueType} is Number or Date.
 * 
 * @param {function} [keyArgs.converter] A function used in the translation phase
 * to convert raw values into values of the dimension's value type.
 * Its signature is:
 * <pre>
 * function(rawValue : any) : valueType
 * </pre>
 * 
 * @param {string} [keyArgs.rawFormat] A protovis format mask adequate to the specified value type.
 * When specified and a converter is not specified, it is used to create a converter
 * for the Date and Number value types.
 * 
 * @param {function} [keyArgs.key] A function used in the translation phase
 * to obtain the string key of each value.
 * Its signature is:
 * <pre>
 * function(value : valueType) : string
 * </pre>
 * <p>
 * Nully values have a fixed key of '', 
 * so to the function never receives a "nully" value argument.
 * A consequence is that no other values can have an empty key.
 * </p>
 * <p>
 * The default key is obtained by calling the value's {@link Object#toString} method.
 * </p>
 * 
 * @param {function} [keyArgs.formatter] A function used in the translation phase
 * to format the values of this dimension type.
 * Its signature is:
 * <pre>
 * function(value : valueType, rawValue : any) : string
 * </pre>
 * <p>
 * Only a "nully" value <i>should</i> have an empty label.
 * </p>
 * <p>
 * The label is not necessarily unique.
 * </p>
 * <p>
 * The default format is the empty string for null values, 
 * or the result of calling the <i>value</i>'s {@link Object#toString} method.
 * </p>
 * 
 * @param {string} [keyArgs.format] A protovis format mask adequate to the specified value type.
 * When specified and a formatter is not specified, it is used to create a formatter
 * for the Date and Number value types.
 *
 * @param {function} [keyArgs.comparer]
 * Specifies a comparator function for the values of this dimension type.
 * Its signature is:
 * <pre>
 * function(valueA : valueType, valueB : valueType) : number
 * </pre>
 * 
 * The default value depends on the value of {@link valueType};
 * it is {@link def.compare} when the {@link valueType} is Date,
 * and null otherwise.
 */

/**
 * Cache of reverse order context-free value comparer function.
 * 
 * @name pvc.data.DimensionType#_reverseComparer
 * @field
 * @type function
 * @private
 */

/**
 * Cache of reverse order context-free atom comparer function.
 * 
 * @name pvc.data.DimensionType#_reverseAtomComparer
 * @field
 * @type function
 * @private
 */

/**
 * Cache of normal order context-free value comparer function.
 * 
 * @name pvc.data.DimensionType#_directComparer
 * @field
 * @type function
 * @private
 */

/**
 * Cache of normal order context-free atom comparer function.
 * 
 * @name pvc.data.DimensionType#_directAtomComparer
 * @field
 * @type function
 * @private
 */
def.type('pvc.data.DimensionType')
.init(
function(complexType, name, keyArgs){
    this.complexType = complexType;
    this.name  = name;
    this.label = def.get(keyArgs, 'label') || pvc.buildTitleFromName(name);

    var groupAndLevel = pvc.splitIndexedId(name);
    this.group = groupAndLevel[0];
    this.groupLevel = def.nullyTo(groupAndLevel[1], 0);

    if(this.label.indexOf('{') >= 0){
        this.label = def.format(this.label, [this.groupLevel+1]);
    }

    this.playedVisualRoles = new def.Map();
    this.isHidden = !!def.get(keyArgs, 'isHidden');
    
    var valueType = def.get(keyArgs, 'valueType') || null;
    var valueTypeName = pvc.data.DimensionType.valueTypeName(valueType);
    var cast = def.getOwn(pvc.data.DimensionType.cast, valueTypeName, null);
    
    this.valueType = valueType;
    this.valueTypeName = valueTypeName;
    this.cast = cast;
    
    this.isDiscreteValueType = (this.valueType !== Number && this.valueType !== Date);
    this.isDiscrete = def.get(keyArgs, 'isDiscrete');
    if(this.isDiscrete == null){
        this.isDiscrete = this.isDiscreteValueType;
    } else {
        // Normalize the value
        this.isDiscrete = !!this.isDiscrete;
        if(!this.isDiscrete && this.isDiscreteValueType) {
            throw def.error.argumentInvalid('isDiscrete', "The only supported continuous value types are Number and Date.");
        }
    }
    
    /** 
     * @private
     * @internal
     * @see pvc.data.Dimension#convert
     */
    this._converter = def.get(keyArgs, 'converter') || null;
    if(!this._converter) {
        var rawFormat = def.get(keyArgs, 'rawFormat');
        if(rawFormat) {
            /*jshint onecase:true */
            switch(this.valueType) {
//                case Number:
//                    // TODO: receive extra format configuration arguments
//                    // this._converter = pv.Format.createParser(pv.Format.number().fractionDigits(0, 2));
//                    break;
                    
                case Date:
                    this._converter = pv.Format.createParser(pv.Format.date(rawFormat));
                    break;
            }
        }
    }
    
    /** 
     * @private
     * @internal
     * @see pvc.data.Dimension#key
     */
    this._key = def.get(keyArgs, 'key') || null;
    
    /** @private */
    this._comparer = def.get(keyArgs, 'comparer');
    if(this._comparer === undefined){ // It is possible to prevent the default specifying null
        switch(this.valueType){
            case Number:
            case Date:
                this._comparer = def.compare;
                break;
                
            default:
                 this._comparer = null;
        }
    }

    this.isComparable = this._comparer != null;
    
    /** 
     * @private
     * @internal
     * @see pvc.data.Dimension#format
     */
    this._formatter = def.get(keyArgs, 'formatter') || null;
    if(!this._formatter) {
        switch(this.valueType) {
            case Number:
                // TODO: receive extra format configuration arguments
                this._formatter = pv.Format.createFormatter(pv.Format.number().fractionDigits(0, 2));
                break;
                
            case Date:
                var format = def.get(keyArgs, 'format'); 
                if(!format){
                    // Try to create one from raw format
                    // slightly modifying it to look like 
                    // protovis' continuous date scale dynamic formats
                    format = def.get(keyArgs, 'rawFormat');
                    if(format){
                        format = format.replace(/-/g, "/");
                    }
                }
                
                if(!format){
                    format = "%Y/%m/%d";
                }
                
                this._formatter = pv.Format.createFormatter(pv.Format.date(format));
                break;
        }
    }
})
.add(/** @lends pvc.data.DimensionType# */{
    
    isCalculated: false,
    
    /**
     * Compares two values of the dimension's {@link #valueType}, in ascending order.
     * <p>
     * To compare two values in descending order multiply the result by -1.
     * </p>
     * <p>
     * Values can be nully.
     * </p>
     * @param {any} a A value of the dimension's {@link #valueType}.
     * @param {any} b A value of the dimension's {@link #valueType}.
     *  
     * @returns {Number}
     * A negative number if {@link a} is before {@link b},
     * a positive number if {@link a} is after {@link b},
     * and 0 if they are considered to have the same order.
     */
    compare: function(a, b){
        if(a == null) {
            if(b == null) {
                return 0;
            }
            return -1;
        } else if(b == null) {
            return 1;
        }
        
        return this._comparer.call(null, a, b);
    },
    
    /**
     * Gets a context-free comparer function 
     * for values of the dimension's {@link #valueType}
     * and for a specified order.
     * 
     * <p>When the dimension type is not comparable, <tt>null</tt> is returned.</p>
     * 
     * @param {boolean} [reverse=false] Indicates if the comparison order should be reversed.
     * 
     * @type function
     */
    comparer: function(reverse){
        if(!this.isComparable) {
            return null;
        }
        
        var me = this;
        if(reverse){
            return this._reverseComparer || 
                   (this._reverseComparer = function(a, b){ return me.compare(b, a); }); 
        }
        
        return this._directComparer || (this._directComparer = function(a, b){ return me.compare(a, b); }); 
    },
    
    /**
     * Gets a context-free atom comparer function, 
     * for a specified order.
     * 
     * @param {boolean} [reverse=false] Indicates if the comparison order should be reversed.
     * 
     * @type function
     */
    atomComparer: function(reverse){
        if(reverse){
            return this._reverseAtomComparer || 
                   (this._reverseAtomComparer = this._createReverseAtomComparer()); 
        }
        
        return this._directAtomComparer ||
                (this._directAtomComparer = this._createDirectAtomComparer());
    },
    
    // Coercion to discrete upon the role binding (irreversible...)
    _toDiscrete: function(){
        this.isDiscrete = true;
    },
    
    _toCalculated: function(){
        this.isCalculated = true;
    },
    
    _createReverseAtomComparer: function(){
        if(!this.isComparable){
            /*global atom_idComparerReverse:true */
            return atom_idComparerReverse;
        }
        
        var me = this;
        
        function reverseAtomComparer(a, b){
            if(a === b) { return 0; } // Same atom
            return me.compare(b.value, a.value); 
        }
        
        return reverseAtomComparer;
    },
    
    _createDirectAtomComparer: function(){
        if(!this.isComparable){
            /*global atom_idComparer:true */
            return atom_idComparer;
        }
        
        var me = this;
        
        function directAtomComparer(a, b){
            if(a === b) { return 0; } // Same atom
            return me.compare(a.value, b.value);
        }
        
        return directAtomComparer;
    },
    
    /**
     * Gets the dimension type's context-free formatter function, if one is defined, or <tt>null</tt> otherwise.
     * @type function
     */
    formatter: function(){
        return this._formatter;
    },
    
    /**
     * Gets the dimension type's context-free converter function, if one is defined, or <tt>null</tt> otherwise.
     * @type function
     */
    converter: function(){
        return this._converter;
    },
    
    /**
     * Obtains a value indicating if this dimension type plays any visual role 
     * such that {@link pvc.visual.Role#isPercent} is <tt>true</tt>.
     * @type boolean
     */
    playingPercentVisualRole: function(){
        return def.query(this.playedVisualRoles.values())
                  .any(function(visualRole){ 
                      return visualRole.isPercent; 
                  }); 
    }
});

pvc.data.DimensionType.cast = {
    'Date': function(value) {
        return value instanceof Date ? value : new Date(value);
    },

    'Number': function(value) {
        value = Number(value);
        return isNaN(value) ? null : value;
    },

    'String':  String,
    'Boolean': Boolean,
    'Object':  Object,
    'Any':     null
};

/**
 * Obtains the default group name for a given dimension name.
 * 
 * @param {string} dimName The dimension name.
 * 
 *  @type string
 */
pvc.data.DimensionType.dimensionGroupName = function(dimName){
    return dimName.replace(/^(.*?)(\d*)$/, "$1");
};

// TODO: Docs
pvc.data.DimensionType.valueTypeName = function(valueType){
    if(valueType == null){
        return "Any";
    }
    
    switch(valueType){
        case Boolean: return 'Boolean';
        case Number:  return 'Number';
        case String:  return 'String';
        case Object:  return 'Object';
        case Date:    return 'Date';
        default: throw def.error.argumentInvalid('valueType', "Invalid valueType function: '{0}'.", [valueType]);
    }
};

/**
 * Extends a dimension type specification with defaults based on
 * group name and specified options.
 *  
 * @param {object} [keyArgs] Keyword arguments.
 * @param {function} [keyArgs.isCategoryTimeSeries=false] Indicates if category dimensions are to be considered time series.
 * @param {string} [keyArgs.timeSeriesFormat] The parsing format to use to parse a Date dimension when the converter and rawFormat options are not specified.
 * @param {function} [keyArgs.valueNumberFormatter] The formatter to use to parse a numeric dimension of the 'value' dimension group, when the formatter and format options are not specified.
 * @param {object} [keyArgs.dimensionGroups] A map of dimension group names to dimension type specifications to be used as prototypes of corresponding dimensions.
 * 
 *  @returns {object} The extended dimension type specification.
 */
pvc.data.DimensionType.extendSpec = function(dimName, dimSpec, keyArgs){
    
    var dimGroup = pvc.data.DimensionType.dimensionGroupName(dimName),
        userDimGroupsSpec = def.get(keyArgs, 'dimensionGroups');
    
    if(userDimGroupsSpec) {
        var groupDimSpec = userDimGroupsSpec[dimGroup];
        if(groupDimSpec) {
            dimSpec = def.create(groupDimSpec, dimSpec /* Can be null */); 
        }
    }
    
    if(!dimSpec) { 
        dimSpec = {};
    }
    
    switch(dimGroup) {
        case 'category':
            var isCategoryTimeSeries = def.get(keyArgs, 'isCategoryTimeSeries', false);
            if(isCategoryTimeSeries) {
                if(dimSpec.valueType === undefined) {
                    dimSpec.valueType = Date; 
                }
            }
            break;
        
        case 'value':
            if(dimSpec.valueType === undefined) {
                dimSpec.valueType = Number;
            }

            if(dimSpec.valueType === Number) {
                if(dimSpec.formatter === undefined && 
                   !dimSpec.format){
                    dimSpec.formatter = def.get(keyArgs, 'valueNumberFormatter');
                }
            }
            break;
    }
    
    if(dimSpec.converter === undefined && 
       dimSpec.valueType === Date && 
       !dimSpec.rawFormat) {
        dimSpec.rawFormat = def.get(keyArgs, 'timeSeriesFormat');
    }
    
    return dimSpec;
};

/**
 * Adds a visual role to the dimension type.
 * 
 * @name pvc.data.DimensionType#_addVisualRole
 * @function
 * @param {pvc.visual.Role} visualRole The visual role.
 * @type undefined
 * @private
 * @internal
 */
function dimType_addVisualRole(visualRole) {
    this.playedVisualRoles.set(visualRole.name, visualRole);
    /*global compType_dimensionRolesChanged:true */
    compType_dimensionRolesChanged.call(this.complexType, this);
}

/**
 * Removes a visual role from the dimension type.
 * 
 * @name pvc.data.DimensionType#_removeVisualRole
 * @function
 * @param {pvc.visual.Role} visualRole The visual role.
 * @type undefined
 * @private
 * @internal
 */
function dimType_removeVisualRole(visualRole) {
    this.playedVisualRoles.rem(visualRole.name);
    compType_dimensionRolesChanged.call(this.complexType, this);
}

/**
 * Initializes a complex type instance.
 * 
 * @name pvc.data.ComplexType
 * 
 * @class A complex type is, essentially, a named set of dimension types.
 *
 * @constructor
 * 
 * @param {object} [dimTypeSpecs]
 * A map of dimension names to dimension type constructor's keyword arguments.
 *
 * @see pvc.data.DimensionType
 */
def.type('pvc.data.ComplexType')
.init(
function(dimTypeSpecs){
    /**
     * A map of the dimension types by name.
     * 
     * @type object
     * @private
     */
    this._dims = {};
    
    /**
     * A list of the dimension types.
     * 
     * @type pvc.data.DimensionType[]
     * @private
     */
    this._dimsList = [];
    
    /**
     * A list of the dimension type names.
     * 
     * @type string[]
     * @private
     */
    this._dimsNames = [];
    
    /**
     * A list of the calculations
     * ordered by calculation order.
     * 
     * @type function[]
     * @private
     */
    this._calculations = [];
    
    /**
     * A set of the names of 
     * dimension types being calculated.
     * 
     * @type map(string boolean)
     * @private
     */
    this._calculatedDimNames = {};
    
    /**
     * An object with the dimension indexes by dimension name.
     * 
     * @type object
     * @private
     */
    this._dimsIndexByName = null;
    
    /**
     * An index of the dimension types by group name.
     * 
     * @type object
     * @private
     */
    this._dimsByGroup = {};
    
    /**
     * An index of the dimension type names by group name.
     * 
     * @type object
     * @private
     */
    this._dimsNamesByGroup = {};
    
    if(dimTypeSpecs) {
        for(var name in dimTypeSpecs){
            this.addDimension(name, dimTypeSpecs[name]);
        }
    }
})
.add(/** @lends pvc.data.ComplexType# */{
    describe: function(){

        var out = ["COMPLEX TYPE INFORMATION", pvc.logSeparator];
        
        this._dimsList.forEach(function(type){
            var features = [];
            
            features.push(type.valueTypeName);
            if(type.isComparable) { features.push("comparable"); }
            if(!type.isDiscrete)  { features.push("continuous"); }
            if(type.isHidden)     { features.push("hidden"); }

            out.push("  " + type.name + " (" + features.join(', ') + ")");
        });
        
        //out.push(pvc.logSeparator);

        return out.join("\n");
    },
    
    /**
     * Obtains a dimension type given its name.
     * 
     * <p>
     * If no name is specified,
     * a map with all dimension types indexed by name is returned.
     * Do <b>NOT</b> modify this map.
     * </p>
     * 
     * @param {string} [name] The dimension type name.
     * 
     * @param {object} [keyArgs] Keyword arguments
     * @param {boolean} [keyArgs.assertExists=true] Indicates that an error is signaled 
     * if a dimension type with the specified name does not exist.
     * 
     * @type pvc.data.DimensionType | pvc.data.DimensionType[] | null
     */
    dimensions: function(name, keyArgs){
        if(name == null) {
            return this._dims;
        }
        
        var dimType = def.getOwn(this._dims, name, null);
        if(!dimType && def.get(keyArgs, 'assertExists', true)) {
            throw def.error.argumentInvalid('name', "Undefined dimension '{0}'", [name]); 
        }
        
        return dimType;
    },
    
    /**
     * Obtains an array with all the dimension types.
     * 
     * <p>
     * Do <b>NOT</b> modify the returned array. 
     * </p>
     * @type pvc.data.DimensionType[]
     */
    dimensionsList: function(){
        return this._dimsList;
    },
    
    /**
     * Obtains an array with all the calculated dimension types,
     * in order of evaluation.
     * 
     * <p>
     * Do <b>NOT</b> modify the returned array. 
     * </p>
     * @type pvc.data.DimensionType[]
     */
    calculatedDimensionsList: function(){
        return this._calcDimsList;
    },
    
    /**
     * Obtains an array with all the dimension type names.
     * 
     * <p>
     * Do <b>NOT</b> modify the returned array. 
     * </p>
     * @type string[]
     */
    dimensionsNames: function(){
        return this._dimsNames;
    },
    
    /**
     * Obtains an array of the dimension types of a given group.
     * 
     * <p>
     * Do <b>NOT</b> modify the returned array. 
     * </p>
     * 
     * @param {object} [keyArgs] Keyword arguments.
     * @param {boolean} [keyArgs.assertExists=true] Indicates if an error is signaled when the specified group name is undefined.
     * 
     * @type pvc.data.DimensionType[]
     */
    groupDimensions: function(group, keyArgs){
        var dims = def.getOwn(this._dimsByGroup, group);
        if(!dims && def.get(keyArgs, 'assertExists', true)) {
            throw def.error.operationInvalid("There is no dimension type group with name '{0}'.", [group]);
        }
        
        return dims;
    },
    
    /**
     * Obtains an array of the dimension type names of a given group.
     * 
     * <p>
     * Do <b>NOT</b> modify the returned array. 
     * </p>
     * 
     * @param {object} [keyArgs] Keyword arguments.
     * @param {boolean} [keyArgs.assertExists=true] Indicates if an error is signaled when the specified group name is undefined.
     *  
     * @type string[]
     */
    groupDimensionsNames: function(group, keyArgs){
        var dimNames = def.getOwn(this._dimsNamesByGroup, group);
        if(!dimNames && def.get(keyArgs, 'assertExists', true)) {
            throw def.error.operationInvalid("There is no dimension type group with name '{0}'.", [group]);
        }
        
        return dimNames;
    },
    
    /**
     * Creates and adds to the complex type a new dimension type, 
     * given its name and specification.
     * 
     * @param {string} name The name of the dimension type.
     * @param {object} [dimTypeSpec] The dimension type specification.
     * Essentially its a <i>keyArgs</i> object.
     * See {@link pvc.data.DimensionType}'s <i>keyArgs</i> constructor
     * to know about available arguments.
     *  
     * @type {pvc.data.DimensionType}
     */
    addDimension: function(name, dimTypeSpec){
        // <Debug>
        /*jshint expr:true */
        name || def.fail.argumentRequired('name');
        !def.hasOwn(this._dims, name) || def.fail.operationInvalid("A dimension type with name '{0}' is already defined.", [name]);
        // </Debug>
        
        var dimension = new pvc.data.DimensionType(this, name, dimTypeSpec);
        this._dims[name] = dimension;
        
        this._dimsIndexByName = null; // reset
        
        var group = dimension.group;
        var groupLevel;
        if(group) {
            var groupDims = def.getOwn(this._dimsByGroup, group),
                groupDimsNames;
            
            if(!groupDims) {
                groupDims = this._dimsByGroup[group] = [];
                groupDimsNames = this._dimsNamesByGroup[group] = [];
            } else {
                groupDimsNames = this._dimsNamesByGroup[group];
            }
            
            // TODO: this sorting is lexicographic...
            // TODO this should be unified with dimension.groupLevel...
            groupLevel = def.array.insert(groupDimsNames, name, def.compare);
            groupLevel = ~groupLevel;
            def.array.insertAt(groupDims, groupLevel, dimension);
        }
        
        var index;
        var L = this._dimsList.length;
        if(!group) {
            index = L;
        } else {
            groupLevel = dimension.groupLevel;
            
            // Find the index of the last dimension of the same group
            // or the one that has a higher level that this one
            for(var i = 0 ; i < L ; i++){
                var dim = this._dimsList[i];
                if(dim.group === group){
                    if(dim.groupLevel > groupLevel){
                        // Before the current one
                        index = i;
                        break;
                    }
                    
                    // After the current one
                    index = i + 1;
                }
            } 
               
            if(index == null){
                index = L;
            }
        }
        
        def.array.insertAt(this._dimsList,  index, dimension);
        def.array.insertAt(this._dimsNames, index, name);
        
        // calculated
        if(dimension._calculate){
            index = def.array.binarySearch(
                        this._calcDimsList, 
                        dimension._calculationOrder, 
                        def.compare,
                        function(dimType){ return dimType._calculationOrder; });
            if(index >= 0){
                // Add after
                index++;
            } else {
                // Add at the two's complement of index
                index = ~index;
            }
            
            def.array.insertAt(this._calcDimsList, index, dimension);
        }
        
        this._isPctRoleDimTypeMap = null;
        
        return dimension;
    },
    
    addCalculation: function(calcSpec, dimsOptions){
        /*jshint expr:true */
        calcSpec || def.fail.argumentRequired('calcSpec');
        
        var calculation = calcSpec.calculation ||
                          def.fail.argumentRequired('calculations[i].calculation');
        
        var dimNames = calcSpec.names;
        if(typeof dimNames === 'string'){
            dimNames = dimNames.split(/\s*\,\s*/);
        } else {
            dimNames = def.array.as(dimNames);
        }
        
        if(dimNames && dimNames.length){
            var calcDimNames = this._calculatedDimNames;
            
            dimNames.forEach(function(name){
                if(name){
                    name = name.replace(/^\s*(.+?)\s*$/, "$1"); // trim
                    
                    !def.hasOwn(calcDimNames, name) || 
                      def.fail.argumentInvalid('calculations[i].names', "Dimension name '{0}' is already being calculated.", [name]);
                    
                    // Dimension need to be created?
                    var dimType = this._dims[name];
                    if(!dimType){
                        var dimSpec = pvc.data.DimensionType.extendSpec(name, null, dimsOptions);
                        this.addDimension(name, dimSpec);
                    }
                    
                    calcDimNames[name] = true;
                    
                    dimType._toCalculated();
                }
            }, this);
        }
        
        this._calculations.push(calculation);
    },
    
    isCalculated: function(dimName){
        return def.hasOwn(this._calculatedDimNames, dimName);
    },
    
    _calculate: function(complex){
        var calcs = this._calculations;
        if(calcs.length){
            var valuesByName = {}; 
            
            calcs.forEach(function(calc){
                calc(complex, valuesByName);
            });
            
            return valuesByName;
        }
    },
    
    /**
     * Obtains a map of the dimension types, indexed by their name,
     * that are playing a role such that {@link pvc.visual.Role#isPercent} is <tt>true</tt>.
     * 
     * @type def.Map
     */
    getPlayingPercentVisualRoleDimensionMap: function(){
        var map = this._isPctRoleDimTypeMap;
        if(!map) {
            map = this._isPctRoleDimTypeMap = new def.Map(
                def.query(def.own(this._dims))
                    .where(function(dimType){ return dimType.playingPercentVisualRole(); })
                    .object({
                        name: function(dimType) { return dimType.name; } 
                    }));
        }
        
        return map;
    },
    
    /**
     * Sorts a specified dimension array in place, 
     * according to the definition order.
     * 
     * @param {any[]} dims Array of dimension names.
     * @param {function} [nameKey] Allows extracting the dimension name from
     * each of the elements of the specified array.
     * 
     * @type any[]
     */
    sortDimensionNames: function(dims, nameKey){
        var dimsIndexByName = this._dimsIndexByName;
        if(!dimsIndexByName){
            dimsIndexByName = 
                def
                .query(this._dimsList)
                .object({
                    name:  function(dim){ return dim.name; },
                    value: function(dim, index){ return index; }
                });
            this._dimsIndexByName = dimsIndexByName;
        }
        
        dims.sort(function(da, db){
            return def.compare(
                    dimsIndexByName[nameKey ? nameKey(da) : da],
                    dimsIndexByName[nameKey ? nameKey(db) : db]);
                    
        });
        
        return dims;
    }
});

/**
 * Called by a dimension type to indicate that its assigned roles have changed.
 * 
 * @name pvc.data.ComplexType#_dimensionRolesChanged
 * @function
 * @param {pvc.data.DimensionType} dimType The affected dimension type.
 * @type undefined
 * @private
 * @internal
 */
function compType_dimensionRolesChanged(dimType) {
    this._isPctRoleDimTypeMap = null;
}

/**
 * Initializes a complex type project.
 * 
 * @name pvc.data.ComplexType
 * 
 * @class A complex type project is a work in progress set of dimension specifications.
 */
def
.type('pvc.data.ComplexTypeProject')
.init(function(dimGroupSpecs) {
    this._dims = {};
    this._dimList = [];
    this._dimGroupsDims = {};
    this._dimGroupSpecs = dimGroupSpecs || {};
    
    this._calcList = [];
})
.add(/** @lends pvc.data.ComplexTypeProject# */{
    _ensureDim: function(name, spec) {
        /*jshint expr:true*/
        name || def.fail.argumentInvalid('name', "Invalid dimension name '{0}'.", [name]);
        
        var info = def.getOwn(this._dims, name);
        if(!info) {
            info = this._dims[name] = this._createDim(name, spec);
            
            this._dimList.push(info);
            
            var groupDimsNames = def.array.lazy(this._dimGroupsDims, info.groupName);
            // TODO: this sorting is lexicographic but should be numeric
            def.array.insert(groupDimsNames, name, def.compare);
        } else if(spec) {
            def.setUDefaults(info.spec, spec);
        }
        
        return info;
    },
    
    hasDim: function(name) { return def.hasOwn(this._dims, name); },
    
    setDim: function(name, spec) {
        var _ = this._ensureDim(name).spec;
        if(spec) { def.copy(_, spec); }
        return this;
    },
    
    setDimDefaults: function(name, spec) {
        def.setUDefaults(this._ensureDim(name).spec, spec);
        return this;
    },
    
    _createDim: function(name, spec) {
        var dimGroupName = pvc.data.DimensionType.dimensionGroupName(name);
        var dimGroupSpec = this._dimGroupSpecs[dimGroupName];
        if(dimGroupSpec) { spec = def.create(dimGroupSpec, spec /* Can be null */); }
        return {
            name: name,
            groupName: dimGroupName,
            spec: spec || {}
        };
    },
    
    readDim: function(name, spec) {
        var info = this._ensureDim(name, spec);
        if(info.isRead) {
            throw def.error.operationInvalid("Dimension '{0}' already is the target of a reader.", [name]);
        }
        if(info.isCalc) {
            throw def.error.operationInvalid("Dimension '{0}' is being calculated, so it cannot be the target of a reader.", [name]);
        }
        
        info.isRead = true;
    },
    
    calcDim: function(name, spec) {
        var info = this._ensureDim(name, spec);
        if(info.isCalc) {
            throw def.error.operationInvalid("Dimension '{0}' already is being calculated.", [name]);
        }
        if(info.isRead) {
            throw def.error.operationInvalid("Dimension '{0}' is the target of a reader, so it cannot be calculated.", [name]);
        }
        
        info.isCalc = true;
    },
    
    isReadOrCalc: function(name) {
        if(name) {
            var info = def.getOwn(this._dims, name);
            if(info) { return info.isRead || info.isCalc; }
        }
        
        return false;
    },
    
    groupDimensionsNames: function(groupDimName) { return this._dimGroupsDims[groupDimName]; },
    
    setCalc: function(calcSpec) {
        /*jshint expr:true */
        calcSpec || def.fail.argumentRequired('calculations[i]');
        calcSpec.calculation || def.fail.argumentRequired('calculations[i].calculation');
        
        var dimNames = calcSpec.names;
        if(typeof dimNames === 'string') { dimNames = dimNames.split(/\s*\,\s*/); } 
        else                             { dimNames = def.array.as(dimNames);     }
        
        if(dimNames && dimNames.length) { dimNames.forEach(this.calcDim, this); }
        
        this._calcList.push(calcSpec);
    },
    
    configureComplexType: function(complexType, translOptions) {
        //var keyArgs = {assertExists: false};
        
        this._dimList.forEach(function(dimInfo) {
            var dimName = dimInfo.name;
            //if(!complexType.dimensions(dimName, keyArgs)){
            var spec = dimInfo.spec;
            
            spec = pvc.data.DimensionType.extendSpec(dimName, spec, translOptions);
            
            complexType.addDimension(dimName, spec);
            //} // TODO: else assert has not changed?
        });
        
        this._calcList.forEach(function(calcSpec) { complexType.addCalculation(calcSpec); });
    }
});

/**
 * Initializes a translation operation.
 * 
 * @name pvc.data.TranslationOper
 * @class Represents one translation operation 
 * from some data source format to the list of atoms format.
 * 
 * @property {pvc.BaseChart} chart The associated chart.
 * @property {pvc.data.ComplexType} complexType The complex type that represents the translated data.
 * @property {pvc.data.Data} data The data object which will be loaded with the translation result.
 * @property {object} source The source object, of some format, being translated.
 * @property {object} metadata A metadata object describing the source.
 * @property {object} options  An object with translation options.
 * 
 * @constructor
 * @param {pvc.BaseChart} chart The associated chart.
 * @param {pvc.data.ComplexTypeProject} complexTypeProj The complex type project that will represent the translated data.
 * @param {object} source The source object, of some format, to be translated.
 * The source is not modified.
 * @param {object} [metadata] A metadata object describing the source.
 * @param {object} [options] An object with translation options.
 * Options are translator specific.
 * TODO: missing common options here
 */
def.type('pvc.data.TranslationOper')
.init(function(chart, complexTypeProj, source, metadata, options) {
    this.chart = chart;
    this.complexTypeProj = complexTypeProj;
    this.source   = source;
    this.metadata = metadata || {};
    this.options  = options  || {};

    this._initType();
    
    if(pvc.debug >= 4) {
        this._logItems = true;
        this._logItemCount = 0;
    }
})
.add(/** @lends pvc.data.TranslationOper# */{
    
    _logItems: false,
    
    /**
     * Logs the contents of the source and metadata properties.
     */
    logSource: def.method({isAbstract: true}),

    /**
     * Logs the structure of the virtual item array.
     */
    logVItem: def.method({isAbstract: true}),
    
    _translType: "Unknown",
    
    logTranslatorType: function() { return this._translType + " data source translator"; },
    
    /**
     * Obtains the number of fields of the virtual item.
     * <p>
     * The default implementation returns the length of the metadata.
     * </p>
     * 
     * @type number
     * @virtual
     */
    virtualItemSize:     function() { return this.metadata.length; },
    
    freeVirtualItemSize: function() { return this.virtualItemSize() - this._userUsedIndexesCount; },
    
    setSource: function(source) {
        if(!source) { throw def.error.argumentRequired('source'); }
        
        this.source   = source;
    },
    
    /**
     * Defines a dimension reader.
     *
     * @param {object} dimReaderSpec A dimensions reader specification.
     *
     * @type undefined
     */
    defReader: function(dimReaderSpec){
        /*jshint expr:true */
        dimReaderSpec || def.fail.argumentRequired('readerSpec');

        var dimNames;
        if(def.string.is(dimReaderSpec)) { dimNames = dimReaderSpec;       }
        else                             { dimNames = dimReaderSpec.names; }
        
        if(def.string.is(dimNames)) { dimNames = dimNames.split(/\s*\,\s*/); } 
        else                        { dimNames = def.array.as(dimNames);     }
        
        // Consumed/Reserved virtual item indexes
        var indexes = def.array.as(dimReaderSpec.indexes);
        if(indexes) { indexes.forEach(this._userUseIndex, this); }
        
        var hasDims = !!(dimNames && dimNames.length);
        var reader = dimReaderSpec.reader;
        if(!reader) {
            // -> indexes, possibly expanded
            if(hasDims) { return this._userCreateReaders(dimNames, indexes); }
            // else a reader that only serves to exclude indexes
            if(indexes) {
                // Mark index as being excluded
                indexes.forEach(function(index) { this._userIndexesToSingleDim[index] = null; }, this);
            }
        } else {
            hasDims || def.fail.argumentRequired('reader.names', "Required argument when a reader function is specified.");
            
            this._userRead(reader, dimNames);
        }
        
        return indexes;
    },

    /**
     * Called once, before {@link #execute},
     * for the translation to configure the complex type project (abstract).
     *
     * <p>
     *    If this method is called more than once,
     *    the consequences are undefined.
     * </p>
     *
     * @name pvc.data.TranslationOper#configureType
     * @function
     * @type undefined
     * @virtual
     */
    configureType: function() { this._configureTypeCore(); },
    
    /** @abstract */
    _configureTypeCore: def.method({isAbstract: true}),
    
    _initType: function() {
        this._userDimsReaders = [];
        this._userDimsReadersByDim = {};
        
        this._userItem = [];
        
        this._userUsedIndexes = {};
        this._userUsedIndexesCount = 0;
        
        // Indexes reserved for a single dimension or (null)
        this._userIndexesToSingleDim = [];
        
        // -------------
        
        var userDimReaders = this.options.readers;
        if(userDimReaders) { userDimReaders.forEach(this.defReader, this); }

        var multiChartIndexes = pvc.parseDistinctIndexArray(this.options.multiChartIndexes);
        if(multiChartIndexes) {
            this._multiChartIndexes = 
                this.defReader({names: 'multiChart', indexes: multiChartIndexes });
        }
    },

    _userUseIndex: function(index) {
        index = +index; // to number

        if(index < 0) { throw def.error.argumentInvalid('index', "Invalid reader index: '{0}'.", [index]); }

        if(def.hasOwn(this._userUsedIndexes, index)) {
            throw def.error.argumentInvalid('index', "Virtual item index '{0}' is already assigned.", [index]);
        }
        
        this._userUsedIndexes[index] = true;
        this._userUsedIndexesCount++;
        this._userItem[index] = true;
        
        return index;
    },

    _userCreateReaders: function(dimNames, indexes) {
        if(!indexes) {
            indexes = [];
        } else {
            // Convert indexes to number
            indexes.forEach(function(index, j) { indexes[j] = +index; });
        }

        // Distribute indexes to names, from left to right
        // Excess indexes go to the last *group* name
        // Missing indexes are padded from available indexes starting from the last provided index
        // If not enough available indexes exist, those names end up reading undefined
        var I = indexes.length,
            N = dimNames.length,
            dimName;
        
        if(N > I) {
            // Pad indexes
            var nextIndex = I > 0 ? (indexes[I - 1] + 1) : 0;
            do {
                nextIndex = this._nextAvailableItemIndex(nextIndex);
                indexes[I] = nextIndex;
                this._userUseIndex(nextIndex);
                I++;
            } while(N > I);
        }

        // If they match, it's one-one name <-- index
        var L = (I === N) ? N : (N - 1);
        var index;
        // The first N-1 names get the first N-1 indexes
        for(var n = 0 ; n < L ; n++) {
            dimName = dimNames[n];
            index = indexes[n];
            this._userIndexesToSingleDim[index] = dimName;
            
            this._userRead(this._propGet(dimName, index), dimName);
        }

        // The last name is the dimension group name that gets all remaining indexes
        if(L < N) {
            // TODO: make a single reader that reads all atoms??
            // Last is a *group* START name
            var splitGroupName = pvc.splitIndexedId(dimNames[N - 1]),
                groupName = splitGroupName[0],
                level     = def.nullyTo(splitGroupName[1], 0);

            for(var i = L ; i < I ; i++, level++) {
                dimName = pvc.buildIndexedId(groupName, level);
                index = indexes[i];
                this._userIndexesToSingleDim[index] = dimName;
                this._userRead(this._propGet(dimName, index), dimName);
            }
        }
        
        return indexes;
    },

    _userRead: function(reader, dimNames) {
        /*jshint expr:true */
        def.fun.is(reader) || def.fail.argumentInvalid('reader', "Reader must be a function.");
        
        if(def.array.is(dimNames)) {
            dimNames.forEach(function(name) { this._readDim(name, reader); }, this);
        } else {
            this._readDim(dimNames, reader);
        }

        this._userDimsReaders.push(reader);
    },

    _readDim: function(name, reader) {
        var info, spec;
        var index = this._userIndexesToSingleDim.indexOf(name);
        if(index >= 0) {
            info = this._itemInfos[index];
            if(info && !this.options.ignoreMetadataLabels) {
                var label = info.label || info.name;
                if(label) { spec = {label: label}; }
            }
            // Not using the type information because it conflicts
            // with defaults specified in other places.
            // (like with the MetricXYAbstract x role valueType being a Date when timeSeries=true)
            //if(info.type != null) { spec.valueType = info.type === 0 ? /*Any*/null : Number; }
        }
        
        this.complexTypeProj.readDim(name, spec);
        this._userDimsReadersByDim[name] = reader;
    },
    
    /**
     * Performs the translation operation for a data instance.
     * 
     * <p>
     *    The returned atoms are interned in 
     *    the dimensions of the specified data instance.
     * </p>
     * 
     * <p>
     *    If this method is called more than once,
     *    the consequences are undefined.
     * </p>
     * 
     * @param {pvc.data.Data} data The data object in whose dimensions returned atoms are interned.
     * 
     * @returns {def.Query} An enumerable of {@link pvc.data.Atom[]}
     */
    execute: function(data) {
        this.data = data;
        
        return this._executeCore();
    },
    
    /**
     * Obtains an enumerable of translated atoms (virtual).
     * 
     * <p>
     *    The default implementation applies 
     *    every dimensions reader returned by {@link #_getDimensionsReaders} 
     *    to every item returned by  {@link #_getItems}.
     *   
     *    Depending on the underlying data source format 
     *    this may or may not be a good translation strategy.
     *    Override to apply a different one.
     * </p>
     * 
     * @returns {def.Query} An enumerable of {@link pvc.data.Atom[]}
     * @virtual
     */
    _executeCore: function() {
        var dimsReaders = this._getDimensionsReaders();
        
        return def.query(this._getItems())
                  .select(function(item) { return this._readItem(item, dimsReaders); }, this);
    },
    
    /**
     * Obtains an enumerable of items to translate (virtual).
     * 
     * <p>
     * The default implementation assumes that {@link #source}
     * is directly the desired enumerable of items. 
     * </p>
     * 
     * @type def.Query
     */
    _getItems: function() { return this.source; },
    
    /**
     * Obtains the dimensions readers array (virtual).
     * 
     * <p>
     * Each dimensions reader function reads one or more dimensions
     * from a source item.
     * It has the following signature:
     * </p>
     * <pre>
     * function(item : any) : pvc.data.Atom[] | pvc.data.Atom
     * </pre>
     * 
     * <p>
     * The default implementation simply returns the {@link #_userDimsReaders} field. 
     * </p>
     * 
     * @name _getDimensionsReaders
     * @type function[]
     * @virtual
     */
    _getDimensionsReaders: function() { return this._userDimsReaders; },
    
    /**
     * Applies all the specified dimensions reader functions to an item 
     * and sets the resulting atoms in a specified array (virtual).
     * 
     * @param {any} item The item to read.
     * @param {function[]} dimsReaders An array of dimensions reader functions.
     * @returns {map(string any)} A map of read raw values by dimension name.
     * @virtual
     */
    _readItem: function(item, dimsReaders) {
        // This function is performance critical and so does not use forEach
        // or array helpers, avoiding function calls, closures, etc.
        var logItem = this._logItems;
        if(logItem) {
            var logItemCount = this._logItemCount;
            if(logItemCount < 10){
                pvc.log('virtual item [' + this._logItemCount + ']: ' + pvc.stringify(item));
                this._logItemCount++;
            } else {
                pvc.log('...');
                
                // Stop logging vitems
                logItem = this._logItems = false;
            }
        }
        
        var r = 0, 
            R = dimsReaders.length, 
            a = 0,
            data = this.data,
            valuesByDimName = {};
        
        while(r < R) {
            dimsReaders[r++].call(data, item, valuesByDimName);
        }
        
        if(logItem) {
            // Log read names/values
            var atoms = {};
            for(var dimName in valuesByDimName) {
                var atom = valuesByDimName[dimName];
                if(def.object.is(atom)) {
                    atom = ('v' in atom) ? atom.v : ('value' in atom) ? atom.value : '...';
                }
                
                atoms[dimName] = atom;
            }
            
            pvc.log('-> read: ' + pvc.stringify(atoms));
        }
        
        return valuesByDimName;
    },
    
    /**
     * Given a dimension name and a property name,
     * creates a corresponding dimensions reader (protected).
     * 
     * @param {string} dimName The name of the dimension on which to intern read values.
     * @param {string} prop The property name to read from each item.
     * @param {object} [keyArgs] Keyword arguments. 
     * @param {boolean} [keyArgs.ensureDim=true] Creates a dimension with the specified name, with default options, if one does not yet exist. 
     * 
     * @type function
     */
    _propGet: function(dimName, prop) {
        
        function propGet(item, atoms) { atoms[dimName] = item[prop]; }
        
        return propGet;
    },

    // TODO: docs
    _nextAvailableItemIndex: function(index, L) {
        if(index == null) { index = 0;    }
        if(L     == null) { L = Infinity; }

        while(index < L && def.hasOwn(this._userItem, index)) { index++; }
        
        return index < L ? index : -1;
    },
    
    _getUnboundRoleDefaultDimNames: function(roleName, count, dims, level) {
        var role = this.chart.visualRoles[roleName];
        if(role && !role.isPreBound()) {
            var dimGroupName = role.defaultDimensionName;
            if(dimGroupName) {
                dimGroupName = dimGroupName.match(/^(.*?)(\*)?$/)[1];
                
                if(!dims        ) { dims = []; }
                if(level == null) { level = 0; }
                if(count == null) { count = 1; }
                
                // Already bound dimensions count
                while(count--) {
                    var dimName = pvc.buildIndexedId(dimGroupName, level++);
                    if(!this.complexTypeProj.isReadOrCalc(dimName)) { dims.push(dimName); }
                }
                
                return dims.length ? dims : null;
            }
        }
    },
    
    collectFreeDiscreteAndConstinuousIndexes: function(freeDisIndexes, freeMeaIndexes) {
        this._itemInfos.forEach(function(info, index) {
            if(!this._userUsedIndexes[index]) {
                var indexes = info.type === 1 ? freeMeaIndexes : freeDisIndexes;
                if(indexes) { indexes.push(index); }
            }
        }, this);
    }
});

/**
 * @name pvc.data.MatrixTranslationOper
 * @class Represents one translation operation, 
 * from a source matrix in some format to 
 * an enumerable of atom arrays.
 * 
 * @extends pvc.data.TranslationOper
 * @abstract
 * 
 * @constructor
 * @param {pvc.BaseChart} chart The associated chart.
 * @param {pvc.data.ComplexType} complexType The complex type that will represent the translated data.
 * @param {pvc.data.Data} data The data object which will be loaded with the translation result.
 * @param {object} source The source matrix, in some format, to be translated.
 * The source is not modified.
 * @param {object} [metadata] A metadata object describing the source.
 * @param {object} [options] An object with translation options.
 * 
 * @param {boolean} [options.seriesInRows=false]
 * Indicates that series are to be switched with categories.
 *
 * @param {Number[]} [options.plot2DataSeriesIndexes]
 * Array of series indexes in {@link #source} that are second axis' series.
 * Any non-null value is converted to an array.
 * Each value of the array is also converted to a number.
 * A negative value is counted from the end
 * of the series values (-1 is the series last value, ...).
 * <p>
 * Note that the option 'seriesInRows'
 * affects what are considered to be series values.
 *
 * Having determined where series are stored,
 * the order of occurrence of a series value in {@link #source}
 * determines its index.
 * </p>
 */
def.type('pvc.data.MatrixTranslationOper', pvc.data.TranslationOper)
.add(/** @lends pvc.data.MatrixTranslationOper# */{
    
    _initType: function() {
        this.J = this.metadata.length;
        this.I = this.source.length; // repeated in setSource
        
        this._processMetadata();
        
        this.base();
    },
    
    setSource: function(source) {
        this.base(source);
        
        this.I = this.source.length;
    },
    
    _knownContinuousColTypes: {'numeric': 1, 'number': 1, 'integer': 1},
    
    _processMetadata: function() {
        // Confirm metadata column types.
        
        // Get the indexes of columns which are 
        // not stated as continuous (numeric..)
        // In these, 
        // we can't trust their stated data type
        // cause when nulls exist on the first row, 
        // they frequently come stated as "string"...
        var knownContinColTypes = this._knownContinuousColTypes;
        var columns = 
            def
            .query(this.metadata)
            // Fix indexes of colDefs
            .select(function(colDef, colIndex) {
                // Ensure colIndex is trustable
                colDef.colIndex = colIndex;
                return colDef;
             })
            .where(function(colDef) {
                var colType = colDef.colType;
                return !colType || knownContinColTypes[colType.toLowerCase()] !== 1;
            })
            .select(function(colDef) { return colDef.colIndex; })
            .array();
        
        // 1 - continuous (number, date)
        // 0 - discrete   (anything else)
        // Assume all are continuous
        var columnTypes = def.array.create(this.J, 1);
        
        // Number of rows in source
        var I = this.I;
        var source = this.source;
        
        // Number of columns remaining to confirm data type
        var J = columns.length;
        
        for(var i = 0 ; i < I && J > 0 ; i++) {
            var row = source[i];
            var m = 0;
            while(m < J) {
                var j = columns[m];
                var value = row[j];
                if(value != null) {
                    columnTypes[j] = this._getSourceValueType(value);
                    
                    columns.splice(m, 1);
                    J--;
                } else {
                    m++;
                }
            }
        }
        
        this._columnTypes = columnTypes;
    },
    
    _buildItemInfoFromMetadata: function(index) {
        var meta = this.metadata[index];
        return {
            type:  this._columnTypes[index],
            name:  meta.colName,
            label: meta.colLabel
        };
    },
    
    // 1 - continuous (number, date)
    // 0 - discrete   (anything else)
    /** @static */
    _getSourceValueType: function(value) {
        switch(typeof value) {
            case 'number': return 1;
            case 'object': if(value instanceof Date) { return 1; }
        }
        
        return 0; // discrete
    },
    
    logSource: function() {
        var out = [
            "DATA SOURCE SUMMARY",
            pvc.logSeparator,
            "ROWS (" + Math.min(10, this.I) + "/" + this.I + ")"
        ];
        
        def
        .query(this.source)
        .take(10)
        .each(function(row, index) {
            out.push("  [" + index + "] " + pvc.stringify(row));
        });
        
        if(this.I > 10) { out.push('  ...'); }
        
        out.push("COLS (" + this.J + ")");
        
        var colTypes = this._columnTypes;
        this
        .metadata
        .forEach(function(col, j) {
            out.push(
                "  [" + j + "] " + 
                "'" + col.colName + "' (" +
                "type: "      + col.colType + ", " + 
                "inspected: " + (colTypes[j] ? 'number' : 'string') +
                 (col.colLabel ? (", label: '" + col.colLabel + "'") : "")  + 
                ")");
        });
        
        out.push("");
        
        return out.join('\n');
    },
    
    _logVItem: function(kindList, kindScope) {
        var out = ["VIRTUAL ITEM ARRAY", pvc.logSeparator];
        var maxName  = 4;// length of column header
        var maxLabel = 5;// idem
        var maxDim   = 9;// idem
        this._itemInfos.forEach(function(info, index) {
            maxName  = Math.max(maxName , (info.name  ||'').length);
            maxLabel = Math.max(maxLabel, (info.label ||'').length);
            var dimName = this._userIndexesToSingleDim[index];
            if(dimName) { maxDim = Math.max(maxDim, dimName.length); }
        }, this);
        
        // TODO: would be better off with a generic ASCII table layout code...
        
        // Headers
        out.push("Index | Kind | Type   | " + 
                 def.string.padRight("Name",  maxName ) + " | " + 
                 def.string.padRight("Label", maxLabel) + " > " + 
                 "Dimension",
                 
                 "------+------+--------+-" + 
                 def.string.padRight("", maxName,  "-") + "-+-" +
                 def.string.padRight("", maxLabel, "-") + "-+-" +
                 def.string.padRight("", maxDim,   "-") + "-");

        var index = 0;
        kindList.forEach(function(kind) {
            for(var i = 0, L = kindScope[kind] ; i < L ; i++) {
                var info = this._itemInfos[index];
                var dimName = this._userIndexesToSingleDim[index];
                if(dimName === undefined) { dimName = ''; }
                out.push(
                    " " + index + "    | " + 
                          kind  + "    | " +
                          (info.type ? 'number' : 'string') + " | " +
                          def.string.padRight(info.name  || '', maxName ) + " | " +
                          def.string.padRight(info.label || '', maxLabel) + " | " +
                          dimName);
                index++;
            }
        }, this);
        
        out.push("");
        
        return out.join("\n");
    },
    
    /**
     * Creates the set of second axis series keys
     * corresponding to the specified
     * plot2DataSeriesIndexes and seriesAtoms arrays (protected).
     *
     * Validates that the specified series indexes are valid
     * indexes of seriesAtoms array.
     *
     * @param {Array} plot2DataSeriesIndexes Array of indexes of the second axis series values.
     * @param {Array} seriesKeys Array of the data source's series atom keys.
     *
     * @returns {Object} A set of second axis series values or null if none.
     *
     * @private
     * @protected
     */
    _createPlot2SeriesKeySet: function(plot2DataSeriesIndexes, seriesKeys) {
        var plot2SeriesKeySet = null,
            seriesCount = seriesKeys.length;
        def.query(plot2DataSeriesIndexes).each(function(indexText) {
            // Validate
            var seriesIndex = +indexText; // + -> convert to number
            if(isNaN(seriesIndex)) {
                throw def.error.argumentInvalid('plot2DataSeriesIndexes', "Element is not a number '{0}'.", [indexText]);
            }

            if(seriesIndex < 0) {
                if(seriesIndex <= -seriesCount) {
                    throw def.error.argumentInvalid('plot2DataSeriesIndexes', "Index is out of range '{0}'.", [seriesIndex]);
                }

                seriesIndex = seriesCount + seriesIndex;
            } else if(seriesIndex >= seriesCount) {
                throw def.error.argumentInvalid('plot2DataSeriesIndexes', "Index is out of range '{0}'.", [seriesIndex]);
            }

            // Set
            if(!plot2SeriesKeySet) { plot2SeriesKeySet = {}; }
            
            plot2SeriesKeySet[seriesKeys[seriesIndex]] = true;
        });

        return plot2SeriesKeySet;
    },

    // TODO: docs
    _dataPartGet: function(calcAxis2SeriesKeySet, seriesReader) {

        var me = this;
        
        var dataPartDimName = this.options.dataPartDimName;

        var dataPartDimension,
            plot2SeriesKeySet,
            part1Atom,
            part2Atom,
            outAtomsSeries = {};

        function dataPartGet(item, outAtoms) {
            /*
             * First time initialization.
             * Done here because *data* isn't available before.
             */
            if(!dataPartDimension) {
                plot2SeriesKeySet = calcAxis2SeriesKeySet();
                dataPartDimension = me.data.dimensions(dataPartDimName);

                if(pvc.debug >=3 && plot2SeriesKeySet) {
                    pvc.log("Second axis series values: " + pvc.stringify(def.keys(plot2SeriesKeySet)));
                }
            }

            var partAtom;
            seriesReader(item, outAtomsSeries);
            var series = outAtomsSeries.series;
            if(series != null && series.v != null) { series = series.v; }
            
            if(def.hasOwn(plot2SeriesKeySet, series)) {
                partAtom = part2Atom || (part2Atom = dataPartDimension.intern("1"));
            } else {
                partAtom = part1Atom || (part1Atom = dataPartDimension.intern("0"));
            }
            
            outAtoms[dataPartDimName] = partAtom;
        }

        return dataPartGet;
    }
});

/**
 * @name pvc.data.CrosstabTranslationOper
 * @class A translation from a matrix in crosstab format.
 * <p>
 *    The default <i>matrix-crosstab</i> format is:
 * </p>
 * <pre>
 * +----------+----------+----------+
 * | -        | S1       | S2       | ... (taken from metadataItem.colName)
 * +==========+==========+==========+
 * | C1       | 12       | 45       |
 * | C2       | 11       | 99       |
 * | C3       | null     |  3       |
 * +----------+----------+----------+
 * </pre>
 * <p>Legend:</p>
 * <ul>
 *   <li>C<sub>i</sub> &mdash; Category value <i>i</i></li>
 *   <li>S<sub>j</sub> &mdash; Series value <i>j</i></li>
 * </ul>
 * 
 * TODO: document crosstab options
 * 
 * @extends pvc.data.MatrixTranslationOper
 */
def.type('pvc.data.CrosstabTranslationOper', pvc.data.MatrixTranslationOper)
.add(/** @lends pvc.data.CrosstabTranslationOper# */{
    /* LEGEND
     * ======
     * 
     * Matrix Algebra
     * --------------
     * 
     *      j
     *    +---+
     * i  | v |
     *    +---+
     * 
     * i - index of matrix line
     * j - index of matrix column
     * 
     * v - value at indexes i,j
     * 
     * ----
     * 
     * line  = matrix[i]
     * value = line[j]
     * 
     * 
     * Crosstab Algebra
     * ----------------
     * 
     *      CC
     *    +----+
     * RR | MM |
     *    +----+
     * 
     * RR = row     space
     * CC = column  space
     * MM = measure space
     * 
     * ----
     * As a function
     * 
     * cross-table: RR X CC -> MM
     * 
     * ----
     * Dimension of spaces (called "depth" in the code to not confuse with Dimension)
     * 
     * R  = number of row      components
     * C  = number of column   components
     * M  = number of measure  components
     * 
     * ----
     * Instances / groups / members
     * 
     * <RG> = <r1, ..., rR> = R-tuple of row     values 
     * <CG> = <s1, ..., sS> = C-tuple of column  values 
     * <MG> = <m1, ..., mM> = M-tuple of measure values
     * 
     * r = index of row     group component
     * c = index of column  group component
     * m = index of measure group component
     * 
     * ----
     * Extent of spaces
     * 
     * RG = number of (distinct) row    groups
     * CG = number of (distinct) column groups
     * MG = RG * CG
     * 
     * rg = index of row    group
     * cg = index of column group
     * 
     * 
     * 
     * Crosstab in a Matrix
     * --------------------
     * 
     * Expand components into own columns:
     * | <...RG...> | <=> | r1 | r2 | r3 | ... | rR |
     * 
     * All component values joined with a separator character, ~,
     * occupying only one column:
     * | <~CG~>     | <=> | "c1~c2~c3~...~cC" |
     * 
     * ----
     * 
     * Format: "Measures in columns" (uniform)
     * 
     *             0            R           R+M    R+M*(CG-1)   R+M*CG
     *             o------------+------------+ ... +------------o (j - matrix column)
     *         
     *                          0            1     CG-1         CG
     *                          o------------+ ... +------------o (cg - column group index)
     *        
     *                          +------------+ ... +------------+    <-- this._colGroups
     *                   X      | <~CG~>     |     | <~CG~>     | 
     *                          +------------+     +------------+
     *        
     *      0 o    +------------+------------+ ... +------------+    <-- this.source
     *        |    | <...RG...> | <...MG...> |     | <...MG...> |
     *        |    |            | <...MG...> |     | <...MG...> |
     *      1 +    +------------+------------+     +------------+
     *                          ^
     *        .                 |
     *        .               m = cg % M
     *        .
     *        
     *        |
     *     RG o
     *       (i - matrix line)
     *       (rg - row group)
     *       
     * i = rg
     * j = R + M*cg
     *
     * Unfortunately, not all measures have to be specified in all column groups.
     * When a measure in column group would have all rows with a null value, it can be omitted.
     * 
     * Virtual Item Structure
     * ----------------------
     * A relational view of the cross groups
     *  
     *    [<...CG...>, <...RG...>, <...MG...>]
     * 
     * This order is chosen to match that of the relational translation.
     *
     * Virtual Item to Dimensions mapping
     * ----------------------------------
     * 
     * A mapping from a virtual item to a list of atoms (of distinct dimensions)
     * 
     * virtual-item --> atom[]
     * 
     * A set of dimensions readers are called and 
     * each returns one or more atoms of distinct dimensions.
     * 
     *  * Each dimension has exactly one dimensions reader that reads its atoms.
     *  * One dimensions reader may read more than one dimension.
     *  * A dimensions reader always reads the same set of dimensions.
     *  
     *  * A dimension consumes data from zero or more virtual item components.
     *  * A virtual item component is consumed by zero or more dimensions.
     *  * A dimension may vary in which virtual item components it consumes, from atom to atom.
     *   
     *  virtual-item-component * <-> * dimension + <-> 1 dimensions reader
     */

    _translType: "Crosstab",
    
    /**
     * Obtains the number of fields of the virtual item.
     * @type number
     * @override
     */
    virtualItemSize: function() { return this.R + this.C + this.M; },
    
    /**
     * Performs the translation operation (override).
     * @returns {def.Query} An enumerable of {@link map(string any)}
     * @override
     */
    _executeCore: function() {
        if(!this.metadata.length) { return def.query(); }
        
        var dimsReaders = this._getDimensionsReaders();
        
        // ----------------
        // Virtual item
        
        var item  = new Array(this.virtualItemSize()),
            itemCrossGroupIndex = this._itemCrossGroupIndex,
            me = this;
        
        // Updates VITEM
        // . <- source = line[0..R]
        // . <- source = colGroup[0..C]
        function updateVItemCrossGroup(crossGroupId, source) {
            // Start index of cross group in item
            var itemIndex   = itemCrossGroupIndex[crossGroupId],
                sourceIndex = 0,
                depth       = me[crossGroupId];
            
            while((depth--) > 0) { item[itemIndex++] = source[sourceIndex++]; }
        }
        
        // . <-  line[colGroupIndexes[0..M]]
        function updateVItemMeasure(line, cg) {
            // Start index of cross group in item
            var itemIndex = itemCrossGroupIndex.M;
            var cgIndexes = me._colGroupsIndexes[cg];
            var depth     = me.M;
            
            for(var i = 0 ; i < depth ; i++) {
                var lineIndex = cgIndexes[i];
                item[itemIndex++] = lineIndex != null ? line[lineIndex] : null;
            }
        }
        
        // ----------------

        function expandLine(line/*, i*/) {
            updateVItemCrossGroup('R', line);
            
            return def.query(this._colGroups)
                .select(function(colGroup, cg) {
                    // Update ITEM
                    updateVItemCrossGroup('C', colGroup);
                    updateVItemMeasure(line, cg);
                  
                    // Naive approach...
                    // Call all readers every time
                    // Dimensions that consume rows and/or columns may be evaluated many times.
                    // So, it's very important that pvc.data.Dimension#intern is as fast as possible
                    //  detecting already interned values.
                    return this._readItem(item, dimsReaders);
                }, this);
        }
        
        return def.query(this.source).selectMany(expandLine, this);
    },
    
    _processMetadata: function() {
        
        this.base();
        
        this._separator = this.options.separator || '~';
        
        /* Determine R, C and M */
        
        // Default values
        var R = this.R = 1;
        this.C = 1;
        this.M = 1;
        
        this.measuresDirection = null;
        
        var colNames;
        var metadata = this.metadata;
        if(this.options.seriesInRows) {
            colNames = metadata.map(function(d) { return d.colName; });
        } else if(this.options.compatVersion <= 1) {
            colNames = metadata.map(function(d) { return {v: d.colName}; });
        } else {
            // Use the column label, if any, as the "column" value's format.
            colNames = metadata.map(function(d) { return {v: d.colName, f: d.colLabel}; });
        }
         
        /*
         * For each cross group,
         * an array with the item info of each of its columns
         * {
         *   'C': [ {name: , type: , label: } ],
         *   'R': [],
         *   'M': []
         * }
        */
        var itemCrossGroupInfos = this._itemCrossGroupInfos = {};
        
        // --------------
        // * isMultiValued
        // * measuresInColumns
        // * measuresIndex, [measuresCount=1]
        // * [categoriesCount = 1]
        
        // ~~~~ R*

        if(!this.options.isMultiValued) {
            //    | C
            // ---|---
            // R* | M
            
            R = this.R = this._getCategoriesCount();
            
            // C = 1
            // M = 1
            
            this._colGroups = colNames.slice(R);
            this._colGroupsIndexes = new Array(this._colGroups.length);
            
            // To Array
            this._colGroups.forEach(function(colGroup, cg){
                this._colGroups[cg] = [colGroup];
                this._colGroupsIndexes[cg] = [R + cg]; // all the same
            }, this);

            // R cross group is set below
            
            // Assume series are discrete (there's no metadata about them)
            // No name or label info.
            itemCrossGroupInfos.C = [{type: 0}];
            
            // The column labels are series labels. Only colType is relevant.
            itemCrossGroupInfos.M = [{type: this._columnTypes[R]}];
        } else {
            /* MULTI-VALUED */
            
            var measuresInColumns = def.get(this.options, 'measuresInColumns', true);
            if(measuresInColumns || this.options.measuresIndex == null) {
                
                R = this.R = this._getCategoriesCount();

                // First R columns are from row space
                var encodedColGroups = colNames.slice(R);
                
                // Remaining are column and measure types
                var L = encodedColGroups.length;

                // Any results in column direction...
                if(L > 0) {
                    if(!measuresInColumns) {
                        // ~~~~ C*  M
                        
                        //    | C*
                        // ---|----
                        // R* | M
                        
                        this._colGroups = encodedColGroups;
                        this._colGroupsIndexes = [];
                        
                        // Split encoded column groups
                        this._colGroups.forEach(function(colGroup, cg){
                            this._colGroups[cg] = this._splitEncodedColGroupCell(colGroup);
                            this._colGroupsIndexes[cg] = [this.R + cg]; // all the same
                        }, this);
                        
                        itemCrossGroupInfos.M = [this._buildItemInfoFromMetadata(R)];
                    } else {
                        // ~~~~ C* M*
                        
                        //    | C*~M*
                        // ---|------
                        // R* | M*
                        
                        this.measuresDirection = 'columns';
                        
                        // Updates: 
                        //   _colGroups, 
                        //   _colGroupsIndexes and 
                        //   M
                        //  itemCrossGroupInfos.M
                        this._processEncodedColGroups(encodedColGroups);
                    }

                    this.C = this._colGroups[0].length; // may be 0!
                    
                    // C discrete columns
                    itemCrossGroupInfos.C = 
                        def.range(0, this.C).select(function() { return {type: 0}; }).array();
                    
                } else {
                    this.C = this.M = 0;
                    itemCrossGroupInfos.M = [];
                    itemCrossGroupInfos.C = [];
                }

            } else {
                // TODO: complete this
                // TODO: itemCrossGroupInfos
                
                /* MEASURES IN ROWS */
                
                this.measuresDirection = 'rows';

                // C = 1 (could also be more if an option to make ~ on existed)
                // R = 1 (could be more...)
                // M >= 1

                // The column index at which measure values (of each series) start
                // is the number of row components
                this.R = +this.options.measuresIndex;

                var measuresCount = this.options.measuresCount;
                if (measuresCount == null) { measuresCount = 1; }

                // TODO: >= 1 check
                this.M = measuresCount;

                // First R columns are from row space
                // Next follows a non-relevant Measure title column
                this._colGroups = colNames.slice(this.R + 1);

                // To Array of Cells
                this._colGroups.forEach(function(colGroup, cg){
                    this._colGroups[cg] = [colGroup];
                }, this);
            }
        }
        
        // First R columns are from row space
        itemCrossGroupInfos.R =
            def.range(0, this.R).select(this._buildItemInfoFromMetadata, this).array();
        
        // ----------------
        // The index at which the first component of
        // each cross group is placed in **virtual item**
        
        var seriesInRows = this.options.seriesInRows;
        
        var itemGroupIndex = this._itemCrossGroupIndex = {
            'C': !seriesInRows ? 0      : this.R,
            'R': !seriesInRows ? this.C : 0,
            'M': this.C + this.R
        };
        
        var itemInfos = this._itemInfos = new Array(this.virtualItemSize()); // R + C + M
        
        def.eachOwn(itemGroupIndex, function(groupStartIndex, crossGroup){
            itemCrossGroupInfos[crossGroup]
            .forEach(function(info, groupIndex){
                itemInfos[groupStartIndex + groupIndex] = info;
            });
        });
        
        // Logical view
        
        this._itemLogicalGroup = {
            'series':   seriesInRows ? this.R : this.C,
            'category': seriesInRows ? this.C : this.R,
            'value':    this.M
        };
        
        this._itemLogicalGroupIndex = {
            'series':   0,
            'category': this._itemLogicalGroup.series,
            'value':    this.C + this.R
        };
    },
    
    logVItem: function() {
        return this._logVItem(['C', 'R', 'M'], {C: this.C, R: this.R, M: this.M});
    },

    _getCategoriesCount: function() {
        var R = this.options.categoriesCount;
        if(R != null && (!isFinite(R) || R < 0)) { R = null; }
        
        if(R == null) {
            // Number of consecutive discrete columns, from left
            R = def
                .query(this._columnTypes)
                .whayl(function(type) { return type === 0; }) // 0 = discrete
                .count();
            if(!R) {
                // Having no R causes problems 
                // when categories are continuous
                // (in MetricDots for example).
                R = 1;
            }
        }
        
        return R;
    },
    
    _splitEncodedColGroupCell: function(colGroup) {
        var values = colGroup.v;
        var labels;
        
        if(values == null) {
            values = [];
        } else {
            values = values.split(this._separator);
            labels = colGroup.f;
            if(labels) { labels = labels.split(this._separator); }
        }

        return values.map(function(value, index) {
            return {
                v: value,
                f: labels && labels[index]
            };
        });
    },

    /**
     * Analyzes the array of encoded column groups.
     * <p>
     * Creates an array of column groups;
     * where each element of the array is 
     * an array of the column values of the group (C values).
     * </p>
     * <p>
     * In the process the number of encoded measures is determined, {@link #M}.
     * In this respect, note that not all measures need to be supplied
     * in every column group.
     * When a measure is not present, that means that the value of the measure
     * in every row is null.
     * </p>
     * <p>
     * It is assumed that the order of measures in column groups is stable.
     * So, if in one column group "measure 1" is before "measure 2",
     * then it must be also the case in every other column group.
     * This order is then used to place values in the virtual item.
     * </p>
     */
    _processEncodedColGroups: function(encodedColGroups) {
        var L = encodedColGroups.length || def.assert("Must have columns"),
            R = this.R,
            colGroups = [],
            currColGroup,
            /*
             * measureName -> {
             *     groupIndex: 0, // Global order of measures within a column group
             *     index: 0       // Index (i, below) of measure's first appearance
             * }
             *
             */
            measuresInfo  = {},
            measuresInfoList = [];
        
        for(var i = 0 ; i < L ; i++) {
            var colGroupCell = encodedColGroups[i];
            
            var encColGroupValues = colGroupCell.v;
            var encColGroupLabels = colGroupCell.f;
            var sepIndex = encColGroupValues.lastIndexOf(this._separator);
            
            var meaName, meaLabel, colGroupValues, colGroupLabels;
            
            // MeasureName has precedence,
            // so we may end up with no column group value (and C = 0).
            if(sepIndex < 0) {
                // C = 0
                meaName  = encColGroupValues;
                meaLabel = encColGroupLabels;
                encColGroupValues = '';
                colGroupValues = [];
            } else {
                meaName = encColGroupValues.substring(sepIndex + 1);
                encColGroupValues = encColGroupValues.substring(0, sepIndex);
                colGroupValues = encColGroupValues.split(this._separator);

                if(encColGroupLabels != null) {
                    colGroupLabels = encColGroupLabels.split(this._separator);
                    meaLabel = colGroupLabels.pop(); // measure label
                }
                
                /*jshint loopfunc:true */
                colGroupValues.forEach(function(value, index) {
                    var label = colGroupLabels && colGroupLabels[index];
                    colGroupValues[index] = {v: value, f: label};
                });
            }

            // New column group?
            if(!currColGroup || currColGroup.encValues !== encColGroupValues) {
                currColGroup = {
                    startIndex:   i,
                    encValues:    encColGroupValues,
                    values:       colGroupValues,
                    measureNames: [meaName]
                };

                colGroups.push(currColGroup);
            } else {
                currColGroup.measureNames.push(meaName);
            }

            // Check the measure
            var currMeaIndex = (i - currColGroup.startIndex),
                meaInfo = def.getOwn(measuresInfo, meaName);
            if(!meaInfo) {
                measuresInfo[meaName] = meaInfo = {
                    name:  meaName,
                    label: meaLabel,
                    type:  this._columnTypes[R + i], // Trust the type of the first column where the measure appears
                    
                    // More than needed info for CGInfo, but it's ok
                    groupIndex: currMeaIndex,
                    index: i
                };
                measuresInfoList.push(meaInfo);
            } else if(currMeaIndex > meaInfo.groupIndex) {
                meaInfo.groupIndex = currMeaIndex;
            }
        }

        // Sort measures
        measuresInfoList.sort(function(meaInfoA, meaInfoB) {
            return def.compare(meaInfoA.groupIndex, meaInfoB.groupIndex) ||
                   def.compare(meaInfoA.index, meaInfoB.index);
        });

        // Reassign measure group indexes
        measuresInfoList.forEach(function(meaInfoA, index) { meaInfoA.groupIndex = index; });
        
        // Publish colgroups and colgroupIndexes, keeping only relevant information
        var CG = colGroups.length,
            colGroupsValues  = new Array(CG),
            colGroupsIndexes = new Array(CG),
            M = measuresInfoList.length;
        
        colGroups.map(function(colGroup, cg) {
            colGroupsValues[cg] = colGroup.values;
            
            var colGroupStartIndex = colGroup.startIndex;
            
            // The index in source *line* where each of the M measures can be read
            var meaIndexes = colGroupsIndexes[cg] = new Array(M);
            colGroup.measureNames.forEach(function(meaName2, localMeaIndex) {
                // The measure index in VITEM
                var meaIndex = measuresInfo[meaName2].groupIndex;
                
                // Where to read the measure in *line*?
                meaIndexes[meaIndex] = R + colGroupStartIndex + localMeaIndex;
            });
        });
        
        this._colGroups        = colGroupsValues;
        this._colGroupsIndexes = colGroupsIndexes;
        this._itemCrossGroupInfos.M = measuresInfoList; 
        this.M = M;
    },
    
    /**
     * Called once, before {@link #execute},
     * for the translation to configure the complex type.
     *
     * @type undefined
     * @override
     */
    configureType: function() {
        // Map: Dimension Group -> Item cross-groups indexes
        if(this.measuresDirection === 'rows') { throw def.error.notImplemented(); }

        this.base();
    },
    
    /** 
     * Default cross tab mapping from virtual item to dimensions. 
     * @override 
     */
    _configureTypeCore: function() {
        var me = this;
        var itemLogicalGroup = me._itemLogicalGroup;
        var itemLogicalGroupIndex = me._itemLogicalGroupIndex;
        
        var index = 0;
        var dimsReaders = [];
        
        function add(dimGroupName, level, count) {
            var crossEndIndex = itemLogicalGroupIndex[dimGroupName] + count; // exclusive
            while(count > 0) {
                var dimName = pvc.buildIndexedId(dimGroupName, level);
                if(!me.complexTypeProj.isReadOrCalc(dimName)) { // Skip name if occupied and continue with next name
                    
                    // use first available slot for auto dims readers as long as within crossIndex and crossIndex + count
                    index = me._nextAvailableItemIndex(index);
                    if(index >= crossEndIndex) {
                        // this group has no more slots available
                        return;
                    }
                    
                    dimsReaders.push({names: dimName, indexes: index});
                    
                    index++; // consume index
                    count--;
                }
                
                level++;
            }
        }
        
        /* plot2DataSeriesIndexes only implemented for single-series */
        var dataPartDimName = this.options.dataPartDimName;
        if(dataPartDimName && this.C === 1 && !this.complexTypeProj.isReadOrCalc(dataPartDimName)) {
            // The null test is required because plot2DataSeriesIndexes can be a number, a string...
            var plot2DataSeriesIndexes = this.options.plot2DataSeriesIndexes;
            if(plot2DataSeriesIndexes != null) {
                var seriesKeys = this._colGroups.map(function(colGroup) { return '' + colGroup[0].v; });
                this._plot2SeriesKeySet = this._createPlot2SeriesKeySet(plot2DataSeriesIndexes, seriesKeys);
            }
        }
        
        ['series', 'category', 'value'].forEach(function(dimGroupName) {
            var L = itemLogicalGroup[dimGroupName];
            if(L > 0) { add(dimGroupName, 0, L); }
        });
        
        if(dimsReaders) { dimsReaders.forEach(this.defReader, this); }
        
        if(this._plot2SeriesKeySet) {
            var seriesReader = this._userDimsReadersByDim.series;
            if(seriesReader) {
                var calcAxis2SeriesKeySet = def.fun.constant(this._plot2SeriesKeySet);
                this._userRead(this._dataPartGet(calcAxis2SeriesKeySet, seriesReader), dataPartDimName);
            }
        }
    }
});

/**
 * @name pvc.data.RelationalTranslationOper
 *
 * @class Represents one translation operation,
 * from a source matrix in relational format to
 * an enumerable of atom arrays.
 *
 * <p>
 * The default matrix-relational format is:
 * </p>
 * <pre>
 * ---------------------------
 *    0   |    1     |   2
 * ---------------------------
 * series | category | value
 * ---------------------------
 *    T   |     A    |   12
 *    T   |     B    |   45
 *    Q   |     A    |   11
 *    Q   |     B    |   99
 *    Z   |     B    |    3
 * </pre>
 * <p>
 * If the option <i>seriesInRows</i> is true
 * the indexes of series and categories are switched.
 * </p>
 * <p>
 * If the option <i>measuresIndexes</i> is specified,
 * additional value dimensions are created to receive the specified columns.
 * Note that these indexes may consume series and/or category indexes as well.
 * </p>
 * <p>
 * If only two metadata columns are provided,
 * then a dummy 'series' column, with the constant null value, is added automatically.
 * </p>
 *
 * @extends pvc.data.MatrixTranslationOper
 *
 * @constructor
 * @param {pvc.BaseChart} chart The associated chart.
 * @param {pvc.data.ComplexType} complexType The complex type that will represent the translated data.
 * @param {object} source The matrix-relational array to be translated.
 * The source is not modified.
 * @param {object} [metadata] A metadata object describing the source.
 *
 * @param {object} [options] An object with translation options.
 * See additional available options in {@link pvc.data.MatrixTranslationOper}.
 *
 * @param {(number|string)[]|number|string} [options.measuresIndexes]
 * An array of indexes of columns of the source matrix
 * that contain value dimensions.
 * <p>
 * Multiple 'value' dimensions ('value', 'value2', 'value3', ...)
 * are bound in order to the specified indexes.
 * </p>
 * <p>
 * The option 'plot2DataSeriesIndexes'
 * is incompatible with and
 * takes precedence over
 * this one.
 * </p>
 * <p>
 * The indexes can be numbers or strings that represent numbers.
 * It is also possible to specify a single index instead of an array.
 * </p>
 */
def
.type('pvc.data.RelationalTranslationOper', pvc.data.MatrixTranslationOper)
.add(/** @lends pvc.data.RelationalTranslationOper# */{
    M: 0, // number of measures
    C: 0, // number of categories
    S: 0, // number of series

    _translType: "Relational",

    _processMetadata: function() {

        this.base();

        var metadata = this.metadata;

        var J = this.J; // metadata.length

        // Split between series and categories
        var C = this.options.categoriesCount;
        if(C != null && (!isFinite(C) || C < 0)) { C = 0; }

        var S;

        // Assuming duplicate valuesColIndexes is not valid
        // (v1 did not make this assumption)
        var valuesColIndexes, M;
        if(this.options.isMultiValued) {
            valuesColIndexes = pvc.parseDistinctIndexArray(this.options.measuresIndexes, 0, J - 1);
            M = valuesColIndexes ? valuesColIndexes.length : 0;
        }

        var D; // discrete count = D = S + C
        if(M == null) {
            if(J > 0 && J <= 3 && (C == null || C === 1) && S == null) {
                // V1 Stability requirement
                // Measure columns with all values = null,
                // would be detected as type string,
                // and not be chosen as measures.
                M = 1;
                valuesColIndexes = [J - 1];
                C = J >= 2 ? 1 : 0;
                S = J >= 3 ? 1 : 0;
                D = C + S;

            } else if(C != null &&  C >= J) {
                D = J;
                C = J;
                S = 0;
                M = 0;
            } else {
                // specified C wins over M, and by last S
                var Mmax = C != null ? (J - C) : Infinity; // >= 1

                // colIndex has already been fixed on _processMetadata
                // 0 = discrete
                valuesColIndexes = def
                    .query(metadata)
                    .where(function(colDef, index) { return this._columnTypes[index] !== 0; }, this)
                    .select(function(colDef) { return colDef.colIndex; })
                    .take(Mmax)
                    .array()
                    ;

                M = valuesColIndexes.length;
            }
        }

        if(D == null) {
            // M wins over C
            D = J - M;
            if(D === 0) {
                S = C = 0;
            } else if(C != null) {
                if(C > D) {
                    C = D;
                    S = 0;
                } else {
                    S = D - C;
                }
            } else {
                // "Distribute" between categories and series
                // Categories have precedence.
                S = D > 1 ? 1 : 0;
                C = D - S;
            }
        }

        var seriesInRows = this.options.seriesInRows;
        var colGroupSpecs = [];
        if(D) {
            if(S && !seriesInRows) { colGroupSpecs.push({name: 'S', count: S}); }
            if(C                 ) { colGroupSpecs.push({name: 'C', count: C}); }
            if(S &&  seriesInRows) { colGroupSpecs.push({name: 'S', count: S}); }
        }

        if(M) { colGroupSpecs.push({name: 'M', count: M}); }

        var availableInputIndexes = def.range(0, J).array();

        // If valuesColIndexes != null, these are reserved for values
        if(valuesColIndexes) {
            // Remove these indexes from available indexes
            valuesColIndexes.forEach(function(inputIndex) {
                availableInputIndexes.splice(inputIndex, 1);
            });
        }

        // Set the fields with actual number of columns of each group
        // Assign the input indexes of each group (Layout)
        var specsByName = {};
        colGroupSpecs.forEach(function(groupSpec) {
            var count = groupSpec.count;
            var name  = groupSpec.name;

            // Index group by name
            specsByName[name] = groupSpec;

            if(valuesColIndexes && name === 'M') { groupSpec.indexes = valuesColIndexes; }
            else                                 { groupSpec.indexes = availableInputIndexes.splice(0, count); }
        });

        this.M = M;
        this.S = S;
        this.C = C;

        // Compose the total permutation array
        // that transforms the input into the virtual item "normal form":
        // S* C* M*
        var itemPerm = [];
        ['S', 'C', 'M'].forEach(function(name) {
            var groupSpec = specsByName[name];
            if(groupSpec) { def.array.append(itemPerm, groupSpec.indexes); }
        });

        this._itemInfos = itemPerm.map(this._buildItemInfoFromMetadata, this);

        // The start indexes of each column group
        this._itemCrossGroupIndex = {S: 0, C: this.S, M: this.S + this.C};

        this._itemPerm = itemPerm;
    },

    logVItem: function() {
        return this._logVItem(['S', 'C', 'M'], {S: this.S, C: this.C, M: this.M});
    },

    /**
     * Default relational mapping from virtual item to dimensions.
     * @override
     */
    _configureTypeCore: function() {
        var me = this;
        var index = 0;
        var dimsReaders = [];

        function add(dimGroupName, colGroupName, level, count) {
            var groupEndIndex = me._itemCrossGroupIndex[colGroupName] + count; // exclusive
            while(count > 0) {
                var dimName = pvc.buildIndexedId(dimGroupName, level);
                if(!me.complexTypeProj.isReadOrCalc(dimName)) { // Skip name if occupied and continue with next name

                    // use first available slot for auto dims readers as long as within the group slots
                    index = me._nextAvailableItemIndex(index);
                    if(index >= groupEndIndex) {
                        // this group has no more slots available
                        return;
                    }

                    dimsReaders.push({names: dimName, indexes: index});

                    index++; // consume index
                    count--;
                }

                level++;
            }
        }

        if(this.S > 0) { add('series',   'S', 0, this.S); }
        if(this.C > 0) { add('category', 'C', 0, this.C); }
        if(this.M > 0) { add('value',    'M', 0, this.M); }

        if(dimsReaders) { dimsReaders.forEach(this.defReader, this); }

        // ----
        // The null test is required because plot2DataSeriesIndexes can be a number, a string...
        var dataPartDimName = this.options.dataPartDimName;
        if(dataPartDimName && !this.complexTypeProj.isReadOrCalc(dataPartDimName)) {
            var plot2DataSeriesIndexes = this.options.plot2DataSeriesIndexes;
            if(plot2DataSeriesIndexes != null) {
                var seriesReader = this._userDimsReadersByDim.series;
                if(seriesReader) {
                    this._userRead(relTransl_dataPartGet.call(this, plot2DataSeriesIndexes, seriesReader), dataPartDimName);
                }
            }
        }
    },

    // Permutes the input rows
    _executeCore: function() {
        var dimsReaders = this._getDimensionsReaders();
        var permIndexes = this._itemPerm;

        return def.query(this._getItems())
                  .select(function(item) {
                      item = pv.permute(item, permIndexes);
                      return this._readItem(item, dimsReaders);
                  }, this);
    }
});

/**
 * Obtains the dimension reader for dimension 'dataPart'.
 *
 * @name pvc.data.RelationalTranslationOper#_dataPartGet
 * @function
 * @param {Array} plot2DataSeriesIndexes The indexes of series that are to be shown on the second axis.
 * @param {function} seriesReader Dimension series atom getter.
 * @type function
 */
function relTransl_dataPartGet(plot2DataSeriesIndexes, seriesReader) {
    var me = this;

    /* Defer calculation of plot2SeriesKeySet because *data* isn't yet available. */
    function calcAxis2SeriesKeySet() {
        var atoms = {};
        var seriesKeys = def.query(me.source)
                                .select(function(item){
                                    seriesReader(item, atoms);
                                    var value = atoms.series;
                                    if(value != null && value.v != null){
                                        value = value.v;
                                    }

                                    return value || null;
                                })
                                /* distinct excludes null keys */
                                .distinct()
                                .array();

        return me._createPlot2SeriesKeySet(plot2DataSeriesIndexes, seriesKeys);
    }

    return this._dataPartGet(calcAxis2SeriesKeySet, seriesReader);
}

/**
 * Initializes an atom instance.
 * 
 * @name pvc.data.Atom
 * 
 * @class An atom represents a unit of information.
 * 
 * <p>
 * To create an atom, 
 * call the corresponding dimension's
 * {@link pvc.data.Dimension#intern} method.
 * 
 * Usually this is done by a {@link pvc.data.TranslationOper}.
 * </p>
 * 
 * @property {pvc.data.Dimension} dimension The owner dimension.
 * 
 * @property {number} id
 *           A unique object identifier.
 *           
 * @property {any} rawValue The raw value from which {@link #value} is derived.
 *           <p>
 *           It is not always defined. 
 *           Values may be the result of
 *           combining multiple source values.
 *            
 *           Values may even be constant
 *           and, as such, 
 *           not be derived from 
 *           any of the source values.
 *           </p>
 * 
 * @property {any} value The typed value of the atom.
 *           It must be consistent with the corresponding {@link pvc.data.DimensionType#valueType}.
 * 
 * @property {string} label The formatted value.
 *           <p>
 *           Only the null atom can have a empty label.
 *           </p>
 *           
 * @property {string} key The value of the atom expressed as a
 *           string in a way that is unique amongst all atoms of its dimension.
 *           <p>
 *           Only the null atom has a key equal to "".
 *           </p>
 * @property {string} globalKey A semantic key that is unique across atoms of every dimensions.
 * 
 * @constructor
 * @private
 * @param {pvc.data.Dimension} dimension The dimension that the atom belongs to.
 * @param {any} value The typed value.
 * @param {string} label The formatted value.
 * @param {any} rawValue The source value.
 * @param {string} key The key.
 */
def.type('pvc.data.Atom')
.init(
function(dimension, value, label, rawValue, key) {
    this.dimension = dimension;
    this.id = (value == null ? -def.nextId() : def.nextId()); // Ensure null sorts first, when sorted by id
    this.value = value;
    this.label = label;
    if(rawValue !== undefined){
        this.rawValue = rawValue;
    }
    this.key = key;
})
.add( /** @lends pvc.data.Atom */{
    isVirtual: false,
    
    rawValue: undefined,

    /**
     * Obtains the label of the atom.
     */
    toString: function(){
        var label = this.label;
        if(label != null){
            return label;
        }
        
        label = this.value;
        return label != null ? ("" + label) : "";
    }
});


/**
 * Comparer for atom according to their id.
 */
function atom_idComparer(a, b) {
    return a.id - b.id; // works for numbers...
}

/**
 * Reverse comparer for atom according to their id.
 */
function atom_idComparerReverse(a, b) {
    return b.id - a.id; // works for numbers...
}

var complex_nextId = 1;

/**
 * Initializes a complex instance.
 *
 * @name pvc.data.Complex
 *
 * @class A complex is a set of atoms,
 *        of distinct dimensions,
 *        all owned by the same data.
 *
 * @property {number} id
 *           A unique object identifier.
 *
 * @property {number} key
 *           A semantic identifier.
 *
 * @property {pvc.data.Data} owner
 *           The owner data instance.
 *
 * @property {object} atoms
 *           A index of {@link pvc.data.Atom} by the name of their dimension type.
 *
 * @constructor
 * @param {pvc.data.Complex} [source]
 *        A complex that provides for an owner and default base atoms.
 *
 * @param {map(string any)} [atomsByName]
 *        A map of atoms or raw values by dimension name.
 *
 * @param {string[]} [dimNames] The dimension names of atoms in {@link atomsByName}.
 * The dimension names in this list will be used to build
 * the key and label of the complex.
 * When unspecified, all the dimensions of the associated complex type
 * will be used to create the key and label.
 * Null atoms are not included in the label.
 *
 * @param {object} [atomsBase]
 *        An object to serve as prototype to the {@link #atoms} object.
 *        <p>
 *        Atoms already present in this object are not set locally.
 *        The key and default label of a complex only contain information
 *        from its own atoms.
 *        </p>
 *        <p>
 *        The default value is the {@link #atoms} of the argument {@link source},
 *        when specified.
 *        </p>
 */
def
.type('pvc.data.Complex')
.init(function(source, atomsByName, dimNames, atomsBase, wantLabel, calculate) {
    /*jshint expr:true */

    /* NOTE: this function is a hot spot and as such is performance critical */

    this.id = complex_nextId++;

    var owner;
    if(source){
        owner = source.owner;
        if(!atomsBase){
            atomsBase = source.atoms;
        }
    }

    this.owner = owner || this;
    this.atoms = atomsBase ? Object.create(atomsBase) : {};

    var hadDimNames = !!dimNames;
    if(!dimNames){
        dimNames = owner.type._dimsNames;
    }

    var atomsMap = this.atoms;
    var D = dimNames.length;
    var i, dimName;

    if(atomsByName){
        /* Fill the atoms map */
        var ownerDims = owner._dimensions;

        var addAtom = function(dimName, value){
            var dimension = def.getOwn(ownerDims, dimName);
            if(value != null){ // nulls are already in base proto object
                var atom = dimension.intern(value);
                if(!atomsBase || atom !== atomsBase[dimName]) { // don't add atoms already in base proto object
                    atomsMap[dimName] = atom;
                }
            } else {
                // But need to make sure it is interned
                dimension.intern(null);
            }
        };

        if(!hadDimNames){
            for(dimName in atomsByName){
                addAtom(dimName, atomsByName[dimName]);
            }
        } else {
            for(i = 0 ; i < D ; i++){
                dimName = dimNames[i];
                addAtom(dimName, atomsByName[dimName]);
            }
        }

        if(calculate){
            var newAtomsByName = owner.type._calculate(this); // may be null
            for(dimName in newAtomsByName){
                if(!def.hasOwnProp.call(atomsMap, dimName)){ // not yet added
                    addAtom(dimName, newAtomsByName[dimName]);
                }
            }
        }
    }

    /* Build Key and Label */
    if(!D){
        this.value = null;
        this.key   = '';
        if(wantLabel){
            this.label = "";
        }
    } else if(D === 1){
        var singleAtom = atomsMap[dimNames[0]];
        this.value     = singleAtom.value;    // always typed when only one
        this.rawValue  = singleAtom.rawValue; // original
        this.key       = singleAtom.key;      // string
        if(wantLabel){
            this.label = singleAtom.label;
        }
    } else {
        var key, label;
        var labelSep = owner.labelSep;
        var keySep   = owner.keySep;

        for(i = 0 ; i < D ; i++){
            dimName = dimNames[i];
            var atom = atomsMap[dimName];

            // Add to key, null or not
            if(!i){
                key = atom.key;
            } else {
                key += keySep + atom.key;
            }

            // Add to label, when non-empty
            if(wantLabel){
                var atomLabel = atom.label;
                if(atomLabel){
                    if(!label){
                        label = atomLabel;
                    } else {
                        label += labelSep + atomLabel;
                    }
                }
            }
        }

        this.value = this.rawValue = this.key = key;
        if(wantLabel){
            this.label = label;
        }
    }
})
.add(/** @lends pvc.data.Complex# */{

    /**
     * The separator used between labels of dimensions of a complex.
     * Generally, it is the owner data's labelSep that is used.
     */
    labelSep: " ~ ",

    /**
     * The separator used between keys of dimensions of a complex,
     * to form a composite key or an absolute key.
     * Generally, it is the owner data's keySep that is used.
     */
    keySep: '~',

    label: null,

    rawValue: undefined,

    ensureLabel: function(){
        var label = this.label;
        if(label == null){
            label = "";
            var labelSep = this.owner.labelSep;
            def.eachOwn(this.atoms, function(atom){
                var alabel = atom.label;
                if(alabel){
                    if(label){
                        label += labelSep + alabel;
                    } else {
                        label = alabel;
                    }
                }
            });

            this.label = label;
        }

        return label;
    },

    view: function(dimNames){
        return new pvc.data.ComplexView(this, dimNames);
    },

    toString : function() {
       var s = [ '' + this.constructor.typeName ];

       if (this.index != null) {
           s.push("#" + this.index);
       }

       this.owner.type.dimensionsNames().forEach(function(name) {
           s.push(name + ": " + pvc.stringify(this.atoms[name].value));
       }, this);

       return s.join(" ");
   }
});

pvc.data.Complex.values = function(complex, dimNames){
    var atoms = complex.atoms;
    return dimNames.map(function(dimName){ return atoms[dimName].value; });
};

pvc.data.Complex.compositeKey = function(complex, dimNames){
    var atoms = complex.atoms;
    return dimNames
        .map(function(dimName){ return atoms[dimName].key; })
        .join(complex.owner.keySep);
};

pvc.data.Complex.labels = function(complex, dimNames){
    var atoms = complex.atoms;
    return dimNames.map(function(dimName){ return atoms[dimName].label; });
};

var complex_id = def.propGet('id');


/**
 * Initializes a complex view instance.
 * 
 * @name pvc.data.ComplexView
 * 
 * @class Represents a view of certain dimensions over a given source complex instance.
 * @extends pvc.data.Complex
 * 
 * @property {pvc.data.Complex} source The source complex instance.
 * @property {string} label The composite label of the own atoms in the view.
 * @constructor
 * @param {pvc.data.Complex} source The source complex instance.
 * @param {string[]} viewDimNames The dimensions that should be revealed by the view.
 */
def.type('pvc.data.ComplexView', pvc.data.Complex)
.init(function(source, viewDimNames){

    this.source = source;
    
    this.viewDimNames = viewDimNames;

    // Call base constructor
    this.base(source, source.atoms, viewDimNames, source.owner.atoms, /* wantLabel */ true);
})
.add({
    values: function(){
        return pvc.data.Complex.values(this, this.viewDimNames);
    },
    labels: function(){
        return pvc.data.Complex.labels(this, this.viewDimNames);
    }
});

/**
 * Initializes a datum instance.
 *
 * @name pvc.data.Datum
 *
 * @class A datum is a complex that contains atoms for all the
 * dimensions of the associated {@link #data}.
 *
 * @extends pvc.data.Complex
 *
 * @property {boolean} isNull Indicates if the datum is a null datum.
 * <p>
 * A null datum is a datum that doesn't exist in the data source,
 * but is created for auxiliary reasons (null pattern).
 * </p>
 *
 * @property {boolean} isSelected The datum's selected state (read-only).
 * @property {boolean} isVisible The datum's visible state (read-only).
 *
 * @constructor
 * @param {pvc.data.Data} data The data instance to which the datum belongs.
 * Note that the datum will belong instead to the owner of this data.
 * However the datums atoms will inherit from the atoms of the specified data.
 * This is essentially to facilitate the creation of null datums.
 * @param {map(string any)} [atomsByName] A map of atoms or raw values by dimension name.
 */
def.type('pvc.data.Datum', pvc.data.Complex)
.init(
function(data, atomsByName) {
    this.base(
        data,
        atomsByName,
        /*dimNames */ null,
        /*atomsBase*/ null,
        /*wantLabel*/ false,
        /*calculate*/ true);
})
.add(/** @lends pvc.data.Datum# */{

    isSelected: false,
    isVisible:  true,
    isNull:     false, // Indicates that all dimensions that are bound to a measure role are null.
    isVirtual:  false, // A datum that did not come in the original data (interpolated, trend)
    isTrend:    false,
    trendType:  null,
    isInterpolated: false,
    interpolation: null, // type of interpolation

    /**
     * Sets the selected state of the datum to a specified value.
     * @param {boolean} [select=true] The desired selected state.
     * @returns {boolean} true if the selected state changed, false otherwise.
     */
    setSelected: function(select) {
        // Null datums are always not selected
        if(this.isNull) { return false; }

        // Normalize 'select'
        select = (select == null) || !!select;

        var changed = this.isSelected !== select;
        if(changed) {
            if(!select) { delete this.isSelected; }
            else        { this.isSelected = true; }

            /*global data_onDatumSelectedChanged:true */
            data_onDatumSelectedChanged.call(this.owner, this, select);
        }

        return changed;
    },

    /**
     * Toggles the selected state of the datum.
     *
     * @type {undefined}
     */
    toggleSelected: function() { return this.setSelected(!this.isSelected); },

    /**
     * Sets the visible state of the datum to a specified value.
     *
     * @param {boolean} [visible=true] The desired visible state.
     *
     * @returns {boolean} true if the visible state changed, false otherwise.
     */
    setVisible: function(visible) {
        // Null datums are always visible
        if(this.isNull) { return false; }

        // Normalize 'visible'
        visible = (visible == null) || !!visible;

        var changed = this.isVisible !== visible;
        if(changed) {
            this.isVisible = visible;

            /*global data_onDatumVisibleChanged:true */
            data_onDatumVisibleChanged.call(this.owner, this, visible);
        }

        return changed;
    },

    /**
     * Toggles the visible state of the datum.
     *
     * @type {undefined}
     */
    toggleVisible: function() { return this.setVisible(!this.isVisible); }
});

/**
 * Called by the owner data to clear the datum's selected state (internal).
 * @name pvc.data.Datum#_deselect
 * @function
 * @type undefined
 * @private
 *
 * @see pvc.data.Data#clearSelected
 */
function datum_deselect() { delete this.isSelected; }

function datum_isNullOrSelected(d) { return d.isNull || d.isSelected; };

var datum_isSelected = def.propGet('isSelected');



/**
 * Initializes a dimension instance.
 * 
 * @name pvc.data.Dimension
 * 
 * @class A dimension holds unique atoms,
 * of a given dimension type,
 * and for a given data instance.
 *
 * @property {pvc.data.Data} data The data that owns this dimension.
 * @property {pvc.data.DimensionType} type The dimension type of this dimension.
 * @property {string} name Much convenient property with the name of {@link #type}.
 * 
 * @property {pvc.data.Dimension} parent The parent dimension.
 * A root dimension has a null parent.
 * 
 * @property {pvc.data.Dimension} linkParent The link parent dimension.
 * 
 * @property {pvc.data.Dimension} root The root dimension.
 * A root dimension has itself as the value of {@link #root}.
 * 
 * @property {pvc.data.Dimension} owner The owner dimension.
 * An owner dimension is the topmost root dimension (accessible from this one).
 * An owner dimension owns its atoms, while others simply contain them.
 * The value of {@link pvc.data.Atom#dimension} is an atom's <i>owner</i> dimension.
 * 
 * @constructor
 * 
 * @param {pvc.data.Data} data The data that owns this dimension.
 * @param {pvc.data.DimensionType} type The type of this dimension.
 */
def.type('pvc.data.Dimension')
.init(function(data, type){
    /* NOTE: this function is a hot spot and as such is performance critical */
    this.data  = data;
    this.type  = type;
    this.root  = this;
    this.owner = this;
    
    var name = type.name;
    
    this.name = name;
    
    // Cache
    // -------
    // The atom id comparer ensures we keep atoms in the order they were added, 
    //  even when no semantic comparer is provided.
    // This is important, at least, to keep the visible atoms cache in the correct order.
    this._atomComparer = type.atomComparer();
    this._atomsByKey = {};
    
    if(data.isOwner()){
        // Owner
        // Atoms are interned by #intern
        this._atoms = [];
        
        dim_createVirtualNullAtom.call(this);
        
    } else {
        // Not an owner
        var parentData = data.parent;
        
        var source; // Effective parent / atoms source
        if(parentData){
            // Not a root
            source = parentData._dimensions[name];
            dim_addChild.call(source, this);
            
            this.root = data.parent.root;
        } else {
            parentData = data.linkParent;
            // A root that is not topmost
            /*jshint expr:true */
            parentData || def.assert("Data must have a linkParent");
            
            source = parentData._dimensions[name];
            dim_addLinkChild.call(source, this);
        }
        
        // Not in _atomsKey
        this._nullAtom = this.owner._nullAtom; // may be null
        
        this._lazyInit = function(){ /* captures 'source' and 'name' variable */
            this._lazyInit = null;
            
            // Collect distinct atoms in data._datums
            var datums = this.data._datums;
            var L = datums.length;
            var atomsByKey = this._atomsByKey;
            for(var i = 0 ; i < L ; i++){
                // NOTE: Not checking if atom is already added,
                // but it has no adverse side-effect.
                var atom = datums[i].atoms[name];
                atomsByKey[atom.key] = atom;
            }
            
            // Filter parentEf dimension's atoms; keeps order.
            this._atoms = source.atoms().filter(function(atom){
                return def.hasOwnProp.call(atomsByKey, atom.key);
            });
        };
    }
})
.add(/** @lends pvc.data.Dimension# */{
    
    parent: null,
    
    linkParent: null,
    
    /**
     * The array of child dimensions.
     * @type pvc.data.Dimension[] 
     */
    _children: null,
    
    /**
     * The array of link child dimensions.
     * @type pvc.data.Dimension[] 
     */
    _linkChildren: null,
    
    /**
     * A map of the contained atoms by their {@link pvc.data.Atom#key} property.
     * 
     * Supports the intern(...), atom(.), and the control of the visible atoms cache.
     *
     * @type object
     */
    _atomsByKey: null,
    
    /**
     * A map of the count of visible datums per atom {@link pvc.data.Atom#key} property.
     *
     * @type object
     */
    _atomVisibleDatumsCount: null, 
    
    /** 
     * Indicates if the object has been disposed.
     * 
     * @type boolean
     * @private 
     */
    _disposed: false,

    /**
     * The atom with a null value.
     *
     * @type pvc.data.Atom
     * @private
     */
    _nullAtom: null,
    
    /**
     * The virtual null atom.
     *
     * <p>
     * This atom exists to resolve situations 
     * where a null atom does not exist in the loaded data.
     * When a null <i>datum</i> is built, it may not specify
     * all dimensions. When such an unspecified dimension
     * is accessed the virtual null atom is returned by 
     * lookup of the atoms prototype chain (see {@link pvc.data.Data#_atomsBase}.
     * </p>
     * 
     * @type pvc.data.Atom
     * @private
     */
    _virtualNullAtom: null,
    
    /**
     * Cache of sorted visible and invisible atoms.
     * A map from visible state to {@link pvc.data.Atom[]}.
     * <p>
     * Cleared whenever any atom's "visible state" changes.
     * </p>
     * 
     * @type object
     * @private
     */
    _visibleAtoms: null, 
    
    /**
     * Cache of sorted visible and invisible indexes.
     * A map from visible state to {@link number[]}.
     * <p>
     * Cleared whenever any atom's "visible state" changes.
     * </p>
     * 
     * @type object
     * @private
     */
    _visibleIndexes: null,
    
    /**
     * Cache of the dimension type's normal order atom comparer.
     * 
     * @type function
     * @private
     */
    _atomComparer: null,
    
    /**
     * The ordered array of contained atoms.
     * <p>
     * The special null atom, if existent, is the first item in the array.
     *</p>
     *<p>
     * On a child dimension it is a filtered version 
     * of the parent's array, 
     * and thus has the same atom relative order.
     * 
     * In a link child dimension it is copy
     * of the link parent's array.
     * </p>
     * 
     * @type pvc.data.Atom[]
     * @see #_nullAtom
     */
    _atoms: null,

    /**
     * An object with cached results of the {@link #sum} method.
     *
     * @type object
     */
    _sumCache: null,

    /**
     * Obtains the number of atoms contained in this dimension.
     * 
     * <p>
     * Consider calling this method on the root or owner dimension.
     * </p>
     *
     * @returns {Number} The number of contained atoms.
     *
     * @see pvc.data.Dimension#root
     * @see pvc.data.Dimension#owner
     */
    count: function(){
        if(this._lazyInit) { this._lazyInit(); }
        return this._atoms.length;
    },
    
    /**
     * Indicates if an atom belonging to this dimension 
     * is considered visible in it.
     * 
     * <p>
     * An atom is considered visible in a dimension
     * if there is at least one datum of the dimension's data
     * that has the atom and is visible.
     * </p>
     *
     * @param {pvc.data.Atom} atom The atom of this dimension whose visible state is desired.
     * 
     * @type boolean
     */
    isVisible: function(atom){
        if(this._lazyInit) { this._lazyInit(); }
        
        // <Debug>
        /*jshint expr:true */
        def.hasOwn(this._atomsByKey, atom.key) || def.assert("Atom must exist in this dimension.");
        // </Debug>
        
        return dim_getVisibleDatumsCountMap.call(this)[atom.key] > 0;
    },
    
    /**
     * Obtains the atoms contained in this dimension,
     * possibly filtered.
     * 
     * <p>
     * Consider calling this method on the root or owner dimension.
     * </p>
     * 
     * @param {Object} [keyArgs] Keyword arguments.
     * @param {boolean} [keyArgs.visible=null] 
     *      Only considers atoms that  
     *      have the specified visible state.
     * 
     * @returns {pvc.data.Atom[]} An array with the requested atoms.
     * Do <b>NOT</b> modify the returned array.
     * 
     * @see pvc.data.Dimension#root
     * @see pvc.data.Dimension#owner
     */
    atoms: function(keyArgs){
        if(this._lazyInit) { this._lazyInit(); }
        
        var visible = def.get(keyArgs, 'visible');
        if(visible == null){
            return this._atoms;
        }
        
        visible = !!visible;
        
        /*jshint expr:true */
        this._visibleAtoms || (this._visibleAtoms = {});
        
        return this._visibleAtoms[visible] || 
               (this._visibleAtoms[visible] = dim_calcVisibleAtoms.call(this, visible));
    },
    
    /**
     * Obtains the local indexes of all, visible or invisible atoms.
     * 
     * @param {Object} [keyArgs] Keyword arguments.
     * @param {boolean} [keyArgs.visible=null] 
     *      Only considers atoms that 
     *      have the specified visible state.
     * 
     * @type number[]
     */
    indexes: function(keyArgs){
        if(this._lazyInit) { this._lazyInit(); }
        
        var visible = def.get(keyArgs, 'visible');
        if(visible == null) {
            // Not used much so generate each time
            return pv.range(0, this._atoms.length);
        }
        
        visible = !!visible;
        
        /*jshint expr:true */
        this._visibleIndexes || (this._visibleIndexes = {});
        return this._visibleIndexes[visible] || 
               (this._visibleIndexes[visible] = dim_calcVisibleIndexes.call(this, visible));
    },
    
    /**
     * Obtains an atom that represents the specified value, if one exists.
     * 
     * @param {any} value A value of the dimension type's {@link pvc.data.DimensionType#valueType}.
     * 
     * @returns {pvc.data.Atom} The existing atom with the specified value, or null if there isn't one.
     */
    atom: function(value){
        if(value == null || value === '') {
            return this._nullAtom; // may be null
        }
        
        if(value instanceof pvc.data.Atom) {
            return value;
        }
        
        if(this._lazyInit) { this._lazyInit(); }

        var key = this.type._key ? this.type._key.call(null, value) : value;
        return this._atomsByKey[key] || null; // undefined -> null
    },
    
    /**
     * Obtains the minimum and maximum atoms of the dimension,
     * possibly filtered.
     * 
     * <p>
     * Assumes that the dimension type is comparable.
     * If not the result will coincide with "first" and "last".
     * </p>
     * 
     * <p>
     * Does not consider the null atom.
     * </p>
     * 
     * <p>
     * Consider calling this method on the root or owner dimension.
     * </p>
     * 
     * @param {object} [keyArgs] Keyword arguments.
     * See {@link #atoms} for additional keyword arguments. 
     * @param {boolean} [keyArgs.abs=false] Determines if the extent should consider the absolute value.
     * 
     * @returns {object} 
     * An extent object with 'min' and 'max' properties, 
     * holding the minimum and the maximum atom, respectively,
     * if at least one atom satisfies the selection;
     * undefined otherwise.
     * 
     * @see #root
     * @see #owner
     * @see #atoms
     * @see pvc.data.DimensionType.isComparable
     */
    extent: function(keyArgs){
        // Assumes atoms are sorted (null, if existent is the first).
        var atoms  = this.atoms(keyArgs);
        var L = atoms.length;
        if(!L){ return undefined; }
        
        var offset = this._nullAtom && atoms[0].value == null ? 1 : 0;
        var countWithoutNull = L - offset;
        if(countWithoutNull > 0){
            var min = atoms[offset];
            var max = atoms[L - 1];
            
            // ------------------
            var tmp;
            if(min !== max && def.get(keyArgs, 'abs', false)){
                var minSign = min.value < 0 ? -1 : 1;
                var maxSign = max.value < 0 ? -1 : 1;
                if(minSign === maxSign){
                    if(maxSign < 0){
                        tmp = max;
                        max = min;
                        min = tmp;
                    }
                } else if(countWithoutNull > 2){
                    // There's a third atom in between
                    // min is <= 0
                    // max is >= 0
                    // and, of course, min !== max
                    
                    // One of min or max has the biggest abs value
                    if(max.value < -min.value){
                        max = min;
                    }
                    
                    // The smallest atom is the one in atoms that is closest to 0, possibly 0 itself
                    var zeroIndex = def.array.binarySearch(atoms, 0, this.type.comparer(), function(a){ return a.value; });
                    if(zeroIndex < 0){
                        zeroIndex = ~zeroIndex;
                        // Not found directly. 
                        var negAtom = atoms[zeroIndex - 1];
                        var posAtom = atoms[zeroIndex];
                        if(-negAtom.value < posAtom.value){
                            min = negAtom;
                        } else {
                            min = posAtom;
                        }
                    } else {
                        // Zero was found
                        // It is the minimum
                        min = atoms[zeroIndex];
                    }
                } else if(max.value < -min.value){
                    // min is <= 0
                    // max is >= 0
                    // and, of course, min !== max
                    tmp = max;
                    max = min;
                    min = tmp;
                }
            }
            
            // -----------------
            
            return {min: min, max: max};
        }
        
        return undefined;
    },
    
    /**
     * Obtains the minimum atom of the dimension,
     * possibly after filtering.
     * 
     * <p>
     * Assumes that the dimension type is comparable.
     * If not the result will coincide with "first".
     * </p>
     * 
     * <p>
     * Does not consider the null atom.
     * </p>
     * 
     * <p>
     * Consider calling this method on the root or owner dimension.
     * </p>
     * 
     * @param {object} [keyArgs] Keyword arguments.
     * See {@link #atoms} for a list of available filtering keyword arguments. 
     *
     * @returns {pvc.data.Atom} The minimum atom satisfying the selection;
     * undefined if none.
     * 
     * @see #root
     * @see #owner
     * @see #atoms
     * @see pvc.data.DimensionType.isComparable
     */
    min: function(keyArgs){
        // Assumes atoms are sorted.
        var atoms = this.atoms(keyArgs);
        var L = atoms.length;
        if(!L){ return undefined; }
        
        var offset = this._nullAtom && atoms[0].value == null ? 1 : 0;
        return (L > offset) ? atoms[offset] : undefined;
    },
    
    /**
     * Obtains the maximum atom of the dimension,
     * possibly after filtering.
     * 
     * <p>
     * Assumes that the dimension type is comparable.
     * If not the result will coincide with "last".
     * </p>
     * 
     * <p>
     * Does not consider the null atom.
     * </p>
     * 
     * <p>
     * Consider calling this method on the root or owner dimension.
     * </p>
     * 
     * @param {object} [keyArgs] Keyword arguments.
     * See {@link #atoms} for a list of available filtering keyword arguments. 
     *
     * @returns {pvc.data.Atom} The maximum atom satisfying the selection;
     * undefined if none.
     * 
     * @see #root
     * @see #owner
     * @see #atoms
     * 
     * @see pvc.data.DimensionType.isComparable
     */
    max: function(keyArgs){
        // Assumes atoms are sorted.
        var atoms = this.atoms(keyArgs);
        var L = atoms.length;
        
        return L && atoms[L - 1].value != null ? atoms[L - 1] : undefined;
    },
    
    /**
     * Obtains the sum of this dimension's values over all datums of the data,
     * possibly after filtering.
     * 
     * <p>
     * Assumes that the dimension type {@link pvc.data.DimensionType#valueType} is "Number".
     * </p>
     * 
     * <p>
     * Does not consider the null atom.
     * </p>
     * 
     * @param {object} [keyArgs] Keyword arguments.
     * See {@link pvc.data.Data#datums} for a list of available filtering keyword arguments. 
     *
     * @param {boolean} [keyArgs.abs=false] Indicates if it is the sum of the absolute value that is desired.
     * @param {boolean} [keyArgs.zeroIfNone=true] Indicates that zero should be returned when there are no datums
     * or no datums with non-null values.
     * When <tt>false</tt>, <tt>null</tt> is returned, in that situation.
     *
     * @returns {number} The sum of considered datums or <tt>0</tt> or <tt>null</tt>, if none.
     * 
     * @see #root
     * @see #owner
     * @see #atoms
     */
    sum: function(keyArgs){
        var isAbs = !!def.get(keyArgs, 'abs', false),
            zeroIfNone = def.get(keyArgs, 'zeroIfNone', true),
            key   = dim_buildDatumsFilterKey(keyArgs) + ':' + isAbs;
              
        var sum = def.getOwn(this._sumCache, key);
        if(sum === undefined) {
            var dimName = this.name;
            sum = this.data.datums(null, keyArgs).reduce(function(sum2, datum){
                var value = datum.atoms[dimName].value;
                if(isAbs && value < 0){ // null < 0 is false
                    value = -value;
                }

                return sum2 != null ? (sum2 + value) : value; // null preservation
            },
            null);
            
            (this._sumCache || (this._sumCache = {}))[key] = sum;
        }
        
        return zeroIfNone ? (sum || 0) : sum;
    },
    
    /**
     * Obtains the percentage of a specified atom or value,
     * over the <i>sum</i> of the absolute values of a specified datum set.
     * 
     * <p>
     * Assumes that the dimension type {@link pvc.data.DimensionType#valueType} is "Number".
     * </p>
     * 
     * <p>
     * Does not consider the null atom.
     * </p>
     * 
     * @param {pvc.data.Atom|any} [atomOrValue] The atom or value on which to calculate the percent.
     * 
     * @param {object} [keyArgs] Keyword arguments.
     * See {@link pvc.data.Dimension#sum} for a list of available filtering keyword arguments. 
     *
     * @returns {number} The calculated percentage.
     * 
     * @see #root
     * @see #owner
     */
    percent: function(atomOrValue, keyArgs){
        var value = (atomOrValue instanceof pvc.data.Atom) ? atomOrValue.value : atomOrValue;
        if(!value) { // nully or zero
            return 0;
        }
        // if value != 0 => sum != 0, but JIC, we test for not 0...
        var sum = this.sum(def.create(keyArgs, {abs: true}));
        return sum ? (Math.abs(value) / sum) : 0;
    },
    
    /**
     * Obtains the percentage of the local <i>sum</i> of a specified selection,
     * over the <i>sum</i> of the absolute values of an analogous selection in the parent data.
     * 
     * <p>
     * Assumes that the dimension type {@link pvc.data.DimensionType#valueType} is "Number".
     * </p>
     * 
     * <p>
     * Does not consider the null atom.
     * </p>
     * 
     * @param {object} [keyArgs] Keyword arguments.
     * See {@link pvc.data.Dimension#sum} for a list of available filtering keyword arguments. 
     *
     * @returns {number} The calculated percentage.
     * 
     * @see #root
     * @see #owner
     */
    percentOverParent: function(keyArgs){
        var value = this.sum(keyArgs); // normal sum
        if(!value) { // nully or zero
            return 0;
        }
        
        // if no parent, we're the root and so we're 100%
        var parentData = this.data.parent;
        if(!parentData) {
            return 1;
        }

        // The following would not work because, in each group,
        //  abs would not be used...
        //var sum = parentData.dimensions(this.name).sum();

        var sum = parentData.dimensionsSumAbs(this.name, keyArgs);

        return sum ? (Math.abs(value) / sum) : 0;
    },
    
    
    format: function(value, sourceValue) {
        return "" + (this.type._formatter ? this.type._formatter.call(null, value, sourceValue) : "");
    },
    
    /**
     * Obtains an atom that represents the specified sourceValue,
     * creating one if one does not yet exist.
     * 
     * <p>
     * Used by a translation to 
     * obtain atoms of a dimension for raw values of source items.
     * </p>
     * <p>
     * If this method is not called on an owner dimension,
     * and if the requested values isn't locally present,
     * the call is recursively forwarded to the dimension's
     * parent or link parent until the atom is found.
     * Ultimately, if the atom does not yet exist, 
     * it is created in the owner dimension. 
     * </p>
     * <p>
     * An empty string value is considered equal to a null value. 
     * </P>
     * @param {any | pvc.data.Atom} sourceValue The source value.
     * @param {boolean} [isVirtual=false] Indicates that 
     * the (necessarily non-null) atom is the result of interpolation or regression.
     * 
     * @type pvc.data.Atom
     */
    intern: function(sourceValue, isVirtual) {
        // NOTE: This function is performance critical!
      
        // The null path and the existing atom path 
        // are as fast and direct as possible
        
        // - NULL -
        if(sourceValue == null || sourceValue === '') {
            return this._nullAtom || dim_createNullAtom.call(this, sourceValue);
        }
        
        if(sourceValue instanceof pvc.data.Atom) {
            if(sourceValue.dimension !== this) {
                throw def.error.operationInvalid("Atom is of a different dimension.");
            }
            
            return sourceValue;
        }
        
        var value, label;
        var type = this.type;
        
        // Is google table style cell {v: , f: } ?
        if(typeof sourceValue === 'object' && ('v' in sourceValue)) {
            // Get info and get rid of the cell
            label = sourceValue.f;
            sourceValue = sourceValue.v;
            if(sourceValue == null || sourceValue === '') {
                // Null
                return this._nullAtom || dim_createNullAtom.call(this);
            }
        }
        
        // - CONVERT - 
        if(!isVirtual) {
            var converter = type._converter;
            if(!converter) {
                value = sourceValue;
            } else {
                value = converter(sourceValue);
                if(value == null || value === '') {
                    // Null after all
                    return this._nullAtom || dim_createNullAtom.call(this, sourceValue);
                }
           }
        } else {
            value = sourceValue;
        }
        
        // - CAST -
        // Any cast function?
        var cast = type.cast;
        if(cast) {
            value = cast(value);
            if(value == null || value === '') {
                // Null after all (normally a cast failure)
                return this._nullAtom || dim_createNullAtom.call(this);
            }
        }
        
        // - KEY -
        var keyFun = type._key;
        var key = '' + (keyFun ? keyFun(value) : value);
        // <Debug>
        /*jshint expr:true */
        key || def.fail.operationInvalid("Only a null value can have an empty key.");
        // </Debug>
        
        // - ATOM -
        var atom = this._atomsByKey[key];
        if(atom) {
            if(!isVirtual && atom.isVirtual) { delete atom.isVirtual; }
            return atom;
        }
        
        return dim_createAtom.call(
                   this,
                   type,
                   sourceValue,
                   key,
                   value,
                   label,
                   isVirtual);
    },
    
    read: function(sourceValue, label){
        // - NULL -
        if(sourceValue == null || sourceValue === '') { return null; }
        
        var value;
        var type = this.type;
        
        // Is google table style cell {v: , f: } ?
        if(typeof sourceValue === 'object' && ('v' in sourceValue)) {
            // Get info and get rid of the cell
            label = sourceValue.f;
            sourceValue = sourceValue.v;
            if(sourceValue == null || sourceValue === '') { return null; }
        }
        
        // - CONVERT - 
        var converter = type._converter;
        value = converter ? converter(sourceValue) : sourceValue;
        if(value == null || value === '') { return null; }
        
        // - CAST -
        // Any cast function?
        var cast = type.cast;
        if(cast) {
            value = cast(value);
            // Null after all? 
            // (normally a cast failure)
            if(value == null || value === '') { return null; }
        }
        
        // - KEY -
        var keyFun = type._key;
        var key = '' + (keyFun ? keyFun(value) : value);
        
        // - ATOM -
        var atom = this._atomsByKey[key];
        if(atom) {
            return {
                rawValue: sourceValue,
                key:      key,
                value:    atom.value,
                label:    '' + (label == null ? atom.label : label)
            };
        }
        
        // - LABEL -
        if(label == null) {
            var formatter = type._formatter;
            label = formatter ? formatter(value, sourceValue) : value;
        }

        label = "" + label; // J.I.C.
        
        return {
            rawValue: sourceValue,
            key:      key,
            value:    value,
            label:    label
        };
    },
    
    /**
     * Disposes the dimension and all its children.
     */
    dispose: function(){
        if(!this._disposed){
            /*global data_disposeChildList:true */
            data_disposeChildList(this._children,     'parent');
            data_disposeChildList(this._linkChildren, 'linkParent');
            
            // myself
            
            if(this.parent)     { dim_removeChild.call(this.parent, this); }
            if(this.linkParent) { dim_removeLinkChild.call(this.linkParent, this); }
            
            dim_clearVisiblesCache.call(this);
            
            this._lazyInit  = null;
            
            this._atoms = 
            this._nullAtom = 
            this._virtualNullAtom = null;
            
            this._disposed = true;
        }
    }
});

/**
 * Creates an atom, 
 * in the present dimension if it is the owner dimension,
 * or delegates the creation to its parent, or linked parent dimension.
 * 
 * The atom must not exist in the present dimension.
 * 
 * @name pvc.data.Dimension#_createAtom
 * @function
 * @param {pvc.data.DimensionType} type The dimension type of this dimension.
 * @param {any} sourceValue The source value.
 * @param {string} key The key of the value.
 * @param {any} value The typed value.
 * @param {string} [label] The label, if it is present directly
 * in {@link sourceValue}, in Google format.
 * @type pvc.data.Atom
 */
function dim_createAtom(type, sourceValue, key, value, label, isVirtual){
    var atom;
    if(this.owner === this){
        // Create the atom
        
        // - LABEL -
        if(label == null){
            var formatter = type._formatter;
            if(formatter){
                label = formatter(value, sourceValue);
            } else {
                label = value;
            }
        }

        label = "" + label; // J.I.C.
        
        if(!label && pvc.debug >= 2){
            pvc.log("Only the null value should have an empty label.");
        }
        
        // - ATOM! -
        atom = new pvc.data.Atom(this, value, label, sourceValue, key);
        if(isVirtual){
            atom.isVirtual = true;
        }
    } else {
        var source = this.parent || this.linkParent;
        atom = source._atomsByKey[key] ||
               dim_createAtom.call(
                    source, 
                    type, 
                    sourceValue, 
                    key, 
                    value, 
                    label,
                    isVirtual);
    }
        
    // Insert atom in order (or at the end when !_atomComparer)
    def.array.insert(this._atoms, atom, this._atomComparer);
    
    dim_clearVisiblesCache.call(this);
    
    this._atomsByKey[key] = atom;
    
    return atom;
}

/**
 * Ensures that the specified atom exists in this dimension.
 * The atom must have been created in a dimension of this dimension tree.
 * 
 * If the virtual null atom is found it is replaced by the null atom,
 * meaning that, after all, the null is really present in the data.
 * 
 * @param {pvc.data.Atom} atom the atom to intern.
 * 
 * @name pvc.data.Dimension#_internAtom
 * @function
 * @type pvc.data.Atom
 */
function dim_internAtom(atom){
    var key = atom.key;
    
    // Root load will fall in this case
    if(atom.dimension === this){
        /*jshint expr:true */
        (this.owner === this) || def.assert("Should be an owner dimension");
        
        if(!key && atom === this._virtualNullAtom){
            /* This indicates that there is a dimension for which 
             * there was no configured reader, 
             * so nulls weren't read.
             * 
             * We will register the real null, 
             * and the virtual null atom will not show up again,
             * because it appears through the prototype chain
             * as a default value.
             */
            atom = this.intern(null);
        }
        
        return atom;
    }
    
    if(!this._lazyInit){
        // Else, not yet initialized, so there's no need to add the atom now
        var localAtom = this._atomsByKey[key];
        if(localAtom){
            if(localAtom !== atom){
                throw def.error.operationInvalid("Atom is from a different root data.");
            }
            
            return atom;
        }
        
        if(this.owner === this) {
            // Should have been created in a dimension along the way.
            throw def.error.operationInvalid("Atom is from a different root data.");
        }
    }
    
    dim_internAtom.call(this.parent || this.linkParent, atom);
    
    if(!this._lazyInit){
        // Insert atom in order (or at the end when !_atomComparer)
        this._atomsByKey[key] = atom;
        
        if(!key){
            this._nullAtom = atom;
            this._atoms.unshift(atom);
        } else {
            def.array.insert(this._atoms, atom, this._atomComparer);
        }
        
        dim_clearVisiblesCache.call(this);
    }
    
    return atom;
}

/**
 * Builds a key string suitable for identifying a call to {@link pvc.data.Data#datums}
 * with no where specification.
 *
 * @name pvc.data.Dimension#_buildDatumsFilterKey
 * @function
 * @param {object} [keyArgs] The keyword arguments used in the call to {@link pvc.data.Data#datums}.
 * @type string
 */
function dim_buildDatumsFilterKey(keyArgs){
    var visible  = def.get(keyArgs, 'visible'),
        selected = def.get(keyArgs, 'selected');
    return (visible == null ? null : !!visible) + ':' + (selected == null ? null : !!selected);
}

/**
 * Creates the null atom if it isn't created yet.
 * 
 * @name pvc.data.Dimension#_createNullAtom
 * @function
 * @param {any} [sourceValue] The source value of null. Can be used to obtain the null format.
 * @type undefined
 * @private
 */
function dim_createNullAtom(sourceValue){
    var nullAtom = this._nullAtom;
    if(!nullAtom){
        if(this.owner === this){
            var typeFormatter = this.type._formatter;
            var label = "" + (typeFormatter ? typeFormatter.call(null, null, sourceValue) : "");
            
            nullAtom = new pvc.data.Atom(this, null, label, null, '');
            
            this.data._atomsBase[this.name] = nullAtom; 
        } else {
            // Recursively set the null atom, up the parent/linkParent chain
            // until reaching the owner (root) dimension.
            nullAtom = dim_createNullAtom.call(this.parent || this.linkParent, sourceValue);
        }
        
        this._atomsByKey[''] = this._nullAtom = nullAtom;
        
        // The null atom is always in the first position
        this._atoms.unshift(nullAtom);
    }
    
    return nullAtom;
}

/**
 * Creates the virtual null atom if it isn't created yet.
 * 
 * @name pvc.data.Dimension#_createNullAtom
 * @function
 * @type undefined
 * @private
 */
function dim_createVirtualNullAtom(){
    // <Debug>
    /*jshint expr:true */
    (this.owner === this) || def.assert("Can only create atoms on an owner dimension.");
    // </Debug>
    
    if(!this._virtualNullAtom){
        // The virtual null's label is always "".
        // Don't bother the formatter with a value that
        // does not exist in the data.
        this._virtualNullAtom = new pvc.data.Atom(this, null, "", null, '');

        this.data._atomsBase[this.name] = this._virtualNullAtom; 
    }
    
    return this._virtualNullAtom;
}

/**
 * Uninternalizes the specified atom from the dimension (internal).
 * 
 * @name pvc.data.Dimension#_unintern
 * @function
 * @param {pvc.data.Atom} The atom to uninternalize.
 * @type undefined
 * @private
 * @internal
 */
function dim_unintern(atom){
    // <Debug>
    /*jshint expr:true */
    (this.owner === this) || def.assert("Can only unintern atoms on an owner dimension.");
    (atom && atom.dimension === this) || def.assert("Not an interned atom");
    // </Debug>
    
    if(atom === this._virtualNullAtom){
        return;
    }
    
    // Remove the atom
    var key = atom.key;
    if(this._atomsByKey[key] === atom){
        def.array.remove(this._atoms, atom, this._atomComparer);
        delete this._atomsByKey[key];
        
        if(!key){
            delete this._nullAtom;
            this.data._atomsBase[this.name] = this._virtualNullAtom;
        }
    }
    
    dim_clearVisiblesCache.call(this);
}

function dim_uninternUnvisitedAtoms(){
    // <Debug>
    /*jshint expr:true */
    (this.owner === this) || def.assert("Can only unintern atoms of an owner dimension.");
    // </Debug>
    
    var atoms = this._atoms;
    if(atoms){
        var atomsByKey = this._atomsByKey;
        var i = 0;
        var L = atoms.length;
        while(i < L){ 
            var atom = atoms[i];
            if(atom.visited){
                delete atom.visited;
                i++;
            } else if(atom !== this._virtualNullAtom) {
                // Remove the atom
                atoms.splice(i, 1);
                L--;
                
                var key = atom.key;
                delete atomsByKey[key];
                if(!key){
                    delete this._nullAtom;
                    this.data._atomsBase[this.name] = this._virtualNullAtom;
                }
            }
        }
        
        dim_clearVisiblesCache.call(this);
    }
}

function dim_uninternVirtualAtoms(){
    // This assumes that this same function has been called on child/link child dimensions
    var atoms = this._atoms;
    if(atoms){
        var atomsByKey = this._atomsByKey;
        var i = 0;
        var L = atoms.length;
        var removed;
        while(i < L){ 
            var atom = atoms[i];
            if(!atom.isVirtual){
                i++;
            } else {
                // Remove the atom
                atoms.splice(i, 1);
                L--;
                removed = true;
                var key = atom.key || def.assert("Cannot be the null or virtual null atom.");
                delete atomsByKey[key];
            }
        }
        
        if(removed){
            dim_clearVisiblesCache.call(this);
        }
    }
}

/**
 * Clears all caches affected by datum/atom visibility.
 * 
 * @name pvc.data.Dimension#_clearVisiblesCache
 * @function
 * @type undefined
 * @private
 * @internal
 */
function dim_clearVisiblesCache(){
    this._atomVisibleDatumsCount =
    this._sumCache =
    this._visibleAtoms = 
    this._visibleIndexes = null;
}

/**
 * Called by a dimension's data when its datums have changed.
 * 
 * @name pvc.data.Dimension#_onDatumsChanged
 * @function
 * @type undefined
 * @private
 * @internal
 */
function dim_onDatumsChanged(){
    dim_clearVisiblesCache.call(this);
}

/**
 * Adds a child dimension.
 * 
 * @name pvc.data.Dimension#_addChild
 * @function
 * @param {pvc.data.Dimension} child The child to add.
 * @type undefined
 * @private
 */
function dim_addChild(child){
    /*global data_addColChild:true */
    data_addColChild(this, '_children', child, 'parent');
    
    child.owner = this.owner;
}

/**
 * Removes a child dimension.
 *
 * @name pvc.data.Dimension#_removeChild
 * @function
 * @param {pvc.data.Dimension} child The child to remove.
 * @type undefined
 * @private
 */
function dim_removeChild(child){
    /*global data_removeColChild:true */
    data_removeColChild(this, '_children', child, 'parent');
}

/**
 * Adds a link child dimension.
 * 
 * @name pvc.data.Dimension#_addLinkChild
 * @function
 * @param {pvc.data.Dimension} child The link child to add.
 * @type undefined
 * @private
 */
function dim_addLinkChild(linkChild){
    data_addColChild(this, '_linkChildren', linkChild, 'linkParent');
    
    linkChild.owner = this.owner;
}

/**
 * Removes a link child dimension.
 *
 * @name pvc.data.Dimension#_removeLinkChild
 * @function
 * @param {pvc.data.Dimension} linkChild The child to remove.
 * @type undefined
 * @private
 */
function dim_removeLinkChild(linkChild){
    data_removeColChild(this, '_linkChildren', linkChild, 'linkParent');
}

/**
 * Called by the data of this dimension when 
 * the visible state of a datum has changed. 
 * 
 * @name pvc.data.Dimension#_onDatumVisibleChanged
 * @function
 * @type undefined
 * @private
 * @internal
 */
function dim_onDatumVisibleChanged(datum, visible) {
    var map;
    if(!this._disposed && (map = this._atomVisibleDatumsCount)) {
        var atom = datum.atoms[this.name],
            key = atom.key;
        
        // <Debug>
        /*jshint expr:true */
        def.hasOwn(this._atomsByKey, key) || def.assert("Atom must exist in this dimension.");
        // </Debug>
        
        var count = map[key];
        
        // <Debug>
        (visible || (count > 0)) || def.assert("Must have had accounted for at least one visible datum."); 
        // </Debug>
        
        map[key] = (count || 0) + (visible ? 1 : -1);
        
        // clear dependent caches
        this._visibleAtoms =
        this._sumCache = 
        this._visibleIndexes = null;
    }
}

/**
 * Obtains the map of visible datums count per atom, 
 * creating the map if necessary.
 * 
 * @name pvc.data.Dimension#_getVisibleDatumsCountMap
 * @function
 * @type undefined
 * @private
 */
function dim_getVisibleDatumsCountMap() {
    var map = this._atomVisibleDatumsCount;
    if(!map) {
        map = {};
        
        this.data.datums(null, {visible: true}).each(function(datum){
            var atom = datum.atoms[this.name],
                key  = atom.key;
            map[key] = (map[key] || 0) + 1;
        }, this);
        
        this._atomVisibleDatumsCount = map;
    }
    
    return map;
}

/**
 * Calculates the list of indexes of visible or invisible atoms.
 * <p>
 * Does not include the null atom.
 * </p>
 * 
 * @name pvc.data.Dimension#_calcVisibleIndexes
 * @function
 * @param {boolean} visible The desired atom visible state.
 * @type number[]
 * @private
 */
function dim_calcVisibleIndexes(visible){
    var indexes = [];
    
    this._atoms.forEach(function(atom, index){
        if(this.isVisible(atom) === visible) {
            indexes.push(index);
        }
    }, this);
    
    return indexes;
}

/**
 * Calculates the list of visible or invisible atoms.
 * <p>
 * Does not include the null atom.
 * </p>
 * 
 * @name pvc.data.Dimension#_calcVisibleAtoms
 * @function
 * @param {boolean} visible The desired atom visible state.
 * @type number[]
 * @private
 */
function dim_calcVisibleAtoms(visible){
    return def.query(this._atoms)
            .where(function(atom){ return this.isVisible(atom) === visible; }, this)
            .array();
}

/**
 * Initializes a data instance.
 * 
 * @name pvc.data.Data
 * 
 * @class A data represents a set of datums of the same complex type {@link #type}.
 * <p>
 * A data <i>may</i> have a set of atoms that are shared by all of its datums. 
 * In that case, the {@link #atoms} property holds those atoms.
 * </p>
 * <p>
 * A data has one dimension per dimension type of the complex type {@link #type}.
 * Each holds information about the atoms of it's type in this data.
 * Dimensions are obtained by calling {@link #dimensions}.
 * </p>
 * <p>
 * A data may have child data instances.
 * </p>
 * 
 * @extends pvc.data.Complex
 * 
 * @borrows pv.Dom.Node#visitBefore as #visitBefore
 * @borrows pv.Dom.Node#visitAfter as #visitAfter
 * 
 * @borrows pv.Dom.Node#nodes as #nodes
 * @borrows pv.Dom.Node#firstChild as #firstChild
 * @borrows pv.Dom.Node#lastChild as #lastChild
 * @borrows pv.Dom.Node#previousSibling as #previousSibling
 * @borrows pv.Dom.Node#nextSibling as #nextSibling
 * 
 * @property {pvc.data.ComplexType} type The type of the datums of this data.
 * 
 * @property {pvc.data.Data} root The root data. 
 * The {@link #root} of a root data is itself.
 * 
 * @property {pvc.data.Data} parent The parent data. 
 * A root data has a no parent.
 * 
 * @property {pvc.data.Data} linkParent The link parent data.
 * 
 * @property {Number} depth The depth of the data relative to its root data.
 * @property {string} label The composite label of the (common) atoms in the data.
 * 
 * @property {string} absLabel The absolute label of the data; 
 * a composition of all labels up to the root data.
 * 
 * @property {number} absKey
 *           The absolute semantic identifier;
 *           a composition of all keys up to the root data.
 * 
 * @constructor
 * @param {object} keyArgs Keyword arguments
 * @param {pvc.data.Data}   [keyArgs.parent]      The parent data.
 * @param {pvc.data.Data}   [keyArgs.linkParent]  The link parent data.
 * @param {map(string union(any pvc.data.Atom))} [keyArgs.atoms] The atoms shared by contained datums.
 * @param {string[]} [keyArgs.dimNames] The dimension names of atoms in {@link keyArgs.atoms}.
 * This argument must be specified whenever {@link keyArgs.atoms} is.
 * @param {pvc.data.Datum[]|def.Query} [keyArgs.datums] The contained datums array or enumerable.
 * @param {pvc.data.Data}    [keyArgs.owner] The owner data.
 * The topmost root data is its own owner.
 * An intermediate root data must specify its owner data.
 * 
 * @param {pvc.data.ComplexType} [keyArgs.type] The complex type.
 * Required when no parent or owner are specified.
 * 
 * @param {number} [index=null] The index at which to insert the child in its parent or linked parent.
 */
def.type('pvc.data.Data', pvc.data.Complex)
.init(function(keyArgs){
    /* NOTE: this function is a hot spot and as such is performance critical */
    
    /*jshint expr:true*/
    keyArgs || def.fail.argumentRequired('keyArgs');
    
    this._visibleNotNullDatums = new def.Map();
    
    var owner,
        atoms,
        atomsBase,
        dimNames,
        datums,
        index,
        parent = this.parent = keyArgs.parent || null;
    if(parent) {
        // Not a root
        this.root  = parent.root;
        this.depth = parent.depth + 1;
        this.type  = parent.type;
        datums     = keyArgs.datums || def.fail.argumentRequired('datums');
        
        owner = parent.owner;
        atoms     = keyArgs.atoms   || def.fail.argumentRequired('atoms');
        dimNames  = keyArgs.dimNames|| def.fail.argumentRequired('dimNames');
        atomsBase = parent.atoms;
    } else {
        // Root (topmost or not)
        this.root = this;
        // depth = 0
        
        dimNames = [];
        
        var linkParent = keyArgs.linkParent || null;
        if(linkParent){
            // A root that is not topmost - owned, linked
            owner = linkParent.owner;
            //atoms = pv.values(linkParent.atoms); // is atomsBase, below
            
            this.type   = owner.type;
            datums      = keyArgs.datums || def.fail.argumentRequired('datums');//linkParent._datums.slice();
            this._leafs = [];
            
            /* 
             * Inherit link parent atoms.
             */
            atomsBase = linkParent.atoms;
            //atoms = null
            
            index = def.get(keyArgs, 'index', null);
            
            data_addLinkChild.call(linkParent, this, index);
        } else {
            // Topmost root - an owner
            owner = this;
            //atoms = null
            atomsBase = {};
            
            if(keyArgs.labelSep) { this.labelSep = keyArgs.labelSep; }
            if(keyArgs.keySep  ) { this.keySep   = keyArgs.keySep;   }
            
            this.type = keyArgs.type || def.fail.argumentRequired('type');
            
            // Only owner datas cache selected datums
            this._selectedNotNullDatums = new def.Map();
        }
    }
    
    /*global data_setDatums:true */
    if(datums){
        data_setDatums.call(this, datums);
    }
    
    // Must anticipate setting this (and not wait for the base constructor)
    // because otherwise new Dimension( ... ) fails.
    this.owner = owner;
    
    /* Need this because of null interning/un-interning and atoms chaining */
    this._atomsBase = atomsBase;
    
    this._dimensions = {};
    this.type.dimensionsList().forEach(this._initDimension, this);
    
    // Call base constructors
    this.base(owner, atoms, dimNames, atomsBase, /* wantLabel */ true);
    
    pv.Dom.Node.call(this, /* nodeValue */null);
    delete this.nodeValue;
    this._children = this.childNodes; // pv.Dom.Node#childNodes
    
    // Build absolute label and key
    // The absolute key is relative to the root data (not the owner - the topmost root)
    if(parent){
        index = def.get(keyArgs, 'index', null);
        
        data_addChild.call(parent, this, index);
        
        if(parent.absLabel){
            this.absLabel = def.string.join(owner.labelSep, parent.absLabel, this.label);
        } else {
            this.absLabel = this.label;
        }
        
        if(parent.absKey){
            this.absKey = def.string.join(owner.keySep, parent.absKey, this.key);
        } else {
            this.absKey = this.key;
        }
    } else {
        this.absLabel = this.label;
        this.absKey   = this.key;
    }
})

// Mix pv.Dom.Node.prototype
.add(pv.Dom.Node)

.add(/** @lends pvc.data.Data# */{
    parent:       null,
    linkParent:   null,
    
    /**
     * The dimension instances of this data.
     * @type pvc.data.Dimension[]
     */
    _dimensions: null, 
    
    /**
     * The names of unbound dimensions.
     * @type string[]
     */
    _freeDimensionNames: null,
    
    /**
     * The child data instances of this data.
     * @type pvc.data.Data[]
     * @internal
     */
    _children: null,
    
    /**
     * The link child data instances of this data.
     * @type pvc.data.Data[]
     * @internal
     */
    _linkChildren: null,
    
    /**
     * The leaf data instances of this data.
     * 
     * @type pvc.data.Data[] 
     * @internal
     */
    _leafs: null,
    
    /** 
     * The map of child datas by their key.
     * 
     * @type string
     * @internal
     */
    _childrenByKey: null,
    
    /**
     * A map of non-null visible datums indexed by id.
     * @type def.Map
     */
    _visibleNotNullDatums: null,
    
    /**
     * A map of non-null selected datums indexed by id.
     * @type def.Map
     */
    _selectedNotNullDatums: null, 
    
    /**
     * Cache of link child data by grouping operation key.
     * @type object
     * @internal
     */
    _groupByCache: null,

    /**
     * An object with cached results of the {@link #dimensionsSumAbs} method.
     *
     * @type object
     */
    _sumAbsCache: null,

    /**
     * The height of the tree of datas headed by a root data.
     * Only defined in root datas. 
     */
    treeHeight: null,
    
    /**
     * The grouping operation object used to create this data. 
     * Only defined in root datas.
     * @type pvc.data.GroupingOper
     */
    _groupOper: null,
    
    /**
     * A grouping specification object used to create this data, 
     * along with {@link #groupLevel}. 
     * Only defined in datas that have children.
     * 
     * @type pvc.data.GroupingSpec
     */
    _groupSpec: null,
    
    /**
     * A grouping level specification object used to create this data, 
     * along with {@link #groupSpec}. 
     * Only defined in datas that have children.
     * 
     * @type pvc.data.GroupingLevelSpec
     */
    _groupLevel: null,
    
    /** 
     * The datums of this data.
     * @type pvc.data.Datum[]
     * @internal
     */
    _datums: null,
    
    /** 
     * A map of the datums of this data indexed by id.
     * @type object
     * @internal
     */
    _datumsById: null, 
    
    depth:    0,
    label:    "",
    absLabel: "",
    
    /** 
     * Indicates if the object has been disposed.
     * 
     * @type boolean 
     */
    _disposed: false,
    
    /**
     * Indicates that the data was a parent group 
     * in the flattening group operation.
     * 
     * @type boolean
     */
    _isFlattenGroup: false,
    _isDegenerateFlattenGroup: false,
    
    _initDimension: function(dimType){
        this._dimensions[dimType.name] = 
                new pvc.data.Dimension(this, dimType);
    },
    
    /**
     * Obtains a dimension given its name.
     * 
     * <p>
     * If no name is specified,
     * a map with all dimensions indexed by name is returned.
     * Do <b>NOT</b> modify this map.
     * </p>
     * 
     * <p>
     * There is one dimension instance per 
     * dimension type of the data's complex type.
     * </p>
     * <p>
     * If this is not a root data,
     * the dimensions will be child dimensions of
     * the corresponding parent data's dimensions.
     * </p>
     * <p>
     * If this is a root data,
     * the dimensions will 
     * have no parent dimension, but instead, an owner dimension.
     * </p>
     * 
     * @param {string} [name] The dimension name.
     * @param {object} [keyArgs] Keyword arguments.
     * @param {string} [keyArgs.assertExists=true} Indicates that a missing child should be signaled as an error.
     * 
     * @type pvc.data.Dimension
     */
    dimensions: function(name, keyArgs){
        if(name == null) {
            return this._dimensions;
        }
        
        var dim = def.getOwn(this._dimensions, name);
        if(!dim && def.get(keyArgs, 'assertExists', true)) {
            throw def.error.argumentInvalid('name', "Undefined dimension '{0}'.", [name]); 
        }
         
        return dim;
    },
    
    /**
     * Obtains an array of the names of dimensions that are not bound in {@link #atoms}.
     * @type string[]
     */
    freeDimensionNames: function(){
        if(!this._freeDimensionNames) {
            var free = this._freeDimensionNames = [];
            def.eachOwn(this._dimensions, function(dim, dimName){
                var atom = this.atoms[dimName];
                if(!(atom instanceof pvc.data.Atom) || atom.value == null){
                    free.push(dimName);
                }
            }, this);
        }
        return this._freeDimensionNames;
    },
    
    /**
     * Indicates if the data is an owner.
     * 
     * @type boolean
     */
    isOwner: function() { return this.owner === this; },
    
    /**
     * Obtains an enumerable of the child data instances of this data.
     * 
     * @type def.Query
     */
    children: function() { return this._children ? def.query(this._children) : def.query(); },
    
    /**
     * Obtains a child data given its key.
     * 
     * @param {string} key The key of the child data.
     * @type pvc.data.Data
     */
    child: function(key) { return this._childrenByKey ? (this._childrenByKey[key] || null) : null; },
    
    /**
     * Obtains the number of children.
     *
     * @type number
     */
    childCount: function() { return this._children ? this._children.length : 0; },

    /**
     * Obtains an enumerable of the leaf data instances of this data.
     * 
     * @type def.Query 
     */
    leafs: function() { return def.query(this._leafs); },
    
    /**
     * Obtains the number of contained datums.
     * @type number
     */
    count: function() { return this._datums.length; },
    
    /**
     * Obtains the first datum of this data, if any.
     * @type {pvc.data.Datum} The first datum or <i>null</i>.
     * @see #singleDatum 
     */
    firstDatum: function() { return this._datums.length ? this._datums[0] : null; },
    
    /**
     * Obtains the single datum of this data, 
     * or null, when the has data no datums or has more than one.
     * 
     * @type pvc.data.Datum
     * @see #firstDatum
     */
    singleDatum: function() {
        var datums = this._datums;
        return datums.length === 1 ? datums[0] : null;
    },
    
    /**
     * Disposes the child datas, the link child datas and the dimensions.
     * @type undefined
     */
    dispose: function() {
        if(!this._disposed) {
            data_disposeChildLists.call(this);
            if(this._selectedNotNullDatums) { this._selectedNotNullDatums.clear(); }
            this._visibleNotNullDatums.clear();
            
            def.eachOwn(this._dimensions, function(dimension){ dimension.dispose(); });
            
            //  myself
            
            if(this.parent) {
                this.parent.removeChild(this);
                this.parent = null;
            }
            
            if(this.linkParent) {
                /*global data_removeLinkChild:true */
                data_removeLinkChild.call(this.linkParent, this);
            }
            
            this._disposed = true;
        }
    },
    
    /**
     * Disposes the child datas and the link child datas.
     * @type undefined
     */
    disposeChildren: function() {
        /*global data_disposeChildLists:true */
        data_disposeChildLists.call(this);
    }
});

/**
 * Adds a child data.
 * 
 * @name pvc.data.Data#_addChild
 * @function
 * @param {pvc.data.Data} child The child data to add.
 * @param {number} [index=null] The index at which to insert the child.
 * @type undefined
 * @private
 */
function data_addChild(child, index) {
    // this   -> ((pv.Dom.Node#)child).parentNode
    // child  -> ((pv.Dom.Node#)this).childNodes
    // ...
    this.insertAt(child, index);
    
    def.lazy(this, '_childrenByKey')[child.key] = child;
}

/**
 * Adds a link child data.
 * 
 * @name pvc.data.Data#_addLinkChild
 * @function
 * @param {pvc.data.Data} child The link child data to add.
 * @param {number} [index=null] The index at which to insert the child.
 * @type undefined
 * @private
 */
function data_addLinkChild(linkChild, index) {
    /*global data_addColChild:true */
    data_addColChild(this, '_linkChildren', linkChild, 'linkParent', index);
}

/**
 * Removes a link child data.
 *
 * @name pvc.data.Data#_removeLinkChild
 * @function
 * @param {pvc.data.Data} child The link child data to remove.
 * @type undefined
 * @private
 */
function data_removeLinkChild(linkChild) {
    /*global data_removeColChild:true */
    data_removeColChild(this, '_linkChildren', linkChild, 'linkParent');
}

/**
 * Disposes the child datas and the link child datas.
 * 
 * @name pvc.data.Data#_disposeChildLists
 * @function
 * @type undefined
 * @private
 */
function data_disposeChildLists() {
    /*global data_disposeChildList:true */
    data_disposeChildList(this._children, 'parent');
    this._childrenByKey = null;
    
    data_disposeChildList(this._linkChildren, 'linkParent');
    this._groupByCache = null;  
    
    // ~ datums.{isSelected, isVisible, isNull}, children
    this._sumAbsCache = null;
}

/**
 * Called to assert that this is an owner data.
 *  
 * @private
 */
function data_assertIsOwner() {
    /*jshint expr:true */
    this.isOwner() || def.fail("Can only be called on the owner data.");
}


pvc.data.Data.add(/** @lends pvc.data.Data# */{
    /**
     * Obtains the number of not-null selected datums.
     * <p>
     * This method is only optimized when called on an owner data.
     * </p>
     *
     * @type Number
     */
    selectedCount: function() {
        // NOTE: isNull: false is not required here, because null datums cannot be selected
        if(!this.isOwner()) { return this.datums(null, {selected: true}).count(); }

        return this._selectedNotNullDatums.count;
    },

    /**
     * Obtains the not-null selected datums, in an unspecified order.
     * <p>
     * If the datums should be sorted,
     * they can be sorted by their {@link pvc.data.Datum#id}.
     *
     * Alternatively, {@link #datums} can be called,
     * with the <tt>selected</tt> keyword argument.
     * </p>
     * @type pvc.data.Datum[]
     */
    selectedDatums: function() {
        // NOTE: isNull: false is not required here, because null datums cannot be selected
        if(!this.isOwner()) { return this.datums(null, {selected: true}).array(); }

        return this._selectedNotNullDatums.values();
    },

    /**
     * Obtains a map containing the not-null selected datums, indexed by id.
     *
     * @type def.Map(pvc.data.Datum)
     */
    selectedDatumMap: function() {
        if(!this.isOwner()) {
            // NOTE: isNull: false is not required here, because null datums cannot be selected
            var datums =
                this.datums(null, {selected: true}).object({name: def.propGet('id')});

            return new def.Set(datums);
        }

        return this._selectedNotNullDatums.clone();
    },

    /**
     * Obtains the number of not-null visible datums.
     * @type Number
     */
    visibleCount: function() { return this._visibleNotNullDatums.count; },

    /**
     * Replaces currently selected datums with the specified datums.
     *
     * @param {pvc.data.Datum[]|def.query<pvc.data.Datum>} [datums] The new datums to be selected.
     * @returns {boolean} Returns <tt>true</tt> if any datum was selected and <tt>false</tt> otherwise.
     */
    replaceSelected: function(datums) {
        /*global datum_deselect:true, datum_isSelected:true, complex_id:true*/

        // Materialize, cause we're using it twice
        if(!def.array.is(datums)) { datums = datums.array(); }

        // Clear all but the ones we'll be selecting.
        // This way we can have a correct changed flag.
        var alreadySelectedById =
            def
            .query(datums)
            .where(datum_isSelected)
            .object({name: complex_id});

        var changed = this.owner.clearSelected(function(datum) {
                return !def.hasOwn(alreadySelectedById, datum.id);
            });

        changed |= pvc.data.Data.setSelected(datums, true);

        return changed;
    },

    /**
     * Clears the selected state of any selected datum.
     *
     * @param {pvc.data.Datum} [funFilter] Allows excluding atoms from the clear operation.
     * @returns {boolean} Returns <tt>true</tt> if any datum was selected and <tt>false</tt> otherwise.
     */
    clearSelected: function(funFilter) {
        /*global datum_deselect:true */

        if(this.owner !== this) { return this.owner.clearSelected(funFilter); }

        if(!this._selectedNotNullDatums.count) { return false; }

        var changed;
        if(funFilter) {
            changed = false;
            this._selectedNotNullDatums.values().filter(funFilter).forEach(function(datum) {
                    changed = true;
                    datum_deselect.call(datum);
                    this._selectedNotNullDatums.rem(datum.id);
                }, this);
        } else {
            changed = true;
            /*global datum_deselect:true */
            this._selectedNotNullDatums.values().forEach(function(datum) { datum_deselect.call(datum); });

            this._selectedNotNullDatums.clear();
        }

        return changed;
    }
});

/**
 * Called by a datum on its owner data
 * when its selected state changes.
 *
 * @name pvc.data.Data#_onDatumSelectedChanged
 * @function
 * @param {pvc.data.Datum} datum The datum whose selected state changed.
 * @param {boolean} selected The new datum selected state.
 * @type undefined
 * @internal
 */
function data_onDatumSelectedChanged(datum, selected) {
    // <Debug>
    /*jshint expr:true */
    !datum.isNull || def.assert("Null datums do not notify selected changes");
    // </Debug>

    if(selected) { this._selectedNotNullDatums.set(datum.id, datum); }
    else         { this._selectedNotNullDatums.rem(datum.id);        }

    this._sumAbsCache = null;
}

/**
 * Called by a datum on its owner data
 * when its visible state changes.
 *
 * @name pvc.data.Data#_onDatumVisibleChanged
 * @function
 * @param {pvc.data.Datum} datum The datum whose visible state changed.
 * @param {boolean} selected The new datum visible state.
 * @type undefined
 * @internal
 */
function data_onDatumVisibleChanged(datum, visible){
    if(def.hasOwn(this._datumsById, datum.id)) {

        // <Debug>
        /*jshint expr:true */
        !datum.isNull || def.assert("Null datums do not notify visible changes");
        // </Debug>

        if(visible) { this._visibleNotNullDatums.set(datum.id, datum); }
        else        { this._visibleNotNullDatums.rem(datum.id);        }

        this._sumAbsCache = null;

        // Notify dimensions
        /*global dim_onDatumVisibleChanged:true */
        def.eachOwn(this._dimensions, function(dimension) {
            dim_onDatumVisibleChanged.call(dimension, datum, visible);
        });

        // Notify child and link child datas
        this._children.forEach(function(data) {
            data_onDatumVisibleChanged.call(data, datum, visible);
        });

        if(this._linkChildren) {
            this._linkChildren.forEach(function(data) {
                data_onDatumVisibleChanged.call(data, datum, visible);
            });
        }
    }
}

/**
 * Sets the selected state of the given datums
 * to the state 'select'.
 *
 * @param {def.Query} datums An enumerable of {@link pvc.data.Datum} to set.
 * @param {boolean} selected The desired selected state.
 *
 * @returns {boolean} true if at least one datum changed its selected state.
 * @static
 */
pvc.data.Data.setSelected = function(datums, selected) {
    var anyChanged = 0;
    if(datums) {
     // data_onDatumSelectedChanged is called
        def.query(datums).each(function(datum) { anyChanged |= datum.setSelected(selected); });
    }
    return !!anyChanged;
};

/**
 * Pseudo-toggles the selected state of the given datums.
 * If all are selected, clears their selected state.
 * Otherwise, selects all.
 *
 * If the `any` argument is <tt>true</tt>, the behavior changes to:
 * if any is selected, clears their selected state.
 * Otherwise, if all are not selected, select all.
 *
 * @param {def.Query} datums An enumerable of {@link pvc.data.Datum} to toggle.
 * @param {boolean} [any=false] If only some must be selected to consider
 *  the set currently selected or if all must be so.
 *
 * @returns {boolean} true if at least one datum changed its selected state.
 * @static
 */
pvc.data.Data.toggleSelected = function(datums, any) {
    if(!def.array.isLike(datums)) { datums = def.query(datums).array(); }

    /*global datum_isSelected:true, datum_isNullOrSelected:true */

    // Null datums are always unselected.
    // In 'all', their existence would impede on ever being true.
    var q  = def.query(datums);
    var on = any ? q.any(datum_isSelected) : q.all(datum_isNullOrSelected);

    return this.setSelected(datums, !on);
};

/**
 * Sets the visible state of the given datums to the value of argument 'visible'.
 *
 * @param {def.Query} datums An enumerable of {@link pvc.data.Datum} to set.
 * @param {boolean} visible The desired visible state.
 *
 * @returns {boolean} true if at least one datum changed its visible state.
 * @static
 */
pvc.data.Data.setVisible = function(datums, visible){
    var anyChanged = 0;
    if(datums) {
        // data_onDatumVisibleChanged is called
        def.query(datums).each(function(datum) { anyChanged |= datum.setVisible(visible); });
    }
    return !!anyChanged;
};

/**
 * Pseudo-toggles the visible state of the given datums.
 * If all are visible, hides them.
 * Otherwise, shows them all.
 *
 * @param {def.Query} datums An enumerable of {@link pvc.data.Datum} to toggle.
 *
 * @returns {boolean} true if at least one datum changed its visible state.
 * @static
 */
pvc.data.Data.toggleVisible = function(datums) {
    if(!def.array.isLike(datums)) { datums = def.query(datums).array(); }

    // Null datums are always visible. So they don't affect the result.
    var allVisible = def.query(datums).all(def.propGet('isVisible'));
    return pvc.data.Data.setVisible(datums, !allVisible);
};


def
.space('pvc.data')
.FlatteningMode =
    def.set(
        def.makeEnum([
            'DfsPre', // Same grouping levels and dimensions, but all nodes are output at level 1 
            'DfsPost' // Idem, but in Dfs-Post order
        ]),
        // Add None with value 0
        'None', 0);

/**
 * Initializes a grouping specification.
 * 
 * <p>
 * A grouping specification contains information similar to that of an SQL 'order by' clause.
 * </p>
 * 
 * <p>
 * A grouping specification supports the grouping operation.
 * </p>
 * 
 * @see pvc.data.GroupingOper
 * 
 * @name pvc.data.GroupingSpec
 * 
 * @class Contains information about a grouping operation.
 * 
 * @property {string} id A <i>semantic</i> identifier of this grouping specification.
 * @property {boolean} isSingleDimension Indicates that there is only one level and dimension.
 * @property {boolean} isSingleLevel Indicates that there is only one level.
 * @property {boolean} hasCompositeLevels Indicates that there is at least one level with more than one dimension.
 * @property {pvc.data.ComplexType} type The complex type against which dimension names were resolved.
 * @property {pvc.data.GroupingLevelSpec} levels An array of level specifications.
 * @property {pvc.data.DimensionType} firstDimension The first dimension type, if any.
 * @property {pvc.data.FlatteningMode} flatteningMode The flattening mode.
 * @property {string} rootLabel The label of the resulting root node.
 *
 * @constructor
 * @param {def.Query} levelSpecs An enumerable of {@link pvc.data.GroupingLevelSpec}.
 * @param {pvc.data.ComplexType} [type] A complex type.
 * @param {object} [ka] Keyword arguments.
 * @param {pvc.data.FlatteningMode} [ka.flatteningMode=pvc.data.FlatteningMode.None] The flattening mode.
 * @param {string} [ka.rootLabel=''] The label of the root node.
 */
def.type('pvc.data.GroupingSpec')
.init(function(levelSpecs, type, ka){
    this.type = type || null;
    
    var ids = [];
    
    this.hasCompositeLevels = false;
    
    var dimNames = []; // accumulated dimension names
    
    this.levels = def.query(levelSpecs || undefined) // -> null query
        .where(function(levelSpec){ return levelSpec.dimensions.length > 0; })
        .select(function(levelSpec){
            ids.push(levelSpec.id);
            
            def.array.append(dimNames, levelSpec.dimensionNames());
            
            if(!this.hasCompositeLevels && levelSpec.dimensions.length > 1) {
                this.hasCompositeLevels = true;
            }
            
            levelSpec._setAccDimNames(dimNames.slice(0));
            
            return levelSpec;
        }, this)
        .array();
    
    this._dimNames = dimNames;
    
    // The null grouping has zero levels
    this.depth             = this.levels.length;
    this.isSingleLevel     = this.depth === 1;
    this.isSingleDimension = this.isSingleLevel && !this.hasCompositeLevels;
    this.firstDimension    = this.depth > 0 ? this.levels[0].dimensions[0] : null;
    
    this.rootLabel = def.get(ka, 'rootLabel') || "";
    this.flatteningMode = def.get(ka, 'flatteningMode') || pvc.data.FlatteningMode.None;
    
    this._cacheKey = this._calcCacheKey();
    this.id = this._cacheKey + "##" + ids.join('||');
})
.add(/** @lends pvc.data.GroupingSpec# */{
    
    _calcCacheKey: function(ka) {
        return [def.get(ka, 'flatteningMode') || this.flatteningMode,
                def.get(ka, 'reverse'       ) || 'false',
                def.get(ka, 'isSingleLevel' ) || this.isSingleLevel,
                def.get(ka, 'rootLabel'     ) || this.rootLabel]
               .join('#');
    },

    /**
     * Late binds a grouping specification to a complex type.
     * @param {pvc.data.ComplexType} type A complex type.
     */
    bind: function(type){
        this.type = type || def.fail.argumentRequired('type');
        this.levels.forEach(function(levelSpec) { levelSpec.bind(type); });
    },

    /**
     * Obtains an enumerable of the contained dimension specifications.
     * @type def.Query
     */
    dimensions: function() { return def.query(this.levels).prop('dimensions').selectMany(); },

    dimensionNames: function() { return this._dimNames; },
    
    view: function(complex) { return complex.view(this.dimensionNames()); },

    /**
     * Indicates if the data resulting from the grouping is discrete or continuous.
     * @type boolean
     */
    isDiscrete: function() {
        var d;
        return !this.isSingleDimension || 
               (!!(d = this.firstDimension) && d.type.isDiscrete);
    },
    
    /**
     * Obtains the dimension type of the first dimension spec., if any.
     * @type pvc.visual.DimensionType
     */
    firstDimensionType: function() {
        var d = this.firstDimension;
        return d && d.type;
    },
    
    /**
     * Obtains the dimension name of the first dimension spec., if any.
     * @type string
     */
    firstDimensionName: function() {
        var dt = this.firstDimensionType();
        return dt && dt.name;
    },
    
    /**
     * Obtains the dimension value type of the first dimension spec., if any.
     * @type string
     */
    firstDimensionValueType: function() {
        var dt = this.firstDimensionType();
        return dt && dt.valueType;
    },
    
    /**
     * Indicates if the grouping has no levels.
     * @type boolean
     */
    isNull: function() { return !this.levels.length; },

    /**
     * Obtains a version of this grouping specification
     * that conforms to the specified arguments.
     *
     * @param {string}  [ka.flatteningMode] The desired flattening mode.
     * @param {boolean} [ka.isSingleLevel=false] Indicates that the grouping should have only a single level.
     * If that is not the case, all grouping levels are collapsed into a single level containing all dimensions.
     * 
     * @param {boolean} [ka.reverse=false] Indicates that each dimension's order should be reversed.
     * @param {string}  [ka.rootLabel] The label of the resulting root node.
     * 
     * @type pvc.data.GroupingSpec
     */
    ensure: function(ka) {
        var result;
        if(ka) {
            var cacheKey = this._calcCacheKey(ka);
            if(cacheKey !== this._cacheKey) {
                var cache = def.lazy(this, '_groupingCache');
                result = def.getOwn(cache, cacheKey);
                if(!result) { result = cache[cacheKey] = this._ensure(ka); }
            }
        }
        
        return result || this;
    },
    
    _ensure: function(ka) {
        var me = this;
        
        if(def.get(ka, 'isSingleLevel') && !me.isSingleLevel) { return me._singleLevelGrouping(ka); }
        if(def.get(ka, 'reverse')) { return me._reverse(ka); }
        
        var flatteningMode = def.get(ka, 'flatteningMode') || me.flatteningMode;
        var rootLabel      = def.get(ka, 'rootLabel') || me.rootLabel;
        if(flatteningMode !== me.flatteningMode || rootLabel !== me.rootLabel) {
            return new pvc.data.GroupingSpec(me.levels, me.type, { // Share Levels
                flatteningMode: flatteningMode,
                rootLabel:      rootLabel
            });
        }
        
        return me;
    },
    
    /**
     * Obtains a single-level version of this grouping specification.
     * 
     * @param {object} [ka] Keyword arguments
     * @param {boolean} [ka.reverse=false] Indicates that each dimension's order should be reversed.
     * @param {string} [ka.rootLabel] The label of the resulting root node.
     * @type pvc.data.GroupingSpec 
     */
    _singleLevelGrouping: function(ka) {
        var reverse = !!def.get(ka, 'reverse');
        var dimSpecs = 
            this
            .dimensions()
            .select(function(dimSpec) {
                return reverse ? 
                       new pvc.data.GroupingDimensionSpec(dimSpec.name, !dimSpec.reverse, dimSpec.type.complexType) :
                       dimSpec;
            });
                        
        var levelSpec = new pvc.data.GroupingLevelSpec(dimSpecs, this.type);
        
        return new pvc.data.GroupingSpec([levelSpec], this.type, {
            flatteningMode: null, // turns into singleLevel
            rootLabel:      def.get(ka, 'rootLabel') || this.rootLabel
        });
    },
    
    /**
     * Obtains a reversed version of this grouping specification.
     * @param {object} [ka] Keyword arguments
     * @param {string} [ka.rootLabel] The label of the resulting root node.
     * @type pvc.data.GroupingSpec 
     */
    _reverse: function(ka) {
        var levelSpecs = 
            def
            .query(this.levels)
            .select(function(levelSpec) {
                var dimSpecs = 
                    def
                    .query(levelSpec.dimensions)
                    .select(function(dimSpec) {
                        return new pvc.data.GroupingDimensionSpec(dimSpec.name, !dimSpec.reverse, dimSpec.type.complexType);
                    });
                
                return new pvc.data.GroupingLevelSpec(dimSpecs, this.type);
            });

        return new pvc.data.GroupingSpec(levelSpecs, this.type, {
            flatteningMode: def.get(ka, 'flatteningMode') || this.flatteningMode,
            rootLabel:      def.get(ka, 'rootLabel'     ) || this.rootLabel
        });
    },

    toString: function(){
        return def.query(this.levels)
                .select(function(level){ return '' + level; })
                .array()
                .join(', ');
    }
});

def.type('pvc.data.GroupingLevelSpec')
.init(function(dimSpecs, type){
    var ids = [];
    var dimNames = [];
    
    this.dimensions = def.query(dimSpecs)
       .select(function(dimSpec){
           ids.push(dimSpec.id);
           dimNames.push(dimSpec.name);
           return dimSpec;
       })
       .array();
    
    this._dimNames = dimNames;
    
    this.dimensionsInDefOrder = this.dimensions.slice(0);
    if(type) { this._sortDimensions(type); }
    
    this.id = ids.join(',');
    this.depth = this.dimensions.length;
    
    var me = this;
    this.comparer = function(a, b) { return me.compare(a, b); };
})
.add( /** @lends pvc.data.GroupingLevelSpec */{
    _sortDimensions: function(type) {
        type.sortDimensionNames(
            this.dimensionsInDefOrder,
            function(d) { return d.name; });
    },
    
    _setAccDimNames: function(accDimNames) { this._accDimNames = accDimNames; },
    
    accDimensionNames: function() { return this._accDimNames; },
    
    dimensionNames: function() { return this._dimNames; },
    
    bind: function(type) {
        this._sortDimensions(type);
        
        this.dimensions.forEach(function(dimSpec) { dimSpec.bind(type); });
    },
    
    compare: function(a, b) {
        for(var i = 0, D = this.depth ; i < D ; i++) {  
            var result = this.dimensions[i].compareDatums(a, b);
            if(result !== 0) { return result; }
        }
        
        return 0;
    },
    
    key: function(datum) {
        var key      = '';
        var atoms    = {};
        var datoms   = datum.atoms;
        var dimNames = this._dimNames;
        var keySep   = datum.owner.keySep;
        
        // This builds a key compatible with that of pvc.data.Complex#key
        // See also pvc.data.Complex.compositeKey
        for(var i = 0, D = this.depth ; i < D ; i++) {
            var dimName = dimNames[i];
            var atom = datoms[dimName];
            atoms[dimName] = atom;
            if(!i) { key = atom.key; } 
            else   { key += keySep + atom.key; }
        }
        
        return {key: key, atoms: atoms, dimNames: dimNames};
    },

    toString: function() {
        return def.query(this.dimensions)
                .select(function(dimSpec) { return '' + dimSpec; })
                .array()
                .join('|');
    }
});

def.type('pvc.data.GroupingDimensionSpec')
.init(function(name, reverse, type) {
    this.name     = name;
    this.reverse  = !!reverse;
    this.id = this.name + ":" + (this.reverse ? '0' : '1');
    if(type) { this.bind(type); }
})
.add( /** @lends pvc.data.GroupingDimensionSpec */ {
    type: null,
    comparer: null,

    /**
     * Late binds a dimension specification to a complex type.
     * @param {pvc.data.ComplexType} type A complex type.
     */
    bind: function(type) {
        /*jshint expr:true */
        type || def.fail.argumentRequired('type');
        
        this.type     = type.dimensions(this.name);
        this.comparer = this.type.atomComparer(this.reverse);
    },

    compareDatums: function(a, b) {
        //if(this.type.isComparable) {
            return this.comparer(a.atoms[this.name], b.atoms[this.name]);
        //}
        
        // Use datum source order
        //return this.reverse ? (b.id - a.id) : (a.id - b.id);
    },

    toString: function() { return this.name + (this.reverse ? ' desc' : ''); }
});

/**
 * Parses a grouping specification string.
 * 
 * @param {string|string[]} [specText] The grouping specification text,
 * or array of grouping specification level text.
 * When unspecified, a null grouping is returned.
 * 
 * <p>
 * An example:
 * </p>
 * <pre>
 * "series1 asc, series2 desc, category"
 * </pre>
 * <p>
 * The following will group all the 'series' in one level and the 'category' in another: 
 * </p>
 * <pre>
 * "series1 asc|series2 desc, category"
 * </pre>
 * 
 * @param {pvc.data.ComplexType} [type] A complex type against which to resolve dimension names.
 * 
 * @type pvc.data.GroupingSpec
 */
pvc.data.GroupingSpec.parse = function(specText, type) {
    if(!specText) { return new pvc.data.GroupingSpec(null, type); }
    
    var levels;
    if(def.array.is(specText)) {
        levels = specText;
    } else if(def.string.is(specText)) {
        levels = specText.split(/\s*,\s*/); 
    }

    var levelSpecs = 
        def
        .query(levels)
        .select(function(levelText){
            var dimSpecs = groupSpec_parseGroupingLevel(levelText, type);
            return new pvc.data.GroupingLevelSpec(dimSpecs, type);
        });
    
    return new pvc.data.GroupingSpec(levelSpecs, type);
};

var groupSpec_matchDimSpec = /^\s*(.+?)(?:\s+(asc|desc))?\s*$/i;

/**
 * @private
 * @static
 */
function groupSpec_parseGroupingLevel(groupLevelText, type) {
    /*jshint expr:true */
    def.string.is(groupLevelText) || def.fail.argumentInvalid('groupLevelText', "Invalid grouping specification.");
    
    return def.query(groupLevelText.split(/\s*\|\s*/))
       .where(def.truthy)
       .select(function(dimSpecText) {
            var match   = groupSpec_matchDimSpec.exec(dimSpecText) ||
                            def.fail.argumentInvalid('groupLevelText', "Invalid grouping level syntax '{0}'.", [dimSpecText]),
                name    = match[1],
                order   = (match[2] || '').toLowerCase(),
                reverse = order === 'desc';
               
            return new pvc.data.GroupingDimensionSpec(name, reverse, type);
        });
}


/**
 * Initializes a data operation.
 * 
 * @name pvc.data.DataOper
 * 
 * @class The base abstract class for a data operation.
 * Performs an initial query on the datums of the opertion's link parent
 * and hands the final implementation to a derived class.
 * 
 * @property {string} key Set on construction with a value that identifies the operation.
 * 
 * @constructor
 *
 * @param {pvc.data.Data} linkParent The link parent data.
 * @param {object} [keyArgs] Keyword arguments.
 */
def.type('pvc.data.DataOper')
.init(function(linkParent, keyArgs){
    /*jshint expr:true */
    linkParent || def.fail.argumentRequired('linkParent');
    
    this._linkParent = linkParent;
}).
add(/** @lends pvc.data.DataOper */{
    
    key: null,

    /**
     * Performs the data operation.
     * 
     * @returns {pvc.data.Data} The resulting root data.
     */
    execute: def.method({isAbstract: true})
});


/**
 * Initializes a grouping operation.
 * 
 * @name pvc.data.GroupingOper
 * 
 * @class Performs one grouping operation according to a grouping specification.
 * @extends pvc.data.DataOper
 * 
 * @constructor
 *
 * @param {pvc.data.Data} linkParent The link parent data.
 * 
 * @param {string|string[]|pvc.data.GroupingSpec|pvc.data.GroupingSpec[]} groupingSpecs A grouping specification as a string, an object or array of either.
 * 
 * @param {object} [keyArgs] Keyword arguments.
 * See {@link pvc.data.DataOper} for any additional arguments.
 * 
 * @param {boolean} [keyArgs.isNull=null]
 *      Only considers datums with the specified isNull attribute.
 * @param {boolean} [keyArgs.visible=null]
 *      Only considers datums that have the specified visible state.
 * @param {boolean} [keyArgs.selected=null]
 *      Only considers datums that have the specified selected state.
 * @param {function} [keyArgs.where] A datum predicate.
 * @param {string} [keyArgs.whereKey] A key for the specified datum predicate,
 * previously returned by this function.
 * <p>
 * If this argument is specified, and it is not the value <c>null</c>,
 * it can be used to cache results.
 * If this argument is specified, and it is the value <c>null</c>,
 * the results are not cached.
 * If it is not specified, and <tt>keyArgs</tt> is specified,
 * one is returned.
 * If it is not specified and <tt>keyArgs</tt> is not specified,
 * then the instance will have a null {@link #key} property value.
 * </p>
 * <p>
 * If a key not previously returned by this operation is specified,
 * then it should be prefixed with a "_" character,
 * in order to not collide with keys generated internally.
 * </p>
 */
def.type('pvc.data.GroupingOper', pvc.data.DataOper)
.init(function(linkParent, groupingSpecs, keyArgs){
    /*jshint expr:true */
    groupingSpecs || def.fail.argumentRequired('groupingSpecs');

    this.base(linkParent, keyArgs);

    this._where    = def.get(keyArgs, 'where');
    this._visible  = def.get(keyArgs, 'visible',  null);
    this._selected = def.get(keyArgs, 'selected', null);
    this._isNull   = def.get(keyArgs, 'isNull',   null);
    
    /* 'Where' predicate and its key */
    var hasKey = this._selected == null, // TODO: Selected state changes do not yet invalidate cache...
        whereKey = '';
    if(this._where){
        whereKey = def.get(keyArgs, 'whereKey');
        if(!whereKey){
            if(!keyArgs || whereKey === null){
                // Force no key
                hasKey = false;
            } else {
                whereKey = '' + def.nextId('dataOperWhereKey');
                keyArgs.whereKey = whereKey;
            }
        }
    }

    // grouping spec ids is semantic keys, although the name is not 'key'
    var ids = [];
    this._groupSpecs = def.array.as(groupingSpecs).map(function(groupSpec){
        if(groupSpec instanceof pvc.data.GroupingSpec) {
            if(groupSpec.type !== linkParent.type) {
                throw def.error.argumentInvalid('groupingSpecText', "Invalid associated complex type.");
            }
        } else {
            // Must be a non-empty string, or throws
            groupSpec = pvc.data.GroupingSpec.parse(groupSpec, linkParent.type);
        }
        
        ids.push(groupSpec.id);

        return groupSpec;
    });
    
    /* Operation key */
    if(hasKey){
        this.key = ids.join('!!') +
                   "||visible:"  + this._visible +
                   "||isNull:"   + this._isNull  +
                   //"||selected:" + this._selected +
                   "||where:"    + whereKey;
    }
}).
add(/** @lends pvc.data.GroupingOper */{

    /**
     * Performs the grouping operation.
     *
     * @returns {pvc.data.Data} The resulting root data.
     */
    execute: function(){
        /* Setup a priori datum filters */
        
        /*global data_whereState: true */
        var datumsQuery = data_whereState(def.query(this._linkParent._datums), {
            visible:  this._visible,
            selected: this._selected,
            isNull:   this._isNull,
            where:    this._where
        });
        
        /* Group datums */
        var rootNode = this._group(datumsQuery);

        /* Render node into a data */
        return this._generateData(rootNode, null, this._linkParent);
    },
    
    executeAdd: function(rootData, datums){
        
        /*global data_whereState: true */
        var datumsQuery = data_whereState(def.query(datums), {
            visible:  this._visible,
            selected: this._selected,
            isNull:   this._isNull,
            where:    this._where
        });
        
        /* Group new datums */
        var rootNode = this._group(datumsQuery);

        /* Render node into specified root data */
        this._generateData(rootNode, null, this._linkParent, rootData);
        
        return rootNode.datums;
    },

    _group: function(datumsQuery) {

        // Create the root node
        var rootNode = {
            isRoot: true,
            treeHeight: def
                .query(this._groupSpecs)
                .select(function(spec) {
                    var levelCount = spec.levels.length;
                    if(!levelCount) { return 0; }
                    return !!spec.flatteningMode ? 1 : levelCount;
                })
                .reduce(def.add, 0),
                
            datums: []
            // children
            // atoms       // not on rootNode
            // isFlattenGroup // on parents of a flattened group spec
        };

        if(rootNode.treeHeight > 0) { this._groupSpecRecursive(rootNode, datumsQuery, 0); }
        
        return rootNode;
    },
    
    _groupSpecRecursive: function(specParentNode, specDatumsQuery, specIndex) {
        var groupSpec     = this._groupSpecs[specIndex];
        var levelSpecs    = groupSpec.levels;
        var L             = levelSpecs.length;
        var doFlatten     = !!groupSpec.flatteningMode;
        var nextSpecIndex = specIndex + 1;
        var isLastSpec    = (nextSpecIndex >= this._groupSpecs.length);
        var isPostOrder   = doFlatten && (groupSpec.flatteningMode === pvc.data.FlatteningMode.DfsPost);
        var specGroupParent;
        
        if(doFlatten) {
            specParentNode.children = [];
            specParentNode.childrenByKey = {}; // Don't create children with equal keys
            
            // Must create a rootNode for the grouping operation
            // Cannot be specParentNode (TODO: Why?)
            specGroupParent = {
                key:      '', // Key is local to groupSpec (when not flattened, it is local to level)
                absKey:   '', 
                atoms:    {},
                datums:   [],
                label:    groupSpec.rootLabel,
                dimNames: []
            };

            if(!isPostOrder) {
                specParentNode.children.push(specGroupParent);
                specParentNode.childrenByKey[''] = specGroupParent;
            }
        } else {
            if(specParentNode.isRoot) {
                specParentNode.label = groupSpec.rootLabel;
            }
            
            specGroupParent = specParentNode;
        }

        /* Group datums */
        groupLevelRecursive.call(this, specGroupParent, specDatumsQuery, 0);

        if(doFlatten) {
            if(isPostOrder) { specParentNode.children.push(specGroupParent); }

            // Add datums of specGroupParent to specParentNode.
            specParentNode.datums = specGroupParent.datums;
        }
        
        function groupLevelRecursive(levelParentNode, levelDatums, levelIndex) {
            
            var levelSpec = levelSpecs[levelIndex];
            
            if(!doFlatten) {
                levelParentNode.children = [];
                levelParentNode.groupSpec = groupSpec;
                levelParentNode.groupLevelSpec = levelSpec;
            }
            
            var childNodes = this._groupDatums(levelSpec, levelParentNode, levelDatums, doFlatten);
            var isLastSpecLevel = levelIndex === L - 1;
            var willRecurseParent = doFlatten && !isLastSpec;
            
            // Add children's datums to levelParentNode, in post order.
            // This way, datums are reordered to follow the grouping "pattern". 
            // 
            // NOTE: levelParentNode.datums is initially empty
            var levelParentDatums = willRecurseParent ? 
                    [] : 
                    levelParentNode.datums;
            
            childNodes
            .forEach(function(child) {
                /* On all but the last level,
                 * the datums of *child* are set to the 
                 * union of datums of its own children.
                 * The datums will have been added, 
                 * by the end of the following recursive call.
                 */
                var childDatums = child.datums; // backup original datums
                if(!(isLastSpec && isLastSpecLevel)) { child.datums = []; }
                
                var specParentChildIndex;
                if(!doFlatten) {
                    levelParentNode.children.push(child);
                } else {
                    // Add children at a "hidden" property
                    // so that the test "if(!child._children.length)"
                    // below, can be done.
                    def.array.lazy(levelParentNode, '_children').push(child);
                    
                    if(def.hasOwn(specParentNode.childrenByKey, child.key)) {
                        // Duplicate key.
                        // Don't add as child of specParentNode.
                        // 
                        // We need to add its datums to group parent, anyway.
                        def.array.append(levelParentDatums, childDatums);
                        return;
                    }
                    
                    specParentChildIndex = specParentNode.children.length;
                    if(!isPostOrder) {
                        specParentNode.children.push(child);
                        specParentNode.childrenByKey[child.key] = child;

                        levelParentNode.isFlattenGroup = true;
                    }
                }
                
                if(!isLastSpecLevel) {
                    groupLevelRecursive.call(this, child, childDatums, levelIndex + 1);
                } else if(!isLastSpec) {
                    this._groupSpecRecursive(child, childDatums, nextSpecIndex);
                }

                // Datums already added to 'child'.
                def.array.append(levelParentDatums, child.datums);

                if(doFlatten && isPostOrder) {
                    if(def.hasOwn(specParentNode.childrenByKey, child.key)) {
                        /*jshint expr:true*/
                        child.isFlattenGroup || def.assert("Must be a parent for duplicate keys to exist.");
                        
                        // A child of child
                        // was registered with the same key,
                        // because it is all-nulls (in descending level's keys).
                        // But it is better to show the parent instead of the child,
                        // so we remove the child and add the parent.
                        // Yet, we cannot show only the parent
                        // if *child* has more than one child,
                        // cause then, the datums of the null child.child
                        // would only be in *child*, but
                        // the datums of the non-null child.child
                        // would be both in *child* and in child.child.
                        // This would mess up the scales and waterfall control code,
                        // not knowing whether to ignore the flatten group or not.
                        if(child._children.length === 1) {
                            specParentNode.children.splice(
                                    specParentChildIndex, 
                                    specParentNode.children.length - specParentChildIndex);
                            
                            // A total group that must be accounted for
                            // because it has own datums.
                            child.isDegenerateFlattenGroup = true;
                        }
                        // else, both are added to specParentNode,
                        // and their datas will be given separate keys
                        // they will both be shown.
                        // Below, we overwrite anyway, with no harmful effect
                    }
                    
                    specParentNode.children.push(child);
                    specParentNode.childrenByKey[child.key] = child;
                    
                    levelParentNode.isFlattenGroup = true;
                }
            }, this);

            if(willRecurseParent) {
                // datums can no longer change
                this._groupSpecRecursive(levelParentNode, levelParentDatums, nextSpecIndex);
            }
        }
    },
    
    _groupDatums: function(levelSpec, levelParentNode, levelDatums, doFlatten) {
        // The first datum of each group is inserted here in order,
        // according to the level's comparer.
        var firstDatums = [];
        
        // The first child is inserted here 
        // at the same index as that of 
        // the first datum in firstDatums.
        var childNodes = new def.OrderedMap();
        
        // Group levelDatums By the levelSpec#key(.)
        def
        .query(levelDatums)
        .each(function(datum) {
            /*  newChild = { key: '', atoms: {}, dimNames: [] } */
            var newChild = levelSpec.key(datum);
            var key      = newChild.key;
            var child    = childNodes.get(key);
            if(child) {
                child.datums.push(datum);
            } else {
                // First datum with key -> new child
                child = newChild;
                child.datums   = [datum];
                
                if(doFlatten) {
                    // child.atoms must contain (locally) those of the levelParentNode,
                    // so that when flattened, they have a unique key 
                    def.copy(child.atoms, levelParentNode.atoms);
                    
                    // The **key** is the **absKey**, trimmed of keySep at the end
                    if(levelParentNode.dimNames.length) {
                        var keySep = datum.owner.keySep;
                        var K = keySep.length;
                        
                        var trimKey = 
                            child.absKey = 
                            levelParentNode.absKey + 
                            keySep + 
                            key;
                        
                        while(trimKey.lastIndexOf(keySep) === trimKey.length - K) {
                            trimKey = trimKey.substr(0, trimKey.length - K);
                        }
                        
                        child.key = trimKey;                        
                    } else {
                        child.absKey = key;
                    }
                    
                    // don't change local key variable
                    child.dimNames = levelSpec.accDimensionNames();
                }
                
                var datumIndex = def.array.insert(firstDatums, datum, levelSpec.comparer);
                childNodes.add(key, child, ~datumIndex);
            }
        });
        
        return childNodes;
    },
    
    _generateData: function(node, parentNode, parentData, rootData) {
        var data, isNew;
        if(node.isRoot) {
            // Root node
            if(rootData) {
                data = rootData;
                /*global data_addDatumsLocal:true*/
                data_addDatumsLocal.call(data, node.datums);
            } else {
                isNew = true;
                
                // Create a *linked* rootNode data
                data = new pvc.data.Data({
                    linkParent: parentData,
                    datums:     node.datums
                });
                data.treeHeight = node.treeHeight;
                data._groupOper = this;
            }
        } else {
            if(rootData) {
                data = def.get(parentData._childrenByKey, node.key);
                if(data) {
                    // Add the datums to the data, and its atoms to its dimensions
                    // Should also update linkedChildren (not children).
                    /*global data_addDatumsSimple:true*/
                    data_addDatumsSimple.call(data, node.datums);
                }
            }
            
            if(!data) {
                isNew = true;
                var index, siblings;
                if(rootData && (siblings = parentData._children)) {
                    // Insert the new sibling in correct order
                    // node.datums[0] is representative of the new Data's position
                    index = ~def.array.binarySearch(siblings, node.datums[0], parentNode.groupLevelSpec.comparer);
                }
                
                data = new pvc.data.Data({
                    parent:   parentData,
                    atoms:    node.atoms,
                    dimNames: node.dimNames,
                    datums: node.datums,
                    index:  index
                });
            }
        }

        if(isNew) {
            if(node.isFlattenGroup) {
                data._isFlattenGroup = true;
                data._isDegenerateFlattenGroup = !!node.isDegenerateFlattenGroup;
            }
            
            var label = node.label;
            if(label) {
                data.label    += label;
                data.absLabel += label;
            }
        }

        var childNodes = node.children;
        if(childNodes && childNodes.length) {
            if(isNew) {
                data._groupSpec      = node.groupSpec;
                data._groupLevelSpec = node.groupLevelSpec;
            }
            
            childNodes.forEach(function(childNode) {
                this._generateData(childNode, node, data, rootData);
            }, this);

        } else if(isNew && !node.isRoot) {
            // A leaf node
            var leafs = data.root._leafs;
            data.leafIndex = leafs.length;
            leafs.push(data);
        }
        
        return data;
    }
});


def
.type('pvc.data.LinearInterpolationOper')
.init(function(allPartsData, data, catRole, serRole, valRole, stretchEnds){
    this._newDatums = [];
    
    this._data = data;
    
    var allCatDataRoot = catRole.flatten(allPartsData, {ignoreNulls: false});
    var allCatDatas    = allCatDataRoot._children;
    
    var serDatas1 = this._serDatas1 = serRole.isBound() ?
                        serRole.flatten(data).children().array() :
                        [null]; // null series
    
    this._isCatDiscrete = catRole.grouping.isDiscrete();
    this._firstCatDim   = !this._isCatDiscrete ? data.owner.dimensions(catRole.firstDimensionName()) : null;
    this._stretchEnds    = stretchEnds;
    var valDim = this._valDim  = data.owner.dimensions(valRole.firstDimensionName());
    
    var visibleKeyArgs = {visible: true, zeroIfNone: false};
    
    this._catInfos = allCatDatas.map(function(allCatData, catIndex){
        
        var catData = data._childrenByKey[allCatData.key];
        
        var catInfo = {
            data:           catData || allCatData, // may be null?
            value:          allCatData.value,
            isInterpolated: false,
            serInfos:       null,
            index:          catIndex
        };
        
        catInfo.serInfos = 
            serDatas1
            .map(function(serData1){
                var group = catData;
                if(group && serData1){
                    group = group._childrenByKey[serData1.key];
                }
                
                var value = group ?
                            group.dimensions(valDim.name)
                                 .sum(visibleKeyArgs) : 
                            null;
                
                return {
                    data:    serData1,
                    group:   group,
                    value:   value,
                    isNull:  value == null,
                    catInfo: catInfo
                };
            }, this);
        
        return catInfo;
    });
    
    this._serCount  = serDatas1.length;
    this._serStates = 
        def
        .range(0, this._serCount)
        .select(function(serIndex){ 
            return new pvc.data.LinearInterpolationOperSeriesState(this, serIndex); 
        }, this)
        .array()
        ;
    
    // Determine the sort order of the continuous base categories
    // Categories assumed sorted.
//    if(!this._isCatDiscrete && catDatas.length >= 2){
//        if((+catDatas[1].value) >= (+catDatas[0].value)){
//            this._comparer = def.compare;
//        } else {
//            this._comparer = def.compareReverse;
//        }
//    }
})
.add({
    interpolate: function(){
        var catInfo;
        while((catInfo = this._catInfos.shift())){
            catInfo.serInfos.forEach(this._visitSeries, this);
        }
        
        // Add datums created during interpolation
        var newDatums = this._newDatums;
        if(newDatums.length){
            this._data.owner.add(newDatums);
        }
    },
    
    _visitSeries: function(catSerInfo, serIndex){
        this._serStates[serIndex].visit(catSerInfo);
    },
    
    nextUnprocessedNonNullCategOfSeries: function(serIndex){
        // NOTE: while interpolating, 
        // only catInfos remaining to be processed
        // remain in the _catInfos array (see {@link #interpolate}).
        // As such, this finds the "next" (unprocessed) 
        // non-null cat. info.
        var catIndex = 0,
            catCount = this._catInfos.length;
        
        while(catIndex < catCount){
            var catInfo = this._catInfos[catIndex++];
            //if(!catInfo.isInterpolated){
            var catSerInfo = catInfo.serInfos[serIndex];
            if(!catSerInfo.isNull){
                return catSerInfo;
            }
            //}
        }
    }

    // NOTE: This was only needed when selection needed to
    // divide in half between the last and next.
//    _setCategory: function(catValue){
//        /*jshint expr:true  */
//        !this._isCatDiscrete || def.assert("Only for continuous base.");
//        
//        // Insert sort into this._catInfos
//        
//        // catValue may be a new dimension value
//        var catAtom = this._firstCatDim.intern(catValue, /* isVirtual */ true);
//        
//        catValue = catAtom.value; // now may be a Date object...
//        
//        // Check if and where to insert
//        var index = 
//            def
//            .array
//            .binarySearch(
//                this._catInfos, 
//                +catValue,
//                this._comparer,
//                function(catInfo){  return +catInfo.value; });
//        
//        if(index < 0){
//            // New category
//            // Insert at the two's complement of index
//            var catInfo = {
//                atom:  catAtom,
//                value: catValue,
//                label: this._firstCatDim.format(catValue),
//                isInterpolated: true
//            };
//            
//            catInfo.serInfos = 
//                def
//                .range(0, this._serCount)
//                .select(function(serScene, serIndex){
//                    return {
//                        value:   null,
//                        isNull:  true,
//                        catInfo: catInfo
//                    };
//                })
//                .array();
//            
//            this._catInfos.splice(~index, 0, catInfo);
//        }
//        
//        return index;
//    }
});

def
.type('pvc.data.LinearInterpolationOperSeriesState')
.init(function(interpolation, serIndex) {
    this.interpolation = interpolation;
    this.index = serIndex;
    
    this._lastNonNull(null);
})
.add({
    visit: function(catSeriesInfo) {
        if(catSeriesInfo.isNull) { this._interpolate(catSeriesInfo); } 
        else                     { this._lastNonNull(catSeriesInfo); }
    },
    
    _lastNonNull: function(catSerInfo) {
        if(arguments.length) {
            this.__lastNonNull = catSerInfo; // Last non-null
            this.__nextNonNull = undefined;
        }
        
        return this.__lastNonNull;
    },

    _nextNonNull: function() { return this.__nextNonNull; },
    
    _initInterpData: function() {
        // When a null category is found, 
        // and it is the first category, or it is right after a non-null category,
        // the prop. __nextNonNull will have the value undefined 
        // (because _nextNonNull is reset to undefined every time that __lastNonNull is set).
        //
        // Then, the __nextNonNull category is determined, 
        // by looking ahead of the current (null) category
        // (see {@link Interpolation#nextUnprocessedNonNullCategOfSeries}).
        // 
        // If both a last and a next exist,
        // the slope of the line connecting these is determined.
        // 
        // The next processed category, if null, will not
        // pass the test this.__nextNonNull !== undefined,
        // guaranteeing that this initialization is only performed
        // once for each series "segment" of null dots that is 
        // surrounded by non-null dots.
        
        // The start of a new segment?
        if(this.__nextNonNull !== undefined) { return; }
        
        // Will be null if the series starts 
        //  with null categories:
        // S: 0 - 0 - x
        var last = this.__lastNonNull;
        
        // Make sure not to store undefined to distinguish from uninitialized.
        // When "last" is null, a non-null "next" is used in 
        //  {@link #_interpolate } to "extend" the beginning of the series.
        var next = this.__nextNonNull = 
           this.interpolation
               .nextUnprocessedNonNullCategOfSeries(this.index) || 
           null;
                                
        if(next && last) {
            var fromValue  = last.value;
            var toValue    = next.value;
            var deltaValue = toValue - fromValue;
            
            if(this.interpolation._isCatDiscrete) {
                var stepCount = next.catInfo.index - last.catInfo.index;
                /*jshint expr:true */
                (stepCount >= 2) || def.assert("Must have at least one interpolation point.");
                
                this._stepValue   = deltaValue / stepCount;
                this._middleIndex = ~~(stepCount / 2); // Math.floor <=> ~~
                
                var dotCount = (stepCount - 1);
                this._isOdd  = (dotCount % 2) > 0;
            } else {
                var fromCat  = +last.catInfo.value;
                var toCat    = +next.catInfo.value;
                var deltaCat = toCat - fromCat;
                
                this._steep = deltaValue / deltaCat; // should not be infinite, cause categories are different
                
                this._middleCat = (toCat + fromCat) / 2;
                
                // NOTE: This was only needed when selection needed to
                // divide in half between the last and next.
                // (Maybe) add a category
                //this.interpolation._setCategory(this._middleCat);
            }
        }
    },
    
    _interpolate: function(catSerInfo) {
        
        this._initInterpData();
        
        var next = this.__nextNonNull;
        var last = this.__lastNonNull;
        var one  = next || last;
        if(!one) { return; }
        
        var value, group;
        var interpolation = this.interpolation;
        var catInfo = catSerInfo.catInfo;
        
        if(next && last) {
            if(interpolation._isCatDiscrete) {
                var groupIndex = (catInfo.index - last.catInfo.index);
                value = last.value + this._stepValue * groupIndex;
                
                if(this._isOdd) {
                    group = groupIndex < this._middleIndex ? last.group : next.group;
                } else {
                    group = groupIndex <= this._middleIndex ? last.group : next.group;
                }
                
            } else {
                var cat = +catInfo.value;
                var lastCat = +last.catInfo.value;
                
                value = last.value + this._steep * (cat - lastCat);
                group = cat < this._middleCat ? last.group : next.group;
                //isInterpolatedMiddle = cat === this._middleCat;
            }
        } else {
            // Only "stretch" ends on stacked visualization
            if(!interpolation._stretchEnds) { return; }
            
            value = one.value;
            group = one.group;
            //isInterpolatedMiddle = false;
        }
        
        // -----------
        
        // Multi, series, ... atoms, other measures besides valDim.
        var atoms = Object.create(group._datums[0].atoms);
        
        // Category atoms
        def.copyOwn(atoms, catInfo.data.atoms);

        // Value atom
        var valueAtom = interpolation._valDim.intern(value, /* isVirtual */ true);
        atoms[valueAtom.dimension.name] = valueAtom;
        
        // Create datum with collected atoms
        var newDatum = new pvc.data.Datum(group.owner, atoms);
        
        newDatum.isVirtual = true;
        newDatum.isInterpolated = true;
        newDatum.interpolation = 'linear';
        
        interpolation._newDatums.push(newDatum);
    }
});

def
.type('pvc.data.ZeroInterpolationOper')
.init(function(allPartsData, data, catRole, serRole, valRole, stretchEnds){
    this._newDatums = [];
    
    this._data = data;
    
    var allCatDataRoot = catRole.flatten(allPartsData, {ignoreNulls: false});
    var allCatDatas    = allCatDataRoot._children;
    
    var serDatas1 = this._serDatas1 = serRole.isBound() ?
                        serRole.flatten(data).children().array() :
                        [null]; // null series
    
    this._isCatDiscrete = catRole.grouping.isDiscrete();
    this._firstCatDim   = !this._isCatDiscrete ? data.owner.dimensions(catRole.firstDimensionName()) : null;
    this._stretchEnds   = stretchEnds;
    var valDim = this._valDim  = data.owner.dimensions(valRole.firstDimensionName());
    
    var visibleKeyArgs = {visible: true, zeroIfNone: false};
    
    this._catInfos = allCatDatas.map(function(allCatData, catIndex){
        
        var catData = data._childrenByKey[allCatData.key];
        
        var catInfo = {
            data:           catData || allCatData,
            value:          allCatData.value,
            isInterpolated: false,
            serInfos:       null,
            index:          catIndex
        };
        
        catInfo.serInfos = 
            serDatas1
            .map(function(serData1){
                var group = catData;
                if(group && serData1){
                    group = group._childrenByKey[serData1.key];
                }
                
                var value = group ?
                            group.dimensions(valDim.name)
                                 .sum(visibleKeyArgs) : 
                            null;
                
                return {
                    data:    serData1,
                    group:   group,
                    value:   value,
                    isNull:  value == null,
                    catInfo: catInfo
                };
            }, this);
        
        return catInfo;
    });
    
    this._serCount  = serDatas1.length;
    this._serStates = 
        def
        .range(0, this._serCount)
        .select(function(serIndex){ 
            return new pvc.data.ZeroInterpolationOperSeriesState(this, serIndex); 
        }, this)
        .array()
        ;
})
.add({
    interpolate: function(){
        var catInfo;
        while((catInfo = this._catInfos.shift())){
            catInfo.serInfos.forEach(this._visitSeries, this);
        }
        
        // Add datums created during interpolation
        var newDatums = this._newDatums;
        if(newDatums.length){
            this._data.owner.add(newDatums);
        }
    },
    
    _visitSeries: function(catSerInfo, serIndex){
        this._serStates[serIndex].visit(catSerInfo);
    },
    
    nextUnprocessedNonNullCategOfSeries: function(serIndex){
        var catIndex = 0,
            catCount = this._catInfos.length;
        
        while(catIndex < catCount){
            var catInfo = this._catInfos[catIndex++];
            var catSerInfo = catInfo.serInfos[serIndex];
            if(!catSerInfo.isNull){
                return catSerInfo;
            }
        }
    }
});

def
.type('pvc.data.ZeroInterpolationOperSeriesState')
.init(function(interpolation, serIndex){
    this.interpolation = interpolation;
    this.index = serIndex;
    
    this._lastNonNull(null);
})
.add({
    visit: function(catSeriesInfo){
        if(catSeriesInfo.isNull){
            this._interpolate(catSeriesInfo);
        } else {
            this._lastNonNull(catSeriesInfo);
        }
    },
    
    _lastNonNull: function(catSerInfo){
        if(arguments.length){
            this.__lastNonNull = catSerInfo; // Last non-null
            this.__nextNonNull = undefined;
        }
        
        return this.__lastNonNull;
    },

    _nextNonNull: function(){
        return this.__nextNonNull;
    },
    
    _initInterpData: function(){
        // The start of a new segment?
        if(this.__nextNonNull !== undefined){
            return;
        }
        
        var last = this.__lastNonNull;
        var next = this.__nextNonNull = 
           this.interpolation
               .nextUnprocessedNonNullCategOfSeries(this.index) || 
           null;
                                
        if(next && last){
            var fromValue  = last.value;
            var toValue    = next.value;
            var deltaValue = toValue - fromValue;
            
            if(this.interpolation._isCatDiscrete){
                var stepCount = next.catInfo.index - last.catInfo.index;
                /*jshint expr:true */
                (stepCount >= 2) || def.assert("Must have at least one interpolation point.");
                
                this._middleIndex = ~~(stepCount / 2); // Math.floor <=> ~~
                
                var dotCount = (stepCount - 1);
                this._isOdd  = (dotCount % 2) > 0;
            } else {
                var fromCat  = +last.catInfo.value;
                var toCat    = +next.catInfo.value;
                this._middleCat = (toCat + fromCat) / 2;
            }
        }
    },
    
    _interpolate: function(catSerInfo){
        this._initInterpData();
        
        var next = this.__nextNonNull;
        var last = this.__lastNonNull;
        var one  = next || last;
        if(!one){
            return;
        }
        
        var group;
        var interpolation = this.interpolation;
        var catInfo = catSerInfo.catInfo;
        
        if(next && last){
            if(interpolation._isCatDiscrete){
                var groupIndex = (catInfo.index - last.catInfo.index);
                if(this._isOdd){
                    group = groupIndex < this._middleIndex ? last.group : next.group;
                } else {
                    group = groupIndex <= this._middleIndex ? last.group : next.group;
                }
                
            } else {
                var cat = +catInfo.value;
                group = cat < this._middleCat ? last.group : next.group;
            }
        } else {
            // Only "stretch" ends on stacked visualization
            if(!interpolation._stretchEnds) {
                return;
            }
            
            group = one.group;
        }
        
        // -----------
        
        // Multi, series, ... atoms, other measures besides valDim.
        var atoms = Object.create(group._datums[0].atoms);
        
        // Category atoms
        def.copyOwn(atoms, catInfo.data.atoms);
        
        // Value atom
        var zeroAtom = interpolation._zeroAtom ||
                       (interpolation._zeroAtom = 
                           interpolation._valDim.intern(0, /* isVirtual */ true));
        
        atoms[zeroAtom.dimension.name] = zeroAtom;
        
        // Create datum with collected atoms
        var newDatum = new pvc.data.Datum(group.owner, atoms);
        newDatum.isVirtual = true;
        newDatum.isInterpolated = true;
        newDatum.interpolation  = 'zero';
        
        interpolation._newDatums.push(newDatum);
    }
});

pvc.data.Data.add(/** @lends pvc.data.Data# */{
    
    /**
     * Loads or reloads the data with the specified enumerable of atoms.
     * 
     * <p>
     * Can only be called on an owner data. 
     * Child datas are instead "loaded" on construction, 
     * with a subset of its parent's datums.
     * </p>
     * 
     * <p>
     * This method was designed to be fed with the output
     * of {@link pvc.data.TranslationOper#execute}.
     * </p>
     * 
     * @param {def.Query} atomz An enumerable of {@link map(string union(any || pvc.data.Atom))}.
     * @param {object} [keyArgs] Keyword arguments.
     * @param {function} [keyArgs.isNull] Predicate that indicates if a datum is considered null.
     * @param {function} [keyArgs.where] Filter function that approves or excludes each newly read new datum.
     */
    load: function(atomz, keyArgs) {
        /*global data_assertIsOwner:true */
        data_assertIsOwner.call(this);
        
        var whereFun  = def.get(keyArgs, 'where');
        var isNullFun = def.get(keyArgs, 'isNull');
        var datums = 
            def
            .query(atomz)
            .select(function(atoms) {
                var datum = new pvc.data.Datum(this, atoms);
                
                if(isNullFun && isNullFun(datum)) { datum.isNull = true; }
                if(whereFun  && !whereFun(datum)) { return null; }
                
                return datum;
            }, this);
        
        data_setDatums.call(this, datums, {doAtomGC: true});
    },
    
    clearVirtuals: function() {
        // Recursively clears all virtual datums and atoms
        var datums = this._datums;
        if(datums) {
            this._sumAbsCache = null;
            
            var visibleNotNullDatums = this._visibleNotNullDatums;
            var selectedNotNullDatums = this._selectedNotNullDatums;
            
            var i = 0;
            var L = datums.length;
            var removed;
            while(i < L) {
                var datum = datums[i];
                if(datum.isVirtual) {
                    var id = datum.id;
                    if(selectedNotNullDatums && datum.isSelected) { selectedNotNullDatums.rem(id); }
                    
                    if(datum.isVisible) { visibleNotNullDatums.rem(id); }
                    
                    datums.splice(i, 1);
                    L--;
                    removed = true;
                } else {
                    i++;
                }
            }
            
            if(removed) {
                if(!datums.length && this.parent) {
                    // "Me is a group"
                    this.dispose();
                    return;
                }

                var children = this._children;
                if(children) {
                    i = 0;
                    L = children.length;
                    while(i < L) {
                        var childData = children[i];
                        childData.clearVirtuals();
                        if(!childData.parent) {
                            // Child group was empty and removed itself
                            L--;
                        } else {
                            i++;
                        }
                    }
                }
                
                if(this._linkChildren) {
                    this._linkChildren.forEach(function(linkChildData) {
                        linkChildData.clearVirtuals();
                    });
                }
            }
        }
        
        /*global dim_uninternVirtualAtoms:true*/
        def.eachOwn(this._dimensions, function(dim) { dim_uninternVirtualAtoms.call(dim); });
    },
    
    /**
     * Adds new datums to the owner data.
     * @param {pvc.data.Datum[]|def.Query} datums The datums to add. 
     */
    add: function(datums) {
        /*global data_assertIsOwner:true, data_setDatums:true*/
        
        data_assertIsOwner.call(this);
        
        data_setDatums.call(this, datums, {isAdditive: true, doAtomGC: true});
    },
    
    /**
     * Groups the datums of this data, possibly filtered,
     * according to a grouping specification.
     * 
     * <p>
     * The result of the grouping operation over a set of datums
     * is a new <i>linked child</i> data.
     * 
     * It is a root data, 
     * but shares the same {@link #owner} and {@link #atoms} with this,
     * and has the considered datums in {@link #datums}.
     * 
     * The data will contain one child data per distinct atom,
     * of the first grouping level dimension, 
     * found in the datums.
     * Each child data will contain the datums sharing that atom.
     * 
     * This logic extends to all following grouping levels.
     * </p>
     * 
     * <p>
     * Datums with null atoms on a grouping level dimension are excluded.
     * </p>
     * 
     * @param {string|string[]|pvc.data.GroupingOperSpec} groupingSpecText A grouping specification string or object.
     * <pre>
     * "series1 asc, series2 desc, category"
     * </pre>
     * 
     * @param {Object} [keyArgs] Keyword arguments object.
     * See additional keyword arguments in {@link pvc.data.GroupingOper}
     * 
     * @see #where
     * @see pvc.data.GroupingLevelSpec
     *
     * @returns {pvc.data.Data} The resulting root data.
     */
    groupBy: function(groupingSpecText, keyArgs) {
        var groupOper = new pvc.data.GroupingOper(this, groupingSpecText, keyArgs),
            cacheKey  = groupOper.key,
            groupByCache,
            data;

        if(cacheKey) {
            groupByCache = this._groupByCache;

            // Check cache for a linked data with that key
            data = groupByCache && groupByCache[cacheKey];
        }

        if(!data) {
            if(pvc.debug >= 7) {
                pvc.log("[GroupBy] " + (cacheKey ? ("Cache key not found: '" + cacheKey + "'") : "No Cache key"));
            }
            
            data = groupOper.execute();

            if(cacheKey) {
                (groupByCache || (this._groupByCache = {}))[cacheKey] = data;
            }
        } else if(pvc.debug >= 7) {
            pvc.log("[GroupBy] Cache key hit '" + cacheKey + "'");
        }
        
        return data;
    },

    /**
     * Creates a linked data with the result of filtering
     * the datums of this data.
     *
     * <p>
     * This operation differs from {@link #datums} only in the type of output,
     * which is a new linked data, instead of an enumerable of the filtered datums.
     * See {@link #datums} for more information on the filtering operation.
     * </p>
     *
     * @param {object} [whereSpec] A "where" specification.
     * @param {object} [keyArgs] Keyword arguments object.
     * See {@link #datums} for information on available keyword arguments.
     *
     * @returns {pvc.data.Data} A linked data containing the filtered datums.
     */
    where: function(whereSpec, keyArgs) {
        var datums = this.datums(whereSpec, keyArgs);
        return new pvc.data.Data({linkParent: this, datums: datums});
    },

    /**
     * Obtains the datums of this data, 
     * possibly filtered according 
     * to a specified "where" specification,
     * datum selected state and 
     * filtered atom visible state.
     *
     * @param {object} [whereSpec] A "where" specification.
     * A structure with the following form:
     * <pre>
     * // OR of datum filters
     * whereSpec = [datumFilter1, datumFilter2, ...] | datumFilter;
     * 
     * // AND of dimension filters
     * datumFilter = {
     *      // OR of dimension values
     *      dimName1: [value1, value2, ...],
     *      dimName2: value1,
     *      ...
     * }
     * </pre>
     * <p>Values of a datum filter can also directly be atoms.</p>
     * <p>
     *    An example of a "where" specification:
     * </p>
     * <pre>
     * whereSpec = [
     *     // Datums whose series is 'Europe' or 'Australia', 
     *     // and whose category is 2001 or 2002 
     *     {series: ['Europe', 'Australia'], category: [2001, 2002]},
     *     
     *     // Union'ed with
     *     
     *     // Datums whose series is 'America' 
     *     {series: 'America'},
     * ];
     * </pre>
     *  
     * @param {object} [keyArgs] Keyword arguments object.
     * 
     * @param {boolean} [keyArgs.isNull=null]
     *      Only considers datums with the specified isNull attribute.
     * 
     * @param {boolean} [keyArgs.visible=null]
     *      Only considers datums that have the specified visible state.
     * 
     * @param {boolean} [keyArgs.selected=null]
     *      Only considers datums that have the specified selected state.
     * 
     * @param {function} [keyArgs.where] A arbitrary datum predicate.
     *
     * @param {string[]} [keyArgs.orderBySpec] An array of "order by" strings to be applied to each 
     * datum filter of <i>whereSpec</i>.
     * <p>
     * An "order by" string is the same as a grouping specification string, 
     * although it is used here with a slightly different meaning.
     * Here's an example of an "order by" string:
     * <pre>
     * "series1 asc, series2 desc, category"
     * </pre
     * </p>
     * 
     * <p>
     * When not specified, altogether or individually, 
     * these are determined to match the corresponding datum filter of <i>whereSpec</i>.
     * </p>
     * 
     * <p>
     * If a string is specified it is treated as the "order by" string corresponding 
     * to the first datum filter.
     * </p>
     * 
     * @returns {def.Query} A query object that enumerates the desired {@link pvc.data.Datum}.
     */
    datums: function(whereSpec, keyArgs) {
        if(!whereSpec) {
            if(!keyArgs) { return def.query(this._datums); }
            
            return data_whereState(def.query(this._datums), keyArgs);
        }
        
        whereSpec = data_processWhereSpec.call(this, whereSpec, keyArgs);
        
        return data_where.call(this, whereSpec, keyArgs);
    },
    
    /**
     * Obtains the first datum that satisfies a specified "where" specification.
     * <p>
     * If no datum satisfies the filter, null is returned.
     * </p>
     * 
     * @param {object} whereSpec A "where" specification.
     * See {@link #datums} to know about this structure.
     * 
     * @param {object} [keyArgs] Keyword arguments object.
     * See {@link #datums} for additional available keyword arguments.
     * 
     * @returns {pvc.data.Datum} The first datum that satisfies the specified filter or <i>null</i>.
     * 
     * @see pvc.data.Data#datums 
     */
    datum: function(whereSpec, keyArgs) {
        /*jshint expr:true */
        whereSpec || def.fail.argumentRequired('whereSpec');
        
        whereSpec = data_processWhereSpec.call(this, whereSpec, keyArgs);
        
        return data_where.call(this, whereSpec, keyArgs).first() || null;
    },
    
    /**
     * Sums the absolute value 
     * of the sum of a specified dimension on each child.
     *
     * @param {string} dimName The name of the dimension to sum on each child data.
     * @param {object} [keyArgs] Optional keyword arguments that are
     * passed to each dimension's {@link pvc.data.Dimension#sum} method.
     * 
     * @type number
     */
    dimensionsSumAbs: function(dimName, keyArgs){
        /*global dim_buildDatumsFilterKey:true */
        var key = dimName + ":" + dim_buildDatumsFilterKey(keyArgs),
            sum = def.getOwn(this._sumAbsCache, key);

        if(sum == null) {
            sum = this.children()
                    /* non-degenerate flattened parent groups would account for the same values more than once */
                    .where(function(childData){ return !childData._isFlattenGroup || childData._isDegenerateFlattenGroup; })
                    .select(function(childData){
                        return Math.abs(childData.dimensions(dimName).sum(keyArgs));
                    }, this)
                    .reduce(def.add, 0);

            (this._sumAbsCache || (this._sumAbsCache = {}))[key] = sum;
        }

        return sum;
    }
});

/**
 * Called to add or replace the contained {@link pvc.data.Datum} instances. 
 * 
 * When replacing, all child datas and linked child datas are disposed.
 * 
 * When adding, the specified datums will be added recursively 
 * to this data's parent or linked parent, and its parent, until the owner data is reached.
 * When crossing a linked parent, 
 * the other linked children of that parent
 * are given a chance to receive a new datum, 
 * and it will be added if it satisfies its inclusion criteria.
 * 
 * The datums' atoms must be consistent with the base atoms of this data.
 * If this data inherits a non-null atom in a given dimension and:
 * <ul>
 * <li>a datum has another non-null atom, an error is thrown.</li>
 * <li>a datum has a null atom, an error is thrown.
 * </ul>
 * 
 * @name pvc.data.Data#_setDatums
 * @function
 * @param {pvc.data.Datum[]|def.Query} newDatums An array or enumerable of datums.
 * When an array, and in replace mode, 
 * it is used directly to keep the stored datums and may be modified if necessary.
 * 
 * @param {object} [keyArgs] Keyword arguments.
 * 
 * @param {boolean} [keyArgs.isAdditive=false] Indicates that the specified datums are to be added, 
 * instead of replace existing datums.
 * 
 * @param {boolean} [keyArgs.doAtomGC=true] Indicates that atom garbage collection should be performed.
 * 
 * @type undefined
 * @private
 */
function data_setDatums(newDatums, keyArgs) {
    // But may be an empty list
    /*jshint expr:true */
    newDatums || def.fail.argumentRequired('newDatums');
    
    var doAtomGC   = def.get(keyArgs, 'doAtomGC',   false);
    var isAdditive = def.get(keyArgs, 'isAdditive', false);
    
    var visibleNotNullDatums  = this._visibleNotNullDatums;
    var selectedNotNullDatums = this._selectedNotNullDatums;
    
    var newDatumsByKey = {};
    var prevDatumsByKey;
    var prevDatums = this._datums;
    if(prevDatums) {
        // Visit atoms of existing datums
        // We cannot simply mark all atoms of every dimension
        // cause now, the dimensions may already contain new atoms
        // used (or not) by the new datums
        var processPrevAtoms = isAdditive && doAtomGC;
        
        // Index existing datums by (semantic) key
        // So that old datums may be preserved
        prevDatumsByKey = 
            def
            .query(prevDatums)
            .uniqueIndex(function(datum) {
                
                if(processPrevAtoms) { // isAdditive && doAtomGC
                    data_processDatumAtoms.call(
                            this, 
                            datum, 
                            /* intern */      false, 
                            /* markVisited */ true);
                }
                
                return datum.key;
            }, this);
        
        // Clear caches and/or children
        if(isAdditive) {
            this._sumAbsCache = null;
        } else {
            /*global data_disposeChildLists:true*/
            data_disposeChildLists.call(this);
            if(selectedNotNullDatums) { selectedNotNullDatums.clear(); }
            visibleNotNullDatums.clear();
        }
    } else {
        isAdditive = false;
    }
    
    var datumsById;
    if(isAdditive) { datumsById = this._datumsById;      } 
    else           { datumsById = this._datumsById = {}; }
    
    if(def.array.is(newDatums)) {
        var i = 0;
        var L = newDatums.length;
        while(i < L) {
            var inDatum  = newDatums[i];
            var outDatum = setDatum.call(this, inDatum);
            if(!outDatum) {
                newDatums.splice(i, 1);
                L--;
            } else {
                if(outDatum !== inDatum) { newDatums[i] = outDatum; }
                i++;
            }
        }
    } else if(newDatums instanceof def.Query) {
        newDatums = 
            newDatums
            .select(setDatum, this)
            .where(def.notNully)
            .array();
    } else {
        throw def.error.argumentInvalid('newDatums', "Argument is of invalid type.");
    }
    
    if(doAtomGC) {
        // Atom garbage collection
        // Unintern unused atoms
        def.eachOwn(this._dimensions, function(dimension) {
            /*global dim_uninternUnvisitedAtoms:true*/
            dim_uninternUnvisitedAtoms.call(dimension);
        });
    }
    
    if(isAdditive) {
        // newDatums contains really new datums (excluding duplicates)
        // These can be further filtered in the grouping operation
        def.array.append(prevDatums, newDatums);
        
        // II - Distribute added datums by linked children
        if(this._linkChildren) {
            this._linkChildren.forEach(function(linkChildData) {
                data_addDatumsSimple.call(linkChildData, newDatums);
            });
        }
    } else {
        this._datums = newDatums;
    }
    
    function setDatum(newDatum) {
        if(!newDatum) {  return; } // Ignore
        
        /* Use already existing same-key datum, if any */
        var key = newDatum.key;
        
        // Duplicate in input datums, ignore
        if(def.hasOwnProp.call(newDatumsByKey, key)) { return; }
        
        if(prevDatumsByKey) {
            var prevDatum = def.getOwn(prevDatumsByKey, key);
            if(prevDatum) {
                // Duplicate with previous datums, ignore
                if(isAdditive) { return; }
                
                // Prefer to *re-add* the old datum and ignore the new one
                // Not new
                newDatum = prevDatum;
                
                // The old datum is going to be kept.
                // In the end, it will only contain the datums that were "removed"
                //delete prevDatumsByKey[key];
            }
            // else newDatum is really new
        }
        
        newDatumsByKey[key] = newDatum;
        
        var id = newDatum.id;
        datumsById[id] = newDatum;
        
        data_processDatumAtoms.call(
                this,
                newDatum,
                /* intern      */ !!this._dimensions, // When creating a linked data, datums are set when dimensions aren't yet created. 
                /* markVisited */ doAtomGC);
        
        // TODO: make this lazy?
        if(!newDatum.isNull) {
            if(selectedNotNullDatums && newDatum.isSelected) { selectedNotNullDatums.set(id, newDatum); }
            if(newDatum.isVisible) { visibleNotNullDatums.set(id, newDatum); }
        }
        
        return newDatum;
    }
}

/**
 * Processes the atoms of this datum.
 * If a virtual null atom is found then the null atom of that dimension
 * is interned.
 * If desired the processed atoms are marked as visited.
 * 
 * @name pvc.data.Datum._processAtoms
 * @function
 * @param {boolean} [intern=false] If virtual nulls should be detected.
 * @param {boolean} [markVisited=false] If the atoms should be marked as visited. 
 * @type undefined
 * @internal
 */
function data_processDatumAtoms(datum, intern, markVisited){
    
    var dims = this._dimensions;
    // data is still initializing and dimensions are not yet created ?
    if(!dims) { intern = false; }
    
    if(intern || markVisited) {
        var atoms = datum.atoms;
        for(var dimName in atoms) {
            var atom = atoms[dimName]; 
            if(intern) {
                // Ensure that the atom exists in the local dimension
                
                var localDim = def.getOwn(dims, dimName) ||
                               def.fail.argumentInvalid("Datum has atoms of foreign dimension.");
                
                /*global dim_internAtom:true */
                dim_internAtom.call(localDim, atom);
            }
            
            // Mark atom as visited
            if(markVisited) { atom.visited = true; }
        }
    }
}

function data_addDatumsSimple(newDatums) {
    // But may be an empty list
    /*jshint expr:true */
    newDatums || def.fail.argumentRequired('newDatums');
    
    var groupOper = this._groupOper;
    if(groupOper) {
        // This data gets its datums, 
        //  possibly filtered (groupOper calls data_addDatumsLocal).
        // Children get their new datums.
        // Linked children of children get their new datums.
        newDatums = groupOper.executeAdd(this, newDatums);
    } else {
        data_addDatumsLocal.call(this, newDatums);
    }
    
    // Distribute added datums by linked children
    if(this._linkChildren) {
        this._linkChildren.forEach(function(linkChildData) {
            data_addDatumsSimple.call(linkChildData, newDatums);
        });
    }
}

function data_addDatumsLocal(newDatums) {
    var visibleNotNullDatums  = this._visibleNotNullDatums;
    var selectedNotNullDatums = this._selectedNotNullDatums;
    
    // Clear caches
    this._sumAbsCache = null;
    
    var datumsById = this._datumsById;
    var datums = this._datums;
    
    newDatums.forEach(addDatum, this);
    
    function addDatum(newDatum) {
        var id = newDatum.id;
        
        datumsById[id] = newDatum;
        
        data_processDatumAtoms.call(
                this,
                newDatum,
                /* intern      */ true, 
                /* markVisited */ false);
        
        // TODO: make this lazy?
        if(!newDatum.isNull) {
            if(selectedNotNullDatums && newDatum.isSelected) { selectedNotNullDatums.set(id, newDatum); }
            if(newDatum.isVisible) { visibleNotNullDatums.set(id, newDatum); }
        }
        
        datums.push(newDatum);
    }
}

/**
 * Processes a given "where" specification.
 * <p>
 * Normalizes and validates the specification syntax, 
 * validates dimension names,
 * readily excludes uninterned (unexistent) and duplicate values and
 * atoms based on their "visible state".
 * </p>
 * 
 * <p>
 * The returned specification contains dimensions instead of their names
 * and atoms, instead of their values. 
 * </p>
 * 
 * @name pvc.data.Data#_processWhereSpec
 * @function
 * 
 * @param {object} whereSpec A "where" specification to be normalized.
 * TODO: A structure with the following form: ... 
 *
 * @return Array A <i>processed</i> "where" of the specification.
 * A structure with the following form:
 * <pre>
 * // OR of processed datum filters
 * whereProcSpec = [datumProcFilter1, datumProcFilter2, ...] | datumFilter;
 * 
 * // AND of processed dimension filters
 * datumProcFilter = {
 *      // OR of dimension atoms
 *      dimName1: [atom1, atom2, ...],
 *      dimName2: atom1,
 *      ...
 * }
 * </pre>
 * 
 * @private
 */
function data_processWhereSpec(whereSpec) {
    var whereProcSpec = [];
    
    whereSpec = def.array.as(whereSpec);
    if(whereSpec) { whereSpec.forEach(processDatumFilter, this); }
    
    return whereProcSpec;
    
    function processDatumFilter(datumFilter) {
        if(datumFilter != null) {
            /*jshint expr:true */
            (typeof datumFilter === 'object') || def.fail.invalidArgument('datumFilter');
            
            /* Map: {dimName1: atoms1, dimName2: atoms2, ...} */
            var datumProcFilter = {},
                any = false;
            for(var dimName in datumFilter) {
                var atoms = processDimensionFilter.call(this, dimName, datumFilter[dimName]);
                if(atoms) {
                    any = true;
                    datumProcFilter[dimName] = atoms;
                }
            }
            
            if(any) { whereProcSpec.push(datumProcFilter); }
        }
    }
    
    function processDimensionFilter(dimName, values) {
        // throws if it doesn't exist
        var dimension = this.dimensions(dimName);
        var getAtom = function(value) { return dimension.atom(value); };  // null if it doesn't exist
        var atoms = def.query   (values)
                       .select  (getAtom)
                       .where   (def.notNully)
                       .distinct(def.propGet('key'))
                       .array();
        
        return atoms.length ? atoms : null;
    }
}

/**
 * Filters a datum query according to a specified predicate, 
 * datum selected and visible state.
 * 
 * @name pvc.data.Data#_whereState
 * @function
 * 
 * @param {def.query} q A datum query.
 * @param {object} [keyArgs] Keyword arguments object.
 * See {@link #groupBy} for additional available keyword arguments.
 * 
 * @returns {def.Query} A query object that enumerates the desired {@link pvc.data.Datum}.
 * @private
 * @static
 */
function data_whereState(q, keyArgs) {
    var selected = def.get(keyArgs, 'selected'),
        visible  = def.get(keyArgs, 'visible'),
        where    = def.get(keyArgs, 'where'),
        isNull   = def.get(keyArgs, 'isNull');

    if(visible  != null) { q = q.where(function(d){ return d.isVisible  === visible;  }); }
    if(isNull   != null) { q = q.where(function(d){ return d.isNull     === isNull;   }); }
    if(selected != null) { q = q.where(function(d){ return d.isSelected === selected; }); }
    
    if(where) { q = q.where(where); }
    
    return q;
}

// All the "Filter" and "Spec" words below should be read as if they were prepended by "Proc"
/**
 * Obtains the datums of this data filtered according to 
 * a specified "where" specification,
 * and optionally, 
 * datum selected state and filtered atom visible state.
 * 
 * @name pvc.data.Data#_where
 * @function
 * 
 * @param {object} [whereSpec] A <i>processed</i> "where" specification.
 * @param {object} [keyArgs] Keyword arguments object.
 * See {@link #groupBy} for additional available keyword arguments.
 * 
 * @param {string[]} [keyArgs.orderBySpec] An array of "order by" strings to be applied to each 
 * datum filter of <i>whereSpec</i>.
 * 
 * @returns {def.Query} A query object that enumerates the desired {@link pvc.data.Datum}.
 * @private
 */
function data_where(whereSpec, keyArgs) {
    
    var orderBys = def.array.as(def.get(keyArgs, 'orderBy')),
        datumKeyArgs = def.create(keyArgs || {}, {orderBy: null});
    
    var query = def.query(whereSpec)
                   .selectMany(function(datumFilter, index){
                      if(orderBys) { datumKeyArgs.orderBy = orderBys[index]; }
                      
                      return data_whereDatumFilter.call(this, datumFilter, datumKeyArgs);
                   }, this);
    
    return query.distinct(def.propGet('id'));
    
    /*
    // NOTE: this is the brute force / unguided algorithm - no indexes are used
    function whereDatumFilter(datumFilter, index){
        // datumFilter = {dimName1: [atom1, OR atom2, OR ...], AND ...}
        
        return def.query(this._datums).where(datumPredicate, this);
        
        function datumPredicate(datum){
            if((selected === null || datum.isSelected === selected) && 
               (visible  === null || datum.isVisible  === visible)) {
                var atoms = datum.atoms;
                for(var dimName in datumFilter) {
                    if(datumFilter[dimName].indexOf(atoms[dimName]) >= 0) {
                        return true;
                    }
                }   
            }
        }
    }
    */    
}

/**
 * Obtains an enumerable of the datums satisfying <i>datumFilter</i>,
 * by constructing and traversing indexes.
 * 
 * @name pvc.data.Data#_whereDatumFilter
 * @function
 * 
 * @param {string} datumFilter A <i>processed</i> datum filter.
 * 
 * @param {Object} keyArgs Keyword arguments object.
 * See {@link #groupBy} for additional available keyword arguments.
 * 
 * @param {string} [keyArgs.orderBy] An "order by" string.
 * When not specified, one is determined to match the specified datum filter.
 * The "order by" string cannot contain multi-dimension levels (dimension names separated with "|").
 * 
 * @returns {def.Query} A query object that enumerates the desired {@link pvc.data.Datum}.
 * 
 * @private
 */
function data_whereDatumFilter(datumFilter, keyArgs) {
     var groupingSpecText = keyArgs.orderBy; // keyArgs is required
     if(!groupingSpecText) {
         // Choose the most convenient one.
         // A sort on dimension names can yield good cache reuse.
         groupingSpecText = Object.keys(datumFilter).sort().join(',');
     } else {
         if(groupingSpecText.indexOf("|") >= 0) {
             throw def.error.argumentInvalid('keyArgs.orderBy', "Multi-dimension order by is not supported.");
         }
         
         // TODO: not validating that groupingSpecText actually contains the same dimensions referred in datumFilter...
     }
     
     /*
        // NOTE:
        // All the code below is just a stack/state-based translation of 
        // the following recursive code (so that it can be used lazily with a def.query):
        
        recursive(rootData, 0);
        
        function recursive(parentData, h) {
            if(h >= H) {
                // Leaf
                parentData._datums.forEach(fun, ctx);
                return;
            }
            
            var dimName = parentData._groupLevelSpec.dimensions[0].name;
            datumFilter[dimName].forEach(function(atom){
                var childData = parentData._childrenByKey[atom.globalKey];
                if(childData) {
                    recursive(childData, h + 1);
                }
            }, this);
        }
     */
     
     var rootData = this.groupBy(groupingSpecText, keyArgs),
     H = rootData.treeHeight;
     
     var stateStack = [];
     
     // Ad-hoq query
     return def.query(function(/* nextIndex */) {
         // Advance to next datum
         var state;

         // No current data means starting
         if(!this._data) {
             this._data = rootData;
             this._dimAtomsOrQuery = def.query(datumFilter[rootData._groupLevelSpec.dimensions[0].name]);
             
         // Are there still any datums of the current data to enumerate?
         } else if(this._datumsQuery) { 
             
             // <Debug>
             /*jshint expr:true */
             this._data || def.assert("Must have a current data");
             stateStack.length || def.assert("Must have a parent data"); // cause the root node is "dummy"
             !this._dimAtomsOrQuery || def.assert();
             // </Debug>
             
             if(this._datumsQuery.next()) {
                 this.item = this._datumsQuery.item; 
                 return 1; // has next
             }
             
             // No more datums here
             // Advance to next leaf data node
             this._datumsQuery = null;
             
             // Pop parent data
             state = stateStack.pop();
             this._data = state.data;
             this._dimAtomsOrQuery = state.dimAtomsOrQuery;
         } 
         
         // <Debug>
         this._dimAtomsOrQuery || def.assert("Invalid programmer");
         this._data || def.assert("Must have a current data");
         // </Debug>
         
         // Are there still any OrAtom paths of the current data to traverse? 
         var depth = stateStack.length;
             
         // Any more atom paths to traverse, from the current data?
         do {
             while(this._dimAtomsOrQuery.next()) {
                 
                 var dimAtomOr = this._dimAtomsOrQuery.item,
                     childData = this._data._childrenByKey[dimAtomOr.key];
                 
                 // Also, advance the test of a leaf child data with no datums, to avoid backtracking
                 if(childData && (depth < H - 1 || childData._datums.length)) {
                     
                     stateStack.push({data: this._data, dimAtomsOrQuery: this._dimAtomsOrQuery});
                     
                     this._data = childData;
                     
                     if(depth < H - 1) {
                         // Keep going up, until a leaf datum is found. Then we stop.
                         this._dimAtomsOrQuery = def.query(datumFilter[childData._groupLevelSpec.dimensions[0].name]);
                         depth++;
                     } else {
                         // Leaf data!
                         // Set first datum and leave
                         this._dimAtomsOrQuery = null;
                         this._datumsQuery = def.query(childData._datums);
                         this._datumsQuery.next();
                         this.item = this._datumsQuery.item;
                         return 1; // has next
                     }
                 }
             } // while(atomsOrQuery)
             
             // No more OR atoms in this _data
             if(!depth) { return 0; } // finished
             
             // Pop parent data
             state = stateStack.pop();
             this._data = state.data;
             this._dimAtomsOrQuery = state.dimAtomsOrQuery;
             depth--;
         } while(true);
         
         // Never executes
         return 0; // finished
     });
}

pvc.data.Data
.add(/** @lends pvc.data.Data# */{
    /**
     * Returns some information on the data points
     */
    getInfo: function(){

        var out = ["DATA SUMMARY", pvc.logSeparator, "  Dimension", pvc.logSeparator];
        
        def.eachOwn(this.dimensions(), function(dimension, name){
            var count = dimension.count(),
                type = dimension.type,
                features = [];
            
            features.push('"' + type.label + '"');
            features.push(type.valueTypeName);
            
            if(type.isComparable){ features.push("comparable"); }
            if(!type.isDiscrete){ features.push("continuous"); }
            if(type.isHidden){ features.push("hidden"); }
            
            out.push(
                "  " + 
                name +
                " (" + features.join(', ') + ")" +
                " (" + count + ")\n\t" + 
                dimension.atoms().slice(0, 10).map(function(atom){ return atom.label; }).join(", ") + 
                (count > 10 ? "..." : ""));
        });
        
        //out.push(pvc.logSeparator);

        return out.join("\n");
    },
    
    /**
     * Returns the values for the dataset
     * BoxPlot, DataTree, ParallelCoordinates
     * 
     * @deprecated
     */
    getValues: function(){
        /**
         * Values is the inner Vs matrix
         *  X | S1  | ... | S2  |
         * ----------------------
         * C1 | V11 | ... | VN1 |
         *  . |   .           .
         * CJ | V1J | ... | VNJ |
         */
        return pv.range(0, this.getCategoriesSize())
            .map(function(categIndex){
                return this._getValuesForCategoryIndex(categIndex);
            }, this);
    },
    
    /**
     * Returns the unique values of a given dimension.
     * 
     * @deprecated
     */
    _getDimensionValues: function(name){
        return this.dimensions(name).atoms().map(function(atom){ return atom.value; });
    },

    /**
     * Returns the unique visible values of a given dimension.
     * 
     * @deprecated
     */
    _getDimensionVisibleValues: function(name){
        return this.dimensions(name).atoms({visible: true}).map(function(atom){ return atom.value; });
    },
    
    /**
     * Returns the unique series values.
     * @deprecated
     */
    getSeries: function(){
        return this._getDimensionValues('series');
    },

    /**
     * Returns an array with the indexes of the visible series values.
     * @deprecated
     */
    getVisibleSeriesIndexes: function(){
        return this.dimensions('series').indexes({visible: true});
    },
    
    /**
     * Returns an array with the indexes of the visible category values.
     * @deprecated
     */
    getVisibleCategoriesIndexes: function(){
        return this.dimensions('category').indexes({visible: true});
    },

    /**
     * Returns an array with the visible series.
     * @deprecated
     */
    getVisibleSeries: function(){
        return this._getDimensionVisibleValues('series');
    },

    /**
     * Returns the categories on the underlying data
     * @deprecated
     */
    getCategories: function(){
        return this._getDimensionValues('category');
    },

    /**
     * Returns an array with the visible categories.
     * 
     * @deprecated
     */
    getVisibleCategories: function(){
        return this._getDimensionVisibleValues('category');
    },
    
    /**
     * Returns the values for a given category index
     * @deprecated
     */
    _getValuesForCategoryIndex: function(categIdx){
        var categAtom = this.dimensions('category').atoms()[categIdx];
        var datumsBySeriesKey = this.datums({category: categAtom})
                                    .uniqueIndex(function(datum){ return datum.atoms.series.key; });
        
        // Sorted series atoms
        return this.dimensions('series')
                   .atoms()
                   .map(function(atom){
                        var datum = def.getOwn(datumsBySeriesKey, atom.key);
                        return datum ? datum.atoms.value.value : null;
                    });
    },
    
    /**
     * Returns how many series we have
     * @deprecated
     */
    getSeriesSize: function(){
        var dim = this.dimensions('series', {assertExists: false});
        return dim ? dim.count() : 0;
    },

    /**
     * Returns how many categories, or data points, we have
     * @deprecated
     */
    getCategoriesSize: function(){
        var dim = this.dimensions('category', {assertExists: false});
        return dim ? dim.count() : 0;
    }
});

def.scope(function() {
    
    var I = def.makeEnum([
        'Interactive',
        'ShowsActivity', 
        'ShowsSelection',
        'ShowsTooltip',
        'Selectable',
        'Unselectable',
        'Hoverable',
        'Clickable',
        'DoubleClickable',
        'SelectableByClick',       // => Selectable && !SelectableByFocusWindow
        'SelectableByRubberband',  // => Selectable && !SelectableByFocusWindow
        'SelectableByFocusWindow', // => Selectable && !SelectableByRubberband
        'Animatable'
    ]);
    
    // Combinations
    I.ShowsInteraction  = I.ShowsActivity    | I.ShowsSelection;
    I.Actionable        = /*I.Selectable | */ I.Hoverable | I.Clickable | I.DoubleClickable | I.SelectableByClick;
    I.HandlesEvents     = I.Actionable       | I.ShowsTooltip;
    I.HandlesClickEvent = I.Clickable        | I.SelectableByClick;
    //I.Interactive       = -1, // any bit
    
    def
    .type('pvc.visual.Interactive')
    .addStatic(I)
    .addStatic({
        ShowsAny:      I.ShowsInteraction | I.ShowsTooltip,
        SelectableAny: I.Selectable | I.SelectableByClick | I.SelectableByRubberband | I.SelectableByFocusWindow 
    })
    
    .add({ _ibits: -1 }) // all ones instance field
    
    // methods showsActivity, showsSelection, ...
    .add(def.query(def.ownKeys(I))
            .object({
                name:  def.firstLowerCase,
                value: function(p) {
                    var mask = I[p];
                    return function() { return !!(this._ibits & mask); };
                }
            })
    );
});

/**
 * Initializes a scene.
 * 
 * @name pvc.visual.Scene
 * @class Scenes guide the rendering of protovis marks;
 * they are supplied to {@link pv.Mark} <tt>data</tt> property.
 * <p>
 * A scene may feed several marks and so is not specific to a given mark 
 * (contrast with protovis' instances/scenes).
 * </p>
 * <p>
 * Scenes provide a well defined interface to pvc's 
 * extension point functions.
 * </p>
 * <p>
 * Scenes hold precomputed data, that does not change with interaction,
 * and that is thus not recalculated in every protovis render caused by interaction.
 * </p>
 * <p>
 * Scenes bridge the gap between data and visual roles. 
 * Data can be accessed by one or the other view.
 * </p>
 * 
 * @borrows pv.Dom.Node#visitBefore as #visitBefore
 * @borrows pv.Dom.Node#visitAfter as #visitAfter
 * 
 * @borrows pv.Dom.Node#nodes as #nodes
 * @borrows pv.Dom.Node#firstChild as #firstChild
 * @borrows pv.Dom.Node#lastChild as #lastChild
 * @borrows pv.Dom.Node#previousSibling as #previousSibling
 * @borrows pv.Dom.Node#nextSibling as #nextSibling
 * 
 * 
 * @property {pvc.data.Data}  group The data group that's present in the scene, or <tt>null</tt>, if none.
 * @property {pvc.data.Datum} datum The datum that's present in the scene, or <tt>null</tt>, if none.
 * @property {object} atoms The map of atoms, by dimension name, that's present in the scene, or <tt>null</tt>, if none.
 * <p>
 * When there is a group, these are its atoms, 
 * otherwise, 
 * if there is a datum, 
 * these are its atoms.
 * </p>
 * <p>
 * Do <b>NOT</b> modify this object.
 * </p>
 * 
 * @constructor
 * @param {pvc.visual.Scene} [parent=null] The parent scene.
 * @param {object} [keyArgs] Keyword arguments.
 * @property {pvc.data.Datum | pvc.data.Data | pvc.data.Datum[] | pvc.data.Data[]} 
 *  [keyArgs.source=null]
 *  The data source(s) that are present in the scene.
 */
def.type('pvc.visual.Scene')
.init(function(parent, keyArgs) {
    if(pvc.debug >= 4) { this.id = def.nextId('scene'); }
    
    this._renderId   = 0;
    this.renderState = {};
    
    pv.Dom.Node.call(this, /* nodeValue */null);
    
    this.parent = parent || null;
    if(parent) {
        this.root = parent.root;
        
        // parent -> ((pv.Dom.Node#)this).parentNode
        // this   -> ((pv.Dom.Node#)parent).childNodes
        // ...
        var index = def.get(keyArgs, 'index', null);
        parent.insertAt(this, index);
    } else {
        /* root scene */
        this.root = this;
        
        this._active = null;
        this._panel = def.get(keyArgs, 'panel') || 
            def.fail.argumentRequired('panel', "Argument is required on root scene.");
    }
    
    /* DATA */
    var first, group, datum, datums, groups, atoms, firstAtoms;
    var dataSource = def.array.to(def.get(keyArgs, 'source')); // array.to: nully remains nully
    if(dataSource && dataSource.length) {
        this.source = dataSource;
        
        first = dataSource[0];
        if(first instanceof pvc.data.Data) {
            // Group/groups
            group  = first;
            groups = dataSource;
            
            // There are datas with no datums.
            // For example, try, hiding all datums (using the legend).
            datum  = group.firstDatum() || 
                     def
                     .query(groups)
                     .select(function(g) { return g.firstDatum(); })
                     .first(def.notNully);
            // datum may still be null!
        } else {
            /*jshint expr:true */
            (first instanceof pvc.data.Datum) || def.assert("not a datum");
            datum  = first;
            datums = dataSource;
        }
        
        atoms      = first.atoms; // firstDataSourceAtoms
        firstAtoms = (datum && datum.atoms) || first.atoms; // firstDatumAtoms
    } else if(parent) {
        atoms = firstAtoms = Object.create(parent.atoms);
    } else {
        atoms = firstAtoms = {};
    }
    
    // Created empty even when there is no data
    this.atoms = atoms;
    this.firstAtoms = firstAtoms;
    
    // Only set when existent, otherwise inherit from prototype
    groups && (this.groups  = groups);
    group  && (this.group   = group );
    datums && (this._datums = datums);
    datum  && (this.datum   = datum );
    
    // Groups may have some null datums and others not null
    // Testing groups first ensures that the only
    // case where isNull is detected is that of a single datum scene.
    // Note that groups do not have isNull property, only datums do.
    if(!first || first.isNull) { this.isNull = true; }

    /* VARS */
    this.vars = parent ? Object.create(parent.vars) : {};
})
.add(pv.Dom.Node)
.add(pvc.visual.Interactive)
.add(/** @lends pvc.visual.Scene# */{
    source: null,
    groups: null,
    group:  null,
    _datums: null,
    datum:  null,
    
    isNull: false,
    
    /** 
     * Obtains the (first) group of this scene, or if inexistent
     * the group of the parent scene, if there is one, and so on.
     * If no data can be obtained in this way,
     * the data of the associated panel is returned.
     */
    data: function() {
        var data = this.group;
        if(!data) {
            var scene = this;
            while(!data && (scene = scene.parent)) { data = scene.group; }
            if(!data) { data = this.panel.data; }
        }
        return data;
    },
    
    /**
     * Obtains an enumerable of the datums present in the scene.
     *
     * @type def.Query
     */
    datums: function() {
        // For efficiency, assumes datums of multiple groups are disjoint sets
        return this.groups  ? def.query(this.groups ).selectMany(function(g){ return g.datums(); }) :
               this._datums ? def.query(this._datums) :
               def.query();
    },

    /*
     * {value} -> <=> this.vars.value.label
     * {value.value} -> <=> this.vars.value.value
     * {#sales} -> <=> this.atoms.sales.label
     */
    format: function(mask) { return def.format(mask, this._formatScope, this); },
    
    _formatScope: function(prop) {
        if(prop.charAt(0) === '#') {
            // An atom name
            prop = prop.substr(1).split('.');
            if(prop.length > 2) { throw def.error.operationInvalid("Scene format mask is invalid."); }
            
            var atom = this.firstAtoms[prop[0]];
            if(atom) {
                if(prop.length > 1) {
                    switch(prop[1]) {
                        case 'value': return atom.value;
                        case 'label': break;
                        default:      throw def.error.operationInvalid("Scene format mask is invalid.");
                    }
                }
                
                // atom.toString() ends up returning atom.label
                return atom;
            }
            
            return null; // Atom does not exist --> ""
        }
        
        // A scene var name
        return def.getPath(this.vars, prop); // Scene vars' toString may end up being called
    },
    
    isRoot: function() { return this.root === this; },
    panel:  function() { return this.root._panel; },
    chart:  function() { return this.root._panel.chart; },
    compatVersion: function() { return this.root._panel.compatVersion(); },
    
    /**
     * Obtains an enumerable of the child scenes.
     * 
     * @type def.Query
     */
    children: function() { return this.childNodes ? def.query(this.childNodes) : def.query(); },
    
    leafs: function() {
        
        function getFirstLeafFrom(leaf) {
            while(leaf.childNodes.length) { leaf = leaf.childNodes[0]; }
            return leaf;
        }
        
        var root = this;
        return def.query(function(nextIndex) {
            if(!nextIndex) {
                // Initialize
                var item = getFirstLeafFrom(root);
                if(item === root) { return 0; }
                
                this.item = item;
                return 1; // has next
            }
            
            // Has a next sibling?
            var next = this.item.nextSibling;
            if(next) {
                this.item = next;
                return 1; // has next
            }
            
            // Go to closest ancestor that has a sibling
            var current = this.item;
            while((current !== root) && (current = current.parentNode)) {
                if((next = current.nextSibling)) {
                    // Take the first leaf from there
                    this.item = getFirstLeafFrom(next);
                    return 1;
                }
            }
            
            return 0;
        });
    },
    
    /* INTERACTION */
    anyInteraction: function() { return (!!this.root._active || this.anySelected()); },

    /* ACTIVITY */
    isActive: false,
    
    setActive: function(isActive) {
        isActive = !!isActive; // normalize
        if(this.isActive !== isActive) {
            rootScene_setActive.call(this.root, this.isActive ? null : this);
        }
    },

    // This is misleading as it clears whatever the active scene is,
    // not necessarily the scene on which it is called.
    clearActive: function() { return rootScene_setActive.call(this.root, null); },
    
    anyActive: function() { return !!this.root._active; },
    
    active: function() { return this.root._active; },
    
    activeSeries: function() {
        var active = this.active();
        var seriesVar;
        return active && (seriesVar = active.vars.series) && seriesVar.value;
    },
    
    isActiveSeries: function() {
        if(this.isActive) { return true; }

        var isActiveSeries = this.renderState.isActiveSeries;
        if(isActiveSeries == null) {
            var activeSeries;
            isActiveSeries = (activeSeries = this.activeSeries()) != null &&
                             (activeSeries === this.vars.series.value);

            this.renderState.isActiveSeries = isActiveSeries;
        }

        return isActiveSeries;
    },

    isActiveDatum: function() {
        if(this.isActive) { return true; }

        // Only testing the first datum of both because of performance
        // so, unless they have the same group or the  order of datums is the same...
        var isActiveDatum = this.renderState.isActiveDatum;
        if(isActiveDatum == null) {
            var activeScene = this.active();
            if(activeScene) {
                isActiveDatum = (this.group && activeScene.group === this.group) ||
                                (this.datum && activeScene.datum === this.datum);
            } else {
                isActiveDatum = false;
            }
            
            this.renderState.isActiveDatum = isActiveDatum;
        }
        
        return isActiveDatum;
    },
    
    isActiveDescendantOrSelf: function() {
        if(this.isActive) { return true; }
        
        return def.lazy(this.renderState, 'isActiveDescOrSelf',  this._calcIsActiveDescOrSelf, this);
    },
    
    _calcIsActiveDescOrSelf: function() {
        var scene = this.active();
        if(scene) {
            while((scene = scene.parent)) { if(scene === this) { return true; } }
        }
        return false;
    },
    
    /* VISIBILITY */
    isVisible:  function() { return this._visibleData().is;  },
    anyVisible: function() { return this._visibleData().any; },
    
    _visibleData: function() {
        return def.lazy(this.renderState, '_visibleData', this._createVisibleData, this);
    },
    
    _createVisibleData: function() {
        var any = this.chart().data.owner.visibleCount() > 0,
            isSelected = any && this.datums().any(def.propGet('isVisible'));
        
        return {any: any, is: isSelected};
    },
    
    /* SELECTION */
    isSelected:  function() { return this._selectedData().is;  },
    anySelected: function() { return this._selectedData().any; },
    
    _selectedData: function() {
        return def.lazy(this.renderState, '_selectedData', this._createSelectedData, this);
    },
    
    _createSelectedData: function() {
        /*global datum_isSelected:true */
        var any = this.chart().data.owner.selectedCount() > 0,
            isSelected = any && this.datums().any(datum_isSelected);
        
        return {any: any, is: isSelected};
    },
    
    /* ACTIONS - Update UI */
    select: function(ka) {
        var me = this;
        var datums = me.datums().array();
        if(datums.length) {
            var chart = me.chart();
            chart._updatingSelections(function() {
                datums = chart._onUserSelection(datums);
                if(datums && datums.length) {
                    if(chart.options.ctrlSelectMode && def.get(ka, 'replace', true)) {
                        chart.data.replaceSelected(datums);
                    } else {
                        pvc.data.Data.toggleSelected(datums);
                    }
                }
            });
        }
    },
    
    isSelectedDescendantOrSelf: function() {
        if(this.isSelected()) { return true; }
        
        return def.lazy(this.renderState, 'isSelectedDescOrSelf',  this._calcIsSelectedDescOrSelf, this);
    },
    
    _calcIsSelectedDescOrSelf: function() {
        var child = this.firstChild;
        if(child) {
            do {
              if(child.isSelectedDescendantOrSelf()) { return true; }
            } while((child = child.nextSibling));
        }
        return false;
    },
    
    // -------
    
    toggleVisible: function() {
        if(pvc.data.Data.toggleVisible(this.datums())) {
            // Re-render chart
            this.chart().render(true, true, false);
        }
    }
});

/** 
 * Called on each sign's pvc.visual.Sign#buildInstance 
 * to ensure cached data per-render is cleared.
 * 
 *  @param {number} renderId The current render id.
 */
function scene_renderId(renderId) {
    if(this._renderId !== renderId) {
        this._renderId   = renderId;
        this.renderState = {};
    }
}

function rootScene_setActive(scene) {
    var ownerScene;
    if(scene && (ownerScene = scene.ownerScene)) { scene = ownerScene; }
    
    if(this._active !== scene) {
        if(this._active) { scene_setActive.call(this._active, false); }
        
        this._active = scene || null;
        
        if(this._active) { scene_setActive.call(this._active, true); }
        
        return true;
    }
    
    return false;
}

function scene_setActive(isActive) {
    if(this.isActive !== isActive) {
        // Inherits isActive = false
        if(!isActive) { delete this.isActive; } 
        else          { this.isActive = true; }
    }
}

// -------------------------

// Custom scene classes
// Define a custom scene subclass that contains certain vars, for serving a certain
// panel's scenes; for example: BarChartSeriesScene and BarChartSeriesAndCategoryScene.
// Each instance of such sub-classes will evaluate the values of its vars.
// 
// External extension must affect all instances of a given custom scene sub-class.
// This implies sub-classing once more, this time the custom sub-class, 
// to be able to override the default vars' methods.
// Note that no new vars will be defined,
// just overrides of the base classes' default var functions.
// Possibly, we could let the user declare additional vars
// that could be used to store shared state.
// Overriding default vars' methods may not be done by normal sub-classing
// as some post-processing is required of the result of such functions.
// Overriding a default _core_ method would make sense though.
//
// To be called on the class prototype, not on instances.
pvc.visual.Scene.prototype.variable = function(name, impl) {
    var proto = this;
    var methods;
    
    // Var already defined (local or inherited)?
    if(!(name in proto)) {
        if(!(proto.hasOwnProperty('_vars'))) {
            proto._vars = def.create(proto._vars);
        }
        
        proto._vars[name] = true;
        
        // Variable Class methods
        // ex:
        // series()                    (non-overridable: in cache or eval)
        // |--> seriesEval()           (internally overridable; dispatches to evalCore; validates/processes/casts)
        //      |--> seriesEvalCore()  (impl.; externally overridable)
        methods = {};
        
        var nameEval = '_' + name + 'Eval';
        methods[name] = scene_createVarMainMethod(name, nameEval);
            
        var nameEvalCore = nameEval + 'Core';
        
        // _Eval_ Already defined?
        if(!def.hasOwn(proto, nameEval)) {
            methods[nameEval] = def.methodCaller(nameEvalCore);
        }
        
        // _EvalCore_ already defined?
        if(!def.hasOwn(proto, nameEvalCore)) {
            // Normalize undefined to null (working as a default value)
            methods[nameEvalCore] = def.fun.to(impl === undefined ? null : impl);
        }
    } else if(impl !== undefined) {
        // Override (EvalCore) implementation
        methods = def.set({}, '_' + name + 'EvalCore', def.fun.to(impl));
    }
    
    // Add methods to class
    if(methods) { proto.constructor.add(methods); }
    
    return proto;
};

/* Not intended to be overridden. */
function scene_createVarMainMethod(name, nameEval) {
    return function() {
        // Evaluate on first time used.
        // If _baseImpl_ depends on other variables, 
        // they too will be evaluated (if not already).
        // No cycle detection is performed.
        var vb = this.vars[name];
        if(vb === undefined) {
            vb = this[nameEval]();
            if(vb === undefined) { vb = null; }
            this.vars[name] = vb;
        }
        
        return vb;
    };
}


/**
 * Initializes a scene variable.
 *
 * @name pvc.visual.ValueLabelVar
 * @class A scene variable holds the concrete value that
 * a {@link pvc.visual.Role} or other relevant piece of information
 * has in a {@link pvc.visual.Scene}.
 * Usually, it also contains a label that describes it.
 *
 * @constructor
 * @param {any} value The value of the variable.
 * @param {any} label The label of the variable.
 * @param {any} [rawValue] The raw value of the variable.
 */
var pvc_ValueLabelVar = pvc.visual.ValueLabelVar = function(value, label, rawValue, absLabel){
    this.value = value;
    this.label = label;
    
    if(rawValue !== undefined) { this.rawValue = rawValue; }
    if(absLabel !== undefined) { this.absLabel = absLabel; } // Only Data have absLabel not undefined
};

def.set(
    pvc_ValueLabelVar.prototype,
    'rawValue', undefined,
    'absLabel', undefined,
    'setValue', function(v) {
        this.value = v;
        return this;
    },
    'setLabel', function(v) {
        this.label = v;
        return this;
    },
    'clone',    function(){
        return new pvc_ValueLabelVar(this.value, this.label, this.rawValue);
    },
    'toString', function(){
        var label = this.label || this.value;
        return label == null ? "" :
               (typeof label !== 'string') ? ('' + label) :
               label;
    }
    //'valueOf', function() { return this.value; }
    );

pvc_ValueLabelVar.fromComplex = function(complex) {
    return complex ?
           new pvc_ValueLabelVar(complex.value, complex.label, complex.rawValue, complex.absLabel) :
           new pvc_ValueLabelVar(null, "", null);
};

pvc_ValueLabelVar.fromAtom = pvc_ValueLabelVar.fromComplex;

/**
 * Initializes a visual context.
 * 
 * @name pvc.visual.Context
 * 
 * @class Represents a visualization context.  
 * The visualization context gives access to all relevant information
 * for rendering or interacting with a visualization.
 * <p>
 * A visualization context object <i>may</i> be reused
 * across extension points invocations and actions.
 * </p>
 * 
 * @property {pvc.BaseChart} chart The chart instance.
 * @property {pvc.BasePanel} panel The panel instance.
 * @property {number} index The render index.
 * @property {pvc.visual.Scene} scene The render scene.
 * @property {object} event An event object, present when a click or double-click action is being processed.
 * @property {pv.Mark} pvMark The protovis mark.
 * 
 * @constructor
 * @param {pvc.BasePanel} panel The panel instance.
 * @param {pv.Mark} mark The protovis mark.
 * @param {object} [event] An event object.
 */
def.type('pvc.visual.Context')
.init(function(panel, mark, event){
    this.chart = panel.chart;
    this.panel = panel;
    
    visualContext_update.call(this, mark, event);
})
.add(/** @lends pvc.visual.Context */{
    isPinned: false,
    
    pin: function() {
        this.isPinned = true;
        return this;
    },
    
    compatVersion: function() { return this.panel.compatVersion(); },
    
    finished: function(v ) { return this.sign.finished(v ); },
    delegate: function(dv) { return this.sign.delegate(dv); },
    
    /* V1 DIMENSION ACCESSORS */
    getV1Series: function() {
        var s;
        return def.nullyTo(
                this.scene.firstAtoms && (s = this.scene.firstAtoms[this.panel._getV1DimName('series')]) && s.rawValue,
                'Series');
    },
    
    getV1Category: function() {
        var c;
        return this.scene.firstAtoms && (c = this.scene.firstAtoms[this.panel._getV1DimName('category')]) && c.rawValue;
    },
               
    getV1Value: function() {
        var v;
        return this.scene.firstAtoms && (v = this.scene.firstAtoms[this.panel._getV1DimName('value')]) && v.value;
    },
    
    getV1Datum: function() { return this.panel._getV1Datum(this.scene); },
    
    select:        function(ka) { return this.scene.select(ka); },
    toggleVisible: function(  ) { return this.scene.toggleVisible(); },
    
    /* EVENT HANDLERS */
    click: function() {
        var me = this;
        if(me.clickable()) {  me.panel._onClick(me); }
        
        if(me.selectableByClick()) {
            var ev = me.event;
            me.select({replace: !ev || !ev.ctrlKey});
        }
    },
    
    doubleClick: function() { if(this.doubleClickable()) { this.panel._onDoubleClick(this); } },
    
    /* Interactive Stuff */
    clickable: function() {
        var me = this;
        return (me.sign ? me.sign.clickable() : me.panel.clickable()) &&
               (!me.scene || me.scene.clickable());
    },
    
    selectableByClick: function() {
        var me = this;
        return (me.sign ? me.sign.selectableByClick() : me.panel.selectableByClick()) &&
               (!me.scene || me.scene.selectableByClick());
    },
    
    doubleClickable: function() {
        var me = this;
        return (me.sign ? me.sign.doubleClickable() : me.panel.doubleClickable()) &&
               (!me.scene || me.scene.doubleClickable());
    },
    
    hoverable: function() {
        var me = this;
        return (me.sign ? me.sign.hoverable() : me.panel.hoverable()) &&
               (!me.scene || me.scene.hoverable());
    }
});

if(Object.defineProperty){
    try{
        Object.defineProperty(pvc.visual.Context.prototype, 'parent', {
            get: function(){
                throw def.error.operationInvalid("The 'this.parent.index' idiom has no equivalent in this version. Please try 'this.pvMark.parent.index'.");
            }
        });
    } catch(ex) {
        /* IE THROWS */
    }
}

/**
 * Used internally to update a visual context.
 * 
 * @name pvc.visual.Context#_update
 * @function
 * @param {pv.Mark} [pvMark] The protovis mark being rendered or targeted by an event.
 * @param {object} [ev] An event object.
 * @type undefined
 * @private
 * @virtual
 * @internal
 */
function visualContext_update(pvMark, ev){

    this.event  = ev || pv.event;
    this.pvMark = pvMark;
    
    var scene;
    if(pvMark) {
        var sign = this.sign = pvMark.sign || null;
        if(sign) { scene = pvMark.instance().data; }
        
        if(!scene) {
            this.index = null;
            scene = new pvc.visual.Scene(null, {panel: this.panel});
        } else {
            this.index = scene.childIndex();
        }
    } else {
        this.sign  = null;
        this.index = null;
        
        scene = new pvc.visual.Scene(null, {
            panel:  this.panel,
            source: this.chart.root.data
        });
    }
    
    this.scene = scene;
}


def
.space('pvc.visual')
.TraversalMode = def.makeEnum([
    'Tree',
    'FlattenedSingleLevel', // Flattened grouping to a single grouping level
    'FlattenDfsPre',        // Same grouping levels and dimensions, but all nodes are output 
    'FlattenDfsPost'        // Idem, but in Dfs-Post order
]);

/**
 * Initializes a visual role.
 * 
 * @name pvc.visual.Role
 * 
 * @class Represents a role that is somehow played by a visualization.
 * 
 * @property {string} name The name of the role.
 *
 * @property {string} label
 * The label of this role.
 * The label <i>should</i> be unique on a visualization.
 *
 * @property {pvc.data.GroupingSpec} grouping The grouping specification currently bound to the visual role.
 * 
 * @property {boolean} isRequired Indicates that the role is required and must be satisfied.
 * 
 * @property {boolean} requireSingleDimension Indicates that the role can only be satisfied by a single dimension.
 * A {@link pvc.visual.Role} of this type must have an associated {@link pvc.data.GroupingSpec}
 * that has {@link pvc.data.GroupingSpec#isSingleDimension} equal to <tt>true</tt>.
 * 
 * @property {boolean} valueType When not nully, 
 * restricts the allowed value type of the single dimension of the 
 * associated {@link pvc.data.GroupingSpec} to this type.
 * 
 * @property {boolean|null} requireIsDiscrete
 * Indicates if 
 * only discrete, when <tt>true</tt>, 
 * continuous, when <tt>false</tt>, 
 * or any, when <tt>null</tt>,
 * groupings are accepted.
 * 
 * @property {string} defaultDimensionName The default dimension name.
 *
 * @property {boolean} autoCreateDimension Indicates if a dimension with the default name (the first level of, when a group name),
 * should be created when the role has not been read by a translator (required or not).
 *
 * @constructor
 * @param {string} name The name of the role.
 * @param {object} [keyArgs] Keyword arguments.
 * @param {string} [keyArgs.label] The label of this role.
 *
 * @param {boolean} [keyArgs.isRequired=false] Indicates a required role.
 * 
 * @param {boolean} [keyArgs.requireSingleDimension=false] Indicates that the role 
 * can only be satisfied by a single dimension. 
 * 
 * @param {boolean} [keyArgs.isMeasure=false] Indicates that <b>datums</b> that do not 
 * contain a non-null atom in any of the dimensions bound to measure roles should be readily excluded.
 * 
 * @param {boolean} [keyArgs.valueType] Restricts the allowed value type of dimensions.
 * 
 * @param {boolean|null} [keyArgs.requireIsDiscrete=null] Indicates if the grouping should be discrete, continuous or any.
 * 
 * @param {string} [keyArgs.defaultDimensionName] The default dimension name.
 * @param {boolean} [keyArgs.autoCreateDimension=false]
 * Indicates if a dimension with the default name (the first level of, when a group name),
 * should be created when the role is required and it has not been read by a translator.
 *
 * @param {pvc.visual.TraversalMode} [keyArgs.traversalMode=pvc.visual.TraversalMode.FlattenedSingleLevel] 
 * Indicates the type of data nodes traversal that the role performs.
 */
def
.type('pvc.visual.Role')
.init(function(name, keyArgs){
    this.name  = name;
    this.label = def.get(keyArgs, 'label') || pvc.buildTitleFromName(name);
    this.index = def.get(keyArgs, 'index') || 0;
    
    this.dimensionDefaults = def.get(keyArgs, 'dimensionDefaults') || {};
    
    if(def.get(keyArgs, 'isRequired', false)) {
        this.isRequired = true;
    }
    
    if(def.get(keyArgs, 'autoCreateDimension', false)) {
        this.autoCreateDimension = true;
    }
    
    var defaultSourceRoleName = def.get(keyArgs, 'defaultSourceRole');
    if(defaultSourceRoleName) {
        this.defaultSourceRoleName = defaultSourceRoleName;
    }
    
    var defaultDimensionName = def.get(keyArgs, 'defaultDimension');
    if(defaultDimensionName) {
        this.defaultDimensionName = defaultDimensionName;
    }

    if(!defaultDimensionName && this.autoCreateDimension){
        throw def.error.argumentRequired('defaultDimension');
    }
    
    var requireSingleDimension;
    var requireIsDiscrete = def.get(keyArgs, 'requireIsDiscrete'); // isSingleDiscrete
    if(requireIsDiscrete != null) {
        if(!requireIsDiscrete) {
            requireSingleDimension = true;
        }
    }
    
    if(requireSingleDimension != null) {
        requireSingleDimension = def.get(keyArgs, 'requireSingleDimension', false);
        if(requireSingleDimension) {
            if(def.get(keyArgs, 'isMeasure', false)) {
                this.isMeasure = true;
                
                if(def.get(keyArgs, 'isPercent', false)) {
                    this.isPercent = true;
                }
            }
            
            var valueType = def.get(keyArgs, 'valueType', null);
            if(valueType !== this.valueType) {
                this.valueType = valueType;
                this.dimensionDefaults.valueType = valueType;
            }
        }
    }
    
    if(requireSingleDimension !== this.requireSingleDimension) {
        this.requireSingleDimension = requireSingleDimension;
    }
    
    if(requireIsDiscrete != this.requireIsDiscrete) {
        this.requireIsDiscrete = !!requireIsDiscrete;
        this.dimensionDefaults.isDiscrete = this.requireIsDiscrete;
    }

    var traversalMode = def.get(keyArgs, 'traversalMode');
    if(traversalMode != null && traversalMode !== this.traversalMode) {
        this.traversalMode = traversalMode;
    }
})
.add(/** @lends pvc.visual.Role# */{
    isRequired: false,
    requireSingleDimension: false,
    valueType: null,
    requireIsDiscrete: null,
    isMeasure: false,
    isPercent: false,
    defaultSourceRoleName: null,
    defaultDimensionName:  null,
    grouping: null,
    traversalMode: pvc.visual.TraversalMode.FlattenedSingleLevel,
    rootLabel: '',
    autoCreateDimension: false,
    isReversed: false,
    label: null,
    sourceRole: null,
    isDefaultSourceRole: false,
    
    /** 
     * Obtains the first dimension type that is bound to the role.
     * @type pvc.data.DimensionType
     */
    firstDimensionType: function() {
        var g = this.grouping;
        return g && g.firstDimensionType();
    },
    
    /** 
     * Obtains the name of the first dimension type that is bound to the role.
     * @type string 
     */
    firstDimensionName: function() {
        var g = this.grouping;
        return g && g.firstDimensionName();
    },
    
    /** 
     * Obtains the value type of the first dimension type that is bound to the role.
     * @type function
     */
    firstDimensionValueType: function() {
        var g = this.grouping;
        return g && g.firstDimensionValueType();
    },

    isDiscrete: function() {
        var g = this.grouping;
        return g && g.isDiscrete();
    },
    
    setSourceRole: function(sourceRole, isDefault) {
        this.sourceRole = sourceRole;
        this.isDefaultSourceRole = !!isDefault;
    },
    
    setIsReversed: function(isReversed) {
        if(!isReversed) { delete this.isReversed; } 
        else            { this.isReversed = true; }
    },
    
    setTraversalMode: function(travMode) {
        var T = pvc.visual.TraversalMode;
        
        travMode = def.nullyTo(travMode, T.FlattenedSingleLevel);
        
        if(travMode !== this.traversalMode) {
            if(travMode === T.FlattenedSingleLevel) { // default value
                delete this.traversalMode;
            } else {
                this.traversalMode = travMode;
            }
        }
    },

    setRootLabel: function(rootLabel) {
        if(rootLabel !== this.rootLabel) {
            if(!rootLabel) { delete this.rootLabel;      } // default value shows through 
            else           { this.rootLabel = rootLabel; }
            
            if(this.grouping) { this._updateBind(this.grouping); }
        }
    },

    /**
     * Applies this role's grouping to the specified data
     * after ensuring the grouping is of a certain type.
     *
     * @param {pvc.data.Data} data The data on which to apply the operation.
     * @param {object} [keyArgs] Keyword arguments.
     * ...
     * 
     * @type pvc.data.Data
     */
    flatten: function(data, keyArgs) {
        var grouping = this.flattenedGrouping(keyArgs) || def.fail.operationInvalid("Role is unbound.");
            
        return data.groupBy(grouping, keyArgs);
    },

    flattenedGrouping: function(keyArgs) {
        var grouping = this.grouping;
        if(grouping) {
            if(!keyArgs){ keyArgs = {}; }
            var flatMode = keyArgs.flatteningMode;
            if(flatMode == null) {
                flatMode = keyArgs.flatteningMode = this._flatteningMode();
            }
            
            if(keyArgs.isSingleLevel == null && !flatMode) {
                keyArgs.isSingleLevel = true;
            }
            
            if(keyArgs.flatteningMode == null) { keyArgs.flatteningMode = this._flatteningMode(); }

            return grouping.ensure(keyArgs);
        }
    },

    _flatteningMode: function() {
        var T = pvc.visual.TraversalMode;
        var F = pvc.data.FlatteningMode;
        switch(this.traversalMode) {
            case T.FlattenDfsPre:  return F.DfsPre;
            case T.FlattenDfsPost: return F.DfsPost;
        }
        return T.None;
    },
    
    select: function(data, keyArgs) {
        var grouping = this.grouping;
        if(grouping) {
            def.setUDefaults(keyArgs, 'flatteningMode', pvc.data.FlatteningMode.None);
            return data.groupBy(grouping.ensure(keyArgs), keyArgs); 
        }
    },

    view: function(complex) {
        var grouping = this.grouping;
        if(grouping){ return grouping.view(complex); }
    },

    /**
     * Pre-binds a grouping specification to playing this role.
     * 
     * @param {pvc.data.GroupingSpec} groupingSpec The grouping specification of the visual role.
     */
    preBind: function(groupingSpec) {
        this.__grouping = groupingSpec;

        return this;
    },

    isPreBound: function() { return !!this.__grouping; },
    
    preBoundGrouping: function() { return this.__grouping; },
    
    isBound: function() { return !!this.grouping; },
    
    /**
     * Finalizes a binding initiated with {@link #preBind}.
     *
     * @param {pvc.data.ComplexType} type The complex type with which
     * to bind the pre-bound grouping and then validate the
     * grouping and role binding.
     */
    postBind: function(type) {
        var grouping = this.__grouping;
        if(grouping) {
            delete this.__grouping;

            grouping.bind(type);

            this.bind(grouping);
        }
        
        return this;
    },

    /**
     * Binds a grouping specification to playing this role.
     * 
     * @param {pvc.data.GroupingSpec} groupingSpec The grouping specification of the visual role.
     */
    bind: function(groupingSpec) {
        
        groupingSpec = this._validateBind(groupingSpec);
        
        this._updateBind(groupingSpec);

        return this;
    },
    
    _validateBind: function(groupingSpec) {
        if(groupingSpec) {
            if(groupingSpec.isNull()) {
                groupingSpec = null;
           } else {
                /* Validate grouping spec according to role */

                if(this.requireSingleDimension && !groupingSpec.isSingleDimension) {
                    throw def.error.operationInvalid(
                            "Role '{0}' only accepts a single dimension.",
                            [this.name]);
                }

                var valueType = this.valueType;
                var requireIsDiscrete = this.requireIsDiscrete;
                groupingSpec.dimensions().each(function(dimSpec) {
                    var dimType = dimSpec.type;
                    if(valueType && dimType.valueType !== valueType) {
                        throw def.error.operationInvalid(
                                "Role '{0}' cannot be bound to dimension '{1}'. \nIt only accepts dimensions of type '{2}' and not of type '{3}'.",
                                [this.name, dimType.name, pvc.data.DimensionType.valueTypeName(valueType), dimType.valueTypeName]);
                    }

                    if(requireIsDiscrete != null &&
                       dimType.isDiscrete !== requireIsDiscrete) {
                        
                        if(requireIsDiscrete) {
                            // A continuous dimension can be "coerced" to behave as discrete
                            dimType._toDiscrete();
                        } else {
                            throw def.error.operationInvalid(
                                "Role '{0}' cannot be bound to dimension '{1}'. \nIt only accepts {2} dimensions.",
                                [this.name, dimType.name, requireIsDiscrete ? 'discrete' : 'continuous']);
                        }
                    }
                }, this);
            }
        }
        
        return groupingSpec;
    },

    _updateBind: function(groupingSpec) {
        if(this.grouping) {
            // unregister from current dimension types
            this.grouping.dimensions().each(function(dimSpec) {
                if(dimSpec.type) {
                    /*global dimType_removeVisualRole:true */
                    dimType_removeVisualRole.call(dimSpec.type, this);
                }
            }, this);
        }
        
        this.grouping = groupingSpec;
        
        if(this.grouping) {
            this.grouping = this.grouping.ensure({
                reverse:   this.isReversed, 
                rootLabel: this.rootLabel
            });
            
            // register in current dimension types
            this.grouping.dimensions().each(function(dimSpec) {
                /*global dimType_addVisualRole:true */
                dimType_addVisualRole.call(dimSpec.type, this);  
            }, this);
        }
    }
});



/*global pvc_ValueLabelVar:true */

def
.type('pvc.visual.RoleVarHelper')
.init(function(rootScene, role, keyArgs){
    var hasPercentSubVar = def.get(keyArgs, 'hasPercentSubVar', false);
    var roleVarName = def.get(keyArgs, 'roleVar');
    
    var g = this.grouping = role && role.grouping;
    if(g) {
        this.role = role;
        this.sourceRoleName = role.sourceRole && role.sourceRole.name;
        var panel = rootScene.panel();
        this.panel = panel;
        
        if(!g.isDiscrete()) {
            this.rootContDim = panel.data.owner.dimensions(g.firstDimensionName());
            if(hasPercentSubVar) { this.percentFormatter = panel.chart.options.percentValueFormat; }
        }
    }
    
    if(!roleVarName) {
        if(!role) {
            throw def.error.operationInvalid("Role is not defined, so the roleVar argument is required.");
        }
        
        roleVarName = role.name;
    }
    
    if(!g) {
        // Unbound role
        // Place a null variable in the root scene
        var roleVar = rootScene.vars[roleVarName] = new pvc_ValueLabelVar(null, "");
        if(hasPercentSubVar) { roleVar.percent = new pvc_ValueLabelVar(null, ""); }
    }
    
    this.roleVarName = roleVarName;
    
    rootScene['is' + def.firstUpperCase(roleVarName) + 'Bound'] = !!g;
    
    if(def.get(keyArgs, 'allowNestedVars')) { this.allowNestedVars = true; }
})
.add({
    allowNestedVars: false,
    
    isBound: function(){
        return !!this.grouping;
    },

    onNewScene: function(scene, isLeaf){
        if(!this.grouping){
            return;
        }
        
        var roleVarName = this.roleVarName;
        if(this.allowNestedVars ? 
            def.hasOwnProp.call(scene.vars, roleVarName) : 
            scene.vars[roleVarName]){
            return;
        }
        
        var sourceName = this.sourceRoleName;
        if(sourceName){
            var sourceVar = def.getOwn(scene.vars, sourceName);
            if(sourceVar){
                scene.vars[roleVarName] = sourceVar.clone();
                return;
            }
        }
        
        // TODO: gotta improve this spaghetti somehow
        
        if(isLeaf){
            // Not grouped, so there's no guarantee that
            // there's a single value for all the datums of the group.
        
            var roleVar;
            var rootContDim = this.rootContDim;
            if(!rootContDim){
                // Discrete
                
                // We choose the value of the first datum of the group...
                var firstDatum = scene.datum;
                if(firstDatum && !firstDatum.isNull){
                    var view = this.grouping.view(firstDatum);
                    roleVar = pvc_ValueLabelVar.fromComplex(view);
                }
            } else {
                var valuePct, valueDim;
                var group = scene.group;
                var singleDatum = group ? group.singleDatum() : scene.datum;
                if(singleDatum){
                    if(!singleDatum.isNull){
                        roleVar = pvc_ValueLabelVar.fromAtom(singleDatum.atoms[rootContDim.name]);
                        if(roleVar.value != null && this.percentFormatter){
                            if(group){
                                valueDim = group.dimensions(rootContDim.name);
                                valuePct = valueDim.percentOverParent({visible: true});
                            } else {
                                valuePct = scene.data().dimensions(rootContDim.name).percent(roleVar.value);
                            }
                        }
                    }
                } else if(group){
                    valueDim = group.dimensions(rootContDim.name);
                    var value = valueDim.sum({visible: true, zeroIfNone: false});
                    if(value != null){
                        var label = rootContDim.format(value);
                        roleVar = new pvc_ValueLabelVar(value, label, value);
                        if(this.percentFormatter){
                            valuePct = valueDim.percentOverParent({visible: true});
                        }
                    }
                }
                
                if(roleVar && this.percentFormatter){
                    if(roleVar.value == null){
                        roleVar.percent = new pvc_ValueLabelVar(null, "");
                    } else {
                        roleVar.percent = new pvc_ValueLabelVar(
                                          valuePct,
                                          this.percentFormatter.call(null, valuePct));
                    }
                }
            }
            
            if(!roleVar){
                roleVar = new pvc_ValueLabelVar(null, "");
                if(this.percentFormatter){
                    roleVar.percent = new pvc_ValueLabelVar(null, "");
                }
            }
            
            scene.vars[roleVarName] = roleVar;
        }
    }
});


/*global pv_Mark:true */

function sign_createBasic(pvMark) {
    var as = mark_getAncestorSign(pvMark) || def.assert("There must exist an ancestor sign");
    var bs = new pvc.visual.BasicSign(as.panel, pvMark);
    var s = pvMark.scene;
    var i, pvInstance;
    if(s && (i = pvMark.index) != null && i >= 0 && (pvInstance = s[i])) {
        // Mark is already rendering; set the current instance in context
        bs._inContext(/*scene*/pvInstance.data, pvInstance);
    }
    
    return bs;
}

// Obtains the first sign accessible from the argument mark.
function mark_getAncestorSign(pvMark) {
    var sign;
    do   { pvMark = pvMark.parent; } 
    while(pvMark && !(sign = pvMark.sign) && (!pvMark.proto || !(sign = pvMark.proto.sign)));
    return sign;
}

pv_Mark.prototype.getSign    = function() { return this.sign || sign_createBasic(this); };
pv_Mark.prototype.getScene   = function() { return this.getSign().scene;     };
pv_Mark.prototype.getContext = function() { return this.getSign().context(); };

// Used to wrap a mark, dynamically, 
// with minimal impact and functionality.
def
.type('pvc.visual.BasicSign')
.init(function(panel, pvMark) {
    this.chart  = panel.chart;
    this.panel  = panel;
    this.pvMark = pvMark;
    
    /*jshint expr:true*/
    !pvMark.sign || def.assert("Mark already has an attached Sign.");

    pvMark.sign = this;
    
    // Intercept the protovis mark's buildInstance
    // Avoid doing a function bind, cause buildInstance is a very hot path
    
    pvMark.__buildInstance = pvMark.buildInstance;
    pvMark.buildInstance   = this._dispatchBuildInstance;
})
.add({
    compatVersion: function() { return this.chart.compatVersion(); },

    // Defines a local property on the underlying protovis mark
    localProperty: function(name, type) {
        this.pvMark.localProperty(name, type);
        return this;
    },
    
    lock: function(pvName, value) {
        return this.lockMark(pvName, this._bindWhenFun(value, pvName));
    },
    
    optional: function(pvName, value, tag) {
        return this.optionalMark(pvName, this._bindWhenFun(value, pvName), tag);
    },
    
    // -------------
    
    lockMark: function(name, value) {
        this.pvMark.lock(name, value);
        return this;
    },
    
    optionalMark: function(name, value, tag) {
        this.pvMark[name](value, tag);
        return this;
    },
    
    // --------------
    
    delegate: function(dv, tag) { return this.pvMark.delegate(dv, tag); },
    
    delegateExtension: function(dv) { return this.pvMark.delegate(dv, pvc.extensionTag); },
    
    hasDelegate: function(tag) { return this.pvMark.hasDelegate(tag); },
    
    // Using it is a smell...
//    hasExtension: function(){
//        return this.pvMark.hasDelegate(pvc.extensionTag);
//    },
    
    // -------------
    
    _createPropInterceptor: function(pvName, fun) {
        var me = this;
        var isDataProp = pvName === 'data';
        
        return function() {
            // Was function inherited by a pv.Mark without a sign?
            var sign = this.sign;
            if(!sign || sign !== me) {
                return me._getPvSceneProp(pvName, /*defaultIndex*/this.index);
            }
            
            // Data prop is evaluated while this.index = -1, and with the parent mark's stack
            if(!isDataProp) {
                // Is sign _inContext or Is a stale context?
                var pvInstance = this.scene[this.index];
                if(!sign.scene || sign.scene !== pvInstance.data) {
                    // This situation happens when animating, because buildInstance is not called.
                    me._inContext(/*scene*/pvInstance.data, pvInstance);
                }
            }
            
            return fun.apply(me, arguments);
        };
    },
    
    _getPvSceneProp: function(prop, defaultIndex) {
        // Property method was inherited via pv proto(s)
        var pvMark   = this.pvMark;
        var pvScenes = pvMark.scene;
        if(pvScenes) {
            // Have a scenes object, but which index should be used?
            var index = pvMark.hasOwnProperty('index') ? 
                pvMark.index : 
                Math.min(defaultIndex, pvScenes.length - 1);
            
           if(index != null) { return pvScenes[index][prop]; }
        }
        
        throw def.error.operationInvalid("Cannot evaluate inherited property.");
    },
    
    // -------------
    
    _bindWhenFun: function(value, pvName) {
        if(def.fun.is(value)) {
            var me = this;
            
            // NOTE: opted by this form, instead of: value.bind(me);
            // because bind does not exist in some browsers
            // and the bind polyfill uses apply (which would then be much slower).
            return me._createPropInterceptor(pvName, function(scene) { 
                return value.call(me, scene);
            });
        }
        
        return value;
    },
    
    _lockDynamic: function(pvName, method) {
        /* def.methodCaller('' + method, this) */
        var me = this;
        return me.lockMark(
            pvName,
            me._createPropInterceptor(pvName, function(scene) {
                return me[method].call(me, scene);
            }));
    },
    
    /* SCENE MAINTENANCE */
    // this: the mark
    _dispatchBuildInstance: function(pvInstance) {
        function callBuildInstanceInContext() {
            this.__buildInstance(pvInstance); 
        }
        
        this.sign._inContext(
                /*scene*/pvInstance.data,
                pvInstance,
                /*f*/callBuildInstanceInContext, 
                /*x*/this);
    },
    
    _inContext: function(scene, pvInstance, f, x) {
        var pvMark = this.pvMark;
        if(!pvInstance) { pvInstance = pvMark.scene[pvMark.index]; }
        if(!scene     ) { scene = pvInstance.data || def.assert("A scene is required!"); }
        
        var index = scene.childIndex();
        
        var oldScene, oldIndex, oldState;
        var oldPvInstance = this.pvInstance;
        if(oldPvInstance) {
            oldScene = this.scene;
            oldIndex = this.index;
            oldState = this.state;
        }
        
        this.pvInstance = pvInstance;
        this.scene = scene;
        this.index = index < 0 ? 0 : index;

        /*
         * Update the scene's render id,
         * which possibly invalidates per-render
         * cached data.
         */
        /*global scene_renderId:true */
        scene_renderId.call(scene, pvMark.renderId());
        
        /* state per: sign & scene & render */
        this.state = {};
        if(f) {
            try {
                return f.call(x, pvInstance);
            } finally {
                this.state = oldState;
                this.pvInstance = oldPvInstance;
                this.scene = oldScene;
                this.index = oldIndex;
            }
        } // otherwise... old stuff gets stale... but there's no big problem
    },
    
    /* CONTEXT */
    context: function(createNew) {
        // This is a hot function
        var state;
        if(createNew || !(state = this.state)) { return this._createContext(); }
        
        return state.context || (state.context = this._createContext());
    },
    
    _createContext: function() { return new pvc.visual.Context(this.panel, this.pvMark); }
});

def.type('pvc.visual.Sign', pvc.visual.BasicSign)
.init(function(panel, pvMark, keyArgs){
    var me = this;
    
    me.base(panel, pvMark, keyArgs);
    
    me._ibits = panel._ibits;
    
    var extensionIds = def.get(keyArgs, 'extensionId');
    if(extensionIds != null){ // empty string is a valid extension id.
        me.extensionAbsIds = def.array.to(panel._makeExtensionAbsId(extensionIds));
    }
    
    me.isActiveSeriesAware = def.get(keyArgs, 'activeSeriesAware', true);
    if(me.isActiveSeriesAware) {
        // Should also check if the corresponding data has > 1 atom?
        var roles = panel.visualRoles;
        var seriesRole = roles && roles.series;
        if(!seriesRole || !seriesRole.isBound()) {
            me.isActiveSeriesAware = false;
        }
    }

    /* Extend the pv mark */
    pvMark.wrapper(def.get(keyArgs, 'wrapper') || me.createDefaultWrapper());
    
    if(!def.get(keyArgs, 'freeColor', true)) {
        me._bindProperty('fillStyle',   'fillColor',   'color')
          ._bindProperty('strokeStyle', 'strokeColor', 'color');
    }
})
.postInit(function(panel, pvMark, keyArgs){
    
    this._addInteractive(keyArgs);

    panel._addSign(this);
})
.add({
    // NOTE: called during init
    createDefaultWrapper: function() {
        // The default wrapper passes the context as JS-context
        // and scene as first argument
        var me = this;
        return function(f) {
            return function(scene) { return f.call(me.context(), scene); };
        };
    },
    
    // To be called on prototype
    property: function(name) {
        var upperName  = def.firstUpperCase(name);
        var baseName   = 'base'        + upperName;
        var defName    = 'default'     + upperName;
        var normalName = 'normal'      + upperName;
        var interName  = 'interactive' + upperName;
        
        var methods = {};
        
        // ex: color
        methods[name] = function(arg) {
            this._finished = false;
            
            this._arg = arg; // for use in calling default methods (see #_bindProperty)
            
                // ex: baseColor
            var value = this[baseName](arg);
                
            if(value == null) { return null; }  // undefined included
            
            if(this._finished) { return value; }
            
            if(this.showsInteraction() && this.anyInteraction()) {
                // ex: interactiveColor
                value = this[interName](value, arg);
            } else {
                // ex: normalColor
                value = this[normalName](value, arg);
            }
            
            // Possible memory leak in case of error
            // but it is not serious.
            // Performance is more important 
            // so no try/finally is added. 
            this._arg = null;
            
            return value;
        };
        
        // baseColor
        //   Override this method if user extension
        //   should not always be called.
        //   It is possible to call the default method directly, if needed.
        //   defName is installed as a user extension and 
        //   is called if the user hasn't extended...
        methods[baseName]   = function(/*arg*/) { return this.delegateExtension(); };
        
        // defaultColor
        methods[defName]    = function(/*arg*/) { return; };
        
        // normalColor
        methods[normalName] = function(value/*, arg*/) { return value; };
        
        // interactiveColor
        methods[interName]  = function(value/*, arg*/) { return value; };
        
        this.constructor.add(methods);
        
        return this;
    },
    
    anyInteraction: function() { return this.scene.anyInteraction(); },
    
    // Call this function with a final property value
    // to ensure that it will not be processed anymore
    finished: function(value) {
        this._finished = true;
        return value;
    },
    
    /* Extensibility */
    /**
     * Any protovis properties that have been specified 
     * before the call to this method
     * are either locked or are defaults.
     * 
     * This method applies user extensions to the protovis mark.
     * Default properties are replaced.
     * Locked properties are respected.
     * 
     * Any function properties that are specified 
     * after the call to this method
     * will have access to the user extension by 
     * calling {@link pv.Mark#delegate}.
     */
    applyExtensions: function() {
        if(!this._extended) {
            this._extended = true;
            
            var extensionAbsIds = this.extensionAbsIds;
            if(extensionAbsIds) {
                extensionAbsIds.forEach(function(extensionAbsId) {
                    this.panel.extendAbs(this.pvMark, extensionAbsId);
                }, this);
            }
        }
        
        return this;
    },
    
    // -------------
    
    intercept: function(pvName, fun) {
        var interceptor = this._createPropInterceptor(pvName, fun);
        
        return this._intercept(pvName, interceptor);
    }, 
    
    // -------------
    
    lockDimensions: function() {
        this.pvMark
            .lock('left')
            .lock('right')
            .lock('top')
            .lock('bottom')
            .lock('width')
            .lock('height');
        
        return this;
    },
    
    // -------------
    _extensionKeyArgs: {tag: pvc.extensionTag},
    
    _bindProperty: function(pvName, prop, realProp) {
        var me = this;
        
        if(!realProp) { realProp = prop; }
        
        var defaultPropName = "default" + def.firstUpperCase(realProp);
        if(def.fun.is(me[defaultPropName])) {
            // Intercept with default method first, before extensions,
            // so that extensions, when ?existent?, can delegate to the default.
            
            // Extensions will be applied next.
            
            // If there already exists an applied extension then
            // do not install the default (used by legend proto defaults,
            // that should act like user extensions, and not be shadowed by prop defaults).
            
            // Mark default as pvc.extensionTag, 
            // so that it is chosen when 
            // the user hasn't specified an extension point.

            if(!me.pvMark.hasDelegateValue(pvName, pvc.extensionTag)) {
                var defaultPropMethod = function() {
                    return me[defaultPropName](me._arg);
                };
                
                me.pvMark.intercept(pvName, defaultPropMethod, me._extensionKeyArgs);
            }
        }
        
        // Intercept with main property method
        // Do not pass arguments, cause property methods do not use them,
        // they use this.scene instead.
        // The "arg" argument can only be specified explicitly,
        // like in strokeColor -> color and fillColor -> color,
        // via "helper property methods" that ?fix? the argument.
        // In these cases, 
        // 'strokeColor' is the "prop", 
        // 'color' is the "realProp" and
        // 'strokeStyle' is the pvName.
        var mainPropMethod = this._createPropInterceptor(
                pvName, 
                function() { return me[prop](); });
        
        return me._intercept(pvName, mainPropMethod);
    },
    
    _intercept: function(name, fun){
        var mark = this.pvMark;
        
        // Apply all extensions, in order
        
        var extensionAbsIds = this.extensionAbsIds;
        if(extensionAbsIds){
            def
            .query(extensionAbsIds)
            .select(function(extensionAbsId){ 
                return this.panel._getExtensionAbs(extensionAbsId, name);
             }, this)
            .where(def.notUndef)
            .each(function(extValue){
                extValue = mark.wrap(extValue, name);
                
                // Gets set on the mark; We intercept it afterwards.
                // Mark with the pvc.extensionTag so that it is 
                // possible to filter extensions.
                mark.intercept(name, extValue, this._extensionKeyArgs);
            }, this);
        }
        
        // Intercept with specified function (may not be a property function)
        
        (mark._intercepted || (mark._intercepted = {}))[name] = true;
        
        mark.intercept(name, fun);
        
        return this;
    }
})
.prototype
.property('color')
.constructor
.add(pvc.visual.Interactive)
.add({
    extensionAbsIds: null,
    
    _addInteractive: function(ka) {
        var me  = this,
            get = def.get;
        
        if(me.interactive()) {
            var bits = me._ibits,
                I    = pvc.visual.Interactive;
            
            if(get(ka, 'noTooltip'    )) { bits &= ~I.ShowsTooltip;    }
            if(get(ka, 'noHover'      )) { bits &= ~I.Hoverable;       }
            if(get(ka, 'noClick'      )) { bits &= ~I.Clickable;       }
            if(get(ka, 'noDoubleClick')) { bits &= ~I.DoubleClickable; }
            if(get(ka, 'noSelect'     )) { bits &= ~I.SelectableAny;   } 
            else if(this.selectable()) {
                if(get(ka, 'noClickSelect' )) { bits &= ~I.SelectableByClick;      }
                if(get(ka, 'noRubberSelect')) { bits &= ~I.SelectableByRubberband; }
            }
            
            // By default interaction is SHOWN if the sign
            // is sensitive to interactive events.
            if(me.showsInteraction()) {
                if(get(ka, 'showsInteraction') === false) { bits &= ~I.ShowsInteraction; }
                
                if(me.showsActivity()) {
                    if(get(ka, 'showsActivity') === false) { bits &= ~I.ShowsActivity; }
                }
                
                if(me.showsSelection()) {
                    if(get(ka, 'showsSelection') === false) { bits &= ~I.ShowsSelection; }
                }
            }
            
            me._ibits = bits;
        }
        
        if(!me.handlesEvents()) { 
            me.pvMark.events('none'); 
        } else {
            if(me.showsTooltip()     ) { me._addPropTooltip(get(ka, 'tooltipArgs')); }
            if(me.hoverable()        ) { me._addPropHoverable();   }
            if(me.handlesClickEvent()) { me._addPropClick();       }
            if(me.doubleClickable()  ) { me._addPropDoubleClick(); }
        }
    },
    
    /* COLOR */
    fillColor:   function() { return this.color('fill'  ); },
    strokeColor: function() { return this.color('stroke'); },

    defaultColor: function(/*type*/) { return this.defaultColorSceneScale()(this.scene); },

    dimColor: function(color, type) {
        if(type === 'text'){
            return pvc.toGrayScale(
                color,
                /*alpha*/-0.75, // if negative, multiplies by color.alpha
                /*maxGrayLevel*/ null,  // null => no clipping
                /*minGrayLevel*/ null); // idem
        }

        // ANALYZER requirements, so until there's no way to configure it...
        return pvc.toGrayScale(
                color,
                /*alpha*/-0.3, // if negative, multiplies by color.alpha
                /*maxGrayLevel*/ null,  // null => no clipping
                /*minGrayLevel*/ null); // idem
    },
    
    defaultColorSceneScale: function() {
        return def.lazy(this, '_defaultColorSceneScale', this._initDefColorScale, this);
    },

    _initDefColorScale: function() {
        var colorAxis = this.panel.axes.color;
        return colorAxis ?
               colorAxis.sceneScale({sceneVarName: 'color'}) :
               def.fun.constant(pvc.defaultColor);
    },

    mayShowActive: function(noSeries) {
        if(!this.showsActivity() ){ return false; }
        
        var scene = this.scene;
        return scene.isActive || 
               (!noSeries && this.isActiveSeriesAware && scene.isActiveSeries()) ||
               scene.isActiveDatum();
    },

    mayShowNotAmongSelected: function() {
        return this.mayShowAnySelected() && !this.scene.isSelected();
    },

    mayShowSelected: function() {
        return this.showsSelection() && this.scene.isSelected();
    },
    
    mayShowAnySelected: function() {
        return this.showsSelection() && this.scene.anySelected();
    },
    
    /* TOOLTIP */
    _addPropTooltip: function(ka) {
        if(this.pvMark.hasTooltip) { return; }

        var tipOptions = def.create(
                            this.chart._tooltipOptions, 
                            def.get(ka, 'options'));
        
        tipOptions.isLazy = def.get(ka, 'isLazy', true);
        
        var tooltipFormatter = def.get(ka, 'buildTooltip') || 
                           this._getTooltipFormatter(tipOptions);
        if(!tooltipFormatter) { return; }
        
        tipOptions.isEnabled = this._isTooltipEnabled.bind(this);
        
        var tipsyEvent = def.get(ka, 'tipsyEvent');
        if(!tipsyEvent) {
//          switch(pvMark.type) {
//                case 'dot':
//                case 'line':
//                case 'area':
//                    this._requirePointEvent();
//                    tipsyEvent = 'point';
//                    tipOptions.usesPoint = true;
//                    break;
                
//                default:
                    tipsyEvent = 'mouseover';
//            }
        }

        this.pvMark
            .localProperty('tooltip'/*, Function | String*/)
            .tooltip(this._createTooltipProp(tooltipFormatter, tipOptions.isLazy))
            .title(def.fun.constant('')) // Prevent browser tooltip
            .ensureEvents()
            .event(tipsyEvent, pv.Behavior.tipsy(tipOptions))
            .hasTooltip = true;
    },
    
    _getTooltipFormatter: function(tipOptions) { return this.panel._getTooltipFormatter(tipOptions); },
    
    // Dynamic result version
    _isTooltipEnabled: function() { return this.panel._isTooltipEnabled(); },
    
    _createTooltipProp: function(tooltipFormatter, isLazy) {
        var me = this;
        
        var formatTooltip;
        if(!isLazy) {
            formatTooltip = function() {
                var context = me.context();
                return tooltipFormatter(context); 
            };
        } else {
            formatTooltip = function() {
                // Capture current context
                var context = me.context(/*createNew*/true);
                var tooltip;
                
                // Function that formats the tooltip only on first use
                return function() {
                    if(context) {
                        tooltip = tooltipFormatter(context);
                        context = null; // release context;
                    } 
                    
                    return tooltip;
                };
            };
        }
        
        return function() {
            var scene = me.scene;
            if(scene && !scene.isIntermediate && scene.showsTooltip()) {
                return formatTooltip();
            }
        };
    },
    
    /* HOVERABLE */
    _addPropHoverable: function() {
        var panel  = this.panel;
        
        var onEvent;
        var offEvent;
//        switch(pvMark.type) {
//            default:
//            case 'dot':
//            case 'line':
//            case 'area':
//            case 'rule':
//                onEvent  = 'point';
//                offEvent = 'unpoint';
//               panel._requirePointEvent();
//                break;

//            default:
                onEvent = 'mouseover';
                offEvent = 'mouseout';
//                break;
//        }
        
        this.pvMark
            .ensureEvents()
            .event(onEvent, function(scene) {
                if(scene.hoverable() && !panel.selectingByRubberband() && !panel.animating()) {
                    scene.setActive(true);
                    panel.renderInteractive();
                }
            })
            .event(offEvent, function(scene) {
                if(scene.hoverable() && !panel.selectingByRubberband() && !panel.animating()) {
                     // Clears THE active scene, if ANY (not necessarily = scene)
                    if(scene.clearActive()) { panel.renderInteractive(); }
                }
            });
    },
    
    /* CLICK & DOUBLE-CLICK */
    /**
     * Shared state between {@link _handleClick} and {@link #_handleDoubleClick}.
     */
    _ignoreClicks: 0,
    
    _propCursorClick: function(s) {
        var ibits = (this._ibits & s._ibits);
        var I = pvc.visual.Interactive;
        return (ibits & I.HandlesClickEvent) || (ibits & I.DoubleClickable) ? 
               'pointer' : 
               null;
    },
    
    _addPropClick: function() {
        var me = this;
        me.pvMark
            .cursor(me._propCursorClick.bind(me))
            .ensureEvents()
            .event('click', me._handleClick.bind(me));
    },
    
    _addPropDoubleClick: function() {
        var me = this;
        me.pvMark
            .cursor(me._propCursorClick.bind(me))
            .ensureEvents()
            .event('dblclick', me._handleDoubleClick.bind(me));
    },
    
    _handleClick: function() {
        /*global window:true*/
        
        // Not yet in context...
        var me = this;
        var pvMark = me.pvMark;
        var pvInstance = pvMark.instance();
        var scene = pvInstance.data;
        
        var wait  = me.doubleClickable() && scene.doubleClickable();
        if(!wait) {
            if(me._ignoreClicks) { me._ignoreClicks--;    }
            else                 { me._handleClickCore(); }
        } else {
            var pvScene = pvMark.scene;
            var pvIndex = pvMark.index;
            var pvEvent = pv.event;
            
            // Delay click evaluation so that
            // it may be canceled if a double-click meanwhile fires.
            // When timeout finished, reestablish protovis context.
            window.setTimeout(function() {
                if(me._ignoreClicks) { me._ignoreClicks--; }
                else {
                    try {
                        pv.event = pvEvent;
                        pvMark.context(pvScene, pvIndex, function() { me._handleClickCore(); });
                    } catch (ex) {
                        pv.error(ex); 
                    } finally {
                        delete pv.event; 
                    }
                }
             },
             me.chart.options.doubleClickMaxDelay || 300);
        }
    },
    
    _handleClickCore: function() {
        var me = this;
        var pvInstance = me.pvMark.instance();
        
        // Setup the sign context
        me._inContext(
            /*scene*/pvInstance.data,
            pvInstance,
            /*f*/function() { me._onClick(me.context()); }, 
            /*x*/me);
    },

    _handleDoubleClick: function() {
        // The following must be tested before delegating to context
        // because we might not need to ignore the clicks.
        // Assumes that: this.doubleClickable()
        var me = this;
        var pvInstance = me.pvMark.instance();
        var scene = pvInstance.data;
        if(scene.doubleClickable()) {
            // TODO: explain why 2 ignores
            me._ignoreClicks = 2;
            
         // Setup the sign context
            me._inContext(
                /*scene*/pvInstance.data,
                pvInstance,
                /*f*/function() { me._onDoubleClick(me.context()); }, 
                /*x*/me);
        }
    },
    
    _onClick:       function(context) { context.click();       },
    _onDoubleClick: function(context) { context.doubleClick(); }
});


def.type('pvc.visual.Panel', pvc.visual.Sign)
.init(function(panel, protoMark, keyArgs){
    var pvPanel = def.get(keyArgs, 'panel');
    if(!pvPanel){
        var pvPanelType = def.get(keyArgs, 'panelType') || pv.Panel;
        
        pvPanel = protoMark.add(pvPanelType);
    }
    
    this.base(panel, pvPanel, keyArgs);
})
.add({
    _addInteractive: function(keyArgs){
        var t = true;
        keyArgs = def.setDefaults(keyArgs,
                        'noSelect',      t,
                        'noHover',       t,
                        'noTooltip',     t,
                        'noClick',       t,
                        'noDoubleClick', t);

        this.base(keyArgs);
    }
});

def.type('pvc.visual.Label', pvc.visual.Sign)
.init(function(panel, protoMark, keyArgs) {

    var pvMark = protoMark.add(pv.Label);

    this.base(panel, pvMark, keyArgs);
})
.add({
    _addInteractive: function(keyArgs) {
        var t = true;
        keyArgs = def.setDefaults(keyArgs,
                        'noSelect',         t,
                        'noHover',          t,
                        'noTooltip',        t,
                        'noClick',          t,
                        'noDoubleClick',    t,
                        'showsInteraction', false);

        this.base(keyArgs);
    },
    
    defaultColor: def.fun.constant(pv.Color.names.black)
});

def
.type('pvc.visual.ValueLabel', pvc.visual.Label)
.init(function(panel, anchorMark, keyArgs) {
    
    var protoMark;
    if(!def.get(keyArgs, 'noAnchor', false)) {
        protoMark = anchorMark.anchor(panel.valuesAnchor);
    } else {
        protoMark = anchorMark;
    }
    
    if(keyArgs && keyArgs.extensionId == null) { keyArgs.extensionId = 'label'; }

    this.base(panel, protoMark, keyArgs);

    this._bindProperty('text', 'text');
    
    this.pvMark.font(panel.valuesFont);

    this._bindProperty('textStyle', 'textColor', 'color');
})
.prototype
.property('text')
.property('textStyle')
.constructor
.addStatic({
    maybeCreate: function(panel, anchorMark, keyArgs) {
        return panel.valuesVisible && panel.valuesMask ?
               new pvc.visual.ValueLabel(panel, anchorMark, keyArgs) :
               null;
    },

    isNeeded: function(panel) { return panel.valuesVisible && panel.valuesMask; }
})
.add({
    _addInteractive: function(keyArgs) {
        // TODO: Until the problem of tooltips being stolen
        // from the target element, its better to not process events.
        keyArgs = def.setDefaults(keyArgs,
            'showsInteraction', true,
            'noSelect',      true,  //false,
            'noTooltip',     true,  //false,
            'noClick',       true,  //false,
            'noDoubleClick', true,  //false,
            'noHover',       true); //false
        
        this.base(keyArgs);
    },
    
    defaultText: function() { return this.scene.format(this.panel.valuesMask); },
    
    normalText: function(text) { return this.trimText(text); },
    
    interactiveText: function(text) {
        return this.showsActivity() && this.scene.isActive ? text : this.trimText(text); 
    },
    
    trimText: function(text) { return text; },
    
    textColor: function() { return this.color('text'); },
    
    backgroundColor: function(type) {
        var state = this.state;
        if(!state) { return this.calcBackgroundColor(type); }
        var cache = def.lazy(state, 'bgColorCache');
        var color = def.getOwn(cache, type);
        if(!color) { color = cache[type] = this.calcBackgroundColor(type); }
        return color;
    },
    
    calcBackgroundColor: def.fun.constant(pv.Color.names.white), // TODO: ??
    
    optimizeLegibilityColor: function(color, type) {
        if(this.panel.valuesOptimizeLegibility) {
            var bgColor = this.backgroundColor();
            return bgColor.isDark() ? color.complementary().alpha(0.9) : color;
        }
        
        return color;
    },
    
    normalColor: function(color, type) { return this.optimizeLegibilityColor(color, type); },
    
    interactiveColor: function(color, type) {
        if(!this.mayShowActive() && this.mayShowNotAmongSelected()) {
            return this.dimColor(color, type);
        }
        
        return this.optimizeLegibilityColor(color, type);
    }
});

def.type('pvc.visual.Dot', pvc.visual.Sign)
.init(function(panel, parentMark, keyArgs){
    
    var pvMark = parentMark.add(pv.Dot);
    
    var protoMark = def.get(keyArgs, 'proto');
    if(protoMark){
        pvMark.extend(protoMark);
    }
    
    keyArgs = def.setDefaults(keyArgs, 'freeColor', false);
    
    this.base(panel, pvMark, keyArgs);
    
    if(!def.get(keyArgs, 'freePosition', false)){
        var a_left   = panel.isOrientationVertical() ? 'left' : 'bottom';
        var a_bottom = panel.anchorOrtho(a_left);

        /* Positions */
        this._lockDynamic(a_left,   'x')
            ._lockDynamic(a_bottom, 'y');
    }

    /* Shape & Size */
    this._bindProperty('shape',       'shape' )
        ._bindProperty('shapeRadius', 'radius')
        ._bindProperty('shapeSize',   'size'  );
        
    /* Colors & Line */
    this.optional('strokeDasharray', undefined) // Break inheritance
        .optional('lineWidth',       1.5);      // Idem
})
.prototype
.property('size')
.property('shape')
.constructor
.add({
    /* Sign Spatial Coordinate System
     *  -> Cartesian coordinates
     *  -> Grows Up, vertically, and Right, horizontally
     *  -> Independent of the chart's orientation
     *  -> X - horizontal axis
     *  -> Y - vertical axis
     *  
     *  y
     *  ^
     *  |
     *  |
     *  o-----> x
     */
    y: def.fun.constant(0),
    x: def.fun.constant(0),
    
    radius: function(){
        // Store extended value, if any
        // See #baseSize
        this.state.radius = this.delegateExtension();
    },
    
    /* SIZE */
    baseSize: function(){
        /* Radius has precedence */
        var radius = this.state.radius;
        return radius != null ? def.sqr(radius) : this.base();
    },

    defaultSize: function(){
        return 12;
    },
    
    interactiveSize: function(size){
        return this.mayShowActive(/*noSeries*/true) ?
               (Math.max(size, 5) * 2.5) : 
               size;
    },
    
    /* COLOR */
    
    /**
     * @override
     */
    interactiveColor: function(color, type){
        if(this.mayShowActive(/*noSeries*/true)) {
            if(type === 'stroke') {
                return color.brighter(1);
            }
        } else if(this.mayShowNotAmongSelected()) {
            if(this.mayShowActive()) {
                return color.alpha(0.8);
            }
            
            switch(type) {
                case 'fill':   return this.dimColor(color, type);
                case 'stroke': return color.alpha(0.45);
            }
        }

        return this.base(color, type);
    }
});


/*global pvc_colorIsGray:true */
def
.type('pvc.visual.DotSizeColor', pvc.visual.Dot)
.init(function(panel, parentMark, keyArgs) {

    this.base(panel, parentMark, keyArgs);

    var isV1Compat = this.compatVersion() <= 1;
    
    this
    ._bindProperty('lineWidth', 'strokeWidth')
    .intercept('visible', function() {
        if(!this.canShow()) { return false; }
        
        var visible = this.delegateExtension();
        if(visible == null) { visible = isV1Compat || this.defaultVisible(); }
        return visible;
    });
    
    this._initColor();
    this._initSize();
    
    if(this.isSizeBound) {
        var sizeAxis = panel.axes.size;
        if(sizeAxis.scaleUsesAbs()) {
            this.isSizeAbs = true;
            
            // Override current default scene color
            var baseSceneDefColor = this._sceneDefColor;
            this._sceneDefColor = function(scene, type) {
                return type === 'stroke' && scene.vars.size.value < 0 ?
                       pv.Color.names.black :
                       baseSceneDefColor.call(this, scene, type);
            };
            
            this.pvMark
                .lineCap('round') // only used by strokeDashArray
                .strokeDasharray(function(scene) {
                    return scene.vars.size.value < 0 ? 'dash' : null; // '-'
                });
        }
    }
})
.prototype
.property('strokeWidth')
.constructor
.add({
    isColorBound: false,
    isColorDiscrete: false,
    isSizeBound:  false,
    isSizeAbs:    false,

    canShow: function() { return !this.scene.isIntermediate; },

    defaultVisible: function(){
        var scene = this.scene;
        return !scene.isNull && 
               ((!this.isSizeBound && !this.isColorBound) ||
                (this.isSizeBound  && scene.vars.size.value  != null) ||
                (this.isColorBound && (this.isColorDiscrete || scene.vars.color.value != null)));
    },

    _initColor: function(){
        // TODO: can't most of this be incorporated in the sizeAxis code
        // or in Sign#_initDefColorScale ??
        var colorConstant;
        var sceneColorScale;
        var panel = this.panel;
        var colorRole = panel.visualRoles.color;
        if(colorRole) {
            this.isColorDiscrete = colorRole.isDiscrete();
            
            var colorAxis = panel.axes.color;
            
            // Has at least one value? (possibly null, in discrete scales)
            if(colorRole.isBound()) { // => colorAxis
                this.isColorBound = true;
                sceneColorScale = colorAxis.sceneScale({sceneVarName: 'color'});
            } else if(colorAxis) {
                colorConstant = colorAxis.option('Unbound');
            }
        }
        
        if(!sceneColorScale) {
            sceneColorScale = def.fun.constant(colorConstant || pvc.defaultColor);
        }

        this._sceneDefColor = sceneColorScale;
    },

    _initSize: function() {
        var panel = this.panel;
        var plot  = panel.plot;
        var shape = plot.option('Shape');
        var nullSizeShape = plot.option('NullShape');
        var sizeRole = panel.visualRoles.size;
        var sceneSizeScale, sceneShapeScale;
        if(sizeRole) {
            var sizeAxis  = panel.axes.size;
            var sizeScale = sizeAxis && sizeAxis.scale;
            var isSizeBound = !!sizeScale && sizeRole.isBound();
            if(isSizeBound) {
                this.isSizeBound = true;
                
                var missingSize = sizeScale.min + (sizeScale.max - sizeScale.min) * 0.05; // 10% size
                this.nullSizeShapeHasStrokeOnly = (nullSizeShape === 'cross');
                
                sceneShapeScale = function(scene) {
                    return scene.vars.size.value != null ? shape : nullSizeShape;
                };
                
                sceneSizeScale = function(scene) {
                    var sizeValue = scene.vars.size.value;
                    return sizeValue != null ? sizeScale(sizeValue) :
                           nullSizeShape     ? missingSize :
                           0;
                };
            }
        }

        if(!sceneSizeScale) {
            // => !isSizeBound
            sceneShapeScale = def.fun.constant(shape);
            sceneSizeScale  = function(scene){ return this.base(scene); };
        }
        
        this._sceneDefSize  = sceneSizeScale;
        this._sceneDefShape = sceneShapeScale;
    },

    // Taken from MetricPoint.pvDot.defaultColor:
    //  When no lines are shown, dots are shown with transparency,
    //  which helps in distinguishing overlapped dots.
    //  With lines shown, it would look strange.
    //  ANALYZER requirements, so until there's no way to configure it...
    //  TODO: this probably can now be done with ColorTransform
    //  if(!me.linesVisible) {
    //     color = color.alpha(color.opacity * 0.85);
    //  }
    defaultColor: function(type) { return this._sceneDefColor(this.scene, type); },

    normalColor: function(color, type) {
        // When normal, the stroke shows a darker color
        return type === 'stroke' ? color.darker() : this.base(color, type);
    },

    interactiveColor: function(color, type) {
        var scene = this.scene;

        if(this.mayShowActive(/*noSeries*/true)){
            switch(type) {
                case 'fill':   return this.isSizeBound ? color.alpha(0.75) : color;
                
                // When active, the stroke shows a darker color, as well
                case 'stroke': return color.darker();
            }
        } else if(this.showsSelection()){
            var isSelected = scene.isSelected();
            var notAmongSelected = !isSelected && scene.anySelected();
            if(notAmongSelected){
                if(this.mayShowActive()) { return color.alpha(0.8); }

                switch(type) {
                    // Metric sets an alpha while HG does not
                    case 'fill':   return this.dimColor(color, type);
                    case 'stroke': return color.alpha(0.45);
                }
            }

            if(isSelected && pvc_colorIsGray(color)) {
                if(type === 'stroke') { color = color.darker(3); }

                return color.darker(2);
            }
        }

        // When some active that's not me, the stroke shows a darker color, as well
        if(type === 'stroke') { return color.darker(); }
        
        // show base color
        return color;
    },

    defaultSize: function() { return this._sceneDefSize(this.scene); },

    defaultShape: function() { return this._sceneDefShape(this.scene); },

    interactiveSize: function(size) {
        if(!this.mayShowActive(/*noSeries*/true)) { return size; }

        // At least 1 px, no more than 10% of the radius, and no more that 3px.
        var radius    = Math.sqrt(size);
        var radiusInc = Math.max(1, Math.min(1.1 * radius, 2));
        return def.sqr(radius + radiusInc);
    },

    defaultStrokeWidth: function() {
        return (this.nullSizeShapeHasStrokeOnly && this.scene.vars.size.value == null) ? 1.8 : 1;
    },

    interactiveStrokeWidth: function(width) {
        return this.mayShowActive(/*noSeries*/true) ? (2 * width) :
               this.mayShowSelected() ? (1.5 * width) :
               width;
    }
});


def.type('pvc.visual.Line', pvc.visual.Sign)
.init(function(panel, protoMark, keyArgs) {
    
    var pvMark = protoMark.add(pv.Line);
    
    this.base(panel, pvMark, keyArgs);
    
    this.lock('segmented', 'smart') // fixed
        .lock('antialias', true);

    if(!def.get(keyArgs, 'freePosition', false)) {
        var basePosProp  = panel.isOrientationVertical() ? "left" : "bottom",
            orthoPosProp = panel.anchorOrtho(basePosProp);

        this/* Positions */
            ._lockDynamic(orthoPosProp, 'y')
            ._lockDynamic(basePosProp,  'x');
    }

    this/* Colors & Line */
        ._bindProperty('strokeStyle', 'strokeColor', 'color')
        ._bindProperty('lineWidth',   'strokeWidth');

    // Segmented lines use fill color instead of stroke...so this doesn't work.
    //this.pvMark.lineCap('square');
})
.prototype
.property('strokeWidth')
.constructor
.add({
    _addInteractive: function(keyArgs) {
        keyArgs = def.setDefaults(keyArgs, 'noTooltip',  true);
        
        this.base(keyArgs);
    },

    /* Sign Spatial Coordinate System
     *  -> Cartesian coordinates
     *  -> Grows Up, vertically, and Right, horizontally
     *  -> Independent of the chart's orientation
     *  -> X - horizontal axis
     *  -> Y - vertical axis
     *
     *  y
     *  ^
     *  |
     *  |
     *  o-----> x
     */
    y: def.fun.constant(0),
    x: def.fun.constant(0),

    /* STROKE WIDTH */
    defaultStrokeWidth: def.fun.constant(1.5),

    interactiveStrokeWidth: function(strokeWidth) {
        return this.mayShowActive() ? 
               Math.max(1, strokeWidth) * 2.5 :
               strokeWidth;
    },
    
    /* STROKE COLOR */
    /**
     * @override
     */
    interactiveColor: function(color, type) {
        if(this.mayShowNotAmongSelected()) {
            return this.mayShowActive() ? 
                   pv.Color.names.darkgray.darker().darker() : 
                   this.dimColor(color, type);
        }

        return this.base(color, type);
    }
});


def.type('pvc.visual.Area', pvc.visual.Sign)
.init(function(panel, protoMark, keyArgs){
    
    var pvMark = protoMark.add(pv.Area);
    
    if(!keyArgs) { keyArgs = {}; }
    
    keyArgs.freeColor = true;
    
    this.base(panel, pvMark, keyArgs);
    
    var antialias = def.get(keyArgs, 'antialias', true);
    
    this
        .lock('segmented', 'smart') // fixed, not inherited
        .lock('antialias', antialias)
        ;

    if(!def.get(keyArgs, 'freePosition', false)){
        var basePosProp  = panel.isOrientationVertical() ? "left" : "bottom",
            orthoPosProp = panel.anchorOrtho(basePosProp),
            orthoLenProp = panel.anchorOrthoLength(orthoPosProp);
        
        /* Positions */
        this
            ._lockDynamic(basePosProp,  'x')  // ex: left
            ._lockDynamic(orthoPosProp, 'y')  // ex: bottom
            ._lockDynamic(orthoLenProp, 'dy') // ex: height
            ;
    }
    
    /* Colors */
    this._bindProperty('fillStyle', 'fillColor', 'color');
    
    // These really have no real meaning in the area and should not be used.
    // If lines are desired, they should be created with linesVisible of LineChart
    this.lock('strokeStyle', null)
        .lock('lineWidth',   0)
        ;
})
.add({
    _addInteractive: function(keyArgs){
        keyArgs = def.setDefaults(keyArgs, 
                        'noTooltip',  true);

        this.base(keyArgs);
    },

    /* Sign Spatial Coordinate System
     *  -> Cartesian coordinates
     *  -> Grows Up, vertically, and Right, horizontally
     *  -> Independent of the chart's orientation
     *  -> X - horizontal axis
     *  -> Y - vertical axis
     *  
     *  y       ^
     *  ^    dY |
     *  |       - y
     *  |
     *  o-----> x
     */
    y:  def.fun.constant(0),
    x:  def.fun.constant(0),
    dy: def.fun.constant(0),
    
    /* COLOR */
    /**
     * @override
     */
    interactiveColor: function(color, type){
        if(type === 'fill' && this.mayShowNotAmongSelected()) {
            return this.dimColor(color, type);
        }
        
        return this.base(color, type);
    }
});


def.type('pvc.visual.Bar', pvc.visual.Sign)
.init(function(panel, protoMark, keyArgs){

    var pvMark = protoMark.add(pv.Bar);
    
    keyArgs = def.setDefaults(keyArgs, 'freeColor', false);
    
    this.base(panel, pvMark, keyArgs);

    this.normalStroke = def.get(keyArgs, 'normalStroke', false);

    this._bindProperty('lineWidth',  'strokeWidth');
})
.prototype
.property('strokeWidth')
.constructor
.add({
    /* COLOR */
    /**
     * @override
     */
    normalColor: function(color, type){
        if(type === 'stroke' && !this.normalStroke){ return null; }

        return color;
    },

    /**
     * @override
     */
    interactiveColor: function(color, type) {
        var scene = this.scene;
        
        if(type === 'stroke') {
            if(this.mayShowActive(/*noSeries*/true)) { return color.brighter(1.3).alpha(0.7); }
            
            if(!this.normalStroke) { return null; }
            
            if(this.mayShowNotAmongSelected()) {
                if(this.mayShowActive()) { return pv.Color.names.darkgray.darker().darker(); }
                
                return this.dimColor(color, type);
            }
            
            if(this.mayShowActive()) { return color.brighter(1).alpha(0.7); }

        } else if(type === 'fill') {
            if(this.mayShowActive(/*noSeries*/true)) { return color.brighter(0.2).alpha(0.8); } 

            if(this.mayShowNotAmongSelected()) {
                if(this.mayShowActive()) { return pv.Color.names.darkgray.darker(2).alpha(0.8); }
                
                return this.dimColor(color, type);
            }
            
            if(this.mayShowActive()) { return color.brighter(0.2).alpha(0.8); }
        }

        return this.base(color, type);
    },

    /* STROKE WIDTH */    
    defaultStrokeWidth: function() { return 0.5; },

    interactiveStrokeWidth: function(strokeWidth) {
        if(this.mayShowActive(/*noSeries*/true)) { return Math.max(1, strokeWidth) * 1.3; }

        return strokeWidth;
    }
});


// Custom protovis mark inherited from pv.Wedge
pv.PieSlice = function() {
    pv.Wedge.call(this);
};

pv.PieSlice.prototype = pv.extend(pv.Wedge)
    // How much radius to offset the slice from the Pie center.
    // Must be a value between 0 and the Pie's:
    // ActiveSliceRadius + ExplodedSliceRadius
    .property('offsetRadius'/*, NumberOrString*/);

// There's already a Wedge#midAngle method
// but it doesn't work well when end-angle isn't explicitly set,
// so we override the method.
pv.PieSlice.prototype.midAngle = function() {
    var instance = this.instance();
    return instance.startAngle + (instance.angle / 2);
};

pv.PieSlice.prototype.defaults = new pv.PieSlice()
    .extend(pv.Wedge.prototype.defaults)
    .offsetRadius(0);

// -----------

def.type('pvc.visual.PieSlice', pvc.visual.Sign)
.init(function(panel, protoMark, keyArgs){

    var pvMark = protoMark.add(pv.PieSlice);

    keyArgs = def.setDefaults(keyArgs, 'freeColor', false);

    this.base(panel, pvMark, keyArgs);

    this._activeOffsetRadius = def.get(keyArgs, 'activeOffsetRadius', 0);
    this._maxOffsetRadius = def.get(keyArgs, 'maxOffsetRadius', 0);
    this._resolvePctRadius = def.get(keyArgs, 'resolvePctRadius');
    this._center = def.get(keyArgs, 'center');

    this/* Colors */
        .optional('lineWidth',  0.6)
        // Ensures that it is evaluated before x and y
        ._bindProperty('angle', 'angle')
        ._bindProperty('offsetRadius', 'offsetRadius')
        ._lockDynamic('bottom', 'y')
        ._lockDynamic('left',   'x')
        .lock('top',   null)
        .lock('right', null);
})
.prototype
.property('offsetRadius')
.constructor
.add({
    angle: def.fun.constant(0),

    x: function() { return this._center.x + this._offsetSlice('cos'); },
    y: function() { return this._center.y - this._offsetSlice('sin'); },

    _offsetSlice: function(fun) {
        var offset = this.pvMark.offsetRadius() || 0;
        if(offset) { offset *= Math[fun](this.pvMark.midAngle()); }
        return offset;
    },

    /* COLOR */

    // @override
    defaultColor: function(type) { return type === 'stroke' ? null : this.base(type); },

    // @override
    interactiveColor: function(color, type) {
        if(this.mayShowActive(/*noSeries*/true)) {
            switch(type) {
                // Like the bar chart
                case 'fill':   return color.brighter(0.2).alpha(0.8);
                case 'stroke': return color.brighter(1.3).alpha(0.7);
            }
        } else if(this.mayShowNotAmongSelected()) {
            //case 'stroke': // ANALYZER requirements, so until there's no way to configure it...
            if(type === 'fill') { return this.dimColor(color, type); }
        }

        return this.base(color, type);
    },

    /* OffsetRadius */
    offsetRadius: function() {
        var offsetRadius = this.base();
        return Math.min(Math.max(0, offsetRadius), this._maxOffsetRadius);
    },

    baseOffsetRadius: function() {
        var offsetRadius = this.base() || 0;
        return this._resolvePctRadius(pvc_PercentValue.parse(offsetRadius));
    },

    interactiveOffsetRadius: function(offsetRadius) {
        return offsetRadius +
            (this.mayShowActive(/*noSeries*/true) ? this._activeOffsetRadius : 0);
    }
});


def.type('pvc.visual.Rule', pvc.visual.Sign)
.init(function(panel, parentMark, keyArgs){

    var pvMark = parentMark.add(pv.Rule);
    
    var protoMark = def.get(keyArgs, 'proto');
    if(protoMark){
        pvMark.extend(protoMark);
    }
    
    this.base(panel, pvMark, keyArgs);
    
    if(!def.get(keyArgs, 'freeStyle')){
        this/* Colors & Line */
            ._bindProperty('strokeStyle', 'strokeColor', 'color')
            ._bindProperty('lineWidth',   'strokeWidth')
            ;
    }
})
.prototype
.property('strokeWidth')
.constructor
.add({
    _addInteractive: function(keyArgs){
        var t = true;
        keyArgs = def.setDefaults(keyArgs,
                        'noHover',       t,
                        'noSelect',      t,
                        'noTooltip',     t,
                        'noClick',       t,
                        'noDoubleClick', t,
                        'showsInteraction', false);

        this.base(keyArgs);
    },

    /* STROKE WIDTH */
    defaultStrokeWidth: function(){
        return 1;
    },

    interactiveStrokeWidth: function(strokeWidth){
        if(this.mayShowActive(/*noSeries*/true)){
            return Math.max(1, strokeWidth) * 2.2;
        }

        return strokeWidth;
    },

    /* STROKE COLOR */
    interactiveColor: function(color, type){
        var scene = this.scene;
        
        if(scene.datum && 
           !this.mayShowActive(/*noSeries*/true) &&
           this.mayShowNotAmongSelected()) {
            return this.dimColor(color, type);
        }
        
        return this.base(color, type);
    }
});


/**
 * Initializes a chart object with options.
 * 
 * @name pvc.visual.OptionsBase
 * 
 * @class Represents a chart object that has options.
 * 
 * @property {pvc.BaseChart} chart The associated chart.
 * @property {string} type The type of object.
 * @property {number} index The index of the object within its type (0, 1, 2...).
 * @property {string} [name] The name of the object.
 * 
 * @constructor
 * @param {pvc.BaseChart} chart The associated chart.
 * @param {string} type The type of the object.
 * @param {number} [index=0] The index of the object within its type.
 * @param {object} [keyArgs] Keyword arguments.
 * @param {string} [keyArgs.name] The name of the object.
 */
def
.type('pvc.visual.OptionsBase')
.init(function(chart, type, index, keyArgs){
    this.chart = chart;
    this.type  = type;
    this.index = index == null ? 0 : index;
    this.name  = def.get(keyArgs, 'name');
    this.id    = this._buildId();
    this.optionId = this._buildOptionId();
    
    var rs = this._resolvers = [];
    
    this._registerResolversFull(rs, keyArgs);
    
    this.option = pvc.options(this._getOptionsDefinition(), this);
})
.add(/** @lends pvc.visual.OptionsBase# */{
    
    _buildId: function(){
        return pvc.buildIndexedId(this.type, this.index);
    },
    
    _buildOptionId: function(){
        return this.id;
    },
        
    _getOptionsDefinition: def.method({isAbstract: true}),
    
    _chartOption: function(name) {
        return this.chart.options[name];
    },
    
    _registerResolversFull: function(rs, keyArgs){
        // I - By Fixed values
        var fixed = def.get(keyArgs, 'fixed');
        if(fixed){
            this._fixed = fixed;
            rs.push(
                pvc.options.specify(function(optionInfo){
                    return fixed[optionInfo.name];
                }));
        }
        
        this._registerResolversNormal(rs, keyArgs);
        
        // VI - By Default Values
        var defaults = def.get(keyArgs, 'defaults');
        if(defaults){
            this._defaults = defaults;
        }
        
        rs.push(this._resolveDefault);
    },
    
    _registerResolversNormal: function(rs, keyArgs){
        // II - By V1 Only Logic
        if(this.chart.compatVersion() <= 1){
            rs.push(this._resolveByV1OnlyLogic);
        }
        
        // III - By Name (ex: plot2, trend)
        if(this.name){
            rs.push(
                pvc.options.specify(function(optionInfo){
                      return this._chartOption(this.name + def.firstUpperCase(optionInfo.name));
                }));
        }
        
        // IV - By OptionId
        rs.push(this._resolveByOptionId);
        
        // V - By Naked Id
        if(def.get(keyArgs, 'byNaked', !this.index)){
            rs.push(this._resolveByNaked);
        }
    },
    
    // -------------
    
    _resolveFull: function(optionInfo){
        var rs = this._resolvers;
        for(var i = 0, L = rs.length ; i < L ; i++){
            if(rs[i].call(this, optionInfo)){
                return true;
            }
        }
        return false;
    },
    
    _resolveFixed: pvc.options.specify(function(optionInfo){
        if(this._fixed){
            return this._fixed[optionInfo.name];
        }
    }),
    
    _resolveByV1OnlyLogic: function(optionInfo){
        var data = optionInfo.data;
        var resolverV1;
        if(data && (resolverV1 = data.resolveV1)){
            return resolverV1.call(this, optionInfo);
        }
    },
    
    _resolveByName: pvc.options.specify(function(optionInfo){
        if(this.name){ 
            return this._chartOption(this.name + def.firstUpperCase(optionInfo.name));
        }
    }),
    
    _resolveByOptionId: pvc.options.specify(function(optionInfo){
        return this._chartOption(this.optionId + def.firstUpperCase(optionInfo.name));
    }),
    
    _resolveByNaked: pvc.options.specify(function(optionInfo){
        // The first of the type receives options without any prefix.
        if(!this.index){
            return this._chartOption(def.firstLowerCase(optionInfo.name));
        }
    }),
    
    _resolveDefault: function(optionInfo){
        // Dynamic default value?
        var data = optionInfo.data;
        var resolverDefault;
        if(data && (resolverDefault = data.resolveDefault)){
            if(resolverDefault.call(this, optionInfo)){
                return true;
            }
        }
        
        if(this._defaults){
            var value = this._defaults[optionInfo.name];
            if(value !== undefined){
                optionInfo.defaultValue(value);
                return true;
            }
        }
    },
    
    _specifyChartOption: function(optionInfo, asName){
        var value = this._chartOption(asName);
        if(value != null){
            optionInfo.specify(value);
            return true;
        }
    }
});



/**
 * Initializes an axis.
 * 
 * @name pvc.visual.Axis
 * 
 * @class Represents an axis for a role in a chart.
 * 
 * @extends pvc.visual.OptionsBase
 * 
 * @property {pvc.visual.Role} role The associated visual role.
 * @property {pv.Scale} scale The associated scale.
 * 
 * @constructor
 * @param {pvc.BaseChart} chart The associated chart.
 * @param {string} type The type of the axis.
 * @param {number} [index=0] The index of the axis within its type.
 * @param {object} [keyArgs] Keyword arguments.
 */
var pvc_Axis =
def
.type('pvc.visual.Axis', pvc.visual.OptionsBase)
.init(function(chart, type, index, keyArgs){
    
    this.base(chart, type, index, keyArgs);
    
    // Fills #axisIndex and #typeIndex
    chart._addAxis(this);
})
.add(/** @lends pvc.visual.Axis# */{
    isVisible: true,
    
    // should null values be converted to zero or to the minimum value in what scale is concerned?
    // 'null', 'zero', 'min', 'value'
    scaleTreatsNullAs: function() { return 'null'; },
    
    scaleNullRangeValue: function() { return null; },
    
    scaleUsesAbs: function() { return false; },
    
    /**
     * Binds the axis to a set of data cells.
     * 
     * <p>
     * Only after this operation is performed will
     * options with a scale type prefix be found.
     * </p>
     * 
     * @param {object|object[]} dataCells The associated data cells.
     * @type pvc.visual.Axis
     */
    bind: function(dataCells) {
        /*jshint expr:true */
        var me = this;
        dataCells || def.fail.argumentRequired('dataCells');
        !me.dataCells || def.fail.operationInvalid('Axis is already bound.');
        
        me.dataCells = def.array.to(dataCells);
        me.dataCell  = me.dataCells[0];
        me.role      = me.dataCell && me.dataCell.role;
        me.scaleType = axis_groupingScaleType(me.role.grouping);
        
        me._checkRoleCompatibility();
        
        return this;
    },
    
    isDiscrete: function() { return this.role && this.role.isDiscrete(); },
    
    isBound: function() { return !!this.role; },
    
    setScale: function(scale, noWrap) {
        /*jshint expr:true */
        this.role || def.fail.operationInvalid('Axis is unbound.');
        
        this.scale = scale ? (noWrap ? scale : this._wrapScale(scale)) : null;

        return this;
    },
    
    _wrapScale: function(scale) {
        scale.type = this.scaleType;
        
        var by;
        
        // Applying 'scaleNullRangeValue' to discrete scales
        // would cause problems in discrete color scales,
        // where we want null to be matched to the first color of the color scale
        // (typically happens when there is only a null series).
        if(scale.type !== 'discrete') {
            var useAbs = this.scaleUsesAbs();
            var nullAs = this.scaleTreatsNullAs();
            if(nullAs && nullAs !== 'null') {
                var nullIsMin = nullAs === 'min';
                // Below, the min valow is evaluated each time on purpose,
                // because otherwise we would have to rewrap when the domain changes.
                // It does change, for example, on MultiChart scale coordination.
                if(useAbs) {
                    by = function(v) {
                        return scale(v == null ? (nullIsMin ? scale.domain()[0] : 0) : (v < 0 ? -v : v));
                    };
                } else {
                    by = function(v) {
                        return scale(v == null ? (nullIsMin ? scale.domain()[0] : 0) : v);
                    };
                }
            } else {
                var nullRangeValue = this.scaleNullRangeValue();
                if(useAbs) {
                    by = function(v) { 
                        return v == null ? nullRangeValue : scale(v < 0 ? -v : v);
                    };
                } else {
                    by = function(v) {
                        return v == null ? nullRangeValue : scale(v);
                    };
                }
            }
        } else {
            // ensure null -> ""
            by = function(v) {
                return scale(v == null ? '' : v);
            };
        }
        
        // don't overwrite scale with by! it would cause infinite recursion...
        return def.copy(by, scale);
    },
    
    /**
     * Obtains a scene-scale function to compute values of this axis' main role.
     * 
     * @param {object} [keyArgs] Keyword arguments object.
     * @param {string} [keyArgs.sceneVarName] The local scene variable name by which this axis's role is known. Defaults to the role's name.
     * @param {boolean} [keyArgs.nullToZero=true] Indicates that null values should be converted to zero before applying the scale.
     * @type function
     */
    sceneScale: function(keyArgs) {
        var varName  = def.get(keyArgs, 'sceneVarName') || this.role.name,
            grouping = this.role.grouping;
        
        // TODO: isn't this redundant with the code in _wrapScale??
        if(grouping.isSingleDimension && grouping.firstDimensionValueType() === Number) {
            var scale = this.scale,
                nullToZero = def.get(keyArgs, 'nullToZero', true);
            
            var by = function(scene){
                var value = scene.vars[varName].value;
                if(value == null) {
                    if(!nullToZero) { return value; }
                    value = 0;
                }
                return scale(value);
            };
            def.copy(by, scale);
            
            return by;
        }

        return this.scale.by1(function(scene) {
            return scene.vars[varName].value;
        });
    },
    
    _checkRoleCompatibility: function() {
        var L = this.dataCells.length;
        if(L > 1) {
            var grouping = this.role.grouping; 
            var i;
            if(this.scaleType === 'discrete') {
                for(i = 1; i < L ; i++) {
                    if(grouping.id !== this.dataCells[i].role.grouping.id) {
                        throw def.error.operationInvalid("Discrete roles on the same axis must have equal groupings.");
                    }
                }
            } else {
                if(!grouping.firstDimensionType().isComparable) {
                    throw def.error.operationInvalid("Continuous roles on the same axis must have 'comparable' groupings.");
                }

                for(i = 1; i < L ; i++) {
                    if(this.scaleType !== axis_groupingScaleType(this.dataCells[i].role.grouping)) {
                        throw def.error.operationInvalid("Continuous roles on the same axis must have scales of the same type.");
                    }
                }
            }
        }
    },
    
    _getOptionsDefinition: function() { return axis_optionsDef; }
});

function axis_groupingScaleType(grouping) {
    return grouping.isDiscrete() ?
                'discrete' :
                (grouping.firstDimensionValueType() === Date ?
                'timeSeries' :
                'numeric');
}

var axis_optionsDef = {
// NOOP
};

/*global pvc_Sides:true, pvc_Size:true, pvc_Axis:true */

/**
 * Initializes a cartesian axis.
 * 
 * @name pvc.visual.CartesianAxis
 * 
 * @class Represents an axis for a role in a cartesian chart.
 * <p>
 * The main properties of an axis: {@link #type}, {@link #orientation} and relevant chart's properties 
 * are related as follows:
 * </p>
 * <pre>
 * axisType={base, ortho} = f(axisOrientation={x,y})
 * 
 *          Vertical   Horizontal   (chart orientation)
 *         +---------+-----------+
 *       x | base    |   ortho   |
 *         +---------+-----------+
 *       y | ortho   |   base    |
 *         +---------+-----------+
 * (axis orientation)
 * </pre>
 * 
 * @extends pvc.visual.Axis
 * 
 * @property {pvc.CartesianAbstract} chart The associated cartesian chart.
 * @property {string} type The type of the axis. One of the values: 'base' or 'ortho'.
 * @property {string} orientation The orientation of the axis. 
 * One of the values: 'x' or 'y', for horizontal and vertical axis orientations, respectively.
 * @property {string} orientedId The id of the axis with respect to the orientation and the index of the axis ("").
 * 
 * @constructor
 * @param {pvc.CartesianAbstract} chart The associated cartesian chart.
 * @param {string} type The type of the axis. One of the values: 'base' or 'ortho'.
 * @param {number} [index=0] The index of the axis within its type.
 * @param {object} [keyArgs] Keyword arguments.
 * See {@link pvc.visual.Axis} for supported keyword arguments. 
 */

var pvc_CartesianAxis = 
def
.type('pvc.visual.CartesianAxis', pvc_Axis)
.init(function(chart, type, index, keyArgs){
    
    var options = chart.options;
    
    // x, y
    this.orientation = pvc_CartesianAxis.getOrientation(type, options.orientation);
    
    // x, y, x2, y2, x3, y3, ...
    this.orientedId = pvc_CartesianAxis.getOrientedId(this.orientation, index);
    
    // secondX, secondY
    if(chart._allowV1SecondAxis &&  index === 1){
        this.v1SecondOrientedId = 'second' + this.orientation.toUpperCase();
    }
    
    // id
    // base, ortho, base2, ortho2, ...
    
    // scaleType
    // discrete, continuous, numeric, timeSeries
    
    // common
    // axis
    
    this.base(chart, type, index, keyArgs);
    
    // For now scale type is left off, 
    // cause it is yet unknown.
    // In bind, prefixes are recalculated (see _syncExtensionPrefixes)
    var extensions = this.extensionPrefixes = [
        this.id + 'Axis', 
        this.orientedId + 'Axis'
    ];
    
    if(this.v1SecondOrientedId){
        extensions.push(this.v1SecondOrientedId + 'Axis');
    }
    
    this._extPrefAxisPosition = extensions.length;
    
    extensions.push('axis');
})
.add(/** @lends pvc.visual.CartesianAxis# */{
    
    bind: function(dataCells){
        
        this.base(dataCells);
        
        this._syncExtensionPrefixes();
        
        return this;
    },
    
    _syncExtensionPrefixes: function(){
        var extensions = this.extensionPrefixes;
        
        // remove until 'axis' (inclusive)
        extensions.length = this._extPrefAxisPosition;
        
        var st = this.scaleType;
        if(st){
            extensions.push(st + 'Axis'); // specific
            if(st !== 'discrete'){
                extensions.push('continuousAxis'); // generic
            }
        }
        
        // Common
        extensions.push('axis');
    },
    
    setScale: function(scale){
        var oldScale = this.scale;
        
        this.base(scale);
        
        if(oldScale){
            // If any
            delete this.domain;
            delete this.ticks;
            delete this._roundingPaddings;
        }
        
        if(scale){
            if(!scale.isNull && this.scaleType !== 'discrete'){
                // Original data domain, before nice or tick rounding
                this.domain = scale.domain();
                this.domain.minLocked = !!scale.minLocked;
                this.domain.maxLocked = !!scale.maxLocked;
                
                if(this.scaleType === 'numeric'){
                    var roundMode = this.option('DomainRoundMode');
                    if(roundMode === 'nice'){
                        scale.nice();
                    }
                    
                    var tickFormatter = this.option('TickFormatter');
                    if(tickFormatter){
                        scale.tickFormatter(tickFormatter);
                    }
                }
            }
        }
        
        return this;
    },
    
    setTicks: function(ticks){
        var scale = this.scale;
        
        /*jshint expr:true */
        (scale && !scale.isNull) || def.fail.operationInvalid("Scale must be set and non-null.");
        
        this.ticks = ticks;
        
        if(scale.type === 'numeric' && this.option('DomainRoundMode') === 'tick'){
            
            delete this._roundingPaddings;
            
            // Commit calculated ticks to scale's domain
            var tickCount = ticks && ticks.length;
            if(tickCount){
                this.scale.domain(ticks[0], ticks[tickCount - 1]);
            } else {
                // Reset scale domain
                this.scale.domain(this.domain[0], this.domain[1]);
            }
        }
    },
    
    setScaleRange: function(size){
        var scale  = this.scale;
        scale.min  = 0;
        scale.max  = size;
        scale.size = size; // original size // TODO: remove this...
        
        // -------------
        
        if(scale.type === 'discrete'){
            if(scale.domain().length > 0){ // Has domain? At least one point is required to split.
                var bandRatio = this.chart.options.panelSizeRatio || 0.8;
                scale.splitBandedCenter(scale.min, scale.max, bandRatio);
            }
        } else {
            scale.range(scale.min, scale.max);
        }

        return scale;
    },
    
    getScaleRoundingPaddings: function(){
        var roundingPaddings = this._roundingPaddings;
        if(!roundingPaddings){
            roundingPaddings = {
                begin: 0, 
                end:   0, 
                beginLocked: false, 
                endLocked:   false
            };
            
            var scale = this.scale;
            if(scale && !scale.isNull && scale.type !== 'discrete'){
                var originalDomain = this.domain;
                
                roundingPaddings.beginLocked = originalDomain.minLocked;
                roundingPaddings.endLocked   = originalDomain.maxLocked;
                
                if(scale.type === 'numeric' && this.option('DomainRoundMode') !== 'none'){
                    var currDomain = scale.domain();
                    var origDomain = this.domain || def.assert("Original domain must be set");
                    var currLength = currDomain[1] - currDomain[0];
                    if(currLength){
                        // begin diff
                        var diff = origDomain[0] - currDomain[0];
                        if(diff > 0){
                            roundingPaddings.begin = diff / currLength;
                        }
                        
                        // end diff
                        diff = currDomain[1] - origDomain[1];
                        if(diff > 0){
                            roundingPaddings.end = diff / currLength;
                        }
                    }
                }
            }
            
            this._roundingPaddings = roundingPaddings;
        }
        
        return roundingPaddings;
    },

    calcContinuousTicks: function(desiredTickCount){
        if(desiredTickCount == null) {
            desiredTickCount = this.option('DesiredTickCount');
        }

        return this.scale.ticks(
            desiredTickCount,
            {
                roundInside:       this.option('DomainRoundMode') !== 'tick',
                numberExponentMin: this.option('TickExponentMin'),
                numberExponentMax: this.option('TickExponentMax')
            });
    },

    _getOptionsDefinition: function(){
        return cartAxis_optionsDef;
    },
    
    _buildOptionId: function(){
        return this.id + "Axis";
    },
    
    _registerResolversNormal: function(rs, keyArgs){
        // II - By V1 Only Logic
        if(this.chart.compatVersion() <= 1){
            rs.push(this._resolveByV1OnlyLogic);
        }
        
        // IV - By OptionId
        rs.push(
           this._resolveByOptionId,
           this._resolveByOrientedId);
        
        if(this.index === 1){
            rs.push(this._resolveByV1OptionId);
        }
        
        rs.push(
           this._resolveByScaleType,
           this._resolveByCommonId);
        
    },
    
    // xAxisOffset, yAxisOffset, x2AxisOffset
    _resolveByOrientedId: pvc.options.specify(function(optionInfo){
        return this._chartOption(this.orientedId + "Axis" + optionInfo.name);
    }),
    
    // secondAxisOffset
    _resolveByV1OptionId: pvc.options.specify(function(optionInfo){
        //if(this.index === 1){
        return this._chartOption('secondAxis' + optionInfo.name);
        //}
    }),
    
    // numericAxisLabelSpacingMin
    _resolveByScaleType: pvc.options.specify(function(optionInfo){
        // this.scaleType
        // * discrete
        // * numeric    | continuous
        // * timeSeries | continuous
        var st = this.scaleType;
        if(st){
            var name  = optionInfo.name;
            var value = this._chartOption(st + 'Axis' + name);
            if(value === undefined && st !== 'discrete'){
                value = this._chartOption('continuousAxis' + name);
            }
            
            return value;
        }
    }),
    
    // axisOffset
    _resolveByCommonId: pvc.options.specify(function(optionInfo){
        return this._chartOption('axis' + optionInfo.name);
    })
});

/**
 * Obtains the orientation of the axis given an axis type and a chart orientation.
 * 
 * @param {string} type The type of the axis. One of the values: 'base' or 'ortho'.
 * @param {string} chartOrientation The orientation of the chart. One of the values: 'horizontal' or 'vertical'.
 * 
 * @type string
 */
pvc_CartesianAxis.getOrientation = function(type, chartOrientation){
    return ((type === 'base') === (chartOrientation === 'vertical')) ? 'x' : 'y';  // NXOR
};

/**
 * Calculates the oriented id of an axis given its orientation and index.
 * @param {string} orientation The orientation of the axis.
 * @param {number} index The index of the axis within its type. 
 * @type string
 */
pvc_CartesianAxis.getOrientedId = function(orientation, index){
    if(index === 0) {
        return orientation; // x, y
    }
    
    return orientation + (index + 1); // x2, y3, x4,...
};

/* PRIVATE STUFF */
var cartAxis_fixedMinMaxSpec = {
    resolve: '_resolveFull',
    data: {
        /* orthoFixedMin, orthoFixedMax */
        resolveV1: function(optionInfo){
            if(!this.index && this.type === 'ortho'){
                // Bare Id (no "Axis")
                this._specifyChartOption(optionInfo, this.id + optionInfo.name);
            }
            return true;
        }
    },
    cast: pvc.castNumber
};

function pvc_castDomainScope(scope, axis) {
    return pvc.parseDomainScope(scope, axis.orientation);
}

function pvc_castAxisPosition(side) {
    if(side){
        if(def.hasOwn(pvc_Sides.namesSet, side)){
            var mapAlign = pvc.BasePanel[this.orientation === 'y' ? 'horizontalAlign' : 'verticalAlign2'];
            return mapAlign[side];
        }
        
        if(pvc.debug >= 2){
            pvc.log(def.format("Invalid axis position value '{0}'.", [side]));
        }
    }
    
    // Ensure a proper value
    return this.orientation === 'x' ? 'bottom' : 'left';
}

var cartAxis_normalV1Data = {
    resolveV1: function(optionInfo){
        if(!this.index){
            if(this._resolveByOrientedId(optionInfo)){
                return true;
            }
        } else if(this._resolveByV1OptionId(optionInfo)) { // secondAxis...
            return true;
        }
        
        this._resolveDefault(optionInfo);
        
        return true;
    }
};

var defaultPosition = pvc.options.defaultValue(function(optionInfo){
    if(!this.typeIndex){
        return this.orientation === 'x' ? 'bottom' : 'left';
    }
    
    // Use the position opposite to that of the first axis 
    // of same orientation (the same as type)
    var firstAxis = this.chart.axesByType[this.type].first;
    var position  = firstAxis.option('Position');
    
    return pvc.BasePanel.oppositeAnchor[position];
});

function cartAxis_castSize(value){
    var position = this.option('Position');
    return pvc_Size.toOrtho(value, position);
}

function cartAxis_castTitleSize(value){
    var position = this.option('Position');
    
    return pvc_Size.to(value, {singleProp: pvc.BasePanel.orthogonalLength[position]});
}

/*global axis_optionsDef:true*/
var cartAxis_optionsDef = def.create(axis_optionsDef, {
    Visible: {
        resolve: '_resolveFull',
        data: {
            /* showXScale, showYScale, showSecondScale */
            resolveV1: function(optionInfo){
                if(this.index <= 1){
                    var v1OptionId = this.index === 0 ? 
                        def.firstUpperCase(this.orientation) :
                        'Second';
                    
                    this._specifyChartOption(optionInfo, 'show' + v1OptionId + 'Scale');
                }
                return true;
            }
        },
        cast:    Boolean,
        value:   true
    },
    
    /*
     * 1     <- useCompositeAxis
     * >= 2  <- false
     */
    Composite: {
        resolve: function(optionInfo){
            // Only first axis can be composite?
            if(this.index > 0) {
                optionInfo.specify(false);
                return true;
            }
            
            return this._resolveFull(optionInfo);
        },
        data: {
            resolveV1: function(optionInfo){
                this._specifyChartOption(optionInfo, 'useCompositeAxis');
                return true;
            }
        },
        cast:  Boolean,
        value: false
    },
    
    /* xAxisSize,
     * secondAxisSize || xAxisSize 
     */
    Size: {
        resolve: '_resolveFull',
        data:    cartAxis_normalV1Data,
        cast:    cartAxis_castSize
    },
    
    SizeMax: {
        resolve: '_resolveFull',
        cast:    cartAxis_castSize
    },
    
    /* xAxisPosition,
     * secondAxisPosition <- opposite(xAxisPosition) 
     */
    Position: {
        resolve: '_resolveFull',
        data: {
            resolveV1: cartAxis_normalV1Data.resolveV1,
            resolveDefault: defaultPosition
        },
        cast: pvc_castAxisPosition
    },
    
    FixedMin: cartAxis_fixedMinMaxSpec,
    FixedMax: cartAxis_fixedMinMaxSpec,
    
    /* 1 <- originIsZero (v1)
     * 2 <- secondAxisOriginIsZero (v1 && bar)
     */
    OriginIsZero: {
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                switch(this.index){
                    case 0: 
                        this._specifyChartOption(optionInfo, 'originIsZero');
                        break;
                    case 1:
                        if(this.chart._allowV1SecondAxis){
                            this._specifyChartOption(optionInfo, 'secondAxisOriginIsZero');
                        }
                        break;
                }
                
                return true;
            } 
        },
        cast:  Boolean,
        value: true 
    }, 
    
    DomainScope: {
        resolve: '_resolveFull',
        cast:    pvc_castDomainScope,
        value:   'global'
    },
    
    /* 1 <- axisOffset, 
     * 2 <- secondAxisOffset (V1 && bar)
     */
    Offset: {
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                switch(this.index) {
                    case 0: 
                        this._specifyChartOption(optionInfo, 'axisOffset');
                        break;
                        
                    case 1:
                        if(this.chart._allowV1SecondAxis){
                            this._specifyChartOption(optionInfo, 'secondAxisOffset');
                            break;
                        }
                        break;
                }
                
                return true;
            }
        },
        cast: pvc.castNumber
    },
    
    // em
    LabelSpacingMin: {
        resolve: '_resolveFull',
        cast:    pvc.castNumber
    },
    
    OverlappedLabelsMode: {
        resolve: '_resolveFull',
        cast:    pvc.parseOverlappedLabelsMode,
        value:   'rotatethenhide'
    },
    
    /* RULES */
    Grid: {
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                if(!this.index){
                    this._specifyChartOption(optionInfo, this.orientation + 'AxisFullGrid');
                }
                return true;
            }
        },
        cast:    Boolean,
        value:   false
    },
    
    GridCrossesMargin: { // experimental
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   true
    },
    
    EndLine:  { // deprecated
        resolve: '_resolveFull',
        cast:    Boolean
    },
    
    ZeroLine: {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   true 
    },
    RuleCrossesMargin: { // experimental
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   true
    },
    
    /* TICKS */
    Ticks: {
        resolve: '_resolveFull',
        cast:    Boolean
    },
    DesiredTickCount: { // secondAxisDesiredTickCount (v1 && bar)
        resolve: '_resolveFull',
        data: {
            resolveV1: cartAxis_normalV1Data.resolveV1,
            resolveDefault: function(optionInfo){
                if(this.chart.compatVersion() <= 1){
                    optionInfo.defaultValue(5);
                    return true;
                }
            }
        },
        cast: pvc.castNumber
    },
    MinorTicks: {
        resolve: '_resolveFull',
        data:    cartAxis_normalV1Data,
        cast:    Boolean,
        value:   true 
    },
    TickFormatter: {
        resolve: '_resolveFull',
        cast:    def.fun.as
    },
    DomainRoundMode: { // secondAxisRoundDomain (bug && v1 && bar), secondAxisDomainRoundMode (v1 && bar)
        resolve: '_resolveFull',
        data: {
            resolveV1: cartAxis_normalV1Data.resolveV1,
            resolveDefault: function(optionInfo){
                if(this.chart.compatVersion() <= 1){
                    optionInfo.defaultValue('none');
                    return true;
                }
            }
        },
        
        cast:    pvc.parseDomainRoundingMode,
        value:   'tick'
    },
    TickExponentMin: {
        resolve: '_resolveFull',
        cast:    pvc.castNumber  
    },
    TickExponentMax: {
        resolve: '_resolveFull',
        cast:    pvc.castNumber 
    },
    
    /* TITLE */
    Title: {
        resolve: '_resolveFull',
        cast:    String
    },
    TitleSize: {
        resolve: '_resolveFull',
        cast:    cartAxis_castTitleSize
    },
    TitleSizeMax: {
        resolve: '_resolveFull',
        cast:    cartAxis_castTitleSize
    }, 
    TitleFont: {
        resolve: '_resolveFull',
        cast:    String 
    },
    TitleMargins:  {
        resolve: '_resolveFull',
        cast:    pvc_Sides.as 
    },
    TitlePaddings: {
        resolve: '_resolveFull',
        cast:    pvc_Sides.as 
    },
    TitleAlign: {
        resolve: '_resolveFull',
        cast: function castAlign(align){
            var position = this.option('Position');
            return pvc.parseAlign(position, align);
        }
    },
    
    Font: { // axisLabelFont (v1 && index == 0 && HeatGrid)
        resolve: '_resolveFull',
        cast:    String
    },
    
    ClickAction: { 
        resolve: '_resolveFull',
        data: cartAxis_normalV1Data
    }, // (v1 && index === 0)
    
    DoubleClickAction: { 
        resolve: '_resolveFull',
        data: cartAxis_normalV1Data
    }, // idem
    
    /* TOOLTIP */
    TooltipEnabled: {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:  true
    },
    
    TooltipFormat: {
        resolve: '_resolveFull',
        cast:   def.fun.as,
        value:  null
    },
    
    TooltipAutoContent: {
        resolve: '_resolveFull',
        cast:   pvc.parseTooltipAutoContent,
        value:  'value'
    }
});

/**
 * Initializes an axis root scene.
 * 
 * @name pvc.visual.CartesianAxisRootScene
 * 
 * @extends pvc.visual.Scene
 * 
 * @constructor
 * @param {pvc.visual.Scene} [parent] The parent scene, if any.
 * @param {object} [keyArgs] Keyword arguments.
 * See {@link pvc.visual.Scene} for supported keyword arguments.
 */
def
.type('pvc.visual.CartesianAxisRootScene', pvc.visual.Scene);

/*global pvc_ValueLabelVar:true*/

/**
 * Initializes an axis tick scene.
 * 
 * @name pvc.visual.CartesianAxisTickScene
 * 
 * @extends pvc.visual.Scene
 * 
 * @constructor
 * @param {pvc.visual.CartesianAxisRootScene} [parent] The parent scene, if any.
 * @param {object} [keyArgs] Keyword arguments.
 * See {@link pvc.visual.Scene} for supported keyword arguments.
 */
def
.type('pvc.visual.CartesianAxisTickScene', pvc.visual.Scene)
.init(function(parent, keyArgs) {
    
    this.base(parent, keyArgs);
    
    this.vars.tick = new pvc_ValueLabelVar(
            def.get(keyArgs, 'tick'),
            def.get(keyArgs, 'tickLabel'),
            def.get(keyArgs, 'tickRaw'));
    
    if(def.get(keyArgs, 'isHidden')) {
        this.isHidden = true;
    }
})
.add({
    // True when the scene contains excluded data(s)
    // due to overlappedLabelsMode:hide exclusion
    isHidden: false
});


/*global axis_optionsDef:true*/

def
.type('pvc.visual.CartesianFocusWindow', pvc.visual.OptionsBase)
.init(function(chart){
    
    this.base(chart, 'focusWindow', 0, {byNaked: false});
    
    // TODO: ortho
    var baseAxis = chart.axes.base;
    this.base = new pvc.visual.CartesianFocusWindowAxis(this, baseAxis);
})
.add(/** @lends pvc.visual.FocusWindow# */{
    _getOptionsDefinition: function() { return focusWindow_optionsDef; },
    
    _exportData: function(){
        return {
            base: def.copyProps(this.base, pvc.visual.CartesianFocusWindow.props)
        };
    },
    
    _importData: function(data){
        var baseData = data.base;
        
        this.base.option.specify({
            Begin:  baseData.begin,
            End:    baseData.end,
            Length: baseData.length
        });
    },
    
    _initFromOptions: function(){
        this.base._initFromOptions();
    },
    
    _onAxisChanged: function(axis) {
        // Fire event
        var changed = this.option('Changed');
        if(changed) {
            changed.call(this.chart.basePanel.context());
        }
    }
});

var focusWindow_optionsDef = def.create(axis_optionsDef, {
    Changed: {
        resolve: '_resolveFull',
        cast:    def.fun.as
    }
});

def
.type('pvc.visual.CartesianFocusWindowAxis', pvc.visual.OptionsBase)
.init(function(fw, axis){
    this.window = fw;
    this.axis = axis;
    this.isDiscrete = axis.isDiscrete();
    
    // focusWindowBase/Ortho
    this.base(
        axis.chart, 
        'focusWindow' + def.firstUpperCase(axis.type), 
        0,
        {byNaked: false});
})
.addStatic({
    props: ['begin', 'end', 'length']
})
.add(/** @lends pvc.visual.FocusWindow# */{
    _getOptionsDefinition: function() { return focusWindowAxis_optionsDef; },
    
    _initFromOptions: function(){
        var o = this.option;
        this.set({
            begin:  o('Begin' ),
            end:    o('End'   ),
            length: o('Length')
        });
    },
    
    set: function(keyArgs){
        var me = this;
        
        var render = def.get(keyArgs, 'render');
        var select = def.get(keyArgs, 'select', true);
        
        var b, e, l;
        keyArgs = me._readArgs(keyArgs);
        if(!keyArgs){
            if(this.begin != null && this.end != null && this.length != null){
                return;
            }
        } else {
            b = keyArgs.begin;
            e = keyArgs.end;
            l = keyArgs.length;
        }
        
        var axis       = me.axis;
        var scale      = axis.scale;
        var isDiscrete = me.isDiscrete;
        var contCast   = !isDiscrete ? axis.role.firstDimensionType().cast : null;
        var domain     = scale.domain();
        
        var a, L;
        if(isDiscrete){
            L = domain.length;
            var ib, ie, ia;
            if(b != null){
                var nb = +b;
                if(!isNaN(nb)){
                    if(nb === Infinity){
                        ib = L - 1;
                        b  = domain[ib];
                    } else if(nb === -Infinity){
                        ib = 0;
                        b  = domain[ib];
                    }
                }
                
                if(ib == null){
                    ib = domain.indexOf(''+b);
                    if(ib < 0){
                        //b = null;
                        ib = 0;
                        b  = domain[ib];
                    }
                }
            }
            
            if(e != null){
                var ne = +e;
                if(!isNaN(ne)){
                    if(ne === Infinity){
                        ie = L - 1;
                        e  = domain[ie];
                    } else if(ne === -Infinity){
                        ie = 0;
                        e  = domain[ie];
                    }
                }
                
                if(ie == null){
                    ie = domain.indexOf(''+e);
                    if(ie < 0){
                        //e = null;
                        ie = L - 1;
                        e  = domain[ie];
                    }
                }
            }
            
            if(l != null){
                l = +l;
                if(isNaN(l)){
                    l = null;
                } else if(l < 0 && (b != null || e != null)) {
                    // Switch b and e 
                    a  = b;
                    ia = ib;
                    b  = e, ib = ie, e = a, ie = ia;
                    l  = -l;
                }
                
                // l > L ??
            }
            
            if(b != null){
                if(e != null){
                    if(ib > ie){
                        // Switch b and e
                        a  = b;
                        ia = ib;
                        b  = e, ib = ie, e = a, ie = ia;
                    }
                    // l is ignored
                    l = ie - ib + 1;
                } else {
                    // b && !e
                    if(l == null){
                        // to the end of the domain?
                        l = L - ib;
                    }
                    
                    ie = ib + l - 1;
                    if(ie > L - 1){
                        ie = L - 1;
                        l = ie - ib + 1;
                    }
                    
                    e = domain[ie];
                }
            } else {
                // !b
                if(e != null){
                    // !b && e
                    if(l == null){
                        // from the beginning of the domain?
                        l = ie;
                        // ib = 0
                    }
                    
                    ib = ie - l + 1;
                    if(ib < 0){
                        ib = 0;
                        l = ie - ib + 1;
                    }
                    
                    b = domain[ib];
                } else {
                    // !b && !e
                    if(l == null){
                        l = Math.max(~~(L / 3), 1); // 1/3 of the width?
                    }
                    
                    if(l > L){
                        l = L;
                        ib = 0;
                        ie = L - 1;
                    } else {
                        // ~~ <=> Math.floor for x >= 0
                        ia = ~~(L / 2); // window center
                        ib = ia - ~~(l/2);
                        ie = ib + l - 1;
                    }
                    
                    b = domain[ib];
                    e = domain[ie];
                }
            }
            
        } else {
            // Continuous
            
            if(l != null){
                l = +l;
                if(isNaN(l)){
                    l = null;
                } else if(l < 0 && (b != null || e != null)) {
                    // Switch b and e 
                    a  = b;
                    b = e, e = a;
                    l = -l;
                }
                
                // l > L ??
            }
            
            var min = domain[0];
            var max = domain[1];
            L  = max - min; 
            if(b != null){
                // -Infinity is a placeholder for min
                if(b < min){
                    b = min;
                }
                
                // +Infinity is a placeholder for max
                if(b > max){
                    b = max;
                }
            }
            
            if(e != null){
                // -Infinity is a placeholder for min
                if(e < min){
                    e = min;
                }
                
                // +Infinity is a placeholder for max
                if(e > max){
                    e = max;
                }
            }
            
            if(b != null){
                if(e != null){
                    if(b > e){
                        // Switch b and e
                        a  = b;
                        b = e, e = a;
                    }
                    l = e - b;
                } else {
                    // b && !e
                    if(l == null){
                        // to the end of the domain?
                        l = max - b;
                    }
                    
                    e = b + l;
                    if(e > max){
                        e = max;
                        l = e - b;
                    }
                }
            } else {
                // !b
                if(e != null){
                    // !b && e
                    if(l == null){
                        // from the beginning of the domain?
                        l = e - min;
                        // b = min
                    }
                    
                    b = e - l;
                    if(b < min){
                        b = min;
                        l = e - b;
                    }
                } else {
                    // !b && !e
                    if(l == null){
                        l = Math.max(~~(L / 3), 1); // 1/3 of the width?
                    }
                    
                    if(l > L){
                        l = L;
                        b = min;
                        e = max;
                    } else {
                        // ~~ <=> Math.floor for x >= 0
                        a = ~~(L / 2); // window center
                        b = a - ~~(l/2);
                        e = (+b) + (+l); // NOTE: Dates subtract, but don't add...
                    }
                }
            }
            
            b = contCast(b);
            e = contCast(e);
            l = contCast(l);
            
            var constraint = me.option('Constraint');
            if(constraint){
                var oper2 = {
                    type:    'new',
                    target:  'begin',
                    value:   b,
                    length:  l,
                    length0: l,
                    min:     min,
                    max:     max,
                    minView: min,
                    maxView: max
                };
                
                constraint(oper2);
                
                b = contCast(oper2.value );
                l = contCast(oper2.length);
                e = contCast((+b) + (+l)); // NOTE: Dates subtract, but don't add...
            }
        }
        
        me._set(b, e, l, select, render);
    },
    
    _updatePosition: function(pbeg, pend, select, render){
        var me = this;
        var axis = me.axis;
        var scale = axis.scale;
        
        var b, e, l;
        
        if(me.isDiscrete){
            var ib = scale.invertIndex(pbeg);
            var ie = scale.invertIndex(pend) - 1;
            var domain = scale.domain();
            
            b = domain[ib];
            e = domain[ie];
            l = ie - ib + 1;
        } else {
            b = scale.invert(pbeg);
            e = scale.invert(pend);
            l = e - b;
        }
        
        this._set(b, e, l, select, render);
    },
    
    /*
        var oper = {
            type:    op,
            target:  target,
            point:   p,
            length:  l,  // new length
            length0: l0, // prev length
            min:     drag.min[a_p],
            max:     drag.max[a_p],
            minView: 0,
            maxView: w
        };
     */
    _constraintPosition: function(oper){
        var me = this;
        var axis = me.axis;
        var scale = axis.scale;
        var constraint;
        
        if(me.isDiscrete){
            // Align to category boundaries
            var index = Math.floor(scale.invertIndex(oper.point, /* noRound */true));
            if(index >= 0){
                var r = scale.range();
                var L = scale.domain().length;
                var S = (r.max - r.min) / L;
                if(index >= L && (oper.type === 'new' || oper.type === 'resize-begin')){
                    index = L - 1;
                }
                oper.point = index * S;
            }
        } else if((constraint = me.option('Constraint'))){
            var contCast = axis.role.firstDimensionType().cast;
            var v = contCast(scale.invert(oper.point));
            
            var sign    = oper.target === 'begin' ? 1 : -1;
            
            var pother  = oper.point + sign * oper.length;
            var vother  = contCast(scale.invert(pother));
            var vlength = contCast(sign * (vother - v));
            
            var vlength0, pother0, vother0;
            if(oper.length === oper.length0){
                vlength0 = vlength;
            } else {
                pother0  = oper.point + sign * oper.length0;
                vother0  = contCast(scale.invert(pother0));
                vlength0 = sign * (vother0 - v);
            }
            
            var vmin = contCast(scale.invert(oper.min));
            var vmax = contCast(scale.invert(oper.max));
            
            var oper2 = {
                type:    oper.type,
                target:  oper.target,
                value:   v,
                length:  vlength,  // new length if value is accepted
                length0: vlength0, // prev length (with previous value)
                min:     vmin,
                max:     vmax,
                minView: contCast(scale.invert(oper.minView)),
                maxView: contCast(scale.invert(oper.maxView))
            };
            
            constraint(oper2);
            
            // detect any changes and update oper
            if(+oper2.value !== +v){
                v = oper2.value;
                oper.point = scale(v);
            }
            
            var vlength2 = oper2.length;
            if(+vlength2 !== +vlength){
                if(+vlength2 === +vlength0){
                    oper.length = oper.length0;
                } else {
                    var vother2 = (+v) + sign * (+vlength2); // NOTE: Dates subtract, but don't add...
                    var pother2 = scale(vother2);
                    
                    oper.length = pother2 - sign * oper.point;
                }
            }
            
            if(+oper2.min !== +vmin){
                oper.min = scale(oper2.min);
            }
            
            if(+oper2.max !== +vmax){
                oper.max = scale(oper2.max);
            }
        }
    },
    
    _compare: function(a, b){
        return this.isDiscrete ? 
               (('' + a) === ('' + b)) : 
               ((+a) === (+b));
    },
    
    _set: function(b, e, l, select, render){
        var me = this;
        var changed = false;
        
        if(!me._compare(b, me.begin)){
            me.begin = b;
            changed  = true;
        }
        
        if(!me._compare(e, me.end)){
            me.end  = e;
            changed = true;
        }
        
        if(!me._compare(l, me.length)){
            me.length = l;
            changed = true;
        }
        
        if(changed){
            me.window._onAxisChanged(this);
        }
        
        if(select){
            me._updateSelection({render: render});
        }
        
        return changed;
    },
    
    _readArgs: function(keyArgs){
        if(keyArgs){
            var out = {};
            var any = 0;
            
            var read = function(p){
                var v = keyArgs[p];
                if(v != null){
                    any = true;
                } else {
                    v = this[p];
                }
                
                out[p] = v;
            };
            
            pvc.visual.CartesianFocusWindowAxis.props.forEach(read, this);
            
            if(any){
                return out;
            }
        }
    },
    
    // keyArgs: render: boolean [true]
    _updateSelection: function(keyArgs){
        var me = this;
        
        // TODO: Only the first dataCell is supported...
        // TODO: cache domainData?
        
        var selectDatums;
        var axis       = me.axis;
        var isDiscrete = axis.isDiscrete();
        var chart      = axis.chart;
        var dataCell   = axis.dataCell;
        var role       = dataCell.role;
        var partData   = chart.partData(dataCell.dataPartValue, {visible: true});
        var domainData;
        if(isDiscrete){
            domainData = role.flatten(partData);
            
            var dataBegin = domainData._childrenByKey[me.begin];
            var dataEnd   = domainData._childrenByKey[me.end  ];
            if(dataBegin && dataEnd){
                var indexBegin = dataBegin.childIndex();
                var indexEnd   = dataEnd  .childIndex();
                selectDatums = def
                    .range(indexBegin, indexEnd - indexBegin + 1)
                    .select(function(index){ return domainData._children[index]; })
                    .selectMany(function(data){ return data._datums; })
                    .distinct(function(datum){ return datum.key; });
            }
        } else {
            domainData = partData;
            
            var dimName = role.firstDimensionName();
            selectDatums = def
                .query(partData._datums)
                .where(function(datum){
                    var v = datum.atoms[dimName].value;
                    return v != null && v >= me.begin && v <= me.end;
                });
        }
        
        if(selectDatums){
            chart.data.replaceSelected(selectDatums);
            
            // Fire events; maybe render (keyArgs)
            chart.root.updateSelections(keyArgs);
        }
    } 
});


var focusWindowAxis_optionsDef = def.create(axis_optionsDef, {
    Resizable: {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   true
    },
    
    Movable:   {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   true
    },
    
    Begin: {
        resolve: '_resolveFull'
    },
    
    End: {
        resolve: '_resolveFull'
    },
    
    Length: {
        resolve: '_resolveFull'
    },
    
    // Continuous Axis function(v, vother, op, vmax) -> vconstrained
    Constraint: {
        resolve: '_resolveFull',
        cast:    def.fun.as
    }
});


/*global pvc_Size:true, pvc_Axis:true */

/**
 * Initializes a color axis.
 * 
 * @name pvc.visual.ColorAxis
 * 
 * @class Represents an axis that maps colors to the values of a role.
 * 
 * @extends pvc.visual.Axis
 */
def
.type('pvc.visual.ColorAxis', pvc_Axis)
.add(/** @lends pvc.visual.ColorAxis# */{
    
    scaleNullRangeValue: function(){
        return this.option('Missing') || null;
    },
    
    scaleUsesAbs: function(){
        return this.option('UseAbs');
    },
    
    bind: function(dataCells){
        this.base(dataCells);
        
        // -- collect distinct plots
        // Transform depends on this
        // Colors depends on Transform
        this._plotList = 
            def
            .query(dataCells)
            .select(function(dataCell){ return dataCell.plot; })
            .distinct(function(plot){ return plot && plot.id; })
            .array();
        
        return this;
    },
    
    // Called from within setScale
    _wrapScale: function(scale){
        // Check if there is a color transform set
        // and if so, transform the color scheme.
        // If the user specified the colors,
        // do not apply default color transforms...
        var applyTransf;
        if(this.scaleType === 'discrete') {
            applyTransf = this.option.isSpecified('Transform') || 
                          (!this.option.isSpecified('Colors') && 
                           !this.option.isSpecified('Map'   ));
        } else {
            applyTransf = true;
        }
        
        if(applyTransf){
            var colorTransf = this.option('Transform');
            if(colorTransf){
                scale = scale.transform(colorTransf);
            }
        }
        
        return this.base(scale);
    },
    
    scheme: function(){
        return def.lazy(this, '_scheme', this._createScheme, this);
    },
    
    _createColorMapFilter: function(colorMap){
        // Fixed Color Values (map of color.key -> first domain value of that color)
        var fixedColors = def.uniqueIndex(colorMap, function(c){ return c.key; });
        
        return {
            domain: function(k){
                return !def.hasOwn(colorMap, k); 
            },
            
            color: function(c){
                return !def.hasOwn(fixedColors, c.key);
            }
        };
    },
    
    _createScheme: function(){
        var me = this;
        var baseScheme = me.option('Colors');
        
        if(me.scaleType !== 'discrete'){
            // TODO: this implementation doesn't support NormByCategory...
            return function(/*domainAsArrayOrArgs*/){
                // Create a fresh baseScale, from the baseColorScheme
                // Use baseScale directly
                var scale = baseScheme.apply(null, arguments);
                
                // Apply Transforms, nulls, etc, according to the axis' rules
                return me._wrapScale(scale);
            };
        }
        
        var colorMap = me.option('Map'); // map domain key -> pv.Color
        if(!colorMap){
            return function(/*domainAsArrayOrArgs*/){
                // Create a fresh baseScale, from the baseColorScheme
                // Use baseScale directly
                var scale = baseScheme.apply(null, arguments);
                
                // Apply Transforms, nulls, etc, according to the axis' rules
                return me._wrapScale(scale);
            };
        } 

        var filter = this._createColorMapFilter(colorMap);
            
        return function(d/*domainAsArrayOrArgs*/){
            
            // Create a fresh baseScale, from the baseColorScheme
            var scale;
            if(!(d instanceof Array)){
                d = def.array.copy(arguments);
            }
            
            // Filter the domain before creating the scale
            d = d.filter(filter.domain);
            
            var baseScale = baseScheme(d);
            
            // Remove fixed colors from the baseScale
            var r = baseScale.range().filter(filter.color);
            
            baseScale.range(r);
            
            // Intercept so that the fixed color is tested first
            scale = function(k){
                var c = def.getOwn(colorMap, k);
                return c || baseScale(k);
            };
            
            def.copy(scale, baseScale);
            
            // override domain and range methods
            var dx, rx;
            scale.domain = function(){
                if (arguments.length) {
                    throw def.operationInvalid("The scale cannot be modified.");
                }
                if(!dx){
                    dx = def.array.append(def.ownKeys(colorMap), d);
                }
                return dx;
            };
            
            scale.range = function(){
                if (arguments.length) {
                    throw def.operationInvalid("The scale cannot be modified.");
                }
                if(!rx){
                    rx = def.array.append(def.own(colorMap), d);
                }
                return rx;
            };
            
            // At last, apply Transforms, nulls, etc, according to the axis' rules
            return me._wrapScale(scale);
        };
    },
    
    sceneScale: function(keyArgs){
        var varName = def.get(keyArgs, 'sceneVarName') || this.role.name;

        var fillColorScaleByColKey = this.scalesByCateg;
        if(fillColorScaleByColKey){
            var colorMissing = this.option('Missing');
            
            return function(scene){
                var colorValue = scene.vars[varName].value;
                if(colorValue == null) {
                    return colorMissing;
                }
                
                var catAbsKey = scene.group.parent.absKey;
                return fillColorScaleByColKey[catAbsKey](colorValue);
            };
        }
        
        return this.scale.by1(function(scene) {
            return scene && scene.vars[varName].value;
        });
    },
    
    _buildOptionId: function(){
        return this.id + "Axis";
    },
    
    _getOptionsDefinition: function(){
        return colorAxis_optionsDef;
    },
    
    _resolveByNaked: pvc.options.specify(function(optionInfo){
        // The first of the type receives options without the "Axis" suffix.
        if(!this.index){
            return this._chartOption(this.id + def.firstUpperCase(optionInfo.name));
        }
    }),
    
    _specifyV1ChartOption: function(optionInfo, asName){
        if(!this.index &&
            this.chart.compatVersion() <= 1 && 
            this._specifyChartOption(optionInfo, asName)){
            return true;
        }
    }
});

/* PRIVATE STUFF */
function colorAxis_castColorMap(colorMap){
    var resultMap;
    if(colorMap){
        var any;
        def.eachOwn(colorMap, function(v, k){
            any = true;
            colorMap[k] = pv.color(v);
        });
        
        if(any){
            resultMap = colorMap;
        }
    }
    
    return resultMap;
}

var colorAxis_legendDataSpec = {
    resolveDefault: function(optionInfo){
        // Naked
        if(!this.index && 
           this._specifyChartOption(optionInfo, def.firstLowerCase(optionInfo.name))){
            return true;
        }
    }
};

var colorAxis_defContColors;

function colorAxis_getDefaultColors(/*optionInfo*/) {
    var colors;
    var scaleType = this.scaleType;
    if(!scaleType) {
        // Axis is unbound
        colors = pvc.createColorScheme();
    } else if(scaleType === 'discrete') {
        if(this.index === 0) {
            // Assumes default pvc scale
            colors = pvc.createColorScheme();
        } else { 
            // Use colors of axes with own colors.
            // Use a color scheme that always returns 
            // the global color scale of the role
            // The following fun ignores passed domain values.
            var me = this;
            colors = function() { return me.chart._getRoleColorScale(me.role.name); };
        }
    } else {
        if(!colorAxis_defContColors) { colorAxis_defContColors = ['red', 'yellow','green'].map(pv.color); }
        colors = colorAxis_defContColors.slice();
    }
    
    return colors;
}


/*global axis_optionsDef:true*/
var colorAxis_optionsDef = def.create(axis_optionsDef, {
    /*
     * colors (special case)
     * colorAxisColors
     * color2AxisColors
     * color3AxisColors
     * 
     * -----
     * secondAxisColor (V1 compatibility)
     */
    Colors: {
        resolve:    '_resolveFull',
        getDefault: colorAxis_getDefaultColors,
        data: {
            resolveV1: function(optionInfo){
                if(this.scaleType === 'discrete'){
                    if(this.index === 0){ 
                        this._specifyChartOption(optionInfo, 'colors');
                    } else if(this.index === 1 && this.chart._allowV1SecondAxis) {
                        this._specifyChartOption(optionInfo, 'secondAxisColor');
                    }
                } else {
                    this._specifyChartOption(optionInfo, 'colorRange');
                }
                
                return true;
            },
            resolveDefault: function(optionInfo){ // after normal resolution
                // Handle naming exceptions
                if(this.index === 0){ 
                   this._specifyChartOption(optionInfo, 'colors');
                }
            }
        },
        cast: pvc.colorScheme
    },
    
    /**
     * For ordinal color scales, a map of keys and their fixed colors.
     * 
     * @example
     * <pre>
     *  {
     *      'Lisbon': 'red',
     *      'London': 'blue'
     *  }
     * </pre>
     */
    Map: {
        resolve: '_resolveFull',
        cast:    colorAxis_castColorMap
    },
    
    /*
     * A function that transforms the colors
     * of the color scheme:
     * pv.Color -> pv.Color
     */
    Transform: {
        resolve: '_resolveFull',
        data: {
            resolveDefault: function(optionInfo){
                var plotList = this._plotList;
                if(plotList.length <= 2){
                    var onlyTrendAndPlot2 = 
                        def
                        .query(plotList)
                        .all(function(plot){
                            var name = plot.name;
                            return (name === 'plot2' || name === 'trend');
                        });
                    
                    if(onlyTrendAndPlot2){
                        optionInfo.defaultValue(pvc.brighterColorTransform);
                        return true;
                    }
                }
            }
        },
        cast: def.fun.to
    },
    
    NormByCategory: {
        resolve: function(optionInfo){
            if(!this.chart._allowColorPerCategory){
                optionInfo.specify(false);
                return true;
            }
            
            return this._resolveFull(optionInfo);
        },
        data: {
            resolveV1: function(optionInfo){
                this._specifyV1ChartOption(optionInfo, 'normPerBaseCategory');
                return true;
            }
        },
        cast:    Boolean,
        value:   false
    },
    
    // ------------
    // Continuous color scale
    ScaleType: {
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                this._specifyV1ChartOption(optionInfo, 'scalingType');
                return true;
            }
        },
        cast:    pvc.parseContinuousColorScaleType,
        value:   'linear'
    },
    
    UseAbs: {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   false
    },
    
    Domain: {
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                this._specifyV1ChartOption(optionInfo, 'colorRangeInterval');
                return true;
            }
        },
        cast: def.array.to
    },
    
    Min: {
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                this._specifyV1ChartOption(optionInfo, 'minColor');
                return true;
            }
        },
        cast: pv.color
    },
    
    Max: {
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                this._specifyV1ChartOption(optionInfo, 'maxColor');
                return true;
            }
        },
        cast: pv.color
    },
    
    Missing: { // Null, in lower case is reserved in JS...
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                this._specifyV1ChartOption(optionInfo, 'nullColor');
                return true;
            }
        },
        cast:  pv.color,
        value: pv.color('lightgray')
    },
    
    Unbound: { // Color to use when color role is unbound (only applies to optional color roles) 
        resolve: '_resolveFull',
        getDefault: function(optionInfo) {
            var scheme = this.option('Colors');
            return scheme().range()[0] || pvc.defaultColor; // J.I.C.?
        },
        cast:  pv.color
    },
    
    // ------------
    
    LegendVisible: {
        resolve: '_resolveFull',
        data:    colorAxis_legendDataSpec,
        cast:    Boolean,
        value:   true
    },
    
    LegendClickMode: {
        resolve: '_resolveFull',
        data:    colorAxis_legendDataSpec,
        cast:    pvc.parseLegendClickMode,
        value:   'togglevisible'
    },
    
    LegendDrawLine: {
        resolve: '_resolveFull',
        data:    colorAxis_legendDataSpec,
        cast:    Boolean,
        value:   false
    },
    
    LegendDrawMarker: {
        resolve: '_resolveFull',
        data:    colorAxis_legendDataSpec,
        cast:    Boolean,
        value:   true
    },
    
    LegendShape: {
        resolve: '_resolveFull',
        data:    colorAxis_legendDataSpec,
        cast:    pvc.parseShape
    }
});

/*global pvc_Axis:true */

/**
 * Initializes a size axis.
 * 
 * @name pvc.visual.SizeAxis
 * 
 * @class Represents an axis that maps sizes to the values of a role.
 * 
 * @extends pvc.visual.Axis
 */
def
.type('pvc.visual.SizeAxis', pvc_Axis)
.init(function(chart, type, index, keyArgs){
    
    // prevent naked resolution of size axis
    keyArgs = def.set(keyArgs, 'byNaked', false);
    
    this.base(chart, type, index, keyArgs);
})
.add(/** @lends pvc.visual.SizeAxis# */{
    _buildOptionId: function(){
        return this.id + "Axis";
    },

    scaleTreatsNullAs: function(){
        return 'min';
    },
    
    scaleUsesAbs: function(){
        return this.option('UseAbs');
    },
    
    setScaleRange: function(range){
        var scale = this.scale;
        scale.min  = range.min;
        scale.max  = range.max;
        scale.size = range.max - range.min;
        
        scale.range(scale.min, scale.max);
        
        if(pvc.debug >= 4){
            pvc.log("Scale: " + pvc.stringify(def.copyOwn(scale)));
        }
        
        return this;
    },
    
    _getOptionsDefinition: function() { return sizeAxis_optionsDef; }
});

/*global axis_optionsDef:true */
var sizeAxis_optionsDef = def.create(axis_optionsDef, {
    /* sizeAxisOriginIsZero
     * Force zero to be part of the domain of the scale to make
     * the scale "proportionally" comparable.
     */
    OriginIsZero: {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   false
    },
    
    FixedMin: {
        resolve: '_resolveFull',
        cast:    pvc.castNumber
    },
    
    FixedMax: {
        resolve: '_resolveFull',
        cast:    pvc.castNumber
    },
    
    UseAbs: {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   false
    }
});

/*global pvc_Size:true, pvc_Sides:true */
    
/**
 * Initializes a legend.
 * 
 * @name pvc.visual.Legend
 * 
 * @class Manages the options of a chart legend.
 * @extends pvc.visual.OptionsBase
 */
def
.type('pvc.visual.Legend', pvc.visual.OptionsBase)
.init(function(chart, type, index, keyArgs){
    // prevent naked resolution of legend
    keyArgs = def.set(keyArgs, 'byNaked', false);
    
    this.base(chart, type, index, keyArgs);
})
.add(/** @lends Legend# */{
    _getOptionsDefinition: function() { return legend_optionsDef; }
});

/* PRIVATE STUFF */
function legend_castSize(size) {
    // Single size or sizeMax (a number or a string)
    // should be interpreted as meaning the orthogonal length.
    
    if(!def.object.is(size)){
        var position = this.option('Position');
        size = new pvc_Size()
            .setSize(size, {
                singleProp: pvc.BasePanel.orthogonalLength[position]
            });
    }
    
    return size;
}

function legend_castAlign(align) {
    var position = this.option('Position');
    return pvc.parseAlign(position, align);
}

/*global axis_optionsDef:true*/
var legend_optionsDef = {
    /* legendPosition */
    Position: {
        resolve: '_resolveFull',
        cast:    pvc.parsePosition,
        value:   'bottom'
    },
    
    /* legendSize,
     * legend2Size 
     */
    Size: {
        resolve: '_resolveFull',
        cast:    legend_castSize
    },
    
    SizeMax: {
        resolve: '_resolveFull',
        cast:    legend_castSize
    },
    
    Align: {
        resolve: '_resolveFull',
        data: {
            resolveDefault: function(optionInfo){
                // Default value of align depends on position
                var position = this.option('Position');
                var align;
                if(position !== 'top' && position !== 'bottom'){
                    align = 'top';
                } else if(this.chart.compatVersion() <= 1) { // centered is better
                    align = 'left';
                }
                
                optionInfo.defaultValue(align);
                return true;
            }
        },
        cast: legend_castAlign
    },
    
    Margins:  {
        resolve: '_resolveFull',
        data: {
            resolveDefault: function(optionInfo){
                // Default value of align depends on position
                // Default value of margins depends on position
                if(this.chart.compatVersion() > 1){
                    var position = this.option('Position');
                    
                    // Set default margins
                    var margins = def.set({}, pvc.BasePanel.oppositeAnchor[position], 5);
                    
                    optionInfo.defaultValue(margins);
                }
                
                return true;
            }
        },
        cast: pvc_Sides.as
    },
    
    Paddings: {
        resolve: '_resolveFull',
        cast:    pvc_Sides.as,
        value:   5
    },
    
    Font: {
        resolve: '_resolveFull',
        cast:    String,
        value:   '10px sans-serif'
    }
};

/**
 * Initializes a legend bullet root scene.
 * 
 * @name pvc.visual.legend.BulletRootScene
 * 
 * @extends pvc.visual.Scene
 * 
 * @constructor
 * @param {pvc.visual.Scene} [parent] The parent scene, if any.
 * @param {object} [keyArgs] Keyword arguments.
 * See {@link pvc.visual.Scene} for supported keyword arguments.
 */
/*global pvc_Sides:true, pvc_Size:true */
def
.type('pvc.visual.legend.BulletRootScene', pvc.visual.Scene)
.init(function(parent, keyArgs){
    
    this.base(parent, keyArgs);
    
    var markerDiam = def.get(keyArgs, 'markerSize', 15);
    var itemPadding = new pvc_Sides(def.get(keyArgs, 'itemPadding', 5))
                          .resolve(markerDiam, markerDiam);
    def.set(this.vars,
        'horizontal',  def.get(keyArgs, 'horizontal', false),
        'font',        def.get(keyArgs, 'font'),
        'markerSize',  markerDiam, // Diameter of bullet/marker zone
        'textMargin',  def.get(keyArgs, 'textMargin', 6),  // Space between marker and text 
        'itemPadding', itemPadding);
})
.add(/** @lends pvc.visual.legend.BulletRootScene# */{
    layout: function(layoutInfo){
        // Any size available?
        var clientSize = layoutInfo.clientSize;
        if(!(clientSize.width > 0 && clientSize.height > 0)){
            return new pvc_Size(0,0);
        }
        
        var desiredClientSize = layoutInfo.desiredClientSize;
        
        // The size of the biggest cell
        var markerDiam  = this.vars.markerSize;
        var textLeft    = markerDiam + this.vars.textMargin;
        var itemPadding = this.vars.itemPadding;
        
        // Names are for legend items when laid out in rows
        var a_width  = this.vars.horizontal ? 'width' : 'height';
        var a_height = pvc.BasePanel.oppositeLength[a_width]; // height or width
        
        var maxRowWidth = desiredClientSize[a_width];
        if(!maxRowWidth || maxRowWidth < 0){
            maxRowWidth = clientSize[a_width]; // row or col
        }
        
        var row;
        var rows = [];
        var contentSize = {width: 0, height: 0};
        
        this.childNodes.forEach(function(groupScene){
            groupScene.childNodes.forEach(layoutItem, this);
        }, this);
        
        // If there's no pending row to commit, there are no rows...
        // No items or just items with no text -> hide
        if(!row){
            return new pvc_Size(0,0);
        }
        
        commitRow(/* isLast */ true);
        
        // In logical "row" naming 
        def.set(this.vars, 
            'rows',     rows,
            'rowCount', row,
            'size',     contentSize);
        
        var isV1Compat = this.compatVersion() <= 1;
        
        // Request used width / all available width (V1)
        var w = isV1Compat ? maxRowWidth : contentSize.width;
        var h = desiredClientSize[a_height];
        if(!h || h < 0){
            h = contentSize.height;
        }
        
        // requestSize
        return def.set({},
            a_width,  Math.min(w, clientSize[a_width]),
            a_height, Math.min(h, clientSize[a_height]));
        
        function layoutItem(itemScene){
            // The names of props  of textSize and itemClientSize 
            // are to be taken literally.
            // This is because items, themselves, are always laid out horizontally...
            var textSize = itemScene.labelTextSize();
            
            var hidden = !textSize || !textSize.width || !textSize.height;
            itemScene.isHidden = hidden;
            if(hidden){
                return;
            }  
            
            // Add small margin to the end of the text eq to 0.5em
            var widthMargin = 0;// (textSize.height / 2);
            
            // not padded size
            var itemClientSize = {
                width:  textLeft + textSize.width + widthMargin,
                height: Math.max(textSize.height, markerDiam)
            };
            
            // -------------
            
            var isFirstInRow;
            if(!row){
                row = new pvc.visual.legend.BulletItemSceneRow(0);
                isFirstInRow = true;
            } else {
                isFirstInRow = !row.items.length;
            }
            
            var newRowWidth = row.size.width + itemClientSize[a_width]; // or bottom
            if(!isFirstInRow){
                newRowWidth += itemPadding[a_width]; // separate from previous item
            }
            
            // If not the first column of a row and the item does not fit
            if(!isFirstInRow && (newRowWidth > maxRowWidth)){
                commitRow(/* isLast */ false);
                
                newRowWidth = itemClientSize[a_width];
            }
            
            // Add item to row
            var rowSize = row.size;
            rowSize.width  = newRowWidth;
            rowSize.height = Math.max(rowSize.height, itemClientSize[a_height]);
            
            var rowItemIndex = row.items.length;
            row.items.push(itemScene);
            
            def.set(itemScene.vars,
                    'row', row, // In logical "row" naming
                    'rowIndex', rowItemIndex, // idem
                    'clientSize', itemClientSize);
        }
        
        function commitRow(isLast){
            var rowSize = row.size;
            contentSize.height += rowSize.height;
            if(rows.length){
                // Separate rows
                contentSize.height += itemPadding[a_height];
            }
            
            contentSize.width = Math.max(contentSize.width, rowSize.width);
            rows.push(row);
            
            // New row
            if(!isLast){
                row = new pvc.visual.legend.BulletItemSceneRow(rows.length);
            }
        }
    },
    
    defaultGroupSceneType: function(){
        var GroupType = this._bulletGroupType;
        if(!GroupType){
            GroupType = def.type(pvc.visual.legend.BulletGroupScene);
            
            // Apply legend group scene extensions
            //this.panel()._extendSceneType('group', GroupType, ['...']);
            
            this._bulletGroupType = GroupType;
        }
        
        return GroupType;
    },
    
    createGroup: function(keyArgs){
        var GroupType = this.defaultGroupSceneType();
        return new GroupType(this, keyArgs);
    }
});

def
.type('pvc.visual.legend.BulletItemSceneRow')
.init(function(index){
    this.index = index;
    this.items = [];
    this.size  = {width: 0, height: 0};
});

/**
 * Initializes a legend bullet group scene.
 * 
 * @name pvc.visual.legend.BulletGroupScene

 * @extends pvc.visual.Scene
 * 
 * @constructor
 * @param {pvc.visual.legend.BulletRootScene} parent The parent bullet root scene.
 * @param {object} [keyArgs] Keyword arguments.
 * See {@link pvc.visual.Scene} for additional keyword arguments.
 * @param {pv.visual.legend.renderer} [keyArgs.renderer] Keyword arguments.
 */
def
.type('pvc.visual.legend.BulletGroupScene', pvc.visual.Scene)
.init(function(rootScene, keyArgs) {
    
    this.base(rootScene, keyArgs);
    
    this.extensionPrefix =  def.get(keyArgs, 'extensionPrefix') || '';
    this._renderer = def.get(keyArgs, 'renderer');
    
    this.colorAxis = def.get(keyArgs, 'colorAxis');
    
    this.clickMode = def.get(keyArgs, 'clickMode');
    if(!this.clickMode && this.colorAxis) {
        this.clickMode = this.colorAxis.option('LegendClickMode');
    }
})
.add(/** @lends pvc.visual.legend.BulletGroupScene# */{
    hasRenderer: function() { return !!this._renderer; },
    
    renderer: function(renderer) {
        if(renderer != null) {
            this._renderer = renderer;
        } else {
            renderer = this._renderer;
            if(!renderer) {
                var keyArgs;
                var colorAxis = this.colorAxis;
                if(colorAxis) {
                    keyArgs = {
                        drawRule:    colorAxis.option('LegendDrawLine'  ),
                        drawMarker:  colorAxis.option('LegendDrawMarker'),
                        markerShape: colorAxis.option('LegendShape')
                    };
                }
                
                renderer = new pvc.visual.legend.BulletItemDefaultRenderer(keyArgs);
                this._renderer = renderer;
            }
        }
        
        return renderer;
    },
    
    itemSceneType: function() {
        var ItemType = this._itemSceneType;
        if(!ItemType) {
            // Inherit, anonymously, from BulletItemScene
            ItemType = def.type(pvc.visual.legend.BulletItemScene);
            
            // Mixin behavior depending on click mode
            var clickMode = this.clickMode;
            switch(clickMode) {
                case 'toggleselected':
                    ItemType.add(pvc.visual.legend.BulletItemSceneSelection);
                    break;
                
                case 'togglevisible':
                    ItemType.add(pvc.visual.legend.BulletItemSceneVisibility);
                    break;
            }
            
            var legendPanel = this.panel();
            
            // Apply legend item scene extensions
            legendPanel._extendSceneType('item', ItemType, ['isOn', 'executable', 'execute', 'value']);
            
            // Apply legend item scene Vars extensions
            // extensionPrefix contains "", "2", "3", "trend"
            // -> "legendItemScene", "legend$ItemScene", or
            // -> "legend2ItemScene", "legend$ItemScene", or
            var itemSceneExtIds = pvc.makeExtensionAbsId(
                    pvc.makeExtensionAbsId("ItemScene", [this.extensionPrefix, '$']),
                    legendPanel._getExtensionPrefix());
            
            var impl = legendPanel.chart._getExtension(itemSceneExtIds, 'value');
            if(impl !== undefined) {
                ItemType.prototype.variable('value', impl);
            }
            
            this._itemSceneType = ItemType;
        }
        
        return ItemType;
    },
    
    createItem: function(keyArgs) {
        var ItemType = this.itemSceneType();
        return new ItemType(this, keyArgs);
    }
});

/*global pvc_ValueLabelVar:true */

/**
 * Initializes a legend bullet item scene.
 * 
 * @name pvc.visual.legend.BulletItemScene
 * 
 * @extends pvc.visual.legend.Scene
 * 
 * @constructor
 * @param {pvc.visual.legend.BulletGroupScene} bulletGroup The parent legend bullet group scene.
 * @param {object} [keyArgs] Keyword arguments.
 * See {@link pvc.visual.Scene} for supported keyword arguments.
 */
def
.type('pvc.visual.legend.BulletItemScene', pvc.visual.Scene)
.init(function(bulletGroup, keyArgs) {
    
    this.base.apply(this, arguments);
    
    if(!this.executable()) {
        // Don't allow default click action
        var I = pvc.visual.Interactive;
        this._ibits = I.Interactive | 
                      I.ShowsInteraction | 
                      I.Hoverable | I.SelectableAny;
    }
})
.add(/** @lends pvc.visual.legend.BulletItemScene# */{
    /**
     * Called during legend render (full or interactive) 
     * to determine if the item is in the "on" state.
     * <p>
     * An item in the "off" state is shown with brighter struck-through text, by default.
     * </p>
     * 
     * <p>
     * The default implementation returns <c>true</c>.
     * </p>
     * 
     * @type boolean
     */
    isOn: def.fun.constant(true),
    
    /**
     * Returns true if the item may be executed. 
     * May be called during construction.
     * @type boolean
     */
    executable: def.fun.constant(false),
    
    /**
     * Called when the user executes the legend item.
     * <p>
     * The default implementation does nothing.
     * </p>
     */
    execute: def.fun.constant(),
    
    /**
     * Measures the item label's text and returns an object
     * with 'width' and 'height' properties, in pixels.
     * @type object
     */
    labelTextSize: function() {
        return pv.Text.measure(this.value().label, this.vars.font);
    },
    
    // Value variable
    // Assumes _value_ variable has not yet been defined, by using "variable".
    // Declaring these methods prevents default _valueEval and _valueEvalCore
    // implementations to be defined.
    _valueEval: function() {
        var valueVar = this._valueEvalCore();
        if(!(valueVar instanceof pvc_ValueLabelVar)) {
            valueVar = new pvc_ValueLabelVar(valueVar, valueVar);
        }
        
        return valueVar;
    },
    
    _valueEvalCore: function() {
        var value, rawValue, label;
        var source = this.group || this.datum;
        if(source) {
            value    = source.value;
            rawValue = source.rawValue;
            label    = source.ensureLabel() + this._getTrendLineSuffix(source);
        }
        
        return new pvc_ValueLabelVar(value || null, label || "", rawValue);
    },
    
    _getTrendLineSuffix: function(source) {
        // TODO: This is to catch trend lines...
        // Normal data source data part values are numbers: 0, 1.
        // Trend data part value is not a number, it is: "trends".
        // Shows the custom trend label after the item's label:
        // ex: 'Lisbon (Linear trend)'  
        var dataPartDim = this.chart()._getDataPartDimName();
        if(dataPartDim) {
            var dataPartAtom = source.atoms[dataPartDim];
            if(isNaN(+dataPartAtom.value)) {
                return " (" + dataPartAtom.label + ")";
            }
        }
        return "";
    }
})
.prototype
.variable('value');


/**
 * @name pvc.visual.legend.BulletItemSceneSelection
 * @class A selection behavior mixin for the legend bullet item scene. 
 * Represents and controls the selected state of its datums.
 * 
 * @extends pvc.visual.legend.BulletItemScene
 */
def
.type('pvc.visual.legend.BulletItemSceneSelection')
.add(/** @lends pvc.visual.legend.BulletItemSceneSelection# */{
    /**
     * Returns <c>true</c> if there are no selected datums in the owner data, 
     * or if at least one datum of the scene's {@link #datums} is selected.
     * @type boolean
     */
    isOn: function() {
        var source = (this.group || this.datum);
        return !source.owner.selectedCount() || this.isSelected();
    },
    
    /**
     * Returns true if the chart is selectable by clicking. 
     * @type boolean
     */
    executable: function() { return this.chart().selectableByClick(); },
    
    /**
     * Toggles the selected state of the datums present in this scene
     * and updates the chart if necessary.
     */
    execute: function() {
        var datums = this.datums().array();
        if(datums.length) {
            var chart = this.chart();
            chart._updatingSelections(function() {
                datums = chart._onUserSelection(datums);
                if(datums && datums.length) {
                    pvc.data.Data.toggleSelected(datums, /*any*/true);
                }
            });
        }
    }
});


/**
 * @name pvc.visual.legend.BulletItemSceneVisibility
 * 
 * @class A visibility behavior mixin for a legend bullet item scene. 
 * Represents and controls the visible state of its datums.
 * 
 * @extends pvc.visual.legend.BulletItemScene
 */
def
.type('pvc.visual.legend.BulletItemSceneVisibility')
.add(/** @lends pvc.visual.legend.BulletItemSceneVisibility# */{
    /**
     * Returns <c>true</c> if at least one non-null datum of the scene's {@link #datums} is visible.
     * @type boolean
     */
    isOn: function() {
        // If null datums were included, as they're always visible,
        // the existence of a single null datum would result in always being true.
        return this.datums().any(function(datum) { return !datum.isNull && datum.isVisible; });
    },
    
    executable: def.fun.constant(true),
    
    /**
     * Toggles the visible state of the datums present in this scene
     * and forces a re-render of the chart (without reloading data).
     * @override
     */
    execute: function() {
        if(pvc.data.Data.toggleVisible(this.datums())) {
            // Re-render chart
            this.chart().render(true, true, false);
        }
    }
});


/**
 * @name pvc.visual.legend.BulletItemRenderer
 * @class Renders bullet items' bullets, i.e. marker, rule, etc.
 */
def.type('pvc.visual.legend.BulletItemRenderer');

/**
 * Creates the marks that render appropriate bullets
 * as children of a given parent bullet panel.
 * <p>
 * The dimensions of this panel, upon each render, 
 * provide bounds for drawing each bullet.
 * </p>
 * <p>
 * The properties of marks created as children of this panel will 
 * receive a corresponding {@link pvc.visual.legend.BulletItemScene} 
 * as first argument. 
 * </p>
 * 
 * @name pvc.visual.legend.BulletItemRenderer#create
 * @function
 * @param {pvc.LegendPanel} legendPanel the legend panel
 * @param {pv.Panel} pvBulletPanel the protovis panel on which bullets are rendered.
 * @param {string} extensionPrefix The extension prefix to be used to build extension keys, without underscore.
 * @param {function} [wrapper] extension wrapper function to apply to created marks.
 * 
 * @type undefined 
 */

/*global pv_Mark:true */

/**
 * Initializes a default legend bullet renderer.
 * 
 * @name pvc.visual.legend.BulletItemDefaultRenderer
 * @class The default bullet renderer.
 * @extends pvc.visual.legend.BulletItemRenderer
 * 
 * @constructor
 * @param {pvc.visual.legend.BulletGroupScene} bulletGroup The parent legend bullet group scene.
 * @param {object} [keyArgs] Optional keyword arguments.
 * @param {string} [keyArgs.drawRule=false] Whether a rule should be drawn.
 * @param {string} [keyArgs.drawMarker=true] Whether a marker should be drawn.
 * When {@link keyArgs.drawRule} is false, then this argument is ignored,
 * because a marker is necessarily drawn.
 * @param {pv.Mark} [keyArgs.markerPvProto] The marker's protovis prototype mark.
 * @param {pv.Mark} [keyArgs.rulePvProto  ] The rule's protovis prototype mark.
 */
def
.type('pvc.visual.legend.BulletItemDefaultRenderer', pvc.visual.legend.BulletItemRenderer)
.init(function(keyArgs) {
    this.drawRule = def.get(keyArgs, 'drawRule', false);
    
    if(this.drawRule) { this.rulePvProto = def.get(keyArgs, 'rulePvProto'); }
    
    this.drawMarker = !this.drawRule || def.get(keyArgs, 'drawMarker', true);
    if(this.drawMarker) {
        this.markerShape   = def.get(keyArgs, 'markerShape', 'square');
        this.markerPvProto = def.get(keyArgs, 'markerPvProto');
    }
})
.add(/** @lends pvc.visual.legend.BulletItemDefaultRenderer# */{
    drawRule: false,
    drawMarker: true,
    markerShape: null,
    rulePvProto: null,
    markerPvProto: null,
    
    create: function(legendPanel, pvBulletPanel, extensionPrefix, wrapper){
        var renderInfo = {};
        var drawRule = this.drawRule;
        var sceneColorProp = function(scene) { return scene.color; };
        
        if(drawRule) {
            var rulePvBaseProto = new pv_Mark()
                .left (0)
                .top  (function() { return this.parent.height() / 2; })
                .width(function() { return this.parent.width();      })
                .lineWidth(1, pvc.extensionTag) // act as if it were a user extension
                .strokeStyle(sceneColorProp, pvc.extensionTag); // idem
            
            var rp = this.rulePvProto;
            if(rp) { rulePvBaseProto = rp.extend(rulePvBaseProto); }
            
            renderInfo.pvRule = new pvc.visual.Rule(legendPanel, pvBulletPanel, {
                    proto: rulePvBaseProto,
                    noSelect: false,
                    noHover:  false,
                    activeSeriesAware: false,// no guarantee that series exist in the scene
                    extensionId: extensionPrefix + "Rule",
                    showsInteraction: true,
                    wrapper: wrapper
                })
                .pvMark;
        }
        
        if(this.drawMarker){
            var markerPvBaseProto = new pv_Mark()
                // Center the marker in the panel
                .left(function() { return this.parent.width () / 2; })
                .top (function() { return this.parent.height() / 2; })
                // If order of properties is changed, by extension, 
                // dependent properties will not work...
                .shapeSize(function() { return this.parent.width(); }, pvc.extensionTag) // width <= height
                .lineWidth(2, pvc.extensionTag)
                .fillStyle(sceneColorProp, pvc.extensionTag)
                .strokeStyle(sceneColorProp, pvc.extensionTag)
                .shape(this.markerShape, pvc.extensionTag)
                .angle(drawRule ? 0 : Math.PI/2, pvc.extensionTag) // So that 'bar' gets drawn vertically
                .antialias(function() {
                    var cos = Math.abs(Math.cos(this.angle()));
                    if(cos !== 0 && cos !== 1) {
                        switch(this.shape()) { case 'square': case 'bar': return false; }
                    }
                    
                    return true;
                }, pvc.extensionTag);
            
            var mp = this.markerPvProto;
            if(mp) { markerPvBaseProto = mp.extend(markerPvBaseProto); }
            
            renderInfo.pvDot = new pvc.visual.Dot(legendPanel, pvBulletPanel, {
                    proto:         markerPvBaseProto,
                    freePosition:  true,
                    activeSeriesAware: false, // no guarantee that series exist in the scene
                    noTooltip:     true,
                    noClick:       true, //otherwise the legend panel handles it and triggers the default action (visibility change)
                    extensionId:   extensionPrefix + "Dot",
                    wrapper:       wrapper
                })
                .pvMark;
        }
        
        return renderInfo;
    }
});


/**
 * Initializes a DataCell instance.
 *
 * @name pvc.visual.DataCell
 * @class Describes data requirements of a plot
 *        in terms of a role, given its name, 
 *        a data part value and 
 *        an axis, given its type and index.
 * 
 * @constructor
 * @param {any} value The value of the variable.
 * @param {any} label The label of the variable.
 * @param {any} [rawValue] The raw value of the variable.
 */
def
.type('pvc.visual.DataCell')
.init(function(plot, axisType, axisIndex, roleName, dataPartValue){
    this.plot = plot;
    this.axisType = axisType;
    this.axisIndex = axisIndex;
    this.role = plot.chart.visualRoles[roleName];
    this.dataPartValue = dataPartValue;
})
.add({
    isBound: function() { return this.role && this.role.isBound(); },
    
    domainData: function() { return def.lazy(this, '_domainData', this._resolveDomainData, this); },
    
    // TODO: should this logic be specified in the role itself?
    // Not cached, because sometimes domainData items may not be available,
    // due to trends and multi-charts...
    domainItemDatas: function() {
        var domainData = this.domainData();
        return def.query((domainData || undefined) && domainData.children());
    },
    
    // TODO: should this logic be specified in the role itself?
    // The item value function
    domainItemDataValue: function(itemData) {  return def.nullyTo(itemData.value, ''); },
    
    domainItemValues: function() {
        return this.domainItemDatas().select(this.domainItemDataValue, this).distinct();
    },
    
    _resolveDomainData: function() {
        var role = this.role;
        if(role && role.isBound()) {
            var partData = this.plot.chart.partData(this.dataPartValue);
            if(partData) { return role.flatten(partData); }
        }
        
        return null;
    }
});


/**
 * Initializes a plot.
 * 
 * @name pvc.visual.Plot
 * @class Represents a plot.
 * @extends pvc.visual.OptionsBase
 */
def
.type('pvc.visual.Plot', pvc.visual.OptionsBase)
.init(function(chart, keyArgs){
    // Peek plot type-index
    var typePlots = def.getPath(chart, ['plotsByType', this.type]);
    var index = typePlots ? typePlots.length : 0;
    
    // Elements of the first plot (of any type)
    // can be accessed without prefix.
    // Peek chart's plotList (globalIndex is only set afterwards in addPlot)
    var globalIndex = chart.plotList.length;
    keyArgs = def.set(keyArgs, 'byNaked', !globalIndex);
    
    this.base(chart, this.type, index, keyArgs);
    
    // fills globalIndex
    chart._addPlot(this);
    
    // -------------
    
    // Last prefix has more precedence.
    
    // The plot id is always a valid prefix (type+index)
    var prefixes = this.extensionPrefixes = [this.id];
    
    if(!this.globalIndex){
        // Elements of the first plot (of any type)
        // can be accessed without prefix
        prefixes.push('');
    }
    
    // The plot name, if any is always a valid prefix (name)
    if(this.name){
        prefixes.push(this.name);
    }
})
.add({
    // Override
    _getOptionsDefinition: function() { return pvc.visual.Plot.optionsDef; },
    
    // Override
    _resolveByNaked: pvc.options.specify(function(optionInfo){
        if(!this.globalIndex){
            return this._chartOption(def.firstLowerCase(optionInfo.name));
        }
    }),
    
    collectDataCells: function(dataCells) {
        var dataCell = this._getColorDataCell();
        if(dataCell) {
            dataCells.push(dataCell);
        }
    },
    
    _getColorDataCell: function(){
        var colorRoleName = this.option('ColorRole');
        if(colorRoleName) {
            return new pvc.visual.DataCell(
                    this,
                    /*axisType*/ 'color',
                    this.option('ColorAxis') - 1, 
                    colorRoleName, 
                    this.option('DataPart'));
        }
    }
});

pvc.visual.Plot.optionsDef = {
    // Box model options?
        
    Orientation: {
        resolve: function(optionInfo){
            optionInfo.specify(this._chartOption('orientation') || 'vertical');
            return true;
        },
        cast: String
    },
    
    ValuesVisible: {
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                if(this.globalIndex === 0){
                    var show = this._chartOption('showValues');
                    if(show !== undefined){
                        optionInfo.specify(show);
                    } else {
                        show = this.type !== 'point';
                        optionInfo.defaultValue(show);
                    }
                    
                    return true;
                }
            }
        },
        cast:  Boolean,
        value: false
    },
    
    ValuesAnchor: {
        resolve: '_resolveFull',
        cast:    pvc.parseAnchor
    },
    
    ValuesFont: {
        resolve: '_resolveFull',
        cast:    String,
        value:   '10px sans-serif'
    },
    
    // Each plot type must provide an appropriate default mask
    // depending on its scene variable names
    ValuesMask: {
        resolve: '_resolveFull',
        cast:    String,
        value:   "{value}"
    },
    
    ValuesOptimizeLegibility: {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   false
    },
    
    DataPart: {
        resolve: '_resolveFixed',
        cast: String,
        value:   '0'
    },
    
    // ---------------
    
    ColorRole: {
        resolve: '_resolveFixed',
        cast:    String,
        value:   'color'
    },
    
    ColorAxis: {
        resolve: pvc.options.resolvers([
            function(optionInfo){
                if(this.globalIndex === 0){
                    // plot0 must use color axis 0!
                    // This also ensures that the color axis 0 is created...
                    optionInfo.specify(1);
                    return true;
                }
            },
            '_resolveFull'
        ]),
        cast:  function(value){
            value = pvc.castNumber(value);
            if(value != null){
                value = def.between(value, 1, 10);
            } else {
                value = 1;
            }
            
            return value;
        },
        value: 1
    }
};

/**
 * Initializes an abstract cartesian plot.
 * 
 * @name pvc.visual.CartesianPlot
 * @class Represents an abstract cartesian plot.
 * @extends pvc.visual.Plot
 */
def
.type('pvc.visual.CartesianPlot', pvc.visual.Plot)
.add({
    _getOptionsDefinition: function(){
        return pvc.visual.CartesianPlot.optionsDef;
    }
});

function pvc_castTrend(trend){
    // The trend plot itself does not have trends...
    if(this.name === 'trend'){
        return null;
    }
    
    var type = this.option('TrendType');
    if(!type && trend){
        type = trend.type;
    }
    
    if(!type || type === 'none'){
        return null;
    }
    
    if(!trend){
        trend = {};
    } else {
        trend = Object.create(trend);
    }
    
    trend.type = type;
   
    var label = this.option('TrendLabel');
    if(label !== undefined){
        trend.label = label;
    }
    
    return trend;
}

pvc.visual.CartesianPlot.optionsDef = def.create(
    pvc.visual.Plot.optionsDef, {
        BaseAxis: {
            value: 1
        },
        
        BaseRole: {
            resolve: '_resolveFixed',
            cast:    String
        },
        
        OrthoAxis: {
            resolve: function(optionInfo){
                if(this.globalIndex === 0){
                    // plot0 must use ortho axis 0!
                    // This also ensures that the ortho axis 0 is created...
                    optionInfo.specify(1);
                    return true;
                }
                
                return this._resolveFull(optionInfo);
            },
            data: {
                resolveV1: function(optionInfo){
                    if(this.name === 'plot2' &&
                        this.chart._allowV1SecondAxis &&
                        this._chartOption('secondAxisIndependentScale')){
                         optionInfo.specify(2);
                    }
                    return true;
                }
            },
            cast: function(value){
                value = pvc.castNumber(value);
                if(value != null){
                    value = def.between(value, 1, 10);
                } else {
                    value = 1;
                }
                
                return value;
            },
            value: 1
        },
        
        OrthoRole: {
            resolve: pvc.options.resolvers([
                  '_resolveFixed',
                  '_resolveDefault'
                ])
            // String or string array
        },
        
        Trend: {
            resolve: '_resolveFull',
            data: {
                resolveDefault: function(optionInfo){
                    var type = this.option('TrendType');
                    if(type){
                        // Cast handles the rest
                        optionInfo.defaultValue({
                            type: type
                        });
                        return true;
                    }
                }
            },
            cast: pvc_castTrend
        },
        
        TrendType: {
            resolve: '_resolveFull',
            cast:    pvc.parseTrendType
            //value:   'none'
        },
        
        TrendLabel: {
            resolve: '_resolveFull',
            cast:    String
        },
        
        NullInterpolationMode: {
            resolve: '_resolveFull',
            cast:    pvc.parseNullInterpolationMode,
            value:   'none' 
        }
    });

/**
 * Initializes an abstract categorical plot.
 * 
 * @name pvc.visual.CategoricalPlot
 * @class Represents an abstract categorical plot.
 * @extends pvc.visual.CartesianPlot
 */
def
.type('pvc.visual.CategoricalPlot', pvc.visual.CartesianPlot)
.add({
    _getOptionsDefinition: function(){
        return pvc.visual.CategoricalPlot.optionsDef;
    }
});

pvc.visual.CategoricalPlot.optionsDef = def.create(
    pvc.visual.CartesianPlot.optionsDef, {
    
    Stacked: {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   false
    },
    
    BaseRole: {
        value: 'category'
    },
    
    OrthoRole: { // override 
        value: 'value'
    }
});


/**
 * Initializes an abstract bar plot.
 * 
 * @name pvc.visual.BarPlotAbstract
 * @class Represents an abstract bar plot.
 * @extends pvc.visual.CategoricalPlot
 */
def
.type('pvc.visual.BarPlotAbstract', pvc.visual.CategoricalPlot)
.add({
    _getOptionsDefinition: function(){
        return pvc.visual.BarPlotAbstract.optionsDef;
    }
});

pvc.visual.BarPlotAbstract.optionsDef = def.create(
    pvc.visual.CategoricalPlot.optionsDef, {
    
    BarSizeRatio: { // for grouped bars
        resolve: '_resolveFull',
        cast: function(value){
            value = pvc.castNumber(value);
            if(value == null){
                value = 1;
            } else if(value < 0.05){
                value = 0.05;
            } else if(value > 1){
                value = 1;
            }
            
            return value;
        },
        value: 0.9
    },
    
    BarSizeMax: {
        resolve: '_resolveFull',
        data: {
            resolveV1: function(optionInfo){
                // default to v1 option
                this._specifyChartOption(optionInfo, 'maxBarSize');
                return true;
            }
        },
        cast: function(value){
            value = pvc.castNumber(value);
            if(value == null){
                value = Infinity;
            } else if(value < 1){
                value = 1;
            }
            
            return value;
        },
        value: 2000
    },
    
    BarStackedMargin: { // for stacked bars
        resolve: '_resolveFull',
        cast: function(value){
            value = pvc.castNumber(value);
            if(value != null && value < 0){
                value = 0;
            }
            
            return value;
        },
        value: 0
    },
    
    OverflowMarkersVisible: {
        resolve: '_resolveFull',
        cast:    Boolean,
        value:   true
    },
    
    ValuesAnchor: { // override default value only
        value: 'center'
    }
});

/**
 * Initializes a bar plot.
 * 
 * @name pvc.visual.BarPlot
 * @class Represents a bar plot.
 * @extends pvc.visual.BarPlotAbstract
 */
def
.type('pvc.visual.BarPlot', pvc.visual.BarPlotAbstract)
.add({
    type: 'bar'
});

/**
 * Initializes a normalized bar plot.
 * 
 * @name pvc.visual.NormalizedBarPlot
 * @class Represents a normalized bar plot.
 * @extends pvc.visual.BarPlotAbstract
 */
def
.type('pvc.visual.NormalizedBarPlot', pvc.visual.BarPlotAbstract)
.add({
    type: 'bar',
    _getOptionsDefinition: function() { return pvc.visual.NormalizedBarPlot.optionsDef; }
});

pvc.visual.NormalizedBarPlot.optionsDef = def.create(
    pvc.visual.BarPlotAbstract.optionsDef, 
    {
        Stacked: {
            resolve: null, 
            value: true
        }
    });


/**
 * Initializes a waterfall plot.
 * 
 * @name pvc.visual.WaterfallPlot
 * @class Represents a waterfall plot.
 * @extends pvc.visual.BarPlotAbstract
 */
def
.type('pvc.visual.WaterfallPlot', pvc.visual.BarPlotAbstract)
.add({
    type: 'water',
    
    _getOptionsDefinition: function() { return pvc.visual.WaterfallPlot.optionsDef; }
});

pvc.visual.WaterfallPlot.optionsDef = def.create(
    pvc.visual.BarPlotAbstract.optionsDef, 
    {
        Stacked: { // override
            resolve: null, 
            value:   true
        },
        
        TotalLineLabel: {
            resolve: '_resolveFull',
            cast:    String,
            value:   "Accumulated"
        },
        
        TotalValuesVisible: { 
            resolve: '_resolveFull',
            data: {
                // Dynamic default
                resolveDefault: function(optionInfo){
                    optionInfo.defaultValue(this.option('ValuesVisible'));
                    return true;
                }
            },
            cast:    Boolean
        },
        
        Direction: { // up/down
            resolve: '_resolveFull',
            cast:    pvc.parseWaterDirection,
            value:   'down'
        },
        
        AreasVisible: {
            resolve: '_resolveFull',
            cast:    Boolean,
            value:   true
        },
        
        AllCategoryLabel: {
            resolve: '_resolveFull',
            cast:    String,
            value:   "All"
        }
    });

/**
 * Initializes a point plot.
 * 
 * @name pvc.visual.PointPlot
 * @class Represents a Point plot.
 * @extends pvc.visual.CategoricalPlot
 */
def
.type('pvc.visual.PointPlot', pvc.visual.CategoricalPlot)
.add({
    type: 'point',
    _getOptionsDefinition: function(){
        return pvc.visual.PointPlot.optionsDef;
    }
});

function pvcPoint_buildVisibleOption(type, dv){
    return {
        resolveV1: function(optionInfo) {
            if(this.globalIndex === 0) {
                if(!this._specifyChartOption(optionInfo, 'show' + type)) {
                    optionInfo.defaultValue(dv);
                }
                return true;
            }
        }
    };
}

pvc.visual.PointPlot.optionsDef = def.create(
    pvc.visual.CategoricalPlot.optionsDef, {
        DotsVisible: {
            resolve: '_resolveFull',
            data:    pvcPoint_buildVisibleOption('Dots', true),
            cast:    Boolean,
            value:   false
        },
        
        LinesVisible: {
            resolve: '_resolveFull',
            data:    pvcPoint_buildVisibleOption('Lines', true),
            cast:    Boolean,
            value:   false
        },
        
        AreasVisible: {
            resolve: '_resolveFull',
            data:    pvcPoint_buildVisibleOption('Areas', false),
            cast:    Boolean,
            value:   false
        },
        
        ValuesAnchor: { // override
            value: 'right'
        }
    });

/**
 * Initializes an abstract metric XY plot.
 * 
 * @name pvc.visual.MetricXYPlot
 * @class Represents an abstract metric XY plot.
 * @extends pvc.visual.CartesianPlot
 */
def
.type('pvc.visual.MetricXYPlot', pvc.visual.CartesianPlot)
.add({
    _getOptionsDefinition: function() { return pvc.visual.MetricXYPlot.optionsDef; }
});

pvc.visual.MetricXYPlot.optionsDef = def.create(
    pvc.visual.CartesianPlot.optionsDef, {
        BaseRole: { // override
            value:   'x'
        },
        
        OrthoAxis: { // override -> value 1
            resolve: null
        },
        
        OrthoRole: {
            value: 'y'
        }
    });

/**
 * Initializes a metric XY plot.
 * 
 * @name pvc.visual.MetricPointPlot
 * @class Represents a metric point plot.
 * @extends pvc.visual.MetricXYPlot
 */
def
.type('pvc.visual.MetricPointPlot', pvc.visual.MetricXYPlot)
.add({
    type: 'scatter',
    _getOptionsDefinition: function() { return pvc.visual.MetricPointPlot.optionsDef; }
});

function pvcMetricPoint_buildVisibleOption(type) {
    return {
        resolveV1: function(optionInfo) {
            this._specifyChartOption(optionInfo, 'show' + type);
            return true;
        }
    };
}

pvc.visual.MetricPointPlot.optionsDef = def.create(
    pvc.visual.MetricXYPlot.optionsDef, {
        SizeRole: {
            resolve: '_resolveFixed',
            value: 'size'
        },
        
        SizeAxis: {
            resolve: '_resolveFixed',
            value: 1
        },
        
        Shape: {
            resolve: '_resolveFull',
            cast:    pvc.parseShape,
            value:   'circle'
        },

        NullShape: {
            resolve: '_resolveFull',
            cast:    pvc.parseShape,
            value:   'cross'
        },

        DotsVisible: {
            resolve: '_resolveFull',
            data:    pvcMetricPoint_buildVisibleOption('Dots'),
            cast:    Boolean,
            value:   false
        },
        
        LinesVisible: {
            resolve: '_resolveFull',
            data:    pvcMetricPoint_buildVisibleOption('Lines'),
            cast:    Boolean,
            value:   false
        },
        
        ValuesAnchor: { // override
            value: 'right'
        },

        ValuesMask: {
            value: "{x},{y}"
        }
    });

/*global pvc_PercentValue:true */

/**
 * Initializes a pie plot.
 * 
 * @name pvc.visual.PiePlot
 * @class Represents a pie plot.
 * @extends pvc.visual.Plot
 */
def
.type('pvc.visual.PiePlot', pvc.visual.Plot)
.add({
    type: 'pie',
    
    _getOptionsDefinition: function() { return pvc.visual.PiePlot.optionsDef; }
});

pvc.visual.PiePlot.optionsDef = def.create(
    pvc.visual.Plot.optionsDef, {
        ActiveSliceRadius: {
            resolve: '_resolveFull',
            cast:    pvc_PercentValue.parse,
            value:   new pvc_PercentValue(0.05)
        },
        
        ExplodedSliceRadius: {
            resolve: '_resolveFull',
            cast:    pvc_PercentValue.parse,
            value:   0
        },
        
        ExplodedSliceIndex:  {
            resolve: '_resolveFull',
            cast:    pvc.castNumber,
            value:   null // all exploded when radius > 0
        },
        
        ValuesAnchor: { // override
            cast:  pvc.parseAnchorWedge,
            value: 'outer'
        },
        
        ValuesVisible: { // override
            value: true
        },
        
        ValuesLabelStyle: {
            resolve: function(optionInfo){
                var isV1Compat = this.chart.compatVersion() <= 1;
                if(isV1Compat){
                    optionInfo.specify('inside');
                    return true;
                }
                
                return this._resolveFull(optionInfo);
            },
            cast: function(value) {
                switch(value){
                    case 'inside':
                    case 'linked': return value;
                }
                
                if(pvc.debug >= 2){
                    pvc.log("[Warning] Invalid 'ValuesLabelStyle' value: '" + value + "'.");
                }
                
                return 'linked';
            },
            value: 'linked'
        },
        
        // Depends on being linked or not
        // Examples:
        // "{value} ({value.percent}) {category}"
        // "{value}"
        // "{value} ({value.percent})"
        // "{#productId}" // Atom name
        ValuesMask: { // OVERRIDE
            resolve: '_resolveFull',
            data: {
                resolveDefault: function(optionInfo){
                    optionInfo.defaultValue(
                            this.option('ValuesLabelStyle') === 'linked' ? 
                            "{value} ({value.percent})" : 
                            "{value}");
                    return true;
                }
            }
        },
        
        /* Linked Label Style
         *                                         
         *     (| elbowX)                         (| anchorX)
         *      +----------------------------------+          (<-- baseY)
         *      |                                    \
         *      |   (link outset)                      \ (targetX,Y)
         *      |                                        +----+ label
         *    -----  <-- current outer radius      |<-------->|<------------>            
         *      |   (link inset)                     (margin)   (label size)
         *      
         */
        
        /**
         * Percentage of the client radius that the 
         * link is inset in a slice.
         */
        LinkInsetRadius:  {
            resolve: '_resolveFull',
            cast:    pvc_PercentValue.parse,
            value:   new pvc_PercentValue(0.05)
        },
        
        /**
         * Percentage of the client radius that the 
         * link extends outwards from the slice, 
         * until it reaches the link "elbow".
         */
        LinkOutsetRadius: {
            resolve: '_resolveFull',
            cast:    pvc_PercentValue.parse,
            value:   new pvc_PercentValue(0.025)
        },
        
        /**
         * Percentage of the client width that separates 
         * a link label from the link's anchor point.
         * <p>
         * Determines the width of the link segment that 
         * connects the "anchor" point with the "target" point.
         * Includes the space for the small handle at the end.
         * </p>
         */
        LinkMargin: {
            resolve: '_resolveFull',
            cast:    pvc_PercentValue.parse,
            value:   new pvc_PercentValue(0.025)
        },
        
        /**
         * Link handle width, in em units.
         */
        LinkHandleWidth: {
            resolve: '_resolveFull',
            cast:    pvc.castNumber,
            value:   0.5
        },
        
        /**
         * Percentage of the client width that is reserved 
         * for labels on each of the sides.
         */
        LinkLabelSize: {
            resolve: '_resolveFull',
            cast:    pvc_PercentValue.parse,
            value:   new pvc_PercentValue(0.15)
        },
        
        /**
         * Minimum vertical space that separates consecutive link labels, 
         * in em units.
         */
        LinkLabelSpacingMin: {
            resolve: '_resolveFull',
            cast:    pvc.castNumber,
            value:   0.5
        }
    });

/**
 * Initializes a heat grid plot.
 * 
 * @name pvc.visual.HeatGridPlot
 * @class Represents a heat grid plot.
 * @extends pvc.visual.CategoricalPlot
 */
def
.type('pvc.visual.HeatGridPlot', pvc.visual.CategoricalPlot)
.add({
    type: 'heatGrid',
    _getOptionsDefinition: function(){
        return pvc.visual.HeatGridPlot.optionsDef;
    }
});

pvc.visual.HeatGridPlot.optionsDef = def.create(
    pvc.visual.CategoricalPlot.optionsDef, 
    {
        SizeRole: {
            value: 'size'
        },
        
        SizeAxis: {
            value: 1
        },
        
        UseShapes: {
            resolve: '_resolveFull',
            cast:    Boolean,
            value:   false
        },
        
        Shape: {
            resolve: '_resolveFull',
            cast:    pvc.parseShape,
            value:   'square'
        },
        
        NullShape: {
            resolve: '_resolveFull',
            cast:    pvc.parseShape,
            value:   'cross'
        },
        
        ValuesVisible: { // override
            getDefault: function(/*optionInfo*/){
                // Values do not work very well when UseShapes
                return !this.option('UseShapes');
            },
            value: null // clear inherited default value
        },

        ValuesMask: { // override, dynamic default
            value: null
        },

        ValuesAnchor: { // override default value only
            value: 'center'
        },

        OrthoRole: { // override
            value: 'series'
        },
        
        OrthoAxis: { // override
            resolve: null
        },
        
        // Not supported
        NullInterpolationMode: {
            resolve: null,
            value: 'none'
        },
        
        // Not supported
        Stacked: { // override
            resolve: null, 
            value: false
        }
    });

/**
 * Initializes a box plot.
 * 
 * @name pvc.visual.BoxPlot
 * @class Represents a box plot.
 * @extends pvc.visual.CategoricalPlot
 */
def
.type('pvc.visual.BoxPlot', pvc.visual.CategoricalPlot)
.add({
    type: 'box',
    
    _getOptionsDefinition: function(){
        return pvc.visual.BoxPlot.optionsDef;
    }
});

pvc.visual.BoxPlot.optionsDef = def.create(
    pvc.visual.CategoricalPlot.optionsDef, 
    {
        // NO Values Label!
        
        Stacked: {
            resolve: null,
            value:   false
        },
        
        OrthoRole: {
            value:   ['median', 'lowerQuartil', 'upperQuartil', 'minimum', 'maximum'] // content of pvc.BoxplotChart.measureRolesNames
        },
        
        BoxSizeRatio: {
            resolve: '_resolveFull',
            cast: function(value){
                value = pvc.castNumber(value);
                if(value == null){
                    value = 1;
                } else if(value < 0.05){
                    value = 0.05;
                } else if(value > 1){
                    value = 1;
                }
                
                return value;
            },
            value: 1/3
        },
        
        BoxSizeMax: {
            resolve: '_resolveFull',
            data: {
                resolveV1: function(optionInfo){
                    // default to v1 option
                    this._specifyChartOption(optionInfo, 'maxBoxSize');
                    return true;
                }
            },
            cast: function(value){
                value = pvc.castNumber(value);
                if(value == null){
                    value = Infinity;
                } else if(value < 1){
                    value = 1;
                }
                
                return value;
            },
            value: Infinity
        }
    });

/**
 * Initializes a bullet plot.
 * 
 * @name pvc.visual.BulletPlot
 * @class Represents a bullet plot.
 * @extends pvc.visual.Plot
 */
def
.type('pvc.visual.BulletPlot', pvc.visual.Plot)
.add({
    type: 'bullet',
    
    _getOptionsDefinition: function(){
        return pvc.visual.BulletPlot.optionsDef;
    }
});

pvc.visual.BulletPlot.optionsDef = def.create(
    pvc.visual.Plot.optionsDef, {
        ValuesVisible: { // override
            value: true
        },
        
        ColorRole: {
            value: null
        }
    });

def
.type('pvc.visual.TreemapColorDataCell', pvc.visual.DataCell)
.init(function(){
    
    this.base.apply(this, arguments);
    
    var g = this.role.grouping;
    this._valueProp = (!g || g.isSingleDimension) ? 'value' : 'absKey';
})
.add({
    domainItemDatas: function() {
        var domainData = this.domainData();
        var candidates = def.query((domainData || undefined) && domainData.nodes());
        
        if(this.plot.option('ColorMode') === 'byparent') {
            return candidates
                .where(function(itemData) {
                    // The hoverable effect needs colors assigned to parents,
                    // in the middle of the hierarchy,
                    // whose color possibly does not show in normal mode,
                    // cause they have no leaf child (or degenerate child)
                    
                    // the root or a non-degenerate child
                    return (!itemData.parent || itemData.value != null) &&
                        
                        // has at least one 
                        itemData
                        .children()
                        .any(function(child) {
                            // non-degenerate leaf-child
                            return child.value != null && 
                                   child.children().prop('value').all(def.nully);
                        });
                 });
        }
        
        return candidates.where(function(itemData) {
            // Is the single node (root and leaf) Or
            // Is a non-degenerate leaf node Or 
            // Is the last non-degenerate node, from the root, along a branch
            
            // Leaf node
            if(!itemData.childCount()) {
                // Single (root) || non-degenerate
                return !itemData.parent || itemData.value != null;
            }
            
            return itemData.value != null && 
                   !itemData.children().prop('value').any(def.notNully);
        });
    },
    
    domainItemDataValue: function(itemData) { return def.nullyTo(itemData[this._valueProp], ''); },
    
    _resolveDomainData: function() {
        var role = this.role;
        if(role && role.isBound()) {
            var partData = this.plot.chart.partData(this.dataPartValue);
            if(partData){
                return role.select(partData);
            }
        }
        
        return null;
    }
});


/**
 * Initializes a treemap plot.
 * 
 * @name pvc.visual.TreemapPlot
 * @class Represents a treemap plot.
 * @extends pvc.visual.Plot
 */
def
.type('pvc.visual.TreemapPlot', pvc.visual.Plot)
.add({
    type: 'treemap',
    
    _getOptionsDefinition: function() { return pvc.visual.TreemapPlot.optionsDef; },
    
    collectDataCells: function(dataCells) {
        
        this.base(dataCells);
        
        // Add Size DataCell
        var sizeRoleName = this.option('SizeRole');
        if(sizeRoleName) {
            dataCells.push(new pvc.visual.DataCell(
                    this,
                    /*axisType*/ 'size', 
                    this.option('SizeAxis') - 1, 
                    sizeRoleName, 
                    this.option('DataPart')));
        }
    },
    
    /** @override */
    _getColorDataCell: function() {
        var colorRoleName = this.option('ColorRole');
        if(colorRoleName) {
            return new pvc.visual.TreemapColorDataCell(
                    this,
                    /*axisType*/ 'color',
                    this.option('ColorAxis') - 1, 
                    colorRoleName, 
                    this.option('DataPart'));
        }
    }
});

pvc.visual.TreemapPlot.optionsDef = def.create(
    pvc.visual.Plot.optionsDef, {
        SizeRole: {
            resolve: '_resolveFixed',
            value:   'size'
        },
        
        SizeAxis: {
            resolve: '_resolveFixed',
            value:   1
        },
        
        ValuesAnchor: { // NOT USED
            cast:  pvc.parseAnchor,
            value: 'center'
        },
        
        ValuesVisible: { // OVERRIDE
            value: true
        },

        ValuesMask: { // OVERRIDE
            resolve: '_resolveFull',
            value:   "{category}"
        },
        
        ValuesOptimizeLegibility: { // OVERRIDE
            value: true
        },
        
        // Treemap specifc
        LayoutMode: {
            resolve: '_resolveFull',
            cast:  pvc.parseTreemapLayoutMode,
            value: 'squarify'
        },
        
        ColorMode: {
            resolve: '_resolveFull',
            cast: pvc.parseTreemapColorMode,
            value: 'byparent'
        },
        
        RootCategoryLabel: {
            resolve: '_resolveFull',
            cast: String,
            value: "All"
        }
    });

def
.type('pvc.Abstract')
.init(function(){
    this._syncLog();
})
.add({
    invisibleLineWidth: 0.001,
    defaultLineWidth:   1.5,
    
    _logInstanceId: null,
    
    _syncLog: function(){
        if (pvc.debug && typeof console !== "undefined"){
            var logId = this._getLogInstanceId();
            
            ['log', 'info', ['trace', 'debug'], 'error', 'warn', ['group', 'groupCollapsed'], 'groupEnd'].forEach(function(ps){
                ps = ps instanceof Array ? ps : [ps, ps];
                /*global pvc_installLog:true */
                pvc_installLog(this, '_' + ps[0],  ps[1], logId);
            }, this);
        }
    },

    _getLogInstanceId: function(){
        return this._logInstanceId || 
               (this._logInstanceId = this._processLogInstanceId(this._createLogInstanceId()));
    },
    
    _createLogInstanceId: function(){
        return '' + this.constructor;
    },
    
    _processLogInstanceId: function(logInstanceId){
        var L = 30;
        var s = logInstanceId.substr(0, L);
        if(s.length < L){
            s += def.array.create(L - s.length, ' ').join('');
        }
        
        return "[" + s + "]";
    }
});

def.scope(function(){
    var o = pvc.Abstract.prototype;
    var syncLogHook = function(){ this._syncLog(); };
    
    ['log', 'info', 'trace', 'error', 'warn', 'group', 'groupEnd'].forEach(function(p){
        o['_' + p] = syncLogHook;
    });
});



/*global pvc_Sides:true, pvc_Size:true */
def
.type('pvc.BaseChart', pvc.Abstract)
.add(pvc.visual.Interactive)
.init(function(options) {
    var originalOptions = options;
    
    var parent = this.parent = def.get(options, 'parent') || null;
    if(parent){
        /*jshint expr:true */
        options || def.fail.argumentRequired('options');
    } else {
        options = def.mixin.copy({}, this.defaults, options);
    }

    this.options = options;

    if(parent) {
        this.root = parent.root;
        this.smallColIndex   = options.smallColIndex; // required for the logId msk, setup in base
        this.smallRowIndex   = options.smallRowIndex;
    } else {
        this.root = this;
    }
    
    this.base();
    
    if(pvc.debug >= 3){
        this._info("NEW CHART\n" + pvc.logSeparator.replace(/-/g, '=') + 
                "\n  DebugLevel: " + pvc.debug);
    }
    
    /* DEBUG options */
    if(pvc.debug >= 3 && !parent && originalOptions){
        this._info("OPTIONS:\n", originalOptions);
        if(pvc.debug >= 5){
            // Log also as text, for easy copy paste of options JSON
            this._trace(pvc.stringify(options, {ownOnly: false, funs: true}));
        }
    }
    
    if(parent) { parent._addChild(this); }

    this._constructData(options);
    this._constructVisualRoles(options);
})
.add({
    /**
     * Indicates if the chart has been disposed.
     */
    _disposed: false,

    _animatable: false,

    /**
     * The chart's parent chart.
     * 
     * <p>
     * The root chart has null as the value of its parent property.
     * </p>
     * 
     * @type pvc.BaseChart
     */
    parent: null,
    
    /**
     * The chart's child charts.
     * 
     * @type pvc.BaseChart[]
     */
    children: null,
    
    /**
     * The chart's root chart.
     * 
     * <p>
     * The root chart has itself as the value of the root property.
     * </p>
     * 
     * @type pvc.BaseChart
     */
    root: null,

    /**
     * Indicates if the chart has been pre-rendered.
     * <p>
     * This field is set to <tt>false</tt>
     * at the beginning of the {@link #_preRender} method
     * and set to <tt>true</tt> at the end.
     * </p>
     * <p>
     * When a chart is re-rendered it can, 
     * optionally, also repeat the pre-render phase. 
     * </p>
     * 
     * @type boolean
     */
    isPreRendered: false,

    /**
     * The version value of the current/last creation.
     * 
     * <p>
     * This value is changed on each pre-render of the chart.
     * It can be useful to invalidate cached information that 
     * is only valid for each creation.
     * </p>
     * <p>
     * Version values can be compared using the identity operator <tt>===</tt>.
     * </p>
     * 
     * @type any
     */
    _createVersion: 0,
    
    /**
     * A callback function that is called 
     * when the protovis' panel render is about to start.
     * 
     * <p>
     * Note that this is <i>after</i> the pre-render phase.
     * </p>
     * 
     * <p>
     * The callback is called with no arguments, 
     * but having the chart instance as its context (<tt>this</tt> value). 
     * </p>
     * 
     * @function
     */
    renderCallback: undefined,

    /**
     * Contains the number of pages that a multi-chart contains
     * when rendered with the previous render options.
     * <p>
     * This property is updated after a render of a chart
     * where the visual role "multiChart" is assigned and
     * the option "multiChartPageIndex" has been specified. 
     * </p>
     * 
     * @type number|null
     */
    multiChartPageCount: null,
    
    /**
     * Contains the currently rendered multi-chart page index, 
     * relative to the previous render options.
     * <p>
     * This property is updated after a render of a chart
     * where the visual role "multiChart" is assigned and
     * the <i>option</i> "multiChartPageIndex" has been specified. 
     * </p>
     * 
     * @type number|null
     */
    multiChartPageIndex: null,
    
    left: 0,
    top:  0,
    
    width: null,
    height: null,
    margins:  null,
    paddings: null,
    
    _allowV1SecondAxis: false, 
        
    //------------------
    compatVersion: function(options) { return (options || this.options).compatVersion; },
    
    _createLogInstanceId: function() {
        return "" + 
            this.constructor + this._createLogChildSuffix();
    },
    
    _createLogChildSuffix: function(){
        return this.parent ? 
               (" (" + (this.smallRowIndex + 1) + "," + 
                       (this.smallColIndex + 1) + ")") : 
               "";
    },
    
    _addChild: function(childChart){
        /*jshint expr:true */
        (childChart.parent === this) || def.assert("Not a child of this chart.");
        
        this.children.push(childChart);
    },
    
    /**
     * Building the visualization is made in 2 stages:
     * First, the {@link #_preRender} method prepares and builds 
     * every object that will be used.
     * 
     * Later the {@link #render} method effectively renders.
     */
    _preRender: function(keyArgs) {
        this._preRenderPhase1(keyArgs);
        this._preRenderPhase2(keyArgs);
    },
    
    _preRenderPhase1: function(keyArgs) {
        /* Increment pre-render version to allow for cache invalidation  */
        this._createVersion++;
        
        this.isPreRendered = false;
        
        if(pvc.debug >= 3){
            this._log("Prerendering");
        }
        
        this.children = [];
        
        if (!this.parent) {
            // Now's as good a time as any to completely clear out all
            //  tipsy tooltips
            pvc.removeTipsyLegends();
        }
        
        /* Options may be changed between renders */
        this._processOptions();
        
        /* Any data exists or throws
         * (must be done AFTER processing options
         *  because of width, height properties and noData extension point...) 
         */
        this._checkNoDataI();
        
        /* Initialize root visual roles.
         * The Complex Type gets defined on the first load of data.
         */
        if(!this.parent && !this.data) {
            this._initVisualRoles();
            
            this._bindVisualRolesPreI();
            
            this._complexTypeProj = this._createComplexTypeProject();
            
            this._bindVisualRolesPreII();
        }
        
        /* Initialize the data (and _bindVisualRolesPost) */
        this._initData(keyArgs);

        /* When data is excluded, there may be no data after all */
        this._checkNoDataII();
        
        var hasMultiRole = this.visualRoles.multiChart.isBound();
        
        /* Initialize plots */
        this._initPlots(hasMultiRole);
        
        /* Initialize axes */
        this._initAxes(hasMultiRole);
        this._bindAxes(hasMultiRole);
        
        /* Trends and Interpolation */
        if(this.parent || !hasMultiRole){
            // Interpolated data affects generated trends
            this._interpolate(hasMultiRole);
            
            this._generateTrends(hasMultiRole);
        }
        
        /* Set axes scales */
        this._setAxesScales(hasMultiRole);
    },
    
    _preRenderPhase2: function(/*keyArgs*/){
        var hasMultiRole = this.visualRoles.multiChart.isBound();
        
        /* Initialize chart panels */
        this._initChartPanels(hasMultiRole);
        
        this.isPreRendered = true;
    },

    // --------------
    
    _setSmallLayout: function(keyArgs){
        if(keyArgs){
            var basePanel = this.basePanel;
            
            if(this._setProp('left', keyArgs) | this._setProp('top', keyArgs)){
                if(basePanel) {
                    def.set(
                       basePanel.position,
                       'left', this.left, 
                       'top',  this.top);
                }
            }
            
            if(this._setProp('width', keyArgs) | this._setProp('height', keyArgs)){
                if(basePanel){
                    basePanel.size = new pvc_Size (this.width, this.height);
                }
            }
            
            if(this._setProp('margins', keyArgs) && basePanel){
                basePanel.margins = new pvc_Sides(this.margins);
            }
            
            if(this._setProp('paddings', keyArgs) && basePanel){
                basePanel.paddings = new pvc_Sides(this.paddings);
            }
        }
    },
    
    _setProp: function(p, keyArgs) {
        var v = keyArgs[p];
        if(v != null) {
            this[p] = v;
            return true;
        }
    },
    
    // --------------
    
    /**
     * Processes options after user options and defaults have been merged.
     * Applies restrictions,
     * performs validations and
     * options values implications.
     */
    _processOptions: function() {
        var options = this.options;
        if(!this.parent) {
            this.width    = options.width; 
            this.height   = options.height;
            this.margins  = options.margins;
            this.paddings = options.paddings;
        }
        
        if(this.compatVersion() <= 1) {
            options.plot2 = this._allowV1SecondAxis && !!options.secondAxis;
        }
        
        this._processOptionsCore(options);
        
        this._processExtensionPoints();
        
        return options;
    },

    /**
     * Processes options after user options and default options have been merged.
     * Override to apply restrictions, perform validation or
     * options values implications.
     * When overridden, the base implementation should be called.
     * The implementation must be idempotent -
     * its successive application should yield the same results.
     * @virtual
     */
    _processOptionsCore: function(options) {
        if(!this.parent) {
            var interactive = (pv.renderer() !== 'batik');
            if(interactive) {
                interactive = options.interactive;
                if(interactive == null) { interactive = true; }
            }
            
            var ibits;
            if(!interactive) {
                ibits = 0;
            } else {
                var I = pvc.visual.Interactive;
                
                ibits = I.Interactive | I.ShowsInteraction;
                
                if(this._processTooltipOptions(options)) { ibits |= I.ShowsTooltip; }
                if(options.animate && $.support.svg) { ibits |= I.Animatable; }
                
                if(options.selectable) {
                    ibits |= I.Selectable;
                    
                    switch(pvc.parseSelectionMode(options.selectionMode)) {
                        case 'rubberband':
                            ibits |= (I.SelectableByRubberband | I.SelectableByClick); 
                            break;
                            
                        case 'focuswindow':
                            ibits |= I.SelectableByFocusWindow; 
                            break;
                    }
                }
                
                if(pvc.parseClearSelectionMode(options.clearSelectionMode) === 'emptyspaceclick') {
                    ibits |= I.Unselectable;
                }
                
                if(options.hoverable) { ibits |= I.Hoverable; }
                if(options.clickable) { ibits |= (I.Clickable | I.DoubleClickable); }
            }
            
            this._ibits = ibits;
        } else {
            this._ibits = this.parent._ibits;
            this._tooltipOptions = this.parent._tooltipOptions;
        }
    },
    
    _tooltipDefaults: {
        gravity:     's',
        delayIn:      200,
        delayOut:     80, // smoother moving between marks with tooltips, possibly slightly separated
        offset:       2,
        opacity:      0.9,
        html:         true,
        fade:         true,
        useCorners:   false,
        arrowVisible: true,
        followMouse:  false,
        format:       undefined
    },
    
    _processTooltipOptions: function(options) {
        var isV1Compat = this.compatVersion() <= 1;
        var tipOptions = options.tooltip;
        var tipEnabled = options.tooltipEnabled;
        if(tipEnabled == null) {
            if(tipOptions) { tipEnabled = tipOptions.enabled; }
            
            if(tipEnabled == null) {
                if(isV1Compat) { tipEnabled = options.showTooltips; }
                
                if(tipEnabled == null) { 
                    tipEnabled = true; 
                }
            }
        }
        
        if(tipEnabled) {
            if(!tipOptions) { tipOptions = {}; }
            
            if(isV1Compat) { this._importV1TooltipOptions(tipOptions, options); }
            
            def.eachOwn(this._tooltipDefaults, function(dv, p) {
                var value = options['tooltip' + def.firstUpperCase(p)];
                if(value !== undefined) {
                    tipOptions[p] = value;
                } else if(tipOptions[p] === undefined) {
                    tipOptions[p] = dv;
                }
            });
        } else {
            tipOptions = {};
        }
        
        this._tooltipOptions = tipOptions;
        
        return tipEnabled;
    },
    
    _importV1TooltipOptions: function(tipOptions, options) {
        var v1TipOptions = options.tipsySettings;
        if(v1TipOptions) {
            this.extend(v1TipOptions, 'tooltip');
            
            for(var p in v1TipOptions) {
                if(tipOptions[p] === undefined) {
                    tipOptions[p] = v1TipOptions[p];
                }
            }
            
            // Force V1 html default
            if(tipOptions.html == null) { tipOptions.html = false; }
        }
    },
    
    // --------------
    
    /**
     * Render the visualization.
     * If not pre-rendered, do it now.
     */
    render: function(bypassAnimation, recreate, reloadData) {
        var hasError;
        
        /*global console:true*/
        if(pvc.debug > 1) { pvc.group("CCC RENDER"); }
        
        // Don't let selection change events to fire before the render is finished
        this._suspendSelectionUpdate();
        try {
            this.useTextMeasureCache(function() {
                try {
                    if (!this.isPreRendered || recreate) {
                        this._preRender({reloadData: reloadData});
                    } else if(!this.parent && this.isPreRendered) {
                        pvc.removeTipsyLegends();
                    }
                    
                    this.basePanel.render({
                        bypassAnimation: bypassAnimation, 
                        recreate: recreate
                    });
                    
                } catch (e) {
                    /*global NoDataException:true*/
                    if (e instanceof NoDataException) {
                        if(pvc.debug > 1){ this._log("No data found."); }

                        this._addErrorPanelMessage("No data found", true);
                    } else {
                        hasError = true;

                        // We don't know how to handle this
                        pvc.logError(e.message);

                        if(pvc.debug > 0){
                            this._addErrorPanelMessage("Error: " + e.message, false);
                        }
                        //throw e;
                    }
                }
            });
        } finally {
            if(!hasError){ this._resumeSelectionUpdate(); }
            
            if(pvc.debug > 1) { pvc.groupEnd(); }
        }
        
        return this;
    },

    _addErrorPanelMessage: function(text, isNoData) {
        var options = this.options,
            pvPanel = new pv.Panel()
                        .canvas(options.canvas)
                        .width(this.width)
                        .height(this.height),
            pvMsg = pvPanel.anchor("center").add(pv.Label)
                        .text(text);

        if(isNoData) { this.extend(pvMsg, "noDataMessage"); }
        
        pvPanel.render();
    },
    
    useTextMeasureCache: function(fun, ctx) {
        var root = this.root;
        var textMeasureCache = root._textMeasureCache || 
                               (root._textMeasureCache = pv.Text.createCache());
        
        return pv.Text.usingCache(textMeasureCache, fun, ctx || this);
    },
    
    /**
     * Animation
     */
    animate: function(start, end) { return this.basePanel.animate(start, end); },
    
    /**
     * Indicates if the chart is currently 
     * rendering the animation start phase.
     * <p>
     * Prefer using this function instead of {@link #animate} 
     * whenever its <tt>start</tt> or <tt>end</tt> arguments
     * involve a non-trivial calculation. 
     * </p>
     * 
     * @type boolean
     */
    animatingStart: function() { return this.basePanel.animatingStart(); },
    
    /* @override Interactive */
    animatable: function() {
        return this._animatable && this.base();
    },
    
    isOrientationVertical: function(orientation) {
        return (orientation || this.options.orientation) === pvc.orientation.vertical;
    },

    isOrientationHorizontal: function(orientation) {
        return (orientation || this.options.orientation) === pvc.orientation.horizontal;
    },
    
    /**
     * Disposes the chart, any of its panels and child charts.
     */
    dispose: function(){
        if(!this._disposed){
            
            // TODO: implement chart dispose
            
            this._disposed = true;
        }
    },
    
    defaults: {
//        canvas: null,

        width:  400,
        height: 300,

//      margins:  undefined,
//      paddings: undefined,
//      contentMargins:  undefined,
//      contentPaddings: undefined,
//      leafContentOverflow: 'auto',
//      multiChartMax: undefined,
//      multiChartColumnsMax: undefined,
//      multiChartSingleRowFillsHeight: undefined,
//      multiChartSingleColFillsHeight: undefined,
        
//      smallWidth:       undefined,
//      smallHeight:      undefined,
//      smallAspectRatio: undefined,
//      smallMargins:     undefined,
//      smallPaddings:    undefined,
        
//      smallContentMargins:  undefined,
//      smallContentPaddings: undefined,
//      smallTitlePosition: undefined,
//      smallTitleAlign:    undefined,
//      smallTitleAlignTo:  undefined,
//      smallTitleOffset:   undefined,
//      smallTitleKeepInBounds: undefined,
//      smallTitleSize:     undefined,
//      smallTitleSizeMax:  undefined,
//      smallTitleMargins:  undefined,
//      smallTitlePaddings: undefined,
//      smallTitleFont:     undefined,
        
        orientation: 'vertical',
        
//        extensionPoints:   undefined,
//        
//        visualRoles:       undefined,
//        dimensions:        undefined,
//        dimensionGroups:   undefined,
//        calculations:      undefined,
//        readers:           undefined,
        
        ignoreNulls:       true, // whether to ignore or keep "null"-measure datums upon loading
        crosstabMode:      true,
//        multiChartIndexes: undefined,
        isMultiValued:     false,
        seriesInRows:      false,
        groupedLabelSep:   undefined,
//        measuresIndexes:   undefined,
//        dataOptions:       undefined,
//        dataSeparator
//        dataMeasuresInColumns
//        dataCategoriesCount
//        dataIgnoreMetadataLabels: false

//        timeSeries:        undefined,
//        timeSeriesFormat:  undefined,

        animate: true,

//        title:         null,
        
        titlePosition: "top", // options: bottom || left || right
        titleAlign:    "center", // left / right / center
//        titleAlignTo:  undefined,
//        titleOffset:   undefined,
//        titleKeepInBounds: undefined,
//        titleSize:     undefined,
//        titleSizeMax:  undefined,
//        titleMargins:  undefined,
//        titlePaddings: undefined,
//        titleFont:     undefined,
        
        legend:           false, // Show Legends
        legendPosition:   "bottom",
//        legendFont:       undefined,
//        legendSize:       undefined,
//        legendSizeMax:    undefined,
//        legendAlign:      undefined,
//        legendAlignTo:    undefined,
//        legendOffset:     undefined,
//        legendKeepInBounds:   undefined,
//        legendMargins:    undefined,
//        legendPaddings:   undefined,
//        legendTextMargin: undefined,
//        legendItemPadding:    undefined, // ATTENTION: this is different from legendPaddings
//        legendMarkerSize: undefined,
        
//        colors: null,

        v1StyleTooltipFormat: function(s, c, v, datum) {
            return s + ", " + c + ":  " + this.chart.options.valueFormat(v) +
                   (datum && datum.percent ? ( " (" + datum.percent.label + ")") : "");
        },
        
        valueFormat: def.scope(function(){
            var pvFormat = pv.Format.number().fractionDigits(0, 2);
            
            return function(d) {
                return pvFormat.format(d);
                // pv.Format.number().fractionDigits(0, 10).parse(d));
            };
        }),
        
        /* For numeric values in percentage */
        percentValueFormat: def.scope(function(){
            var pvFormat = pv.Format.number().fractionDigits(0, 1);
            
            return function(d){
                return pvFormat.format(d * 100) + "%";
            };
        }),
        
        //interactive: true,
        
        // Content/Plot area clicking
        clickable:  false,
//        clickAction: null,
//        doubleClickAction: null,
        doubleClickMaxDelay: 300, //ms
//      
        hoverable:  false,
        
        selectable:    false,
        selectionMode: 'rubberband', // focuswindow, // single (click-only) // custom (by code only)
        //selectionCountMax: 0, // <= 0 -> no limit
        
//        selectionChangedAction: null,
//        userSelectionAction: null, 
            
        // Use CTRL key to make fine-grained selections
        ctrlSelectMode: true,
        clearSelectionMode: 'emptySpaceClick', // or null <=> 'manual' (i.e., by code)
        
//        renderCallback: undefined,

        compatVersion: Infinity // numeric, 1 currently recognized
    }
});



pvc.BaseChart
.add({
    /**
     * A map of {@link pvc.visual.Role} by name.
     * 
     * @type object
     */
    visualRoles: null,
    visualRoleList: null,
    
    _serRole: null,
    _dataPartRole: null,
    
    /**
     * An array of the {@link pvc.visual.Role} that are measures.
     * 
     * @type pvc.visual.Role[]
     */
    _measureVisualRoles: null,
    
    /**
     * Obtains an existing visual role given its name.
     * An error is thrown if a role with the specified name is not defined.
     * 
     * @param {string} roleName The role name.
     * @type pvc.data.VisualRole 
     */
    visualRole: function(roleName) {
        var role = def.getOwn(this.visualRoles, roleName);
        if(!role) { 
            throw def.error.operationInvalid('roleName', "There is no visual role with name '{0}'.", [roleName]);
        }
        return role;
    },

    measureVisualRoles: function() { return this._measureVisualRoles; },

    measureDimensionsNames: function() {
        return def.query(this._measureVisualRoles)
           .select(function(role) { return role.firstDimensionName(); })
           .where(def.notNully)
           .array();
    },
    
    _constructVisualRoles: function(/*options*/) {
        var parent = this.parent;
        if(parent) {
            this.visualRoles = parent.visualRoles;
            this.visualRoleList = parent.visualRoleList;
            this._measureVisualRoles = parent._measureVisualRoles;
            
            ['_multiChartRole', '_serRole', '_colorRole', '_dataPartRole']
            .forEach(function(p) {
                var parentRole = parent[p];
                if(parentRole) { this[p] = parentRole; }
            }, this);
            
        } else {
            this.visualRoles = {};
            this.visualRoleList = [];
            this._measureVisualRoles = [];
        }
    },
    
    _hasDataPartRole:   def.fun.constant(false),
    _getSeriesRoleSpec: def.fun.constant(null),
    _getColorRoleSpec:  def.fun.constant(null),
    
    _addVisualRole: function(name, keyArgs) {
        keyArgs = def.set(keyArgs, 'index', this.visualRoleList.length);
        
        var role = new pvc.visual.Role(name, keyArgs);
        
        this.visualRoleList.push(role);
        this.visualRoles[name] = role;
        if(role.isMeasure) { this._measureVisualRoles.push(role); }
        return role;
    },
    
    /**
     * Initializes each chart's specific roles.
     * @virtual
     */
    _initVisualRoles: function() {
        this._multiChartRole = this._addVisualRole(
            'multiChart', 
            {defaultDimension: 'multiChart*', requireIsDiscrete: true});

        if(this._hasDataPartRole()) {
            this._dataPartRole = this._addVisualRole(
                'dataPart', 
                {
                    defaultDimension: 'dataPart',
                    requireSingleDimension: true,
                    requireIsDiscrete: true,
                    dimensionDefaults: {isHidden: true, comparer: def.compare}
                });
        }

        var serRoleSpec = this._getSeriesRoleSpec();
        if(serRoleSpec  ) { this._serRole = this._addVisualRole('series', serRoleSpec); }
        
        var colorRoleSpec = this._getColorRoleSpec();
        if(colorRoleSpec) { this._colorRole = this._addVisualRole('color', colorRoleSpec); }
    },

    _assertUnboundRoleIsOptional: function(role) {
        if(role.isRequired) {
            throw def.error.operationInvalid("Chart type requires unassigned role '{0}'.", [role.name]);
        }
    },
        
    /**
     * Binds visual roles to grouping specifications
     * that have not yet been bound to and validated against a complex type.
     *
     * This allows inferring proper defaults to
     * dimensions bound to roles, 
     * by taking them from the roles requirements.
     */
    _bindVisualRolesPreI: function() {
        // Clear reversed status of visual roles
        def.eachOwn(this.visualRoles, function(role) { role.setIsReversed(false); });
        
        var sourcedRoles = [];
        
        // Process the visual roles with options
        // It is important to process them in visual role definition order
        // cause the processing that is done generally 
        // depends on the processing order;
        // A chart definition must behave the same 
        // in every environment, independently of the order in which
        // object properties are enumerated.
        var options = this.options;
        var roleOptions = options.visualRoles;
        
        // Accept visual roles directly in the options
        // as <roleName>Role: 
        this.visualRoleList.forEach(function(role) {
            var name = role.name;
            var roleSpec = options[name + 'Role'];
            if(roleSpec !== undefined) {
                if(!roleOptions) { roleOptions = options.visualRoles = {}; }
                
                if(roleOptions[name] === undefined) { roleOptions[name] = roleSpec; }
            }
        });
        
        var dimsBoundToSingleRole;
        if(roleOptions) {
            dimsBoundToSingleRole = {};
            
            // Decode role names and validate their existence
            var rolesWithOptions = 
                def.query(def.keys(roleOptions)).select(this.visualRole, this).array();
            
            rolesWithOptions.sort(function(a, b) { return a.index - b.index; });
                
            /* Process options.visualRoles */
            rolesWithOptions.forEach(function(role) {
                var name     = role.name;
                var roleSpec = roleOptions[name];
                
                // Process the visual role specification
                // * a string with the grouping dimensions, or
                // * {dimensions: "product", isReversed:true, from: "series" }
                var groupingSpec, sourceRoleName;
                if(def.object.is(roleSpec)) {
                    if(def.nullyTo(roleSpec.isReversed, false)) { role.setIsReversed(true); }
                    
                    sourceRoleName = roleSpec.from;
                    if(sourceRoleName && (sourceRoleName !== name)) {
                        var sourceRole = this.visualRoles[sourceRoleName] ||
                            def.fail.operationInvalid("Source role '{0}' is not supported by the chart type.", [sourceRoleName]);
                        
                        role.setSourceRole(sourceRole);
                        sourcedRoles.push(role);
                    } else {
                        groupingSpec = roleSpec.dimensions;
                    }
                } else {
                    // Assumed to be a string (or null, undefined)
                    groupingSpec = roleSpec;
                }
                
                // !groupingSpec (null or "") results in a null grouping being preBound
                // A pre-bound null grouping is later discarded in the post bind,
                // but, in between, prevents translators from 
                // reading to dimensions that would bind into those roles...
                if(groupingSpec !== undefined) {
                    if(!groupingSpec) { this._assertUnboundRoleIsOptional(role); } // throws if required
                    
                    var grouping = pvc.data.GroupingSpec.parse(groupingSpec);
    
                    role.preBind(grouping);
    
                    /* Collect dimension names bound to a *single* role */
                    grouping.dimensions().each(function(groupDimSpec) {
                        if(def.hasOwn(dimsBoundToSingleRole, groupDimSpec.name)) {
                            // two roles => no defaults at all
                            delete dimsBoundToSingleRole[groupDimSpec.name];
                        } else {
                            dimsBoundToSingleRole[groupDimSpec.name] = role;
                        }
                    });
                }
            }, this);
    
        }

        this._sourcedRoles = sourcedRoles;
        this._dimsBoundToSingleRole = dimsBoundToSingleRole;
    },
    
    _bindVisualRolesPreII: function() {
        // Provide defaults to dimensions bound to a single role
        // by using the role's requirements 
        var dimsBoundToSingleRole = this._dimsBoundToSingleRole;
        if(dimsBoundToSingleRole) {
            delete this._dimsBoundToSingleRole; // free memory
            
            def.eachOwn(dimsBoundToSingleRole, this._setRoleBoundDimensionDefaults, this);
        }
        
        var sourcedRoles = this._sourcedRoles;
        delete this._sourcedRoles; // free memory
        
        /* Apply defaultSourceRole to roles not pre-bound */
        def
        .query(this.visualRoleList)
        .where(function(role) { 
            return role.defaultSourceRoleName && !role.sourceRole && !role.isPreBound(); 
         })
        .each (function(role) {
            var sourceRole = this.visualRoles[role.defaultSourceRoleName];
            if(sourceRole) {
                role.setSourceRole(sourceRole, /*isDefault*/true);
                sourcedRoles.push(role);
            }
        }, this);
        
        /* Pre-bind sourced roles whose source role is itself pre-bound */
        // Only if the role has no default dimension, cause otherwise, 
        // it would prevent binding to it, if it comes to exist.
        // In those cases, sourcing only effectively happens in the post phase.
        sourcedRoles.forEach(function(role) {
            var sourceRole = role.sourceRole;
            if(sourceRole.isReversed) { role.setIsReversed(!role.isReversed); }
            
            if(!role.defaultDimensionName && sourceRole.isPreBound()) {
                role.preBind(sourceRole.preBoundGrouping());
            }
        });
    },
    
    _setRoleBoundDimensionDefaults: function(role, dimName) {
        this._complexTypeProj.setDimDefaults(dimName, role.dimensionDefaults);
    },
    
    _bindVisualRolesPostI: function(){
        var me = this;
        
        var complexTypeProj = me._complexTypeProj;
        
        // Dimension names to roles bound to it
        var boundDimTypes = {};
        
        var unboundSourcedRoles = [];
        
        def
        .query(me.visualRoleList)
        .where(function(role) { return role.isPreBound(); })
        .each (markPreBoundRoleDims);
        
        /* (Try to) Automatically bind **unbound** roles:
         * -> to their default dimensions, if they exist and are not yet bound to
         * -> if the default dimension does not exist and the 
         *    role allows auto dimension creation, 
         *    creates 1 *hidden* dimension (that will receive only null data)
         * 
         * Validates role required'ness.
         */
        def
        .query(me.visualRoleList)
        .where(function(role) { return !role.isPreBound(); })
        .each (autoBindUnboundRole);
        
        // Sourced roles that could not be normally bound are now finally sourced 
        unboundSourcedRoles.forEach(tryPreBindSourcedRole);
        
        // Apply defaults to single-bound-to dimensions
        // TODO: this is being repeated for !pre-bound! dimensions
        def
        .query(def.ownKeys(boundDimTypes))
        .where(function(dimName) { return boundDimTypes[dimName].length === 1; })
        .each (function(dimName) {
            var singleRole = boundDimTypes[dimName][0];
            me._setRoleBoundDimensionDefaults(singleRole, dimName);
        });

        // ----------------
        
        function markDimBoundTo(dimName, role) { def.array.lazy(boundDimTypes, dimName).push(role); }
        
        function dimIsDefined(dimName) { return complexTypeProj.hasDim(dimName); }
        
        function preBindRoleTo(role, dimNames) {
            if(def.array.is(dimNames)) {
                dimNames.forEach(function(dimName) { markDimBoundTo(dimName, role); });
            } else {
                markDimBoundTo(dimNames, role);
            }
            
            role.setSourceRole(null); // if any
            role.preBind(pvc.data.GroupingSpec.parse(dimNames));
        }
        
        function preBindRoleToGroupDims(role, groupDimNames) {
            if(groupDimNames.length) {
                if(role.requireSingleDimension) { preBindRoleTo(role, groupDimNames[0]); } 
                else                            { preBindRoleTo(role, groupDimNames);    }
            }
        }
        
        function preBindRoleToNewDim(role, dimName) {
            // Create a hidden dimension and bind the role and the dimension
            complexTypeProj.setDim(dimName, {isHidden: true});
            
            preBindRoleTo(role, dimName);
        }
        
        function roleIsUnbound(role) {
            me._assertUnboundRoleIsOptional(role); // throws if required
            
            // Unbind role from any previous binding
            role.bind(null);
            role.setSourceRole(null); // if any
        }
        
        function markPreBoundRoleDims(role) {
            role.preBoundGrouping().dimensionNames().forEach(markDimBoundTo);
        }
        
        function autoBindUnboundRole(role) {
            // !role.isPreBound()
            
            if(role.sourceRole && !role.isDefaultSourceRole) {
                unboundSourcedRoles.push(role);
                return;
            }
            
            // Try to bind automatically, to defaultDimensionName
            var dimName = role.defaultDimensionName;
            if(!dimName) {
                if(role.sourceRole) { unboundSourcedRoles.push(role); } 
                else                { roleIsUnbound(role);            }
                return;
            }
                
            /* An asterisk at the end of the name indicates
             * that any dimension of that group is allowed.
             * If the role allows multiple dimensions,
             * then the meaning is greedy - use them all.
             * Otherwise, use only one.
             * 
             *   "product*"
             */
            var match = dimName.match(/^(.*?)(\*)?$/) ||
                        def.fail.argumentInvalid('defaultDimensionName');
            
            var defaultName =  match[1];
            var greedy = /*!!*/match[2];
            if(greedy) {
                // TODO: does not respect any index explicitly specified
                // before the *. Could mean >=...
                var groupDimNames = complexTypeProj.groupDimensionsNames(defaultName);
                if(groupDimNames) {
                    // Default dimension(s) is defined
                    preBindRoleToGroupDims(role, groupDimNames);
                    return;
                }
                
                // Follow to auto create dimension
                
            } else if(dimIsDefined(defaultName)) { // defaultName === dimName
                preBindRoleTo(role, defaultName);
                return;
            }

            if(role.autoCreateDimension) {
                preBindRoleToNewDim(role, defaultName);
                return;
            }
            
            if(role.sourceRole) { unboundSourcedRoles.push(role); } 
            else                { roleIsUnbound(role);            }
        }
    
        function tryPreBindSourcedRole(role) {
            var sourceRole = role.sourceRole;
            if(sourceRole.isPreBound()) { role.preBind(sourceRole.preBoundGrouping()); } 
            else                        { roleIsUnbound(role);                         }
        }
    },
    
    _bindVisualRolesPostII: function(complexType) {
        // Commits and validates the grouping specification.
        // Null groupings are discarded.
        // Sourced roles that were also pre-bound are here normally bound.
        def
        .query(this.visualRoleList)
        .where(function(role) { return role.isPreBound();   })
        .each (function(role) { role.postBind(complexType); });
    },

    _logVisualRoles: function() {
        var names  = def.ownKeys(this.visualRoles);
        var maxLen = Math.max(10, def.query(names).select(function(s){ return s.length; }).max());
        var header = def.string.padRight("VisualRole", maxLen) + " < Dimension(s)";
        var out = [
            "VISUAL ROLES MAP SUMMARY", 
            pvc.logSeparator, 
            header, 
            def.string.padRight('', maxLen + 1, '-') + '+--------------'
        ];
        
        def.eachOwn(this.visualRoles, function(role, name) {
            out.push(def.string.padRight(name, maxLen) + ' | ' + (role.grouping || '-'));
        });
        out.push("");
        this._log(out.join("\n"));
    },
    
    _getDataPartDimName: function() {
        var role = this._dataPartRole;
        if(role) {
            if(role.isBound()) { return role.firstDimensionName(); } 
            
            var preGrouping = role.preBoundGrouping();
            if(preGrouping) { return preGrouping.firstDimensionName(); }
            
            return role.defaultDimensionName;
        }
    }
});



pvc.BaseChart
.add({
    
    /**
     * The data that the chart is to show.
     * @type pvc.data.Data
     * @deprecated
     */
    dataEngine: null,
    
    /**
     * The data that the chart is to show.
     * @type pvc.data.Data
     */
    data: null,
    
    /**
     * The resulting data of 
     * grouping {@link #data} by the data part role, 
     * when bound.
     * 
     * @type pvc.data.Data
     */
    _partData: null,


    _visibleDataCache: null,
    
    /**
     * The data source of the chart.
     * <p>
     * The {@link #data} of a root chart 
     * is loaded with the data in this array.
     * </p>
     * @type any[]
     */
    resultset: [],
    
    /**
     * The meta-data that describes each 
     * of the data components of {@link #resultset}.
     * @type any[]
     */
    metadata: [],
    
    _constructData: function(options) {
        if(this.parent) {
            this.dataEngine = this.data = options.data || def.fail.argumentRequired('options.data');            
        }
    },
    
    _checkNoDataI: function() {
        // Child charts are created to consume *existing* data
        // If we don't have data, we just need to set a "no data" message and go on with life.
        if (!this.parent && !this.allowNoData && this.resultset.length === 0) {
            /*global NoDataException:true */
            throw new NoDataException();
        }
    },
    
    _checkNoDataII: function() {
        // Child charts are created to consume *existing* data
        // If we don't have data, we just need to set a "no data" message and go on with life.
        if (!this.parent && !this.allowNoData && (!this.data || !this.data.count())) {
            
            this.data = null;
            
            /*global NoDataException:true */
            throw new NoDataException();
        }
    },
    
    /**
     * Initializes the data engine and roles.
     */
    _initData: function(ka) {
        // Root chart
        if(!this.parent) {
            var data = this.data;
            if(!data) {
                this._onLoadData();
            } else if(def.get(ka, 'reloadData', true)) {
                // This **replaces** existing data (datums also existing in the new data are kept)
                this._onReloadData();
            } else {
                // Existing data is kept.
                // This is used for re-layouting only.
                // Yet...
                
                // Remove virtual datums (they are regenerated each time)
                data.clearVirtuals();
                
                // Dispose all data children and linked children (recreated as well)
                data.disposeChildren();
            }
        }
        
        // Cached data stuff
        delete this._partData;
        delete this._visibleDataCache;
        
        if(pvc.debug >= 3) { this._log(this.data.getInfo()); }
    },

    _onLoadData: function() {
        /*jshint expr:true*/
        var data = this.data;
        var translation = this._translation;
        
        (!data && !translation) || def.assert("Invalid state.");
        
        var options  = this.options;
        
        var dataPartDimName = this._getDataPartDimName();
        var complexTypeProj = this._complexTypeProj || def.assert("Invalid state.");
        var translOptions   = this._createTranslationOptions(dataPartDimName);
        translation = this._translation = this._createTranslation(translOptions);
        
        if(pvc.debug >= 3) {
            this._log(translation.logSource());
            this._log(translation.logTranslatorType());
        }
        
        // Now the translation can also configure the type
        translation.configureType();
        
        // If the the dataPart dimension isn't being read or calculated
        // its value must be defaulted to 0.
        if(dataPartDimName && !complexTypeProj.isReadOrCalc(dataPartDimName)) {
            this._addDefaultDataPartCalculation(dataPartDimName);
        }
        
        if(pvc.debug >= 3) {
            this._log(translation.logVItem());
        }
        
        // ----------
        // Roles are bound before actually loading data.
        // i) roles add default properties to dimensions bound to them
        // ii) in order to be able to filter datums
        //     whose "every dimension in a measure role is null".
        this._bindVisualRolesPostI();
        
        // Setup the complex type from complexTypeProj;
        var complexType = new pvc.data.ComplexType();
        complexTypeProj.configureComplexType(complexType, translOptions);
        
        this._bindVisualRolesPostII(complexType);
            
        if(pvc.debug >= 10) { this._log(complexType.describe()); }
        if(pvc.debug >= 3 ) { this._logVisualRoles(); }
            
        data =
            this.dataEngine = // V1 property
            this.data = new pvc.data.Data({
                type:     complexType,
                labelSep: options.groupedLabelSep,
                keySep:   translOptions.separator
            });

        // ----------

        var loadKeyArgs = {where: this._getLoadFilter(), isNull: this._getIsNullDatum()};
        
        var resultQuery = translation.execute(data);
        
        data.load(resultQuery, loadKeyArgs);
    },
    
    _onReloadData: function() {
        /*jshint expr:true*/
        
        var data = this.data;
        var translation = this._translation;
        
        (data && translation) || def.assert("Invalid state.");
        
        var options  = this.options;
        
        // pass new resultset to the translation (metadata is maintained!).
        translation.setSource(this.resultset);
        
        if(pvc.debug >= 3) { this._log(translation.logSource()); }
        
        var loadKeyArgs = {where: this._getLoadFilter(), isNull: this._getIsNullDatum()};
        
        var resultQuery = translation.execute(data);
        
        data.load(resultQuery, loadKeyArgs);
    },
    
    _createComplexTypeProject: function() {
        var options = this.options;
        var complexTypeProj = new pvc.data.ComplexTypeProject(options.dimensionGroups);
        
        // Add specified dimensions
        var userDimsSpec = options.dimensions;
        for(var dimName in userDimsSpec) { // userDimsSpec can be null; 'for' accepts null!
            complexTypeProj.setDim(dimName, userDimsSpec[dimName]);
        }
        
        // Add data part dimension and
        // dataPart calculation from series values
        var dataPartDimName = this._getDataPartDimName();
        if(dataPartDimName) {
            complexTypeProj.setDim(dataPartDimName);
            
            this._addPlot2SeriesDataPartCalculation(complexTypeProj, dataPartDimName);
        }
        
        // Add specified calculations
        var calcSpecs = options.calculations;
        if(calcSpecs) {
            calcSpecs.forEach(function(calcSpec) { complexTypeProj.setCalc(calcSpec); });
        }
        
        return complexTypeProj;
    },
    
    _getLoadFilter: function() {
        if(this.options.ignoreNulls) {
            var me = this;
            return function(datum) {
                var isNull = datum.isNull;
                if(isNull && pvc.debug >= 4) { me._info("Datum excluded."); }
                return !isNull;
            };
        }
    },
    
    _getIsNullDatum: function() {
        var measureDimNames = this.measureDimensionsNames(),
            M = measureDimNames.length;
        if(M) {
            // Must have all measure role dimensions = null
            return function(datum) {
                var atoms = datum.atoms;
                for(var i = 0 ; i < M ; i++) {
                    if(atoms[measureDimNames[i]].value != null) { return false; }
                }

                return true;
            };
        }
    },
    
    _createTranslation: function(translOptions){
        var TranslationClass = this._getTranslationClass(translOptions);
        
        return new TranslationClass(this, this._complexTypeProj, this.resultset, this.metadata, translOptions);
    },
    
    _getTranslationClass: function(translOptions) {
        return translOptions.crosstabMode ? 
               pvc.data.CrosstabTranslationOper : 
               pvc.data.RelationalTranslationOper;
    },
    
    _createTranslationOptions: function(dataPartDimName) {
        var options = this.options;
        
        var dataOptions = options.dataOptions || {};
        
        var dataSeparator = options.dataSeparator;
        if(dataSeparator === undefined) { dataSeparator = dataOptions.separator; }
        if(!dataSeparator) { dataSeparator = '~'; }
        
        var dataMeasuresInColumns = options.dataMeasuresInColumns;
        if(dataMeasuresInColumns === undefined) { dataMeasuresInColumns = dataOptions.measuresInColumns; }
        
        var dataCategoriesCount = options.dataCategoriesCount;
        if(dataCategoriesCount === undefined) { dataCategoriesCount = dataOptions.categoriesCount; }
        
        var dataIgnoreMetadataLabels = options.dataIgnoreMetadataLabels;
        if(dataIgnoreMetadataLabels === undefined) { dataIgnoreMetadataLabels = dataOptions.ignoreMetadataLabels; }
        
        var plot2 = options.plot2;
        
        var valueFormat = options.valueFormat,
            valueFormatter;
        if(valueFormat && valueFormat !== this.defaults.valueFormat) {
            valueFormatter = function(v) { return v != null ? valueFormat(v) : ""; };
        }
        
        var plot2Series, plot2DataSeriesIndexes;
        if(plot2) {
            if(this._allowV1SecondAxis && (this.compatVersion() <= 1)) {
                plot2DataSeriesIndexes = options.secondAxisIdx;
            } else {
                plot2Series = (this._serRole != null) && 
                              options.plot2Series && 
                              def.array.as(options.plot2Series);
                
                // TODO: temporary implementation based on V1s secondAxisIdx's implementation
                // until a real "series visual role" based implementation exists. 
                if(!plot2Series || !plot2Series.length) {
                    plot2Series = null;
                    plot2DataSeriesIndexes = options.plot2SeriesIndexes;
                }
            }
            
            if(!plot2Series) {
                plot2DataSeriesIndexes = pvc.parseDistinctIndexArray(plot2DataSeriesIndexes, -Infinity) || -1;
            }
        }
        
        return {
            compatVersion:     this.compatVersion(),
            plot2DataSeriesIndexes: plot2DataSeriesIndexes,
            seriesInRows:      options.seriesInRows,
            crosstabMode:      options.crosstabMode,
            isMultiValued:     options.isMultiValued,
            dataPartDimName:   dataPartDimName,
            dimensionGroups:   options.dimensionGroups,
            dimensions:        options.dimensions,
            readers:           options.readers,
            
            measuresIndexes:   options.measuresIndexes, // relational multi-valued

            multiChartIndexes: options.multiChartIndexes,

            // crosstab
            separator:         dataSeparator,
            measuresInColumns: dataMeasuresInColumns,
            categoriesCount:   dataCategoriesCount,
            
            // TODO: currently measuresInRows is not implemented...
            measuresIndex:     dataOptions.measuresIndex || dataOptions.measuresIdx, // measuresInRows
            measuresCount:     dataOptions.measuresCount || dataOptions.numMeasures, // measuresInRows

            // Timeseries *parse* format
            isCategoryTimeSeries: options.timeSeries,

            timeSeriesFormat:     options.timeSeriesFormat,
            valueNumberFormatter: valueFormatter,
            ignoreMetadataLabels:  dataIgnoreMetadataLabels
        };
    },
    
    _addPlot2SeriesDataPartCalculation: function(complexTypeProj, dataPartDimName) {
        if(this.compatVersion() <= 1) { return; }
        
        var options = this.options;
        var serRole = this._serRole;
        var plot2Series = (serRole != null) && 
                          options.plot2 && 
                          options.plot2Series && 
                          def.array.as(options.plot2Series);
        
        if(!plot2Series || !plot2Series.length) { return; }
        
        var inited = false;
        var plot2SeriesSet = def.query(plot2Series).uniqueIndex();
        var dimNames, dataPartDim, part1Atom, part2Atom;
        
        complexTypeProj.setCalc({
            names: dataPartDimName,
            calculation: function(datum, atoms) {
                if(!inited){
                    // LAZY init
                    if(serRole.isBound()) {
                        dimNames    = serRole.grouping.dimensionNames();
                        dataPartDim = datum.owner.dimensions(dataPartDimName);
                    }
                    inited = true;
                }
                
                if(dataPartDim) {
                    var seriesKey = pvc.data.Complex.compositeKey(datum, dimNames);
                    atoms[dataPartDimName] = 
                        def.hasOwnProp.call(plot2SeriesSet, seriesKey) ?
                           (part2Atom || (part2Atom = dataPartDim.intern('1'))) :
                           (part1Atom || (part1Atom = dataPartDim.intern('0')));
                }
            }
        });
    },
    
    _addDefaultDataPartCalculation: function(dataPartDimName){
        var dataPartDim, part1Atom;
        
        this._complexTypeProj.setCalc({
            names: dataPartDimName,
            calculation: function(datum, atoms) {
                if(!dataPartDim) { dataPartDim = datum.owner.dimensions(dataPartDimName); }
                
                atoms[dataPartDimName] = part1Atom || (part1Atom = dataPartDim.intern('0'));
            }
        });
    },
    
    partData: function(dataPartValues) {
        var partRole = this._dataPartRole;
        
        if(!this._partData) {
            // Undefined or unbound 
            if(!partRole || !partRole.grouping) { return this._partData = this.data; }
            
            // Visible and not
            this._partData = partRole.flatten(this.data);
        }
        
        if(!dataPartValues || !partRole || !partRole.grouping) { return this._partData; }
        
        var dataPartDimName = partRole.firstDimensionName();
        
        if(def.array.is(dataPartValues)) {
            if(dataPartValues.length > 1) {
                return this._partData.where([def.set({}, dataPartDimName, dataPartValues)]);
            }
            
            dataPartValues = dataPartValues[0];
        }
        
        // TODO: should, at least, call some static method of Atom to build a global key
        var child = this._partData._childrenByKey[/*dataPartDimName + ':' +*/ dataPartValues + ''];
        if(!child) {
            // NOTE: 
            // This helps, at least, the ColorAxis.dataCells setting
            // the .data property, in a time where there aren't yet any datums of
            // the 'trend' data part value.
            // So we create a dummy empty place-holder child here,
            // so that when the trend datums are added they end up here,
            // and not in another new Data...
            var dataPartCell = {v: dataPartValues};
            
            // TODO: HACK: To make trend label fixing work in multi-chart scenarios... 
            if(dataPartValues === 'trend') {
                var firstTrendAtom = this._firstTrendAtomProto;
                if(firstTrendAtom) { dataPartCell.f = firstTrendAtom.f; }
            }
            
            child = new pvc.data.Data({
                parent:   this._partData,
                atoms:    def.set({}, dataPartDimName, dataPartCell), 
                dimNames: [dataPartDimName],
                datums:   []
                // TODO: index
            });
        }
        return child;
    },

    // --------------------
    
    /*
     * Obtains the chart's visible data
     * grouped according to the charts "main grouping".
     * 
     * @param {string|string[]} [dataPartValue=null] The desired data part value or values.
     * @param {object} [ka=null] Optional keyword arguments object.
     * @param {boolean} [ka.ignoreNulls=true] Indicates that null datums should be ignored.
     * @param {boolean} [ka.inverted=false] Indicates that the inverted data grouping is desired.
     * 
     * @type pvc.data.Data
     */
    visibleData: function(dataPartValue, ka) {
        var ignoreNulls = def.get(ka, 'ignoreNulls', true);
        var inverted    = def.get(ka, 'inverted', false);
        
        // If already globally ignoring nulls, there's no need to do it explicitly anywhere
        if(ignoreNulls && this.options.ignoreNulls) { ignoreNulls = false; }
        
        var cache = def.lazy(this, '_visibleDataCache');
        var key   = inverted + '|' + ignoreNulls + '|' + dataPartValue; // relying on Array#toString, when an array
        var data  = cache[key];
        if(!data) {
            ka = ka ? Object.create(ka) : {};
            ka.ignoreNulls = ignoreNulls;
            data = cache[key] = this._createVisibleData(dataPartValue, ka);
        }
        return data;
    },

    /*
     * Creates the chart's visible data
     * grouped according to the charts "main grouping".
     *
     * <p>
     * The default implementation groups data by series visual role.
     * </p>
     *
     * @param {string|string[]} [dataPartValue=null] The desired data part value or values.
     * 
     * @type pvc.data.Data
     * @protected
     * @virtual
     */
    _createVisibleData: function(dataPartValue, ka) {
        var partData = this.partData(dataPartValue);
        if(!partData) { return null; }
        
        // TODO: isn't this buggy? When no series role, all datums are returned, visible or not 
        
        var ignoreNulls = def.get(ka, 'ignoreNulls');
        var serRole = this._serRole;
        return serRole && serRole.grouping ?
               serRole.flatten(partData, {visible: true, isNull: ignoreNulls ? false : null}) :
               partData;
    },
    
    // --------------------
    
    _generateTrends: function() {
        if(this._dataPartRole) {
            def
            .query(def.own(this.axes))
            .selectMany(def.propGet('dataCells'))
            .where(def.propGet('trend'))
            .distinct(function(dataCell) {
                 return dataCell.role.name  + '|' + (dataCell.dataPartValue || '');
            })
            .each(this._generateTrendsDataCell, this);
        }
    },
    
    _interpolate: function() {
        // TODO: add some switch to activate interpolation
        // Many charts do not support it and we're traversing for nothing
        def
        .query(def.own(this.axes))
        .selectMany(def.propGet('dataCells'))
        .where(function(dataCell) {
            var nim = dataCell.nullInterpolationMode;
            return !!nim && nim !== 'none'; 
         })
         .distinct(function(dataCell) {
             return dataCell.role.name  + '|' + (dataCell.dataPartValue || '');
         })
         .each(this._interpolateDataCell, this);
    },
    
    _interpolateDataCell:    function(/*dataCell*/) {},
    _generateTrendsDataCell: function(/*dataCell*/) {},
    
    // ---------------
        
    /**
     * Method to set the data to the chart.
     * Expected object is the same as what comes from the CDA: 
     * {metadata: [], resultset: []}
     */
    setData: function(data, options) {
        this.setResultset(data.resultset);
        this.setMetadata(data.metadata);

        // TODO: Danger!
        $.extend(this.options, options);
        
        return this;
    },
    
    /**
     * Sets the resultset that will be used to build the chart.
     */
    setResultset: function(resultset) {
        /*jshint expr:true */
        !this.parent || def.fail.operationInvalid("Can only set resultset on root chart.");
        
        this.resultset = resultset;
        if (!resultset.length) {
            this._log("Warning: Resultset is empty");
        }
        
        return this;
    },

    /**
     * Sets the metadata that, optionally, 
     * will give more information for building the chart.
     */
    setMetadata: function(metadata) {
        /*jshint expr:true */
        !this.parent || def.fail.operationInvalid("Can only set resultset on root chart.");
        
        this.metadata = metadata;
        if (!metadata.length) {
            this._log("Warning: Metadata is empty");
        }
        
        return this;
    }
});



pvc.BaseChart
.add({
    _initPlots: function(hasMultiRole){
        
        this.plotPanelList = null;
        
        // reset plots
        if(!this.parent){
            this.plots = {};
            this.plotList = [];
            this.plotsByType = {};
            
            this._initPlotsCore(hasMultiRole);
        } else {
            var root = this.root;
            
            this.plots = root.plots;
            this.plotList = root.plotList;
            this.plotsByType = root.plotsByType;
        }
    },
    
    _initPlotsCore: function(/*hasMultiRole*/){
        // NOOP
    },

    _addPlot: function(plot){
        var plotsByType = this.plotsByType;
        var plots = this.plots;
        
        var plotType  = plot.type;
        var plotIndex = plot.index;
        var plotName  = plot.name;
        var plotId    = plot.id;
        
        if(plotName && def.hasOwn(plots, plotName)){
            throw def.error.operationInvalid("Plot name '{0}' already taken.", [plotName]);
        }
        
        if(def.hasOwn(plots, plotId)){
            throw def.error.operationInvalid("Plot id '{0}' already taken.", [plotId]);
        }
        
        var typePlots = def.array.lazy(plotsByType, plotType);
        if(def.hasOwn(typePlots, plotIndex)){
            throw def.error.operationInvalid("Plot index '{0}' of type '{1}' already taken.", [plotIndex, plotType]);
        }
        
        plot.globalIndex = this.plotList.length;
        typePlots[plotIndex] = plot;
        this.plotList.push(plot);
        plots[plotId] = plot;
        if(plotName){
            plots[plotName] = plot;
        }
    },
    
    _collectPlotAxesDataCells: function(plot, dataCellsByAxisTypeThenIndex){
        /* Configure Color Axis Data Cell */
        var dataCells = [];
        
        plot.collectDataCells(dataCells);
        
        if(dataCells.length){
            def
            .query(dataCells)
            .where(function(dataCell) { return dataCell.isBound(); })
            .each (function(dataCell) {
                /* Index DataCell in dataCellsByAxisTypeThenIndex */
                var dataCellsByAxisIndex = 
                    def.array.lazy(dataCellsByAxisTypeThenIndex, dataCell.axisType);
                
                def.array.lazy(dataCellsByAxisIndex, dataCell.axisIndex)
                    .push(dataCell);
            });
        }
    },
    
    // Called by the pvc.PlotPanel class
    _addPlotPanel: function(plotPanel){
        def.lazy(this, 'plotPanels')[plotPanel.plot.id] = plotPanel;
        def.array.lazy(this, 'plotPanelList').push(plotPanel);
    },
    
    /* @abstract */
    _createPlotPanels: function(/*parentPanel, baseOptions*/){
        throw def.error.notImplemented();
    }
});



/*global pvc_CartesianAxis:true, pvc_colorScale:true, pvc_colorScales:true */

pvc.BaseChart
.add({
    /**
     * An array of colors, represented as names, codes or {@link pv.Color} objects
     * that is associated to each distinct value of the "color" visual role.
     * 
     * <p>
     * The legend panel associates each distinct dimension value to a color of {@link #colors},
     * following the dimension's natural order.
     * </p>
     * <p>
     * The default dimension is the 'series' dimension.
     * </p>
     * 
     * @type (string|pv.Color)[]
     */
    colors: null,

    /**
     * A map of {@link pvc.visual.Axis} by axis id.
     */
    axes: null,
    axesList: null,
    axesByType: null,

    _axisClassByType: {
        'color': pvc.visual.ColorAxis,
        'size':  pvc.visual.SizeAxis,
        'base':  pvc_CartesianAxis,
        'ortho': pvc_CartesianAxis
    },
    
    // 1 = root, 2 = leaf, 1|2=3 = everywhere
    _axisCreateWhere: {
        'color': 1,
        'size':  2,
        'base':  3,
        'ortho': 3
    },
    
    _axisCreationOrder: [
        'color',
        'size',
        'base',
        'ortho'
    ],

    _axisCreateIfUnbound: {

    },

    _initAxes: function(hasMultiRole){
        this.axes = {};
        this.axesList = [];
        this.axesByType = {};
        
        // Clear any previous global color scales
        delete this._rolesColorScale;
        
        // type -> index -> [datacell array]
        // Used by sub classes.
        var dataCellsByAxisTypeThenIndex;
        if(!this.parent){
            dataCellsByAxisTypeThenIndex = {};
            
            this.plotList.forEach(function(plot){
                this._collectPlotAxesDataCells(plot, dataCellsByAxisTypeThenIndex);
            }, this);
            
            this._fixTrendsLabel(dataCellsByAxisTypeThenIndex);
        } else {
            dataCellsByAxisTypeThenIndex = this.root._dataCellsByAxisTypeThenIndex;
        }
        
        // Used later in _bindAxes as well.
        this._dataCellsByAxisTypeThenIndex = dataCellsByAxisTypeThenIndex;
        
        /* NOTE: Cartesian axes are created even when hasMultiRole && !parent
         * because it is needed to read axis options in the root chart.
         * Also binding occurs to be able to know its scale type. 
         * Yet, their scales are not setup at the root level.
         */
        
        // 1 = root, 2 = leaf, 1 | 2 = 3 = everywhere
        var here = 0;
        // Root?
        if(!this.parent){
            here |= 1;
        }
        // Leaf?
        if(this.parent || !hasMultiRole){
            here |= 2;
        }
        
        // Used later in _bindAxes as well.
        this._axisCreateHere = here;
        
        this._axisCreationOrder.forEach(function(type){
            // Create **here** ?
            if((this._axisCreateWhere[type] & here) !== 0){
                var AxisClass;
                var dataCellsByAxisIndex = dataCellsByAxisTypeThenIndex[type];
                if(dataCellsByAxisIndex){
                    
                    AxisClass = this._axisClassByType[type];
                    if(AxisClass){
                        dataCellsByAxisIndex.forEach(function(dataCells, axisIndex){
                            
                            new AxisClass(this, type, axisIndex);
                            
                        }, this);
                    }
                } else if(this._axisCreateIfUnbound[type]){
                    AxisClass = this._axisClassByType[type];
                    if(AxisClass){
                        new AxisClass(this, type, 0);
                    }
                }
            }
        }, this);
        
        if(this.parent){
            // Copy axes that exist in root and not here
            this.root.axesList.forEach(function(axis){
                if(!def.hasOwn(this.axes, axis.id)){
                    this._addAxis(axis);
                }
            }, this);
        }
    },
    
    _fixTrendsLabel: function(dataCellsByAxisTypeThenIndex){
        // Pre-register the label of the first trend type 
        // in the "trend" data part atom, cause in multi-charts
        // an empty label would be registered first...
        // We end up using this to 
        // allow to specify an alternate label for the trend.
        var dataPartDimName = this._getDataPartDimName();
        if(dataPartDimName){
            // Find the first data cell with a trend type
            var firstDataCell = def
                .query(def.ownKeys(dataCellsByAxisTypeThenIndex))
                .selectMany(function(axisType){
                    return dataCellsByAxisTypeThenIndex[axisType];
                })
                .selectMany()
                .first (function(dataCell){ return !!dataCell.trend; })
                ;
            
            if(firstDataCell){
                var trendInfo = pvc.trends.get(firstDataCell.trend.type);
                var dataPartAtom = trendInfo.dataPartAtom;
                var trendLabel = firstDataCell.trend.label;
                if(trendLabel === undefined){
                    trendLabel = dataPartAtom.f;
                }
                
                this._firstTrendAtomProto = {
                    v: dataPartAtom.v,
                    f: trendLabel
                };
            } else {
                delete this._firstTrendAtomProto;
            }
        }
    },
    
    /**
     * Adds an axis to the chart.
     * 
     * @param {pvc.visual.Axis} axis The axis.
     *
     * @type pvc.visual.Axis
     */
    _addAxis: function(axis){
        
        this.axes[axis.id] = axis;
        if(axis.chart === this){
            axis.axisIndex = this.axesList.length;
        }
        
        this.axesList.push(axis);
        
        var typeAxes  = def.array.lazy(this.axesByType, axis.type);
        var typeIndex = typeAxes.count || 0;
        axis.typeIndex = typeIndex;
        typeAxes[axis.index] = axis;
        if(!typeIndex){
            typeAxes.first = axis;
        }
        typeAxes.count = typeIndex + 1;
        
        // For child charts, that simply copy color axes
        if(axis.type === 'color' && axis.isBound()){
            this._onColorAxisScaleSet(axis);
        }
        
        return this;
    },
    
    _getAxis: function(type, index){
        var typeAxes = this.axesByType[type];
        if(typeAxes && index != null && (+index >= 0)){
            return typeAxes[index];
        }
    },
    
    _bindAxes: function(/*hasMultiRole*/){
        // Bind all axes with dataCells registered in #_dataCellsByAxisTypeThenIndex
        // and which were created **here**
        var here = this._axisCreateHere;
        
        def.eachOwn(
            this._dataCellsByAxisTypeThenIndex, 
            function(dataCellsByAxisIndex, type) {
                // Should create **here** ?
                if((this._axisCreateWhere[type] & here)) {
                    dataCellsByAxisIndex.forEach(function(dataCells, index) {
                        var axis = this.axes[pvc.buildIndexedId(type, index)];
                        if(!axis.isBound()) { axis.bind(dataCells); }
                    }, this);
                }
            }, 
            this);
    },
    
    _setAxesScales: function(/*isMulti*/) {
        if(!this.parent) {
            var colorAxes = this.axesByType.color;
            if(colorAxes) {
                colorAxes.forEach(function(axis) {
                    if(axis.isBound()) {
                        this._createColorAxisScale(axis);
                        this._onColorAxisScaleSet (axis);
                    }
                }, this);
            }
        }
    },
    
    /**
     * Creates a scale for a given axis, with domain applied, but no range yet.
     * Assigns the scale to the axis.
     * 
     * @param {pvc.visual.Axis} axis The axis.
     * @type pv.Scale
     */
    _createAxisScale: function(axis) {
        var scale = this._createScaleByAxis(axis);
        if(scale.isNull && pvc.debug >= 3){
            this._log(def.format("{0} scale for axis '{1}'- no data", [axis.scaleType, axis.id]));
        }
        
        return axis.setScale(scale).scale;
    },
    
    /**
     * Creates a scale for a given axis.
     * Only the scale's domain is set.
     * 
     * @param {pvc.visual.Axis} axis The axis.
     * @type pv.Scale
     */
    _createScaleByAxis: function(axis){
        var createScale = this['_create' + def.firstUpperCase(axis.scaleType) + 'ScaleByAxis'];
        
        return createScale.call(this, axis);
    },
    
    /**
     * Creates a discrete scale for a given axis.
     * 
     * @param {pvc.visual.Axis} axis The axis.
     * @virtual
     * @type pv.Scale
     */
    _createDiscreteScaleByAxis: function(axis){
        /* DOMAIN */

        // With composite axis, only 'singleLevel' flattening works well
        var dataPartValues = 
            axis.
            dataCells.
            map(function(dataCell){ return dataCell.dataPartValue; });
        
        var baseData = this.visibleData(dataPartValues, {ignoreNulls: false});
        var data = baseData && axis.role.flatten(baseData);
        
        var scale = new pv.Scale.ordinal();
        if(!data || !data.count()){
            scale.isNull = true;
        } else {
            var values = data.children()
                             .select(function(child){ return def.nullyTo(child.value, ""); })
                             .array();
            
            scale.domain(values);
        }
        
        return scale;
    },
    
    /**
     * Creates a continuous time-series scale for a given axis.
     * 
     * @param {pvc.visual.Axis} axis The axis.
     * @virtual
     * @type pv.Scale
     */
    _createTimeSeriesScaleByAxis: function(axis){
        /* DOMAIN */
        var extent = this._getContinuousVisibleExtent(axis); // null when no data...
        
        var scale = new pv.Scale.linear();
        if(!extent){
            scale.isNull = true;
        } else {
            var dMin = extent.min;
            var dMax = extent.max;

            if((dMax - dMin) === 0) {
                dMax = new Date(dMax.getTime() + 3600000); // 1 h
            }
        
            scale.domain(dMin, dMax);
            scale.minLocked = extent.minLocked;
            scale.maxLocked = extent.maxLocked;
        }
        
        return scale;
    },

    /**
     * Creates a continuous numeric scale for a given axis.
     *
     * @param {pvc.visual.Axis} axis The axis.
     * @virtual
     * @type pv.Scale
     */
    _createNumericScaleByAxis: function(axis) {
        /* DOMAIN */
        var extent = this._getContinuousVisibleExtentConstrained(axis);
        
        var scale = new pv.Scale.linear();
        if(!extent) {
            scale.isNull = true;
        } else {
            var tmp;
            var dMin = extent.min;
            var dMax = extent.max;
            var epsi = 1e-10;
            
            var normalize = function() {
                var d = dMax - dMin;
                
                // very close to zero delta (<0 or >0) 
                // is turned into 0 delta
                if(d && Math.abs(d) <= epsi) {
                    dMin = (dMin + dMax) / 2;
                    dMin = dMax = +dMin.toFixed(10);
                    d = 0;
                }
                
                // zero delta?
                if(!d) {
                    // Adjust *all* that are not locked, or, if all locked, max
                    if(!extent.minLocked) {
                        dMin = Math.abs(dMin) > epsi ? (dMin * 0.99) : -0.1;
                    }
                    
                    // If both are locked, ignore max lock!
                    if(!extent.maxLocked || extent.minLocked) {
                        dMax = Math.abs(dMax) > epsi ? (dMax * 1.01) : +0.1;
                    }
                } else if(d < 0) {
                    // negative delta, bigger than epsi
                    
                    // adjust max if it is not locked, or
                    // adjust min if it is not locked, or
                    // adjust max (all locked)
                    
                    if(!extent.maxLocked || extent.minLocked) {
                        dMax = Math.abs(dMin) > epsi ? dMin * 1.01 : +0.1;
                    } else /*if(!extent.minLocked)*/{
                        dMin = Math.abs(dMax) > epsi ? dMax * 0.99 : -0.1;
                    }
                }
            };
            
            normalize();
            
            var originIsZero = axis.option('OriginIsZero');
            if(originIsZero) {
                if(dMin === 0) {
                    extent.minLocked = true;
                } else if(dMax === 0) {
                    extent.maxLocked = true;
                } else if((dMin * dMax) > 0) {
                    /* If both negative or both positive
                     * the scale does not contain the number 0.
                     */
                    if(dMin > 0) {
                        if(!extent.minLocked) {
                            extent.minLocked = true;
                            dMin = 0;
                        }
                    } else {
                        if(!extent.maxLocked) {
                            extent.maxLocked = true;
                            dMax = 0;
                        }
                    }
                }
            }

            normalize();
            
            scale.domain(dMin, dMax);
            scale.minLocked = extent.minLocked;
            scale.maxLocked = extent.maxLocked;
        }
        
        return scale;
    },
        
    _warnSingleContinuousValueRole: function(valueRole){
        if(!valueRole.grouping.isSingleDimension) {
            this._warn("A linear scale can only be obtained for a single dimension role.");
        }
        
        if(valueRole.grouping.isDiscrete()) {
            this._warn(def.format("The single dimension of role '{0}' should be continuous.", [valueRole.name]));
        }
    },
    
    /**
     * @virtual
     */
    _getContinuousVisibleExtentConstrained: function(axis, min, max){
        var minLocked = false;
        var maxLocked = false;
        
        if(min == null) {
            min = axis.option('FixedMin');
            minLocked = (min != null);
        }
        
        if(max == null) {
            max = axis.option('FixedMax');
            maxLocked = (max != null);
        }
        
        if(min == null || max == null) {
            var baseExtent = this._getContinuousVisibleExtent(axis); // null when no data
            if(!baseExtent){
                return null;
            }
            
            if(min == null){
                min = baseExtent.min;
            }
            
            if(max == null){
                max = baseExtent.max;
            }
        }
        
        return {min: min, max: max, minLocked: minLocked, maxLocked: maxLocked};
    },
    
    /**
     * Gets the extent of the values of the specified axis' roles
     * over all datums of the visible data.
     * 
     * @param {pvc.visual.Axis} valueAxis The value axis.
     * @type object
     *
     * @protected
     * @virtual
     */
    _getContinuousVisibleExtent: function(valueAxis){
        
        var dataCells = valueAxis.dataCells;
        if(dataCells.length === 1){
            // Most common case is faster
            return this._getContinuousVisibleCellExtent(valueAxis, dataCells[0]);
        }
        
        // This implementation takes the union of 
        // the extents of each data cell.
        // Even when a data cell has multiple data parts, 
        // it is evaluated as a whole.
        
        return def
            .query(dataCells)
            .select(function(dataCell){
                return this._getContinuousVisibleCellExtent(valueAxis, dataCell);
            }, this)
            .reduce(pvc.unionExtents, null);
    },

    /**
     * Gets the extent of the values of the specified role
     * over all datums of the visible data.
     *
     * @param {pvc.visual.Axis} valueAxis The value axis.
     * @param {pvc.visual.Role} valueDataCell The data cell.
     * @type object
     *
     * @protected
     * @virtual
     */
    _getContinuousVisibleCellExtent: function(valueAxis, valueDataCell){
        var valueRole = valueDataCell.role;
        
        this._warnSingleContinuousValueRole(valueRole);

        if(valueRole.name === 'series') {
            /* not supported/implemented? */
            throw def.error.notImplemented();
        }
        
        var useAbs = valueAxis.scaleUsesAbs();
        var data  = this.visibleData(valueDataCell.dataPartValue); // [ignoreNulls=true]
        var extent = data && data
            .dimensions(valueRole.firstDimensionName())
            .extent({ abs: useAbs });
        
        if(extent){
            var minValue = extent.min.value;
            var maxValue = extent.max.value;
            return {
                min: (useAbs ? Math.abs(minValue) : minValue), 
                max: (useAbs ? Math.abs(maxValue) : maxValue) 
            };
        }
    },
    
    // -------------
    
    _createColorAxisScale: function(axis){
        var setScaleArgs;
        var dataCells = axis.dataCells;
        if(dataCells) {
            var me = this;
            if(axis.scaleType === 'discrete') {
                setScaleArgs = this._createDiscreteColorAxisScale(axis);
            } else {
                setScaleArgs = this._createContinuousColorAxisScale(axis); // may return == null
            }
        }
        
        return axis.setScale.apply(axis, setScaleArgs);
    },
    
    _createDiscreteColorAxisScale: function(axis) {
        // Discrete
        // -> Local Scope
        // -> Visible or Not
        var domainValues = 
            def
            .query(axis.dataCells)
            .selectMany(function(dataCell) {
                // TODO: this does not work on trend datapart data
                // when in multicharts. DomainItemDatas are not yet created.
                return dataCell.domainItemValues();
            })
            .array();
        
        axis.domainValues = domainValues;
        
        // Call the transformed color scheme with the domain values
        //  to obtain a final scale object
        return [axis.scheme()(domainValues), /*noWrap*/ true];
    },
    
    _createContinuousColorAxisScale: function(axis) {
        if(axis.dataCells.length === 1){ // TODO: how to handle more?
            // Single Continuous
            // -> Global Scope
            // -> Visible only
            this._warnSingleContinuousValueRole(axis.role);
            
            var visibleDomainData = this.root.visibleData(axis.dataCell.dataPartValue); // [ignoreNulls=true]
            var normByCateg = axis.option('NormByCategory');
            var scaleOptions = {
                type:        axis.option('ScaleType'),
                colors:      axis.option('Colors')().range(), // obtain the underlying colors array
                colorDomain: axis.option('Domain'), 
                colorMin:    axis.option('Min'),
                colorMax:    axis.option('Max'),
                colorMissing:axis.option('Missing'), // TODO: already handled by the axis wrapping
                data:        visibleDomainData,
                colorDimension: axis.role.firstDimensionName(),
                normPerBaseCategory: normByCateg
            };
            
            if(!normByCateg){
                return [pvc_colorScale(scaleOptions)];
            }
            
            axis.scalesByCateg = pvc_colorScales(scaleOptions);
            // no single scale...
        }
        
        return [];
    },
    
    _onColorAxisScaleSet: function(axis){
        switch(axis.index){
            case 0:
                this.colors = axis.scheme();
                break;
            
            case 1:
                if(this._allowV1SecondAxis){
                    this.secondAxisColor = axis.scheme();
                }
                break;
        }
    },
    
    /**
     * Obtains an unified color scale, 
     * of all the color axes with specified colors.
     * 
     * This color scale is used to satisfy axes
     * with non-specified colors.
     * 
     * Each color-role has a different unified color-scale,
     * so that the color keys are of the same types.
     */
    _getRoleColorScale: function(roleName){
        return def.lazy(
            def.lazy(this, '_rolesColorScale'),
            roleName,
            this._createRoleColorScale, this);
    },
    
    _createRoleColorScale: function(roleName){
        var firstScale, scale;
        var valueToColorMap = {};
        
        this.axesByType.color.forEach(function(axis){
            // Only use color axes with specified Colors
            var axisRole = axis.role;
            var isRoleCompatible = 
                (axisRole.name === roleName) ||
                (axisRole.sourceRole && axisRole.sourceRole.name === roleName);
            
            if(isRoleCompatible &&
               axis.scale &&
               (axis.index === 0 || 
               axis.option.isSpecified('Colors') || 
               axis.option.isSpecified('Map'))){
                
                scale = axis.scale;
                if(!firstScale){ firstScale = scale; }
                
                axis.domainValues.forEach(addDomainValue);
            }
        }, this);
        
        function addDomainValue(value){
            // First color wins
            var key = '' + value;
            if(!def.hasOwnProp.call(valueToColorMap, key)){
                valueToColorMap[key] = scale(value);
            }
        }
        
        if(!firstScale){
            return pvc.createColorScheme()();
        }
        
        scale = function(value){
            var key = '' + value;
            if(def.hasOwnProp.call(valueToColorMap, key)){
                return valueToColorMap[key];
            }
            
            // creates a new entry...
            var color = firstScale(value);
            valueToColorMap[key] = color;
            return color;
        };
        
        def.copy(scale, firstScale); // TODO: domain() and range() should be overriden...
        
        return scale;
    },
    
    _onLaidOut: function(){
        // NOOP
    }
});



pvc.BaseChart
.add({
    /**
     * The base panel is the root container of a chart.
     * <p>
     * The base panel of a <i>root chart</i> is the top-most root container.
     * It has {@link pvc.BasePanel#isTopRoot} equal to <tt>true</tt>.
     * </p>
     * <p>
     * The base panel of a <i>non-root chart</i> is the root of the chart's panels,
     * but is not the top-most root panel, over the charts hierarchy.
     * </p>
     * 
     * @type pvc.BasePanel
     */
    basePanel:   null,
    
    /**
     * The panel that shows the chart's title.
     * <p>
     * This panel is the first child of {@link #basePanel} to be created.
     * It is only created when the chart has a non-empty title.
     * </p>
     * <p>
     * Being the first child causes it to occupy the 
     * whole length of the side of {@link #basePanel} 
     * to which it is <i>docked</i>.
     * </p>
     * 
     * @type pvc.TitlePanel
     */
    titlePanel:  null,
    
    /**
     * The panel that shows the chart's main legend.
     * <p>
     * This panel is the second child of {@link #basePanel} to be created.
     * There is an option to not show the chart's legend,
     * in which case this panel is not created.
     * </p>
     * 
     * <p>
     * The current implementation of the legend panel
     * presents a <i>discrete</i> association of colors and labels.
     * </p>
     * 
     * @type pvc.LegendPanel
     */
    legendPanel: null,
    
    /**
     * The panel that hosts child chart's base panels.
     * 
     * @type pvc.MultiChartPanel
     */
    _multiChartPanel: null,
    
    _initChartPanels: function(hasMultiRole) {
        this._initBasePanel ();
        this._initTitlePanel();
        
        var isMultichartRoot = hasMultiRole && !this.parent;
        
        // null on small charts or when not enabled
        var legendPanel = this._initLegendPanel();
        
        // Is multi-chart root?
        if(isMultichartRoot) { this._initMultiChartPanel(); }
        
        if(legendPanel) { this._initLegendScenes(legendPanel); }
        
        if(!isMultichartRoot) {
            var o = this.options;
            this._preRenderContent({
                margins:           hasMultiRole ? o.smallContentMargins  : o.contentMargins,
                paddings:          hasMultiRole ? o.smallContentPaddings : o.contentPaddings,
                clickAction:       o.clickAction,
                doubleClickAction: o.doubleClickAction
            });
        }
    },
    
    /**
     * Override to create chart specific content panels here.
     * No need to call base.
     * 
     * @param {object} contentOptions Object with content specific options. Can be modified.
     * @param {pvc.Sides} [contentOptions.margins] The margins for the content panels. 
     * @param {pvc.Sides} [contentOptions.paddings] The paddings for the content panels.
     * @virtual
     */
    _preRenderContent: function(/*contentOptions*/) { /* NOOP */ },
    
    /**
     * Creates and initializes the base panel.
     */
    _initBasePanel: function() {
        var p = this.parent;
        
        this.basePanel = new pvc.BasePanel(this, p && p._multiChartPanel, {
            margins:  this.margins,
            paddings: this.paddings,
            size:     {width: this.width, height: this.height}
        });
    },
    
    /**
     * Creates and initializes the title panel,
     * if the title is specified.
     */
    _initTitlePanel: function() {
        var me = this;
        var o = me.options;
        var title = o.title;
        if (!def.empty(title)) { // V1 depends on being able to pass "   " spaces... 
            var isRoot = !me.parent;
            this.titlePanel = new pvc.TitlePanel(me, me.basePanel, {
                title:        title,
                font:         o.titleFont,
                anchor:       o.titlePosition,
                align:        o.titleAlign,
                alignTo:      o.titleAlignTo,
                offset:       o.titleOffset,
                keepInBounds: o.titleKeepInBounds,
                margins:      o.titleMargins,
                paddings:     o.titlePaddings,
                titleSize:    o.titleSize,
                titleSizeMax: o.titleSizeMax
            });
        }
    },
    
    /**
     * Creates and initializes the legend panel,
     * if the legend is active.
     */
    _initLegendPanel: function() {
        var o = this.options;
        // global legend(s) switch
        if (o.legend) { // legend is disabled on small charts...
            var legend = new pvc.visual.Legend(this, 'legend', 0);
            
            // TODO: pass all these options to LegendPanel class
            return this.legendPanel = new pvc.LegendPanel(this, this.basePanel, {
                anchor:       legend.option('Position'),
                align:        legend.option('Align'),
                alignTo:      o.legendAlignTo,
                offset:       o.legendOffset,
                keepInBounds: o.legendKeepInBounds,
                size:         legend.option('Size'),
                sizeMax:      legend.option('SizeMax'),
                margins:      legend.option('Margins'),
                paddings:     legend.option('Paddings'),
                font:         legend.option('Font'),
                scenes:       def.getPath(o, 'legend.scenes'),
                
                // Bullet legend
                textMargin:   o.legendTextMargin,
                itemPadding:  o.legendItemPadding,
                markerSize:   o.legendMarkerSize
                //shape:        options.legendShape // TODO: <- doesn't this come from the various color axes?
            });
        }
    },
    
    _getLegendBulletRootScene: function() {
        return this.legendPanel && this.legendPanel._getBulletRootScene();
    },
    
    /**
     * Creates and initializes the multi-chart panel.
     */
    _initMultiChartPanel: function() {
        var basePanel = this.basePanel;
        var options = this.options;
        
        this._multiChartPanel = new pvc.MultiChartPanel(
            this, 
            basePanel, 
            {
                margins:  options.contentMargins,
                paddings: options.contentPaddings
            });
        
        this._multiChartPanel.createSmallCharts();
        
        // BIG HACK: force legend to be rendered after the small charts, 
        // to allow them to register legend renderers.
        // Currently is: Title -> Legend -> MultiChart
        // Changes to: MultiChart -> Title -> Legend
        basePanel._children.unshift(basePanel._children.pop());
    },
    
    _coordinateSmallChartsLayout: function(/*scopesByType*/) {},
    
    /**
     * Creates the legend group scenes of a chart.
     *
     * The default implementation creates
     * one legend group per each data cell of each color axis.
     * 
     * One legend item per domain data value of each data cell.
     */
    _initLegendScenes: function(legendPanel) {
        // For all color axes...
        var colorAxes = this.axesByType.color;
        if(!colorAxes) { return; }
        
        var rootScene;
        var legendIndex = 0; // always start from 0 (whatever the color axis index)
        var dataPartDimName = this._getDataPartDimName();
        
        def
        .query(colorAxes)
        .where(function(axis) { return axis.option('LegendVisible'); })
        .each (function(axis) {
            if(axis.dataCells) {
                axis.dataCells.forEach(function(dataCell) {
                    if(dataCell.role.isDiscrete()) { createLegendGroup(dataCell, axis); }
                });
            }
        });
        
        function createLegendGroup(dataCell, colorAxis) {
            var isToggleVisible = colorAxis.option('LegendClickMode') === 'togglevisible';
            
            var domainData = dataCell.domainData();
            
            if(!rootScene) { rootScene = legendPanel._getBulletRootScene(); }
            
            // Trend series cannot be set to invisible.
            // They are created each time that visible changes.
            // So trend legend groups are created locked (clicMode = 'none')
            var clickMode;
            if(isToggleVisible) {
                var dataPartAtom = domainData.atoms[dataPartDimName];
                if(dataPartAtom && dataPartAtom.value === 'trend') {
                    clickMode = 'none';
                }
            }
            
            var groupScene = rootScene.createGroup({
                source:          domainData,
                colorAxis:       colorAxis,
                clickMode:       clickMode,
                extensionPrefix: pvc.buildIndexedId('', legendIndex++)
             });
            
            // For later binding an appropriate bullet renderer
            dataCell.legendBulletGroupScene = groupScene;
            
            // Create one item scene per domain item data
            dataCell
            .domainItemDatas()
            .each(function(itemData) { 
                var itemScene  = groupScene.createItem({source: itemData});
                var colorValue = dataCell.domainItemDataValue(itemData);
                
                // TODO: HACK...
                itemScene.color = colorAxis.scale(colorValue);
            });
        }
    }
});



pvc.BaseChart
.add({
    _updateSelectionSuspendCount: 0,
    _lastSelectedDatums: null,

    /**
     * Clears any selections and, if necessary,
     * re-renders the parts of the chart that show selected marks.
     *
     * @type undefined
     * @virtual
     */
    clearSelections: function() {
        if(this.data.owner.clearSelected()) { this.updateSelections(); }
        return this;
    },

    _updatingSelections: function(method, context) {
        this._suspendSelectionUpdate();

        try     { method.call(context || this);  }
        finally { this._resumeSelectionUpdate(); }
    },

    _suspendSelectionUpdate: function() {
        if(this === this.root) { this._updateSelectionSuspendCount++; }
        else                   { this.root._suspendSelectionUpdate(); }
    },

    _resumeSelectionUpdate: function() {
        if(this === this.root) {
            if(this._updateSelectionSuspendCount > 0) {
                if(!(--this._updateSelectionSuspendCount)) { this.updateSelections(); }
            }
        } else {
            this.root._resumeSelectionUpdate();
        }
    },

    /**
     * Re-renders the parts of the chart that show marks.
     *
     * @type undefined
     * @virtual
     */
    updateSelections: function(keyArgs) {
        if(this === this.root) {
            if(this._inUpdateSelections || this._updateSelectionSuspendCount) { return this; }

            var selectedChangedDatumMap = this._calcSelectedChangedDatums();
            if(!selectedChangedDatumMap) { return this; }

            pvc.removeTipsyLegends();

            // Reentry control
            this._inUpdateSelections = true;
            try {
                // Fire action
                var action = this.options.selectionChangedAction;
                if(action) {
                    // Can change selection further...although it's probably
                    // better to do that in userSelectionAction, called
                    // before chosen datums' selected state is actually affected.
                    var selectedDatums = this.data.selectedDatums();
                    var selectedChangedDatums = selectedChangedDatumMap.values();
                    action.call(
                        this.basePanel.context(),
                        selectedDatums,
                        selectedChangedDatums);
                }

                // Rendering afterwards allows the action to change the selection in between
                if(def.get(keyArgs, 'render', true)) {
                    this.useTextMeasureCache(function() { this.basePanel.renderInteractive(); }, this);
                }
            } finally {
                this._inUpdateSelections = false;
            }
        } else {
            this.root.updateSelections();
        }

        return this;
    },

    _calcSelectedChangedDatums: function() {
        // Capture currently selected datums
        // Calculate the ones that changed.

        // Caused by NoDataException ?
        if(!this.data) { return; }

        var selectedChangedDatums;
        var nowSelectedDatums  = this.data.selectedDatumMap();
        var lastSelectedDatums = this._lastSelectedDatums;
        if(!lastSelectedDatums) {
            if(!nowSelectedDatums.count) { return; }

            selectedChangedDatums = nowSelectedDatums.clone();
        } else {
            selectedChangedDatums = lastSelectedDatums.symmetricDifference(nowSelectedDatums);

            if(!selectedChangedDatums.count) { return; }
        }

        this._lastSelectedDatums = nowSelectedDatums;

        return selectedChangedDatums;
    },

    _onUserSelection: function(datums) {
        if(!datums || !datums.length) { return datums; }

        if(this === this.root) {
            // Fire action
            var action = this.options.userSelectionAction;
            return action ?
                   (action.call(this.basePanel.context(), datums) || datums) :
                   datums;
        }

        return this.root._onUserSelection(datums);
    }
});



/*global pv_Mark:true */

pvc.BaseChart
.add({
    _processExtensionPoints: function() {
        var components;
        if(!this.parent) {
            var points = this.options.extensionPoints;
            components = {};
            if(points) {
                for(var p in points) {
                    var id, prop;
                    var splitIndex = p.indexOf("_");
                    if(splitIndex > 0) {
                        id   = p.substring(0, splitIndex);
                        prop = p.substr(splitIndex + 1);
                        if(id && prop) {
                            var component = def.getOwn(components, id) ||
                                            (components[id] = new def.OrderedMap());
                            
                            component.add(prop, points[p]);
                        }
                    }
                }
            }
        } else {
            components = this.parent._components;
        }
        
        this._components = components;
    },
    
    extend: function(mark, ids, keyArgs) {
        if(def.array.is(ids)) {
            ids.forEach(function(id) { this._extendCore(mark, id, keyArgs); }, this);
        } else {
            this._extendCore(mark, ids, keyArgs);
        }
    },
    
    _extendCore: function(mark, id, keyArgs) {
        // if mark is null or undefined, skip
        if (mark) {
            var component = def.getOwn(this._components, id);
            if(component){
                if(mark.borderPanel) { mark = mark.borderPanel; }
                
                var logOut     = pvc.debug >= 3 ? [] : null;
                var constOnly  = def.get(keyArgs, 'constOnly', false); 
                var wrap       = mark.wrap;
                var keyArgs2   = {tag: pvc.extensionTag};
                var isRealMark = mark instanceof pv_Mark;
                
                component.forEach(function(v, m) {
                    // Not everything that is passed to 'mark' argument
                    //  is actually a mark...(ex: scales)
                    // Not locked and
                    // Not intercepted and
                    if(mark.isLocked && mark.isLocked(m)) {
                        if(logOut) {logOut.push(m + ": locked extension point!");}
                    } else if(mark.isIntercepted && mark.isIntercepted(m)) {
                        if(logOut) {logOut.push(m + ":" + pvc.stringify(v) + " (controlled)");}
                    } else {
                        if(logOut) {logOut.push(m + ": " + pvc.stringify(v)); }

                        // Extend object css and svg properties
                        if(v != null) {
                            var type = typeof v;
                            if(type === 'object') {
                                if(m === 'svg' || m === 'css') {
                                    var v2 = mark.propertyValue(m);
                                    if(v2) { v = def.copy(v2, v); }
                                }
                            } else if(isRealMark && (wrap || constOnly) && type === 'function') {
                                if(constOnly) { return; }
                                
                                // TODO: "add" extension idiom - any other exclusions?
                                if(m !== 'add') { v = wrap.call(mark, v, m); }
                            }
                        }
                        
                        // Distinguish between mark methods and properties
                        if (typeof mark[m] === "function") {
                            if(m != 'add' && mark.intercept && mark.properties[m]) {
                                mark.intercept(m, v, keyArgs2);
                            } else {
                                // Not really a mark or not a real protovis property 
                                mark[m](v);
                            }
                        } else {
                            mark[m] = v;
                        }
                    }
                });

                if(logOut) {
                    if(logOut.length) {
                        this._log("Applying Extension Points for: '" + id + "'\n\t* " + logOut.join("\n\t* "));
                    } else if(pvc.debug >= 5) {
                        this._log("No Extension Points for: '" + id + "'");
                    }
                }
            }
        } else if(pvc.debug >= 4) {
            this._log("Applying Extension Points for: '" + id + "' (target mark does not exist)");
        }
    },

    /**
     * Obtains the specified extension point.
     */
    _getExtension: function(id, prop) {
        var component;
        if(!def.array.is(id)) {
            component = def.getOwn(this._components, id);
            if(component) { return component.get(prop); }
        } else {
            // Last extension points are applied last, and so have priority...
            var i = id.length - 1, value;
            while(i >= 0) {
                component = def.getOwn(this._components, id[i--]);
                if(component && (value = component.get(prop)) !== undefined) {
                    return value;
                }
            }
        }
    },
    
    _getComponentExtensions: function(id) {
        return def.getOwn(this._components, id);
    },
    
    _getConstantExtension: function(id, prop) {
        var value = this._getExtension(id, prop);
        if(!def.fun.is(value)) { return value; }
    }
});



/*global pvc_Sides:true, pvc_Size:true, pvc_PercentValue:true, pvc_Offset:true */

/**
 * Base panel. 
 * A lot of them will exist here, with some common properties. 
 * Each class that extends pvc.base will be 
 * responsible to know how to use it.
 */
def
.type('pvc.BasePanel', pvc.Abstract)
.add(pvc.visual.Interactive)
.init(function(chart, parent, options) {
    
    this.chart = chart; // must be set before base() because of log init
    
    this.base();
    
    this.axes = {};
    
    if(options){
        if(options.scenes){
            this._sceneTypeExtensions = options.scenes;
            delete options.scenes;
        }
        
        var axes = options.axes;
        if(axes){
            def.copy(this.axes, axes);
            delete options.axes;
        }
    }
    
    // TODO: Danger...
    $.extend(this, options); // clickAction and doubleClickAction are set here

    if(!this.axes.color){
        this.axes.color = chart.axes.color;
    }
    
    this.position = {
        /*
        top:    0,
        right:  0,
        bottom: 0,
        left:   0
        */
    };
    
    var margins = options && options.margins;
    if(!parent && margins === undefined){
        // TODO: FIXME: Give a default margin on the root panel
        //  because otherwise borders of panels may be clipped..
        // Even now that the box model supports borders,
        // the "plot" panel still gets drawn outside
        // cause it is drawn over? See the box plot...
        // The rubber band also should take its border into account
        //  to not be drawn off...
        margins = 3;
    }
    
    this.margins  = new pvc_Sides(margins);
    this.paddings = new pvc_Sides(options && options.paddings);
    this.size     = new pvc_Size (options && options.size    );
    this.sizeMax  = new pvc_Size (options && options.sizeMax );
    
    if(!parent) {
        this.parent    = null;
        this.root      = this;
        this.topRoot   = this;
        this.isRoot    = true;
        this.isTopRoot = true;
        this._ibits    = chart._ibits;
        
    } else {
        this.parent    = parent;
        this.isTopRoot = false;
        this.isRoot    = (parent.chart !== chart);
        this.root      = this.isRoot ? this : parent.root;
        this.topRoot   = parent.topRoot;
        this._ibits    = parent._ibits;
        
        if(this.isRoot) {
            this.position.left = chart.left; 
            this.position.top  = chart.top;
        }
        
        parent._addChild(this);
    }
    
    this.data = (this.isRoot ? chart : parent).data;

    /* Root panels do not need layout */
    if(this.isRoot) {
        this.anchor  = null;
        this.align   = null;
        this.alignTo = null;
        this.offset  = null;
    } else {
        this.align = pvc.parseAlign(this.anchor, this.align);
        
        // * a string with a named alignTo value
        // * a number
        // * a PercentValue object
        var alignTo = this.alignTo;
        var side = this.anchor;
        if(alignTo != null && alignTo !== '' && (side === 'left' || side === 'right')){
            if(alignTo !== 'page-middle'){
                if(!isNaN(+alignTo.charAt(0))){
                    alignTo = pvc_PercentValue.parse(alignTo); // percent or number
                } else {
                    alignTo = pvc.parseAlign(side, alignTo);
                }
            }
        } else {
            alignTo = this.align;
        }
        
        this.alignTo = alignTo;
        
        this.offset = new pvc_Offset(this.offset);
    }
    
    if(this.borderWidth == null){
        var borderWidth;
        var extensionId = this._getExtensionId();
        if(extensionId){
            var strokeStyle = this._getExtension(extensionId, 'strokeStyle');
            if(strokeStyle != null){
                borderWidth = +this._getConstantExtension(extensionId, 'lineWidth'); 
                if(isNaN(borderWidth) || !isFinite(borderWidth)){
                    borderWidth = null;
                }
            }
        }
        
        this.borderWidth = borderWidth == null ? 0 : 1.5;
    }
    
    // Parent panel may not have a clickAction, 
    // and so, inheriting its clickable and doubleClickable doesn't work.
    var I = pvc.visual.Interactive;
    var ibits = this._ibits;
    ibits = def.bit.set(ibits, I.Clickable,       (chart._ibits & I.Clickable      ) && !!this.clickAction      );
    ibits = def.bit.set(ibits, I.DoubleClickable, (chart._ibits & I.DoubleClickable) && !!this.doubleClickAction);
    this._ibits = ibits;
})
.add({
    chart: null,
    parent: null,
    _children: null,
    type: pv.Panel, // default one
    
    _extensionPrefix: '',
    
    _rubberSelectableMarks: null,

    /**
     * Total height of the panel in pixels.
     * Includes vertical paddings and margins.
     * @type number  
     */
    height: null,
    
    /**
     * Total width of the panel in pixels.
     * Includes horizontal paddings and margins.
     * @type number
     */
    width: null,
    
    /**
     * The static effective border width of the panel.
     * 
     * If a constant extension point exists,
     * its value is used to initialize this property.
     * 
     * If an extension point exists for the <tt>strokeStyle</tt> property,
     * and its value is not null, 
     * the width, taken from the extension point, or defaulted, is considered.
     * Otherwise, the effective width is 0.
     * 
     * The default active value is <tt>1.5</tt>.
     * 
     * @type number
     */
    borderWidth: null,
    
    anchor: "top",
    
    pvPanel: null, // padding/client pv panel (within border box, separated by paddings)
    
    margins:   null,
    paddings:  null,
    
    isRoot:    false,
    isTopRoot: false,
    root:      null, 
    topRoot:   null,
    
    _layoutInfo: null, // once per layout info
    
    _signs: null,
    
    /**
     * The data that the panel uses to obtain "data".
     * @type pvc.data.Data
     */
    data: null,

    dataPartValue: null,
    
    /**
     * Indicates if the top root panel is rendering with animation
     * and, if so, the current phase of animation.
     * 
     * <p>This property can assume the following values:</p>
     * <ul>
     * <li>0 - Not rendering with animation (may even not be rendering at all).</li>
     * <li>1 - Rendering the animation's <i>start</i> point,</li>
     * <li>2 - Rendering the animation's <i>end</i> point.</li>
     * </ul>
     * 
     * @see #animate
     * @see #animatingStart
     * 
     * @type number
     */
    _animating: 0,
    
    _selectingByRubberband: false,
    
    /**
     * Indicates the name of the role that should be used whenever a V1 dimension value is required.
     * Only the first dimension of the specified role is considered.
     * <p>
     * In a derived class use {@link Object.create} to override this object for only certain
     * v1 dimensions.
     * </p>
     * @ type string
     */
    _v1DimRoleName: {
        'series':   'series',
        'category': 'category',
        'value':    'value'
    },
    
    _sceneTypeExtensions: null,
    
    clickAction:       null,
    doubleClickAction: null,
    
    compatVersion: function(options){
        return this.chart.compatVersion(options);
    },
    
    _createLogInstanceId: function(){
        return "" + 
               this.constructor + this.chart._createLogChildSuffix();
    },
    
    defaultVisibleBulletGroupScene: function() {
        // Return legendBulletGroupScene, 
        // from the first data cell of same dataPartValue and 
        // having one legendBulletGroupScene.
        var colorAxis = this.axes.color;
        if(colorAxis && colorAxis.option('LegendVisible')) {
            var dataPartValue = this.dataPartValue;
            return def
                .query (colorAxis.dataCells)
                .where (function(dataCell) { return dataCell.dataPartValue === dataPartValue; })
                .select(function(dataCell) { return dataCell.legendBulletGroupScene; })
                .first (def.truthy);
        }
        
        return null;
    },
    
    _getLegendBulletRootScene: function(){
        return this.chart._getLegendBulletRootScene();
    },
    
    /**
     * Adds a panel as child.
     */
    _addChild: function(child){
        // <Debug>
        /*jshint expr:true */
        child.parent === this || def.assert("Child has a != parent.");
        // </Debug>
        
        (this._children || (this._children = [])).push(child);
    },
    
    _addSign: function(sign){
        def.array.lazy(this, '_signs').push(sign);
        if(sign.selectableByRubberband()){
            def.array.lazy(this, '_rubberSelectableMarks').push(sign.pvMark);
        }
    },

    visibleData: function(ka) { return this.chart.visibleData(this.dataPartValue, ka); },

    partData: function() { return this.chart.partData(this.dataPartValue); },
    
    /* LAYOUT PHASE */
    
    /** 
     * Calculates and sets its size,
     * taking into account a specified total size.
     * 
     * @param {pvc.Size} [availableSize] The total size available for the panel.
     * <p>
     * On root panels this argument is not specified,
     * and the panels' current {@link #width} and {@link #height} are used as default. 
     * </p>
     * @param {object}  [ka] Keyword arguments.
     * @param {boolean} [ka.force=false] Indicates that the layout should be
     * performed even if it has already been done.
     * @param {pvc.Size} [ka.referenceSize] The size that should be used for 
     * percentage size calculation. 
     * This will typically be the <i>client</i> size of the parent.
     * @param {pvc.Sides} [ka.paddings] The paddings that should be used for 
     * the layout. Default to the panel's paddings {@link #paddings}.
     * @param {pvc.Sides} [ka.margins] The margins that should be used for 
     * the layout. Default to the panel's margins {@link #margins}.
     * @param {boolean} [ka.canChange=true] Whether this is a last time layout. 
     */
    layout: function(availableSize, ka){
        if(!this._layoutInfo || def.get(ka, 'force', false)) {
            
            var referenceSize = def.get(ka, 'referenceSize');
            if(!referenceSize && availableSize){
                referenceSize = def.copyOwn(availableSize);
            }
            
            // Does this panel have a **desired** fixed size specified?
            
            // * size may have no specified components 
            // * referenceSize may be null
            var desiredSize = this.size.resolve(referenceSize);
            var sizeMax     = this.sizeMax.resolve(referenceSize);
            
            if(!availableSize) {
                if(desiredSize.width == null || desiredSize.height == null){
                    throw def.error.operationInvalid("Panel layout without width or height set.");
                }
                
                availableSize = def.copyOwn(desiredSize);
            }
            
            if(!referenceSize && availableSize){
                referenceSize = def.copyOwn(availableSize);
            }
            
            // Apply max size to available size
            if(sizeMax.width != null && availableSize.width > sizeMax.width){
                availableSize.width = sizeMax.width;
            }
            
            if(sizeMax.height != null && availableSize.height > sizeMax.height){
                availableSize.height = sizeMax.height;
            }
            
            var halfBorder   = this.borderWidth / 2;
            var realMargins  = (def.get(ka, 'margins' ) || this.margins ).resolve(referenceSize);
            var realPaddings = (def.get(ka, 'paddings') || this.paddings).resolve(referenceSize);
            
            var margins  = pvc_Sides.inflate(realMargins,  halfBorder);
            var paddings = pvc_Sides.inflate(realPaddings, halfBorder);
            
            var spaceWidth  = margins.width  + paddings.width;
            var spaceHeight = margins.height + paddings.height;
            
            var availableClientSize = new pvc_Size(
                    Math.max(availableSize.width  - spaceWidth,  0),
                    Math.max(availableSize.height - spaceHeight, 0)
                );
            
            var desiredClientSize = def.copyOwn(desiredSize);
            if(desiredClientSize.width != null){
                desiredClientSize.width = Math.max(desiredClientSize.width - spaceWidth, 0);
            }
            
            if(desiredClientSize.height != null){
                desiredClientSize.height = Math.max(desiredClientSize.height - spaceHeight, 0);
            }
            
            var prevLayoutInfo = this._layoutInfo || null;
            var canChange = def.get(ka, 'canChange', true);
            
            var layoutInfo = 
                this._layoutInfo = {
                    canChange:         canChange,
                    referenceSize:     referenceSize,
                    
                    realMargins:       realMargins,
                    realPaddings:      realPaddings,
                    
                    borderWidth:       this.borderWidth,
                    
                    margins:           margins,
                    paddings:          paddings,
                    
                    desiredClientSize: desiredClientSize,
                    clientSize:        availableClientSize,
                    
                    pageClientSize:    prevLayoutInfo ? prevLayoutInfo.pageClientSize : availableClientSize.clone(),
                    previous:          prevLayoutInfo
                };
            
            if(prevLayoutInfo){
                // Free old memory
                delete prevLayoutInfo.previous;
                delete prevLayoutInfo.pageClientSize;
            }
            
            var clientSize = this._calcLayout(layoutInfo);
            
            var size;
            if(!clientSize){
                size = availableSize; // use all available size
                clientSize = availableClientSize;
            } else {
                layoutInfo.clientSize = clientSize;
                size = {
                    width:  clientSize.width  + spaceWidth,
                    height: clientSize.height + spaceHeight
                };
            }
            
            this.isVisible = (clientSize.width > 0 && clientSize.height > 0);
            
            delete layoutInfo.desiredClientSize;
            
            this.width  = size.width;
            this.height = size.height;
            
            if(!canChange && prevLayoutInfo){
                delete layoutInfo.previous;
            }
            
            if(pvc.debug >= 5){
                this._log("Size       = " + pvc.stringify(size));
                this._log("Margins    = " + pvc.stringify(layoutInfo.margins));
                this._log("Paddings   = " + pvc.stringify(layoutInfo.paddings));
                this._log("ClientSize = " + pvc.stringify(layoutInfo.clientSize));
            }
            
            this._onLaidOut();
        }
    },
    
    _onLaidOut: function(){
        if(this.isRoot){
            this.chart._onLaidOut();
        }
    },
    
    /**
     * Override to calculate panel client size.
     * <p>
     * The default implementation performs a dock layout {@link #layout} on child panels
     * and uses all of the available size. 
     * </p>
     * 
     * @param {object} layoutInfo An object that is supplied with layout information
     * and on which to export custom layout information.
     * <p>
     * This object is later supplied to the method {@link #_createCore},
     * and can thus be used to store any layout by-product
     * relevant for the creation of the protovis marks and
     * that should be cleared whenever a layout becomes invalid.
     * </p>
     * <p>
     * The object is supplied with the following properties:
     * </p>
     * <ul>
     *    <li>referenceSize - size that should be used for percentage size calculation. 
     *        This will typically be the <i>client</i> size of the parent.
     *    </li>
     *    <li>margins - the resolved margins object. All components are present, possibly with the value 0.</li>
     *    <li>paddings - the resolved paddings object. All components are present, possibly with the value 0.</li>
     *    <li>desiredClientSize - the desired fixed client size. Do ignore a null width or height property value.</li>
     *    <li>clientSize - the available client size, already limited by a maximum size if specified.</li>
     * </ul>
     * <p>
     * Do not modify the contents of the objects of 
     * any of the supplied properties.
     * </p>
     * @virtual
     */
    _calcLayout: function(layoutInfo){
        var clientSize;
        var me = this;

        // These are used in layoutCycle
        var margins, remSize, useLog;

        if(me._children) {
            var aolMap = pvc.BasePanel.orthogonalLength;
            var aoMap  = pvc.BasePanel.relativeAnchor;
            var altMap = pvc.BasePanel.leftTopAnchor;
            var aofMap = pvc_Offset.namesSidesToOffset;

            // Classify children

            var fillChildren = [];
            var sideChildren = [];

            me._children.forEach(function(child) {
                var a = child.anchor;
                if(a){ // requires layout
                    if(a === 'fill') {
                        fillChildren.push(child);
                    } else {
                        /*jshint expr:true */
                        def.hasOwn(aoMap, a) || def.fail.operationInvalid("Unknown anchor value '{0}'", [a]);

                        sideChildren.push(child);
                    }
                }
            });

            useLog = pvc.debug >= 5;
            
            // When expanded (see checkChildLayout)
            // a re-layout is performed.
            clientSize = def.copyOwn(layoutInfo.clientSize);
            var childKeyArgs = {
                    force: true,
                    referenceSize: clientSize
                };

            if(useLog){ me._group("CCC DOCK LAYOUT clientSize = " + pvc.stringify(clientSize)); }
            try{
                doMaxTimes(5, layoutCycle, me);
            } finally {
                if(useLog){ me._groupEnd(); }
            }
        }

        /* Return possibly changed clientSize */
        return clientSize;
        
        // --------------------
        function doMaxTimes(maxTimes, fun, ctx){
            var index = 0;
            while(maxTimes--){
                // remTimes = maxTimes
                if(fun.call(ctx, maxTimes, index) === false){
                    return true;
                }
                index++;
            }
            
            return false;
        }
        
        function layoutCycle(remTimes, iteration){
            if(useLog){ me._group("LayoutCycle #" + (iteration + 1) + " (remaining: " + remTimes + ")"); }
            try{
               var canResize = (remTimes > 0);

                // Reset margins and remSize
                // ** Instances we can mutate
                margins = new pvc_Sides(0);
                remSize = def.copyOwn(clientSize);

                // Lay out SIDE child panels
                var child;
                var index = 0;
                var count = sideChildren.length;
                while(index < count){
                    child = sideChildren[index];
                    if(useLog){ me._group("SIDE Child #" + (index + 1) + " at " + child.anchor); }
                    try{
                        if(layoutChild.call(this, child, canResize)){
                            return true; // resized => break
                        }
                    } finally {
                        if(useLog){ me._groupEnd(); }
                    }
                    index++;
                }

                // Lay out FILL child panels
                index = 0;
                count = fillChildren.length;
                while(index < count){
                    child = fillChildren[index];
                    if(useLog){ me._group("FILL Child #" + (index + 1)); }
                    try{
                        if(layoutChild.call(this, child, canResize)){
                            return true; // resized => break
                        }
                    } finally {
                        if(useLog){ me._groupEnd(); }
                    }
                    index++;
                }

                return false; // !resized
            } finally {
                if(useLog){ me._groupEnd(); }
            }
        }
        
        function layoutChild(child, canResize) {
            var resized = false;
            var paddings;
            
            childKeyArgs.canChange = canResize;
            
            doMaxTimes(3, function(remTimes, iteration){
                if(useLog){ me._group("Attempt #" + (iteration + 1)); }
                try{

                    childKeyArgs.paddings  = paddings;
                    childKeyArgs.canChange = remTimes > 0;

                    child.layout(new pvc_Size(remSize), childKeyArgs);
                    if(child.isVisible){
                        resized = checkChildResize.call(this, child, canResize);
                        if(resized){
                            return false; // stop
                        }

                        var requestPaddings = child._layoutInfo.requestPaddings;
                        if(checkPaddingsChanged(paddings, requestPaddings)){
                            paddings = requestPaddings;

                            // Child wants to repeat its layout with != paddings
                            if(remTimes > 0){
                                paddings = new pvc_Sides(paddings);
                                if(useLog){ this._log("Child requested paddings change: " + pvc.stringify(paddings)); }
                                return true; // again
                            }

                            if(pvc.debug >= 2){
                                this._warn("Child requests paddings change but iterations limit has been reached.");
                            }
                            // ignore overflow
                        }

                        // --------

                        positionChild.call(this, child);

                        if(child.anchor !== 'fill'){
                            updateSide.call(this, child);
                        }
                    }

                    return false; // stop
                } finally {
                    if(useLog){ me._groupEnd(); }
                }
            }, this);
            
            return resized;
        }

        function checkPaddingsChanged(paddings, newPaddings){
            if(!newPaddings){
                return false;
            }
            
            // true if stopped, false otherwise
            return def.query(pvc_Sides.names).each(function(side){
                var curPad = (paddings && paddings[side]) || 0;
                var newPad = (newPaddings && newPaddings[side]) || 0;
                 if(Math.abs(newPad - curPad) >= 0.1){
                     // Stop iteration
                     return false;
                 }
            });
        }

        function checkChildResize(child, canResize){
            var resized = false;
            var addWidth = child.width - remSize.width;
            if(addWidth > 0){
                if(pvc.debug >= 3){
                    this._log("Child added width = " + addWidth);
                }
                
                if(!canResize){
                    if(pvc.debug >= 2){
                        this._warn("Child wanted more width, but layout iterations limit has been reached.");
                    }
                } else {
                    resized = true;
                    
                    remSize   .width += addWidth;
                    clientSize.width += addWidth;
                }
            }
            
            var addHeight = child.height - remSize.height;
            if(addHeight > 0){
                if(pvc.debug >= 3){
                    this._log("Child added height =" + addHeight);
                }
                
                if(!canResize){
                    if(pvc.debug >= 2){
                        this._warn("Child wanted more height, but layout iterations limit has been reached.");
                    }
                } else {
                    resized = true;
                    
                    remSize   .height += addHeight;
                    clientSize.height += addHeight;
                }
            }
            
            return resized;
        }
        
        function positionChild(child) {
            var side  = child.anchor;
            var align = child.align;
            var alignTo = child.alignTo;
            var sidePos;
            if(side === 'fill'){
                side = 'left';
                sidePos = margins.left + remSize.width / 2 - (child.width / 2);
                align = alignTo = 'middle';
            } else {
                sidePos = margins[side];
            }
            
            var sideo, sideOPosChildOffset;
            switch(align){
                case 'top':
                case 'bottom':
                case 'left':
                case 'right':
                    sideo = align;
                    sideOPosChildOffset = 0;
                    break;
                
                case 'center':
                case 'middle':
                    // 'left', 'right' -> 'top'
                    // else -> 'left'
                    sideo = altMap[aoMap[side]];
                    
                    // left -> width; top -> height
                    sideOPosChildOffset = - child[aolMap[sideo]] / 2;
                    break;
            }
            
            
            var sideOPosParentOffset;
            var sideOTo;
            switch(alignTo){
                case 'top':
                case 'bottom':
                case 'left':
                case 'right':
                    sideOTo = alignTo;
                    sideOPosParentOffset = (sideOTo !== sideo) ? remSize[aolMap[sideo]] : 0;
                    break;

                case 'center':
                case 'middle':
                    sideOTo = altMap[aoMap[side]];
                    
                    sideOPosParentOffset = remSize[aolMap[sideo]] / 2;
                    break;
                        
                case 'page-center':
                case 'page-middle':
                    sideOTo = altMap[aoMap[side]];
                    
                    var lenProp = aolMap[sideo];
                    var pageLen = Math.min(remSize[lenProp], layoutInfo.pageClientSize[lenProp]);
                    sideOPosParentOffset = pageLen / 2;
                    break;
            }
            
            var sideOPos = margins[sideOTo] + sideOPosParentOffset + sideOPosChildOffset;
            
            var resolvedOffset = child.offset.resolve(remSize);
            if(resolvedOffset){
                sidePos  += resolvedOffset[aofMap[side ]] || 0;
                sideOPos += resolvedOffset[aofMap[sideo]] || 0;
            }
            
            if(child.keepInBounds){
                if(sidePos < 0){
                    sidePos = 0;
                }
                
                if(sideOPos < 0){
                    sideOPos = 0;
                }
            }
            
            child.setPosition(
                    def.set({}, 
                        side,  sidePos,
                        sideo, sideOPos));
        }
        
        // Decreases available size and increases margins
        function updateSide(child) {
            var side   = child.anchor;
            var sideol = aolMap[side];
            var olen   = child[sideol];
            
            margins[side]   += olen;
            remSize[sideol] -= olen;
        }
    },
    
    invalidateLayout: function() {
        this._layoutInfo = null;
        
        if(this._children) {
            this._children.forEach(function(child) {
                child.invalidateLayout();
            });
        }
    },
    
    /** 
     * CREATION PHASE
     * 
     * Where the protovis main panel, and any other marks, are created.
     * 
     * If the layout has not been performed it is so now.
     */
    _create: function(force) {
        if(!this.pvPanel || force) {
            
            this.pvPanel = null;
            
            delete this._signs;
            
            /* Layout */
            this.layout();
            
            if(!this.isVisible) { return; }
            
            if(this.isRoot) { this._creating(); }
            
            var margins  = this._layoutInfo.margins;
            var paddings = this._layoutInfo.paddings;
            
            /* Protovis Panel */
            if(this.isTopRoot) {
                this.pvRootPanel = 
                this.pvPanel = new pv.Panel().canvas(this.chart.options.canvas);
                
                // Ensure there's always a scene, right from the root mark
                var scene = new pvc.visual.Scene(null, {panel: this});
                this.pvRootPanel.lock('data', [scene]);
                
                if(margins.width > 0 || margins.height > 0) {
                    this.pvPanel
                        .width (this.width )
                        .height(this.height);
                    
                    // As there is no parent panel,
                    // the margins cannot be accomplished by positioning
                    // on the parent panel and sizing.
                    // We thus create another panel to be a child of pvPanel
                   
                    this.pvPanel = this.pvPanel.add(pv.Panel);
                }
            } else {
                this.pvPanel = this.parent.pvPanel.add(this.type);
            }
            
            var pvBorderPanel = this.pvPanel;
            
            // Set panel size
            var width  = this.width  - margins.width;
            var height = this.height - margins.height;
            pvBorderPanel
                .width (width)
                .height(height);

            if(pvc.debug >= 15 && (margins.width > 0 || margins.height > 0)) {
                // Outer Box
                (this.isTopRoot ? this.pvRootPanel : this.parent.pvPanel)
                    .add(this.type)
                    .width (this.width)
                    .height(this.height)
                    .left  (this.position.left   != null ? this.position.left   : null)
                    .right (this.position.right  != null ? this.position.right  : null)
                    .top   (this.position.top    != null ? this.position.top    : null)
                    .bottom(this.position.bottom != null ? this.position.bottom : null)
                    .strokeStyle('orange')
                    .lineWidth(1)
                    .strokeDasharray('- .')
                    ;
            }

            // Set panel positions
            var hasPositions = {};
            def.eachOwn(this.position, function(v, side){
                pvBorderPanel[side](v + margins[side]);
                hasPositions[this.anchorLength(side)] = true;
            }, this);
            
            if(!hasPositions.width){
                if(margins.left > 0){
                    pvBorderPanel.left(margins.left);
                }
                if(margins.right > 0){
                    pvBorderPanel.right(margins.right);
                }
            }
            
            if(!hasPositions.height){
                if(margins.top > 0){
                    pvBorderPanel.top(margins.top);
                }
                if(margins.bottom > 0){
                    pvBorderPanel.bottom(margins.bottom);
                }
            }
            
            // Check padding
            if(paddings.width > 0 || paddings.height > 0){
                // We create separate border (outer) and inner (padding) panels
                this.pvPanel = pvBorderPanel.add(pv.Panel)
                    .width (width  - paddings.width )
                    .height(height - paddings.height)
                    .left(paddings.left)
                    .top (paddings.top );
            }
            
            pvBorderPanel.borderPanel  = pvBorderPanel;
            pvBorderPanel.paddingPanel = this.pvPanel;
            
            this.pvPanel.paddingPanel  = this.pvPanel;
            this.pvPanel.borderPanel   = pvBorderPanel;
            
            if(pvc.debug >= 15){
                // Client Box
                this.pvPanel
                    .strokeStyle('lightgreen')
                    .lineWidth(1)
                    .strokeDasharray('- ');

                if(this.pvPanel !== pvBorderPanel){
                    // Border Box
                    pvBorderPanel
                        .strokeStyle('blue')
                        .lineWidth(1)
                        .strokeDasharray('. ');
                }
            }
            
            var extensionId = this._getExtensionId();
//            if(extensionId != null){ // '' is allowed cause this is relative to #_getExtensionPrefix
            // Wrap the panel that is extended with a Panel sign
            new pvc.visual.Panel(this, null, {
                panel:       pvBorderPanel,
                extensionId: extensionId
            });
//            }
            
            /* Protovis marks that are pvc Panel specific,
             * and/or create child panels.
             */
            this._createCore(this._layoutInfo);
            
            /* RubberBand */
            if (this.isTopRoot) {
                this._initRubberBand();
            }

            /* Extensions */
            this.applyExtensions();
            
            /* Log Axes Scales */
            if(this.isRoot && pvc.debug > 5){
                var out = ["SCALES SUMMARY", pvc.logSeparator];
                
                this.chart.axesList.forEach(function(axis){
                    var scale = axis.scale;
                    if(scale){
                        var d = scale.domain && scale.domain();
                        var r = scale.range  && scale.range ();
                        out.push(axis.id);
                        out.push("    domain: " + (!d ? '?' : pvc.stringify(d)));
                        out.push("    range : " + (!r ? '?' : pvc.stringify(r)));
                        
                    }
                }, this);
                
                this._log(out.join("\n"));
            }
        }
    },

    _creating: function(){
        if(this._children) {
            this._children.forEach(function(child){
                child._creating();
            });
        }
    },
    
    /**
     * Override to create specific protovis components for a given panel.
     * 
     * The default implementation calls {@link #_create} on each child panel.
     * 
     * @param {object} layoutInfo The object with layout information 
     * "exported" by {@link #_calcLayout}.
     * 
     * @virtual
     */
    _createCore: function(/*layoutInfo*/){
        if(this._children) {
            this._children.forEach(function(child){
                child._create();
            });
        }
    },
    
    /** 
     * RENDER PHASE
     * 
     * Where protovis components are rendered.
     * 
     * If the creation phase has not been performed it is so now.
     */
    
    /**
     * Renders the top root panel.
     * <p>
     * The render is always performed from the top root panel,
     * independently of the panel on which the method is called.
     * </p>
     * 
     * @param {object} [ka] Keyword arguments.
     * @param {boolean} [ka.bypassAnimation=false] Indicates that animation should not be performed.
     * @param {boolean} [ka.recreate=false] Indicates that the panel and its descendants should be recreated.
     */
    render: function(ka){
        
        if(!this.isTopRoot) {
            return this.topRoot.render(ka);
        }
        
        this._create(def.get(ka, 'recreate', false));
        
        if(!this.isVisible){
            return;
        }
        
        this._onRender();
        
        var options = this.chart.options;
        var pvPanel = this.pvRootPanel;
        
        var animate = this.chart.animatable();
        this._animating = animate && !def.get(ka, 'bypassAnimation', false) ? 1 : 0;
        try {
            // When animating, renders the animation's 'start' point
            pvPanel.render();
            
            // Transition to the animation's 'end' point
            if (this._animating) {
                this._animating = 2;
                
                var me = this;
                pvPanel
                    .transition()
                    .duration(2000)
                    .ease("cubic-in-out")
                    .start(function() {
                        me._animating = 0;
                        me._onRenderEnd(true);
                    });
            } else {
                this._onRenderEnd(false);
            }
        } finally {
            this._animating = 0;
        }
    },
    
    _onRender: function(){
        var renderCallback = this.chart.options.renderCallback;
        if (renderCallback) {
            if(this.compatVersion() <= 1){
                renderCallback.call(this.chart);
            } else {
                var context = this.context();
                renderCallback.call(context, context.scene);
            }
        }
    },
    
    /**
     * Called when a render has ended.
     * When the render performed an animation
     * and the 'animated' argument will have the value 'true'.
     *
     * The default implementation calls each child panel's
     * #_onRenderEnd method.
     * @virtual
     */
    _onRenderEnd: function(animated){
        if(this._children){
            this._children.forEach(function(child){
                child._onRenderEnd(animated);
            });
        }
    },
    
    /**
     * The default implementation renders
     * the marks returned by #_getSelectableMarks, 
     * or this.pvPanel if none is returned (and it has no children)
     * which is generally in excess of what actually requires
     * to be re-rendered.
     * The call is then propagated to any child panels.
     * 
     * @virtual
     */
    renderInteractive: function(){
        if(this.isVisible){
            var pvMarks = this._getSelectableMarks();
            if(pvMarks && pvMarks.length){
                pvMarks.forEach(function(pvMark){ pvMark.render(); });
            } else if(!this._children) {
                this.pvPanel.render();
                return;
            }
            
            if(this._children){
                this._children.forEach(function(child){
                    child.renderInteractive();
                });
            }
        }
    },

    /**
     * Returns an array of marks whose instances are associated to a datum, or null.
     * @virtual
     */
    _getSelectableMarks: function(){
        return this._rubberSelectableMarks;
    },
    
    
    /* ANIMATION */
    
    animate: function(start, end) {
        return (this.topRoot._animating === 1) ? start : end;
    },
    
    /**
     * Indicates if the panel is currently 
     * rendering the animation start phase.
     * <p>
     * Prefer using this function instead of {@link #animate} 
     * whenever its <tt>start</tt> or <tt>end</tt> arguments
     * involve a non-trivial calculation. 
     * </p>
     * 
     * @type boolean
     */
    animatingStart: function() {
        return (this.topRoot._animating === 1);
    },
    
    /**
     * Indicates if the panel is currently 
     * rendering animation.
     * 
     * @type boolean
     */
    animating: function() { return (this.topRoot._animating > 0); },
    
    /* SIZE & POSITION */
    setPosition: function(position){
        for(var side in position){
            if(def.hasOwn(pvc_Sides.namesSet, side)){
                var s = position[side]; 
                if(s === null) {
                    delete this.position[side];
                } else {
                    s = +s; // -> to number
                    if(!isNaN(s) && isFinite(s)){
                        this.position[side] = s;
                    }
                }
            }
        }
    },
    
    createAnchoredSize: function(anchorLength, size){
        if (this.isAnchorTopOrBottom()) {
            return new pvc_Size(size.width, Math.min(size.height, anchorLength));
        } 
        return new pvc_Size(Math.min(size.width, anchorLength), size.height);
    },
    
    /* EXTENSION */
    
    /**
     * Override to apply specific extensions points.
     * @virtual
     */
    applyExtensions: function(){
        if (this._signs) {
            this._signs.forEach(function(sign){
                sign.applyExtensions();
            });
        }
    },

    /**
     * Extends a protovis mark with extension points 
     * having a given panel-relative component id.
     */
    extend: function(mark, id, ka) {
        this.chart.extend(mark, this._makeExtensionAbsId(id), ka);
    },
    
    /**
     * Extends a protovis mark with extension points 
     * having a given absolute component id.
     */
    extendAbs: function(mark, absId, ka) {
        this.chart.extend(mark, absId, ka);
    },
    
    _extendSceneType: function(typeKey, type, names){
        var typeExts = def.get(this._sceneTypeExtensions, typeKey);
        if(typeExts) {
            pvc.extendType(type, typeExts, names);
        }
    },
    
    _absBaseExtId: {abs: 'base'},
    _absSmallBaseExtId: {abs: 'smallBase'},
    
    _getExtensionId: function(){
        if (this.isRoot) {
            return !this.chart.parent ? this._absBaseExtId : this._absSmallBaseExtId;
        }
    },
    
    _getExtensionPrefix: function() { return this._extensionPrefix; },
    
    _makeExtensionAbsId: function(id) {
        return pvc.makeExtensionAbsId(id, this._getExtensionPrefix());
    },
    
    /**
     * Obtains an extension point given its identifier and property.
     */
    _getExtension: function(id, prop) {
        return this.chart._getExtension(this._makeExtensionAbsId(id), prop);
    },
    
    _getExtensionAbs: function(absId, prop) {
        return this.chart._getExtension(absId, prop);
    },

    _getConstantExtension: function(id, prop) {
        return this.chart._getConstantExtension(this._makeExtensionAbsId(id), prop);
    },
    
    // -----------------------------
    
    /**
     * Returns the underlying protovis Panel.
     * If 'layer' is specified returns
     * the protovis panel for the specified layer name.
     */
    getPvPanel: function(layer) {
        var mainPvPanel = this.pvPanel;
        if(!layer) { return mainPvPanel; }

        if(!this.parent){
            throw def.error.operationInvalid("Layers are not possible in a root panel.");
        }

        if(!mainPvPanel){
            throw def.error.operationInvalid(
               "Cannot access layer panels without having created the main panel.");
        }

        var pvPanel = null;
        if(!this._layers) {
            this._layers = {};
        } else {
            pvPanel = this._layers[layer];
        }

        if(!pvPanel) {
            var pvParentPanel = this.parent.pvPanel;
            
            pvPanel = pvParentPanel.borderPanel.add(this.type)
                .extend(mainPvPanel.borderPanel);
            
            var pvBorderPanel = pvPanel;

            if(mainPvPanel !== mainPvPanel.borderPanel) {
                pvPanel = pvBorderPanel.add(pv.Panel)
                                       .extend(mainPvPanel);
            }
            
            pvBorderPanel.borderPanel  = pvBorderPanel;
            pvBorderPanel.paddingPanel = pvPanel;
            
            pvPanel.paddingPanel  = pvPanel;
            pvPanel.borderPanel   = pvBorderPanel;
            
            this.initLayerPanel(pvPanel, layer);

            this._layers[layer] = pvPanel;
        }

        return pvPanel;
    },
    
    /**
     * Initializes a new layer panel.
     * @virtual
     */
    initLayerPanel: function(/*pvPanel, layer*/) {},
    
    /* EVENTS & VISUALIZATION CONTEXT */
    _getV1DimName: function(v1Dim){
        var dimNames = this._v1DimName || (this._v1DimNameCache = {});
        var dimName  = dimNames[v1Dim];
        if(dimName == null) {
            var role = this.chart.visualRoles[this._v1DimRoleName[v1Dim]];
            dimName = role ? role.firstDimensionName() : '';
            dimNames[v1Dim] = dimName;
        }
        
        return dimName;
    },
    
    _getV1Datum: function(scene){ return scene.datum; },
    
    /**
     * Obtains the visualization context of the panel.
     *  
     * Creates a new context when necessary.
     * 
     * <p>
     * Override to perform specific updates. 
     * </p>
     * 
     * @type pvc.visual.Context
     * @virtual
     */
    context: function() {
        var context = this._context;
        if(!context || context.isPinned) {
            context = this._context = new pvc.visual.Context(this);
        } else {
            /*global visualContext_update:true */
            visualContext_update.call(context);
        }
        
        return context;
    },
    
    /* TOOLTIP */ 
    _isTooltipEnabled: function(){
        return !this.selectingByRubberband() && !this.animating();
    },
    
    // Axis panel overrides this
    _getTooltipFormatter: function(tipOptions) {
        var isV1Compat = this.compatVersion() <= 1;
        
        var tooltipFormat = tipOptions.format;
        if(!tooltipFormat) {
            if(!isV1Compat){ return this._summaryTooltipFormatter; }
            
            tooltipFormat = this.chart.options.v1StyleTooltipFormat;
            if(!tooltipFormat) { return; }
        }
        
        if(isV1Compat) {
            return function(context) {
                return tooltipFormat.call(
                        context.panel, 
                        context.getV1Series(),
                        context.getV1Category(),
                        context.getV1Value() || '',
                        context.getV1Datum());
            };
        }
        
        return function(context) { return tooltipFormat.call(context, context.scene); };
    },
  
    _summaryTooltipFormatter: function(context) {
        var scene = context.scene;
        
        // No group and no datum?
        if(!scene.datum) { return ""; }
        
        var group = scene.group;
        var isMultiDatumGroup = group && group.count() > 1;
        
        // Single null datum?
        var firstDatum = scene.datum;
        if(!isMultiDatumGroup && (!firstDatum || firstDatum.isNull)) { return ""; }
        
        var data = scene.data();
        var visibleKeyArgs = {visible: true};
        var tooltip = [];
        
        if(firstDatum.isInterpolated){
            tooltip.push('<i>Interpolation</i>: ' + def.html.escape(firstDatum.interpolation) + '<br/>');
        } else if(firstDatum.isTrend){
            tooltip.push('<i>Trend</i>: ' + def.html.escape(firstDatum.trendType) + '<br/>');
        }
        
        var complexType = data.type;
        
        /* TODO: Big HACK to prevent percentages from
         * showing up in the Lines of BarLine
         */
        var playingPercentMap = context.panel.stacked === false ? 
                                null :
                                complexType.getPlayingPercentVisualRoleDimensionMap();

        var percentValueFormat = playingPercentMap ?
                                 context.chart.options.percentValueFormat:
                                 null;

        var commonAtoms = isMultiDatumGroup ? group.atoms : scene.datum.atoms;
        var commonAtomsKeys = complexType.sortDimensionNames(def.keys(commonAtoms));
        
        function addDim(escapedDimLabel, label){
            tooltip.push('<b>' + escapedDimLabel + "</b>: " + (def.html.escape(label) || " - ") + '<br/>');
        }
        
        function calcPercent(atom, dimName) {
            var pct;
            if(group) {
                pct = group.dimensions(dimName).percentOverParent(visibleKeyArgs);
            } else {
                pct = data.dimensions(dimName).percent(atom.value, visibleKeyArgs);
            }
            
            return percentValueFormat(pct);
        }
        
        var anyCommonAtom = false;
        commonAtomsKeys.forEach(function(dimName){
            var atom = commonAtoms[dimName];
            var dimType = atom.dimension.type;
            if(!dimType.isHidden){
                if(!isMultiDatumGroup || atom.value != null) {
                    anyCommonAtom = true;
                    
                    var valueLabel = atom.label;
                    if(playingPercentMap && playingPercentMap.has(dimName)) {
                        valueLabel += " (" + calcPercent(atom, dimName) + ")";
                    }
                    
                    addDim(def.html.escape(atom.dimension.type.label), valueLabel);
                }
            }
        });
        
        if(isMultiDatumGroup) {
            if(anyCommonAtom){ tooltip.push('<hr />'); }
            
            tooltip.push("<b>#</b>: " + group._datums.length + '<br/>');
            
            complexType
            .sortDimensionNames(group.freeDimensionNames())
            .forEach(function(dimName){
                var dim = group.dimensions(dimName);
                if(!dim.type.isHidden){
                    var dimLabel = def.html.escape(dim.type.label),
                        valueLabel;
                    
                    if(dim.type.valueType === Number) {
                        // Sum
                        valueLabel = dim.format(dim.sum(visibleKeyArgs));
                        if(playingPercentMap && playingPercentMap.has(dimName)) {
                            valueLabel += " (" + calcPercent(null, dimName) + ")";
                        }
                        
                        dimLabel = "&sum; " + dimLabel;
                    } else {
                        valueLabel = dim
                            .atoms(visibleKeyArgs)
                            .map(function(atom){ return atom.label || "- "; }).join(", ");
                    }
                    
                    addDim(dimLabel, valueLabel);
                }
            });
        }
        
        return '<div style="text-align: left;">' + tooltip.join('\n') + '</div>';
    },
    
//  _requirePointEvent: function(radius) {
//      if(!this.isTopRoot) { return this.topRoot._requirePointEvent(radius); }
//
//      if(!this._attachedPointEvent) {
//          // Fire point and unpoint events
//          this.pvPanel
//              .events('all')
//              .event('mousemove', pv.Behavior.point(radius || 20));
//
//          this._attachedPointEvent = true;
//      }
//  },
  
    /* CLICK & DOUBLE-CLICK */
    // Default implementation dispatches to panel's clickAction
    // Overriden by Legend Panel
    _onClick: function(context) {
        var handler = this.clickAction;
        if(handler) {
            if(this.compatVersion() <= 1) {
                this._onV1Click(context, handler);
            } else {
                handler.call(context, context.scene);
            }
        }
    },
    
    // Default implementation dispatches to panel's doubleClickAction
    _onDoubleClick: function(context) {
        var handler = this.doubleClickAction;
        if(handler) {
            if(this.compatVersion() <= 1) {
                this._onV1DoubleClick(context, handler);
            } else {
                handler.call(context, context.scene);
            }
        }
    },
    
    // Overriden by Axis Panel
    _onV1Click: function(context, handler) {
        handler.call(context.pvMark, 
                /* V1 ARGS */
                context.getV1Series(),
                context.getV1Category(),
                context.getV1Value(),
                context.event,
                context.getV1Datum());
    },
    
    // Overriden by Axis Panel
    _onV1DoubleClick: function(context, handler) {
        handler.call(context.pvMark, 
                /* V1 ARGS */
                context.getV1Series(),
                context.getV1Category(),
                context.getV1Value(),
                context.event,
                context.getV1Datum());
    },
    
    /* SELECTION & RUBBER-BAND */
    selectingByRubberband: function() { return this.topRoot._selectingByRubberband; },
    
    /**
     * Add rubber-band functionality to panel.
     * Override to prevent rubber band selection.
     * 
     * @virtual
     */
    _initRubberBand: function() {
        var me = this,
            chart = me.chart;
        
        if(!chart.interactive()) { return; }
        
        var options = chart.options,
            clickClearsSelection = options.clearSelectionMode === 'emptySpaceClick',
            useRubberband = this.chart.selectableByRubberband();
        
        if(!useRubberband && !clickClearsSelection) { return; }
        
        var data = chart.data,
            pvParentPanel = me.pvRootPanel || me.pvPanel.paddingPanel;
        
        // IE must have a fill style to fire events
        if(!me._getExtensionAbs('base', 'fillStyle')) {
            pvParentPanel.fillStyle(pvc.invisibleFill);
        }
        
        // Require all events, wether it's painted or not
        pvParentPanel.lock('events', 'all');
        
        if(!useRubberband) {
            if(clickClearsSelection) {
                // Install clearSelectionMode click
                pvParentPanel
                    .event("click", function() {
                        /*jshint expr:true */
                        data.owner.clearSelected() && chart.updateSelections();
                    });
            }
            return;
        }
        
        var dMin2 = 4; // Minimum dx or dy, squared, for a drag to be considered a rubber band selection

        this._selectingByRubberband = false;

        // Rubber band
        var toScreen, rb;
        
        var selectBar = 
            this.selectBar = 
            new pvc.visual.Bar(this, pvParentPanel, {
                extensionId:   'rubberBand',
                normalStroke:  true,
                noHover:       true,
                noSelect:      true,
                noClick:       true,
                noDoubleClick: true,
                noTooltip:     true
            })
            .override('defaultStrokeWidth', def.fun.constant(1.5))
            .override('defaultColor', function(type) {
                return type === 'stroke' ? 
                       '#86fe00' :                 /* 'rgb(255,127,0)' */ 
                       'rgba(203, 239, 163, 0.6)'  /* 'rgba(255, 127, 0, 0.15)' */
                       ;
            })
            .override('interactiveColor', function(color) { return color; })
            .pvMark
            .lock('visible', function() { return !!rb;  })
            .lock('left',    function() { return rb.x;  })
            .lock('right')
            .lock('top',     function() { return rb.y;  })
            .lock('bottom')
            .lock('width',   function() { return rb.dx; })
            .lock('height',  function() { return rb.dy; })
            .lock('cursor')
            .lock('events', 'none')
            ;
        
        // NOTE: Rubber band coordinates are always transformed to canvas/client 
        // coordinates (see 'select' and 'selectend' events)
        var selectionEndedDate;
        pvParentPanel
            .intercept('data', function() {
                var scenes = this.delegate();
                if(scenes) {
                    scenes.forEach(function(scene) {
                        // Initialize x,y,dx and dy properties
                        if(scene.x == null) { scene.x = scene.y = scene.dx = scene.dy = 0; }
                    });
                }
                return scenes;
            })
            .event('mousedown', pv.Behavior.select().autoRender(false))
            .event('select', function(scene) {
                if(!rb) {
                    if(me.animating()) { return; }
                    if(scene.dx * scene.dx + scene.dy * scene.dy <= dMin2) { return; }
                    
                    rb = new pv.Shape.Rect(scene.x, scene.y, scene.dx, scene.dy);
                    
                    me._selectingByRubberband = true;
                    
                    if(!toScreen) { toScreen = pvParentPanel.toScreenTransform(); }
                    
                    me.rubberBand = rb.apply(toScreen);
                } else {
                    rb = new pv.Shape.Rect(scene.x, scene.y, scene.dx, scene.dy);
                    // not updating rubberBand ?
                }
                
                selectBar.render();
            })
            .event('selectend', function() {
                if(rb) {
                    var ev = arguments[arguments.length - 1];
                    
                    if(!toScreen) { toScreen = pvParentPanel.toScreenTransform(); }
                    
                    var rbs = rb.apply(toScreen);
                    
                    rb = null;
                    me._selectingByRubberband = false;
                    selectBar.render(); // hide rubber band
                    
                    // Process selection
                    try     { me._processRubberBand(rbs, ev);  }
                    finally { selectionEndedDate = new Date(); }
                }
            });
        
        if(clickClearsSelection) {
            pvParentPanel
                .event("click", function() {
                    // It happens sometimes that the click is fired 
                    // after mouse up, ending up clearing a just made selection.
                    if(selectionEndedDate) {
                        var timeSpan = new Date() - selectionEndedDate;
                        if(timeSpan < 300) {
                            selectionEndedDate = null;
                            return;
                        }
                    }
                    
                    if(data.owner.clearSelected()) { chart.updateSelections(); }
                });
        }
    },
    
    _processRubberBand: function(rb, ev, ka) {
        this.rubberBand = rb;
        try     { this._onRubberBandSelectionEnd(ev, ka); } 
        finally { this.rubberBand  = null; }
    },
    
    _onRubberBandSelectionEnd: function(ev, ka) {
        if(pvc.debug >= 20) { this._log("rubberBand " + pvc.stringify(this.rubberBand)); }
        
        ka = Object.create(ka || {});
        ka.toggle = false; // output argument
        
        var datums = this._getDatumsOnRubberBand(ev, ka);
        if(datums) {
            var chart = this.chart;
            
            // Make sure selection changed action is called only once
            // Checks if any datum's selected changed, at the end
            chart._updatingSelections(function() {
                var clearBefore = (!ev.ctrlKey && chart.options.ctrlSelectMode);
                if(clearBefore) {
                    chart.data.owner.clearSelected();
                    pvc.data.Data.setSelected(datums, true);
                } else if(ka.toggle) {
                    pvc.data.Data.toggleSelected(datums);
                } else {
                    pvc.data.Data.setSelected(datums, true);
                }
            });
        }
    },
    
    _getDatumsOnRubberBand: function(ev, ka) {
        var datumMap = new def.Map();
        
        this._getDatumsOnRect(datumMap, this.rubberBand, ka);
            
        var datums = datumMap.values();
        if(datums.length) {
            datums = this.chart._onUserSelection(datums);
            if(datums && !datums.length) { datums = null; }
        }
        
        return datums;
    },
    
    // Callback to handle end of rubber band selection
    _getDatumsOnRect: function(datumMap, rect, ka) {
        this._getOwnDatumsOnRect(datumMap, rect, ka);
        
        var cs = this._children;
        if(cs) {
            cs.forEach(function(c) { c._getDatumsOnRect(datumMap, rect, ka); });
        }
    },
    
    _getOwnDatumsOnRect: function(datumMap, rect, ka) {
        var me = this;
        if(!me.isVisible) { return false; }
        
        var pvMarks = me._getSelectableMarks();
        if(!pvMarks || !pvMarks.length) { return false; }
        
        var inCount = datumMap.count;
        
        var selectionMode = def.get(ka, 'markSelectionMode');
        var processDatum = function(datum) {
            if(!datum.isNull) { datumMap.set(datum.id, datum); }
        };
        var processScene = function(scene) {
            if(scene.selectableByRubberband()) {
                scene.datums().each(processDatum);
            }
        };
        var processMark = function(pvMark) {
            pvMark.eachSceneWithDataOnRect(rect, processScene, null, selectionMode);
        };
        
        pvMarks.forEach(processMark);
        
        return inCount < datumMap.count; // any locally added?
    },
    
    /* ANCHORS & ORIENTATION */
    
    /**
     * Returns true if the anchor is one of the values 'top' or 'bottom'.
     */
    isAnchorTopOrBottom: function(anchor) {
        if (!anchor) { anchor = this.anchor; }
        return anchor === "top" || anchor === "bottom";
    },

    isOrientationVertical:   function(o) { return this.chart.isOrientationVertical  (o); },
    isOrientationHorizontal: function(o) { return this.chart.isOrientationHorizontal(o); }
})
.addStatic({
    // Determine what is the associated method to
    // call to position the labels correctly
    relativeAnchor: {
        top: "left",
        bottom: "left",
        left: "bottom",
        right: "bottom"
    },
    
    leftBottomAnchor: {
        top:    "bottom",
        bottom: "bottom",
        left:   "left",
        right:  "left"
    },
    
    leftTopAnchor: {
        top:    "top",
        bottom: "top",
        left:   "left",
        right:  "left"
    },
    
    horizontalAlign: {
        top:    "right",
        bottom: "left",
        middle: "center",
        right:  "right",
        left:   "left",
        center: "center"
    },
    
    verticalAlign: {
        top:    "top",
        bottom: "bottom",
        middle: "middle",
        right:  "bottom",
        left:   "top",
        center: "middle"
    },
    
    verticalAlign2: {
        top:    "top",
        bottom: "bottom",
        middle: "middle",
        right:  "top",
        left:   "bottom",
        center: "middle"
    },

    relativeAnchorMirror: {
        top: "right",
        bottom: "right",
        left: "top",
        right: "top"
    },

    oppositeAnchor: {
        top: "bottom",
        bottom: "top",
        left: "right",
        right: "left"
    },

    parallelLength: {
        top: "width",
        bottom: "width",
        right: "height",
        left: "height"
    },

    orthogonalLength: {
        top: "height",
        bottom: "height",
        right: "width",
        left: "width"
    },

    oppositeLength: {
        width:  "height",
        height: "width"
    }
});

def.scope(function() {
    // Create Anchor methods
    
    var BasePanel = pvc.BasePanel;
    var methods = {};
    var anchorDicts = {
        anchorOrtho:       'relativeAnchor',
        anchorOrthoMirror: 'relativeAnchorMirror',
        anchorOpposite:    'oppositeAnchor',
        anchorLength:      'parallelLength',
        anchorOrthoLength: 'orthogonalLength'
    };
    
    def.eachOwn(anchorDicts, function(d, am) {
        var dict = BasePanel[d];
        methods[am] = function(a) { return dict[a || this.anchor]; };
    });
    
    BasePanel.add(methods);
});


def
.type('pvc.PlotPanel', pvc.BasePanel)
.init(function(chart, parent, plot, options) {
    // Prevent the border from affecting the box model,
    // providing a static 0 value, independently of the actual drawn value...
    //this.borderWidth = 0;
    
    this.base(chart, parent, options);
    
    this.plot = plot;
    this._extensionPrefix = plot.extensionPrefixes;
    this.dataPartValue = plot.option('DataPart');
    this.axes.color    = chart._getAxis('color', (plot.option('ColorAxis') || 0) - 1);
    this.orientation   = plot.option('Orientation'  );
    this.valuesVisible = plot.option('ValuesVisible');
    this.valuesAnchor  = plot.option('ValuesAnchor' );
    this.valuesMask    = plot.option('ValuesMask'   );
    this.valuesFont    = plot.option('ValuesFont'   );
    this.valuesOptimizeLegibility = plot.option('ValuesOptimizeLegibility');
    
    var roles = this.visualRoles = Object.create(chart.visualRoles);
    
    var colorRoleName = plot.option('ColorRole');
    roles.color = colorRoleName ? chart.visualRole(colorRoleName) : null;
    
    this.chart._addPlotPanel(this);
})
.add({
    anchor:  'fill',

    visualRoles: null,

    _getExtensionId: function(){
        // chart is deprecated
        var extensionIds = ['chart', 'plot'];
        if(this.plotName){
            extensionIds.push(this.plotName);
        }
        
        return extensionIds;
    },
    
    /* @override */
    isOrientationVertical: function(){
        return this.orientation === pvc.orientation.vertical;
    },

    /* @override */
    isOrientationHorizontal: function(){
        return this.orientation === pvc.orientation.horizontal;
    }
});

/*global pvc_Sides:true, pvc_PercentValue:true */

def
.type('pvc.MultiChartPanel', pvc.BasePanel)
.add({
    anchor: 'fill',
    _multiInfo: null,
    
    createSmallCharts: function(){
        var chart = this.chart;
        var options = chart.options;
        
        /* I - Determine how many small charts to create */
        
        // multiChartMax can be Infinity
        var multiChartMax = Number(options.multiChartMax);
        if(isNaN(multiChartMax) || multiChartMax < 1) {
            multiChartMax = Infinity;
        }
        
        var multiChartRole = chart.visualRoles.multiChart;
        var data = multiChartRole.flatten(chart.data, {visible: true});
        var leafCount = data._children.length;
        var count = Math.min(leafCount, multiChartMax);
        if(count === 0) {
            // Shows no message to the user.
            // An empty chart, like when all series are hidden through the legend.
            return;
        }
        
        /* II - Determine basic layout (row and col count) */
        
        // multiChartColumnsMax can be Infinity
        var multiChartColumnsMax = +options.multiChartColumnsMax; // to number
        if(isNaN(multiChartColumnsMax) || multiChartMax < 1) {
            multiChartColumnsMax = 3;
        }
        
        var colCount = Math.min(count, multiChartColumnsMax);
        // <Debug>
        /*jshint expr:true */
        colCount >= 1 && isFinite(colCount) || def.assert("Must be at least 1 and finite");
        // </Debug>
        
        var rowCount = Math.ceil(count / colCount);
        // <Debug>
        /*jshint expr:true */
        rowCount >= 1 || def.assert("Must be at least 1");
        // </Debug>

        /* III - Determine if axes need coordination (null if no coordination needed) */
        
        var coordRootAxesByScopeType = this._getCoordinatedRootAxesByScopeType();
        var coordScopesByType, addChartToScope, indexChartByScope;
        if(coordRootAxesByScopeType){
            coordScopesByType = {};
            
            // Each scope is a specific 
            // 'row', 'column' or the single 'global' scope 
            addChartToScope = function(childChart, scopeType, scopeIndex){
                var scopes = def.array.lazy(coordScopesByType, scopeType);
                
                def.array.lazy(scopes, scopeIndex).push(childChart);
            };
            
            indexChartByScope = function(childChart){
                // Index child charts by scope
                //  on scopes having axes requiring coordination.
                if(coordRootAxesByScopeType.row){
                    addChartToScope(childChart, 'row', childChart.smallRowIndex);
                }
                
                if(coordRootAxesByScopeType.column){
                    addChartToScope(childChart, 'column', childChart.smallColIndex);
                }
                
                if(coordRootAxesByScopeType.global){
                    addChartToScope(childChart, 'global', 0);
                }
            };
        }
        
        /* IV - Create and _preRender small charts */
        var childOptionsBase = this._buildSmallChartsBaseOptions();
        var ChildClass = chart.constructor;
        for(var index = 0 ; index < count ; index++) {
            var childData = data._children[index];
            
            var colIndex = (index % colCount);
            var rowIndex = Math.floor(index / colCount);
            var childOptions = def.set(
                Object.create(childOptionsBase),
                'smallColIndex', colIndex,
                'smallRowIndex', rowIndex,
                'title',         childData.absLabel, // does not change with trends 
                'data',          childData);
            
            var childChart = new ChildClass(childOptions);
            
            if(!coordRootAxesByScopeType){
                childChart._preRender();
            } else {
                // options, data, plots, axes, 
                // trends, interpolation, axes_scales
                childChart._preRenderPhase1();
                
                indexChartByScope(childChart);
            }
        }
        
        // Need _preRenderPhase2?
        if(coordRootAxesByScopeType){
            // For each scope type having scales requiring coordination
            // find the union of the scales' domains for each
            // scope instance
            // Finally update all scales of the scope to have the 
            // calculated domain.
            def.eachOwn(coordRootAxesByScopeType, function(axes, scopeType){
                axes.forEach(function(axis){
                    
                    coordScopesByType[scopeType]
                        .forEach(function(scopeCharts){
                            this._coordinateScopeAxes(axis.id, scopeCharts);
                        }, this);
                    
                }, this);
            }, this);
            
            // Finalize _preRender, now that scales are coordinated
            chart.children.forEach(function(childChart){
                childChart._preRenderPhase2();
            });
        }
        
        // By now, trends and interpolation
        // have updated the data's with new Datums, if any.
        
        this._multiInfo = {
          data:     data,
          count:    count,
          rowCount: rowCount,
          colCount: colCount,
          multiChartColumnsMax: multiChartColumnsMax,
          coordScopesByType: coordScopesByType
        };
    },
    
    _getCoordinatedRootAxesByScopeType: function(){
        // Index axes that need to be coordinated, by scopeType
        var hasCoordination = false;
        var rootAxesByScopeType = 
            def
            .query(this.chart.axesList)
            .multipleIndex(function(axis){
                if(axis.scaleType !== 'discrete' && // Not implemented (yet...)
                   axis.option.isDefined('DomainScope')){
                    
                    var scopeType = axis.option('DomainScope');
                    if(scopeType !== 'cell'){
                        hasCoordination = true;
                        return scopeType;
                    }
                }
            })
            ;
        
        return hasCoordination ? rootAxesByScopeType : null;
    },
    
    _coordinateScopeAxes: function(axisId, scopeCharts){
        var unionExtent =
            def
            .query(scopeCharts)
            .select(function(childChart){
                var scale = childChart.axes[axisId].scale;
                if(!scale.isNull){
                    var domain = scale.domain();
                    return {min: domain[0], max: domain[1]};
                }
            })
            .reduce(pvc.unionExtents, null)
            ;
        
        if(unionExtent){
            // Fix the scale domain of every scale.
            scopeCharts.forEach(function(childChart){
                var axis  = childChart.axes[axisId];
                var scale = axis.scale;
                if(!scale.isNull){
                    scale.domain(unionExtent.min, unionExtent.max);
                    
                    axis.setScale(scale); // force update of dependent info.
                }
            });
        }
    },
    
    _buildSmallChartsBaseOptions: function(){
        // All size-related information is only supplied later in #_createCore.
        var chart = this.chart;
        var options = chart.options;
        return def.set(
            Object.create(options), 
               'parent',        chart,
               'legend',        false,
               'titleFont',     options.smallTitleFont,
               'titlePosition', options.smallTitlePosition,
               'titleAlign',    options.smallTitleAlign,
               'titleAlignTo',  options.smallTitleAlignTo,
               'titleOffset',   options.smallTitleOffset,
               'titleKeepInBounds', options.smallTitleKeepInBounds,
               'titleMargins',  options.smallTitleMargins,
               'titlePaddings', options.smallTitlePaddings,
               'titleSize',     options.smallTitleSize,
               'titleSizeMax',  options.smallTitleSizeMax);
    },
    
    /**
     * <p>
     * Implements small multiples chart layout.
     * Currently, it's essentially a flow-layout, 
     * from left to right and then top to bottom.
     * </p>
     * 
     * <p>
     * One small multiple chart is generated per unique combination 
     * of the values of the 'multiChart' visual role.
     * </p>
     * 
     * <p>
     * The option "multiChartMax" is the maximum number of small charts 
     * that can be laid out.
     * 
     * This can be useful if the chart's size cannot grow or 
     * if it cannot grow too much.
     * 
     * Pagination can be implemented with the use of this and 
     * the option 'multiChartPageIndex', to allow for effective printing of 
     * small multiple charts.
     * </p>
     * 
     * <p>
     * The option "multiChartPageIndex" is the desired page index.
     * This option requires that "multiChartMax" is also specified with
     * a finite and >= 1 value.
     * 
     * After a render is performed, 
     * the chart properties
     * {@link pvc.BaseChart#multiChartPageCount} and 
     * {@link pvc.BaseChart#multiChartPageIndex} will have been updated. 
     * </p>
     * 
     * <p>
     * The option 'multiChartColumnsMax' is the
     * maximum number of charts that can be laid  out in a row.
     * The default value is 3.
     * 
     * The value +Infinity can be specified, 
     * in which case there is no direct limit on the number of columns.
     * 
     * If the width of small charts does not fit in the available width 
     * then the chart's width is increased. 
     * </p>
     * <p>
     * The option 'smallWidth' can be specified to fix the width, 
     * of each small chart, in pixels or, in string "1%" format, 
     * as a percentage of the available width.
     * 
     * When not specified, but the option "multiChartColumnsMax" is specified and finite,
     * the width of the small charts is the available width divided
     * by the maximum number of charts in a row that <i>actually</i> occur
     * (so that if there are less small charts than 
     *  the maximum that can be placed on a row, 
     *  these, nevertheless, take up the whole width).
     * 
     * When both the options "smallWidth" and "multiChartColumnsMax" 
     * are unspecified, then the behavior is the same as if
     * the value "33%" had been specified for "smallWidth":
     * 3 charts will fit in the chart's initially specified width,
     * yet the chart's width can grow to accommodate for further small charts.
     * </p>
     * <p>
     * The option "multiChartSingleRowFillsHeight" affects the 
     * determination of the small charts height for the case where a single
     * row exists.
     * When the option is true, or unspecified, and a single row exists,
     * the height of the small charts will be all the available height,
     * looking similar to a non-multi-chart version of the same chart.
     *  When the option is false, 
     *  the determination of the small charts height does not depend
     *  on the number of rows, and proceeds as follows.
     * </p>
     * <p>
     * If the layout results in more than one row or 
     * when "multiChartSingleRowFillsHeight" is false,
     * the height of the small charts is determined using the option
     * 'smallAspectRatio', which is, by definition, width / height.
     * A typical aspect ratio value would be 5/4, 4/3 or the golden ratio (~1.62).
     * 
     * When the option is unspecified, 
     * a suitable value is determined,
     * using internal heuristic methods 
     * that generally depend on the concrete chart type
     * and specified options.
     * 
     * No effort is made to fill all the available height. 
     * The layout can result in two rows that occupy only half of the 
     * available height.
     * If the layout is such that the available height is exceeded, 
     * then the chart's height is increased.
     * </p>
     * <p>
     * The option 'margins' can be specified to control the 
     * spacing between small charts.
     * The default value is "2%".
     * Margins are only applied between small charts: 
     * the outer margins of border charts are always 0.  
     * </p>
     * <p>The option 'paddings' is applied to each small chart.</p>
     * 
     * ** Orthogonal scroll bar on height/width overflow??
     * ** Legend vertical center on page height ?? Dynamic?
     * 
     * @override
     */
    _calcLayout: function(layoutInfo){
        var multiInfo = this._multiInfo;
        if(!multiInfo){
            return;
        }
        
        var chart = this.chart;
        var options = chart.options;
        var clientSize = layoutInfo.clientSize;
        
        // TODO - multi-chart pagination
//        var multiChartPageIndex;
//        if(isFinite(multiChartMax)) {
//            multiChartPageIndex = chart.multiChartPageIndex;
//            if(isNaN(multiChartPageIndex)){
//                multiChartPageIndex = null;
//            } else {
//                // The next page number
//                // Initially, the chart property must have -1 to start iterating.
//                multiChartPageIndex++;
//            }
//        }
        
        var prevLayoutInfo = layoutInfo.previous;
        var initialClientWidth  = prevLayoutInfo ? prevLayoutInfo.initialClientWidth  : clientSize.width ;
        var initialClientHeight = prevLayoutInfo ? prevLayoutInfo.initialClientHeight : clientSize.height;
        
        var smallWidth  = pvc_PercentValue.parse(options.smallWidth);
        if(smallWidth != null){
            smallWidth = pvc_PercentValue.resolve(smallWidth, initialClientWidth);
        }
        
        var smallHeight = pvc_PercentValue.parse(options.smallHeight);
        if(smallHeight != null){
            smallHeight = pvc_PercentValue.resolve(smallHeight, initialClientHeight);
        }
        
        var ar = +options.smallAspectRatio; // + is to number
        if(isNaN(ar) || ar <= 0){
            ar = this._calulateDefaultAspectRatio();
        }
        
        if(smallWidth == null){
            if(isFinite(multiInfo.multiChartColumnsMax)){
                // Distribute currently available client width by the effective max columns.
                smallWidth = clientSize.width / multiInfo.colCount;
            } else {
                // Single Row
                // Chart grows in width as needed
                if(smallHeight == null){
                    // Both null
                    // Height uses whole height
                    smallHeight = initialClientHeight;
                }
                
                // Now use aspect ratio to calculate width
                smallWidth = ar * smallHeight;
            }
        }
        
        if(smallHeight == null){
            if((multiInfo.rowCount === 1 && def.get(options, 'multiChartSingleRowFillsHeight', true)) ||
               (multiInfo.colCount === 1 && def.get(options, 'multiChartSingleColFillsHeight', true))){
                
                // Height uses whole height
                smallHeight = initialClientHeight;
            } else {
                smallHeight = smallWidth / ar;
            }
        }
        
        // ----------------------
        
        def.set(
           layoutInfo, 
            'initialClientWidth',  initialClientWidth,
            'initialClientHeight', initialClientHeight,
            'width',  smallWidth,
            'height', smallHeight);
        
        return {
            width:  smallWidth * multiInfo.colCount,
            height: Math.max(clientSize.height, smallHeight * multiInfo.rowCount) // vertical align center: pass only: smallHeight * multiInfo.rowCount
        };
    },
    
    _calulateDefaultAspectRatio: function(/*totalWidth*/){
        if(this.chart instanceof pvc.PieChart){
            // 5/4 <=> 10/8 < 10/7 
            return 10/7;
        }
        
        // Cartesian, ...
        return 5/4;
        
        // TODO: this is not working well horizontal bar charts, for example
//        var chart = this.chart;
//        var options = chart.options;
//        var chromeHeight = 0;
//        var chromeWidth  = 0;
//        var defaultBaseSize  = 0.4;
//        var defaultOrthoSize = 0.2;
//        
//        // Try to estimate "chrome" of small chart
//        if(chart instanceof pvc.CartesianAbstract){
//            var isVertical = chart.isOrientationVertical();
//            var size;
//            if(options.showXScale){
//                size = parseFloat(options.xAxisSize || 
//                                  (isVertical ? options.baseAxisSize : options.orthoAxisSize) ||
//                                  options.axisSize);
//                if(isNaN(size)){
//                    size = totalWidth * (isVertical ? defaultBaseSize : defaultOrthoSize);
//                }
//                
//                chromeHeight += size;
//            }
//            
//            if(options.showYScale){
//                size = parseFloat(options.yAxisSize || 
//                                  (isVertical ? options.orthoAxisSize : options.baseAxisSize) ||
//                                  options.axisSize);
//                if(isNaN(size)){
//                    size = totalWidth * (isVertical ? defaultOrthoSize : defaultBaseSize);
//                }
//                
//                chromeWidth += size;
//            }
//        }
//        
//        var contentWidth  = Math.max(totalWidth - chromeWidth, 10);
//        var contentHeight = contentWidth / this._getDefaultContentAspectRatio();
//        
//        var totalHeight = chromeHeight + contentHeight;
//        
//        return totalWidth / totalHeight;
    },
    
//    _getDefaultContentAspectRatio: function(){
//        if(this.chart instanceof pvc.PieChart){
//            // 5/4 <=> 10/8 < 10/7 
//            return 10/7;
//        }
//        
//        // Cartesian
//        return 5/2;
//    },
    
    _getExtensionId: function(){
        return 'content';
    },
    
    _createCore: function(li){
        var mi = this._multiInfo;
        if(!mi){
            // Empty
            return;
        }
        
        var chart = this.chart;
        var options = chart.options;
        
        var smallMargins = options.smallMargins;
        if(smallMargins == null){
            smallMargins = new pvc_Sides(new pvc_PercentValue(0.02));
        } else {
            smallMargins = new pvc_Sides(smallMargins);
        }
        
        var smallPaddings = new pvc_Sides(options.smallPaddings);
        
        chart.children.forEach(function(childChart){
            childChart._setSmallLayout({
                left:      childChart.smallColIndex * li.width,
                top:       childChart.smallRowIndex * li.height,
                width:     li.width,
                height:    li.height,
                margins:   this._buildSmallMargins(childChart, smallMargins),
                paddings:  smallPaddings
            });
        }, this);
        
        var coordScopesByType = mi.coordScopesByType;
        if(coordScopesByType){
            chart._coordinateSmallChartsLayout(coordScopesByType);
        }
        
        this.base(li); // calls _create on child chart's basePanel
    },
    
    _buildSmallMargins: function(childChart, smallMargins){
        var mi = this._multiInfo;
        var lastColIndex = mi.colCount - 1;
        var lastRowIndex = mi.rowCount - 1;
        var colIndex = childChart.smallColIndex;
        var rowIndex = childChart.smallRowIndex;
        
        var margins = {};
        if(colIndex > 0){
            margins.left = smallMargins.left;
        }
        if(colIndex < lastColIndex){
            margins.right = smallMargins.right;
        }
        if(rowIndex > 0){
            margins.top = smallMargins.top;
        }
        if(rowIndex < lastRowIndex){
            margins.bottom = smallMargins.bottom;
        }
        
        return margins;
    }
});


/*global pvc_Size:true */

def
.type('pvc.TitlePanelAbstract', pvc.BasePanel)
.init(function(chart, parent, options) {

    if (!options) {
        options = {};
    }

    var anchor = options.anchor || this.anchor;

    // titleSize
    if (options.size == null) {
        var size = options.titleSize;
        if (size != null) {
            // Single size (a number or a string with only one number)
            // should be interpreted as meaning the orthogonal length.
            options.size = new pvc_Size().setSize(size, {
                singleProp: this.anchorOrthoLength(anchor)
            });
        }
    }

    // titleSizeMax
    if (options.sizeMax == null) {
        var sizeMax = options.titleSizeMax;
        if (sizeMax != null) {
            // Single size (a number or a string with only one number)
            // should be interpreted as meaning the orthogonal length.
            options.sizeMax = new pvc_Size().setSize(sizeMax, {
                singleProp: this.anchorOrthoLength(anchor)
            });
        }
    }

    if (options.paddings == null) {
        options.paddings = this.defaultPaddings;
    }

    this.base(chart, parent, options);

    if (options.font === undefined) {
        var extensionFont = this._getExtension('label', 'font');
        if (typeof extensionFont === 'string') {
            this.font = extensionFont;
        }
    }
})
.add({
    pvLabel: null,
    anchor: 'top',

    title: null,
    titleSize: undefined,
    font: "12px sans-serif",

    defaultPaddings: 2,
    
    _extensionPrefix: 'title',
    
    /**
     * @override
     */
    _calcLayout: function(layoutInfo) {
            
        var requestSize = new pvc_Size();

        // TODO: take textAngle, textMargin and textBaseline into account

        // Naming is for anchor = top
        var a = this.anchor;
        var a_width = this.anchorLength(a);
        var a_height = this.anchorOrthoLength(a);
        
        // 2 - Small factor to avoid cropping text on either side
        var textWidth    = pv.Text.measure(this.title, this.font).width + 2;
        var clientWidth  = layoutInfo.clientSize[a_width];
        var desiredWidth = layoutInfo.desiredClientSize[a_width];
        
        if (desiredWidth == null) {
            desiredWidth = textWidth > clientWidth ? clientWidth : textWidth;
        } else if(desiredWidth > clientWidth) {
            desiredWidth = clientWidth;
        }

        var lines;
        if (textWidth > desiredWidth) {
            lines = pvc.text.justify(this.title, desiredWidth, this.font);
        } else {
            lines = this.title ? [ this.title ] : [];
        }

        // -------------

        var lineHeight = pv.Text.fontHeight(this.font);
        var realHeight = lines.length * lineHeight;
        var availableHeight = layoutInfo.clientSize[a_height];
        
        var desiredHeight = layoutInfo.desiredClientSize[a_height];
        if (desiredHeight == null) {
            desiredHeight = realHeight;
        } else if(desiredHeight > availableHeight) {
            desiredHeight = availableHeight;
        }
        
        if (realHeight > desiredHeight) {
            // Don't show partial lines unless it is the only one left
            var maxLineCount = Math.max(1, Math.floor(desiredHeight / lineHeight));
            if (lines.length > maxLineCount) {
                var firstCroppedLine = lines[maxLineCount];

                lines.length = maxLineCount;

                realHeight = desiredHeight = maxLineCount * lineHeight;

                var lastLine = lines[maxLineCount - 1] + " " + firstCroppedLine;

                lines[maxLineCount - 1] = 
                    pvc.text.trimToWidthB(
                        desiredWidth, 
                        lastLine, 
                        this.font, 
                        "..");
            }
        }

        layoutInfo.lines = lines;
        layoutInfo.topOffset = (desiredHeight - realHeight) / 2;
        layoutInfo.lineSize = {
            width: desiredWidth,
            height: lineHeight
        };

        layoutInfo.a_width = a_width;
        layoutInfo.a_height = a_height;

        requestSize[a_width] = desiredWidth;
        requestSize[a_height] = desiredHeight;

        return requestSize;
    },

    /**
     * @override
     */
    _createCore: function(layoutInfo) {
        var rootScene = this._buildScene(layoutInfo);
        
        // Label
        var rotationByAnchor = {
            top: 0,
            right: Math.PI / 2,
            bottom: 0,
            left: -Math.PI / 2
        };

        var textAlign = pvc.BasePanel.horizontalAlign[this.align];
    
        var textAnchor = pvc.BasePanel.leftTopAnchor[this.anchor];
        
        var wrapper;
        if(this.compatVersion() <= 1) {
            wrapper = function(v1f) {
                return function(itemScene) {
                    return v1f.call(this);
                };
            };
        }
        
        this.pvLabel = new pvc.visual.Label(this, this.pvPanel, {
                extensionId: 'label',
                wrapper:     wrapper
            })
            .lock('data', rootScene.lineScenes)
            .pvMark
            [textAnchor](function(lineScene){
                return layoutInfo.topOffset + 
                       lineScene.vars.size.height / 2 +
                       this.index * lineScene.vars.size.height;
            })
            .textAlign(textAlign)
            [this.anchorOrtho(textAnchor)](function(lineScene){
                switch(this.textAlign()) {
                    case 'center': return lineScene.vars.size.width / 2;
                    case 'left':   return 0;
                    case 'right':  return lineScene.vars.size.width;
                }
            })
            .text(function(lineScene) { return lineScene.vars.textLines[this.index]; })
            .font(this.font)
            .textBaseline('middle') // layout code does not support changing this
            .textAngle(rotationByAnchor[this.anchor])
            ;
    },
    
    _buildScene: function(layoutInfo) {
        var rootScene = new pvc.visual.Scene(null, {panel: this, source: this.chart.data});
        var textLines = layoutInfo.lines;
        
        rootScene.vars.size  = layoutInfo.lineSize;
        rootScene.vars.textLines = textLines;
        
        rootScene.lineScenes = def.array.create(textLines.length, rootScene);
        
        return rootScene;
    },
    
    _getExtensionId: def.fun.constant('')
});


def
.type('pvc.TitlePanel', pvc.TitlePanelAbstract)
.init(function(chart, parent, options){
    
    if(!options){
        options = {};
    }
    
    var isV1Compat = chart.compatVersion() <= 1;
    if(isV1Compat){
        var size = options.titleSize;
        if(size == null){
            options.titleSize = 25;
        }
    }
    
    // Must be done before calling base, cause it uses _getExtension
    this._extensionPrefix = !chart.parent ? "title" : "smallTitle";
    
    this.base(chart, parent, options);
})
.add({

    font: "14px sans-serif",
    
    defaultPaddings: 4
});


/*
 * Legend panel. Generates the legend. Specific options are:
 * <i>legendPosition</i> - top / bottom / left / right. Default: bottom
 * <i>legendSize</i> - The size of the legend in pixels. Default: 25
 *
 * Has the following protovis extension points:
 *
 * <i>legend_</i> - for the legend Panel
 * <i>legendRule_</i> - for the legend line (when applicable)
 * <i>legendDot_</i> - for the legend marker (when applicable)
 * <i>legendLabel_</i> - for the legend label
 * 
 */
def
.type('pvc.LegendPanel', pvc.BasePanel)
.init(function(chart, parent, options) {
    
    this.base(chart, parent, options);
    
    // Undo base Clickable handling 
    // It doesn't matter if the chart's clickable is false.
    // Legend clickable depends on each legend group scene's clickMode.
    var I = pvc.visual.Interactive;
    if(this._ibits & I.Interactive) { this._ibits |= I.Clickable; }
})
.add({
    pvRule: null,
    pvDot: null,
    pvLabel: null,
    
    anchor: 'bottom',
    
    pvLegendPanel: null,
    
    textMargin:  6,    // The space *between* the marker and the text, in pixels.
    itemPadding: 2.5,  // Half the space *between* legend items, in pixels.
    markerSize:  15,   // *diameter* of marker *zone* (the marker itself may be a little smaller)
    font:  '10px sans-serif',

    /**
     * @override
     */
    _calcLayout: function(layoutInfo){
        return this._getBulletRootScene().layout(layoutInfo);
    },
    
    /**
     * @override
     */
    _createCore: function(layoutInfo) {
      var clientSize = layoutInfo.clientSize,
          rootScene = this._getBulletRootScene(),
          itemPadding = rootScene.vars.itemPadding,
          contentSize = rootScene.vars.size;
      
       // Names are for horizontal layout (anchor = top or bottom)
      var isHorizontal = this.isAnchorTopOrBottom();
      var a_top    = isHorizontal ? 'top' : 'left';
      var a_bottom = this.anchorOpposite(a_top);    // top or bottom
      var a_width  = this.anchorLength(a_top);      // width or height
      var a_height = this.anchorOrthoLength(a_top); // height or width
      var a_center = isHorizontal ? 'center' : 'middle';
      var a_left   = isHorizontal ? 'left' : 'top';
      var a_right  = this.anchorOpposite(a_left);   // left or right
      
      // When V1 compat or size is fixed to less/more than content needs, 
      // it is still needed to align content inside
      
      // We align all rows left (or top), using the length of the widest row.
      // So "center" is a kind of centered-left align?
      
      var leftOffset = 0;
      switch(this.align){
          case a_right:
              leftOffset = clientSize[a_width] - contentSize.width;
              break;
              
          case a_center:
              leftOffset = (clientSize[a_width] - contentSize.width) / 2;
              break;
      }
      
      this.pvPanel.overflow("hidden");
      
      // ROW - A panel instance per row
      var pvLegendRowPanel = this.pvPanel.add(pv.Panel)
          .data(rootScene.vars.rows) // rows are "lists" of bullet item scenes
          [a_left  ](leftOffset)
          [a_top   ](function() {
              var prevRow = this.sibling(); 
              return prevRow ? (prevRow[a_top] + prevRow[a_height] + itemPadding[a_height]) : 0;
          })
          [a_width ](function(row) { return row.size.width;  })
          [a_height](function(row) { return row.size.height; })
          ;
      
      var wrapper;
      if(this.compatVersion() <= 1) {
          wrapper = function(v1f) {
              return function(itemScene) { return v1f.call(this, itemScene.vars.value.rawValue); };
          };
      }
      
      // ROW > ITEM - A pvLegendPanel instance per bullet item in a row
      this.pvLegendPanel = new pvc.visual.Panel(this, pvLegendRowPanel, {
              extensionId:   'panel',
              wrapper:       wrapper,
              noSelect:      false,
              noHover:       true,
              noClick:       false, // see also #_onClick below and constructor change of Clickable
              noClickSelect: true   // just rubber-band (the click is for other behaviors)
          })
          .lockMark('data', function(row) { return row.items; }) // each row has a list of bullet item scenes
          .lock(a_right,  null)
          .lock(a_bottom, null)
          .lockMark(a_left, function(clientScene) {
              var itemPadding  = clientScene.vars.itemPadding;
              var prevItem = this.sibling();
              return prevItem ? 
                      (prevItem[a_left] + prevItem[a_width] + itemPadding[a_width]) : 
                      0;
          })
          .lockMark('height', function(itemScene) { return itemScene.vars.clientSize.height; })
          .lockMark(a_top,
                  isHorizontal ?
                  // Center items in row's height, that may be higher
                  function(itemScene) {
                      var vars = itemScene.vars;
                      return vars.row.size.height / 2 - vars.clientSize.height / 2;
                  } :
                  // Left align items of a same column
                  0)
          .lockMark('width',  
                  isHorizontal ?
                  function(itemScene) { return itemScene.vars.clientSize.width; } :
                  
                   // The biggest child width of the column
                  function(/*itemScene*/) { return this.parent.width(); })
          .pvMark
          .def("hidden", "false")
          .fillStyle(function() { // TODO: ??
              return this.hidden() == "true" ? 
                     "rgba(200,200,200,1)" : 
                     "rgba(200,200,200,0.0001)";
          });
          
      // ROW > ITEM > MARKER
      var pvLegendMarkerPanel = new pvc.visual.Panel(this, this.pvLegendPanel)
          .pvMark
          .left(0)
          .top (0)
          .right (null)
          .bottom(null)
          .width (function(itemScene){ return itemScene.vars.markerSize; })
          .height(function(itemScene){ return itemScene.vars.clientSize.height; })
          ;
      
      if(pvc.debug >= 20) {
          pvLegendRowPanel.strokeStyle('red');
          this.pvLegendPanel.strokeStyle('green');
          pvLegendMarkerPanel.strokeStyle('blue');
      }
      
      /* RULE/MARKER */
      rootScene.childNodes.forEach(function(groupScene) {
          var pvGroupPanel = new pvc.visual.Panel(this, pvLegendMarkerPanel)
                  .pvMark
                  .visible(function(itemScene) { return itemScene.parent === groupScene; });
          
          groupScene.renderer().create(this, pvGroupPanel, groupScene.extensionPrefix, wrapper);
      }, this);

      /* LABEL */
      this.pvLabel = new pvc.visual.Label(this, pvLegendMarkerPanel.anchor("right"), {
              extensionId: 'label',
              wrapper: wrapper
          })
          .intercept('textStyle', function(itemScene) {
              var baseTextStyle = this.delegateExtension() || "black";
              return itemScene.isOn() ? 
                  baseTextStyle : 
                  pvc.toGrayScale(baseTextStyle, null, undefined, 150);
          })
          .pvMark
          .textAlign('left') // panel type anchors don't adjust textAlign this way
          .text(function(itemScene) { return itemScene.vars.value.label; })
          // -4 is to compensate for now the label being anchored to the panel instead of the rule or the dot...
          .lock('textMargin', function(itemScene) { return itemScene.vars.textMargin - 4; })
          .font(function(itemScene) { return itemScene.vars.font; }) // TODO: lock?
          .textDecoration(function(itemScene) { return itemScene.isOn() ? "" : "line-through"; });
      
      if(pvc.debug >= 16) {
          var font = this.font;
          var textHeight = pv.Text.fontHeight(font) * 2/3;
          
          pvLegendMarkerPanel.anchor("right")
              // Single-point panel (w=h=0)
              .add(pv.Panel)
                  [this.anchorLength()](0)
                  [this.anchorOrthoLength()](0)
                  .fillStyle(null)
                  .strokeStyle(null)
                  .lineWidth(0)
               .add(pv.Line)
                  .data(function(scene) {
                      var labelBBox = pvc.text.getLabelBBox(
                              pv.Text.measure(scene.vars.value.label, font).width, 
                              textHeight,  // shared stuff
                              'left', 
                              'middle', 
                              0, 
                              2);
                      var corners = labelBBox.source.points();
                      
                      // Close the path
                      // not changing corners on purpose
                      if(corners.length > 1) { corners = corners.concat(corners[0]); }
                      
                      return corners;
                  })
                  .left(function(p) { return p.x; })
                  .top (function(p) { return p.y; })
                  .strokeStyle('red')
                  .lineWidth(0.5)
                  .strokeDasharray('-');
      }
    },

    _onClick: function(context) {
        var scene = context.scene;
        if(def.fun.is(scene.execute) && scene.executable()) { 
            scene.execute(); 
        }
    },
    
    _getExtensionPrefix: function() { return 'legend'; },
    _getExtensionId:     function() { return 'area';   },
    
    // Catches both the marker and the label.
    // Also, if selection changes, renderInteractive re-renders these.
    _getSelectableMarks: function() { return [this.pvLegendPanel]; },
    
    _getBulletRootScene: function(){
        var rootScene = this._rootScene;
        if(!rootScene){
            // The legend root scene contains all datums of its chart
            rootScene = new pvc.visual.legend.BulletRootScene(null, {
                panel:       this, 
                source:      this.chart.data,
                horizontal:  this.isAnchorTopOrBottom(),
                font:        this.font,
                markerSize:  this.markerSize,
                textMargin:  this.textMargin, 
                itemPadding: this.itemPadding
            });
            
            this._rootScene = rootScene;
        }
        
        return rootScene;
    }
});


/**
 * CartesianAbstract is the base class for all 2D cartesian space charts.
 */
def
.type('pvc.CartesianAbstract', pvc.BaseChart)
.init(function(options){
    
    this.axesPanels = {};
    
    this.base(options);
})
.add({
    _gridDockPanel: null,
    
    axesPanels: null, 
    
    // V1 properties
    yAxisPanel: null,
    xAxisPanel: null,
    secondXAxisPanel: null,
    secondYAxisPanel: null,
    yScale: null,
    xScale: null,
    
    _getSeriesRoleSpec: function(){
        return { isRequired: true, defaultDimension: 'series*', autoCreateDimension: true, requireIsDiscrete: true };
    },
    
    _getColorRoleSpec: function(){
        return { isRequired: true, defaultDimension: 'color*', defaultSourceRole: 'series', requireIsDiscrete: true };
    },
    
    _collectPlotAxesDataCells: function(plot, dataCellsByAxisTypeThenIndex){
        
        this.base(plot, dataCellsByAxisTypeThenIndex);
        
        /* NOTE: Cartesian axes are created even when hasMultiRole && !parent
         * because it is needed to read axis options in the root chart.
         * Also binding occurs to be able to know its scale type. 
         * Yet, their scales are not setup at the root level.
         */
        
        /* Configure Base Axis Data Cell */
        if(plot.option.isDefined('BaseAxis')){
            var baseDataCellsByAxisIndex = 
                def
                .array
                .lazy(dataCellsByAxisTypeThenIndex, 'base');
            
            def
            .array
            .lazy(baseDataCellsByAxisIndex, plot.option('BaseAxis') - 1)
            .push({
                plot:          plot,
                role:          this.visualRole(plot.option('BaseRole')),
                dataPartValue: plot.option('DataPart')
            });
        }
        
        /* Configure Ortho Axis Data Cell */
        if(plot.option.isDefined('OrthoAxis')){
            
            var trend = plot.option('Trend');
            var isStacked = plot.option.isDefined('Stacked') ?
                            plot.option('Stacked') :
                            undefined;
            
            var orthoDataCellsByAxisIndex = 
                def
                .array
                .lazy(dataCellsByAxisTypeThenIndex, 'ortho');
            
            var orthoRoleNames = def.array.to(plot.option('OrthoRole'));
            
            var dataCellBase = {
                dataPartValue: plot.option('DataPart' ),
                isStacked:     isStacked,
                trend:         trend,
                nullInterpolationMode: plot.option('NullInterpolationMode')
            };
            
            var orthoDataCells = 
                def
                .array
                .lazy(orthoDataCellsByAxisIndex, plot.option('OrthoAxis') - 1);
            
            orthoRoleNames.forEach(function(orthoRoleName){
                var dataCell = Object.create(dataCellBase);
                dataCell.role = this.visualRole(orthoRoleName);
                orthoDataCells.push(dataCell);
            }, this)
            ;
        }
    },
    
    _addAxis: function(axis){
        this.base(axis);
        
        switch(axis.type){
            case 'base':
            case 'ortho':
                this.axes[axis.orientedId] = axis;
                if(axis.v1SecondOrientedId){
                    this.axes[axis.v1SecondOrientedId] = axis;
                }
                break;
        }
        
        return this;
    },
        
    _generateTrendsDataCell: function(dataCell){
        /*jshint onecase:true */
        var trend =  dataCell.trend;
        if(trend){
            var trendInfo = pvc.trends.get(trend.type);
            
            var newDatums = [];
            
            this._generateTrendsDataCellCore(newDatums, dataCell, trendInfo);
            
            if(newDatums.length){
                this.data.owner.add(newDatums);
            }
        }
    },
    
    _generateTrendsDataCellCore: function(/*dataCell, trendInfo*/){
        // abstract
        // see Metric and Categorical implementations
    },
        
    _setAxesScales: function(hasMultiRole){
        
        this.base(hasMultiRole);
        
        if(!hasMultiRole || this.parent){
            ['base', 'ortho'].forEach(function(type){
                var axisOfType = this.axesByType[type];
                if(axisOfType){
                    axisOfType.forEach(this._createAxisScale, this);
                }
            }, this);
        }
    },
    
    /**
     * Creates a scale for a given axis, with domain applied, but no range yet,
     * assigns it to the axis and assigns the scale to special v1 chart instance fields.
     * 
     * @param {pvc.visual.Axis} axis The axis.
     * @type pv.Scale
     */
    _createAxisScale: function(axis){
        var scale = this.base(axis);
        
        var isOrtho = axis.type === 'ortho';
        var isCart  = isOrtho || axis.type === 'base';
        if(isCart){
            /* V1 fields xScale, yScale, secondScale */
            if(isOrtho && axis.index === 1) {
                this.secondScale = scale;
            } else if(!axis.index) {
                this[axis.orientation + 'Scale'] = scale;
            }
        }
        
        return scale;
    },
    
    _preRenderContent: function(contentOptions){
        
        this._createFocusWindow();
        
        /* Create the grid/docking panel */
        this._gridDockPanel = new pvc.CartesianGridDockingPanel(this, this.basePanel, {
            margins:  contentOptions.margins,
            paddings: contentOptions.paddings
        });
        
        /* Create child axis panels
         * The order is relevant because of docking order. 
         */
        ['base', 'ortho'].forEach(function(type){
            var typeAxes = this.axesByType[type];
            if(typeAxes){
                def
                .query(typeAxes)
                .reverse()
                .each(function(axis){
                    this._createAxisPanel(axis);
                }, this)
                ;
            }
        }, this);
        
        /* Create main content panel 
         * (something derived from pvc.CartesianAbstractPanel) */
        this._createPlotPanels(this._gridDockPanel, {
            clickAction:       contentOptions.clickAction,
            doubleClickAction: contentOptions.doubleClickAction
        });
    },
    
    _createFocusWindow: function(){
        if(this.selectableByFocusWindow()){
            // In case we're being re-rendered,
            // capture the axes' focusWindow, if any.
            // and set it as the next focusWindow.
            var fwData;
            var fw = this.focusWindow;
            if(fw){
                fwData = fw._exportData();
            }
            
            fw = this.focusWindow = new pvc.visual.CartesianFocusWindow(this);
            
            if(fwData){
                fw._importData(fwData);
            }
            
            fw._initFromOptions();
            
        } else if(this.focusWindow){
            delete this.focusWindow;
        }
    },
    
    /**
     * Creates an axis panel, if it is visible.
     * @param {pvc.visual.CartesianAxis} axis The cartesian axis.
     * @type pvc.AxisPanel
     */
    _createAxisPanel: function(axis){
        if(axis.option('Visible')) {
            var titlePanel;
            var title = axis.option('Title');
            if (!def.empty(title)) {
                titlePanel = new pvc.AxisTitlePanel(this, this._gridDockPanel, axis, {
                    title:        title,
                    font:         axis.option('TitleFont') || axis.option('Font'),
                    anchor:       axis.option('Position'),
                    align:        axis.option('TitleAlign'),
                    margins:      axis.option('TitleMargins'),
                    paddings:     axis.option('TitlePaddings'),
                    titleSize:    axis.option('TitleSize'),
                    titleSizeMax: axis.option('TitleSizeMax')
                });
            }
            
            var panel = new pvc.AxisPanel(this, this._gridDockPanel, axis, {
                anchor:            axis.option('Position'),
                size:              axis.option('Size'),
                sizeMax:           axis.option('SizeMax'),
                clickAction:       axis.option('ClickAction'),
                doubleClickAction: axis.option('DoubleClickAction'),
                useCompositeAxis:  axis.option('Composite'),
                font:              axis.option('Font'),
                labelSpacingMin:   axis.option('LabelSpacingMin'),
                grid:              axis.option('Grid'),
                gridCrossesMargin: axis.option('GridCrossesMargin'),
                ruleCrossesMargin: axis.option('RuleCrossesMargin'),
                zeroLine:          axis.option('ZeroLine'),
                desiredTickCount:  axis.option('DesiredTickCount'),
                showTicks:         axis.option('Ticks'),
                showMinorTicks:    axis.option('MinorTicks')
            });
            
            if(titlePanel){
                panel.titlePanel = titlePanel;
            }
            
            this.axesPanels[axis.id] = panel;
            this.axesPanels[axis.orientedId] = panel;
            
            // V1 fields
            if(axis.index <= 1 && axis.v1SecondOrientedId) {
                this[axis.v1SecondOrientedId + 'AxisPanel'] = panel;
            }
            
            return panel;
        }
    },
    
    _onLaidOut: function(){
        if(this.plotPanelList && this.plotPanelList[0]){ // not the root of a multi chart
            /* Set scale ranges, after layout */
            ['base', 'ortho'].forEach(function(type){
                var axes = this.axesByType[type];
                if(axes){
                    axes.forEach(this._setCartAxisScaleRange, this);
                }
            }, this);
        }
    },
    
    _setCartAxisScaleRange: function(axis){
        var info   = this.plotPanelList[0]._layoutInfo;
        var size   = info.clientSize;
        var length = (axis.orientation === 'x') ?
                     size.width :
                     size.height;
        
        axis.setScaleRange(length);

        return axis.scale;
    },
        
    _getAxesRoundingPaddings: function(){
        var axesPaddings = {};
        
        var axesByType = this.axesByType;
        ['base', 'ortho'].forEach(function(type){
            var typeAxes = axesByType[type];
            if(typeAxes){
                typeAxes.forEach(processAxis);
            }
        });
        
        return axesPaddings;
        
        function setSide(side, pct, locked){
            var value = axesPaddings[side];
            if(value == null || pct > value){
                axesPaddings[side] = pct;
                axesPaddings[side + 'Locked'] = locked;
            } else if(locked) {
                axesPaddings[side + 'Locked'] = locked;
            }
        }
        
        function processAxis(axis){
            if(axis){
                // {begin: , end: , beginLocked: , endLocked: }
                var tickRoundPads = axis.getScaleRoundingPaddings();
                if(tickRoundPads){
                    var isX = axis.orientation === 'x';
                    setSide(isX ? 'left'  : 'bottom', tickRoundPads.begin, tickRoundPads.beginLocked);
                    setSide(isX ? 'right' : 'top'   , tickRoundPads.end,   tickRoundPads.endLocked);
                }
            }
        }
    },
    
    markEventDefaults: {
        strokeStyle: "#5BCBF5",        /* Line Color */
        lineWidth: "0.5",              /* Line Width */
        textStyle: "#5BCBF5",          /* Text Color */
        verticalOffset: 10,            /* Distance between vertical anchor and label */
        verticalAnchor: "bottom",      /* Vertical anchor: top or bottom */
        horizontalAnchor: "right",     /* Horizontal anchor: left or right */
        forceHorizontalAnchor: false,  /* Horizontal anchor position will be respected if true */
        horizontalAnchorSwapLimit: 80, /** @deprecated  Horizontal anchor will switch if less than this space available */
        font: '10px sans-serif'
    },
    
    // TODO: chart orientation 
    // TODO: horizontal lines 
    // TODO: discrete scales
    markEvent: function(sourceValue, label, options){
        var me = this;
        var baseAxis  = me.axes.base;
        var orthoAxis = me.axes.ortho;
        var baseRole  = baseAxis.role;
        var baseScale = baseAxis.scale;
        var baseDim   = me.data.owner.dimensions(baseRole.grouping.firstDimensionName());
        var baseDimType = baseDim.type;
        
        if(baseAxis.isDiscrete()) {
            me._warn("Can only mark events in charts with a continuous base scale.");
            return me;
        }

        var o = $.extend({}, me.markEventDefaults, options);
        
        var pseudoAtom = baseDim.read(sourceValue, label);
        var basePos    = baseScale(pseudoAtom.value);
        var baseRange  = baseScale.range();
        var baseEndPos = baseRange[1];
        if(basePos < baseRange[0] || basePos > baseEndPos) {
            this._warn("Cannot mark event because it is outside the base scale's domain.");
            return this;
        }
        
        // Chart's main plot
        var pvPanel = this.plotPanelList[0].pvPanel;
        
        var h = orthoAxis.scale.range()[1];

        // Detect where to place the label
        var ha = o.horizontalAnchor;
        if(!o.forceHorizontalAnchor) {
            var alignRight    = ha === "right";
            var availableSize = alignRight ? (baseEndPos - basePos) : basePos;
            
            var labelSize = pv.Text.measure(pseudoAtom.label, o.font).width;
            if (availableSize < labelSize) {
                ha = alignRight ? "left" : "right";
            }
        }
        
        var topPos = o.verticalAnchor === "top" ? o.verticalOffset : (h - o.verticalOffset);
        
        // Shouldn't this be a pv.Rule?
        var line = pvPanel.add(pv.Line)
            .data([0, h])
            .bottom(def.identity) // from 0 to h
            .left  (basePos)
            .lineWidth  (o.lineWidth)
            .strokeStyle(o.strokeStyle);

        line.anchor(ha)
            .visible(function(){ return !this.index; })
            .top(topPos)
            .add(pv.Label)
            .font(o.font)
            .text(pseudoAtom.label)
            .textStyle(o.textStyle)
            ;
        
        return me;
    },
    
    defaults: {
        /* Percentage of occupied space over total space in a discrete axis band */
        panelSizeRatio: 0.9,

        // Indicates that the *base* axis is a timeseries
        timeSeries: false,
        timeSeriesFormat: "%Y-%m-%d"
        
        // Show a frame around the plot area
        // plotFrameVisible: undefined
    }
});


/*global pvc_Sides:true, pvc_Size:true */

def
.type('pvc.GridDockingPanel', pvc.BasePanel)
.add({
    anchor: 'fill',
    
    /**
     * Implements a docking/grid layout variant.
     * <p>
     * The layout contains 5 target positions: top, bottom, left, right and center.
     * These are mapped to a 3x3 grid. The corner cells always remain empty.
     * In the center cell, panels are superimposed.
     * </p>
     * <p>
     * Additionally, panels' paddings are shared:
     * Left and right paddings are shared by the top, center and bottom panels.
     * Top and bottom paddings are shared by the left, center and right panels.
     * </p>
     * <p>
     * Child panel's can inform of existing overflowPaddings - 
     * resulting of things that are ok to overflow, 
     * as long as they don't leave the parent panel's space, 
     * and that the parent panel itself tries to reserve space for it or 
     * ensure it is in a free area.
     * </p>
     * <p>
     * The empty corner cells of the grid layout can absorb some of the overflow 
     * content from non-fill child panels. 
     * If, for example, a child panel is placed at the 'left' cell and it
     * overflows in 'top', that overflow can be partly absorbed by 
     * the top-left corner cell, as long as there's a panel in the top cell that
     * imposes that much height. 
     * </p>
     * <p>
     * If the corner space is not enough to absorb the overflow paddings
     * 
     * </p>
     * 
     * @override
     */
    _calcLayout: function(layoutInfo){
        var me = this;
        
        if(!me._children) {
            return;
        }

        var useLog = pvc.debug >= 5;
        
        // Objects we can mutate
        var margins  = new pvc_Sides(0);
        var paddings = new pvc_Sides(0);
        var remSize = def.copyOwn(layoutInfo.clientSize);
        var aolMap = pvc.BasePanel.orthogonalLength;
        var aoMap  = pvc.BasePanel.relativeAnchor;
        var alMap  = pvc.BasePanel.parallelLength;
        
        var childKeyArgs = {
                force: true,
                referenceSize: layoutInfo.clientSize
            };
        
        var fillChildren = [];
        var sideChildren = [];
        
        // loop detection
        var paddingHistory = {}; 

        var LoopDetected = 1;
        var NormalPaddingsChanged = 2;
        var OverflowPaddingsChanged = 4;

        var emptyNewPaddings = new pvc_Sides(); // used below in place of null requestPaddings
        var isDisasterRecovery = false;

        if(useLog){ me._group("CCC GRID LAYOUT clientSize = " + pvc.stringify(remSize)); }
        try{
            // PHASE 0 - Initialization
            //
            // Splits children in two groups: FILL and SIDE, according to its anchor.
            // Children explicitly not requiring layout are excluded (!child.anchor).
            //
            // For FILL children, finds the maximum of the resolved paddings.
            // These paddings will be the minimum that will result from this layout.
            /*global console:true*/
            this._children.forEach(initChild);
            
            // PHASE 1 - "MARGINS" are imposed by SIDE children
            //
            // Lays out non-fill children receiving each, the remaining space as clientSize.
            //
            // Each adds its orthogonal length to the margin side where it is anchored.
            // The normal length is only correctly known after all non-fill
            // children have been laid out once.
            //
            // As such the child is only positioned on the anchor coordinate.
            // The orthogonal anchor coordinate is only set on the second phase.
            //
            // SIDE children may change paddings as well.
            if(useLog){ me._group("Phase 1 - Determine MARGINS and FILL SIZE from SIDE panels"); }
            try{
                sideChildren.forEach(layoutChild1Side);
            } finally {
                // -> remSize now contains the size of the CENTER cell and is not changed any more
                
                if(useLog){
                    me._groupEnd();
                    me._log("Final FILL margins = " + pvc.stringify(margins));
                    me._log("Final FILL border size = " + pvc.stringify(remSize));
                }
            }

            // PHASE 2 - Relayout each SIDE child with its final orthogonal length
            // PHASE 3 - Layout FILL children
            //
            // Repeat 2 and 3 while paddings changed
            if(useLog){ me._group("Phase 2 - Determine COMMON PADDINGS"); }
            try{
                doMaxTimes(9, layoutCycle);
            } finally {
                if(useLog){
                    me._groupEnd();
                    me._log("Final FILL clientSize = " + pvc.stringify({width: (remSize.width - paddings.width), height: (remSize.height - paddings.height)}));
                    me._log("Final COMMON paddings = " + pvc.stringify(paddings));
                }
            }

            layoutInfo.gridMargins  = new pvc_Sides(margins );
            layoutInfo.gridPaddings = new pvc_Sides(paddings);
            layoutInfo.gridSize     = new pvc_Size(remSize  );

            // All available client space is consumed.
            // As such, there's no need to return anything.
            // return;
        } finally {
            if(useLog){ me._groupEnd(); }
        }

        // --------
        
        function layoutCycle(remTimes, iteration){
            if(useLog){ me._group("LayoutCycle " + (isDisasterRecovery ? "- Disaster MODE" : ("#" + (iteration + 1)))); }
            try{
                var index, count;
                var canChange = layoutInfo.canChange !== false && !isDisasterRecovery && (remTimes > 0);
                var paddingsChanged;
                var ownPaddingsChanged = false;
                var breakAndRepeat;

                index = 0;
                count = sideChildren.length;
                while(index < count){
                    if(useLog){ me._group("SIDE Child #" + (index + 1)); }
                    try{
                        paddingsChanged = layoutChild2Side(sideChildren[index], canChange);
                        if(!isDisasterRecovery && paddingsChanged){
                            breakAndRepeat = false;
                            if((paddingsChanged & OverflowPaddingsChanged) !== 0){
                                // Don't stop right away cause there might be
                                // other overflow paddings requests,
                                // of other side childs.
                                // Translate children overflow paddings in
                                // own paddings.
                                if(useLog){ me._log("SIDE Child #" + (index + 1) + " changed overflow paddings"); }
                                if(!ownPaddingsChanged){
                                    ownPaddingsChanged = true;
                                    // If others change we don't do nothing.
                                    // The previous assignment remains.
                                    // It's layoutInfo.paddings that is changed, internally.
                                    layoutInfo.requestPaddings = layoutInfo.paddings;
                                }
                            }

                            if((paddingsChanged & NormalPaddingsChanged) !== 0){
                                if(remTimes > 0){
                                    if(useLog){ me._log("SIDE Child #" + (index + 1) + " changed normal paddings"); }
                                    breakAndRepeat = true;
                                } else if(pvc.debug >= 2){
                                    me._warn("SIDE Child #" + (index + 1) + " changed paddings but no more iterations possible.");
                                }
                            }
                            
                            if((paddingsChanged & LoopDetected) !== 0){
                                // Oh no...
                                isDisasterRecovery = true;
                                
                                layoutCycle(0);
                                
                                return false; // stop;
                            }

                            if(breakAndRepeat) { 
                                return true;
                            }
                        }
                    } finally {
                        if(useLog){ me._groupEnd(); }
                    }
                    index++;
                }
                
                if(ownPaddingsChanged){
                    if(useLog){ me._log("Restarting due to overflowPaddings change"); }
                    return false; // stop;
                }

                index = 0;
                count = fillChildren.length;
                while(index < count){
                    if(useLog){ me._group("FILL Child #" + (index + 1)); }
                    try{
                        paddingsChanged = layoutChildFill(fillChildren[index], canChange);
                        if(!isDisasterRecovery && paddingsChanged){
                            breakAndRepeat = false;

                            if((paddingsChanged & NormalPaddingsChanged) !== 0){
                                if(remTimes > 0){
                                    if(pvc.debug >= 5){
                                        me._log("FILL Child #" + (index + 1) + " increased paddings");
                                    }
                                    breakAndRepeat = true; // repeat
                                } else if(pvc.debug >= 2){
                                    me._warn("FILL Child #" + (index + 1) + " increased paddings but no more iterations possible.");
                                }
                            }

                            if((paddingsChanged & LoopDetected) !== 0){
                                // Oh no...
                                isDisasterRecovery = true;
                                layoutCycle(0);
                                return false; // stop;
                            }

                            if(breakAndRepeat) {
                                return true;
                            }
                        }
                    } finally {
                        if(useLog){ me._groupEnd(); }
                    }
                    
                    index++;
                }

                return false; // stop
            } finally {
                if(useLog){ me._groupEnd(); }
            }
        }
        
        function doMaxTimes(maxTimes, fun){
            var index = 0;
            while(maxTimes--){
                // remTimes = maxTimes
                if(fun(maxTimes, index) === false){
                    return true;
                }
                index++;
            }
            
            return false;
        }
        
        function initChild(child) {
            var a = child.anchor;
            if(a){
                if(a === 'fill') {
                    fillChildren.push(child);
                    
                    var childPaddings = child.paddings.resolve(childKeyArgs.referenceSize);
                    
                    // After the op. it's not a pvc.Side anymore, just an object with same named properties.
                    paddings = pvc_Sides.resolvedMax(paddings, childPaddings);
                } else {
                    /*jshint expr:true */
                    def.hasOwn(aoMap, a) || def.fail.operationInvalid("Unknown anchor value '{0}'", [a]);
                    
                    sideChildren.push(child);
                }
            }
        }
        
        function layoutChild1Side(child, index) {
            if(useLog){ me._group("SIDE Child #" + (index + 1)); }
            try{
                var paddingsChanged = 0;

                var a = child.anchor;

                childKeyArgs.paddings = filterAnchorPaddings(a, paddings);

                child.layout(new pvc_Size(remSize), childKeyArgs);

                if(child.isVisible){

                    paddingsChanged |= checkAnchorPaddingsChanged(a, paddings, child);

                    // Only set the *anchor* position
                    // The other orthogonal position is dependent on the size of the other non-fill children
                    positionChildNormal(a, child);

                    updateSide(a, child);
                }

                return paddingsChanged;
            } finally {
                if(useLog){ me._groupEnd(); }
            }
        }
        
        function layoutChildFill(child, canChange) {
            var paddingsChanged = 0;
            
            var a = child.anchor; // 'fill'
            
            childKeyArgs.paddings  = filterAnchorPaddings(a, paddings);
            childKeyArgs.canChange = canChange;
            
            child.layout(new pvc_Size(remSize), childKeyArgs);
            
            if(child.isVisible){
                paddingsChanged |= checkAnchorPaddingsChanged(a, paddings, child, canChange);
                
                positionChildNormal(a, child);
                positionChildOrtho (child, a);
            }
            
            return paddingsChanged;
        }
        
        function layoutChild2Side(child, canChange) {
            var paddingsChanged = 0;
            if(child.isVisible){
                var a = child.anchor;
                var al  = alMap[a];
                var aol = aolMap[a];
                var length  = remSize[al];
                var olength = child[aol];
                
                var childSize2 = new pvc_Size(def.set({}, al, length, aol, olength));
                
                childKeyArgs.paddings = filterAnchorPaddings(a, paddings);
                childKeyArgs.canChange = canChange;
                
                child.layout(childSize2, childKeyArgs);
                
                if(child.isVisible){
                    paddingsChanged = checkAnchorPaddingsChanged(a, paddings, child, canChange) |   // <-- NOTE BITwise OR
                                      checkOverflowPaddingsChanged(a, layoutInfo.paddings, child, canChange);
                    
                    if(!paddingsChanged){
                        positionChildOrtho(child, child.align);
                    }
                }
            }
            
            return paddingsChanged;
        }
        
        function positionChildNormal(side, child) {
            var sidePos;
            if(side === 'fill'){
                side = 'left';
                sidePos = margins.left + remSize.width / 2 - (child.width / 2);
            } else {
                sidePos = margins[side];
            }
            
            child.setPosition(def.set({}, side, sidePos));
        }
        
        // Decreases available size and increases margins
        function updateSide(side, child) {
            var sideol = aolMap[side],
                olen   = child[sideol];
            
            margins[side]   += olen;
            remSize[sideol] -= olen;
        }
        
        function positionChildOrtho(child, align) {
            var sideo;
            if(align === 'fill'){
                align = 'middle';
            }
            
            var sideOPos;
            switch(align){
                case 'top':
                case 'bottom':
                case 'left':
                case 'right':
                    sideo = align;
                    sideOPos = margins[sideo];
                    break;
                
                case 'middle':
                    sideo    = 'bottom';
                    sideOPos = margins.bottom + (remSize.height / 2) - (child.height / 2);
                    break;
                    
                case 'center':
                    sideo    = 'left';
                    sideOPos = margins.left + remSize.width / 2 - (child.width / 2);
                    break;
            }
            
            child.setPosition(def.set({}, sideo, sideOPos));
        }
        
        function filterAnchorPaddings(a, paddings){
            var filtered = new pvc_Sides();
            
            getAnchorPaddingsNames(a).forEach(function(side){
                filtered.set(side, paddings[side]);
            });
            
            return filtered;
        }

        function checkAnchorPaddingsChanged(a, paddings, child, canChange){
            var newPaddings = child._layoutInfo.requestPaddings;

            var changed = 0;
            
            // Additional paddings are requested?
            if(newPaddings){
                if(useLog && pvc.debug >= 10){
                    me._log("=> clientSize=" + pvc.stringify(child._layoutInfo.clientSize));
                    me._log("<= requestPaddings=" + pvc.stringify(newPaddings));
                }

                // Compare requested paddings with existing paddings
                getAnchorPaddingsNames(a).forEach(function(side){
                    var value     = paddings[side] || 0;
                    var newValue  = Math.floor(10000 * (newPaddings[side] || 0)) / 10000;
                    var increase  = newValue - value;
                    var minChange = Math.max(1, Math.abs(0.01 * value));

                    // STABILITY requirement
                    if(increase !== 0 && Math.abs(increase) >= minChange){
                        if(!canChange){
                            if(pvc.debug >= 2){
                                me._warn("CANNOT change but child wanted to: " + side + "=" + newValue);
                            }
                        } else {
                            changed |= NormalPaddingsChanged;
                            paddings[side] = newValue;

                            if(useLog){
                                me._log("Changed padding " + side + " <- " + newValue);
                            }
                        }
                    }
                });

                if(changed){
                    var paddingKey = pvc_Sides
                                        .names
                                        .map(function(side){ return (paddings[side] || 0).toFixed(0); })
                                        .join('|');

                    if(def.hasOwn(paddingHistory, paddingKey)){
                        // LOOP detected
                        if(pvc.debug >= 2){
                            me._warn("LOOP detected!!!!");
                        }
                        changed |= LoopDetected;
                    } else {
                        paddingHistory[paddingKey] = true;
                    }

                    paddings.width  = paddings.left + paddings.right ;
                    paddings.height = paddings.top  + paddings.bottom;
                }
            }
            
            return changed;
        }
        
        function checkOverflowPaddingsChanged(a, ownPaddings, child, canChange){
            var overflowPaddings = child._layoutInfo.overflowPaddings || emptyNewPaddings;
            
            var changed = 0;
            
            if(useLog && pvc.debug >= 10){
                me._log("<= overflowPaddings=" + pvc.stringify(overflowPaddings));
            }
                
            getAnchorPaddingsNames(a).forEach(function(side){
                if(overflowPaddings.hasOwnProperty(side)){
                    var value    = ownPaddings[side] || 0;
                    var newValue = Math.floor(10000 * (overflowPaddings[side] || 0)) / 10000;
                    newValue -= margins[side]; // corners absorb some of it

                    var increase = newValue - value;
                    var minChange = Math.max(1, Math.abs(0.05 * value));

                    // STABILITY & SPEED requirement
                    if(increase >= minChange){
                        if(!canChange){
                            if(pvc.debug >= 2){
                                me._warn("CANNOT change overflow padding but child wanted to: " + side + "=" + newValue);
                            }
                        } else {
                            changed |= OverflowPaddingsChanged;
                            ownPaddings[side] = newValue;

                            if(useLog){
                                me._log("changed overflow padding " + side + " <- " + newValue);
                            }
                        }
                    }
                }
            });

            if(changed){
                ownPaddings.width  = ownPaddings.left + ownPaddings.right ;
                ownPaddings.height = ownPaddings.top  + ownPaddings.bottom;
            }
            
            return changed;
        }
        
        function getAnchorPaddingsNames(a){
            switch(a){
                case 'left':
                case 'right':  return pvc_Sides.vnames;
                case 'top':
                case 'bottom': return pvc_Sides.hnames;
                case 'fill':   return pvc_Sides.names;
            }
        }
    }
});


def
.type('pvc.CartesianGridDockingPanel', pvc.GridDockingPanel)
.init(function(chart, parent, options) {
    this.base(chart, parent, options);
    
    this._plotBgPanel = new pvc.PlotBgPanel(chart, this);
})
.add({
    
    _getExtensionId: function(){
        return !this.chart.parent ? 'content' : 'smallContent';
    },
    
    /**
     * @override
     */
    _createCore: function(layoutInfo){
        var chart = this.chart;
        var axes  = chart.axes;
        var xAxis = axes.x;
        var yAxis = axes.y;
        
        
        // Full grid lines
        if(xAxis.option('Grid')) {
            this.xGridRule = this._createGridRule(xAxis);
        }
        
        if(yAxis.option('Grid')) {
            this.yGridRule = this._createGridRule(yAxis);
        }
        
        this.base(layoutInfo);
        
        if(chart.focusWindow){
            this._createFocusWindow(layoutInfo);
        }
        
        var plotFrameVisible;
        if(chart.compatVersion() <= 1){
            plotFrameVisible = !!(xAxis.option('EndLine') || yAxis.option('EndLine'));
        } else {
            plotFrameVisible = def.get(chart.options, 'plotFrameVisible', true);
        }
            
        if(plotFrameVisible) {
            this.pvFrameBar = this._createFrame(layoutInfo, axes);
        }
            
        if(xAxis.scaleType !== 'discrete' && xAxis.option('ZeroLine')) {
            this.xZeroLine = this._createZeroLine(xAxis, layoutInfo);
        }

        if(yAxis.scaleType !== 'discrete' && yAxis.option('ZeroLine')) {
            this.yZeroLine = this._createZeroLine(yAxis, layoutInfo);
        }
    },
    
    _createGridRule: function(axis){
        var scale = axis.scale;
        if(scale.isNull){
            return;
        } 
        
        // Composite axis don't fill ticks
        var isDiscrete = axis.role.grouping.isDiscrete();
        var rootScene  = this._getAxisGridRootScene(axis);
        if(!rootScene){
            return;
        }
        
        var margins   = this._layoutInfo.gridMargins;
        var paddings  = this._layoutInfo.gridPaddings;
        
        var tick_a = axis.orientation === 'x' ? 'left' : 'bottom';
        var len_a  = this.anchorLength(tick_a);
        var obeg_a = this.anchorOrtho(tick_a);
        var oend_a = this.anchorOpposite(obeg_a);
        
        var tick_offset = margins[tick_a] + paddings[tick_a];
        
        var obeg = margins[obeg_a];
        var oend = margins[oend_a];
        
//      TODO: Implement GridCrossesMargin ...
//        var orthoAxis = this._getOrthoAxis(axis.type);
//        if(!orthoAxis.option('GridCrossesMargin')){
//            obeg += paddings[obeg_a];
//            oend += paddings[oend_a];
//        }
        
        var tickScenes = rootScene.leafs().array();
        var tickCount = tickScenes.length;
        if(isDiscrete && tickCount){
            // Grid rules are generated for MAJOR ticks only.
            // For discrete axes, each category
            // has a grid line at the beginning of the band,
            // and an extra end line in the last band
            tickScenes.push(tickScenes[tickCount - 1]);
        }
        
        var wrapper;
        if(this.compatVersion() <= 1){
            wrapper = function(v1f){
                return function(tickScene){
                    return v1f.call(this, tickScene.vars.tick.rawValue);
                };
            };
        }
        
        var pvGridRule = new pvc.visual.Rule(this, this.pvPanel, {
                extensionId: axis.extensionPrefixes.map(function(prefix){ return prefix + 'Grid'; }),
                wrapper:     wrapper
            })
            .lock('data', tickScenes)
            .lock(len_a, null)
            .override('defaultColor', function(){
                return pv.color("#f0f0f0");
            })
            .pvMark
            .lineWidth(1)
            .antialias(true)
            [obeg_a](obeg)
            [oend_a](oend)
            .zOrder(-12)
            .events('none')
            ;
        
        if(isDiscrete){
            // TODO: now that the grid rules' scenes are independent of the
            // axes scenes, we should not have to use the end scene twice.
            var halfStep = scale.range().step / 2;
            pvGridRule
                .lock(tick_a, function(tickScene){
                    var tickPosition = tick_offset + scale(tickScene.vars.tick.value);
                    
                    // Use **pvMark** index, cause the last two scenes report the same index.
                    var isLastLine = this.index === tickCount;
                    
                    return tickPosition + (isLastLine ? halfStep : -halfStep);
                })
                ;
        } else {
            pvGridRule
                .lock(tick_a, function(tickScene){
                    return tick_offset + scale(tickScene.vars.tick.value);
                });
        }
        
        return pvGridRule;
    },

    _getAxisGridRootScene: function(axis){
        var data = this.data;
        var isDiscrete = axis.isDiscrete();
        if(isDiscrete) { data = axis.role.flatten(data, {visible: true}); }

        var rootScene =
            new pvc.visual.CartesianAxisRootScene(null, {
                panel:  this,
                source: data
            });
            
        if (isDiscrete){
            data._children.forEach(function(tickData){
                new pvc.visual.CartesianAxisTickScene(rootScene, {
                    source:    tickData,
                    tick:      tickData.value,
                    tickRaw:   tickData.rawValue,
                    tickLabel: tickData.label
                });
            });
        } else {
            // When the axis panel is visible, ticks will have been set in the axis.
            var ticks = axis.ticks || axis.calcContinuousTicks();

            ticks.forEach(function(majorTick){
                new pvc.visual.CartesianAxisTickScene(rootScene, {
                    tick:      majorTick,
                    tickRaw:   majorTick,
                    tickLabel: axis.scale.tickFormat(majorTick)
                });
            }, this);
        }

        return rootScene;
    },

    /* zOrder
     *
     * TOP
     * -------------------
     * Axis Rules:     0
     * Line/Dot/Area Content: -7
     * Frame/EndLine: -8
     * ZeroLine:      -9   <<------
     * Content:       -10 (default)
     * Grid:      -12
     * -------------------
     * BOT
     */
    
    _createFrame: function(layoutInfo, axes){
        if(axes.base.scale.isNull || 
           (axes.ortho.scale.isNull && (!axes.ortho2 || axes.ortho2.scale.isNull))){
            return;
        }
                
        var margins = layoutInfo.gridMargins;
        var left   = margins.left;
        var right  = margins.right;
        var top    = margins.top;
        var bottom = margins.bottom;
        
        // TODO: Implement GridCrossesMargin ...
        // Need to find the correct bounding box.
        // xScale(xScale.domain()[0]) -> xScale(xScale.domain()[1])
        // and
        // yScale(yScale.domain()[0]) -> yScale(yScale.domain()[1])
        var extensionIds = [];
        if(this.compatVersion() <= 1){
            extensionIds.push('xAxisEndLine');
            extensionIds.push('yAxisEndLine');
        }
        
        extensionIds.push('plotFrame');
        
        return new pvc.visual.Panel(this, this.pvPanel, {
                extensionId: extensionIds
            })
            .pvMark
            .lock('left',   left)
            .lock('right',  right)
            .lock('top',    top)
            .lock('bottom', bottom)
            .lock('fillStyle', null)
            .events('none')
            .strokeStyle("#666666")
            .lineWidth(1)
            .antialias(false)
            .zOrder(-8)
            ;
    },
    
    _createZeroLine: function(axis, layoutInfo){
        var scale = axis.scale;
        if(!scale.isNull){
            var domain = scale.domain();
    
            // Domain crosses zero?
            if(domain[0] * domain[1] < -1e-12){
                // TODO: Implement GridCrossesMargin ...
                
                var a = axis.orientation === 'x' ? 'left' : 'bottom';
                var len_a  = this.anchorLength(a);
                var obeg_a = this.anchorOrtho(a);
                var oend_a = this.anchorOpposite(obeg_a);
                
                var margins = layoutInfo.gridMargins;
                var paddings = layoutInfo.gridPaddings;
                
                var zeroPosition = margins[a] + paddings[a] + scale(0);
                
                var obeg = margins[obeg_a];
                var oend = margins[oend_a];
                
                var rootScene = new pvc.visual.Scene(null, {
                        panel: this 
                    });
                
                return new pvc.visual.Rule(this, this.pvPanel, {
                        extensionId: axis.extensionPrefixes.map(function(prefix){ return prefix + 'ZeroLine'; })
                    })
                    .lock('data', [rootScene])
                    .lock(len_a,  null)
                    .lock(obeg_a, obeg)
                    .lock(oend_a, oend)
                    .lock(a,      zeroPosition)
                    .override('defaultColor', function(){
                        return pv.color("#666666");
                    })
                    .pvMark
                    .events('none')
                    .lineWidth(1)
                    .antialias(true)
                    .zOrder(-9)
                    ;
            }
        }
    },
    
    _createFocusWindow: function(layoutInfo){
        var me = this;
        var topRoot = me.topRoot;
        var chart   = me.chart;
        
        var focusWindow = chart.focusWindow.base;
        
        var axis  = focusWindow.axis;
        var scale = axis.scale;
        if(scale.isNull){
            return;
        }
        
        var resizable  = focusWindow.option('Resizable');
        var movable    = focusWindow.option('Movable'  );
        var isDiscrete = axis.isDiscrete();
        
        var isV     = chart.isOrientationVertical();
        var a_left  = isV ? 'left' : 'top';
        var a_top   = isV ? 'top' : 'left';
        var a_width = me.anchorOrthoLength(a_left);
        var a_right = me.anchorOpposite(a_left);
        var a_height= me.anchorOrthoLength(a_top);
        var a_bottom= me.anchorOpposite(a_top);
        var a_x     = isV ? 'x' : 'y';
        var a_dx    = 'd' + a_x;
        var a_y     = isV ? 'y' : 'x';
        var a_dy    = 'd' + a_y;
        
        var margins     = layoutInfo.gridMargins;
        var paddings    = layoutInfo.gridPaddings;
        
        var space = {
            left:   margins.left   + paddings.left,
            right:  margins.right  + paddings.right,
            top:    margins.top    + paddings.top,
            bottom: margins.bottom + paddings.bottom
        };
        
        space.width  = space.left + space.right;
        space.height = space.top  + space.bottom;
        
        var clientSize = layoutInfo.clientSize;
        
        var wf = clientSize[a_width ];
        var hf = clientSize[a_height];
        
        // Child plot's client size
        var w  = wf - space[a_width ];
        var h  = hf - space[a_height];
        
        var padLeft  = paddings[a_left ];
        var padRight = paddings[a_right];
        
        // -----------------
        
        var scene = new pvc.visual.Scene(null, {panel: this});
        
        // Initialize x,y,dx and dy properties from focusWindow
        var band     = isDiscrete ? scale.range().step : 0;
        var halfBand = band/2;
        
        scene[a_x] = scale(focusWindow.begin) - halfBand,
        
        // Add band for an inclusive discrete end
        scene[a_dx] = band + (scale(focusWindow.end) - halfBand) - scene[a_x],
        
        resetSceneY();
        
        function resetSceneY(){
            scene[a_y ] = 0 - paddings[a_top   ];
            scene[a_dy] = h + paddings[a_top] + paddings[a_bottom];
        }
        
        // -----------------
        
        var sceneProp = function(p){
            return function(){ return scene[p]; };
        };
        
        var boundLeft = function(){
            var begin = scene[a_x];
            return Math.max(0, Math.min(w, begin));
        };

        var boundWidth = function(){
            var begin = boundLeft();
            var end   = scene[a_x] + scene[a_dx];
            end = Math.max(0, Math.min(w, end));
            return end - begin;
        };
            
        var addSelBox = function(panel, id){
            return new pvc.visual.Bar(me, panel, {
                extensionId:   id,
                normalStroke:  true,
                noHover:       true,
                noSelect:      true,
                noClick:       true,
                noDoubleClick: true,
                noTooltip:     true,
                showsInteraction: false
            })
            //.override('defaultStrokeWidth', function( ){ return 0; })
            .pvMark
            .lock('data')
            .lock('visible')
            .lock(a_left,  boundLeft )
            .lock(a_width, boundWidth)
            .lock(a_top,    sceneProp(a_y ))
            .lock(a_height, sceneProp(a_dy))
            .lock(a_bottom)
            .lock(a_right )
            .sign
            ;
        };
        
        // BACKGROUND
        var baseBgPanel = this._plotBgPanel.pvPanel.borderPanel;
        baseBgPanel
            .lock('data', [scene])
            ;
        
        if(movable && resizable){ // cannot activate resizable while we can't guarantee that it respects length
            // Allow creating a new focus area.
            // Works when "dragging" on the courtains area,
            // (if inside the paddings area).
            baseBgPanel.paddingPanel
                .lock('events', 'all')
                .lock('cursor', 'crosshair')
                .event('mousedown',
                      pv.Behavior.select()
                          .autoRender(false)
                          .collapse(isV ? 'y' : 'x')
                          //.preserveLength(!resizable)
                          .positionConstraint(function(drag){
                              var op = drag.phase ==='start' ? 
                                      'new' : 
                                      'resize-end';
                              return positionConstraint(drag, op);
                          }))
                .event('selectstart', function(ev){
                    // reset the scene's orthogonal props
                    resetSceneY();
                    
                    // Redraw on mouse down.
                    onDrag(ev);
                })
                .event('select',    onDrag)
                .event('selectend', onDrag)
                ;
        } else {
            baseBgPanel.paddingPanel
                .events('all')
                ;
        }
        
        // This panel serves mainly to enable dragging of the focus area,
        // and possibly, for bg coloring.
        // The drag action is only available when there aren't visual elements
        // in the front. This allows to keep elements interactive.
        var focusBg = addSelBox(baseBgPanel.paddingPanel, 'focusWindowBg')
            .override('defaultColor', function(type){ 
                return pvc.invisibleFill;
            })
            .pvMark
            ;
        
        if(movable){
            focusBg
                .lock('events', 'all' )
                .lock('cursor', 'move')
                .event("mousedown", 
                        pv.Behavior.drag()
                            .autoRender(false)
                            .collapse(isV ? 'y' : 'x')
                            .positionConstraint(function(drag){
                                positionConstraint(drag, 'move');
                             }))
                .event("drag",    onDrag)
                .event("dragend", onDrag)
                ;
        } else {
            focusBg.events('none');
        }
        
        // --------------------------------------
        // FOREGROUND
        
        // Coordinate system like that of the plots.
        // X and Y scales can be used on this.
        var baseFgPanel = new pvc.visual.Panel(me, me.pvPanel)
            .pvMark
            .lock('data', [scene])
            .lock('visible')
            .lock('fillStyle', pvc.invisibleFill)
            .lock('left',      space.left  )
            .lock('right',     space.right )
            .lock('top',       space.top   )
            .lock('bottom',    space.bottom)
            .lock('zOrder',    10) // above axis rules
            /* Use the panel to show a steady cursor while moving/resizing,
             * by receiving all events.
             * The drag continues to live because it listens to the 
             * root's mousemove/up and we're not cancelling the events.
             * Visual elements do not receive the events because
             * they're in a sibling panel.
             */
            .lock('events', function(){
                var drag = scene.drag;
                return drag && drag.phase !== 'end' ? 'all' : 'none';
            })
            .lock('cursor', function(){
                var drag = scene.drag;
                return drag && drag.phase !== 'end' ? 
                        ((drag.type === 'drag' || (drag.type === 'select' && !resizable)) ? 
                         'move' :
                         (isV ? 'ew-resize' : 'ns-resize')) : null;
            })
            .antialias(false)
            ;
        
        // FG BASE CURTAIN
        var curtainFillColor = 'rgba(20, 20, 20, 0.1)';
        
        new pvc.visual.Bar(me, baseFgPanel, {
                extensionId:   'focusWindowBaseCurtain',
                normalStroke:  true,
                noHover:       true,
                noSelect:      true,
                noClick:       true,
                noDoubleClick: true,
                noTooltip:     true,
                showsInteraction: false
            })
            .override('defaultColor', function(type){
                return type === 'stroke' ? null : curtainFillColor; 
            })
            .pvMark
            .lock('data', [scene, scene])
            .lock('visible')
            .lock('events', 'none')
            .lock(a_left,   function(){ return !this.index ? -padLeft : boundLeft() + boundWidth(); })
            .lock(a_right,  function(){ return !this.index ? null     : -padRight; })
            .lock(a_width,  function(){ return !this.index ?  padLeft + boundLeft() : null; })
            .lock(a_top,    sceneProp(a_y ))
            .lock(a_height, sceneProp(a_dy))
            .lock(a_bottom)
            ;
        
        // FG FOCUS BOX
        // for coloring and anchoring
        var selectBoxFg = addSelBox(baseFgPanel, 'focusWindow')
            .override('defaultColor', function(type){ return null; })
            .pvMark
            .lock('events', 'none')
            ;
        
        // FG BOUNDARY/RESIZE GRIP
        var addResizeSideGrip = function(side){
            // TODO: reversed scale??
            var a_begin = (side === 'left' || side === 'top') ? 'begin' : 'end';
            
            var opposite  = me.anchorOpposite(side);
            var fillColor = 'linear-gradient(to ' + opposite + ', ' + curtainFillColor + ', #444 90%)';
            var grip = new pvc.visual.Bar(me, selectBoxFg.anchor(side), {
                    extensionId:   focusWindow.id + 'Grip' + def.firstUpperCase(a_begin),
                    normalStroke:  true,
                    noHover:       true,
                    noSelect:      true,
                    noClick:       true,
                    noDoubleClick: true,
                    noTooltip:     true,
                    showsInteraction: false
                })
                .override('defaultColor', function(type){
                    return type === 'stroke' ? null : fillColor;
                })
                .pvMark
                .lock('data')
                .lock('visible')
                [a_top   ](scene[a_y ])
                [a_height](scene[a_dy])
                ;
            
            if(resizable){
                var opId = 'resize-' + a_begin;
                grip
                    .lock('events', 'all')
                    [a_width](5)
                    .cursor(isV ? 'ew-resize' : 'ns-resize')
                    .event("mousedown", 
                        pv.Behavior.resize(side)
                            .autoRender(false)
                            .positionConstraint(function(drag){
                                positionConstraint(drag, opId);
                             })
                            .preserveOrtho(true))
                    .event("resize",    onDrag)
                    .event("resizeend", onDrag)
                    ;
            } else {
                grip
                    .events('none')
                    [a_width](1)
                    ;
            }
            
            return grip;
        };
            
        addResizeSideGrip(a_left );
        addResizeSideGrip(a_right);
        
        // --------------------
        
        function onDrag(){
            var ev = arguments[arguments.length - 1];
            var isEnd = ev.drag.phase === 'end';
            
            // Prevent tooltips and hovers
            topRoot._selectingByRubberband = !isEnd;
            
            baseBgPanel.render();
            baseFgPanel.render();
            
            
            var pbeg = scene[a_x];
            var pend = scene[a_x] + scene[a_dx];
            if(!isV){
                // from bottom, instead of top...
                var temp = w - pbeg;
                pbeg = w - pend;
                pend = temp;
            }
            
            focusWindow._updatePosition(pbeg, pend, /*select*/ isEnd, /*render*/ true);
        }
        
        // ----------------
        var a_p  = a_x;
        var a_dp = a_dx;
        
        function positionConstraint(drag, op){
            // Never called on drag.phase === 'end'
            var m = drag.m;
            
            // Only constraining the base position
            var p = m[a_p];
            var b, e, l;
            var l0 = scene[a_dp];
            
            var target;
            switch(op){
                case 'new':
                    l = 0;
                    target = 'begin';
                    break;
                
                case 'resize-begin':
                    l = l0;
                    target = 'begin';
                    break;
                    
                case 'move':
                    l = l0;
                    target = 'begin';
                    break;
                
                case 'resize-end': // use on Select and Resize
                    l = p - scene[a_p];
                    target = 'end';
                    break;
            }
            
            var min = drag.min[a_p];
            var max = drag.max[a_p];
            
            var oper = {
                type:    op,
                target:  target,
                point:   p,
                length:  l,  // new length
                length0: l0, // prev length
                min:     min,
                max:     max,
                minView: 0,
                maxView: w
            };
            
            focusWindow._constraintPosition(oper);
            
            // Sync
            m[a_p] = oper.point;
            
            // TODO: not working on horizontal orientation???
            // Overwrite min or max on resize
            switch(op){
                case 'resize-begin':
                    // The maximum position is the end grip
                    oper.max = Math.min(oper.max, scene[a_p] + scene[a_dp]);
                    break;
                    
                case 'resize-end':
                    // The minimum position is the begin grip
                    oper.min = Math.max(oper.min, scene[a_p]);
                    break;
            }
            
            drag.min[a_p] = oper.min;
            drag.max[a_p] = oper.max;
        }
    },

    /*
     * @override
     */
    _getDatumsOnRect: function(datumMap, rect, keyArgs){
        // TODO: this is done for x and y axis only, which is ok for now,
        // as only discrete axes use selection and
        // multiple axis are only continuous...
        var chart = this.chart,
            xAxisPanel = chart.axesPanels.x,
            yAxisPanel = chart.axesPanels.y,
            xDatumMap,
            yDatumMap;

        //1) x axis
        if(xAxisPanel){
            xDatumMap = new def.Map();
            xAxisPanel._getDatumsOnRect(xDatumMap, rect, keyArgs);
            if(!xDatumMap.count) {
                xDatumMap = null;
            }
        }

        //2) y axis
        if(yAxisPanel){
            yDatumMap = new def.Map();
            yAxisPanel._getOwnDatumsOnRect(yDatumMap, rect, keyArgs);
            if(!yDatumMap.count) {
                yDatumMap = null;
            }
        }

        // Rubber band selects on both axes?
        if(xDatumMap && yDatumMap) {
            xDatumMap.intersect(yDatumMap, /* into */ datumMap);
            
            keyArgs.toggle = true;

            // Rubber band selects over any of the axes?
        } else if(xDatumMap) {
            datumMap.copy(xDatumMap);
        } else if(yDatumMap) {
            datumMap.copy(yDatumMap);
        } else {
            chart.plotPanelList.forEach(function(plotPanel){
                plotPanel._getDatumsOnRect(datumMap, rect, keyArgs);
            }, this);
        }
    }
});


/*global pvc_Sides:true */

def
.type('pvc.CartesianAbstractPanel', pvc.PlotPanel)
.init(function(chart, parent, plot, options) {
    
    // Prevent the border from affecting the box model,
    // providing a static 0 value, independently of the actual drawn value...
    //this.borderWidth = 0;
    
    this.base(chart, parent, plot, options);
    
    var axes = this.axes;
    
    function addAxis(axis){
        axes[axis.type] = axis;
        
        // TODO: are these really needed??
        axes[axis.orientedId] = axis;
        if(axis.v1SecondOrientedId){
            axes[axis.v1SecondOrientedId] = axis;
        }
    }
    
    addAxis(chart._getAxis('base',  plot.option('BaseAxis' ) - 1));
    addAxis(chart._getAxis('ortho', plot.option('OrthoAxis') - 1));
    
    // ----------------
    
    // Initialize paddings from **chart** axes offsets
    // TODO: move this to the chart??
    var pctPaddings = {};
    var hasAny = false;
    
    function setSide(side, pct){
        var value = pctPaddings[side];
        if(value == null || pct > value){
            hasAny = true;
            pctPaddings[side] = pct;
        }
    }
    
    function processAxis(axis){
        var offset = axis && axis.option('Offset');
        if(offset != null && offset > 0 && offset < 1) {
            if(axis.orientation === 'x'){
                setSide('left',  offset);
                setSide('right', offset);
            } else {
                setSide('top',    offset);
                setSide('bottom', offset);
            }
        }
    }
    
    var chartAxes = chart.axesByType;
    
    ['base', 'ortho'].forEach(function(type){
        var typeAxes = chartAxes[type];
        if(typeAxes){
            typeAxes.forEach(processAxis);
        }
    });
    
    if(hasAny){
        this.offsetPaddings = pctPaddings;
    }
})
.add({
    
    offsetPaddings: null,
    
    _calcLayout: function(layoutInfo){
        layoutInfo.requestPaddings = this._calcRequestPaddings(layoutInfo);
    },
    
    _calcRequestPaddings: function(layoutInfo){
        var reqPads;
        var offPads = this.offsetPaddings;
        if(offPads){
            var tickRoundPads = this.chart._getAxesRoundingPaddings();
            var clientSize = layoutInfo.clientSize;
            var pads       = layoutInfo.paddings;

            pvc_Sides.names.forEach(function(side){
                var len_a = pvc.BasePanel.orthogonalLength[side];

                var clientLen  = clientSize[len_a];
                var paddingLen = pads[len_a];

                var len = clientLen + paddingLen;

                // Only request offset-padding if the tickRoundPads.side is not locked
                if(!tickRoundPads[side + 'Locked']){
                    // Offset paddings are a percentage of the outer length
                    // (there are no margins in this panel).
                    var offLen = len * (offPads[side] || 0);

                    // Rounding paddings are the percentage of the
                    // client length that already actually is padding
                    // due to domain rounding.
                    var roundLen = clientLen * (tickRoundPads[side] || 0);

                    // So, if the user wants offLen padding but the
                    // client area already contains roundLen of padding,
                    // request only the remaining, if any.
                    (reqPads || (reqPads = {}))[side] = Math.max(offLen - roundLen, 0);
                }
            }, this);
        }
        
        return reqPads;
    },
    
    /**
     * @override
     */
    _createCore: function() {
        // Send the panel behind the axis, title and legend, panels
        this.pvPanel.zOrder(-10);
        
        var hideOverflow;
        var contentOverflow = this.chart.options.leafContentOverflow || 'auto';
        if(contentOverflow === 'auto'){
            // Overflow
            hideOverflow =
                def
                .query(['ortho', 'base'])
                .select(function(axisType) { return this.axes[axisType]; }, this)
                .any(function(axis){
                    return axis.option('FixedMin') != null ||
                           axis.option('FixedMax') != null;
                });
        } else {
            hideOverflow = (contentOverflow === 'hidden');
        }
        
        if (hideOverflow){
            // Padding area is used by bubbles and other vizs without problem
            this.pvPanel.borderPanel.overflow('hidden');
        }
    }
});

def
.type('pvc.PlotBgPanel', pvc.BasePanel)
.init(function(chart, parent, options) {
    // Prevent the border from affecting the box model,
    // providing a static 0 value, independently of the actual drawn value...
    //this.borderWidth = 0;
    
    this.base(chart, parent, options);
    
    //this._extensionPrefix = "plotBg";
})
.add({
    anchor:  'fill',

    _getExtensionId: function(){
        return 'plotBg';
    },
    
    _createCore: function(layoutInfo) {
        // Send the panel behind grid rules
        this.pvPanel
            .borderPanel
            .lock('zOrder', -13)
            .antialias(false)
            ;
        
        this.base(layoutInfo);
    }
});

/**
 * CategoricalAbstract is the base class for all categorical or timeseries
 */
def
.type('pvc.CategoricalAbstract', pvc.CartesianAbstract)
.init(function(options) {
    
    this.base(options);

    var parent = this.parent;
    if(parent) { this._catRole = parent._catRole; }
})
.add({
    /**
     * Initializes each chart's specific roles.
     * @override
     */
    _initVisualRoles: function() {
        this.base();
      
        this._catRole = this._addVisualRole('category', this._getCategoryRoleSpec());
    },
    
    _getCategoryRoleSpec: function() {
        return {
            isRequired: true, 
            defaultDimension: 'category*', 
            autoCreateDimension: true 
        };
    },
    
    _generateTrendsDataCellCore: function(newDatums, dataCell, trendInfo) {
        var serRole = this._serRole;
        var xRole   = this._catRole;
        var yRole   = dataCell.role;
        var trendOptions = dataCell.trend;
        
        this._warnSingleContinuousValueRole(yRole);
        
        var dataPartDimName = this._dataPartRole.firstDimensionName();
        var yDimName = yRole.firstDimensionName();
        var xDimName;
        var isXDiscrete = xRole.isDiscrete();
        if(!isXDiscrete) { xDimName = xRole.firstDimensionName(); }
        
        var sumKeyArgs = {zeroIfNone: false};
        var ignoreNullsKeyArgs = {ignoreNulls: false};
                
        // Visible data grouped by category and then series
        var data = this.visibleData(dataCell.dataPartValue); // [ignoreNulls=true]
        
        // TODO: It is usually the case, but not certain, that the base axis' 
        // dataCell(s) span "all" data parts.
        // The data that will be shown in the base scale...
        // Ideally the base scale would already be set up...
        var allPartsData   = this.visibleData(null, ignoreNullsKeyArgs);
        var allCatDataRoot = xRole.flatten(allPartsData, ignoreNullsKeyArgs);
        var allCatDatas    = allCatDataRoot._children;
        
        // For each series...
        def
        .scope(function() {
            return (serRole && serRole.isBound())   ?
                   serRole.flatten(data).children() : // data already only contains visible data
                   def.query([null]) // null series
                   ;
        })
        .each(genSeriesTrend, this);
          
        function genSeriesTrend(serData1) {
            var funX = isXDiscrete ? 
                       null : // means: "use *index* as X value"
                       function(allCatData) { return allCatData.atoms[xDimName].value; };

            var funY = function(allCatData) {
                var group = data._childrenByKey[allCatData.key];
                if(group && serData1) {
                    group = group._childrenByKey[serData1.key];
                }
                
                // When null, the data point ends up being ignored
                return group ? group.dimensions(yDimName).sum(sumKeyArgs) : null;
            };
            
            var options = def.create(trendOptions, {
                rows: def.query(allCatDatas),
                x: funX,
                y: funY
            });
            
            var trendModel = trendInfo.model(options);
            
            // If a label has already been registered, it is preserved... (See BaseChart#_fixTrendsLabel)
            var dataPartAtom = data.owner
                                .dimensions(dataPartDimName)
                                .intern(this.root._firstTrendAtomProto);
            
            if(trendModel) {
                // At least one point...
                // Sample the line on each x and create a datum for it
                // on the 'trend' data part
                allCatDatas.forEach(function(allCatData, index) {
                    var trendX = isXDiscrete ? 
                                 index :
                                 allCatData.atoms[xDimName].value;
                    
                    var trendY = trendModel.sample(trendX, funY(allCatData), index);
                    if(trendY != null) {
                        var catData   = data._childrenByKey[allCatData.key];
                        var efCatData = catData || allCatData;
                        
                        var atoms;
                        if(serData1) {
                            var catSerData = catData && 
                                             catData._childrenByKey[serData1.key];
                            
                            if(catSerData) {
                                atoms = Object.create(catSerData._datums[0].atoms);
                            } else {
                                // Missing data point
                                atoms = Object.create(efCatData._datums[0].atoms);
                                
                                // Now copy series atoms
                                def.copyOwn(atoms, serData1.atoms);
                            }
                        } else {
                            // Series is unbound
                            atoms = Object.create(efCatData._datums[0].atoms);
                        }
                        
                        atoms[yDimName] = trendY;
                        atoms[dataPartDimName] = dataPartAtom;
                        
                        newDatums.push(
                            def.set(
                                new pvc.data.Datum(efCatData.owner, atoms),
                                'isVirtual', true,
                                'isTrend',   true,
                                'trendType', trendInfo.type));
                    }
                }, this);
            }
        }
    },
    
    _interpolateDataCell: function(dataCell){
        var nullInterpMode = dataCell.nullInterpolationMode;
        if(nullInterpMode){
            var InterpType;
            switch(dataCell.nullInterpolationMode){
                case 'linear': InterpType = pvc.data.LinearInterpolationOper; break;
                case 'zero':   InterpType = pvc.data.ZeroInterpolationOper;   break;
                case 'none':   break;
                default: throw def.error.argumentInvalid('nullInterpolationMode', '' + nullInterpMode);
            }
        
            if(InterpType){
                this._warnSingleContinuousValueRole(dataCell.role);
                
                // TODO: It is usually the case, but not certain, that the base axis' 
                // dataCell(s) span "all" data parts.
                var visibleData = this.visibleData(dataCell.dataPartValue);// [ignoreNulls=true]
                if(visibleData.childCount() > 0){
                    var allPartsData = this.visibleData(null, {ignoreNulls: false});
                    new InterpType(
                         allPartsData,
                         visibleData, 
                         this._catRole,
                         this._serRole,
                         dataCell.role,
                         true) // dataCell.isStacked
                    .interpolate();
                }
            }
        }
    },
    
    /**
     * @override
     */
    _createVisibleData: function(dataPartValue, keyArgs) {
        var serGrouping = this._serRole && this._serRole.flattenedGrouping();
        var catGrouping = this._catRole.flattenedGrouping();
        var partData    = this.partData(dataPartValue);
        
        var ignoreNulls = def.get(keyArgs, 'ignoreNulls');
        var inverted    = def.get(keyArgs, 'inverted', false);
        
        // Allow for more caching when isNull is null
        var groupKeyArgs = {visible: true, isNull: ignoreNulls ? false : null};
        
        return serGrouping ?
           // <=> One multi-dimensional, two-levels data grouping
           partData.groupBy(inverted ? [serGrouping, catGrouping] : [catGrouping, serGrouping], groupKeyArgs) :
           partData.groupBy(catGrouping, groupKeyArgs);
    },
    
    /**
     * Obtains the extent of the specified value axis' role
     * and data part values.
     *
     * <p>
     * Takes into account that values are shown grouped per category.
     * </p>
     *
     * <p>
     * The fact that values are stacked or not, per category,
     * is also taken into account.
     * Each data part can have its own stacking.
     * </p>
     *
     * <p>
     * When more than one datum exists per series <i>and</i> category,
     * the sum of its values is considered.
     * </p>
     *
     * @param {pvc.visual.CartesianAxis} valueAxis The value axis.
     * @param {pvc.visual.Role} valueDataCell The data cell.
     * @type object
     *
     * @override
     */
    _getContinuousVisibleCellExtent: function(valueAxis, valueDataCell) {
        var valueRole = valueDataCell.role;
        
        switch(valueRole.name) {
            case 'series':// (series throws in base)
            case 'category':
                /* Special case.
                 * The category role's single dimension belongs to the grouping dimensions of data.
                 * As such, the default method is adequate
                 * (gets the extent of the value dim on visible data).
                 *
                 * Continuous baseScale's, like timeSeries go this way.
                 */
                return this.base(valueAxis, valueDataCell);
        }
        
        this._warnSingleContinuousValueRole(valueRole);
        
        var dataPartValue = valueDataCell.dataPartValue;
        var valueDimName = valueRole.firstDimensionName();
        var data = this.visibleData(dataPartValue); // [ignoreNulls=true]
        var useAbs = valueAxis.scaleUsesAbs();
        
        if(valueAxis.type !== 'ortho' || !valueDataCell.isStacked) {
            return data.leafs()
                       .select(function(serGroup) {
                           var value = serGroup.dimensions(valueDimName).sum();
                           return useAbs && value < 0 ? -value : value;
                        })
                       .range();
        }

        /*
         * data is grouped by category and then by series
         * So direct childs of data are category groups
         */
        return data.children()
            /* Obtain the value extent of each category */
            .select(function(catGroup) {
                var range = this._getStackedCategoryValueExtent(catGroup, valueDimName, useAbs);
                if(range) { return {range: range, group: catGroup}; }
            }, this)
            .where(def.notNully)

            /* Combine the value extents of all categories */
            .reduce(function(result, rangeInfo) {
                return this._reduceStackedCategoryValueExtent(
                            result,
                            rangeInfo.range,
                            rangeInfo.group);
            }.bind(this), null);

//        The following would not work:
//        var max = data.children()
//                    .select(function(catGroup){ return catGroup.dimensions(valueDimName).sum(); })
//                    .max();
//
//        return max != null ? {min: 0, max: max} : null;
    },

    /**
     * Obtains the extent of a value dimension in a given category group.
     * The default implementation determines the extent by separately
     * summing negative and positive values.
     * Supports {@link #_getContinuousVisibleExtent}.
     */
    _getStackedCategoryValueExtent: function(catGroup, valueDimName, useAbs) {
        var posSum = null,
            negSum = null;

        catGroup
            .children()
            /* Sum all datum's values on the same leaf */
            .select(function(serGroup) {
                var value = serGroup.dimensions(valueDimName).sum();
                return useAbs && value < 0 ? -value : value;
            })
            /* Add to positive or negative totals */
            .each(function(value) {
                // Note: +null === 0
                if(value != null) {
                    if(value >= 0) { posSum += value; } 
                    else           { negSum += value; }
                }
            });

        if(posSum == null && negSum == null){ return null; }

        return {max: posSum || 0, min: negSum || 0};
    },
    
    /**
     * Reduce operation of category ranges, into a global range.
     *
     * The default implementation performs a range "union" operation.
     *
     * Supports {@link #_getContinuousVisibleExtent}.
     */
    _reduceStackedCategoryValueExtent: function(result, catRange, catGroup) {
        return pvc.unionExtents(result, catRange);
    },
    
    _coordinateSmallChartsLayout: function(scopesByType) {
        // TODO: optimize the case were 
        // the title panels have a fixed size and
        // the x and y FixedMin and FixedMax are all specified...
        // Don't need to coordinate in that case.

        this.base(scopesByType);
        
        // Force layout and retrieve sizes of
        // * title panel
        // * y panel if column or global scope (column scope coordinates x scales, but then the other axis' size also affects the layout...)
        // * x panel if row    or global scope
        var titleSizeMax  = 0;
        var titleOrthoLen;
        
        var axisIds = null;
        var sizesMaxByAxisId = {}; // {id:  {axis: axisSizeMax, title: titleSizeMax} }
        
        // Calculate maximum sizes
        this.children.forEach(function(childChart) {
            
            childChart.basePanel.layout();
            
            var size;
            var panel = childChart.titlePanel;
            if(panel) {
                if(!titleOrthoLen) { titleOrthoLen = panel.anchorOrthoLength(); }
                
                size = panel[titleOrthoLen];
                if(size > titleSizeMax) { titleSizeMax = size; }
            }
            
            // ------
            
            var axesPanels = childChart.axesPanels;
            if(!axisIds) {
                axisIds = 
                    def
                    .query(def.ownKeys(axesPanels))
                    .where(function(alias) { return alias === axesPanels[alias].axis.id; })
                    .select(function(id) {
                        // side effect
                        sizesMaxByAxisId[id] = {axis: 0, title: 0};
                        return id;
                    })
                    .array();
            }
            
            axisIds.forEach(function(id) {
                var axisPanel = axesPanels[id];
                var sizes = sizesMaxByAxisId[id];
                
                var ol = axisPanel.axis.orientation === 'x' ? 'height' : 'width';
                size = axisPanel[ol];
                if(size > sizes.axis) { sizes.axis = size; }
                
                var titlePanel = axisPanel.titlePanel;
                if(titlePanel) {
                    size = titlePanel[ol];
                    if(size > sizes.title){
                        sizes.title = size;
                    }
                }
            });
        }, this);
        
        // Apply the maximum sizes to the corresponding panels
        this.children.forEach(function(childChart) {
            
            if(titleSizeMax > 0) {
                var panel  = childChart.titlePanel;
                panel.size = panel.size.clone().set(titleOrthoLen, titleSizeMax);
            }
            
            // ------
            
            var axesPanels = childChart.axesPanels;
            axisIds.forEach(function(id) {
                var axisPanel = axesPanels[id];
                var sizes = sizesMaxByAxisId[id];
                
                var ol = axisPanel.axis.orientation === 'x' ? 'height' : 'width';
                axisPanel.size = axisPanel.size.clone().set(ol, sizes.axis);

                var titlePanel = axisPanel.titlePanel;
                if(titlePanel) {
                    titlePanel.size = titlePanel.size.clone().set(ol, sizes.title);
                }
            });
            
            // Invalidate their previous layout
            childChart.basePanel.invalidateLayout();
        }, this);
    },
    
    defaults: {
     // Ortho <- value role
        // TODO: this should go somewhere else
        orthoAxisOrdinal: false // when true => ortho axis gets the series role (instead of the value role)
    }
});


def
.type('pvc.CategoricalAbstractPanel', pvc.CartesianAbstractPanel)
.init(function(chart, parent, plot, options){
    
    this.base(chart, parent, plot, options);
    
    this.stacked = plot.option('Stacked');
});

/*global pvc_ValueLabelVar:true */

/**
 * AxisPanel panel.
 */
def
.type('pvc.AxisPanel', pvc.BasePanel)
.init(function(chart, parent, axis, options) {
    
    options = def.create(options, {
        anchor: axis.option('Position')
    });
    
    var anchor = options.anchor || this.anchor;
    
    // Prevent the border from affecting the box model,
    // providing a static 0 value, independently of the actual drawn value...
    //this.borderWidth = 0;
    
    this.axis = axis; // must be set before calling base, because of log id
    
    this.base(chart, parent, options);
    
    
    this.roleName = axis.role.name;
    this.isDiscrete = axis.role.isDiscrete();
    this._extensionPrefix = axis.extensionPrefixes;
    
    if(this.labelSpacingMin == null){
        // The user tolerance for "missing" stuff is much smaller with discrete data
        this.labelSpacingMin = this.isDiscrete ? 0.25 : 1.5; // em
    }
    
    if(this.showTicks == null){
        this.showTicks = !this.isDiscrete;
    }

    if(options.font === undefined){
        var extFont = this._getConstantExtension('label', 'font');
        if(extFont){
            this.font = extFont;
        }
    }
    
    if(options.tickLength === undefined){
        // height or width
        // TODO: Document the use of width/height for finding the tick length.
        var tickLength = +this._getConstantExtension('ticks', this.anchorOrthoLength(anchor)); 
        if(!isNaN(tickLength) && isFinite(tickLength)){
            this.tickLength = tickLength;
        }
    }
})
.add({
    pvRule:     null,
    pvTicks:    null,
    pvLabel:    null,
    pvRuleGrid: null,
    pvScale:    null,
    
    isDiscrete: false,
    roleName: null,
    axis: null,
    anchor: "bottom",
    tickLength: 6,
    
    scale: null,
    ruleCrossesMargin: true,
    font: '9px sans-serif', // label font
    labelSpacingMin: null,
    // To be used in linear scales
    desiredTickCount: null,
    showMinorTicks:   true,
    showTicks:        null,
    
    // bullet:       "\u2022"
    // middle-point: "\u00B7"
    // this.isAnchorTopOrBottom() ? ".." : ":"
    hiddenLabelText: "\u00B7",
    
    _isScaleSetup: false,
    
    _createLogInstanceId: function(){
        return this.base() + " - " + this.axis.id;
    },
    
    getTicks: function(){
        return this._layoutInfo && this._layoutInfo.ticks;
    },
    
    _calcLayout: function(layoutInfo){
        
        var scale = this.axis.scale;

        // First time setup
        if(!this._isScaleSetup){
            this.pvScale = scale;
            this.scale   = scale; // TODO: At least HeatGrid depends on this. Maybe Remove?
            
            this.extend(scale, "scale"); // TODO - review extension interface - not documented
            
            this._isScaleSetup = true;
        }
        
        if(scale.isNull){
            layoutInfo.axisSize = 0;
        } else {
            this._calcLayoutCore(layoutInfo);
        }
        
        return this.createAnchoredSize(layoutInfo.axisSize, layoutInfo.clientSize);
    },
    
    _calcLayoutCore: function(layoutInfo){
        // Fixed axis size?
        var axisSize = layoutInfo.desiredClientSize[this.anchorOrthoLength()];
        
        layoutInfo.axisSize = axisSize; // may be undefined
        
        if (this.isDiscrete && this.useCompositeAxis){
            // TODO: composite axis auto axisSize determination
            if(layoutInfo.axisSize == null){
                layoutInfo.axisSize = 50;
            }
        } else {
            this._readTextProperties(layoutInfo);
            
            /* I  - Calculate ticks
             * --> layoutInfo.{ ticks, ticksText, maxTextWidth } 
             */
            this._calcTicks();
            
            if(this.scale.type === 'discrete'){
                this._tickIncludeModulo = this._calcDiscreteTicksIncludeModulo();
            }
            
            /* II - Calculate NEEDED axisSize so that all tick's labels fit */
            this._calcAxisSizeFromLabel(layoutInfo); // -> layoutInfo.requiredAxisSize, layoutInfo.maxLabelBBox, layoutInfo.ticksBBoxes
            
            if(layoutInfo.axisSize == null){
                layoutInfo.axisSize = layoutInfo.requiredAxisSize;
            }
            
            /* III - Calculate Trimming Length if: FIXED/NEEDED > AVAILABLE */
            this._calcMaxTextLengthThatFits();
            
            /* IV - Calculate overflow paddings */
            this._calcOverflowPaddings();
        }
    },
    
    _calcAxisSizeFromLabel: function(layoutInfo){
        this._calcTicksLabelBBoxes(layoutInfo);
        this._calcAxisSizeFromLabelBBox(layoutInfo);
    },

    _readTextProperties: function(layoutInfo){
        var textAngle = this._getExtension('label', 'textAngle');
        layoutInfo.isTextAngleFixed = (textAngle != null);
        
        layoutInfo.textAngle  = def.number.as(textAngle, 0);
        layoutInfo.textMargin = def.number.as(this._getExtension('label', 'textMargin'), 3);
        
        var align = this._getExtension('label', 'textAlign');
        if(typeof align !== 'string'){
            align = this.isAnchorTopOrBottom() ? 
                    "center" : 
                    (this.anchor == "left") ? "right" : "left";
        }
        layoutInfo.textAlign = align;
        
        var baseline = this._getExtension('label', 'textBaseline');
        if(typeof baseline !== 'string'){
            switch (this.anchor) {
                case "right":
                case "left":
                case "center":
                    baseline = "middle";
                    break;
                    
                case "bottom": 
                    baseline = "top";
                    break;
                  
                default:
                //case "top":
                    baseline = "bottom";
            }
        }
        layoutInfo.textBaseline = baseline;
    },
    
    _calcAxisSizeFromLabelBBox: function(layoutInfo){
        var maxLabelBBox = layoutInfo.maxLabelBBox;
        
        // The length not over the plot area
        var length = this._getLabelBBoxQuadrantLength(maxLabelBBox, this.anchor);

        // --------------
        
        var axisSize = this.tickLength + length;
        
        // Add equal margin on both sides?
        var angle = maxLabelBBox.sourceAngle;
        if(!(angle === 0 && this.isAnchorTopOrBottom())){
            // Text height already has some free space in that case
            // so no need to add more.
            axisSize += this.tickLength;
        }
        
        layoutInfo.requiredAxisSize = axisSize;
    },
    
    _getLabelBBoxQuadrantLength: function(labelBBox, quadrantSide){
        // labelBBox coordinates are relative to the anchor point
        // x points to the right, y points downwards
        //        T
        //        ^
        //        |
        // L  <---0--->  R
        //        |
        //        v
        //        B
        //
        //  +--> xx
        //  |
        //  v yy
        //
        //  x1 <= x2
        //  y1 <= y2
        // 
        //  p1 +-------+
        //     |       |
        //     +-------+ p2
        
        var length;
        switch(quadrantSide){
            case 'left':   length = -labelBBox.x;  break;
            case 'right':  length =  labelBBox.x2; break;
            case 'top':    length = -labelBBox.y;  break;
            case 'bottom': length =  labelBBox.y2; break;
        }
        
        return Math.max(length, 0);
    },
    
    _calcOverflowPaddings: function(){
        if(!this._layoutInfo.canChange){
            if(pvc.debug >= 2){
                this._warn("Layout cannot change. Skipping calculation of overflow paddings.");
            }
            return;
        }

        this._calcOverflowPaddingsFromLabelBBox();
    },

    _calcOverflowPaddingsFromLabelBBox: function(){
        var overflowPaddings = null;
        var me = this;
        var li = me._layoutInfo;
        var ticks = li.ticks;
        var tickCount = ticks.length;
        if(tickCount){
            var ticksBBoxes  = li.ticksBBoxes;
            var paddings     = li.paddings;
            var isTopOrBottom = me.isAnchorTopOrBottom();
            var begSide      = isTopOrBottom ? 'left'  : 'bottom';
            var endSide      = isTopOrBottom ? 'right' : 'top';
            var scale        = me.scale;
            var isDiscrete   = scale.type === 'discrete';
            var clientLength = li.clientSize[me.anchorLength()];
            
            this.axis.setScaleRange(clientLength);
            
            var evalLabelSideOverflow = function(labelBBox, side, isBegin, index) {
                var sideLength = me._getLabelBBoxQuadrantLength(labelBBox, side);
                if(sideLength > 1) {// small delta to avoid frequent re-layouts... (the reported font height often causes this kind of "error" in BBox calculation)
                    var anchorPosition = scale(isDiscrete ? ticks[index].value : ticks[index]);
                    var sidePosition = isBegin ? (anchorPosition - sideLength) : (anchorPosition + sideLength);
                    var sideOverflow = Math.max(0, isBegin ? -sidePosition : (sidePosition - clientLength));
                    if(sideOverflow > 1) { 
                        // Discount this panels' paddings 
                        // cause they're, in principle, empty space that can be occupied.
                        sideOverflow -= (paddings[side] || 0);
                        if(sideOverflow > 1) {
                            if(isDiscrete){
                                // reduction of space causes reduction of band width
                                // which in turn usually causes the overflowPadding to increase,
                                // as the size of the text usually does not change.
                                // Ask a little bit more to hit the target faster.
                                sideOverflow *= 1.05;
                            }
                                
                            if(!overflowPaddings) { 
                                overflowPaddings= def.set({}, side, sideOverflow); 
                            } else {
                                var currrOverflowPadding = overflowPaddings[side];
                                if(currrOverflowPadding == null || 
                                   (currrOverflowPadding < sideOverflow)){
                                    overflowPaddings[side] = sideOverflow;
                                }
                            }
                        }
                    }
                }
            };
            
            ticksBBoxes.forEach(function(labelBBox, index){
                evalLabelSideOverflow(labelBBox, begSide, true,  index); 
                evalLabelSideOverflow(labelBBox, endSide, false, index);
            });
            
            if(pvc.debug >= 6 && overflowPaddings){
                me._log("OverflowPaddings = " + pvc.stringify(overflowPaddings));
            }
        }
        
        li.overflowPaddings = overflowPaddings;
    },
    
    _calcMaxTextLengthThatFits: function(){
        var layoutInfo = this._layoutInfo;
        
        if(this.compatVersion() <= 1){
            layoutInfo.maxTextWidth = null;
            return;
        }
        
        var availableClientLength = layoutInfo.clientSize[this.anchorOrthoLength()];
        
        var efSize = Math.min(layoutInfo.axisSize, availableClientLength);
        if(efSize >= (layoutInfo.requiredAxisSize - this.tickLength)){ // let overflow by at most tickLength
            // Labels fit
            // Clear to avoid any unnecessary trimming
            layoutInfo.maxTextWidth = null;
        } else {
            // Text may not fit. 
            // Calculate maxTextWidth where text is to be trimmed.
            var maxLabelBBox = layoutInfo.maxLabelBBox;
            
            // Now move backwards, to the max text width...
            var maxOrthoLength = efSize - 2 * this.tickLength;
            
            // A point at the maximum orthogonal distance from the anchor
            // Points in the outwards orthogonal direction.
            var mostOrthoDistantPoint;
            var parallelDirection;
            switch(this.anchor){
                case 'left':
                    parallelDirection = pv.vector(0, 1);
                    mostOrthoDistantPoint = pv.vector(-maxOrthoLength, 0);
                    break;
                
                case 'right':
                    parallelDirection = pv.vector(0, 1);
                    mostOrthoDistantPoint = pv.vector(maxOrthoLength, 0);
                    break;
                    
                case 'top':
                    parallelDirection = pv.vector(1, 0);
                    mostOrthoDistantPoint = pv.vector(0, -maxOrthoLength);
                    break;
                
                case 'bottom':
                    parallelDirection = pv.vector(1, 0);
                    mostOrthoDistantPoint = pv.vector(0, maxOrthoLength);
                    break;
            }
            
            var orthoOutwardsDir = mostOrthoDistantPoint.norm();
            
            // Intersect the line that passes through mostOrthoDistantPoint,
            // and has the direction parallelDirection with 
            // the top side and with the bottom side of the *original* label box.
            var corners = maxLabelBBox.source.points();
            var botL = corners[0];
            var botR = corners[1];
            var topR = corners[2];
            var topL = corners[3];
            
            var topLRSideDir = topR.minus(topL);
            var botLRSideDir = botR.minus(botL);
            var intersect = pv.SvgScene.lineIntersect;
            var botI = intersect(mostOrthoDistantPoint, parallelDirection, botL, botLRSideDir);
            var topI = intersect(mostOrthoDistantPoint, parallelDirection, topL, topLRSideDir);
            
            // botI and topI will replace two of the original BBox corners
            // The original corners that are at the side of the 
            // the line that passes at mostOrthoDistantPoint and has direction parallelDirection (dividing line)
            // further away to the axis, are to be replaced.
            
            var sideLRWidth  = maxLabelBBox.sourceTextWidth;
            var maxTextWidth = sideLRWidth;
            
            var botLI = botI.minus(botL);
            var botLILen = botLI.length();
            if(botLILen <= sideLRWidth && botLI.dot(topLRSideDir) >= 0){
                // botI is between botL and botR
                // One of botL and botR is in one side and 
                // the other at the other side of the dividing line.
                // On of the sides will be cut-off.
                // The cut-off side is the one whose points have the biggest
                // distance measured relative to orthoOutwardsDir
                
                if(botL.dot(orthoOutwardsDir) < botR.dot(orthoOutwardsDir)){
                    // botR is farther, so is on the cut-off side
                    maxTextWidth = botLILen; // surely, botLILen < maxTextWidth
                } else {
                    maxTextWidth = botI.minus(botR).length(); // idem
                }
            }
            
            var topLI = topI.minus(topL);
            var topLILen = topLI.length();
            if(topLILen <= sideLRWidth && topLI.dot(topLRSideDir) >= 0){
                // topI is between topL and topR
                
                if(topL.dot(orthoOutwardsDir) < topR.dot(orthoOutwardsDir)){
                    // topR is farther, so is on the cut-off side
                    maxTextWidth = Math.min(maxTextWidth, topLILen);
                } else {
                    maxTextWidth = Math.min(maxTextWidth, topI.minus(topR).length());
                }
            }
            
            // One other detail.
            // When align (anchor) is center,
            // just cutting on one side of the label original box
            // won't do, because when text is centered, the cut we make in length
            // ends up distributed by both sides...
            if(maxLabelBBox.sourceAlign === 'center'){
                var cutWidth = sideLRWidth - maxTextWidth;
                
                // Cut same width on the opposite side. 
                maxTextWidth -= cutWidth;
            }
            
            layoutInfo.maxTextWidth = maxTextWidth;
            
            if(pvc.debug >= 3){
                this._log("Trimming labels' text at length " + maxTextWidth.toFixed(2) + "px maxOrthoLength=" + maxOrthoLength.toFixed(2) + "px");
            }
        }
    },
    
    // ----------------
    
    _calcTicks: function(){
        var layoutInfo = this._layoutInfo;

        /**
         * The bbox's width is usually very close to the width of the text.
         * The bbox's height is usually about 1/3 bigger than the height of the text,
         * because it includes space for both the descent and the ascent of the font.
         * We'll compensate this by reducing the height of text.
         */
        layoutInfo.textHeight = pv.Text.fontHeight(this.font) * 2/3;
        layoutInfo.maxTextWidth = null;
        
        // Reset scale to original un-rounded domain
        this.axis.setTicks(null);
        
        // update maxTextWidth, ticks and ticksText
        switch(this.scale.type){
            case 'discrete':   this._calcDiscreteTicks();   break;
            case 'timeSeries': this._calcTimeSeriesTicks(); break;
            case 'numeric':    this._calcNumberTicks(layoutInfo); break;
            default: throw def.error.operationInvalid("Undefined axis scale type"); 
        }

        this.axis.setTicks(layoutInfo.ticks);
        
        var clientLength = layoutInfo.clientSize[this.anchorLength()];
        this.axis.setScaleRange(clientLength);

        if(layoutInfo.maxTextWidth == null){
            this._calcTicksTextLength(layoutInfo);
        }
    },
    
    _calcDiscreteTicks: function(){
        var layoutInfo = this._layoutInfo;
        var role = this.axis.role;
        var data = role.flatten(this.data, {visible: true});
        
        layoutInfo.data  = data;
        layoutInfo.ticks = data._children;
        
        // If the discrete data is of a single Date value type,
        // we want to format the category values with an appropriate precision,
        // instead of showing the default label.
        var format, dimType;
        var grouping = role.grouping;
        if(grouping.isSingleDimension && 
           (dimType = grouping.firstDimensionType()) &&
           (dimType.valueType === Date)){
            // Calculate precision from data dimension's extent 
            var extent = data.dimensions(dimType.name).extent();
            // At least two atoms are required
            if(extent && extent.min !== extent.max){
                var scale = new pv.Scale.linear(extent.min.value, extent.max.value);
                // Force "best" tick and tick format determination 
                scale.ticks();
                var tickFormatter = this.axis.option('TickFormatter');
                if(tickFormatter){
                    scale.tickFormatter(tickFormatter);
                }
                
                format = function(child){ return scale.tickFormat(child.value); };
            }
        }
        
        if(!format){
            format = function(child){ return child.absLabel; };
        }
        
        layoutInfo.ticksText = data._children.map(format);
        
        this._clearTicksTextDeps(layoutInfo);
    },
    
    _clearTicksTextDeps: function(ticksInfo) {
        ticksInfo.maxTextWidth = 
        ticksInfo.ticksTextLength = 
        ticksInfo.ticksBBoxes = null;
    },

    _calcTimeSeriesTicks: function() {
        this._calcContinuousTicks(this._layoutInfo/*, this.desiredTickCount */); // not used
    },
    
    _calcNumberTicks: function(/*layoutInfo*/) {
        var desiredTickCount = this.desiredTickCount;
        if(desiredTickCount == null) {
            if(this.isAnchorTopOrBottom()){
                this._calcNumberHTicks();
                return;
            }
            
            desiredTickCount = this._calcNumberVDesiredTickCount();
        }
        
        this._calcContinuousTicks(this._layoutInfo, desiredTickCount);
    },
    
    // --------------
    
    _calcContinuousTicks: function(ticksInfo, desiredTickCount) {
        this._calcContinuousTicksValue(ticksInfo, desiredTickCount);
        this._calcContinuousTicksText(ticksInfo);
    },
    
    _calcContinuousTicksValue: function(ticksInfo, desiredTickCount) {
        ticksInfo.ticks = this.axis.calcContinuousTicks(desiredTickCount);

        if(pvc.debug > 4){
            this._log("DOMAIN: " + pvc.stringify(this.scale.domain()));
            this._log("TICKS:  " + pvc.stringify(ticksInfo.ticks));
        }
    },
    
    _calcContinuousTicksText: function(ticksInfo) {
        var ticksText = ticksInfo.ticksText = ticksInfo.ticks.map(function(tick) { return this.scale.tickFormat(tick); }, this);
        
        this._clearTicksTextDeps(ticksInfo);

        return ticksText;
    },
    
    _calcTicksTextLength: function(ticksInfo) {
        var max  = 0;
        var font = this.font;
        var ticksText = ticksInfo.ticksText || this._calcContinuousTicksText(ticksInfo);

        var ticksTextLength = ticksInfo.ticksTextLength = ticksText.map(function(text) {
            var len = pv.Text.measure(text, font).width;
            if(len > max){ max = len; }
            return len; 
        });
        
        ticksInfo.maxTextWidth = max;
        ticksInfo.ticksBBoxes  = null;

        return ticksTextLength;
    },
    
    _calcTicksLabelBBoxes: function(ticksInfo) {
        var me = this;
        var li = me._layoutInfo;
        var ticksTextLength = ticksInfo.ticksTextLength || 
                              me._calcTicksTextLength(ticksInfo);
        
        var maxBBox;
        var maxLen = li.maxTextWidth;
        
        ticksInfo.ticksBBoxes = ticksTextLength.map(function(len) {
            var labelBBox = me._calcLabelBBox(len);
            if(!maxBBox && len === maxLen) { maxBBox = labelBBox; }
            return labelBBox;
        }, me);
        
        li.maxLabelBBox = maxBBox;
    },
    
    _calcLabelBBox: function(textWidth) {
        var li = this._layoutInfo;
        return pvc.text.getLabelBBox(
                    textWidth, 
                    li.textHeight,  // shared stuff
                    li.textAlign, 
                    li.textBaseline, 
                    li.textAngle, 
                    li.textMargin);
    },

    // --------------
    
    _calcDiscreteTicksIncludeModulo: function(){
        var mode = this.axis.option('OverlappedLabelsMode');
        if(mode !== 'hide' && mode !== 'rotatethenhide'){
            return 1;
        }
        
        var li = this._layoutInfo;
        var ticks = li.ticks;
        var tickCount = ticks.length;
        if(tickCount <= 2) {
            return 1;
        }
        
        // Calculate includeModulo depending on labelSpacingMin
            
        // Scale is already setup
        
        // How much are label anchors separated from each other
        // (in the axis direction)
        var b = this.scale.range().step; // don't use .band, cause it does not include margins...
        
        var h = li.textHeight;
        var w = li.maxTextWidth;  // Should use the average value?
        
        if(!(w > 0 && h > 0 && b > 0)){
            return 1;
        }
        
        // Minimum space that the user wants separating 
        // the closest edges of the bounding boxes of two consecutive labels, 
        // measured perpendicularly to the label text direction.
        var sMin = h * this.labelSpacingMin; /* parameter in em */
        
        var sMinH = sMin; // Between baselines
        
        // Horizontal distance between labels' text is easily taken
        // to the distance between words a the same label.
        // Vertically, it is much easier to differentiate different lines.
        // So the minimum horizontal space between labels has the length
        // a white space character, and sMin is the additional required spacing.
        var spaceW = pv.Text.measure('x', this.font).width;
        var sMinW  = spaceW + sMin; // Between sides (orthogonal to baseline)
        
        // The angle that the text makes to the x axis (clockwise,y points downwards) 
        var a = li.textAngle;
        
        // * Effective distance between anchors,
        //   that results from showing only 
        //   one in every 'tickIncludeModulo' (tim) ticks.
        // 
        //   bEf = (b * tim)
        //
        // * The space that separates the closest edges, 
        //   that are parallel to the text direction,
        //   of the bounding boxes of 
        //   two consecutive (not skipped) labels: 
        // 
        //   sBase  = (b * timh) * |sinOrCos(a)| - h;
        //
        // * The same, for the edges orthogonal to the text direction:
        //
        //   sOrtho = (b * timw) * |cosOrSin(a)| - w;
        // 
        // * At least one of the distances, sBase or sOrtho must be 
        //   greater than or equal to sMin:
        //
        //   NoOverlap If (sBase >= sMin) Or (sOrtho >= sMin)
        //
        // * Resolve each of the inequations in function of tim (timh/timw)
        //

        var isH = this.isAnchorTopOrBottom();
        var sinOrCos = Math.abs( Math[isH ? 'sin' : 'cos'](a) );
        var cosOrSin = Math.abs( Math[isH ? 'cos' : 'sin'](a) );
        
        var timh = sinOrCos < 1e-8 ? Infinity : Math.ceil((sMinH + h) / (b * sinOrCos));
        var timw = cosOrSin < 1e-8 ? Infinity : Math.ceil((sMinW + w) / (b * cosOrSin));
        var tim  = Math.min(timh, timw);
        if(!isFinite(tim) || tim < 1 || Math.ceil(tickCount / tim) < 2) {
            tim = 1;
        }
        
        if(tim > 1 && pvc.debug >= 3) {
            this._info("Showing only one in every " + tim + " tick labels");
        }
        
        return tim;
    },
    
    /* # For textAngles we're only interested in the [0, pi/2] range.
         Taking the absolute value of the following two expressions, guarantees:
         * asin from [0, 1] --> [0, pi/2]
         * acos from [0, 1] --> [pi/2, 0]
    
       # textAngle will assume values from [-pi/2, 0] (<=> [vertical, horizontal])
    
         var sinOrCos = Math.abs((sMin + h) / bEf);
         var cosOrSin = Math.abs((sMin + w) / bEf);
    
         var aBase  = Math.asin(sinOrCosVal);
         var aOrtho = Math.acos(cosOrSinVal);
    */

    _tickMultipliers: [1, 2, 5, 10],
    
    _calcNumberVDesiredTickCount: function() {
        var li = this._layoutInfo;
        var lineHeight   = li.textHeight * (1 + Math.max(0, this.labelSpacingMin /*em*/)); 
        var clientLength = li.clientSize[this.anchorLength()];
        
        var tickCountMax = Math.max(1, ~~(clientLength / lineHeight));
        if(tickCountMax <= 1) {
            return 1;
        }

        var domain = this.scale.domain();
        var span   = domain[1] - domain[0];
        if(span <= 0) {
            return tickCountMax;
        }

        var stepMin = span / tickCountMax;

        // TODO: does not account for exponentMin and exponentMax options

        // Find an adequate step = k * 10^n where k={1,2,5} and n is an integer
        var exponMin = Math.floor(pv.log(stepMin, 10));
        var stepBase = Math.pow(10, exponMin);
        var step;
        
        // stepBase <= stepMin <= stepBase * 10
        // Choose the first/smallest multiplier (among 1,2,5,10) 
        // for which step = stepBase * m >= stepMin
        var ms = this._tickMultipliers;
        for(var i = 0 ; i < ms.length ; i++) {
            step = ms[i] * stepBase;
            if(step >= stepMin) {
                break;
            }
        }
        // else [should not happen], keep the highest (10)

        return Math.max(1, Math.floor(span / step));
    },
    
    _calcNumberHTicks: function(){
        var layoutInfo = this._layoutInfo;
        var clientLength = layoutInfo.clientSize[this.anchorLength()];
        var spacing = layoutInfo.textHeight * Math.max(0, this.labelSpacingMin/*em*/);
        var desiredTickCount = this._calcNumberHDesiredTickCount(spacing);
        
        var doLog = (pvc.debug >= 7);
        var dir, prevResultTickCount;
        var ticksInfo, lastBelow, lastAbove;
        do {
            if(doLog){ this._log("calculateNumberHTicks TickCount IN desired = " + desiredTickCount); }
            
            ticksInfo = {};
            
            this._calcContinuousTicksValue(ticksInfo, desiredTickCount);
            
            var ticks = ticksInfo.ticks;
            
            var resultTickCount = ticks.length;
            
            if(ticks.exponentOverflow){
                // TODO: Check if this part of the algorithm is working ok
                
                // Cannot go anymore in the current direction, if any
                if(dir == null){
                    if(ticks.exponent === this.exponentMin){
                        lastBelow = ticksInfo;
                        dir =  1;
                    } else {
                        lastAbove = ticksInfo;
                        dir = -1;
                    }
                } else if(dir === 1){
                    if(lastBelow){
                        ticksInfo = lastBelow;
                    }
                    break;
                } else { // dir === -1
                    if(lastAbove){
                        ticksInfo = lastAbove;
                    }
                    break;
                }
                
            } else if(prevResultTickCount == null || resultTickCount !== prevResultTickCount){
                
                if(doLog){ 
                    this._log("calculateNumberHTicks TickCount desired/resulting = " + desiredTickCount + " -> " + resultTickCount); 
                }
                
                prevResultTickCount = resultTickCount;
                
                this._calcContinuousTicksText(ticksInfo);
                
                var length = this._calcNumberHLength(ticksInfo, spacing);
                var excessLength = ticksInfo.excessLength = length - clientLength;
                var pctError = ticksInfo.error = Math.abs(excessLength / clientLength);
                
                if(doLog){
                    this._log("calculateNumberHTicks error=" + (excessLength >= 0 ? "+" : "-") + (ticksInfo.error * 100).toFixed(0) + "% count=" + resultTickCount + " step=" + ticks.step);
                    this._log("calculateNumberHTicks Length client/resulting = " + clientLength + " / " + length + " spacing = " + spacing);
                }
                
                if(excessLength > 0){
                    // More ticks than can fit
                    if(desiredTickCount === 1){
                        // Edge case
                        // Cannot make dir = -1 ...
                        if(resultTickCount === 3 && pctError <= 1){
                         // remove the middle tick
                            ticksInfo.ticks.splice(1,1);
                            ticksInfo.ticksText.splice(1,1);
                            ticksInfo.ticks.step *= 2;
                        } else {
                         // keep only the first tick
                            ticksInfo.ticks.length = 1;
                            ticksInfo.ticksText.length = 1;
                        }
                        delete ticksInfo.maxTextWidth;
                        break;
                    }
                    
                    if(lastBelow){
                        // We were below max length and then overshot...
                        // Choose the best conforming one
                        // Always choose the one that conforms to MinSpacing
                        //if(pctError > lastBelow.error){
                            ticksInfo = lastBelow;
                        //}
                        break;
                    }
                    
                    // Backup last *above* calculation
                    lastAbove = ticksInfo;
                    
                    dir = -1;
                } else {
                    // Less ticks than could fit
                    
                    if(pctError <= 0.05 || dir === -1){
                        // Acceptable
                        // or
                        // Already had exceeded the length and had decided to go down
//                        if(lastAbove && pctError > lastAbove.error){
//                            ticksInfo = lastAbove;
//                        }
                        
                        break;
                    }
                    
                    // Backup last *below* calculation
                    lastBelow = ticksInfo;
                                            
                    dir = +1;
                }
            }
            
            desiredTickCount += dir;
        } while(true);
        
        if(ticksInfo) {
            layoutInfo.ticks = ticksInfo.ticks;
            layoutInfo.ticksText = ticksInfo.ticksText;
            layoutInfo.maxTextWidth = ticksInfo.maxTextWidth;
            
            if(pvc.debug >= 5) {
                this._log("calculateNumberHTicks RESULT error=" + (ticksInfo.excessLength >= 0 ? "+" : "-") + (ticksInfo.error * 100).toFixed(0) + "% count=" + ticksInfo.ticks.length + " step=" + ticksInfo.ticks.step);
            }
        }
        
        if(doLog){ this._log("calculateNumberHTicks END"); }
    },
    
    _calcNumberHDesiredTickCount: function(spacing){
        // The initial tick count is determined 
        // from the formatted min and max values of the domain.
        var layoutInfo = this._layoutInfo;
        var domainTextLength = this.scale.domain().map(function(tick){
                tick = +tick.toFixed(2); // crop some decimal places...
                var text = this.scale.tickFormat(tick);
                return pv.Text.measure(text, this.font).width;
            }, this);
        
        var avgTextLength = Math.max((domainTextLength[1] + domainTextLength[0]) / 2, layoutInfo.textHeight);
        
        var clientLength = layoutInfo.clientSize[this.anchorLength()];
        
        return Math.max(1, ~~(clientLength / (avgTextLength + spacing)));
    },
    
    _calcNumberHLength: function(ticksInfo, spacing){
        // Measure full width, with spacing
        var ticksText = ticksInfo.ticksText;
        var maxTextWidth = 
            def.query(ticksText)
                .select(function(text){ 
                    return pv.Text.measure(text, this.font).width; 
                }, this)
                .max();
        
        /*
         * Include only half the text width on edge labels, 
         * cause centered labels are the most common scenario.
         * 
         * |w s ww s ww s w|
         * 
         */
        return Math.max(maxTextWidth, (ticksText.length - 1) * (maxTextWidth + spacing));
    },
    
    _createCore: function() {
        if(this.scale.isNull) { return; }
        
        // Range
        var clientSize = this._layoutInfo.clientSize;
        var paddings   = this._layoutInfo.paddings;
        
        var begin_a = this.anchorOrtho();
        var end_a   = this.anchorOpposite(begin_a);
        var size_a  = this.anchorOrthoLength(begin_a);
        
        var rMin = this.ruleCrossesMargin ? -paddings[begin_a] : 0;
        var rMax = clientSize[size_a] + (this.ruleCrossesMargin ? paddings[end_a] : 0);
        var rSize = rMax - rMin;
        
        this._rSize = rSize;
        
        var rootScene = this._getRootScene();
        
        this.pvRule = new pvc.visual.Rule(this, this.pvPanel, {
                extensionId: 'rule'
            })
            .lock('data', [rootScene])
            .override('defaultColor', def.fun.constant("#666666"))
            // ex: anchor = bottom
            .lock(this.anchorOpposite(), 0) // top (of the axis panel)
            .lock(begin_a, rMin )  // left
            .lock(size_a,  rSize) // width
            .pvMark
            .zOrder(30)
            .strokeDasharray(null) // don't inherit from parent panel
            .lineCap('square')     // So that begin/end ticks better join with the rule
            ;

        if (this.isDiscrete){
            if(this.useCompositeAxis){
                this.renderCompositeOrdinalAxis();
            } else {
                this.renderOrdinalAxis();
            }
        } else {
            this.renderLinearAxis();
        }
    },
  
    _getExtensionId: function(){
        return ''; // NOTE: this is different from specifying null
    },
    
    _getRootScene: function(){
        if(!this._rootScene){
            var rootScene = 
                this._rootScene = 
                new pvc.visual.CartesianAxisRootScene(null, {
                    panel:  this, 
                    source: this._getRootData()
                });
            
            var layoutInfo = this._layoutInfo;
            var ticks     = layoutInfo.ticks;
            var ticksText = layoutInfo.ticksText;
            if (this.isDiscrete){
                if(this.useCompositeAxis){
                    this._buildCompositeScene(rootScene);
                } else {
                    var includeModulo   = this._tickIncludeModulo;
                    var hiddenLabelText = this.hiddenLabelText;
                    
                    rootScene.vars.tickIncludeModulo = includeModulo;
                    rootScene.vars.hiddenLabelText   = hiddenLabelText;
                    
                    var hiddenDatas, hiddenTexts, createHiddenScene, hiddenIndex;
                    if(includeModulo > 2) {
                        var keySep = rootScene.group.owner.keySep;
                        
                        createHiddenScene = function() {
                            var k = hiddenDatas.map(function(d) { return d.key; }).join(keySep);
                            var l = hiddenTexts.slice(0, 10).join(', ') + (hiddenTexts.length > 10 ? ', ...' : '');
                            var scene = new pvc.visual.CartesianAxisTickScene(rootScene, {
                                source:    hiddenDatas,
                                tick:      k,
                                tickRaw:   k,
                                tickLabel: l,
                                isHidden:  true
                            });
                            scene.dataIndex = hiddenIndex;
                            hiddenDatas = hiddenTexts = hiddenIndex = null;
                        };
                    }
                    
                    ticks.forEach(function(tickData, index){
                        var isHidden = (index % includeModulo) !== 0;
                        if(isHidden && includeModulo > 2) {
                            if(hiddenIndex == null){ hiddenIndex = index; }
                            (hiddenDatas || (hiddenDatas = [])).push(tickData);
                            (hiddenTexts || (hiddenTexts = [])).push(ticksText[index]);
                        } else {
                            if(hiddenDatas) { createHiddenScene(); }
                            var scene = new pvc.visual.CartesianAxisTickScene(rootScene, {
                                source:    tickData,
                                tick:      tickData.value,
                                tickRaw:   tickData.rawValue,
                                tickLabel: ticksText[index],
                                isHidden:  isHidden
                            });
                            
                            scene.dataIndex = index;
                        }
                    });
                    
                    if(hiddenDatas) { createHiddenScene(); }
                }
            } else {
                ticks.forEach(function(majorTick, index){
                    var scene = new pvc.visual.CartesianAxisTickScene(rootScene, {
                        tick:      majorTick,
                        tickRaw:   majorTick,
                        tickLabel: ticksText[index]
                    });
                    scene.dataIndex = index;
                }, this);
            }
        }
        
        return this._rootScene;
    },
    
    _buildCompositeScene: function(rootScene){
        
        var isV1Compat = this.compatVersion() <= 1;
         
        // Need this for code below not to throw when drawing the root
        rootScene.vars.tick = new pvc_ValueLabelVar('', "");
        
        recursive(rootScene);
        
        function recursive(scene){
            var data = scene.group;
            if(isV1Compat){
                // depending on the specific version the
                // properties nodeLabel and label existed as well
                var tickVar = scene.vars.tick;
                scene.nodeValue = scene.value = tickVar.rawValue;
                scene.nodeLabel = scene.label = tickVar.label;
            }
            
            if(data.childCount()){
                data
                    .children()
                    .each(function(childData){
                        var childScene = new pvc.visual.CartesianAxisTickScene(scene, {
                            source:    childData,
                            tick:      childData.value,
                            tickRaw:   childData.rawValue,
                            tickLabel: childData.label
                        });
                        childScene.dataIndex = childData.childIndex();
                        recursive(childScene);
                    });
            }
        }
    },
    
    _getRootData: function() {
        var chart = this.chart;
        var data  = chart.data;
        
        if (this.isDiscrete && this.useCompositeAxis) {
            var orientation = this.anchor;
            var reverse  = orientation == 'bottom' || orientation == 'left';
            data = chart.visualRoles[this.roleName].select(data, {visible: true, reverse: reverse});
        }
        
        return data;
    },
    
    renderOrdinalAxis: function(){
        var scale = this.scale,
            hiddenLabelText   = this.hiddenLabelText,
            includeModulo     = this._tickIncludeModulo,
            hiddenStep2       = includeModulo * scale.range().step / 2,
            anchorOpposite    = this.anchorOpposite(),
            anchorLength      = this.anchorLength(),
            anchorOrtho       = this.anchorOrtho(),
            anchorOrthoLength = this.anchorOrthoLength(),
            pvRule            = this.pvRule,
            rootScene         = this._getRootScene(),
            layoutInfo        = this._layoutInfo,
            isV1Compat        = this.compatVersion() <= 1;
        
        var wrapper;
        if(isV1Compat){
            // For use in child marks of pvTicksPanel
            var DataElement = function(tickVar){
                this.value = 
                this.absValue = tickVar.rawValue;
                this.nodeName = '' + (this.value || '');
                this.path = this.nodeName ? [this.nodeName] : [];
                this.label = 
                this.absLabel = tickVar.label;    
            };
            
            DataElement.prototype.toString = function(){
                return ''+this.value;
            };
            
            wrapper = function(v1f){
                return function(tickScene){
                    // Fix index due to the introduction of 
                    // pvTicksPanel panel.
                    var markWrapped = Object.create(this);
                    markWrapped.index = this.parent.index;
                    
                    return v1f.call(markWrapped, new DataElement(tickScene.vars.tick));
                };
            };
        }
        
        // Ticks correspond to each data in datas.
        // Ticks are drawn at the center of each band.
        
        var pvTicksPanel = new pvc.visual.Panel(this, this.pvPanel, {
                extensionId: 'ticksPanel'
            })
            .lock('data', rootScene.childNodes)
            .lock(anchorOpposite, 0) // top (of the axis panel)
            .lockMark(anchorOrtho, function(tickScene){
                return tickScene.isHidden ?
                       scale(tickScene.previousSibling.vars.tick.value) + hiddenStep2 :
                       scale(tickScene.vars.tick.value);
            })
            .lock('strokeDasharray', null)
            .lock('strokeStyle', null)
            .lock('fillStyle',   null)
            .lock('lineWidth',   0)
            .pvMark
            .zOrder(20) // below axis rule
            ;
        
        if(isV1Compat || this.showTicks){
            var pvTicks = this.pvTicks = new pvc.visual.Rule(this, pvTicksPanel, {
                    extensionId: 'ticks',
                    wrapper:  wrapper
                })
                .lock('data') // Inherited
                .intercept('visible', function(){
                    return !this.scene.isHidden && this.delegateExtension(true);
                })
                .optional('lineWidth', 1)
                .lock(anchorOpposite,  0) // top
                .lock(anchorOrtho,     0) // left
                .lock(anchorLength,    null)
                .optional(anchorOrthoLength, this.tickLength * 2/3) // slightly smaller than continuous ticks
                .override('defaultColor', function(type){
                    if(isV1Compat) {
                        return pv.Color.names.transparent;
                    }
                    
                    // Inherit ticks color from rule
                    // Control visibility through .visible or lineWidth
                    return pvRule.scene ? 
                           pvRule.scene[0].strokeStyle : 
                           "#666666";
                })
                .pvMark
                ;
        }
        
        var font = this.font;
        var maxTextWidth = this._layoutInfo.maxTextWidth;
        if(!isFinite(maxTextWidth)){
            maxTextWidth = 0;
        }
        
        // An pv anchor on pvTick is not used, on purpose,
        // cause if it were, hidding the tick with .visible,
        // would mess the positioning of the label...
        this.pvLabel = new pvc.visual.Label(
            this,
            pvTicksPanel,
            {
                extensionId:  'label',
                showsInteraction: true,
                noClick:       false,
                noDoubleClick: false,
                noSelect:      false,
                noTooltip:     false,
                noHover:       false, // TODO: to work, scenes would need a common root
                wrapper:       wrapper
            })
            .intercept('visible', function(tickScene) {
                return !tickScene.isHidden  ?
                       this.delegateExtension(true) :
                       !!tickScene.vars.hiddenLabelText;
            })
            .intercept('text', function(tickScene) {
                // Allow late overriding (does not affect layout..)
                var text;
                if(tickScene.isHidden) {
                    text = hiddenLabelText;
                } else {
                    text = this.delegateExtension();
                    if(text === undefined) {
                        text = tickScene.vars.tick.label;
                    }
                    
                    if(maxTextWidth && (!this.showsInteraction() || !this.scene.isActive)) {
                        text = pvc.text.trimToWidthB(maxTextWidth, text, font, "..", false);
                    }
                }
                
                return text;
             })
            .pvMark
            .zOrder(40) // above axis rule
            
            .lock(anchorOpposite, this.tickLength)
            .lock(anchorOrtho,    0)
            
            .font(font)
            .textStyle("#666666")
            .textAlign(layoutInfo.textAlign)
            .textBaseline(layoutInfo.textBaseline)
            ;
        
        this._debugTicksPanel(pvTicksPanel);
    },
    
    /** @override */
    _getTooltipFormatter: function(tipOptions) {
        if(this.axis.option('TooltipEnabled')) {
            
            tipOptions.gravity = this._calcTipsyGravity();
            
            var tooltipFormat = this.axis.option('TooltipFormat');
            if(tooltipFormat) {
                return function(context) {
                    return tooltipFormat.call(context, context.scene);
                };
            }
            
            var autoContent = this.axis.option('TooltipAutoContent');
            if(autoContent === 'summary') {
                return this._summaryTooltipFormatter;
            }
            
            if(autoContent === 'value') {
                tipOptions.isLazy = false;
                return function(context) { return context.scene.vars.tick.label; };
            }
        }
    },
    
    _debugTicksPanel: function(pvTicksPanel) {
        if(pvc.debug >= 16){ // one more than general debug box model
            var font = this.font;
            var li = this._layoutInfo;
            var ticksBBoxes = li.ticksBBoxes || this._calcTicksLabelBBoxes(li);
            
            pvTicksPanel
                // Single-point panel (w=h=0)
                .add(pv.Panel)
                    [this.anchorOpposite()](this.tickLength)
                    [this.anchorOrtho()](0)
                    [this.anchorLength()](0)
                    [this.anchorOrthoLength()](0)
                    .fillStyle(null)
                    .strokeStyle(null)
                    .lineWidth(0)
                    .visible(function(tickScene){ return !tickScene.isHidden; })
                 .add(pv.Line)
                    .data(function(scene){
                        var labelBBox = ticksBBoxes[scene.dataIndex];
                        var corners   = labelBBox.source.points();
                        
                        // Close the path
                        if(corners.length > 1){
                            // not changing corners on purpose
                            corners = corners.concat(corners[0]);
                        }
                        
                        return corners;
                    })
                    .left(function(p){ return p.x; })
                    .top (function(p){ return p.y; })
                    .strokeStyle('red')
                    .lineWidth(0.5)
                    .strokeDasharray('-')
                    ;
        }
    },
    
    renderLinearAxis: function(){
        // NOTE: Includes time series, 
        // so "tickScene.vars.tick.value" may be a number or a Date object...
        
        var scale  = this.scale,
            pvRule = this.pvRule,
            anchorOpposite    = this.anchorOpposite(),
            anchorLength      = this.anchorLength(),
            anchorOrtho       = this.anchorOrtho(),
            anchorOrthoLength = this.anchorOrthoLength(),
            rootScene         = this._getRootScene();
        
        var wrapper;
        if(this.compatVersion() <= 1){
            wrapper = function(v1f){
                return function(tickScene){
                    // Fix index due to the introduction of 
                    // pvTicksPanel panel.
                    var markWrapped = Object.create(this);
                    markWrapped.index = this.parent.index;
                    
                    return v1f.call(markWrapped, tickScene.vars.tick.rawValue);
                };
            };
        }
        
        var pvTicksPanel = new pvc.visual.Panel(this, this.pvPanel, {
                extensionId: 'ticksPanel'
            })
            .lock('data', rootScene.childNodes)
            .lock(anchorOpposite, 0) // top (of the axis panel)
            .lockMark(anchorOrtho, function(tickScene){
                return scale(tickScene.vars.tick.value);
            })
            .lock('strokeStyle', null)
            .lock('fillStyle',   null)
            .lock('lineWidth',   0)
            .pvMark
            .zOrder(20) // below axis rule
            ;
        
        if(this.showTicks){
            // (MAJOR) ticks
            var pvTicks = this.pvTicks = new pvc.visual.Rule(this, pvTicksPanel, {
                    extensionId: 'ticks',
                    wrapper: wrapper
                })
                .lock('data') // Inherited
                .override('defaultColor', function(){
                    // Inherit axis color
                    // Control visibility through color or through .visible
                    // NOTE: the rule only has one scene/instance
                    return pvRule.scene ? 
                           pvRule.scene[0].strokeStyle :
                           "#666666";
                })
                .lock(anchorOpposite, 0) // top
                .lock(anchorOrtho,    0) // left
                .lock(anchorLength,   null)
                .optional(anchorOrthoLength, this.tickLength)
                .pvMark
                ;
            
            // MINOR ticks are between major scale ticks
            if(this.showMinorTicks){
                var layoutInfo = this._layoutInfo;
                var ticks      = layoutInfo.ticks;
                var tickCount  = ticks.length;
                // Assume a linear scale
                var minorTickOffset = tickCount > 1 ? 
                        Math.abs(scale(ticks[1]) - scale(ticks[0])) / 2 : 
                        0;
                        
                this.pvMinorTicks = new pvc.visual.Rule(this, this.pvTicks, {
                        extensionId: 'minorTicks',
                        wrapper: wrapper
                    })
                    .lock('data') // Inherited
                    .intercept('visible', function(){
                        // The last minor tick isn't visible - only show between major ticks.
                        // Hide if the previous major tick is hidden.
                        var visible = (this.index < tickCount - 1) && 
                                      (!pvTicks.scene || pvTicks.scene[0].visible);
                        
                        return visible && this.delegateExtension(true);
                    })    
                    .override('defaultColor', function(){
                        // Inherit ticks color
                        // Control visibility through color or through .visible
                        return pvTicks.scene ? 
                               pvTicks.scene[0].strokeStyle : 
                               pv.Color.names.d;
                    })
                    .lock(anchorOpposite, 0) // top
                    .lock(anchorLength,   null)
                    .optional(anchorOrthoLength, this.tickLength / 2)
                    .lockMark(anchorOrtho, minorTickOffset)
                    .pvMark
                    ;
            }
        }
        
        this.renderLinearAxisLabel(pvTicksPanel, wrapper);
        
        this._debugTicksPanel(pvTicksPanel);
    },
    
    renderLinearAxisLabel: function(pvTicksPanel, wrapper){
        // Labels are visible (only) on MAJOR ticks,
        // On first and last tick care is taken
        // with their H/V alignment so that
        // the label is not drawn off the chart.

        var pvTicks = this.pvTicks;
        var anchorOpposite = this.anchorOpposite();
        var anchorOrtho    = this.anchorOrtho();
        var scale = this.scale;
        var font  = this.font;
        
        var maxTextWidth = this._layoutInfo.maxTextWidth;
        if(!isFinite(maxTextWidth)){
            maxTextWidth = 0;
        }
        
        var label = this.pvLabel = new pvc.visual.Label(this, pvTicksPanel, {
                extensionId: 'label',
                noHover: false,
                showsInteraction: true,
                wrapper: wrapper
            })
            .lock('data') // inherited
            .intercept('text', function(tickScene) {
                var text = tickScene.vars.tick.label;
                if(maxTextWidth && (!this.showsInteraction() || !this.scene.isActive)) {
                    text = pvc.text.trimToWidthB(maxTextWidth, text, font, '..', false);
                }
                return text;
             })
            .pvMark
            .lock(anchorOpposite, this.tickLength)
            .lock(anchorOrtho,    0)
            .zOrder(40) // above axis rule
            .font(this.font)
            .textStyle("#666666")
            //.textMargin(0.5) // Just enough for some labels not to be cut (vertical)
            ;
        
        // Label alignment
        var rootPanel = this.pvPanel.root;
        if(this.isAnchorTopOrBottom()){
            label
                .textBaseline(anchorOpposite)
                .textAlign(function(tickScene){
                    var absLeft;
                    if(this.index === 0){
                        absLeft = label.toScreenTransform().transformHPosition(label.left());
                        if(absLeft <= 0){
                            return 'left'; // the "left" of the text is anchored to the tick's anchor
                        }
                    } else if(this.index === tickScene.parent.childNodes.length - 1) { 
                        absLeft = label.toScreenTransform().transformHPosition(label.left());
                        if(absLeft >= rootPanel.width()){
                            return 'right'; // the "right" of the text is anchored to the tick's anchor
                        }
                    }
                    
                    return 'center';
                });
        } else {
            label
                .textAlign(anchorOpposite)
                .textBaseline(function(tickScene){
                    var absTop;
                    if(this.index === 0){
                        absTop = label.toScreenTransform().transformVPosition(label.top());
                        if(absTop >= rootPanel.height()){
                            return 'bottom'; // the "bottom" of the text is anchored to the tick's anchor
                        }
                    } else if(this.index === tickScene.parent.childNodes.length - 1) { 
                        absTop = label.toScreenTransform().transformVPosition(label.top());
                        if(absTop <= 0){
                            return 'top'; // the "top" of the text is anchored to the tick's anchor
                        }
                    }
                    
                    return 'middle';
                });
        }
    },

    // ----------------------------
    // Click / Double-click
    _onV1Click: function(context, handler){
        if(this.isDiscrete && this.useCompositeAxis){
            handler.call(context.pvMark, context.scene, context.event);
        }
    },
    
    _onV1DoubleClick: function(context, handler){
        if(this.isDiscrete && this.useCompositeAxis){
            handler.call(context.pvMark, context.scene, context.event);
        }
    },
    
    /** @override */
    _getSelectableMarks: function(){
        if(this.isDiscrete && this.isVisible && this.pvLabel){
            return this.base();
        }
    },

    /////////////////////////////////////////////////
    //begin: composite axis
    renderCompositeOrdinalAxis: function(){
        var isTopOrBottom = this.isAnchorTopOrBottom(),
            axisDirection = isTopOrBottom ? 'h' : 'v',
            diagDepthCutoff = 2, // depth in [-1/(n+1), 1]
            vertDepthCutoff = 2,
            font = this.font;
        
        var diagMargin = pv.Text.fontHeight(font) / 2;
        
        var layout = this._pvLayout = this.getLayoutSingleCluster();

        // See what will fit so we get consistent rotation
        layout.node
            .def("fitInfo", null)
            .height(function(tickScene, e, f){
                // Just iterate and get cutoff
                var fitInfo = pvc.text.getFitInfo(tickScene.dx, tickScene.dy, tickScene.vars.tick.label, font, diagMargin);
                if(!fitInfo.h){
                    if(axisDirection === 'v' && fitInfo.v){ // prefer vertical
                        vertDepthCutoff = Math.min(diagDepthCutoff, tickScene.depth);
                    } else {
                        diagDepthCutoff = Math.min(diagDepthCutoff, tickScene.depth);
                    }
                }

                this.fitInfo(fitInfo);

                return tickScene.dy;
            });

        // label space (left transparent)
        // var lblBar =
        layout.node.add(pv.Bar)
            .fillStyle('rgba(127,127,127,.001)')
            .strokeStyle(function(tickScene){
                if(tickScene.maxDepth === 1 || !tickScene.maxDepth) { // 0, 0.5, 1
                    return null;
                }

                return "rgba(127,127,127,0.3)"; //non-terminal items, so grouping is visible
            })
            .lineWidth( function(tickScene){
                if(tickScene.maxDepth === 1 || !tickScene.maxDepth) {
                    return 0;
                }
                return 0.5; //non-terminal items, so grouping is visible
            })
            .text(function(tickScene){
                return tickScene.vars.tick.label;
            });

        //cutoffs -> snap to vertical/horizontal
        var H_CUTOFF_ANG = 0.30,
            V_CUTOFF_ANG = 1.27;
        
        var align = isTopOrBottom ?
                    "center" :
                    (this.anchor == "left") ? "right" : "left";
        
        var wrapper;
        if(this.compatVersion() <= 1){
            wrapper = function(v1f){
                return function(tickScene){
                    return v1f.call(this, tickScene);
                };
            };
        }
        
        // draw labels and make them fit
        this.pvLabel = new pvc.visual.Label(this, layout.label, {
                extensionId:  'label',
                noClick:       false,
                noDoubleClick: false,
                noSelect:      false,
                noTooltip:     false,
                noHover:       false, // TODO: to work, scenes would need a common root
                showsInteraction: true,
                wrapper:       wrapper,
                tooltipArgs:   {
                    options: {offset: diagMargin * 2}
                }
            })
            .pvMark
            .def('lblDirection', 'h')
            .textAngle(function(tickScene){
                if(tickScene.depth >= vertDepthCutoff && tickScene.depth < diagDepthCutoff){
                    this.lblDirection('v');
                    return -Math.PI/2;
                }

                if(tickScene.depth >= diagDepthCutoff){
                    var tan = tickScene.dy/tickScene.dx;
                    var angle = Math.atan(tan);
                    //var hip = Math.sqrt(tickScene.dy*tickScene.dy + tickScene.dx*tickScene.dx);

                    if(angle > V_CUTOFF_ANG){
                        this.lblDirection('v');
                        return -Math.PI/2;
                    }

                    if(angle > H_CUTOFF_ANG) {
                        this.lblDirection('d');
                        return -angle;
                    }
                }

                this.lblDirection('h');
                return 0;//horizontal
            })
            .textMargin(1)
            //override central alignment for horizontal text in vertical axis
            .textAlign(function(tickScene){
                return (axisDirection != 'v' || tickScene.depth >= vertDepthCutoff || tickScene.depth >= diagDepthCutoff)? 'center' : align;
            })
            .left(function(tickScene) {
                return (axisDirection != 'v' || tickScene.depth >= vertDepthCutoff || tickScene.depth >= diagDepthCutoff)?
                     tickScene.x + tickScene.dx/2 :
                     ((align == 'right')? tickScene.x + tickScene.dx : tickScene.x);
            })
            .font(font)
            .textStyle("#666666")
            .text(function(tickScene){
                var label = tickScene.vars.tick.label;
                var sign = this.sign;
                if(!sign.scene.isActive || !sign.showsInteraction()){
                    var fitInfo = this.fitInfo();
                    switch(this.lblDirection()){
                        case 'h':
                            if(!fitInfo.h){
                                return pvc.text.trimToWidthB(tickScene.dx, label, font, '..');
                            }
                            break;
                        case 'v':
                            if(!fitInfo.v){
                                return pvc.text.trimToWidthB(tickScene.dy, label, font, '..');
                            }
                            break;
                        case 'd':
                           if(!fitInfo.d){
                              //var ang = Math.atan(tickScene.dy/tickScene.dx);
                              var diagonalLength = Math.sqrt(def.sqr(tickScene.dy) + def.sqr(tickScene.dx));
                              return pvc.text.trimToWidthB(diagonalLength - diagMargin, label, font, '..');
                            }
                            break;
                    }
                }
                
                return label;
            })
            ;
    },
    
    getLayoutSingleCluster: function(){
        var rootScene   = this._getRootScene(),
            orientation = this.anchor,
            maxDepth    = rootScene.group.treeHeight,
            depthLength = this._layoutInfo.axisSize;
        
        // displace to take out bogus-root
        maxDepth++;
        
        var baseDisplacement = depthLength / maxDepth,
            margin = maxDepth > 2 ? ((1/12) * depthLength) : 0; // heuristic compensation
        
        baseDisplacement -= margin;
        
        var scaleFactor = maxDepth / (maxDepth - 1),
            orthoLength = pvc.BasePanel.orthogonalLength[orientation];
        
        var displacement = (orthoLength == 'width') ?
                (orientation === 'left' ? [-baseDisplacement, 0] : [baseDisplacement, 0]) :
                (orientation === 'top'  ? [0, -baseDisplacement] : [0, baseDisplacement]);

        this.pvRule
            .sign
            .override('defaultColor',       def.fun.constant(null))
            .override('defaultStrokeWidth', def.fun.constant(0)   )
            ;

        var panel = this.pvRule
            .add(pv.Panel)
                [orthoLength](depthLength)
                .strokeStyle(null)
                .lineWidth(0) //cropping panel
            .add(pv.Panel)
                [orthoLength](depthLength * scaleFactor)
                .strokeStyle(null)
                .lineWidth(0);// panel resized and shifted to make bogus root disappear
        
        panel.transform(pv.Transform.identity.translate(displacement[0], displacement[1]));
        
        // Create with bogus-root
        // pv.Hierarchy must always have exactly one root and
        //  at least one element besides the root
        return panel.add(pv.Layout.Cluster.Fill)
                    .nodes(rootScene.nodes())
                    .orient(orientation);
    },
    
    _calcTipsyGravity: function(){
        switch(this.anchor){
            case 'bottom': return 's';
            case 'top':    return 'n';
            case 'left':   return 'w';
            case 'right':  return 'e';
        }
        return 's';
    }
    // end: composite axis
    /////////////////////////////////////////////////
});


/*global pvc_Size:true */

def
.type('pvc.AxisTitlePanel', pvc.TitlePanelAbstract)
.init(function(chart, parent, axis, options) {
    
    this.axis = axis;
    
    this.base(chart, parent, options);
    
    this._extensionPrefix = 
        axis
        .extensionPrefixes
        .map(function(prefix){
            return prefix + 'Title';
        });
})
.add({
    _calcLayout: function(layoutInfo){
        var scale = this.axis.scale;
        if(!scale || scale.isNull){
            return new pvc_Size(0, 0);
        }
        
        return this.base(layoutInfo);
    },
    
    _createCore: function(layoutInfo){
        var scale = this.axis.scale;
        if(!scale || scale.isNull){
            return;
        }
        
        return this.base(layoutInfo);
    }
});


/*global pvc_Size:true, pvc_PercentValue:true, pvc_ValueLabelVar:true */

/*
 * Pie chart panel. Generates a pie chart.
 *
 * Specific options are:
 *
 * <i>valuesVisible</i> - Show or hide slice value. Default: false
 *
 * <i>explodedSliceIndex</i> - Index of the slice which is <i>always</i> exploded, or null to explode every slice. Default: null.
 *
 * <i>explodedOffsetRadius</i> - The radius by which an exploded slice is offset from the center of the pie (in pixels).
 * If one wants a pie with an exploded effect, specify a value in pixels here.
 * If above argument is specified, explodes only one slice, else explodes all.
 * Default: 0
 *
 * <i>activeOffsetRadius</i> - Percentage of slice radius to (additionally) explode an active slice.
 * Only used if the chart has option hoverable equal to true.
 *
 * <i>innerGap</i> - The percentage of (the smallest of) the panel width or height used by the pie.
 * Default: 0.9 (90%)
 *
 * Deprecated in favor of options <i>leafContentMargins</i> and <i>leafContentPaddings</i>.
 *
 * Has the following protovis extension points:
 * <i>chart_</i> - for the main chart Panel
 * <i>slice_</i> - for the main pie wedge
 * <i>sliceLabel_</i> - for the main pie label
 * <i>sliceLinkLine_</i> - for the link lines, for when labelStyle = 'linked'
 *
 * Example Pie Category Scene extension:
 * pie: {
 *     scenes: {
 *         category: {
 *             sliceLabelMask: "{value} ({value.percent})"
 *         }
 *     }
 * }
 */

def
.type('pvc.PiePanel', pvc.PlotPanel)
.init(function(chart, parent, plot, options){

    // Before base, just to bring to attention that ValuesMask depends on it
    var labelStyle = plot.option('ValuesLabelStyle');

    this.base(chart, parent, plot, options);

    this.explodedOffsetRadius = plot.option('ExplodedSliceRadius');
    this.explodedSliceIndex   = plot.option('ExplodedSliceIndex' );
    this.activeOffsetRadius   = plot.option('ActiveSliceRadius'  );
    this.labelStyle           = labelStyle;
    if(labelStyle === 'linked') {
        this.linkInsetRadius     = plot.option('LinkInsetRadius'    );
        this.linkOutsetRadius    = plot.option('LinkOutsetRadius'   );
        this.linkMargin          = plot.option('LinkMargin'         );
        this.linkHandleWidth     = plot.option('LinkHandleWidth'    );
        this.linkLabelSize       = plot.option('LinkLabelSize'      );
        this.linkLabelSpacingMin = plot.option('LinkLabelSpacingMin');
    }
})
.add({
    pvPie: null,
    pvPieLabel: null,

    valueRoleName: 'value',

    _getV1Datum: function(scene){
        // Ensure V1 tooltip function compatibility
        var datum = scene.datum;
        if(datum){
            var datumEx = Object.create(datum);
            datumEx.percent = scene.vars.value.percent;
            datum = datumEx;
        }

        return datum;
    },

    /**
     * @override
     */
    _calcLayout: function(layoutInfo) {
        var clientSize   = layoutInfo.clientSize;
        var clientWidth  = clientSize.width;
        var clientRadius = Math.min(clientWidth, clientSize.height) / 2;
        if(!clientRadius) { return new pvc_Size(0, 0); }

        var center = pv.vector(clientSize.width / 2, clientSize.height / 2);

        function resolvePercentRadius(radius) {
            return def.between(pvc_PercentValue.resolve(radius, clientRadius), 0, clientRadius);
        }

        function resolvePercentWidth(width) {
            return def.between(pvc_PercentValue.resolve(width, clientWidth), 0, clientWidth);
        }

        // ---------------------

        var labelFont = this._getConstantExtension('label', 'font');
        if(!def.string.is(labelFont)) { labelFont = this.valuesFont; }

        var maxPieRadius = clientRadius;

        if(this.valuesVisible && this.labelStyle === 'linked') {
            // Reserve space for labels and links
            var linkInsetRadius  = resolvePercentRadius(this.linkInsetRadius );
            var linkOutsetRadius = resolvePercentRadius(this.linkOutsetRadius);
            var linkMargin       = resolvePercentWidth (this.linkMargin      );
            var linkLabelSize    = resolvePercentWidth (this.linkLabelSize   );

            var textMargin = def.number.to(this._getConstantExtension('label', 'textMargin'), 3);
            var textHeight = pv.Text.fontHeight(labelFont) * 2/3;

            var linkHandleWidth = this.linkHandleWidth * textHeight; // em
            linkMargin += linkHandleWidth;

            var linkLabelSpacingMin = this.linkLabelSpacingMin * textHeight; // em

            var freeWidthSpace = Math.max(0, clientWidth / 2 - clientRadius);

            // Radius stolen to pie by link and label
            var spaceH = Math.max(0, linkOutsetRadius + linkMargin + linkLabelSize - freeWidthSpace);
            var spaceV = linkOutsetRadius + textHeight; // at least one line of text (should be half line, but this way there's a small margin...)

            var linkAndLabelRadius = Math.max(0, spaceV, spaceH);

            // Use the extra width on the label
            //linkLabelSize += freeWidthSpace / 2;

            if(linkAndLabelRadius >= maxPieRadius) {
                this.valuesVisible = false;
                if(pvc.debug >= 2) {
                    this._log("Hiding linked labels due to insufficient space.");
                }
            } else {

                maxPieRadius -= linkAndLabelRadius;

                layoutInfo.link = {
                    insetRadius:     linkInsetRadius,
                    outsetRadius:    linkOutsetRadius,
                    elbowRadius:     maxPieRadius + linkOutsetRadius,
                    linkMargin:      linkMargin,
                    handleWidth:     linkHandleWidth,
                    labelSize:       linkLabelSize,
                    maxTextWidth:    linkLabelSize - textMargin,
                    labelSpacingMin: linkLabelSpacingMin,
                    textMargin:      textMargin,
                    lineHeight:      textHeight
                };
            }
        }

        // ---------------------

        var explodedOffsetRadius = resolvePercentRadius(this.explodedOffsetRadius);

        var activeOffsetRadius = 0;
        if(this.hoverable()) {
            activeOffsetRadius = resolvePercentRadius(this.activeOffsetRadius);
        }

        var maxOffsetRadius = explodedOffsetRadius + activeOffsetRadius;

        var normalPieRadius = maxPieRadius - maxOffsetRadius;
        if(normalPieRadius < 0) { return new pvc_Size(0,0); }

        // ---------------------
        layoutInfo.resolvePctRadius = resolvePercentRadius;
        layoutInfo.center = center;
        layoutInfo.clientRadius = clientRadius;
        layoutInfo.normalRadius = normalPieRadius;
        layoutInfo.explodedOffsetRadius = explodedOffsetRadius;
        layoutInfo.activeOffsetRadius = activeOffsetRadius;
        layoutInfo.maxOffsetRadius = maxOffsetRadius;
        layoutInfo.labelFont = labelFont;
    },

    /**
     * @override
     */
    _createCore: function(layoutInfo) {
        var me = this;
        var chart = me.chart;
        var rootScene = this._buildScene();
        var center = layoutInfo.center;
        var normalRadius = layoutInfo.normalRadius;

        // ------------

        var wrapper;
        var extensionIds = ['slice'];
        if(this.compatVersion() <= 1) {
            extensionIds.push(''); // let access as "pie_"
            wrapper = function(v1f) {
                return function(pieCatScene) {
                    return v1f.call(this, pieCatScene.vars.value.value);
                };
            };
        }

        this.pvPie = new pvc.visual.PieSlice(this, this.pvPanel, {
                extensionId: extensionIds,
                center: center,
                activeOffsetRadius: layoutInfo.activeOffsetRadius,
                maxOffsetRadius: layoutInfo.maxOffsetRadius,
                resolvePctRadius: layoutInfo.resolvePctRadius,
                wrapper: wrapper,
                tooltipArgs: {
                    options: {
                        useCorners: true,
                        gravity: function() {
                            var ma = this.midAngle();
                            var isRightPlane = Math.cos(ma) >= 0;
                            var isTopPlane   = Math.sin(ma) >= 0;
                            return  isRightPlane ?
                                    (isTopPlane ? 'nw' : 'sw') :
                                    (isTopPlane ? 'ne' : 'se');
                        }
                    }
                }
            })

            .lock('data', rootScene.childNodes)

            .override('angle', function() { return this.scene.vars.value.angle;  })

            .override('defaultOffsetRadius', function() {
                var explodeIndex = me.explodedSliceIndex;
                if (explodeIndex == null || explodeIndex == this.pvMark.index) {
                    return layoutInfo.explodedOffsetRadius;
                }

                return 0;
            })

            .lock('outerRadius', function() { return chart.animate(0, normalRadius); })

            .localProperty('innerRadiusEx', pvc_PercentValue.parse)

            // In case the inner radius is specified, we better animate it as well
            .intercept('innerRadius', function(scene) {
                var innerRadius = this.delegateExtension();
                if(innerRadius == null) {
                    var innerRadiusPct = this.pvMark.innerRadiusEx();
                    if(innerRadiusPct != null) {
                        innerRadius = pvc_PercentValue.resolve(
                                    innerRadiusPct,
                                    this.pvMark.outerRadius()) || 0;
                    } else {
                        innerRadius = 0;
                    }
                }

                return innerRadius > 0 ? chart.animate(0, innerRadius) : 0;
            })
            .pvMark;

        if(this.valuesVisible) {
            this.valuesFont = layoutInfo.labelFont;

            if(this.labelStyle === 'inside') {
                this.pvPieLabel = pvc.visual.ValueLabel.maybeCreate(this, this.pvPie, {
                        wrapper: wrapper
                    })
                    .intercept('visible', function(scene) {
                        return (scene.vars.value.angle >= 0.001) && this.delegateExtension(true);
                    })
                    .override('defaultText', function() { return this.scene.vars.value.sliceLabel; })
                    .pvMark
                    .textMargin(10);

            } else if(this.labelStyle === 'linked') {
                var linkLayout = layoutInfo.link;

                rootScene.layoutLinkLabels(layoutInfo);

                this.pvLinkPanel = this.pvPanel.add(pv.Panel)
                    .data(rootScene.childNodes)
                    .localProperty('pieSlice')
                    .pieSlice(function(/*scene*/) { return me.pvPie.scene[this.index]; });

                var f = false, t = true;
                this.pvLinkLine = new pvc.visual.Line(
                    this,
                    this.pvLinkPanel,
                    {
                        extensionId:  'linkLine',
                        freePosition:  t,
                        noClick:       t,
                        noDoubleClick: t,
                        noSelect:      t,
                        noTooltip:     t,
                        noHover:       t,
                        showsActivity: f
                    })
                    .lockMark('data', function(scene) {
                        // Calculate the dynamic dot at the
                        // slice's middle angle and outer radius...
                        var pieSlice = this.parent.pieSlice();
                        var midAngle = pieSlice.startAngle + pieSlice.angle / 2;
                        var outerRadius = pieSlice.outerRadius - linkLayout.insetRadius;
                        var x = pieSlice.left + outerRadius * Math.cos(midAngle);
                        var y = pieSlice.top  + outerRadius * Math.sin(midAngle);

                        var firstDotScene = scene.childNodes[0];
                        if(!firstDotScene || !firstDotScene._isFirstDynamicScene) {
                            firstDotScene = new pvc.visual.PieLinkLineScene(
                                scene, x, y, /* index */ 0);

                            firstDotScene._isFirstDynamicScene = t;
                        } else {
                            firstDotScene.x = x;
                            firstDotScene.y = y;
                        }

                        return scene.childNodes;
                    })
                    .override('defaultColor', function(type) {
                        return type === 'stroke' ? 'black' : this.base(type);
                    })
                    .override('defaultStrokeWidth', def.fun.constant(0.5))
                    .pvMark
                    .lock('visible')
                    .lock('top',  function(dot) { return dot.y; })
                    .lock('left', function(dot) { return dot.x; });

                this.pvPieLabel = new pvc.visual.Label(
                    this,
                    this.pvLinkPanel,
                    {
                        extensionId:   'label',
                        noClick:       f,
                        noDoubleClick: f,
                        noSelect:      f,
                        noHover:       f,
                        showsInteraction: t
                    })
                    .lockMark('data', function(scene) {
                        // Repeat the scene, once for each line
                        return scene.lineScenes;
                    })
                    .intercept('textStyle', function() {
                        this._finished = f;
                        var style = this.delegate();
                        if(style &&
                           !this._finished &&
                           !this.mayShowActive() &&
                           this.mayShowNotAmongSelected()){
                            style = this.dimColor(style, 'text');
                        }

                        return style;
                    })
                    .pvMark
                    .lock('visible')
                    .left     (function(scene) { return scene.vars.link.labelX; })
                    .top      (function(scene) { return scene.vars.link.labelY + ((this.index + 1) * linkLayout.lineHeight); }) // must be mark.index because of repeating scene...
                    .textAlign(function(scene) { return scene.vars.link.labelAnchor; })
                    .textMargin(linkLayout.textMargin)
                    .textBaseline('bottom')
                    .text     (function(scene) { return scene.vars.link.labelLines[this.index]; });

                // <Debug>
                if(pvc.debug >= 20) {
                    this.pvPanel.add(pv.Panel)
                        .zOrder(-10)
                        .left  (center.x - layoutInfo.clientRadius)
                        .top   (center.y - layoutInfo.clientRadius)
                        .width (layoutInfo.clientRadius * 2)
                        .height(layoutInfo.clientRadius * 2)
                        .strokeStyle('red');

                    // Client Area
                    this.pvPanel
                        .strokeStyle('green');

                    var linkColors = pv.Colors.category10();
                    this.pvLinkLine
                        .segmented(t)
                        .strokeStyle(function() { return linkColors(this.index); });
                }
                // </Debug>
            }

            this.pvPieLabel
                .font(layoutInfo.labelFont);
        }
    },

    _getExtensionId: function() {
        // 'chart' is deprecated
        // 'content' coincides, visually, with 'plot', in this chart type
        // - actually it shares the same panel...

        var extensionIds = [{abs: 'content'}];
        if(this.chart.parent) {
            extensionIds.push({abs: 'smallContent'});
        }

        return extensionIds.concat(this.base());
    },

    /**
     * Renders this.pvBarPanel - the parent of the marks that are affected by selection changes.
     * @override
     */
    renderInteractive: function() {
        this.pvPanel.render();
    },

    _buildScene: function() {
        var rootScene  = new pvc.visual.PieRootScene(this);

        // v1 property
        this.sum = rootScene.vars.sumAbs.value;

        return rootScene;
    }
});

def
.type('pvc.visual.PieRootScene', pvc.visual.Scene)
.init(function(panel) {
    var data = panel.visualRoles.category.flatten(panel.data, pvc.data.visibleKeyArgs);

    this.base(null, {panel: panel, source: data});

    var colorVarHelper = new pvc.visual.RoleVarHelper(this, panel.visualRoles.color, {roleVar: 'color'});

    // ---------------

    var valueRoleName = panel.valueRoleName;
    var valueDimName  = panel.visualRoles[valueRoleName].firstDimensionName();
    var valueDim      = data.dimensions(valueDimName);

    var options = panel.chart.options;
    var percentValueFormat = options.percentValueFormat;

    var rootScene = this;
    var sumAbs = 0;

    /* Create category scene sub-class */
    var CategSceneClass = def.type(pvc.visual.PieCategoryScene)
        .init(function(categData, value) {

            // Adds to parent scene...
            this.base(rootScene, {source: categData});

            this.vars.category = pvc_ValueLabelVar.fromComplex(categData);

            sumAbs += Math.abs(value);

            this.vars.value = new pvc_ValueLabelVar(
                            value,
                            formatValue(value, categData));

            colorVarHelper.onNewScene(this, /* isLeaf */ true);
        });

    /* Extend with any user extensions */
    panel._extendSceneType('category', CategSceneClass, ['sliceLabel', 'sliceLabelMask']);

    /* Create child category scenes */
    data.children().each(function(categData) {
        // Value may be negative
        // Don't create 0-value scenes
        var value = categData.dimensions(valueDimName).sum(pvc.data.visibleKeyArgs);
        if(value !== 0) { new CategSceneClass(categData, value); }
    });

    // -----------

    // TODO: should this be in something like: chart.axes.angle.scale ?
    this.angleScale = pv.Scale
                        .linear(0, sumAbs)
                        .range(0, 2 * Math.PI)
                        .by1(Math.abs);

    this.vars.sumAbs = new pvc_ValueLabelVar(sumAbs, formatValue(sumAbs));

    this.childNodes.forEach(function(categScene) {
        completeBuildCategScene.call(categScene);
    });

    function formatValue(value, categData) {
        if(categData) {
            var datums = categData._datums;
            if(datums.length === 1) {
                // Prefer to return the already formatted/provided label
                return datums[0].atoms[valueDimName].label;
            }
        }

        return valueDim.format(value);
    }

    /**
     * @private
     * @instance pvc.visual.PieCategoryScene
     */
    function completeBuildCategScene() {
        var valueVar = this.vars.value;

        // Calculate angle (span)
        valueVar.angle = this.parent.angleScale(valueVar.value);

        // Create percent sub-var of the value var
        var percent = Math.abs(valueVar.value) / sumAbs;

        valueVar.percent = new pvc_ValueLabelVar(
                percent,
                percentValueFormat(percent));

        // Calculate slice label
        valueVar.sliceLabel = this.sliceLabel();
    }
})
.add({
    layoutLinkLabels: function(layoutInfo) {
        var startAngle = -Math.PI / 2;

        var leftScenes  = [];
        var rightScenes = [];

        this.childNodes.forEach(function(categScene) {
            startAngle = categScene.layoutI(layoutInfo, startAngle);

            (categScene.vars.link.dir > 0 ? rightScenes : leftScenes)
            .push(categScene);
        });

        // Distribute left and right labels and finish their layout
        this._distributeLabels(-1, leftScenes,  layoutInfo);
        this._distributeLabels(+1, rightScenes, layoutInfo);
    },

    _distributeLabels: function(dir, scenes, layoutInfo) {
        // Initially, for each category scene,
        //   targetY = elbowY
        // Taking additionally labelHeight into account,
        //  if this position causes overlapping, find a != targetY
        //  that does not cause overlap.

        // Sort scenes by Y position
        scenes.sort(function(sceneA, sceneB) {
            return def.compare(sceneA.vars.link.targetY, sceneB.vars.link.targetY);
        });

        /*jshint expr:true */
        this._distributeLabelsDownwards(scenes, layoutInfo) &&
        this._distributeLabelsUpwards  (scenes, layoutInfo) &&
        this._distributeLabelsEvenly   (scenes, layoutInfo);

        scenes.forEach(function(categScene) { categScene.layoutII(layoutInfo); });
    },

    _distributeLabelsDownwards: function(scenes, layoutInfo) {
        var linkLayout = layoutInfo.link;
        var labelSpacingMin = linkLayout.labelSpacingMin;
        var yMax = layoutInfo.clientSize.height;
        var overlapping = false;
        for(var i = 0, J = scenes.length - 1 ; i < J ; i++) {
            var linkVar0 = scenes[i].vars.link;

            if(!i && linkVar0.labelTop() < 0) { overlapping = true; }

            var linkVar1 = scenes[i + 1].vars.link;
            var labelTopMin1 = linkVar0.labelBottom() + labelSpacingMin;
            if (linkVar1.labelTop() < labelTopMin1) {

                var halfLabelHeight1 = linkVar1.labelHeight / 2;
                var targetY1 = labelTopMin1 + halfLabelHeight1;
                var targetYMax = yMax - halfLabelHeight1;
                if(targetY1 > targetYMax) {
                    overlapping = true;
                    linkVar1.targetY = targetYMax;
                } else {
                    linkVar1.targetY = targetY1;
                }
            }
        }

        return overlapping;
    },

    _distributeLabelsUpwards: function(scenes, layoutInfo) {
        var linkLayout = layoutInfo.link;
        var labelSpacingMin = linkLayout.labelSpacingMin;

        var overlapping = false;
        for(var i = scenes.length - 1 ; i > 0 ; i--) {
            var linkVar1 = scenes[i - 1].vars.link;
            var linkVar0 = scenes[i].vars.link;
            if(i === 1 && linkVar1.labelTop() < 0) { overlapping = true; }

            var labelBottomMax1 = linkVar0.labelTop() - labelSpacingMin;
            if (linkVar1.labelBottom() > labelBottomMax1) {
                var halfLabelHeight1 = linkVar1.labelHeight / 2;
                var targetY1   = labelBottomMax1 - halfLabelHeight1;
                var targetYMin = halfLabelHeight1;
                if(targetY1 < targetYMin) {
                    overlapping = true;
                    linkVar1.targetY = targetYMin;
                } else {
                    linkVar1.targetY = targetY1;
                }
            }
        }

        return overlapping;
    },

    _distributeLabelsEvenly: function(scenes, layoutInfo) {
        var totalHeight = 0;
        scenes.forEach(function(categScene) {
            totalHeight += categScene.vars.link.labelHeight;
        });

        var freeSpace = layoutInfo.clientSize.height - totalHeight; // may be < 0
        var labelSpacing = freeSpace;
        if(scenes.length > 1) {
            labelSpacing /= (scenes.length - 1);
        }

        var y = 0;
        scenes.forEach(function(scene) {
            var linkVar = scene.vars.link;
            var halfLabelHeight = linkVar.labelHeight / 2;
            y += halfLabelHeight;
            linkVar.targetY = y;
            y += halfLabelHeight + labelSpacing;
        });

        return true;
    }
});

def
.type('pvc.visual.PieLinkLabelVar') // TODO : Var base class
.add({
    labelTop:    function() { return this.targetY - this.labelHeight / 2; },
    labelBottom: function() { return this.targetY + this.labelHeight / 2; }
});

def
.type('pvc.visual.PieCategoryScene', pvc.visual.Scene)
.add({
    // extendable
    sliceLabelMask: function() { return this.panel().valuesMask; },

    // extendable
    sliceLabel: function() { return this.format(this.sliceLabelMask()); },

    layoutI: function(layoutInfo, startAngle) {
        var valueVar = this.vars.value;
        var endAngle = startAngle + valueVar.angle;
        var midAngle = (startAngle + endAngle) / 2;

        // Overwrite existing link var, if any.
        var linkVar = (this.vars.link = new pvc.visual.PieLinkLabelVar());

        var linkLayout = layoutInfo.link;

        var labelLines = pvc.text.justify(valueVar.sliceLabel, linkLayout.maxTextWidth, layoutInfo.labelFont);
        var lineCount = labelLines.length;
        linkVar.labelLines  = labelLines;
        linkVar.labelHeight = lineCount * linkLayout.lineHeight;

        this.lineScenes = def.array.create(lineCount, this);

        var cosMid = Math.cos(midAngle);
        var sinMid = Math.sin(midAngle);

        var isAtRight = cosMid >= 0;
        var dir = isAtRight ? 1 : -1;

        // Label anchor is at the side with opposite name to the side of the pie where it is placed.
        linkVar.labelAnchor = isAtRight ?  'left' : 'right';

        var center = layoutInfo.center;
        var elbowRadius = linkLayout.elbowRadius;
        var elbowX = center.x + elbowRadius * cosMid;
        var elbowY = center.y + elbowRadius * sinMid; // baseY

        var anchorX = center.x + dir * elbowRadius;
        var targetX = anchorX + dir * linkLayout.linkMargin;

        new pvc.visual.PieLinkLineScene(this, elbowX,  elbowY);
        new pvc.visual.PieLinkLineScene(this, anchorX, elbowY);

        linkVar.elbowY  = elbowY;
        linkVar.targetY = elbowY + 0;
        linkVar.targetX = targetX;
        linkVar.dir = dir;

        return endAngle;
    },

    layoutII: function(layoutInfo) {
        var linkVar = this.vars.link;

        var targetY = linkVar.targetY;
        var targetX = linkVar.targetX;

        var handleWidth = layoutInfo.link.handleWidth;
        if(handleWidth > 0) {
            new pvc.visual.PieLinkLineScene(this, targetX - linkVar.dir * handleWidth, targetY);
        }

        new pvc.visual.PieLinkLineScene(this, targetX, targetY);

        linkVar.labelX = targetX;
        linkVar.labelY = targetY - linkVar.labelHeight/2;
    }
});

def
.type('pvc.visual.PieLinkLineScene', pvc.visual.Scene)
.init(function(catScene, x, y, index) {
    this.base(catScene, {source: catScene.group, index: index});

    this.x = x;
    this.y = y;
})
.add(pv.Vector);

/*global pvc_PercentValue:true */

/**
 * PieChart is the main class for generating... pie charts (surprise!).
 */
def
.type('pvc.PieChart', pvc.BaseChart)
.add({
    _animatable: true,
    
    pieChartPanel: null,

    _getColorRoleSpec: function(){
        return { isRequired: true, defaultSourceRole: 'category', defaultDimension: 'color*', requireIsDiscrete: true };
    },
    
    /**
     * Initializes each chart's specific roles.
     * @override
     */
    _initVisualRoles: function(){
        
        this.base();
        
        this._addVisualRole('category', { 
                isRequired: true, 
                defaultDimension: 'category*', 
                autoCreateDimension: true 
            });
            
        this._addVisualRole('value', { 
                isMeasure:  true,
                isRequired: true,
                isPercent:  true,
                requireSingleDimension: true, 
                requireIsDiscrete: false,
                valueType: Number, 
                defaultDimension: 'value' 
            });
    },
    
    _initPlotsCore: function(/*hasMultiRole*/){
        new pvc.visual.PiePlot(this);
    },
    
    _preRenderContent: function(contentOptions) {

        this.base();
        
        var isV1Compat = this.compatVersion() <= 1;
        if(isV1Compat){
            var innerGap = pvc.castNumber(this.options.innerGap) || 0.95;
            innerGap = def.between(innerGap, 0.1, 1);
            contentOptions.paddings = ((1 - innerGap) * 100 / 2).toFixed(2) + "%";
        } else if(contentOptions.paddings == null) {
            contentOptions.paddings = new pvc_PercentValue(0.025);
        }
        
        var piePlot = this.plots.pie;
        this.pieChartPanel = new pvc.PiePanel(this, this.basePanel, piePlot, def.create(contentOptions, {
            scenes: def.getPath(this.options, 'pie.scenes')
        }));
    }
});


/*global pv_Mark:true, pvc_ValueLabelVar:true */

/**
 * Bar Abstract Panel.
 * The base panel class for bar charts.
 * 
 * Specific options are:
 * <i>orientation</i> - horizontal or vertical. Default: vertical
 * <i>valuesVisible</i> - Show or hide bar value. Default: false
 * <i>barSizeRatio</i> - In multiple series, percentage of inner
 * band occupied by bars. Default: 0.9 (90%)
 * <i>barSizeMax</i> - Maximum size (width) of a bar in pixels. Default: 2000
 *
 * Has the following protovis extension points:
 * <i>chart_</i> - for the main chart Panel
 * <i>bar_</i> - for the actual bar
 * <i>barPanel_</i> - for the panel where the bars sit
 * <i>barLabel_</i> - for the main bar label
 */
def
.type('pvc.BarAbstractPanel', pvc.CategoricalAbstractPanel)
.add({
    
    pvBar: null,
    pvBarLabel: null,
    pvCategoryPanel: null,
    pvSecondLine: null,
    pvSecondDot: null,
    
    _creating: function(){
        // Register BULLET legend prototype marks
        var groupScene = this.defaultVisibleBulletGroupScene();
        if(groupScene && !groupScene.hasRenderer()){
            var colorAxis  = groupScene.colorAxis;
            var drawLine   = colorAxis.option('LegendDrawLine');
            var drawMarker = !drawLine || colorAxis.option('LegendDrawMarker');
            if(drawMarker){
                var keyArgs = {
                    drawMarker:    true,
                    markerShape:   colorAxis.option('LegendShape'),
                    drawRule:      drawLine,
                    markerPvProto: new pv_Mark()
                };
                
                this.extend(keyArgs.markerPvProto, '', {constOnly: true}); // '' => bar itself
                
                groupScene.renderer(
                    new pvc.visual.legend.BulletItemDefaultRenderer(keyArgs));
            }
        }
    },
    
    /**
     * @override
     */
    _createCore: function(){
        this.base();
        var me = this,
            chart = me.chart,
            plot = me.plot,
            isStacked = !!me.stacked,
            isVertical = me.isOrientationVertical(),
            data       = me.visibleData({ignoreNulls: false}), // shared "categ then series" grouped data
            seriesData = me.visualRoles.series.flatten(data),
            rootScene  = me._buildScene(data, seriesData),
            orthoAxis  = me.axes.ortho,
            baseAxis   = me.axes.base,
            orthoScale = orthoAxis.scale,
            orthoZero  = orthoScale(0),
            sceneOrthoScale = orthoAxis.sceneScale({sceneVarName: 'value', nullToZero: false}),
            sceneBaseScale  = baseAxis .sceneScale({sceneVarName: 'category'}),
            barSizeRatio = plot.option('BarSizeRatio'),
            barSizeMax   = plot.option('BarSizeMax'),
            barStackedMargin = plot.option('BarStackedMargin'),
            baseRange = baseAxis.scale.range(),
            bandWidth = baseRange.band,
            barStepWidth = baseRange.step,
            barWidth,
            reverseSeries = isVertical === isStacked; // (V && S) || (!V && !S)

        if(isStacked){
            barWidth = bandWidth;
        } else {
            var S = seriesData.childCount();
            barWidth = S > 0 ? (bandWidth * barSizeRatio / S) : 0;
        }
        
        if (barWidth > barSizeMax) {
            barWidth = barSizeMax;
        }

        me.barWidth     = barWidth;
        me.barStepWidth = barStepWidth;
        
        var wrapper; // bar and label wrapper
        if(me.compatVersion() <= 1){
            /*
             * V1 Data
             * ----------
             * Stacked:   dataSet = Series x Categ values [[]...]    (type == undef -> 0)
             * 
             * !Stacked:  Categ -> Series
             *            Panel dataSet = VisibleCategoriesIndexes array
             *            Bar, Label -->  padZeros( getVisibleValuesForCategIndex( . ) )
             * 
             * var visibleSerIndex = this.stacked ? mark.parent.index : index,
             *     visibleCatIndex = this.stacked ? index : mark.parent.index;
             */
            wrapper = function(v1f){
                return function(scene){
                    var markParent = Object.create(this.parent);
                    var mark = Object.create(this);
                    mark.parent = markParent;
                    
                    var serIndex = scene.parent.childIndex();
                    var catIndex = scene.childIndex();
                    
                    if(isStacked){
                        markParent.index = serIndex;
                        mark.index = catIndex;
                    } else {
                        markParent.index = catIndex;
                        mark.index = serIndex;
                    }
                    
                    return v1f.call(mark, scene.vars.value.rawValue);
                };
            };
        }
        
        me.pvBarPanel = new pvc.visual.Panel(me, me.pvPanel, {
                panelType:   pv.Layout.Band,
                extensionId: 'panel'
            })
            .lock('layers', rootScene.childNodes) // series -> categories
            .lockMark('values', function(seriesScene){ return seriesScene.childNodes; })
            .lockMark('orient', isVertical ? 'bottom-left' : 'left-bottom')
            .lockMark('layout', isStacked  ? 'stacked' : 'grouped')
            .lockMark('verticalMode', me._barVerticalMode())
            .lockMark('yZero',  orthoZero)
            .pvMark
            .band // categories
                .x(sceneBaseScale)
                .w(bandWidth)
                .differentialControl(me._barDifferentialControl())
            .item
                // Stacked Vertical bar charts show series from
                // top to bottom (according to the legend)
                .order(reverseSeries ? "reverse" : null)
                .h(function(scene){
                    /* May be negative */
                    var y = sceneOrthoScale(scene);
                    return y != null ? chart.animate(0, y - orthoZero) : null;
                })
                .w(barWidth)
                .horizontalRatio(barSizeRatio)
                .verticalMargin(barStackedMargin)
            .end
            ;
        
        this.pvBar = new pvc.visual.Bar(me, me.pvBarPanel.item, {
                extensionId: '', // with the prefix, it gets 'bar_'
                freePosition: true,
                wrapper:      wrapper
            })
            .lockDimensions()
            .pvMark
            .antialias(false)
            ;

        if(plot.option('OverflowMarkersVisible')){
            this._addOverflowMarkers(wrapper);
        }
        
        var label = pvc.visual.ValueLabel.maybeCreate(me, me.pvBar, {wrapper: wrapper});
        if(label){
            me.pvBarLabel = label.pvMark
                .visible(function() { // no space for text otherwise
                    // this === pvMark
                    var length = this.scene.target[this.index][isVertical ? 'height' : 'width'];
                    
                    // Too small a bar to show any value?
                    return length >= 4;
                });
        }
    },
    
    /**
     * Called to obtain the bar verticalMode property value.
     * If it returns a function,
     * 
     * that function will be called once.
     * @virtual
     */
    _barVerticalMode: function(){
        return null;
    },
    
    /**
     * Called to obtain the bar differentialControl property value.
     * If it returns a function,
     * that function will be called once per category,
     * on the first series.
     * @virtual
     */
    _barDifferentialControl: function(){
        return null;
    },
    
    _getV1Datum: function(scene){
        // Ensure V1 tooltip function compatibility 
        var datum = scene.datum;
        if(datum){
            var datumEx = Object.create(datum);
            datumEx.percent = scene.vars.value.percent;
            datum = datumEx;
        }
        
        return datum;
    },
    
    _addOverflowMarkers: function(wrapper){
        var orthoAxis = this.axes.ortho;
        if(orthoAxis.option('FixedMax') != null){
            this.pvOverflowMarker = this._addOverflowMarker(false, orthoAxis.scale, wrapper);
        }

        if(orthoAxis.option('FixedMin') != null){
            this.pvUnderflowMarker = this._addOverflowMarker(true, orthoAxis.scale, wrapper);
        }
    },

    _addOverflowMarker: function(isMin, orthoScale, wrapper){
        /* NOTE: pv.Bar is not a panel,
         * and as such markers will be children of bar's parent,
         * yet have bar's anchor as a prototype.
         */
        
        var isVertical = this.isOrientationVertical(),
            a_bottom = isVertical ? "bottom" : "left",
            a_top    = this.anchorOpposite(a_bottom),
            a_height = this.anchorOrthoLength(a_bottom),
            a_width  = this.anchorLength(a_bottom),
            paddings = this._layoutInfo.paddings,
            rOrthoBound = isMin ? 
                          (orthoScale.min - paddings[a_bottom]) : 
                          (orthoScale.max + paddings[a_top]),
            angle;

        // 0 degrees
        //  /\
        // /__\
        //
        if(!isMin){
            angle = isVertical ? Math.PI: -Math.PI/2;
        } else {
            angle = isVertical ? 0: Math.PI/2;
        }
        
        return new pvc.visual.Dot(
            this,
            this.pvBar.anchor('center'), 
            {
                noSelect:      true,
                noHover:       true,
                noClick:       true,
                noDoubleClick: true,
                noTooltip:     true,
                freePosition:  true,
                extensionId:   isMin ? 'underflowMarker' : 'overflowMarker',
                wrapper:       wrapper
            })
            .intercept('visible', function(scene){
                var visible = this.delegateExtension();
                if(visible !== undefined && !visible){
                    return false;
                }
                
                var value = scene.vars.value.value;
                if(value == null){
                    return false;
                }

                var targetInstance = this.pvMark.scene.target[this.index];
                
                // Where is the position of the max of the bar?
                var orthoMaxPos = targetInstance[a_bottom] +
                                  (value > 0 ? targetInstance[a_height] : 0);
                return isMin ?
                        (orthoMaxPos < rOrthoBound) :
                        (orthoMaxPos > rOrthoBound);
            })
            .lock(a_top, null)
            .lock('shapeSize')
            .pvMark
            .shape("triangle")
            .shapeRadius(function(){
                return Math.min(
                        Math.sqrt(10),
                        this.scene.target[this.index][a_width] / 2);
            })
            .shapeAngle(angle)
            .lineWidth(1.5)
            .strokeStyle("red")
            .fillStyle("white")
            [a_bottom](function(){
                return rOrthoBound + (isMin ? 1 : -1) * (this.shapeRadius() + 2);
            })
            ;
    },

    /**
     * Renders this.pvPanel - the parent of the marks that are affected by selection changes.
     * @override
     */
    renderInteractive: function(){
        this.pvPanel.render();
    },

    _buildScene: function(data, seriesData){
        var rootScene  = new pvc.visual.Scene(null, {panel: this, source: data});
        
        var categDatas = data._children;
        var roles = this.visualRoles;
        var valueVarHelper = new pvc.visual.RoleVarHelper(rootScene, roles.value, {roleVar: 'value', hasPercentSubVar: this.stacked});
        var colorVarHelper = new pvc.visual.RoleVarHelper(rootScene, roles.color, {roleVar: 'color'});
        
        /**
         * Create starting scene tree
         */
        seriesData
            .children()
            .each(createSeriesScene);

        return rootScene;

        function createSeriesScene(seriesData1){
            /* Create series scene */
            var seriesScene = new pvc.visual.Scene(rootScene, {source: seriesData1}),
                seriesKey   = seriesData1.key;

            seriesScene.vars.series = pvc_ValueLabelVar.fromComplex(seriesData1);

            colorVarHelper.onNewScene(seriesScene, /* isLeaf */ false);
            
            categDatas.forEach(function(categData1){
                /* Create leaf scene */
                var group = data._childrenByKey[categData1.key]._childrenByKey[seriesKey],
                    scene = new pvc.visual.Scene(seriesScene, {source: group});

                var categVar =
                    scene.vars.category = pvc_ValueLabelVar.fromComplex(categData1);

                categVar.group = categData1;

                valueVarHelper.onNewScene(scene, /* isLeaf */ true);
                colorVarHelper.onNewScene(scene, /* isLeaf */ true);
            });
        }
    }
});

/*global pvc_Size:true */

/**
 * BarAbstract is the base class for generating charts of the bar family.
 */
def
.type('pvc.BarAbstract', pvc.CategoricalAbstract)
.init(function(options){

    this.base(options);

    var parent = this.parent;
    if(parent) {
        this._valueRole = parent._valueRole;
    }
})
.add({
    // NOTE
    // Timeseries category with bar charts are supported differently in V2 than in V1
    // They worked in v1 if the data set brought all
    // categories, according to chosen timeseries scale date unit
    // Then, bars were drawn with a category scale, 
    // whose positions ended up coinciding with the ticks in a linear axis...
    // To mimic v1 behavior the category dimensions are "coerced" to isDiscrete
    // The axis will be categoric, the parsing will work, 
    // and the formatting will be the desired one

    /**
     * Initializes each chart's specific roles.
     * @override
     */
    _initVisualRoles: function(){
        
        this.base();
        
        this._addVisualRole('value', {
            isMeasure: true,
            isRequired: true,
            isPercent: this.options.stacked,
            requireSingleDimension: true,
            requireIsDiscrete: false,
            valueType: Number,
            defaultDimension: 'value'
        });

        this._valueRole = this.visualRoles.value;
    },
    
    _getCategoryRoleSpec: function(){
        var catRoleSpec = this.base();
        
        // Force dimension to be discrete!
        catRoleSpec.requireIsDiscrete = true;
        
        return catRoleSpec;
    },
    
    _initData: function(){
        this.base.apply(this, arguments);

        var data = this.data;

        // Cached
        this._valueDim = data.dimensions(this._valueRole.firstDimensionName());
    }
});

/**
 * Bar Panel.
 */
def
.type('pvc.BarPanel', pvc.BarAbstractPanel)
.add({
});

/**
 * BarChart is the main class for generating... bar charts (another surprise!).
 */
def
.type('pvc.BarChart', pvc.BarAbstract)
.add({
    _animatable: true,

    _allowV1SecondAxis: true, 
    
    _initPlotsCore: function(/*hasMultiRole*/){
        var options = this.options;
        
        var barPlot = new pvc.visual.BarPlot(this);
        var trend   = barPlot.option('Trend');
        
        if(options.plot2){
            // Line Plot
            var plot2Plot = new pvc.visual.PointPlot(this, {
                name: 'plot2',
                fixed: {
                    DataPart: '1'
                },
                defaults: {
                    ColorAxis:    2,
                    LinesVisible: true,
                    DotsVisible:  true
                }});
            
            if(!trend){
                trend = plot2Plot.option('Trend');
            }
        }
        
        if(trend){
            // Trend Plot
            new pvc.visual.PointPlot(this, {
                name: 'trend',
                fixed: {
                    DataPart: 'trend',
                    TrendType: 'none',
                    ColorRole: 'series', // one trend per series
                    NullInterpolatioMode: 'none'
                },
                defaults: {
                    ColorAxis:    2,
                    LinesVisible: true,
                    DotsVisible:  false
                }
            });
        }
    },
    
    _hasDataPartRole: function(){
        return true;
    },
    
    /**
     * @override 
     */
    _createPlotPanels: function(parentPanel, baseOptions){
        var plots = this.plots;
        
        var barPlot = plots.bar;
        var barPanel = new pvc.BarPanel(
                this, 
                parentPanel, 
                barPlot, 
                Object.create(baseOptions));

        // legacy field
        this.barChartPanel = barPanel;
        
        var plot2Plot = plots.plot2;
        if(plot2Plot){
            if(pvc.debug >= 3){
                this._log("Creating Point panel.");
            }
            
            var pointPanel = new pvc.PointPanel(
                    this, 
                    parentPanel, 
                    plot2Plot,
                    Object.create(baseOptions));
            
            // Legacy fields
            barPanel.pvSecondLine = pointPanel.pvLine;
            barPanel.pvSecondDot  = pointPanel.pvDot;
            
            pointPanel._applyV1BarSecondExtensions = true;
        }
        
        var trendPlot = plots.trend;
        if(trendPlot){
            if(pvc.debug >= 3){
                this._log("Creating Trends Point panel.");
            }
            
            new pvc.PointPanel(
                    this, 
                    parentPanel, 
                    trendPlot,
                    Object.create(baseOptions));
        }
    }
});


/**
 * Normalized Bar Panel.
 */
def
.type('pvc.NormalizedBarPanel', pvc.BarAbstractPanel)
.add({
    _barVerticalMode: function(){
        return 'expand';
    }
});

/**
 * A NormalizedBarChart is a 100% stacked bar chart.
 */
def
.type('pvc.NormalizedBarChart', pvc.BarAbstract)
.add({
    
    /**
     * Processes options after user options and default options have been merged.
     * @override
     */
    _processOptionsCore: function(options){
        // Still affects default data cell settings
        options.stacked = true;

        this.base(options);
    },

    /**
     * @override
     */
    _getContinuousVisibleExtentConstrained: function(axis, min, max){
        if(axis.type === 'ortho') {
            /* 
             * Forces showing 0-100 in the axis.
             * Note that the bars are stretched automatically by the band layout,
             * so this scale ends up being ignored by the bars.
             * Note also that each category would have a different scale,
             * so it isn't possible to provide a single correct scale,
             * that would satisfy all the bars...
             */
            return {min: 0, max: 100, minLocked: true, maxLocked: true};
        }

        return this.base(axis, min, max);
    },
    
    _initPlotsCore: function(hasMultiRole){
        
        new pvc.visual.NormalizedBarPlot(this);
    },
    
    /* @override */
    _createPlotPanels: function(parentPanel, baseOptions){
        var barPlot = this.plots.bar;
        
        this.barChartPanel = 
            new pvc.NormalizedBarPanel(
                this, 
                parentPanel, 
                barPlot, 
                Object.create(baseOptions));
    }
});

/*global pvc_ValueLabelVar:true */

/**
 * Initializes a waterfall legend bullet group scene.
 * 
 * @name pvc.visual.legend.WaterfallBulletGroupScene

 * @extends pvc.visual.legend.BulletGroupScene
 * 
 * @constructor
 * @param {pvc.visual.legend.BulletRootScene} parent The parent bullet root scene.
 * @param {object} [keyArgs] Keyword arguments.
 * See {@link pvc.visual.Scene} for additional keyword arguments.
 * @param {pv.visual.legend.renderer} [keyArgs.renderer] Keyword arguments.
 */
def
.type('pvc.visual.legend.WaterfallBulletGroupScene', pvc.visual.legend.BulletGroupScene)
.init(function(rootScene, keyArgs) {
    
    keyArgs = def.set(keyArgs, 'clickMode', 'none');
    
    this.base(rootScene, keyArgs);
    
    this.createItem(keyArgs); // label && color
})
.add(/** @lends pvc.visual.legend.WaterfallBulletGroupScene# */{
    renderer: function(renderer) {
        if(renderer != null) { this._renderer = renderer; }
        return this._renderer;
    },
    
    itemSceneType: function() {
        return pvc.visual.legend.WaterfallBulletItemScene;
    }
});

def
.type('pvc.visual.legend.WaterfallBulletItemScene', pvc.visual.legend.BulletItemScene)
.init(function(bulletGroup, keyArgs) {
    
    this.base.apply(this, arguments);
    
    // Don't allow any Action
    var I = pvc.visual.Interactive;
    this._ibits = I.Interactive | I.ShowsInteraction;
    
    this.color = def.get(keyArgs, 'color');
    
    // Pre-create 'value' variable
    this.vars.value = new pvc_ValueLabelVar(null, def.get(keyArgs, 'label'));
});


/*global pv_Mark:true, pvc_ValueLabelVar:true */

/**
 * Waterfall chart panel.
 * Specific options are:
 * <i>orientation</i> - horizontal or vertical. Default: vertical
 * <i>valuesVisible</i> - Show or hide bar value. Default: false
 * <i>barSizeRatio</i> - In multiple series, percentage of inner
 * band occupied by bars. Default: 0.9 (90%)
 * <i>barSizeMax</i> - Maximum size (width) of a bar in pixels. Default: 2000
 *
 * Has the following protovis extension points:
 *
 * <i>chart_</i> - for the main chart Panel
 * <i>bar_</i> - for the actual bar
 * <i>barPanel_</i> - for the panel where the bars sit
 * <i>barLabel_</i> - for the main bar label
 */
def
.type('pvc.WaterfallPanel', pvc.BarAbstractPanel)
.add({
    pvWaterfallLine: null,
    ruleData: null,

    /**
     * Called to obtain the bar differentialControl property value.
     * If it returns a function,
     * that function will be called once per category,
     * on the first series.
     * @virtual
     */
    _barDifferentialControl: function() {
        var isFalling = this.chart._isFalling;
        /*
         * From protovis help:
         * 
         * Band differential control pseudo-property.
         * 
         *  > 0 => go up
         *  < 0 => go down
         *  ...............
         *  2 - Drawn starting at previous band offset. Multiply values by  1. Don't update offset.
         *  1 - Drawn starting at previous band offset. Multiply values by  1. Update offset.
         *  
         *  0 - Reset offset to 0. Drawn starting at 0. Default. Leave offset at 0.
         *  
         *  > 0 => go down 
         *  < 0 => go up
         *  ...............
         * -1 - Drawn starting at previous band offset. Multiply values by -1. Update offset.
         * -2 - Drawn starting at previous band offset. Multiply values by -1. Don't update offset.
         */
        return function(scene) {
            if(isFalling && !this.index) {
                // First falling bar is the main total
                // Must be accounted up and update the offset
                return 1;
            }

            var group = scene.vars.category.group;
            var isProperGroup = group._isFlattenGroup && !group._isDegenerateFlattenGroup;
            if(isProperGroup) {
                // Groups don't update the offset
                // Groups always go down (except for the first when falling)
                return -2;
            }
            
            return isFalling ? -1 : 1;
        };
    },
    
    _creating: function() {
        // Register BULLET legend prototype marks
        var rootScene = this._getLegendBulletRootScene();
        if(rootScene) {
            var waterfallGroupScene = rootScene.firstChild;
            if(waterfallGroupScene && !waterfallGroupScene.hasRenderer()){
                var keyArgs = {
                        drawRule:      true,
                        drawMarker:    false,
                        rulePvProto:   new pv_Mark()
                    };
                
                this.extend(keyArgs.rulePvProto, 'line', {constOnly: true});
                
                waterfallGroupScene.renderer(
                        new pvc.visual.legend.BulletItemDefaultRenderer(keyArgs));
            }
        }
    },
    
    _createCore: function(){

        this.base();

        var chart = this.chart,
            isVertical = this.isOrientationVertical(),
            anchor = isVertical ? "bottom" : "left",
            ao = this.anchorOrtho(anchor),
            ruleRootScene = this._buildRuleScene(),
            orthoScale = chart.axes.ortho.scale,
            orthoZero = orthoScale(0),
            sceneOrthoScale = chart.axes.ortho.sceneScale({sceneVarName: 'value'}),
            sceneBaseScale  = chart.axes.base.sceneScale({sceneVarName: 'category'}),
            baseScale = chart.axes.base.scale,
            barWidth2 = this.barWidth/2,
            barWidth = this.barWidth,
            barStepWidth = this.barStepWidth,
            isFalling = chart._isFalling,
            waterColor = chart._waterColor;

        if(this.plot.option('AreasVisible')) {
            var panelColors = pv.Colors.category10();
            var waterGroupRootScene = this._buildWaterGroupScene();
            
            var orthoRange = orthoScale.range();
            var orthoPanelMargin = 0.04 * (orthoRange[1] - orthoRange[0]);
            
            this.pvWaterfallGroupPanel = new pvc.visual.Panel(this, this.pvPanel, {
                    extensionId: 'group'
                })
                .lock('data', waterGroupRootScene.childNodes)
                .pvMark
                .zOrder(-1)
                .fillStyle(function(/*scene*/) {
                    return panelColors(0)/* panelColors(scene.vars.category.level - 1)*/.alpha(0.15);
                })
                [ao](function(scene) {
                    var c = scene.vars.category;
                    return baseScale(c.valueLeft) - barStepWidth / 2;
                })
                [this.anchorLength(anchor)](function(scene) {
                    var c = scene.vars.category;
                    var len = Math.abs(baseScale(c.valueRight) - baseScale(c.valueLeft ));

                    return len + barStepWidth;
                })
                [anchor](function(scene) { // bottom
                    // animate: zero -> bottom
                    var v = scene.vars.value;
                    var b = orthoScale(v.valueBottom) - orthoPanelMargin/2;
                    return chart.animate(orthoZero, b);
                })
                [this.anchorOrthoLength(anchor)](function(scene){ // height
                    // animate: 0 -> height
                    var v = scene.vars.value;
                    var h = orthoScale(v.valueTop) - orthoScale(v.valueBottom) + orthoPanelMargin;
                    return chart.animate(0, h);
                })
                ;
        }
        
        this.pvBar
            .sign
            .override('baseColor', function(type){
                var color = this.base(type);
                if(type === 'fill'){
                    if(!this.scene.vars.category.group._isFlattenGroup){
                        return pv.color(color).alpha(0.5);
                    } 
//                    else {
//                        return pv.color(color).darker();
//                    }
                }
                
                return color;
            });
        
        // If Falling,  
        //   the offset is the initial y
        //   the line covers this and the previous
        //   the line goes from the left of the previous bar to the right of this bar
        // If Climbing
        //   the offset is the final y
        //   the line covers this and the next
        //   the line goes from the left of this bar to the right of the next bar
        this.pvWaterfallLine = new pvc.visual.Rule(this, this.pvPanel, {
                extensionId:  'line',
                noTooltip:    false,
                noHover:       false,
                noSelect:      false,
                noClick:       false,
                noDoubleClick: false
            })
            .lock('data', ruleRootScene.childNodes)
            .optional('visible', function(){
                return ( isFalling && !!this.scene.previousSibling) ||
                       (!isFalling && !!this.scene.nextSibling);
            })
            .optional(anchor, function(){ 
                return orthoZero + chart.animate(0, sceneOrthoScale(this.scene) - orthoZero);
            })
            
            .optional(this.anchorLength(anchor), barStepWidth + barWidth)
            .optional(ao,
                isFalling ?
                    function(){ return sceneBaseScale(this.scene) - barStepWidth - barWidth2; } :
                    function(){ return sceneBaseScale(this.scene) - barWidth2; })
            .override('defaultColor', function(){ return waterColor; })
            .pvMark
            .antialias(true)
            .lineCap('butt');

        if(this.plot.option('TotalValuesVisible')) {
            this.pvWaterfallLabel = new pvc.visual.Label(
                this,
                this.pvWaterfallLine, 
                {
                    extensionId: 'lineLabel'
                })
                .intercept('visible', function(scene){
                    if(scene.vars.category.group._isFlattenGroup){
                        return false;
                    }
    
                    return isFalling || !!scene.nextSibling;
                })
                .pvMark
                [anchor](function(scene){
                    return orthoZero + chart.animate(0, sceneOrthoScale(scene) - orthoZero);
                })
                [this.anchorOrtho(anchor)](sceneBaseScale)
                .textAlign(isVertical ? 'center' : 'left')
                .textBaseline(function(categScene) {
                    // Rules are drawn on the starting y position.
                    if(!isVertical) { return 'middle'; }
                    
                    var direction = categScene.vars.direction;
                    if(direction == null) { return 'bottom'; }
                    
                    var isRising = !isFalling;
                    return isRising === (direction === 'up') ? 'bottom' : 'top';
                })
                .textStyle(pv.Color.names.darkgray.darker(2))
                .textMargin(5)
                .text(function(scene){ return scene.vars.value.label; });
        }
    },

    _buildRuleScene: function() {
        var rootScene  = new pvc.visual.Scene(null, {panel: this, source: this.visibleData({ignoreNulls: false})});
        var prevValue, isClimbing;
        var ris = this.chart._ruleInfos;
        if(ris) {
            // Create scenes in order 
            ris.forEach(createCategScene, this);
            
            // But fill in the direction variable this way
            
            var q = def.query(rootScene.childNodes);
            
            // When falling, 
            // traversing from the end to the beginning 
            // makes us be climbing...
            isClimbing = !this.chart._isFalling;
            
            if(!isClimbing) { q = q.reverse(); }
            
            q.each(completeCategScene, this);
        }
        
        return rootScene;

        function createCategScene(ruleInfo) {
            var categData1 = ruleInfo.group;
            
            var categScene = new pvc.visual.Scene(rootScene, {source: categData1});
            
            var categVar = 
                categScene.vars.category =
                pvc_ValueLabelVar.fromComplex(categData1);
            
            categVar.group = categData1;
            
            var value = ruleInfo.offset;
            
            categScene.vars.value = new pvc_ValueLabelVar(
                                value,
                                this.chart._valueDim.format(value));
        }
        
        function completeCategScene(categScene, index) {
            var value = categScene.vars.value.value;
            
            categScene.vars.direction = 
                (!index || prevValue === value)      ? null : 
                (isClimbing === (prevValue < value)) ? 'up' : 
                'down';
            
            prevValue = value;
        }
    },

    _buildWaterGroupScene: function() {
        var chart = this.chart;
        var rootCatData = chart._catRole.select(
                            chart.partData(this.dataPartValue),
                            {visible: true});
        
        var rootScene = new pvc.visual.Scene(null, {panel: this, source: rootCatData});

        var ruleInfoByCategKey, isFalling;
        var ris = chart._ruleInfos;
        if(ris) {
            ruleInfoByCategKey = def.query(ris).object({name: function(ri) { return ri.group.absKey; }});
            isFalling = chart._isFalling;
            
            createRectangleSceneRecursive(rootCatData, 0); 
        }
        
        return rootScene;

        function createRectangleSceneRecursive(catData, level) {
            // TODO: ?? explain what key != "" excludes
            var q = catData.children().where(function(c) { return c.key !== ""; });
            if(q.next()) {
                // Group node (has at least one ?proper? child)
                
                // All parent categories, except the root category, 
                // draw a rectangle around their descendants.
                if(level) { createRectangleScene(catData, level); }
                
                level++;
                do { createRectangleSceneRecursive(q.item, level); } while(q.next());
            }
        }
        
        function createRectangleScene(catData, level) {
            // Rectangle scenes are all direct children of rootScene
            var rectScene = new pvc.visual.Scene(rootScene, {source: catData});
            
            var categVar = rectScene.vars.category = pvc_ValueLabelVar.fromComplex(catData);
            
            categVar.group = catData;
            categVar.level = level;

            var valueVar = rectScene.vars.value = {}; // TODO: Not A Var
            
            var ri = ruleInfoByCategKey[catData.absKey];
            
            // At which offset value this parent category starts
            var offset = ri.offset;
            
            // Maximum value-height of all descendants
            var range  = ri.range;
            var height = -range.min + range.max;

            /* Find the rectangle value coordinates
             *  
             *  When falling
             *    catData
             *  lc  x----------------+
             *      |                |
             *      |                |
             *  rc  +----------------x
             *                    lastLeafData
             *     lc                rc
             *      
             */
            var leafData, leafRuleInfo, lc, rc, bv;
            if(isFalling) {
                leafData = lastLeaf(catData);
                leafRuleInfo = ruleInfoByCategKey[leafData.absKey];
                
                lc = ri.group.value;
                rc = leafRuleInfo.group.value;
                
                bv = offset - range.max;
            } else {
                leafData = firstLeaf(catData);
                leafRuleInfo = ruleInfoByCategKey[leafData.absKey];
                
                lc = leafRuleInfo.group.value;
                rc = ri.group.value;
                
                bv = offset - range.max;
            }
            
            categVar.valueLeft   = lc;
            categVar.valueRight  = rc;
            
            valueVar.valueHeight = height;
            valueVar.valueBottom = bv;
            valueVar.valueTop    = bv + height;
        }
        
        function firstLeaf(data) {
            var children = data._children;
            var first = children && children[0];
            return first ? firstLeaf(first) : data;
        }

        function lastLeaf(data) {
            var children = data._children;
            var last = children && children[children.length - 1];
            return last ? lastLeaf(last) : data;
        }
    }
});


/**
 * WaterfallChart is the class that generates waterfall charts.
 *
 * The waterfall chart is an alternative to the pie chart for
 * showing distributions. The advantage of the waterfall chart is that
 * it possibilities to visualize sub-totals and offers more convenient
 * possibilities to compare the size of categories (in a pie-chart you
 * have to compare wedges that are at a different angle, which
 * requires some additional processing/brainpower of the end-user).
 *
 * Waterfall charts are basically Bar-charts with some added
 * functionality. Given the complexity of the added features this
 * class has it's own code-base. However, it would be easy to
 * derive a BarChart class from this class by switching off a few
 * features.
 *
 * If you have an issue or suggestions regarding the Waterfall-charts
 * please contact CvK at cde@vinzi.nl
 */
def
.type('pvc.WaterfallChart', pvc.BarAbstract)
.init(function(options){

    this.base(options);
    
    var parent = this.parent;
    if(parent) {
        this._isFalling = parent._isFalling;
    }
})
.add({
    _animatable: true,
    
    _isFalling: true,
    _ruleInfos: null,
    _waterColor: pv.color("#1f77b4").darker(),

    /**
     * Processes options after user options and default options have been merged.
     * @override
     */
    _processOptionsCore: function(options){

        // Might still affect scale calculation
        options.stacked = true;
        
        // Doesn't work (yet?);
        options.baseAxisComposite = false;
        
        this.base(options);
        
        // Not supported
        options.plot2 = false;
    },
  
    _initPlotsCore: function(){
        var options = this.options;
        
        var waterPlot = new pvc.visual.WaterfallPlot(this);
        
        this._isFalling = waterPlot.option('Direction') === 'down';
        
        var travProp = this._isFalling ? 'FlattenDfsPre' : 'FlattenDfsPost';
        this._catRole.setTraversalMode(pvc.visual.TraversalMode[travProp]);
        
        this._catRole.setRootLabel(waterPlot.option('AllCategoryLabel'));
    },
    
    _initLegendScenes: function(legendPanel){
        
        var waterPlot = this.plots.water;
        
        var extAbsId = pvc.makeExtensionAbsId('line', waterPlot.extensionPrefixes);
        var strokeStyle = this._getConstantExtension(extAbsId, 'strokeStyle');
        if(strokeStyle){
            this._waterColor = pv.color(strokeStyle);
        }
        
        var rootScene = legendPanel._getBulletRootScene();
        
        new pvc.visual.legend.WaterfallBulletGroupScene(rootScene, {
            extensionPrefix: pvc.buildIndexedId('', 1),
            label: waterPlot.option('TotalLineLabel'),
            color: this._waterColor
        });
        
        this.base(legendPanel);
    },
    
    /**
     * Reduce operation of category ranges, into a global range.
     *
     * Propagates the total value.
     *
     * Also creates the array of rule information {@link #_ruleInfos}
     * used by the waterfall panel to draw the rules.
     *
     * Supports {@link #_getContinuousVisibleExtent}.
     */
    _reduceStackedCategoryValueExtent: function(result, catRange, catGroup){
        /*
         * That min + max are the variation of this category
         * relies on the concrete base._getStackedCategoryValueExtent() implementation...
         * Max always contains the sum of positives, if any, or 0
         * Min always contains the sum of negatives, if any, or 0
         * max >= 0
         * min <= 0
         */
        /*
         * When falling, the first category is surely *the* global total.
         * When falling, the first category must set the initial offset
         * and, unlike every other category group such that _isFlattenGroup===true,
         * it does contribute to the offset, and positively.
         * The offset property accumulates the values.
         */
        
        // previous offset
        var offsetPrev  = result ? result.offset : 0;
        var offsetDelta = catRange.min + catRange.max;
        var offsetNext;
        if(!result) {
            if(catRange) {
                offsetNext = offsetPrev + offsetDelta;
                this._ruleInfos = [{
                    offset: offsetNext,
                    group:  catGroup,
                    range:  catRange
                }];

                // Copy the range object
                return {
                    min:    catRange.min,
                    max:    catRange.max,
                    offset: offsetNext
                };
            }

            return null;
        }
        
        var isFalling = this._isFalling;
        var isProperGroup = catGroup._isFlattenGroup && !catGroup._isDegenerateFlattenGroup;
        if(!isProperGroup) {
            // offset, min, max may be affected
            var dir = isFalling ? -1 : 1;
            offsetNext = result.offset = offsetPrev + dir * offsetDelta;
            
            if(offsetNext > result.max) { result.max = offsetNext; }
            else 
            if(offsetNext < result.min) { result.min = offsetNext; }
            
        } else {
            // offset not affected
            // min, max may be affected
            var deltaUp = -catRange.min; // positive
            if(deltaUp > 0) {
                var top = offsetPrev + deltaUp;
                if(top > result.max) { result.max = top; }
            }
            
            var deltaDown = -catRange.max; // negative
            if(deltaDown < 0) {
                var bottom = offsetPrev + deltaDown;
                if(bottom < result.min) { result.min = bottom; }
            }
        }

        this._ruleInfos.push({
            offset: isFalling ? offsetPrev : result.offset,
            group:  catGroup,
            range:  catRange
        });
        
        return result;
    },
    
    /* @override */
    _createPlotPanels: function(parentPanel, baseOptions){
        this.wfChartPanel = 
            new pvc.WaterfallPanel(
                    this, 
                    parentPanel, 
                    this.plots.water, 
                    def.create(baseOptions, {
                        waterfall:  this.options.waterfall
                    }));
    }
});


/*global pv_Mark:true, pvc_ValueLabelVar:true */

/*
 * Point panel.
 * Class that draws all line/dot/area combinations.
 * Specific options are:
 * <i>dotsVisible</i> - Show or hide dots. Default: true
 * <i>areasVisible</i> - Show or hide dots. Default: false
 * <i>linesVisible</i> - Show or hide dots. Default: true
 * <i>valuesVisible</i> - Show or hide line value. Default: false
 *
 * Has the following protovis extension points:
 *
 * <i>chart_</i> - for the main chart Panel
 * <i>line_</i> - for the actual line
 * <i>linePanel_</i> - for the panel where the lines sit
 * <i>lineDot_</i> - the dots on the line
 * <i>lineLabel_</i> - for the main line label
 */
def
.type('pvc.PointPanel', pvc.CategoricalAbstractPanel)
.init(function(chart, parent, plot, options) {
    
    this.base(chart, parent, plot, options);
    
    this.linesVisible  = plot.option('LinesVisible'); // TODO
    this.dotsVisible   = plot.option('DotsVisible' ); // TODO
    this.areasVisible  = plot.option('AreasVisible'); // TODO
    if(!this.linesVisible && !this.dotsVisible && !this.areasVisible){
        this.linesVisible = true;
        plot.option.specify({'LinesVisible': true});
    }
     
    this.visualRoles.value = chart.visualRole(plot.option('OrthoRole'));
})
.add({
    pvLine: null,
    pvArea: null,
    pvDot: null,
    pvLabel: null,
    pvScatterPanel: null,
    
    _creating: function(){
        // Register BULLET legend prototype marks
        var groupScene = this.defaultVisibleBulletGroupScene();
        if(groupScene && !groupScene.hasRenderer()){
            var colorAxis = groupScene.colorAxis;
            var drawMarker = def.nullyTo(colorAxis.option('LegendDrawMarker', /*no default*/ true), this.dotsVisible || this.areasVisible);
            var drawRule   = !drawMarker || 
                             def.nullyTo(colorAxis.option('LegendDrawLine',   /*no default*/ true), this.linesVisible && !this.areasVisible);
            if(drawMarker || drawRule){
                var keyArgs = {drawMarker: drawMarker, drawRule: drawRule};
                if(drawMarker){
                    var markerShape = colorAxis.option('LegendShape', true);
                    
                    if(this.dotsVisible){
                        if(!markerShape){ 
                            markerShape = 'circle'; // Dot's default shape
                        }
                        
                        keyArgs.markerPvProto = new pv.Dot()
                                .lineWidth(1.5, pvc.extensionTag) // act as if it were a user extension
                                .shapeSize(12,  pvc.extensionTag); // idem
                    } else {
                        keyArgs.markerPvProto = new pv_Mark();
                    }
                    
                    keyArgs.markerShape = markerShape;
                    
                    if(this._applyV1BarSecondExtensions){
                        this.chart.extend(keyArgs.markerPvProto, 'barSecondDot', {constOnly: true});
                    }       
                    this.extend(keyArgs.markerPvProto, 'dot', {constOnly: true});
                }
                
                if(drawRule){
                    keyArgs.rulePvProto = new pv.Line()
                           .lineWidth(1.5, pvc.extensionTag);
                    
                    if(this._applyV1BarSecondExtensions){
                        this.chart.extend(keyArgs.rulePvProto, 'barSecondLine', {constOnly: true});
                    }
                    this.extend(keyArgs.rulePvProto, 'line', {constOnly: true});
                }
                
                groupScene.renderer(
                    new pvc.visual.legend.BulletItemDefaultRenderer(keyArgs));
            }
        }
    },
    
    /**
     * @override
     */
    _createCore: function() {
        this.base();
        
        var myself = this;
        var chart = this.chart;
        var isStacked = this.stacked;
        var dotsVisible  = this.dotsVisible;
        var areasVisible = this.areasVisible;
        var linesVisible = this.linesVisible;
        var anchor = this.isOrientationVertical() ? "bottom" : "left";

        // ------------------
        // DATA
        var isBaseDiscrete = this.axes.base.role.grouping.isDiscrete();
        var data = this.visibleData({ignoreNulls: false}); // shared "categ then series" grouped data
        var rootScene = this._buildScene(data, isBaseDiscrete);
       
        // ---------------
        // BUILD
        if(areasVisible) {
            // Areas don't look good above the axes
            this.pvPanel.zOrder(-7);
        } else {
            // // Above axes
            this.pvPanel.zOrder(1);
        }
        
        this.pvScatterPanel = new pvc.visual.Panel(this, this.pvPanel, {extensionId: 'panel'})
            .lock('data', rootScene.childNodes)
            .pvMark;
        
        // -- AREA --
        var areaFillColorAlpha = areasVisible && linesVisible && !isStacked ? 0.5 : null;
        
        var wrapper;
        if(this.compatVersion() <= 1) {
            if(isStacked) {
                wrapper = function(v1f) {
                    return function(dotScene) {
                        return v1f.call(this, dotScene.vars.value.rawValue);
                    };
                };
            } else {
                wrapper = function(v1f) {
                    return function(dotScene) {
                        var d = {
                                category: dotScene.vars.category.rawValue,
                                value:    dotScene.vars.value.rawValue
                            };
                        
                        // Compensate for the effect of intermediate scenes on mark's index
                        var pseudo = Object.create(this);
                        pseudo.index = dotScene.dataIndex;
                        return v1f.call(pseudo, d);
                    };
                };
            }
        }
        
        var lineAreaVisibleProp = isBaseDiscrete && isStacked ? 
                function(scene) { return !scene.isNull || scene.isIntermediate; } :
                function(scene) { return !scene.isNull; };
        
        var isLineAreaNoSelect = /*dotsVisible && */chart.selectableByFocusWindow();
        
        this.pvArea = new pvc.visual.Area(this, this.pvScatterPanel, {
                extensionId: 'area',
                noTooltip:   false,
                wrapper:     wrapper,
                noSelect:    isLineAreaNoSelect,
                noRubberSelect: true, // Line is better for selection
                showsSelection: !isLineAreaNoSelect
            })
            /* Data */
            .lockMark('data',   function(seriesScene) { return seriesScene.childNodes; }) // TODO
            
            // TODO: If it were allowed to hide the area, the anchored line would fail to evaluate
            // Do not use anchors to connect Area -> Line -> Dot ...
            .lockMark('visible', lineAreaVisibleProp)
            
            /* Position & size */
            .override('x',  function() { return this.scene.basePosition;  }) // left
            .override('y',  function() { return this.scene.orthoPosition; }) // bottom
            .override('dy', function() { return chart.animate(0, this.scene.orthoLength); }) // height
            
            /* Color & Line */
            .override('color', function(type) { return areasVisible ? this.base(type) : null; })
            .override('baseColor', function(type) {
                var color = this.base(type);
                if(!this._finished && color && areaFillColorAlpha != null) {
                    color = color.alpha(areaFillColorAlpha);
                }
                return color;
            })
            .override('dimColor', function(color, type) {
                return isStacked ? 
                    pvc.toGrayScale(color, 1, null, null).brighter() :
                    this.base(color, type);
            })
            .lock('events', areasVisible ? 'painted' : 'none')
            .pvMark;
        
        // -- LINE --
        var dotsVisibleOnly = dotsVisible && !linesVisible && !areasVisible;
            
        /* When areas are shown with no alpha (stacked), 
         * make dots darker so they get 
         * distinguished from areas. 
         */
        var darkerLineAndDotColor = isStacked && areasVisible;
         
        var extensionIds = ['line'];
        if(this._applyV1BarSecondExtensions) { extensionIds.push({abs: 'barSecondLine'}); }
        
        /* 
         * Line.visible =
         *  a) linesVisible
         *     or
         *  b) (!linesVisible and) areasVisible
         *      and
         *  b.1) discrete base and stacked
         *       and
         *       b.1.1) not null or is an intermediate null
         *  b.2) not null
         */
        // NOTE: false or a function
        var lineVisibleProp = !dotsVisibleOnly && lineAreaVisibleProp;
        
        // When areasVisible && !linesVisible, lines are shown when active/activeSeries
        // and hidden if not. If lines that show/hide would react to events
        // they would steal events to the area and generate strange flicker-like effects.
        var noLineInteraction = areasVisible && !linesVisible;

        this.pvLine = new pvc.visual.Line(
            this, 
            this.pvArea.anchor(this.anchorOpposite(anchor)), 
            {
                extensionId:    extensionIds,
                freePosition:   true,
                wrapper:        wrapper,
                noTooltip:      noLineInteraction,
                noDoubleClick:  noLineInteraction,
                noClick:        noLineInteraction,
                noHover:        noLineInteraction,
                noSelect:       noLineInteraction || isLineAreaNoSelect,
                showsSelection: !isLineAreaNoSelect
            })
            // TODO: If it were allowed to hide the line, the anchored dot would fail to evaluate
            .lockMark('visible', lineVisibleProp)
            .override('defaultColor', function(type) {
                var color = this.base(type);
                if(!this._finished && darkerLineAndDotColor && color) { color = color.darker(0.6); }
                return color;
            })
            .override('normalColor', function(color/*, type*/) {
                return linesVisible ? color : null;
            })
            .override('interactiveColor', function(color, type) {
                // When !linesVisible, 
                // keep them hidden if nothing is selected and it is not active
                if(!linesVisible && !this.mayShowAnySelected() && !this.mayShowActive()) {
                    return null;
                }
                
                return this.base(color, type);
            })
            .override('baseStrokeWidth', function() {
                var strokeWidth;
                if(linesVisible) { strokeWidth = this.base(); }
                return strokeWidth == null ? 1.5 : strokeWidth; 
            })
            .intercept('strokeDasharray', function() {
                var dashArray = this.delegateExtension();
                if(dashArray === undefined) {
                    var scene = this.scene;
                    var useDash = scene.isInterpolated;
                    if(!useDash) {
                        var next = scene.nextSibling;
                        useDash = next && next.isIntermediate && next.isInterpolated;
                        if(!useDash) {
                            var previous = scene.previousSibling;
                            useDash = previous  && scene.isIntermediate && previous.isInterpolated;
                        }
                    }
                    
                    dashArray = useDash ? '. ' : null;
                }
                
                return dashArray;
            })
            .pvMark;
           
        // -- DOT --
        var showAloneDots = !(areasVisible && isBaseDiscrete && isStacked);
        
        extensionIds = ['dot'];
        if(this._applyV1BarSecondExtensions) { extensionIds.push({abs: 'barSecondDot'}); }
        
        this.pvDot = new pvc.visual.Dot(this, this.pvLine, {
                extensionId:  extensionIds,
                freePosition: true,
                wrapper:      wrapper
            })
            .intercept('visible', function() {
                var scene = this.scene;
                return (!scene.isNull && !scene.isIntermediate /*&& !scene.isInterpolated*/) && 
                       this.delegateExtension(true);
            })
            .override('color', function(type) {
                /* 
                 * Handle dotsVisible
                 * -----------------
                 * Despite !dotsVisible,
                 * show a dot anyway when:
                 * 1) it is active, or
                 * 2) it is single  (the only dot in its series and there's only one category) (and in areas+discreteCateg+stacked case)
                 * 3) it is alone   (surrounded by null dots) (and not in areas+discreteCateg+stacked case)
                 */
                if(!dotsVisible) {
                    var visible = this.scene.isActive ||
                                  (!showAloneDots && this.scene.isSingle) ||
                                  (showAloneDots && this.scene.isAlone);
                    if(!visible) { return pvc.invisibleFill; }
                }
                
                // Normal logic
                var color = this.base(type);
                
                // TODO: review interpolated style/visibility
                if(this.scene.isInterpolated && type === 'fill') {
                    return color && pv.color(color).brighter(0.5);
                }
                
                return color;
            })
//            .override('interactiveColor', function(color, type){
//              return this.scene.isInterpolated && type === 'stroke' ? 
//                     color :
//                     this.base(color, type);
//            })
//            .optionalMark('lineCap', 'round')
//            .intercept('strokeDasharray', function(){
//                var dashArray = this.delegateExtension();
//                if(dashArray === undefined){
//                    // TODO: review interpolated style/visibility
//                    dashArray = this.scene.isInterpolated ? '.' : null; 
//                }
//                
//                return dashArray;
//            })
            .override('defaultColor', function(type) {
                var color = this.base(type);
                if(!this._finished && darkerLineAndDotColor && color) { color = color.darker(0.6); }
                return color;
            })
            .override('baseSize', function() {
                /* When not showing dots, 
                 * but a datum is alone and 
                 * wouldn't be visible using lines or areas,  
                 * show the dot anyway, 
                 * with a size = to the line's width^2
                 * (ideally, a line would show as a dot when only one point?)
                 */
                if(!dotsVisible) {
                    var visible = this.scene.isActive ||
                                  (!showAloneDots && this.scene.isSingle) ||
                                  (showAloneDots && this.scene.isAlone);
                    
                    if(visible && !this.scene.isActive) {
                        // Obtain the line Width of the "sibling" line
                        var lineWidth = Math.max(myself.pvLine.lineWidth(), 0.2) / 2;
                        return def.sqr(lineWidth);
                    }
                }
                
                // TODO: review interpolated style/visibility
                if(this.scene.isInterpolated) { return 0.8 * this.base(); }
                
                return this.base();
            })
            .pvMark;
        
        var label = pvc.visual.ValueLabel.maybeCreate(this, this.pvDot, {wrapper: wrapper});
        if(label) {
            this.pvLabel = label.pvMark;
        }
    },

    /**
     * Renders this.pvScatterPanel - the parent of the marks that are affected by interaction changes.
     * @override
     */
    renderInteractive: function() { this.pvScatterPanel.render(); },

    /* On each series, scenes for existing categories are interleaved with intermediate scenes.
     * 
     * Protovis Dots are only shown for main (non-intermediate) scenes.
     * 
     * The desired effect is that selecting a dot selects half of the
     * line on the left and half of the line on the right.
     *  
     *  * main scene
     *  + intermediate scene
     *  - line that starts from the previous scene
     *  
     *  
     *        * - + - * - + - *
     *            [-------[
     *                ^ line extent of a dot
     *             
     * Each segment of a Protovis segmented line starts from the initial point 
     * till just before the next point.
     * 
     * So, selecting a dot must select the the line that starts on the 
     * main dot, but also the line that starts on the previous intermediate dot.
     * 
     * If a main dot shares its datums (or group) with its preceding
     * intermediate dot, the selection will work like so.
     * 
     * -------
     * 
     * Another influencing concern is interpolation.
     * 
     * The desired effect is that any two dots separated by a number of missing/null
     * categories get connected by linearly interpolating the missing values.
     * Moreover, the left half of the new line should be selected
     * when the left dot is selected and the right half of the new line
     * should be selected when the right dot is selected .
     * 
     * In the discrete-base case, the "half of the line" point always coincides
     *  a) with the point of an existing category (when count of null categs is odd)
     *  or 
     *  b) with an intermediate point added afterwards (when count of null categs is even).
     * 
     *  a) Interpolate missing/null category in S1 (odd case)
     *  mid point ----v
     *  S1    * - + - 0 - + - * - + - * 
     *  S2    * - + - * - + - * - + - *
     *  Data  A   A   B   B   B   B   C
     *  
     *  a) Interpolate missing/null category in S1 (even case)
     *    mid point ------v
     *  S1    * - + - 0 - + - 0 - + - * - + - * 
     *  S2    * - + - * - + - * - + - * - + - *
     *  Data  A   A   A   B   B   B   B
     *  
     * In the continuous-base case, 
     * the middle point between two non-null categories 
     * separated by missing/null categories in between,
     * does not, in general, coincide with the position of an existing category...
     * 
     * As such, interpolation may add new "main" points (to all the series),
     * and interpolation of one series leads to the interpolation
     * on a series that did not initially need interpolation... 
     * 
     * Interpolated dots to the left of the mid point are bound to 
     * the left data and interpolated dots to the right and 
     * including the mid point are bound to the right data. 
     */

    _buildScene: function(data, isBaseDiscrete) {
        var rootScene  = new pvc.visual.Scene(null, {panel: this, source: data});
        var categDatas = data._children;
        var chart = this.chart;
        var serRole = this.visualRoles.series;
        var valueRole = this.visualRoles.value;
        var isStacked = this.stacked;
        var valueVarHelper = new pvc.visual.RoleVarHelper(rootScene, valueRole, {roleVar: 'value', hasPercentSubVar: isStacked});
        var colorVarHelper = new pvc.visual.RoleVarHelper(rootScene, this.visualRoles.color, {roleVar: 'color'});
        var valueDimName  = valueRole.firstDimensionName();
        var valueDim = data.owner.dimensions(valueDimName);
        
        var orthoScale = this.axes.ortho.scale;
        var orthoNullValue = def.scope(function() {
                // If the data does not cross the origin, 
                // Choose the value that's closer to 0.
                var domain = orthoScale.domain(),
                    dmin = domain[0],
                    dmax = domain[1];
                if(dmin * dmax >= 0) {
                    // Both positive or both negative or either is zero
                    return dmin >= 0 ? dmin : dmax;
                }
                
                return 0;
            });
        var orthoZero = orthoScale(orthoNullValue/*0*/);
        var sceneBaseScale = this.axes.base.sceneScale({sceneVarName: 'category'});
        
        // ----------------------------------
        // I   - Create series scenes array.
        // ----------------------------------
        def
        .scope(function() {
            return (serRole && serRole.grouping) ?
                    serRole.flatten(data).children() : // data already only contains visible data
                    def.query([null]); // null series
        })
        /* Create series scene */
        .each(function(seriesData1/*, seriesIndex*/) {
            var seriesScene = new pvc.visual.Scene(rootScene, {source: seriesData1 || data});

            seriesScene.vars.series = pvc_ValueLabelVar.fromComplex(seriesData1);
            
            colorVarHelper.onNewScene(seriesScene, /* isLeaf */ false);
            
            /* Create series-categ scene */
            categDatas.forEach(function(categData, categIndex) {
                var group = categData;
                if(seriesData1) {
                    group = group._childrenByKey[seriesData1.key];
                }
                
                var serCatScene = new pvc.visual.Scene(seriesScene, {source: group});
                
                // -------------
                
                serCatScene.dataIndex = categIndex;
                
                serCatScene.vars.category = pvc_ValueLabelVar.fromComplex(categData);
                
                // -------------

                valueVarHelper.onNewScene(serCatScene, /* isLeaf */ true);

                var valueVar = serCatScene.vars.value;
                var value    = valueVar.value;
                
                // accumulated value, for stacked
                valueVar.accValue = value != null ? value : orthoNullValue;
                
                // -------------
                
                colorVarHelper.onNewScene(serCatScene, /* isLeaf */ true);
                
                // -------------
                // When ignoreNulls=false and nullInterpolatedMode!='none'
                // an interpolated datum may appear along a null datum...
                // Testing if the first datum is interpolated is thus not sufficient.
                var isInterpolated = group != null && 
                                     group.datums().prop('isInterpolated').any(def.truthy);
                //isInterpolatedMiddle = firstDatum.isInterpolatedMiddle;
                
                serCatScene.isInterpolated = isInterpolated;
                //serCatScene.isInterpolatedMiddle = isInterpolatedMiddle;
                
                // TODO: selection, owner Scene ?
                //if(scene.isInterpolated){
                //    scene.ownerScene = toScene;
                //}
                
                // -------------
                
                serCatScene.isNull = value == null;
                serCatScene.isIntermediate = false;
            }, this);
            
        }, this);
        
        // reversed so that "below == before" w.r.t. stacked offset calculation
        // See {@link belowSeriesScenes2} variable.
        var reversedSeriesScenes = rootScene.children().reverse().array();
        
        /** 
         * Update the scene tree to include intermediate leaf-scenes,
         * to help in the creation of lines and areas. 
         */
        var belowSeriesScenes2; // used below, by completeSeriesScenes
        reversedSeriesScenes.forEach(completeSeriesScenes, this);
        
        /** 
         * Trim leading and trailing null scenes.
         */
        reversedSeriesScenes.forEach(trimNullSeriesScenes, this);
        
        return rootScene;
        
        function completeSeriesScenes(seriesScene) {
            var seriesScenes2 = [],
                seriesScenes = seriesScene.childNodes, 
                fromScene,
                notNullCount = 0,
                firstAloneScene = null;
            
            /* As intermediate nodes are added, 
             * seriesScene.childNodes array is changed.
             * 
             * The var 'toChildIndex' takes inserts into account;
             * its value is always the index of 'toScene' in 
             * seriesScene.childNodes.
             */
            for(var c = 0, /* category index */
                    toChildIndex = 0, 
                    categCount = seriesScenes.length ; 
                c < categCount ;
                c++, 
                toChildIndex++) {
                
                var toScene = seriesScenes[toChildIndex],
                    c2 = c * 2; /* doubled category index, for seriesScenes2  */
                
                seriesScenes2[c2] = toScene;
                
                /* Complete toScene */
                completeMainScene.call(this,
                        fromScene, 
                        toScene,
                        /* belowScene */
                        belowSeriesScenes2 && belowSeriesScenes2[c2]);
                
                if(toScene.isAlone && !firstAloneScene) { firstAloneScene = toScene; }
                
                if(!toScene.isNull) { notNullCount++; }
                
                /* Possibly create intermediate scene 
                 * (between fromScene and toScene) 
                 */
                if(fromScene) {
                    var interScene = createIntermediateScene.call(this,
                            seriesScene,
                            fromScene, 
                            toScene,
                            toChildIndex,
                            /* belowScene */
                            belowSeriesScenes2 && belowSeriesScenes2[c2 - 1]);
                    
                    if(interScene) {
                        seriesScenes2[c2 - 1] = interScene;
                        toChildIndex++;
                    }
                }
                
                // --------
                
                fromScene = toScene;
            }
            
            if(notNullCount === 1 && firstAloneScene && categCount === 1) {
                firstAloneScene.isSingle = true;
            }
            
            if(isStacked) { belowSeriesScenes2 = seriesScenes2; } 
        }
        
        function completeMainScene(fromScene, toScene, belowScene) {
            
            var toAccValue = toScene.vars.value.accValue;
            
            if(belowScene) {
                if(toScene.isNull && !isBaseDiscrete) { toAccValue = orthoNullValue; } 
                else { toAccValue += belowScene.vars.value.accValue; }
                
                toScene.vars.value.accValue = toAccValue;
            }
            
            toScene.basePosition  = sceneBaseScale(toScene);
            toScene.orthoPosition = orthoZero;
            toScene.orthoLength   = orthoScale(toAccValue) - orthoZero;
            
            var isNullFrom = (!fromScene || fromScene.isNull),
                isAlone    = isNullFrom && !toScene.isNull;
            if(isAlone) {
                // Confirm, looking ahead
                var nextScene = toScene.nextSibling;
                isAlone  = !nextScene || nextScene.isNull;
            }
            
            toScene.isAlone  = isAlone;
            toScene.isSingle = false;
        }
        
        function createIntermediateScene(seriesScene, fromScene, toScene, toChildIndex, belowScene) {
            
            var interIsNull = fromScene.isNull || toScene.isNull;
            if(interIsNull && !this.areasVisible) { return null; }
            
            var interValue, interAccValue, interBasePosition;
                
            if(interIsNull) {
                /* Value is 0 or the below value */
                if(belowScene && isBaseDiscrete) {
                    var belowValueVar = belowScene.vars.value;
                    interAccValue = belowValueVar.accValue;
                    interValue = belowValueVar[valueRole.name];
                } else {
                    interValue = interAccValue = orthoNullValue;
                }
                
                if(isStacked && isBaseDiscrete) {
                    // The intermediate point is at the start of the "to" band
                    // don't use .band, cause it does not include margins...
                    interBasePosition = toScene.basePosition - (sceneBaseScale.range().step / 2); 
                } else if(fromScene.isNull) { // Come from NULL
                    // Align directly below the (possibly) non-null dot
                    interBasePosition = toScene.basePosition;
                } else /*if(toScene.isNull) */{ // Go to NULL
                    // Align directly below the non-null from dot
                    interBasePosition = fromScene.basePosition;
                } 
//                    else {
//                        interBasePosition = (toScene.basePosition + fromScene.basePosition) / 2;
//                    }
            } else {
                var fromValueVar = fromScene.vars.value,
                    toValueVar   = toScene.vars.value;
                
                interValue = (toValueVar.value + fromValueVar.value) / 2;
                
                // Average of the already offset values
                interAccValue     = (toValueVar.accValue  + fromValueVar.accValue ) / 2;
                interBasePosition = (toScene.basePosition + fromScene.basePosition) / 2;
            }
            
            //----------------
            
            var interScene = new pvc.visual.Scene(seriesScene, {
                    /* insert immediately before toScene */
                    index:  toChildIndex,
                    source: /*toScene.isInterpolatedMiddle ? fromScene.group: */toScene.source
                });
            
            interScene.dataIndex = toScene.dataIndex;
            interScene.vars.category = toScene.vars.category;
            
            var interValueVar = new pvc_ValueLabelVar(
                                    interValue,
                                    valueDim.format(interValue),
                                    interValue);
            
            interValueVar.accValue = interAccValue;
            
            interScene.vars.value = interValueVar;
            interScene.ownerScene     = toScene;
            interScene.isInterpolated = toScene.isInterpolated;
            interScene.isIntermediate = true;
            interScene.isSingle       = false;
            interScene.isNull         = interIsNull;
            interScene.isAlone        = interIsNull && toScene.isNull && fromScene.isNull;
            interScene.basePosition   = interBasePosition;
            interScene.orthoPosition  = orthoZero;
            interScene.orthoLength    = orthoScale(interAccValue) - orthoZero;
            
            colorVarHelper.onNewScene(interScene, /* isLeaf */ true);
            
            return interScene;
        }
        
        function trimNullSeriesScenes(seriesScene) {
            
            var seriesScenes = seriesScene.childNodes,
                L = seriesScenes.length;
            
            // from beginning
            var scene, siblingScene;
            while(L && (scene = seriesScenes[0]).isNull) {
                
                // Don't remove the intermediate dot before the 1st non-null dot
                siblingScene = scene.nextSibling;
                if(siblingScene && !siblingScene.isNull) { break; }
                
                seriesScene.removeAt(0);
                L--;
            }
            
            // from end
            while(L && (scene = seriesScenes[L - 1]).isNull) {
                
                // Don't remove the intermediate dot after the last non-null dot
                siblingScene = scene.previousSibling;
                if(siblingScene && !siblingScene.isNull) { break; }
                
                seriesScene.removeAt(L - 1);
                L--;
            }
        }
    }
});


/**
 * PointAbstract is the class that will be extended by
 * dot, line, stacked-line and area charts.
 */
def
.type('pvc.PointAbstract', pvc.CategoricalAbstract)
.add({
    _animatable: true,
    
    _processOptionsCore: function(options){
        // Has no meaning in this chart type
        options.panelSizeRatio = 1;
        
        this.base(options);
    },
    
    _hasDataPartRole: function(){
        return true;
    },
    
    /**
     * Initializes each chart's specific roles.
     * @override
     */
    _initVisualRoles: function(){
        
        this.base();
        
        this._addVisualRole('value', { 
                isMeasure: true, 
                isRequired: true, 
                isPercent: this.options.stacked,
                requireSingleDimension: true, 
                requireIsDiscrete: false, 
                valueType: Number, 
                defaultDimension: 'value' 
            });
    },
    
    _initPlotsCore: function(/*hasMultiRole*/){
        var options = this.options;
        
        var pointPlot = this._createPointPlot();
        var trend = pointPlot.option('Trend');
        
        if(options.plot2){
            // Line Plot
            var plot2Plot = new pvc.visual.PointPlot(this, {
                name: 'plot2',
                fixed: {
                    DataPart: '1'
                },
                defaults: {
                    ColorAxis:    2,
                    LinesVisible: true,
                    DotsVisible:  true
                }});

            if(!trend){
                trend = plot2Plot.option('Trend');
            }
        }
        
        if(trend){
            // Trend Plot
            new pvc.visual.PointPlot(this, {
                name: 'trend',
                fixed: {
                    DataPart: 'trend',
                    TrendType: 'none',
                    ColorRole: 'series', // one trend per series
                    NullInterpolatioMode: 'none'
                },
                defaults: {
                    ColorAxis:    2,
                    LinesVisible: true,
                    DotsVisible:  false
                }
            });
        }
    },
    
    _bindAxes: function(hasMultiRole){
        
        this.base(hasMultiRole);
        
        // Set defaults of Offset property
        var typeAxes = this.axesByType.base;
        if(typeAxes){
            typeAxes.forEach(function(axis){
                var isDiscrete = axis.scaleType === 'discrete';
                if(!isDiscrete){
                    axis.option.defaults({Offset: 0.01});
                }
            });
        }
        
        typeAxes = this.axesByType.ortho;
        if(typeAxes){
            typeAxes.forEach(function(axis){
                axis.option.defaults({Offset: 0.04});
            });
        }
    },
    
    //_createPointPlot: function(){},
    
    /* @override */
    _createPlotPanels: function(parentPanel, baseOptions){
        var plots   = this.plots;
        
        var pointPlot = plots.point;
            this.scatterChartPanel = 
            new pvc.PointPanel(
                this, 
                parentPanel, 
                pointPlot, 
                Object.create(baseOptions));
        
        var plot2Plot = plots.plot2;
        if(plot2Plot){
            if(pvc.debug >= 3){
                this._log("Creating second Point panel.");
            }
            
            new pvc.PointPanel(
                    this, 
                    parentPanel, 
                    plot2Plot,
                    Object.create(baseOptions));
        }
        
        var trendPlot = plots.trend;
        if(trendPlot){
            if(pvc.debug >= 3){
                this._log("Creating Trends Point panel.");
            }
            
            new pvc.PointPanel(
                    this, 
                    parentPanel, 
                    trendPlot,
                    Object.create(baseOptions));
        }
    },
    
    defaults: {
        tooltipOffset: 10
    }
});

/**
 * Dot Chart
 */
def
.type('pvc.DotChart', pvc.PointAbstract)
.add({
    _createPointPlot: function(){
        return new pvc.visual.PointPlot(this, {
            fixed: {DotsVisible: true}
        });
    }
});

/**
 * Line Chart
 */
def
.type('pvc.LineChart', pvc.PointAbstract)
.add({
    _createPointPlot: function(){
        return new pvc.visual.PointPlot(this, {
            fixed: {LinesVisible: true}
        });
    }
});

/**
 * Area Chart
 */
def
.type('pvc.AreaChart', pvc.PointAbstract)
.add({
    _createPointPlot: function(){
        return new pvc.visual.PointPlot(this, {
            fixed: {AreasVisible: true}
        });
    }
});

/**
 * Stacked Line Chart
 */
pvc.mStackedLineChart = // V1 compatibility    
def
.type('pvc.StackedLineChart', pvc.PointAbstract)
.add({
    _createPointPlot: function(){
        return new pvc.visual.PointPlot(this, {
            fixed: {LinesVisible: true, Stacked: true}
        });
    }
});

/**
 * Stacked Dot Chart
 */
def
.type('pvc.StackedDotChart', pvc.PointAbstract)
.add({
    _createPointPlot: function(){
        return new pvc.visual.PointPlot(this, {
            fixed: {DotsVisible: true, Stacked: true}
        });
    }
});

/**
 * Stacked Area Chart
 */
pvc.mStackedAreaChart = // V1 compatibility
def
.type('pvc.StackedAreaChart', pvc.PointAbstract)
.add({
    _createPointPlot: function(){
        return new pvc.visual.PointPlot(this, {
            fixed:    {AreasVisible: true, Stacked: true},
            defaults: {LinesVisible: true}
        });
    }
});


/*global pvc_ValueLabelVar:true */

/*
 * HeatGrid chart panel. Generates a heatGrid chart. Specific options are:
 * <i>orientation</i> - horizontal or vertical. Default: vertical
 * <i>valuesVisible</i> - Show or hide heatGrid value. Default: false
 * <i>maxHeatGridSize</i> - Maximum size of a heatGrid in pixels. Default: 2000
 *
 * Has the following protovis extension points:
 *
 * <i>chart_</i> - for the main chart Panel
 * <i>heatGrid_</i> - for the actual heatGrid
 * <i>heatGridPanel_</i> - for the panel where the heatGrids sit
 * <i>heatGridLabel_</i> - for the main heatGrid label
 */
def
.type('pvc.HeatGridPanel', pvc.CategoricalAbstractPanel)
.init(function(chart, parent, plot, options) {

    this.base(chart, parent, plot, options);

    this.axes.size = chart._getAxis('size', plot.option('SizeAxis') - 1); // may be undefined

    var roles = this.visualRoles;

    var sizeRoleName = plot.option('SizeRole'); // assumed to be always defined
    roles.size = chart.visualRole(sizeRoleName);

    this.useShapes = plot.option('UseShapes');
    this.shape     = plot.option('Shape');
    this.nullShape = plot.option('NullShape');
})
.add({
    defaultBorder:  1,
    nullBorder:     2,
    selectedBorder: 2,

    /** @override */
    _createCore: function(){
        var me = this;

        me.base();

        var cellSize = me._calcCellSize();

        var a_bottom = me.isOrientationVertical() ? "bottom" : "left";
        var a_left   = pvc.BasePanel.relativeAnchor[a_bottom];
        var a_width  = pvc.BasePanel.parallelLength[a_bottom];
        var a_height = pvc.BasePanel.orthogonalLength[a_bottom];

        /* Column and Row datas  */

        // One multi-dimension single-level data grouping
        var rowRootData = me.visualRoles.series.flatten(me.data, {visible: true});

        // One multi-dimensional, two-levels grouping (Series -> Categ)
        var rootScene  = me._buildScene(me.visibleData({ignoreNulls: false}), rowRootData, cellSize);
        var hasColor   = rootScene.isColorBound;
        var hasSize    = rootScene.isSizeBound;
        var wrapper    = me._buildSignsWrapper(rootScene);
        var isV1Compat = me.compatVersion() <= 1;
        
        var rowScale   = this.axes.base.scale;
        var colScale   = this.axes.ortho.scale;
        
        var rowStep = rowScale.range().step;
        var colStep = colScale.range().step;
        var rowStep2 = rowStep/2;
        var colStep2 = colStep/2;
        
        /* PV Panels */
        var pvRowPanel = new pvc.visual.Panel(me, me.pvPanel)
            .pvMark
            .data(rootScene.childNodes)
            [a_bottom](function(scene){ return colScale(scene.vars.series.value) - colStep2; })
            [a_height](colStep);

        /* Cell panel */
        var extensionIds = ['panel'];
        if(isV1Compat) {
            extensionIds.push(''); // let access as "heatGrid_"
        }

        var keyArgs = {
            extensionId: extensionIds,
            wrapper:     wrapper
        };

        if(!me.useShapes){
            // When no shapes are used,
            // clicks, double-clicks and tooltips are directly handled by
            // the cell panel
            var f = false;
            def.copy(keyArgs, {
                noSelect: f,
                noHover:  f,
                noClick:  f,
                noDoubleClick: f,
                freeColor: f,
                noTooltip: isV1Compat
            });
        }

        me.pvHeatGrid = new pvc.visual.Panel(me, pvRowPanel, keyArgs)
            .lock('data', function(serScene){ return serScene.childNodes; })
            .pvMark
            .lock(a_left,  function(scene){ return rowScale(scene.vars.category.value) - rowStep2; })
            .lock(a_width, rowStep)
            .antialias(false);
            // THIS caused HUGE memory consumption and speed reduction (at least in use Shapes mode, and specially in Chrome)
            // Overflow can be important if valuesVisible=true
            //.overflow('hidden') 

        me.shapes = me.useShapes ? 
                    me._createShapesHeatMap(cellSize, wrapper, hasColor, hasSize) :
                    me._createNoShapesHeatMap(hasColor);

        
        if(me.valuesVisible && !me.valuesMask){
            me.valuesMask = me._getDefaultValuesMask(hasColor, hasSize);
        }
        
        var label = pvc.visual.ValueLabel.maybeCreate(me, me.pvHeatGrid, {wrapper: wrapper});
        if(label){
            me.pvHeatGridLabel = label.pvMark;
        }
    },

    _calcCellSize: function(){
        /* Use existing scales */
        var xScale = this.axes.x.scale;
        var yScale = this.axes.y.scale;

        /* Determine cell dimensions. */
        var w = (xScale.max - xScale.min) / xScale.domain().length;
        var h = (yScale.max - yScale.min) / yScale.domain().length;

        if (!this.isOrientationVertical()) {
            var tmp = w;
            w = h;
            h = tmp;
        }

        return {width: w, height: h};
    },

    _buildSignsWrapper: function(rootScene){
        if(this.compatVersion() > 1){ return null; }

        var colorValuesBySerAndCat =
            def
            .query(rootScene.childNodes)
            .object({
                name:  function(serScene){ return '' + serScene.vars.series.value; },
                value: function(serScene){
                    return def
                        .query(serScene.childNodes)
                        .object({
                            name:  function(leafScene){ return '' + leafScene.vars.category.value; },
                            value: function(leafScene){
                                var colorVar = leafScene.vars.color;
                                return colorVar ? ('' + colorVar.value) : null;
                            }
                        });
                }
            });

        return function(v1f){

            return function(leafScene){
                var colorValuesByCat = colorValuesBySerAndCat[leafScene.vars.series.value];
                var cat = leafScene.vars.category.rawValue;

                var wrapperParent = Object.create(this.parent);
                var wrapper = Object.create(this);
                wrapper.parent = wrapperParent;

                // Previously, first panel was by cats and
                // the second (child) panel was by series
                var catIndex = leafScene.childIndex();
                var serIndex = leafScene.parent.childIndex();

                wrapperParent.index = catIndex;
                wrapper.index = serIndex;

                return v1f.call(wrapper, colorValuesByCat, cat);
            };
        };
    },

    _getDefaultValuesMask: function(hasColor, hasSize){
        // The "value" concept is mapped to one of the color or size roles, by default.
        var roles = this.visualRoles;
        var roleName = hasColor ? 'color' :
                       hasSize  ? 'size'  :
                       null;
        if(roleName){
            var valueDimName = roles[roleName].firstDimensionName();
            return "{#" + valueDimName + "}";
        }
    },

    _createNoShapesHeatMap: function(hasColor){
        var getBaseColor = this._buildGetBaseFillColor(hasColor);
        return this.pvHeatGrid
            .sign
            .override('defaultColor', function(type){
                if(type === 'stroke'){
                    return null;
                }

                return getBaseColor.call(this.pvMark, this.scene);
            })
            .override('interactiveColor', function(color, type){
                var scene = this.scene;
                if(scene.isActive) {
                    return color.alpha(0.6);
                }

                if(scene.anySelected() && !scene.isSelected()) {
                    return this.dimColor(color, type);
                }

                return this.base(color, type);
            })
            .override('dimColor', function(color/*, type*/){
                return pvc.toGrayScale(color, 0.6);
            })
            .pvMark
            .lineWidth(1.5)
            ;
    },

    _buildGetBaseFillColor: function(hasColor) {
        var colorAxis = this.axes.color;
        return hasColor ? 
                colorAxis.sceneScale({sceneVarName: 'color'}) :
                def.fun.constant(colorAxis.option('Unbound'));
    },
            
    _createShapesHeatMap: function(cellSize, wrapper, hasColor, hasSize) {
        var me = this;

        /* SIZE */
        var areaRange = me._calcDotAreaRange(cellSize);
        if(hasSize) { me.axes.size.setScaleRange(areaRange); }

        // Dot Sign
        var keyArgs = {
            extensionId: 'dot',
            freePosition: true,
            activeSeriesAware: false,
            //noHover:      false,
            wrapper:      wrapper,
            tooltipArgs:  me._buildShapesTooltipArgs(hasColor, hasSize)
        };
        
        var pvDot = new pvc.visual.DotSizeColor(me, me.pvHeatGrid, keyArgs)
            .override('dimColor', function(color/*, type*/) { return pvc.toGrayScale(color, 0.6); })
            .pvMark
            .lock('shapeAngle'); // TODO - rotation of shapes can cause them to not fit the calculated cell. Would have to improve the radius calculation code.
            
        if(!hasSize){
            pvDot.sign.override('defaultSize', def.fun.constant(areaRange.max));
        }
        
        return pvDot;
    },

    _calcDotAreaRange: function(cellSize){
        var w = cellSize.width;
        var h = cellSize.height;

        var maxRadius = Math.min(w, h) / 2;

        if(this.shape === 'diamond'){
            // Protovis draws diamonds inscribed on
            // a square with half-side radius*Math.SQRT2
            // (so that diamonds just look like a rotated square)
            // For the height of the diamond not to exceed the cell size
            // we compensate that factor here.
            maxRadius /= Math.SQRT2;
        }

        // Small margin
        maxRadius -= 2;

        var maxArea  = def.sqr(maxRadius), // apparently treats as square area even if circle, triangle is different
            minArea  = 12,
            areaSpan = maxArea - minArea;

        if(areaSpan <= 1){
            // Very little space
            // Rescue Mode - show *something*
            maxArea = Math.max(maxArea, 2);
            minArea = 1;
            areaSpan = maxArea - minArea;

            if(pvc.debug >= 2){
                this._warn("Using rescue mode dot area calculation due to insufficient space.");
            }
        }

        //var missingArea = minArea + areaSpan * 0.2; // 20% size

        return {
            min:  minArea,
            max:  maxArea,
            span: areaSpan
        };
    },

    _buildShapesTooltipArgs: function(hasColor, hasSize){
        var chart = this.chart;

        if(this.compatVersion() <= 1 && this.showsTooltip()){
            var options = chart.options;
            var customTooltip = options.customTooltip;
            if(!customTooltip){
                customTooltip = function(s,c,d){
                    if(d != null && d[0] !== undefined){
                        return d.join(', ');
                    }
                    return d;
                };
            }

            var roles   = this.visualRoles;
            var seriesDimsNames = roles.series.grouping.dimensionNames();
            var categDimsNames  = roles.category.grouping.dimensionNames();

            // For use in keyArgs.tooltipArgs
            return {
                buildTooltip: options.isMultiValued ?
                    function(context){
                        var group = context.scene.group;
                        if(!group) { return ""; } // null scene
                        
                        var s = pvc.data.Complex.values(group, seriesDimsNames);
                        var c = pvc.data.Complex.values(group, categDimsNames);

                        var d = [];
                        var vars = context.scene.vars;
                        if(hasSize ) { d[options.sizeValIdx  || 0] = vars.size.value;  }
                        if(hasColor) { d[options.colorValIdx || 0] = vars.color.value; }

                        return customTooltip.call(options, s, c, d);
                    } :
                    function(context){
                        var vars = context.scene.vars;
                        var s = vars.series.rawValue;
                        var c = vars.category.rawValue;
                        var valueVar = vars[hasColor ? 'color' : 'size'];
                        var d = valueVar ? valueVar.value : null;
                        return customTooltip.call(options, s, c, d);
                    }
            };
        }
    },

    /**
     * Renders the heat grid panel.
     * @override
     */
    renderInteractive: function(){
        this.pvPanel.render();
    },

    _buildScene: function(data, seriesRootData, cellSize){
        var me = this;
        var rootScene  = new pvc.visual.Scene(null, {panel: me, source: data});
        var categDatas = data._children;

        var roles = me.visualRoles;
        var colorVarHelper = new pvc.visual.RoleVarHelper(rootScene, roles.color, {roleVar: 'color'});
        var sizeVarHelper  = new pvc.visual.RoleVarHelper(rootScene, roles.size,  {roleVar: 'size' });
        
        rootScene.cellSize = cellSize;

        seriesRootData
            .children()
            .each(createSeriesScene);

        return rootScene;

        function createSeriesScene(serData1){
            /* Create series scene */
            var serScene = new pvc.visual.Scene(rootScene, {source: serData1});

            serScene.vars.series = pvc_ValueLabelVar.fromComplex(serData1);

            categDatas.forEach(function(catData1){
                createSeriesCategoryScene.call(me, serScene, catData1, serData1);
            });
        }

        function createSeriesCategoryScene(serScene, catData1, serData1){
            var group = data._childrenByKey[catData1.key]._childrenByKey[serData1.key];

            var serCatScene = new pvc.visual.Scene(serScene, {source: group});

            serCatScene.vars.category = pvc_ValueLabelVar.fromComplex(catData1);

            colorVarHelper.onNewScene(serCatScene, /* isLeaf */true);
            sizeVarHelper .onNewScene(serCatScene, /* isLeaf */true);
        }
    }
});


/**
 * HeatGridChart is the main class for generating... heatGrid charts.
 *  A heatGrid visualizes a matrix of values by a grid (matrix) of *
 *  bars, where the color of the bar represents the actual value.
 *  By default the colors are a range of green values, where
 *  light green represents low values and dark green high values.
 *  A heatGrid contains:
 *     - two categorical axis (both on x and y-axis)
 *     - no legend as series become rows on the perpendicular axis 
 *  Please contact CvK if there are issues with HeatGrid at cde@vinzi.nl.
 */
def
.type('pvc.HeatGridChart', pvc.CategoricalAbstract)
.add({
    _allowColorPerCategory: true,

    // Create color axis, even if the role is unbound
    // cause we need to check the axis options any way
    _axisCreateIfUnbound: {
        'color': true
    },

    _processOptionsCore: function(options){
        
        this.base(options);
        
        def.set(options, 
            'orthoAxisOrdinal', true,
            'legend', false,
                
            // Has no meaning in the current implementation
            'panelSizeRatio', 1);
        
     // TODO: get a translator for this!!
        
        var colorDimName = 'value',
            sizeDimName  = 'value2';

        if(this.compatVersion() <= 1){
            switch(this.options.colorValIdx){
                case 0:  colorDimName = 'value';  break;
                case 1:  colorDimName = 'value2'; break;
                default: colorDimName = 'value';
            }
    
            switch(this.options.sizeValIdx){
                case 0:  sizeDimName = 'value' ; break;
                case 1:  sizeDimName = 'value2'; break;
                default: sizeDimName = 'value' ;
            }
        }
        
        this._colorDimName = colorDimName;
        this._sizeDimName  = sizeDimName ;
    },
    
    _getCategoryRoleSpec: function(){
        var catRoleSpec = this.base();
        
        // Force dimension to be discrete!
        catRoleSpec.requireIsDiscrete = true;
        
        return catRoleSpec;
    },
    
    _getColorRoleSpec: function(){
        return {
            isMeasure: true,
            requireSingleDimension: true,
            requireIsDiscrete: false,
            valueType: Number,
            defaultDimension: this._colorDimName
        };
    },
    
    /**
     * Initializes each chart's specific roles.
     * @override
     */
    _initVisualRoles: function(){
        
        this.base();
        
        this._addVisualRole('size', {
            isMeasure: true,
            requireSingleDimension: true,
            requireIsDiscrete: false,
            valueType: Number,
            defaultDimension: this._sizeDimName
        });
    },

    _initPlotsCore: function(/*hasMultiRole*/){
        new pvc.visual.HeatGridPlot(this);
    },
    
    _collectPlotAxesDataCells: function(plot, dataCellsByAxisTypeThenIndex){
        
        this.base(plot, dataCellsByAxisTypeThenIndex);
        
        /* Configure Base Axis Data Cell */
        if(plot.type === 'heatGrid' && plot.option('UseShapes')){
            
            var sizeRole = this.visualRole(plot.option('SizeRole'));
            if(sizeRole.isBound()){
                
                var sizeDataCellsByAxisIndex = 
                    def
                    .array
                    .lazy(dataCellsByAxisTypeThenIndex, 'size');
                
                def
                .array
                .lazy(sizeDataCellsByAxisIndex, plot.option('SizeAxis') - 1)
                .push({
                    plot:          plot,
                    role:          sizeRole,
                    dataPartValue: plot.option('DataPart')
                });
            }
        }
    },
    
    _setAxesScales: function(hasMultiRole){
        
        this.base(hasMultiRole);
        
        if(!hasMultiRole || this.parent){
            
            var sizeAxis = this.axes.size;
            if(sizeAxis && sizeAxis.isBound()){
                this._createAxisScale(sizeAxis);
            }
        }
    },
    
    /* @override */
    _createPlotPanels: function(parentPanel, baseOptions){
        var heatGridPlot = this.plots.heatGrid;
        
        this.heatGridChartPanel = 
                new pvc.HeatGridPanel(
                        this, 
                        parentPanel, 
                        heatGridPlot, 
                        Object.create(baseOptions));
    },
    
    defaults: {
        colorValIdx: 0,
        sizeValIdx:  1,
        measuresIndexes: [2], // TODO: ???
        axisOffset: 0,
        plotFrameVisible: false,
        colorNormByCategory: true,
        numSD: 2   // width (only for normal distribution)
    }
});


/**
 * MetricXYAbstract is the base class of metric XY charts.
 * (Metric stands for:
 *   Measure, Continuous or Not-categorical base and ortho axis)
 */
def
.type('pvc.MetricXYAbstract', pvc.CartesianAbstract)
.add({
    _processOptionsCore: function(options) {
        
        this.base(options);
        
        // Has no meaning in this chart type
        // Only used by discrete scales
        options.panelSizeRatio = 1;
    },

    /**
     * Initializes each chart's specific roles.
     * @override
     */
    _initVisualRoles: function() {

        this.base();

        this._addVisualRole('x', {
            isMeasure:  true,
            isRequired: true,
            requireSingleDimension: true,
            requireIsDiscrete: false,
            defaultDimension: 'x',
            dimensionDefaults: {
                valueType: this.options.timeSeries ? Date : Number
            }
        });
        
        this._addVisualRole('y', {
            isMeasure:  true,
            isRequired: true,
            requireSingleDimension: true,
            requireIsDiscrete: false,
            defaultDimension: 'y',
            dimensionDefaults: {valueType: Number}
        });
    },
            
    _generateTrendsDataCellCore: function(newDatums, dataCell, trendInfo) {
        var serRole = this._serRole;
        var xRole   = this.visualRoles.x;
        var yRole   = dataCell.role;
        var trendOptions = dataCell.trend;
        
        this._warnSingleContinuousValueRole(yRole);
        
        var dataPartDimName = this._dataPartRole.firstDimensionName();
        var xDimName = xRole.firstDimensionName();
        var yDimName = yRole.firstDimensionName();
        
        // Visible part data, possibly grouped by series (if series is bound)
        var data = this.visibleData(dataCell.dataPartValue); // [ignoreNulls=true]
        
        // For each series...
        // Or data already only contains visible data
        // Or null series
        def
        .scope(function() { return serRole.isBound() ? data.children() : def.query([data]); })
        .each(genSeriesTrend, this);
        
        function genSeriesTrend(serData) {
            var funX    = function(datum) { return datum.atoms[xDimName].value; };
            var funY    = function(datum) { return datum.atoms[yDimName].value; };
            var datums  = serData.datums().sort(null, /* by */funX).array();
            var options = def.create(trendOptions, {rows: def.query(datums), x: funX, y: funY});

            var trendModel = trendInfo.model(options);
            if(trendModel) {
                // If a label has already been registered, it is preserved... (See BaseChart#_fixTrendsLabel)
                var dataPartAtom = 
                    data.owner
                        .dimensions(dataPartDimName)
                        .intern(this.root._firstTrendAtomProto);
                
                datums.forEach(function(datum, index) {
                    var trendX = funX(datum);
                    if(trendX) {
                        var trendY = trendModel.sample(trendX, funY(datum), index);
                        if(trendY != null) {
                            var atoms = 
                                def.set(
                                    Object.create(serData.atoms), // just common atoms
                                    xDimName, trendX,
                                    yDimName, trendY,
                                    dataPartDimName, dataPartAtom);
                            
                            newDatums.push(
                                def.set(
                                    new pvc.data.Datum(data.owner, atoms),
                                    'isVirtual', true,
                                    'isTrend',   true,
                                    'trendType', trendInfo.type));
                        }
                    }
                });
            }
        }
    }
});


/**
 * @name pvc.data.MetricPointChartTranslationOper
 * 
 * @class The translation mixin of the Metric XY charts.
 * 
 * <p>
 * The default format is:
 * </p>
 * <pre>
 * +----------+----------+----------+----------+----------+
 * | 0        | 1        | 2        | 3        | 4        |
 * +----------+----------+----------+----------+----------+
 * | series   | x        | y        | color    | size     |
 * +----------+----------+----------+----------+----------+
 * | discrete | number   | number   | num/disc | number   |
 * +----------+----------+----------+----------+----------+
 * </pre>
 * 
 * <p>
 * Color dimensions will be continuous by default.
 * If that is not the case, 
 * an explicit dimension valueType definition must be provided.
 * </p>
 * 
 * @extends pvc.data.MatrixTranslationOper
 */
def.type('pvc.data.MetricPointChartTranslationOper')
.add(/** @lends pvc.data.MetricPointChartTranslationOper# */{
    
    _meaLayoutRoles: ['x', 'y', 'color', 'size'],
    
    configureType: function(){
        // VItem Indexes of continuous columns not yet being read
        var freeMeaIndexes = [];
        
        // Idem, but for discrete columns
        var freeDisIndexes = [];
        
        this.collectFreeDiscreteAndConstinuousIndexes(freeDisIndexes, freeMeaIndexes);
        
        // Distribute free measure columns by unbound measure roles 
        var N;
        var autoDimNames = [];
        var F = freeMeaIndexes.length;
        if(F > 0){
            // Collect the default dimension names of the 
            // first F unbound roles
            var R = this._meaLayoutRoles.length;
            var i = 0;
            while(i < R && autoDimNames.length < F){
                // If the measure role is unbound and has a default dimension,
                //  the next unused dimension of the default dimension group name
                //  is placed in autoDimNames.
                // If any, this dimension will be fed with the next freeMeaIndexes
                this._getUnboundRoleDefaultDimNames(this._meaLayoutRoles[i], 1, autoDimNames);
                i++;
            }
            
            N = autoDimNames.length;
            if(N > 0){
                freeMeaIndexes.length = N;
                this.defReader({names: autoDimNames, indexes: freeMeaIndexes});
            }
        }
        
        // All discrete columns go to series dimensions
        F = freeDisIndexes.length;
        if(F > 0) {
            autoDimNames.length = 0;
            this._getUnboundRoleDefaultDimNames('series', F, autoDimNames);
            
            N = autoDimNames.length;
            if(N > 0){
                freeDisIndexes.length = N;
                this.defReader({names: autoDimNames, indexes: freeDisIndexes});
            }
        }
    }
});

/*global pvc_Sides:true, pvc_ValueLabelVar:true */

/*
 * Metric Line/Dot panel.
 * Class that draws dot and line plots.
 * Specific options are:
 * <i>dotsVisible</i> - Show or hide dots. Default: true
 * <i>linesVisible</i> - Show or hide dots. Default: true
 * <i>valuesVisible</i> - Show or hide line value. Default: false
 *
 * Has the following protovis extension points:
 *
 * <i>chart_</i> - for the main chart Panel
 * <i>line_</i> - for the actual line
 * <i>linePanel_</i> - for the panel where the lines sit
 * <i>lineDot_</i> - the dots on the line
 * <i>lineLabel_</i> - for the main line label
 */
def
.type('pvc.MetricPointPanel', pvc.CartesianAbstractPanel)
.init(function(chart, parent, plot, options) {
    
    this.base(chart, parent, plot, options);
    
    this.axes.size  = chart._getAxis('size', (plot.option('SizeAxis') || 0) - 1); // may be undefined

    var sizeRoleName = plot.option('SizeRole'); // assumed to be always defined
    this.visualRoles.size = sizeRoleName ? chart.visualRole(sizeRoleName) : null;
    
    this.linesVisible = plot.option('LinesVisible'); // TODO
    this.dotsVisible  = plot.option('DotsVisible' ); // TODO
    if(!this.linesVisible && !this.dotsVisible){
        this.linesVisible = true;
        plot.option.specify({'LinesVisible': true});
    }

    if(!this.offsetPaddings){
        this.offsetPaddings = new pvc_Sides(0.01);
    }
})
.add({
    // Ratio of the biggest bubble diameter to 
    // the length of plot area dimension according to option 'sizeAxisRatioTo'
    sizeAxisRatio: 1/5,
    sizeAxisRatioTo: 'minWidthHeight', // 'height', 'width', 
    autoPaddingByDotSize: true,
    
    // Override default mappings
    _v1DimRoleName: {
        //'series':   'series', // inherited
        'category': 'x',
        'value':    'y'
    },
    
    _creating: function(){
        // Register BULLET legend prototype marks
        var groupScene = this.defaultVisibleBulletGroupScene();
        if(groupScene && !groupScene.hasRenderer()){
            var colorAxis = groupScene.colorAxis;
            var drawMarker = def.nullyTo(colorAxis.option('LegendDrawMarker', true), this.dotsVisible);
            var drawRule   = def.nullyTo(colorAxis.option('LegendDrawLine',   true), this.linesVisible);
            if(drawMarker || drawRule){
                var keyArgs = {drawMarker: drawMarker, drawRule: drawRule};
                if(drawMarker){
                    keyArgs.markerShape = 
                        colorAxis.option('LegendShape', true) || 
                        'circle'; // Dot's default shape
                    
                    keyArgs.markerPvProto = new pv.Dot()
                            .lineWidth(1.5, pvc.extensionTag) // act as if it were a user extension
                            .shapeSize(12, pvc.extensionTag); // idem
                    
                    this.extend(keyArgs.markerPvProto, 'dot', {constOnly: true});
                }
                
                if(drawRule){
                    keyArgs.rulePvProto = new pv.Line()
                            .lineWidth(1.5, pvc.extensionTag);
                    
                    this.extend(keyArgs.rulePvProto, 'line', {constOnly: true});
                }
                
                groupScene.renderer(
                    new pvc.visual.legend.BulletItemDefaultRenderer(keyArgs));
            }
        }
    },
    
    _getRootScene: function(){
        return def.lazy(this, '_rootScene', this._buildScene, this);
    },
    
    /*
    * @override
    */
    _calcLayout: function(layoutInfo){
        var rootScene = this._getRootScene();
        if(rootScene.isSizeBound){
            this.axes.size.setScaleRange(
                    this._calcDotAreaRange(layoutInfo));
        }
        
        /* Adjust axis offset to avoid dots getting off the content area */
        this._calcAxesPadding(layoutInfo, rootScene);
    },
  
    _getDotDiameterRefLength: function(layoutInfo){
        // Use the border box to always have the same size for != axis offsets (paddings)
       
        var clientSize = layoutInfo.clientSize;
        var paddings   = layoutInfo.paddings;
       
        switch(this.sizeAxisRatioTo){
            case 'minWidthHeight': 
                return Math.min(
                        clientSize.width  + paddings.width, 
                        clientSize.height + paddings.height);
           
            case 'width':  return clientSize.width  + paddings.width ;
            case 'height': return clientSize.height + paddings.height;
        }
       
        if(pvc.debug >= 2){
            this._log(
                def.format(
                    "Invalid option 'sizeAxisRatioTo' value. Assuming 'minWidthHeight'.", 
                    [this.sizeAxisRatioTo]));
        }
       
        this.sizeRatioTo = 'minWidthHeight';
       
        return this._getDotDiameterRefLength(layoutInfo);
    },
   
    _calcDotRadiusRange: function(layoutInfo){
        var refLength = this._getDotDiameterRefLength(layoutInfo);
       
        // Diameter is 1/5 of ref length
        var max = (this.sizeAxisRatio / 2) * refLength;
       
        // Minimum SIZE (not radius) is 12
        var min = Math.sqrt(12); 
       
        return {min: min, max: max};
    },
   
    _calcDotAreaRange: function(layoutInfo){
       
        var radiusRange = this._calcDotRadiusRange(layoutInfo);
       
        // Diamond Adjustment
        if(this.shape === 'diamond'){
            // Protovis draws diamonds inscribed on
            // a square with half-side radius*Math.SQRT2
            // (so that diamonds just look like a rotated square)
            // For the height/width of the diamond not to exceed the cell size
            // we compensate that factor here.
            radiusRange.max /= Math.SQRT2;
            radiusRange.min /= Math.SQRT2;
        }
      
        var maxArea  = def.sqr(radiusRange.max),
            minArea  = def.sqr(radiusRange.min),
            areaSpan = maxArea - minArea;
      
        if(areaSpan <= 1){
            // Very little space
            // Rescue Mode - show *something*
            maxArea  = Math.max(maxArea, 2);
            minArea  = 1;
            areaSpan = maxArea - minArea;
          
            radiusRange = {
                min: Math.sqrt(minArea),
                max: Math.sqrt(maxArea)
            };
          
            if(pvc.debug >= 3){
                this._log("Using rescue mode dot area calculation due to insufficient space.");
            }
        }
      
        return {
            min:  minArea,
            max:  maxArea,
            span: areaSpan
        };
    },
   
    _calcAxesPadding: function(layoutInfo, rootScene){
        // If we were not to take axes rounding padding effect
        // into account, it could be as simple as:
        // var offsetRadius = radiusRange.max + 6;
        // requestPaddings = new pvc_Sides(offsetRadius);
       
        var requestPaddings;
       
        if(!this.autoPaddingByDotSize){
            requestPaddings = this._calcRequestPaddings(layoutInfo);
        } else {
            var axes  = this.axes;
            var clientSize = layoutInfo.clientSize;
            var paddings   = layoutInfo.paddings;
           
            requestPaddings = {};
           
            /* The Worst case implementation would be like:
             *   Use more padding than is required in many cases,
             *   but ensures that no dot ever leaves the "stage".
             * 
             *   Half a circle must fit in the client area
             *   at any edge of the effective plot area 
             *   (the client area minus axis offsets).
             */
           
            // X and Y axis orientations
            axes.x.setScaleRange(clientSize.width );
            axes.y.setScaleRange(clientSize.height);
           
            // X and Y visual roles
            var sceneXScale = axes.base .sceneScale({sceneVarName: 'x'});
            var sceneYScale = axes.ortho.sceneScale({sceneVarName: 'y'});
           
            var xLength = axes.base .scale.max;
            var yLength = axes.ortho.scale.max;
           
            var hasSizeRole = rootScene.isSizeBound;
            var sizeScale   = hasSizeRole ? this.axes.size.scale : null;
            if(!sizeScale){
                // Use the dot default size
                var defaultSize = def.number.as(this._getExtension('dot', 'shapeRadius'), 0);
                if(defaultSize <= 0){
                    defaultSize = def.number.as(this._getExtension('dot', 'shapeSize'), 0);
                    if(defaultSize <= 0){
                        defaultSize = 12;
                    }
                } else {
                    // Radius -> Size
                    defaultSize = def.sqr(defaultSize);
                }
               
                sizeScale = def.fun.constant(defaultSize);
            }
           
            // TODO: these padding requests do not take the resulting new scale into account
            // and as such do not work exactly...
            //var xMinPct = xScale(xDomain.min) /  clientSize.width;
            //var overflowLeft = (offsetRadius - xMinPct * (paddings.left + clientSize.width)) / (1 - xMinPct);
           
            requestPaddings = {};
           
            // Resolve (not of PercentValue so cannot use pvc.Sides#resolve)
            var op;
            if(this.offsetPaddings){
                op = {};
                pvc_Sides.names.forEach(function(side){
                    var len_a = pvc.BasePanel.orthogonalLength[side];
                    op[side] = (this.offsetPaddings[side] || 0) * (clientSize[len_a] + paddings[len_a]);
                }, this);
            }
            
            // TODO: this seems to not be working on negative x, y values
            var setSide = function(side, padding){
                if(op){
                    padding += (op[side] || 0);
                }
               
                if(padding < 0){
                    padding = 0;
                }
               
                var value = requestPaddings[side];
                if(value == null || padding > value){
                    requestPaddings[side] = padding;
                }
            };
           
            var processScene = function(scene){
                var x = sceneXScale(scene);
                var y = sceneYScale(scene);
                var r = Math.sqrt(sizeScale(hasSizeRole ? scene.vars.size.value : 0));
               
                // How much overflow on each side?
                setSide('left',   r - x);
                setSide('bottom', r - y);
                setSide('right',  x + r - xLength );
                setSide('top',    y + r - yLength);
            };
           
            rootScene
                .children()
                .selectMany(function(seriesScene){ return seriesScene.childNodes; })
                .each(processScene);
        }
       
        layoutInfo.requestPaddings = requestPaddings;
    },
   
    /**
     * @override
     */
    _createCore: function(/*layoutInfo*/){
        var me = this;

        me.base();
         
        var chart      = me.chart;
        var rootScene  = me._getRootScene();
        var wrapper    = me._buildSignsWrapper();
        var isV1Compat = me.compatVersion() <= 1;
        
        this._finalizeScene(rootScene);

        // ---------------
        // BUILD
        
        me.pvPanel.zOrder(1); // Above axes
        
        this.pvScatterPanel = new pvc.visual.Panel(me, me.pvPanel, {
                extensionId: 'panel'
            })
            .lock('data', rootScene.childNodes)
            .pvMark
            ;
        
        // -- LINE --
        var isLineNoSelect = /*dotsVisible && */chart.selectableByFocusWindow();
        
        // A discrete color role may have null values; the line is not hidden.
        var isColorDiscrete = rootScene.isColorBound && this.visualRoles.color.isDiscrete();
        
        var line = new pvc.visual.Line(me, me.pvScatterPanel, {
                extensionId: 'line',
                wrapper:     wrapper,
                noTooltip:   false,
                noSelect:       isLineNoSelect,
                showsSelection: !isLineNoSelect
            })
            .lockMark('data', function(seriesScene){ return seriesScene.childNodes; })
            .intercept('visible', function(scene){
                if(!me.linesVisible){
                    return false;
                }

                var visible = this.delegateExtension();
                if(visible == null){
                    visible = !scene.isNull &&
                             ((!rootScene.isSizeBound && !rootScene.isColorBound) ||
                              (rootScene.isSizeBound  && scene.vars.size.value  != null) ||
                              (rootScene.isColorBound && (isColorDiscrete || scene.vars.color.value != null)));
                }
                
                return visible;
            })
            .override('x',   function(){ return this.scene.basePosition;  })
            .override('y',   function(){ return this.scene.orthoPosition; });
        
        me.pvLine = line.pvMark;
        
        // -- DOT --
        var dot = new pvc.visual.DotSizeColor(me, me.pvLine, {
                extensionId: 'dot',
                wrapper:     wrapper,
                activeSeriesAware: me.linesVisible
            })
            .override('x',  function(){ return this.scene.basePosition;  })
            .override('y',  function(){ return this.scene.orthoPosition; })
            .override('color', function(type) {
                /* 
                 * Handle dotsVisible
                 * -----------------
                 * Despite !dotsVisible,
                 * show a dot anyway when:
                 * 1) it is active, or
                 * 2) it is single  (the only dot in the dataset)
                 */
                if(!me.dotsVisible && !this.scene.isActive && !this.scene.isSingle){
                    return pvc.invisibleFill;
                }
                
                // Follow normal logic
                return this.base(type);
            });
            
        if(!rootScene.isSizeBound){           
            dot
            .override('baseSize', function(){
                /* When not showing dots, 
                 * but a datum is alone and 
                 * wouldn't be visible using lines,  
                 * show the dot anyway, 
                 * with a size = to the line's width^2
                 */
                if(!me.dotsVisible) {
                    if(this.scene.isSingle) {
                        // Obtain the line Width of the "sibling" line
                        var lineWidth = Math.max(me.pvLine.scene[this.pvMark.index].lineWidth, 0.2) / 2;
                        return def.sqr(lineWidth);
                    }
                }

                return this.base();
            });
        } else if(!(me.autoPaddingByDotSize && me.sizeAxisRatioTo === 'minWidthHeight')){
            // Default is to hide overflow dots, 
            // for a case where the provided offset, or calculated one is not enough 
            // (sizeAxisRatioTo='width' or 'height' don't guarantee no overflow)
            // Padding area is used by the bubbles.
            me.pvPanel.borderPanel.overflow("hidden");
        }

        me.pvDot = dot.pvMark;
        me.pvDot.rubberBandSelectionMode = 'center';
        
        if(pvc.visual.ValueLabel.isNeeded(me)){
            var extensionIds = ['label'];
            if(isV1Compat) { extensionIds.push('lineLabel'); }
        
            var label = pvc.visual.ValueLabel.maybeCreate(me, me.pvDot, {
                extensionId: extensionIds,
                wrapper: wrapper
            });
            
            if(label){
                me.pvHeatGridLabel = label.pvMark;
            }
        }
    },

    _buildSignsWrapper: function() {
        if(this.compatVersion() > 1) { return null; }

        return function(v1f) {
            return function(scene) {
                var d = {
                        category: scene.vars.x.rawValue,
                        value:    scene.vars.y.rawValue
                    };

                // Compensate for the effect of intermediate scenes on mark's index
                var pseudo = Object.create(this);
                pseudo.index = scene.dataIndex;
                return v1f.call(pseudo, d);
            };
        };
    },

    /**
     * Renders this.pvScatterPanel - the parent of the marks that are affected by interaction changes.
     * @override
     */
    renderInteractive: function(){
        this.pvScatterPanel.render();
    },

    _buildScene: function(){
        var data = this.visibleData({ignoreNulls: false});
        var rootScene = new pvc.visual.Scene(null, {panel: this, source: data});

        var roles = this.visualRoles;
        var axes  = this.axes;
        var colorVarHelper = new pvc.visual.RoleVarHelper(rootScene, roles.color, {roleVar: 'color'});
        var sizeVarHelper  = new pvc.visual.RoleVarHelper(rootScene, roles.size,  {roleVar: 'size' });
        
        var xDim = data.owner.dimensions(roles.x.firstDimensionName());
        var yDim = data.owner.dimensions(roles.y.firstDimensionName());
        
        // --------------
        
        data.children()
            .each(createSeriesScene, this);
        
        /** 
         * Update the scene tree to include intermediate leaf-scenes,
         * to add in the creation of lines and areas. 
         */
        rootScene
            .children()
            .each(completeSeriesScenes, this);
        
        return rootScene;
        
        function createSeriesScene(seriesGroup){
            /* Create series scene */
            var seriesScene = new pvc.visual.Scene(rootScene, {source: seriesGroup});
            
            seriesScene.vars.series =
                    pvc_ValueLabelVar.fromComplex(seriesGroup);
            
            colorVarHelper.onNewScene(seriesScene, /* isLeaf */ false);
            
            seriesGroup
            .datums()
            .each(function(datum, dataIndex){
                var xAtom = datum.atoms[xDim.name];
                if(xAtom.value == null){
                    return;
                }
                
                var yAtom = datum.atoms[yDim.name];
                if(yAtom.value == null){
                    return;
                }
                
                /* Create leaf scene */
                var scene = new pvc.visual.Scene(seriesScene, {source: datum});
                scene.dataIndex = dataIndex;
                
                scene.vars.x = pvc_ValueLabelVar.fromAtom(xAtom);
                scene.vars.y = pvc_ValueLabelVar.fromAtom(yAtom);
                
                sizeVarHelper .onNewScene(scene, /* isLeaf */ true);
                colorVarHelper.onNewScene(scene, /* isLeaf */ true);
                
                scene.isIntermediate = false;
            });
        }
        
        function completeSeriesScenes(seriesScene) {
            var seriesScenes = seriesScene.childNodes, 
                fromScene;
            
            /* As intermediate nodes are added, 
             * seriesScene.childNodes array is changed.
             * 
             * The var 'toChildIndex' takes inserts into account;
             * its value is always the index of 'toScene' in 
             * seriesScene.childNodes.
             */
            for(var c = 0, /* category index */
                    toChildIndex = 0,
                    pointCount = seriesScenes.length ; c < pointCount ; c++, toChildIndex++) {
                
                /* Complete toScene */
                var toScene = seriesScenes[toChildIndex];
                toScene.isSingle = !fromScene && !toScene.nextSibling;  // Look ahead
                
                /* Possibly create intermediate scene 
                 * (between fromScene and toScene)
                 */
                if(fromScene) {
                    var interScene = createIntermediateScene(
                            seriesScene,
                            fromScene, 
                            toScene,
                            toChildIndex);
                    
                    if(interScene){
                        toChildIndex++;
                    }
                }
                
                // --------
                
                fromScene = toScene;
            }
        }
        
        function createIntermediateScene(
                     seriesScene, 
                     fromScene, 
                     toScene, 
                     toChildIndex){
            
            /* Code for single, continuous and date/numeric dimensions
             * Calls corresponding dimension's cast to ensure we have a date object,
             * when that's the dimension value type.
             */
            var interYValue = yDim.type.cast.call(null, ((+toScene.vars.y.value) + (+fromScene.vars.y.value)) / 2);
            var interXValue = xDim.type.cast.call(null, ((+toScene.vars.x.value) + (+fromScene.vars.x.value)) / 2);
            
            //----------------
            
            var interScene = new pvc.visual.Scene(seriesScene, {
                    /* insert immediately before toScene */
                    index:  toChildIndex,
                    source: toScene.datum
                });
            
            interScene.dataIndex = toScene.dataIndex;
            
            interScene.vars.x = new pvc_ValueLabelVar(
                                    interXValue,
                                    xDim.format(interXValue),
                                    interXValue);
            
            interScene.vars.y = new pvc_ValueLabelVar(
                                    interYValue,
                                    yDim.format(interYValue),
                                    interYValue);
            
            sizeVarHelper .onNewScene(interScene, /* isLeaf */ true);
            colorVarHelper.onNewScene(interScene, /* isLeaf */ true);
            
            interScene.ownerScene = toScene;
            interScene.isIntermediate = true;
            interScene.isSingle = false;
            
            return interScene;
        }
    },

    _finalizeScene: function(rootScene){
        var axes = this.axes,
            sceneBaseScale  = axes.base.sceneScale ({sceneVarName: 'x'}),
            sceneOrthoScale = axes.ortho.sceneScale({sceneVarName: 'y'});

        rootScene
            .children()
            .selectMany(function(seriesScene){ return seriesScene.childNodes; })
            .each(function(leafScene){
                leafScene.basePosition  = sceneBaseScale (leafScene);
                leafScene.orthoPosition = sceneOrthoScale(leafScene);
            });

        return rootScene;
    }
});


/**
 * MetricPointAbstract is the base class of metric dot and line.
 */
def
.type('pvc.MetricPointAbstract', pvc.MetricXYAbstract)
.add({
    _initPlotsCore: function(){
        var pointPlot = this._createPointPlot();
        
        var trend = pointPlot.option('Trend');
        if(trend){
            // Trend Plot
            new pvc.visual.MetricPointPlot(this, {
                name: 'trend',
                fixed: {
                    DataPart: 'trend',
                    TrendType: 'none',
                    NullInterpolatioMode: 'none',
                    ColorRole: 'series', // one trend per series
                    SizeRole:  null,
                    SizeAxis:  null,
                    OrthoAxis:    1
                },
                defaults: {
                    ColorAxis:    2,
                    LinesVisible: true,
                    DotsVisible:  false
                }
            });
        }
    },
    
    //_createPointPlot: function(){},
    
    /* Required because of trends */
    _hasDataPartRole: function(){
        return true;
    },
    
    _getColorRoleSpec: function(){
        return {
            //isMeasure: true, // TODO: not being set as measure when continuous...
            defaultSourceRole: 'series',
            defaultDimension:  'color*',
            dimensionDefaults: {
                valueType: Number
            }
        };
    },
    
    /**
     * Initializes each chart's specific roles.
     * @override
     */
    _initVisualRoles: function(){
        
        this.base();
        
        this._addVisualRole('size', {
                isMeasure: true,
                requireSingleDimension: true,
                requireIsDiscrete: false,
                defaultDimension: 'size',
                dimensionDefaults: {
                    valueType: Number
                }
            });
    },
    
    _getTranslationClass: function(translOptions){
        return def
            .type(this.base(translOptions))
            .add(pvc.data.MetricPointChartTranslationOper);
    },
    
    _collectPlotAxesDataCells: function(plot, dataCellsByAxisTypeThenIndex){
        
        this.base(plot, dataCellsByAxisTypeThenIndex);
        
        /* NOTE: Cartesian axes are created even when hasMultiRole && !parent
         * because it is needed to read axis options in the root chart.
         * ??Also binding occurs to be able to know its scale type.?? 
         * Yet, their scales are not setup at the root level.
         */
        
        /* Configure Base Axis Data Cell */
        if(plot.type === 'scatter' && plot.option('DotsVisible')){
            
            var sizeRoleName = plot.option('SizeRole');
            if(sizeRoleName){
                var sizeRole = this.visualRole(sizeRoleName);
                if(sizeRole.isBound()){
                    var sizeDataCellsByAxisIndex = 
                        def
                        .array
                        .lazy(dataCellsByAxisTypeThenIndex, 'size');
                    
                    def
                    .array
                    .lazy(sizeDataCellsByAxisIndex, plot.option('SizeAxis') - 1)
                    .push({
                        plot:          plot,
                        role:          sizeRole,
                        dataPartValue: plot.option('DataPart')
                    });
                }
            }
        }
    },
    
    _setAxesScales: function(hasMultiRole){
        
        this.base(hasMultiRole);
        
        if(!hasMultiRole || this.parent){
            var sizeAxis = this.axes.size;
            if(sizeAxis && sizeAxis.isBound()){
                this._createAxisScale(sizeAxis);
            }
        }
    },
    
     /**
      * @override 
      */
    _createPlotPanels: function(parentPanel, baseOptions){
        // TODO: integrate these options in the MetricPointPlot or in the SizeAxis?
        var options = this.options;
        var panelOptions = def.set(
            Object.create(baseOptions),
            'sizeAxisRatio',        options.sizeAxisRatio,
            'sizeAxisRatioTo',      options.sizeAxisRatioTo,
            'autoPaddingByDotSize', options.autoPaddingByDotSize);
        
        var scatterPlot = this.plots.scatter;
            this.scatterChartPanel = // V1 property 
            new pvc.MetricPointPanel(this, parentPanel, scatterPlot, panelOptions);

        var trendPlot = this.plots.trend;
        if(trendPlot){
            new pvc.MetricPointPanel(
                this, 
                parentPanel, 
                trendPlot, 
                Object.create(panelOptions));
        }
    },
    
    defaults: {
        axisOriginIsZero: false,
        tooltipOffset: 10
    }
});

/**
 * Metric Dot Chart
 */
def
.type('pvc.MetricDotChart', pvc.MetricPointAbstract)
.add({
    _createPointPlot: function(){
        return new pvc.visual.MetricPointPlot(this, {
            fixed: {DotsVisible: true}
        });
    }
});

/**
 * Metric Line Chart
 */
def
.type('pvc.MetricLineChart', pvc.MetricPointAbstract)
.add({
    _createPointPlot: function(){
        return new pvc.visual.MetricPointPlot(this, {
            fixed: {LinesVisible: true}
        });
    }
});

/**
 * Bullet chart generation
 */
def
.type('pvc.BulletChart', pvc.BaseChart)
.init(function(options){
    
    options = options || {};

    // Add range and marker dimension group defaults
    // This only helps in default bindings...
    var dimGroups = options.dimensionGroups || (options.dimensionGroups = {});
    var rangeDimGroup = dimGroups.range  || (dimGroups.range  = {});
    if(rangeDimGroup.valueType === undefined){
        rangeDimGroup.valueType = Number;
    }

    var markerDimGroup = dimGroups.marker || (dimGroups.marker = {});
    if(markerDimGroup.valueType === undefined){
        markerDimGroup.valueType = Number;
    }

    this.base(options);
})
.add({

    bulletChartPanel : null,
    allowNoData: true,

    /**
     * @override 
     */
    _processOptionsCore: function(options){
        
        options.legend     = false;
        options.selectable = false; // not supported yet
        
        this.base(options);
    },
    
    /**
     * Initializes each chart's specific roles.
     * @override
     */
    _initVisualRoles: function(){

        this.base();

        this._addVisualRole('title',    { defaultDimension: 'title*'    });
        this._addVisualRole('subTitle', { defaultDimension: 'subTitle*' });
        this._addVisualRole('value', {
                //isRequired: true, // due to the no data mode
                isMeasure:  true,
                requireIsDiscrete: false,
                valueType: Number,
                defaultDimension: 'value*'
            });
        this._addVisualRole('marker', {
                isMeasure:  true,
                requireIsDiscrete: false,
                valueType: Number,
                defaultDimension: 'marker*'
            });
        this._addVisualRole('range', {
                isMeasure:  true,
                requireIsDiscrete: false,
                valueType: Number,
                defaultDimension: 'range*'
            });
    },

    _createTranslation: function(translOptions){
        
        var translation = this.base(translOptions),
            /*
             * By now the translation has already been initialized
             * and its virtualItemSize is determined.
             */
            size = translation.virtualItemSize()
            ;

        /* Configure the translation with default dimensions.
         *  1       Value
         *  2       Title | Value
         *  3       Title | Value | Marker
         *  >= 4    Title | Subtitle | Value | Marker | Ranges
         */
        // TODO: respect user reader definitions (names and indexes)
        // TODO: create a translator class for this, like with the boxplot?
        if(size){
            switch(size){
                case 1:
                    translation.defReader({names: 'value'});
                    break;

                case 2:
                    translation.defReader({names: ['title', 'value']});
                    break;

                case 3:
                    translation.defReader({names: ['title', 'value', 'marker']});
                    break;

                default:
                    translation.defReader({names: ['title', 'subTitle', 'value', 'marker']});
                    if(size > 4){
                        // 4, 5, 6, ...
                        translation.defReader({names: 'range', indexes: pv.range(4, size)});
                    }
                    break;
            }
        }

        return translation;
    },
    
    _initPlotsCore: function(hasMultiRole){
        new pvc.visual.BulletPlot(this);
    },
    
    _preRenderContent: function(contentOptions){
        var bulletPlot = this.plots.bullet;
        this.bulletChartPanel = new pvc.BulletChartPanel(
            this, 
            this.basePanel, 
            bulletPlot, 
            contentOptions);
    },
  
    defaults: {
        compatVersion: 1,
      
        orientation: 'horizontal',
        
        bulletSize:     30,  // Bullet size
        bulletSpacing:  50,  // Spacing between bullets
        bulletMargin:  100,  // Left margin

        // Defaults
//      bulletMarkers:  null,     // Array of markers to appear
//      bulletMeasures: null,     // Array of measures
//      bulletRanges:   null,     // Ranges
        bulletTitle:    "Title",  // Title
        bulletSubtitle: "",       // Subtitle
        bulletTitlePosition: "left", // Position of bullet title relative to bullet

//      axisDoubleClickAction: null,
        tooltipFormat: function(s, c, v) {
            return this.chart.options.valueFormat(v);
        },
        
        crosstabMode: false,
        seriesInRows: false
    }
});

/*
 * Bullet chart panel. Generates a bar chart. Specific options are:
 * <i>orientation</i> - horizontal or vertical. Default: vertical
 *
 * Has the following protovis extension points:
 *
 * <i>chart_</i> - for the main chart Panel
 * <i>bulletsPanel_</i> - for the bullets panel
 * <i>bulletPanel_</i> - for the bullets pv.Layout.Bullet
 * <i>bulletRange_</i> - for the bullet range
 * <i>bulletMeasure_</i> - for the bullet measure
 * <i>bulletMarker_</i> - for the marker
 * <i>bulletRule_</i> - for the axis rule
 * <i>bulletRuleLabel_</i> - for the axis rule label
 * <i>bulletTitle_</i> - for the bullet title
 * <i>bulletSubtitle_</i> - for the main bar label
 */


def
.type('pvc.BulletChartPanel', pvc.PlotPanel)
.add({
    pvBullets: null,
    pvBullet: null,
    data: null,
    onSelectionChange: null,
    
    /**
     * @override
     */
    _createCore: function(layoutInfo) {
        var chart   = this.chart,
            options = chart.options,
            data    = this.buildData();
    
        var anchor = options.orientation=="horizontal"?"left":"bottom";
        var size, angle, align, titleLeftOffset, titleTopOffset, ruleAnchor, leftPos, topPos, titleSpace;
    
        if(options.orientation=="horizontal"){
            size = layoutInfo.clientSize.width - this.chart.options.bulletMargin - 20;
            angle=0;
            switch (options.bulletTitlePosition) {
            case 'top':
                leftPos = this.chart.options.bulletMargin;
                titleLeftOffset = 0;
                align = 'left';
                titleTopOffset = -12;
                titleSpace = parseInt(options.titleSize/2, 10);
                break;
            case 'bottom':
                leftPos = this.chart.options.bulletMargin;
                titleLeftOffset = 0;
                align = 'left';
                titleTopOffset = options.bulletSize + 32;
                titleSpace = 0;
                break;
            case 'right':
                leftPos = 5;
                titleLeftOffset = size + 5;
                align = 'left';
                titleTopOffset = parseInt(options.bulletSize/2, 10);
                titleSpace = 0;
                break;
            case 'left':
                // The next comment is for JSHint
                /* falls through */
            default:
                leftPos = this.chart.options.bulletMargin;
                titleLeftOffset = 0;
                titleTopOffset = parseInt(options.bulletSize/2, 10);
                align = 'right';
                titleSpace = 0;
            }
            ruleAnchor = "bottom";
            topPos = function(){
                // TODO: 10
                return (this.index * (options.bulletSize + options.bulletSpacing)) + titleSpace;
            };
        } else {
            size = layoutInfo.clientSize.height - this.chart.options.bulletMargin - 20;
            switch (options.bulletTitlePosition) {
                case 'top':
                    leftPos = this.chart.options.bulletMargin;
                    titleLeftOffset = 0;
                    align = 'left';
                    titleTopOffset = -20;
                    angle = 0;
                    topPos = undefined;
                    break;
                case 'bottom':
                    leftPos = this.chart.options.bulletMargin;
                    titleLeftOffset = 0;
                    align = 'left';
                    titleTopOffset = size + 20;
                    angle = 0;
                    topPos = 20;
                    break;
                case 'right':
                    leftPos = 5;
                    titleLeftOffset = this.chart.options.bulletSize + 40;
                    align = 'left';
                    titleTopOffset = size;
                    angle = -Math.PI/2;
                    topPos = undefined;
                    break;
                case 'left':
                    // The next comment is for JSHint
                    /* falls through */
                default:
                    leftPos = this.chart.options.bulletMargin;
                    titleLeftOffset = -12;
                    titleTopOffset = this.height - this.chart.options.bulletMargin - 20;
                    align = 'left';
                    angle = -Math.PI/2;
                    topPos = undefined;
            }
            ruleAnchor = "right";
            leftPos = function(){
                return options.bulletMargin + this.index * (options.bulletSize + options.bulletSpacing);
            };

        }

        this.pvBullets = this.pvPanel.add(pv.Panel)
            .data(data)
            [pvc.BasePanel.orthogonalLength[anchor]](size)
            [pvc.BasePanel.parallelLength[anchor]](this.chart.options.bulletSize)
            .margin(20)
            .left(leftPos)
            .top(topPos);
        

        this.pvBullet = this.pvBullets.add(pv.Layout.Bullet)
            .orient  (anchor)
            .ranges  (function(d){ return d.ranges;   })
            .measures(function(d){ return d.measures; })
            .markers (function(d){ return d.markers;  });
    
        if (chart.clickable() && this.clickAction){
            var me = this;
      
            this.pvBullet
                .cursor("pointer")
                .event("click",function(d){
                    var s = d.title;
                    var c = d.subtitle;
                    var ev = pv.event;
                    return me.clickAction(s,c, d.measures, ev);
                });
        }
    
        this.pvBulletRange = this.pvBullet.range.add(pv.Bar);
        
        this.pvBulletMeasure = this.pvBullet.measure.add(pv.Bar)
            .text(function(v, d){
                return d.formattedMeasures[this.index];
            });

        this.pvBulletMarker = this.pvBullet.marker.add(pv.Dot)
            .shape("square")
            .fillStyle("white")
            .text(function(v, d){
                return d.formattedMarkers[this.index];
            });

        if(this.showsTooltip()) {
            // Extend default
            // TODO: how to deal with different measures in tooltips depending on mark
            var myself = this;
            this.pvBulletMeasure
                .localProperty('tooltip')
                .tooltip(function(v, d){
                    var s = d.title;
                    var c = d.subtitle;
                    return chart.options.tooltipFormat.call(myself,s,c,v);
                })
                ;

            this.pvBulletMarker
                .localProperty('tooltip')
                .tooltip(function(v, d){
                    var s = d.title;
                    var c = d.subtitle;
                    return chart.options.tooltipFormat.call(myself,s,c,v);
                })
                ;
      
            this.pvBulletMeasure.event("mouseover", pv.Behavior.tipsy(this.chart._tooltipOptions));
            this.pvBulletMarker .event("mouseover", pv.Behavior.tipsy(this.chart._tooltipOptions));
        }

        this.pvBulletRule = this.pvBullet.tick.add(pv.Rule);

        this.pvBulletRuleLabel = this.pvBulletRule.anchor(ruleAnchor).add(pv.Label)
            .text(this.pvBullet.x.tickFormat);

        this.pvBulletTitle = this.pvBullet.anchor(anchor).add(pv.Label)
            .font("bold 12px sans-serif")
            .textAngle(angle)
            .left(-10)
            .textAlign(align)
            .textBaseline("bottom")
            .left(titleLeftOffset)
            .top(titleTopOffset)
            .text(function(d){
                return d.formattedTitle;
            });

        this.pvBulletSubtitle = this.pvBullet.anchor(anchor).add(pv.Label)
            .textStyle("#666")
            .textAngle(angle)
            .textAlign(align)
            .textBaseline("top")
            .left(titleLeftOffset)
            .top(titleTopOffset)
            .text(function(d){
                return d.formattedSubtitle;
            });

        var doubleClickAction = (typeof(options.axisDoubleClickAction) == 'function') ?
                    function(d, e) {
                        //ignoreClicks = 2;
                        options.axisDoubleClickAction(d, e);
    
                    } : null;
    
        if (chart.doubleClickable() && doubleClickAction) {
            this.pvBulletTitle
                .cursor("pointer")
                .events('all')  //labels don't have events by default
                .event("dblclick", function(d){
                    doubleClickAction(d, arguments[arguments.length-1]);
                });

            this.pvBulletSubtitle
                .cursor("pointer")
                .events('all')  //labels don't have events by default
                .event("dblclick", function(d){
                    doubleClickAction(d, arguments[arguments.length-1]);
                });

        }
    },
   
    applyExtensions: function(){
      
        this.base();
      
        this.extend(this.pvBullets,"bulletsPanel");
        this.extend(this.pvBullet,"bulletPanel");
        this.extend(this.pvBulletRange,"bulletRange");
        this.extend(this.pvBulletMeasure,"bulletMeasure");
        this.extend(this.pvBulletMarker,"bulletMarker");
        this.extend(this.pvBulletRule,"bulletRule");
        this.extend(this.pvBulletRuleLabel,"bulletRuleLabel");
        this.extend(this.pvBulletTitle,"bulletTitle");
        this.extend(this.pvBulletSubtitle,"bulletSubtitle");
    },
    
    _getExtensionId: function(){
        // content coincides, visually in this chart type
        return [{abs: 'content'}].concat(this.base());
    },
    
    /*
     * Data array to back up bullet charts.
     */
    buildData: function(){
        var data,
            chart = this.chart,
            options = chart.options,
            
            titleRole = chart.visualRoles.title,
            titleGrouping = titleRole.grouping,
            
            subTitleRole = chart.visualRoles.subTitle,
            subTitleGrouping = subTitleRole.grouping,
            
            valueRole = chart.visualRoles.value,
            valueGrouping = valueRole.grouping,
            
            markerRole = chart.visualRoles.marker,
            markerGrouping = markerRole.grouping,
            
            rangeRole = chart.visualRoles.range,
            rangeGrouping = rangeRole.grouping;
        
        var defaultData = {
            title:             options.bulletTitle,
            formattedTitle:    options.bulletTitle,
            
            subtitle:          options.bulletSubtitle,
            formattedSubtitle: options.bulletSubtitle,
            
            ranges:            def.array.to(options.bulletRanges)   || [],
            measures:          def.array.to(options.bulletMeasures) || [],
            markers:           def.array.to(options.bulletMarkers)  || []
        };
        
        def.set(defaultData,
            'formattedRanges',   defaultData.ranges  .map(String),
            'formattedMeasures', defaultData.measures.map(String),
            'formattedMarkers',  defaultData.markers .map(String)
            );
        
        if(!valueGrouping    &&
           !titleGrouping    &&
           !markerGrouping   &&
           !subTitleGrouping &&
           !rangeGrouping){

            data = [defaultData];
       } else {
            data = chart.data.datums().select(function(datum){
                var d = Object.create(defaultData),
                    view;

                if(valueGrouping){
                    view = valueGrouping.view(datum);
                    d.measures = view.values();
                    d.formattedMeasures = view.labels();
                }

                if(titleGrouping){
                    view = titleGrouping.view(datum);
                    d.title = view.value;
                    d.formattedTitle = view.label;
                }

                if(subTitleGrouping){
                    view = subTitleGrouping.view(datum);
                    d.subtitle = view.value;
                    d.formattedSubtitle = view.label;
                }

                if(markerGrouping){
                    view = markerGrouping.view(datum);
                    d.markers = view.values();
                    d.formattedMarkers = view.labels();
                }

                if(rangeGrouping){
                    view = rangeGrouping.view(datum);
                    d.ranges = view.values();
                    d.formattedRanges = view.labels();
                }

                return d;
            }, this)
            .array();
        }
        
        return data;
    }
});


/**
 * Parallel coordinates offer a way to visualize data and make (sub-)selections
 * on this dataset.
 * This code has been based on a protovis example:
 *    http://vis.stanford.edu/protovis/ex/cars.html
 */
def
.type('pvc.ParallelCoordinates', pvc.BaseChart)
.init(function(options){

    // Force the value dimension not to be a number
    options = options || {};
    options.dimensions = options.dimensions || {};
    if(!options.dimensions.value) {
        options.dimensions.value = {valueType: null};
    }
  
    this.base(options);
})
.add({

    parCoordPanel : null,
    
    _preRenderContent: function(contentOptions){
        this.parCoordPanel = new pvc.ParCoordPanel(this, this.basePanel, def.create(contentOptions, {
            topRuleOffset : this.options.topRuleOffset,
            botRuleOffset : this.options.botRuleOffset,
            leftRuleOffset : this.options.leftRuleOffset,
            rightRuleOffset : this.options.rightRuleOffset,
            sortCategorical : this.options.sortCategorical,
            mapAllDimensions : this.options.mapAllDimensions,
            numDigits : this.options.numDigits
        }));
    },
  
    defaults: def.create(pvc.BaseChart.prototype.defaults, {
        compatVersion: 1,
      
        topRuleOffset: 30,
        botRuleOffset: 30,
        leftRuleOffset: 60,
        rightRuleOffset: 60,
        // sort the categorical (non-numerical dimensions)
        sortCategorical: true,
        // map numerical dimension too (uniform (possible non-linear)
        // distribution of the observed values)
        mapAllDimensions: true,
        // number of digits after decimal point.
        numDigits: 0
    })
});

/*
 * ParCoord chart panel. Generates a serie of Parallel Coordinate axis 
 * and allows you too make selections on these parallel coordinates.
 * The selection will be stored in java-script variables and can be
 * used as part of a where-clause in a parameterized SQL statement.
 * Specific options are:
 *   << to be filled in >>

 * Has the following protovis extension points:
 *
 * <i>chart_</i> - for the main chart Panel
 * <i>parCoord_</i> - for the parallel coordinates
 *    << to be completed >>
 */
def
.type('pvc.ParCoordPanel', pvc.BasePanel)
.add({
    anchor: 'fill',
    pvParCoord: null,

    dimensions: null, 
    dimensionDescr: null,

    data: null,

    /*****
     * retrieve the data from database and transform it to maps.
     *    - this.dimensions: all dimensions
     *    - this.dimensionDescr: description of dimensions
     *    - this.data: array with hashmap per data-point
     *****/
    retrieveData: function () {
        var data = this.chart.data;
        var numDigit = this.chart.options.numDigits;

        this.dimensions = data.getVisibleCategories();
        var values = data.getValues();

        var dataRowIndex = data.getVisibleSeriesIndexes();
        var pCoordIndex = data.getVisibleCategoriesIndexes();

        var pCoordKeys = data.getCategories();

        /******
         *  Generate a Coordinate mapping. 
         *  This mapping is required for categorical dimensions and
         *  optional for the numerical dimensions (in 4 steps)
         ********/
        // 1: generate an array of coorMapping-functions
        // BEWARE: Only the first row (index 0) is used to test whether 
        // a dimension is categorical or numerical!
        var pCoordMapping = this.chart.options.mapAllDimensions ?
            pCoordIndex.map(function(d) {
                return isNaN(values[d][0]) ? 
                    {categorical: true,  len: 0, map: [] } : 
                    {categorical: false, len: 0, map: [], displayValue: [] }; 
            }) : 
            pCoordIndex.map(function(d) {
                return isNaN(values[d][0]) ? 
                    {categorical: true, len: 0, map: [] } : 
                    null; 
            })
            ;
  
        // 2: and generate a helper-function to update the mapping
        // For non-categorical value the original-value is store in displayValue
        var coordMapUpdate = function(i, val) {
            var cMap = pCoordMapping[i];
            var k = null; // define in outer scope.
            if (!cMap.categorical) {
                var keyVal = val.toFixed(numDigit);   // force the number to be a string
                k = cMap.map[keyVal];
                if (k == null) {
                    k = cMap.len;
                    cMap.len++;
                    cMap.map[keyVal] = k;
                    cMap.displayValue[keyVal] = val;
                }
            } else {
                k = cMap.map[val];
                if (k == null) {
                    k = cMap.len;
                    cMap.len++;
                    cMap.map[val] = k;
                }
            }
            return k;
        };

        // 3. determine the value to be displayed
        //   for the categorical dimensions map == displayValue
        for(var d in pCoordMapping){
            if (pCoordMapping.hasOwnProperty(d) && 
                pCoordMapping[d] && 
                pCoordMapping[d].categorical) {
                pCoordMapping[d].displayValue = pCoordMapping[d].map;
            }
        }
    
        var i, item, k;
    
        // 4. apply the sorting of the dimension
        if (this.chart.options.sortCategorical || 
            this.chart.options.mapAllDimensions) {
            // prefill the coordMapping in order to get it in sorted order.
            // sorting is required if all dimensions are mapped!!
            for (i=0; i<pCoordMapping.length; i++) {
                if (pCoordMapping[i]) {
                    // add all data
                    for (var col=0; col<values[i].length; col++) {
                        coordMapUpdate(i, values[i][col]);
                    }
           
                    // create a sorted array
                    var cMap = pCoordMapping[i].map;
                    var sorted = [];
                    for(item in cMap){
                        if(cMap.hasOwnProperty(item)){
                            sorted.push(item);
                        }
                    }
                    sorted.sort();
                    // and assign a new index to all items
                    if (pCoordMapping[i].categorical){
                        for(k=0; k<sorted.length; k++){
                            cMap[sorted[k]] = k;
                        }
                    } else {
                        for(k=0; k<sorted.length; k++) {
                            cMap[sorted[k]].index = k;
                        }
                    }
                }      
            }
        }

        /*************
         *  Generate the full dataset (using the coordinate mapping).
         *  (in 2 steps)
         ******/
        //   1. generate helper-function to transform a data-row to a hashMap
        //   (key-value pairs). 
        //   closure uses pCoordKeys and values
        var generateHashMap = function(col) {
            var record = {};
            for(var j in pCoordIndex) {
                if(pCoordIndex.hasOwnProperty(j)){
                    record[pCoordKeys[j]] = (pCoordMapping[j]) ?
                            coordMapUpdate(j, values[j][col]) :
                                values[j][col];
                }
            }
            return record;
        };
    
        // 2. generate array with a hashmap per data-point
        this.data = dataRowIndex.map(function(col) { return generateHashMap (col); });
    
        /*************
         *  Generate an array of descriptors for the dimensions (in 3 steps).
         ******/
        // 1. find the dimensions
        var descrVals = this.dimensions.map(function(cat){
            var item2 = {};
            // the part after "__" is assumed to be the units
            var elements = cat.split("__");
            item2.id = cat;
            item2.name = elements[0];
            item2.unit = (elements.length >1)? elements[1] : "";
            return item2;
        });

        // 2. compute the min, max and step(-size) per dimension)
        for(i=0; i<descrVals.length; i++) {
            item = descrVals[i];
            var index = pCoordIndex[i];
            // orgRowIndex is the index in the original dataset
            // some indices might be (non-existent/invisible)
            item.orgRowIndex = index;

            // determine min, max and estimate step-size
            var len = values[index].length;
            var theMin, theMax, theMin2, theMax2;
            var v;
      
            // two version of the same code (one with mapping and one without)
            if (pCoordMapping[index]) {
                theMin = theMax = theMin2 = theMax2 =
                pCoordMapping[index].displayValue[ values[index][0] ];

                for(k=1; k<len; k++) {
                    v = pCoordMapping[index].displayValue[ values[index][k] ] ;
                    if (v < theMin){
                        theMin2 = theMin;
                        theMin = v;
                    }
                    
                    if (v > theMax) {
                        theMax2 = theMax;
                        theMax = v;
                    }
                }
            } else {  // no coordinate mapping applied
                theMin = theMax = theMin2 = theMax2 = values[index][0];

                for(k=1; k<len; k++) {
                    v = values[index][k];
                    if (v < theMin) {
                        theMin2 = theMin;
                        theMin = v;
                    }
                    
                    if (v > theMax) {
                        theMax2 = theMax;
                        theMax = v;
                    }
                }
            }   // end else:  coordinate mapping applied

            var theStep = ((theMax - theMax2) + (theMin2-theMin))/2;
            item.min = theMin;
            item.max = theMax;
            item.step = theStep;

            // 3. and include the mapping (and reverse mapping) 
            item.categorical = false; 
            if (pCoordMapping[index]) {
                item.map = pCoordMapping[index].map;
                item.mapLength = pCoordMapping[index].len;
                item.categorical = pCoordMapping[index].categorical; 

                // create the reverse-mapping from key to original value
                if (!item.categorical) {
                    item.orgValue = [];
                    var theMap =  pCoordMapping[index].map;
                    for (var key in theMap){
                        if(theMap.hasOwnProperty(key)){
                            item.orgValue[ theMap[key] ] = 0.0+key;
                        }
                    }
                }
            }
        }

        // generate a object using the given set of keys and values
        //  (map from keys[i] to vals[i])
        var genKeyVal = function (keys, vals) {
            var record = {};
            for (var i = 0; i<keys.length; i++){
                record[keys[i]] = vals[i];
            }
            return record;
        };
        this.dimensionDescr = genKeyVal(this.dimensions, descrVals);
    },

    _createCore: function(){

        var myself = this;

        this.retrieveData();

        // used in the different closures
        var height = this.height,
            numDigits = this.chart.options.numDigits,
            topRuleOffs = this.chart.options.topRuleOffset,
            botRuleOffs = this.chart.options.botRuleOffset,
            leftRuleOffs = this.chart.options.leftRuleOffset,
            rightRulePos = this.width - this.chart.options.rightRuleOffset,
            topRulePos = this.height- topRuleOffs,
            ruleHeight = topRulePos - botRuleOffs,
            labelTopOffs = topRuleOffs - 12,
            // use dims to get the elements of dimDescr in the appropriate order!!
            dims = this.dimensions,
            dimDescr = this.dimensionDescr;

        /*****
         *   Generate the scales x, y and color
         *******/
        // getDimSc is the basis for getDimensionScale and getDimColorScale
        var getDimSc = function(t, addMargin) {
            var theMin = dimDescr[t].min;
            var theMax = dimDescr[t].max;
            var theStep = dimDescr[t].step;
            // add some margin at top and bottom (based on step)
            if (addMargin) {
                theMin -= theStep;
                theMax += theStep;
            }
            
            return pv.Scale.linear(theMin, theMax)
                .range(botRuleOffs, topRulePos);
        };
        
        var getDimensionScale = function(t) {
            var scale = getDimSc(t, true)
                .range(botRuleOffs, topRulePos);
            var dd = dimDescr[t];
            if (dd.orgValue && !dd.categorical) {
                // map the value to the original value
                var func = function(x) { 
                    var res = scale( dd.orgValue[x]);
                    return res; 
                };
        
                // wire domain() and invert() to the original scale
                func.domain  = function() { return scale.domain();  };
                func.invert = function(d) { return scale.invert(d); };
                return func;
            }
      
            return scale;
        };
        
        var getDimColorScale = function(t) {
            var scale = getDimSc(t, false)
                .range("steelblue", "brown");
            return scale;
        }; 

        var x = pv.Scale.ordinal(dims).splitFlush(leftRuleOffs, rightRulePos);
        var y = pv.dict(dims, getDimensionScale);
        var colors = pv.dict(dims, getDimColorScale);

        /*****
         *   Generate tools for computing selections.
         *******/
        // Interaction state. 
        var filter = pv.dict(dims, function(t) {
            return {min: y[t].domain()[0], max: y[t].domain()[1]};  });
        
        var active = dims[0];   // choose the active dimension 

        var selectVisible = this.chart.options.mapAllDimensions ?
            function(d) { 
                return dims.every(  
                   // all dimension are handled via a mapping.
                    function(t) {
                        var dd = dimDescr[t];
                        var val = (dd.orgValue && !dd.categorical) ?
                                  dd.orgValue[d[t]] : 
                                  d[t];
                        return (val >= filter[t].min) && (val <= filter[t].max); 
                    });
             } : 
             function(d) { 
                 return dims.every(function(t) {
                     // TO DO: check whether this operates correctly for
                     // categorical dimensions  (when mapAllDimensions == false
                     return (d[t] >= filter[t].min) && (d[t] <= filter[t].max); 
                 });
             }
             ;
 
        /*****
         *   generateLinePattern produces a line pattern based on
         *          1. the current dataset.
         *          2. the current filter settings.
         *          3. the provided colorMethod.
         *  The result is an array where each element contains at least
         *            {x1, y1, x2, y2, color}
         *  Two auxiliary fields are 
         *  Furthermore auxiliary functions are provided
         *     - colorFuncFreq
         *     - colorFuncActive
         *******/
         var auxData = null;
      
         /*****
          *   Draw the chart and its annotations (except dynamic content)
          *  ******/
         // Draw the data to the parallel dimensions 
         // (the light grey dataset is a fixed background)
         this.pvParCoord = this.pvPanel.add(pv.Panel)
             .data(myself.data)
             .visible(selectVisible)
             .add(pv.Line)
             .data(dims)
             .left(function(t, d) { return x(t); } )
             .bottom(function(t, d) { 
                 var res = y[t] (d[t]);
                 return res; 
             })
             .strokeStyle("#ddd")
             .lineWidth(1)
             .antialias(false)
             ;

         // Rule per dimension.
         var rule = this.pvPanel.add(pv.Rule)
             .data(dims)
             .left(x)
             .top(topRuleOffs)
             .bottom(botRuleOffs)
             ;

         // Dimension label
         rule.anchor("top").add(pv.Label)
             .top(labelTopOffs)
             .font("bold 10px sans-serif")
             .text(function(d) { return dimDescr[d].name; })
             ;

         // add labels on the categorical dimension
         //  compute the array of labels
         var labels = [];
         var labelXoffs = 6,
         labelYoffs = 3;
         for(var d in dimDescr) {
             if(dimDescr.hasOwnProperty(d)){
                 var dim = dimDescr[d];
                 if (dim.categorical) {
                     var  xVal = x(dim.id) + labelXoffs;
                     for (var l in dim.map){
                         if(dim.map.hasOwnProperty(l)){
                             labels[labels.length] = {
                                     x:  xVal,
                                     y:  y[dim.id](dim.map[l]) + labelYoffs,
                                     label: l
                             };
                         }
                     }
                 }
             }
         }
         
         var dimLabels = this.pvPanel.add(pv.Panel)
             .data(labels)
             .add(pv.Label)
             .left(function(d) {return d.x;})
             .bottom(function(d) { return d.y;})
             .text(function(d) { return d.label;})
             .textAlign("left")
             ;
    
      
         /*****
          *   Add an additional panel over the top for the dynamic content
          *    (and draw the (full) dataset)
          *******/
         // Draw the selected (changeable) data on a new panel on top
         var change = this.pvPanel.add(pv.Panel);
         var line = change.add(pv.Panel)
             .data(myself.data)
             .visible(selectVisible)
             .add(pv.Line)
             .data(dims)
             .left(function(t, d) { return x(t);})
             .bottom(function(t, d) { return y[t](d[t]); })
             .strokeStyle(function(t, d) { 
                 var dd = dimDescr[active];
                 var val = (dd.orgValue && !dd.categorical) ?
                           dd.orgValue[ d[active] ] :
                           d[active]
                           ;
                 return colors[active](val);
             })
             .lineWidth(1)
             ;

         /*****
          *   Add the user-interaction (mouse-interface)
          *   and the (dynamic) labels of the selection.
          *******/

         // Updater for slider and resizer.
         function update(d) {
             var t = d.dim;
             filter[t].min = Math.max(y[t].domain()[0], y[t].invert(height - d.y - d.dy));
             filter[t].max = Math.min(y[t].domain()[1], y[t].invert(height - d.y));
             active = t;
             change.render();
             return false;
         }

         // Updater for slider and resizer.
         function selectAll(d) {
             if (d.dy < 3) {  // 
                 var t = d.dim;
                 filter[t].min = Math.max(y[t].domain()[0], y[t].invert(0));
                 filter[t].max = Math.min(y[t].domain()[1], y[t].invert(height));
                 d.y = botRuleOffs; d.dy = ruleHeight;
                 active = t;
                 change.render();
             }
             return false;
         }
       
         // Handle select and drag 
         var handle = change.add(pv.Panel)
             .data(dims.map(function(dim) { return {y:botRuleOffs, dy:ruleHeight, dim:dim}; }))
             .left(function(t) { return x(t.dim) - 30; })
             .width(60)
             .fillStyle("rgba(0,0,0,.001)")
             .cursor("crosshair")
             .event("mousedown", pv.Behavior.select())
             .event("select", update)
             .event("selectend", selectAll)
             .add(pv.Bar)
             .left(25)
             .top(function(d) {return d.y;})
             .width(10)
             .height(function(d) { return d.dy;})
             .fillStyle(function(t) { 
                 return (t.dim == active) ? 
                        colors[t.dim]((filter[t.dim].max + filter[t.dim].min) / 2) : 
                        "hsla(0,0,50%,.5)";
             })
             .strokeStyle("white")
             .cursor("move")
             .event("mousedown", pv.Behavior.drag())
             .event("dragstart", update)
             .event("drag", update)
             ;

         handle.anchor("bottom").add(pv.Label)
             .textBaseline("top")
             .text(function(d) { 
                 return (dimDescr[d.dim].categorical) ?
                        "" :
                        filter[d.dim].min.toFixed(numDigits) + dimDescr[d.dim].unit
                        ;
             })
             ;

         handle.anchor("top").add(pv.Label)
             .textBaseline("bottom")
             .text(function(d) {
                 return (dimDescr[d.dim].categorical) ?
                        "" :
                        filter[d.dim].max.toFixed(numDigits) + dimDescr[d.dim].unit;
             })
             ;


         /*****
          *  add the extension points
          *******/

         // Extend ParallelCoordinates
         this.extend(this.pvParCoord,"parCoord");
         // the parCoord panel is the base-panel (not the colored dynamic overlay)

         // Extend body
         this.extend(this.pvPanel,"chart");
    }
});

/**
 * DataTree visualises a data-tree (also called driver tree).
 * It uses a data-sources to obtain the definition of data tree.
 * Each node of the tree can have it's own datasource to visualize the
 * node. 
 */
def
.type('pvc.DataTree', pvc.BaseChart)
.init(function(options){
    // Force the value dimension not to be a number
    options = options || {};
    options.dimensionGroups = options.dimensionGroups || {};
    if(!options.dimensionGroups.value) {
        options.dimensionGroups.value = {valueType: null};
    }
    
    this.base(options);
})
.add({

    // the structure of the dataTree is provided by a separate datasource
    structEngine:   null,
    structMetadata: null,
    structDataset:  null,

    DataTreePanel : null,
    
    _getColorRoleSpec: function(){
        return { isRequired: true, defaultSourceRole: 'category', requireIsDiscrete: true };
    },
    
    setStructData: function(data){
        this.structDataset = data.resultset;
        if (!this.structDataset.length){
            this._log("Warning: Structure-dataset is empty");
        }

        this.structMetadata = data.metadata;
        if (!this.structMetadata.length){
            this._log("Warning: Structure-Metadata is empty");
        }
    },
  
    _preRenderContent: function(contentOptions){
        // Create DataEngine
        var structEngine  = this.structEngine;
        var structType    = structEngine ? structEngine.type : new pvc.data.ComplexType();
        // Force the value dimension not to be a number
        structType.addDimension('value', {});
        
        var translOptions = {
            seriesInRows: true,
            crosstabMode: true
        };
        
        var translation = new pvc.data.CrosstabTranslationOper(structType, this.structDataset, this.structMetadata, translOptions);
        translation.configureType();
        if(!structEngine) {
            structEngine = this.structEngine = new pvc.data.Data({type: structType});
        }
        
        structEngine.load(translation.execute(structEngine));

        if(pvc.debug >= 3){
            this._log(this.structEngine.getInfo());
        }

        // ------------------
        
        this.dataTreePanel = new pvc.DataTreePanel(this, this.basePanel, def.create(contentOptions, {
            topRuleOffset : this.options.topRuleOffset,
            botRuleOffset : this.options.botRuleOffset,
            leftRuleOffset : this.options.leftRuleOffset,
            rightRuleOffset : this.options.rightRuleOffset,
            boxplotColor:  this.options.boxplotColor,
            valueFontsize: this.options.valueFontsize,
            headerFontsize: this.options.headerFontsize,
            border: this.options.border,
            perpConnector: this.options.perpConnector,
            numDigits: this.options.numDigits,
            minVerticalSpace: this.options.minVerticalSpace,
            connectorSpace: this.options.connectorSpace,
            minAspectRatio: this.options.minAspectRatio
        }));
    },
    
    defaults: {
        compatVersion: 1,
        
     // margins around the full tree
        topRuleOffset: 30,
        botRuleOffset: 30,
        leftRuleOffset: 60,
        rightRuleOffset: 60,
        // box related parameters
        boxplotColor: "grey",
        headerFontsize: 16,
        valueFontsize: 20,
        border:  2,     // bordersize in pixels
        // use perpendicular connector lines  between boxes.
        perpConnector: false,
        // number of digits (after dot for labels)
        numDigits: 0,
        // the space for the connectors is 15% of the width of a grid cell
        connectorSpace: 0.15,
        // the vertical space between gridcells is at least 5%
        minVerticalSpace: 0.05,
        // aspect ratio = width/height  (used to limit AR of the boxes)
        minAspectRatio: 2.0

        //selectParam: undefined
    }
});

/*
 * DataTree chart panel. 
 *   << to be filled out >>
 *
 * Has the following protovis extension points:
 *
 * <i>chart_</i> - for the main chart Panel
 *    << to be filled out >>
 */
def
.type('pvc.DataTreePanel', pvc.PlotPanel)
.add({
  
  pvDataTree: null,

  treeElements: null, 

  structMap: null,
  structArr: null,

  hRules: null,
  vRules: null,
  rules: null,

  // generating Perpendicular connectors 
  // (only using horizontal and vertical rules)
  // leftLength gives the distance from the left box to the
  // splitting point of the connector
  generatePerpConnectors: function(leftLength) {

    this.hRules = [];
    this.vRules = [];
    this.rules  = [];  // also initialize this rule-set

    for(var e in this.structMap) {
      var elem = this.structMap[e];
      if (elem.children != null) {
        var min = +10000, max = -10000;
        var theLeft = elem.left + elem.width;
        this.hRules.push({"left": theLeft,
                    "width": leftLength,
                    "bottom": elem.bottom + elem.height/2});
        theLeft += leftLength;
        for(var i in elem.children) {
          var child = this.structMap[ elem.children[i] ];
          var theBottom = child.bottom + child.height/2;
          if (theBottom > max) { max = theBottom; }
          if (theBottom < min) { min = theBottom; }
          this.hRules.push({"left": theLeft,
                      "width": child.left - theLeft,
                      "bottom": theBottom});
        }

        // a vertical rule is only added when needed
        if (max > min) {
          this.vRules.push({"left": theLeft,
                      "bottom": min,
                      "height": max - min});
        }
      }
    }
  },

  // generate a line segment and add it to rules
  generateLineSegment: function(x1, y1, x2, y2) {
    var line = [];
    line.push({"x":  x1,
               "y":  y1});
    line.push({"x":  x2,
               "y":  y2});
    this.rules.push(line);
  },

  // leftLength gives the distance from the left box to the
  // splitting point of the connector
  generateConnectors: function(leftLength) {

    this.hRules = [];
    this.vRules = [];

    if (this.chart.options.perpConnector) {
      this.generatePerpConnectors(leftLength);
      return;
    }

    // this time were using diagonal rules
    this.rules = [];

    for(var e in this.structMap) {
      var elem = this.structMap[e];
      if (elem.children != null) {
        var theCenter, child, i;
        
        // compute the mid-point
        var min = +10000, max = -10000;
        for(i in elem.children) {
          child = this.structMap[ elem.children[i] ];
          theCenter = child.bottom + child.height/2;
          if (theCenter > max) { max = theCenter; }
          if (theCenter < min) { min = theCenter; }
        }
        var mid = (max + min)/2;

        var theLeft1 = elem.left + elem.width;
        var theLeft2 = theLeft1 + leftLength;

        // outbound line of the left-hand box
        this.generateLineSegment(theLeft1, elem.bottom + elem.height/2,
                                theLeft2, mid);

        // incoming lines of the right-hand boxes
        for(i in elem.children) {
          child = this.structMap[ elem.children[i] ];
          theCenter = child.bottom + child.height/2;

          this.generateLineSegment(theLeft2, mid,
                                   child.left, theCenter);
        }
      }
    }
  },

  retrieveStructure: function () {
    var data = this.chart.structEngine;
    var options = this.chart.options;

    var colLabels = data.getVisibleCategories();
    this.treeElements = data.getVisibleSeries();
    var values = data.getValues();

    // if a fifth column is added, then
    //  bottom and height are provided in the dataset.
    var bottomHeightSpecified = (colLabels.length > 4);
    
    var e;
    
    // trim al element labels (to allow for matching without spaces)
    for(e in this.treeElements) { 
      this.treeElements[e] = $.trim(this.treeElements[e]);
    }

    // get the bounds (minimal and maximum column and row indices)
    // first a bounds object with two helper-functions is introduced
    var bounds = [];
    bounds.getElement = function(label) {
      // create the element if it does not exist
      if (bounds[label] == null){
        bounds[label] = {"min": +10000, "max": -10000};
      }
      return bounds[label];
    };
    
    bounds.addValue = function(label, value) {
      var bnd = bounds.getElement(label);
      if (value < bnd.min){
        bnd.min = value;
      }
      if (value > bnd.max){
        bnd.max = value;
      }
      return bnd;
    };
    
    var col, colnr, elem, row;
    for(e in this.treeElements) {
      elem = this.treeElements[e];
      col = elem[0];
      colnr = col.charCodeAt(0);
      row = parseInt(elem.slice(1), 10);
      bounds.addValue("__cols", colnr);
      bounds.addValue(col,row);
    }

    // determine parameters to find column-bounds    
    var bnds = bounds.getElement("__cols");
    var gridWidth  = this.innerWidth/(bnds.max - bnds.min + 1); // integer
    var connectorWidth = options.connectorSpace * gridWidth;
    var cellWidth = gridWidth - connectorWidth;
    var maxCellHeight = cellWidth/options.minAspectRatio;
    var colBase = bnds.min;
    delete bounds.__cols;

    // compute additional values for each column
    for (e in bounds) {
      bnds = bounds[e];
      
      if (typeof bnds == "function"){
        continue;
      }
      var numRows = bnds.max - bnds.min + 1;

      bnds.gridHeight = this.innerHeight/numRows;
      bnds.cellHeight = bnds.gridHeight*(1.0 - options.minVerticalSpace);
      if (bnds.cellHeight > maxCellHeight){
        bnds.cellHeight = maxCellHeight;
      }
      bnds.relBottom = (bnds.gridHeight - bnds.cellHeight)/2;
      bnds.numRows = numRows;
    }

    // generate the elements
    var whitespaceQuote = new RegExp ('[\\s\"\']+',"g"); 
    this.structMap = {};
    for(e in this.treeElements) {
      var box = {};
      elem = this.treeElements[e];
      box.box_id = elem;
      this.structMap[elem] = box;

      col = elem[0];
      colnr = col.charCodeAt(0);
      row = parseInt(elem.slice(1), 10);
      bnds = bounds.getElement(col);

      box.colIndex = colnr - colBase;
      box.rowIndex = bnds.numRows - (row - bnds.min) - 1;

      box.left = this.leftOffs + box.colIndex * gridWidth;
      box.width = cellWidth;
      if (bottomHeightSpecified) {
          box.bottom = values[4][e];
          box.height = values[5][e];
      } else {
          box.bottom = this.botOffs + box.rowIndex * bnds.gridHeight + bnds.relBottom;
          box.height = bnds.cellHeight;
      }
      
      box.label = values[0][e];
      box.selector = values[1][e];
      box.aggregation = values[2][e];
      
      var children = (values[3][e] || '').replace(whitespaceQuote, " ");
      
      box.children = (children === " " || children ===  "") ?
         null : children.split(" ");
    }

    this.generateConnectors((gridWidth - cellWidth)/2);

    // translate the map to an array (needed by protovis)
    this.structArr = [];
    for(e in this.structMap) {
      elem = this.structMap[e];
      this.structArr.push(elem);
    }
  },

  findDataValue: function(key, data) {
    for(var i=0; i < data[0].length; i++) {
      if (data[0][ i ] == key) {
        return data[1][ i ];
      }
    }
    
    this._log("Error: value with key : "+key+" not found.");
  },

  generateBoxPlots: function() {
    var options = this.chart.options;

    for(var e in this.structArr) {
      var elem = this.structArr[e];
      if (!elem.values.length) {
        continue;
      }
      
      elem.subplot = {};
      var sp = elem.subplot;

      // order the data elements from 5% bound to 95% bound
      // and determine the horizontal scale
      var dat = [];
      var margin = 15;
      var rlMargin = elem.width/6;

      // generate empty rule sets (existing sets are overwritten !)
      sp.hRules = [];
      sp.vRules = [];
      sp.marks = [];
      sp.labels = [];

      dat.push(this.findDataValue("_p5", elem.values));
      dat.push(this.findDataValue("_p25", elem.values));
      dat.push(this.findDataValue("_p50", elem.values));
      dat.push(this.findDataValue("_p75", elem.values));
      dat.push(this.findDataValue("_p95", elem.values));

      var noBox = false;

	if (typeof(dat[2]) != "undefined") {
        // switch order (assume computational artifact)
        if (dat[4] < dat[0]) {
          dat = dat.reverse();
          this._log(" dataset "+ elem.box_id +
                  " repaired (_p95 was smaller than _p5)");
          }
        if (dat[4] > dat[0]) {
          sp.hScale = pv.Scale.linear( dat[0], dat[4]);
        } else {
          noBox = true;
          // generate a fake scale centered around dat[0] (== dat[4])
          sp.hScale = pv.Scale.linear( dat[0] - 1e-10, dat[0] + 1e-10);
        }
        sp.hScale.range(elem.left + rlMargin, elem.left + elem.width - rlMargin);
        var avLabel = "" + dat[2];   // prepare the label
        
        var i;
        
        for(i=0; i< dat.length; i++) {
            dat[i] = sp.hScale( dat[i]);
        }

        sp.bot = elem.bottom + elem.height / 3;
        sp.top = elem.bottom + 2 * elem.height / 3;
        sp.mid = (sp.top + sp.bot) / 2;   // 2/3 of height
        sp.textBottom = elem.bottom + margin;
        sp.textBottom = sp.bot - options.valueFontsize - 1;

        // and add the new set of rules for a box-plot.
        var lwa = 3;   // constant for "lineWidth Average"
        if (noBox) {
            sp.vRules.push({"left": dat[0],
                          "bottom": sp.bot,
                          "lWidth": lwa,
                          "height": sp.top - sp.bot});
        } else {
          sp.hRules.push({"left": dat[0],
                        "width":  dat[1] - dat[0],
                        "lWidth": 1,
                        "bottom": sp.mid});
          sp.hRules.push({"left": dat[1],
                        "width":  dat[3] - dat[1],
                        "lWidth": 1,
                        "bottom": sp.bot});
          sp.hRules.push({"left": dat[1],
                        "width":  dat[3] - dat[1],
                        "lWidth": 1,
                        "bottom": sp.top});
          sp.hRules.push({"left": dat[3],
                        "width":  dat[4] - dat[3],
                        "lWidth": 1,
                        "bottom": sp.mid});
          for(i=0; i<dat.length; i++) {
            sp.vRules.push({"left": dat[i],
                          "bottom": sp.bot,
                          "lWidth": (i == 2) ? lwa : 1,
                          "height": sp.top - sp.bot});
          }
        }

        sp.labels.push({left: dat[2],
                      bottom: sp.textBottom,
                      text: this.labelFixedDigits(avLabel),
                      size: options.smValueFont,
                      color: options.boxplotColor});
    }
    }
  } ,

  labelFixedDigits: function(value) {
    if (typeof value == "string") {
        value = parseFloat(value);
    }

    if (typeof value == "number") {
      var nd = this.chart.options.numDigits;

      value = value.toFixed(nd);
    }

    // translate to a string again
    return "" + value;
  } ,

  addDataPoint: function(key) {
    var options = this.chart.options;

    for(var e in this.structArr) {
      var elem = this.structArr[e];

      if (!elem.values.length) {
        continue;
      }
      
      var value = this.findDataValue(key, elem.values);
      if (typeof value == "undefined") {
        continue;
      }
      
      var sp = elem.subplot;
      var theLeft = sp.hScale(value); 

      var theColor = "green";
      sp.marks.push( {
        left: theLeft,
        bottom: sp.mid,
        color: theColor });
      
      sp.labels.push({left: theLeft,
                      bottom: sp.textBottom,
                      text: this.labelFixedDigits(value),
                      size: options.valueFont,
                      color: theColor});
    }
  }, 

  retrieveData: function () {
    var data = this.chart.data;
    var options = this.chart.options;

    var colLabels = data.getVisibleCategories();
    var selectors = data.getVisibleSeries();
    var values = data.getValues();
    var selMap = {};
    var i;
    
    // create empty datasets and selMap
    var numCols = values.length;
    for(var e in this.structArr) {
      var elem = this.structArr[e];
      elem.values = [];
      for(i=0; i<numCols; i++) {
          elem.values.push([]);
      }
      selMap[ elem.selector ] = elem; 
    }

    // distribute the dataset over the elements based on the selector
    var boxNotFound = {};
    for(i in selectors) {
      var box = selMap[ selectors[ i ] ];
      if (typeof(box) != "undefined") {
        for(var j in values) {
            box.values[j].push(values[ j ][ i ]);
        }
      } else {
        boxNotFound[ selectors[i] ] = true;
      }
    }

    for (var sel in boxNotFound) {
        this._log("Could'nt find box for selector: "+ sel);
    }
    
    this.generateBoxPlots();

    var whitespaceQuote = new RegExp ('[\\s\"\']+',"g");
    if(options.selectParam){
        var selPar = options.selectParam.replace(whitespaceQuote, '');
        /*global window:true*/
        if ((selPar != "undefined") && 
            (selPar.length > 0) &&
            (typeof window[selPar] != "undefined")) {
            selPar = window[selPar];
            this.addDataPoint(selPar);
        }
    }
  } ,

  /**
   * @override
   */
  _createCore: function() {
      
    var myself  = this;

    var options = this.chart.options;
    options.smValueFontsize = Math.round(0.6 * options.valueFontsize);
    options.smValueFont = "" + options.smValueFontsize + "px sans-serif";
    options.valueFont = "" + options.valueFontsize + "px sans-serif";

    // used in the different closures
    var topRuleOffs = options.topRuleOffset,
        botRuleOffs = options.botRuleOffset,
        leftRuleOffs = options.leftRuleOffset;

    // set a few parameters which will be used during data-retrieval
    this.innerWidth = this.width - leftRuleOffs - options.rightRuleOffset;
    this.innerHeight = this.height - topRuleOffs - botRuleOffs;
    this.botOffs = botRuleOffs;
    this.leftOffs = leftRuleOffs;

    // retrieve the data and transform it to the internal representation.
    this.retrieveStructure();

    this.retrieveData();

    /*****
     *   Generate the scales x, y and color
     *******/

/*
pv_Mark.prototype.property("testAdd");
    pv_Mark.prototype.testAdd = function(x) { 
return pv.Label(x);
                      }
*/
    var topMargin = options.headerFontsize + 3;

    // draw the connectors first (rest has to drawn over the top)
    var rules = this.rules;
    var i;
    
    for (i = 0; i < rules.length; i++) {
      /*jshint loopfunc:true */
      this.pvPanel.add(pv.Line)
        .data(rules[ i ])
        .left(function(d) { return d.x;})
        .bottom(function(d) { return d.y;})
        .lineWidth(1)
        .strokeStyle("black");
    }
    
    // draw the data containers with decorations
    this.pvDataTree = this.pvPanel.add(pv.Bar)
      .data(myself.structArr)
      .left(function(d) { return d.left;})
      .bottom(function(d) { return d.bottom;})
      .height(function(d) { return d.height;})
      .width(function(d) { return d.width;})
      .fillStyle("green")
//;  this.pvDataTree
    .add(pv.Bar)
//      .data(function(d) {return d; })
      .left(function(d) { return d.left + options.border;})
      .bottom(function(d) { return d.bottom + options.border;})
      .height(function(d) { return d.height - options.border - topMargin;})
      .width(function(d) { return d.width - 2 * options.border;})
      .fillStyle("white")
    .add(pv.Label)
      .text(function(d) { return d.label;})
      .textAlign("center")
      .left(function (d) {return  d.left + d.width/2;})
      .bottom(function(d) {
          return d.bottom + d.height - options.headerFontsize - 5 + options.headerFontsize/5;
})
      .font("" + options.headerFontsize + "px sans-serif")
      .textStyle("white")
      .fillStyle("blue");

    // add the box-plots
    for(i=0; i<this.structArr.length; i++) {
      var box = this.structArr[i];
      this.pvPanel.add(pv.Rule)
        .data(box.subplot.hRules)
        .left(function(d) { return d.left;})
        .width( function(d) { return d.width;})
        .bottom( function(d) { return d.bottom;})
        .lineWidth( function(d) { return d.lWidth; })
        .strokeStyle(myself.chart.options.boxplotColor);

      this.pvPanel.add(pv.Rule)
        .data(box.subplot.vRules)
        .left(function(d) { return d.left;})
        .height( function(d) { return d.height;})
        .bottom( function(d) { return d.bottom;})
        .lineWidth( function(d) { return d.lWidth; })
        .strokeStyle(myself.chart.options.boxplotColor);

      this.pvPanel.add(pv.Dot)
        .data(box.subplot.marks)
        .left(function(d) { return d.left; })
        .bottom(function(d){ return d.bottom;})
        .fillStyle(function(d) {return d.color;});


      this.pvPanel.add(pv.Label)
        .data(box.subplot.labels)
        .left(function(d) { return d.left; })
        .bottom(function(d){ return d.bottom;})
        .font(function(d) { return d.size;})
        .text(function(d) { return d.text;})
        .textAlign("center")
        .textStyle(function(d) {return d.color;});

    }

    // add the connecting rules (perpendicular rules)
    if (options.perpConnector) {
      this.pvPanel.add(pv.Rule)
        .data(myself.vRules)
        .left(function(d) { return d.left;})
        .bottom(function(d) { return d.bottom;})
        .height(function(d) { return d.height;})
        .strokeStyle("black");
      this.pvPanel.add(pv.Rule)
        .data(myself.hRules)
        .left(function(d) { return d.left;})
        .bottom(function(d) { return d.bottom;})
        .width(function(d) { return d.width;})
        .strokeStyle("black");
    }

    /*****
     *   draw the data-tree
     *******/
   },
  
   applyExtensions: function(){
      // Extend the dataTree
      this.extend(this.pvDataTree,"dataTree");
   }
});

/**
 * @name pvc.data.BoxplotChartTranslationOper
 * 
 * @class The translation mixin operation of the box plot chart.
 * 
 * <p>
 * The default box plot format is:
 * </p>
 * <pre>
 * +----------+----------+--------------+--------------+------------+-------------+
 * | 0        | 1        | 2            | 3            | 4          | 5           |
 * +----------+----------+--------------+--------------+------------+-------------+
 * | category | median   | lowerQuartil | upperQuartil | minimum    | maximum     |
 * +----------+----------+--------------+--------------+------------+-------------+
 * | any      | number   | number       | number       | number     | number      |
 * +----------+----------+--------------+--------------+------------+-------------+
 * </pre>
 * 
 * @extends pvc.data.MatrixTranslationOper
 */
def.type('pvc.data.BoxplotChartTranslationOper')
.add(/** @lends pvc.data.BoxplotChartTranslationOper# */{
    /**
     * @override
     */
    _configureTypeCore: function(){
        var autoDimNames = [];
       
        // VItem Indexes of continuous columns not yet being read
        var freeMeaIndexes = [];
        
        // Idem, but for discrete columns
        var freeDisIndexes = [];
        
        this.collectFreeDiscreteAndConstinuousIndexes(freeDisIndexes, freeMeaIndexes);
        
        this._getUnboundRoleDefaultDimNames('category', freeDisIndexes.length, autoDimNames);
        
        // Try to bind as much measure roles as there are free measures
        def
        .query(pvc.BoxplotChart.measureRolesNames)
        .take (freeMeaIndexes.length) // first free measures
        .each(function(roleName) {
            this._getUnboundRoleDefaultDimNames(roleName, 1, autoDimNames);
        }, this);

        if(autoDimNames.length) { this.defReader({names: autoDimNames}); }
    }
});

/*global pvc_ValueLabelVar:true */

def
.type('pvc.BoxplotPanel', pvc.CategoricalAbstractPanel)
.init(function(chart, parent, plot, options) {
    
    this.base(chart, parent, plot, options);
    
    this.boxSizeRatio = plot.option('BoxSizeRatio');
    this.maxBoxSize   = plot.option('BoxSizeMax');
})
.add({
    plotType: 'box',
    
    anchor: 'fill',
    
    // Override default mappings
    _v1DimRoleName: {
        //'series':   'series',
        //'category': 'category',
        'value':    'median'
    },
    
    /**
     * @override
     */
    _createCore: function(){

        this.base();
        
        var rootScene = this._buildScene();

        var a_bottom = this.isOrientationVertical() ? "bottom" : "left",
            a_left   = this.anchorOrtho(a_bottom),
            a_width  = this.anchorLength(a_bottom),
            a_height = this.anchorOrthoLength(a_bottom);

        function defaultColor(type){
            var color = this.base(type);
            return type === 'stroke' ? color.darker(1) : color;
        }
        
        /* Category Panel */
        var extensionIds = ['panel'];
        if(this.compatVersion() <= 1){
            extensionIds.push(''); // let access as "box_"
        }
        
        this.pvBoxPanel = new pvc.visual.Panel(this, this.pvPanel, {
                extensionId: extensionIds
            })
            .lock('data', rootScene.childNodes)
            .lockMark(a_left, function(scene){
                var catVar = scene.vars.category;
                return catVar.x - catVar.width / 2;
            })
            .pvMark
            [a_width](function(scene){ return scene.vars.category.width; })
            ;
        
        /* V Rules */
        function setupRuleWhisker(rule){
            rule.lock(a_left, function(){ 
                    return this.pvMark.parent[a_width]() / 2;
                })
                .override('defaultColor', defaultColor)
                ;

            return rule;
        }

        this.pvRuleWhiskerUpper = setupRuleWhisker(new pvc.visual.Rule(this, this.pvBoxPanel, {
                extensionId:   'boxRuleWhisker',
                freePosition:  true,
                noHover:       false,
                noSelect:      false,
                noClick:       false,
                noDoubleClick: false,
                showsInteraction: true
            }))
            .intercept('visible', function(scene){
                return scene.vars.category.showRuleWhiskerUpper && this.delegateExtension(true);
            })
            .lock(a_bottom, function(scene){ return scene.vars.category.ruleWhiskerUpperBottom; })
            .lock(a_height, function(scene){ return scene.vars.category.ruleWhiskerUpperHeight; })
            .pvMark
            ;

        this.pvRuleWhiskerLower = setupRuleWhisker(new pvc.visual.Rule(this, this.pvBoxPanel, {
                extensionId:   'boxRuleWhisker',
                freePosition:  true,
                noHover:       false,
                noSelect:      false,
                noClick:       false,
                noDoubleClick: false,
                showsInteraction: true
            }))
            .intercept('visible', function(scene){
                return scene.vars.category.showRuleWhiskerBelow && this.delegateExtension(true);
            })
            .lock(a_bottom, function(scene){ return scene.vars.category.ruleWhiskerLowerBottom; })
            .lock(a_height, function(scene){ return scene.vars.category.ruleWhiskerLowerHeight; })
            .pvMark
            ;

        /* Box Bar */
        function setupHCateg(sign){
            sign.lock(a_left,  function(scene){ return scene.vars.category.boxLeft;  })
                .lock(a_width, function(scene){ return scene.vars.category.boxWidth; })
                ;
            
            return sign;
        }

        this.pvBar = setupHCateg(new pvc.visual.Bar(this, this.pvBoxPanel, {
                extensionId:   'boxBar',
                freePosition:  true,
                normalStroke:  true
            }))
            .intercept('visible', function(scene){
                return scene.vars.category.showBox && this.delegateExtension(true);
            })
            .lock(a_bottom, function(scene){ return scene.vars.category.boxBottom; })
            .lock(a_height, function(scene){ return scene.vars.category.boxHeight; })
            .override('defaultColor', defaultColor)
            .override('defaultStrokeWidth', def.fun.constant(1))
            .pvMark
            ;

        /* H Rules */
        function setupHRule(rule){
            setupHCateg(rule);
            
            rule.override('defaultColor', defaultColor);
            
            return rule;
        }
        
        this.pvRuleMin = setupHRule(new pvc.visual.Rule(this, this.pvBoxPanel, {
                extensionId:   'boxRuleMin',
                freePosition:  true,
                noHover:       false,
                noSelect:      false,
                noClick:       false,
                noDoubleClick: false,
                showsInteraction: true
            }))
            .intercept('visible', function(){
                return this.scene.vars.minimum.value != null && this.delegateExtension(true);
            })
            .lock(a_bottom,  function(){ return this.scene.vars.minimum.position; }) // bottom
            .pvMark
            ;

        this.pvRuleMax = setupHRule(new pvc.visual.Rule(this, this.pvBoxPanel, {
                extensionId:   'boxRuleMax',
                freePosition:  true,
                noHover:       false,
                noSelect:      false,
                noClick:       false,
                noDoubleClick: false,
                showsInteraction: true
            }))
            .intercept('visible', function(){
                return this.scene.vars.maximum.value != null && this.delegateExtension(true);
            })
            .lock(a_bottom,  function(){ return this.scene.vars.maximum.position; }) // bottom
            .pvMark
            ;

        this.pvRuleMedian = setupHRule(new pvc.visual.Rule(this, this.pvBoxPanel, {
                extensionId:   'boxRuleMedian',
                freePosition:  true,
                noHover:       false,
                noSelect:      false,
                noClick:       false,
                noDoubleClick: false,
                showsInteraction: true
            }))
            .intercept('visible', function(){
                return this.scene.vars.median.value != null && this.delegateExtension(true);
            })
            .lock(a_bottom,  function(){ return this.scene.vars.median.position; }) // bottom
            .override('defaultStrokeWidth', def.fun.constant(2))
            .pvMark
            ;
    },

    /**
     * Renders this.pvScatterPanel - the parent of the marks that are affected by interaction changes.
     * @override
     */
    renderInteractive: function(){
        this.pvBoxPanel.render();
    },

    _buildScene: function(){
        var chart = this.chart,
            measureRolesDimNames = 
                def
                .query(chart.measureVisualRoles())
                .object({
                    name:  function(role) { return role.name; },
                    value: function(role) { return role.firstDimensionName(); }
                }),
            visibleKeyArgs = {visible: true, zeroIfNone: false},
            data       = this.visibleData({ignoreNulls: false}),
            rootScene  = new pvc.visual.Scene(null, {panel: this, source: data}),
            baseScale  = this.axes.base.scale,
            bandWidth  = baseScale.range().band,
            boxWidth   = Math.min(bandWidth * this.boxSizeRatio, this.maxBoxSize),
            orthoScale = this.axes.ortho.scale,
            colorVarHelper = new pvc.visual.RoleVarHelper(rootScene, this.visualRoles.color, {roleVar: 'color'});

        /**
         * Create starting scene tree
         */
        data.children() // categories
            .each(createCategScene, this);

        return rootScene;
        
        function createCategScene(categData){
            var categScene = new pvc.visual.Scene(rootScene, {source: categData});
            var vars = categScene.vars;
            
            // Series distinction is ignored
            // If the role is bound by the user, its data will not be visible 
            vars.series = new pvc_ValueLabelVar(null, "");
            var catVar  = vars.category = new pvc_ValueLabelVar(categData.value, categData.label);
            
            def.set(catVar,
                'group',    categData,
                'x',        baseScale(categData.value),
                'width',    bandWidth,
                'boxWidth', boxWidth,
                'boxLeft',  bandWidth / 2 - boxWidth / 2);
            
            chart.measureVisualRoles().forEach(function(role){
                var dimName = measureRolesDimNames[role.name],
                    svar;

                if(dimName){
                    var dim = categData.dimensions(dimName),
                        value = dim.sum(visibleKeyArgs);
                    
                    svar = new pvc_ValueLabelVar(value, dim.format(value));
                    svar.position = orthoScale(value);
                } else {
                    svar = new pvc_ValueLabelVar(null, "");
                    svar.position = null;
                }

                vars[role.name] = svar;
            });
            
            colorVarHelper.onNewScene(categScene, /* isLeaf */ true);
            
            // ------------

            var hasMin    = vars.minimum.value  != null,
                hasLower  = vars.lowerQuartil.value != null,
                hasMedian = vars.median.value != null,
                hasUpper  = vars.upperQuartil.value != null,
                bottom,
                top;

            var show = hasLower || hasUpper;
            if(show){
                bottom = hasLower  ? vars.lowerQuartil.position :
                         hasMedian ? vars.median.position :
                         vars.upperQuartil.position
                         ;

                top    = hasUpper  ? vars.upperQuartil.position :
                         hasMedian ? vars.median.position :
                         vars.lowerQuartil.position
                         ;

                show = (top !== bottom);
                if(show){
                    catVar.boxBottom = bottom;
                    catVar.boxHeight = top - bottom;
                }
            }
            
            catVar.showBox  = show;
            
            // vRules
            show = vars.maximum.value != null;
            if(show){
                bottom = hasUpper  ? vars.upperQuartil.position :
                         hasMedian ? vars.median.position :
                         hasLower  ? vars.lowerQuartil.position :
                         hasMin    ? vars.minimum.position  :
                         null
                         ;
                
                show = bottom != null;
                if(show){
                    catVar.ruleWhiskerUpperBottom = bottom;
                    catVar.ruleWhiskerUpperHeight = vars.maximum.position - bottom;
                }
            }

            catVar.showRuleWhiskerUpper = show;

            // ----

            show = hasMin;
            if(show){
                top = hasLower  ? vars.lowerQuartil.position :
                      hasMedian ? vars.median.position :
                      hasUpper  ? vars.upperQuartil.position :
                      null
                      ;

                show = top != null;
                if(show){
                    bottom = vars.minimum.position;
                    catVar.ruleWhiskerLowerHeight = top - bottom;
                    catVar.ruleWhiskerLowerBottom = bottom;
                }
            }
            
            catVar.showRuleWhiskerBelow = show;
            
            // hasMin = vars.minimum.value  != null,
        }
    }
});


/**
 * BoxplotChart is the main class for generating... categorical boxplotcharts.
 * 
 * The boxplot is used to represent the distribution of data using:
 *  - a box to represent the region that contains 50% of the datapoints,
 *  - the whiskers to represent the regions that contains 95% of the datapoints, and
 *  - a center line (in the box) that represents the median of the dataset.
 * For more information on boxplots you can visit  http://en.wikipedia.org/wiki/Box_plot
 *
 * If you have an issue or suggestions regarding the ccc BoxPlot-charts
 * please contact CvK at cde@vinzi.nl
 */
def
.type('pvc.BoxplotChart', pvc.CategoricalAbstract)
.add({
    _processOptionsCore: function(options){
        this.base.apply(this, arguments);

        // Not supported
        options.stacked = false;
    },

    /**
     * Initializes each chart's specific roles.
     * @override
     */
    _initVisualRoles: function(){

        this.base();

        var roleSpecBase = {
                isMeasure: true,
                requireSingleDimension: true,
                requireIsDiscrete: false,
                valueType: Number
            };

        [
            {name: 'median',       label: 'Median',        defaultDimension: 'median', isRequired: true},
            {name: 'lowerQuartil', label: 'Lower Quartil', defaultDimension: 'lowerQuartil'},
            {name: 'upperQuartil', label: 'Upper Quartil', defaultDimension: 'upperQuartil'},
            {name: 'minimum',      label: 'Minimum',       defaultDimension: 'minimum' },
            {name: 'maximum',      label: 'Maximum',       defaultDimension: 'maximum'}
        ].forEach(function(info){
            this._addVisualRole(info.name, def.create(roleSpecBase, info));
        }, this);
    },
    
    _getTranslationClass: function(translOptions){
        return def
            .type(this.base(translOptions))
            .add(pvc.data.BoxplotChartTranslationOper);
    },
    
    _initPlotsCore: function(/*hasMultiRole*/){
        new pvc.visual.BoxPlot(this);

        if(this.options.plot2){
            this._animatable = true;
            // Line Plot
            new pvc.visual.PointPlot(this, {
                name: 'plot2',
                defaults: {
                    LinesVisible: true,
                    DotsVisible:  true,
                    OrthoRole:    'median',
                    ColorAxis:    2
                },
                fixed: {
                    OrthoAxis: 1
                }});
        }
    },
    
    _bindAxes: function(hasMultiRole){
        
        this.base(hasMultiRole);
        
        // Set defaults of Offset property
        var typeAxes = this.axesByType.ortho;
        if(typeAxes){
            typeAxes.forEach(function(axis){
                axis.option.defaults({Offset: 0.02});
            });
        }
    },
    
    /* @override */
    _createPlotPanels: function(parentPanel, baseOptions){
        var plots   = this.plots;
            
        var boxPlot  = plots.box; 
        
        var boxPanel = new pvc.BoxplotPanel(
            this, 
            parentPanel, 
            boxPlot, 
            Object.create(baseOptions));

        // v1 field
        this.bpChartPanel = boxPanel;
        
        var plot2Plot = plots.plot2;
        if(plot2Plot){
            if(pvc.debug >= 3){
                this._log("Creating Point panel.");
            }
            
            var pointPanel = new pvc.PointPanel(
                    this, 
                    parentPanel, 
                    plot2Plot,
                    Object.create(baseOptions));
            
            // HACK:
            pointPanel._v1DimRoleName.value = plot2Plot.option('OrthoRole');
        }
    },
    
    defaults: {
        // plot2: false
        // legend: false,
        crosstabMode: false
        // panelSizeRatio
    }
})
.addStatic({
    measureRolesNames: ['median', 'lowerQuartil', 'upperQuartil', 'minimum', 'maximum']
});

/**
 * @name pvc.data.TreemapChartTranslationOper
 * 
 * @class The translation mixin operation of the treemap chart.
 * 
 * <p>
 * The default treemap format is:
 * </p>
 * <pre>
 * +----------+----------+--------------+
 * | 0        | 1        | 2            |
 * +----------+----------+--------------+
 * | category | size     | color        |
 * +----------+----------+--------------+
 * | any      | number   | number/disc  |
 * +----------+----------+--------------+
 * </pre>
 * 
 * @extends pvc.data.MatrixTranslationOper
 */
def.type('pvc.data.TreemapChartTranslationOper')
.add(/** @lends pvc.data.TreemapChartTranslationOper# */{
    /**
     * @override
     */
    _configureTypeCore: function() {
        var autoDimNames = [];
        
        // VItem Indexes of continuous columns not yet being read
        var freeMeaIndexes = [];
        
        // Idem, but for discrete columns
        var freeDisIndexes = [];
        
        this.collectFreeDiscreteAndConstinuousIndexes(freeDisIndexes, freeMeaIndexes);
        
        var D = freeDisIndexes.length;
        var M = freeMeaIndexes.length;
        
        if(D) { this._getUnboundRoleDefaultDimNames('category', D, autoDimNames); }
        if(M) {
            def.query(['size', 'color']).take(M).each(function(roleName) {
                this._getUnboundRoleDefaultDimNames(roleName, 1, autoDimNames);
            }, this);
        }
        
        if(autoDimNames.length) { this.defReader({names: autoDimNames}); }
    }
});

/*global pvc_ValueLabelVar:true */

def
.type('pvc.TreemapPanel', pvc.PlotPanel)
.init(function(chart, parent, plot, options){
    
    this.base(chart, parent, plot, options);
    
    this.axes.size = chart._getAxis('size', (plot.option('SizeAxis') || 0) - 1); // may be undefined

    this.visualRoles.size = chart.visualRole(plot.option('SizeRole'));
    
    this.layoutMode = plot.option('LayoutMode');
})
.add({
    _createCore: function(layoutInfo) {
        var me = this;
        var cs = layoutInfo.clientSize;
        var rootScene = me._buildScene();
        if(!rootScene) { return; } // Everything hidden
        
        var lw0 = def.number.to(me._getConstantExtension('leaf', 'lineWidth'), 1);
        var lw  = lw0;
        var lw2 = lw/2;
        
        var sizeProp = me.visualRoles.size.isBound() ?
                       // Does not use sceneScale on purpose because of the 'nullToZero'
                       // code not calling the base scale when null.
                       // The base scale already handles the null case, 
                       // translating it to the minimum value.
                       me.axes.size.scale.by1(function(scene) { return scene.vars.size.value; }) :
                       100;
                
        var panel = me.pvTreemapPanel = new pvc.visual.Panel(me, me.pvPanel, {
                panelType:   pv.Layout.Treemap,
                extensionId: 'panel'
            })
            .pvMark
            .lock('visible', true)
            .lock('nodes',   rootScene.nodes())
            // Reserve space for interaction borders
            .lock('left',    lw2)
            .lock('top',     lw2)
            .lock('width',   cs.width  - lw)
            .lock('height',  cs.height - lw)
            .lock('size',    sizeProp)
            .lock('mode',    me.layoutMode)
            .lock('order',   null) // TODO: option for this?
            .lock('round',   false);
        
        // Node prototype
        // Reserve space for interaction borders
        panel.node
            .left  (function(n) { return n.x  + lw2; })
            .top   (function(n) { return n.y  + lw2; })
            .width (function(n) { return n.dx - lw;  })
            .height(function(n) { return n.dy - lw;  });
        
        // ------------------
        
        var colorAxis = me.axes.color;
        var colorScaleDirect, colorScaleLeaf;
        if(me.visualRoles.color.isBound()) {
            colorScaleLeaf = colorScaleDirect = colorAxis.sceneScale({sceneVarName: 'color'});
            if(me.plot.option('ColorMode') === 'byparent') {
                colorScaleLeaf = colorScaleLeaf.by(def.propGet('parent'));
            }
        } else {
            colorScaleLeaf = colorScaleDirect = def.fun.constant(colorAxis.option('Unbound'));
        }
        
        // ------------------
        
        var pvLeafMark = new pvc.visual.Bar(me, panel.leaf, {extensionId: 'leaf'})
            .lockMark('visible')
            .override('defaultColor', function(type) { return colorScaleLeaf(this.scene); })
            .override('defaultStrokeWidth', function() { return lw0; })
            .pvMark
            .antialias(false)
            .lineCap('round') // only used by strokeDashArray
            .strokeDasharray(function(scene) {
                return scene.vars.size.value < 0 ? 'dash' : null; // Keep this in sync with the style in pvc.sign.DotSizeColor
            });
       
        new pvc.visual.Bar(me, panel.node, {
            extensionId: 'ascendant',
            noHover:  true,
            noSelect: true,
            noClick:  true,
            noDoubleClick:  true
        })
        .intercept('visible', function(scene) {
            return !!scene.parent && 
                   !!scene.firstChild &&
                   this.delegateExtension(true); 
         })
        .override('anyInteraction', function() {
            return this.scene.anyInteraction() ||
                   this.scene.isActiveDescendantOrSelf(); // special kind of interaction
        })
        .override('defaultStrokeWidth', function() { return 1.5 * lw; })
        .override('interactiveStrokeWidth', function(w) {
            if(this.showsActivity() && 
               this.scene.isActiveDescendantOrSelf()) {
               w = Math.max(1, w) * 1.5;
            }
            return w;
        })
        .override('defaultColor',     function(type) { return colorScaleDirect(this.scene); })
        .override('normalColor',      def.fun.constant(null))
        .override('interactiveColor', function(color, type) {
            if(type === 'stroke') {
                if(this.showsActivity()) {
                    if(this.scene.isActiveDescendantOrSelf()) {
                        return pv.color(color).brighter(0.5)/*.alpha(0.7)*/;
                    }
                    
                    if(this.scene.anyActive()) { return null; }
               }
                
               if(this.showsSelection() && this.scene.isSelectedDescendantOrSelf()) {
                   return pv.color(color).brighter(0.5)/*.alpha(0.7)*/;
               }
            }
            return null;
        })
        .pvMark
        .antialias(false);
        
        var label = pvc.visual.ValueLabel.maybeCreate(me, panel.label, {noAnchor: true});
        if(label) {
            var valuesFont = this.valuesFont;
            label
            .override('trimText', function(text) {
                // Vertical/Horizontal orientation?
                var side = this.pvMark.textAngle() ? 'dy' : 'dx';
                // Add a small margin (2 px)
                var maxWidth = this.scene[side] - 2;
                return pvc.text.trimToWidthB(maxWidth, text, valuesFont, "..");
            })
            .override('calcBackgroundColor', function(/*type*/) {
                // Corresponding scene on pvLeafMark sibling mark (rendered before)
                var pvSiblingScenes = pvLeafMark.scene;
                var pvLeafScene     = pvSiblingScenes[this.pvMark.index];
                return pvLeafScene.fillStyle;
            });
        }
    },
    
    _getExtensionId: function(){
        // 'content' coincides, visually, with 'plot', in this chart type
        // Actually it shares the same panel...
        
        var extensionIds = [{abs: !this.chart.parent ? 'content' : 'smallContent'}];
        return extensionIds.concat(this.base());
    },
    
    renderInteractive: function(){
        this.pvTreemapPanel.render();
    },
    
    _buildScene: function() {
        // Hierarchical data, by categ1 (level1) , categ2 (level2), categ3 (level3),...
        var data = this.visibleData({ignoreNulls: false});

        // Everything hidden?
        if(!data.childCount()) { return null; }
        
        var roles = this.visualRoles;
        var rootScene = new pvc.visual.Scene(null, {panel: this, source: data});
        var sizeVarHelper  = new pvc.visual.RoleVarHelper(rootScene, roles.size,  {roleVar: 'size',  allowNestedVars: true, hasPercentSubVar: true});
        //var colorVarHelper = new pvc.visual.RoleVarHelper(rootScene, roles.color, {roleVar: 'color', allowNestedVars: true});
        
        var colorGrouping = roles.color && roles.color.grouping;
        
        var recursive = function(scene) {
            var group = scene.group;
            
            // The 'category' var value is the local group's value...
            // 
            // When all categories are flattened into a single level
            // of a data hierarchy, 
            // each data's local key is compatible to the role key
            // (the one obtained by using:
            // pvc.data.Complex.compositeKey(complex, role.dimensioNames())
            // That key will be the concatenation of the keys of all atoms 
            // (corresponding to the single level's dimensions).
            // If any of these keys is empty, the key will contain 
            // consecutive ~ separator characters, like "Foo~Bar~~Guru",
            // or even a trailing one: "Foo~Bar~Guru~".
            //
            // On the other hand, the key obtained by an abs key, at a given node,
            // will contain all the keys of ascendant nodes, but no *trailing* empty keys.
            // The keys of compositeKeys are like if all keys were obtained
            // at the leaves of a regular tree (all branches have the same depth).
            // When a leaf did not, in fact, exist, 
            // an empty data node would be placed there anyway, 
            // with an empty key.
            // 
            // The two keys cannot currently be made compatible because
            // it seems that the waterfall's DfsPre/DfsPost flattening
            // needs the distinction between the key of the ancestor,
            // and the key of the unexistent leaf under it...
            //
            // 
            
            // TODO: Should be the abs key (no trailing empty keys)
            scene.vars.category = pvc_ValueLabelVar.fromComplex(group);
            
            // All nodes are considered leafs, for what the var helpers are concerned
            sizeVarHelper.onNewScene(scene, /* isLeaf */ true);
            //colorVarHelper.onNewScene(scene, /* isLeaf */ true);
            
            if(!colorGrouping){
                if(!scene.parent) { scene.vars.color = new pvc_ValueLabelVar(null, ""); }
            } else {
                scene.vars.color = new pvc_ValueLabelVar(
                        group.absKey,
                        group.absLabel);
            }
            
            if(group.childCount()){
                group
                    .children()
                    .each(function(childData){
                        if(childData.value != null){ // Stop when a level is not detailed in a given branch
                            recursive(new pvc.visual.Scene(scene, {source: childData}));
                        }
                    });
            }
            
            return scene;
        };
        
        return recursive(rootScene);
    }
});


def
.type('pvc.TreemapChart', pvc.BaseChart)
.add({
    _animatable: false,

    // Create color axis, even if the role is unbound
    // cause we need to check the axis options any way
    _axisCreateIfUnbound: {
        'color': true
    },
    
    _getColorRoleSpec: function() {
        return { 
            defaultSourceRole: 'category', 
            defaultDimension:  'color*'
            /*, requireIsDiscrete: true*/
        };
    },
    
    _initVisualRoles: function() {
        
        this.base();
        
        this._addVisualRole('category', { 
                isRequired: true, 
                defaultDimension: 'category*', 
                autoCreateDimension: true 
            });
            
        this._addVisualRole('size', {
                isMeasure:  true,
                isRequired: false,
                isPercent:  true,
                requireSingleDimension: true, 
                requireIsDiscrete: false,
                valueType: Number, 
                defaultDimension: 'size' 
            });
    },
    
    _getTranslationClass: function(translOptions) {
        return def.type(this.base(translOptions)).add(pvc.data.TreemapChartTranslationOper);
    },
    
    // Consider all datums to be not-null.
    // All measures are optional...
    // @override
    _getIsNullDatum: def.fun.constant(),
    
    _setAxesScales: function(hasMultiRole) {
        
        this.base(hasMultiRole);
        
        if(!hasMultiRole || this.parent) {
            var sizeAxis = this.axes.size;
            if(sizeAxis && sizeAxis.isBound()) {
                this._createAxisScale(sizeAxis);
                
                // This range has been determined by experimentation.
                // Some ranges result in strange proportions.
                sizeAxis.setScaleRange({min: 100, max: 1000});
            }
        }
    },
    
    _initPlotsCore: function(/*hasMultiRole*/) {
        var treemapPlot = new pvc.visual.TreemapPlot(this);
        
        if(this.options.legend == null) {
            // Only show the legend by default if color mode is byparent
            this.options.legend = treemapPlot.option('ColorMode') === 'byparent';
        }
        
        var rootCategoryLabel = treemapPlot.option('RootCategoryLabel');
        this.visualRoles.category.setRootLabel(rootCategoryLabel);
        this.visualRoles.color   .setRootLabel(rootCategoryLabel);
    },
    
    _preRenderContent: function(contentOptions) {

        this.base();
        
        var treemapPlot = this.plots.treemap;
        new pvc.TreemapPanel(this, this.basePanel, treemapPlot, contentOptions);
    },
    
    _createVisibleData: function(dataPartValue, ka) {
        var visibleData = this.base(dataPartValue, ka);
        
        // There are no null datums in this chart type (see #_getIsNullDatum) 
        return visibleData ? this.visualRoles.category.select(visibleData, {visible: true}) : null;
    },
    
    defaults: {
        legend: null  // dynamic default, when nully
    }
});
    
    return pvc;
}(def, pv));
