effects/pie.js

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

/**
 * [Effect]{@link Effect} 에서 생성, 관리하는 Pie 차트의 이펙트 클래스
 * @alias PieEffect
 */
export default class PieEffect {
  /**
   * @description Pie차트 이펙트에서 필요한 값들을 설정
   * @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 이펙트를 표현할 Graphics
     * @type { Graphics }
     */
    this.graphics = new Graphics(this.pixi);

    /**
     * @description 이펙트를 표현할 Graphics (2개의 Graphics가 필요함)
     * @type { Graphics }
     */
    this.topGraphics = new Graphics(this.pixi);

    /**
     * @description label을 표현할 Text
     * @type { Text }
     */
    this.labelText = new Text(this.pixi, 'red', {});

    /**
     * @description value를 표현할 Text
     * @type { Text }
     */
    this.valueText = new Text(this.pixi, '10', {});

    /**
     * @description percent를 표현할 Text
     * @type { Text }
     */
    this.percentText = new Text(this.pixi, '57%', {});

    /**
     * @description label, value, percent를 가지고 있는 배열
     * @type { Array }
     */
    this.text = [this.labelText, this.valueText, this.percentText];
    this.text.map(object => object.setAlpha(0));


    /**
     * @description extrude 효과 시간
     * @type { number }
     */
    this.extrudeDuration = 1;

    /**
     * @description extrude 효과 길이
     * @type { number }
     */
    this.extrudeDistance = 35;
    Math.radians = degrees => degrees * Math.PI / 180;
    Math.degrees = radians => radians * 180 / Math.PI;
  }


  /**
   * 마우스를 Pie 차트에 올리면 돌출되는 효과와 함께 텍스트를 보여줌
   * @memberof PieEffect
   * @instance
   */
  extrude() {
    this.excuetedEffect = 'extrude';
    this.chart.options.tooltips.enabled = false;
    this.chart.options.hover.mode = null;
    this.extrudePrevIndex = -1;

    const {
      radiusLength, chartArea,
    } = this.chart.controller;

    this.radiusLength = radiusLength;

    this.center = {
      x: (chartArea.right - chartArea.left) / 2 + chartArea.left,
      y: (chartArea.bottom - chartArea.top) / 2 + chartArea.top,
    };

    const { data } = this.chart.config.data.datasets[0];
    this.chartValueSum = 0;
    for (let i = 0; i < data.length; i++) {
      this.chartValueSum += data[i];
    }

    this.extrudeUpdateInterval = setInterval(() => {
      this.extrudeUpdate();
    }, 16);

    this.chart.canvas.onmousemove = (event) => {
      const activePoints = this.chart.getElementsAtEvent(event);
      if (activePoints[0]) {
        const chartData = activePoints[0]._chart.config.data;
        const index = activePoints[0]._index;

        if (this.extrudePrevIndex !== index) {
          this.extrudePrevIndex = index;
          const model = activePoints[0]._model;
          const { startAngle, endAngle } = model;

          const colorString = chartData.datasets[0].backgroundColor[index];
          const colorArray = colorString.substring(5, colorString.length - 3).split(',');
          const color = Utility.rgb2hex(colorArray);

          const label = chartData.labels[index];
          const value = chartData.datasets[0].data[index];

          const textAngle = Math.min(startAngle, endAngle) + Math.abs(endAngle - startAngle) / 2;
          const labelTextPosition = Utility.findNewPointByAngleWithDistance(this.center, Math.degrees(textAngle), this.radiusLength * 9 / 10);
          const valueTextPosition = Utility.findNewPointByAngleWithDistance(this.center, Math.degrees(textAngle), this.radiusLength * 8 / 10);
          const percentTextPosition = Utility.findNewPointByAngleWithDistance(this.center, Math.degrees(textAngle), this.radiusLength * 5 / 10);
          let textRoatation = Math.atan2(labelTextPosition.y - this.center.y, labelTextPosition.x - this.center.x) + Math.radians(90);
          textRoatation = Math.degrees(textRoatation) >= 135 && Math.degrees(textRoatation) <= 225 ? textRoatation + Math.radians(180) : textRoatation;

          for (let i = 0; i < this.text.length; i++) {
            this.text[i].setAlpha(0);
            this.text[i].object.rotation = textRoatation;
            this.text[i].object.style.fill = '0xffffff';
            this.text[i].setColorBasedOnBackground(colorArray);
            this.text[i].object.style.stroke = Utility.hex2string(color);
            this.text[i].object.style.strokeThickness = 6;
            this.text[i].object.style.dropShadow = true;
            this.text[i].object.style.dropShadowColor = '#000000';
            this.text[i].object.style.dropShadowBlur = 4;
            this.text[i].object.style.dropShadowAngle = Math.PI / 3;
            this.text[i].object.style.dropShadowDistance = 6;
            this.text[i].setScale(this.parent.ratio.mid * 0.9);
          }

          this.labelText.object.text = label;
          this.valueText.object.text = value;
          this.percentText.object.text = `${Math.floor((value / this.chartValueSum) * 100)}%`;

          this.extrudeInformation = {
            first: Utility.findNewPointByAngleWithDistance(this.center, Math.degrees(startAngle), radiusLength),
            second: Utility.findNewPointByAngleWithDistance(this.center, Math.degrees(endAngle), radiusLength),
            startAngle,
            endAngle,
            color,
            labelTextPosition,
            valueTextPosition,
            percentTextPosition,
          };
        }
      }
    };
  }

  /**
   * Extrude 효과의 상태 업데이트
   * @memberof PieEffect
   * @instance
   */
  extrudeUpdate() {
    if (_.isUndefined(this.extrudeInformation)) {
      return;
    }

    this.extrudeStore = this.extrudeStore || {};
    if (_.isUndefined(this.extrudeStore.information) || !_.isEqual(this.extrudeStore.information, this.extrudeInformation)) {
      this.graphics.object.clear();
      this.extrudeStore.information = this.extrudeInformation;
      this.extrudeStore.startTime = new Date();
      this.extrudeStore.prevMinusY = 0;
    }

    const {
      first, second, startAngle, endAngle, color, labelTextPosition, valueTextPosition, percentTextPosition,
    } = this.extrudeStore.information;
    const elapsedTime = new Date() - this.extrudeStore.startTime;
    const easingTime = elapsedTime / (this.extrudeDuration * 1000);
    const easing = EasingFunctions.easeOutQuart;
    const progress = easingTime < 1 ? easing(easingTime) : 1;
    const minusY = this.extrudeDistance * progress;

    if (progress >= 1) { return; }

    this.graphics.object.beginFill(color, 1);
    this.graphics.object.moveTo(this.center.x, this.center.y - minusY);
    this.graphics.object.lineTo(first.x, first.y - minusY);
    this.graphics.object.arc(this.center.x, this.center.y - minusY, this.radiusLength, startAngle, endAngle, false);
    this.graphics.object.lineTo(second.x, second.y - minusY);
    this.graphics.object.lineTo(this.center.x, this.center.y - minusY);
    this.graphics.object.endFill();

    this.topGraphics.object.clear();
    this.topGraphics.object.beginFill(0xFFFFFF, 0.3);
    this.topGraphics.object.lineStyle(2, color, 1);
    this.topGraphics.object.moveTo(this.center.x, this.center.y - minusY);
    this.topGraphics.object.lineTo(first.x, first.y - minusY);
    this.topGraphics.object.arc(this.center.x, this.center.y - minusY, this.radiusLength, startAngle, endAngle, false);
    this.topGraphics.object.lineTo(second.x, second.y - minusY);
    this.topGraphics.object.lineTo(this.center.x, this.center.y - minusY);
    this.topGraphics.object.endFill();

    this.labelText.setPosition(labelTextPosition.x, labelTextPosition.y - minusY);
    this.labelText.setAlpha(progress);

    this.valueText.setPosition(valueTextPosition.x, valueTextPosition.y - minusY);
    this.valueText.setAlpha(progress);

    this.percentText.setPosition(percentTextPosition.x, percentTextPosition.y - minusY);
    this.percentText.setAlpha(progress / 2);

    this.extrudeStore.prevMinusY = minusY;
  }

  /**
   * 현재 생성되어 있는 Pie Effect 제거
   * <p> [Effect]{@link Effect} 클래스에 의해 호출된다.
   * @memberof PieEffect
   * @instance
   */
  destroy() {
    clearInterval(this.extrudeUpdateInterval);
    delete this.extrudeUpdateInterval;
    this.topGraphics.object.clear();
    this.graphics.object.clear();
    for (let i = 0; i < 3; i++) {
      this.text[i].setAlpha(0);
    }
    this.chart.options.tooltips.enabled = true;
    this.chart.options.hover.mode = 'single';
  }
}