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,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/chart/xml_writer"
|
|
4
|
+
|
|
5
|
+
module Pptx
|
|
6
|
+
# Describes a chart that draws more than one plot over the same categories --
|
|
7
|
+
# a bar chart with a line over it, most often.
|
|
8
|
+
#
|
|
9
|
+
# python-pptx can read such a chart but only ever writes one plot, so this is
|
|
10
|
+
# new ground rather than a port.
|
|
11
|
+
#
|
|
12
|
+
# slide.shapes.add_combo_chart(data, at: [x, y], size: [w, h]) do |combo|
|
|
13
|
+
# combo.plot :column_clustered, series: "Revenue"
|
|
14
|
+
# combo.plot :line, series: "Margin", secondary_axis: true
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# All the series live in one {ChartData}, because they share a worksheet;
|
|
18
|
+
# each plot names the ones it draws.
|
|
19
|
+
class ComboChartBuilder
|
|
20
|
+
# What a plot draws and how.
|
|
21
|
+
PlotSpec = Data.define(:chart_type, :series, :secondary_axis)
|
|
22
|
+
|
|
23
|
+
# The families that can share a category axis. A pie or a scatter cannot,
|
|
24
|
+
# so they are not combinable.
|
|
25
|
+
COMBINABLE = %i[bar line area].freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :chart_data, :specs
|
|
28
|
+
|
|
29
|
+
def initialize(chart_data)
|
|
30
|
+
@chart_data = chart_data
|
|
31
|
+
@specs = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Add a plot drawing +series+.
|
|
35
|
+
#
|
|
36
|
+
# @param series [String, Integer, Array] series names or indices; all of
|
|
37
|
+
# them when omitted
|
|
38
|
+
# @param secondary_axis [Boolean] draw against a second value axis on the
|
|
39
|
+
# right, for values on a different scale
|
|
40
|
+
def plot(chart_type, series: nil, secondary_axis: false)
|
|
41
|
+
member = Enum::XL_CHART_TYPE.fetch(chart_type)
|
|
42
|
+
family = ChartXmlWriter.family_of(member)
|
|
43
|
+
unless COMBINABLE.include?(family)
|
|
44
|
+
raise Error,
|
|
45
|
+
"a #{member.name} chart cannot share a category axis, so it " \
|
|
46
|
+
"cannot be part of a combo chart"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@specs << PlotSpec.new(chart_type: member, series: resolve(series),
|
|
50
|
+
secondary_axis: secondary_axis)
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @raise [Error] when the combination could not be drawn
|
|
55
|
+
def validate!
|
|
56
|
+
raise Error, "a combo chart needs at least two plots" if @specs.size < 2
|
|
57
|
+
|
|
58
|
+
drawn = @specs.flat_map(&:series)
|
|
59
|
+
duplicated = drawn.map(&:index).tally.select { |_, count| count > 1 }.keys
|
|
60
|
+
raise Error, "series #{duplicated.inspect} appear in more than one plot" unless duplicated.empty?
|
|
61
|
+
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def secondary_axis? = @specs.any?(&:secondary_axis)
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Accepts a name, an index, or a list of either; nil means every series.
|
|
70
|
+
def resolve(selector)
|
|
71
|
+
return @chart_data.series if selector.nil?
|
|
72
|
+
|
|
73
|
+
Array(selector).map do |key|
|
|
74
|
+
case key
|
|
75
|
+
when Integer then @chart_data.series[key]
|
|
76
|
+
when String then @chart_data.series.find { |s| s.name == key }
|
|
77
|
+
else key
|
|
78
|
+
end || raise(NotFoundError, "no series #{key.inspect} in this chart data")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Writes the `c:chartSpace` for a combo chart.
|
|
84
|
+
#
|
|
85
|
+
# The plot area holds one element per plot followed by the axes, which is the
|
|
86
|
+
# order the schema requires. Every plot names exactly two axis ids and each
|
|
87
|
+
# must be backed by a real axis element, so a secondary value axis brings a
|
|
88
|
+
# category axis with it -- a hidden one, since the categories are already
|
|
89
|
+
# drawn by the primary pair.
|
|
90
|
+
class ComboChartWriter < ChartXmlWriter::Base
|
|
91
|
+
CAT_AX_ID = "-2068027336"
|
|
92
|
+
VAL_AX_ID = "-2113994440"
|
|
93
|
+
SECONDARY_CAT_AX_ID = "-2068027337"
|
|
94
|
+
SECONDARY_VAL_AX_ID = "-2113994441"
|
|
95
|
+
|
|
96
|
+
def initialize(builder)
|
|
97
|
+
super(nil, builder.chart_data)
|
|
98
|
+
@builder = builder
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def xml
|
|
102
|
+
<<~XML
|
|
103
|
+
#{ChartXmlWriter::Base::DECLARATION}
|
|
104
|
+
<c:chartSpace #{ChartXmlWriter::Base::CHART_SPACE_NS}>
|
|
105
|
+
<c:date1904 val="0"/>
|
|
106
|
+
<c:chart>
|
|
107
|
+
<c:autoTitleDeleted val="0"/>
|
|
108
|
+
<c:plotArea>
|
|
109
|
+
<c:layout/>
|
|
110
|
+
#{indent(plot_elements, 6).chomp}
|
|
111
|
+
#{indent(axes_xml, 6).chomp}
|
|
112
|
+
</c:plotArea>
|
|
113
|
+
#{indent(ChartXmlWriter::Base::LEGEND, 4).chomp}
|
|
114
|
+
<c:plotVisOnly val="1"/>
|
|
115
|
+
<c:dispBlanksAs val="gap"/>
|
|
116
|
+
<c:showDLblsOverMax val="0"/>
|
|
117
|
+
</c:chart>
|
|
118
|
+
#{indent(ChartXmlWriter::Base::TEXT_PROPERTIES, 2).chomp}
|
|
119
|
+
</c:chartSpace>
|
|
120
|
+
XML
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def plot_elements = @builder.specs.map { |spec| plot_element(spec) }.join
|
|
126
|
+
|
|
127
|
+
def plot_element(spec)
|
|
128
|
+
family = ChartXmlWriter.family_of(spec.chart_type)
|
|
129
|
+
value_ax = spec.secondary_axis ? SECONDARY_VAL_AX_ID : VAL_AX_ID
|
|
130
|
+
cat_ax = spec.secondary_axis ? SECONDARY_CAT_AX_ID : CAT_AX_ID
|
|
131
|
+
body = send(:"#{family}_plot_body", spec)
|
|
132
|
+
|
|
133
|
+
<<~XML
|
|
134
|
+
<#{plot_tag(family)}>
|
|
135
|
+
#{indent(body, 2).chomp}
|
|
136
|
+
#{indent(spec.series.map { |s| series_xml(s) }.join, 2).chomp}
|
|
137
|
+
#{indent(plot_tail(family, spec), 2).chomp}
|
|
138
|
+
<c:axId val="#{cat_ax}"/>
|
|
139
|
+
<c:axId val="#{value_ax}"/>
|
|
140
|
+
</#{plot_tag(family)}>
|
|
141
|
+
XML
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def plot_tag(family) = { bar: "c:barChart", line: "c:lineChart", area: "c:areaChart" }[family]
|
|
145
|
+
|
|
146
|
+
def bar_plot_body(spec)
|
|
147
|
+
direction = ChartXmlWriter::BarChartWriter::BAR_TYPES.include?(spec.chart_type.name) ? "bar" : "col"
|
|
148
|
+
grouping = ChartXmlWriter::BarChartWriter::GROUPINGS.fetch(spec.chart_type.name)
|
|
149
|
+
%(<c:barDir val="#{direction}"/>\n<c:grouping val="#{grouping}"/>\n)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def line_plot_body(spec)
|
|
153
|
+
grouping = ChartXmlWriter::LineChartWriter::GROUPINGS.fetch(spec.chart_type.name)
|
|
154
|
+
%(<c:grouping val="#{grouping}"/>\n<c:varyColors val="0"/>\n)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def area_plot_body(spec)
|
|
158
|
+
grouping = ChartXmlWriter::AreaChartWriter::GROUPINGS.fetch(spec.chart_type.name)
|
|
159
|
+
%(<c:grouping val="#{grouping}"/>\n<c:varyColors val="0"/>\n)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def plot_tail(family, spec)
|
|
163
|
+
case family
|
|
164
|
+
when :bar
|
|
165
|
+
stacked = ChartXmlWriter::BarChartWriter::STACKED_TYPES.include?(spec.chart_type.name)
|
|
166
|
+
stacked ? %(<c:overlap val="100"/>\n) : ""
|
|
167
|
+
when :line
|
|
168
|
+
%(<c:marker val="1"/>\n<c:smooth val="0"/>\n)
|
|
169
|
+
else ""
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def axes_xml
|
|
174
|
+
xml = +""
|
|
175
|
+
xml << category_axis_xml(CAT_AX_ID, VAL_AX_ID, hidden: false)
|
|
176
|
+
xml << value_axis_xml(VAL_AX_ID, CAT_AX_ID, position: "l", crosses: "autoZero")
|
|
177
|
+
return xml unless @builder.secondary_axis?
|
|
178
|
+
|
|
179
|
+
# The secondary pair: a value axis on the right, and the category axis
|
|
180
|
+
# the schema demands to go with it, hidden because the primary pair
|
|
181
|
+
# already draws the categories.
|
|
182
|
+
xml << category_axis_xml(SECONDARY_CAT_AX_ID, SECONDARY_VAL_AX_ID, hidden: true)
|
|
183
|
+
xml << value_axis_xml(SECONDARY_VAL_AX_ID, SECONDARY_CAT_AX_ID,
|
|
184
|
+
position: "r", crosses: "max")
|
|
185
|
+
xml
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def category_axis_xml(ax_id, cross_ax_id, hidden:)
|
|
189
|
+
<<~XML
|
|
190
|
+
<c:catAx>
|
|
191
|
+
<c:axId val="#{ax_id}"/>
|
|
192
|
+
<c:scaling>
|
|
193
|
+
<c:orientation val="minMax"/>
|
|
194
|
+
</c:scaling>
|
|
195
|
+
<c:delete val="#{hidden ? 1 : 0}"/>
|
|
196
|
+
<c:axPos val="b"/>
|
|
197
|
+
<c:majorTickMark val="out"/>
|
|
198
|
+
<c:minorTickMark val="none"/>
|
|
199
|
+
<c:tickLblPos val="nextTo"/>
|
|
200
|
+
<c:crossAx val="#{cross_ax_id}"/>
|
|
201
|
+
<c:crosses val="autoZero"/>
|
|
202
|
+
<c:auto val="1"/>
|
|
203
|
+
<c:lblAlgn val="ctr"/>
|
|
204
|
+
<c:lblOffset val="100"/>
|
|
205
|
+
<c:noMultiLvlLbl val="0"/>
|
|
206
|
+
</c:catAx>
|
|
207
|
+
XML
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def value_axis_xml(ax_id, cross_ax_id, position:, crosses:)
|
|
211
|
+
<<~XML
|
|
212
|
+
<c:valAx>
|
|
213
|
+
<c:axId val="#{ax_id}"/>
|
|
214
|
+
<c:scaling>
|
|
215
|
+
<c:orientation val="minMax"/>
|
|
216
|
+
</c:scaling>
|
|
217
|
+
<c:delete val="0"/>
|
|
218
|
+
<c:axPos val="#{position}"/>
|
|
219
|
+
#{"<c:majorGridlines/>" if position == "l"}
|
|
220
|
+
<c:numFmt formatCode="General" sourceLinked="1"/>
|
|
221
|
+
<c:majorTickMark val="out"/>
|
|
222
|
+
<c:minorTickMark val="none"/>
|
|
223
|
+
<c:tickLblPos val="nextTo"/>
|
|
224
|
+
<c:crossAx val="#{cross_ax_id}"/>
|
|
225
|
+
<c:crosses val="#{crosses}"/>
|
|
226
|
+
</c:valAx>
|
|
227
|
+
XML
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/errors"
|
|
4
|
+
require "ruby_pptx/chart/categories"
|
|
5
|
+
|
|
6
|
+
module Pptx
|
|
7
|
+
# The categories and series a chart depicts.
|
|
8
|
+
#
|
|
9
|
+
# data = Pptx::ChartData.new
|
|
10
|
+
# data.categories = ["East", "West", "Midwest"]
|
|
11
|
+
# data.add_series("Q1", [1.2, 2.0, 3.5])
|
|
12
|
+
# data.add_series("Q2", [4.1, 5.0, 6.2])
|
|
13
|
+
#
|
|
14
|
+
# Categories can be grouped, which draws a multi-level axis, and can be
|
|
15
|
+
# dates, which draws a date axis; see {ChartDataCategories}.
|
|
16
|
+
#
|
|
17
|
+
# The values also become an embedded Excel workbook, which is what PowerPoint
|
|
18
|
+
# opens when the user chooses "Edit Data". The worksheet references in the
|
|
19
|
+
# chart XML are derived from the same layout, so the two always agree.
|
|
20
|
+
class ChartData
|
|
21
|
+
include Enumerable
|
|
22
|
+
|
|
23
|
+
# The row the first data value sits on; row 1 holds the series names.
|
|
24
|
+
FIRST_DATA_ROW = 2
|
|
25
|
+
# Category labels fill the first columns, one per level; series follow.
|
|
26
|
+
WORKSHEET_NAME = "Sheet1"
|
|
27
|
+
DEFAULT_NUMBER_FORMAT = "General"
|
|
28
|
+
|
|
29
|
+
attr_reader :series, :number_format, :categories
|
|
30
|
+
|
|
31
|
+
def initialize(number_format: DEFAULT_NUMBER_FORMAT)
|
|
32
|
+
@categories = ChartDataCategories.new
|
|
33
|
+
@series = []
|
|
34
|
+
@number_format = number_format
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Replace the categories: a list of labels, or a Hash of groups whose
|
|
38
|
+
# values are their sub-categories.
|
|
39
|
+
def categories=(values)
|
|
40
|
+
@categories = values.is_a?(ChartDataCategories) ? values : ChartDataCategories.from(values)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Add a series of values, one per category.
|
|
44
|
+
#
|
|
45
|
+
# @return [ChartSeries]
|
|
46
|
+
def add_series(name, values, number_format: nil)
|
|
47
|
+
series = ChartSeries.new(self, @series.size, name, values.to_a,
|
|
48
|
+
number_format || @number_format)
|
|
49
|
+
@series << series
|
|
50
|
+
series
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def each(&) = @series.each(&)
|
|
54
|
+
|
|
55
|
+
def size = @series.size
|
|
56
|
+
|
|
57
|
+
# The number of categories plotted: the leaves, when they are grouped.
|
|
58
|
+
def category_count = @categories.leaf_count
|
|
59
|
+
|
|
60
|
+
# True when the categories are numbers or dates, which makes the category
|
|
61
|
+
# cache numeric rather than textual. Decided by the first category, as
|
|
62
|
+
# python-pptx decides it.
|
|
63
|
+
def numeric_categories? = @categories.numeric?
|
|
64
|
+
|
|
65
|
+
# @api private
|
|
66
|
+
# The category block: one column per level, one row per leaf.
|
|
67
|
+
def categories_ref
|
|
68
|
+
raise Error, "chart data contains no categories" if @categories.depth.zero?
|
|
69
|
+
|
|
70
|
+
right = ChartData.column_reference(@categories.depth)
|
|
71
|
+
"#{WORKSHEET_NAME}!$A$#{FIRST_DATA_ROW}:$#{right}$#{FIRST_DATA_ROW + category_count - 1}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @api private
|
|
75
|
+
def series_name_ref(series) = "#{WORKSHEET_NAME}!$#{column_letter(series)}$1"
|
|
76
|
+
|
|
77
|
+
# @api private
|
|
78
|
+
# As long as the series itself, which python-pptx also measures by the
|
|
79
|
+
# series rather than by the categories.
|
|
80
|
+
def series_values_ref(series)
|
|
81
|
+
letter = column_letter(series)
|
|
82
|
+
last_row = FIRST_DATA_ROW + series.size - 1
|
|
83
|
+
"#{WORKSHEET_NAME}!$#{letter}$#{FIRST_DATA_ROW}:$#{letter}$#{last_row}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @api private
|
|
87
|
+
# The Excel column letter for a series: after the category columns.
|
|
88
|
+
def column_letter(series) = ChartData.column_reference(1 + @categories.depth + series.index)
|
|
89
|
+
|
|
90
|
+
# Excel's bijective base-26 column names: 1 => "A", 27 => "AA".
|
|
91
|
+
def self.column_reference(column_number)
|
|
92
|
+
raise ArgumentError, "column number must be positive" unless column_number.positive?
|
|
93
|
+
|
|
94
|
+
letters = +""
|
|
95
|
+
remaining = column_number
|
|
96
|
+
while remaining.positive?
|
|
97
|
+
remaining, remainder = (remaining - 1).divmod(26)
|
|
98
|
+
letters.prepend((remainder + 65).chr)
|
|
99
|
+
end
|
|
100
|
+
letters
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @api private
|
|
104
|
+
# Bytes of the embedded Excel workbook holding this data.
|
|
105
|
+
def xlsx_blob = ChartWorkbookWriter.new(self).blob
|
|
106
|
+
|
|
107
|
+
def inspect = "#<Pptx::ChartData categories=#{category_count} series=#{size}>"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# One series of a chart: a name and one value per category.
|
|
111
|
+
class ChartSeries
|
|
112
|
+
include Enumerable
|
|
113
|
+
|
|
114
|
+
attr_reader :chart_data, :index, :name, :values, :number_format
|
|
115
|
+
|
|
116
|
+
def initialize(chart_data, index, name, values, number_format)
|
|
117
|
+
@chart_data = chart_data
|
|
118
|
+
@index = index
|
|
119
|
+
@name = name
|
|
120
|
+
@values = values
|
|
121
|
+
@number_format = number_format
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def each(&) = @values.each(&)
|
|
125
|
+
|
|
126
|
+
def size = @values.size
|
|
127
|
+
|
|
128
|
+
# Append one value, for building a series up point by point.
|
|
129
|
+
#
|
|
130
|
+
# python-pptx's add_data_point also takes a per-point number format, but
|
|
131
|
+
# nothing it writes ever reads it; the series' format is the one used.
|
|
132
|
+
def <<(value)
|
|
133
|
+
@values << value
|
|
134
|
+
self
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def categories = @chart_data.categories
|
|
138
|
+
|
|
139
|
+
def name_ref = @chart_data.series_name_ref(self)
|
|
140
|
+
|
|
141
|
+
def values_ref = @chart_data.series_values_ref(self)
|
|
142
|
+
|
|
143
|
+
def categories_ref = @chart_data.categories_ref
|
|
144
|
+
|
|
145
|
+
def inspect = "#<Pptx::ChartSeries #{@name.inspect} #{size} values>"
|
|
146
|
+
end
|
|
147
|
+
end
|