effects/bar.js

import _ from 'lodash';
import { Howl } from 'howler';
import Graphics from '../graphics';
import Text from '../text';
import Utility from '../utility';

/**
 * [Effect]{@link Effect} 에서 생성, 관리하는 Bar 차트의 이펙트 클래스
 * @alias BarEffect
 */
export default class BarEffect {
  /**
   * @description Bar차트 이펙트에서 필요한 값들을 설정
   * @param  { object } _object 이펙트 생성에 필요한 각종 오브젝트들
   * @param  { object } _option 이펙트 생성에 필요한 옵션
   */
  constructor(_object, _option) {
    /**
     * @description ChartJS 오브젝트
     * @type { ChartJS }
     */
    this.chart = _object.chart;

    /**
     * @description 이펙트를 생성할 PIXI 오브젝트
     * @type { PIXI }
     */
    this.pixi = _object.pixi;

    /**
     * @description Effect 클래스
     * @type { Effect }
     */
    this.parent = _object.effect;

    /**
     * @description 생성할 이펙트 옵션
     * @type { object }
     */
    this.option = _option;

    /**
     * @description 텍스트 여백
     * @type { number }
     */
    this.textMargin = 10;
    const textScale = this.parent.ratio.mid * 1.2;

    /**
     * @description value 텍스트를 표현할 [Text]{@link Text} 클래스의 배열
     * @type { Array }
     */
    this.valueText = [];

    /**
     * @description label 텍스트를 표현할 [Text]{@link Text} 클래스의 배열
     * @type { Array }
     */
    this.labelText = [];
    for (let i = 0; i < this.chart.config.data.labels.length; i++) {
      this.valueText.push(new Text(this.pixi, '', {
        alpha: 0,
      }));
      this.valueText[i].setScale(textScale);

      this.labelText.push(new Text(this.pixi, this.chart.config.data.labels[i], {
        alpha: 0,
      }));
      this.labelText[i].setScale(textScale);
    }

    /**
     * @description 이펙트를 표현할 Graphics
     * @type { Graphics }
     */
    this.graphics = new Graphics(this.pixi);

    /**
     * @description chartJS의 애니메이션이 진행중인지 여부
     * @type { boolean }
     */
    this.chartAnimation = true;

    /**
     * @description alpha값 flag
     * @type { boolean }
     */
    this.flag = 0;

    /**
     * @description alpha값
     * @type { boolean }
     */
    this.alpha = 0;

    /**
     * @description 이벤트 등록을 위한 제어 변수
     * @type { boolean }
     */
    this.upperLimitOneTime = false;

    /**
     * @description upperLimit 효과일 때 최초적용 옵션을 위한 변수
     * @type { boolean }
     */
    this.upperLimitCompleteOneTime = false;

    /**
     * @description lowerLimit 효과일 때 최초적용 옵션을 위한 변수
     * @type { boolean }
     */
    this.lowerLimitCompleteOneTime = false;

    /**
     * @description 이벤트 등록을 위한 제어 변수
     * @type { boolean }
     */
    this.lowerLimitOneTime = false;

    /**
     * @description 등록한 사운드가 로드되었는지 여부
     * @type { boolean }
     */
    this.isSoundReady = false;


    if (!_.isUndefined(this.option.sound)) {
      /**
     * @description 사운드 제어 변수
     * @type { Howler }
     */
      this.sound = new Howl({
        src: [this.option.sound],
      });
      this.sound.once('load', () => {
        this.isSoundReady = true;
      });
    }
  }

  /**
   * Warning의 삼각형을 그리기 위한 함수
   * <p>삼각형 빗변들의 시작과 끝 좌표를 반환해줌
   * @param {number} triangleLength 삼각형 한 변의 길이
   * @memberof BarEffect
   * @returns {array} TriangleHypotenuse
   * @instance
   */
  makeTriangleHypotenuse(triangleLength) {
    const vertex = this.makeTriangleVertex(triangleLength);

    const separatePoint1 = 1 / 5;
    const separatePoint2 = 4 / 5;

    return [
      [{ x: vertex[1].x * separatePoint1, y: vertex[1].y * separatePoint1 }, { x: vertex[1].x * separatePoint2, y: vertex[1].y * separatePoint2 }],
      [{ x: vertex[1].x + Math.abs(vertex[2].x - vertex[1].x) * separatePoint1, y: vertex[1].y }, { x: vertex[1].x + Math.abs(vertex[2].x - vertex[1].x) * separatePoint2, y: vertex[1].y }],
      [{ x: vertex[2].x * separatePoint2, y: vertex[2].y * separatePoint2 }, { x: vertex[2].x * separatePoint1, y: vertex[2].y * separatePoint1 }],
    ];
  }

  /**
   * Warning의 삼각형을 그리기 위한 함수
   * <p>삼각형의 각 꼭지점의 좌표를 찾아서 반환
   * @param {number} triangleLength 삼각형 한 변의 길이
   * @memberof BarEffect
   * @returns {array} TriangleVertex
   * @instance
   */
  makeTriangleVertex(triangleLength) {
    const point1 = { x: 0, y: 0 };
    const point2 = Utility.findNewPointByAngleWithDistance(point1, 210 - 90, triangleLength);
    const point3 = Utility.findNewPointByAngleWithDistance(point2, 90 - 90, triangleLength);
    return [point1, point2, point3];
  }

  /**
   * 정해진 상한값보다 초과가 되면 경고를 해줌
   * @memberof BarEffect
   * @instance
   */
  upperLimit() {
    this.limitInformation = [];
    if (!this.upperLimitOneTime) {
      this.chartAnimationProgressIndex = this.parent._addOnProgressFunction(() => {
        this.chartAnimation = true;
        this.upperLimit();
      });
      this.upperLimitOneTime = true;
    }

    this.chartAnimationCompleteIndex = this.parent._addOnCompleteFunction(() => {
      this.chartAnimation = false;
      this.alpha = 0;
      this.flag = 0;
      if (!this.upperLimitCompleteOneTime) {
        this.upperLimitCompleteOneTime = true;
        this.upperLimit();
      }
    });

    this.excuetedEffect = 'upperLimit';
    const limit = this.option.value;
    const { chartArea } = this.chart;
    const eachWidth = (chartArea.right - chartArea.left) / this.chart.data.labels.length;
    for (let i = 0; i < this.chart.data.datasets.length; i++) {
      const dataset = this.chart.data.datasets[i];
      for (let j = 0; j < dataset.data.length; j++) {
        const value = dataset.data[j];
        if (value > limit) {
          const datasetMeta = this.chart.getDatasetMeta(i);
          const dataView = datasetMeta.data[j]._view;
          const { max, margins } = datasetMeta.data[j]._yScale;
          margins.left = datasetMeta.data[j]._xScale.margins.left;
          margins.right = datasetMeta.data[j]._xScale.margins.right;
          const top = dataView.y + dataView.borderWidth;
          // const bottom = dataView.base - top;
          // const eachHeight = bottom / value;
          const eachHeight = (dataView.base - margins.top) / max;
          const line = (max - limit) * eachHeight + margins.top;
          const left = dataView.x - eachWidth / 2;
          const width = eachWidth;
          const rectInfo = {
            x: dataView.x - dataView.width / 2,
            y: top,
            width: dataView.width,
            height: line - top,
          };

          const dottedLineSpace = width / 10 / 5;
          const dottedLineLength = width / 10 - dottedLineSpace;

          const startX = [];
          for (let k = 0; k < 10; k++) {
            startX.push(left + (dottedLineLength * k) + (dottedLineSpace * k));
          }
          this.limitInformation.push({
            rectInfo, startX, dottedLineLength, line, index: j, value,
          });
        }
      }
    }
    this._limitInterval = this._limitInterval || setInterval(() => { this._limitUpdate(); }, 16);
  }

  /**
   * 정해진 하한값보다 미만이 되면 경고를 해줌
   * @memberof BarEffect
   * @instance
   */
  lowerLimit() {
    this.limitInformation = [];
    if (!this.lowerLimitOneTime) {
      this.chartAnimationProgressIndex = this.parent._addOnProgressFunction(() => {
        this.chartAnimation = true;
        this.lowerLimit();
      });
      this.base = this.chart.getDatasetMeta(0).data[0]._view.base;
      this.lowerLimitOneTime = true;
    }

    this.chartAnimationCompleteIndex = this.parent._addOnCompleteFunction(() => {
      this.chartAnimation = false;
      this.alpha = 0;
      this.flag = 0;
      if (!this.lowerLimitCompleteOneTime) {
        this.lowerLimitCompleteOneTime = true;
        this.lowerLimit();
      }
    });

    this.excuetedEffect = 'lowerLimit';
    const limit = this.option.value;
    const { chartArea } = this.chart;
    const eachWidth = (chartArea.right - chartArea.left) / this.chart.data.labels.length;
    for (let i = 0; i < this.chart.data.datasets.length; i++) {
      const dataset = this.chart.data.datasets[i];
      for (let j = 0; j < dataset.data.length; j++) {
        const value = dataset.data[j];
        if (value <= limit) {
          const datasetMeta = this.chart.getDatasetMeta(i);
          const dataView = datasetMeta.data[j]._view;
          const { max, margins } = datasetMeta.data[j]._yScale;
          margins.left = datasetMeta.data[j]._xScale.margins.left;
          margins.right = datasetMeta.data[j]._xScale.margins.right;
          const top = dataView.y + dataView.borderWidth;
          const bottom = dataView.base - top;
          const eachHeight = (dataView.base - margins.top) / max;
          const line = (max - limit) * eachHeight + margins.top;
          const left = dataView.x - eachWidth / 2;
          const width = eachWidth;
          const rectInfo = {
            x: dataView.x - dataView.width / 2,
            y: top,
            width: dataView.width,
            height: bottom > 0 ? bottom : 0,
          };

          const dottedLineSpace = width / 10 / 5;
          const dottedLineLength = width / 10 - dottedLineSpace;

          const startX = [];
          for (let k = 0; k < 10; k++) {
            startX.push(left + (dottedLineLength * k) + (dottedLineSpace * k));
          }
          this.limitInformation.push({
            rectInfo, startX, dottedLineLength, line, index: j, value,
          });
        }
      }
    }
    this._limitInterval = this._limitInterval || setInterval(() => { this._limitUpdate(); }, 16);
  }

  /**
   * 정해 놓은 상한값과 하한값에 의해 초과나 미만이 되면 생기는 경고를 관리 및 표현해줌
   * @memberof BarEffect
   * @instance
   */
  _limitUpdate() {
    this.graphics.object.clear();
    for (let i = 0; i < this.valueText.length; i++) {
      this.valueText[i].setAlpha(0);
      this.labelText[i].setAlpha(0);
    }

    if (this.chartAnimation) {
      return;
    }

    if (this.flag && this.alpha > 0) {
      this.alpha -= 0.05;
      if (this.alpha <= 0) {
        this.flag = 0;
      }
    } else if (this.alpha < 1) {
      this.alpha += 0.05;
      if (this.alpha > 1) {
        this.flag = 1;
        if (this.isSoundReady) {
          this.sound.play();
        }
      }
    }

    const color = this.excuetedEffect === 'upperLimit' ? 0xFF0000 : 0x004D40;
    const stroke = this.excuetedEffect === 'upperLimit' ? 0xffffff : null;
    const strokeThickness = this.excuetedEffect === 'upperLimit' ? 1 : null;
    for (let i = 0; i < this.limitInformation.length; i++) {
      const information = this.limitInformation[i];
      const {
        rectInfo, dottedLineLength, line, index, value,
      } = information;

      // this.chart.config.data.labels
      this.valueText[index].setAlpha(this.alpha);
      this.labelText[index].setAlpha(this.alpha);
      this.valueText[index].object.text = value.toLocaleString();

      const valueTextHeight = this.valueText[index].object.height;
      const labelTextHeight = this.labelText[index].object.height;
      const upperLabelTextY = line + labelTextHeight / 2 + this.textMargin;
      const lowerLabelTextY = line - labelTextHeight / 2 - this.textMargin;
      const labelTextPositionY = this.excuetedEffect === 'upperLimit' ? upperLabelTextY : lowerLabelTextY;

      const upperValueTextY = upperLabelTextY + labelTextHeight / 2 + this.textMargin + valueTextHeight / 2;
      const lowerValueTextY = lowerLabelTextY - labelTextHeight / 2 - this.textMargin - valueTextHeight / 2;

      const valueTextPositionY = this.excuetedEffect === 'upperLimit' ? upperValueTextY : lowerValueTextY;// 코드 반대로 됏으뮤ㅠㅠ
      this.valueText[index].object.style.fill = color;
      this.valueText[index].object.style.stroke = stroke;
      this.valueText[index].object.style.strokeThickness = strokeThickness;

      this.valueText[index].setPosition(rectInfo.x + rectInfo.width / 2, valueTextPositionY);
      this.labelText[index].setPosition(rectInfo.x + rectInfo.width / 2, labelTextPositionY);
      this.graphics.object.beginFill(color, this.alpha);
      this.graphics.object.drawRect(rectInfo.x, rectInfo.y, rectInfo.width, rectInfo.height);
      this.graphics.object.endFill();
      for (let k = 0; k < 10; k++) {
        const startX = information.startX[k];

        this.graphics.object.lineStyle(2, color, 1);
        this.graphics.object.moveTo(startX, line);
        this.graphics.object.lineTo(startX + dottedLineLength, line);
      }

      this.graphics.object.lineStyle(0);
      const preSize = (rectInfo.height < rectInfo.width ? rectInfo.height / 1.4 : rectInfo.width / 1.4);
      const size = preSize > 0 ? preSize : rectInfo.width / 1.4;
      const warningY = rectInfo.height > 0 ? rectInfo.y + rectInfo.height / 2 : line + ((this.base - line) / 2);
      const special = rectInfo.height > 0 ? 0 : 1;
      this._drawWarning(rectInfo.x + rectInfo.width / 2, warningY, size, special);
    }
  }

  /**
   * warning 을 그려줌
   * @param {number} _x x 좌표
   * @param {number} _y y 좌표
   * @param {number} size warning 사이즈
   * @param {number} special 값이 0 일때 true
   * @memberof BarEffect
   * @instance
   */
  _drawWarning(_x, _y, size, special) {
    const triangleHypotenuse = this.makeTriangleHypotenuse(size);
    const triangleVertex = this.makeTriangleVertex(size);
    const height = triangleVertex[1].y;
    const width = triangleVertex[2].x;
    const moveY = _y - height / 2;
    const color = special ? 0x004D40 : 0xFFFFFF;
    const alpha = special ? this.alpha : 1;

    this.graphics.object.lineStyle(size / 16.7, color, alpha);
    for (let i = 0; i < triangleHypotenuse.length; i++) {
      const hypotenuse = triangleHypotenuse[i];
      this.graphics.object.moveTo(hypotenuse[0].x + _x, hypotenuse[0].y + moveY);
      this.graphics.object.lineTo(hypotenuse[1].x + _x, hypotenuse[1].y + moveY);

      const nextIndex = (i + 1 === triangleHypotenuse.length ? 0 : i + 1);
      const nextHypotenuse = triangleHypotenuse[nextIndex];
      const quadraticCurvePoint = triangleVertex[i + 1 > 2 ? 0 : i + 1];

      this.graphics.object.moveTo(hypotenuse[1].x + _x, hypotenuse[1].y + moveY);
      this.graphics.object.quadraticCurveTo(quadraticCurvePoint.x + _x, quadraticCurvePoint.y + moveY, nextHypotenuse[0].x + _x, nextHypotenuse[0].y + moveY);
    }

    this.graphics.object.lineStyle(0);
    this.graphics.object.beginFill(color, alpha);
    this.graphics.object.drawRoundedRect(_x - width * 1 / 8 / 2, _y - height * 1 / 3 / 3, width * 1 / 8, height * 1 / 3, size / 30);
    this.graphics.object.drawCircle(_x, _y + height / 3, size / 30);
    this.graphics.object.endFill();
  }

  /**
   * 현재 생성되어 있는 Bar Effect 제거
   * <p> [Effect]{@link Effect} 클래스에 의해 호출된다.
   * @memberof BarEffect
   * @instance
   */
  destroy() {
    switch (this.excuetedEffect) {
      case 'upperLimit':
        break;
      default:
        break;
    }
    this.parent._removeOnProgressFunction(this.chartAnimationProgressIndex);
    this.parent._removeOnCompleteFunction(this.chartAnimationCompleteIndex);
    clearInterval(this._limitInterval);
    delete this._limitInterval;

    for (let i = 0; i < this.valueText.length; i++) {
      this.valueText[i].destroy();
      this.labelText[i].destroy();
    }
    this.graphics.destroy();
  }
}