-
Notifications
You must be signed in to change notification settings - Fork 0
[Chart] Pie chart #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[Chart] Pie chart #258
Changes from all commits
bb9fa5a
d555c3b
3cb5400
79a908f
fd18166
e3708e0
ec7352e
078625b
e66d7c6
8a7e471
7bf478f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import React from 'react' | ||
| import _ from 'lodash' | ||
| import styled from 'styled-components' | ||
|
|
||
| const WrapLegendBox = styled.div` | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 100%; | ||
| margin-top: 8px; | ||
| ` | ||
|
|
||
| const WrapLegendItem = styled.div` | ||
| display: flex; | ||
|
|
||
| & + & { | ||
| margin-top: 4px; | ||
| } | ||
| ` | ||
|
|
||
| const WrapKey = styled.div.attrs({ | ||
| className: 'body_02_m', | ||
| })` | ||
| display: flex; | ||
| align-items: center; | ||
| width: 5rem; | ||
| color: #6c7a89; | ||
|
|
||
| &::before { | ||
| content: ''; | ||
| display: inline-block; | ||
| width: 0.5rem; | ||
| height: 0.5rem; | ||
| margin-right: 6px; | ||
| border-radius: 50%; | ||
| background-color: ${({ color }) => color}; | ||
| } | ||
| ` | ||
|
|
||
| const WrapValue = styled.span.attrs({ | ||
| className: 'subtitle_01', | ||
| })` | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 5rem; | ||
| color: #3b424a; | ||
| ` | ||
|
|
||
| const Index = ({ data, colors }) => { | ||
| const totalCount = _.reduce(data, (acc, cur) => (acc += cur.value), 0) | ||
|
|
||
| return ( | ||
| <WrapLegendBox id="WrapLegendBox"> | ||
| {_.map(data, ({ key, value }, idx) => ( | ||
| <WrapLegendItem key={`WrapLegendItem__${key}`}> | ||
| <WrapKey color={colors[idx]}>{key}</WrapKey> | ||
| <WrapValue>{value || 0}</WrapValue> | ||
| <WrapValue>{((value * 100) / totalCount).toFixed(2)}%</WrapValue> | ||
| </WrapLegendItem> | ||
| ))} | ||
| </WrapLegendBox> | ||
| ) | ||
| } | ||
|
|
||
| export default Index | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| ### Usage | ||
|
|
||
| #### Props | ||
|
|
||
| | Props Name | Types | Default | | ||
| | :--------: | :---: | :-----: | | ||
|
|
||
| #### Basic PieChart | ||
|
|
||
| ```js | ||
| import React, { useState } from 'react' | ||
| import PieChart from './PieChart' | ||
|
|
||
| const render = () => { | ||
| return ( | ||
| <div style={{ width: 350, height: 350 }}> | ||
| <PieChart | ||
| data={[ | ||
| { key: 'Male', value: 230 }, | ||
| { key: 'Female', value: 450 }, | ||
| ]} | ||
| colors={['#5A88D8', ' #ee807c']} | ||
| /> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| render() | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| import React, { useEffect } from 'react' | ||
| import _ from 'lodash' | ||
| import * as d3 from 'd3' | ||
| import styled from 'styled-components' | ||
|
|
||
| import Legend from './Legend' | ||
|
|
||
| interface Data { | ||
| key: string | ||
| value: number | ||
| } | ||
|
|
||
| interface PieStyleObj { | ||
| width?: number | ||
| height?: number | ||
| margin?: number | ||
| radius?: number | ||
| innerRadius?: number | ||
| hoverStorkeWidth?: number | ||
| hoverStrokeColor?: number | ||
| } | ||
|
|
||
| interface DurationObj { | ||
| init?: number | ||
| hover?: number | ||
| } | ||
|
|
||
| interface TooltipObj { | ||
| x?: number | ||
| y?: number | ||
| } | ||
|
|
||
| interface PieChartProps { | ||
| data?: Data[] | ||
| colors?: string[] | ||
| style?: PieStyleObj | ||
| duration?: DurationObj | ||
| tooltip?: TooltipObj | ||
| id?: string | ||
| } | ||
|
|
||
| const PieChart = ({ | ||
| // props 논의 필요. 임의 지정. | ||
| data, | ||
| colors, | ||
| style, | ||
| duration, | ||
| tooltip, | ||
| id = _.uniqueId('pie_chart'), | ||
| }: PieChartProps) => { | ||
| const colorSizeDiff = _.size(data) - _.size(colors) | ||
| if (colorSizeDiff !== 0) { | ||
| _.forEach(_.range(colorSizeDiff + 1), () => | ||
| colors.push(`#${Math.round(Math.random() * 0xffffff).toString(16)}`), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p4. 랜덤색상 말고 고정적인 default color 가 있는게 더 좋지 않을까요? |
||
| ) | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| // selector | ||
| const chartBox = d3.select(`#${id}`) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p5. select 코드는 useEffect 바깥으로 빼도 괜찮지 않을까요 ..? |
||
| const svg = chartBox.select('.svg') | ||
| const tooltipBox = chartBox.select('.tooltip-box') | ||
| const tooltipSircle = chartBox.select('.tooltip-sircle') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p5. sircle 은 어떤 의미로 사용된건가요? circle 의 오타인가 하여 남깁니다. |
||
| const tooltipTextBox = chartBox.select('.tooltip-text-box') | ||
|
|
||
| // default value | ||
| const SVG_WIDTH = svg.node().getBoundingClientRect().width - 16 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p5. 여기서 - 16 은 무엇을 의미할까요 ..?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p4. 16은 어떤값인가요? 상수화할 수 없을까요? |
||
| const SVG_HEIGHT = svg.node().getBoundingClientRect().height | ||
| const MARGIN = 8 | ||
| const INIT_DURATION_MS = 600 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p5. 상수도 바깥으로 빼도 괜찮을 것 같습니다 ~ |
||
| const HOVER_DURATION_MS = 300 | ||
| const TOOLTIP_DISTANCE_X = 30 | ||
| const TOOLTIP_DISTANCE_Y = 0 | ||
| const HOVER_STROKE_WIDTH = 1.5 | ||
| const HOVER_STROKE_COLOR = '#24292d' | ||
|
|
||
| const { | ||
| width: svgWidth = SVG_WIDTH, | ||
| height: svgHeight = SVG_HEIGHT, | ||
| margin = MARGIN, | ||
| radius = Math.min(svgWidth, svgHeight) / 2 - margin, | ||
| innerRadius = radius / 2, | ||
| hoverStorkeWidth = HOVER_STROKE_WIDTH, | ||
| hoverStrokeColor = HOVER_STROKE_COLOR, | ||
| } = style || {} | ||
|
|
||
| const { | ||
| init: initDuration = INIT_DURATION_MS, | ||
| hover: hoverDuration = HOVER_DURATION_MS, | ||
| } = duration || {} | ||
|
|
||
| const { | ||
| x: tooltipDistanceX = TOOLTIP_DISTANCE_X, | ||
| y: tooltipDistanceY = TOOLTIP_DISTANCE_Y, | ||
| } = tooltip || {} | ||
|
|
||
| // event fucntion | ||
| const mouseover = (e, { data, index }) => { | ||
| d3.select(e.target) | ||
| .transition() | ||
| .duration(hoverDuration) | ||
| .attr('stroke-width', hoverStorkeWidth) | ||
| .style('stroke', hoverStrokeColor) | ||
|
|
||
| tooltipBox.style('visibility', 'visible') | ||
| tooltipSircle.style('background-color', colors[index]) | ||
| tooltipTextBox | ||
| .append('div') | ||
| .style('margin-right', '0.5rem') | ||
| .text(`${data.key} :`) | ||
| tooltipTextBox.append('div').text(`${data.value}`) | ||
|
Comment on lines
+110
to
+111
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3. tooltip 내용을 커스텀할 수 있는 방법은 없을까요? |
||
| } | ||
|
|
||
| const mousemove = (e) => { | ||
| tooltipBox | ||
| .style('left', `${e.pageX + tooltipDistanceX}px`) | ||
| .style('top', `${e.pageY + tooltipDistanceY}px`) | ||
| } | ||
|
|
||
| const mouseleave = ({ target }) => { | ||
| d3.select(target).transition().duration(300).attr('stroke-width', 0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p5. durtaion(300)에서 300도 상수처리하면 좋을 것 같습니다 ~ |
||
| tooltipBox.style('visibility', 'hidden') | ||
| tooltipTextBox.selectAll('*').remove() | ||
| } | ||
|
|
||
| // default chart method | ||
| const pie = d3 | ||
| .pie() | ||
| .value((d) => d.value) | ||
| .sort(null) | ||
|
|
||
| const arc = d3.arc().outerRadius(radius).innerRadius(innerRadius) | ||
|
|
||
| const interpolate = d3.interpolate(pie.startAngle()(), pie.endAngle()()) | ||
|
|
||
| const chart = svg | ||
| .attr('width', svgWidth) | ||
| .attr('height', svgHeight) | ||
| .append('g') | ||
| .attr('transform', `translate(${svgWidth / 2},${svgHeight / 2})`) | ||
|
|
||
| chart | ||
| .selectAll('arc') | ||
| .data(pie(data)) | ||
| .enter() | ||
| .append('path') | ||
| .attr('d', arc) | ||
| .attr('fill', (d) => colors[d.index]) | ||
| .on('mouseover', mouseover) | ||
| .on('mousemove', mousemove) | ||
| .on('mouseleave', mouseleave) | ||
| .transition() | ||
| .duration(initDuration) | ||
| .attrTween('d', (d) => { | ||
| const originalEnd = d.endAngle | ||
| return (t) => { | ||
| const currentAngle = interpolate(t) | ||
| if (currentAngle < d.startAngle) return '' | ||
| d.endAngle = Math.min(currentAngle, originalEnd) | ||
| return arc(d) | ||
| } | ||
| }) | ||
| }, [JSON.stringify(data)]) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p5. 다른 props(colors, style, duration, tooltip 등)은 안들어가도 될까요
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p5. JSON.stringify(data)가 좋은 방법이 아니어서 이리저리 다르게 시도하고 있는데ㅜ 나중에는 대체되어야 할 것 같아요.. |
||
|
|
||
| return ( | ||
| <WrapChartBox id={id}> | ||
| <WrapSvg className="svg" /> | ||
| <Legend data={data} colors={colors} /> | ||
| <WrapTooltip className="tooltip-box"> | ||
| <WrapSircle className="tooltip-sircle" /> | ||
| <WrapTextBox className="tooltip-text-box" /> | ||
| </WrapTooltip> | ||
| </WrapChartBox> | ||
| ) | ||
| } | ||
|
|
||
| const WrapChartBox = styled.div` | ||
| display: flex; | ||
| flex-direction: column; | ||
| width: 100%; | ||
| height: 100%; | ||
| ` | ||
|
|
||
| const WrapSvg = styled.svg` | ||
| width: 100%; | ||
| height: 100%; | ||
| ` | ||
|
|
||
| const WrapTooltip = styled.div` | ||
| visibility: hidden; | ||
| display: flex; | ||
| align-items: center; | ||
| position: absolute; | ||
| min-width: 5rem; | ||
| width: max-content; | ||
| padding: 12px; | ||
| background-color: rgba(255, 255, 255, 0.95); | ||
| box-shadow: 0 3px 24px 0 rgba(36, 41, 45, 0.24); | ||
| border-radius: 0.3rem; | ||
| opacity: 1; | ||
| ` | ||
|
|
||
| const WrapSircle = styled.div` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p2. WrapCircle로 수정해야할 것 같습니다 ~ |
||
| width: 0.5rem; | ||
| height: 0.5rem; | ||
| margin-right: 8px; | ||
| border-radius: 50%; | ||
| ` | ||
|
|
||
| const WrapTextBox = styled.div` | ||
| display: flex; | ||
| width: 100%; | ||
| font-size: 1rem; | ||
| ` | ||
|
|
||
| export default PieChart | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
p3. legend 에 value를 노출 시킬 필요가 있을까요? 기본적으로 이름을 노출하고 필요하다면 legend 를 custom 할 수 있도록 옵션을 전달할 수 있으면 좋겠습니다.