I'm trying to call the Chart class in the pptx module. Chart has two arguments: chartSpace and chart_part. The problem is that I have no idea what those two arguments are. There is probably a simple answer to this, but I've tried looking over all the documentation and can't find anything about these arguments. Can someone explain what these arguments are looking for?
from pptx import Presentation
from pptx.enum.chart import XL_CHART_TYPE, XL_TICK_LABEL_POSITION
from pptx.chart.data import CategoryChartData
from pptx.chart.data import ChartData
from pptx.enum.shapes import PP_PLACEHOLDER
from pandas import DataFrame as DF
from pptx.chart.chart import Chart
prs_dir = 'Directory'
layout = prs.slide_layouts[6]
slide = prs.slides.add_slide( layout )
chart_data = ChartData()
chart_data.categories = ['Budget','Actuals']
chart_data.add_series('Budget', (1,21,23,4,5,6,7,35))
chart_data.add_series('Actuals', (1,21,23,4,5,6,7,35))
chart = Chart()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-207-e560d7744421> in <module>
----> 1 chart = Chart()
TypeError: __init__() missing 2 required positional arguments: 'chartSpace' and 'chart_part'```
The Chart class is not intended to be instantiated directly. Use the .add_chart() method on the slide shapes collection to add a chart.
from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE
from pptx.util import Inches
# create presentation with 1 slide ------
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])
# define chart data ---------------------
chart_data = CategoryChartData()
chart_data.categories = ['East', 'West', 'Midwest']
chart_data.add_series('Series 1', (19.2, 21.4, 16.7))
# add chart to slide --------------------
x, y, cx, cy = Inches(2), Inches(2), Inches(6), Inches(4.5)
slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data
)
prs.save('chart-01.pptx')
More details are available in the documentation here: https://python-pptx.readthedocs.io/en/latest/user/charts.html
Related
I already referred these posts here here, here and here. Please don't mark it as a duplicate.
I have a chart embedded inside the ppt like below
I wish to replace the axis headers from FY2021 HC to FY1918 HC. Similarly, FY2122 HC should be replaced with FY1718 HC.
How can I do this using python pptx? This chart is coming from embedded Excel though. Is there anyway to change it in ppt?
When I tried the below, it doesn't get the axis headers
text_runs = []
for shape in slide.shapes:
if not shape.has_text_frame:
continue
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs:
text_runs.append(run.text)
when I did the below, I find the list of shape types from the specific slide. I wish to change only the chart headers. So, the screenshot shows only two charts that I have in my slide.
for slide in ip_ppt.slides:
for shape in slide.shapes:
print("id: %s, type: %s" % (shape.shape_id, shape.shape_type))
id: 24, type: TEXT_BOX (17)
id: 10242, type: TEXT_BOX (17)
id: 11306, type: TEXT_BOX (17)
id: 11, type: AUTO_SHAPE (1)
id: 5, type: TABLE (19)
id: 7, type: TABLE (19)
id: 19, type: AUTO_SHAPE (1)
id: 13, type: CHART (3)
id: 14, type: CHART (3)
When I try to access the shape using id, I am unable to as well
ip_ppt.slides[5].shapes[13].Chart
I also tried the code below
from pptx import chart
from pptx.chart.data import CategoryChartData
chart_data = CategoryChartData()
chart.datalabel = ['FY1918 HC', 'FY1718 HC']
Am new to python and pptx. Any solution on how to edit the embedded charts headers would really be useful. Help please
You can get to the category labels the following way:
from pptx import Presentation
from pptx.shapes.graphfrm import GraphicFrame
prs = Presentation('chart-01.pptx')
for slide in prs.slides:
for shape in slide.shapes:
print("slide: %s, id: %s, index: %s, type: %s" % (slide.slide_id, shape.shape_id, slide.shapes.index(shape), shape.shape_type))
if isinstance(shape, GraphicFrame) and shape.has_chart:
plotIndex = 0
for plot in shape.chart.plots:
catIndex = 0
for cat in plot.categories:
print(" plot %s, category %s, category label: %s" % (plotIndex, catIndex, cat.label))
catIndex += 1
plotIndex += 1
which will put out something like that:
slide: 256, id: 2, index: 0, type: PLACEHOLDER (14)
slide: 256, id: 3, index: 1, type: CHART (3)
plot 0, category 0, category label: East
plot 0, category 1, category label: West
plot 0, category 2, category label: Midwest
Unfortunately you can not change the category label, because it is stored in the embedded Excel. The only way to change those is to replace the chart data by using the chart.replace_data() method.
Recreating the ChartData object you need for the call to replace_data based on the existing chart is a bit more involved, but here is my go at it based on a chart that I created with the following code:
from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE,XL_LABEL_POSITION
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
# create presentation with 1 slide ------
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])
# define chart data ---------------------
chart_data = CategoryChartData()
chart_data.categories = ['FY2021 HC', 'FY2122 HC']
chart_data.add_series('blue', (34.5, 31.5))
chart_data.add_series('orange', (74.1, 77.8))
chart_data.add_series('grey', (56.3, 57.3))
# add chart to slide --------------------
x, y, cx, cy = Inches(2), Inches(2), Inches(6), Inches(4.5)
gframe = slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_STACKED, x, y, cx, cy, chart_data
)
chart = gframe.chart
plot = chart.plots[0]
plot.has_data_labels = True
data_labels = plot.data_labels
data_labels.font.size = Pt(13)
data_labels.font.color.rgb = RGBColor(0x0A, 0x42, 0x80)
data_labels.position = XL_LABEL_POSITION.INSIDE_END
prs.save('chart-01.pptx')
and that looks almost identical to your picture in the question:
The following code will change the category labels in that chart:
from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.shapes.graphfrm import GraphicFrame
from pptx.enum.chart import XL_CHART_TYPE
from pptx.util import Inches
# read presentation from file
prs = Presentation('chart-01.pptx')
# find the first chart object in the presentation
slideIdx = 0
for slide in prs.slides:
for shape in slide.shapes:
if shape.has_chart:
chart = shape.chart
print("Chart of type %s found in slide[%s, id=%s] shape[%s, id=%s, type=%s]"
% (chart.chart_type, slideIdx, slide.slide_id,
slide.shapes.index(shape), shape.shape_id, shape.shape_type ))
break
slideIdx += 1
# create list with changed category names
categorie_map = { 'FY2021 HC': 'FY1918 HC', 'FY2122 HC': 'FY1718 HC' }
new_categories = list(categorie_map[c] for c in chart.plots[0].categories)
# build new chart data with new category names and old data values
new_chart_data = CategoryChartData()
new_chart_data.categories = new_categories
for series in chart.series:
new_chart_data.add_series(series.name,series.values)
# write the new chart data to the chart
chart.replace_data(new_chart_data)
# save everything in a new file
prs.save('chart-02.pptx')
The comments should explain what is going on and if you open chart-02.pptx with PowerPoint, this is what you will see:
Hope that solves your problem!
I am creating a pie chart in Powerpoint using Python and acquiring data from Excel. For some reason my tuple for the data series within the chart data is poorly formed. I see IndexError: list index out of range for the following statement-
chart = slide.shapes.add_chart(
XL_CHART_TYPE.PIE, x, y, cx, cy, chart_data
).chart
My tuple created programmatically shows as:
('Series 1', (0.05, 0.25, 0.5, 0.1, 0.1))
When I hard code the values as chart_data.add_series('Series 1', (0.05, 0.25, 0.5, 0.10, 0.10)) it accepts the value and has no error.
Any advice would be greatly appreciated! The code is shown below:
Thank you,
Ken
from openpyxl import load_workbook
from pptx import Presentation
from pptx.chart.data import ChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LABEL_POSITION
from pptx.util import Inches
# Load in the workbook
wb = load_workbook('c:/test/testcaseexcel.xlsx')
# Get sheet names
print(wb.sheetnames)
sheet = wb['Sheet1']
print(sheet['A1'].value)
chart_data = ChartData()
cat = []
chart_data_temp = ()
chart_data.add_series = () #foo = ((0,),) + foo
data_series_points = []
chart_data_series_list = [('Series 1'),]
chart_data_series_tuple = (('Series 1'),)
for i in range(2, 7):
data_series_points.append(sheet.cell(row=i, column=2).value)
t1=tuple(data_series_points)
chart_data_series_list.append(t1)
chart_data_series_tuple = tuple(chart_data_series_list)
chart_data.add_series = tuple(chart_data_series_list)
for i in range(2, 7):
cat.append(sheet.cell(row=i, column=1).value)
#print(i, sheet.cell(row=i, column=1).value)
chart_data.categories = cat
# create presentation with 1 slide ------
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])
#chart_data.categories = ['AAA', 'A', 'BBB', 'BB-', 'FFF']
#chart_data.add_series('Series 1', (0.05, 0.25, 0.5, 0.10, 0.10))
x, y, cx, cy = Inches(2), Inches(2), Inches(6), Inches(4.5)
chart = slide.shapes.add_chart(
XL_CHART_TYPE.PIE, x, y, cx, cy, chart_data
).chart
chart.has_legend = False
# set labels to contain category and value
for i in range(len(chart_data.categories)):
point = chart.series[0].points[i]
point.data_label.text_frame.text = "{}: {:.0%}".format(chart_data.categories[i].label, chart.series[0].values[i])
point.data_label.position = XL_LABEL_POSITION.OUTSIDE_END
prs.save('chart-02.pptx')
I'm trying to make a dropdown menu with Bokeh that highlights the points in clusters I found. I have the dropdown menu working, but now I want to be able to visualize another categorical variable by color: Noun Class with levels of Masc, Fem, and Neuter. The problem is that the legend won't update when I switch which cluster I'm visualizing. Furthermore, if the first cluster I visualize doesn't have all 3 noun classes in it, the code starts treating all the other clusters I try to look at as (incorrectly) having that first cluster's noun class. For example, if Cluster 0 is the default and only has Masc points, all other clusters I look at using the dropdown menu are treated as only having Masc points even if they have Fem or Neuter in the actual DF.
My main question is this: how can I update the legend such that it's only attending to the respective noun classes of 'Curr'
Here's some reproducible code:
import pandas as pd
from bokeh.io import output_file, show, output_notebook, save, push_notebook
from bokeh.models import ColumnDataSource, Select, DateRangeSlider, CustomJS
from bokeh.plotting import figure, Figure, show
from bokeh.models import CustomJS
from bokeh.layouts import row,column,layout
import random
import numpy as np
from bokeh.transform import factor_cmap
from bokeh.palettes import Colorblind
import bokeh.io
from bokeh.resources import INLINE
#Generate reproducible DF
noun_class_names = ["Masc","Fem","Neuter"]
x = [random.randint(0,50) for i in range(100)]
y = [random.randint(0,50) for i in range(100)]
rand_clusters = [str(random.randint(0,10)) for i in range(100)]
noun_classes = [random.choice(noun_class_names) for i in range(100)]
df = pd.DataFrame({'x_coord':x, 'y_coord':y,'noun class':noun_classes,'cluster labels':rand_clusters})
df.loc[df['cluster labels'] == '0', 'noun class'] = 'Masc' #ensure that cluster 0 has all same noun class to illustrate error
clusters = [str(i) for i in range(len(df['cluster labels'].unique()))]
cols1 = df#[['cluster labels','x_coord', 'y_coord']]
cols2 = cols1[cols1['cluster labels'] == '0']
Overall = ColumnDataSource(data=cols1)
Curr = ColumnDataSource(data=cols2)
#plot and the menu is linked with each other by this callback function
callback = CustomJS(args=dict(source=Overall, sc=Curr), code="""
var f = cb_obj.value
sc.data['x_coord']=[]
sc.data['y_coord']=[]
for(var i = 0; i <= source.get_length(); i++){
if (source.data['cluster labels'][i] == f){
sc.data['x_coord'].push(source.data['x_coord'][i])
sc.data['y_coord'].push(source.data['y_coord'][i])
sc.data['noun class'].push(source.data['noun class'][i])
sc.data['cluster labels'].push(source.data['cluster labels'][i])
}
}
sc.change.emit();
""")
menu = Select(options=clusters, value='0', title = 'Cluster #') # create drop down menu
bokeh_p=figure(x_axis_label ='X Coord', y_axis_label = 'Y Coord', y_axis_type="linear",x_axis_type="linear") #creating figure object
mapper = factor_cmap(field_name = "noun class", palette = Colorblind[6], factors = df['noun class'].unique()) #color mapper for noun classes
bokeh_p.circle(x='x_coord', y='y_coord', color='gray', alpha = .5, source=Overall) #plot all other points in gray
bokeh_p.circle(x='x_coord', y='y_coord', color=mapper, line_width = 1, source=Curr, legend_group = 'noun class') # plotting the desired cluster using glyph circle and colormapper
bokeh_p.legend.title = "Noun Classes"
menu.js_on_change('value', callback) # calling the function on change of selection
bokeh.io.output_notebook(INLINE)
show(layout(menu,bokeh_p), notebook_handle=True)
Thanks in advance and I hope you have a nice day :)
Imma keep it real with y'all... The code works how I want now and I'm not entirely sure what I did. What I think I did was reset the noun classes in the Curr data source and then update the legend label field after selecting a new cluster to visualize and updating the xy coords. If anyone can confirm or correct me for posterity's sake I would appreciate it :)
Best!
import pandas as pd
import random
import numpy as np
from bokeh.plotting import figure, Figure, show
from bokeh.io import output_notebook, push_notebook, show, output_file, save
from bokeh.transform import factor_cmap
from bokeh.palettes import Colorblind
from bokeh.layouts import layout, gridplot, column, row
from bokeh.models import ColumnDataSource, Slider, CustomJS, Select, DateRangeSlider, Legend, LegendItem
import bokeh.io
from bokeh.resources import INLINE
#Generate reproducible DF
noun_class_names = ["Masc","Fem","Neuter"]
x = [random.randint(0,50) for i in range(100)]
y = [random.randint(0,50) for i in range(100)]
rand_clusters = [str(random.randint(0,10)) for i in range(100)]
noun_classes = [random.choice(noun_class_names) for i in range(100)]
df = pd.DataFrame({'x_coord':x, 'y_coord':y,'noun class':noun_classes,'cluster labels':rand_clusters})
df.loc[df['cluster labels'] == '0', 'noun class'] = 'Masc' #ensure that cluster 0 has all same noun class to illustrate error
clusters = [str(i) for i in range(len(df['cluster labels'].unique()))]
cols1 = df#[['cluster labels','x_coord', 'y_coord']]
cols2 = cols1[cols1['cluster labels'] == '0']
Overall = ColumnDataSource(data=cols1)
Curr = ColumnDataSource(data=cols2)
#plot and the menu is linked with each other by this callback function
callback = CustomJS(args=dict(source=Overall, sc=Curr), code="""
var f = cb_obj.value
sc.data['x_coord']=[]
sc.data['y_coord']=[]
sc.data['noun class'] =[]
for(var i = 0; i <= source.get_length(); i++){
if (source.data['cluster labels'][i] == f){
sc.data['x_coord'].push(source.data['x_coord'][i])
sc.data['y_coord'].push(source.data['y_coord'][i])
sc.data['noun class'].push(source.data['noun class'][i])
sc.data['cluster labels'].push(source.data['cluster labels'][i])
}
}
sc.change.emit();
bokeh_p.legend.label.field = sc.data['noun class'];
""")
menu = Select(options=clusters, value='0', title = 'Cluster #') # create drop down menu
bokeh_p=figure(x_axis_label ='X Coord', y_axis_label = 'Y Coord', y_axis_type="linear",x_axis_type="linear") #creating figure object
mapper = factor_cmap(field_name = "noun class", palette = Colorblind[6], factors = df['noun class'].unique()) #color mapper- sorry this was a thing that carried over from og code (fixed now)
bokeh_p.circle(x='x_coord', y='y_coord', color='gray', alpha = .05, source=Overall)
bokeh_p.circle(x = 'x_coord', y = 'y_coord', fill_color = mapper, line_color = mapper, source = Curr, legend_field = 'noun class')
bokeh_p.legend.title = "Noun Classes"
menu.js_on_change('value', callback) # calling the function on change of selection
bokeh.io.output_notebook(INLINE)
show(layout(menu,bokeh_p), notebook_handle=True)
I am asking a duplicate of this question, except that the answer submitted does not work for me. I would like to toggle the data_labels' "Wrap text in shape" button from the powerpoint UI via python-pptx. The linked answer ends up removing the data labels altogether instead. I am using the latest python-pptx version (0.6.18).
Here is a simple example to replicate:
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.text.text import TextFrame
# create presentation with 1 slide ------
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])
x = ['one','two','three', 'four']
y = [['diff',
[1,
2,
3,
4,
]],
]
specs = {
'height': Cm(7.82),
'width': Cm(14.8),
'left': Cm(2.53),
'top': Cm(5.72)}
data = ChartData()
data.categories = x
data.add_series('diff', [j for j in y[0][1]])
frame = slide.shapes.add_chart(
XL_CHART_TYPE.BAR_CLUSTERED, specs['left'], specs['top'],
specs['width'], specs['height'], data
)
plot = frame.chart.plots[0]
plot.has_data_labels = True
data_labels = plot.series[0].data_labels
dLbls = data_labels._element
# ---use its <c:txPr> child to create TextFrame object---
text_frame = TextFrame(dLbls.get_or_add_txPr(), None)
# ---turn off word-wrap in the usual way---
text_frame.wrap = False
prs.save('chart-01.pptx')
I believe the second to last line should be text_frame.word_wrap = False, not .wrap; that's my mistake on the earlier answer (now fixed).
Also change this line:
data_labels = plot.series[0].data_labels
to:
data_labels = plot.data_labels
And I think you'll get what you're looking for.
I am using reportlab to generate a LinePlot chart. I can't seem to get non numeric labels for the X axis.
Does anyone have any ideas?
This is my Lineplot chart class (note: im donig some calculations and setup outside this class, but you get the gist
import reportlab
from advisor.charting.Font import Font
from reportlab.lib.colors import Color, HexColor
from reportlab.lib.pagesizes import cm, inch
from reportlab.graphics.charts.legends import Legend
from reportlab.graphics.charts.textlabels import Label
from reportlab.graphics.charts.linecharts import HorizontalLineChart
from reportlab.graphics.charts.lineplots import LinePlot
from reportlab.graphics.shapes import Drawing, String, _DrawingEditorMixin
from reportlab.graphics.widgets.markers import makeMarker
class TacticalAugLineGraph(_DrawingEditorMixin, Drawing):
def __init__(self, width=100, height=110, legend=False, *args, **kw):
apply(Drawing.__init__, (self, width, height) + args, kw)
chartFont = Font('Gotham-Bold')
self._add(self, LinePlot(), name='chart', validate=None, desc=None)
self.chart._inFill = 1
self.chart.x = 20
self.chart.y = 15
self.chart.width = 85
self.chart.height = 95
#self.chart.lineLabelFormat = '%d%%'
self.chart.yValueAxis.valueMin = 0
self.chart.yValueAxis.valueMax = 100
self.chart.yValueAxis.valueStep = 10
def apply_colors(self, colors):
self.chart.lines[0].strokeColor = colors[0]
self.chart.lines[1].strokeColor = colors[1]
self.chart.lines[2].strokeColor = colors[2]
self.chart.lines[3].strokeColor = colors[3]
I produced a simple example that you could test. The result could look like this:
#!/usr/bin/python
from reportlab.graphics.charts.lineplots import LinePlot
from reportlab.graphics.shapes import Drawing
from reportlab.lib import colors
from random import randint
from datetime import date, timedelta
# Generate some testdata
data = [
[(x,randint(90,100)) for x in range(0,2001,100)],
[(x,randint(30,80)) for x in range(0,2001,100)],
[(x,randint(5,20)) for x in range(0,2001,100)],
]
# Create the drawing and the lineplot
drawing = Drawing(400, 200)
lp = LinePlot()
lp.x = 50
lp.y = 50
lp.height = 125
lp.width = 300
lp._inFill = 1
lp.data = data
for i in range(len(data)):
lp.lines[i].strokeColor = colors.toColor('hsl(%s,80%%,40%%)'%(i*60))
# Specify where the labels should be
lp.xValueAxis.valueSteps = [5, 500, 1402, 1988]
# Create a formatter that takes the value and format it however you like.
def formatter(val):
#return val
#return 'x=%s'%val
return (date(2010,1,1) + timedelta(val)).strftime('%Y-%m-%d')
# Use the formatter
lp.xValueAxis.labelTextFormat = formatter
drawing.add(lp)
from reportlab.graphics import renderPDF
renderPDF.drawToFile(drawing, 'example.pdf', 'lineplot with dates')
In the formatter there is 2 alternative return statements that you could play with to get a better grip on it.
Of course if the data on the x-axis is dates to begin with there need not be any formatter, in that case you could just specify where the labels should be (if you are not satisfied with the default positioning).
The example above borrows the ideas from page 105 in the reportlab documentation.
Good luck :)