I have a Chart Placeholder, into which I have inserted a chart of chart_type 'DOUGHNUT'. I've added data labels to it and want to change their positions. For some reason, the method given in the documentation has no effect on my chart.
Here is my code, please help if I'm doing something wrong -
from pptx import Presentation
from pptx.chart.data import ChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LABEL_POSITION, XL_DATA_LABEL_POSITION, XL_TICK_MARK, XL_TICK_LABEL_POSITION
chart_data = ChartData()
chart_data.add_series('', tuple(input_chart_data[x] for x in input_chart_data))
graphic_frame = content_placeholder.insert_chart(XL_CHART_TYPE.DOUGHNUT, chart_data)
chart = graphic_frame.chart
chart.has_legend = False
#Adding Data-Labels with custom text
chart.plots[0].has_data_labels = True
data_labels = chart.plots[0].data_labels
i = 0
series = chart.series[0]
for point in series.points:
fill = point.format.fill
fill.solid()
fill.fore_color.rgb = RGBColor(<color_code>)
point.data_label.has_text_frame = True
#Assigning custom text for data label associated with each data-point
point.data_label.text_frame.text = str(chart_data.categories[i].label) + "\n" + str(float(chart.series[0].values[i])) + "%"
for run in point.data_label.text_frame.paragraphs[0].runs:
run.font.size = Pt(10)
i+=1
data_labels.position = XL_LABEL_POSITION.OUTSIDE_END
PowerPoint is finicky about where you place certain chart attributes and feels free to ignore them when it wants (although it does so consistently).
A quick option worth trying is to set the value individually, point-by-point in the series. So something like:
for point in series.points:
point.data_label.position = XL_LABEL_POSITION.OUTSIDE_END
The most reliable method is to start by producing the effect you want by hand, using PowerPoint itself on an example chart, then inspecting the XML PowerPoint produces in the saved file, perhaps using opc-diag. Once you've identified what XML produces the desired effect (or discovered PowerPoint won't let you do it), then you can proceed to working out how to get the XML generated by python-pptx. That might make a good second question if you're able to get that far.
I made it work by writing the below code.
def apply_data_labels(self, chart):
plot = chart.plots[0]
plot.has_data_labels = True
for series in plot.series:
values = series.values
counter = 0
for point in series.points:
data_label = point.data_label
data_label.has_text_frame = True
data_label.text_frame.text = str(values[counter])
counter = counter + 1
the cause of error is setting the label position. no matter what you set it asks to repair the PPT. will have to drill down more to see why is it so.
Also to save some more time the formatting doesn't works(font color, size)
If anybody has any leads then please help.
To add on Vibhanshu's response, I could get the formatting (font type, font color, size etc) to work using the following code:
for idx, point in enumerate(chart.series[0].points):
# set position
point.data_label.position = XL_LABEL_POSITION.OUTSIDE_END
# set text
point.data_label.has_text_frame = True
point.data_label.text_frame.text = "This is an example"
# set formatting
for paragraph_idx, paragraph in enumerate(point.data_label.text_frame.paragraphs):
paragraph.line_spacing = 0.6 # set paragraph line spacing
for run in paragraph.runs:
run.font.size = Pt(30) #set font size
run.font.name = 'Poppins Medium' #set font name
run.font.color.rgb = RGBColor.from_string("FF0000") #set font color
Related
I am writing a Python script to automatically adjust cell borders in LibreOffice Calc. I think I know what property I need to change, however when I assign a new value to this property, the value does not change.
For instance, I wrote this code to change the TopLine.LineWidth of a single Cell from 0 to 10.
# Access the current calc document
model = desktop.getCurrentComponent()
# Access the active sheet
active_sheet = model.CurrentController.ActiveSheet
# Get the cell and change the value of LineWidth
cell = active_sheet.getCellByPosition(2, 2)
cell.TableBorder2.TopLine.LineWidth = 10
I don't get any errors after running this code. And I have also made sure that I am accessing the cell I wish to modify. However, this code does not change the cell's border width.
I tried doing some debugging by printing the value before and after the assignment:
# This first print statement returns 0 because the cell has no borders
print(cell.TableBorder2.TopLine.LineWidth)
cell.TableBorder2.TopLine.LineWidth = 10
# This second print statement still returns 0, but I was expecting it to return 10
print(cell.TableBorder2.TopLine.LineWidth)
Does anyone know what I am doing wrong?
You need to set the cell property to a changed border object. From https://ask.libreoffice.org/en/question/145885/border-macro-no-longer-works/:
aThinBorder = oRange.TopBorder2
aThinBorder.LineWidth = 1
oRange.TopBorder2 = aThinBorder
So, after doing a lot of research, I found at least three methods to change border settings. Because it took me so much effort, I figured I should leave them here so in the future other people may find the answer more easily.
In all examples I'll set the LineWidth of the TopBorder of a single cell to 10.
Method 1: Using getPropertyValue() and setPropertyValue()
cell = active_sheet.getCellByPosition(1, 1)
border_prop = cell.getPropertyValue("TopBorder")
border_prop.LineWidth = 10
cell.setPropertyValue("TopBorder", border_prop)
Method 2 (derived from Jim K's answer)
cell = active_sheet.getCellByPosition(1, 1)
border_prop = cell.TopBorder2
border_prop.LineWidth = 10
cell.TopBorder2 = border_prop
Method 3: Using a BorderLine2 struct
border_prop = uno.createUnoStruct("com.sun.star.table.BorderLine2")
border_prop.LineWidth = 10
cell = active_sheet.getCellByPosition(1, 1)
cell.setPropertyValue("TopBorder", border_prop)
I am working on an automated ppt through a python-pptx. I am interested in applying character formatting to the data label through the next function but it does not work. I need help with figuring out why.
def apply_data_labels(self, chart):
plot = chart.plots[0]
plot.has_data_labels = True
for series in plot.series:
values = series.values
counter = 0
for point in series.points:
data_label = point.data_label
data_label.has_text_frame = True
data_label.text_frame.text = str(values[counter])
data_label.font.size = Pt(22)
data_label.font.color.rgb = RGBColor(255,160,122)
counter = counter + 1
Try:
data_label.text_frame.paragraphs[0].font.size = Pt(22)
Things work a little differently when you're setting the font of an individual data label rather than all of them at once.
I am using the python-pptx library for pptx manipulation. I want to add a bullet list in the pptx document.
I am using the following snippet to add list item:
p = text_frame.add_paragraph()
run = p.add_run()
p.level = 0
run.text = "First"
But it does not display bullet points; please guide.
It is currently not possible to access the bullet property using python-pptx, but I want to share a workaround that has served me well.
This requires the use of a pptx template, in which we exploit the fact that the levels in a slide layout can be customized individually.
For instance, in the slide layout you could set level 0 to be normal text, level 1 to be bullets, and level 2 to be numbers or any other list style you want. You can then modify font size, indentation (using the ruler at the top), and any other property of each level to get the look you want.
For my use-case, I just set levels 1 and 2 to have the same indentation and size as level 0, making it possible to create bullet lists and numbered lists by simply setting the level to the corresponding value.
This is how my slide layout looks in the template file:
slide layout example
And this is how I set the corresponding list style in the code:
p.level = 0 # Regular text
p.level = 1 # Bullet
p.level = 2 # Numbers
In theory, you should be able to set it up exactly the way you want, even with indented sub-lists and so on. The only limitation I am aware of is that there seems to be a maximum of 8 levels that can be customized in the slide layout.
My solution:
from pptx.oxml.xmlchemy import OxmlElement
def SubElement(parent, tagname, **kwargs):
element = OxmlElement(tagname)
element.attrib.update(kwargs)
parent.append(element)
return element
def makeParaBulletPointed(para):
"""Bullets are set to Arial,
actual text can be a different font"""
pPr = para._p.get_or_add_pPr()
## Set marL and indent attributes
pPr.set('marL','171450')
pPr.set('indent','171450')
## Add buFont
_ = SubElement(parent=pPr,
tagname="a:buFont",
typeface="Arial",
panose="020B0604020202020204",
pitchFamily="34",
charset="0"
)
## Add buChar
_ = SubElement(parent=pPr,
tagname='a:buChar',
char="•")
This question is still up to date on May 27, 2021.
Following up on #OD1995's answer I would like to add a little more detail as well as my turn on the problem.
I created a new package with the following code:
from pptx.oxml.xmlchemy import OxmlElement
def getBulletInfo(paragraph, run=None):
"""Returns the attributes of the given <a:pPr> OxmlElement
as well as its runs font-size.
*param: paragraph* pptx _paragraph object
*param: run* [optional] specific _run object
"""
pPr = paragraph._p.get_or_add_pPr()
if run is None:
run = paragraph.runs[0]
p_info = {
"marL": pPr.attrib['marL'],
"indent": pPr.attrib['indent'],
"level": paragraph.level,
"fontName": run.font.name,
"fontSize": run.font.size,
}
return p_info
def SubElement(parent, tagname, **kwargs):
"""Helper for Paragraph bullet Point
"""
element = OxmlElement(tagname)
element.attrib.update(kwargs)
parent.append(element)
return element
def pBullet(
paragraph, # paragraph object
font, # fontName of that needs to be applied to bullet
marL='864000',
indent='-322920',
size='350000' # fontSize (in )
):
"""Bullets are set to Arial,
actual text can be a different font
"""
pPr = paragraph._p.get_or_add_pPr()
# Set marL and indent attributes
# Indent is the space between the bullet and the text.
pPr.set('marL', marL)
pPr.set('indent', indent)
# Add buFont
_ = SubElement(parent=pPr,
tagname="a:buSzPct",
val="350000"
)
_ = SubElement(parent=pPr,
tagname="a:buFont",
typeface=font,
# panose="020B0604020202020204",
# pitchFamily="34",
# charset="0"
)
# Add buChar
_ = SubElement(parent=pPr,
tagname='a:buChar',
char="•"
)
The reason I did this is because I was frustrated that the bullet character was not of the same size as the original and the text was stuck to the bullet.
getBulletInfo() allows me to retrieve information from an existing paragraph.
I use this information to populate the element's attributes (so that it is identical to the template).
Anyways the main add-on is the creation of a sub-element <a:buSzPct> (documentation here and here). This is a size percentage that can go from 25% to 350% (100000 = 100%).
Try this:
p = text_frame.add_paragraph()
p.level = 0
p.text = "First"
Or if the text_frame already has a paragraph:
p = text_frame.paragraphs[0]
p.level = 0
p.text = "First"
I have a script which is designed to place inset maps onto specific pages while exporting Data Driven Pages, the script is amalgamation of a friend's work and some of my own code from other projects.
The issue is the code exports pages 15 and 16 twice one with my inset maps and the other without and I can't figure out why.
I think it is something to do with the indentation within the Loop but I cant get it so it behaves in any other way. Any help would be appreciated!
import arcpy, os, time, datetime
from datetime import datetime
start_time = datetime.now()
PageNumber = "Page "
# Create an output directory variable i.e the location of your maps folder
outDir = r"C:\Users\support\Desktop\Python\Book of Reference"
# Create a new, empty pdf document in the specified output directory
# This will be your final product
finalpdf_filename = outDir + r"\FinalMapBook.pdf"
if os.path.exists(finalpdf_filename): # Check to see if file already exists, delete if it does
os.remove(finalpdf_filename)
finalPdf = arcpy.mapping.PDFDocumentCreate(finalpdf_filename)
# Create a Data Driven Pages object from the mxd you wish to export
mxdPath = r"C:\Users\support\Desktop\Python\Book Of Reference\Book_Of_Reference_20160526_Python_Test.mxd"
tempMap = arcpy.mapping.MapDocument(mxdPath)
tempDDP = tempMap.dataDrivenPages
# Create objects for the layout elements that will be moving, e.g., inset data frame, scale text
Page15 = arcpy.mapping.ListDataFrames(tempMap)[1]
Page16 = arcpy.mapping.ListDataFrames(tempMap)[2]
# Instead of exporting all pages at once, you will need to use a loop to export one at a time
# This allows you to check each index and execute code to add inset maps to the correct pages
for pgIndex in range(1, tempDDP.pageCount + 1, 1):
# Create a name for the pdf file you will create for each page
temp_filename = r"C:\Users\support\Desktop\Python\Book of Reference\Book of Reference" + \
str(pgIndex) + ".pdf"
if os.path.exists(temp_filename):
os.remove(temp_filename) #Removes pdf if it is already in the folder
# Code for setting up the inset map on the first page #
if (pgIndex == 15):
# Set position of inset map to place it on the page layout
Page15.elementPositionX = 20.1717
Page15.elementPositionY = 2.0382
# Set the desired size of the inset map for this page
Page15.elementHeight = 9.7337
Page15.elementWidth = 12.7115
# Set the desired extent for the inset map
Page15insetExtent = arcpy.Extent(518878,108329,519831,107599)
Page15insetExtent = Page15insetExtent
arcpy.RefreshActiveView()
tempDDP.exportToPDF(temp_filename, "RANGE", pgIndex)
finalPdf.appendPages(temp_filename)
Page15.elementPositionX = 50 #Move the Inset back off the page
arcpy.RefreshActiveView() #Refresh to ensure the Inset has been removed
print PageNumber + str(pgIndex)
if (pgIndex == 16):
# Set up inset map
Page16.elementPositionX = 2.1013
Page16.elementPositionY = 18.1914
Page16.elementHeight = 9.7337
Page16.elementWidth = 12.7115
Page16insetExtent = arcpy.Extent(520012, 107962, 521156,107086)
Page16insetExtent = Page16insetExtent
arcpy.RefreshActiveView()
print PageNumber + str(pgIndex)
tempDDP.exportToPDF(temp_filename, "RANGE", pgIndex)
finalPdf.appendPages(temp_filename)
print PageNumber + str(pgIndex)
Page16.elementPositionX = 50
arcpy.RefreshActiveView()
# Else Fuction takes care of the pages that dont have insets and just itterates through using the loop on line 28
else :
tempDDP.exportToPDF(temp_filename, "RANGE", pgIndex)
finalPdf.appendPages(temp_filename)
print PageNumber + str(pgIndex)
# Clean up
del tempMap
# Update the properties of the final pdf
finalPdf.updateDocProperties(pdf_open_view="USE_THUMBS",
pdf_layout="SINGLE_PAGE")
# Save your result
finalPdf.saveAndClose()
end_time = datetime.now()
print('Duration: {}'.format(end_time - start_time))
I believe your problem is that when the pgIndex is 15 it performs the export as intended. Then it checks if the pgIndex is 16. The pgIndex is not 16 so it drops into the else and re-exports without the inset maps. I would recommend changing the second if to an elif
I have this problem with chaco.
In the plot, I need select some points (are points that I generate). This points, I can select with two tools: RangenSelection and ScatterInspector. If I work with only one tool, the code work well and I can detect with points I select, but when I work with both tools, both tools write the same metadata name: selections. This is the most important part of the code:
#this are all the tools.append
my_plot.tools.append(ScatterInspector(my_plot, selection_mode="toggle", persistent_hover=False))
my_plot.overlays.append(
ScatterInspectorOverlay(my_plot,
hover_color = "transparent",
hover_marker_size = 10,
hover_outline_color = "purple",
hover_line_width = 2,
selection_marker_size = 8,
selection_color = "red")
)
my_plot.tools.append(RangeSelection(my_plot, left_button_selects = False, rigth_button_selects = True, auto_handle_event = False))
my_plot.overlays.append(RangeSelectionOverlay(component=my_plot))
my_plot.tools.append(PanTool(my_plot))
my_plot.overlays.append(ZoomTool(my_plot, drag_button="right"))
return plot
#the rest of the code
def _metadata_handler(self):
sel_indices = self.index_datasource.metadata.get('selections', [])
su = self.index_datasource.metadata.get('annotations', [])
print su
print "Selection indices:", sel_indices
def _plot_default(self):
plot = _create_plot_component()
# Retrieve the plot hooked to the tool.
my_plot = plot.plots["my_plot"][0]
# Set up the trait handler for the selection
self.index_datasource = my_plot.index
self.index_datasource.on_trait_change(self._metadata_handler,
"metadata_changed")
return plot
When I run the code, and see what are in annotations, is always empty. But in selections the code write with both tools and this give an error.
How can I tell to some tool in where metadata key write??
Thanks for your help.
The solution is put metadata_name="annotations" in RangeSelection and in RangeSelectionOverlay