Source: chart/tooltip.js

/**
 * Chart tooltip module
 * You create a tooltip module by adding it as plugins to one
 * of your chart views.
 *
 * The text of the tooltip is populated from the textOf method of
 * <a href="ludo.chart.DataSource.html">ludo.chart.DataSource</a>.
 *
 * Example:
 * <code>
 * textOf:function(record, caller){
            // Text for the tooltip module
            if(caller.type == 'chart.Tooltip'){
                return '<p><b>{parent.name}</b><br>' + record.date + '<br>Share Price: {record.price}</p>';
            }
            // Text for all others.
            return record.date;
        },
    </code>
 *
 * textOf is a function returning text to chart views. Many views ask this method
 * for data, so a test on caller.type or caller.id is usually required.
 * 
 * Simple HTML tags like &lt;b>, &lt;strong> &lt;i>, &lt;em> and &lt;br>
 * in the returned text is allowed.
 *
 * @class ludo.chart.Tooltip
 * @param {Object} config
 * @param {Object} config.type Always set to chart.Tooltip
 * @param {Object} config.textStyles Text styling
 * @param {Object} config.boxStyles Styling of tooltip box
 * @param {Number} config.animationDuration Animation duration in ms, default: 200
 * @example:
 * plugins:[
 *  {
 *      type:'chart.Tooltip',
 *      textStyles:{
 *          'font-size': '12px',
 *          'fill': '#aeb0b0'
 *      },
 *      boxStyles:{
 *          fill:'#222',
 *          'fill-opacity': 0.9
 *      }
 * ]
 */
ludo.chart.Tooltip = new Class({
    Extends: ludo.chart.AddOn,
    type: 'chart.Tooltip',
    tpl: '{label}: {value}',
    shown: false,

    offset: undefined,

    size: {
        x: 0, y: 0
    },

    arrowSize: 5,
    /*
     * Styling of box where the tooltip is rendered
     * @config {Object} boxStyles
     * @default { "fill":"#fff", "fill-opacity":.8, "stroke-width" : 1, "stroke-location": "inside" }
     */
    boxStyles: {},

    /*
     * Overall styling of text
     * @config {Object} textStyles
     * @default { "fill" : "#000" }
     */
    textStyles: {},

    record: undefined,

    _autoLeave: undefined,

    animationDuration:100,

    group:undefined,

    __construct: function (config) {
        this.parent(config);
        this.offset = {x: 0,y:0};
        this.setConfigParams(config, ['tpl', 'boxStyles', 'textStyles','animationDuration']);
        this.createDOM();


        var p = this.getParent();

        p.on('enter', this.show.bind(this));
        p.on('leave', this.autoHide.bind(this));

        if (p.tooltipAtMouseCursor()) {
            this.getParent().getNode().on('mousemove', this.move.bind(this));
        } else {

        }

        this.chart().on('entergroup', this.leaveGroup.bind(this));
    },

    leaveGroup:function(groupId){
        if(!this.group)return;
        if(groupId != this.group.id)this.hide();
    },

    createDOM: function () {
        this.node = new ludo.svg.Node('g');
        this.getParent().svg().append(this.node);
        this.node.hide();
        this.node.toFront.delay(50, this.node);

        this.rect = new ludo.svg.Path();
        this.rect.css(this.getBoxStyling());
        this.rect.set('stroke-linejoin', 'round');
        this.rect.set('stroke-linecap', 'round');

        this.node.append(this.rect);

        this.textBox = new ludo.svg.TextBox();
        this.node.append(this.textBox);
        this.textBox.getNode().setTranslate(6, 2);
        this.textBox.getNode().css(this.getTextStyles());
    },

    getBoxStyling: function () {
        var ret = this.boxStyles || {};
        if (!ret['fill'])ret['fill'] = '#fff';
        if (!ret['stroke-location'])ret['stroke-location'] = 'inside';
        if (ret['fill-opacity'] === undefined)ret['fill-opacity'] = .8;
        if (ret['stroke-width'] === undefined)ret['stroke-width'] = 1;
        return ret;
    },

    getTextStyles: function () {
        var ret = this.textStyles || {};
        if (!ret['fill'])ret['fill'] = '#000';
        return ret;
    },

    show: function (fragment, rec, node, event) {


        this.group = fragment.getParent();

        this._autoLeave = false;
        if (rec == this.record)return;

        if(fragment.getParent().node.el != this.node.el.parentNode){
            fragment.getParent().append(this.node);

        }

        this.record = rec;

        if (rec == undefined) {
            this.hide();
            return;
        }

        var animate = !this.node.isHidden();
        this.node.set('fill-opacity', 1);
        this.rect.set('fill-opacity', 1);
        this.node.show();
        this.node.toFront();
        this.shown = true;

        this.textBox.setText(this.getParsedHtml());

        this.rect.css('stroke', this.record.__color);

        this.updateRect(fragment, xy);

        this.rect.show();

        var followMouse = this.getParent().tooltipAtMouseCursor();
        var xy;
        if (followMouse) {
            this.move(event);
        } else {
            xy = this.getXY(fragment, node);

            if(this.offset.x != 0 || this.offset.y != 0){
                this.updateRect(fragment);
            }
            if (animate) {
                this.node.animate({
                    'translate': [xy.x, xy.y]
                }, {
                    duration:this.animationDuration,
                    queue:false,
                    validate:function(a,b){
                        return a == b;
                    }
                })
            } else {
                this.node.setTranslate(xy.x, xy.y);
            }
        }


    },

    autoHide: function () {
        this._autoLeave = true;
        this.autoHideAfterDelay.delay(2000, this);
    },

    autoHideAfterDelay: function () {
        if (!this._autoLeave)return;
        this.node.hide();
        this.record = undefined;
    },

    updateRect: function (fragment, pos) {
        this.size = this.textBox.getNode().getSize();
        this.size.x += 12;
        this.size.y += 15;


        var middle = this.size.x / 2 + this.offset.x;
        var middleY = this.size.y / 2;

        if(middle + this.arrowSize > this.size.x){
            middle -= ((middle + this.arrowSize ) - this.size.x);
        }
        if(middle < 0){
            middle = 0;
        }

        var p;
        var tp = this.getParent().getTooltipPosition();

        switch (tp) {
            case 'above':
                p = [
                    this.size.x, 0,
                    this.size.x, this.size.y,
                    middle + this.arrowSize, this.size.y,
                    middle, this.size.y + this.arrowSize,
                    middle - this.arrowSize, this.size.y,
                    0, this.size.y,
                    0, 0];
                break;
            case 'left':
                p = [
                    this.size.x, 0,
                    this.size.x, middleY - this.arrowSize,
                    this.size.x + this.arrowSize, middleY,
                    this.size.x, middleY + this.arrowSize,
                    this.size.x, this.size.y,
                    0, this.size.y,
                    0, 0
                ];


                break;
            case 'right':
                p = [
                    this.size.x, 0,
                    this.size.x, this.size.y,
                    0, this.size.y,
                    0, middleY + this.arrowSize,
                    0 - this.arrowSize, middleY,
                    0, middleY - this.arrowSize,
                    0, 0
                ];
                break;

            default:
                p = [
                    this.size.x, 0,
                    this.size.x, this.size.y,
                    0, this.size.y,
                    0, 0
                ];
        }




        this.rect.setPath('M 0 0 L ' + p.join(' ') + " Z");

    },

    getXY: function (fragment, target) {

        this.offset.x = 0;
        this.offset.y = 0;

        var bounds = target.getBBox();



        var size = this.rect.getBBox();

        var pos = target.position();
        
        var tp = this.getParent().getTooltipPosition();

        switch (tp) {
            case 'left':
                return {
                    x: pos.left - size.width,
                    y: pos.top + bounds.height / 2 - size.height / 2
                };

            case 'right':
                return {
                    x: pos.left + bounds.width + this.arrowSize,
                    y: pos.top + bounds.height / 2 - size.height / 2
                };


            default:

                var x = pos.left + bounds.width / 2 - size.width / 2;
                var aw = this.getParent().svg().width;
                var overflow = (x + size.width) - aw;
                if(overflow > 0){
                    this.offset.x = overflow;
                    x-=overflow;
                }
                return {
                    x: x,
                    y: pos.top - size.height
                };


        }

    },

    move: function (event) {
        this.node.setTranslate(event.clientX - (this.size.x / 2), event.clientY - this.size.y - 15);
    },


    hide: function () {
        this.node.hide();
        this.rect.hide();
        this.shown = false;
        this.record = undefined;
    },

    getParsedHtml: function () {
        var text = this.getDataSource().textOf(this.record, this);


        jQuery.each(this.record, function (key, value) {
            var r = new RegExp("{record\." + key + "}", "g");
            text = text.replace(r, value);
        });

        if (this.record.__parent && text.indexOf('parent.') >= 0) {

            var p = this.getDataSource().byId(this.record.__parent);
            jQuery.each(p, function (key, value) {
                var r = new RegExp("{parent\." + key + "}", "g");
                text = text.replace(r, value);
            });

        }

        text = text.replace(/{.*?}/g, '');
        return text
    },

    getRecord: function () {
        return this.record;
    }

});