프로젝트/할일목록 앱 (RN)

8. 통계 화면 - 바 그래프로 보여주기

syleemomo 2023. 10. 16. 10:32
728x90

* 통계화면을 보여주기 위한 라이브러리 설치하기

{
  "name": "learnreactnative",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "lint": "eslint .",
    "start": "react-native start",
    "test": "jest"
  },
  "dependencies": {
    "@react-native-firebase/app": "^18.4.0",
    "@react-native-firebase/auth": "^18.4.0",
    "@react-native-firebase/firestore": "^18.4.0",
    "@react-native-firebase/storage": "^18.4.0",
    "@react-navigation/bottom-tabs": "^6.5.8",
    "@react-navigation/native": "^6.1.7",
    "@react-navigation/native-stack": "^6.9.13",
    "chart.js": "^4.4.0",
    "faker": "^6.6.6",
    "moment": "^2.29.4",
    "react": "18.2.0",
    "react-chartjs-2": "^5.2.0",
    "react-native": "0.72.4",
    "react-native-gifted-charts": "^1.3.11",
    "react-native-linear-gradient": "^2.8.3",
    "react-native-safe-area-context": "^4.7.1",
    "react-native-screens": "^3.24.0",
    "react-native-svg": "^13.14.0",
    "react-native-vector-icons": "^10.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@babel/preset-env": "^7.20.0",
    "@babel/runtime": "^7.20.0",
    "@react-native/eslint-config": "^0.72.2",
    "@react-native/metro-config": "^0.72.11",
    "@tsconfig/react-native": "^3.0.0",
    "@types/react": "^18.0.24",
    "@types/react-test-renderer": "^18.0.0",
    "babel-jest": "^29.2.1",
    "eslint": "^8.19.0",
    "jest": "^29.2.1",
    "metro-react-native-babel-preset": "0.76.8",
    "prettier": "^2.4.1",
    "react-test-renderer": "18.2.0",
    "typescript": "4.8.4"
  },
  "engines": {
    "node": ">=16"
  }
}
"chart.js": "^4.4.0",
"faker": "^6.6.6",
"react-chartjs-2": "^5.2.0",
"react-native-gifted-charts": "^1.3.11", // 사용
"react-native-linear-gradient": "^2.8.3", // 사용
"react-native-svg": "^13.14.0",

해당 라이브러리를 추가로 설치하였다. 바 그래프를 위하여 사용할 라이브러리는 아래 링크를 참고하기 바란다. chart.js, faker, react-chartjs-2 는 현재 사용하지 않는 라이브러리다. 또한, react-native-linear-gradient 라이브러리는 react-native-gifted-charts 라이브러에서 사용되는 의존성 라이브러리다.

https://gifted-charts.web.app/

 

Gifted Charts

 

gifted-charts.web.app

App 컴포넌트에서 아래와 같이 코드를 수정한다. 대쉬보드 화면에서 통계분석을 위하여 전체 할일목록을 사용할 것이므로 todos 속성으로 해당 화면에 데이터를 전달해준다. 

<Tab.Screen name="DashBoard" children={(props) => <DashBoardSceen todos={todos}/>} options={{
  title: '통계',
  tabBarIcon: ({ color, size }) => <Icon name="dashboard" color={color} size={size}/>
}}/>

 

* 대쉬보드 화면 구현하기

import { max } from 'moment'
import React, { useRef } from 'react'
import { SafeAreaView, View, Text, StyleSheet, StatusBar } from 'react-native'
import { BarChart } from "react-native-gifted-charts"

function DashBoardSceen({navigation, todos}){
  console.log("할일목록 - 통계: ", todos.length)
  const groupedByCategory = todos.reduce((groupedByCategory, todo) => {
    if(!groupedByCategory[todo.category]) groupedByCategory[todo.category] = 0
    groupedByCategory[todo.category]++
    return groupedByCategory  
  }, {})
  console.log(groupedByCategory)
  const groupedByStatus = todos.reduce((groupedByStatus, todo) => {
    const propName = `${todo.isDone? "완료" : "진행중"}`
    if(!groupedByStatus[propName]) groupedByStatus[propName] = 0
    groupedByStatus[propName]++
    return groupedByStatus  
  }, {})
  console.log(groupedByStatus)

  const data = []
  let maxValue = -Infinity
  for(let category in groupedByCategory){
    if(maxValue < groupedByCategory[category]) maxValue = groupedByCategory[category]
    data.push({value: groupedByCategory[category], frontColor: '#006DFF', gradientColor: '#009FFF', spacing: 6, label: category})
  }

  const noOfSections = 10
  const yAxisLabelTexts = Array(noOfSections).fill(0).map((_, id) => {
    console.log("아이디 -------", id)
    let unit = id * maxValue/noOfSections 
    // let unit = id * maxValue/noOfSections * 1000
    // let unit = id * maxValue/noOfSections * 1000000
    if(unit > 1000000) unit = (unit / 1000000).toString() + 'M'
    else if(unit > 1000) unit = (unit / 1000).toString() + 'K'
    
    return unit
  })
  console.log(yAxisLabelTexts)
  return (
    <SafeAreaView style={styles.block}>
      <StatusBar backgroundColor="#a8c8ffff"></StatusBar>
      <View style={styles.graphBg}>
        <View style={styles.itemBg}>
          <View><Text style={styles.statusText}>진행중</Text></View><View style={styles.badge}><Text style={styles.badgeText}>{groupedByStatus["진행중"]}</Text></View>
        </View>
        <View style={styles.itemBg}>
          <View><Text style={styles.statusText}>완료</Text></View><View style={styles.badge}><Text style={styles.badgeText}>{groupedByStatus["완료"]}</Text></View>
        </View>
      </View>
      <View style={styles.graphBg}>
      <Text style={{color: 'white', fontSize: 16, fontWeight: 'bold'}}>
        카테고리별 할일목록 수
      </Text>
      <View style={{padding: 20, alignItems: 'center'}}>
        <BarChart
          isAnimated
          data={data}
          barWidth={30}
          initialSpacing={10}
          spacing={30}
          barBorderRadius={7}
          showGradient
          yAxisThickness={0}
          xAxisType={'dashed'}
          xAxisColor={'lightgray'}
          yAxisTextStyle={{color: 'lightgray'}}
          stepValue={1}
          maxValue={maxValue}
          noOfSections={noOfSections}
          yAxisLabelTexts={yAxisLabelTexts}
          labelWidth={40}
          xAxisLabelTextStyle={{color: 'lightgray', textAlign: 'center'}}
          lineConfig={{
            color: '#F29C6E',
            thickness: 3,
            curved: true,
            hideDataPoints: true,
            shiftY: 20,
            initialSpacing: -30,
          }}
        />
      </View>
    </View>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  block: {
    flex: 1
  },
  graphBg: {
    margin: 10,
    padding: 16,
    borderRadius: 20,
    backgroundColor: '#232B5D',
  },
  itemBg: {
    margin: 10,
    padding: 16,
    borderRadius: 20,
    backgroundColor: '#262d3d',
    flexDirection: 'row',
    justifyContent: 'flex-end',
    alignItems: 'center'
  },
  statusText: { 
    color: 'lightgray', 
    fontWeight: 'bold', 
    fontSize: 20, 
    marginRight: 10
  },
  badge: { 
    backgroundColor: '#385499', 
    borderRadius: 50, 
    padding: 10, 
    width: 50, height: 50, 
    justifyContent: 'center', 
    alignItems: 'center'
  },
  badgeText: {
    fontSize: 20,
    color: 'lightgray'
  }
})
export default DashBoardSceen

screens > DashBoardScreen.js 파일을 위와 같이 수정한다.

import { max } from 'moment'

왜 추가되었는지 잘 모르겠다. 아마 maxValue 변수를 추가할때 비주얼편집기에서 자동으로 추가한것 같다.

React, { useRef } from 'react'

useRef 도 현재 사용하지 않는데 왜 추가되었는지 모르겠다.

import { BarChart } from "react-native-gifted-charts"

바 그래프를 그리기 위하여 설치한 라이브러리에서 BarChart 컴포넌트를 임포트한다.

function DashBoardSceen({navigation, todos}){
	// 중략
}

대쉬보드 화면에서 전체 할일목록이 필요하므로 todos 를 props 로 전달받는다.

const groupedByCategory = todos.reduce((groupedByCategory, todo) => {
    if(!groupedByCategory[todo.category]) groupedByCategory[todo.category] = 0
    groupedByCategory[todo.category]++
    return groupedByCategory  
  }, {})
console.log(groupedByCategory)

할일목록을 카테고리별로 분류하기 위하여 위와 같이 작성한다. reduce 메서드는 배열을 하나의 값(현재는 객체)으로 만들어준다. 맨 처음에 groupedByCategory 는 빈 객체({})로 시작한다. reduce 는 todos 배열에서 각각의 할일정보를 추출한 다음 카테고리별로 분류한다. 분류에 대한 로직은 자바스크립트 배열 시간에 설명하였으므로 자세한 설명은 생략하도록 한다. 

const groupedByStatus = todos.reduce((groupedByStatus, todo) => {
    const propName = `${todo.isDone? "완료" : "진행중"}`
    if(!groupedByStatus[propName]) groupedByStatus[propName] = 0
    groupedByStatus[propName]++
    return groupedByStatus  
}, {})
console.log(groupedByStatus)

할일목록을 진행상태에 따라 분류하는 코드이다. 카테고리별 분류와 동일한 로직을 사용한다. 다만 todo 의 isDone 속성은 true 나 false 값이므로 프로퍼티 이름을 "완료" 나 "진행중" 으로 변경한 다음 분류를 수행한다.

const data = []
let maxValue = -Infinity
for(let category in groupedByCategory){
if(maxValue < groupedByCategory[category]) maxValue = groupedByCategory[category]
data.push({value: groupedByCategory[category], frontColor: '#006DFF', gradientColor: '#009FFF', spacing: 6, label: category})
}

카테고리별로 분류된 객체(groupedByCategory)는 라이브러리에 그대로 전달해주지 못한다. 해당 라이브러리에 데이터를 전달하기 위해서는 객체를 엘리먼트로 가지는 배열 형태가 되어야 한다. 정확한 데이터 형식은 라이브러리 문서를 참고하기 바란다. 그래서 빈 배열을 생성한 다음 반복문을 돌면서 객체를 추가한다. 객체의 value 속성은 바 그래프에서 보여줄 숫자(바 그래프의 길이)를 의미한다. spacing 은 바 그래프 사이의 간격이다. label 은 카테고리 이름이다. maxValue 는 바 그래프에서 바(bar)가 그래프 밖으로 삐져나오지 않도록 최대값을 바 그래프의 전체높이로 설정하기 위함이다. 

const noOfSections = 10

noOfSections 는 바 그래프에서 행(줄)의 갯수이다.

const yAxisLabelTexts = Array(noOfSections).fill(0).map((_, id) => {
    console.log("아이디 -------", id)
    let unit = id * maxValue/noOfSections 
    // let unit = id * maxValue/noOfSections * 1000 // K 확인용 테스트 코드
    // let unit = id * maxValue/noOfSections * 1000000 // M 확인용 테스트 코드
    if(unit > 1000000) unit = (unit / 1000000).toString() + 'M'
    else if(unit > 1000) unit = (unit / 1000).toString() + 'K'
    
    return unit
  })
  console.log(yAxisLabelTexts)

yAxisLabelTexts 변수는 Y 축에 보여줄 라벨(행의 단위)의 리스트를 배열로 저장한다. 행의 갯수가 10개라면 라벨도 10개가 필요하므로 noOfSections 변수를 이용하여 0 으로 초기화된 10개의 배열을 생성한다. map 메서드를 이용하여 각 엘리먼트의 id 값을 조회하고, id 값에 maxValue/noOfSections 를 곱하여 라벨(단위)을 생성한다. 예를 들어 10개의 라벨이 필요하면 일단 id 값은 0 ~ 9 가 된다. 만약 전체 데이터에서 최대값(maxValue)이 100 이라면 바 그래프의 높이는 100 이 된다. 100 을 기준으로 행의 갯수가 10 이라면 한 행의 간격은 10 이 된다. 즉, maxValue / noOfSections 가 된다. 결과적으로 Y 축에 보여줄 단위는 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 이므로 id 값(0 ~ 9)에 위에서 구한 간격을 곱해주면 된다. 

단위(unit)는 문자열이고 toString 메서드를 이용하여 문자열로 변환해준다. unit 값이 1000000 을 넘어가면 1M 이므로 1000000 으로 나눈 다음 "M"을 붙여준다. unit 값이 1000 을 넘어가면 1K 이므로 1000으로 나눈 다음 "K"를 붙여준다.

<View style={styles.graphBg}>
        <View style={styles.itemBg}>
          <View><Text style={styles.statusText}>진행중</Text></View><View style={styles.badge}><Text style={styles.badgeText}>{groupedByStatus["진행중"]}</Text></View>
        </View>
        <View style={styles.itemBg}>
          <View><Text style={styles.statusText}>완료</Text></View><View style={styles.badge}><Text style={styles.badgeText}>{groupedByStatus["완료"]}</Text></View>
        </View>
      </View>
      <View style={styles.graphBg}>
      <Text style={{color: 'white', fontSize: 16, fontWeight: 'bold'}}>
        카테고리별 할일목록 수
      </Text>
      <View style={{padding: 20, alignItems: 'center'}}>
        <BarChart
          isAnimated
          data={data}
          barWidth={30}
          initialSpacing={10}
          spacing={30}
          barBorderRadius={7}
          showGradient
          yAxisThickness={0}
          xAxisType={'dashed'}
          xAxisColor={'lightgray'}
          yAxisTextStyle={{color: 'lightgray'}}
          stepValue={1}
          maxValue={maxValue}
          noOfSections={noOfSections}
          yAxisLabelTexts={yAxisLabelTexts}
          labelWidth={40}
          xAxisLabelTextStyle={{color: 'lightgray', textAlign: 'center'}}
          lineConfig={{
            color: '#F29C6E',
            thickness: 3,
            curved: true,
            hideDataPoints: true,
            shiftY: 20,
            initialSpacing: -30,
          }}
        />
      </View>

통계 데이터를 화면에 보여주는 코드이다. 

<View style={styles.graphBg}>
    <View style={styles.itemBg}>
      <View><Text style={styles.statusText}>진행중</Text></View><View style={styles.badge}><Text style={styles.badgeText}>{groupedByStatus["진행중"]}</Text></View>
    </View>
    <View style={styles.itemBg}>
      <View><Text style={styles.statusText}>완료</Text></View><View style={styles.badge}><Text style={styles.badgeText}>{groupedByStatus["완료"]}</Text></View>
    </View>
  </View>

전체 할일목록에서 할일의 진행상황을 분류해서 화면에 보여준다.

<View style={styles.graphBg}>
      <Text style={{color: 'white', fontSize: 16, fontWeight: 'bold'}}>
        카테고리별 할일목록 수
      </Text>
      <View style={{padding: 20, alignItems: 'center'}}>
        <BarChart
          isAnimated          // 애니메이션 적용
          data={data}         // 그래프에 사용되는 배열 데이터
          barWidth={30}       // 바 그래프 너비
          initialSpacing={10} 
          spacing={30}
          barBorderRadius={7} // 바 그래프 모서리 
          showGradient
          yAxisThickness={0}
          xAxisType={'dashed'}
          xAxisColor={'lightgray'}
          yAxisTextStyle={{color: 'lightgray'}}
          stepValue={1}
          maxValue={maxValue}  // 바 그래프 최대값 설정
          noOfSections={noOfSections} // 바 그래프 행의 갯수
          yAxisLabelTexts={yAxisLabelTexts} // 바 그래프 Y축 라벨 리스트
          labelWidth={40} // 라벨 간격
          xAxisLabelTextStyle={{color: 'lightgray', textAlign: 'center'}} // 바 그래프 X축 라벨 스타일
          lineConfig={{
            color: '#F29C6E',
            thickness: 3,
            curved: true,
            hideDataPoints: true,
            shiftY: 20,
            initialSpacing: -30,
          }}
        />
      </View>
</View>

라이브러리에서 임포트한 BarChart 컴포넌트를 이용하여 카테고리별 할일목록의 갯수를 화면에 바 그래프로 보여준다.  

const styles = StyleSheet.create({
  block: {
    flex: 1
  },
  graphBg: {        // 통계분석 블럭 배경화면
    margin: 10,
    padding: 16,
    borderRadius: 20,
    backgroundColor: '#232B5D',
  },
  itemBg: {     // 진행상태가 보이는 부분의 배경
    margin: 10,
    padding: 16,
    borderRadius: 20,
    backgroundColor: '#262d3d',
    flexDirection: 'row',
    justifyContent: 'flex-end',
    alignItems: 'center'
  },
  statusText: {  // 진행상태 문자열 스타일
    color: 'lightgray', 
    fontWeight: 'bold', 
    fontSize: 20, 
    marginRight: 10
  },
  badge: {   // 각 진행상태에 대한 갯수를 표시하는 배지 스타일 
    backgroundColor: '#385499', 
    borderRadius: 50, 
    padding: 10, 
    width: 50, height: 50, 
    justifyContent: 'center', 
    alignItems: 'center'
  },
  badgeText: { // 배지 텍스트 스타일
    fontSize: 20,
    color: 'lightgray'
  }
})

진행상태와 바 그래프에 대한 전체적인 스타일 코드이다. 

대쉬보드 화면

 

728x90