Home Python을 활용한 파이차트 이미지 생성
Post
Cancel

Python을 활용한 파이차트 이미지 생성

Python을 활용한 파이 차트 이미지 생성

이번 글은 python에서 파이 차트를 만들어 js에서 이미지로 보여주는 과정을 정리한 글이다.

AC_ 20250123-014953



1. DB 연결하기

환경변수 설정

환경 변수 파일(.env)을 활용하여 DB 연결 정보를 보호한다.

DB_TYPE=mysql
DB_DRIVER=pymysql
DB_HOST=127.0.0.1
DB_USER=root
DB_PASSWORD=@
DB_NAME=wow
DB_PORT=3300

127.0.0.1은 로컬 컴퓨터를 가리키는 루프백 주소로 localhost와 동일하다.



환경변수 로드

dotenv 패키지를 사용해 .env 파일의 내용을 불러온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
from dotenv import load_dotenv
import os

# .env 파일 로드
load_dotenv()

db_type = os.getenv("DB_TYPE")
db_driver = os.getenv("DB_DRIVER")
db_host = os.getenv("DB_HOST")
db_user = os.getenv("DB_USER")
db_password = os.getenv("DB_PASSWORD")
db_name = os.getenv("DB_NAME")
db_port = int(os.getenv("DB_PORT")) # port should be of type int   




PyMySQL로 DB 조회하기

PyMySQL은 MySQL을 Python에서 사용할 수 있도록 하는 라이브러리다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import pymysql

# DB 연결 설정  
conn = pymysql.connect(
    host=db_host,
    user=db_user,
    password=db_password,
    db=db_name,
    port=db_port,
    charset="utf8"
)

cursor = conn.cursor() # 튜플 형식으로 반환
'''
# DATA DICTRIONARY로 변환 시)
cursor = conn.cursor(pymysql.cursors.DictCursor)

# 출력
[{'id': 1, 'username': 'username', 'password': 'pw'}]
'''

# SQL 실행
username = "username"  
sql = "SELECT * FROM user WHERE username = %s"
cursor.execute(sql, (username,))

# 결과 가져오기
result = cursor.fetchall()
print(result) # ((1, 'username', 'pw'),)

# 연결 닫기
cursor.close()
conn.close()
  • connect : DB 연결 객체를 생성

  • cursor : SQL 쿼리를 실행하거나 결과를 가져온다.

  • execute : SQL 쿼리를 실행

    • execute(sql, params) 형식으로 호출하며 파라미터를 튜플 형태로 전달한다.
  • fetchall : 실행한 쿼리의 결과를 모두 가져온다.

  • close : 연결 종료





pandas로 DB 조회하기

pandas.read_sql은 SQL 쿼리를 실행하고 결과를 바로 DataFrame으로 반환한다.

*SQLAlchemy: Java의 JPA와 같이 Python의 ORM 라이브러리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from sqlalchemy import create_engine
from urllib.parse import quote_plus
import pandas as pd

# 비밀번호 인코딩
encoded_password = quote_plus(db_password)

# SQLAlchemy 엔진 생성(mysql+pymysql://username:password@host:port/database_name)
engine = create_engine(
    f"{db_type}+{db_driver}://{db_user}:{encoded_password}@{db_host}:{db_port}/{db_name}"
)

'''
# session 사용할 경우
from sqlalchemy.orm import sessionmaker

# SessionLocal 세션 클래스를 생성
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

- autocommit=Fasle: 자동커밋 비활성화
- autoflush=False: 자동 flush 비활성 
  - fk 문제는 true로 설정해 flush를 통해 트랜잭션 순서를 보장
- bind로 create_engine으로 생성한 db 엔진을 바인딩
'''

# pandas.read_sql 사용
query = "SELECT user.id FROM user WHERE username = %s"
username = "hello"
df = pd.read_sql_query(query, con=engine, params=(username,))

print(df)
'''
   id
0   1
'''

create_engine 참고 옵션

  • pool_size : 연결할 수 있는 connection의 크기를 지정

  • pool_recycle: 주어진 초 이후에 connection을 재사용 (ex.pool_recycle=600 : 600초 후 재사용)

  • echo=True : query문 출력


*mysql의 경우 일정 시간동안 connection이 없을 경우 connection을 끊어버리게 되는데

pool_recycle을 설정함으로써 강제로 끊어지는 현상을 막을 수가 있다.

참고로 pool_recycle 시간이 mysql의 wait_timeout 시간보다 작게 설정되어야 한다.



*여러번의 쿼리(INSERT, UPDATE 등)을 수행할 때는 sessionmaker를 적용하지만

여기서는 조회 한번 후 더 이상의 연결이 필요없어서 적용하지 않았다.



pd.read_sqlpd.read_sql_query의 차이를 찾아봤을 때

pd.read_sql은 표준 쿼리에 더 적합한 반면

pd.read_sql_query는 사용자 지정 조건이 있는 복잡한 쿼리를 실행하는 데 더 적합하다.



SQLAlchemy는 파이썬의 대표적인 ORM(Object-Relational Mapping) 라이브러리로

데이터베이스와의 상호작용을 객체 지향적으로 처리할 수 있게 해준다.

ORM의 특징인 SQL 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있으며

코드의 가독성과 유지보수성을 크게 향상시킬 수 있다.

SQLAlchemy에서 트랜잭션 관리는 Session 객체를 통해 수행한다.

Java는 @Transactional으로 관리하듯 Python은 session으로 관리하는 것 같다.




오류

(pymysql.err.OperationalError)
(2003, “Can’t connect to MySQL server on ‘@127.0.0.1’ ([Errno 11003] getaddrinfo failed)”)

engine을 출력해보고 오류의 원인을 찾았다. 비번이 @로 설정되면서 생긴 문제였다.

engine에서 출력할 때 Engine(mysql+pymysql://root:***@127.0.0.1:3306/db_name)와 같이 출력되어야 하는데

Engine(mysql+pymysql://root:***@@127.0.0.1:3306/db_name)으로 출력되고 있었다.

따라서 quote_plus()를 통해 해결했다.





파이 차트 생성하기

DB에서 가져온 데이터를 활용해 matplotlib를 사용하여 파이 차트를 생성했다.

동적 쿼리를 작성할 때 %s를 통해 구현했다.

1
2
@Query("select new com.eng.dto.LevelResponseDto(c.level, count(c.level)) from Sentence c where c.id in (select s.sentence.id from Study s where s.user.id = :userId) group by c.level")
List<LevelResponseDto> findLevelByUserId(Long userId);

🔽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 결과 출력
user_id = df["id"][0]

query = ("select s.level, count(s.level) cnt "
         "from sentence s "
         "join study st on s.id = st.sentence_id "
         "where st.user_id = %s "
         "group by s.level")

df = pd.read_sql(query, con=engine, params=(user_id,))

# level 별 cnt 값 가져오기 
level_0 = df[df['level'] == 0]['cnt'].iloc[0] # .iloc[0] : 첫 번째 값을 가져온다.    
level_1 = df[df['level'] == 1]['cnt'].iloc[0]
level_2 = df[df['level'] == 2]['cnt'].iloc[0]

참고)

  • Study 테이블에서 UNIQUE user_id (user_id, word_id, meaning_id, sentence_id)로 설정을 해 해당 조합은 중복이 되지않는다.

  • count()는 중복을 포함해서 중복을 제거하고 싶다면 DISTINCT를 적용하면 된다.




1
2
3
4
5
6
7
8
9
10
11
import matplotlib.pyplot as plt

ratio = [level_0, level_1, level_2]
labels = ['Easy', 'Medium', 'Hard']
colors = ['#FF9999', '#FFCC99', '#99CCFF'] # 색상 지정 
explode = [0.03, 0.03, 0.03]

plt.pie(ratio, labels=labels, autopct='%.1f%%',
        startangle=90, counterclock=False,
        colors=colors, explode=explode, shadow=True)
plt.show()
  • autopct : 부채꼴 안에 표시될 숫자의 형식을 지정
    • → 소수점 한자리까지 표시하도록 설정
  • startangle : 부채꼴이 그려지는 시작 각도를 설정(default : 0도)
    • counterclock=False로 설정하면 시계 방향 순서로 부채꼴 영역이 표시 (true : 반시계방향)
  • explode : 부채꼴이 파이 차트의 중심에서 벗어나는 정도를 설정
    • 3% 만큼 벗어나도록 설정
  • shadow = True : 파이 차트에 그림자가 표시

image



이미지 저장 및 반환

matplotlib.pyplot 모듈의 savefig() 함수를 사용해서 그래프를 이미지 파일 등으로 저장한다.

1
2
3
4
5
6
7
8
9
10
import io
from flask import send_file
import matplotlib.pyplot as plt

# 이미지 저장 및 반환
img = io.BytesIO()
plt.savefig(img, format='png')
img.seek(0)  # 메모리 포인터를 처음으로 이동
plt.close()  # 그래프 닫기
return send_file(img, mimetype='image/png')
  • format : 저장할 이미지 형식




Flask로 이미지 전송

Flask에서는 RequestParam을 통해서 작성할 떄 request.args.get('username')로 값을 받으면 되지만

PathVariable을 사용할 때는 /<username>와 같이 <>를 활용해야한다.

1
2
3
4
5
6
7
8
from flask_cors import CORS
from src.py_mysql import levelPie

CORS(app, resources={r"/my-page/*": {"origins": "*"}})

@app.route('/my-page/level/image/<username>', methods=['GET'])
def level_pie_image(username):
    return levelPie(username)

본 프로젝트의 메인은 java+spring이기 때문에 cors에러가 뜬다.

따라서 해당 요청사항의 url만 cors를 허용했다.




Error

RuntimeError: main thread is not in main loop

→ Agg 백엔드 적용하기

1
2
import matplotlib
matplotlib.use('Agg')  # GUI 백엔드 비활성화 

matplotlib는 기본적으로 TkAgg 백엔드를 사용하며 GUI 기반이다. (TkAgg, FltkAgg, GTK, GTKAgg, GTKCairo, Wx)

대부분의 GUI 백엔드는 메인 스레드에서 실행해야한다.

GUI가 없는 환경에서 실행하는 경우 에러가 발생한다. 따라서 GUI를 사용하지 않는 백엔드로 전환한다.




js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function levelPieIMG(){
    const url = `http://127.0.0.1:5000/my-page/level/image/${username}`; // Flask 서버 URL
    $.ajax({
        type: "GET",
        url: url,
        xhrFields: {
            responseType: 'blob'  // 이미지 데이터를 Blob 형식으로 요청 
        },
        success: function (response) {
            // Blob URL 생성
            const imgBlob = new Blob([response], { type: 'image/png' });
            const imgURL = URL.createObjectURL(imgBlob);

            // 새 창 열기
            const newWindow = window.open("", "_blank", "width=640,height=480");
            const imgElement = newWindow.document.createElement("img");
            imgElement.src = imgURL;
            imgElement.alt = "Level Pie Chart";
            newWindow.document.body.appendChild(imgElement);
        },
        error: function (err) {
            console.error("이미지를 로드하는 중 오류가 발생했습니다.");
        }
    })
}






REFERENCE
SQLAlchemy

인코딩

read_sql

matplotlib

RuntimeError: main thread is not in main loop

이미지 저장 및 띄우기

This post is licensed under CC BY 4.0 by the author.