Source: widgets/chart.js

/*
 * This file is part of Toolkit.
 *
 * Toolkit is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * Toolkit is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU General
 * Public License along with this program; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */
"use strict";
(function(w, TK){
    
function calculate_overlap(X, Y) {
    /* no overlap, return 0 */
    if (X[2] < Y[0] || Y[2] < X[0] || X[3] < Y[1] || Y[3] < X[1]) return 0;

    return (Math.min(X[2], Y[2]) - Math.max(X[0], Y[0])) *
           (Math.min(X[3], Y[3]) - Math.max(X[1], Y[1]));
}

function show_handles() {
    var handles = this.handles;

    for (var i = 0; i < handles.length; i++) {
        this.add_child(handles[i]);
    }
}

function hide_handles() {
    var handles = this.handles;

    for (var i = 0; i < handles.length; i++) {
        this.remove_child(handles[i]);
    }
}

var STOP = function(e) {
    e.preventDefault();
    e.stopPropagation();
    return false;
}
function draw_key() {
    var __key, bb;

    var _key = this._key;
    var _key_bg = this._key_background;
    
    if (!_key || !_key_bg) return;

    while (_key.firstChild !== _key.lastChild)
        _key.removeChild(_key.lastChild);

    TK.empty(_key.firstChild);

    var O = this.options;
    
    var disp = "none";
    var gpad = TK.css_space(_key, "padding");
    var gmarg = TK.css_space(_key, "margin");
    var c   = 0;
    var w   = 0;
    var top = 0;
    var lines = [];
    for (var i = 0; i < this.graphs.length; i++) {
        if (this.graphs[i].get("key") !== false) {
            var t = TK.make_svg("tspan", {"class": "toolkit-label",
                                     style: "dominant-baseline: central;"
            });
            t.textContent = this.graphs[i].get("key");
            t.setAttribute("x", gpad.left);
            _key.firstChild.appendChild(t);
            
            if (!bb) bb = _key.getBoundingClientRect();
            top += c ? parseInt(TK.get_style(t, "line-height")) : gpad.top;
            t.setAttribute("y", top + bb.height / 2);
            
            lines.push({
                x:       (parseInt(TK.get_style(t, "margin-right")) || 0),
                y:       Math.round(top),
                width:   Math.round(bb.width),
                height:  Math.round(bb.height),
                "class": this.graphs[i].element.getAttribute("class"),
                color:   (this.graphs[i].element.getAttribute("color") || ""),
                style:   this.graphs[i].element.getAttribute("style")
            })
            w = Math.max(w, t.getComputedTextLength());
            disp = "block";
            c++;
        }
    }
    for (var i = 0; i < lines.length; i++) {
        var b = TK.make_svg("rect", {
            "class": lines[i]["class"] + " toolkit-rect",
            color:   lines[i].color,
            style:   lines[i].style,
            x:       lines[i].x + 0.5 + w + gpad.left,
            y:       lines[i].y + 0.5 + parseInt(lines[i].height / 2 - O.key_size.y / 2),
            height:  O.key_size.y,
            width:   O.key_size.x
        });
        _key.appendChild(b);
    }
    _key_bg.style.display = disp;
    _key.style.display = disp;
    
    bb = _key.getBoundingClientRect();
    var width  = this.range_x.options.basis;
    var height = this.range_y.options.basis;
    
    switch (O.key) {
        case "top-left":
            __key = {
                x1: gmarg.left,
                y1: gmarg.top,
                x2: gmarg.left + parseInt(bb.width) + gpad.left + gpad.right,
                y2: gmarg.top + parseInt(bb.height) + gpad.top + gpad.bottom
            }
            break;
        case "top-right":
            __key = {
                x1: width - gmarg.right - parseInt(bb.width) - gpad.left - gpad.right,
                y1: gmarg.top,
                x2: width - gmarg.right,
                y2: gmarg.top + parseInt(bb.height) + gpad.top + gpad.bottom
            }
            break;
        case "bottom-left":
            __key = {
                x1: gmarg.left,
                y1: height - gmarg.bottom - parseInt(bb.height) - gpad.top - gpad.bottom,
                x2: gmarg.left + parseInt(bb.width) + gpad.left + gpad.right,
                y2: height - gmarg.bottom
            }
            break;
        case "bottom-right":
            __key = {
                x1: width - gmarg.right - parseInt(bb.width) - gpad.left - gpad.right,
                y1: height -gmarg.bottom - parseInt(bb.height) - gpad.top - gpad.bottom,
                x2: width - gmarg.right,
                y2: height - gmarg.bottom
            }
            break;
        default:
            TK.warn("Unsupported key", O.key);
    }
    _key.setAttribute("transform", "translate(" + __key.x1 + "," + __key.y1 + ")");
    _key_bg.setAttribute("x", __key.x1);
    _key_bg.setAttribute("y", __key.y1);
    _key_bg.setAttribute("width", __key.x2 - __key.x1);
    _key_bg.setAttribute("height", __key.y2 - __key.y1);
}
function draw_title() {
    var _title  = this._title;
    if (!_title) return;

    _title.textContent = this.options.title;

    /* FORCE_RELAYOUT */
    TK.S.add(function() {
        var mtop    = parseInt(TK.get_style(_title, "margin-top") || 0);
        var mleft   = parseInt(TK.get_style(_title, "margin-left") || 0);
        var mbottom = parseInt(TK.get_style(_title, "margin-bottom") || 0);
        var mright  = parseInt(TK.get_style(_title, "margin-right") || 0);
        var bb      = _title.getBoundingClientRect();
        var x,y,anchor, range_x = this.range_x, range_y = this.range_y;
        switch (this.options.title_position) {
            case "top-left":
                anchor = "start";
                x = mleft;
                y = mtop + bb.height / 2;
                break;
            case "top":
                anchor = "middle";
                x = range_x.options.basis / 2;
                y = mtop + bb.height / 2;
                break;
            case "top-right":
                anchor = "end";
                x = range_x.options.basis - mright;
                y = mtop + bb.height / 2;
                break;
            case "left":
                anchor = "start";
                x = mleft;
                y = range_y.options.basis / 2;
                break;
            case "center":
                anchor = "middle";
                x = range_x.options.basis / 2;
                y = range_y.options.basis / 2;
                break;
            case "right":
                anchor = "end";
                x = range_x.options.basis - mright;
                y = range_y.options.basis / 2;
                break;
            case "bottom-left":
                anchor = "start";
                x = mleft;
                y = range_y.options.basis - mtop - bb.height / 2;
                break;
            case "bottom":
                anchor = "middle";
                x = range_x.options.basis / 2;
                y = range_y.options.basis - mtop - bb.height / 2;
                break;
            case "bottom-right":
                anchor = "end";
                x = range_x.options.basis - mright;
                y = range_y.options.basis - mtop - bb.height / 2;
                break;
            default:
                TK.warn("Unsupported title_position", this.options.title_position);
        }
        TK.S.add(function() {
            _title.setAttribute("text-anchor", anchor);
            _title.setAttribute("x", x);
            _title.setAttribute("y", y);
        }, 1);
    }.bind(this));
}

/**
 * TK.Chart is an SVG image containing one or more Graphs. TK.Chart
 * extends {@link TK.Widget} and contains a {@link TK.Grid} and two
 * {@link TK.Range}s.
 *
 * @param {Object} [options={ }] - An object containing initial options.
 * 
 * @property {String|Boolean} [options.title=""] - A title for the Chart.
 *   Set to `false` to remove the title from the DOM.
 * @property {String} [options.title_position="top-right"] - Position of the
 *   title inside of the chart. Possible values are
 *   <code>"top-left"</code>, <code>"top"</code>, <code>"top-right"</code>,
 *   <code>"left"</code>, <code>"center"</code>, <code>"right"</code>,
 *   <code>"bottom-left"</code>, <code>"bottom"</code> and
 *   <code>"bottom-right"</code>.
 * @property {Boolean|String} [options.key=false] - If set to a string
 *   a key is rendered into the chart at the given position. The key
 *   will detail names and colors of the graphs inside of this chart.
 *   Possible values are <code>"top-left"</code>, <code>"top-right"</code>,
 *   <code>"bottom-left"</code> and <code>"bottom-right"</code>. Set to `false`
 *   to remove the key from the DOM.
 * @property {Object} [options.key_size={x:20,y:10}] - Size of the colored
 *   rectangles inside of the key describing individual graphs.
 * @property {Array<Object>} [options.grid_x=[]] - An array containing
 *   objects with the following optional members to draw the grid:
 * @property {Number} [options.grid_x.pos] - The value where to draw  grid line and corresponding label.
 * @property {String} [options.grid_x.color] - A valid CSS color string to colorize the elements.
 * @property {String} [options.grid_x.class] - A class name for the elements.
 * @property {String} [options.grid_x.label] - A label string.
 * @property {Array<Object>} [options.grid_y=[]] - An array containing
 *   objects with the following optional members to draw the grid:
 * @property {Number} [options.grid_y.pos] - The value where to draw  grid line and corresponding label.
 * @property {String} [options.grid_y.color] - A valid CSS color string to colorize the elements.
 * @property {String} [options.grid_y.class] - A class name for the elements.
     * @property {String} [options.grid_y.label] - A label string.
 * @property {Boolean} [options.show_grid=true] - Set to <code>false</code> to
 *   hide the grid.
 * @property {Function|Object} [options.range_x={}] - Either a function
 *   returning a {@link TK.Range} or an object containing options for a
 *   new {@link TK.Range}.
 * @property {Function|Object} [options.range_y={}] - Either a function
 *   returning a {@link TK.Range} or an object containing options for a
 *   new {@link TK.Range}.
 * @property {Object|Function} [options.range_z={ scale: "linear", min: 0, max: 1 }] -
 *   Either a function returning a {@link TK.Range} or an object
 *   containing options for a new {@link TK.Range}.
 * @property {Number} [options.importance_label=4] - Multiplicator of
 *   square pixels on hit testing labels to gain importance.
 * @property {Number} [options.importance_handle=1] - Multiplicator of
 *   square pixels on hit testing handles to gain importance.
 * @property {Number} [options.importance_border=50] - Multiplicator of
 *   square pixels on hit testing borders to gain importance.
 * @property {Array<Object>} [options.handles=[]] - An array of options for
 *   creating {@link TK.ResponseHandle} on init.
 * @property {Boolean} [options.show_handles=true] - Show or hide all
 *   handles.
 * 
 * @class TK.Chart
 * 
 * @extends TK.Widget
 */
function geom_set(value, key) {
    this.set_style(key, value+"px");
    TK.error("using deprecated '"+key+"' options");
}
TK.Chart = TK.class({
    _class: "Chart",
    Extends: TK.Widget,
    Implements: TK.Ranges,
    _options: Object.assign(Object.create(TK.Widget.prototype._options), {
        width: "int",
        height: "int",
        _width: "int",
        _height: "int",
        range_x: "object",
        range_y: "object",
        range_z: "object",
        key: "string",
        key_size: "object",
        title: "string",
        title_position: "string",
        resized: "boolean",
        
        importance_label:  "number",
        importance_handle: "number",
        importance_border: "number",
        handles: "array", 
        show_handles: "boolean",
        depth: "number",
    }),
    options: {
        grid_x: [],
        grid_y: [],
        range_x: {}, // an object with options for a range for the x axis
                     // or a function returning a TK.Range instance (only on init)
        range_y: {}, // an object with options for a range for the y axis
                     // or a function returning a TK.Range instance (only on init)
        range_z: { scale: "linear", min: 0, max: 1 }, // TK.Range z options
        key: false,  // key draws a description for the graphs at the given
                     // position, use false for no key
        key_size: {x:20, y:10}, // size of the key rects
        title:   "", // a title for the chart
        title_position: "top-right", // the position of the title
        resized: false,
        
        importance_label:  4,   // multiplicator of square pixels on hit testing
                                // labels to gain importance
        importance_handle: 1,   // multiplicator of square pixels on hit testing
                                // handles to gain importance
        importance_border: 50,  // multiplicator of square pixels on hit testing
                                // borders to gain importance
        handles:           [],  // list of bands to create on init
        show_handles: true,
    },
    static_events: {
        set_width: geom_set,
        set_height: geom_set,
        
        mousewheel: STOP,
        DOMMouseScroll: STOP,
        set_depth: function(value) {
            this.range_z.set("basis", value);
        },
        set_show_handles: function(value) {
            (value ? show_handles : hide_handles).call(this);
        },
    },
    initialize: function (options) {
        var E, S;
        /**
         * @member {Array} TK.Chart#graphs - An array containing all SVG paths acting as graphs.
         */
        this.graphs = [];
        /**
         * @member {Array} TK.Chart#handles - An array containing all {@link TK.ResponseHandle} instances.
         */
        this.handles = [];
        TK.Widget.prototype.initialize.call(this, options);
        
        /**
         * @member {TK.Range} TK.Chart#range_x - The {@link TK.Range} for the x axis. 
         */
        /**
         * @member {TK.Range} TK.Chart#range_y - The {@link TK.Range} for the y axis.
         */
        this.add_range(this.options.range_x, "range_x");
        this.add_range(this.options.range_y, "range_y");
        this.add_range(this.options.range_z, "range_z");
        this.range_y.set("reverse", true, true, true);
        
        /**
         * @member {HTMLDivElement} TK.Chart#element - The main DIV container.
         *   Has class <code>toolkit-chart</code>.
         */
        if (!(E = this.element)) this.element = E = TK.element("div");
        TK.add_class(E, "toolkit-chart");
        this.widgetize(E, true, true, true);
        
        this.svg = S = TK.make_svg("svg");

        if (!this.options.width)
            this.options.width = this.range_x.options.basis;
        if (!this.options.height)
            this.options.height = this.range_y.options.basis;
        
        /** 
         * @member {SVGGroup} TK.Chart#_graphs - The SVG group containing all graphs.
         *      Has class <code>toolkit-graphs</code>.
         */
        this._graphs = TK.make_svg("g", {"class": "toolkit-graphs"});
        S.appendChild(this._graphs);
        E.appendChild(S);
        
        if (this.options.width) this.set("width", this.options.width);
        if (this.options.height) this.set("height", this.options.height);
        
        /** 
         * @member {SVGGroup} TK.Chart#_handles - The SVG group containing all handles.
         *      Has class <code>toolkit-handles</code>.
         */
        this._handles = TK.make_svg("g", {"class": "toolkit-handles"});
        this.svg.appendChild(this._handles);
        this.svg.onselectstart = function () { return false; };
        this.add_handles(this.options.handles);
    },
    resize: function () {
        var E = this.element;
        var O = this.options;
        var S = this.svg;

        TK.Widget.prototype.resize.call(this);

        var tmp = TK.css_space(S, "border", "padding");
        var w = TK.inner_width(E) - tmp.left - tmp.right;
        var h = TK.inner_height(E) - tmp.top - tmp.bottom;

        if (w > 0 && O._width !== w) {
            this.set("_width", w);
            this.range_x.set("basis", w);
            this.invalid._width = true;
            this.trigger_draw();
        }
        if (h > 0 && O._height !== h) {
            this.set("_height", h);
            this.range_y.set("basis", h);
            this.invalid._height = true;
            this.trigger_draw();
        }
    },
    redraw: function () {
        var I = this.invalid;
        var E = this.svg;
        var O = this.options;

        TK.Widget.prototype.redraw.call(this);

        if (I.validate("ranges", "_width", "_height", "range_x", "range_y")) {
            /* we need to redraw both key and title, because
             * they do depend on the size */
            I.title = true;
            I.key = true;
            var w = O._width;
            var h = O._height;
            if (w && h) {
                E.setAttribute("width", w + "px");
                E.setAttribute("height", h + "px");
            }
        }

        if (I.graphs) {
            for (var i = 0; i < this.graphs.length; i++) {
                this.graphs[i].redraw();
            }
        }
        if (I.validate("title", "title_position")) {
            draw_title.call(this);
        }
        if (I.validate("key", "key_size", "graphs")) {
            draw_key.call(this);
        }
        if (I.show_handles) {
            I.show_handles = false;
            if (O.show_handles) {
                this._handles.style.removeProperty("display");
            } else {
                this._handles.style.display = "none";
            }
        }
    },
    destroy: function () {
        for (var i = 0; i < this._graphs.length; i++) {
            this._graphs[i].destroy();
        }
        this._graphs.remove();
        this._handles.remove();
        TK.Widget.prototype.destroy.call(this);
    },
    add_child: function(child) {
        if (child instanceof TK.Graph) {
            this.add_graph(child);
            return;
        }

        TK.Widget.prototype.add_child.call(this, child);
    },
    remove_child: function(child) {
        if (child instanceof TK.Graph) {
            this.remove_graph(child);
            return;
        }

        TK.Widget.prototype.remove_child.call(this, child);
    },
    /**
     * Add a graph to the chart.
     *
     * @method TK.Chart#add_graph
     * 
     * @param {Object} graph - The graph to add. This can be either an
     *  instance of {@link TK.Graph} or an object of options to
     *  {@link TK.Graph}.
     * 
     * @returns {Object} The instance of {@link TK.Graph}.
     * 
     * @emits TK.Chart#graphadded
     */
    add_graph: function (options) {
        var g;

        if (TK.Graph.prototype.isPrototypeOf(options)) {
            g = options;
        } else {
            g = new TK.Graph(options);
        }

        g.set("container", this._graphs);
        if (!g.options.range_x) g.set("range_x", this.range_x);
        if (!g.options.range_y) g.set("range_y", this.range_y);

        this.graphs.push(g);
        g.add_event("set", function (key, value, obj) {
            if (key === "color" || key === "class" || key === "key") {
                this.invalid.graphs = true;
                this.trigger_draw();
            }
        }.bind(this));
        /**
         * Is fired when a graph was added. Arguments are the graph
         * and its position in the array.
         * 
         * @event TK.Chart#graphadded
         * 
         * @param {TK.Graph} graph - The {@link TK.Graph} which was added.
         * @param {int} id - The ID of the added {@link TK.Graph}.
         */
        this.fire_event("graphadded", g, this.graphs.length - 1);

        this.invalid.graphs = true;
        this.trigger_draw();
        TK.Widget.prototype.add_child.call(this, g);
        return g;
    },
    /**
     * Remove a graph from the chart.
     *
     * @method TK.Chart#remove_graph
     * 
     * @param {TK.Graph} graph - The {@link TK.Graph} to remove.
     * 
     * @emits TK.Chart#graphremoved
     */
    remove_graph: function (g) {
        var i;
        if ((i = this.graphs.indexOf(g)) !== -1) {
            /**
             * Is fired when a graph was removed. Arguments are the graph
             * and its position in the array.
             * 
             * @event TK.Chart#graphremoved
             * 
             * @param {TK.Graph} graph - The {@link TK.Graph} which was removed.
             * @param {int} id - The ID of the removed {@link TK.Graph}.
             */
            this.fire_event("graphremoved", g, i);
            g.destroy();
            this.graphs.splice(i, 1);
            TK.Widget.prototype.remove_child.call(this, g);
            this.invalid.graphs = true;
            this.trigger_draw();
        }
    },
    /**
     * Remove all graphs from the chart.
     *
     * @method TK.Chart#empty
     * 
     * @emits TK.Chart#emptied
     */
    empty: function () {
        this.graphs.map(this.remove_graph, this);
        /**
         * Is fired when all graphs are removed from the chart.
         * 
         * @event TK.Chart#emptied
         */
        this.fire_event("emptied");
    },
    
    /**
     * Add a new handle to the widget. Options is an object containing
     * options for the {@link TK.ResponseHandle}.
     * 
     * @method TK.Chart#add_handle
     * 
     * @param {Object} [options={ }] - An object containing initial options. - The options for the {@link TK.ResponseHandle}.
     * @param {Object} [type=TK.ResponseHandle] - A widget class to be used as the new handle.
     * 
     * @emits TK.Chart#handleadded
     */
    add_handle: function (options, type) {
        type = type || TK.ResponseHandle;
        options.container = this._handles;
        if (options.range_x === void(0))
            options.range_x = function () { return this.range_x; }.bind(this);
        if (options.range_y === void(0))
            options.range_y = function () { return this.range_y; }.bind(this);
        if (options.range_z === void(0))
            options.range_z = function () { return this.range_z; }.bind(this);
        
        options.intersect = this.intersect.bind(this);
        
        var h = new type(options);
        this.handles.push(h);
        if (this.options.show_handles)
            this.add_child(h);
        /**
         * Is fired when a new handle was added.
         * 
         * @param {TK.ResponseHandle} handle - The {@link TK.ResponseHandle} which was added.
         * 
         * @event TK.Chart#handleadded
         */
        this.fire_event("handleadded", h);
        return h;
    },
    /**
     * Add multiple new {@link TK.ResponseHandle} to the widget. Options is an array
     * of objects containing options for the new instances of {@link TK.ResponseHandle}.
     * 
     * @method TK.Chart#add_handles
     * 
     * @param {Array<Object>} options - An array of options objects for the {@link TK.ResponseHandle}.
     * @param {Object} [type=TK.ResponseHandle] - A widget class to be used for the new handles.
     */
    add_handles: function (handles, type) {
        for (var i = 0; i < handles.length; i++)
            this.add_handle(handles[i], type);
    },
    /**
     * Remove a handle from the widget.
     * 
     * @method TK.Chart#remove_handle
     * 
     * @param {TK.ResponseHandle} handle - The {@link TK.ResponseHandle} to remove.
     * 
     * @emits TK.Chart#handleremoved
     */
    remove_handle: function (handle) {
        // remove a handle from the widget.
        for (var i = 0; i < this.handles.length; i++) {
            if (this.handles[i] === handle) {
                if (this.options.show_handles)
                    this.remove_child(handle);
                this.handles[i].destroy();
                this.handles.splice(i, 1);
                /**
                 * Is fired when a handle was removed.
                 * 
                 * @event TK.Chart#handleremoved
                 */
                this.fire_event("handleremoved");
                break;
            }
        }
    },
    /**
     * Remove multiple or all {@link TK.ResponseHandle} from the widget.
     * 
     * @method TK.Chart#remove_handles
     * 
     * @param {Array<TK.ResponseHandle>} handles - An array of
     *   {@link TK.ResponseHandle} instances. If the argument reveals to
     *   `false`, all handles are removed from the widget.
     */
    remove_handles: function (handles) {
        var H = handles || this.handles.slice();
        for (var i = 0; i < H.length; i++) {
            this.remove_handle(H[i]);
        }
        if (!handles) {
            this.handles = [];
            /**
             * Is fired when all handles are removed.
             * 
             * @event TK.Chart#emptied
             */
            this.fire_event("emptied");
        }
    },
    
    intersect: function (X, handle) {
        // this function walks over all known handles and asks for the coords
        // of the label and the handle. Calculates intersecting square pixels
        // according to the importance set in options. Returns an object
        // containing intersect (the amount of intersecting square pixels) and
        // count (the amount of overlapping elements)
        var c = 0;
        var a = 0, _a;
        var O = this.options;
        var importance_handle = O.importance_handle
        var importance_label = O.importance_label

        for (var i = 0; i < this.handles.length; i++) {
            var h = this.handles[i];
            if (h === handle || !h.get("active") || !h.get("show_handle")) continue;
            _a = calculate_overlap(X, h.handle);

            if (_a) {
                c ++;
                a += _a * importance_handle;
            }
            
            _a = calculate_overlap(X, h.label);

            if (_a) {
                c ++;
                a += _a * importance_label;
            }
        }
        if (this.bands && this.bands.length) {
            for (var i = 0; i < this.bands.length; i++) {
                var b = this.bands[i];
                if (b === handle || !b.get("active") || !b.get("show_handle")) continue;
                _a = calculate_overlap(X, b.handle);

                if (_a > 0) {
                    c ++;
                    a += _a * importance_handle;
                }
                
                _a = calculate_overlap(X, b.label);
                if (_a > 0) {
                    c ++;
                    a += _a * importance_label;
                }
            }
        }
        /* calculate intersection with border */
        _a = calculate_overlap(X, [ 0, 0, this.range_x.options.basis, this.range_y.options.basis ]);
        a += ((X[2] - X[0]) * (X[3] - X[1]) - _a) * O.importance_border;
        return {intersect: a, count: c};
    },
});
/**
 * @member {TK.Grid} TK.Chart#grid - The grid element of the chart.
 *   Has class <code>toolkit-grid</code>.
 */
TK.ChildWidget(TK.Chart, "grid", {
    create: TK.Grid,
    show: true,
    append: function() {
        this.svg.insertBefore(this.grid.element, this.svg.firstChild);
    },
    map_options: {
        grid_x: "grid_x",
        grid_y: "grid_y",
    },
    default_options: function () {
        return {
            range_x: this.range_x,
            range_y: this.range_y,
        };
    },
});
function key_hover_cb(ev) {
    var b = ev.type === "mouseenter";
    TK.toggle_class(this, "toolkit-hover", b);
    /* this.nextSibling is the key */
    TK.toggle_class(this.nextSibling, "toolkit-hover", b);
}
/**
 * @member {SVGRect} TK.Chart#_key_background - The SVG rectangle of the key.
 *   Has class <code>toolkit-background</code>.
 */
TK.ChildElement(TK.Chart, "key_background", {
    option: "key",
    display_check: function(v) {
        return !!v;
    },
    create: function() {
        var k = TK.make_svg("rect", {"class": "toolkit-background"});
        k.addEventListener("mouseenter", key_hover_cb);
        k.addEventListener("mouseleave", key_hover_cb);
        return k;
    },
    append: function() {
        this.svg.appendChild(this._key_background);
    },
});
/**
 * @member {SVGGroup} TK.Chart#_key - The SVG group containing all descriptions.
 *   Has class <code>toolkit-key</code>.
 */
TK.ChildElement(TK.Chart, "key", {
    option: "key",
    display_check: function(v) {
        return !!v;
    },
    create: function() {
        var key = TK.make_svg("g", {"class": "toolkit-key"});
        key.appendChild(TK.make_svg("text", {"class": "toolkit-key-text"}));
        return key;
    },
    append: function() {
        this.svg.appendChild(this._key);
    },
});
/**
 * @member {SVGText} TK.Chart#_title - The title of the chart.
 *   Has class <code>toolkit-title</code>.
 */
TK.ChildElement(TK.Chart, "title", {
    option: "title",
    display_check: function(v) {
        return typeof(v) === "string" && v.length;
    },
    create: function() {
        return TK.make_svg("text", {
            "class": "toolkit-title",
            style: "dominant-baseline: central;"
        });
    },
    append: function() {
        var svg = this.svg;
        svg.insertBefore(this._title, svg.firstChild);
    },
});
})(this, this.TK);