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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +83 -0
- data/LICENSE +21 -0
- data/NOTICE +48 -0
- data/README.md +272 -0
- data/lib/ruby_pptx/action.rb +120 -0
- data/lib/ruby_pptx/autoshape_spec.rb +337 -0
- data/lib/ruby_pptx/builder.rb +228 -0
- data/lib/ruby_pptx/chart/categories.rb +208 -0
- data/lib/ruby_pptx/chart/chart.rb +443 -0
- data/lib/ruby_pptx/chart/combo.rb +230 -0
- data/lib/ruby_pptx/chart/data.rb +147 -0
- data/lib/ruby_pptx/chart/format.rb +647 -0
- data/lib/ruby_pptx/chart/workbook_writer.rb +212 -0
- data/lib/ruby_pptx/chart/xml_writer.rb +953 -0
- data/lib/ruby_pptx/chart/xy_data.rb +182 -0
- data/lib/ruby_pptx/core_ext.rb +12 -0
- data/lib/ruby_pptx/dml/color.rb +155 -0
- data/lib/ruby_pptx/dml/effect.rb +33 -0
- data/lib/ruby_pptx/dml/fill.rb +234 -0
- data/lib/ruby_pptx/element_proxy.rb +53 -0
- data/lib/ruby_pptx/enum/action.rb +46 -0
- data/lib/ruby_pptx/enum/base.rb +139 -0
- data/lib/ruby_pptx/enum/chart.rb +278 -0
- data/lib/ruby_pptx/enum/dml.rb +237 -0
- data/lib/ruby_pptx/enum/lang.rb +441 -0
- data/lib/ruby_pptx/enum/prog_id.rb +38 -0
- data/lib/ruby_pptx/enum/shapes.rb +514 -0
- data/lib/ruby_pptx/enum/text.rb +108 -0
- data/lib/ruby_pptx/errors.rb +15 -0
- data/lib/ruby_pptx/geometry.rb +89 -0
- data/lib/ruby_pptx/image.rb +347 -0
- data/lib/ruby_pptx/length.rb +120 -0
- data/lib/ruby_pptx/media.rb +64 -0
- data/lib/ruby_pptx/numeric_lengths.rb +30 -0
- data/lib/ruby_pptx/opc/constants.rb +205 -0
- data/lib/ruby_pptx/opc/oxml.rb +99 -0
- data/lib/ruby_pptx/opc/pack_uri.rb +146 -0
- data/lib/ruby_pptx/opc/package.rb +482 -0
- data/lib/ruby_pptx/opc/serialized.rb +229 -0
- data/lib/ruby_pptx/opc/spec.rb +37 -0
- data/lib/ruby_pptx/oxml/action.rb +43 -0
- data/lib/ruby_pptx/oxml/chart.rb +785 -0
- data/lib/ruby_pptx/oxml/content_model.rb +186 -0
- data/lib/ruby_pptx/oxml/core_properties.rb +145 -0
- data/lib/ruby_pptx/oxml/dml/color.rb +115 -0
- data/lib/ruby_pptx/oxml/dml/fill.rb +137 -0
- data/lib/ruby_pptx/oxml/element.rb +329 -0
- data/lib/ruby_pptx/oxml/ns.rb +115 -0
- data/lib/ruby_pptx/oxml/presentation.rb +110 -0
- data/lib/ruby_pptx/oxml/section.rb +65 -0
- data/lib/ruby_pptx/oxml/shapes/autoshape.rb +310 -0
- data/lib/ruby_pptx/oxml/shapes/groupshape.rb +217 -0
- data/lib/ruby_pptx/oxml/shapes/other.rb +405 -0
- data/lib/ruby_pptx/oxml/shapes/shared.rb +292 -0
- data/lib/ruby_pptx/oxml/simple_types.rb +563 -0
- data/lib/ruby_pptx/oxml/slide.rb +290 -0
- data/lib/ruby_pptx/oxml/table.rb +253 -0
- data/lib/ruby_pptx/oxml/text.rb +324 -0
- data/lib/ruby_pptx/oxml/theme.rb +93 -0
- data/lib/ruby_pptx/package.rb +139 -0
- data/lib/ruby_pptx/parts/chart.rb +75 -0
- data/lib/ruby_pptx/parts/core_properties.rb +40 -0
- data/lib/ruby_pptx/parts/embedded_package.rb +43 -0
- data/lib/ruby_pptx/parts/image.rb +55 -0
- data/lib/ruby_pptx/parts/media.rb +17 -0
- data/lib/ruby_pptx/parts/presentation.rb +79 -0
- data/lib/ruby_pptx/parts/slide.rb +221 -0
- data/lib/ruby_pptx/parts/theme.rb +26 -0
- data/lib/ruby_pptx/pattern_matching.rb +50 -0
- data/lib/ruby_pptx/presentation.rb +128 -0
- data/lib/ruby_pptx/refinements.rb +21 -0
- data/lib/ruby_pptx/section.rb +130 -0
- data/lib/ruby_pptx/shapes/adjustments.rb +83 -0
- data/lib/ruby_pptx/shapes/authoring.rb +110 -0
- data/lib/ruby_pptx/shapes/base.rb +138 -0
- data/lib/ruby_pptx/shapes/freeform.rb +141 -0
- data/lib/ruby_pptx/shapes/placeholder.rb +184 -0
- data/lib/ruby_pptx/shapes/shape.rb +337 -0
- data/lib/ruby_pptx/shapes/shape_tree.rb +634 -0
- data/lib/ruby_pptx/sliceable.rb +34 -0
- data/lib/ruby_pptx/slide.rb +430 -0
- data/lib/ruby_pptx/table.rb +316 -0
- data/lib/ruby_pptx/table_paging.rb +91 -0
- data/lib/ruby_pptx/templates/default.pptx +0 -0
- data/lib/ruby_pptx/templates/docx-icon.emf +0 -0
- data/lib/ruby_pptx/templates/generic-icon.emf +0 -0
- data/lib/ruby_pptx/templates/media-speaker.png +0 -0
- data/lib/ruby_pptx/templates/notes.xml +23 -0
- data/lib/ruby_pptx/templates/notesMaster.xml +352 -0
- data/lib/ruby_pptx/templates/pptx-icon.emf +0 -0
- data/lib/ruby_pptx/templates/slideMaster.xml +277 -0
- data/lib/ruby_pptx/templates/theme.xml +321 -0
- data/lib/ruby_pptx/templates/xlsx-icon.emf +0 -0
- data/lib/ruby_pptx/text/fitter.rb +72 -0
- data/lib/ruby_pptx/text/font_metrics.rb +221 -0
- data/lib/ruby_pptx/text/text.rb +393 -0
- data/lib/ruby_pptx/theme.rb +98 -0
- data/lib/ruby_pptx/version.rb +5 -0
- data/lib/ruby_pptx.rb +90 -0
- metadata +174 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/element_proxy"
|
|
4
|
+
require "ruby_pptx/sliceable"
|
|
5
|
+
require "ruby_pptx/text/text"
|
|
6
|
+
require "ruby_pptx/enum/chart"
|
|
7
|
+
require "ruby_pptx/dml/fill"
|
|
8
|
+
|
|
9
|
+
module Pptx
|
|
10
|
+
# The fill and outline of a chart element -- a series, a point, an axis,
|
|
11
|
+
# gridlines, a title or a marker.
|
|
12
|
+
#
|
|
13
|
+
# Reached through the `format` of whatever it belongs to.
|
|
14
|
+
class ChartFormat < ElementProxy
|
|
15
|
+
def fill = @fill ||= FillFormat.from_fill_parent(@element.get_or_add_spPr)
|
|
16
|
+
|
|
17
|
+
def line = @line ||= LineFormat.new(@element.get_or_add_spPr)
|
|
18
|
+
|
|
19
|
+
def inspect = "#<Pptx::ChartFormat #{fill.type&.name}>"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A chart's legend.
|
|
23
|
+
class ChartLegend < ElementProxy
|
|
24
|
+
# The legend's font, created on first use.
|
|
25
|
+
def font = @font ||= Font.new(@element.defRPr)
|
|
26
|
+
|
|
27
|
+
# Where the legend sits. Absent from the file means PowerPoint's default,
|
|
28
|
+
# which is the right-hand side.
|
|
29
|
+
#
|
|
30
|
+
# @return [Pptx::Enum::Member] a member of XL_LEGEND_POSITION
|
|
31
|
+
def position = @element.legendPos&.val || Enum::XL_LEGEND_POSITION::RIGHT
|
|
32
|
+
|
|
33
|
+
def position=(value)
|
|
34
|
+
@element.get_or_add_legendPos.val = Enum::XL_LEGEND_POSITION.fetch(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# How far the legend is shifted sideways, as a fraction of the chart
|
|
38
|
+
# width: -1.0 to 1.0, with 0.0 meaning PowerPoint places it.
|
|
39
|
+
def horz_offset = @element.horz_offset
|
|
40
|
+
|
|
41
|
+
def horz_offset=(value)
|
|
42
|
+
@element.horz_offset = value
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# True when the legend sits inside the plot area, overlapping it, rather
|
|
46
|
+
# than having space made for it. An absent `c:overlay` reads as true.
|
|
47
|
+
def include_in_layout? = @element.overlay.nil? || @element.overlay.val
|
|
48
|
+
|
|
49
|
+
# nil removes the setting and returns to the default.
|
|
50
|
+
def include_in_layout=(value)
|
|
51
|
+
if value.nil?
|
|
52
|
+
@element.remove_overlay
|
|
53
|
+
else
|
|
54
|
+
@element.get_or_add_overlay.val = value ? true : false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inspect = "#<Pptx::ChartLegend #{position.name}>"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A chart or axis title.
|
|
62
|
+
#
|
|
63
|
+
# The title carries a text frame like any shape, so it can be formatted the
|
|
64
|
+
# same way.
|
|
65
|
+
class ChartTitle < ElementProxy
|
|
66
|
+
# The title box's fill and outline.
|
|
67
|
+
def format = @format ||= ChartFormat.new(@element)
|
|
68
|
+
|
|
69
|
+
# True when the title has text of its own rather than being generated.
|
|
70
|
+
def text_frame? = !@element.rich.nil?
|
|
71
|
+
|
|
72
|
+
# The title's text, created on first use.
|
|
73
|
+
def text_frame = TextFrame.new(@element.get_or_add_rich, self)
|
|
74
|
+
|
|
75
|
+
# Titles are formatting, not content; there is no part to reach for.
|
|
76
|
+
def part = nil
|
|
77
|
+
|
|
78
|
+
def text = text_frame? ? text_frame.text : ""
|
|
79
|
+
|
|
80
|
+
def text=(value)
|
|
81
|
+
text_frame.text = value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def inspect = "#<Pptx::ChartTitle #{text.inspect}>"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The major gridlines of an axis.
|
|
88
|
+
class Gridlines < ElementProxy
|
|
89
|
+
def format = @format ||= ChartFormat.new(@element.get_or_add_majorGridlines)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The labels along an axis: their font, number format and spacing.
|
|
93
|
+
class TickLabels < ElementProxy
|
|
94
|
+
def font = @font ||= Font.new(@element.defRPr)
|
|
95
|
+
|
|
96
|
+
# The Excel number format, "General" when none is set.
|
|
97
|
+
def number_format = @element.numFmt&.formatCode || "General"
|
|
98
|
+
|
|
99
|
+
# Setting a format stops it following the source data's format.
|
|
100
|
+
def number_format=(value)
|
|
101
|
+
@element.get_or_add_numFmt.formatCode = value
|
|
102
|
+
self.number_format_linked = false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# True when the labels take their number format from the worksheet.
|
|
106
|
+
def number_format_linked?
|
|
107
|
+
format = @element.numFmt
|
|
108
|
+
return false if format.nil?
|
|
109
|
+
|
|
110
|
+
format.sourceLinked.nil? || format.sourceLinked
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def number_format_linked=(value)
|
|
114
|
+
@element.get_or_add_numFmt.sourceLinked = value
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Distance of the labels from the axis as a percentage of the default;
|
|
118
|
+
# 100 is the default. Only a category axis has one.
|
|
119
|
+
def offset = @element.respond_to?(:lblOffset) ? (@element.lblOffset&.val || 100) : 100
|
|
120
|
+
|
|
121
|
+
def offset=(value)
|
|
122
|
+
raise Error, "only a category axis has a label offset" unless @element.nsptag == "c:catAx"
|
|
123
|
+
|
|
124
|
+
@element.remove_lblOffset
|
|
125
|
+
return if value == 100
|
|
126
|
+
|
|
127
|
+
@element.get_or_add_lblOffset.val = value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# One axis of a chart. {CategoryAxis}, {DateAxis} and {ValueAxis} add what
|
|
132
|
+
# only they have.
|
|
133
|
+
class ChartAxis < ElementProxy
|
|
134
|
+
# The axis line and its fill.
|
|
135
|
+
def format = @format ||= ChartFormat.new(@element)
|
|
136
|
+
|
|
137
|
+
# Whether the axis is drawn. A `c:delete` of true hides it.
|
|
138
|
+
#
|
|
139
|
+
# An axis with no `c:delete` at all is drawn, which is what the schema and
|
|
140
|
+
# PowerPoint say. python-pptx reports it as hidden; see PORTING.md.
|
|
141
|
+
def visible? = @element.delete.nil? || !@element.delete.val
|
|
142
|
+
|
|
143
|
+
def visible=(value)
|
|
144
|
+
unless [true, false].include?(value)
|
|
145
|
+
raise ArgumentError, "visible must be true or false, got #{value.inspect}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
@element.get_or_add_delete.val = !value
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def major_gridlines? = !@element.majorGridlines.nil?
|
|
152
|
+
|
|
153
|
+
def major_gridlines=(value)
|
|
154
|
+
value ? @element.get_or_add_majorGridlines : @element.remove_majorGridlines
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The major gridlines, for formatting. Asking for them switches them on.
|
|
158
|
+
def major_gridlines = @major_gridlines ||= Gridlines.new(@element)
|
|
159
|
+
|
|
160
|
+
def minor_gridlines? = !@element.minorGridlines.nil?
|
|
161
|
+
|
|
162
|
+
def minor_gridlines=(value)
|
|
163
|
+
value ? @element.get_or_add_minorGridlines : @element.remove_minorGridlines
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @return [Pptx::Enum::Member] a member of XL_TICK_MARK
|
|
167
|
+
def major_tick_mark = @element.majorTickMark&.val || Enum::XL_TICK_MARK::CROSS
|
|
168
|
+
|
|
169
|
+
# The default, cross, is written by leaving the element out.
|
|
170
|
+
def major_tick_mark=(value)
|
|
171
|
+
set_tick_mark(:majorTickMark, value)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def minor_tick_mark = @element.minorTickMark&.val || Enum::XL_TICK_MARK::CROSS
|
|
175
|
+
|
|
176
|
+
def minor_tick_mark=(value)
|
|
177
|
+
set_tick_mark(:minorTickMark, value)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The fixed end of the scale, or nil when PowerPoint scales automatically.
|
|
181
|
+
def maximum_scale = @element.scaling.maximum
|
|
182
|
+
|
|
183
|
+
def maximum_scale=(value)
|
|
184
|
+
@element.scaling.maximum = value
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def minimum_scale = @element.scaling.minimum
|
|
188
|
+
|
|
189
|
+
def minimum_scale=(value)
|
|
190
|
+
@element.scaling.minimum = value
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# True when the axis runs from its maximum to its minimum.
|
|
194
|
+
def reverse_order? = @element.orientation == Oxml::SimpleTypes::ST_Orientation::MAX_MIN
|
|
195
|
+
|
|
196
|
+
def reverse_order=(value)
|
|
197
|
+
@element.orientation = if value
|
|
198
|
+
Oxml::SimpleTypes::ST_Orientation::MAX_MIN
|
|
199
|
+
else
|
|
200
|
+
Oxml::SimpleTypes::ST_Orientation::MIN_MAX
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @return [Pptx::Enum::Member] a member of XL_TICK_LABEL_POSITION
|
|
205
|
+
def tick_label_position
|
|
206
|
+
@element.tickLblPos&.val || Enum::XL_TICK_LABEL_POSITION::NEXT_TO_AXIS
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def tick_label_position=(value)
|
|
210
|
+
@element.get_or_add_tickLblPos.val = Enum::XL_TICK_LABEL_POSITION.fetch(value)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The labels along the axis.
|
|
214
|
+
def tick_labels = @tick_labels ||= TickLabels.new(@element)
|
|
215
|
+
|
|
216
|
+
# Shortcuts for the tick-label number format, the setting most often
|
|
217
|
+
# wanted from an axis.
|
|
218
|
+
def number_format = tick_labels.number_format
|
|
219
|
+
|
|
220
|
+
def number_format=(value)
|
|
221
|
+
tick_labels.number_format = value
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def title? = !@element.title.nil?
|
|
225
|
+
|
|
226
|
+
# @return [ChartTitle, nil] nil when the axis has no title
|
|
227
|
+
def title
|
|
228
|
+
element = @element.title
|
|
229
|
+
element && ChartTitle.new(element)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# A String sets the title's text; true shows a title with no text of its
|
|
233
|
+
# own; nil or false removes it.
|
|
234
|
+
def title=(value)
|
|
235
|
+
case value
|
|
236
|
+
when nil, false then @element.remove_title
|
|
237
|
+
when true then @element.get_or_add_title
|
|
238
|
+
else ChartTitle.new(@element.get_or_add_title).text = value
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def inspect = "#<#{self.class.name} visible=#{visible?}>"
|
|
243
|
+
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
def set_tick_mark(name, value)
|
|
247
|
+
member = Enum::XL_TICK_MARK.fetch(value)
|
|
248
|
+
@element.public_send(:"remove_#{name}")
|
|
249
|
+
return if member == Enum::XL_TICK_MARK::CROSS
|
|
250
|
+
|
|
251
|
+
@element.public_send(:"get_or_add_#{name}").val = member
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# The horizontal axis of most charts: one label per category.
|
|
256
|
+
class CategoryAxis < ChartAxis
|
|
257
|
+
def category_type = Enum::XL_CATEGORY_TYPE::CATEGORY_SCALE
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# A category axis whose categories are dates.
|
|
261
|
+
class DateAxis < ChartAxis
|
|
262
|
+
def category_type = Enum::XL_CATEGORY_TYPE::TIME_SCALE
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# An axis measuring values. An XY or bubble chart has two.
|
|
266
|
+
class ValueAxis < ChartAxis
|
|
267
|
+
# Where the *other* axis crosses this one. `crosses` describes the
|
|
268
|
+
# crossing and is stored on the axis being crossed, which is why these
|
|
269
|
+
# two read and write the perpendicular axis.
|
|
270
|
+
#
|
|
271
|
+
# @return [Pptx::Enum::Member] a member of XL_AXIS_CROSSES; CUSTOM when a
|
|
272
|
+
# specific value is set through {#crosses_at}
|
|
273
|
+
def crosses = cross_axis.crosses&.val || Enum::XL_AXIS_CROSSES::CUSTOM
|
|
274
|
+
|
|
275
|
+
def crosses=(value)
|
|
276
|
+
member = Enum::XL_AXIS_CROSSES.fetch(value)
|
|
277
|
+
axis = cross_axis
|
|
278
|
+
return if member == Enum::XL_AXIS_CROSSES::CUSTOM && axis.crossesAt
|
|
279
|
+
|
|
280
|
+
axis.remove_crosses
|
|
281
|
+
axis.remove_crossesAt
|
|
282
|
+
if member == Enum::XL_AXIS_CROSSES::CUSTOM
|
|
283
|
+
axis.get_or_add_crossesAt.val = 0.0
|
|
284
|
+
else
|
|
285
|
+
axis.get_or_add_crosses.val = member
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# The value on this axis at which the other crosses, or nil.
|
|
290
|
+
def crosses_at = cross_axis.crossesAt&.val
|
|
291
|
+
|
|
292
|
+
def crosses_at=(value)
|
|
293
|
+
axis = cross_axis
|
|
294
|
+
axis.remove_crosses
|
|
295
|
+
axis.remove_crossesAt
|
|
296
|
+
axis.get_or_add_crossesAt.val = value unless value.nil?
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# The distance between major tick marks, or nil when automatic.
|
|
300
|
+
def major_unit = @element.majorUnit&.val
|
|
301
|
+
|
|
302
|
+
def major_unit=(value)
|
|
303
|
+
@element.remove_majorUnit
|
|
304
|
+
@element.get_or_add_majorUnit.val = value unless value.nil?
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def minor_unit = @element.minorUnit&.val
|
|
308
|
+
|
|
309
|
+
def minor_unit=(value)
|
|
310
|
+
@element.remove_minorUnit
|
|
311
|
+
@element.get_or_add_minorUnit.val = value unless value.nil?
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
private
|
|
315
|
+
|
|
316
|
+
# The axis whose `c:axId` this one names in its `c:crossAx`.
|
|
317
|
+
def cross_axis
|
|
318
|
+
id = @element.crossAx.val
|
|
319
|
+
@element.xpath("(../c:catAx | ../c:valAx | ../c:dateAx)/c:axId[@val=\"#{id}\"]").first.parent
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# The data labels of a plot or series.
|
|
324
|
+
class ChartDataLabels < ElementProxy
|
|
325
|
+
SHOW_FLAGS = {
|
|
326
|
+
value: "c:showVal", category_name: "c:showCatName", series_name: "c:showSerName",
|
|
327
|
+
percentage: "c:showPercent", legend_key: "c:showLegendKey",
|
|
328
|
+
bubble_size: "c:showBubbleSize"
|
|
329
|
+
}.freeze
|
|
330
|
+
|
|
331
|
+
# Reading a flag does not add it. python-pptx's getters do -- asking
|
|
332
|
+
# whether values are shown writes `c:showVal val="0"` into the file --
|
|
333
|
+
# and a read that changes the document is not one this library will make.
|
|
334
|
+
SHOW_FLAGS.each do |name, tag|
|
|
335
|
+
local = Oxml::Ns.split_tag(tag).last
|
|
336
|
+
|
|
337
|
+
define_method("show_#{name}?") do
|
|
338
|
+
flag = @element.public_send(local)
|
|
339
|
+
flag.nil? ? false : flag.val
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
define_method("show_#{name}=") do |value|
|
|
343
|
+
@element.public_send("get_or_add_#{local}").val = value ? true : false
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def font = @font ||= Font.new(@element.defRPr)
|
|
348
|
+
|
|
349
|
+
# The Excel number format, "General" when none is set.
|
|
350
|
+
def number_format = @element.numFmt&.formatCode || "General"
|
|
351
|
+
|
|
352
|
+
def number_format=(value)
|
|
353
|
+
@element.get_or_add_numFmt.formatCode = value
|
|
354
|
+
self.number_format_linked = false
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# True when the labels take their number format from the worksheet. Data
|
|
358
|
+
# labels with no number format at all follow the source.
|
|
359
|
+
def number_format_linked?
|
|
360
|
+
format = @element.numFmt
|
|
361
|
+
format.nil? || format.sourceLinked.nil? || format.sourceLinked
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def number_format_linked=(value)
|
|
365
|
+
@element.get_or_add_numFmt.sourceLinked = value
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# @return [Pptx::Enum::Member, nil] a member of XL_DATA_LABEL_POSITION
|
|
369
|
+
def position = @element.dLblPos&.val
|
|
370
|
+
|
|
371
|
+
def position=(value)
|
|
372
|
+
if value.nil?
|
|
373
|
+
@element.remove_dLblPos
|
|
374
|
+
else
|
|
375
|
+
@element.get_or_add_dLblPos.val = Enum::XL_DATA_LABEL_POSITION.fetch(value)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def inspect = "#<Pptx::ChartDataLabels value=#{show_value?}>"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# The label of a single data point, overriding the series' labels for it.
|
|
383
|
+
class DataLabel
|
|
384
|
+
def initialize(ser, idx)
|
|
385
|
+
@ser = ser
|
|
386
|
+
@idx = idx
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def font = @font ||= TextFrame.new(label_element.get_or_add_txPr, self).paragraphs[0].font
|
|
390
|
+
|
|
391
|
+
# True when the label has text of its own rather than a generated value.
|
|
392
|
+
def text_frame? = !@ser.dLbl_for_point(@idx)&.rich.nil?
|
|
393
|
+
|
|
394
|
+
# The label's own text, replacing the generated value. Created on use.
|
|
395
|
+
def text_frame = TextFrame.new(label_element.get_or_add_rich, self)
|
|
396
|
+
|
|
397
|
+
# Labels are formatting, not content; there is no part to reach for.
|
|
398
|
+
def part = nil
|
|
399
|
+
|
|
400
|
+
# @return [Pptx::Enum::Member, nil] a member of XL_DATA_LABEL_POSITION
|
|
401
|
+
def position = @ser.dLbl_for_point(@idx)&.dLblPos&.val
|
|
402
|
+
|
|
403
|
+
def position=(value)
|
|
404
|
+
if value.nil?
|
|
405
|
+
@ser.dLbl_for_point(@idx)&.remove_dLblPos
|
|
406
|
+
else
|
|
407
|
+
label_element.get_or_add_dLblPos.val = Enum::XL_DATA_LABEL_POSITION.fetch(value)
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def inspect = "#<Pptx::DataLabel point=#{@idx}>"
|
|
412
|
+
|
|
413
|
+
private
|
|
414
|
+
|
|
415
|
+
# This point's `c:dLbl`, created on first use.
|
|
416
|
+
def label_element = @ser.get_or_add_dLbl_for_point(@idx)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# The symbol drawn at each point of a line, radar or XY series, or at one
|
|
420
|
+
# point.
|
|
421
|
+
class Marker < ElementProxy
|
|
422
|
+
def format = @format ||= ChartFormat.new(@element.get_or_add_marker)
|
|
423
|
+
|
|
424
|
+
# Size in points, 2 to 72, or nil when inherited.
|
|
425
|
+
def size = @element.marker&.size&.val
|
|
426
|
+
|
|
427
|
+
def size=(value)
|
|
428
|
+
marker = @element.get_or_add_marker
|
|
429
|
+
marker.remove_size
|
|
430
|
+
marker.get_or_add_size.val = value unless value.nil?
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# @return [Pptx::Enum::Member, nil] a member of XL_MARKER_STYLE
|
|
434
|
+
def style = @element.marker&.symbol&.val
|
|
435
|
+
|
|
436
|
+
def style=(value)
|
|
437
|
+
marker = @element.get_or_add_marker
|
|
438
|
+
marker.remove_symbol
|
|
439
|
+
marker.get_or_add_symbol.val = Enum::XL_MARKER_STYLE.fetch(value) unless value.nil?
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# One data point of a series, for formatting it apart from the rest.
|
|
444
|
+
class ChartPoint
|
|
445
|
+
def initialize(ser, idx)
|
|
446
|
+
@ser = ser
|
|
447
|
+
@idx = idx
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def data_label = @data_label ||= DataLabel.new(@ser, @idx)
|
|
451
|
+
|
|
452
|
+
def format = @format ||= ChartFormat.new(@ser.get_or_add_dPt_for_point(@idx))
|
|
453
|
+
|
|
454
|
+
def marker = @marker ||= Marker.new(@ser.get_or_add_dPt_for_point(@idx))
|
|
455
|
+
|
|
456
|
+
def inspect = "#<Pptx::ChartPoint #{@idx}>"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# The points of a series, indexed from 0.
|
|
460
|
+
class ChartPoints
|
|
461
|
+
include Enumerable
|
|
462
|
+
include Sliceable
|
|
463
|
+
|
|
464
|
+
def initialize(ser, count)
|
|
465
|
+
@ser = ser
|
|
466
|
+
@count = count
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def size = @count
|
|
470
|
+
alias length size
|
|
471
|
+
|
|
472
|
+
def [](index, length = nil)
|
|
473
|
+
slice_members((0...@count).to_a, index, length) { |i| ChartPoint.new(@ser, i) }
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def each
|
|
477
|
+
return enum_for(:each) { size } unless block_given?
|
|
478
|
+
|
|
479
|
+
@count.times { |i| yield ChartPoint.new(@ser, i) }
|
|
480
|
+
self
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def inspect = "#<Pptx::ChartPoints size=#{size}>"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# One plot -- a "chart group" in the MS API -- within a chart.
|
|
487
|
+
#
|
|
488
|
+
# A chart usually has exactly one; a combo chart has several, which is why
|
|
489
|
+
# these are a collection rather than properties of the chart itself.
|
|
490
|
+
class ChartPlot < ElementProxy
|
|
491
|
+
attr_reader :chart
|
|
492
|
+
|
|
493
|
+
def initialize(element, chart)
|
|
494
|
+
super(element)
|
|
495
|
+
@chart = chart
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# The categories this plot's series are plotted against.
|
|
499
|
+
def categories = ChartCategories.new(@element)
|
|
500
|
+
|
|
501
|
+
# This plot's series, in the order the chart draws them.
|
|
502
|
+
def series = @element.sers.map { |ser| ChartSeriesView.for(ser, @chart) }
|
|
503
|
+
|
|
504
|
+
# The space between bars, as a percentage of bar width.
|
|
505
|
+
def gap_width = @element.gapWidth&.val || 150
|
|
506
|
+
|
|
507
|
+
def gap_width=(value)
|
|
508
|
+
@element.get_or_add_gapWidth.val = value
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# How far bars in a group overlap, -100 to 100. Zero is written by
|
|
512
|
+
# leaving the element out.
|
|
513
|
+
def overlap = @element.overlap&.val || 0
|
|
514
|
+
|
|
515
|
+
def overlap=(value)
|
|
516
|
+
if value.zero?
|
|
517
|
+
@element.remove_overlap
|
|
518
|
+
else
|
|
519
|
+
@element.get_or_add_overlap.val = value
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# The bubble size as a percentage of the default, 0 to 300.
|
|
524
|
+
def bubble_scale = @element.bubbleScale&.val || 100
|
|
525
|
+
|
|
526
|
+
# nil returns to the default.
|
|
527
|
+
def bubble_scale=(value)
|
|
528
|
+
@element.remove_bubbleScale
|
|
529
|
+
@element.get_or_add_bubbleScale.val = value unless value.nil?
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Whether each data point gets its own colour, as a pie does. An absent
|
|
533
|
+
# `c:varyColors` reads as true, the schema default.
|
|
534
|
+
def vary_by_categories? = @element.varyColors.nil? || @element.varyColors.val
|
|
535
|
+
|
|
536
|
+
def vary_by_categories=(value)
|
|
537
|
+
@element.get_or_add_varyColors.val = value ? true : false
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Whether this plot shows data labels at all.
|
|
541
|
+
def data_labels? = !@element.dLbls.nil?
|
|
542
|
+
|
|
543
|
+
# Switching labels on shows values, as PowerPoint's default does.
|
|
544
|
+
def data_labels=(value)
|
|
545
|
+
if value
|
|
546
|
+
@element.get_or_add_default_dLbls.showVal.val = true unless @element.dLbls
|
|
547
|
+
else
|
|
548
|
+
@element.remove_dLbls
|
|
549
|
+
end
|
|
550
|
+
@data_labels = nil
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# The data-label settings, switched on with PowerPoint's defaults if the
|
|
554
|
+
# plot has none yet. python-pptx makes you set `has_data_labels` first;
|
|
555
|
+
# asking for them is enough here.
|
|
556
|
+
def data_labels = @data_labels ||= ChartDataLabels.new(@element.get_or_add_default_dLbls)
|
|
557
|
+
|
|
558
|
+
def inspect = "#<Pptx::ChartPlot #{@element.nsptag}>"
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# The categories of a plot, read back from the chart's cached values.
|
|
562
|
+
#
|
|
563
|
+
# A category can be empty -- an empty worksheet cell -- in which case it
|
|
564
|
+
# has no cached point but still counts, so {#size} is the declared count
|
|
565
|
+
# rather than the number of labels found.
|
|
566
|
+
class ChartCategories
|
|
567
|
+
include Enumerable
|
|
568
|
+
|
|
569
|
+
def initialize(plot_element)
|
|
570
|
+
@plot = plot_element
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def each
|
|
574
|
+
return enum_for(:each) { size } unless block_given?
|
|
575
|
+
|
|
576
|
+
@plot.cat_pts.each_with_index { |point, i| yield ChartCategory.new(point, i) }
|
|
577
|
+
self
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def [](index) = to_a[index]
|
|
581
|
+
|
|
582
|
+
def size = @plot.cat_pt_count
|
|
583
|
+
alias length size
|
|
584
|
+
|
|
585
|
+
# How many levels of labels there are: 0 with no categories, 1 for a
|
|
586
|
+
# plain list, more for grouped categories.
|
|
587
|
+
def depth
|
|
588
|
+
cat = @plot.cat
|
|
589
|
+
return 0 if cat.nil?
|
|
590
|
+
return 1 if cat.multiLvlStrRef.nil?
|
|
591
|
+
|
|
592
|
+
cat.lvls.size
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# Each level's labels, leaf level first.
|
|
596
|
+
def levels
|
|
597
|
+
cat = @plot.cat
|
|
598
|
+
return [] if cat.nil?
|
|
599
|
+
|
|
600
|
+
cat.lvls.map { |lvl| lvl.xpath("./c:pt").map { |pt| ChartCategory.new(pt) } }
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# One tuple per leaf category, its labels listed root first -- the form
|
|
604
|
+
# ChartData takes them in.
|
|
605
|
+
def flattened_labels
|
|
606
|
+
return [] if @plot.cat.nil?
|
|
607
|
+
return map { |category| [category.label] } if @plot.cat.multiLvlStrRef.nil?
|
|
608
|
+
|
|
609
|
+
leaf, *parents = levels
|
|
610
|
+
leaf.map { |category| lineage(category, parents).reverse.map(&:label) }
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def inspect = "#<Pptx::ChartCategories #{map(&:label).inspect}>"
|
|
614
|
+
|
|
615
|
+
private
|
|
616
|
+
|
|
617
|
+
# The category followed by its parent at each level above it: the last
|
|
618
|
+
# parent whose index is not past the child's.
|
|
619
|
+
def lineage(category, parents)
|
|
620
|
+
parents.each_with_object([category]) do |level, chain|
|
|
621
|
+
break chain if level.empty?
|
|
622
|
+
|
|
623
|
+
chain << (level.take_while { |parent| parent.idx <= chain.last.idx }.last || level.first)
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# One category label.
|
|
629
|
+
class ChartCategory
|
|
630
|
+
def initialize(point, idx = nil)
|
|
631
|
+
@point = point
|
|
632
|
+
@idx = idx
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# The label, "" for an empty category.
|
|
636
|
+
def label = @point.nil? ? "" : @point.v.text
|
|
637
|
+
|
|
638
|
+
def idx = @point.nil? ? @idx : @point.idx
|
|
639
|
+
|
|
640
|
+
def to_s = label
|
|
641
|
+
alias to_str to_s
|
|
642
|
+
|
|
643
|
+
def ==(other) = other.is_a?(ChartCategory) ? label == other.label && idx == other.idx : label == other
|
|
644
|
+
|
|
645
|
+
def inspect = "#<Pptx::ChartCategory #{idx}:#{label.inspect}>"
|
|
646
|
+
end
|
|
647
|
+
end
|