ui/legendWidget.js

var svgWidget = require('./svgWidget');
var inherit = require('../inherit');
var registerWidget = require('../registry').registerWidget;

/**
 * @typedef {object} geo.gui.legendWidget.categorySpec
 *
 * @property {string} name The name of the category.
 * @property {string} type The type of the category.  `point` shows as a
 *   circle, `line` as a line segment, all others as a rounded rectangle.
 * @property {geo.gui.legendWidget.styleSpec} style The style for the category.
 */

/**
 * Style specification for a legend category.
 *
 * @typedef {geo.feature.styleSpec} geo.gui.legendWidget.styleSpec
 * @extends geo.feature.styleSpec
 * @property {boolean|function} [stroke=true] True to stroke legend.
 * @property {geo.geoColor|function} [strokeColor] Color to stroke each legend.
 * @property {number|function} [strokeOpacity=1] Opacity for each legend's
 *   stroke.  Opacity is on a [0-1] scale.
 * @property {number|function} [strokeWidth=1.5] The weight of the legend's
 *   stroke in pixels.
 * @property {boolean|function} [fill=true] True to fill legend.
 * @property {geo.geoColor|function} [fillColor] Color to fill each legend.
 * @property {number|function} [fillOpacity=1] Opacity for each legend.
 *   Opacity is on a [0-1] scale.
 */

/**
 * Create a new instance of class geo.gui.legendWidget.
 *
 * @class
 * @alias geo.gui.legendWidget
 * @extends geo.gui.svgWidget
 * @param {geo.gui.widget.spec} arg Options for the widget.
 * @returns {geo.gui.legendWidget}
 */
var legendWidget = function (arg) {
  'use strict';
  if (!(this instanceof legendWidget)) {
    return new legendWidget(arg);
  }
  svgWidget.call(this, arg);

  var d3 = require('../svg/svgRenderer').d3;
  var geo_event = require('../event');

  var m_this = this,
      m_categories = [],
      m_top = null,
      m_group = null,
      m_border = null,
      m_spacing = 20, // distance in pixels between lines
      m_padding = 12; // padding in pixels inside the border

  /**
   * Get or set the category array associated with the legend.
   *
   * @param {geo.gui.legendWidget.categorySpec[]} [arg] The categories to
   *   display.
   * @returns {geo.gui.legendWidget.categorySpec[]|this} The current categories
   *   or the widget instance.
   */
  this.categories = function (arg) {
    if (arg === undefined) {
      return m_categories.slice();
    }
    m_categories = arg.slice().map(function (d) {
      if (d.type === 'line') {
        d.style.fill = false;
        d.style.stroke = true;
      }
      return d;
    });
    m_this.draw();
    return m_this;
  };

  /**
   * Return the size of the widget.
   *
   * @returns {geo.screenSize}
   */
  this.size = function () {
    var width = 1, height;
    var test = d3.select(m_this.canvas()).append('text')
          .style('opacity', 1e-6);

    m_categories.forEach(function (d) {
      test.text(d.name);
      width = Math.max(width, test.node().getBBox().width);
    });
    test.remove();

    height = m_spacing * (m_categories.length + 1);
    return {
      width: width + 50,
      height: height
    };
  };

  /**
   * Redraw the legend.
   *
   * @returns {this}
   */
  this.draw = function () {

    m_this._init();
    function applyColor(selection) {
      selection.style('fill', function (d) {
        if (d.style.fill || d.style.fill === undefined) {
          return d.style.fillColor;
        } else {
          return 'none';
        }
      })
        .style('fill-opacity', function (d) {
          if (d.style.fillOpacity === undefined) {
            return 1;
          }
          return d.style.fillOpacity;
        })
        .style('stroke', function (d) {
          if (d.style.stroke || d.style.stroke === undefined) {
            return d.style.strokeColor;
          } else {
            return 'none';
          }
        })
        .style('stroke-opacity', function (d) {
          if (d.style.strokeOpacity === undefined) {
            return 1;
          }
          return d.style.strokeOpacity;
        })
        .style('stroke-width', function (d) {
          if (d.style.strokeWidth === undefined) {
            return 1.5;
          }
          return d.style.strokeWidth;
        });
    }

    m_border.attr('height', m_this.size().height + 2 * m_padding)
      .style('display', null);

    var scale = m_this._scale();

    var labels = m_group.selectAll('g.geo-label')
          .data(m_categories, function (d) { return d.name; });

    var g = labels.enter().append('g')
          .attr('class', 'geo-label')
          .attr('transform', function (d, i) {
            return 'translate(0,' + scale.y(i) + ')';
          });

    applyColor(g.filter(function (d) {
      return d.type !== 'point' && d.type !== 'line';
    }).append('rect')
      .attr('x', 0)
      .attr('y', -6)
      .attr('rx', 5)
      .attr('ry', 5)
      .attr('width', 40)
      .attr('height', 12)
    );

    applyColor(g.filter(function (d) {
      return d.type === 'point';
    }).append('circle')
      .attr('cx', 20)
      .attr('cy', 0)
      .attr('r', 6)
    );

    applyColor(g.filter(function (d) {
      return d.type === 'line';
    }).append('line')
      .attr('x1', 0)
      .attr('y1', 0)
      .attr('x2', 40)
      .attr('y2', 0)
    );

    g.append('text')
      .attr('x', '50px')
      .attr('y', 0)
      .attr('dy', '0.3em')
      .text(function (d) {
        return d.name;
      });

    m_this.reposition();

    return m_this;
  };

  /**
   * Get scales for the x and y axis for the current size.
   *
   * @returns {object} An object with `x` and `y`, each containing a d3 scale.
   */
  this._scale = function () {
    return {
      x: d3.scaleLinear()
        .domain([0, 1])
        .range([0, m_this.size().width]),
      y: d3.scaleLinear()
        .domain([0, m_categories.length - 1])
        .range([m_padding / 2, m_this.size().height - m_padding / 2])
    };
  };

  /**
   * Private initialization.  Creates the widget's DOM container and internal
   * variables.
   * @private
   */
  this._init = function () {
    // adding categories redraws the entire thing by calling _init, see
    // the m_top.remove() line below
    if (!m_top) {
      m_this._createCanvas();
      m_this._appendCanvasToParent();
    }

    // total size = interior size + 2 * padding + 2 * width of the border
    var w = m_this.size().width + 2 * m_padding + 4,
        h = m_this.size().height + 2 * m_padding + 4;

    // @todo - removing after creating to maintain the appendChild structure
    if (m_top) {
      m_top.remove();
    }

    d3.select(m_this.canvas()).attr('width', w).attr('height', h);

    m_top = d3.select(m_this.canvas()).append('g');
    m_group = m_top
      .append('g')
      .attr('transform', 'translate(' + [m_padding + 2, m_padding + 2] + ')');
    m_border = m_group.append('rect')
      .attr('x', -m_padding)
      .attr('y', -m_padding)
      .attr('width', w - 4)
      .attr('height', h - 4)
      .attr('rx', 3)
      .attr('ry', 3);
    m_border
      .style('stroke', 'black')
      .style('stroke-width', '1.5px')
      .style('fill', 'white')
      .style('fill-opacity', 0.75)
      .style('display', 'none');
    m_group.on('mousedown', function (evt) {
      evt.stopPropagation();
    });
    m_group.on('mouseover', function () {
      m_border.transition()
        .duration(250)
        .style('fill-opacity', 1);
    });
    m_group.on('mouseout', function () {
      m_border.transition()
        .duration(250)
        .style('fill-opacity', 0.75);
    });

    m_this.reposition();
  };

  this.geoOn(geo_event.resize, function () {
    m_this.draw();
  });

};

inherit(legendWidget, svgWidget);

registerWidget('dom', 'legend', legendWidget);
module.exports = legendWidget;