Highcharts 라이브러리 활용 후기(feat. python)

손병진·2020년 10월 4일
0

wecode

목록 보기
25/27
post-custom-banner

결과물

미니프로젝트 개요

  • 추석 연휴동안 개인적으로 진행하였고, 코인원 암호화폐 거래 사이트의 웹디자인을 모티브로 하여 레이아웃을 구성하였습니다.
  • 최소한의 목표는 올해 초에 배웠던 데이터 분석 python을 활용하여 javascript에 적용해보고 그래프를 구현하는 것이었습니다.

데이터 출저
kaggle S&P 500 stock data

  • 캐글에서 5년간의 S&P 지수 데이터를 받았고, 15-16년도 부분만 잘라내서 활용하였습니다.
  • csv 파일을 가공하여 json 형식으로 변환하였고, mockData 처럼 직접적인 호출 방식으로 사용하였습니다.

주요 개념(Highcharts 제외)

함수형 컴포넌트

  • 왜 클래쓰 대신에 함수형 컴포넌트를 사용하는가?
    1. 클래쓰는 많은 기능을 한번에 묶는건 성공했지만 각각을 따로 쓰기는 힘들다.
    2. 클래쓰에서 state 혹은 상속할 때 매번 같은 형식이어서 코드를 파악하기 쉽지 않다.
    3. 기조 클래쓰 구조에 불변성 개념이 도입되면 더욱 복잡해진다.

HOOKS

  • useState & useEffect
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import "./Main.scss";

const Main = () => {
  // useState(초기값) = [초기값, 값을 통제하는 함수]
  // 비구조화 할당 (배열, 객체)
  const [value, setValue] = useState({
    id: 0,
    number: 0,
  });

  const { id, number } = value;
  // 이렇게 썼지만 useState로 모든 변수를 한번에 관리하는 것보다
  // 변하는 값끼리 묶어서

  // state 값을 갱신하는 함수
  // 기존값(...value)에 변하는 key, value 값을 새롭게!!! 추가한다.
  // 이런 식으로 해야 변화를 인식할 수 있다고 한다.
  // useState 다룰 때에는 두번째 파라미터로 콜백함수를 넣을 수 없다고 한다.
  const plusNum = () => {
    setValue({
      ...value,
      number: number + 1,
    });
  };

  // render 직후에 실행되는데 콜백함수로 [] 넣어주면 처음에만 실행된다(=컴디마)
  useEffect(() => {
    console.log("렌더링!");
  }, []);

  // render 직후에 실행되는데 콜백함수로 [변수] 넣어주면
  // 해당 변수가 변했을 때에만 실행된다(=컴디업, 비동기처리)
  useEffect(() => {
    console.log(number);
  }, [number]);

  useEffect(() => {
    console.log(id);
  }, [id]);

  return (
    <Flex>
      <div className="Main">
        <Border>
          <div>Main</div>
          <div>
            <span>{number}만큼 담대하게</span>
          </div>
          <button name={id} onClick={plusNum}>
            담대버튼
          </button>
        </Border>
      </div>
    </Flex>
  );
};

export default Main;

Styled Component

  • props
  • state 값에 따라 css 속성을 변화시킬 때 props 기능을 활용할 수 있다.
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import menuData from "./menuData";
import "./Nav.scss";

// 네이게이션바 js
const Nav = () => {
  const [menu, changeMenu] = useState(menuData);

  const { menuContents, menuValid } = menu;

  const clickMenu = (idx) => {
    let handleMenuDeatail = menuValid;
    handleMenuDeatail[idx] = !handleMenuDeatail[idx];
    changeMenu({
      ...menu,
      menuValid: handleMenuDeatail,
    });
  };

  return (
    ...

// 'menu' state값내에 있는 menuDetail과 menuValid를 props로 전달
  <MenuDetailBox
    menuDatil={menuDetail}
    validIdx={menuValid[idx]}
    >
    <ul className="menuDetail">
      {menuDetail.map((menuDetail) => {
        return <li>{menuDetail}</li>;
      })}
    </ul>
  </MenuDetailBox>
    ...
  );
};

// props 활용하여 조건문을 생성할 수 있다
const MenuDetailBox = styled.ul`
  display: ${(props) => (props.menuDatil.length === 0 ? "none" : null)};
  border: 1px solid lightgray;
  border-radius: 10px;
  padding: 20px;
  background-color: white;
  opacity: ${(props) => (props.validIdx ? 1 : 0)};
  li {
    padding: 10px;
    &:hover {
      cursor: ${(props) => props.validIdx && "pointer"};
      background-color: rgb(229, 249, 255);
    }
  }
`;

export default Nav;

Python

  • 5년짜리 데이터 중에서 활용하는 주가 데이터만 남기고,
    15-16년도 자료만 뽑아내어 종목별로 묶어준다.

데이터 가공 코드

import pandas as pd

# 먼저 csv파일을 불러온다.
df = pd.read_csv('c:/data/all_stocks_5yr.csv')

# 쓰지 않을 데이터를 잘라내준다.
df.drop(['open', 'high', 'low', 'volume'], axis='columns', inplace=True)

# 15-16년도 데이터만 남긴다.
idx = df[(df['date'] < '2015-01-01') | (df['date'] >='2017-01-01')].index
re_df = df.drop(idx)
re_df.reset_index(drop=True, inplace=True) # 리인덱싱
re_df.drop(['date'], axis='columns', inplace=True) # 날짜 데이터는 js에서 넣어준다.
re_df = re_df.rename({"close":"value"}, axis="columns") # 컬럼 이름 변경

# 종목 이름을 기준으로 주가 데이터를 배열로 묶어준다.
re_df_binding = re_df.groupby('Name')['value'].apply(list)
re_df_binding_df = pd.DataFrame(re_df_binding)

#  json 파일로 저장해준다.
re_df_binding_df.to_json("c:/data/stockSeriesData.json", orient="table")

데이터 리뷰

before(all_stocks_5yr.csv)

after(stockSeriesData.json)

  • 여기서 data key값 만을 추출하여 사용한다.
{
  "schema": {
    "fields":[
      {"name":"Name","type":"string"},
      {"name":"value","type":"string"}],
    "primaryKey":["Name"],
    "pandas_version":"0.20.0"}, 
  "data": [
    {
      "Name":"A",
      "value":[40.56,39.8,39.18,39.7,40.89,40.59,40.11,39.55,39.06,38.01,38.25,37.93,38.16,
               39.65,38.81,39.15,38.75,38.0,38.46,37.77,38.69,39.62,39.11,39.53,39.34,39.04,
               39.67,39.33,40.02,40.15,40.52,41.54,41.15,41.95,41.73,42.06,42.2,42.36,42.21,
               42.7,42.26,42.0,42.22,41.53,41.74,40.63,40.85,41.1,40.87,41.81,41.58,42.12,
               42.21,42.21,42.2,41.09,40.81,40.7,41.11,41.72,41.55,41.39,42.05,41.93,42.44,
               ...]
     },
     {
      "Name":"ATS",
      "value":[...]
               },
    {
      "Name":"ZWS",
      "value":[...]
               },
    ...]
}

Highcharts

  • 먼저 두개의 컴포넌트가 필요하다
    1. Chart.js : highcharts 라이브러리가 import되어 그래프를 만드는 컴포넌트
    2. Graph.js : 해당 그래프가 삽입되는 컴포넌트
    • 추가적으로 차트에 들어갈 데이터 형식을 json 파일로 만들어 놓았다.

chart 만드는 컴포넌트

// Chart.js
import React, { Component } from "react";
import Highcharts from "highcharts";

class Chart extends Component {
  constructor(props) {
    super(props);
    this.chartContainer = React.createRef();
  }

  componentDidMount() {
    this.chart = new Highcharts[this.props.type || "Chart"](
      this.chartContainer.current,
      this.props.options
    );
  }

  // 그래프 데이터를 갱신할 때에는 state 관리하는 것 이외에 componentDidUpdate 함수를 선언해주어야 한다.
  componentDidUpdate() {
    if (this.props.allowChartUpdate !== false) {
      this.chart.update(
        this.props.options,
        ...(this.props.updateArgs || [true, true])
      );
    }
  }

  componentWillUnmount() {
    this.chart.destroy();
  }

  render() {
    return <div ref={this.chartContainer} />;
  }
}

export default Chart;

chart 삽입되는 컴포넌트

// Graph.js
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import Chart from "./Chart";
import "./Graph.scss";

const Graph = () => {
  
  // 인덱스 값이 바뀜에 따라 데이터가 변하도록 세팅하였다.
  const [chartData, updateData] = useState({});
  const [stockIdx, plusIdx] = useState(1);

  // 데이터의 형식을 가져온다.
  const ClickEvent = () => {
    fetch("/data/stockData.json")
//  아래 주석이 stockData.json 하이차트에서 인식하는 데이터의 형식이다.
//  {
//   "stock": [
//     {
//       "title": { "text": "S&P 500 stock data (15'~16`)" },
//       "xAxis": {
//         "type": "datetime",
//         "dateTimeLabelFormats": {
//           "day": "%d %b"
//         },
//         "labels": {
//           "enabled": true
//         }
//       },
//       "chart": {
//         "type": "line"
//       },
//       "series": []
//     }
//   ]
// }
      .then((res) => res.json())
      .then((res) => {
        updateData(res.stock[0]);
      });
  };

  // 주가 데이터 내용을 가져온다.
  const getStockData = (idx) => {
    console.log("success");
    fetch("/data/stockSeriesData.json")
      .then((res) => res.json())
      .then((res) => {
        updateData({
          ...chartData,
          
          // pointStart : x축 날짜의 시작점을 지정해준다.
          // pointInterval : 데이터간의 간격이 얼마만큼인지 설정해준다(데이터에 영향을 주지 않고 x축 표기에 영향을 준다).
          series: [
            {
              data: res.data[idx].value,
              pointStart: Date.UTC(2015, 1, 1),
              pointInterval: 24 * 3600 * 1000 * 1,
              name: res.data[idx].Name,
            },
          ],
        });
      });
  };

  const changeData = () => {
    plusIdx(stockIdx + 1);
    getStockData(stockIdx);
  };

  useEffect(() => {
    ClickEvent();
    getStockData(0);
  }, []);

  useEffect(() => {
    console.log(chartData);
  });

  return (
    <div className="Graph">
      <GraphContainer>
        <div className="graphContainer">
    
    	  // 하이차트 들어갈 위치
          <Chart options={chartData} />
        </div>
      </GraphContainer>
      <Container>
        <div className="stocksContainer">
          <button onClick={changeData}>ChageStockData</button>
        </div>
      </Container>
    </div>
  );
};

export default Graph;

profile
https://castie.tistory.com
post-custom-banner

0개의 댓글