widgets/chart.js

/*
 * This file is part of AUX.
 *
 * AUX 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.
 *
 * AUX 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
 * 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
 */

import { defineChildElement } from './../widget_helpers.js';
import { S } from '../dom_scheduler.js';
import {
  empty,
  CSSSpace,
  getStyle,
  element,
  addClass,
  innerWidth,
  toggleClass,
  innerHeight,
} from '../utils/dom.js';
import { defineRange } from '../utils/define_range.js';
import { makeSVG } from '../utils/svg.js';
import { error, warn } from '../utils/log.js';
import { Widget } from './widget.js';
import { Graph } from './graph.js';
import { ChartHandle } from './charthandle.js';
import { defineChildWidget } from '../child_widget.js';
import { Grid } from './grid.js';

function calculateOverlap(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 showHandles() {
  const handles = this.handles;

  if (handles.length === 0) return;
  if (handles[0].parent === this) return;

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

function hideHandles() {
  const handles = this.handles;

  if (handles.length === 0) return;
  if (handles[0].parent !== this) return;

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

function STOP(e) {
  e.preventDefault();
  e.stopPropagation();
  return false;
}
function drawKey() {
  let __key, bb;

  const _key = this._key;
  const _key_bg = this._key_background;

  if (!_key || !_key_bg) return;

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

  empty(_key.firstChild);

  const O = this.options;

  let disp = 'none';
  const gpad = CSSSpace(_key, 'padding');
  const gmarg = CSSSpace(_key, 'margin');
  let c = 0;
  let w = 0;
  let top = 0;
  const lines = [];
  for (let i = 0; i < this.graphs.length; i++) {
    if (this.graphs[i].get('key') !== false) {
      const t = makeSVG('tspan', {
        class: 'aux-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(getStyle(t, 'line-height')) : gpad.top;
      t.setAttribute('y', top + bb.height / 2);

      lines.push({
        x: parseInt(getStyle(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 (let i = 0; i < lines.length; i++) {
    const b = makeSVG('rect', {
      class: lines[i]['class'] + '.aux-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();
  const width = this.range_x.options.basis;
  const 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:
      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 drawLabel() {
  const _label = this._label;
  if (!_label) return;

  _label.textContent = this.options.label;

  /* FORCE_RELAYOUT */
  S.add(
    function () {
      const mtop = parseInt(getStyle(_label, 'margin-top') || 0);
      const mleft = parseInt(getStyle(_label, 'margin-left') || 0);
      const mbottom = parseInt(getStyle(_label, 'margin-bottom') || 0);
      const mright = parseInt(getStyle(_label, 'margin-right') || 0);
      const bb = _label.getBoundingClientRect();
      const range_x = this.range_x;
      const range_y = this.range_y;

      let x, y, anchor;

      switch (this.options.label_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 - mbottom - bb.height / 2;
          break;
        case 'bottom-right':
          anchor = 'end';
          x = range_x.options.basis - mright;
          y = range_y.options.basis - mbottom - bb.height / 2;
          break;
        default:
          warn('Unsupported label_position', this.options.label_position);
      }
      S.add(function () {
        _label.setAttribute('text-anchor', anchor);
        _label.setAttribute('x', x);
        _label.setAttribute('y', y);
      }, 1);
    }.bind(this)
  );
}

/**
 * Chart is an SVG image containing one or more Graphs. Chart
 * extends {@link Widget} and contains a {@link Grid} and two
 * {@link Range}s.
 *
 * @class Chart
 * @extends Widget
 *
 * @param {Object} [options={ }] - An object containing initial options.
 *
 * @property {String|Boolean} [options.label=""] - A label for the Chart.
 *   Set to `false` to remove the label from the DOM.
 * @property {String} [options.label_position="top-left"] - Position of the
 *   label 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 Range} or an object containing options for a
 *   new {@link Range}.
 * @property {Function|Object} [options.range_y={}] - Either a function
 *   returning a {@link Range} or an object containing options for a
 *   new {@link Range}.
 * @property {Object|Function} [options.range_z={ scale: "linear", min: 0, max: 1 }] -
 *   Either a function returning a {@link Range} or an object
 *   containing options for a new {@link 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 ChartHandle} on init.
 * @property {Boolean} [options.show_handles=true] - Show or hide all
 *   handles.
 * @property {Boolean} [options.square=false] - Keep the Graph as a square.
 *
 */
function geomSet(value, key) {
  this.setStyle(key, value + 'px');
  error("using deprecated '" + key + "' options");
}
export class Chart extends Widget {
  static get _options() {
    return Object.assign({}, Widget.getOptionTypes(), {
      width: 'int',
      height: 'int',
      _width: 'int',
      _height: 'int',
      range_x: 'object',
      range_y: 'object',
      range_z: 'object',
      key: 'string',
      key_size: 'object',
      label: 'string',
      label_position: 'string',
      resized: 'boolean',

      importance_label: 'number',
      importance_handle: 'number',
      importance_border: 'number',
      handles: 'array',
      show_handles: 'boolean',
      depth: 'number',
      square: 'boolean',
    });
  }

  static get options() {
    return {
      grid_x: [],
      grid_y: [],
      range_x: {}, // an object with options for a range for the x axis
      // or a function returning a Range instance (only on init)
      range_y: {}, // an object with options for a range for the y axis
      // or a function returning a Range instance (only on init)
      range_z: { scale: 'linear', min: 0, max: 1 }, // 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
      label: '', // a label for the chart
      label_position: 'top-left', // the position of the label
      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,
      square: false,
      role: 'group',
    };
  }

  static get static_events() {
    return {
      set_width: geomSet,
      set_height: geomSet,

      mousewheel: STOP,
      DOMMouseScroll: STOP,
      set_depth: function (value) {
        this.range_z.set('basis', value);
      },
      set_show_handles: function (value) {
        (value ? showHandles : hideHandles).call(this);
      },
    };
  }

  initialize(options) {
    let SVG;
    /**
     * @member {Array} Chart#graphs - An array containing all SVG paths acting as graphs.
     */
    this.graphs = [];
    /**
     * @member {Array} Chart#handles - An array containing all {@link ChartHandle} instances.
     */
    this.handles = [];
    if (!options.element) options.element = element('div');
    super.initialize(options);

    /**
     * @member {Range} Chart#range_x - The {@link Range} for the x axis.
     */
    /**
     * @member {Range} Chart#range_y - The {@link Range} for the y axis.
     */
    defineRange(this, this.options.range_x, 'range_x');
    defineRange(this, this.options.range_y, 'range_y');
    defineRange(this, this.options.range_z, 'range_z');
    this.range_y.set('reverse', true, true, true);

    /**
     * @member {HTMLDivElement} Chart#element - The main DIV container.
     *   Has class <code>.aux-chart</code>.
     */

    this.svg = SVG = makeSVG('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} Chart#_graphs - The SVG group containing all graphs.
     *      Has class <code>.aux-graphs</code>.
     */
    this._graphs = makeSVG('g', { class: 'aux-graphs' });
    SVG.appendChild(this._graphs);

    if (this.options.width) this.set('width', this.options.width);
    if (this.options.height) this.set('height', this.options.height);

    /**
     * @member {SVGGroup} Chart#_handles - The SVG group containing all handles.
     *      Has class <code>.aux-handles</code>.
     */
    this._handles = makeSVG('g', { class: 'aux-handles' });
    SVG.appendChild(this._handles);
    SVG.onselectstart = function () {
      return false;
    };
    this.addHandles(this.options.handles);
  }

  draw(O, element) {
    addClass(element, 'aux-chart');
    element.appendChild(this.svg);

    super.draw(O, element);
  }

  resize() {
    const E = this.element;
    const O = this.options;
    const SVG = this.svg;

    super.resize();

    const tmp = CSSSpace(SVG, 'border', 'padding');
    let w = innerWidth(E) - tmp.left - tmp.right;
    let h = innerHeight(E) - tmp.top - tmp.bottom;

    if (O.square) {
      w = h = Math.min(h, w);
    }

    if (w > 0 && O._width !== w) {
      this.set('_width', w);
      this.range_x.set('basis', w);
      this.invalid._width = true;
      this.triggerDraw();
    }
    if (h > 0 && O._height !== h) {
      this.set('_height', h);
      this.range_y.set('basis', h);
      this.invalid._height = true;
      this.triggerDraw();
    }
  }

  redraw() {
    const I = this.invalid;
    const E = this.svg;
    const O = this.options;

    super.redraw();

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

    if (I.graphs) {
      for (let i = 0; i < this.graphs.length; i++) {
        this.graphs[i].redraw();
      }
    }
    if (I.validate('label', 'label_position')) {
      drawLabel.call(this);
    }
    if (I.validate('key', 'key_size', 'graphs')) {
      drawKey.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() {
    for (let i = 0; i < this.graphs.length; i++) {
      this.graphs[i].destroy();
    }
    this._graphs.remove();
    this._handles.remove();
    super.destroy();
  }

  addChild(child) {
    if (!(child instanceof ChartHandle) || this.options.show_handles)
      super.addChild(child);

    if (child instanceof Graph) {
      const g = child;
      g.set('range_x', this.range_x);
      g.set('range_y', this.range_y);

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

      this.invalid.graphs = true;
      this.triggerDraw();
    } else if (child instanceof ChartHandle) {
      child.set('intersect', this.intersect.bind(this));
      child.set('range_x', () => this.range_x);
      child.set('range_y', () => this.range_y);
      child.set('range_z', () => this.range_z);
      this.handles.push(child);
      this._handles.appendChild(child.element);
      /**
       * Is fired when a new handle was added.
       *
       * @param {ChartHandle} handle - The {@link ChartHandle} which was added.
       *
       * @event Chart#handleadded
       */
      this.emit('handleadded', child);
    }
  }

  removeChild(child) {
    if (child instanceof Graph) {
      const G = this.graphs;
      const i = G.indexOf(child);

      if (i !== -1) {
        /**
         * Is fired when a graph was removed. Arguments are the graph
         * and its position in the array.
         *
         * @event Chart#graphremoved
         *
         * @param {Graph} graph - The {@link Graph} which was removed.
         * @param {int} id - The ID of the removed {@link Graph}.
         */
        this.emit('graphremoved', child, i);
        this.graphs.splice(i, 1);
        child.element.remove();
        this.invalid.graphs = true;
        this.triggerDraw();
      }
    } else if (child instanceof ChartHandle) {
      const H = this.handles;
      const i = H.indexOf(child);

      if (i !== -1) {
        this.handles.splice(i, 1);
        /**
         * Is fired when a handle was removed.
         *
         * @event Chart#handleremoved
         */
        this.emit('handleremoved');

        if (this.options.show_handles) return;
      }
    }

    super.removeChild(child);
  }

  /**
   * Add a graph to the chart.
   *
   * @method Chart#addGraph
   *
   * @param {Object} graph - The graph to add. This can be either an
   *  instance of {@link Graph} or an object of options to
   *  {@link Graph}.
   *
   * @returns {Object} The instance of {@link Graph}.
   *
   * @emits Chart#graphadded
   */
  addGraph(options) {
    let g;

    if (options instanceof Graph) {
      g = options;
    } else {
      g = new Graph(options);
    }

    this.addChild(g);

    return g;
  }

  /**
   * Remove a graph from the chart.
   *
   * @method Chart#removeGraph
   *
   * @param {Graph} graph - The {@link Graph} to remove.
   *
   * @emits Chart#graphremoved
   */
  removeGraph(g) {
    this.removeChild(g);
  }

  /**
   * Remove all graphs from the chart.
   *
   * @method Chart#empty
   *
   * @emits Chart#emptied
   */
  empty() {
    this.graphs.map(this.removeGraph, this);
    /**
     * Is fired when all graphs are removed from the chart.
     *
     * @event Chart#emptied
     */
    this.emit('emptied');
  }

  /**
   * Add a new handle to the widget. Options is an object containing
   * options for the {@link ChartHandle}.
   *
   * @method Chart#addHandle
   *
   * @param {Object} [options={ }] - An object containing initial options. - The options for the {@link ChartHandle}.
   * @param {Object} [type=ChartHandle] - A widget class to be used as the new handle.
   *
   * @emits Chart#handleadded
   */
  addHandle(options, type) {
    let handle;

    if (options instanceof ChartHandle) {
      handle = options;
    } else {
      type = type || ChartHandle;
      handle = new type(options);
    }

    this.addChild(handle);

    return handle;
  }

  /**
   * Add multiple new {@link ChartHandle} to the widget. Options is an array
   * of objects containing options for the new instances of {@link ChartHandle}.
   *
   * @method Chart#addHandles
   *
   * @param {Array<Object>} options - An array of options objects for the {@link ChartHandle}.
   * @param {Object} [type=ChartHandle] - A widget class to be used for the new handles.
   */
  addHandles(handles, type) {
    for (let i = 0; i < handles.length; i++) this.addHandle(handles[i], type);
  }

  /**
   * Remove a handle from the widget.
   *
   * @method Chart#removeHandle
   *
   * @param {ChartHandle} handle - The {@link ChartHandle} to remove.
   *
   * @emits Chart#handleremoved
   */
  removeHandle(handle) {
    this.removeChild(handle);
  }

  /**
   * Remove multiple or all {@link ChartHandle} from the widget.
   *
   * @method Chart#removeHandles
   *
   * @param {Array<ChartHandle>} handles - An array of
   *   {@link ChartHandle} instances. If the argument reveals to
   *   `false`, all handles are removed from the widget.
   */
  removeHandles(handles) {
    const H = handles || this.handles.slice();
    for (let i = 0; i < H.length; i++) {
      this.removeHandle(H[i]);
    }
    if (!handles) {
      this.handles = [];
      /**
       * Is fired when all handles are removed.
       *
       * @event Chart#emptied
       */
      this.emit('emptied');
    }
  }

  intersect(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)
    let c = 0;
    let a = 0,
      _a;
    const O = this.options;
    const importance_handle = O.importance_handle;
    const importance_label = O.importance_label;

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

      if (_a) {
        c++;
        a += _a * importance_handle;
      }

      _a = calculateOverlap(X, h.label);

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

        if (_a > 0) {
          c++;
          a += _a * importance_handle;
        }

        _a = calculateOverlap(X, b.label);
        if (_a > 0) {
          c++;
          a += _a * importance_label;
        }
      }
    }
    /* calculate intersection with border */
    _a = calculateOverlap(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 {Grid} Chart#grid - The grid element of the chart.
 *   Has class <code>.aux-grid</code>.
 */
defineChildWidget(Chart, 'grid', {
  create: Grid,
  show: true,
  no_resize: 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 keyHoverCallback(ev) {
  const b = ev.type === 'mouseenter';
  toggleClass(this, 'aux-hover', b);
  /* this.nextSibling is the key */
  toggleClass(this.nextSibling, 'aux-hover', b);
}
/**
 * @member {SVGRect} Chart#_key_background - The SVG rectangle of the key.
 *   Has class <code>.aux-background</code>.
 */
defineChildElement(Chart, 'key_background', {
  option: 'key',
  display_check: function (v) {
    return !!v;
  },
  create: function () {
    const k = makeSVG('rect', { class: 'aux-background' });
    k.addEventListener('mouseenter', keyHoverCallback);
    k.addEventListener('mouseleave', keyHoverCallback);
    return k;
  },
  append: function () {
    this.svg.appendChild(this._key_background);
  },
});
/**
 * @member {SVGGroup} Chart#_key - The SVG group containing all descriptions.
 *   Has class <code>.aux-key</code>.
 */
defineChildElement(Chart, 'key', {
  option: 'key',
  display_check: function (v) {
    return !!v;
  },
  create: function () {
    const key = makeSVG('g', { class: 'aux-key' });
    key.appendChild(makeSVG('text', { class: 'aux-keytext' }));
    return key;
  },
  append: function () {
    this.svg.appendChild(this._key);
  },
});
/**
 * @member {SVGText} Chart#_label - The label of the chart.
 *   Has class <code>.aux-label</code>.
 */
defineChildElement(Chart, 'label', {
  option: 'label',
  display_check: function (v) {
    return typeof v === 'string' && v.length;
  },
  create: function () {
    return makeSVG('text', {
      class: 'aux-label',
      style: 'dominant-baseline: central;',
    });
  },
  append: function () {
    const svg = this.svg;
    svg.insertBefore(this._label, svg.firstChild);
  },
});