jens-ox/metrics-graphics

View on GitHub
packages/lib/src/js/components/tooltip.js

Summary

Maintainability
A
0 mins
Test Coverage
import constants from '../misc/constants'

/**
 * Set up a new tooltip.
 *
 * @param {Object} args argument object.
 * @param {String} [args.legendObject='line'] symbol to show in the tooltip (circle, line, square).
 * @param {Array} [args.legend=[]] legend used to correctly describe data in multi-dimensional cases.
 * @param {Array} [args.colors=schemeCategory10] color scheme for multi-dimensional cases.
 * @param {Function} [args.textFunction] custom text function for the tooltip text. Generated from xAccessor and yAccessor if not
 * @param {Array} [args.data=[]] entries to show in the tooltip. This is usually empty when first instantiating the tooltip.
 * @param {Number} [args.left=0] margin to the left of the tooltip.
 * @param {Number} [args.top=0] margin to the top of the tooltip.
 * @param {Function} [args.xAccessor] if no custom text function is specified, this function specifies how to get the x value from a specific data point.
 * @param {Function} [args.yAccessor] if no custom text function is specified, this function specifies how to get the y value from a specific data point.
 */
export default class Tooltip {
  legendObject = 'line'
  legend = []
  colors = constants.defaultColors
  data = []
  left = 0
  top = 0
  node = null

  constructor ({ legendObject, legend, colors, textFunction, data, left, top, xAccessor, yAccessor } = {}) {
    this.legendObject = legendObject ?? this.legendObject
    this.legend = legend ?? this.legend
    this.colors = colors ?? this.colors
    this.setTextFunction(textFunction, xAccessor, yAccessor)
    this.data = data ?? this.data
    this.left = left ?? this.left
    this.top = top ?? this.top
  }

  /**
   * Sets the text function for the tooltip.
   *
   * @param {Function} [textFunction] custom text function for the tooltip text. Generated from xAccessor and yAccessor if not
   * @param {Function} [xAccessor] if no custom text function is specified, this function specifies how to get the x value from a specific data point.
   * @param {Function} [yAccessor] if no custom text function is specified, this function specifies how to get the y value from a specific data point.
   * @returns {void}
   */
  setTextFunction (textFunction, xAccessor, yAccessor) {
    this.textFunction = textFunction || (xAccessor && yAccessor
      ? this.baseTextFunction({ xAccessor, yAccessor })
      : this.textFunction)
  }

  /**
   * If no textFunction was specified when creating the tooltip instance, this method generates a text function based on the xAccessor and yAccessor.
   *
   * @param {Function} xAccessor returns the x value of a given data point.
   * @param {Function} yAccessor returns the y value of a given data point.
   * @returns {Function} base text function used to render the tooltip for a given datapoint.
   */
  baseTextFunction ({ xAccessor, yAccessor }) {
    return point => `${xAccessor(point)}: ${yAccessor(point)}`
  }

  /**
   * Update the tooltip.
   *
   * @param {Array} data array of data points to be shown in the tooltip.
   * @param {String} legendObject update the type of legend object to be shown in the tooltip (line, circle, square).
   * @param {Array} legend legend used by the graph.
   * @returns {void}
   */
  update ({ data, legendObject, legend }) {
    this.data = data ?? this.data
    this.legendObject = legendObject ?? this.legendObject
    this.legend = legend ?? this.legend
    this.addText()
  }

  /**
   * Hide the tooltip (without destroying it).
   * @returns {void}
   */
  hide () {
    this.node.attr('opacity', 0)
  }

  /**
   * Mount the tooltip to the given d3 node.
   *
   * @param {Object} svg d3 node to mount the tooltip to.
   * @returns {void}
   */
  mountTo (svg) {
    this.node = svg
      .append('g')
      .style('font-size', '0.7rem')
      .attr('transform', `translate(${this.left},${this.top})`)
      .attr('opacity', 0)
    this.addText()
  }

  /**
   * Adds the text to the tooltip.
   * For each datapoint in the data array, one line is added to the tooltip.
   *
   * @returns {void}
   */
  addText () {
    // first, clear existing content
    this.node.selectAll('*').remove()

    // second, add one line per data entry
    this.node.attr('opacity', 1)
    this.data.forEach((datum, index) => {
      const symbol = constants.symbol[this.legendObject]
      const realIndex = datum.arrayIndex ?? index
      const color = this.colors[realIndex]
      const node = this.node
        .append('text')
        .attr('text-anchor', 'end')
        .attr('y', index * 12)

      // category
      node
        .append('tspan')
        .classed('text-category', true)
        .attr('fill', color)
        .text(this.legend[realIndex])

      // symbol
      node
        .append('tspan')
        .attr('dx', '0.5rem')
        .attr('fill', color)
        .text(symbol)

      // text
      node
        .append('tspan')
        .attr('dx', '0.5rem')
        .text(this.textFunction(datum))
    })
  }
}