widgets/circular.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 { FORMAT } from '../utils/sprintf.js';
import { error } from '../utils/log.js';
import { empty, addClass, getStyle } from '../utils/dom.js';
import { makeSVG } from '../utils/svg.js';
import { warning } from '../utils/warning.js';
import { mergeObjects } from '../utils/merge_objects.js';
import {
  rangedOptionsDefaults,
  rangedOptionsTypes,
  rangedRenderers,
  rangedEvents,
} from '../utils/ranged.js';
import { defineChildElement, mergeStaticEvents } from '../widget_helpers.js';
import { SymResize, Widget } from './widget.js';
import {
  defineRender,
  defineMeasure,
  deferMeasure,
  deferRender,
  defineRecalculation,
} from '../renderer.js';

function createInternalDot(dot, dotDefaults) {
  const tmp =
    typeof dot === 'object'
      ? dot
      : typeof dot === 'number'
      ? { pos: dot }
      : null;

  if (tmp === null) return null;

  return mergeObjects(dotDefaults, tmp);
}

function createInternalLabel(label, labelDefaults) {
  const internalLabel = createInternalDot(label, labelDefaults);

  if (internalLabel && internalLabel.label === void 0) {
    const label = internalLabel.format(internalLabel.pos);
    return { label, ...internalLabel };
  } else {
    return internalLabel;
  }
}

function createInternalMarker(marker, markerDefaults) {
  return createInternalDot(marker, markerDefaults);
}

const __rad = Math.PI / 180;
function _getCoords(deg, inner, outer, pos) {
  deg = +deg;
  inner = +inner;
  outer = +outer;
  pos = +pos;
  deg = deg * __rad;
  return {
    x1: Math.cos(deg) * outer + pos,
    y1: Math.sin(deg) * outer + pos,
    x2: Math.cos(deg) * inner + pos,
    y2: Math.sin(deg) * inner + pos,
  };
}
function _getCoordsSingle(deg, inner, pos) {
  deg = +deg;
  inner = +inner;
  pos = +pos;
  deg = deg * __rad;
  return {
    x: Math.cos(deg) * inner + pos,
    y: Math.sin(deg) * inner + pos,
  };
}
const formatPath = FORMAT(
  'M %f,%f ' + 'A %f,%f 0 %d,%d %f,%f ' + 'L %f,%f ' + 'A %f,%f 0 %d,%d %f,%f z'
);
const formatTranslate = FORMAT('translate(%f, %f)');
const formatTranslateRotate = FORMAT('translate(%f %f) rotate(%f %f %f)');
const formatRotate = FORMAT('rotate(%f %f %f)');

const SymLabelsChanged = Symbol('_labels changed');

function drawSlice(a_from, a_to, r_inner, r_outer, pos, slice) {
  a_from = +a_from;
  a_to = +a_to;
  r_inner = +r_inner;
  r_outer = +r_outer;
  pos = +pos;
  // ensure from !== to
  if (a_from % 360 === a_to % 360) a_from += 0.001;
  // ensure from and to in bounds
  while (a_from < 0) a_from += 360;
  while (a_to < 0) a_to += 360;
  if (a_from > 360) a_from %= 360;
  if (a_to > 360) a_to %= 360;

  if (a_from > a_to) {
    const tmp = a_from;
    a_from = a_to;
    a_to = tmp;
  }

  // get large flag
  let large;
  if (Math.abs(a_from - a_to) >= 180) large = 1;
  else large = 0;
  // draw this slice
  const from = _getCoords(a_from, r_inner, r_outer, pos);
  const to = _getCoords(a_to, r_inner, r_outer, pos);

  const path = formatPath(
    from.x1,
    from.y1,
    r_outer,
    r_outer,
    large,
    1,
    to.x1,
    to.y1,
    to.x2,
    to.y2,
    r_inner,
    r_inner,
    large,
    0,
    from.x2,
    from.y2
  );
  slice.setAttribute('d', path);
}
/**
 * Circular is a SVG group element containing two paths for displaying
 * numerical values in a circular manner. Circular is able to draw labels,
 * dots and markers and can show a hand. Circular e.g. is implemented by
 * {@link Clock} to draw hours, minutes and seconds. Circular is based on {@link Range}.
 *
 * @class Circular
 *
 * @param {Object} [options={ }] - An object containing initial options.
 *
 * @property {Number} [options.value=0] - Sets the value on the hand and on the
 *   ring at the same time.
 * @property {Number} [options.value_hand=0] - Sets the value on the hand.
 * @property {Number} [options.value_ring=0] - Sets the value on the ring.
 * @property {Number} [options.size=100] - The diameter of the circle. This
 *   is the base value for all following layout-related parameters. Keeping
 *   it set to 100 offers percentual lenghts. Set the final size of the widget
 *   via CSS.
 * @property {Number} [options.thickness=3] - The thickness of the circle.
 * @property {Number} [options.margin=0] - The margin between base and value circles.
 * @property {Boolean} [options.show_hand=true] - Draw the hand.
 * @property {Object} [options.hand] - Dimensions of the hand.
 * @property {Number} [options.hand.width=2] - Width of the hand.
 * @property {Number} [options.hand.length=30] - Length of the hand.
 * @property {Number} [options.hand.margin=10] - Margin of the hand.
 * @property {Number} [options.start=135] - The starting point in degrees.
 * @property {Number} [options.angle=270] - The maximum degree of the rotation when
 *   <code>options.value === options.max</code>.
 * @property {Number|Boolean} [options.base=false] - If a base value is set in degrees,
 *   circular starts drawing elements from this position.
 * @property {Boolean} [options.show_base=true] - Draw the base ring.
 * @property {Boolean} [options.show_value=true] - Draw the value ring.
 * @property {Number} [options.x=0] - Horizontal displacement of the circle.
 * @property {Number} [options.y=0] - Vertical displacement of the circle.
 * @property {Boolean} [options.show_dots=true] - Show/hide all dots.
 * @property {Object} [options.dots_defaults] - This option acts as default values for the individual dots
 *   specified in <code>options.dots</code>.
 * @property {Number} [options.dots_defaults.width=2] - Width of the dots.
 * @property {Number} [options.dots_defaults.length=2] - Length of the dots.
 * @property {Number} [options.dots_defaults.margin=5] - Margin of the dots.
 * @property {Array<Object|Number>} [options.dots=[]] - An array of objects describing where dots should be placed
 *   along the circle. Members are position <code>pos</code> in the value range and optionally
 *   <code>color</code> and <code>class</code> and any of the properties of <code>options.dot</code>.
 *   If it is a number, it is equivalent to an object containing just <code>pos</code>.
 * @property {Boolean} [options.show_markers=true] - Show/hide all markers.
 * @property {Object} [options.markers_defaults] - This option acts as default values of the individual markers
 *   specified in <code>options.markers</code>.
 * @property {Number} [options.markers_defaults.thickness=3] - Thickness of the marker.
 * @property {Number} [options.markers_defaults.margin=3] - Margin of the marker.
 * @property {Array<Object>} [options.markers=[]] - An array containing objects which describe where markers
 *   are to be places. Members are the position as <code>from</code> and <code>to</code> and optionally
 *   <code>color</code>, <code>class</code> and any of the properties of <code>options.marker</code>.
 * @property {Boolean} [options.show_labels=true] - Show/hide all labels.
 * @property {Object} [options.labels_defaults] - This option acts as default values for the individual labels
 *   specified in <code>options.labels</code>.
 * @property {Integer} [options.labels_defaults.margin=8] - Distance of the label from the circle of diameter
 *   <code>options.size</code>.
 * @property {String} [options.labels_defaults.align="outer"] - This option controls if labels are positioned
 *   inside or outside of the circle with radius <code>options.size/2 - margin</code>.
 * @property {Function} [options.labels_defaults.format] - Optional formatting function for the label.
 *   Receives the label value as first argument.
 * @property {Array<Object>} [options.labels=[]] - An array containing objects which describe labels
 *   to be displayed. Either a value or an object whose members are the position <code>pos</code>
 *   inside the value range and optionally <code>label</code>, <code>color</code>, <code>class</code> and any of the
 *   properties of <code>options.labels_defaults</code>.
 *
 * @extends Widget
 */
export class Circular extends Widget {
  static get _options() {
    return [
      rangedOptionsTypes,
      {
        _stroke_width: 'number',
        value: 'number',
        value_hand: 'number',
        value_ring: 'number',
        size: 'number',
        thickness: 'number',
        margin: 'number',
        hand: 'object',
        start: 'number',
        angle: 'number',
        base: 'number|boolean',
        show_base: 'boolean',
        show_value: 'boolean',
        show_hand: 'boolean',
        show_labels: 'boolean',
        show_dots: 'boolean',
        show_markers: 'boolean',
        x: 'number',
        y: 'number',
        dots_defaults: 'object',
        dots: 'array',
        markers_defaults: 'object',
        markers: 'array',
        labels_defaults: 'object',
        labels: 'array',
        presets: 'object',
        preset: 'string',
      },
    ];
  }

  static get static_events() {
    return mergeStaticEvents(rangedEvents, {
      set_value: function (value) {
        this.set('value_hand', value);
        this.set('value_ring', value);
      },
      initialized: function () {
        // calculate the stroke here once. this happens before
        // the initial redraw
        this.set('value', this.options.value);
      },
    });
  }

  static get options() {
    return [
      rangedOptionsDefaults,
      {
        _stroke_width: 0,
        value: 0,
        value_hand: 0,
        value_ring: 0,
        size: 100,
        thickness: 3,
        margin: 0,
        hand: { width: 2, length: 30, margin: 10 },
        start: 135,
        angle: 270,
        base: false,
        show_base: true,
        show_value: true,
        show_hand: true,
        show_labels: true,
        show_dots: true,
        show_markers: true,
        x: 0,
        y: 0,
        dots_defaults: { width: 1, length: 3, margin: 0.5 },
        dots: [],
        markers_defaults: { thickness: 3, margin: 0 },
        markers: [],
        labels_defaults: {
          margin: 8,
          align: 'inner',
          format: function (val) {
            return val;
          },
        },
        labels: [],
      },
    ];
  }

  getPresetOption(presets, presetName, optionName, value) {
    const defaultValue = this.getDefault(optionName);
    if (value !== defaultValue) {
      return value;
    }

    if (presets && presetName) {
      const preset = presets[presetName];

      if (preset && optionName in preset) return preset[optionName];
    }

    return value;
  }

  getPresetOptionMerged(presets, presetName, optionName, value) {
    const defaultValue = this.getDefault(optionName);

    const preset = presets && presetName ? presets[presetName] : null;

    return mergeObjects(
      defaultValue,
      preset ? preset[optionName] : null,
      defaultValue !== value ? value : null
    );
  }

  static get renderers() {
    return [
      ...rangedRenderers,
      defineRender('show_hand', function (show_hand) {
        this._hand.style.display = show_hand ? 'block' : 'none';
      }),
      defineMeasure(SymResize, function () {
        this.set('_stroke_width', this.getStroke());
        this.invalidate('labels');
      }),
      defineRender(
        [
          'show_dots',
          '_dots',
          'angle',
          'transformation',
          'snap_module',
          'size',
        ],
        function (show_dots, dots, angle, transformation, snap_module, size) {
          const _dots = this._dots;

          if (!_dots) return;

          empty(_dots);

          if (!show_dots) return;

          // TODO: consider caching nodes

          if (dots)
            dots.forEach((dot) => {
              const r = makeSVG('rect', { class: 'aux-dot' });

              const { length, width, margin, color, class: cl } = dot;
              const pos = transformation.clampValue(dot.pos);
              if (cl) addClass(r, cl);
              if (color) r.style.fill = color;

              r.setAttribute('x', size - length - margin);
              r.setAttribute('y', size / 2 - width / 2);

              r.setAttribute('width', length);
              r.setAttribute('height', width);

              r.setAttribute(
                'transform',
                'rotate(' +
                  transformation.valueToCoef(snap_module.snap(pos)) * angle +
                  ' ' +
                  size / 2 +
                  ' ' +
                  size / 2 +
                  ')'
              );
              _dots.appendChild(r);
            });
          /**
           * Is fired when dots are (re)drawn.
           * @event Circular#dotsdrawn
           */
          this.emit('dotsdrawn');
        }
      ),
      defineRender(
        [
          SymLabelsChanged,
          'show_labels',
          '_labels',
          'transformation',
          'snap_module',
          'angle',
          'start',
          'size',
          'presets',
          'preset',
        ],
        function (
          show_labels,
          labels,
          transformation,
          snap_module,
          angle,
          start,
          size,
          presets,
          preset
        ) {
          // depends on size, _labels, label, start
          const _labels = this._labels;

          if (!_labels) return;

          empty(this._labels);

          show_labels = this.getPresetOption(
            presets,
            preset,
            'show_labels',
            show_labels
          );

          if (!show_labels || !labels || !labels.length) return;

          const outerSize = size / 2;

          const elements = labels.map((l) => {
            const p = makeSVG('text', {
              class: 'aux-label',
              style: 'dominant-baseline: central;',
            });

            if (l['class']) addClass(p, l['class']);
            if (l.color) p.style.fill = l.color;

            if (l.label !== void 0) p.textContent = l.label;

            p.setAttribute('text-anchor', 'middle');

            _labels.appendChild(p);
            return p;
          });
          /* FORCE_RELAYOUT */

          return deferMeasure(() => {
            const positions = labels.map((l, i) => {
              const element = elements[i];

              const margin = l.margin;
              const alignOffset = l.align === 'inner' ? -2 : 2;

              const pos = transformation.clampValue(l.pos);
              const bb = element.getBBox();
              const _angle =
                (transformation.valueToCoef(snap_module.snap(pos)) * angle +
                  start) %
                360;
              const outer_p = outerSize - margin;
              const coords = _getCoordsSingle(_angle, outer_p, outerSize);

              const mx =
                (((coords.x - outerSize) / outer_p) *
                  (bb.width + bb.height / 2.5)) /
                alignOffset;
              const my =
                (((coords.y - outerSize) / outer_p) * bb.height) / alignOffset;

              return formatTranslate(coords.x + mx, coords.y + my);
            });

            return deferRender(() => {
              elements.forEach((element, i) => {
                element.setAttribute('transform', positions[i]);
              });
              /**
               * Is fired when labels are (re)drawn.
               * @event Circular#labelsdrawn
               */
              this.emit('labelsdrawn');
            });
          });
        }
      ),
      defineRender(['x', 'y', 'start', 'size'], function (x, y, start, size) {
        const outerSize = size / 2;
        this.element.setAttribute(
          'transform',
          formatTranslateRotate(x, y, start, outerSize, outerSize)
        );
      }),
      defineRender(['start', 'size'], function (start, size) {
        const _labels = this._labels;
        const outerSize = size / 2;
        if (!_labels) return;
        _labels.setAttribute(
          'transform',
          formatRotate(-start, outerSize, outerSize)
        );
      }),
      defineRender(
        [
          'show_markers',
          '_markers',
          'transformation',
          'snap_module',
          'size',
          'angle',
          'min',
          'max',
          '_stroke_width',
        ],
        function (
          show_markers,
          markers,
          transformation,
          snap_module,
          size,
          angle,
          min,
          max,
          _stroke_width
        ) {
          const _markers = this._markers;

          if (!_markers) return;

          empty(this._markers);

          if (!show_markers) return;

          const outerSize = size / 2;

          if (markers)
            markers.forEach((marker) => {
              const { thickness, margin, color, class: cl, nosnap } = marker;
              const inner = outerSize - thickness;
              const outer_p = outerSize - margin - _stroke_width / 2;
              const inner_p = inner - margin - _stroke_width / 2;

              let from = transformation.clampValue(
                marker.from === void 0 ? min : marker.from
              );
              let to = transformation.clampValue(
                marker.to === void 0 ? max : marker.to
              );

              const s = makeSVG('path', { class: 'aux-marker' });

              if (cl) addClass(s, cl);
              if (color) s.style.fill = color;
              if (!nosnap) {
                from = snap_module.snap(from);
                to = snap_module.snap(to);
              }
              from = transformation.valueToCoef(from) * angle;
              to = transformation.valueToCoef(to) * angle;

              drawSlice(from, to, inner_p, outer_p, outerSize, s);
              _markers.appendChild(s);
            });
          /**
           * Is fired when markers are (re)drawn.
           * @event Circular#markersdrawn
           */
          this.emit('markersdrawn');
        }
      ),
      defineRender(
        [
          'show_value',
          '_coef_base',
          'angle',
          '_coef_ring',
          '_stroke_width',
          'thickness',
          'margin',
          'size',
          'presets',
          'preset',
        ],
        function (
          show_value,
          _coef_base,
          angle,
          _coef_ring,
          _stroke_width,
          thickness,
          margin,
          size,
          presets,
          preset
        ) {
          const _value = this._value;
          const { snap_module, transformation } = this.options;

          margin = this.getPresetOption(presets, preset, 'margin', margin);
          thickness = this.getPresetOption(
            presets,
            preset,
            'thickness',
            thickness
          );

          if (show_value) {
            if (!isFinite(_coef_base) || !isFinite(_coef_ring)) return;

            const outerSize = size / 2;
            const inner = outerSize - thickness;
            const outer_p = outerSize - _stroke_width / 2 - margin;
            const inner_p = inner - _stroke_width / 2 - margin;

            drawSlice(
              _coef_base * angle,
              _coef_ring * angle,
              inner_p,
              outer_p,
              outerSize,
              _value
            );
          } else {
            _value.removeAttribute('d');
          }
        }
      ),
      defineRender(
        [
          'show_base',
          'size',
          'margin',
          '_stroke_width',
          'thickness',
          'angle',
          'presets',
          'preset',
        ],
        function (
          show_base,
          size,
          margin,
          _stroke_width,
          thickness,
          angle,
          presets,
          preset
        ) {
          const _base = this._base;

          margin = this.getPresetOption(presets, preset, 'margin', margin);
          thickness = this.getPresetOption(
            presets,
            preset,
            'thickness',
            thickness
          );

          if (show_base) {
            const outerSize = size / 2;
            const inner = outerSize - thickness;
            const outer_p = outerSize - _stroke_width / 2 - margin;
            const inner_p = inner - _stroke_width / 2 - margin;

            drawSlice(0, angle, inner_p, outer_p, outerSize, _base);
          } else {
            _base.removeAttribute('d');
          }
        }
      ),
      defineRender(
        ['size', '_coef_hand', 'hand', 'angle', 'presets', 'preset'],
        function (size, _coef_hand, hand, angle, presets, preset) {
          const _hand = this._hand;

          const hand_angle = _coef_hand * angle;

          if (isNaN(hand_angle) || !isFinite(hand_angle)) return;

          hand = this.getPresetOptionMerged(presets, preset, 'hand', hand);
          _hand.setAttribute('x', size - hand.length - hand.margin);
          _hand.setAttribute('y', (size - hand.width) / 2.0);
          _hand.setAttribute('width', hand.length);
          _hand.setAttribute('height', hand.width);
          _hand.setAttribute(
            'transform',
            formatRotate(hand_angle, size / 2, size / 2)
          );
        }
      ),
      defineRecalculation(['value', 'transformation', 'snap_module'], function (
        value,
        transformation,
        snap_module
      ) {
        const _value = snap_module.snap(transformation.clampValue(value));
        this.update('_value', _value);
      }),
      defineRecalculation(
        ['value_hand', 'transformation', 'snap_module'],
        function (value_hand, transformation, snap_module) {
          const value = transformation.valueToCoef(
            snap_module.snap(transformation.clampValue(value_hand))
          );
          this.update('_coef_hand', value);
        }
      ),
      defineRecalculation(
        ['value_ring', 'transformation', 'snap_module'],
        function (value_ring, transformation, snap_module) {
          const value = transformation.valueToCoef(
            snap_module.snap(transformation.clampValue(value_ring))
          );
          this.update('_coef_ring', value);
        }
      ),
      defineRecalculation(
        ['base', 'min', 'transformation', 'snap_module'],
        function (base, min, transformation, snap_module) {
          const value = base === false ? min : base;
          const _coef_base = transformation.valueToCoef(
            snap_module.snap(transformation.clampValue(value))
          );
          this.update('_coef_base', _coef_base);
        }
      ),
      defineRecalculation(
        ['labels', 'labels_defaults', 'presets', 'preset'],
        function (labels, labels_defaults, presets, preset) {
          let _labels = null;

          labels_defaults = this.getPresetOptionMerged(
            presets,
            preset,
            'labels_defaults',
            labels_defaults
          );

          if (Array.isArray(labels) && labels.length) {
            _labels = labels.map((entry) =>
              createInternalLabel(entry, labels_defaults)
            );

            if (_labels.includes(null))
              _labels = _labels.filter((label) => label !== null);
          }

          this.update('_labels', _labels);
        }
      ),
      defineRecalculation(
        ['dots', 'dots_defaults', 'presets', 'preset'],
        function (dots, dots_defaults, presets, preset) {
          let _dots = null;
          dots_defaults = this.getPresetOptionMerged(
            presets,
            preset,
            'dots_defaults',
            dots_defaults
          );

          if (Array.isArray(dots) && dots.length) {
            _dots = dots.map((entry) =>
              createInternalDot(entry, dots_defaults)
            );

            if (_dots.includes(null))
              _dots = _dots.filter((dot) => dot !== null);
          }

          this.update('_dots', _dots);
        }
      ),
      defineRecalculation(
        ['markers', 'markers_defaults', 'presets', 'preset'],
        function (markers, markers_defaults, presets, preset) {
          let _markers = null;

          markers_defaults = this.getPresetOptionMerged(
            presets,
            preset,
            'markers_defaults',
            markers_defaults
          );

          if (Array.isArray(markers) && markers.length) {
            _markers = markers.map((entry) =>
              createInternalMarker(entry, markers_defaults)
            );

            if (_markers.includes(null))
              _markers = _markers.filter((marker) => marker !== null);
          }

          this.update('_markers', _markers);
        }
      ),
    ];
  }

  initialize(options) {
    if (!options.element) options.element = makeSVG('g');
    super.initialize(options);

    /**
     * @member {SVGImage} Circular#element - The main SVG element.
     *      Has class <code>.aux-circular</code>
     */
    /**
     * @member {SVGPath} Circular#_base - The base of the ring.
     *      Has class <code>.aux-base</code>
     */
    this._base = makeSVG('path', { class: 'aux-base' });

    /**
     * @member {SVGPath} Circular#_value - The ring showing the value.
     *      Has class <code>.aux-value</code>
     */
    this._value = makeSVG('path', { class: 'aux-value' });

    /**
     * @member {SVGRect} Circular#_hand - The hand of the knob.
     *      Has class <code>.aux-hand</code>
     */
    this._hand = makeSVG('rect', { class: 'aux-hand' });

    /**
     * @member {SVGGroup} Circular#_markers - A group containing all markers.
     *      Has class <code>.aux-markers</code>
     */
    this._markers = makeSVG('g', { class: 'aux-markers' });

    /**
     * @member {SVGGroup} Circular#_dots - A group containing all dots.
     *      Has class <code>.aux-dots</code>
     */
    this._dots = makeSVG('g', { class: 'aux-dots' });

    if (this.options.labels) this.set('labels', this.options.labels);
  }

  draw(O, element) {
    addClass(element, 'aux-circular');
    element.appendChild(this._base);
    element.appendChild(this._value);
    element.appendChild(this._markers);
    element.appendChild(this._dots);
    element.appendChild(this._hand);

    super.draw(O, element);
  }

  destroy() {
    this._dots.remove();
    this._markers.remove();
    this._base.remove();
    this._value.remove();
    this._hand.remove();
    super.destroy();
  }

  getStroke() {
    if (Object.prototype.hasOwnProperty.call(this, '_stroke'))
      return this._stroke;
    const strokeb = parseInt(getStyle(this._base, 'stroke-width')) || 0;
    const strokev = parseInt(getStyle(this._value, 'stroke-width')) || 0;
    this._stroke = Math.max(strokeb, strokev);
    return this._stroke;
  }

  // GETTERS & SETTERS
  set(key, value) {
    const O = this.options;
    switch (key) {
      case 'value':
        if (value > O.max || value < O.min) warning(this.element);
        break;
    }

    return super.set(key, value);
  }
}

/**
 * @member {SVGGroup} Circular#_labels - A group containing all labels.
 *      Has class <code>.aux-labels</code>
 */
defineChildElement(Circular, 'labels', {
  //option: "labels",
  //display_check: function(v) { return !!v.length; },
  show: true,
  dependency: SymLabelsChanged,
  create: function () {
    return makeSVG('g', { class: 'aux-labels' });
  },
});