try:
from pptx.chart.data import CategoryChartData, XyChartData
from pptx.enum.chart import XL_CHART_TYPE
from pptx.util import Pt
except ImportError:
CategoryChartData = None
XyChartData = None
XL_CHART_TYPE = None
CHART_TYPES = {
"bar": XL_CHART_TYPE.COLUMN_CLUSTERED,
"line": XL_CHART_TYPE.LINE_MARKERS,
"pie": XL_CHART_TYPE.PIE,
"stacked_bar": XL_CHART_TYPE.COLUMN_STACKED,
"stacked_bar_pct": XL_CHART_TYPE.COLUMN_STACKED_100,
"area": XL_CHART_TYPE.AREA,
"scatter": XL_CHART_TYPE.XY_SCATTER,
"donut": XL_CHART_TYPE.DOUGHNUT,
"combo": None, }
def _try_category_axis(chart):
try:
return chart.category_axis
except ValueError:
return None
def _try_value_axis(chart):
try:
return chart.value_axis
except ValueError:
return None
def _apply_rgb_to_textframe(tf, rgb):
if tf is None:
return
try:
tf.paragraphs[0].font.color.rgb = rgb
except (AttributeError, TypeError, ValueError):
pass
for p in tf.paragraphs:
try:
p.font.color.rgb = rgb
except (AttributeError, TypeError, ValueError):
pass
for run in p.runs:
try:
run.font.color.rgb = rgb
except (AttributeError, TypeError, ValueError):
pass
def _apply_theme_to_chart(chart, cd, t):
body = t["body"]
title_rgb = t.get("title", body)
label_rgb = body
try:
chart.chart_style = None
except (AttributeError, TypeError, ValueError):
pass
try:
chart.font.size = Pt(11)
chart.font.color.rgb = label_rgb
if t.get("font"):
chart.font.name = t["font"]
except (AttributeError, TypeError, ValueError):
pass
try:
chart.fill.background()
except (AttributeError, TypeError, ValueError):
pass
if chart.has_title:
try:
tf = chart.chart_title.text_frame
_apply_rgb_to_textframe(tf, title_rgb)
for p in tf.paragraphs:
try:
p.font.size = Pt(14)
except (AttributeError, TypeError, ValueError):
pass
except (AttributeError, TypeError, ValueError):
try:
chart.chart_title.text_frame.paragraphs[0].font.color.rgb = title_rgb
except (AttributeError, TypeError, ValueError):
pass
cat_ax = _try_category_axis(chart)
val_ax = _try_value_axis(chart)
x_label = cd.get("x_label", "")
y_label = cd.get("y_label", "")
if cat_ax is not None:
try:
if x_label and cat_ax.has_title:
tf = cat_ax.axis_title.text_frame
_apply_rgb_to_textframe(tf, label_rgb)
cat_ax.tick_labels.font.size = Pt(10)
cat_ax.tick_labels.font.color.rgb = label_rgb
except (AttributeError, TypeError, ValueError):
pass
if val_ax is not None:
try:
if y_label and val_ax.has_title:
tf = val_ax.axis_title.text_frame
_apply_rgb_to_textframe(tf, label_rgb)
val_ax.tick_labels.font.size = Pt(10)
val_ax.tick_labels.font.color.rgb = label_rgb
except (AttributeError, TypeError, ValueError):
pass
try:
if val_ax is not None and val_ax.has_major_gridlines:
val_ax.major_gridlines.format.line.color.rgb = t.get("muted", label_rgb)
except (AttributeError, TypeError, ValueError):
pass
if chart.has_legend and chart.legend is not None:
try:
chart.legend.font.size = Pt(11)
chart.legend.font.color.rgb = label_rgb
except (AttributeError, TypeError, ValueError):
pass
if cd.get("data_labels") is not False and chart.series:
for s in chart.series:
try:
s.has_data_labels = True
s.data_labels.font.size = Pt(9)
s.data_labels.font.color.rgb = label_rgb
except AttributeError:
pass
def add_chart(slide, chart_data, left, top, width, height, t=None):
cd = chart_data
chart_type = cd.get("type", "bar")
is_combo = (chart_type == "combo")
ct = XL_CHART_TYPE.COLUMN_CLUSTERED if is_combo else CHART_TYPES.get(
chart_type, XL_CHART_TYPE.COLUMN_CLUSTERED
)
categories = cd.get("categories", [])
series_list = cd.get("series", [])
if not categories or not series_list:
return 0
is_scatter = (chart_type == "scatter")
if is_scatter:
chart_data_obj = XyChartData()
x_vals = [float(c) for c in categories]
for s in series_list:
y_vals = [float(v) for v in s.get("values", [])]
xy_series = chart_data_obj.add_series(str(s.get("name", "")))
for x, y in zip(x_vals, y_vals):
xy_series.add_data_point(x, y)
else:
chart_data_obj = CategoryChartData()
chart_data_obj.categories = [str(c) for c in categories]
for s in series_list:
vals = [float(v) for v in s.get("values", [])]
chart_data_obj.add_series(str(s.get("name", "")), vals)
chart_frame = slide.shapes.add_chart(ct, left, top, width, height, chart_data_obj)
chart = chart_frame.chart
chart.has_legend = len(series_list) > 1 or is_combo
if is_combo and len(series_list) >= 2:
line_series = chart.series[-1]
line_series.chart_type = XL_CHART_TYPE.LINE_MARKERS
chart_title_text = cd.get("chart_title", "")
if chart_title_text:
chart.has_title = True
chart.chart_title.text_frame.paragraphs[0].text = chart_title_text
chart.chart_title.text_frame.paragraphs[0].font.size = Pt(14)
x_label = cd.get("x_label", "")
y_label = cd.get("y_label", "")
cat_ax = _try_category_axis(chart)
val_ax = _try_value_axis(chart)
if x_label and cat_ax is not None:
cat_ax.has_title = True
cat_ax.axis_title.text_frame.paragraphs[0].text = x_label
if y_label and val_ax is not None:
val_ax.has_title = True
val_ax.axis_title.text_frame.paragraphs[0].text = y_label
if cd.get("data_labels") is not False and chart.series:
try:
for s in chart.series:
s.has_data_labels = True
s.data_labels.font.size = Pt(9)
except AttributeError:
pass
if t:
_apply_theme_to_chart(chart, cd, t)
return _inches(height)
def _inches(emu_val):
return emu_val / 914400.0