본 포스팅에서는 파이썬 차트 스타일 적용하는 방법에 대해서 알아 보려고 합니다. Python-pptx 패키지를 활용하여 차트를 그리고 스타일을 적용하는 방법에 대해 소개를 하려고 하는데요, python으로 차트를 그리는 방법을 다루는 블로그들은 많지만 디자인 변경까지 알려주는 튜토리얼은 찾기 힘들었던 기억이 있는데요, 차트를 좀더 예쁘게 그려보고 싶은 분들께 오늘 글이 도움이 되었으면 좋겠습니다.
파이썬 차트 스타일 적용하기 차트 그리기
준비물 리스트
위 글에서는 차트를 ppt에서 그리지 않고 그림 파일로 만들어져 있는 차트(charts 폴더에 있는 이미지들)를 삽입하는 방법을 소개했는데, 이번에는 좀더 심화된 버전으로 ppt 차트를 직접 그리는 방법을 다루어보겠습니다.
결과물(우측상단 차트) 예시
먼저 Apple의 연도별 브랜드가치를 담은 차트를 먼저 만들어본 후 지난 포스트에서 처럼 100대 브랜드에 대한 슬라이드를 모두 만들어보겠습니다.
[Apple의 연도별 브랜드가치]
2018 | 2019 | 2020 | 2021 | 2022 |
$ 214,480M | $ 234,241M | $ 322,999M | $ 408,251M | $ 482,215M |
가장 기본적인 형태의 line chart를 그리는 방법은 다음과 같습니다.
from pptx import Presentation
from pptx.chart.data import ChartData
from pptx.enum.chart import XL_CHART_TYPE
from pptx.util import Cm
prs = Presentation()
# 레이아웃 이름이 한글로 '빈 화면'으로 설정되어있는 경우는 'Blank' 대신 '빈 화면'으로 작성하면 됩니다.
blank_slide_layout = prs.slide_layouts.get_by_name('Blank')
slide = prs.slides.add_slide(blank_slide_layout)
brand = 'Apple'
values_by_year = [214480, 234241,322999,408251,482215]
chart_data = ChartData()
# x축 데이터입니다.
chart_data.categories = [x for x in range(2018,2023)]
# y축 데이터입니다.
chart_data.add_series(brand, values_by_year)
chart = slide.shapes.add_chart(XL_CHART_TYPE.LINE,
x = Cm(2), # 좌상단기준 가로 위치
y = Cm(1), # 좌상단기준 세로 위치
cx = Cm(21), # 넓이
cy = Cm(17), # 높이
chart_data = chart_data).chart
prs.save('test.pptx')
큼직하게 그려봤는데요, 맨위의 예시처럼 하나하나 바꿔보겠습니다.
먼저 타이틀과 범례를 지웁니다.
chart.has_title = False
chart.has_legend = False
Y축(value_axis)에 삐죽 튀어나온 틱마크를 지워줍니다.
from pptx.enum.chart import XL_TICK_MARK
value_axis = chart.value_axis
value_axis.major_tick_mark = XL_TICK_MARK.NONE
# 틱마크는 CROSS, INSIDE, NONE, OUTSIDE 네가지 중 선택할 수 있습니다.
가로 세로축 레이블의 폰트, 사이즈, 서식 등을 바꾸어줍니다.
from pptx.util import Pt
# value_axis : 브랜드 가치 값이 들어있는 y축
value_axis.tick_labels.font.name = '맑은 고딕' #시스템에 설치된 폰트명을 그대로 입력해줍니다.
value_axis.tick_labels.font.size = Pt(10)
value_axis.tick_labels.number_format= '#,###"m"' # 엑셀등에서 쓰는 사용자 서식을 그대로 입력해줍니다.
# category_axis : 연도 값이 들어있는 x축
category_axis = chart.category_axis
category_axis.tick_labels.font.name = '맑은 고딕'
category_axis.tick_labels.font.size = Pt(10)
주 눈금선을 회색 점선으로 바꾸어줍니다.
from pptx.enum.dml import MSO_LINE
from pptx.dml.color import RGBColor
value_axis.major_gridlines.format.line.dash_style = MSO_LINE.DASH
value_axis.major_gridlines.format.line.color.rgb = RGBColor(127, 127, 127)
# 점선 스타일은 아래 유형들이 있습니다.
# DASH
# DASH_DOT
# DASH_DOT_DOT
# LONG_DASH
# LONG_DASH_DOT
# ROUND_DOT
# SOLID
# SQUARE_DOT
# DASH_STYLE_MIXED
라인 색상과 굵기를 조정하고 표식을 삽입해주겠습니다.
from pptx.enum.chart import XL_MARKER_STYLE
plot = chart.plots[0]
series = plot.series[0]
line = series.format.line
line.width = Pt(1.5)
line.color.rgb = RGBColor(0, 173, 181)
marker = series.marker
marker.style = XL_MARKER_STYLE.CIRCLE # 표식 종류 설정
marker_fill = marker.format.fill # 표식 채우기
marker_fill.solid() # 단색 채우기
marker_fill.fore_color.rgb = RGBColor(255,255,255) # RBG로 색상 지정
marker_line= marker.format.line # 표식 테두리
marker_line.fill.solid() # 실선
marker_line.fill.fore_color.rgb = RGBColor(0,173,181) # RBG로 색상 지정
marker_line.width = Pt(1.5) # 표식 테두리 굵기
세로축의 범위(최대, 최소) 및 주눈금선의 단위를 지정해주겠습니다.
value_axis.maximum_scale = 500000
value_axis.minimum_scale = 200000
value_axis.major_unit = 50000
이렇게 직접 최대, 최소값 및 단위를 입력할 수도 있지만 Apple 외 다른 브랜드의 차트를 그릴때에는 해당 브랜드 가치에 맞게 최대 최소값을 설정해주어야 하는데요, 5개년 브랜드가치의 최대, 최소값을 먼저 찾고 거기에 가장 가까운 십만단위수를 (또는 만단위, 천단위 등등)를 축의 Range로 지정하는 식으로 코드를 짜보겠습니다.
import math
min_value = min(values_by_year)
# 가장 작은 값인 214480가 리턴됩니다.
min_digits = 10 ** (len(str(min_value))-1)
# (len(str(min_value))을 통해 min_value가 6자리라는 것을 알 수 있고,
# min_digits는 10의 5승 (10 ** 5) 즉 100000이 됩니다.
min_scale = math.floor(min_value/min_digits)*min_digits
# 214480에 가장 가까운 100000자리수 200000이 리턴됩니다.
# math.floor은 버림하는 함수입니다.
max_value = max(values_by_year)
min_digits = 10 ** (len(str(min_value))-1)
max_scale = math.ceil(max_value/min_digits)*min_digits
# 같은 방식으로 max_scale(세로축 최대값)이 500000으로 정해집니다.
axis_unit = int((max_scale - min_scale) / 5)
axis_digit = 10**(len(str(axis_unit))-1)
axis_unit = math.floor(axis_unit/axis_digit)*axis_digit
# axis_unit(주눈금선 단위)는 축의 최대값과 최소값의 차이를 5정도로 나누고
# 나누어진 값을 천, 만, 십만단위로 변환하여 정합니다.
value_axis.minimum_scale = min_scale
value_axis.maximum_scale = max_scale
value_axis.major_unit = axis_unit
지금까지의 코드를 모두 종합하면 아래와 같습니다.
from pptx import Presentation
from pptx.chart.data import ChartData
from pptx.enum.chart import XL_CHART_TYPE
from pptx.util import Cm
from pptx.enum.chart import XL_TICK_MARK
from pptx.util import Pt
from pptx.enum.dml import MSO_LINE
from pptx.dml.color import RGBColor
from pptx.enum.chart import XL_MARKER_STYLE
import math
prs = Presentation()
blank_slide_layout = prs.slide_layouts.get_by_name('Blank')
slide = prs.slides.add_slide(blank_slide_layout)
brand = 'Apple'
values_by_year = [214480, 234241,322999,408251,482215]
chart_data = ChartData()
# x축 데이터입니다.
chart_data.categories = [x for x in range(2018,2023)]
# y축 데이터입니다.
chart_data.add_series(brand, values_by_year)
chart = slide.shapes.add_chart(XL_CHART_TYPE.LINE,
x = Cm(2), # 좌상단기준 가로 위치
y = Cm(1), # 좌상단기준 세로 위치
cx = Cm(21), # 넓이
cy = Cm(17), # 높이
chart_data = chart_data).chart
chart.has_title = False
chart.has_legend = False
value_axis = chart.value_axis
value_axis.major_tick_mark = XL_TICK_MARK.NONE
value_axis.tick_labels.font.name = '맑은 고딕'
value_axis.tick_labels.font.size = Pt(10)
value_axis.tick_labels.number_format= '#,###"m"'
category_axis = chart.category_axis
category_axis.tick_labels.font.name = '맑은 고딕'
category_axis.tick_labels.font.size = Pt(10)
value_axis.major_gridlines.format.line.dash_style = MSO_LINE.DASH
value_axis.major_gridlines.format.line.color.rgb = RGBColor(127, 127, 127)
plot = chart.plots[0]
series = plot.series[0]
line = series.format.line
line.width = Pt(1.5)
line.color.rgb = RGBColor(0, 173, 181)
marker = series.marker
marker.style = XL_MARKER_STYLE.CIRCLE
marker_fill = marker.format.fill
marker_fill.solid()
marker_fill.fore_color.rgb = RGBColor(255,255,255)
marker_line= marker.format.line
marker_line.fill.solid()
marker_line.fill.fore_color.rgb = RGBColor(0,173,181)
marker_line.width = Pt(1.5)
min_value = min(values_by_year)
min_digits = 10 ** (len(str(min_value))-1)
min_scale = math.floor(min_value/min_digits)*min_digits
max_value = max(values_by_year)
min_digits = 10 ** (len(str(min_value))-1)
max_scale = math.ceil(max_value/min_digits)*min_digits
axis_unit = int((max_scale - min_scale) / 5)
axis_digit = 10**(len(str(axis_unit))-1)
axis_unit = math.floor(axis_unit/axis_digit)*axis_digit
value_axis.minimum_scale = min_scale
value_axis.maximum_scale = max_scale
value_axis.major_unit = axis_unit
prs.save('test.pptx')
지난 글에서 소개했던 코드를 합치면 아래와 같고, 다음과 같은 PPT파일이 생성됩니다.
from pptx import Presentation from pptx.util import Cm import pandas as pd import os import copy from pptx.dml.color import RGBColor from pptx.chart.data import ChartData ## from pptx.enum.chart import XL_CHART_TYPE ## from pptx.util import Pt # import math # from pptx.enum.chart import XL_MARKER_STYLE # from pptx.enum.dml import MSO_LINE # from pptx.enum.chart import XL_TICK_MARK # prs = Presentation('Best Global Brands 2022_interbrand_template.pptx') slide = prs.slides[0] df = pd.read_excel("Best Global Brands 2022_interbrand.xlsx") cwd = os.getcwd() logos = [file for file in os.listdir(f"{cwd}\logos") if os.path.isfile(f"{cwd}\logos\{file}")] charts = [file for file in os.listdir(f"{cwd}\charts") if os.path.isfile(f"{cwd}\charts\{file}")] check = f"{cwd}\images\check.png" def draw_chart(chart_data, slide, chrt_type, x, y, width, height, min_scale, max_scale, value_axis_unit): chart = slide.shapes.add_chart(chrt_type, x, y, width, height, chart_data).chart chart.has_title = False chart.has_legend = False value_axis = chart.value_axis value_axis.major_tick_mark = XL_TICK_MARK.NONE value_axis.tick_labels.font.name = '맑은 고딕' value_axis.tick_labels.font.size = Pt(10) value_axis.tick_labels.number_format= '#,###"m"' category_axis = chart.category_axis category_axis.tick_labels.font.name = '맑은 고딕' category_axis.tick_labels.font.size = Pt(10) value_axis.major_gridlines.format.line.dash_style = MSO_LINE.DASH plot = chart.plots[0] series = plot.series[0] line = series.format.line line.width = Pt(1.5) line.color.rgb = RGBColor(0, 173, 181) marker = series.marker marker.style = XL_MARKER_STYLE.CIRCLE marker_fill = marker.format.fill marker_fill.solid() marker_fill.fore_color.rgb = RGBColor(255,255,255) marker_line= marker.format.line marker_line.fill.solid() marker_line.fill.fore_color.rgb = RGBColor(0,173,181) marker_line.width = Pt(1.5) value_axis.maximum_scale = max_scale value_axis.minimum_scale = min_scale value_axis.major_unit = value_axis_unit return chart def select_shape_by_text(slide, text): for x in slide.shapes: if x.has_text_frame and x.text == text: return x print('요청한 Shape를 찾을 수 없습니다.') def select_table_by_text(slide, text): for x in slide.shapes: if x.has_table and x.table.cell(0,0).text == text: return x.table print('요청한 Shape를 찾을 수 없습니다.') def copy_slide(prs, index): template = prs.slides[index] try: blank_slide_layout = prs.slide_layouts.get_by_name('빈 화면') except: blank_slide_layout = prs.slide_layouts[0] copied_slide = prs.slides.add_slide(blank_slide_layout) for shape in template.shapes: elem = shape.element new_elem = copy.deepcopy(elem) copied_slide.shapes._spTree.insert_element_before(new_elem, 'p:extLst') return copied_slide for i, r in df.iterrows(): copied_slide = copy_slide(prs, 0) brand_name = select_shape_by_text(copied_slide, 'Brand name').text_frame p = brand_name.paragraphs[0] run = p.runs[0] run.text = r['브랜드명'] comment = select_shape_by_text(copied_slide, 'comment').text_frame p = comment.paragraphs[0] run = p.runs[0] run.text = r['코멘트'] table = select_table_by_text(copied_slide, 'Rank') rank = table.cell(0,1).text_frame p = rank.paragraphs[0] run = p.runs[0] run.text = str(r['순위']) value = table.cell(1,1).text_frame p = value.paragraphs[0] run = p.runs[0] run.text = f"{r['브랜드가치']:,} $m" growth = table.cell(2,1).text_frame p = growth.paragraphs[0] run = p.runs[0] run.text = f"{r['성장률']:.0%}" copied_slide.shapes.add_picture(check, Cm(18.05), Cm(4.05), width=None, height=None) # 그림파일로 저장된 차트를 삽입할때 사용했던 코드입니다. # chart = [x for x in charts if r['브랜드명'] in x][0] # chart = f"{cwd}\charts\{chart}" # copied_slide.shapes.add_picture(chart, Cm(16.93), Cm(2.77), width=Cm(16), height=Cm(8)) chart_data = ChartData() chart_data.categories = [x for x in range(2018,2023)] # 연도별 브랜드 가치가 nan인 경우가 있어서 그때는 데이터를 None으로 처리합니다. values = [int(r[str(x)]) if pd.notna(r[str(x)]) else None for x in range(2018,2023)] chart_data.add_series(r['브랜드명'], values) # data값이 None이 아닌 값들 중에서만 min값을 찾습니다. min_scale = min([x for x in values if pd.notna(x)]) min_digits = 10**(len(str(min_scale))-1) min_scale = math.floor(min_scale/min_digits)*min_digits # data값이 None이 아닌 값들 중에서만 max값을 찾습니다. max_scale = max([x for x in values if pd.notna(x)]) max_scale = math.ceil(max_scale/min_digits)*min_digits axis_unit = int((max_scale - min_scale) / 5) axis_digit = 10**(len(str(axis_unit))-1) axis_unit = math.floor(axis_unit/axis_digit)*axis_digit chart = draw_chart(chart_data, copied_slide, XL_CHART_TYPE.LINE, Cm(18.05), Cm(4.81), Cm(12.73), Cm(5.13), min_scale, max_scale, axis_unit) logo = [x for x in logos if r['브랜드명'] in x][0] logo = f"{cwd}\logos\{logo}" inserted_logo = copied_slide.shapes.add_picture(logo, Cm(2.38), Cm(4.81)) if inserted_logo.height>Cm(2): inserted_logo.width = int(inserted_logo.width * (Cm(2)/inserted_logo.height)) inserted_logo.height = Cm(2) if inserted_logo.width>Cm(6): inserted_logo.height = int(inserted_logo.height * (Cm(6)/inserted_logo.width)) inserted_logo.width = Cm(6) if inserted_logo.height < Cm(2) : inserted_logo.top = int(Cm(4.81) + (Cm(2)-inserted_logo.height)*0.5) prs.save('Best Global Brands 2022_interbrand.pptx')
최종 완성된 결과물