ABOUT ME

웹개발과 일상에 대해 포스팅하는 블로그입니다.

Today
Yesterday
Total
  • 8. 통계 화면 - 바 그래프로 보여주기
    프로젝트/할일목록 앱 (RN) 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
Designed by Tistory.