Gapminder/vizabi

View on GitHub
src/helpers/labels.js

Summary

Maintainability
F
1 wk
Test Coverage
import * as utils from "base/utils";
import Class from "base/class";
import cssEscape from "css.escape";

import { close as iconClose } from "base/iconset";

const label = function(context) {

  return (function d3_label() {

    const _this = context;

    let _cssPrefix;
    label.setCssPrefix = function(cssPrefix) {
      _cssPrefix = cssPrefix;
      return label;
    };

    const labelDragger = d3.drag()
      .on("start", (d, i) => {
        d3.event.sourceEvent.stopPropagation();
        const KEY = _this.KEY;
      })
      .on("drag", function(d, i) {
        const KEY = _this.KEY;
        if (!_this.model.ui.chart.labels.dragging) return;
        if (!_this.dragging) _this.dragging = d[KEY];
        const cache = _this.cached[d[KEY]];
        cache.labelFixed = true;

        const viewWidth = _this.context.width;
        const viewHeight = _this.context.height;

        cache.labelX_ += d3.event.dx / viewWidth;
        cache.labelY_ += d3.event.dy / viewHeight;

        const resolvedX = _this.xScale(cache.labelX0) + cache.labelX_ * viewWidth;
        const resolvedY = _this.yScale(cache.labelY0) + cache.labelY_ * viewHeight;

        const resolvedX0 = _this.xScale(cache.labelX0);
        const resolvedY0 = _this.yScale(cache.labelY0);

        const lineGroup = _this.entityLines.filter(f => f[KEY] == d[KEY]);

        label._repositionLabels(d, i, null, this, resolvedX, resolvedY, resolvedX0, resolvedY0, 0, null, lineGroup);
      })
      .on("end", (d, i) => {
        const KEY = _this.KEY;
        if (_this.dragging) {
          const cache = _this.cached[d[KEY]];
          _this.dragging = null;
          cache.labelOffset[0] = cache.labelX_;
          cache.labelOffset[1] = cache.labelY_;
          _this.model.marker.setLabelOffset(d, [cache.labelX_, cache.labelY_]);
        }
      });

    function label(container, isTooltip) {
      container
        .each(function(d, index) {
          const view = d3.select(this);

          // Ola: Clicking bubble label should not zoom to countries boundary #811
          // It's too easy to accidentally zoom
          // This feature will be activated later, by making the label into a "context menu" where users can click Split, or zoom,.. hide others etc....

          view.append("rect")
            .attr("class", "vzb-label-glow")
            .attr("filter", "url(" + location.pathname + "#vzb-glow-filter)");
          view.append("rect")
            .attr("class", "vzb-label-fill vzb-tooltip-border");
          //          .on("click", function(d, i) {
          //            //default prevented is needed to distinguish click from drag
          //            if(d3.event.defaultPrevented) return;
          //
          //            var maxmin = _this.cached[d[KEY]].maxMinValues;
          //            var radius = utils.areaToRadius(_this.sScale(maxmin.valueSmax));
          //            _this._panZoom._zoomOnRectangle(_this.element,
          //              _this.xScale(maxmin.valueXmin) - radius,
          //              _this.yScale(maxmin.valueYmin) + radius,
          //              _this.xScale(maxmin.valueXmax) + radius,
          //              _this.yScale(maxmin.valueYmax) - radius,
          //              false, 500);
          //          });

          const text = view.append("text").attr("class", _cssPrefix + "-label-content stroke");
          if (!view.style("paint-order").length) {
            view.insert("text", `.${_cssPrefix}-label-content`)
              .attr("class", _cssPrefix + "-label-content " + _cssPrefix + "-label-shadow vzb-noexport");

            text.classed("stroke", false);
          }

          if (!isTooltip) {
            const cross = view.append("g").attr("class", _cssPrefix + "-label-x vzb-transparent");
            utils.setIcon(cross, iconClose);

            cross.insert("circle", "svg");

            cross.select("svg")
              .attr("class", _cssPrefix + "-label-x-icon")
              .attr("width", "0px")
              .attr("height", "0px");

            cross.on("click", () => {
              //default prevented is needed to distinguish click from drag
              if (d3.event.defaultPrevented) return;
              d3.event.stopPropagation();
              _this.model.marker.clearHighlighted();
              _this.model.marker.selectMarker(d);
            });
          }

        });

      if (!isTooltip) {
        container
          .call(labelDragger)
          .on("mouseenter", function(d) {
            if (utils.isTouchDevice() || _this.dragging) return;
            _this.model.marker.highlightMarker(d);
            const KEY = _this.KEY || _this.context.KEY;
            // hovered label should be on top of other labels: if "a" is not the hovered element "d", send "a" to the back
            _this.entityLabels.sort((a, b) => a[KEY] != d[KEY] ? -1 : 1);
            d3.select(this).selectAll("." + _cssPrefix + "-label-x")
              .classed("vzb-transparent", false);
          })
          .on("mouseleave", function(d) {
            if (utils.isTouchDevice() || _this.dragging) return;
            _this.model.marker.clearHighlighted();
            d3.select(this).selectAll("." + _cssPrefix + "-label-x")
              .classed("vzb-transparent", true);
          })
          .on("click", function(d) {
            if (!utils.isTouchDevice()) return;
            const cross = d3.select(this).selectAll("." + _cssPrefix + "-label-x");
            const KEY = _this.KEY || _this.context.KEY;
            const hidden = cross.classed("vzb-transparent");
            if (hidden) {
              // hovered label should be on top of other labels: if "a" is not the hovered element "d", send "a" to the back
              _this.entityLabels.sort((a, b) => a[KEY] != d[KEY] ? -1 : 1);
              _this.showCloseCross(null, false);
            }
            cross.classed("vzb-transparent", !hidden);
            if (!_this.options.SUPPRESS_HIGHLIGHT_DURING_PLAY || !_this.model.time.playing) {
              if (hidden) {
                _this.model.marker.setHighlight(d);
              } else {
                _this.model.marker.clearHighlighted();
              }
            }
          });
      }

      return label;
    }

    label.line = function(container) {
      container.append("line").attr("class", _cssPrefix + "-label-line");
    };


    label._repositionLabels = _repositionLabels;
    function _repositionLabels(d, i, _cache, labelContext, _X, _Y, _X0, _Y0, duration, showhide, lineGroup) {

      const cache = _cache || _this.cached[d[_this.KEY]];

      const labelGroup = d3.select(labelContext);

      //protect label and line from the broken data
      const brokenInputs = !_X && _X !== 0 || !_Y && _Y !== 0 || !_X0 && _X0 !== 0 || !_Y0 && _Y0 !== 0;
      if (brokenInputs) {
        labelGroup.classed("vzb-invisible", brokenInputs);
        lineGroup.classed("vzb-invisible", brokenInputs);
        return;
      }

      const viewWidth = _this.context.width;
      const viewHeight = _this.context.height;
      const rectBBox = cache.rectBBox;
      const height = rectBBox.height;
      const offsetX = cache.rectOffsetX;
      const offsetY = cache.rectOffsetY;

      //apply limits so that the label doesn't stick out of the visible field
      if (_X + rectBBox.x <= 0) { //check left
        _X = -rectBBox.x;
        cache.labelX_ = (_X - _this.xScale(cache.labelX0)) / viewWidth;
      } else if (_X + offsetX > viewWidth) { //check right
        _X = viewWidth - offsetX;
        cache.labelX_ = (_X - _this.xScale(cache.labelX0)) / viewWidth;
      }
      if (_Y + rectBBox.y <= 0) { // check top
        _Y = -rectBBox.y;
        cache.labelY_ = (_Y - _this.yScale(cache.labelY0)) / viewHeight;
      } else if (_Y + offsetY > viewHeight) { //check bottom
        _Y = viewHeight - offsetY;
        cache.labelY_ = (_Y - _this.yScale(cache.labelY0)) / viewHeight;
      }
      // if (_Y - height * 0.75 <= 0) { // check top
      //   _Y = height * 0.75;
      //   cache.labelY_ = (_Y - _this.yScale(cache.labelY0)) / viewHeight;
      // } else if (_Y + height * 0.35 > viewHeight) { //check bottom
      //   _Y = viewHeight - height * 0.35;
      //   cache.labelY_ = (_Y - _this.yScale(cache.labelY0)) / viewHeight;
      // }

      if (duration == null) duration = _this.context.duration;
      if (cache._new) {
        duration = 0;
        delete cache._new;
      }
      if (duration) {
        if (showhide && !d.hidden) {
          //if need to show label

          labelGroup.classed("vzb-invisible", d.hidden);
          labelGroup
            .attr("transform", "translate(" + _X + "," + _Y + ")")
            .style("opacity", 0)
            .transition().duration(duration).ease(d3.easeExp)
            .style("opacity", 1)
          //i would like to set opactiy to null in the end of transition.
          //but then fade in animation is not working for some reason
            .on("interrupt", () => {
              labelGroup
                .style("opacity", 1);
            });
          lineGroup.classed("vzb-invisible", d.hidden);
          lineGroup
            .attr("transform", "translate(" + _X + "," + _Y + ")")
            .style("opacity", 0)
            .transition().duration(duration).ease(d3.easeExp)
            .style("opacity", 1)
          //i would like to set opactiy to null in the end of transition.
          //but then fade in animation is not working for some reason
            .on("interrupt", () => {
              lineGroup
                .style("opacity", 1);
            });

        } else if (showhide && d.hidden) {
          //if need to hide label

          labelGroup
            .style("opacity", 1)
            .transition().duration(duration).ease(d3.easeExp)
            .style("opacity", 0)
            .on("end", () => {
              labelGroup
                .style("opacity", 1) //i would like to set it to null. but then fade in animation is not working for some reason
                .classed("vzb-invisible", d.hidden);
            });
          lineGroup
            .style("opacity", 1)
            .transition().duration(duration).ease(d3.easeExp)
            .style("opacity", 0)
            .on("end", () => {
              lineGroup
                .style("opacity", 1) //i would like to set it to null. but then fade in animation is not working for some reason
                .classed("vzb-invisible", d.hidden);
            });

        } else {
          // just update the position

          labelGroup
            .transition().duration(duration).ease(d3.easeLinear)
            .attr("transform", "translate(" + _X + "," + _Y + ")");
          lineGroup
            .transition().duration(duration).ease(d3.easeLinear)
            .attr("transform", "translate(" + _X + "," + _Y + ")");
        }

      } else {
        labelGroup
          .interrupt()
          .attr("transform", "translate(" + _X + "," + _Y + ")")
          .transition();
        lineGroup
          .interrupt()
          .attr("transform", "translate(" + _X + "," + _Y + ")")
          .transition();
        if (showhide) labelGroup.classed("vzb-invisible", d.hidden);
        if (showhide) lineGroup.classed("vzb-invisible", d.hidden);
      }

      const diffX1 = _X0 - _X;
      const diffY1 = _Y0 - _Y;
      const textBBox = cache.textBBox;
      let diffX2 = -textBBox.width * 0.5;
      let diffY2 = -height * 0.2;
      const labels = _this.model.ui.chart.labels;

      const bBox = labels.removeLabelBox ? textBBox : rectBBox;

      const FAR_COEFF = _this.activeProfile.labelLeashCoeff || 0;

      const lineHidden = circleRectIntersects({ x: diffX1, y: diffY1, r: cache.scaledS0 },
        { x: diffX2, y: diffY2, width: (bBox.height * 2 * FAR_COEFF + bBox.width), height: (bBox.height * (2 * FAR_COEFF + 1)) });
      lineGroup.select("line").classed("vzb-invisible", lineHidden);
      if (lineHidden) return;

      if (labels.removeLabelBox) {
        const angle = Math.atan2(diffX1 - diffX2, diffY1 - diffY2) * 180 / Math.PI;
        const deltaDiffX2 = (angle >= 0 && angle <= 180) ? (bBox.width * 0.5) : (-bBox.width * 0.5);
        const deltaDiffY2 = (Math.abs(angle) <= 90) ? (bBox.height * 0.55) : (-bBox.height * 0.45);
        diffX2 += Math.abs(diffX1 - diffX2) > textBBox.width * 0.5 ? deltaDiffX2 : 0;
        diffY2 += Math.abs(diffY1 - diffY2) > textBBox.height * 0.5 ? deltaDiffY2 : (textBBox.height * 0.05);
      }

      const longerSideCoeff = Math.abs(diffX1) > Math.abs(diffY1) ? Math.abs(diffX1) : Math.abs(diffY1);
      lineGroup.select("line").style("stroke-dasharray", "0 " + (cache.scaledS0) + " " + ~~(longerSideCoeff) * 2);

      lineGroup.selectAll("line")
        .attr("x1", diffX1)
        .attr("y1", diffY1)
        .attr("x2", diffX2)
        .attr("y2", diffY2);

    }

    /*
    * Adapted from
    * http://stackoverflow.com/questions/401847/circle-rectangle-collision-detection-intersection
    *
    * circle {
    *  x: center X
    *  y: center Y
    *  r: radius
    * }
    *
    * rect {
    *  x: center X
    *  y: center Y
    *  width: width
    *  height: height
    * }
    */
    function circleRectIntersects(circle, rect) {
      const circleDistanceX = Math.abs(circle.x - rect.x);
      const circleDistanceY = Math.abs(circle.y - rect.y);
      const halfRectWidth = rect.width * 0.5;
      const halfRectHeight = rect.height * 0.5;

      if (circleDistanceX > (halfRectWidth + circle.r)) { return false; }
      if (circleDistanceY > (halfRectHeight + circle.r)) { return false; }

      if (circleDistanceX <= halfRectWidth) { return true; }
      if (circleDistanceY <= halfRectHeight) { return true; }

      const cornerDistance_sq = Math.pow(circleDistanceX - halfRectWidth, 2) +
                          Math.pow(circleDistanceY - halfRectHeight, 2);

      return (cornerDistance_sq <= Math.pow(circle.r, 2));
    }

    return label;
  })();
};

const OPTIONS = {
  LABELS_CONTAINER_CLASS: "",
  LINES_CONTAINER_CLASS: "",
  LINES_CONTAINER_SELECTOR: "",
  CSS_PREFIX: "",
  SUPPRESS_HIGHLIGHT_DURING_PLAY: true
};

const Labels = Class.extend({

  init(context, conditions) {
    const _this = this;
    this.context = context;

    this.options = utils.extend({}, OPTIONS);
    this.label = label(this);
    this._xScale = null;
    this._yScale = null;
    this._closeCrossHeight = 0;
    this.labelSizeTextScale = null;
  },

  ready() {
    this.KEYS = this.context.KEYS;
    this.KEY = this.context.KEY;
    this._clearInitialFontSize();
    this.updateIndicators();
    this.updateLabelSizeLimits();
    //this.updateLabelsOnlyTextSize();
  },

  readyOnce() {
    const _this = this;

    this.model = this.context.model;

    this.model.on("change:marker.select", (evt, path) => {
      if (!_this.context._readyOnce) return;
      if (path.indexOf("select.labelOffset") !== -1) return;

      //console.log("EVENT change:entities:select");
      _this.selectDataPoints();
    });

    if (this.model.marker.size_label)
      this.model.on("change:marker.size_label.extent", (evt, path) => {
        //console.log("EVENT change:marker:size:max");
        if (!_this.context._readyOnce) return;
        _this.updateLabelSizeLimits();
        if (_this.model.time.splash) return;
        _this.updateLabelsOnlyTextSize();
      });

    if (this.model.ui.chart.labels.hasOwnProperty("removeLabelBox"))
      this.model.on("change:ui.chart.labels.removeLabelBox", (evt, path) => {
        //console.log("EVENT change:marker:size:max");
        if (!_this.context._readyOnce) return;
        _this.updateLabelsOnlyTextSize();
      });

    if (this.model.ui.chart.labels.hasOwnProperty("enabled"))
      this.model.on("change:ui.chart.labels.enabled", (evt, path) => {
        if (!_this.context._readyOnce) return;
        _this.selectDataPoints();
      });


    this.KEYS = this.context.KEYS;
    this.KEY = this.context.KEY;

    this.cached = {};

    this.label.setCssPrefix(this.options.CSS_PREFIX);

    this.rootEl = this.context.root.element instanceof Array ? this.context.root.element : d3.select(this.context.root.element);
    this.labelsContainer = this.rootEl.select("." + this.options.LABELS_CONTAINER_CLASS);
    this.linesContainer = this.rootEl.select("." + this.options.LINES_CONTAINER_CLASS);
    this.updateIndicators();
    this.updateSize();
    this.selectDataPoints();
    this._initLabelTooltip();
  },

  config(newOptions) {
    utils.extend(this.options, newOptions);
  },

  updateLabelSizeLimits() {
    const _this = this;
    if (!this.model.marker.size_label) return;
    const extent = this.model.marker.size_label.extent || [0, 1];

    const minLabelTextSize = this.activeProfile.minLabelTextSize;
    const maxLabelTextSize = this.activeProfile.maxLabelTextSize;
    const minMaxDelta = maxLabelTextSize - minLabelTextSize;

    this.minLabelTextSize = Math.max(minLabelTextSize + minMaxDelta * extent[0], minLabelTextSize);
    this.maxLabelTextSize = Math.max(minLabelTextSize + minMaxDelta * extent[1], minLabelTextSize);

    if (this.model.marker.size_label.use == "constant") {
      // if(!this.model.marker.size_label.which) {
      //   this.maxLabelTextSize = this.activeProfile.defaultLabelTextSize;
      //   this.model.marker.size_label.set({'domainMax': (this.maxLabelTextSize - minLabelTextSize) / minMaxDelta, 'which': '_default'});
      //   return;
      // }
      this.minLabelTextSize = this.maxLabelTextSize;
    }

    if (this.model.marker.size_label.scaleType !== "ordinal" || this.model.marker.size_label.use == "constant") {
      this.labelSizeTextScale.range([_this.minLabelTextSize, _this.maxLabelTextSize]);
    } else {
      this.labelSizeTextScale.rangePoints([_this.minLabelTextSize, _this.maxLabelTextSize], 0).range();
    }

  },

  updateIndicators() {
    const _this = this;

    //scales
    if (this.model.marker.size_label) {
      this.labelSizeTextScale = this.model.marker.size_label.getScale();
    }
  },

  setScales(xScale, yScale) {
    this._xScale = xScale;
    this._yScale = yScale;
  },

  setCloseCrossHeight(closeCrossHeight) {
    if (this._closeCrossHeight != closeCrossHeight) {
      this._closeCrossHeight = closeCrossHeight;
      this.updateLabelCloseGroupSize(this.entityLabels.selectAll("." + this.options.CSS_PREFIX + "-label-x"), this._closeCrossHeight);
    }
  },

  xScale(x) {
    return this._xScale ? this._xScale(x) : (x * this.context.width);
  },

  yScale(y) {
    return this._yScale ? this._yScale(y) : (y * this.context.height);
  },

  selectDataPoints() {
    const _this = this;
    const KEYS = this.KEYS;
    const KEY = this.KEY;
    const _cssPrefix = this.options.CSS_PREFIX;

    const select = _this.model.marker.select.map(d => {
      const p = utils.clone(d, KEYS);
      p[KEY] = utils.getKey(d, KEYS);
      return p;
    });
    this.entityLabels = this.labelsContainer.selectAll("." + _cssPrefix + "-entity")
      .data(select, d => (d[KEY]));
    this.entityLines = this.linesContainer.selectAll("g.entity-line." + _cssPrefix + "-entity")
      .data(select, d => (d[KEY]));

    this.entityLabels.exit()
      .each(d => {
        if (_this.cached[d[KEY]] != null) {
          _this.cached[d[KEY]] = void 0;
        }
      })
      .remove();
    this.entityLines.exit()
      .remove();

    this.entityLines = this.entityLines
      .enter().insert("g", function(d) {
        return this.querySelector("." + _this.options.LINES_CONTAINER_SELECTOR_PREFIX + cssEscape(d[KEY]));
      })
      .attr("class", (d, index) => _cssPrefix + "-entity entity-line line-" + d[KEY])
      .each(function(d, index) {
        _this.label.line(d3.select(this));
      })
      .merge(this.entityLines)
      .classed("vzb-hidden", utils.getProp(_this.model.ui, ["chart", "labels", "enabled"]) === false);

    this.entityLabels = this.entityLabels
      .enter().append("g")
      .attr("class", (d, index) => _cssPrefix + "-entity label-" + d[KEY])
      .each(function(d, index) {
        _this.cached[d[KEY]] = { _new: true };
        _this.label(d3.select(this));
      })
      .merge(this.entityLabels)
      .classed("vzb-hidden", utils.getProp(_this.model.ui, ["chart", "labels", "enabled"]) === false);
  },

  showCloseCross(d, show) {
    const KEY = this.KEY;
    //show the little cross on the selected label
    this.entityLabels
      .filter(f => d ? f[KEY] == d[KEY] : true)
      .select("." + this.options.CSS_PREFIX + "-label-x")
      .classed("vzb-transparent", !show);
  },

  highlight(d, highlight) {
    const KEY = this.KEY;
    let labels = this.entityLabels;
    if (d) {
      labels = labels.filter(f => d ? f[KEY] == d[KEY] : true);
    }
    labels.classed("vzb-highlighted", highlight);
  },

  updateLabel(d, index, cache, valueX, valueY, valueS, valueC, valueL, valueLST, duration, showhide) {
    const _this = this;
    const KEYS = this.KEYS;
    const KEY = this.KEY;
    if (d[KEY] == _this.dragging)
      return;

    const _cssPrefix = this.options.CSS_PREFIX;

    // only for selected entities
    if (_this.model.marker.isSelected(d)  && _this.entityLabels != null) {
      if (_this.cached[d[KEY]] == null) this.selectDataPoints();

      const cached = _this.cached[d[KEY]];
      if (cache) utils.extend(cached, cache);


      if (cached.scaledS0 == null || cached.labelX0 == null || cached.labelY0 == null) { //initialize label once
        this._initNewCache(cached, valueX, valueY, valueS, valueC, valueLST);
      }

      if (cached.labelX_ == null || cached.labelY_ == null)
      {
        const select = utils.find(_this.model.marker.select, f => utils.getKey(f, KEYS) == d[KEY]);
        cached.labelOffset = select.labelOffset || [0, 0];
      }

      const brokenInputs = !cached.labelX0 && cached.labelX0 !== 0 || !cached.labelY0 && cached.labelY0 !== 0 || !cached.scaledS0 && cached.scaledS0 !== 0;

      const lineGroup = _this.entityLines.filter(f => f[KEY] == d[KEY]);
      // reposition label
      _this.entityLabels.filter(f => f[KEY] == d[KEY])
        .each(function(groupData) {

          const labelGroup = d3.select(this);

          if (brokenInputs) {
            labelGroup.classed("vzb-invisible", brokenInputs);
            lineGroup.classed("vzb-invisible", brokenInputs);
            return;
          }

          const text = labelGroup.selectAll("." + _cssPrefix + "-label-content")
            .text(valueL);

          _this._updateLabelSize(d, index, null, labelGroup, valueLST, text);

          _this.positionLabel(d, index, null, this, duration, showhide, lineGroup);
        });
    }
  },

  _initNewCache(cached, valueX, valueY, valueS, valueC, valueLST) {
    if (valueS || valueS === 0) cached.scaledS0 = utils.areaToRadius(this.context.sScale(valueS));
    cached.labelX0 = valueX;
    cached.labelY0 = valueY;
    cached.valueLST = valueLST;
    cached.scaledC0 = valueC != null ? this.context.cScale(valueC) : this.context.COLOR_WHITEISH;
  },

  _initLabelTooltip() {
    this.tooltipEl = this.labelsContainer.append("g")
      .attr("class", this.options.CSS_PREFIX + "-tooltip");
  },

  setTooltip(d, tooltipText, tooltipCache, labelValues) {
    if (tooltipText) {
      let position = 0;
      const _cssPrefix = this.options.CSS_PREFIX;
      this.tooltipEl.raise().text(null);
      this.label(this.tooltipEl, true);
      if (d) {
        const cache = {};
        this._initNewCache(cache, labelValues.valueX, labelValues.valueY, labelValues.valueS, labelValues.valueC, labelValues.valueLST);
        this.tooltipEl
          .classed(this.options.CSS_PREFIX + "-tooltip", false)
          .classed(this.options.CSS_PREFIX + "-entity", true)
          .selectAll("." + _cssPrefix + "-label-content")
          .text(labelValues.labelText);
        this._updateLabelSize(d, null, cache, this.tooltipEl, labelValues.valueLST);
        position = this.positionLabel(d, null, cache, this.tooltipEl.node(), 0, null, this.tooltipEl.select(".lineemptygroup"));
      }
      this.tooltipEl
        .classed(this.options.CSS_PREFIX + "-entity", false)
        .classed(this.options.CSS_PREFIX + "-tooltip", true)
        .selectAll("." + _cssPrefix + "-label-content")
        .text(tooltipText);
      this._updateLabelSize(d, null, tooltipCache, this.tooltipEl, null);
      this.positionLabel(d, null, tooltipCache, this.tooltipEl.node(), 0, null, this.tooltipEl.select(".lineemptygroup"), position);
    } else {
      this.tooltipEl.text(null);
    }
  },

  setTooltipFontSize(fontSize) {
    this.tooltipEl.style("font-size", fontSize);
  },

  _updateLabelSize(d, index, cache, labelGroup, valueLST, text) {
    const _this = this;
    const KEY = this.KEY;
    const cached = cache || _this.cached[d[KEY]];


    const _cssPrefix = this.options.CSS_PREFIX;

    const labels = _this.model.ui.chart.labels || {};
    labelGroup.classed("vzb-label-boxremoved", labels.removeLabelBox);

    const _text = text || labelGroup.selectAll("." + _cssPrefix + "-label-content");

    if (_this.labelSizeTextScale) {
      if (valueLST != null) {
        const range = _this.labelSizeTextScale.range();
        const fontSize = range[0] + Math.sqrt((_this.labelSizeTextScale(valueLST) - range[0]) * (range[1] - range[0]));
        _text.attr("font-size", fontSize + "px");
        cached.fontSize = fontSize;
        if (!cached.initFontSize) cached.initFontSize = fontSize;
      } else {
        _text.attr("font-size", null);
        cached.fontSize = parseFloat(_text.style("font-size"));
        if (!cached.initFontSize) cached.initFontSize = cached.fontSize;
      }
    } else {
      cached.fontSize = parseFloat(_text.style("font-size"));
      if (!cached.initFontSize) cached.initFontSize = cached.fontSize;
    }

    let contentBBox;
    if (!cached.initTextBBox) {
      //turn off stroke because ie11/edge return stroked bounding box for text
      _text.style("stroke", "none");
      cached.initTextBBox = _text.node().getBBox();
      _text.style("stroke", null);
      contentBBox = cached.textBBox = {
        width: cached.initTextBBox.width,
        height: cached.initTextBBox.height
      };
    }

    const scale = cached.fontSize / cached.initFontSize;
    cached.textBBox.width = cached.initTextBBox.width * scale;
    cached.textBBox.height = cached.initTextBBox.height * scale;

    contentBBox = cached.textBBox;

    const rect = labelGroup.selectAll("rect");

    if (!cached.textWidth || cached.textWidth != contentBBox.width) {
      cached.textWidth = contentBBox.width;

      const labelCloseHeight = _this._closeCrossHeight || contentBBox.height;//_this.activeProfile.infoElHeight * 1.2;//contentBBox.height;

      const isRTL = _this.model.locale.isRTL();
      const labelCloseGroup = labelGroup.select("." + _cssPrefix + "-label-x")
        .attr("transform", "translate(" + (isRTL ? -contentBBox.width - 4 : 4) + "," + (-contentBBox.height * 0.85) + ")");

      this.updateLabelCloseGroupSize(labelCloseGroup, labelCloseHeight);

      //cache label bound rect for reposition
      const rectBBox = cached.rectBBox = {
        x: -contentBBox.width - 4,
        y: -contentBBox.height * 0.85,
        width: contentBBox.width + 8,
        height: contentBBox.height * 1.2
      };
      cached.rectOffsetX = rectBBox.width + rectBBox.x;
      cached.rectOffsetY = rectBBox.height + rectBBox.y;

      rect.attr("width", rectBBox.width)
        .attr("height", rectBBox.height)
        .attr("x", rectBBox.x)
        .attr("y", rectBBox.y)
        .attr("rx", contentBBox.height * 0.2)
        .attr("ry", contentBBox.height * 0.2);
    }

    const glowRect = labelGroup.select(".vzb-label-glow");
    if (glowRect.attr("stroke") !== cached.scaledC0) {
      glowRect.attr("stroke", cached.scaledC0);
    }
  },

  _clearInitialFontSize() {
    utils.forEach(this.cached, cache => {
      if (!cache) return;
      cache.initFontSize = null;
      cache.initTextBBox = null;
    });
  },

  updateLabelCloseGroupSize(labelCloseGroup, labelCloseHeight) {
    labelCloseGroup.select("circle")
      .attr("cx", /*contentBBox.height * .0 + */ 0)
      .attr("cy", 0)
      .attr("r", labelCloseHeight * 0.5);

    labelCloseGroup.select("svg")
      .attr("x", -labelCloseHeight * 0.5)
      .attr("y", labelCloseHeight * -0.5)
      .attr("width", labelCloseHeight)
      .attr("height", labelCloseHeight);

  },

  updateLabelsOnlyTextSize() {
    const _this = this;
    const KEYS = this.KEYS;
    const KEY = this.KEY;

    this.entityLabels.each(function(d, index) {
      const cached = _this.cached[d[KEY]];
      _this._updateLabelSize(d, index, null, d3.select(this), _this.context.frame.size_label[utils.getKey(d, KEYS)]);
      const lineGroup = _this.entityLines.filter(f => f[KEY] == d[KEY]);
      _this.positionLabel(d, index, null, this, 0, null, lineGroup);
    });
  },

  updateLabelOnlyPosition(d, index, cache) {
    const _this = this;
    const KEY = this.KEY;
    const cached = this.cached[d[KEY]];
    if (cache) utils.extend(cached, cache);

    const lineGroup = _this.entityLines.filter(f => f[KEY] == d[KEY]);

    this.entityLabels.filter(f => f[KEY] == d[KEY])
      .each(function(groupData) {
        _this.positionLabel(d, index, null, this, 0, null, lineGroup);
      });
  },

  updateLabelOnlyColor(d, index, cache) {
    const _this = this;
    const KEY = this.KEY;
    const cached = this.cached[d[KEY]];
    if (cache) utils.extend(cached, cache);

    const labelGroup = _this.entityLabels.filter(f => f[KEY] == d[KEY]);

    _this._updateLabelSize(d, index, null, labelGroup, null);

  },

  positionLabel(d, index, cache, context, duration, showhide, lineGroup, position) {
    const KEY = this.KEY;
    const cached = cache || this.cached[d[KEY]];

    const lockPosition = (position || position === 0);
    const hPos = (position || 0) & 1;
    const vPos = ((position || 0) & 2) >> 1;
    let hPosNew = 0;
    let vPosNew = 0;
    const viewWidth = this.context.width;
    const viewHeight = this.context.height;

    const resolvedX0 = this.xScale(cached.labelX0);
    const resolvedY0 = this.yScale(cached.labelY0);

    const offsetX = cached.rectOffsetX;
    const offsetY = cached.rectOffsetY;

    if (!cached.labelOffset) cached.labelOffset = [0, 0];

    cached.labelX_ = cached.labelOffset[0] || (-cached.scaledS0 * 0.75 - offsetX) / viewWidth;
    cached.labelY_ = cached.labelOffset[1] || (-cached.scaledS0 * 0.75 - offsetY) / viewHeight;

    //check default label position and switch to mirror position if position
    //does not bind to visible field
    let resolvedX = resolvedX0 + cached.labelX_ * viewWidth;
    let resolvedY = resolvedY0 + cached.labelY_ * viewHeight;
    if (cached.labelOffset[0] + cached.labelOffset[1] == 0) {
      if ((!lockPosition && (resolvedY - cached.rectBBox.height + offsetY <= 0)) || vPos) { // check top
        vPosNew = 1;
        cached.labelY_ = (cached.scaledS0 * 0.75 + cached.rectBBox.height - offsetY) / viewHeight;
        resolvedY = resolvedY0 + cached.labelY_ * viewHeight;
      }
      //  else if (resolvedY + 10 > viewHeight) { //check bottom
      //   cached.labelY_ = (viewHeight - 10 - resolvedY0) / viewHeight;
      //   resolvedY = resolvedY0 + cached.labelY_ * viewHeight;
      // }

      if ((!lockPosition && (resolvedX - cached.rectBBox.width + offsetX <= 0)) || hPos) { //check left
        hPosNew = 1;
        cached.labelX_ = (cached.scaledS0 * 0.75 + cached.rectBBox.width - offsetX) / viewWidth;
        resolvedX = resolvedX0 + cached.labelX_ * viewWidth;
        if (resolvedX > viewWidth) {
          hPosNew = 0;
          vPosNew = (vPosNew == 0 && (resolvedY0 - offsetY * 0.5 - cached.scaledS0) < cached.rectBBox.height) ? 1 : vPosNew;
          cached.labelY_ = vPosNew ? -offsetY * 0.5 + cached.rectBBox.height + cached.scaledS0 : -offsetY * 1.5 - cached.scaledS0;
          cached.labelY_ /= viewHeight;
          resolvedY = resolvedY0 + cached.labelY_ * viewHeight;
          cached.labelX_ = (cached.rectBBox.width - offsetX - resolvedX0) / viewWidth;
          resolvedX = resolvedX0 + cached.labelX_ * viewWidth;
        }

      }
      //  else if (resolvedX + 15 > viewWidth) { //check right
      //   cached.labelX_ = (viewWidth - 15 - resolvedX0) / viewWidth;
      //   resolvedX = resolvedX0 + cached.labelX_ * viewWidth;
      // }
    }

    if (lockPosition) {
      let topCornerCase = false;
      if (resolvedX - cached.rectBBox.width + offsetX <= 0) {
        const deltaX = resolvedX0 - cached.rectBBox.width;
        const deltaY = deltaX > 0 ? utils.cathetus(cached.scaledS0, deltaX) : cached.scaledS0;
        resolvedY = vPosNew ?
          resolvedY0 + cached.rectBBox.height - offsetY * 0.5 + deltaY
          :
          resolvedY0 - offsetY * 1.5 - deltaY;
        if (resolvedY - cached.rectBBox.height < 0) {
          topCornerCase = true;
        }
      }
      if (resolvedY - cached.rectBBox.height + offsetY <= 0) {
        const deltaY = resolvedY0 - cached.rectBBox.height;
        const deltaX = deltaY > 0 ? utils.cathetus(cached.scaledS0, deltaY) : cached.scaledS0;
        resolvedX = hPosNew ?
          resolvedX0 + cached.rectBBox.width + deltaX
          :
          resolvedX0 - offsetX * 2 - deltaX;
        if (resolvedX - cached.rectBBox.width < 0 || resolvedX > viewWidth) {
          topCornerCase = true;
        }
      }
      if (topCornerCase) {
        vPosNew++;
        const deltaX = resolvedX0 - cached.rectBBox.width;
        resolvedY = resolvedY0 + cached.rectBBox.height - offsetY * 0.5 + (deltaX > 0 ? utils.cathetus(cached.scaledS0, deltaX) : cached.scaledS0);
      }
    }

    this.label._repositionLabels(d, index, cache, context, resolvedX, resolvedY, resolvedX0, resolvedY0, duration, showhide, lineGroup);

    return vPosNew * 2 + hPosNew;
  },

  updateSize() {
    const profiles = {
      small: {
        minLabelTextSize: 7,
        maxLabelTextSize: 21,
        defaultLabelTextSize: 12,
        labelLeashCoeff: 0.4
      },
      medium: {
        minLabelTextSize: 7,
        maxLabelTextSize: 30,
        defaultLabelTextSize: 15,
        labelLeashCoeff: 0.3
      },
      large: {
        minLabelTextSize: 6,
        maxLabelTextSize: 48,
        defaultLabelTextSize: 20,
        labelLeashCoeff: 0.2
      }
    };

    const presentationProfiles = {
      medium: {
        minLabelTextSize: 15,
        maxLabelTextSize: 35,
        defaultLabelTextSize: 15,
        labelLeashCoeff: 0.3
      },
      large: {
        minLabelTextSize: 20,
        maxLabelTextSize: 55,
        defaultLabelTextSize: 20,
        labelLeashCoeff: 0.2
      }
    };

    this.activeProfile = this.context.getActiveProfile(profiles, presentationProfiles);
    this.updateLabelSizeLimits();
  }

});

export default Labels;