Open diego-EA opened 3 years ago
Can you do it in PowerPoint? If PowerPoint doesn't support it, it is unlikely python-pptx
will ever be able to.
Yes, in PowerPoint it's possible to format each data label individually (including presence and format of border).
Okay, then the XML must support it. There's no API support for that in python-pptx
so you'd have to manipulate the XML there directly. If you're willing to dig into that then the best place to start is probably having a look at the before an after XML:
prs = Presentation("deck-with-bar-chart-with-no-data-label-borders.pptx")
print("before XML == %s" % prs. ... .points[0].data_label._ser.xml)
prs = Presentation("deck-with-bar-chart-with-data-label-borders-added-by-hand.pptx")
print("after XML == %s" % prs. ... .points[0].data_label._ser.xml)
That should show the difference. Make the bar char small, like three bars so the XML doesn't scroll off the page.
Many thanks for the advise. Both XML outputs are different (I attach the differences in the image below). How can I change those XML properties in the data labels with no border? I have serialized (pickle) the output of data_label._ser.xml and then I tried to apply it to the labels with no borders;
my_border = pickle.load(open("C:\...\data_label_border.p", "rb"))
shape.chart.plots[0].series[0].points[0].data_label._ser.xml = my_border
But I got the following error:
AttributeError: can't set attribute
How could I get advantage of those XML-differences to achieve my purpose?
I can't read the XML in that format. Just paste it in here as a properly indented code block along with the line of code you used to print it and I can help you find the element you need to work on.
Sure, here is the section for the shape with borders in the chart labels:
<c:dLbls>
<c:dLbl>
<c:idx val="0"/>
<c:spPr>
<a:noFill/>
<a:ln w="28575">
<a:solidFill>
<a:srgbClr val="671A3D"/>
</a:solidFill>
</a:ln>
<a:effectLst/>
</c:spPr>
<c:txPr>
<a:bodyPr rot="0" spcFirstLastPara="1" vertOverflow="ellipsis" vert="horz" wrap="square" lIns="38100" tIns="19050" rIns="38100" bIns="19050" anchor="ctr" anchorCtr="1">
<a:spAutoFit/>
</a:bodyPr>
<a:lstStyle/>
<a:p>
<a:pPr>
<a:defRPr sz="1197" b="0" i="0" u="none" strike="noStrike" kern="1200" baseline="0">
<a:solidFill>
<a:schemeClr val="tx1">
<a:lumMod val="75000"/>
<a:lumOff val="25000"/>
</a:schemeClr>
</a:solidFill>
<a:latin typeface="+mn-lt"/>
<a:ea typeface="+mn-ea"/>
<a:cs typeface="+mn-cs"/>
</a:defRPr>
</a:pPr>
<a:endParaRPr lang="de-DE"/>
</a:p>
</c:txPr>
<c:showLegendKey val="0"/>
<c:showVal val="1"/>
<c:showCatName val="0"/>
<c:showSerName val="0"/>
<c:showPercent val="0"/>
<c:showBubbleSize val="0"/>
<c:extLst>
<c:ext xmlns:c16="http://schemas.microsoft.com/office/drawing/2014/chart" uri="{C3380CC4-5D6E-409C-BE32-E72D297353CC}">
<c16:uniqueId val="{00000000-4FFA-4F1E-80B2-0345540D9C12}"/>
</c:ext>
</c:extLst>
</c:dLbl>
<c:dLbl>
<c:idx val="1"/>
<c:spPr>
<a:noFill/>
<a:ln w="28575">
<a:solidFill>
<a:srgbClr val="671A3D"/>
</a:solidFill>
</a:ln>
And here is the same section for the shape without borders in the chart labels:
<c:dLbls>
<c:dLbl>
<c:idx val="1"/>
<c:spPr>
<a:noFill/>
<a:ln w="28575">
<a:noFill/>
</a:ln>
Okay, so this might be pretty easy then:
dLbl = point.data_label._dLbl
line = pptx.dml.line.LineFormat(dLbl.spPr)
line.color.rgb = RGBColor(0xFF, 0x00, 0x00)
It looks like the spPr
element is removed if you set the label to a custom string, so you might need dLbl.get_or_add_spPr()
in the second line instead of dLbl.spPr
if for some reason it complains about not having one.
I have just tried those lines but the second one did not work for me: dLbl
is a NoneType
object and it has neither attribute spPr
nor method get_or_add_spPr()
.
The following lines worked but they placed the border around the bar and not around the label:
dl = point.data_label
line = pptx.dml.line.LineFormat(dl._element.get_or_add_spPr())
line.color.rgb = RGBColor(0, 0, 255)
Sorry, I ignored the 'if you set the label to a custom string' part of your comment. When I take that into account, your solution works perfectly (I finally got the border around the label). Many thanks. I paste here my final code to get the border around the first label of the chart:
tf = shape.chart.plots[0].series[0].points[0].data_label.text_frame
tf.word_wrap = True
tf.text = str(shape.chart.series[0].values[0])
dLbl = shape.chart.plots[0].series[0].points[0].data_label._dLbl
line = pptx.dml.line.LineFormat(dLbl.get_or_add_spPr())
line.color.rgb = RGBColor(0, 0, 255)
Okay, so I think your code is equivalent to this function called with parameters (0, 0):
def box_data_label(series_idx, point_idx):
"""Draw box around value in data-label for point at `point_idx` in `series_idx`."""
series = shape.chart.series[series_idx]
data_label = series.points[point_idx].data_label
# --- set data-label text to numeric value of its point ---
point_value = series.values[point_idx]
data_label.text_frame.text = str(point_value)
data_label.text_frame.word_wrap = True
# --- display a border (box) around the data-label perimeter ---
line = pptx.dml.line.LineFormat(data_label._dLbl.get_or_add_spPr())
line.color.rgb = RGBColor(0, 0, 255)
Why does the text of the data label need to be set? I vaguely remember that if you set anything on a data-label it becomes a custom data label and then you have to set everything for that data-label. Is that why? What do you get if comment out the middle three lines and don't set the text or word-wrap? Does it just show an empty box then?
If I don't set the word_wrap
of the text frame to True
I get the problems mentioned here (dLbl
is a NoneType
object): https://github.com/scanny/python-pptx/issues/716#issuecomment-880473764
And if I set the word_wrap
of the text frame to True
I need to set everything for that data label, including it's value (otherwise the data label doesn't show any value).
Ah, okay, so it's the "if you want to set any of it you have to set all of it" characteristic. You don't have to set .word_wrap
per se, that's optional, but you do have to set .text
, or .has_text_frame = True
, or .position
otherwise the dLbl
element won't exist yet (is a NoneType
object). The dLbl
element isn't created until the first time you set one of those three.
It might be worth a try to set .position
to one of these values without setting the text and see if you can then get a box around the data-label without having to make the text "static". An unfortunate consequence of setting custom text is that it doesn't automatically update if you change the chart values, like do a chart.replace_data()
the next month or whatever. That's something to watch out for. Once you've set the text you'll need to explicitly make any updates as they become necessary.
You are right: my data labels are 'static' (if I update chart values, labels remain immutable). I tried both suggestion to avoid .word_wrap
and here are the results:
.has_text_frame = True
I become the border around the label, but the value disappears..position = XL_LABEL_POSITION.OUTSIDE_END
I get both value and border (and value remains dynamic). So this looks like the best solution.Okay, @diego-EA that's really good to know. So I think the short answer to your original question is this::
def box_data_label(chart, series_idx, point_idx):
"""Draw box around value in data-label for point at `point_idx` in `series_idx`."""
series = chart.series[series_idx]
data_label = series.points[point_idx].data_label
# --- set position of data-label as trick to create `dLbl` element for it ---
data_label.position = XL_LABEL_POSITION.OUTSIDE_END
# --- display a border (box) around the data-label perimeter ---
line = pptx.dml.line.LineFormat(data_label._dLbl.get_or_add_spPr())
line.color.rgb = RGBColor(0, 0, 255)
It's a pretty big advantage to be able to retain the auto-updating behavior, so this will be good to know for others who find this on search.
Exactly, that's the answer to my original question. Thank you a lot!
Install
pip install python-pptx -U
Code
from pptx.util import Inches
from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_DATA_LABEL_POSITION
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])
shapes = slide.shapes
shapes.title.text = ' '
chart_data = CategoryChartData()
chart_data.categories = ['East', 'West', 'Midwest']
chart_data.add_series('Series 1', (19.2, 21.4, 16.7))
x, y, cx, cy = Inches(2), Inches(2), Inches(6), Inches(4.5)
graphic_frame = slide.shapes.add_chart(XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data)
chart = graphic_frame.chart
from pptx.dml.color import RGBColor
from pptx.dml.line import LineFormat
series = chart.series[0]
for point in series.points:
data_label = point.data_label
data_label.position = XL_DATA_LABEL_POSITION.OUTSIDE_END
line = LineFormat(data_label._dLbl.get_or_add_spPr())
line.color.rgb = RGBColor(255, 0, 0)
prs.save('test.pptx')
I have a bar chart and I need to put a line border around the data labels. I have read that works for text boxs
but I don't know if there is anyway to achieve that for data labels. I can reach them
but I don't find a way of placing there a border.