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();
}
}