Open gergness opened 2 years ago
A slightly less ugly and more robust method would be the following:
for point in chart.series[0].points:
point_invert_if_negative(point, False)
def point_set_invert_if_negative(point: Point, value: bool):
"""Set the invert-if-negative attribute for `point` to `value`."""
def get_or_add_invertIfNegative(dPt):
invertIfNegatives = dPt.xpath("./c:invertIfNegative")
if invertIfNegatives:
return invertIfNegatives[0]
invertIfNegative = OxmlElement("c:invertIfNegative")
dPt.insert_element_before(
invertIfNegative,
(
"c:marker",
"c:bubble3D",
"c:explosion",
"c:spPr",
"c:pictureOptions",
"c:extLst",
)
)
dPt = point._ser.get_or_add_dPt_for_point(self._idx)
invertIfNegative = get_or_add_invertIfNegative(dPt)
invertIfNegative.set("val", 1 if value else 0)
Less ugly here meaning you can hide the point_set_invert_if_negative()
function somewhere you don't have to look at it and the calling code get quite simple.
This implementation (not tested by the way) is essentially what would be happening in python-pptx
if this feature was implemented, just the call would become point.invert_if_negative = False
in that case.
The main robustness difference here is that the <c:invertIfNegative>
element is inserted in the right sequence among the eight possible children of <c:dPt>
. Typically that matters and might come up to bite later on after a seemingly unrelated change if not attended to here.
If you want to give this a try I'm happy to help with any debugging that might be necessary.
A fuller solution (actually buiding this in) would likely require sponsorship. I'd say not to exceed two days, likely only one.
Awesome, thanks so much for the quick response and the tip about order mattering.
I think we're okay with this hack for now, but I definitely owe you (another) coffee or beer! Hope you're doing well!
For posterity, here are a few edits to that function to get it working.
from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.chart.series import BarSeries
from pptx.dml.color import RGBColor
from pptx.enum.chart import XL_CHART_TYPE
from pptx.oxml.xmlchemy import OxmlElement
from pptx.util import Inches
def point_set_invert_if_negative(point, value):
"""Set the invert-if-negative attribute for `point` to `value`."""
def get_or_add_invertIfNegative(dPt):
invertIfNegatives = dPt.xpath("./c:invertIfNegative")
if invertIfNegatives:
return invertIfNegatives[0]
invertIfNegative = OxmlElement("c:invertIfNegative")
dPt.insert_element_before(
invertIfNegative,
*(
"c:marker",
"c:bubble3D",
"c:explosion",
"c:spPr",
"c:pictureOptions",
"c:extLst",
)
)
return invertIfNegative
dPt = point._ser.get_or_add_dPt_for_point(point._idx)
invertIfNegative = get_or_add_invertIfNegative(dPt)
invertIfNegative.set("val", "1" if value else "0")
# 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 = ['apple', 'banana', 'carrot']
chart_data.add_series('Series 1', (5.2, -2.1, -25))
# add chart to slide --------------------
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
# ---Set invert_if_negative=False for bar charts---
for ser in chart.series:
ser.invert_if_negative = False
# ---But also set the colors for each bar
points = chart.series[0].points
for idx, rgb in enumerate(("FF0000", "00FF00", "0000FF")):
if rgb is None:
continue
fill = points[idx].format.fill
fill.solid()
fill.fore_color.rgb = RGBColor.from_string(rgb)
# --- Also set the invertIfNegative on the points
point_set_invert_if_negative(points[idx], False)
prs.save('fixed-scanny.pptx')
Glad to hear you got it working and thanks very much for the working code, that will help others in future I'm sure :)
Greetings, I was going through the same path when trying to solve this issue, but looking into python-pptx internals (amazing BTW) I tried this locally and it works for my particular scenario. Would this change break other charts?
index 2974a226..94c99612 100644
--- a/pptx/oxml/chart/series.py
+++ b/pptx/oxml/chart/series.py
@@ -54,6 +54,7 @@ class CT_DPt(BaseOxmlElement):
"c:extLst",
)
idx = OneAndOnlyOne("c:idx")
+ invertIfNegative = ZeroOrOne("c:invertIfNegative", successors=_tag_seq[1:])
marker = ZeroOrOne("c:marker", successors=_tag_seq[3:])
spPr = ZeroOrOne("c:spPr", successors=_tag_seq[6:])
del _tag_seq```
It is currently possible to set
invertIfNegative
property on aBarSeries
, but when the fill colors for the data points within that bar series have been overridden, PowerPoint does not respect the series level property, it instead expects each data point to have its owninvertIfNegative
.I don't think it is currently possible to set this property on a data point without digging into python-pptx's internal XML representation. Would it be possible to add a property on
Point
(or possibly make a subclass ofPoint
for only points belonging to aBarSeries
) that allows settinginvertIfNegative
?Here's code to reproduce, an ugly work around, and below are the pptx files and images of them rendering in powerpoint.
ignores-invertIfNegative.pptx
fixed.pptx