-
React로 Gauge Chart 만들기Language/React 2025. 2. 27. 17:07
2년전 차트로 화면을 표시할때 비슷한 차트 라이브러리를 찾고 커스터마이징해서 작업하는데 시간이 꽤 많이 소요되었다. 찾는것도 일이지만 직접 만들지 않았다보니 커스터마이징이 헬일경우가 많다.
이번에 리뉴얼이 많이 되면서 비슷한 차트를 찾기도 힘들고 유지보수도 힘들것 같으니 직접 만들어보기로 했다.
1. Figma에서 화면 캡쳐 및 GPT에서 html로 변환 요청
- 캡쳐 및 GTP 요청
이렇게 요청하면 react-gauge-char등 기존 라이브러리를 찾아서 사용하라고 한다.. OTL..
이렇게 했더니 react-gauge-char 라이브러리를 사용하라고 한다.. OTL..
- 다시요청
예상대로 svg로 만드는 법을 알려준다. 대충 만들어 진 코드 react에 복붙하면된다.
react에서 직접만드는 방법으로 다시요청한다. 2. React 적용 및 커스터 마이징
GPT로 요청한건 아직까지는 완벽하지 않다. 많이 부족하다.. 그래도 없는것 보다는!!
아래 겁나 커스터 마이징 해서 결과만만 정리해두자.- 호출코드
<div onClick={() => setChartData([ { label: 'AA', value: 30, color: 'red', tooltipText: 'ㅇㅇㅇㅇ' }, { label: 'BB', value: 60, color: '#E8A43A' }, { label: 'CC', value: 10, color: '#4B4B4B' }, ]) } > test </div> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '100px', backgroundColor: '#FFF' }}> <GaugeChart // data={[ // { label: 'A', value: 50, color: '#61B329', tooltipText: 'ㅇㅇㅇㅇ' }, // { label: 'B', value: 30, color: '#E8A43A' }, // { label: 'C', value: 20, color: '#4B4B4B' }, // ]} data={chartData} width={200} height={200} gap={1.5} strokeWidth={16} hoverEffect={false} showTooltip={false} label={ <div style={{ position: 'absolute', bottom: '5px' }}> <div style={{ fontWeight: 'bold' }}>50/30/20</div> <div style={{ color: '#666' }}>현재 공실</div> </div> } /> <GaugeChart data={[ { label: 'A', value: 1, color: '#FF0000' }, { label: 'B', value: 2, color: '#00FF00' }, { label: 'C', value: 3, color: '#0000FF' }, { label: 'D', value: 4, color: '#FFFF00' }, ]} width={250} height={260} gap={2} strokeWidth={10} hoverEffect={false} showTooltip={false} label="250 * 260, gap: 2, strokeWidth: 10" /> <GaugeChart data={[ { label: 'A', value: 40, color: '#FF5733' }, { label: 'B', value: 30, color: '#33FF57' }, // 랜덤 색상 적용 { label: 'C', value: 30, color: '#3498DB' }, ]} width={300} height={330} gap={0} strokeWidth={20} label="300 * 330, gap: 0, strokeWidth: 20" /> </div>
- GaugeChart
import React, { useState, useCallback, useMemo } from 'react'; // 랜덤 색상 생성 함수 const getRandomColor = () => { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; }; interface ChartData { label?: string; value: number; color?: string; tooltipText?: string; // 툴팁에 표시할 텍스트 추가 } interface GaugeChartProps { data?: ChartData[]; gap?: number; strokeWidth?: number; width?: number; height?: number; label?: React.ReactNode; hoverEffect?: boolean; // 마우스 오버 시 강조 효과 적용 여부 showTooltip?: boolean; // 툴팁 표시 여부 } const GaugeChart: React.FC<GaugeChartProps> = ({ data = [ { label: 'A', value: 40, tooltipText: 'A: 40개' }, { label: 'B', value: 30, tooltipText: 'B: 30개' }, { label: 'C', value: 30, tooltipText: 'C: 30개' }, ], gap = 2, strokeWidth = 6, width = 200, height = 120, label, hoverEffect = true, // 기본적으로 hover 효과 활성화 showTooltip = true, // 기본적으로 툴팁 활성화 }) => { const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); const [tooltipData, setTooltipData] = useState<string | null>(null); const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); const [isMouseOverChart, setIsMouseOverChart] = useState(false); // 차트 영역 위에 마우스가 있는지 추적 const radius = (width / 2) * 0.8; // 반지름 (차트 크기에 맞춰 자동 조정) const circumference = Math.PI * radius; // 반원의 둘레 길이 const total = data.reduce((sum, item) => sum + item.value, 0); // 전체 값 합산 // **gap과 strokeWidth를 차트 크기에 비례하도록 조정** const adjustedGap = (gap / 100) * circumference; // gap을 크기에 맞게 조정 const adjustedStrokeWidth = (strokeWidth / 200) * width; // strokeWidth 비율 적용 // 여백 개수 (첫 번째, 마지막 제외) const totalGaps = data.length - 1; const gapSize = totalGaps > 0 ? adjustedGap * totalGaps : 0; // 총 여백 길이 const availableCircumference = circumference - gapSize; // 마지막 색상은 여백 제외 // 색상 배열 생성 (색상이 없을 경우 랜덤 색상 사용) const formattedData = data.map((item, index) => ({ ...item, color: item.color || getRandomColor(), // 지정된 색상이 없으면 랜덤 색상 적용 ratio: (item.value / total) * availableCircumference, // 비율에 맞게 길이 조정 })); // 마우스 이벤트 핸들러 const handleMouseEnter = useCallback( (e: React.MouseEvent, index: number, item: (typeof formattedData)[0]) => { if (isMouseOverChart) { setHoveredIndex(index); if (showTooltip) { setTooltipData(item.tooltipText || `${item.label}: ${item.value}`); setTooltipPosition({ x: e.clientX, y: e.clientY - 40 }); } } }, [isMouseOverChart, showTooltip] ); const handleMouseMove = useCallback( (e: React.MouseEvent, index: number) => { if (showTooltip && isMouseOverChart && hoveredIndex === index) { setTooltipPosition({ x: e.clientX, y: e.clientY - 40 }); } }, [showTooltip, isMouseOverChart, hoveredIndex] ); // 차트 경로 생성 (메모이제이션으로 성능 최적화) const chartPaths = useMemo(() => { return formattedData.reduce<{ prevTotal: number; elements: JSX.Element[] }>( (acc, item, index) => { const prevTotal = acc.prevTotal; const dashArray = `${item.ratio} ${circumference}`; const dashOffset = `-${prevTotal}`; acc.elements.push( <path key={index} d={`M ${width * 0.1} ${height / 2} A ${radius} ${radius} 0 0 1 ${width * 0.9} ${height / 2}`} stroke={item.color} strokeWidth={hoverEffect && hoveredIndex === index ? adjustedStrokeWidth * 1.5 : adjustedStrokeWidth} fill="none" strokeDasharray={dashArray} strokeDashoffset={dashOffset} onMouseEnter={(e) => handleMouseEnter(e, index, item)} onMouseLeave={(e) => { // 각 path 요소에서 마우스가 벗어날 때 hoveredIndex를 null로 설정하지 않음 // 이렇게 하면 마우스가 path 사이를 이동할 때 깜빡임 방지 // 전체 차트에서 마우스가 벗어날 때만 hoveredIndex를 null로 설정 }} onMouseMove={(e) => handleMouseMove(e, index)} style={{ transition: 'stroke-width 0.2s ease-in-out', cursor: 'pointer', }} /> ); // 첫 번째와 마지막 선에는 gap 적용하지 않음 if (index < totalGaps) { acc.prevTotal += item.ratio + adjustedGap; } else { acc.prevTotal += item.ratio; } return acc; }, { prevTotal: 0, elements: [] } ).elements; }, [ formattedData, hoveredIndex, circumference, radius, width, height, adjustedStrokeWidth, hoverEffect, totalGaps, adjustedGap, handleMouseEnter, handleMouseMove, ]); return ( <div style={{ textAlign: 'center', width: `${width}px`, position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', }} onMouseLeave={() => { // 전체 차트 영역을 벗어나면 모든 상태 초기화 setIsMouseOverChart(false); setHoveredIndex(null); setTooltipData(null); }} onMouseEnter={() => { setIsMouseOverChart(true); }} > {/* 차트 (SVG) */} <svg width={width} height={height / 2} viewBox={`0 0 ${width} ${height / 2}`}> {/* 배경 원 */} <path d={`M ${width * 0.1} ${height / 2} A ${radius} ${radius} 0 0 1 ${width * 0.9} ${height / 2}`} stroke="#E0E0E0" strokeWidth={adjustedStrokeWidth} fill="none" /> {chartPaths} </svg> {label && label} {/* 툴팁 (showTooltip이 true일 때만 표시) */} {showTooltip && tooltipData && isMouseOverChart && ( <div style={{ position: 'fixed', top: `${tooltipPosition.y}px`, left: `${tooltipPosition.x}px`, background: 'rgba(0, 0, 0, 0.75)', color: '#FFF', padding: '6px 12px', borderRadius: '4px', fontSize: '14px', pointerEvents: 'none', transform: 'translate(-50%, -100%)', whiteSpace: 'nowrap', zIndex: 1000, }} > {tooltipData} </div> )} </div> ); }; export default GaugeChart;
완성
결과물
그럭저럭 만족할만하게 만들어 졌다. 사용법
interface GaugeChartProps { data?: ChartData[]; gap?: number; strokeWidth?: number; width?: number; height?: number; label?: React.ReactNode; hoverEffect?: boolean; // 마우스 오버 시 강조 효과 적용 여부 showTooltip?: boolean; // 툴팁 표시 여부 }
GaugeChartProps Interface에 기록된것처럼 사용법도 예상되지 아니한가? 그대로 때려 넣으면 된다.
AI대세가 맞긴 한가보다..
예전에는 이런거 만들려면 몇일 걸렸을텐데 GPT사용해서 커스터 마이징하니깐 몇시간이면 뚝딱이다.'Language > React' 카테고리의 다른 글
InfiniteScroll 컴포넌트 간단구현 (1) 2025.02.24 env-cmd란? (1) 2024.12.06 react-hook-form을 사용하여 배열 형태 input 사용 (0) 2024.12.02 타입스크립트 예외처리 (0) 2024.11.22 Swiper - Cannot read properties of undefined (reading 'autoplay') (1) 2024.11.07