개발자

d3.js 사용 시 레이블 텍스트 표시

2023년 12월 14일조회 275

리액트에서 d3.js 도넛 차트를 구현하고자 합니다. 하단 "g.append('text')" 이하 부분에서 레이블과 퍼센티지(%)를 차트 중심부에 표시되도록 설정했는데, arc 면적이 작을 경우, 레이블과 퍼센티지 전부 표시되지 않습니다. 어떻게 수정해야 할까요? (현재 해결되었습니다. 수정 후 정상적으로 표시되는 이미지도 같이 보여드립니다.)

1import React, { useEffect } from 'react';
2import * as d3 from 'd3';
3
4interface DonutChartProps {
5  data: {
6    pass: number;
7    fail: number;
8    notApplicable: number;
9  };
10  style?: React.CSSProperties;
11}
12
13const DonutChart: React.FC<DonutChartProps> = ({ data, style }) => {
14  useEffect(() => {
15   
16    d3.select('#chart-container').selectAll('*').remove();
17
18    const width = 280;
19    const height = 400;
20    const radius = Math.min(width, height) / 2;
21
22    const colorScheme = ['#006FFD', '#ED3241', '#E86339'];
23    const color = d3.scaleOrdinal<string>().range(colorScheme);
24
25    const arc = d3.arc<d3.PieArcDatum<{ category: string; count: number }>>()
26      .innerRadius(radius - 100)
27      .outerRadius(radius - 20);
28
29    const pie = d3.pie<{ category: string; count: number }>().value(d => d.count);
30
31    const svg = d3.select('#chart-container')
32      .append('svg')
33      .attr('width', width)
34      .attr('height', height)
35      .append('g')
36      .attr('transform', `translate(${width / 2},${height / 2})`);
37
38    const labelMap: Record<string, string> = {
39      pass: 'PASS',
40      fail: 'FAIL',
41      notApplicable: 'N/A',
42    };
43
44    const g = svg.selectAll('.arc')
45      .data(pie(Object.entries(data).map(([category, count]) => ({ category, count }))))
46      .enter().append('g')
47      .attr('class', 'arc');
48
49    g.append('path')
50      .attr('d', arc)
51      .style('fill', d => color(d.data.category));
52
53    
54
55    g.append('text')
56      .filter(d => d.data.count > 0) 
57      .attr('transform', d => `translate(${arc.centroid(d)})`)
58      .attr('dy', '-0.5em')
59      .style('text-anchor', 'middle')
60      .style('fill', 'white')
61      .html(d => {
62        const categoryLabel = labelMap[d.data.category];
63        const percentage = Math.round((d.data.count / Object.values(data).reduce((acc, val) => acc + val, 0)) * 100).toString();
64        const lines = `${categoryLabel}\n${percentage}%`.split('\n');
65        return lines.map((line, index) => (
66          `<tspan x="0" dy="${index === 0 ? '0' : '1.2em'}">${line}</tspan>`
67        )).join('');
68      });
69  }, [data]);
70
71  return <div id="chart-container" style={style}></div>;
72};
73
74export default DonutChart;
75
76
77/ *********** /
78   const result = {
79        pass: 15,
80        fail: 110,
81        notApplicable: 1
82      };
83      
84 <DetailDonutChart data={result}/>
이 질문이 도움이 되었나요?
'추천해요' 버튼을 누르면 좋은 질문이 더 많은 사람에게 노출될 수 있어요. '보충이 필요해요' 버튼을 누르면 질문자에게 질문 내용 보충을 요청하는 알림이 가요.

답변 2

삭제된 사용자님의 프로필 사진

삭제된 사용자

2023년 12월 14일

안녕하세요. d3.js를 써보진 않아서 좋은 해결책일지는 모르겠으나 레이블과 퍼센티지의 z-index를 높여서 차트 위에 덮어지게끔 그려보시는건 어떨까요?

그린티라떼님의 프로필 사진

그린티라떼

작성자

프론트엔드 개발자2023년 12월 14일

안녕하세요. 저도 z-index 속성을 지원하는지 확인해 보았는데 제공하지 않는것 같습니다. Arc의 영역이 일정 범위 이하일때 레이블과 퍼센티지를 바깥에 표시하고 꺾은선으로 영역을 연결하는 방법으로 진행해야 할것 같습니다.

그린티라떼님의 프로필 사진

그린티라떼

작성자

프론트엔드 개발자2023년 12월 15일

안녕하세요. GPT로 코드 수정을 해보았는데, 일부를 변경하여 적용하니 정상적으로 표시되었습니다. 다만, 레이블과 퍼센티지에 배치에 관한 것이 아니라 데이터 계산 방법만 변경된 것 같아서 유의미한 차이는 못느끼겠습니다.

그린티라떼님의 프로필 사진

chat GPT를 활용해서 코드를 다음과 같이 수정하였습니다. const percentage = ((d.data.count / Object.values(data).reduce((acc, val) => acc + val, 0)) * 100).toFixed(0).toString(); 부분이 const percentage = Math.round((d.data.count / Object.values(data).reduce((acc, val) => acc + val, 0)) * 100).toString(); 로 바뀌었습니다. 수정코드로 적용하니 정상적으로 표시가 되었습니다.

1import React, { useEffect, useRef } from 'react';
2import * as d3 from 'd3';
3
4interface DeviceInfo {
5  device_manuf: string;
6  device_memory: number;
7  device_os: number;
8}
9
10interface DonutChartProps {
11  data: DeviceInfo[];
12  type: keyof DeviceInfo;
13}
14
15const DonutChart: React.FC<DonutChartProps> = ({ data, type }) => {
16  const chartContainerRef = useRef<HTMLDivElement>(null);
17
18  
19  useEffect(() => {
20
21      if (!data || data.length === 0) {
22      return;
23  }
24
25    d3.select(chartContainerRef.current).selectAll('*').remove();
26
27    const categoryCounts: Record<string, number> = data.reduce((acc: any, device) => {
28      acc[device[type]] = (acc[device[type]] || 0) + 1;
29      return acc;
30    }, {});
31
32    const sortedCategories = Object.keys(categoryCounts).sort((a, b) => categoryCounts[b] - categoryCounts[a]);
33    const topCategories = sortedCategories.slice(0, 4);
34
35    const totalCount = sortedCategories.reduce((sum, category) => sum + categoryCounts[category], 0);
36
37    const dataForChart = topCategories.map(category => ({ category, count: categoryCounts[category] }));
38    const etcCount = totalCount - dataForChart.reduce((sum, entry) => sum + entry.count, 0);
39
40
41    if (sortedCategories.length > 4) {
42      dataForChart.push({ category: 'ETC', count: etcCount });
43    }
44
45    const width = 280;
46    const height = 400;
47    const radius = Math.min(width, height) / 2;
48
49    const colorScheme = ['#006FFD', '#ED3241', '#E86339', '#298267', '#C5C6CC'];
50
51    const color = d3.scaleOrdinal<string>().range(colorScheme);
52
53    const chartTitle =
54      type === 'device_manuf' ? '디바이스 제조사' : type === 'device_memory' ? '디바이스 램용량' : '디바이스 OS 버전';
55
56    const arc = d3.arc<d3.PieArcDatum<{ category: string; count: number }>>()
57      .innerRadius(radius - 100)
58      .outerRadius(radius - 20);
59
60    const pie = d3.pie<{ category: string; count: number }>().value(d => d.count);
61
62    const svg = d3.select(chartContainerRef.current)
63      .append('svg')
64      .attr('width', width)
65      .attr('height', height)
66      .append('g')
67      .attr('transform', `translate(${width / 2},${height / 2})`);
68
69    svg.append('text')
70      .attr('x', 0)
71      .attr('y', -height / 2 + 40)
72      .text(chartTitle)
73      .style('font-size', '16px')
74      .style('font-weight', '900')
75      .style('text-anchor', 'middle')
76      .style('fill', 'black');
77
78    const g = svg.selectAll('.arc')
79      .data(pie(dataForChart))
80      .enter()
81      .append('g')
82      .attr('class', 'arc');
83
84    g.append('path')
85      .attr('d', arc)
86      .style('fill', d => color(d.data.category));
87
88    g.append('text')
89      .attr('transform', d => `translate(${arc.centroid(d)})`)
90      .attr('dy', '-0.5em')
91      .style('text-anchor', 'middle')
92      .style('fill', 'white')
93      .html(d => {
94        const lines = `${d.data.category}\n${Math.round((d.data.count / totalCount) * 100)}%`.split('\n');
95        return lines
96          .map((line, index) => `<tspan x="0" dy="${index === 0 ? '0' : '1.2em'}">${line}</tspan>`)
97          .join('');
98      });
99  }, [data, type]);
100
101  return <div ref={chartContainerRef}></div>;
102};
103
104export default DonutChart;

지금 가입하면 모든 질문의 답변을 볼 수 있어요!

현직자들의 명쾌한 답변을 얻을 수 있어요.

또는

이미 회원이신가요?

AI 답변
커리어리 AI 봇 프로필 이미지

커리어리 AI 봇

GPT가 달아주는 답변입니다.

2023년 12월 14일

레이블과 퍼센티지가 표시되지 않는 문제는 arc 면적이 작아 음영으로 인해 가려지거나 svg의 뷰포트를 벗어나기 때문일 수 있습니다. 1. **데이터 필터링 적절하게 이루어졌는지 확인**: `filter` 함수를 통해 `count > 0`인 데이터만 선택하여 처리하고 있는데, 이 부분이 올바르게 동작하는지 확인하세요. 만약에 문제가 있다면, 걸러져야 할 항목들이 차트에 그려질 수 있습니다. 2. **SVG 뷰포트 벗어나는 문제**: SVG 요소의 넓이와 높이를 조정하여 해결할 수 있습니다. 현재 설정된 넓이와 높이보다 좀 더 넓게 설정해서 모든 요소들이 화면에 다 보일 수 있도록 해보세요. 3. **Arc 크기 조정**: arc의 innerRadius나 outerRadius를 조절하여 각 pie slice의 크기를 조정할 수 있습니다. 크기를 충분히 크게 하여 각각의 pie slice에 text가 잘 보일 수 있도록 하세요. 4. **Text 위치 조정**: 현재 arc 중심부로 translate 되도록 설정되어 있는데, 아마도 이 부분에서 일부 텍스트가 보여지지 않을 가능성이 있습니다. 텍스트 위치를 조금씩 이동시켜서 가장 잘 보이는 위치를 찾아보세요. 5. **Text 크기 조정**: 라벨의 글자 크기가 너무 커서 보이지 않는 경우도 있을 것입니다. 이 경우, 텍스트의 'font-size'를 조절하여 해결할 수 있습니다. 따라서 현재 코드에서는 SVG의 뷰포트, arc 크기, text 위치 및 크기 등에 대해 조절하면서 문제를 해결해 나갈 필요가 있어 보입니다.

목록으로

지금 가입하면 모든 질문의 답변을 볼 수 있어요!