effects/line.js

import _ from 'lodash';
import { Howl } from 'howler';
import * as PIXI from 'pixi.js';
import Graphics from '../graphics';
// eslint-disable-next-line
import tweenManager from 'pixi-tween';
import Text from '../text';

/**
 * Line Chart의 Effect를 생성하는 클래스입니다.
 * <p>
 * @class
 * @alias LineEffect
 */
export default class LineEffect {
  /**
     * @description 사용자 옵션을 받아서 Line Chart의 Effect를 생성합니다.
     * @param  { object } _object - ChartJS, PixiJS, 호출한 Effect 클래스
     * @param  { object } _option - 사용자 지정 Effect 옵션
     */
  constructor(_object, _option) {
    /**
     * @description 생성한 ChartJS 오브젝트
     * @type { ChartJS }
     */
    this.chart = _object.chart;

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

    /**
     * @description Effect를 호출한 상위 Effect Class
     * @type { Effect }
     */
    this.parent = _object.effect;

    /**
     * @description Effect에 대한 사용자 옵션
     * @type { object }
     */
    this.option = _option;

    /**
     * @description 차트가 업데이트 중인지 아닌지 여부
     * @type { boolean }
     */
    this.chartAnimation = false;

    /**
     * @description 알파 값 플래그
     * @type { number }
     */
    this.flag = 0;

    /**
     * @description 알파 값
     * @type { number }
     */
    this.alpha = 0;

    /**
     * @description lineTracer 차트 애니메이션 플래그
     * @type { boolean }
     */
    this.lineTracerOneTime = false;

    /**
     * @description upperLimit 차트 애니메이션 플래그
     * @type { boolean }
     */
    this.upperLimitOneTime = false;

    /**
     * @description lowerLimit 차트 애니메이션 플래그
     * @type { boolean }
     */
    this.lowerLimitOneTime = false;

    /**
     * @description 경고 아이콘 및 limit 선을 그리기 위한 PIXI.Graphics
     * @type { Graphics }
     */
    this.graphics = new Graphics(this.pixi);

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

    /**
     * @description upperLimit 효과일 때 최초적용 옵션을 위한 변수
     * @type { boolean }
     */
    this.upperLimitCompleteOneTime = 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;
      });
    }

    this._createTexts();
    this._initRoundedTriangle();
  }

  /**
   * 사용자가 지정한 이미지가 라인 그래프를 따라 움직이는 Effect 입니다.
   * @memberof LineEffect
   * @example
   * const option = {
   *     dataset: 0, // 차트 데이터 셋 번호 지정
   *     height: 48, // 이미지 높이
   *     width: 48, // 이미지 너비
   *     src: '../img/img1.png', // 이미지 경로
   *     angle: true, // 이미지 각도 변화 여부
   *     easing: 'linear', // 애니메이션 Easing 이름
   *     fadeIn: false, // 이미지 페이드 인 여부
   *     fadeOut: false, // 이미지 페이드 아웃 여부
   *     loop: true, // 애니메이션 반복 여부
   *     time: 2.5, // 애니메이션 수행 시간(초)
   * };
   *
   * window.chart[0].effect.add('lineTracer', option);
   * @instance
   */
  lineTracer() {
    // 실행 된 이펙트 이름을 지정
    this.executedEffect = 'lineTracer';
    this.chartMetaData = this.chart.getDatasetMeta(this.option.dataset);
    // 차트 데이터 값 중 첫 번째
    const initModel = this.chartMetaData.data[0]._model;
    const lineTension = this.chartMetaData.dataset._model.tension;
    // 이미지 이동에 대한 트윈 패스 객체 생성
    const tweenPath = new PIXI.tween.TweenPath();
    const optionTime = this.option.time * 1000;

    // 차트 애니메이션 업데이트 시 해야 할 작업을 지정
    if (!this.lineTracerOneTime) {
      this.chartAnimationProgressIndex = this.parent._addOnProgressFunction(() => {
        this.chartAnimation = true;
        if (!this.trigger) {
          this._createTempSprite();
          this.trigger = true;
        }
        this.tween.stop();
        if (tweenPath !== undefined) {
          tweenPath.clear();
        }
        PIXI.tweenManager.removeTween(this.tween);
        this.pixi.ticker.remove(this.lineTracerUpdate);
        this.tweenSprite.destroy();
        this.parent._removeOnProgressFunction(this.chartAnimationProgressIndex);
        this.parent._removeOnCompleteFunction(this.chartAnimationCompleteIndex);
        this.lineTracerOneTime = false;
        this.lineTracer();
      });
      this.lineTracerOneTime = true;
    }

    this.chartAnimationCompleteIndex = this.parent._addOnCompleteFunction(() => {
      this.trigger = false;
      this.pixi.ticker.remove(this.fadeOutTempSprite);
      if (this.tmpSprite !== undefined) {
        this.tmpSprite.destroy();
      }
      this.chartAnimation = false;
      this.alpha = 0;
      this.flag = 0;
    });

    // 베지어 곡선 계산에 필요한 데이터에 대한 현재, 이전 차트 데이터 값
    let model;
    let prevModel;

    // 차트 데이터 값들에 대한 좌표를 트윈 패스에 추가
    for (let i = 0; i < this.chartMetaData.data.length; i++) {
      // 현재 차트 데이터 값을 가져옴
      model = this.chartMetaData.data[i]._model;

      // 첫 번째 차트 데이터의 좌표값으로 시작 점 세팅
      if (i === 0) {
        tweenPath.moveTo(model.x, model.y);
      } else {
        // 두 번째 부터 다음 이동 경로를 트윈 패스에 추가
        prevModel = this.chartMetaData.data[i - 1]._model;

        // 차트 옵션의 lineTension 값이 0인 경우는 일반적인 직선 이동
        if (lineTension === undefined || lineTension === 0) {
          tweenPath.lineTo(model.x, model.y);
        // 아닐 경우 베지어 커브 이동
        } else {
          tweenPath.bezierCurveTo(prevModel.controlPointNextX, prevModel.controlPointNextY, model.controlPointPreviousX, model.controlPointPreviousY, model.x, model.y);
        }
      }
    }
    // 라인 차트의 특성에 따라 트윈 패스는 항상 열린 경로이다.
    tweenPath.closed = false;

    // 이미지 경로로 스프라이트 생성 및 기본 값 세팅
    this.tweenSprite = PIXI.Sprite.fromImage(this.option.src);
    this.tweenSprite.x = initModel.x;
    this.tweenSprite.y = initModel.y;
    this.tweenSprite.alpha = 0.0;
    this.tweenSprite.anchor.set(0.5, 0.5);
    if (this.option.width !== undefined) {
      this.tweenSprite.width = this.option.width;
    }
    if (this.option.height !== undefined) {
      this.tweenSprite.height = this.option.height;
    }
    // 생성한 스프라이트를 Pixi에 추가
    this.pixi.stage.addChild(this.tweenSprite);

    /** Tween animation setting */
    // 각도 계산을 위해 스프라이트 이전 좌표를 저장하는 변수
    const prevPoint = {
      x: this.tweenSprite.x,
      y: this.tweenSprite.y,
    };
    // 스프라이트가 실제로 나타나는 시점(밀리 초)
    const appearDelay = 25;
    // 스프라이트로 트윈 매니저 생성
    this.tween = PIXI.tweenManager.createTween(this.tweenSprite);
    // 경로를 생성한 트윈 패스로 세팅
    this.tween.path = tweenPath;
    // 애니메이션 시간 세팅
    let animationTime;
    let updateTime = this.parent.option.data.interval * 1000 - this.chart.options.animation.duration + 105;
    if (Number.isNaN(this.chart.updateElapsedTime) || this.chart.updateElapsedTime === undefined || this.chart.updateElapsedTime === null) {
      updateTime = this.parent.option.data.interval * 1000;
    }
    if (updateTime !== undefined && optionTime > updateTime) {
      animationTime = updateTime;
    } else {
      animationTime = optionTime;
    }

    this.tween.time = animationTime;
    // 전체 시간의 8분의 1 만큼 페이드 인 아웃시 할당해 줌
    const dividedTime = animationTime / 8;
    // 애니메이션 반복 여부
    this.tween.loop = this.option.loop;
    // Easing 옵션 세팅, 기본값은 linear
    if (this.option.easing !== undefined && this.option.easing !== '') {
      const name = this.option.easing;
      this.tween.easing = PIXI.tween.Easing[name]();
    }
    // 트윈 매니저 시작
    this.tween.start();

    // lineTracer 업데이트 함수
    this.lineTracerUpdate = () => {
      PIXI.tweenManager.update();
      // 애니메이션 경과 시간
      const elapsedTime = this.tween._elapsedTime;

      // 각도 번화 옵션이 true이면 이전 좌표와 현재 좌표로부터 스프라이트 각도를 계산
      if (this.option.angle && this.option.angle !== undefined) {
        this.tweenSprite.rotation = this._getLineAngle(prevPoint, this.tweenSprite);
        prevPoint.x = this.tweenSprite.x;
        prevPoint.y = this.tweenSprite.y;
      }

      // 차트 업데이트 중에는 효과 무시
      if (this.chartAnimation) {
        return;
      }

      // 스프라이트가 최소 딜레이 시간 이후에 나타남
      if (elapsedTime > appearDelay && elapsedTime < (optionTime - appearDelay)) {
        // 처음 애니메이션 시간의 5분의 1동안
        if (elapsedTime < dividedTime) {
          // 페이드 인 옵션이 true이면 알파 값 증가
          if (this.option.fadeIn !== undefined && this.option.fadeIn) {
            this.tweenSprite.alpha += (1 / dividedTime) * 25;
            if (this.tweenSprite.alpha > 1.0) {
              this.tweenSprite.alpha = 1.0;
            }
          // 페이드 인 옵션이 false이면 알파값 기본 1.0으로 세팅
          } else {
            this.tweenSprite.alpha = 1.0;
          }
        }

        // 마지막 애니메이션 시간의 8분의 1동안
        if (elapsedTime > dividedTime * 7) {
          // 페이드 아웃 옵션이 true이면 알파 값 감소
          if (this.option.fadeOut !== undefined && this.option.fadeOut) {
            this.tweenSprite.alpha -= (1 / dividedTime) * 18;
            if (this.tweenSprite.alpha < 0.0) {
              this.tweenSprite.alpha = 0.0;
            }
          } else {
            this.tweenSprite.alpha = 1.0;
          }
        }

      // 애니메이션 실행 시간이 다 되면 반복여부 옵션을 체크해서 알파값을 유지시킬지 말지 결정
      } else if (elapsedTime >= (optionTime - appearDelay)) {
        // 반복 여부가 false이고 페이드 아웃이 false이면 알파값 1.0으로 유지 - 이미지를 끝에 멈춘 상태로 유지함
        if (!this.option.loop && (!this.option.fadeOut || this.option.fadeOut === undefined)) {
          this.tweenSprite.alpha = 1.0;
        } else {
          this.tweenSprite.alpha = 0.0;
        }
      }
    };

    // lineTracer 업데이트 함수를 Pixi의 ticker에 추가
    this.pixi.ticker.add(this.lineTracerUpdate);
  }

  /**
   * 기준치 초과시 경고 아이콘 깜박임과 함께 기준선이 표시되는 Effect 입니다.
   * @memberof LineEffect
   * @example
   * const option = {
   *   dataset: [0, 2], // 지정할 데이터 셋 번호 배열(지정하지 않을 경우 전체 데이터 셋에 적용됨)
   *   value: 80 // 초과 값
   * };
   *
   * window.chart[0].effect.add('upperLimit', option);
   * @instance
   */
  upperLimit() {
    this.chart.options.tooltips.enabled = false;
    this.chart.options.hover.mode = null;
    this.limitInformation = [];
    this.limitLines = [];
    if (!this.upperLimitOneTime) {
      this.chartAnimationProgressIndex = this.parent._addOnProgressFunction(() => {
        this.chartAnimation = true;
        this.parent._removeOnProgressFunction(this.chartAnimationProgressIndex);
        this.parent._removeOnCompleteFunction(this.chartAnimationCompleteIndex);
        this.upperLimitOneTime = false;
        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();
      }
    });

    if (!this.upperLimitCompleteOneTime) { return; }

    this.executedEffect = 'upperLimit';
    const limit = this.option.value;
    const { chartArea } = this.chart;
    const eachWidth = (chartArea.right - chartArea.left) / (this.chart.data.labels.length - 1);
    const chartData = this.chart.data;
    let isDatasetDefined = false;
    const optDataset = this.option.dataset;
    this.chartMetaData = null;

    if (optDataset === undefined || optDataset === '' || optDataset === -1) {
      isDatasetDefined = false;
    } else {
      isDatasetDefined = true;
    }

    for (let i = 0; i < chartData.datasets.length; i++) {
      const tmpDatasets = this.chart.data.datasets;
      const dataset = (tmpDatasets[i]);
      for (let j = 0; j < dataset.data.length; j++) {
        const value = dataset.data[j];
        this.chartMetaData = this.chart.getDatasetMeta(i);
        const dataView = this.chartMetaData.data[j]._view;
        const { max, margins, bottom } = this.chartMetaData.data[j]._yScale;
        margins.left = this.chartMetaData.data[j]._xScale.margins.left;
        margins.right = this.chartMetaData.data[j]._xScale.margins.right;
        const eachHeight = (bottom - margins.top) / max;
        const line = (max - limit) * eachHeight + margins.top;
        const areaSize = (chartArea.right - chartArea.left) * (chartArea.bottom - chartArea.top);
        let lineDivider = 1;
        let left = 0.0;
        let width = 0.0;

        if (j === 0) {
          left = dataView.x;
          width = eachWidth / 2;
          lineDivider = 2;
        } else if (j === (dataset.data.length - 1)) {
          left = dataView.x - eachWidth / 2;
          width = eachWidth / 2;
          lineDivider = 2;
        } else {
          left = dataView.x - eachWidth / 2;
          width = eachWidth;
          lineDivider = 1;
        }

        const lineDataInfo = {
          x: dataView.x,
          y: dataView.y,
        };

        const dottedLineSpace = width / (10 / lineDivider) / 5;
        const dottedLineLength = width / (10 / lineDivider) - dottedLineSpace;
        const lastDataX = this.chartMetaData.data[dataset.data.length - 1]._view.x;

        const startX = [];
        for (let k = 0; k < (10 / lineDivider); k++) {
          startX.push(left + (dottedLineLength * k) + (dottedLineSpace * k));
        }

        const minChartValue = undefined;
        const maxChartValue = Math.max.apply(null, dataset.data);

        if (isDatasetDefined && _.includes(optDataset, i)) {
          if (value > limit) {
            this.limitInformation.push({
              lineDataInfo, areaSize: areaSize / 100, value, index: j, lastDataX, minChartValue, maxChartValue,
            });
          }
        } else if (!isDatasetDefined && value > limit) {
          this.limitInformation.push({
            lineDataInfo, areaSize: areaSize / 100, value, index: j, lastDataX, minChartValue, maxChartValue,
          });
        }

        this.limitLines.push({
          dottedLineLength, line, startX,
        });
      }
    }
    this._limitInterval = this._limitInterval || setInterval(() => { this._limitUpdate(); }, 16);
  }

  /**
   * 기준치 미만시 경고 아이콘 깜박임과 함께 기준선이 표시되는 Effect 입니다.
   * @memberof LineEffect
   * @example
   * const option = {
   *   dataset: [1, 3], // 지정할 데이터 셋 번호 배열(지정하지 않을 경우 전체 데이터 셋에 적용됨)
   *   value: 20 // 미만 값
   * };
   *
   * window.chart[0].effect.add('lowerLimit', option);
   * @instance
   */
  lowerLimit() {
    this.chart.options.tooltips.enabled = false;
    this.chart.options.hover.mode = null;
    this.limitInformation = [];
    this.limitLines = [];
    if (!this.lowerLimitOneTime) {
      this.chartAnimationProgressIndex = this.parent._addOnProgressFunction(() => {
        this.chartAnimation = true;
        this.parent._removeOnProgressFunction(this.chartAnimationProgressIndex);
        this.parent._removeOnCompleteFunction(this.chartAnimationCompleteIndex);
        this.lowerLimitOneTime = false;
        this.lowerLimit();
      });
      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();
      }
    });

    if (!this.lowerLimitCompleteOneTime) { return; }

    this.executedEffect = 'lowerLimit';
    const limit = this.option.value;
    const { chartArea } = this.chart;
    const eachWidth = (chartArea.right - chartArea.left) / (this.chart.data.labels.length - 1);
    const chartData = this.chart.data;
    let isDatasetDefined = false;
    const optDataset = this.option.dataset;
    this.chartMetaData = null;

    if (optDataset === undefined || optDataset === '' || optDataset === -1) {
      isDatasetDefined = false;
    } else {
      isDatasetDefined = true;
    }

    for (let i = 0; i < chartData.datasets.length; i++) {
      const tmpDatasets = this.chart.data.datasets;
      const dataset = (tmpDatasets[i]);
      for (let j = 0; j < dataset.data.length; j++) {
        const value = dataset.data[j];

        this.chartMetaData = this.chart.getDatasetMeta(i);
        const dataView = this.chartMetaData.data[j]._view;
        const { max, margins, bottom } = this.chartMetaData.data[j]._yScale;
        margins.left = this.chartMetaData.data[j]._xScale.margins.left;
        margins.right = this.chartMetaData.data[j]._xScale.margins.right;
        const eachHeight = (bottom - margins.top) / max;
        const line = (max - limit) * eachHeight + margins.top;
        const areaSize = (chartArea.right - chartArea.left) * (chartArea.bottom - chartArea.top);
        let lineDivider = 1;
        let left = 0.0;
        let width = 0.0;

        if (j === 0) {
          left = dataView.x;
          width = eachWidth / 2;
          lineDivider = 2;
        } else if (j === (dataset.data.length - 1)) {
          left = dataView.x - eachWidth / 2;
          width = eachWidth / 2;
          lineDivider = 2;
        } else {
          left = dataView.x - eachWidth / 2;
          width = eachWidth;
          lineDivider = 1;
        }

        const lineDataInfo = {
          x: dataView.x,
          y: dataView.y,
        };

        const dottedLineSpace = width / (10 / lineDivider) / 5;
        const dottedLineLength = width / (10 / lineDivider) - dottedLineSpace;
        const lastDataX = this.chartMetaData.data[dataset.data.length - 1]._view.x;

        const startX = [];
        for (let k = 0; k < (10 / lineDivider); k++) {
          startX.push(left + (dottedLineLength * k) + (dottedLineSpace * k));
        }

        const minChartValue = Math.min.apply(null, dataset.data);
        const maxChartValue = undefined;

        if (isDatasetDefined && _.includes(optDataset, i)) {
          if (value < limit) {
            this.limitInformation.push({
              lineDataInfo, areaSize: areaSize / 100, value, index: j, lastDataX, minChartValue, maxChartValue,
            });
          }
        } else if (!isDatasetDefined && value < limit) {
          this.limitInformation.push({
            lineDataInfo, areaSize: areaSize / 100, value, index: j, lastDataX, minChartValue, maxChartValue,
          });
        }

        this.limitLines.push({
          dottedLineLength, line, startX,
        });
      }
    }
    this._limitInterval = this._limitInterval || setInterval(() => { this._limitUpdate(); }, 16);
  }

  /**
   * <b>내부 함수</b>
   * <p>lineTracer 에서 스프라이트 각도를 계산하는 함수입니다.
   * @param { object } _start 첫 번째 지점의 x, y좌표 셋
   * @param { object } _end 첫 번째 지점의 x, y좌표 셋
   * @memberof LineEffect
   * @returns { number } Sprite.rotation 라디안 값
   * @instance
   */
  _getLineAngle(_start, _end) {
    const dx = _end.x - _start.x;
    const dy = _end.y - _start.y;
    let angle = Math.atan(dy / dx);
    if (dx < 0.0) {
      angle += 180.0;
    }
    return angle;
  }

  /**
   * <b>내부 함수</b>
   * <p>차트 데이터 업데이트 및 변화 시 lineTracer 페이드 아웃 처리를 위해 마지막 위치에 임시 이미지 생성하는 함수입니다.
   * @memberof LineEffect
   * @instance
   */
  _createTempSprite() {
    this.tmpSprite = PIXI.Sprite.fromImage(this.option.src);
    this.tmpSprite.x = this.tweenSprite.x;
    this.tmpSprite.y = this.tweenSprite.y;
    this.tmpSprite.width = this.tweenSprite.width;
    this.tmpSprite.height = this.tweenSprite.height;
    this.tmpSprite.alpha = this.tweenSprite.alpha;
    this.tmpSprite.anchor.set(0.5, 0.5);
    this.tmpSprite.rotation = this.tweenSprite.rotation;
    this.pixi.stage.addChild(this.tmpSprite);
    this.fadeOutTempSprite = () => {
      this.tmpSprite.alpha -= 0.067;
    };
    this.pixi.ticker.add(this.fadeOutTempSprite);
  }

  /**
   * <b>내부 함수</b>
   * <p>기준치 제한하는 Effect에 대한 업데이트 함수입니다.
   * @memberof LineEffect
   * @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) {
        if (this.isSoundReady) {
          this.sound.play();
        }
        this.flag = 1;
      }
    }

    const color = this.executedEffect === 'upperLimit' ? 0xD62A00 : 0x316B00;

    for (let i = 0; i < this.limitLines.length; i++) {
      const information = this.limitLines[i];
      const { dottedLineLength, line } = information;
      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);
      }
    }

    for (let i = 0; i < this.limitInformation.length; i++) {
      const minSize = 16;
      const sizeGauge = 400;
      const information = this.limitInformation[i];
      const {
        lineDataInfo, areaSize, value, index, lastDataX, minChartValue, maxChartValue,
      } = information;
      const chartRightMargin = this.chart.legend.paddingRight;

      this.valueText[i].setAlpha(1);
      this.labelText[i].setAlpha(1);
      this.valueText[i].object.text = value.toLocaleString();
      this.labelText[i].object.text = this.chart.config.data.labels[index];
      const labelTextHeight = this.labelText[i].object.height;
      const labelTextWidth = this.labelText[i].object.width;
      const valueTextWidth = this.valueText[i].object.width;
      const size = ((areaSize / sizeGauge) < minSize) ? minSize : (areaSize / sizeGauge);
      const textSize = size / 50;
      let iconObjectMargin = (size * 2.56) / 2 + labelTextWidth * 0.5;
      let textAlign = -(labelTextWidth / 2 - valueTextWidth * 0.5);

      const labelsMaxWidth = Math.max(labelTextWidth, valueTextWidth);

      if (lineDataInfo.x > lastDataX - size && chartRightMargin < labelsMaxWidth + this.textMargin * 2) {
        iconObjectMargin *= -1;
        textAlign *= -1;
      }

      const labelTextY = lineDataInfo.y - labelTextHeight * 0.667;
      const valueTextY = lineDataInfo.y + labelTextHeight * 0.667;
      const labelTextX = lineDataInfo.x + iconObjectMargin;
      const valueTextX = lineDataInfo.x + iconObjectMargin + textAlign;

      this.valueText[i].object.style.fill = color;
      this.valueText[i].setPosition(valueTextX, valueTextY);
      this.labelText[i].setPosition(labelTextX, labelTextY);
      this.valueText[i].setScale(textSize);
      this.labelText[i].setScale(textSize);

      this.graphics.object.lineStyle(0);

      if ((minChartValue !== undefined && value === minChartValue) || (maxChartValue !== undefined && value === maxChartValue)) {
        this._drawWarningBackground(lineDataInfo.x, lineDataInfo.y, size, this.alpha);
        this._drawWarning(lineDataInfo.x, lineDataInfo.y, size * 1.25, 0xFFFFFF, this.alpha);
      } else {
        this._drawWarningBackground(lineDataInfo.x, lineDataInfo.y, size, 1);
        this._drawWarning(lineDataInfo.x, lineDataInfo.y, size * 1.25, 0xFFFFFF, 1);
      }
    }
  }

  /**
   * <b>내부 함수</b>
   * <p>경고 아이콘 그래픽을 생성하는 함수입니다.
   * @memberof LineEffect
   * @instance
   */
  _drawWarning(_x, _y, size, color, alpha) {
    // offset y
    _y -= (size / 10);
    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;
    this.graphics.object.lineStyle(size / 14.4, 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(0xFFFFFF, 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();
  }

  /**
   * <b>내부 함수</b>
   * <p>경고 아이콘 백그라운드 둥근 사각형 그래픽을 생성하는 함수입니다.
   * @memberof LineEffect
   * @instance
   */
  _drawWarningBackground(_x, _y, size, alpha) {
    // _y -= (size / 16);
    const radius = size / 1.5;
    const outerSize = size * 1.92;
    const innerSize = size * 1.72;
    const outerColor = this.executedEffect === 'upperLimit' ? 0xD62A00 : 0x316B00;
    const innerColor = this.executedEffect === 'upperLimit' ? 0xFF3200 : 0x3B7F00;

    // draw outer rect first
    this.graphics.object.lineStyle(0);
    this.graphics.object.beginFill(outerColor, alpha);
    this.graphics.object.drawRoundedRect(_x - outerSize / 2, _y - outerSize / 2, outerSize, outerSize, radius);
    this.graphics.object.endFill();

    // next, draw inner rect
    this.graphics.object.lineStyle(0);
    this.graphics.object.beginFill(innerColor, alpha);
    this.graphics.object.drawRoundedRect(_x - innerSize / 2, _y - innerSize / 2, innerSize, innerSize, radius * 0.84);
    this.graphics.object.endFill();
  }

  /**
   * <b>내부 함수</b>
   * <p>모서리가 둥근 삼각형을 생성하는 핵심 함수입니다.
   * @memberof LineEffect
   * @instance
   */
  _initRoundedTriangle() {
    const findNewPoint = (point, angle, distance) => {
      const result = {};
      angle -= 90;
      result.x = Math.round(Math.cos(angle * Math.PI / 180) * distance + point.x);
      result.y = Math.round(Math.sin(angle * Math.PI / 180) * distance + point.y);

      return result;
    };

    this.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 }],
      ];
    };

    this.makeTriangleVertex = (triangleLength) => {
      const point1 = { x: 0, y: 0 };
      const point2 = findNewPoint(point1, 210, triangleLength);
      const point3 = findNewPoint(point2, 90, triangleLength);
      return [point1, point2, point3];
    };
  }

  /**
   * <b>내부 함수</b>
   * <p>upperLimit, lowerLimit에서 데이터 이름과 값을 표시하기 위한 PIXI.Text 객체를 생성하는 함수임니다.
   * @memberof LineEffect
   * @instance
   */
  _createTexts() {
    this.textMargin = 8;
    const textScale = this.parent.ratio * 1.0;
    this.valueText = [];
    this.labelText = [];
    for (let j = 0; j < this.chart.data.datasets.length; j++) {
      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);
      }
    }
    for (let i = 0; i < this.valueText.length; i++) {
      this.valueText[i].setAlpha(0);
      this.labelText[i].setAlpha(0);
    }
  }

  /**
   * 생성한 Effect를 제거하는 함수입니다.
   * @memberof LineEffect
   * @instance
   */
  destroy() {
    // 수행중인 Effect이름으로 제거 대상을 선택
    switch (this.executedEffect) {
      case 'lineTracer':
        this.tween.stop();
        this.pixi.ticker.remove(this.lineTracerUpdate);
        this.tweenSprite.destroy();
        break;

      case 'upperLimit':
        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();
        break;

      case 'lowerLimit':
        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();
        break;

      default:
        break;
    }

    // common destroy
    this.parent._removeOnProgressFunction(this.chartAnimationProgressIndex);
    this.parent._removeOnCompleteFunction(this.chartAnimationCompleteIndex);
  }
}