scanny / python-pptx

Create Open XML PowerPoint documents in Python
MIT License
2.45k stars 528 forks source link

Charts not using theme/Accent colors #529

Open mszbot opened 5 years ago

mszbot commented 5 years ago

When inserting a chart into a chart placeholder (not creating a brand new chart), if the df/table has more than 5 columns of data then the colors used in the chart are approx 13% darker than the Accent colors.

For example, if the first accent color is 'purple' rgb(114, 87, 183), then the chart will use rgb(100,62,165) instead.

Image shows 2 charts, one chart with less than 5 columns of data and the other chart with more than 5 columns of data. As you can see, the chart with less than 5 columns of data uses the correct accent colors from the color palette from the slide master, the other doesnt. chart colour issue

Here's the pptx generated attached. See the following slides: slide 5: chart with 5 columns of data slide 6: chart with 5+ columns of data Chart colour issue.pptx

I have tested this on several pptxs, fresh ones, old ones and all of them have the same issue.

mszbot commented 5 years ago

Been digging a bit deeper and I believe the problem is somehow linked to the 'chart styles '.

PPTX below shows the same chart with different chart styles, from 1 to 48. None of these use the accent colors. color_test 1 to 48.pptx

Apparently, there are more than 48 chart styles, 201 to 352 are valid chart styles but not documented. See discussion here: https://stackoverflow.com/questions/38813522/themed-chart-styles-for-excel-charts-using-vba

Excel below shows all the available chart styles, 1 to 48 and 201 to 325. AddShapes2ChartStyles.xlsx

Interestingly, none of the 1 to 48 chart types use Accent colors either, but a few from the 201 to 325 range do. Namely, 201. If you click on one of the first 6 series and check the color you will see it selects the theme color.

Based on this information I tried to replace chart's style with "201" but it corrupted it.

 ele = chart._chartSpace.xpath(r'c:style')[0]
 ele.set("val", "201")

Not sure what to do next?

scanny commented 5 years ago

I think what you'll find if you inspect the XML for a PPTX file that looks the way you want is that the 201 chart style is implemented as a "schema extension" with a fallback for earlier versions of PowerPoint and clients that don't support the extension.

I don't remember the namespace and tagname exactly, but it will be right at the top, where the style element goes.

You can get a quick look with

print(chart._element.xml)

But using opc-diag is probably a better long-term bet, since it allows you to actually extract, edit, and then repackage the XML to experiment.

Other folks have also encountered this problem, so if we can get clear on what it takes to make it work we can consider elaborating the chart.chart_style interface to accept the high-order numbers.

mszbot commented 5 years ago

Just had a go at this and it looks like python-pptx does not add 'colors1.xml' and 'style1.xml' files in the '..ppt/charts' folder. It only adds a 'chart1.xml' file.

I got this error message: KeyError: "No item with name 'ppt/charts/colors1.xml'"

It seems each chart in a pptx comes with with these two files, when creating charts manually by hand.

Next, I tried to unzip/extract the python-pptx generated pptx file and copy and paste the missing files into the correct location, then I repackaged it. This had no affect on the chart color.

Then I tried to compare the two pptxs and this is what it returned:

--- before/[Content_Types].xml

+++ new_pythoncht/[Content_Types].xml

@@ -7,8 +7,6 @@

   <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
   <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
   <Override PartName="/ppt/charts/chart1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>
-  <Override PartName="/ppt/charts/colors1.xml" ContentType="application/vnd.ms-office.chartcolorstyle+xml"/>
-  <Override PartName="/ppt/charts/style1.xml" ContentType="application/vnd.ms-office.chartstyle+xml"/>
   <Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/>
   <Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
   <Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>

--- before/ppt/charts/_rels/chart1.xml.rels

+++ new_pythoncht/ppt/charts/_rels/chart1.xml.rels

@@ -1,6 +1,4 @@

 <?xml version='1.0' encoding='UTF-8' standalone='yes'?>
 <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
-  <Relationship Id="x" Type="http://schemas.microsoft.com/office/2011/relationships/chartColorStyle" Target="colors1.xml"/>
-  <Relationship Id="x" Type="http://schemas.microsoft.com/office/2011/relationships/chartStyle" Target="style1.xml"/>
   <Relationship Id="x" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" Target="../embeddings/Microsoft_Excel_Worksheet.xlsx"/>
 </Relationships>
--- before/ppt/charts/chart1.xml

+++ new_pythoncht/ppt/charts/chart1.xml

@@ -7,53 +7,16 @@

     >
   <c:date1904 val="0"/>
   <c:lang val="en-US"/>
-  <c:roundedCorners val="0"/>
-  <mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
-    <mc:Choice xmlns:c14="http://schemas.microsoft.com/office/drawing/2007/8/2/chart" Requires="c14">
-      <c14:style val="102"/>
-    </mc:Choice>
-    <mc:Fallback>
-      <c:style val="2"/>
-    </mc:Fallback>
-  </mc:AlternateContent>
+  <c:roundedCorners val="1"/>
+  <c:style val="2"/>
   <c:chart>
-    <c:title>
-      <c:overlay val="0"/>
-      <c:spPr>
-        <a:noFill/>
-        <a:ln>
-          <a:noFill/>
-        </a:ln>
-        <a:effectLst/>
-      </c:spPr>
-      <c:txPr>
-        <a:bodyPr rot="0" spcFirstLastPara="1" vertOverflow="ellipsis" vert="horz" wrap="square" anchor="ctr" anchorCtr="1"/>
-        <a:lstStyle/>
-        <a:p>
-          <a:pPr>
-            <a:defRPr sz="1862" b="0" i="0" u="none" strike="noStrike" kern="1200" spc="0" baseline="0">
-              <a:solidFill>
-                <a:schemeClr val="tx1">
-                  <a:lumMod val="65000"/>
-                  <a:lumOff val="35000"/>
-                </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="en-US"/>
-        </a:p>
-      </c:txPr>
-    </c:title>
-    <c:autoTitleDeleted val="0"/>
+    <c:autoTitleDeleted val="1"/>
     <c:plotArea>
       <c:layout/>
       <c:barChart>
         <c:barDir val="col"/>
         <c:grouping val="percentStacked"/>
-        <c:varyColors val="0"/>
+        <c:varyColors val="1"/>
         <c:ser>
           <c:idx val="0"/>
           <c:order val="0"/>
scanny commented 5 years ago

You can see the alternate content bit I mentioned here in your last paste:

  <mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
    <mc:Choice xmlns:c14="http://schemas.microsoft.com/office/drawing/2007/8/2/chart" Requires="c14">
      <c14:style val="102"/>
    </mc:Choice>
    <mc:Fallback>
      <c:style val="2"/>
    </mc:Fallback>
  </mc:AlternateContent>
  <c:roundedCorners val="1"/>
  <c:style val="2"/>

Basically what's happening is it's saying "Hey client, if you understand c14 --http://schemas.microsoft.com/office/drawing/2007/8/2/chart, then make the chart style 102. Otherwise make it 2".

This allows earlier versions to "fall-back" to the behavior PowerPoint had back then while newer versions can do something "better".


Adding new "parts" (e.g. xyz.xml) to a package will do nothing, as you saw. All parts must be related to the correct other parts. In particular, it looks like chart colors and chart styles need to be related to the chart they go with.

The presence of these is probably related to the version of PowerPoint you're using and may also have to do with the use of a chart template (which is optional and not used by default PowerPoint installation). I've never come across them for example and I use the latest Mac version.

It could be interesting to have a look inside those though and see what you find.


You'll have to say more about what code you executed where to get the error you mentioned. I can't make it out from the bare error.

mszbot commented 5 years ago

All the files outlined in the post below are available here: https://drive.google.com/drive/folders/1_-tyAFWCryMGzhGAfjaOymf76USkNkTu?usp=sharing

Here's what I've done:

  1. I created a pie chart with 6 data points via python-pptx and named the file created-by-python-pptx.pptx

  2. I downloaded the PowerPoint embedded in this library - pptx/templates/default.pptx, using this pptx I created a pie chart with 6 data points by hand using the blank slide layout, and named the file created-by-hand.pptx

  3. Then I used this opc command: opc diff created-by-hand.pptx created-by-python-pptx.pptx. I got this error again: image

  4. I extracted both pptx files to find out which pptx file was missing 'colors1.xml' and which had it

  5. created-by-hand.pptx had colors1.xml and style1.xml files, created-by-python-pptx did not.

  6. I then checked out the xml in colors1.xml and styles1.xml:

  7. colors1.xml looks like this: image

  8. styles1.xml looks like this, it had many lines but here's the first few lines : image

  9. I then did some googling and came across a msdn forum where a microsoft escalation engineer says that these two xml files are 'used when changing the chart style for a chart'. Link to discussion: https://bit.ly/2YdLmeH image

I'm using the latest windows machine, office 365. I ran this test on a couple of other windows machines and had the same results. I'm beginning to think that this must be a mac vs windows issue? Don't have a mac to test unfortunately.

Other observations I made during my tests:

  1. using python-pptx, if you insert/create a chart with 5 data points, then the data points correctly use the accent colors. However, if you manually add a new data point then all the colors automatically change to an extended version. If you delete the 6th data point the colors automatically go back to using the accent colors.
  2. creating a chart manually, by hand, selects the first chart style is auto-selected image
  3. using python-pptx, no chart style is selected, despite specifying one (or not) image

Hope this helps

scanny commented 5 years ago

On the "no item with name colors1.xml" error, that is raised by opc-diag just because it's not sophisticated enough to say "the first one has this part and the second one doesn't", but that's all that error is really telling you.

So it looks like there's a way to change this shade/tint adjustment behavior, at least until the seventh data point by adding these extra "chart style application" parts. That feature is not likely to be added soon, so I think the only alternative at the moment is to manually set the colors of each segment to the ones you want.

scanny commented 5 years ago

Ok, so I ended up encountering a need to "fix" this problem myself. After several false starts digging into the style1.xml and colors1.xml parts that get added beside a chart when PowerPoint creates a new chart (or applies a chart style), it turns out my original advice (change colors explicitly) is the best solution, although not for the reasons I was originally thinking.

Here's a paste-able function to get it done that is pretty robust:

from pptx.enum.dml import MSO_THEME_COLOR

def explicitly_apply_accent_colors(plot):
    """Make first up-to-six chart colors explicitly the accent colors.

    Works for both single-series (e.g. pie) and multi-series charts.
    """
    accent_colors = (
        MSO_THEME_COLOR.ACCENT_1,
        MSO_THEME_COLOR.ACCENT_2,
        MSO_THEME_COLOR.ACCENT_3,
        MSO_THEME_COLOR.ACCENT_4,
        MSO_THEME_COLOR.ACCENT_5,
        MSO_THEME_COLOR.ACCENT_6,
    )

    # ---keeps single-series colors after six from all showing as Accent 1---
    plot.vary_by_categories = True

    # ---apply to each point if only one series, otherwise apply to each series---
    targets = plot.series[0].points if len(plot.series) == 1 else plot.series

    for target, accent_color in zip(targets, accent_colors):
        fill = target.format.fill
        fill.solid()
        fill.fore_color.theme_color = accent_color

It turns out this is exactly what PowerPoint does when it applies a chart-style. The style{n}.xml and colors{n}.xml files are somewhat interesting, but no settings in either of these will produce the "colors-don't-darken-on-sixth-one" behavior we're looking for here. In addition to adding these two parts when the chart-style is applied, PowerPoint explicitly sets the fill colors for the chart bars (or whatever symbols the chart-type uses to represent values), and that's what actually produces the desired behavior.

The colors{n}.xml file allows you to specify how PowerPoint chooses the seventh and later colors by specifying particular tints and shades to apply. The style{n}.xml allows setting of line, font, and fill of most chart properties, relative to the theme. Chart "points" are conspicuously missing as a settings target, but you can specify axis and gridline properties, that sort of thing.

Between the two of these "extra" files, a chart that's carefully formatted in one deck doesn't change to ugly when pasted into another with a different theme or whatever. At least that's the rationale as far as I can tell.