_treemap.js

import _ from 'lodash';
import Graphics from './graphics';
import { colors, EasingFunctions } from './variable';
import Text from './text';
import Tooltip from './tooltip';
import Utility from './utility';
// import Container from './_container';

/**
 * Treemap Chart를 생성, 관리하는 클래스
 * <p>ChartJS에서 지원하지 않는 타입의 차트라 직접 만들어줌
 * @alias Treemap
 */
export default class Treemap {
  /**
   * @description Treemap차트에서 필요한 값들을 설정
   * @param  { object } option 차트 생성에 필요한 옵션
   * @param  { object } data 차트 생성에 필요한 데이터
   */
  constructor(option, data) {
    const self = this;

    /**
     * @description PIXI 오브젝트
     * @type { PIXI }
     */
    this.pixi = Utility.makePixi(option, true);

    /**
     * @description 값의 단위
     * @type { string }
     */
    this.unit = option.data.unit;

    /**
     * @description 최상단 DOM 사이즈
     * @type { object }
     */
    this.rootSize = option.rootSize;

    /**
     * @description 차트 데이터
     * @type { object }
     */
    this.data = data;

    /**
     * @description 현재 Treemap 깊이
     * @type { number }
     */
    this.depth = 1;

    /**
     * @description Treemap Animation 여부
     * @type { boolean }
     */
    this.animation = true;

    /**
     * @description Treemap 한단계 더 깊이 들어갈 때 펼쳐지는 효과 시간
     * @type { number }
     */
    this.expandDuration = 0.5;

    /**
     * @description 한단계 더 들어가서 새로운 Treemap 요소들이 펼쳐지는 시간
     * @type { number }
     */
    this.drawTreemapWithAnimationDuration = 0.5;

    /**
     * @description 새로운 요소들이 펼쳐지면서 각각이 온전한 상태로 변하는 시간
     * @type { number }
     */
    this.drawTreemapSeparatelyDuration = 0.3;

    /**
     * @description 새로운 요소들이 펼쳐지면서 각각이 온전한 상태로 변하는 길이
     * @type { number }
     */
    this.drawTreemapSeparatelyDistance = this.pixi.renderer.height / 60;

    /**
     * @description 현재 애니메이션 진행중 여부
     * @type { boolean }
     */
    this.animating = false;

    /**
     * @description 지워줘야 할 오브젝트 저장
     * @type { array }
     */
    this.deletableDepth = [];

    /**
     * @description 펼쳐지는 Treemap 요소 정보를 저장
     * @type { array }
     */
    this.expandInformation = [];

    /**
     * @description Treemap 차트에서 사용 될 Tooltip 오브젝트
     * @type { Tooltip }
     */
    this.tooltip = new Tooltip(this.pixi);
    this.tooltip.setUnit(this.unit);

    /**
     * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 사용된 컨테이너 개념
     * @alias Container
     * @memberof Treemap
     */
    this.Container = class Container {
      constructor(xoffset, yoffset, width, height) {
        this.xoffset = xoffset;
        this.yoffset = yoffset;
        this.height = height;
        this.width = width;
      }

      shortestEdge() {
        return Math.min(this.height, this.width);
      }

      getCoordinates(row) {
        const coordinates = [];
        let subxoffset = this.xoffset;
        let subyoffset = this.yoffset; // our offset within the container
        const areawidth = self.sumArray(row) / this.height;
        const areaheight = self.sumArray(row) / this.width;
        let i;

        if (this.width >= this.height) {
          for (i = 0; i < row.length; i++) {
            coordinates.push([subxoffset, subyoffset, subxoffset + areawidth, subyoffset + row[i] / areawidth]);
            subyoffset += row[i] / areawidth;
          }
        } else {
          for (i = 0; i < row.length; i++) {
            coordinates.push([subxoffset, subyoffset, subxoffset + row[i] / areaheight, subyoffset + areaheight]);
            subxoffset += row[i] / areaheight;
          }
        }
        return coordinates;
      }

      cutArea(area) {
        let newcontainer;

        if (this.width >= this.height) {
          const areawidth = area / this.height;
          const newwidth = this.width - areawidth;
          newcontainer = new Container(this.xoffset + areawidth, this.yoffset, newwidth, this.height);
        } else {
          const areaheight = area / this.width;
          const newheight = this.height - areaheight;
          newcontainer = new Container(this.xoffset, this.yoffset + areaheight, this.width, newheight);
        }
        return newcontainer;
      }
    };
    const calculatedTreemap = this.calculateTreemap(this.data);
    this.drawTreemap(calculatedTreemap, this.data);
  }

  /**
   * @description Treemap의 데이터를 통해 그려야할 영역을 계산
   * @param {object} datas Treemap의 데이터
   * @memberof Treemap
   * @instance
   */
  calculateTreemap(datas) {
    this.treemapData = [];
    this.labels = [];
    for (let i = 0; i < datas.length; i++) {
      const eachData = datas[i];
      const value = this.getSumOfChildrenData(eachData);
      this.treemapData.push(value);
      this.labels.push(eachData.label);
    }
    const size = this.rootSize;
    const calculatedData = this.treemapMultidimensional(this.treemapData, size.width, size.height, 0, 0);
    return calculatedData;
  }

  /**
   * @description calculateTreemap 함수를 통해 계산된 데이터를 가지고 Treemap을 그려줌
   * @param {object} calculatedData 계산된 Treemap 영역 데이터
   * @param {object} originalDatas Treemap 오리지널 데이터
   * @memberof Treemap
   * @instance
   */
  drawTreemap(calculatedData, originalDatas) {
    let colorIndex = (this.depth - 1);
    const objectsArray = [];
    for (let i = 0; i < calculatedData.length; i++) {
      const data = calculatedData[i];
      const originalData = originalDatas[i];
      const width = data[2] - data[0];
      const height = data[3] - data[1];
      const centerX = data[0] + width / 2;
      const centerY = data[1] + height / 2;
      const graphics = new Graphics(this.pixi);
      const color = colors[colorIndex];

      objectsArray.push(graphics);
      graphics.setColor(color);
      graphics.object.zOrder = -this.depth;
      // graphics.setAlpha(0.7);

      graphics.object.interactive = true;
      if (_.isArray(originalData.data)) {
        graphics.object.buttonMode = true;
      }


      graphics.drawRect(data[0], data[1], width, height);
      const text = new Text(this.pixi, `${this.labels[i]}`);
      text.object.zOrder = -this.depth;
      // text.setColorBasedOnBackground(color);
      // text.setColor([0, 0, 0]);
      text.object.style.fill = 0x000000;

      objectsArray.push(text);

      const textScale = 0.5 * this.pixi.renderer.width * (1 / 339);
      text.object.scale.set(textScale > 1 ? 1 : textScale);
      if (text.object.width > width) {
        text.object.text = '…';
      }

      text.setPosition(centerX, centerY);

      colorIndex += 6;
      if (colorIndex >= colors.length) {
        colorIndex -= colors.length;
      }

      graphics.object.on('mouseover', () => {
        this.tooltip.setData(originalData.label, this.getSumOfChildrenData(originalData));
        this.tooltip.setPosition(centerX, centerY);
        this.tooltip.show();
        this.tooltipShowTime = new Date();
      });

      graphics.object.on('mouseout', () => {
        if (new Date() - this.tooltipShowTime > 100) {
          this.tooltip.hide();
        }
      });

      // eslint-disable-next-line no-loop-func
      graphics.object.on('pointerdown', (e) => {
        const clickButton = e.data.originalEvent.button; // 0 = left, 1 = right
        switch (clickButton) {
          case 0:
            if (graphics.object.buttonMode && this.animating === false) {
              this.animating = true;
              this.depth += 1;
              this.tooltip.hide();
              this.expand({
                centerX,
                centerY,
                width,
                height,
                rootWidth: this.pixi.renderer.width,
                rootHeight: this.pixi.renderer.height,
              }, color);
              const calculatedDataOfChild = this.calculateTreemap(originalData.data);
              setTimeout(() => {
                this.drawTreemapWithAnimation(calculatedDataOfChild, originalData.data);
              }, this.expandDuration * 1000 - (this.drawTreemapWithAnimationDuration / calculatedDataOfChild.length) * 1000);
              // this.drawTreemap(calculatedDataOfChild, originalData.data);
            }
            break;
          case 2:
            if (this.depth > 1) {
              // this.deleteDepth();
              // this.reduce();
            }
            break;
          default:
            break;
        }
      });
    }


    Utility.arrangePixiZOrder(this.pixi);

    if (this.depth > 1) {
      this.deletableDepth.push(objectsArray);
    }
  }

  /**
   * @description 애니메이션 효과와 함께 Treemap을 나타내기 위해 초기값을 설정하고 drawTreemapWithAnimationUpdate 함수를 실행시켜줌
   * @param {object} calculatedData 계산된 Treemap 영역 데이터
   * @param {object} originalDatas Treemap 오리지널 데이터
   * @memberof Treemap
   * @instance
   */
  drawTreemapWithAnimation(calculatedData, originalDatas) {
    if (this.drawTreemapWithAnimationUpdateInterval === undefined) {
      this.drawTreemapWithAnimationIndexArray = [];
      this.drawTreemapWithAnimationCheckPoint = 1 / calculatedData.length;
      const objectsArray = [];
      const colorIndex = this.depth - 1;
      this.drawTreemapWithAnimationStartTime = new Date();
      this.drawTreemapWithAnimationUpdateInterval = setInterval(() => { this.drawTreemapWithAnimationUpdate(calculatedData, originalDatas, colorIndex, objectsArray); }, 16);
    }
  }

  /**
   * @description 애니메이션 효과와 함께 Treemap을 나타내줌
   * @param {object} calculatedData 계산된 Treemap 영역 데이터
   * @param {object} originalDatas Treemap 오리지널 데이터
   * @param {number} colorIndex 색상 인덱스
   * @param {object} objectsArray 오브젝트를 담을 배열
   * @memberof Treemap
   * @instance
   */
  drawTreemapWithAnimationUpdate(calculatedData, originalDatas, colorIndex, objectsArray) {
    const elapsedTime = new Date() - this.drawTreemapWithAnimationStartTime;
    const progress = elapsedTime / (this.drawTreemapWithAnimationDuration * 1000);
    const checkPointLength = 1 / calculatedData.length;
    if (progress >= this.drawTreemapWithAnimationCheckPoint) {
      const index = Math.round(this.drawTreemapWithAnimationCheckPoint / checkPointLength) - 1; // 순서대로 인덱스
      // 랜덤 인덱스
      // let index;
      // do {
      //   index = Math.floor(Math.random() * calculatedData.length);
      // } while (this.drawTreemapWithAnimationIndexArray.includes(index));

      // this.drawTreemapWithAnimationIndexArray.push(index);

      const graphics = new Graphics(this.pixi);
      graphics.object.zOrder = -this.depth;
      graphics.object.interactive = true;
      if (_.isArray(originalDatas[index].data)) {
        graphics.object.buttonMode = true;
      }
      objectsArray.push(graphics);

      const width = calculatedData[index][2] - calculatedData[index][0];
      const height = calculatedData[index][3] - calculatedData[index][1];
      const centerX = calculatedData[index][0] + width / 2;
      const centerY = calculatedData[index][1] + height / 2;
      const text = new Text(this.pixi, `${originalDatas[index].label}`);
      objectsArray.push(text);
      text.object.zOrder = -this.depth;

      const textScale = 0.5 * this.pixi.renderer.width * (1 / 339);
      text.object.scale.set(textScale > 1 ? 1 : textScale);
      if (text.object.width > calculatedData) {
        text.object.text = '…';
      }

      text.setPosition(centerX, centerY);
      text.object.setAlpha = 0;

      colorIndex = 6 * index + colorIndex;
      if (colorIndex >= colors.length) {
        colorIndex -= colors.length * Math.floor(colorIndex / colors.length);
      }
      const color = colors[colorIndex];

      graphics.object.on('mouseover', () => {
        this.tooltip.setData(originalDatas[index].label, this.getSumOfChildrenData(originalDatas[index]));
        this.tooltip.setPosition(centerX, centerY);
        this.tooltip.show();
        this.tooltipShowTime = new Date();
      });

      graphics.object.on('mouseout', () => {
        if (new Date() - this.tooltipShowTime > 100) {
          this.tooltip.hide();
        }
      });

      graphics.object.on('pointerdown', (e) => {
        const clickButton = e.data.originalEvent.button; // 0 = left, 1 = right
        switch (clickButton) {
          case 0:
            if (graphics.object.buttonMode && this.animating === false) {
              this.animating = true;
              this.depth += 1;
              this.tooltip.hide();
              this.expand({
                centerX,
                centerY,
                width,
                height,
                rootWidth: this.pixi.renderer.width,
                rootHeight: this.pixi.renderer.height,
              }, color);
              const calculatedDataOfChild = this.calculateTreemap(originalDatas[index].data);
              setTimeout(() => {
                this.drawTreemapWithAnimation(calculatedDataOfChild, originalDatas[index].data);
              }, this.expandDuration * 1000);
              // this.drawTreemap(calculatedDataOfChild, originalData.data);
            }
            break;
          case 2:
            if (this.depth > 1) {
              if (this.animating === false) {
                this.tooltip.hide();
                this.animating = true;
                const tempArray = [];
                for (let j = 1; j < this.deletableDepth[this.depth - 2].length; j += 2) {
                  const eachGraphics = this.deletableDepth[this.depth - 2][j - 1];
                  const eachText = this.deletableDepth[this.depth - 2][j];
                  tempArray.push({ graphics: eachGraphics, text: eachText });
                  this.hideTreemapWithAnimation(calculatedData, originalDatas, tempArray);
                }
                setTimeout(() => {
                  this.reduce();
                }, this.drawTreemapWithAnimationDuration * 1000 + this.drawTreemapSeparatelyDuration * 1000);
              }
            }
            break;
          default:
            break;
        }
      });

      this.drawTreemapSeparately({ graphics, text }, {
        calculatedData: calculatedData[index],
        originalDatas: originalDatas[index],
        color,
      }, 1);

      this.drawTreemapWithAnimationCheckPoint += checkPointLength;
    }

    if (progress >= 1) {
      clearInterval(this.drawTreemapWithAnimationUpdateInterval);
      this.deletableDepth.push(objectsArray);
      setTimeout(() => { this.animating = false; }, this.drawTreemapSeparatelyDuration * 1000);
      delete this.drawTreemapWithAnimationUpdateInterval;
    }
  }

  /**
   * @description 애니메이션 효과와 함께 Treemap을 숨기기 위해 초기값을 설정하고 hideTreemapWithAnimationUpdate 함수를 실행시켜줌
   * @param {object} calculatedData 계산된 Treemap 영역 데이터
   * @param {object} originalDatas Treemap 오리지널 데이터
   * @param {objects} objects drawTreemapWithAnimationUpdate에서 담아두었던 오브젝트들
   * @memberof Treemap
   * @instance
   */
  hideTreemapWithAnimation(calculatedData, originalDatas, objects) {
    this.hideTreemapWithAnimationCheckPoint = 1 / calculatedData.length;
    if (this.hideTreemapWithAnimationUpdateInterval === undefined) {
      this.hideTreemapWithAnimationStartTime = new Date();
      this.hideTreemapWithAnimationUpdateInterval = setInterval(() => { this.hideTreemapWithAnimationUpdate(calculatedData, originalDatas, objects); }, 16);
    }
    this.tempvalue = 0;
  }

  /**
   * @description 애니메이션 효과와 함께 Treemap을 숨김
   * @param {object} calculatedData 계산된 Treemap 영역 데이터
   * @param {object} originalDatas Treemap 오리지널 데이터
   * @param {objects} objects drawTreemapWithAnimationUpdate에서 담아두었던 오브젝트들
   * @memberof Treemap
   * @instance
   */
  hideTreemapWithAnimationUpdate(calculatedData, originalDatas, objects) {
    const elapsedTime = new Date() - this.hideTreemapWithAnimationStartTime;
    const progress = elapsedTime / (this.drawTreemapWithAnimationDuration * 1000);
    const checkPointLength = 1 / calculatedData.length;
    if (progress >= this.hideTreemapWithAnimationCheckPoint) {
      const index = calculatedData.length - Math.round(this.hideTreemapWithAnimationCheckPoint / checkPointLength); // 순서대로 인덱스
      this.drawTreemapSeparately(objects[index], {
        calculatedData: calculatedData[index],
        originalDatas: originalDatas[index],
      }, -1);
      this.hideTreemapWithAnimationCheckPoint += checkPointLength;
    }

    if (progress >= 1) {
      clearInterval(this.hideTreemapWithAnimationUpdateInterval);
      delete this.hideTreemapWithAnimationUpdateInterval;
    }
  }

  /**
   * @description 애니메이션 효과와 함께 Treemap을 나타낼 때 개별적으로 Treemap 요소를 그려주기 위해 셋팅
   * @param {object} object Treemap 개별 요소의 오브젝트
   * @param {object} datas Treemap 요소 데이터
   * @param {objects} visible 1 = show , -1 = hide
   * @memberof Treemap
   * @instance
   */
  drawTreemapSeparately(object, datas, visible) {
    const startTime = new Date();
    visible = visible || 1;
    if (!_.isUndefined(object)) {
      const interval = setInterval(() => {
        this.drawTreemapSeparatelyUpdate(object, datas, startTime, interval, visible);
      }, 16);
    }
  }

  /**
   * @description 애니메이션 효과와 함께 Treemap을 나타낼 때 개별적으로 Treemap 요소를 그려줌
   * @param {object} object Treemap 개별 요소의 오브젝트
   * @param {object} datas Treemap 요소 데이터
   * @param {date} startTime 시작 시간
   * @param {number} interval setInterval 인덱스
   * @param {objects} visible 1 = show , -1 = hide
   * @memberof Treemap
   * @instance
   */
  drawTreemapSeparatelyUpdate(object, datas, startTime, interval, visible) {
    const elapsedTime = new Date() - startTime;
    const progress = elapsedTime / (this.drawTreemapSeparatelyDuration * 1000) > 1 ? 1 : elapsedTime / (this.drawTreemapSeparatelyDuration * 1000);
    const progressOnVisible = visible > 0 ? progress : 1 - progress;
    const drawX = datas.calculatedData[0] + (this.drawTreemapSeparatelyDistance - this.drawTreemapSeparatelyDistance * progressOnVisible);
    const drawWidth = datas.calculatedData[2] - drawX;
    const drawY = datas.calculatedData[1] - (this.drawTreemapSeparatelyDistance - this.drawTreemapSeparatelyDistance * progressOnVisible);
    const drawHeight = datas.calculatedData[3] - datas.calculatedData[1];
    const color = visible > 0 ? datas.color : object.graphics.colorRGB;

    object.graphics.object.clear();
    object.graphics.setAlpha(progressOnVisible);

    object.graphics.setColor(color);
    object.graphics.drawRect(drawX, drawY, drawWidth, drawHeight);

    object.text.setAlpha(progressOnVisible);
    // object.text.setColorBasedOnBackground(color);
    object.text.setPosition(drawWidth / 2 + drawX, drawHeight / 2 + drawY);
    if (progress >= 1 || _.isUndefined(object)) {
      clearInterval(interval);
      // if (visible === -1) {
      //   object.graphics.destroy();
      //   object.text.destroy();
      //   delete object.graphics;
      //   delete object.text;
      // }
      Utility.arrangePixiZOrder(this.pixi);
    }
  }

  /**
   * @description Treemap의 깊이 레벨이 낮아질 때 확대되었던 요소를 축소시켜줌
   * @memberof Treemap
   * @instance
   */
  reduce() {
    const expandInformation = this.expandInformation.pop();
    const { graphics, calculatedSquare, color } = expandInformation;
    if (this.expandUpdateInterval === undefined) {
      this.expandStartTime = new Date();
    }

    if (this.expandUpdateInterval === undefined) {
      this.expandUpdateInterval = setInterval(() => { this.expandUpdate(calculatedSquare, color, graphics, -1); }, 16);
    }
  }

  /**
   * @description Treemap의 깊이 레벨이 높아질 때 요소를 확대시켜주기 위해 데이터 셋팅
   * @memberof Treemap
   * @param {object} square 요소 사각형 정보
   * @param {number} color 색상 값
   * @instance
   */
  expand(square, color) {
    if (this.expandUpdateInterval === undefined) {
      const background = new Graphics(this.pixi);
      background.object.zOrder = -this.depth;

      Utility.arrangePixiZOrder(this.pixi);
      this.expandStartTime = new Date();
      const squareLeft = square.centerX - square.width / 2;
      const squareRight = square.centerX + square.width / 2;
      const squareTop = square.centerY - square.height / 2;
      const squareBottom = square.centerY + square.height / 2;

      const leftDistance = squareLeft;
      const rightDistance = square.rootWidth - squareRight;
      const topDistance = squareTop;
      const bottomDistance = square.rootHeight - squareBottom;


      const calculatedSquare = {
        left: squareLeft,
        right: squareRight,
        top: squareTop,
        bottom: squareBottom,
        distance: {
          left: leftDistance,
          right: rightDistance,
          top: topDistance,
          bottom: bottomDistance,
        },
        width: square.width,
        height: square.height,
      };

      this.expandInformation.push({ graphics: background, calculatedSquare, color });
      this.expandUpdateInterval = setInterval(() => { this.expandUpdate(calculatedSquare, color, background, 1); }, 16);
    }
  }

  /**
   * @description Treemap의 깊이 레벨이 높아질 때 요소를 확대/축소
   * @memberof Treemap
   * @param {object} square 요소 사각형 정보
   * @param {number} color 색상 값
   * @param {Graphics} background 요소를 그려주는 Graphics 오브젝트
   * @param {number} mode 확대 or 축소
   * @instance
   */
  expandUpdate(square, color, background, mode) {
    const elapsedTime = new Date() - this.expandStartTime;

    const easingTime = elapsedTime / (this.expandDuration * 1000);
    const easing = EasingFunctions.easeOutQuart;
    const progress = easingTime < 1 ? easing(easingTime) : 1;
    const applyProgress = mode > 0 ? progress : 1 - progress;
    const drawX = square.left - square.distance.left * applyProgress;
    const drawY = square.top - square.distance.top * applyProgress;
    const drawWidth = square.width + square.distance.right * applyProgress + square.distance.left * applyProgress;
    const drawHeight = square.height + square.distance.bottom * applyProgress + square.distance.top * applyProgress;

    const rDistance = 255 - color[0];
    const gDistance = 255 - color[1];
    const bDistance = 255 - color[2];
    const drawR = color[0] + Math.floor(rDistance * applyProgress);
    const drawG = color[1] + Math.floor(gDistance * applyProgress);
    const drawB = color[2] + Math.floor(bDistance * applyProgress);
    // eslint-disable-next-line no-unused-vars
    const drawColor = [drawR, drawG, drawB];
    const drawColorTemp = [color[0], color[1], color[2]];

    background.object.clear();
    background.setColor(drawColorTemp);
    background.drawRect(drawX, drawY, drawWidth, drawHeight);

    if (progress >= 1) {
      clearInterval(this.expandUpdateInterval);
      delete this.expandUpdateInterval;
      if (mode === -1) {
        this.animating = false;
        this.deleteDepth();
        background.destroy();
      }
    }
  }

  /**
   * @description Treemap의 깊이 레벨이 한단계 낮춰줌
   * @memberof Treemap
   * @instance
   */
  deleteDepth() {
    const objectsArray = this.deletableDepth.pop();
    for (let i = 0; i < objectsArray.length; i++) {
      const object = objectsArray[i];
      object.destroy();
      delete objectsArray[i];
    }
    this.depth -= 1;
  }

  /**
   * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 가져옴
   * <p> Gamify에 맞게 변형
   * @memberof Treemap
   * @instance
   */
  treemapSingledimensional(data, width, height, xoffset, yoffset) {
    xoffset = (typeof xoffset === 'undefined') ? 0 : xoffset;
    yoffset = (typeof yoffset === 'undefined') ? 0 : yoffset;

    const rawtreemap = this.squarify(this.normalize(data, width * height), [], new this.Container(xoffset, yoffset, width, height), []);
    return this.flattenTreemap(rawtreemap);
  }

  /**
   * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 가져옴
   * <p> Gamify에 맞게 변형
   * @memberof Treemap
   * @instance
   */
  treemapMultidimensional(data, width, height, xoffset, yoffset) {
    xoffset = (typeof xoffset === 'undefined') ? 0 : xoffset;
    yoffset = (typeof yoffset === 'undefined') ? 0 : yoffset;

    const mergeddata = [];
    let mergedtreemap;
    let results = [];
    let i;

    if (_.isArray(data[0])) { // if we've got more dimensions of depth
      for (i = 0; i < data.length; i++) {
        mergeddata[i] = this.sumMultidimensionalArray(data[i]);
      }
      mergedtreemap = this.treemapSingledimensional(mergeddata, width, height, xoffset, yoffset);

      for (i = 0; i < data.length; i++) {
        results.push(this.treemapMultidimensional(data[i], mergedtreemap[i][2] - mergedtreemap[i][0], mergedtreemap[i][3] - mergedtreemap[i][1], mergedtreemap[i][0], mergedtreemap[i][1]));
      }
    } else {
      results = this.treemapSingledimensional(data, width, height, xoffset, yoffset);
    }
    return results;
  }

  /**
   * @description 자신과 자식들의 데이터 총합을 구해줌
   * @param {object} object 데이터 오브젝트
   * @memberof Treemap
   * @instance
   */
  getSumOfChildrenData(object) {
    let sum = 0;
    if (_.isArray(object.data)) {
      for (let i = 0; i < object.data.length; i++) {
        sum += this.getSumOfChildrenData(object.data[i]);
      }
    } else {
      sum = parseInt(object.data, 10);
    }
    return sum;
  }

  /**
   * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 가져옴
   * <p> Gamify에 맞게 변형
   * @memberof Treemap
   * @instance
   */
  squarify(data, currentrow, container, stack) {
    let newcontainer;

    if (data.length === 0) {
      stack.push(container.getCoordinates(currentrow));
      return;
    }

    const length = container.shortestEdge();
    const nextdatapoint = data[0];

    if (this.improvesRatio(currentrow, nextdatapoint, length)) {
      currentrow.push(nextdatapoint);
      this.squarify(data.slice(1), currentrow, container, stack);
    } else {
      newcontainer = container.cutArea(this.sumArray(currentrow), stack);
      stack.push(container.getCoordinates(currentrow));
      this.squarify(data, [], newcontainer, stack);
    }
    // eslint-disable-next-line consistent-return
    return stack;
  }

  /**
   * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 가져옴
   * <p> Gamify에 맞게 변형
   * @memberof Treemap
   * @instance
   */
  normalize(data, area) {
    const normalizeddata = [];
    const sum = this.sumArray(data);
    const multiplier = area / sum;
    let i;

    for (i = 0; i < data.length; i++) {
      normalizeddata[i] = data[i] * multiplier;
    }
    return normalizeddata;
  }

  /**
   * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 가져옴
   * <p> Gamify에 맞게 변형
   * @memberof Treemap
   * @instance
   */
  flattenTreemap(rawtreemap) {
    const flattreemap = [];
    let i; let
      j;

    for (i = 0; i < rawtreemap.length; i++) {
      for (j = 0; j < rawtreemap[i].length; j++) {
        flattreemap.push(rawtreemap[i][j]);
      }
    }
    return flattreemap;
  }

  /**
   * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 가져옴
   * <p> Gamify에 맞게 변형
   * @memberof Treemap
   * @instance
   */
  improvesRatio(currentrow, nextnode, length) {
    if (currentrow.length === 0) {
      return true;
    }

    const newrow = currentrow.slice();
    newrow.push(nextnode);

    const currentratio = this.calculateRatio(currentrow, length);
    const newratio = this.calculateRatio(newrow, length);

    // the pseudocode in the Bruls paper has the direction of the comparison
    // wrong, this is the correct one.
    return currentratio >= newratio;
  }

  /**
   * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 가져옴
   * <p> Gamify에 맞게 변형
   * @memberof Treemap
   * @instance
   */
  calculateRatio(row, length) {
    const min = Math.min(...row);
    const max = Math.max(...row);
    const sum = this.sumArray(row);
    return Math.max((length ** 2) * max / (sum ** 2), (sum ** 2) / ((length ** 2) * min));
  }

  /**
   * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 가져옴
   * <p> Gamify에 맞게 변형
   * @memberof Treemap
   * @instance
   */
  sumArray(arr) {
    let sum = 0;
    let i;

    for (i = 0; i < arr.length; i++) {
      sum += arr[i];
    }
    return sum;
  }

  /**
   * @description [Treemap-squared]{@link https://github.com/imranghory/treemap-squared}에서 가져옴
   * <p> Gamify에 맞게 변형
   * @memberof Treemap
   * @instance
   */
  sumMultidimensionalArray(arr) {
    let i; let
      total = 0;

    if (_.isArray(arr[0])) {
      for (i = 0; i < arr.length; i++) {
        total += this.sumMultidimensionalArray(arr[i]);
      }
    } else {
      total = this.sumArray(arr);
    }
    return total;
  }
}