JavaScript[JS]
[JS] ChartJS Drag plugin 구현
HANdeveloper
2025. 4. 11. 16:13
chartjs - scatter chart에 drag 기능을 넣어 영역 안에 있는 포인트에 대한 정보를 모달창으로 띄우려고 함
1. 차트 컴포넌트에 넣을 플러그인 코드 생성
// ChartDragPlugin.js
// Store chart data
const states = new WeakMap();
const getState = (chart) => {
const state = states.get(chart);
return state || null;
};
const setState = (chart, updatedState) => {
const originalState = getState(chart);
// states.set(chart, Object.assign({}, originalState, updatedState));
states.set(chart, { ...originalState, ...updatedState });
return updatedState;
};
// Store options
const pluginOptions = {
colors: {
selection: '#454a50',
selectedElements: '#1f77b4',
unselectedElements: '#cccccc',
}
};
// Export main plugin
export default {
id: 'selectdrag',
start: (chart, _args, options) => {
// Check if enabled
if (!chart?.config?.options?.plugins?.selectdrag?.enabled) {
return;
}
// Get chart canvas
const canvasElement = chart.canvas;
// Draw begin
canvasElement.addEventListener('mousedown', (e) => {
// Get elements
const elements = chart.getElementsAtEventForMode(e, 'index', {
intersect: false,
axis: 'xy'
});
if (elements.length === 0) {
return;
}
const element = elements[0];
const datasetIndex = element.datasetIndex;
const index = element.index;
const xValue = chart.data.labels[index];
const yValue = chart.data.datasets[datasetIndex].data[index];
// Set selection origin
setState(chart, {
selectionXY: {
drawing: true,
start: {
x: e.offsetX,
y: e.offsetY,
xValue,
yValue,
datasetIndex,
index
},
end: {},
}
});
});
// Draw end
window.addEventListener('mouseup', (e) => {
// Check drawing status
const state = getState(chart);
if (!state || state?.selectionXY?.drawing === false) {
return;
}
// Get axis value
const elements = chart.getElementsAtEventForMode(e, 'index', {
intersect: false,
axis: 'xy'
});
const element = elements.length > 0 ? elements[0] : {
datasetIndex: state.selectionXY.start.datasetIndex,
index: chart.data.labels.length - 1
}
const endX = e.offsetX;
const endY = e.offsetY;
const endXValue = chart.data.labels[element.index];
const endYValue = elements.length > 0 ?
chart.data.datasets[element.datasetIndex].data[element.index] :
chart.scales.y.getValueForPixel(endY);
// 드래그 영역
const startX = Math.min(state.selectionXY.start.x, endX);
const endX2 = Math.max(state.selectionXY.start.x, endX);
const startY = Math.min(state.selectionXY.start.y, endY);
const endY2 = Math.max(state.selectionXY.start.y, endY);
// 드래그 영역 포인트
const selectedPoints = [];
let no = 0;
chart.data.datasets.forEach((dataset, datasetIndex) => {
dataset.data.forEach((point, index) => {
const x = chart.scales.x.getPixelForValue(point.x);
const y = chart.scales.y.getPixelForValue(point.y);
if (x >= startX && x <= endX2 && y >= startY && y <= endY2) {
const matchingPoint = dataset.data.find(i => i.x === point.x && i.y === point.y);
const mstSeq = matchingPoint?.properties.mstSeq;
const serviceEnvironment = matchingPoint?.properties.serviceEnvironment;
const serviceType = matchingPoint?.properties.serviceType;
const serviceName = matchingPoint?.properties.serviceName;
selectedPoints.push({
// datasetIndex,
index: no += 1,
datetime: point.x,
interfaceId: dataset.properties.interfaceId,
mstSeq,
datasetLabel: dataset.label,
serviceEnvironment,
serviceType,
serviceName,
})
}
})
})
state.selectionXY.end = {
x: endX,
y: endY,
xValue: endXValue,
yValue: endYValue,
datasetIndex: element.datasetIndex,
index: element.index
};
// End drawing
state.selectionXY.drawing = false;
setState(chart, state);
// Render rectangle
chart.update();
// Emit event
const selectCompleteCallback = chart?.config?.options?.plugins?.selectdrag?.onSelectComplete;
if (selectCompleteCallback) {
selectCompleteCallback({
range: {
x: [state.selectionXY.start.xValue, endXValue],
y: [state.selectionXY.start.yValue, endYValue]
},
boundingBox: {
startX,
startY,
endX: endX2,
endY: endY2
},
selectedPoints
});
}
});
// Draw extend
canvasElement.addEventListener('mousemove', (e) => {
// Check drawing status
const state = getState(chart);
if (!state || state?.selectionXY?.drawing === false) {
return;
}
// Set end origin
state.selectionXY.end = {
x: e.offsetX,
y: e.offsetY,
};
chart.render();
setState(chart, state);
});
},
// 선택 영역에 반투명한 사각형 그림
afterDraw: (chart, args, options) => {
// Check drawing status
const state = getState(chart);
if (!state || (state?.selectionXY?.drawing === false && !state.selectionXY.end?.x) || state?.selectionXY?.drawing === false) {
return;
}
// Save canvas state
const { ctx } = chart;
ctx.save();
// Draw user rectangle
ctx.globalCompositeOperation = 'destination-over';
// Draw selection
ctx.fillStyle = pluginOptions.colors.selection;
const startX = Math.min(state.selectionXY.start.x, state.selectionXY.end.x);
const width = Math.abs(state.selectionXY.end.x - state.selectionXY.start.x);
const startY = Math.min(state.selectionXY.start.y, state.selectionXY.end.y);
const height = Math.abs(state.selectionXY.end.y - state.selectionXY.start.y);
ctx.fillRect(startX, startY, width, height);
// Restore canvas
ctx.restore();
},
setSelection: (chart, range = []) => {
// Check has data
if (chart.data.labels.length === 0 || chart.data.datasets.length === 0) {
return;
}
// Check if new data blank
if (range.length === 0) {
// Clear selection'
setState(chart, null);
chart.update();
}
// Create state
const state = {
selectionXY: {
drawing: false,
start: {},
end: {}
},
};
// Set start axis
const startAxisIndex = chart.data.labels.findIndex((item) => {
return item === range[0];
});
state.selectionXY.start = {
axisValue: range[0],
axisIndex: startAxisIndex,
x: chart.scales.x.getPixelForValue(chart.data.labels[startAxisIndex]),
y: 0,
};
// Set end axis
const endAxisIndex = chart.data.labels.findIndex((item) => {
return item === range[1];
});
state.selectionXY.end = {
axisValue: range[0],
axisIndex: endAxisIndex,
x: chart.scales.x.getPixelForValue(chart.data.labels[endAxisIndex]),
y: chart.chartArea.height,
};
setState(chart, state);
chart.update();
},
clearSelection: (chart) => {
// Clear state
setState(chart, null);
chart.update();
},
}
2. 플러그인 코드 차트에 적용
// ScatterChart.js
// chart.js
import {
Chart as ChartJS,
LinearScale,
PointElement,
LineElement,
Tooltip,
Legend,
Colors,
CategoryScale,
TimeScale,
} from 'chart.js';
import 'chartjs-adapter-moment';
import ChartDragPlugin from './ChartDragPlugin';
(나머지 코드는 생략)
// 드래그플러그인 등록
ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend, CategoryScale, TimeScale, ChartDragPlugin);
ChartJS.defaults.font.family = "'LINESeedKR-Rg', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif";
...
const [selectedDragDatas, setSelectedDragDatas] = useState([]);
const [dragModalOpen, setDragModalOpen] = useState(false);
// 차트 옵션
const options = {
plugins: {
// 드래그 플러그인 적용
selectdrag: {
enabled: true,
onSelectComplete: ({ range, boundingBox, selectedPoints }) => {
setSelectedDragDatas(selectedPoints);
setDragModalOpen(true);
},
},
}
}