ruby_pptx 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +83 -2
- data/lib/ruby_pptx/action.rb +12 -4
- data/lib/ruby_pptx/autoshape_spec.rb +6 -2
- data/lib/ruby_pptx/builder.rb +3 -1
- data/lib/ruby_pptx/chart/categories.rb +45 -15
- data/lib/ruby_pptx/chart/chart.rb +77 -27
- data/lib/ruby_pptx/chart/combo.rb +9 -3
- data/lib/ruby_pptx/chart/data.rb +45 -15
- data/lib/ruby_pptx/chart/format.rb +229 -76
- data/lib/ruby_pptx/chart/workbook_writer.rb +9 -3
- data/lib/ruby_pptx/chart/xml_writer.rb +54 -18
- data/lib/ruby_pptx/chart/xy_data.rb +68 -22
- data/lib/ruby_pptx/dml/color.rb +27 -9
- data/lib/ruby_pptx/dml/effect.rb +6 -2
- data/lib/ruby_pptx/dml/fill.rb +42 -14
- data/lib/ruby_pptx/element_proxy.rb +12 -4
- data/lib/ruby_pptx/enum/base.rb +30 -10
- data/lib/ruby_pptx/enum/prog_id.rb +3 -1
- data/lib/ruby_pptx/geometry.rb +32 -10
- data/lib/ruby_pptx/image.rb +27 -9
- data/lib/ruby_pptx/length.rb +94 -27
- data/lib/ruby_pptx/media.rb +12 -4
- data/lib/ruby_pptx/numeric_lengths.rb +34 -9
- data/lib/ruby_pptx/opc/oxml.rb +7 -5
- data/lib/ruby_pptx/opc/pack_uri.rb +15 -5
- data/lib/ruby_pptx/opc/package.rb +97 -31
- data/lib/ruby_pptx/opc/serialized.rb +15 -5
- data/lib/ruby_pptx/oxml/backend.rb +585 -0
- data/lib/ruby_pptx/oxml/chart.rb +112 -42
- data/lib/ruby_pptx/oxml/content_model.rb +6 -2
- data/lib/ruby_pptx/oxml/dml/color.rb +6 -2
- data/lib/ruby_pptx/oxml/dml/fill.rb +6 -2
- data/lib/ruby_pptx/oxml/element.rb +138 -101
- data/lib/ruby_pptx/oxml/ns.rb +7 -7
- data/lib/ruby_pptx/oxml/presentation.rb +9 -3
- data/lib/ruby_pptx/oxml/section.rb +3 -1
- data/lib/ruby_pptx/oxml/shapes/autoshape.rb +39 -13
- data/lib/ruby_pptx/oxml/shapes/groupshape.rb +15 -7
- data/lib/ruby_pptx/oxml/shapes/other.rb +47 -16
- data/lib/ruby_pptx/oxml/shapes/shared.rb +96 -30
- data/lib/ruby_pptx/oxml/simple_types.rb +169 -52
- data/lib/ruby_pptx/oxml/slide.rb +24 -8
- data/lib/ruby_pptx/oxml/table.rb +39 -13
- data/lib/ruby_pptx/oxml/text.rb +37 -15
- data/lib/ruby_pptx/oxml/theme.rb +13 -4
- data/lib/ruby_pptx/package.rb +9 -3
- data/lib/ruby_pptx/parts/chart.rb +6 -2
- data/lib/ruby_pptx/parts/image.rb +12 -4
- data/lib/ruby_pptx/parts/media.rb +3 -1
- data/lib/ruby_pptx/parts/presentation.rb +15 -5
- data/lib/ruby_pptx/parts/slide.rb +33 -11
- data/lib/ruby_pptx/parts/theme.rb +3 -1
- data/lib/ruby_pptx/pattern_matching.rb +6 -2
- data/lib/ruby_pptx/presentation.rb +24 -8
- data/lib/ruby_pptx/section.rb +30 -10
- data/lib/ruby_pptx/shapes/adjustments.rb +9 -3
- data/lib/ruby_pptx/shapes/authoring.rb +3 -1
- data/lib/ruby_pptx/shapes/base.rb +74 -24
- data/lib/ruby_pptx/shapes/freeform.rb +36 -12
- data/lib/ruby_pptx/shapes/placeholder.rb +44 -15
- data/lib/ruby_pptx/shapes/shape.rb +91 -30
- data/lib/ruby_pptx/shapes/shape_tree.rb +66 -22
- data/lib/ruby_pptx/slide.rb +123 -41
- data/lib/ruby_pptx/table.rb +105 -35
- data/lib/ruby_pptx/text/font_metrics.rb +30 -9
- data/lib/ruby_pptx/text/text.rb +108 -36
- data/lib/ruby_pptx/theme.rb +30 -10
- data/lib/ruby_pptx/version.rb +1 -1
- data/lib/ruby_pptx.rb +8 -1
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f8ea512f0616c3815f62322b952b77ff602f61cbb907de7168308aebd26507b
|
|
4
|
+
data.tar.gz: '00468b31384c6b3dee28b54c28934e802eff15a7e1f9a7654d30db47c72d1e7f'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c696588e9468af5de7c99e48fa7129b1066579eb85d1a955e9d1994d42c71da96765465c108653734b54a4fd301a73d6ef159b6e5a2c58c984bbb2612e05e8f
|
|
7
|
+
data.tar.gz: 19fe045441180661f4629bf93c46a1efc12e6ac62d6d31207bd792f871ff60f9c3bcb7e96ef74fb4632d98dea56a4d24192a9425071e0c811a9db37322dce9e3
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to this gem are recorded here. The format follows
|
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). Before 1.0 a minor
|
|
6
6
|
version may change the public API.
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-09-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- A pure-Ruby XML backend built on REXML, used automatically where Nokogiri
|
|
13
|
+
cannot be loaded -- notably ruby.wasm. It writes the same packages as the
|
|
14
|
+
Nokogiri backend; CI runs every spec under both, and builds, saves and
|
|
15
|
+
rereads a deck inside ruby.wasm. `Pptx.xml_backend` reports which is in
|
|
16
|
+
use, and `RUBY_PPTX_XML_BACKEND` chooses one explicitly.
|
|
17
|
+
- `rexml` is now a runtime dependency. Nokogiri remains one too.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `require "ruby_pptx"` no longer requires Nokogiri directly; the XML
|
|
22
|
+
backend loads it.
|
|
23
|
+
|
|
24
|
+
## [0.1.1] - 2026-09-28
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `gem install ruby_pptx` failed on Windows while generating ri
|
|
29
|
+
documentation (`Errno::ENOENT` on a very long `.ri` path). RDoc 7 and
|
|
30
|
+
earlier, which Ruby 3.3 to 4.0 bundle, cannot parse endless method
|
|
31
|
+
definitions (`def x = ...`); each one ended its class early, so every later
|
|
32
|
+
class was documented inside the one before it. On other platforms the
|
|
33
|
+
install succeeded but the ri documentation was wrong. The library no longer
|
|
34
|
+
uses endless methods, and CI checks that RDoc places every class correctly.
|
|
35
|
+
- `Presentation.open(path)` left a file handle open for every member of the
|
|
36
|
+
package until garbage collection, so on Windows the `.pptx` stayed locked
|
|
37
|
+
and could not be deleted or overwritten after opening it.
|
|
38
|
+
|
|
8
39
|
## [0.1.0] - 2026-09-28
|
|
9
40
|
|
|
10
41
|
The first release: a Ruby port of [python-pptx](https://github.com/scanny/python-pptx)
|
|
@@ -80,4 +111,6 @@ or where matching it cannot be done. Each case is listed, with its reason, in
|
|
|
80
111
|
- Getters that write to the document in python-pptx -- data-label flags, axis
|
|
81
112
|
and chart titles -- do not here.
|
|
82
113
|
|
|
114
|
+
[0.2.0]: https://github.com/Largo/ruby_pptx/releases/tag/v0.2.0
|
|
115
|
+
[0.1.1]: https://github.com/Largo/ruby_pptx/releases/tag/v0.1.1
|
|
83
116
|
[0.1.0]: https://github.com/Largo/ruby_pptx/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -92,6 +92,52 @@ end
|
|
|
92
92
|
deck.save("out.pptx")
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
Speaker notes are plain text for the common case, with the full text frame
|
|
96
|
+
underneath when you want formatting. Reading `notes` never creates a notes
|
|
97
|
+
page; assigning does.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
slide.notes = "Mention the Q3 dip.\nThank the team."
|
|
101
|
+
slide.notes #=> "Mention the Q3 dip.\nThank the team."
|
|
102
|
+
slide.notes_slide.notes_text_frame.paragraphs.last.runs.first.font.bold = true
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Picture, table and chart placeholders are filled in place, so the content takes
|
|
106
|
+
the layout's position and size. A picture is cropped to fill its placeholder,
|
|
107
|
+
never stretched.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
photo = deck.slides.add(deck.slide_layouts["Picture with Caption"])
|
|
111
|
+
photo.placeholders[1].insert_picture("office.jpg")
|
|
112
|
+
photo.placeholders[2].text_frame.text = "Our new office"
|
|
113
|
+
|
|
114
|
+
# The default template has no table or chart placeholders, so add a layout
|
|
115
|
+
# with both to its master.
|
|
116
|
+
layout = deck.slide_masters[0].slide_layouts.add("Table and Chart", type: "twoObj") do |l|
|
|
117
|
+
l.placeholders.add(:title)
|
|
118
|
+
l.placeholders.add(:table, idx: 1, at: [Pptx.inches(0.5), Pptx.inches(1.5)],
|
|
119
|
+
size: [Pptx.inches(4.25), Pptx.inches(4.5)])
|
|
120
|
+
l.placeholders.add(:chart, idx: 2, at: [Pptx.inches(5.25), Pptx.inches(1.5)],
|
|
121
|
+
size: [Pptx.inches(4.25), Pptx.inches(4.5)])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
report = deck.slides.add(layout)
|
|
125
|
+
report.placeholders[1].insert_table(3, 2).table[0, 0].text = "Region"
|
|
126
|
+
report.placeholders[2].insert_chart(:column_clustered, data)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Any file can be embedded as an OLE object, shown as an icon that opens it when
|
|
130
|
+
double-clicked. Word, Excel and PowerPoint files get Office's own icon; for
|
|
131
|
+
anything else, name its ProgID and optionally supply an icon.
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
sheet = slide.shapes.add_ole_object("budget.xlsx", prog_id: :xlsx, at: [x, y])
|
|
135
|
+
sheet.ole_format.prog_id #=> "Excel.Sheet.12"
|
|
136
|
+
|
|
137
|
+
slide.shapes.add_ole_object("report.pdf", prog_id: "AcroExch.Document", at: [x, y],
|
|
138
|
+
icon_file: "pdf-icon.png")
|
|
139
|
+
```
|
|
140
|
+
|
|
95
141
|
Five things python-pptx does not do: **SVG pictures**, **slide sections**,
|
|
96
142
|
**paging a long table across as many slides as it needs**, **combo charts**
|
|
97
143
|
with a secondary axis, and **defining a slide master in code**.
|
|
@@ -213,6 +259,36 @@ end
|
|
|
213
259
|
Collections index like arrays — `slides[2]`, `slides[-1]`, `slides[1..3]`,
|
|
214
260
|
`slides[1, 2]` — and deconstruct into array patterns.
|
|
215
261
|
|
|
262
|
+
## Nokogiri, REXML and ruby.wasm
|
|
263
|
+
|
|
264
|
+
ruby_pptx reads and writes XML with Nokogiri. Where Nokogiri cannot load --
|
|
265
|
+
ruby.wasm, in a browser or elsewhere -- it falls back to REXML, which is pure
|
|
266
|
+
Ruby, with no configuration:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
Pptx.xml_backend #=> :nokogiri, or :rexml without Nokogiri
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The output does not depend on the backend: CI runs every spec under both, and
|
|
273
|
+
the packages they write are identical part for part. REXML is slower -- in a
|
|
274
|
+
60-slide deck with text, tables and charts, about 3.3 s against 0.5 s to build
|
|
275
|
+
and save, and 0.7 s against 0.1 s to open and read it back. Set
|
|
276
|
+
`RUBY_PPTX_XML_BACKEND=rexml` (or `nokogiri`) to choose explicitly.
|
|
277
|
+
|
|
278
|
+
Nokogiri stays a declared dependency, so a normal `gem install` or `bundle
|
|
279
|
+
install` gets the fast path. Two things to know when loading the gem in
|
|
280
|
+
ruby.wasm yourself:
|
|
281
|
+
|
|
282
|
+
- Load `ruby_pptx`, `rubyzip` and `rexml`, but not `nokogiri`, which is
|
|
283
|
+
native. A loader that installs a gem together with its runtime
|
|
284
|
+
dependencies will stop at Nokogiri.
|
|
285
|
+
- Ship the whole `lib/` directory, not only its `.rb` files: the templates
|
|
286
|
+
in `lib/ruby_pptx/templates` (the default deck, notes pages, icons) are
|
|
287
|
+
read as files.
|
|
288
|
+
|
|
289
|
+
`tools/wasm/run.mjs` runs the gem this way under ruby.wasm's browser
|
|
290
|
+
filesystem, and CI uses it to build, save and reread a deck on every push.
|
|
291
|
+
|
|
216
292
|
## Development
|
|
217
293
|
|
|
218
294
|
```bash
|
|
@@ -258,8 +334,13 @@ git tag -a v0.1.0 -m "ruby_pptx 0.1.0" && git push origin v0.1.0
|
|
|
258
334
|
|
|
259
335
|
`.github/workflows/release.yml` runs the full CI suite, builds the gem, installs
|
|
260
336
|
it into an empty gem home to prove it loads, and creates a GitHub release with
|
|
261
|
-
the gem attached and the changelog entry as its notes.
|
|
262
|
-
|
|
337
|
+
the gem attached and the changelog entry as its notes.
|
|
338
|
+
|
|
339
|
+
Publishing to RubyGems is a separate step: in the Actions tab, open **Publish to
|
|
340
|
+
RubyGems**, choose **Run workflow**, and pick the release's tag under "Use
|
|
341
|
+
workflow from". It refuses to run from a branch. RubyGems' `release-gem` action
|
|
342
|
+
builds the tagged commit and pushes it, authenticating by RubyGems trusted
|
|
343
|
+
publishing rather than a stored API key.
|
|
263
344
|
|
|
264
345
|
## License
|
|
265
346
|
|
data/lib/ruby_pptx/action.rb
CHANGED
|
@@ -33,7 +33,9 @@ module Pptx
|
|
|
33
33
|
@hover = hover
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def part
|
|
36
|
+
def part
|
|
37
|
+
@parent.part
|
|
38
|
+
end
|
|
37
39
|
|
|
38
40
|
# @return [Pptx::Enum::PP_ACTION] NONE when nothing happens on click
|
|
39
41
|
def action
|
|
@@ -78,7 +80,9 @@ module Pptx
|
|
|
78
80
|
#
|
|
79
81
|
# Unlike {#address} this does not report a slide partname for a jump: a
|
|
80
82
|
# thing called a URL should be a URL.
|
|
81
|
-
def url
|
|
83
|
+
def url
|
|
84
|
+
action == Enum::PP_ACTION::HYPERLINK ? address : nil
|
|
85
|
+
end
|
|
82
86
|
|
|
83
87
|
# The slide this click jumps to, or nil.
|
|
84
88
|
def target_slide
|
|
@@ -107,11 +111,15 @@ module Pptx
|
|
|
107
111
|
self
|
|
108
112
|
end
|
|
109
113
|
|
|
110
|
-
def inspect
|
|
114
|
+
def inspect
|
|
115
|
+
"#<Pptx::ActionSetting #{action.name} #{address.inspect}>"
|
|
116
|
+
end
|
|
111
117
|
|
|
112
118
|
private
|
|
113
119
|
|
|
114
|
-
def hlink
|
|
120
|
+
def hlink
|
|
121
|
+
@element.find(@hover ? "a:hlinkHover" : "a:hlinkClick")
|
|
122
|
+
end
|
|
115
123
|
|
|
116
124
|
def get_or_add_hlink
|
|
117
125
|
@hover ? @element.get_or_add_hlinkHover : @element.get_or_add_hlinkClick
|
|
@@ -330,8 +330,12 @@ module Pptx
|
|
|
330
330
|
end
|
|
331
331
|
end
|
|
332
332
|
|
|
333
|
-
def basename(shape_type)
|
|
333
|
+
def basename(shape_type)
|
|
334
|
+
self.for(shape_type)[:basename]
|
|
335
|
+
end
|
|
334
336
|
|
|
335
|
-
def default_adjustments(shape_type)
|
|
337
|
+
def default_adjustments(shape_type)
|
|
338
|
+
self.for(shape_type)[:default_adjustments]
|
|
339
|
+
end
|
|
336
340
|
end
|
|
337
341
|
end
|
data/lib/ruby_pptx/builder.rb
CHANGED
|
@@ -223,6 +223,8 @@ module Pptx
|
|
|
223
223
|
end
|
|
224
224
|
|
|
225
225
|
# A colour may be given as an RGBColor or as a hex string.
|
|
226
|
-
def rgb(value)
|
|
226
|
+
def rgb(value)
|
|
227
|
+
value.is_a?(RGBColor) ? value : RGBColor.from_string(value)
|
|
228
|
+
end
|
|
227
229
|
end
|
|
228
230
|
end
|
|
@@ -54,14 +54,22 @@ module Pptx
|
|
|
54
54
|
ChartDataCategory.new(label, self).tap { |category| @categories << category }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
def each(&)
|
|
57
|
+
def each(&)
|
|
58
|
+
@categories.each(&)
|
|
59
|
+
end
|
|
58
60
|
|
|
59
|
-
def [](index)
|
|
61
|
+
def [](index)
|
|
62
|
+
@categories[index]
|
|
63
|
+
end
|
|
60
64
|
|
|
61
|
-
def size
|
|
65
|
+
def size
|
|
66
|
+
@categories.size
|
|
67
|
+
end
|
|
62
68
|
alias length size
|
|
63
69
|
|
|
64
|
-
def empty?
|
|
70
|
+
def empty?
|
|
71
|
+
@categories.empty?
|
|
72
|
+
end
|
|
65
73
|
|
|
66
74
|
# Levels of labels: 0 with no categories, 1 for a plain list.
|
|
67
75
|
#
|
|
@@ -76,7 +84,9 @@ module Pptx
|
|
|
76
84
|
end
|
|
77
85
|
|
|
78
86
|
# The number of categories actually plotted.
|
|
79
|
-
def leaf_count
|
|
87
|
+
def leaf_count
|
|
88
|
+
@categories.sum(&:leaf_count)
|
|
89
|
+
end
|
|
80
90
|
|
|
81
91
|
# Each level as [index, label] pairs, leaf level first. The index is the
|
|
82
92
|
# leaf position where that label's run begins.
|
|
@@ -85,11 +95,17 @@ module Pptx
|
|
|
85
95
|
end
|
|
86
96
|
|
|
87
97
|
# The leaf labels in plotting order.
|
|
88
|
-
def leaf_labels
|
|
98
|
+
def leaf_labels
|
|
99
|
+
levels.first.to_a.map(&:last)
|
|
100
|
+
end
|
|
89
101
|
|
|
90
|
-
def dates?
|
|
102
|
+
def dates?
|
|
103
|
+
depth == 1 && date?(first.label)
|
|
104
|
+
end
|
|
91
105
|
|
|
92
|
-
def numeric?
|
|
106
|
+
def numeric?
|
|
107
|
+
depth == 1 && (first.label.is_a?(Numeric) || date?(first.label))
|
|
108
|
+
end
|
|
93
109
|
|
|
94
110
|
# The number format for the categories: as set, or a date format for
|
|
95
111
|
# dates, or "General".
|
|
@@ -111,7 +127,9 @@ module Pptx
|
|
|
111
127
|
raise Error, "category not in these categories"
|
|
112
128
|
end
|
|
113
129
|
|
|
114
|
-
def inspect
|
|
130
|
+
def inspect
|
|
131
|
+
"#<Pptx::ChartDataCategories depth=#{depth} #{leaf_labels.inspect}>"
|
|
132
|
+
end
|
|
115
133
|
|
|
116
134
|
private
|
|
117
135
|
|
|
@@ -121,7 +139,9 @@ module Pptx
|
|
|
121
139
|
lower + [categories.map { |category| [category.idx, category.label] }]
|
|
122
140
|
end
|
|
123
141
|
|
|
124
|
-
def date?(value)
|
|
142
|
+
def date?(value)
|
|
143
|
+
value.is_a?(Date) || value.is_a?(Time)
|
|
144
|
+
end
|
|
125
145
|
end
|
|
126
146
|
|
|
127
147
|
# One category, or a group of them when it has sub-categories.
|
|
@@ -135,7 +155,9 @@ module Pptx
|
|
|
135
155
|
end
|
|
136
156
|
|
|
137
157
|
# The label; "" for a category given as nil.
|
|
138
|
-
def label
|
|
158
|
+
def label
|
|
159
|
+
@label.nil? ? "" : @label
|
|
160
|
+
end
|
|
139
161
|
|
|
140
162
|
# @return [ChartDataCategory] the new sub-category
|
|
141
163
|
def add_sub_category(label)
|
|
@@ -161,10 +183,14 @@ module Pptx
|
|
|
161
183
|
depths.first + 1
|
|
162
184
|
end
|
|
163
185
|
|
|
164
|
-
def leaf_count
|
|
186
|
+
def leaf_count
|
|
187
|
+
@sub_categories.empty? ? 1 : @sub_categories.sum(&:leaf_count)
|
|
188
|
+
end
|
|
165
189
|
|
|
166
190
|
# The leaf position at which this category's run begins.
|
|
167
|
-
def idx
|
|
191
|
+
def idx
|
|
192
|
+
@parent.index_of(self)
|
|
193
|
+
end
|
|
168
194
|
|
|
169
195
|
# @api private
|
|
170
196
|
def index_of(sub_category)
|
|
@@ -190,9 +216,13 @@ module Pptx
|
|
|
190
216
|
end
|
|
191
217
|
|
|
192
218
|
# The label as it appears in the category cache and worksheet.
|
|
193
|
-
def to_s
|
|
219
|
+
def to_s
|
|
220
|
+
label.to_s
|
|
221
|
+
end
|
|
194
222
|
|
|
195
|
-
def inspect
|
|
223
|
+
def inspect
|
|
224
|
+
"#<Pptx::ChartDataCategory #{label.inspect}>"
|
|
225
|
+
end
|
|
196
226
|
|
|
197
227
|
private
|
|
198
228
|
|
|
@@ -31,10 +31,14 @@ module Pptx
|
|
|
31
31
|
# python-pptx's PlotTypeInspector does.
|
|
32
32
|
#
|
|
33
33
|
# @return [Pptx::Enum::Member] a member of XL_CHART_TYPE
|
|
34
|
-
def chart_type
|
|
34
|
+
def chart_type
|
|
35
|
+
ChartTypeInspector.chart_type(plot_area.plot_elements.first)
|
|
36
|
+
end
|
|
35
37
|
|
|
36
38
|
# The chart style number, 1 to 48, or nil when none is set.
|
|
37
|
-
def chart_style
|
|
39
|
+
def chart_style
|
|
40
|
+
@element.style&.val
|
|
41
|
+
end
|
|
38
42
|
|
|
39
43
|
def chart_style=(value)
|
|
40
44
|
@element.remove_style
|
|
@@ -42,7 +46,9 @@ module Pptx
|
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
# The default font for all text in the chart, created on first use.
|
|
45
|
-
def font
|
|
49
|
+
def font
|
|
50
|
+
@font ||= Font.new(@element.defRPr)
|
|
51
|
+
end
|
|
46
52
|
|
|
47
53
|
# Every series in the chart: plot by plot, and within a plot in the order
|
|
48
54
|
# it draws them.
|
|
@@ -65,7 +71,9 @@ module Pptx
|
|
|
65
71
|
end
|
|
66
72
|
|
|
67
73
|
# Whether a legend is drawn.
|
|
68
|
-
def legend?
|
|
74
|
+
def legend?
|
|
75
|
+
!chart_element.legend.nil?
|
|
76
|
+
end
|
|
69
77
|
|
|
70
78
|
def legend=(value)
|
|
71
79
|
value ? chart_element.get_or_add_legend : chart_element.remove_legend
|
|
@@ -78,7 +86,9 @@ module Pptx
|
|
|
78
86
|
element.nil? ? nil : (@legend ||= ChartLegend.new(element))
|
|
79
87
|
end
|
|
80
88
|
|
|
81
|
-
def title?
|
|
89
|
+
def title?
|
|
90
|
+
!chart_element.title.nil?
|
|
91
|
+
end
|
|
82
92
|
|
|
83
93
|
# @return [ChartTitle, nil]
|
|
84
94
|
def title
|
|
@@ -105,7 +115,9 @@ module Pptx
|
|
|
105
115
|
#
|
|
106
116
|
# Nearly every chart has exactly one; a combo chart has several, which is
|
|
107
117
|
# why this is a collection rather than a property of the chart.
|
|
108
|
-
def plots
|
|
118
|
+
def plots
|
|
119
|
+
plot_area.plot_elements.map { |element| ChartPlot.new(element, self) }
|
|
120
|
+
end
|
|
109
121
|
|
|
110
122
|
# The horizontal axis: a category or date axis, or, on an XY or bubble
|
|
111
123
|
# chart, the first value axis. nil for a chart with no axes, such as a pie.
|
|
@@ -129,7 +141,9 @@ module Pptx
|
|
|
129
141
|
ValueAxis.new(axes[axes.size > 1 ? 1 : 0])
|
|
130
142
|
end
|
|
131
143
|
|
|
132
|
-
def value_axes
|
|
144
|
+
def value_axes
|
|
145
|
+
plot_area.value_axes.map { |element| ValueAxis.new(element) }
|
|
146
|
+
end
|
|
133
147
|
|
|
134
148
|
# The category labels cached in the chart XML.
|
|
135
149
|
def categories
|
|
@@ -139,13 +153,19 @@ module Pptx
|
|
|
139
153
|
first.xpath("./c:cat//c:pt/c:v").map(&:text)
|
|
140
154
|
end
|
|
141
155
|
|
|
142
|
-
def inspect
|
|
156
|
+
def inspect
|
|
157
|
+
"#<Pptx::Chart #{chart_type.name} series=#{series.size}>"
|
|
158
|
+
end
|
|
143
159
|
|
|
144
160
|
private
|
|
145
161
|
|
|
146
|
-
def chart_element
|
|
162
|
+
def chart_element
|
|
163
|
+
@element.chart
|
|
164
|
+
end
|
|
147
165
|
|
|
148
|
-
def plot_area
|
|
166
|
+
def plot_area
|
|
167
|
+
chart_element.plotArea
|
|
168
|
+
end
|
|
149
169
|
end
|
|
150
170
|
|
|
151
171
|
# Works out the full chart type from a plot element, after python-pptx's
|
|
@@ -200,11 +220,17 @@ module Pptx
|
|
|
200
220
|
rule.call(plot)
|
|
201
221
|
end
|
|
202
222
|
|
|
203
|
-
def line_markers?(plot)
|
|
223
|
+
def line_markers?(plot)
|
|
224
|
+
plot.xpath('c:ser/c:marker/c:symbol[@val="none"]').empty?
|
|
225
|
+
end
|
|
204
226
|
|
|
205
|
-
def exploded?(plot)
|
|
227
|
+
def exploded?(plot)
|
|
228
|
+
!plot.xpath("./c:ser/c:explosion").empty?
|
|
229
|
+
end
|
|
206
230
|
|
|
207
|
-
def first_symbol(plot)
|
|
231
|
+
def first_symbol(plot)
|
|
232
|
+
plot.xpath("c:ser/c:marker/c:symbol").first&.get("val")
|
|
233
|
+
end
|
|
208
234
|
|
|
209
235
|
def bubble(plot)
|
|
210
236
|
bubble3d = plot.xpath("c:ser/c:bubble3D").first
|
|
@@ -260,17 +286,27 @@ module Pptx
|
|
|
260
286
|
end
|
|
261
287
|
|
|
262
288
|
# The fill and outline of this series.
|
|
263
|
-
def format
|
|
289
|
+
def format
|
|
290
|
+
@format ||= ChartFormat.new(@element)
|
|
291
|
+
end
|
|
264
292
|
|
|
265
293
|
# The series' chart-wide index.
|
|
266
|
-
def index
|
|
294
|
+
def index
|
|
295
|
+
@element.idx.val
|
|
296
|
+
end
|
|
267
297
|
|
|
268
|
-
def name
|
|
298
|
+
def name
|
|
299
|
+
@element.xpath("./c:tx//c:pt/c:v").first&.text.to_s
|
|
300
|
+
end
|
|
269
301
|
|
|
270
302
|
# Cached values, with nil where the chart records a gap.
|
|
271
|
-
def values
|
|
303
|
+
def values
|
|
304
|
+
cached(@element.val)
|
|
305
|
+
end
|
|
272
306
|
|
|
273
|
-
def inspect
|
|
307
|
+
def inspect
|
|
308
|
+
"#<#{self.class.name} #{name.inspect}>"
|
|
309
|
+
end
|
|
274
310
|
|
|
275
311
|
private
|
|
276
312
|
|
|
@@ -286,15 +322,21 @@ module Pptx
|
|
|
286
322
|
# Data labels and per-point formatting, for series plotted by category.
|
|
287
323
|
module CategorySeriesFeatures
|
|
288
324
|
# This series' own data labels, created on first use.
|
|
289
|
-
def data_labels
|
|
325
|
+
def data_labels
|
|
326
|
+
@data_labels ||= ChartDataLabels.new(@element.get_or_add_dLbls)
|
|
327
|
+
end
|
|
290
328
|
|
|
291
329
|
# The points of this series, one per category.
|
|
292
|
-
def points
|
|
330
|
+
def points
|
|
331
|
+
ChartPoints.new(@element, @element.cat_ptCount_val)
|
|
332
|
+
end
|
|
293
333
|
end
|
|
294
334
|
|
|
295
335
|
# Markers, for the kinds of series that draw them.
|
|
296
336
|
module MarkerFeatures
|
|
297
|
-
def marker
|
|
337
|
+
def marker
|
|
338
|
+
@marker ||= Marker.new(@element)
|
|
339
|
+
end
|
|
298
340
|
end
|
|
299
341
|
|
|
300
342
|
class AreaSeriesView < ChartSeriesView
|
|
@@ -310,7 +352,9 @@ module Pptx
|
|
|
310
352
|
|
|
311
353
|
# Whether a negative bar is drawn in inverted colours. An absent
|
|
312
354
|
# `c:invertIfNegative` reads as true, the schema default.
|
|
313
|
-
def invert_if_negative?
|
|
355
|
+
def invert_if_negative?
|
|
356
|
+
@element.invertIfNegative.nil? || @element.invertIfNegative.val
|
|
357
|
+
end
|
|
314
358
|
|
|
315
359
|
def invert_if_negative=(value)
|
|
316
360
|
@element.get_or_add_invertIfNegative.val = value ? true : false
|
|
@@ -323,7 +367,9 @@ module Pptx
|
|
|
323
367
|
|
|
324
368
|
# Whether the line is drawn as a smooth curve. An absent `c:smooth` reads
|
|
325
369
|
# as true, the schema default.
|
|
326
|
-
def smooth?
|
|
370
|
+
def smooth?
|
|
371
|
+
@element.smooth.nil? || @element.smooth.val
|
|
372
|
+
end
|
|
327
373
|
|
|
328
374
|
def smooth=(value)
|
|
329
375
|
@element.get_or_add_smooth.val = value ? true : false
|
|
@@ -339,9 +385,13 @@ module Pptx
|
|
|
339
385
|
class XySeriesView < ChartSeriesView
|
|
340
386
|
include MarkerFeatures
|
|
341
387
|
|
|
342
|
-
def values
|
|
388
|
+
def values
|
|
389
|
+
cached(@element.yVal)
|
|
390
|
+
end
|
|
343
391
|
|
|
344
|
-
def points
|
|
392
|
+
def points
|
|
393
|
+
ChartPoints.new(@element, [@element.xVal_ptCount_val, @element.yVal_ptCount_val].min)
|
|
394
|
+
end
|
|
345
395
|
end
|
|
346
396
|
|
|
347
397
|
class BubbleSeriesView < XySeriesView
|
|
@@ -387,10 +437,10 @@ module Pptx
|
|
|
387
437
|
|
|
388
438
|
next_index = plot_area.series_elements.size
|
|
389
439
|
count.times do |offset|
|
|
390
|
-
copy = last.parent.
|
|
440
|
+
copy = last.parent.import(last)
|
|
391
441
|
copy.get_or_add_idx.val = next_index + offset
|
|
392
442
|
copy.get_or_add_order.val = next_index + offset
|
|
393
|
-
last.
|
|
443
|
+
last.add_next_sibling(copy)
|
|
394
444
|
last = copy
|
|
395
445
|
end
|
|
396
446
|
end
|
|
@@ -62,7 +62,9 @@ module Pptx
|
|
|
62
62
|
self
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def secondary_axis?
|
|
65
|
+
def secondary_axis?
|
|
66
|
+
@specs.any?(&:secondary_axis)
|
|
67
|
+
end
|
|
66
68
|
|
|
67
69
|
private
|
|
68
70
|
|
|
@@ -122,7 +124,9 @@ module Pptx
|
|
|
122
124
|
|
|
123
125
|
private
|
|
124
126
|
|
|
125
|
-
def plot_elements
|
|
127
|
+
def plot_elements
|
|
128
|
+
@builder.specs.map { |spec| plot_element(spec) }.join
|
|
129
|
+
end
|
|
126
130
|
|
|
127
131
|
def plot_element(spec)
|
|
128
132
|
family = ChartXmlWriter.family_of(spec.chart_type)
|
|
@@ -141,7 +145,9 @@ module Pptx
|
|
|
141
145
|
XML
|
|
142
146
|
end
|
|
143
147
|
|
|
144
|
-
def plot_tag(family)
|
|
148
|
+
def plot_tag(family)
|
|
149
|
+
{ bar: "c:barChart", line: "c:lineChart", area: "c:areaChart" }[family]
|
|
150
|
+
end
|
|
145
151
|
|
|
146
152
|
def bar_plot_body(spec)
|
|
147
153
|
direction = ChartXmlWriter::BarChartWriter::BAR_TYPES.include?(spec.chart_type.name) ? "bar" : "col"
|