aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/pptx/chart/plot.py
blob: 6e7235855f011db8540c53b1fd713cd672900fa8 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
"""Plot-related objects.

A plot is known as a chart group in the MS API. A chart can have more than one plot overlayed on
each other, such as a line plot layered over a bar plot.
"""

from __future__ import annotations

from pptx.chart.category import Categories
from pptx.chart.datalabel import DataLabels
from pptx.chart.series import SeriesCollection
from pptx.enum.chart import XL_CHART_TYPE as XL
from pptx.oxml.ns import qn
from pptx.oxml.simpletypes import ST_BarDir, ST_Grouping
from pptx.util import lazyproperty


class _BasePlot(object):
    """
    A distinct plot that appears in the plot area of a chart. A chart may
    have more than one plot, in which case they appear as superimposed
    layers, such as a line plot appearing on top of a bar chart.
    """

    def __init__(self, xChart, chart):
        super(_BasePlot, self).__init__()
        self._element = xChart
        self._chart = chart

    @lazyproperty
    def categories(self):
        """
        Returns a |category.Categories| sequence object containing
        a |category.Category| object for each of the category labels
        associated with this plot. The |category.Category| class derives from
        ``str``, so the returned value can be treated as a simple sequence of
        strings for the common case where all you need is the labels in the
        order they appear on the chart. |category.Categories| provides
        additional properties for dealing with hierarchical categories when
        required.
        """
        return Categories(self._element)

    @property
    def chart(self):
        """
        The |Chart| object containing this plot.
        """
        return self._chart

    @property
    def data_labels(self):
        """
        |DataLabels| instance providing properties and methods on the
        collection of data labels associated with this plot.
        """
        dLbls = self._element.dLbls
        if dLbls is None:
            raise ValueError("plot has no data labels, set has_data_labels = True first")
        return DataLabels(dLbls)

    @property
    def has_data_labels(self):
        """
        Read/write boolean, |True| if the series has data labels. Assigning
        |True| causes data labels to be added to the plot. Assigning False
        removes any existing data labels.
        """
        return self._element.dLbls is not None

    @has_data_labels.setter
    def has_data_labels(self, value):
        """
        Add, remove, or leave alone the ``<c:dLbls>`` child element depending
        on current state and assigned *value*. If *value* is |True| and no
        ``<c:dLbls>`` element is present, a new default element is added with
        default child elements and settings. When |False|, any existing dLbls
        element is removed.
        """
        if bool(value) is False:
            self._element._remove_dLbls()
        else:
            if self._element.dLbls is None:
                dLbls = self._element._add_dLbls()
                dLbls.showVal.val = True

    @lazyproperty
    def series(self):
        """
        A sequence of |Series| objects representing the series in this plot,
        in the order they appear in the plot.
        """
        return SeriesCollection(self._element)

    @property
    def vary_by_categories(self):
        """
        Read/write boolean value specifying whether to use a different color
        for each of the points in this plot. Only effective when there is
        a single series; PowerPoint automatically varies color by series when
        more than one series is present.
        """
        varyColors = self._element.varyColors
        if varyColors is None:
            return True
        return varyColors.val

    @vary_by_categories.setter
    def vary_by_categories(self, value):
        self._element.get_or_add_varyColors().val = bool(value)


class AreaPlot(_BasePlot):
    """
    An area plot.
    """


class Area3DPlot(_BasePlot):
    """
    A 3-dimensional area plot.
    """


class BarPlot(_BasePlot):
    """
    A bar chart-style plot.
    """

    @property
    def gap_width(self):
        """
        Width of gap between bar(s) of each category, as an integer
        percentage of the bar width. The default value for a new bar chart is
        150, representing 150% or 1.5 times the width of a single bar.
        """
        gapWidth = self._element.gapWidth
        if gapWidth is None:
            return 150
        return gapWidth.val

    @gap_width.setter
    def gap_width(self, value):
        gapWidth = self._element.get_or_add_gapWidth()
        gapWidth.val = value

    @property
    def overlap(self):
        """
        Read/write int value in range -100..100 specifying a percentage of
        the bar width by which to overlap adjacent bars in a multi-series bar
        chart. Default is 0. A setting of -100 creates a gap of a full bar
        width and a setting of 100 causes all the bars in a category to be
        superimposed. A stacked bar plot has overlap of 100 by default.
        """
        overlap = self._element.overlap
        if overlap is None:
            return 0
        return overlap.val

    @overlap.setter
    def overlap(self, value):
        """
        Set the value of the ``<c:overlap>`` child element to *int_value*,
        or remove the overlap element if *int_value* is 0.
        """
        if value == 0:
            self._element._remove_overlap()
            return
        self._element.get_or_add_overlap().val = value


class BubblePlot(_BasePlot):
    """
    A bubble chart plot.
    """

    @property
    def bubble_scale(self):
        """
        An integer between 0 and 300 inclusive indicating the percentage of
        the default size at which bubbles should be displayed. Assigning
        |None| produces the same behavior as assigning `100`.
        """
        bubbleScale = self._element.bubbleScale
        if bubbleScale is None:
            return 100
        return bubbleScale.val

    @bubble_scale.setter
    def bubble_scale(self, value):
        bubbleChart = self._element
        bubbleChart._remove_bubbleScale()
        if value is None:
            return
        bubbleScale = bubbleChart._add_bubbleScale()
        bubbleScale.val = value


class DoughnutPlot(_BasePlot):
    """
    An doughnut plot.
    """


class LinePlot(_BasePlot):
    """
    A line chart-style plot.
    """


class PiePlot(_BasePlot):
    """
    A pie chart-style plot.
    """


class RadarPlot(_BasePlot):
    """
    A radar-style plot.
    """


class XyPlot(_BasePlot):
    """
    An XY (scatter) plot.
    """


def PlotFactory(xChart, chart):
    """
    Return an instance of the appropriate subclass of _BasePlot based on the
    tagname of *xChart*.
    """
    try:
        PlotCls = {
            qn("c:areaChart"): AreaPlot,
            qn("c:area3DChart"): Area3DPlot,
            qn("c:barChart"): BarPlot,
            qn("c:bubbleChart"): BubblePlot,
            qn("c:doughnutChart"): DoughnutPlot,
            qn("c:lineChart"): LinePlot,
            qn("c:pieChart"): PiePlot,
            qn("c:radarChart"): RadarPlot,
            qn("c:scatterChart"): XyPlot,
        }[xChart.tag]
    except KeyError:
        raise ValueError("unsupported plot type %s" % xChart.tag)

    return PlotCls(xChart, chart)


class PlotTypeInspector(object):
    """
    "One-shot" service object that knows how to identify the type of a plot
    as a member of the XL_CHART_TYPE enumeration.
    """

    @classmethod
    def chart_type(cls, plot):
        """
        Return the member of :ref:`XlChartType` that corresponds to the chart
        type of *plot*.
        """
        try:
            chart_type_method = {
                "AreaPlot": cls._differentiate_area_chart_type,
                "Area3DPlot": cls._differentiate_area_3d_chart_type,
                "BarPlot": cls._differentiate_bar_chart_type,
                "BubblePlot": cls._differentiate_bubble_chart_type,
                "DoughnutPlot": cls._differentiate_doughnut_chart_type,
                "LinePlot": cls._differentiate_line_chart_type,
                "PiePlot": cls._differentiate_pie_chart_type,
                "RadarPlot": cls._differentiate_radar_chart_type,
                "XyPlot": cls._differentiate_xy_chart_type,
            }[plot.__class__.__name__]
        except KeyError:
            raise NotImplementedError(
                "chart_type() not implemented for %s" % plot.__class__.__name__
            )
        return chart_type_method(plot)

    @classmethod
    def _differentiate_area_3d_chart_type(cls, plot):
        return {
            ST_Grouping.STANDARD: XL.THREE_D_AREA,
            ST_Grouping.STACKED: XL.THREE_D_AREA_STACKED,
            ST_Grouping.PERCENT_STACKED: XL.THREE_D_AREA_STACKED_100,
        }[plot._element.grouping_val]

    @classmethod
    def _differentiate_area_chart_type(cls, plot):
        return {
            ST_Grouping.STANDARD: XL.AREA,
            ST_Grouping.STACKED: XL.AREA_STACKED,
            ST_Grouping.PERCENT_STACKED: XL.AREA_STACKED_100,
        }[plot._element.grouping_val]

    @classmethod
    def _differentiate_bar_chart_type(cls, plot):
        barChart = plot._element
        if barChart.barDir.val == ST_BarDir.BAR:
            return {
                ST_Grouping.CLUSTERED: XL.BAR_CLUSTERED,
                ST_Grouping.STACKED: XL.BAR_STACKED,
                ST_Grouping.PERCENT_STACKED: XL.BAR_STACKED_100,
            }[barChart.grouping_val]
        if barChart.barDir.val == ST_BarDir.COL:
            return {
                ST_Grouping.CLUSTERED: XL.COLUMN_CLUSTERED,
                ST_Grouping.STACKED: XL.COLUMN_STACKED,
                ST_Grouping.PERCENT_STACKED: XL.COLUMN_STACKED_100,
            }[barChart.grouping_val]
        raise ValueError("invalid barChart.barDir value '%s'" % barChart.barDir.val)

    @classmethod
    def _differentiate_bubble_chart_type(cls, plot):
        def first_bubble3D(bubbleChart):
            results = bubbleChart.xpath("c:ser/c:bubble3D")
            return results[0] if results else None

        bubbleChart = plot._element
        bubble3D = first_bubble3D(bubbleChart)

        if bubble3D is None:
            return XL.BUBBLE
        if bubble3D.val:
            return XL.BUBBLE_THREE_D_EFFECT
        return XL.BUBBLE

    @classmethod
    def _differentiate_doughnut_chart_type(cls, plot):
        doughnutChart = plot._element
        explosion = doughnutChart.xpath("./c:ser/c:explosion")
        return XL.DOUGHNUT_EXPLODED if explosion else XL.DOUGHNUT

    @classmethod
    def _differentiate_line_chart_type(cls, plot):
        lineChart = plot._element

        def has_line_markers():
            matches = lineChart.xpath('c:ser/c:marker/c:symbol[@val="none"]')
            if matches:
                return False
            return True

        if has_line_markers():
            return {
                ST_Grouping.STANDARD: XL.LINE_MARKERS,
                ST_Grouping.STACKED: XL.LINE_MARKERS_STACKED,
                ST_Grouping.PERCENT_STACKED: XL.LINE_MARKERS_STACKED_100,
            }[plot._element.grouping_val]
        else:
            return {
                ST_Grouping.STANDARD: XL.LINE,
                ST_Grouping.STACKED: XL.LINE_STACKED,
                ST_Grouping.PERCENT_STACKED: XL.LINE_STACKED_100,
            }[plot._element.grouping_val]

    @classmethod
    def _differentiate_pie_chart_type(cls, plot):
        pieChart = plot._element
        explosion = pieChart.xpath("./c:ser/c:explosion")
        return XL.PIE_EXPLODED if explosion else XL.PIE

    @classmethod
    def _differentiate_radar_chart_type(cls, plot):
        radarChart = plot._element
        radar_style = radarChart.xpath("c:radarStyle")[0].get("val")

        def noMarkers():
            matches = radarChart.xpath("c:ser/c:marker/c:symbol")
            if matches and matches[0].get("val") == "none":
                return True
            return False

        if radar_style is None:
            return XL.RADAR
        if radar_style == "filled":
            return XL.RADAR_FILLED
        if noMarkers():
            return XL.RADAR
        return XL.RADAR_MARKERS

    @classmethod
    def _differentiate_xy_chart_type(cls, plot):
        scatterChart = plot._element

        def noLine():
            return bool(scatterChart.xpath("c:ser/c:spPr/a:ln/a:noFill"))

        def noMarkers():
            symbols = scatterChart.xpath("c:ser/c:marker/c:symbol")
            if symbols and symbols[0].get("val") == "none":
                return True
            return False

        scatter_style = scatterChart.xpath("c:scatterStyle")[0].get("val")

        if scatter_style == "lineMarker":
            if noLine():
                return XL.XY_SCATTER
            if noMarkers():
                return XL.XY_SCATTER_LINES_NO_MARKERS
            return XL.XY_SCATTER_LINES

        if scatter_style == "smoothMarker":
            if noMarkers():
                return XL.XY_SCATTER_SMOOTH_NO_MARKERS
            return XL.XY_SCATTER_SMOOTH

        return XL.XY_SCATTER