개발자
리액트에서 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 봇
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 위치 및 크기 등에 대해 조절하면서 문제를 해결해 나갈 필요가 있어 보입니다.
지금 가입하면 모든 질문의 답변을 볼 수 있어요!