Source: modules/grid.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 draw_lines(a, mode, last) {
    var labels = new Array(a.length);
    var coords = new Array(a.length);
    var i, label, obj;

    for (i = 0; i < a.length; i++) {
        obj = a[i];
        if (obj.label) {
            label = TK.make_svg("text");
            label.textContent = obj.label;
            label.style["dominant-baseline"] = "central";
            TK.add_class(label, "toolkit-grid-label");
            TK.add_class(label, mode ? "toolkit-horizontal" : "toolkit-vertical");
            if (obj["class"]) TK.add_class(label, obj["class"]);

            this.element.appendChild(label);
            labels[i] = label;
        }
    }

    var w  = this.range_x.options.basis;
    var h  = this.range_y.options.basis;


    TK.S.add(function() {
        /* FORCE_RELAYOUT */

        for (i = 0; i < a.length; i++) {
            obj = a[i];
            label = labels[i];
            if (!label) continue;
            var bb;
            try {
                bb = label.getBBox();
            } catch(e) {
                // if we are hidden, this may throw
                // we should force redraw at some later point, but
                // its hard to do. the grid should really be deactivated
                // by an option.
                continue;
            }
            var tw = bb.width;
            var th = bb.height;
            var p  = TK.get_style(label, "padding").split(" ");
            if (p.length < 2)
                p[1] = p[2] = p[3] = p[0];
            if (p.length < 3) {
                p[2] = p[0];
                p[3] = p[1];
            }
            if (p.length < 4)
                p[3] = p[1];
            var pt = parseInt(p[0]) || 0;
            var pr = parseInt(p[1]) || 0;
            var pb = parseInt(p[2]) || 0;
            var pl = parseInt(p[3]) || 0;
            var x, y;
            if (mode) {
                y = Math.max(th / 2, Math.min(h - th / 2 - pt, this.range_y.val2px(obj.pos)));
                if (y > last) continue;
                x = w - tw - pl;
                coords[i] = {
                    x : x,
                    y : y,
                    m : tw + pl + pr,
                };
                last = y - th;
            } else {
                x = Math.max(pl, Math.min(w - tw - pl, this.range_x.val2px(obj.pos) - tw / 2));
                if (x < last) continue;
                y = h-th/2-pt;
                coords[i] = {
                    x : x,
                    y : y,
                    m : th + pt + pb,
                };
                last = x + tw;
            }
        }

        TK.S.add(function() {
            for (i = 0; i < a.length; i++) {
                label = labels[i];
                if (label) {
                    obj = coords[i];
                    if (obj) {
                        label.setAttribute("x", obj.x);
                        label.setAttribute("y", obj.y);
                    } else {
                        if (label.parentElement == this.element)
                            this.element.removeChild(label);
                    }
                }
            }

            for (i = 0; i < a.length; i++) {
                obj = a[i];
                label = coords[i];
                var m;
                if (label) m = label.m;
                else m = 0;

                if ((mode && obj.pos === this.range_y.options.min)
                || ( mode && obj.pos === this.range_y.options.max)
                || (!mode && obj.pos === this.range_x.options.min)
                || (!mode && obj.pos === this.range_x.options.max))
                    continue;
                var line = TK.make_svg("path");
                TK.add_class(line, "toolkit-grid-line");
                TK.add_class(line, mode ? "toolkit-horizontal" : "toolkit-vertical");
                if (obj["class"]) TK.add_class(line, obj["class"]);
                if (obj.color) line.setAttribute("style", "stroke:" + obj.color);
                if (mode) {
                    // line from left to right
                    line.setAttribute("d", "M0 " + Math.round(this.range_y.val2px(obj.pos))
                        + ".5 L"  + (this.range_x.options.basis - m) + " "
                        + Math.round(this.range_y.val2px(obj.pos)) + ".5");
                } else {
                    // line from top to bottom
                    line.setAttribute("d", "M" + Math.round(this.range_x.val2px(obj.pos))
                        + ".5 0 L"  + Math.round(this.range_x.val2px(obj.pos))
                        + ".5 " + (this.range_y.options.basis - m));
                }
                this.element.appendChild(line);
            }
        }.bind(this), 1);
    }.bind(this));
}
TK.Grid = TK.class({
    /**
     * TK.Grid creates a couple of lines and labels in a SVG
     * image on the x and y axis. It is used in e.g. {@link TK.Graph} and
     * {@link TK.FrequencyResponse} to draw markers and values. TK.Grid needs a
     * parent SVG image do draw into. The base element of a TK.Grid is a
     * SVG group containing all the labels and lines.
     *
     * @class TK.Grid
     * 
     * @param {Object} [options={ }] - An object containing initial options.
     * 
     * @property {Array<Object>} [options.grid_x=[]] - Array for vertical grid line definitions with the members:
     * @property {Number} [options.grid_x.pos] - The value where to draw grid line and correspon ding 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=[]] - Array for horizontal grid lines with the members:
     * @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 {Function|Object} [options.range_x={}] - A function returning
     *   a {@link TK.Range} instance for vertical grid lines or an object
     *   containing options. for a new {@link Range}.
     * @property {Function|Object} [options.range_y={}] - A function returning
     *   a {@link TK.Range} instance for horizontal grid lines or an object
     *   containing options. for a new {@link Range}.
     * @property {Number} [options.width=0] - Width of the grid.
     * @property {Number} [options.height=0] - Height of the grid.
     * 
     * @extends TK.Widget
     * 
     * @mixes TK.Ranges
     */
    _class: "Grid",
    Extends: TK.Widget,
    Implements: TK.Ranges,
    _options: Object.assign(Object.create(TK.Widget.prototype._options), {
        grid_x: "array",
        grid_y: "array",
        range_x: "object",
        range_y: "object",
        width: "number",
        height: "number",
    }),
    options: {
        grid_x:  [],
        grid_y:  [],
        range_x: {}, 
        range_y: {},
        width:   0,
        height:  0
    },
    initialize: function (options) {
        TK.Widget.prototype.initialize.call(this, options);
        /**
         * @member {SVGGroup} TK.Grid#element - The main SVG group containing all grid elements. Has class <code>toolkit-grid</code>.
         */
        this.element = this.widgetize(
                       TK.make_svg("g", {"class": "toolkit-grid"}), true, true, true);
        /**
         * @member {TK.Range} TK.Grid#range_x - The range for the x axis. 
         */
        /**
         * @member {TK.Range} TK.Grid#range_y - The range for the y axis.
         */
        this.add_range(this.options.range_x, "range_x");
        this.add_range(this.options.range_y, "range_y");
        if (this.options.width)
            this.set("width", this.options.width);
        if (this.options.height)
            this.set("height", this.options.width);
        this.invalidate_ranges = function (key, value) {
            this.invalid.range_x = true;
            this.invalid.range_y = true;
            this.trigger_draw();
        }.bind(this);
        this.range_x.add_event("set", this.invalidate_ranges);
        this.range_y.add_event("set", this.invalidate_ranges);
    },
    
    redraw: function () {
        var I = this.invalid, O = this.options;
        if (I.validate("grid_x", "grid_y", "range_x", "range_y")) {
            TK.empty(this.element);

            draw_lines.call(this, O.grid_x, false, 0);
            draw_lines.call(this, O.grid_y, true, this.range_y.options.basis);
        }
        TK.Widget.prototype.redraw.call(this);
    },
    destroy: function () {
        this.range_x.remove_event("set", this.invalidate_ranges);
        this.range_y.remove_event("set", this.invalidate_ranges);
        TK.Widget.prototype.destroy.call(this);
    },
    // GETTER & SETTER
    set: function (key, value) {
        this.options[key] = value;
        switch (key) {
            case "grid_x":
            case "grid_y":
                /**
                 * Is fired when the grid has changed.
                 * 
                 * @event TK.Grid#gridchanged
                 * 
                 * @param {Array} grid_x - The grid elements for x axis.
                 * @param {Array} grid_y - The grid elements for y axis.
                 */
                this.fire_event("gridchanged", this.options.grid_x, this.options.grid_y);
                break;
            case "width":
                this.range_x.set("basis", value);
                break;
            case "height":
                this.range_y.set("basis", value);
                break;
        }
        TK.Widget.prototype.set.call(this, key, value);
    }
});
})(this, this.TK);