ruby_pptx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +83 -0
  3. data/LICENSE +21 -0
  4. data/NOTICE +48 -0
  5. data/README.md +272 -0
  6. data/lib/ruby_pptx/action.rb +120 -0
  7. data/lib/ruby_pptx/autoshape_spec.rb +337 -0
  8. data/lib/ruby_pptx/builder.rb +228 -0
  9. data/lib/ruby_pptx/chart/categories.rb +208 -0
  10. data/lib/ruby_pptx/chart/chart.rb +443 -0
  11. data/lib/ruby_pptx/chart/combo.rb +230 -0
  12. data/lib/ruby_pptx/chart/data.rb +147 -0
  13. data/lib/ruby_pptx/chart/format.rb +647 -0
  14. data/lib/ruby_pptx/chart/workbook_writer.rb +212 -0
  15. data/lib/ruby_pptx/chart/xml_writer.rb +953 -0
  16. data/lib/ruby_pptx/chart/xy_data.rb +182 -0
  17. data/lib/ruby_pptx/core_ext.rb +12 -0
  18. data/lib/ruby_pptx/dml/color.rb +155 -0
  19. data/lib/ruby_pptx/dml/effect.rb +33 -0
  20. data/lib/ruby_pptx/dml/fill.rb +234 -0
  21. data/lib/ruby_pptx/element_proxy.rb +53 -0
  22. data/lib/ruby_pptx/enum/action.rb +46 -0
  23. data/lib/ruby_pptx/enum/base.rb +139 -0
  24. data/lib/ruby_pptx/enum/chart.rb +278 -0
  25. data/lib/ruby_pptx/enum/dml.rb +237 -0
  26. data/lib/ruby_pptx/enum/lang.rb +441 -0
  27. data/lib/ruby_pptx/enum/prog_id.rb +38 -0
  28. data/lib/ruby_pptx/enum/shapes.rb +514 -0
  29. data/lib/ruby_pptx/enum/text.rb +108 -0
  30. data/lib/ruby_pptx/errors.rb +15 -0
  31. data/lib/ruby_pptx/geometry.rb +89 -0
  32. data/lib/ruby_pptx/image.rb +347 -0
  33. data/lib/ruby_pptx/length.rb +120 -0
  34. data/lib/ruby_pptx/media.rb +64 -0
  35. data/lib/ruby_pptx/numeric_lengths.rb +30 -0
  36. data/lib/ruby_pptx/opc/constants.rb +205 -0
  37. data/lib/ruby_pptx/opc/oxml.rb +99 -0
  38. data/lib/ruby_pptx/opc/pack_uri.rb +146 -0
  39. data/lib/ruby_pptx/opc/package.rb +482 -0
  40. data/lib/ruby_pptx/opc/serialized.rb +229 -0
  41. data/lib/ruby_pptx/opc/spec.rb +37 -0
  42. data/lib/ruby_pptx/oxml/action.rb +43 -0
  43. data/lib/ruby_pptx/oxml/chart.rb +785 -0
  44. data/lib/ruby_pptx/oxml/content_model.rb +186 -0
  45. data/lib/ruby_pptx/oxml/core_properties.rb +145 -0
  46. data/lib/ruby_pptx/oxml/dml/color.rb +115 -0
  47. data/lib/ruby_pptx/oxml/dml/fill.rb +137 -0
  48. data/lib/ruby_pptx/oxml/element.rb +329 -0
  49. data/lib/ruby_pptx/oxml/ns.rb +115 -0
  50. data/lib/ruby_pptx/oxml/presentation.rb +110 -0
  51. data/lib/ruby_pptx/oxml/section.rb +65 -0
  52. data/lib/ruby_pptx/oxml/shapes/autoshape.rb +310 -0
  53. data/lib/ruby_pptx/oxml/shapes/groupshape.rb +217 -0
  54. data/lib/ruby_pptx/oxml/shapes/other.rb +405 -0
  55. data/lib/ruby_pptx/oxml/shapes/shared.rb +292 -0
  56. data/lib/ruby_pptx/oxml/simple_types.rb +563 -0
  57. data/lib/ruby_pptx/oxml/slide.rb +290 -0
  58. data/lib/ruby_pptx/oxml/table.rb +253 -0
  59. data/lib/ruby_pptx/oxml/text.rb +324 -0
  60. data/lib/ruby_pptx/oxml/theme.rb +93 -0
  61. data/lib/ruby_pptx/package.rb +139 -0
  62. data/lib/ruby_pptx/parts/chart.rb +75 -0
  63. data/lib/ruby_pptx/parts/core_properties.rb +40 -0
  64. data/lib/ruby_pptx/parts/embedded_package.rb +43 -0
  65. data/lib/ruby_pptx/parts/image.rb +55 -0
  66. data/lib/ruby_pptx/parts/media.rb +17 -0
  67. data/lib/ruby_pptx/parts/presentation.rb +79 -0
  68. data/lib/ruby_pptx/parts/slide.rb +221 -0
  69. data/lib/ruby_pptx/parts/theme.rb +26 -0
  70. data/lib/ruby_pptx/pattern_matching.rb +50 -0
  71. data/lib/ruby_pptx/presentation.rb +128 -0
  72. data/lib/ruby_pptx/refinements.rb +21 -0
  73. data/lib/ruby_pptx/section.rb +130 -0
  74. data/lib/ruby_pptx/shapes/adjustments.rb +83 -0
  75. data/lib/ruby_pptx/shapes/authoring.rb +110 -0
  76. data/lib/ruby_pptx/shapes/base.rb +138 -0
  77. data/lib/ruby_pptx/shapes/freeform.rb +141 -0
  78. data/lib/ruby_pptx/shapes/placeholder.rb +184 -0
  79. data/lib/ruby_pptx/shapes/shape.rb +337 -0
  80. data/lib/ruby_pptx/shapes/shape_tree.rb +634 -0
  81. data/lib/ruby_pptx/sliceable.rb +34 -0
  82. data/lib/ruby_pptx/slide.rb +430 -0
  83. data/lib/ruby_pptx/table.rb +316 -0
  84. data/lib/ruby_pptx/table_paging.rb +91 -0
  85. data/lib/ruby_pptx/templates/default.pptx +0 -0
  86. data/lib/ruby_pptx/templates/docx-icon.emf +0 -0
  87. data/lib/ruby_pptx/templates/generic-icon.emf +0 -0
  88. data/lib/ruby_pptx/templates/media-speaker.png +0 -0
  89. data/lib/ruby_pptx/templates/notes.xml +23 -0
  90. data/lib/ruby_pptx/templates/notesMaster.xml +352 -0
  91. data/lib/ruby_pptx/templates/pptx-icon.emf +0 -0
  92. data/lib/ruby_pptx/templates/slideMaster.xml +277 -0
  93. data/lib/ruby_pptx/templates/theme.xml +321 -0
  94. data/lib/ruby_pptx/templates/xlsx-icon.emf +0 -0
  95. data/lib/ruby_pptx/text/fitter.rb +72 -0
  96. data/lib/ruby_pptx/text/font_metrics.rb +221 -0
  97. data/lib/ruby_pptx/text/text.rb +393 -0
  98. data/lib/ruby_pptx/theme.rb +98 -0
  99. data/lib/ruby_pptx/version.rb +5 -0
  100. data/lib/ruby_pptx.rb +90 -0
  101. metadata +174 -0
@@ -0,0 +1,785 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_pptx/oxml/element"
4
+ require "ruby_pptx/oxml/content_model"
5
+ require "ruby_pptx/oxml/simple_types"
6
+ require "ruby_pptx/oxml/text"
7
+ require "ruby_pptx/enum/chart"
8
+
9
+ module Pptx
10
+ module Oxml
11
+ # Chart elements, after python-pptx's `oxml/chart` package.
12
+ #
13
+ # The class boundaries, tag sequences and the split between the two kinds
14
+ # of boolean element follow python-pptx's registrations exactly: what a
15
+ # chart property writes depends on which class its element belongs to,
16
+ # and byte parity with python-pptx is the point.
17
+
18
+ # -------------------------------------------------------------------------
19
+ # Value elements -- one `val` attribute, typed by what it holds
20
+ # -------------------------------------------------------------------------
21
+
22
+ # A boolean `val` whose schema default is true, so writing true removes
23
+ # the attribute: `<c:smooth/>` means smooth. Absent reads as true.
24
+ class CT_Boolean < Element
25
+ tag "c:delete", "c:varyColors", "c:smooth", "c:bubble3D", "c:date1904"
26
+ optional_attr "val", type: SimpleTypes::XsdBoolean, default: true
27
+ end
28
+
29
+ # A boolean `val` always written out, `val="1"` or `val="0"`, even though
30
+ # the schema default is the same true. PowerPoint writes these explicitly
31
+ # and python-pptx does too.
32
+ class CT_BooleanExplicit < Element
33
+ tag "c:overlay", "c:showVal", "c:showCatName", "c:showSerName", "c:showPercent",
34
+ "c:showLegendKey", "c:showBubbleSize", "c:autoTitleDeleted", "c:invertIfNegative",
35
+ "c:plotVisOnly", "c:showDLblsOverMax", "c:showLeaderLines"
36
+
37
+ def val
38
+ raw = get("val")
39
+ raw.nil? || SimpleTypes::XsdBoolean.from_xml(raw)
40
+ end
41
+
42
+ def val=(value)
43
+ set("val", value ? "1" : "0")
44
+ end
45
+ end
46
+
47
+ # `c:autoUpdate`, whether the chart refreshes itself from the workbook.
48
+ class CT_AutoUpdate < Element
49
+ tag "c:autoUpdate"
50
+ optional_attr "val", type: SimpleTypes::XsdBoolean
51
+ end
52
+
53
+ # A double-valued `val`: axis scale limits, a crossing point, a manual
54
+ # layout coordinate.
55
+ class CT_Double < Element
56
+ tag "c:max", "c:min", "c:crossesAt", "c:x"
57
+ required_attr "val", type: SimpleTypes::XsdDouble
58
+ end
59
+
60
+ # `c:majorUnit` and `c:minorUnit`, which must be positive.
61
+ class CT_AxisUnit < Element
62
+ tag "c:majorUnit", "c:minorUnit"
63
+ required_attr "val", type: SimpleTypes::ST_AxisUnit
64
+ end
65
+
66
+ # An unsigned integer `val`: series index and order, axis ids.
67
+ class CT_UnsignedInt < Element
68
+ tag "c:idx", "c:order", "c:crossAx", "c:axId", "c:ptCount"
69
+ required_attr "val", type: SimpleTypes::XsdUnsignedInt
70
+ end
71
+
72
+ # String-valued elements this library writes from templates but has no
73
+ # typed accessor for.
74
+ class CT_ChartString < Element
75
+ tag "c:axPos", "c:radarStyle", "c:scatterStyle", "c:dispBlanksAs"
76
+ optional_attr "val", type: SimpleTypes::XsdString
77
+ end
78
+
79
+ class CT_LegendPos < Element
80
+ tag "c:legendPos"
81
+ optional_attr "val", type: Enum::XL_LEGEND_POSITION, default: Enum::XL_LEGEND_POSITION::RIGHT
82
+ end
83
+
84
+ class CT_DLblPos < Element
85
+ tag "c:dLblPos"
86
+ required_attr "val", type: Enum::XL_DATA_LABEL_POSITION
87
+ end
88
+
89
+ class CT_TickLblPos < Element
90
+ tag "c:tickLblPos"
91
+ optional_attr "val", type: Enum::XL_TICK_LABEL_POSITION
92
+ end
93
+
94
+ class CT_TickMark < Element
95
+ tag "c:majorTickMark", "c:minorTickMark"
96
+ optional_attr "val", type: Enum::XL_TICK_MARK, default: Enum::XL_TICK_MARK::CROSS
97
+ end
98
+
99
+ class CT_Crosses < Element
100
+ tag "c:crosses"
101
+ required_attr "val", type: Enum::XL_AXIS_CROSSES
102
+ end
103
+
104
+ class CT_Orientation < Element
105
+ tag "c:orientation"
106
+ optional_attr "val", type: SimpleTypes::ST_Orientation,
107
+ default: SimpleTypes::ST_Orientation::MIN_MAX
108
+ end
109
+
110
+ class CT_LblOffset < Element
111
+ tag "c:lblOffset"
112
+ optional_attr "val", type: SimpleTypes::ST_LblOffset, default: 100
113
+ end
114
+
115
+ class CT_Grouping < Element
116
+ tag "c:grouping"
117
+ optional_attr "val", type: SimpleTypes::ST_Grouping
118
+ end
119
+
120
+ class CT_BarDir < Element
121
+ tag "c:barDir"
122
+ optional_attr "val", type: SimpleTypes::ST_BarDir, default: SimpleTypes::ST_BarDir::COL
123
+ end
124
+
125
+ # `c:gapWidth`: space between bar groups, as a percentage of bar width.
126
+ class CT_GapAmount < Element
127
+ tag "c:gapWidth"
128
+ optional_attr "val", type: SimpleTypes::ST_GapAmount, default: 150
129
+ end
130
+
131
+ class CT_Overlap < Element
132
+ tag "c:overlap"
133
+ required_attr "val", type: SimpleTypes::ST_Overlap
134
+ end
135
+
136
+ class CT_BubbleScale < Element
137
+ tag "c:bubbleScale"
138
+ optional_attr "val", type: SimpleTypes::ST_BubbleScale, default: 100
139
+ end
140
+
141
+ # `c:style`, the chart style number shown in PowerPoint's style gallery.
142
+ class CT_Style < Element
143
+ tag "c:style"
144
+ required_attr "val", type: SimpleTypes::ST_Style
145
+ end
146
+
147
+ class CT_MarkerSize < Element
148
+ tag "c:size"
149
+ required_attr "val", type: SimpleTypes::ST_MarkerSize
150
+ end
151
+
152
+ class CT_MarkerStyle < Element
153
+ tag "c:symbol"
154
+ required_attr "val", type: Enum::XL_MARKER_STYLE
155
+ end
156
+
157
+ class CT_LayoutMode < Element
158
+ tag "c:xMode"
159
+ optional_attr "val", type: SimpleTypes::ST_LayoutMode,
160
+ default: SimpleTypes::ST_LayoutMode::FACTOR
161
+ end
162
+
163
+ # `c:numFmt`, a number format applied to an axis or to data labels.
164
+ class CT_NumberFormat < Element
165
+ tag "c:numFmt"
166
+ optional_attr "formatCode", type: SimpleTypes::XsdString
167
+ optional_attr "sourceLinked", type: SimpleTypes::XsdBoolean
168
+ end
169
+
170
+ # -------------------------------------------------------------------------
171
+ # Shared structure
172
+ # -------------------------------------------------------------------------
173
+
174
+ # Anything carrying a `c:txPr`, which is where a chart element's font
175
+ # lives. The text body is created on first use.
176
+ module ChartTextProperties
177
+ def defRPr = get_or_add_txPr.defRPr
178
+
179
+ def new_txPr = build_from_xml(CT_TextBody::TXPR_XML)
180
+ end
181
+
182
+ # `c:layout`, holding an optional manual layout.
183
+ class CT_Layout < Element
184
+ tag "c:layout"
185
+ zero_or_one "c:manualLayout", successors: %w[c:extLst]
186
+
187
+ # The horizontal offset as a fraction of the chart width; 0.0 when the
188
+ # position is automatic.
189
+ def horz_offset = manualLayout&.horz_offset || 0.0
190
+
191
+ # Setting 0.0 hands the position back to PowerPoint.
192
+ def horz_offset=(offset)
193
+ if offset.zero?
194
+ remove_manualLayout
195
+ return
196
+ end
197
+
198
+ get_or_add_manualLayout.horz_offset = offset
199
+ end
200
+ end
201
+
202
+ class CT_ManualLayout < Element
203
+ tag "c:manualLayout"
204
+ TAG_SEQ = %w[c:layoutTarget c:xMode c:yMode c:wMode c:hMode c:x c:y c:w c:h
205
+ c:extLst].freeze
206
+ zero_or_one "c:xMode", successors: TAG_SEQ[2..]
207
+ zero_or_one "c:x", successors: TAG_SEQ[6..]
208
+
209
+ # Only a factor-mode x is an offset; an edge-mode x is a position.
210
+ def horz_offset
211
+ return 0.0 if x.nil? || xMode.nil? || xMode.val != SimpleTypes::ST_LayoutMode::FACTOR
212
+
213
+ x.val
214
+ end
215
+
216
+ def horz_offset=(offset)
217
+ get_or_add_xMode.val = SimpleTypes::ST_LayoutMode::FACTOR
218
+ get_or_add_x.val = offset
219
+ end
220
+ end
221
+
222
+ # `c:tx`: a title's or data label's rich text, or a series name's cell
223
+ # reference. The same element serves all three, so one class does.
224
+ class CT_Tx < Element
225
+ tag "c:tx"
226
+ zero_or_one "c:strRef", successors: []
227
+ zero_or_one "c:rich", successors: []
228
+
229
+ RICH_XML = <<~XML.freeze
230
+ <c:rich #{Ns.nsdecls("c", "a")}>
231
+ <a:bodyPr/>
232
+ <a:lstStyle/>
233
+ <a:p>
234
+ <a:pPr>
235
+ <a:defRPr/>
236
+ </a:pPr>
237
+ </a:p>
238
+ </c:rich>
239
+ XML
240
+
241
+ def new_rich = build_from_xml(RICH_XML)
242
+ end
243
+
244
+ # `c:title`, the chart or axis title.
245
+ class CT_Title < Element
246
+ tag "c:title"
247
+ TAG_SEQ = %w[c:tx c:layout c:overlay c:spPr c:txPr c:extLst].freeze
248
+
249
+ zero_or_one "c:tx", successors: TAG_SEQ[1..]
250
+ zero_or_one "c:spPr", successors: TAG_SEQ[4..]
251
+
252
+ # A title with no text of its own yet, which is what PowerPoint and
253
+ # python-pptx both write when a title is switched on.
254
+ def self.new_title(context)
255
+ context.build_from_xml(<<~XML)
256
+ <c:title #{Ns.nsdecls("c")}>
257
+ <c:layout/>
258
+ <c:overlay val="0"/>
259
+ </c:title>
260
+ XML
261
+ end
262
+
263
+ # The rich-text body, or nil when the title has none.
264
+ def rich = xpath("./c:tx/c:rich").first
265
+
266
+ # A title takes rich text or a cell reference, never both.
267
+ def get_or_add_rich
268
+ tx = get_or_add_tx
269
+ tx.remove_strRef
270
+ tx.get_or_add_rich
271
+ end
272
+ end
273
+
274
+ # `c:majorGridlines` and `c:minorGridlines`.
275
+ class CT_ChartLines < Element
276
+ tag "c:majorGridlines", "c:minorGridlines"
277
+ zero_or_one "c:spPr", successors: []
278
+ end
279
+
280
+ # -------------------------------------------------------------------------
281
+ # Axes
282
+ # -------------------------------------------------------------------------
283
+
284
+ # `c:scaling`, the axis range and direction.
285
+ class CT_Scaling < Element
286
+ tag "c:scaling"
287
+ zero_or_one "c:logBase", successors: %w[c:orientation c:max c:min c:extLst]
288
+ zero_or_one "c:orientation", successors: %w[c:max c:min c:extLst]
289
+ zero_or_one "c:max", successors: %w[c:min c:extLst]
290
+ zero_or_one "c:min", successors: %w[c:extLst]
291
+
292
+ # nil means "auto", which is what an absent element says.
293
+ def maximum = max&.val
294
+
295
+ def maximum=(value)
296
+ remove_max
297
+ get_or_add_max.val = value unless value.nil?
298
+ end
299
+
300
+ def minimum = min&.val
301
+
302
+ def minimum=(value)
303
+ remove_min
304
+ get_or_add_min.val = value unless value.nil?
305
+ end
306
+ end
307
+
308
+ # What the three axis elements share. They agree up to `c:crossesAt` and
309
+ # then diverge, which is why each declares its own tail below.
310
+ module BaseAxisElement
311
+ include ChartTextProperties
312
+
313
+ def self.included(base)
314
+ base.class_eval do
315
+ seq = self::TAG_SEQ
316
+ one_and_only_one "c:scaling"
317
+ zero_or_one "c:delete", successors: seq[3..]
318
+ zero_or_one "c:majorGridlines", successors: seq[5..]
319
+ zero_or_one "c:minorGridlines", successors: seq[6..]
320
+ zero_or_one "c:title", successors: seq[7..]
321
+ zero_or_one "c:numFmt", successors: seq[8..]
322
+ zero_or_one "c:majorTickMark", successors: seq[9..]
323
+ zero_or_one "c:minorTickMark", successors: seq[10..]
324
+ zero_or_one "c:tickLblPos", successors: seq[11..]
325
+ zero_or_one "c:spPr", successors: seq[12..]
326
+ zero_or_one "c:txPr", successors: seq[13..]
327
+ zero_or_one "c:crossAx", successors: seq[14..]
328
+ zero_or_one "c:crosses", successors: seq[15..]
329
+ zero_or_one "c:crossesAt", successors: seq[16..]
330
+ end
331
+ end
332
+
333
+ def orientation = scaling.orientation&.val || SimpleTypes::ST_Orientation::MIN_MAX
334
+
335
+ # Only a reversed axis is written; the normal direction is the default.
336
+ def orientation=(value)
337
+ scaling.remove_orientation
338
+ return unless value == SimpleTypes::ST_Orientation::MAX_MIN
339
+
340
+ scaling.get_or_add_orientation.val = value
341
+ end
342
+
343
+ def new_title = CT_Title.new_title(self)
344
+ end
345
+
346
+ COMMON_AXIS_SEQ = %w[c:axId c:scaling c:delete c:axPos c:majorGridlines c:minorGridlines
347
+ c:title c:numFmt c:majorTickMark c:minorTickMark c:tickLblPos c:spPr
348
+ c:txPr c:crossAx c:crosses c:crossesAt].freeze
349
+
350
+ class CT_CatAx < Element
351
+ tag "c:catAx"
352
+ TAG_SEQ = (COMMON_AXIS_SEQ + %w[c:auto c:lblAlgn c:lblOffset c:tickLblSkip
353
+ c:tickMarkSkip c:noMultiLvlLbl c:extLst]).freeze
354
+ include BaseAxisElement
355
+
356
+ zero_or_one "c:lblOffset", successors: TAG_SEQ[19..]
357
+ end
358
+
359
+ class CT_DateAx < Element
360
+ tag "c:dateAx"
361
+ TAG_SEQ = (COMMON_AXIS_SEQ + %w[c:auto c:lblOffset c:baseTimeUnit c:majorUnit
362
+ c:majorTimeUnit c:minorUnit c:minorTimeUnit
363
+ c:extLst]).freeze
364
+ include BaseAxisElement
365
+
366
+ zero_or_one "c:lblOffset", successors: TAG_SEQ[18..]
367
+ end
368
+
369
+ class CT_ValAx < Element
370
+ tag "c:valAx"
371
+ TAG_SEQ = (COMMON_AXIS_SEQ + %w[c:crossBetween c:majorUnit c:minorUnit c:dispUnits
372
+ c:extLst]).freeze
373
+ include BaseAxisElement
374
+
375
+ zero_or_one "c:majorUnit", successors: TAG_SEQ[18..]
376
+ zero_or_one "c:minorUnit", successors: TAG_SEQ[19..]
377
+ end
378
+
379
+ # -------------------------------------------------------------------------
380
+ # Legend
381
+ # -------------------------------------------------------------------------
382
+
383
+ class CT_Legend < Element
384
+ include ChartTextProperties
385
+
386
+ tag "c:legend"
387
+ TAG_SEQ = %w[c:legendPos c:legendEntry c:layout c:overlay c:spPr c:txPr c:extLst].freeze
388
+
389
+ zero_or_one "c:legendPos", successors: TAG_SEQ[1..]
390
+ zero_or_one "c:layout", successors: TAG_SEQ[3..]
391
+ zero_or_one "c:overlay", successors: TAG_SEQ[4..]
392
+ zero_or_one "c:txPr", successors: TAG_SEQ[6..]
393
+
394
+ def horz_offset = layout&.horz_offset || 0.0
395
+
396
+ def horz_offset=(offset)
397
+ get_or_add_layout.horz_offset = offset
398
+ end
399
+ end
400
+
401
+ # -------------------------------------------------------------------------
402
+ # Data labels
403
+ # -------------------------------------------------------------------------
404
+
405
+ # `c:dLbls`, data-label settings for a plot or a series.
406
+ class CT_DataLabels < Element
407
+ include ChartTextProperties
408
+
409
+ tag "c:dLbls"
410
+ TAG_SEQ = %w[c:dLbl c:numFmt c:spPr c:txPr c:dLblPos c:showLegendKey c:showVal
411
+ c:showCatName c:showSerName c:showPercent c:showBubbleSize
412
+ c:separator c:showLeaderLines c:leaderLines c:extLst].freeze
413
+
414
+ zero_or_more "c:dLbl", successors: TAG_SEQ[1..], as: :dLbl
415
+ zero_or_one "c:numFmt", successors: TAG_SEQ[2..]
416
+ zero_or_one "c:txPr", successors: TAG_SEQ[4..]
417
+ zero_or_one "c:dLblPos", successors: TAG_SEQ[5..]
418
+ zero_or_one "c:showLegendKey", successors: TAG_SEQ[6..]
419
+ zero_or_one "c:showVal", successors: TAG_SEQ[7..]
420
+ zero_or_one "c:showCatName", successors: TAG_SEQ[8..]
421
+ zero_or_one "c:showSerName", successors: TAG_SEQ[9..]
422
+ zero_or_one "c:showPercent", successors: TAG_SEQ[10..]
423
+ zero_or_one "c:showBubbleSize", successors: TAG_SEQ[11..]
424
+ zero_or_one "c:showLeaderLines", successors: TAG_SEQ[13..]
425
+
426
+ # What PowerPoint writes when labels are switched on: every kind of
427
+ # label named and turned off, leader lines allowed.
428
+ DEFAULT_XML = <<~XML.freeze
429
+ <c:dLbls #{Ns.nsdecls("c")}>
430
+ <c:showLegendKey val="0"/>
431
+ <c:showVal val="0"/>
432
+ <c:showCatName val="0"/>
433
+ <c:showSerName val="0"/>
434
+ <c:showPercent val="0"/>
435
+ <c:showBubbleSize val="0"/>
436
+ <c:showLeaderLines val="1"/>
437
+ </c:dLbls>
438
+ XML
439
+
440
+ def self.new_data_labels(context) = context.build_from_xml(DEFAULT_XML)
441
+
442
+ # A flag added on its own starts off, as PowerPoint writes them.
443
+ %w[showLegendKey showVal showCatName showSerName showPercent].each do |flag|
444
+ define_method(:"new_#{flag}") { build_from_xml(%(<c:#{flag} #{Ns.nsdecls("c")} val="0"/>)) }
445
+ end
446
+
447
+ # The label for the point at +idx+, or nil if it has none of its own.
448
+ def dLbl_for_point(idx) = dLbl_list.find { |label| label.idx.val == idx }
449
+
450
+ # The label for the point at +idx+, created in index order if absent.
451
+ def get_or_add_dLbl_for_point(idx)
452
+ dLbl_for_point(idx) || insert_dLbl_in_sequence(idx)
453
+ end
454
+
455
+ private
456
+
457
+ def insert_dLbl_in_sequence(idx)
458
+ label = CT_DLbl.new_dLbl(self)
459
+ label.idx.val = idx
460
+ following = dLbl_list.find { |existing| existing.idx.val > idx }
461
+ if following
462
+ following.node.add_previous_sibling(label.node)
463
+ elsif (last = dLbl_list.last)
464
+ last.node.add_next_sibling(label.node)
465
+ else
466
+ node.prepend_child(label.node)
467
+ end
468
+ label
469
+ end
470
+ end
471
+
472
+ # `c:dLbl`, the label of one data point.
473
+ class CT_DLbl < Element
474
+ include ChartTextProperties
475
+
476
+ tag "c:dLbl"
477
+ TAG_SEQ = %w[c:idx c:layout c:tx c:numFmt c:spPr c:txPr c:dLblPos c:showLegendKey
478
+ c:showVal c:showCatName c:showSerName c:showPercent c:showBubbleSize
479
+ c:separator c:extLst].freeze
480
+
481
+ one_and_only_one "c:idx"
482
+ zero_or_one "c:tx", successors: TAG_SEQ[3..]
483
+ zero_or_one "c:spPr", successors: TAG_SEQ[5..]
484
+ zero_or_one "c:txPr", successors: TAG_SEQ[6..]
485
+ zero_or_one "c:dLblPos", successors: TAG_SEQ[7..]
486
+
487
+ DEFAULT_XML = <<~XML.freeze
488
+ <c:dLbl #{Ns.nsdecls("c", "a")}>
489
+ <c:idx val="666"/>
490
+ <c:spPr/>
491
+ <c:txPr>
492
+ <a:bodyPr/>
493
+ <a:lstStyle/>
494
+ <a:p>
495
+ <a:pPr>
496
+ <a:defRPr/>
497
+ </a:pPr>
498
+ </a:p>
499
+ </c:txPr>
500
+ <c:showLegendKey val="0"/>
501
+ <c:showVal val="1"/>
502
+ <c:showCatName val="0"/>
503
+ <c:showSerName val="0"/>
504
+ <c:showPercent val="0"/>
505
+ <c:showBubbleSize val="0"/>
506
+ </c:dLbl>
507
+ XML
508
+
509
+ def self.new_dLbl(context) = context.build_from_xml(DEFAULT_XML)
510
+
511
+ # The label's own text replaces the generated one. A `c:spPr` or
512
+ # `c:txPr` alongside `c:tx` makes a bubble chart unsaveable in
513
+ # PowerPoint, so both go -- python-pptx does the same.
514
+ def get_or_add_rich
515
+ remove_spPr
516
+ remove_txPr
517
+ tx = get_or_add_tx
518
+ tx.remove_strRef
519
+ tx.get_or_add_rich
520
+ end
521
+
522
+ def rich = xpath("./c:tx/c:rich").first
523
+
524
+ def remove_tx_rich
525
+ tx = xpath("./c:tx[c:rich]").first
526
+ remove(tx) if tx
527
+ end
528
+ end
529
+
530
+ # -------------------------------------------------------------------------
531
+ # Series, points and markers
532
+ # -------------------------------------------------------------------------
533
+
534
+ # `c:marker`, the symbol drawn at each point of a line, radar or XY series.
535
+ class CT_Marker < Element
536
+ tag "c:marker"
537
+ zero_or_one "c:symbol", successors: %w[c:size c:spPr c:extLst]
538
+ zero_or_one "c:size", successors: %w[c:spPr c:extLst]
539
+ zero_or_one "c:spPr", successors: %w[c:extLst]
540
+ end
541
+
542
+ # `c:dPt`, the formatting of one data point.
543
+ class CT_DPt < Element
544
+ tag "c:dPt"
545
+ TAG_SEQ = %w[c:idx c:invertIfNegative c:marker c:bubble3D c:explosion c:spPr
546
+ c:pictureOptions c:extLst].freeze
547
+
548
+ one_and_only_one "c:idx"
549
+ zero_or_one "c:marker", successors: TAG_SEQ[3..]
550
+ zero_or_one "c:spPr", successors: TAG_SEQ[6..]
551
+
552
+ def self.new_dPt(context)
553
+ context.build_from_xml(%(<c:dPt #{Ns.nsdecls("c")}><c:idx val="0"/></c:dPt>))
554
+ end
555
+ end
556
+
557
+ # `c:ser`, one series of a plot.
558
+ #
559
+ # Each plot family allows a different subset of these children, in this
560
+ # relative order; python-pptx's superset sequence is used as-is so an
561
+ # inserted element lands where it does there.
562
+ class CT_Series < Element
563
+ tag "c:ser"
564
+ TAG_SEQ = %w[c:idx c:order c:tx c:spPr c:invertIfNegative c:pictureOptions c:marker
565
+ c:explosion c:dPt c:dLbls c:trendline c:errBars c:cat c:val c:xVal c:yVal
566
+ c:shape c:smooth c:bubbleSize c:bubble3D c:extLst].freeze
567
+
568
+ zero_or_one "c:idx", successors: TAG_SEQ[1..]
569
+ zero_or_one "c:order", successors: TAG_SEQ[2..]
570
+ zero_or_one "c:tx", successors: TAG_SEQ[3..]
571
+ zero_or_one "c:spPr", successors: TAG_SEQ[4..]
572
+ zero_or_one "c:invertIfNegative", successors: TAG_SEQ[5..]
573
+ zero_or_one "c:marker", successors: TAG_SEQ[7..]
574
+ zero_or_more "c:dPt", successors: TAG_SEQ[9..], as: :dPt
575
+ zero_or_one "c:dLbls", successors: TAG_SEQ[10..]
576
+ zero_or_one "c:cat", successors: TAG_SEQ[13..]
577
+ zero_or_one "c:val", successors: TAG_SEQ[14..]
578
+ zero_or_one "c:xVal", successors: TAG_SEQ[15..]
579
+ zero_or_one "c:yVal", successors: TAG_SEQ[16..]
580
+ zero_or_one "c:smooth", successors: TAG_SEQ[18..]
581
+ zero_or_one "c:bubbleSize", successors: TAG_SEQ[19..]
582
+
583
+ def index = idx&.val.to_i
584
+
585
+ def name = xpath("./c:tx//c:pt/c:v").first&.text.to_s
586
+
587
+ def new_dLbls = CT_DataLabels.new_data_labels(self)
588
+
589
+ def new_dPt = CT_DPt.new_dPt(self)
590
+
591
+ %w[cat xVal yVal bubbleSize].each do |source|
592
+ define_method(:"#{source}_ptCount_val") do
593
+ xpath("./c:#{source}//c:ptCount/@val").first&.value.to_i
594
+ end
595
+ end
596
+
597
+ # The label of the point at +idx+, or nil.
598
+ def dLbl_for_point(idx) = dLbls&.dLbl_for_point(idx)
599
+
600
+ def get_or_add_dLbl_for_point(idx) = get_or_add_dLbls.get_or_add_dLbl_for_point(idx)
601
+
602
+ # The formatting record for the point at +idx+, created if absent.
603
+ def get_or_add_dPt_for_point(idx)
604
+ dPt_list.find { |point| point.idx.val == idx } || add_dPt.tap { |point| point.idx.val = idx }
605
+ end
606
+ end
607
+
608
+ # A series' category, value and XY sources: a reference into the
609
+ # worksheet plus the cached values.
610
+ class CT_SeriesData < Element
611
+ tag "c:cat", "c:val", "c:xVal", "c:yVal", "c:bubbleSize"
612
+ zero_or_one "c:multiLvlStrRef", successors: []
613
+
614
+ # The levels of a multi-level category source, leaf level first.
615
+ def lvls = xpath(".//c:lvl")
616
+ end
617
+
618
+ # `c:pt`, one cached value in a series source.
619
+ class CT_ChartPoint < Element
620
+ tag "c:pt"
621
+ required_attr "idx", type: SimpleTypes::XsdUnsignedInt
622
+
623
+ def v = xpath("./c:v").first
624
+
625
+ def value = Float(v.text)
626
+ end
627
+
628
+ # -------------------------------------------------------------------------
629
+ # Plots
630
+ # -------------------------------------------------------------------------
631
+
632
+ # A plot element -- `c:barChart` and its siblings.
633
+ #
634
+ # They differ in which options they accept, but share enough that one
635
+ # class covers what this library reads and writes.
636
+ class CT_Plot < Element
637
+ tag "c:barChart", "c:lineChart", "c:pieChart", "c:doughnutChart",
638
+ "c:areaChart", "c:radarChart", "c:scatterChart", "c:bubbleChart", "c:area3DChart"
639
+
640
+ zero_or_one "c:barDir", successors: %w[c:grouping c:varyColors c:ser c:dLbls
641
+ c:gapWidth c:overlap c:serLines c:axId c:extLst]
642
+ zero_or_one "c:grouping", successors: %w[c:varyColors c:ser c:dLbls c:gapWidth
643
+ c:overlap c:serLines c:axId c:extLst]
644
+ zero_or_one "c:varyColors", successors: %w[c:ser c:dLbls c:gapWidth c:overlap
645
+ c:serLines c:axId c:extLst]
646
+ zero_or_one "c:dLbls", successors: %w[c:dropLines c:gapWidth c:overlap c:serLines
647
+ c:bubble3D c:bubbleScale c:axId c:extLst]
648
+ zero_or_one "c:gapWidth", successors: %w[c:overlap c:serLines c:axId c:extLst]
649
+ zero_or_one "c:overlap", successors: %w[c:serLines c:axId c:extLst]
650
+ zero_or_one "c:bubbleScale", successors: %w[c:showNegBubbles c:sizeRepresents c:axId
651
+ c:extLst]
652
+
653
+ # Series in document order.
654
+ def ser_list = find_all("c:ser")
655
+
656
+ # Series in the order the chart draws them, which is `c:order`.
657
+ def sers = ser_list.sort_by { |ser| ser.order&.val.to_i }
658
+
659
+ def grouping_val = grouping&.val || SimpleTypes::ST_Grouping::STANDARD
660
+
661
+ def new_dLbls = CT_DataLabels.new_data_labels(self)
662
+
663
+ # The data-label settings, created with PowerPoint's defaults if the
664
+ # plot has none yet.
665
+ def get_or_add_default_dLbls
666
+ dLbls || begin
667
+ created = new_dLbls
668
+ insert_dLbls(created)
669
+ created
670
+ end
671
+ end
672
+
673
+ # The first series' category source, which the others share.
674
+ def cat = xpath("./c:ser[1]/c:cat").first
675
+
676
+ def cat_pt_count = xpath("./c:ser//c:cat//c:ptCount").first&.val.to_i
677
+
678
+ # One entry per category, nil where the workbook cell is empty -- such
679
+ # a category has no `c:pt` but still counts in `c:ptCount`.
680
+ def cat_pts
681
+ points = xpath("./c:ser[1]/c:cat//c:lvl[1]/c:pt")
682
+ points = xpath("./c:ser[1]/c:cat//c:pt") if points.empty?
683
+ by_idx = points.to_h { |point| [point.idx, point] }
684
+ Array.new(cat_pt_count) { |i| by_idx[i] }
685
+ end
686
+ end
687
+
688
+ # -------------------------------------------------------------------------
689
+ # Plot area, chart and chart space
690
+ # -------------------------------------------------------------------------
691
+
692
+ class CT_PlotArea < Element
693
+ tag "c:plotArea"
694
+
695
+ def plot_elements
696
+ @node.element_children
697
+ .select { |child| child.name.end_with?("Chart") }
698
+ .map { |child| Element.wrap(child) }
699
+ end
700
+
701
+ def category_axis = find("c:catAx") || find("c:dateAx")
702
+
703
+ def value_axes = find_all("c:valAx")
704
+
705
+ # Every series in the chart, in plot order then series order -- which is
706
+ # the order their chart-wide indices follow.
707
+ def series_elements = plot_elements.flat_map(&:ser_list)
708
+
709
+ def last_plot_element = plot_elements.last
710
+ end
711
+
712
+ # `c:chart`, which OOXML uses for two different things under one tag name.
713
+ #
714
+ # Inside `a:graphicData` it is a *reference*: an empty element carrying the
715
+ # relationship id of the chart part. Inside `c:chartSpace` it is the chart
716
+ # itself, with a title, plot area and legend. The registry is keyed by tag,
717
+ # so one class covers both; an element only ever uses one half.
718
+ class CT_Chart < Element
719
+ tag "c:chart"
720
+
721
+ # -- used in the `a:graphicData` reference form --
722
+ optional_attr "r:id", type: SimpleTypes::ST_RelationshipId, as: :rId
723
+
724
+ CHART_NS =
725
+ 'xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" ' \
726
+ 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"'
727
+
728
+ def self.new_chart(r_id)
729
+ Element.parse(%(<c:chart #{CHART_NS} r:id="#{r_id}"/>))
730
+ end
731
+
732
+ # -- used in the `c:chartSpace` form --
733
+ TAG_SEQ = %w[c:title c:autoTitleDeleted c:pivotFmts c:view3D c:floor c:sideWall
734
+ c:backWall c:plotArea c:legend c:plotVisOnly c:dispBlanksAs
735
+ c:showDLblsOverMax c:extLst].freeze
736
+
737
+ zero_or_one "c:title", successors: TAG_SEQ[1..]
738
+ zero_or_one "c:autoTitleDeleted", successors: TAG_SEQ[2..]
739
+ one_and_only_one "c:plotArea"
740
+ zero_or_one "c:legend", successors: TAG_SEQ[9..]
741
+
742
+ def new_title = CT_Title.new_title(self)
743
+ end
744
+
745
+ # `c:chartSpace`, the root of a chart part.
746
+ class CT_ChartSpace < Element
747
+ include ChartTextProperties
748
+
749
+ tag "c:chartSpace"
750
+ TAG_SEQ = %w[c:date1904 c:lang c:roundedCorners c:style c:clrMapOvr
751
+ c:pivotSource c:protection c:chart c:spPr c:txPr
752
+ c:externalData c:printSettings c:userShapes c:extLst].freeze
753
+
754
+ zero_or_one "c:date1904", successors: TAG_SEQ[1..]
755
+ zero_or_one "c:style", successors: TAG_SEQ[4..]
756
+ one_and_only_one "c:chart"
757
+ zero_or_one "c:txPr", successors: TAG_SEQ[10..]
758
+ zero_or_one "c:externalData", successors: TAG_SEQ[11..]
759
+
760
+ # The relationship id of the embedded workbook, or nil when the chart
761
+ # has none.
762
+ def xlsx_part_rId = externalData&.rId
763
+
764
+ # The plot element, e.g. `c:barChart`, that this chart draws with.
765
+ def plot_element
766
+ xpath("./c:chart/c:plotArea/*").find { |e| e.nsptag.end_with?("Chart") }
767
+ end
768
+
769
+ def series_elements = xpath("./c:chart/c:plotArea/*/c:ser")
770
+ end
771
+
772
+ # `c:externalData`, the link from a chart to its embedded workbook.
773
+ class CT_ExternalData < Element
774
+ tag "c:externalData"
775
+ optional_attr "r:id", type: SimpleTypes::ST_RelationshipId, as: :rId
776
+ zero_or_one "c:autoUpdate", successors: []
777
+
778
+ # PowerPoint always writes the auto-update flag, explicitly off.
779
+ def ensure_auto_update
780
+ get_or_add_autoUpdate.set("val", "0")
781
+ self
782
+ end
783
+ end
784
+ end
785
+ end