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