import _ from 'lodash';
import Utility from './utility';
import Graphics from './graphics';
import Text from './text';
/**
* 차트들을 시간별로 볼 수 있는 Timline 기능을 담당하는 클래스
* @alias Timeline
*/
export default class Timeline {
/**
* 타임라인 생성에 필요한 각종 값들을 설정하고 마우스 이벤트를 등록한다.
* @param {object} _objects 타임라인 생성에 필요한 각종 오브젝트들
* @param {object} _datasets 타임라인 생성에 필요한 데이터셋 전체
*/
constructor(_objects, _datasets) {
/**
* @description Chart.js에서 생성된 차트 오브젝트
* @type { ChartJS }
*/
this.chart = _objects.chart;
/**
* @description 차트를 생성할 때 사용자들이 설정했던 옵션
* @type { object }
*/
this.option = _objects.option;
/**
* @description Chart 클래스에서 생성하지만 부모는 Chart클래스의 부모와 동일하게 설정
* @type { Gamify }
*/
this.parent = _objects.parent;
/**
* @description 타임라인 생성에 필요한 데이터셋 전체
* @type { object }
*/
this.datasets = _datasets;
/**
* @description PIXI 오브젝트
* @type { PIXI }
*/
this.pixi = Utility.makePixi(this.option, true);
/**
* @description Timeline에 bar를 그려줄 Graphics
* @type { Graphics }
*/
this.barGraphics = new Graphics(this.pixi);
this.barGraphics.setInteractive(true);
/**
* @description Timeline에 control을 그려줄 Graphics
* @type { Graphics }
*/
this.controlGraphics = new Graphics(this.pixi);
this.controlGraphics.setInteractive(true);
const { ratio } = this.parent;
const { rootSize } = this.option;
const marginLR = 10; // percent
/**
* @description 좌우 여백 퍼센트의 실제 값
* @type { number }
*/
this.marginLRValue = rootSize.width * (marginLR / 100);
/**
* @description Timeline의 주 색상 ( 16진수 )
* @type { number }
*/
this.color = 0x0288D1;
/**
* @description control의 원 크기
* @type { number }
*/
this.radius = ratio.mid * 10;
/**
* @description Timeline의 투명도
* @type { number }
*/
this.alpha = 0.5;
/**
* @description control의 x좌표
* @type { number }
*/
this.circleX = this.marginLRValue;
/**
* @description 현재 Timeline의 위치
* @type { number }
*/
this.current = 0;
/**
* @description control에 mouseover 여부
* @type { boolean }
*/
this.controlIn = false;
/**
* @description control에 mounsedown 여부
* @type { boolean }
*/
this.controlDown = false;
const circleStart = this.marginLRValue;
const circleEnd = rootSize.width - this.marginLRValue * 2 + circleStart;
const barLength = circleEnd - circleStart;
/**
* @description Timeline의 전체 넓이에서 하나의 넓이
* @type { number }
*/
this.hitLength = barLength / (this.datasets.length - 1);
/**
* @description Timeline에서 시간별 위치
* @type { array }
*/
this.hitPoint = [];
/**
* @description Timeline의 텍스트들
* @type { Text }
*/
this.text = [];
const barY = rootSize.height * 0.9 + this.radius / 4;
for (let i = 0; i < this.datasets.length; i++) {
this.hitPoint.push(i * this.hitLength + circleStart);
this.text.push(new Text(this.pixi, this.timeToString(i), {}));
this.text[i].setPosition(this.hitPoint[i], barY + this.text[i].object.height);
this.text[i].setAlpha(0.3);
let textScale = ratio.mid * (50 / (this.text[i].object.width));
textScale = textScale > 0.9 ? 0.9 : textScale;
this.text[i].setScale(textScale);
}
this.text[0].setAlpha(1);
this.drawTimeline();
this.barGraphics.object.on('mouseover', () => {
this.drawTimeline('bar-in');
});
this.barGraphics.object.on('mouseout', () => {
if (!this.controlIn) { this.drawTimeline('out'); }
});
this.controlGraphics.object.on('mouseover', () => {
this.controlIn = true;
this.drawTimeline('control-in');
});
this.controlGraphics.object.on('mouseout', () => {
this.controlIn = false;
this.drawTimeline('out');
});
this.controlGraphics.object.on('mousedown', (e) => {
this.controlDown = true;
this.drawTimeline('control-down', e);
});
this.controlGraphics.object.on('mouseup', (e) => {
this.controlDown = false;
this.drawTimeline('control-up', e);
this.changeDataset(this.index);
});
this.controlGraphics.object.on('mousemove', (e) => {
if (this.controlDown) { this.drawTimeline('control-move', e); }
});
this.parent.interaction.addKeyboardEvent('ArrowLeft', () => {
const next = this.current - 1 < 0 ? 0 : this.current - 1;
this.changeDataset(next);
this.drawTimeline('force');
});
this.parent.interaction.addKeyboardEvent('ArrowRight', () => {
const next = this.current + 1 > this.datasets.length - 1 ? this.datasets.length - 1 : this.current + 1;
this.changeDataset(next);
this.drawTimeline('force');
});
}
/**
* 인덱스를 통해 데이터셋을 변경시켜 데이터의 시간을 바꾼다.
* @param {number} index 변경시킬 인덱스
* @memberof Timeline
* @instance
*/
changeDataset(index) {
this.text[this.current].setAlpha(0.3);
for (let i = 0; i < this.text.length; i++) {
this.text[i].setAlpha(0.3);
}
this.text[index].setAlpha(1);
this.current = index;
const datasets = [_.cloneDeep(this.datasets[index])];
this.parent.update(datasets);
// const dataset = this.datasets[index];
// this.chart.config.data.datasets[0].data = dataset.data;
// this.chart.update();
}
/**
* Timeline을 현재 상태를 바탕으로 그려줌
* @param {string} state 현재 Timeline에 가해진 이벤트 상태
* @param {object} e 이벤트 상테에서 가져온 event 오브젝트 ( 마우스 위치 )
* @memberof Timeline
* @instance
*/
drawTimeline(state, e) {
const { rootSize } = this.option;
let controlBackgroundRadiusMagnification = 1;
switch (state) {
case 'out':
this.alpha = 0.5;
break;
case 'bar-in':
this.alpha = 1;
break;
case 'control-in':
this.alpha = 1;
controlBackgroundRadiusMagnification = 1.4;
break;
case 'control-down':
controlBackgroundRadiusMagnification = 2;
break;
case 'control-up':
controlBackgroundRadiusMagnification = 1.4;
break;
case 'control-move':
controlBackgroundRadiusMagnification = 2;
break;
default:
break;
}
this.barGraphics.object.clear();
this.barGraphics.object.beginFill(this.color, this.alpha / 2);
this.barGraphics.object.drawRect(this.marginLRValue, rootSize.height * 0.9, rootSize.width - this.marginLRValue * 2, this.radius / 2);
for (let i = 0; i < this.hitPoint.length; i++) {
this.barGraphics.object.drawCircle(this.hitPoint[i], rootSize.height * 0.9 + this.radius / 4, 5);
}
this.barGraphics.object.endFill();
this.controlGraphics.object.clear();
const circleStart = this.marginLRValue;
const circleEnd = rootSize.width - this.marginLRValue * 2 + circleStart;
this.circleX = _.isUndefined(e) ? this.circleX : e.data.global.x;
this.circleX = this.circleX < circleStart ? circleStart : this.circleX;
this.circleX = this.circleX > circleEnd ? circleEnd : this.circleX;
if (state === 'control-up') {
this.index = this.findNearest(this.hitPoint, this.circleX);
this.circleX = this.hitPoint[this.index];
} else if (this.controlDown) {
for (let i = 0; i < this.hitPoint.length; i++) {
if (this.circleX >= this.hitPoint[i] - this.hitLength * 0.2 && this.circleX <= this.hitPoint[i] + this.hitLength * 0.2) {
this.circleX = this.hitPoint[i];
this.changeDataset(i);
}
}
if (this.circleX > this.current * this.hitLength + this.marginLRValue) {
const nextIndex = this.current + 1;
const nextAlpha = ((this.circleX - this.marginLRValue) / this.hitLength) - this.current;
const currentAlpha = 1 - nextAlpha;
if (nextIndex <= this.datasets.length - 1) {
this.text[this.current].setAlpha(currentAlpha);
this.text[nextIndex].setAlpha(nextAlpha);
}
} else if (Math.abs(this.circleX - (this.current * this.hitLength + this.marginLRValue)) > 1) {
const nextIndex = this.current - 1;
const nextAlpha = this.current - ((this.circleX - this.marginLRValue) / this.hitLength);
const currentAlpha = 1 - nextAlpha;
if (nextIndex >= 0) {
this.text[this.current].setAlpha(currentAlpha);
this.text[nextIndex].setAlpha(nextAlpha);
}
}
}
if (_.includes(state, 'control')) {
this.controlGraphics.object.beginFill(this.color, 0.5);
this.controlGraphics.object.drawCircle(this.circleX, rootSize.height * 0.9 + this.radius / 4, this.radius * controlBackgroundRadiusMagnification);
this.controlGraphics.object.endFill();
}
if (state === 'force') {
this.circleX = this.marginLRValue + this.current * this.hitLength;
}
this.controlGraphics.object.beginFill(this.color, this.alpha);
this.controlGraphics.object.drawCircle(this.circleX, rootSize.height * 0.9 + this.radius / 4, this.radius * 0.7);
this.controlGraphics.object.endFill();
}
/**
* 제시한 수를 배열에서 검색해서 가장 가까운 값의 인덱스를 찾아줌
* @param {array} array 검색할 배열
* @param {number} value 찾을 수
* @returns {number} index
* @memberof Timeline
* @instance
*/
findNearest(array, value) {
let nearest = 0;
for (let i = 1; i < array.length; i++) {
const distance = Math.abs(value - array[i]);
nearest = distance < Math.abs(value - array[nearest]) ? i : nearest;
}
return nearest;
}
/**
* 시간을 제시한 포맷으로 변경시켜줌
* @param {index} index 제시한 포맷으로 변경시킬 시간
* @returns {string} time
* @memberof Timeline
* @instance
*/
timeToString(index) {
const originalTime = this.datasets[index].time.toLowerCase();
const timeObject = new Date(originalTime);
let format = this.option.data.timelineFormat;
const year = timeObject.getFullYear();
const month = timeObject.getMonth() + 1;
const date = timeObject.getDate();
format = _.replace(format, 'yyyy', year);
format = _.replace(format, 'mm', month < 10 ? `0${month}` : month);
format = _.replace(format, 'm', month);
format = _.replace(format, 'dd', date < 10 ? `0${date}` : date);
format = _.replace(format, 'd', date);
return format;
}
}