React 프로그램에서 D3.js 활용하여 그래프 그리기

우디·2024년 3월 4일
0
post-thumbnail

안녕하세요:) 개발자 우디입니다! 아래 내용 관련하여 작업 중이신 분들께 도움이되길 바라며 글을 공유하니 참고 부탁드립니다😊
(이번에 벨로그로 이사오면서 예전 글을 옮겨적었습니다. 이 점 양해 부탁드립니다!)

작업 시점: 2021년 8월

배경

  • 프로그램 내에서 분석한 데이터들을 그래프로 보여주기 위해 D3.js 활용함.

D3.js 개요

  • Data-Driven Documents(데이터 중심)
    ⇒ 데이터 수량, 종류에 따라서 다양한 시도 가능
  • 웹브라우저 상에서 동적이고 인터렉티브한 정보시각화를 구현하기 위한 자바스크립트 라이브러리
  • 사용자가 입력한 시각화 규칙을 바탕으로 데이터를 반영해 시각화 결과물을 생성함
  • 데이터 불러오기(loading) → 데이터 엮기(binding) → 시각화 요소 지정(transforming) → 사용자 반응 지정(transitioning) 과정을 통해 구현 가능.

구현 과정

  • 개괄하자면.

    • match 별로 데이터 수량에 따라 기본 엘리먼트들을 구성 → 시각화 요소 지정
  • 실제 코드 적용

    • 데이터 불러오기

      const gameEventData = this.props.projectContext.state.gameEventGraph;
    • 데이터 바인딩

      let lolHorizontalUpperBoxJoin = matchRange
        .selectAll('rect.lolHorizontalUpperBox')
        .data([data['matchRange']]); //array
      
      // 데이터 수에 따라 엘리먼트 생성
      lolHorizontalUpperBoxJoin.enter().append('rect').attr('class', 'lolHorizontalUpperBox');
      • selectAll: 엘리먼트 여러 개를 선택 (select는 특정 태그 하나 선택)
      • data: 데이터 엮기
      • enter: data가 기존의 요소의 수보다 많을 때 사용 (반대는 exit())
      • append: enter메소드를 사용해서 넘치는 데이터 수만큼 추가로 엘리먼트를 넣어주는 메소드 (반대는 remove())
      • attr: 속성 부여
    • 시각화 요소 지정 & 사용자 반응 지정

      let lolHorizontalUpperBoxJoinAll = gameEventGraph.selectAll('rect.lolHorizontalUpperBox');
      
       lolHorizontalUpperBoxJoinAll
         .attr('x', d => {
           return d['start'];
         })
         .attr('y', 0)
         .attr('width', d => {
           return d['end'] - d['start'];
         })
         .attr('height', halfViewBoxHeight)
         .attr('fill', RED_TEAM_BACKGROUND_COLOR_STR)
         .on('mouseover', showUpperBoxTooltip)
         .on('mousemove', moveTooltip)
         .on('mouseleave', hideTooltip);
         .on('click', buttonClick);
      • 시각화 요소
        • rect 요소의 x, y, width, height 속성 등.
      • 사용자 반응
        • .on() 메서드로 붙어있는 요소들
    • 오브젝트에 마우스 호버 시 툴팁으로 추가 정보 제공하는 코드 추가

      • 툴팁 엘리먼트 생성
        const tooltip = d3
          .select('div#gameEventGraphTooltip')
          .style('display', 'none');
      • 툴팁 메소드 정의
        • showTooltip: 사용자가 각 오브젝트에 마우스 호버 시 툴팁 보여주는 메소드. 내부 메시지는 오브젝트 정보에 따라 달라짐.

        • showUpperBoxTooltip: 사용자 마우스가 상단 박스에 호버 시 툴팁 보여주는 메소드.

        • showLowerBoxTooltip: 사용자 마우스가 하단 박스에 호버 시 툴팁 보여주는 메소드.

        • moveTooltip: 사용자 마우스가 호버한 상태에서 움직일 경우의 메소드

        • hideTooltip: 사용자 마우스가 특정 영역을 호버했다가 벗어날 경우의 메소드.

          let showTooltip = (e, d) => {
            tooltip.transition().duration(100).style('display', 'block');
          
            let rect = this.gameEventTrackRef.current.getBoundingClientRect();
            let relativeX = e.pageX - rect.left;
          
            tooltip
              .html(this.getTooltipHtml(d))
              .style('left', relativeX + 10 + 'px')
              .style('top', '30px')
              .style('padding', '8px 15px 8px 9px')
              .style('background-color', TOOLTIP_BACKGROUND_COLOR);
          };
          
          let showUpperBoxTooltip = (e, d) => {
            tooltip.transition().duration(100).style('display', 'block');
          
            let rect = this.gameEventTrackRef.current.getBoundingClientRect();
            let relativeX = e.pageX - rect.left;
          
            tooltip
              .html('...')
              .style('left', relativeX + 10 + 'px').style('top', '30px')
              .style('padding', '8px 15px 8px 9px')
              .style('background-color', TOOLTIP_BACKGROUND_COLOR);
          };
          
          let showLowerBoxTooltip = (e, d) => {
            tooltip.transition().duration(100).style('display', 'block');
          
            let rect = this.gameEventTrackRef.current.getBoundingClientRect();
            let relativeX = e.pageX - rect.left;
          
            tooltip
              .html('...')
              .style('left', relativeX + 10 + 'px')
              .style('top', '30px')
              .style('padding', '8px 15px 8px 9px')
              .style('background-color', TOOLTIP_BACKGROUND_COLOR);
          };
          
          let moveTooltip = (e, d) => {
            let rect = this.gameEventTrackRef.current.getBoundingClientRect();
            let relativeX = e.pageX - rect.left;
          
            tooltip
              .style('left', relativeX + 10 + 'px')
              .style('top', '30px')
              .style('padding', '8px 15px 8px 9px')
              .style('background-color', TOOLTIP_BACKGROUND_COLOR);
          };
          
          let hideTooltip = (e, d) => {
            tooltip.transition().duration(100).style('display', 'none');
          };
      • 각 오브젝트에 알맞는 툴팁 html 구성하는 코드
        getTooltipHtml = (d) => {
          ...생략...        
          return `${eventType}<br/>${startTime}`;
        };
      • 각 오브젝트 속성에 툴팁 메소드 적용
        lolInhibitorRedYellowAll
          ...
          .on('mouseover', showTooltip)
          .on('mousemove', moveTooltip)
          .on('mouseleave', hideTooltip)
          ...
    • 클릭 이벤트 추가

      • 메서드 정의
         let buttonClick = (e, d) => {
           ...생략...
         };
      • 각 속성에 메서드 적용
        lolInhibitorRedYellowAll
          ...
          .on('click', buttonClick);
    • 줌 속성에 따라 적절한 width 값으로 설정하기

      • 오브젝트 아이콘들이 이미지로 들어가다보니, 줌 in/out 시에 같이 크기가 커지거나 작아지는 문제 발생 → 줌 속성에 따른 적절한 width 값으로 세팅하는 작업이 필요함
      • 줌 속성에 따라 적절한 width 값으로 설정하기 위한 코드
        getProperWidthOfObject = () => {
          let targetUnitWidth = this.props.timelineContext.api.getTargetUnitWidth();
          let defaultWidth = 50; // 단위: 초
          let properWidth =
            targetUnitWidth === 3600 //줌in 1회
              ? defaultWidth / 2
              : targetUnitWidth === 7200 //줌in 2회
                ? defaultWidth / 4
                : targetUnitWidth === 14400 //줌in 3회
                  ? defaultWidth / 8
                  : defaultWidth;
          return properWidth;
        };
        • 줌 in / out 을 관리하고 있는 변수 targetUnitWidth 를 받아와서, 이에 따른 적절한 width 값이 반환되도록 구현함
        • 핵심은 줌 in 시에 아이콘이 커지지 않도록, 줌 out 시 아이콘이 작아지지 않도록 하는 것임
        • 줌 out 시에는 defaultWidth 를 미리 설정해둬서, 아무리 줌 out이 되더라도 아이콘의 width는 변하지 않도록 구현
        • 줌 in 시에는 defaultWidth 를 나누는 방식으로 구현
          • defaultWidth 보다 작아지는 구조가 맞나?
            • 지금 프로그램에서 줌 in 기능은 타임라인을 옆으로 늘려서 보여주는 방식으로 진행되고 있음
            • 줌 in이 1회 되면 내부 요소들은 가로로 2배 늘어나는 구조임
            • 그렇기 때문에 줌 in이 되면 1회의 경우 1/2, 2회의 경우 1/4, 3회의 경우 1/8 해줘야 함
        • 각 요소들에는 아래와 같이 width 속성 값 코드가 적용됨.
          lolInhibitorRedYellowAll
            ...
            .attr('width', this.getProperWidthOfObject())
            ...

배우고 느낀 점

  • 짜여진 데이터 구조에 알맞게 D3.js를 활용하는 것에 대한 고민이 많이 들었음.
  • svg는 z-index 속성이 없고, 나중에 그려지는 요소가 가장 상위에 올라오게 되어있음.
    • 상위에 올라와야 하는 것들은 나중에 그려지도록 엘리먼트를 배치해야 함.
  • 고정종횡비 해제 관련
    • 종횡비가 고정되어 있으면 사용자가 부여한 width 속성이 적용되지 않고 줌in 시 가로로 늘어나는 현상 발생
      ⇒ 아래와 같이 코드 작성하면 줌in을 하더라도 사용자가 부여한 width가 적용되어 가로로 늘어나는 현상이 발생하지 않음.
      .attr('preserveAspectRatio', 'none')
  • 프로그램 내에서 사용자들이 데이터를 더욱 잘 활용할 것이라는 생각에 뿌듯했음.
  • 그래프 그리는 것과 동시에 사용자 액션, 반응형 등을 고려하는 과정에서 많은 것을 배움.
profile
넓고 깊은 지식을 보유한 개발자를 꿈꾸고 있습니다:) 기억 혹은 공유하고 싶은 내용들을 기록하는 공간입니다

0개의 댓글