python-pptx icon indicating copy to clipboard operation
python-pptx copied to clipboard

border for data labels

Open diego-EA opened this issue 4 years ago • 17 comments

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

    textbox.line.color.rgb = RGBColor(0x00, 0x16, 0xBC)

but I don't know if there is anyway to achieve that for data labels. I can reach them

   shape.chart.plots[0].series[0].points[0].data_label.textframe

but I don't find a way of placing there a border.

diego-EA avatar Jul 08 '21 08:07 diego-EA

Can you do it in PowerPoint? If PowerPoint doesn't support it, it is unlikely python-pptx will ever be able to.

scanny avatar Jul 08 '21 18:07 scanny

Yes, in PowerPoint it's possible to format each data label individually (including presence and format of border).

diego-EA avatar Jul 12 '21 07:07 diego-EA

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.

scanny avatar Jul 12 '21 16:07 scanny

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?

data_label_border

diego-EA avatar Jul 13 '21 09:07 diego-EA

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.

scanny avatar Jul 13 '21 16:07 scanny

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>

diego-EA avatar Jul 14 '21 10:07 diego-EA

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.

scanny avatar Jul 14 '21 16:07 scanny

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)

diego-EA avatar Jul 15 '21 07:07 diego-EA

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)

diego-EA avatar Jul 15 '21 09:07 diego-EA

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?

scanny avatar Jul 15 '21 19:07 scanny

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).

diego-EA avatar Jul 16 '21 08:07 diego-EA

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.

scanny avatar Jul 16 '21 16:07 scanny

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:

  • using .has_text_frame = True I become the border around the label, but the value disappears.
  • using .position = XL_LABEL_POSITION.OUTSIDE_END I get both value and border (and value remains dynamic). So this looks like the best solution.

diego-EA avatar Jul 20 '21 08:07 diego-EA

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.

scanny avatar Jul 20 '21 17:07 scanny

Exactly, that's the answer to my original question. Thank you a lot!

diego-EA avatar Jul 21 '21 07:07 diego-EA

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')

vba34520 avatar Feb 25 '22 07:02 vba34520

I am using a doughnut chart in python-pptx; I want to set the datalabels to br away from the chart (exactly 2 inches) as datalabel position is not sufficient, I am lloking for other alternatives:

for idx, point in enumerate(chart.series[0].points): dl = point.data_label dl.text_frame.text = f"{chart_data.categories[idx].label}" dl.show_leader_line = true for para in dl.text_frame.paragraphs: for rune in para.runs: rune.font.size = Pt(int(shape_filled_text_ls[1]) #set this datalabel 2 inches away from chart center so that leader lines are visible.

SaiRahul-Draup avatar Oct 26 '24 20:10 SaiRahul-Draup