ECharts-GL 이벤트 처리
목차
이벤트 메커니즘
ECharts-GL은 사용자 상호작용을 위한 강력한 이벤트 처리 시스템을 제공합니다. 이 시스템은 ECharts의 기본 이벤트 시스템을 확장하여 3D 환경에서의 상호작용을 지원합니다.
일반적인 이벤트 흐름
ECharts-GL에서 이벤트가 처리되는 일반적인 흐름은 다음과 같습니다:
sequenceDiagram
participant User
participant DOM
participant ZRender
participant ECharts
participant EChartsGL
participant Mesh
User->>DOM: 마우스/터치 이벤트
DOM->>ZRender: 이벤트 전달
ZRender->>ECharts: 이벤트 처리
ECharts->>EChartsGL: GL 이벤트 처리
EChartsGL->>Mesh: 레이캐스팅
Mesh-->>EChartsGL: 교차점 반환
EChartsGL-->>ECharts: 이벤트 결과 전달
ECharts-->>User: 시각적 피드백
이벤트 처리 단계
- 이벤트 캡처: DOM 이벤트(마우스, 터치)가 캡처됩니다.
- 좌표 변환: 화면 좌표가 ECharts 내부 좌표로 변환됩니다.
- 레이캐스팅: 3D 공간에서 어떤 객체가 선택되었는지 결정합니다.
- 이벤트 디스패치: 적절한 컴포넌트나 시리즈에 이벤트가 전달됩니다.
- 이벤트 핸들링: 컴포넌트나 시리즈가 이벤트에 응답합니다.
- 시각적 피드백: 하이라이트, 툴팁 등의 시각적 피드백이 제공됩니다.
마우스/터치 이벤트 처리
ECharts-GL은 다음과 같은 마우스/터치 이벤트를 처리합니다:
// 이벤트 리스너 등록 예시
chart.on('click', function(params) {
if (params.componentType === 'series' && params.seriesType === 'bar3D') {
console.log('Clicked on bar:', params.data);
}
});
// 지원되는 이벤트 유형
// 'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove',
// 'mouseout', 'mouseover', 'globalout', 'contextmenu'
내부적으로 ECharts-GL은 이벤트를 다음과 같이 처리합니다:
// Cone3DView.js 내부 이벤트 처리 예시
_initHandler: function (seriesModel, api) {
var data = seriesModel.getData();
var coneMesh = this._coneMesh;
var lastDataIndex = -1;
coneMesh.off('mousemove');
coneMesh.off('mouseout');
// 마우스 이동 이벤트 처리
coneMesh.on('mousemove', function (e) {
// 레이캐스팅으로 선택된 데이터 인덱스 가져오기
var dataIndex = coneMesh.geometry.getDataIndexOfVertex(e.triangle[0]);
if (dataIndex !== lastDataIndex) {
// 이전 항목 다운플레이, 새 항목 하이라이트
this._downplay(lastDataIndex);
this._highlight(dataIndex);
// 라벨 업데이트
this._labelsBuilder.updateLabels([dataIndex]);
// 축 포인터 표시 (좌표계가 cartesian3D인 경우)
if (seriesModel.coordinateSystem.type === 'cartesian3D') {
api.dispatchAction({
type: 'grid3DShowAxisPointer',
value: [
data.get('x', dataIndex),
data.get('y', dataIndex),
data.get('z', dataIndex, true)
]
});
}
}
lastDataIndex = dataIndex;
coneMesh.dataIndex = dataIndex;
}, this);
// 마우스 아웃 이벤트 처리
coneMesh.on('mouseout', function (e) {
this._downplay(lastDataIndex);
this._labelsBuilder.updateLabels();
lastDataIndex = -1;
coneMesh.dataIndex = -1;
// 축 포인터 숨기기
if (seriesModel.coordinateSystem.type === 'cartesian3D') {
api.dispatchAction({
type: 'grid3DHideAxisPointer'
});
}
}, this);
}
레이캐스팅 구현 방식
레이캐스팅(Raycasting)은 3D 공간에서 마우스 위치에 해당하는 객체를 찾는 기술입니다. ECharts-GL에서는 다음과 같이 구현됩니다:
graph TD
A[마우스 좌표] --> B[NDC 좌표 변환]
B --> C[레이 생성]
C --> D[장면 객체와 교차 테스트]
D --> E[가장 가까운 교차점 선택]
E --> F[데이터 인덱스 반환]
// 레이캐스팅 구현 예시
ViewGL.prototype.pickObject = function (x, y) {
// 정규화된 장치 좌표(NDC)로 변환
var ndc = new Vector2();
ndc.x = (x / this.renderer.getWidth()) * 2 - 1;
ndc.y = -(y / this.renderer.getHeight()) * 2 + 1;
// 레이 생성
this.camera.update();
var ray = new Ray();
ray.setFromCamera(ndc, this.camera);
// 교차 테스트
var intersects = [];
this.scene.traverse(function (mesh) {
if (mesh.isRenderable && mesh.isRenderable()) {
// 메시와 레이의 교차 테스트
ray.intersectMesh(mesh, intersects);
}
});
// 가장 가까운 교차점 반환
if (intersects.length) {
intersects.sort(function (a, b) {
return a.distance - b.distance;
});
return intersects[0];
}
return null;
};
이벤트 확장
ECharts-GL은 기본 이벤트 시스템을 확장하여 다양한 상호작용을 구현할 수 있습니다.
컨텍스트 메뉴 구현
3D 차트에 컨텍스트 메뉴를 추가하는 방법은 다음과 같습니다:
// 컨텍스트 메뉴 구현 예시
chart.on('contextmenu', function (params) {
// 기본 컨텍스트 메뉴 방지
params.event.event.preventDefault();
if (params.componentType === 'series') {
// 컨텍스트 메뉴 표시
var menu = document.getElementById('custom-context-menu');
menu.style.display = 'block';
menu.style.left = params.event.event.clientX + 'px';
menu.style.top = params.event.event.clientY + 'px';
// 선택된 데이터 저장
menu.dataset.dataIndex = params.dataIndex;
menu.dataset.seriesIndex = params.seriesIndex;
}
});
// 문서 클릭 시 컨텍스트 메뉴 숨기기
document.addEventListener('click', function () {
document.getElementById('custom-context-menu').style.display = 'none';
});
// 컨텍스트 메뉴 항목 클릭 처리
document.getElementById('menu-item-detail').addEventListener('click', function () {
var menu = document.getElementById('custom-context-menu');
var dataIndex = parseInt(menu.dataset.dataIndex);
var seriesIndex = parseInt(menu.dataset.seriesIndex);
// 데이터 세부 정보 표시
var data = chart.getModel().getSeries()[seriesIndex].getData().get('value', dataIndex);
console.log('Data detail:', data);
// 메뉴 숨기기
menu.style.display = 'none';
});
드래깅 구현
3D 객체를 드래그하는 기능을 구현하는 방법은 다음과 같습니다:
// 드래깅 구현 예시
var isDragging = false;
var dragTarget = null;
var lastMousePosition = null;
// 마우스 다운 이벤트
chart.on('mousedown', function (params) {
if (params.componentType === 'series' && params.seriesType === 'scatter3D') {
isDragging = true;
dragTarget = {
seriesIndex: params.seriesIndex,
dataIndex: params.dataIndex
};
lastMousePosition = [params.event.offsetX, params.event.offsetY];
// 드래그 시작 시각적 피드백
chart.setOption({
series: [{
id: params.seriesId,
data: chart.getOption().series[params.seriesIndex].data.map(function (item, idx) {
if (idx === params.dataIndex) {
// 드래그 중인 항목 강조
return Object.assign({}, item, {
itemStyle: {
opacity: 0.8,
borderWidth: 2,
borderColor: '#fff'
}
});
}
return item;
})
}]
});
}
});
// 마우스 이동 이벤트
chart.getZr().on('mousemove', function (e) {
if (isDragging && dragTarget) {
// 마우스 이동량 계산
var deltaX = e.offsetX - lastMousePosition[0];
var deltaY = e.offsetY - lastMousePosition[1];
lastMousePosition = [e.offsetX, e.offsetY];
// 3D 좌표계에서의 이동량 계산
var series = chart.getModel().getSeries()[dragTarget.seriesIndex];
var coordSys = series.coordinateSystem;
// 현재 데이터 가져오기
var data = series.getData();
var oldValue = data.getValues(['x', 'y', 'z'], dragTarget.dataIndex);
// 새 위치 계산 (간단한 예시, 실제로는 더 복잡한 변환 필요)
var newValue = [
oldValue[0] + deltaX * 0.01,
oldValue[1] - deltaY * 0.01,
oldValue[2]
];
// 데이터 업데이트
var option = chart.getOption();
option.series[dragTarget.seriesIndex].data[dragTarget.dataIndex] = newValue;
chart.setOption(option);
}
});
// 마우스 업 이벤트
chart.getZr().on('mouseup', function () {
if (isDragging && dragTarget) {
// 드래그 종료 시각적 피드백
var option = chart.getOption();
var item = option.series[dragTarget.seriesIndex].data[dragTarget.dataIndex];
if (typeof item === 'object' && item.itemStyle) {
delete item.itemStyle;
}
chart.setOption(option);
isDragging = false;
dragTarget = null;
}
});
3D 공간에서 상호작용
3D 공간에서의 상호작용은 2D와 다른 접근 방식이 필요합니다. ECharts-GL은 다양한 3D 상호작용 방법을 제공합니다.
3D 객체와의 상호작용 구현 방법
객체 선택 및 하이라이트
3D 객체를 선택하고 하이라이트하는 방법은 다음과 같습니다:
// 객체 선택 및 하이라이트 예시
chart.on('click', function (params) {
if (params.componentType === 'series') {
// 모든 시리즈 다운플레이
chart.dispatchAction({
type: 'downplay',
seriesIndex: 'all'
});
// 선택된 시리즈 하이라이트
chart.dispatchAction({
type: 'highlight',
seriesIndex: params.seriesIndex,
dataIndex: params.dataIndex
});
// 선택된 데이터 정보 표시
console.log('Selected data:', params.data);
}
});
내부적으로 하이라이트는 다음과 같이 구현됩니다:
// Cone3DView.js 내부 하이라이트 구현
_highlight: function (dataIndex) {
var data = this._data;
if (!data) {
return;
}
var coneIndex = this._coneIndexOfData[dataIndex];
if (coneIndex < 0) {
return;
}
// 강조 스타일 가져오기
var itemModel = data.getItemModel(dataIndex);
var emphasisItemStyleModel = itemModel.getModel('emphasis.itemStyle');
var emphasisColor = emphasisItemStyleModel.get('color');
var emphasisOpacity = emphasisItemStyleModel.get('opacity');
// 기본 색상에서 강조 색상 생성
if (emphasisColor == null) {
var color = getItemVisualColor(data, dataIndex);
emphasisColor = echarts.color.lift(color, -0.4);
}
if (emphasisOpacity == null) {
emphasisOpacity = getItemVisualOpacity(data, dataIndex);
}
// 색상 설정
var colorArr = graphicGL.parseColor(emphasisColor);
colorArr[3] *= emphasisOpacity;
// 기하 데이터의 색상 변경
this._coneMesh.geometry.setColor(coneIndex, colorArr);
// 화면 갱신
this._api.getZr().refresh();
}
카메라 제어
ECharts-GL은 OrbitControl을 통해 3D 장면의 카메라를 제어합니다:
// 카메라 제어 옵션 설정
chart.setOption({
grid3D: {
viewControl: {
// 자동 회전
autoRotate: true,
// 자동 회전 속도
autoRotateSpeed: 10,
// 댐핑 계수 (관성)
damping: 0.8,
// 회전 감도
rotateSensitivity: 1.5,
// 줌 감도
zoomSensitivity: 1,
// 이동 감도
panSensitivity: 1,
// 초기 카메라 거리
distance: 150,
// 초기 카메라 각도
alpha: 40,
beta: 30,
// 카메라 중심
center: [0, 0, 0],
// 최소/최대 거리
minDistance: 50,
maxDistance: 300
}
}
});
내부적으로 OrbitControl은 다음과 같이 구현됩니다:
// OrbitControl 클래스 구현 (간략화)
var OrbitControl = function (options) {
this.target = new Vector3();
this.camera = null;
// 카메라 각도 및 거리
this._alpha = 0;
this._beta = 0;
this._distance = 100;
// 애니메이션 상태
this._animating = false;
this._zoomSpeed = 0;
this._rotateSpeed = new Vector2();
// 이벤트 핸들러
this._mouseDownHandler = this._mouseDownHandler.bind(this);
this._mouseWheelHandler = this._mouseWheelHandler.bind(this);
this._mouseMoveHandler = this._mouseMoveHandler.bind(this);
this._mouseUpHandler = this._mouseUpHandler.bind(this);
// 옵션 설정
if (options) {
this.setOption(options);
}
};
OrbitControl.prototype = {
// 마우스 다운 이벤트 처리
_mouseDownHandler: function (e) {
// 마우스 위치 저장
// 드래그 시작 플래그 설정
},
// 마우스 이동 이벤트 처리
_mouseMoveHandler: function (e) {
// 마우스 이동량 계산
// 카메라 각도 업데이트
},
// 마우스 휠 이벤트 처리
_mouseWheelHandler: function (e) {
// 휠 방향에 따라 줌 인/아웃
},
// 카메라 업데이트
update: function () {
// 애니메이션 상태 업데이트
// 카메라 위치 및 방향 계산
// 카메라 매트릭스 업데이트
}
};
객체 조작
3D 객체를 조작(이동, 회전, 크기 조절)하는 방법은 다음과 같습니다:
// 객체 조작 예시
var isTransforming = false;
var transformType = null; // 'translate', 'rotate', 'scale'
var transformTarget = null;
var startPosition = null;
// 변환 모드 버튼 이벤트 리스너
document.getElementById('translate-btn').addEventListener('click', function () {
transformType = 'translate';
updateTransformUI();
});
document.getElementById('rotate-btn').addEventListener('click', function () {
transformType = 'rotate';
updateTransformUI();
});
document.getElementById('scale-btn').addEventListener('click', function () {
transformType = 'scale';
updateTransformUI();
});
// 객체 선택
chart.on('click', function (params) {
if (params.componentType === 'series' && transformType) {
transformTarget = {
seriesIndex: params.seriesIndex,
dataIndex: params.dataIndex
};
// 선택된 객체 하이라이트
chart.dispatchAction({
type: 'highlight',
seriesIndex: params.seriesIndex,
dataIndex: params.dataIndex
});
}
});
// 마우스 다운 이벤트
chart.getZr().on('mousedown', function (e) {
if (transformTarget && transformType) {
isTransforming = true;
startPosition = [e.offsetX, e.offsetY];
}
});
// 마우스 이동 이벤트
chart.getZr().on('mousemove', function (e) {
if (isTransforming && transformTarget) {
var deltaX = e.offsetX - startPosition[0];
var deltaY = e.offsetY - startPosition[1];
startPosition = [e.offsetX, e.offsetY];
var option = chart.getOption();
var series = option.series[transformTarget.seriesIndex];
var data = series.data[transformTarget.dataIndex];
// 데이터 형식에 따라 처리
var newData;
if (Array.isArray(data)) {
// 배열 형식 데이터
newData = data.slice();
} else {
// 객체 형식 데이터
newData = Object.assign({}, data);
}
// 변환 유형에 따라 처리
switch (transformType) {
case 'translate':
// 이동 처리
if (Array.isArray(newData)) {
newData[0] += deltaX * 0.1;
newData[1] -= deltaY * 0.1;
} else {
newData.value[0] += deltaX * 0.1;
newData.value[1] -= deltaY * 0.1;
}
break;
case 'rotate':
// 회전 처리 (3D 회전은 복잡하므로 간단한 예시만 제공)
// 실제로는 quaternion이나 rotation matrix를 사용해야 함
break;
case 'scale':
// 크기 조절 처리
if (series.type === 'bar3D' || series.type === 'cone3D') {
// 높이 조절
if (Array.isArray(newData)) {
newData[2] *= (1 + deltaY * 0.01);
} else {
newData.value[2] *= (1 + deltaY * 0.01);
}
} else if (series.type === 'scatter3D') {
// 심볼 크기 조절
if (!newData.symbolSize) {
newData.symbolSize = series.symbolSize || 10;
}
newData.symbolSize *= (1 + deltaY * 0.01);
}
break;
}
// 데이터 업데이트
series.data[transformTarget.dataIndex] = newData;
chart.setOption(option);
}
});
// 마우스 업 이벤트
chart.getZr().on('mouseup', function () {
isTransforming = false;
});
// UI 업데이트 함수
function updateTransformUI() {
document.getElementById('translate-btn').classList.toggle('active', transformType === 'translate');
document.getElementById('rotate-btn').classList.toggle('active', transformType === 'rotate');
document.getElementById('scale-btn').classList.toggle('active', transformType === 'scale');
}
고급 상호작용 기법
피킹 및 선택
복잡한 3D 장면에서 정확한 객체 선택을 위한 고급 피킹 기법:
// 고급 피킹 구현 예시
function advancedPicking(chart, x, y) {
var zr = chart.getZr();
var egl = zr.__egl;
// 모든 레이어 순회
for (var zlevel in egl._layers) {
var layer = egl._layers[zlevel];
// 각 뷰 순회
for (var i = 0; i < layer.views.length; i++) {
var view = layer.views[i];
// 레이캐스팅 수행
var result = view.pickObject(x, y);
if (result) {
// 교차점 정보 추출
var mesh = result.mesh;
var dataIndex = mesh.geometry.getDataIndexOfVertex(result.triangle[0]);
// 시리즈 및 컴포넌트 정보 찾기
var seriesIndex = -1;
var componentModel = null;
chart.getModel().eachComponent(function (componentType, model) {
if (model.coordinateSystem && model.coordinateSystem.viewGL === view) {
componentModel = model;
}
});
chart.getModel().eachSeries(function (seriesModel, idx) {
if (seriesModel.coordinateSystem &&
seriesModel.coordinateSystem.viewGL === view) {
seriesIndex = idx;
}
});
return {
componentType: componentModel ? componentModel.mainType : null,
componentIndex: componentModel ? componentModel.componentIndex : -1,
seriesIndex: seriesIndex,
dataIndex: dataIndex,
mesh: mesh,
point: result.point.toArray()
};
}
}
}
return null;
}
// 사용 예시
chart.getZr().on('click', function (e) {
var result = advancedPicking(chart, e.offsetX, e.offsetY);
if (result) {
console.log('Picked:', result);
// 선택된 객체 처리
if (result.seriesIndex >= 0 && result.dataIndex >= 0) {
chart.dispatchAction({
type: 'highlight',
seriesIndex: result.seriesIndex,
dataIndex: result.dataIndex
});
}
}
});
제스처 인식
터치 기기에서의 제스처 인식 및 처리:
// 제스처 인식 구현 예시
var hammer = new Hammer(document.getElementById('main'));
// 핀치 제스처 활성화
hammer.get('pinch').set({ enable: true });
hammer.get('rotate').set({ enable: true });
// 핀치 제스처 처리 (줌)
hammer.on('pinch', function (e) {
var viewControl = chart.getModel().getComponent('grid3D').getModel('viewControl');
var distance = viewControl.get('distance');
// 핀치 스케일에 따라 거리 조정
var newDistance = distance / e.scale;
// 최소/최대 거리 제한
var minDistance = viewControl.get('minDistance') || 50;
var maxDistance = viewControl.get('maxDistance') || 400;
newDistance = Math.max(minDistance, Math.min(maxDistance, newDistance));
chart.setOption({
grid3D: {
viewControl: {
distance: newDistance
}
}
});
});
// 회전 제스처 처리
hammer.on('rotate', function (e) {
var viewControl = chart.getModel().getComponent('grid3D').getModel('viewControl');
var alpha = viewControl.get('alpha');
var beta = viewControl.get('beta');
// 회전 각도에 따라 카메라 각도 조정
chart.setOption({
grid3D: {
viewControl: {
alpha: alpha - e.rotation * 0.5,
beta: beta + e.rotation * 0.1
}
}
});
});
// 팬 제스처 처리
hammer.on('pan', function (e) {
var viewControl = chart.getModel().getComponent('grid3D').getModel('viewControl');
var center = viewControl.get('center') || [0, 0, 0];
// 팬 이동에 따라 카메라 중심 조정
chart.setOption({
grid3D: {
viewControl: {
center: [
center[0] - e.deltaX * 0.1,
center[1] + e.deltaY * 0.1,
center[2]
]
}
}
});
});
이러한 이벤트 처리 메커니즘을 통해 ECharts-GL은 사용자에게 풍부한 상호작용 경험을 제공하며, 3D 시각화의 탐색과 분석을 더욱 직관적으로 만듭니다.