D3.js와 SolidJS를 활용한 동적 SVG 시각화 구현

이 문서는 SVG 기반 데이터 시각화에서 중요한 세 가지 기능 구현 방법을 다룹니다. 첫째, 뷰 확대/축소 시 SVG 요소의 크기를 동적으로 조절하여 가독성을 높이는 방법. 둘째, 특정 데이터 포인트(파란색 점)를 두 번 클릭했을 때 시각적으로 강조하는 효과를 추가하는 방법. 셋째, 강조된 데이터 포인트와 관련된 다른 요소들의 불투명도를 조절하여 시각적 계층을 만드는 방법입니다.

1. 뷰 확대/축소 시 SVG 요소 크기 동적 조절

사용자가 SVG 뷰를 확대하거나 축소할 때, 텍스트 레이블이나 데이터 마커(원) 같은 요소들이 적절한 크기를 유지하도록 조정하는 것은 사용자 경험에 필수적입니다. 이 섹션에서는 D3.js의 줌(Zoom) 동작과 연동하여 이러한 요소들의 크기를 동적으로 변경하는 방법을 설명합니다.

먼저, 제어할 SVG 요소들을 선택하여 변수에 저장합니다. 여기서는 Y 좌표를 나타내는 빨간색 원과 X 좌표에 대한 텍스트 레이블을 예로 듭니다.


// Y 좌표 데이터 포인트를 나타내는 원 그리기
const dataPointMarkers = g.selectAll('circle.data-point-marker')
    .data(getYCoordinates()) // 데이터 바인딩
    .enter().append('circle') // 원 요소 생성
    .attr('class', 'data-point-marker') // 클래스 설정
    .attr('cx', d => xScale(d.coords[0])) // X 좌표 설정
    .attr('cy', d => yScale(d.coords[1])) // Y 좌표 설정
    .attr('r', 2) // 초기 반지름 설정
    .attr('fill', 'red') // 채우기 색상 설정
    .on('mouseover', (event, d) => { // 마우스 오버 이벤트
        const tooltip = d3.select('body') // 툴팁 div 생성
            .append('div')
            .attr('class', 'custom-tooltip')
            .style('position', 'absolute')
            .style('background-color', 'white')
            .style('border', '1px solid gray')
            .style('padding', '6px')
            .style('pointer-events', 'none') // 마우스 이벤트 무시
            .style('left', `${event.pageX + 12}px`)
            .style('top', `${event.pageY + 12}px`)
            .text(d.label); // 툴팁 텍스트 설정
    })
    .on('mouseout', () => { // 마우스 아웃 이벤트
        d3.select('.custom-tooltip').remove(); // 툴팁 제거
    });

// X 좌표 텍스트 레이블 그리기
const axisLabels = g.selectAll('text.axis-label')
    .data(getXLabels()) // 데이터 바인딩
    .enter().append('text') // 텍스트 요소 생성
    .attr('class', 'axis-label')
    .attr('x', d => xScale(d.coords[0]) - 5) // X 위치 설정
    .attr('y', d => yScale(d.coords[1]) + 5) // Y 위치 설정
    .text(d => d.name) // 텍스트 내용 설정
    .attr('font-family', 'sans-serif')
    .attr('font-size', '10px') // 초기 폰트 크기 설정
    .attr('fill', 'black')
    .attr('opacity', 0); // 초기에는 투명하게 설정

다음으로, D3.js 줌 동작의 'zoom' 이벤트 핸들러 내에서 이 요소들의 속성을 조절합니다.


// SVG 줌 동작 정의
const svgZoomHandler = d3.zoom()
    .scaleExtent([0.5, 7]) // 확대/축소 범위 설정
    .on('zoom', (event) => {
        g.attr('transform', event.transform); // g 요소에 변환 적용
        updateAuxiliaryView(event.transform); // 보조 뷰 업데이트 (아래 3번 섹션 참조)

        // 줌 레벨에 따라 폰트 크기 조정
        const currentFontSize = 14 / event.transform.k; // 줌 인/아웃에 반비례
        axisLabels.attr('font-size', `${currentFontSize}px`);

        // 줌 레벨에 따라 텍스트 레이블의 가시성(불투명도) 조정
        if (event.transform.k > 1.8) { // 특정 줌 레벨 이상일 때 보이게 함
            axisLabels.attr('opacity', 1);
        } else {
            axisLabels.attr('opacity', 0);
        }

        // 줌 레벨에 따라 데이터 마커 원의 반지름 조정
        dataPointMarkers.attr('r', 2.5 / (event.transform.k * 0.7));
    });

2. 메인 뷰의 데이터 포인트에 더블 클릭 강조 효과 추가

사용자가 특정 데이터 포인트를 두 번 클릭했을 때, 해당 포인트를 시각적으로 강조하는 효과를 추가하여 중요한 정보를 부각시킬 수 있습니다. 여기서는 D3.js를 사용하여 파란색 데이터 포인트에 더블 클릭 이벤트를 추가하고, 강조 링을 표시하는 방법을 보여줍니다.


// 메인 데이터 포인트(파란색 원) 생성
mainSvgGroup.selectAll('circle.main-data-point')
    .data(getPrimaryDataPoints()) // 데이터 바인딩
    .enter().append('circle') // 새 원 생성
    .attr('class', 'main-data-point') // 클래스 할당
    .attr('cx', d => xScale(d.x)) // X 좌표
    .attr('cy', d => yScale(d.y)) // Y 좌표
    .attr('r', 6) // 반지름 설정
    .attr('fill', 'steelblue') // 채우기 색상
    .attr('data-id', d => d.id || `${d.x}-${d.y}`) // 고유 ID 할당 (선택 관리용)
    .on('dblclick', function(event, d) {
        event.preventDefault(); // 기본 더블 클릭 동작 방지

        const currentCircle = d3.select(this);
        const pointId = currentCircle.attr('data-id');
        const existingHighlight = mainSvgGroup.select(`.highlight-ring[data-target-id='${pointId}']`);

        if (existingHighlight.empty()) {
            // 강조 링이 없으면 새로 생성
            mainSvgGroup.append('circle')
                .attr('class', 'highlight-ring')
                .attr('data-target-id', pointId) // 강조할 포인트 ID 연결
                .attr('cx', currentCircle.attr('cx'))
                .attr('cy', currentCircle.attr('cy'))
                .attr('r', 12) // 강조 링 반지름 (원본보다 크게)
                .attr('fill', 'none')
                .attr('stroke', 'gold') // 강조 색상 (황금색)
                .attr('stroke-width', 2.5);
        } else {
            // 강조 링이 이미 있으면 제거
            existingHighlight.remove();
        }

        // 선택된 데이터 포인트 목록을 SolidJS 신호를 통해 업데이트
        toggleSelectedDisplayPoint(d); // 이 함수는 3번 섹션에서 정의됩니다.
    });

3. 강조된 포인트에 따라 보조 뷰 요소의 투명도 조절

특정 데이터 포인트가 강조(선택)되면, 보조 뷰(예: 'lenSVG' 또는 '보조 뷰')에서 해당 포인트와 관련되지 않은 다른 요소들을 반투명하게 만들어 강조된 포인트를 더욱 부각시킬 수 있습니다. SolidJS의 신호(Signal)를 사용하여 선택 상태를 관리하고, 이 신호의 변경에 따라 시각적 요소를 업데이트합니다.

참고: 데이터가 부동 소수점 객체를 포함하는 경우, 정확한 비교를 위해 고유 식별자(예: ID 속성)를 사용하거나 문자열로 변환하여 비교하는 것이 좋습니다.

먼저, SolidJS의 createSignal을 사용하여 선택된 포인트들의 목록을 저장하는 신호를 생성합니다.


import { createSignal } from 'solid-js';

// 선택된 데이터 포인트를 저장하는 SolidJS 신호
const [selectedDisplayPoints, setSelectedDisplayPoints] = createSignal([]);

// 데이터 포인트 선택/해제 로직
const toggleSelectedDisplayPoint = (pointData) => {
    const pointId = pointData.id || `${pointData.x}-${pointData.y}`;
    const currentSelections = selectedDisplayPoints();

    if (currentSelections.some(item => (item.id || `${item.x}-${item.y}`) === pointId)) {
        // 이미 선택된 경우, 선택 해제
        setSelectedDisplayPoints(currentSelections.filter(item => (item.id || `${item.x}-${item.y}`) !== pointId));
    } else {
        // 선택되지 않은 경우, 선택
        setSelectedDisplayPoints([...currentSelections, pointData]);
    }
    // 선택 상태 변경 시 보조 뷰 업데이트가 필요할 수 있습니다.
    // updateAuxiliaryView();
};

위 2번 섹션의 더블 클릭 이벤트 핸들러는 이제 toggleSelectedDisplayPoint(d) 함수를 호출하여 selectedDisplayPoints 신호를 업데이트하고, 강조 링의 추가/제거를 관리합니다. 다음으로, 보조 뷰를 업데이트하는 함수(예: updateAuxiliaryView) 내에서 selectedDisplayPoints 신호의 데이터를 기반으로 다른 요소들의 불투명도를 조정합니다. 이 함수는 줌 이벤트나 데이터 변경, 또는 선택 상태 변경 시 호출될 수 있습니다.


// 보조 뷰(예: 연결선)를 업데이트하는 함수
const updateAuxiliaryView = (transform) => {
    // 예시: 파란색 점과 빨간색 점을 연결하는 선 그리기
    const linkedDataPairs = getPrimaryDataPoints().map((bluePoint, i) => ({
        primary: bluePoint,
        secondary: getSecondaryDataPoints()[i],
    }));

    // 기존 연결선 제거 (D3 업데이트 패턴에 따라 merge 또는 exit 처리 가능)
    g.selectAll('line.connection-line').remove();

    // 새로운 연결선 생성 또는 업데이트
    g.selectAll('line.connection-line')
        .data(linkedDataPairs)
        .enter().append('line')
        .attr('class', 'connection-line')
        .attr('x1', d => xScale(d.primary.x))
        .attr('y1', d => yScale(d.primary.y))
        .attr('x2', d => xScale(d.secondary.x))
        .attr('y2', d => yScale(d.secondary.y))
        // 선택된 파란색 점에 연결된 선은 진하게, 나머지는 반투명하게
        .attr('stroke', (d) => {
            const primaryPointId = d.primary.id || `${d.primary.x}-${d.primary.y}`;
            return selectedDisplayPoints().some(item => (item.id || `${item.x}-${item.y}`) === primaryPointId)
                   ? 'black' // 선택된 점과 연결된 선 (불투명)
                   : 'rgba(0, 0, 0, 0.2)'; // 선택되지 않은 점과 연결된 선 (반투명)
        })
        .attr('stroke-width', 1);

    // 보조 뷰에 줌 변환 적용 (필요에 따라)
    // auxiliarySvgGroup.attr('transform', transform); 
};

태그: D3.js SolidJS svg 데이터_시각화 상호작용_그래프

8월 25일 19:20에 게시됨