Source

widgets/voronoi.js

import processStructure from '@devoinc/applications-data-library/structures/voronoi';
import downloads from '@devoinc/applications-builder/libs/downloads';
import { __ } from '@devoinc/applications-builder/i18n';
import objects from '@devoinc/applications-builder/utils/objects';
import widgetFactory from '@devoinc/applications-builder/widgetFactory';
import dependencies from '../data/dependencies';

const VoronoiWidget = dependencies.require('widgets').VoronoiWidget;

/**
 * This widget represents grouped data as variables, displayed as
 * a tessellation of polygons whose proportions depend on the number of
 * events represented. These polygons are subdivided into smaller polygons
 * and constitute a hierarchical structure with as many levels as those
 * into which the data is divided (there is a legend above the chart
 * explaining that hierarchy).
 *
 * This widget is based on
 * [FoamTree]{@link https://get.carrotsearch.com/foamtree/demo/api/index.html#quick-start}
 * library.
 *
 *
 * @category Widgets
 * @module Voronoi
 * @see [base](module-base.html)
 * @see [collapser](module-collapser.html)
 * @see [dataSearch](module-dataSearch.html)
 * @see [download](module-download.html)
 * @see [info](module-info.html)
 * @see [lifeCycle](module-lifeCycle.html)
 * @see [listeners](module-listeners.html)
 * @see [loading](module-loading.html)
 * @see [menu](module-menu.html)
 * @see [screenshot](module-screenshot.html)
 * @see [zoom](module-zoom.html)
 * @tutorial widgets-voronoi
 */
export function mixin(self) {
  // Always apply the default info
  // Later could be replaced
  setDefaultInfo(self);

  return {
    /**
     * Determines the general type of layout to generate.
     * Depending on this option, FoamTree will produce polygon-based
     * organic-looking visualization or the traditional rectangular
     * tree map.
     *
     * The following values are supported:
     * - <strong>relaxed</strong>: produces polygonal organic-looking
     * arrangements based on Voronoi diagrams.
     * - <strong>ordered</strong>: produces the traditional rectangular
     * treemap using the strip algorithm.
     * - <strong>squarified</strong>: produces the traditional treemap
     * using the squarified algorithm.
     *
     * @param {string} layout - Layout type.
     * @instance
     */
    setLayout(layout) {
      self.settings.layout = layout;
    },

    /**
     * Determines the initial layout of groups' polygons.
     *
     * The following values are supported:
     * - <strong>fisheye</strong>: puts large groups in the center of the
     * visualization area and the smaller groups towards the corners.
     * - <strong>blackhole</strong>: puts small groups in the center of the
     * visualization and the larger groups towards the corners.
     * - <strong>ordered</strong>: lays out the groups in a left-to-right
     * lines, in the order the groups were specified in the data object.
     * - <strong>squarified</strong>: puts large groups close to the top-left
     * corner of the visualization and the small groups close to the
     * bottom-right corner.
     * - <strong>random</strong>: puts groups at random positions.
     *
     * <i>Note</i>: Applicable only when <i>setLayout</i> is set to <i>relaxed</i>.
     * @param {string} [relaxation='fisheye'] - Layout relaxation.
     * @instance
     */
    setRelaxationInitializer(relaxation) {
      self.settings.relaxationInitializer = relaxation;
      self.settings.initializer = relaxation;
    },

    /**
     * Enable legend display.
     * @param {boolean} enabled - Indicates if it's enabled.
     * @instance
     */
    setLegendVal(enabled) {
      self.settings.showLegendVal = enabled;
    },

    /**
     * Enable legend percentage display.
     * @param {boolean} enabled - Indicates if it's enabled.
     * @instance
     */
    setLegendPercentage(enabled) {
      self.settings.showLegendPerc = enabled;
    },

    /**
     * Display animation effects.
     * @param {boolean} enabled - Indicates if it's enabled.
     * @instance
     */
    setAnimation(enabled) {
      self.settings.annimation = enabled;
    },

    /**
     * The image to display in the attribution group.
     * The image can be specified as a relative or absolute
     * HTTP URL or a data URI.
     * @param {string} path - Path or url of an image.
     * @instance
     */
    setAttributionLogo(path) {
      self.settings.attributionLogo = path;
    },

    /**
     * Set groups whose weight is zero as visible.
     *
     * For more info vistit
     * [this info]{@link https://get.carrotsearch.com/foamtree/demo/api/index.html#showZeroWeightGroups}.
     *
     * @param {boolean} showzero - Indicates if it's enabled.
     * @instance
     */
    setShowZero(showzero) {
      self.settings.showZeroVal = showzero;
    },

    /**
     * Set the operation used to scale data.
     *
     * The following values are supported:
     * - <b>none</b>: No operation.
     * - <b>log</b>: Logarithmic operation.
     * - <b>atan</b>: Arctangent operation.
     * - <b>pow.75</b>: Power 0.75 operation.
     * - <b>pow.33</b>: Power 0.33 operation.
     * - <b>pow.25</b>: Power 0.25 operation.
     * - <b>sqrt</b>: Square root operation.
     * @param {string} operation - Operation name.
     * @default 'none'
     * @instance
     */
    setOperation(operation) {
      self.settings.operation = operation;
    },

    /**
     * Set the data representation.
     *
     * The following values are supported:
     * - <b>['toBytes']</b>: Byte representation.
     * - <b>['secondsToHMS']</b>: Time representation
     * @param {string[]} valueFormat - Array with the string name of the format.
     * @instance
     */
    setValueFormat(valueFormat) {
      self.settings.valueFormat = valueFormat;
    },

    /**
     * Set scroll or zoom to enabled.
     * @param {boolean} enable - Indicates if it's enabled.
     * @default false
     * @instance
     */
    setScrollZoom(enable) {
      self.settings.scrollZoom = enable;
    },

    /**
     * Set maximum total nodes per level.
     * @param {number} [num=1000] - Number of nodes to show.
     * @instance
     */
    setMaxLevelItems(num = 1000) {
      self.settings.maxLevelItems = num;
    },

    /**
     * Set maximum total nodes.
     * @param {number} [num=1000] - Number of nodes to show.
     * @instance
     */
    setMaxItems(num = 1000) {
      self.settings.maxItems = num;
    },

    /**
     * When color scale is enabled, select how color should be calculated.
     * It can be according to all nodes, by siblings or by all elements within
     * the same depth.
     *
     * The following values are supported:
     * - <b>all</b>: All nodes.
     * - <b>siblings</b>: Siblings nodes.
     * - <b>level</b>: Per level.
     * @default 'all'
     * @param {string} relation - Scale type.
     * @instance
     */
    setColorScaleRelativeTo(relation) {
      self.settings.colorScaleRelativeTo = relation;
    },

    /**
     * Set the key color for gradient:
     *
     * The following values are supported:
     * - <b>Default</b>
     * - <b>Artic</b>
     * - <b>Boreal</b>
     * - <b>Bruise</b>
     * - <b>Sunset</b>
     * - <b>Antartic</b>
     * - <b>Sky Burst</b>
     * - <b>Rioja Lightning</b>
     * - <b>Kamehameha</b>
     * - <b>Alien</b>
     * - <b>Rainbow</b>
     * @param {String} colorScale - Color scale name.
     * @instance
     */
    setColorScale(colorScale) {
      self.settings.colorScale = colorScale;
    },

    /**
     * Set the exposed groups.
     *
     * For more info vistit
     * [this info]{@link https://get.carrotsearch.com/foamtree/demo/api/index.html#expose}.
     * @param {string[]} exposeCurrentNode - Array of groups to expose.
     * @instance
     */
    setExposeCurrentNode(exposeCurrentNode) {
      self.settings.exposeCurrentNode = exposeCurrentNode;
    },

    /**
     * Set the name of the values.
     * @default ['count']
     * @param {string[]} dataLabels - Array of labels.
     * @instance
     */
    setDataLabels(dataLabels) {
      self.settings.dataLabels = dataLabels;
    },

    /**
     * Set the keys to display.
     * @param {string[]} keys - Array of keys.
     * @instance
     */
    setKeys(keys) {
      self.settings.keyToShow = keys;
    },

    /**
     * Set the order of keys.
     * @param {string[]} keysOrder - Array of keys ordered.
     * @instance
     */
    setKeysOrder(keysOrder) {
      self.settings.keysOrder = keysOrder;
    },

    /**
     * Set the value to display.
     * @example { value: 'count'}
     * @param {Object} value
     * @param {string} value.value - Value to show.
     * @instance
     */
    setValue(value) {
      self.settings.valToShow = value;
    },

    /**
     * Determines how FoamTree will display nested groups.
     * When it's set true, produces a flattened view of the hierarchy.
     * @param {boolean} bool - Boolean value.
     * @instance
     */
    setFlatSignals(bool) {
      self.settings.flatSignals = bool;
    },

    /**
     * Enable detailed view for comparations.
     * @param {boolean} [bool=true] Enable detailed view.
     * @instance
     */
    setEnableDetailView(bool = true) {
      self.settings.enableDetailView = bool;
    },

    /**
     * Set enabled color scale selector.
     * @param {boolean} [bool=true] - Enable color scale.
     * @instance
     */
    setEnableColorScale(bool = true) {
      self.settings.enableColorScale = bool;
    },

    /**
     * Enable the size selector.
     * @param {boolean} [bool=true] - Enable size selector.
     * @instance
     */
    setEnableSizeSelect(bool = true) {
      self.settings.enableSizeSelect = bool;
    },

    /**
     * Enable the partitioning functionality.
     * @param {boolean} [bool=true] - Enable partitioning
     * @instance
     */
    setEnablePartitioning(bool = true) {
      self.settings.enablePartitioning = bool;
    },

    /**
     * Set the border width on partitioning.
     * @param {number} [num=2] - Number of width border.
     * @instance
     */
    setPartitioningBorder(num = 2) {
      self.settings.enablePartitioning = num;
    },

    /**
     * Set the darken flat parents.
     * @param {boolean} [bool=true] - Enable darken flat parents.
     * @instance
     */
    setDarkenFlatParents(bool = true) {
      self.settings.darkenFlatParents = bool;
    },

    /**
     * Set the darken flat parents colors when it's enabled.
     * @param {string[]} [arr=['#595959', '#080808']] - Array of colors.
     * @instance
     */
    setDarkenFlatParentsColor(arr = ['#595959', '#080808']) {
      self.settings.darkenFlatParentsColor = arr;
    },

    /**
     * Set the hover effect color when use darken flat parents is enabled.
     * @param {string} [color='#797878'] - Color for the hover effect.
     * @instance
     */
    setDarkenFlatParentsHoveredColor(color = '#797878') {
      self.settings.darkenFlatParentsHoveredColor = color;
    },

    /**
     * Index for the dataLabels value that it's used to calculate the
     * node color when color scale is enabled.
     *
     * It is related to the setting <i>setDataLabels</i>.
     * @param {number} [index=0] - Index number of datalabel.
     * @see setEnableColorScale
     * @see setDataLabels
     * @instance
     */
    setColorSelection(index = 0) {
      self.settings.colorSelection = index;
    },

    /**
     * Set the index for the dataLabels value that it's used to calculate
     * the node size.
     *
     * It's related to the setting <i>setDataLabels</i>.
     * @param {Number} [index=0] - Index number of datalabel.
     * @see setEnableSizeSelect
     * @see setDataLabels
     * @instance
     */
    setSizeSelection(index = 0) {
      self.settings.sizeSelection = index;
    },

    /**
     * Attach an event handler function for one event to the widget.
     *
     * The list of events allowed is:
     * - <b>displayHelp</b>
     * - <b>voroSearchChange</b>
     * - <b>onKeyUp</b>
     * - <b>onGroupSelectionChanged</b>
     * - <b>onGroupClick</b>
     * - <b>onGroupDoubleClick</b>
     * - <b>onGroupHover</b>
     * - <b>onModelChanging</b>
     * - <b>onRolloutComplete</b>
     * - <b>onRedraw</b>
     * - <b>onModelChanged</b>
     * - <b>onRolloutStart</b>
     * - <b>onRolloutComplete</b>
     * - <b>onRelaxationStep</b>
     * - <b>onRedraw</b>
     * - <b>onViewReset</b>
     * - <b>onGroupSelectionChanging</b>
     * - <b>onGroupExposureChanging</b>
     * - <b>onGroupExposureChanged</b>
     * - <b>onGroupOpenOrCloseChanging</b>
     * - <b>onGroupOpenOrCloseChanged</b>
     * - <b>onGroupHold</b>
     * - <b>onGroupMouseMove</b>
     * - <b>onGroupMouseWheel</b>
     * - <b>onGroupMouseDown</b>
     * - <b>onGroupMouseUp</b>
     * - <b>onGroupDragStart</b>
     * - <b>onGroupDrag</b>
     * - <b>onGroupDragEnd</b>
     * - <b>onGroupTransformStart</b>
     * - <b>onGroupTransform</b>
     * - <b>onGroupTransformEnd</b>
     * @param {string} str - Name of the event to listen.
     * @param {Function} func - Function to execute when the event is triggered.
     * @instance
     */
    on(str, func) {
      if (!self.settings.on) self.settings.on = [];
      self.settings.on.push({ on: [str], perform: func });
    },

    /**
     * Set a custom property to the widget.
     * @param {string} key - Key to set, using dot notation.
     * @param {*} val - Value to set
     * @instance
     */
    setCustomProperty(key, val) {
      objects.set(self.settings, key, val);
    },

    // Common
    // -------------------------------------------------------------------------

    /**
     * Download CSV
     * @instance
     * @ignore
     */
    downloadCSV() {
      let s = null;
      let isCorrectData =
        self.widget &&
        self.widget.data &&
        self.widget.data.data &&
        Array.isArray(self.widget.data.data);
      if (isCorrectData) {
        let data = self.widget.data.data;
        let totVals = self.widget.data.data.length;
        let totSeries = totVals > 0 ? self.widget.data.data[0].length : 0;
        let header = self.widget.data.kKeys.keys.join(',') + ',value\n';
        let content = '';
        if (totVals > 0 && totSeries > 0) {
          for (var i = 0; i < totSeries; i += 1) {
            for (var j = 0; j < totVals; j += 1) {
              content += data[j][i] + ',';
            }
            content = content.slice(0, -1) + '\n';
          }
          s = header + content;
        }
      } else {
        console.error(`No data for download in widget "${self.id}"`);
      }
      if (s) downloads.downloadCSV(s, `${self.id}-data`);
    },

    /**
     * Redraw function
     * @instance
     * @ignore
     */
    redraw() {
      if (self.widget) {
        self.widget._initFoamtreeParentContainer($(self.graphic).height());
        self.widget._addFoamtreeWidget({ size: true });
      }
    },

    /**
     * Resize function
     * @instance
     * @ignore
     */
    resize() {
      this.redraw();
    },

    // Life Cycle
    // -------------------------------------------------------------------------

    /**
     * Render the data
     * @param {Object} orig Object of data
     * @ignore
     */
    render(orig) {
      if (!self.el) return; // If not have element not render
      let cfg = self.settings;
      let data = processStructure(
        orig,
        cfg.keyToShow,
        cfg.valToShow,
        cfg.keysOrder
      );
      if (data.data) {
        if (!data.error) {
          if (self.widget && self.widget._domContainer) {
            self.widget._domContainer = null;
          }
          this.displayWashemoWidget(VoronoiWidget, cfg, data);
        } else {
          this.debugError({
            msg: 'WRONG DATA',
            console: {
              method: 'error',
              msg: 'Wrong data arrive to render function',
            },
          });
        }
      } else {
        this.debugError({
          msg: 'NO DATA',
          console: {
            method: 'error',
            msg: 'No data arrive to render function',
          },
        });
      }
    },
  };
}

function setDefaultInfo(self) {
  var interactions = VoronoiWidget.prototype.interactions
    ? VoronoiWidget.prototype.interactions
    : [];

  let message = `<ul>
    <li>
      <label class="stronger">${__('Voronoi shortcuts')}</label>
    </li>`;
  for (let i = 0; i < interactions.length; i++) {
    message += '<li><ul>';
    for (let j = 0; j < interactions[i].length; j++) {
      message +=
        '<li><label class="stronger">' +
        interactions[i][j].desktop +
        '</label> ' +
        interactions[i][j].action +
        '</li>';
    }
    message += '</ul></li>';
  }
  message += '</ul>';

  self.info = {
    title: __('Control panel'),
    content: message,
  };
}

export default widgetFactory(mixin);