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,953 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_pptx/enum/chart"
|
|
4
|
+
require "ruby_pptx/errors"
|
|
5
|
+
|
|
6
|
+
module Pptx
|
|
7
|
+
# Builds the `c:chartSpace` XML for a new chart.
|
|
8
|
+
#
|
|
9
|
+
# Each chart family has its own skeleton, transcribed from the one
|
|
10
|
+
# PowerPoint itself writes. The values are cached in this XML, which is what
|
|
11
|
+
# PowerPoint renders from; the embedded workbook is only consulted when the
|
|
12
|
+
# user edits the data.
|
|
13
|
+
module ChartXmlWriter
|
|
14
|
+
XL = Enum::XL_CHART_TYPE
|
|
15
|
+
|
|
16
|
+
# Chart types this library can create, grouped by the plot element they
|
|
17
|
+
# use. Anything outside this list raises rather than producing XML
|
|
18
|
+
# PowerPoint would reject.
|
|
19
|
+
FAMILIES = {
|
|
20
|
+
bar: %i[BAR_CLUSTERED BAR_STACKED BAR_STACKED_100
|
|
21
|
+
COLUMN_CLUSTERED COLUMN_STACKED COLUMN_STACKED_100],
|
|
22
|
+
line: %i[LINE LINE_STACKED LINE_STACKED_100
|
|
23
|
+
LINE_MARKERS LINE_MARKERS_STACKED LINE_MARKERS_STACKED_100],
|
|
24
|
+
xy: %i[XY_SCATTER XY_SCATTER_LINES XY_SCATTER_LINES_NO_MARKERS
|
|
25
|
+
XY_SCATTER_SMOOTH XY_SCATTER_SMOOTH_NO_MARKERS],
|
|
26
|
+
bubble: %i[BUBBLE BUBBLE_THREE_D_EFFECT],
|
|
27
|
+
pie: %i[PIE],
|
|
28
|
+
doughnut: %i[DOUGHNUT],
|
|
29
|
+
area: %i[AREA AREA_STACKED AREA_STACKED_100],
|
|
30
|
+
radar: %i[RADAR RADAR_MARKERS RADAR_FILLED]
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# @param chart_type [Pptx::Enum::XL_CHART_TYPE]
|
|
36
|
+
# @param chart_data [Pptx::ChartData]
|
|
37
|
+
# @return [String] the chart part XML
|
|
38
|
+
def write(chart_type, chart_data)
|
|
39
|
+
member = XL.fetch(chart_type)
|
|
40
|
+
case family_of(member)
|
|
41
|
+
when :bar then BarChartWriter.new(member, chart_data).xml
|
|
42
|
+
when :line then LineChartWriter.new(member, chart_data).xml
|
|
43
|
+
when :pie then PieChartWriter.new(member, chart_data).xml
|
|
44
|
+
when :doughnut then DoughnutChartWriter.new(member, chart_data).xml
|
|
45
|
+
when :area then AreaChartWriter.new(member, chart_data).xml
|
|
46
|
+
when :radar then RadarChartWriter.new(member, chart_data).xml
|
|
47
|
+
when :xy then XyChartWriter.new(member, chart_data).xml
|
|
48
|
+
when :bubble then BubbleChartWriter.new(member, chart_data).xml
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def family_of(member)
|
|
53
|
+
FAMILIES.each { |family, names| return family if names.include?(member.name) }
|
|
54
|
+
raise Error,
|
|
55
|
+
"creating a #{member.name} chart is not supported yet; " \
|
|
56
|
+
"supported types: #{FAMILIES.values.flatten.sort.join(", ")}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def supported?(chart_type)
|
|
60
|
+
member = XL[chart_type]
|
|
61
|
+
!member.nil? && FAMILIES.values.any? { |names| names.include?(member.name) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Behaviour shared by the per-family writers.
|
|
65
|
+
class Base
|
|
66
|
+
DECLARATION = "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>"
|
|
67
|
+
CHART_SPACE_NS =
|
|
68
|
+
'xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" ' \
|
|
69
|
+
'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' \
|
|
70
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"'
|
|
71
|
+
|
|
72
|
+
TEXT_PROPERTIES = <<~XML
|
|
73
|
+
<c:txPr>
|
|
74
|
+
<a:bodyPr/>
|
|
75
|
+
<a:lstStyle/>
|
|
76
|
+
<a:p>
|
|
77
|
+
<a:pPr>
|
|
78
|
+
<a:defRPr sz="1800"/>
|
|
79
|
+
</a:pPr>
|
|
80
|
+
<a:endParaRPr lang="en-US"/>
|
|
81
|
+
</a:p>
|
|
82
|
+
</c:txPr>
|
|
83
|
+
XML
|
|
84
|
+
|
|
85
|
+
# Doughnut and area charts leave the language off the trailing run
|
|
86
|
+
# properties where every other family sets it. There is no reason for
|
|
87
|
+
# the difference beyond what the reference implementation writes, and a
|
|
88
|
+
# byte comparison is the only thing that notices.
|
|
89
|
+
TEXT_PROPERTIES_WITHOUT_LANG = TEXT_PROPERTIES.sub(
|
|
90
|
+
%(<a:endParaRPr lang="en-US"/>), "<a:endParaRPr/>"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
LEGEND = <<~XML
|
|
94
|
+
<c:legend>
|
|
95
|
+
<c:legendPos val="r"/>
|
|
96
|
+
<c:layout/>
|
|
97
|
+
<c:overlay val="0"/>
|
|
98
|
+
</c:legend>
|
|
99
|
+
XML
|
|
100
|
+
|
|
101
|
+
def initialize(chart_type, chart_data)
|
|
102
|
+
@chart_type = chart_type
|
|
103
|
+
@chart_data = chart_data
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Protected rather than private: ComboChartWriter builds its plots from
|
|
107
|
+
# the same fragments, and is a sibling rather than a subclass of the
|
|
108
|
+
# per-family writers.
|
|
109
|
+
protected
|
|
110
|
+
|
|
111
|
+
attr_reader :chart_type, :chart_data
|
|
112
|
+
|
|
113
|
+
# Ruby's squiggly heredoc only indents the first line of interpolated
|
|
114
|
+
# content, so every fragment below is produced at zero indentation and
|
|
115
|
+
# placed by its caller.
|
|
116
|
+
def indent(text, spaces)
|
|
117
|
+
pad = " " * spaces
|
|
118
|
+
text.lines.map { |line| line.strip.empty? ? line : pad + line }.join
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# One `c:ser` element: index, name, categories and values.
|
|
122
|
+
def series_xml(series, extra_before_cat: "", extra_after_val: "")
|
|
123
|
+
parts = [
|
|
124
|
+
%( <c:idx val="#{series.index}"/>\n),
|
|
125
|
+
%( <c:order val="#{series.index}"/>\n),
|
|
126
|
+
indent(tx_xml(series), 2),
|
|
127
|
+
extra_before_cat.empty? ? "" : indent(extra_before_cat, 2),
|
|
128
|
+
indent(cat_xml(series), 2),
|
|
129
|
+
indent(val_xml(series), 2),
|
|
130
|
+
extra_after_val.empty? ? "" : indent(extra_after_val, 2)
|
|
131
|
+
]
|
|
132
|
+
"<c:ser>\n#{parts.join}</c:ser>\n"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def tx_xml(series)
|
|
136
|
+
<<~XML
|
|
137
|
+
<c:tx>
|
|
138
|
+
<c:strRef>
|
|
139
|
+
<c:f>#{series.name_ref}</c:f>
|
|
140
|
+
<c:strCache>
|
|
141
|
+
<c:ptCount val="1"/>
|
|
142
|
+
#{indent(point_xml(0, escape(series.name)), 6).chomp}
|
|
143
|
+
</c:strCache>
|
|
144
|
+
</c:strRef>
|
|
145
|
+
</c:tx>
|
|
146
|
+
XML
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The categories: a numeric cache for numbers and dates, a string cache
|
|
150
|
+
# for a plain list, and a multi-level cache for grouped categories.
|
|
151
|
+
def cat_xml(series)
|
|
152
|
+
categories = chart_data.categories
|
|
153
|
+
if categories.numeric?
|
|
154
|
+
numeric_cat_xml(series, categories)
|
|
155
|
+
elsif categories.depth == 1
|
|
156
|
+
string_cat_xml(series, categories)
|
|
157
|
+
else
|
|
158
|
+
multi_level_cat_xml(series, categories)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def val_xml(series)
|
|
163
|
+
num_ref_xml("c:val", series.values_ref, series.values, series.number_format)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# A `c:val`, `c:xVal`, `c:yVal` or `c:bubbleSize` block: a worksheet
|
|
167
|
+
# reference plus the cached values.
|
|
168
|
+
#
|
|
169
|
+
# A nil value means no data at that position: the point is omitted while
|
|
170
|
+
# ptCount still counts it, which is how a gap is expressed.
|
|
171
|
+
def num_ref_xml(tag, reference, values, number_format)
|
|
172
|
+
points = values.each_with_index.filter_map do |value, index|
|
|
173
|
+
point_xml(index, format_value(value)) unless value.nil?
|
|
174
|
+
end.join
|
|
175
|
+
|
|
176
|
+
<<~XML
|
|
177
|
+
<#{tag}>
|
|
178
|
+
<c:numRef>
|
|
179
|
+
<c:f>#{reference}</c:f>
|
|
180
|
+
<c:numCache>
|
|
181
|
+
<c:formatCode>#{number_format}</c:formatCode>
|
|
182
|
+
<c:ptCount val="#{values.size}"/>
|
|
183
|
+
#{indent(points, 6).chomp}
|
|
184
|
+
</c:numCache>
|
|
185
|
+
</c:numRef>
|
|
186
|
+
</#{tag}>
|
|
187
|
+
XML
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def numeric_cat_xml(series, categories)
|
|
191
|
+
points = categories.each_with_index.map do |category, index|
|
|
192
|
+
point_xml(index, category.numeric_str_val(date_1904: date_1904?))
|
|
193
|
+
end.join
|
|
194
|
+
<<~XML
|
|
195
|
+
<c:cat>
|
|
196
|
+
<c:numRef>
|
|
197
|
+
<c:f>#{series.categories_ref}</c:f>
|
|
198
|
+
<c:numCache>
|
|
199
|
+
<c:formatCode>#{categories.number_format}</c:formatCode>
|
|
200
|
+
<c:ptCount val="#{categories.leaf_count}"/>
|
|
201
|
+
#{indent(points, 6).chomp}
|
|
202
|
+
</c:numCache>
|
|
203
|
+
</c:numRef>
|
|
204
|
+
</c:cat>
|
|
205
|
+
XML
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def string_cat_xml(series, categories)
|
|
209
|
+
points = categories.each_with_index.map do |category, index|
|
|
210
|
+
point_xml(index, escape(category.label))
|
|
211
|
+
end.join
|
|
212
|
+
<<~XML
|
|
213
|
+
<c:cat>
|
|
214
|
+
<c:strRef>
|
|
215
|
+
<c:f>#{series.categories_ref}</c:f>
|
|
216
|
+
<c:strCache>
|
|
217
|
+
<c:ptCount val="#{categories.leaf_count}"/>
|
|
218
|
+
#{indent(points, 6).chomp}
|
|
219
|
+
</c:strCache>
|
|
220
|
+
</c:strRef>
|
|
221
|
+
</c:cat>
|
|
222
|
+
XML
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# One `c:lvl` per level, leaf level first. Each label sits at the leaf
|
|
226
|
+
# index where its run begins, so a parent's points are sparse.
|
|
227
|
+
def multi_level_cat_xml(series, categories)
|
|
228
|
+
levels = categories.levels.map do |level|
|
|
229
|
+
points = level.map { |index, label| point_xml(index, escape(label)) }.join
|
|
230
|
+
"<c:lvl>\n#{indent(points, 2)}</c:lvl>\n"
|
|
231
|
+
end.join
|
|
232
|
+
<<~XML
|
|
233
|
+
<c:cat>
|
|
234
|
+
<c:multiLvlStrRef>
|
|
235
|
+
<c:f>#{series.categories_ref}</c:f>
|
|
236
|
+
<c:multiLvlStrCache>
|
|
237
|
+
<c:ptCount val="#{categories.leaf_count}"/>
|
|
238
|
+
#{indent(levels, 6).chomp}
|
|
239
|
+
</c:multiLvlStrCache>
|
|
240
|
+
</c:multiLvlStrRef>
|
|
241
|
+
</c:cat>
|
|
242
|
+
XML
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# The axis written instead of a category axis when the categories are
|
|
246
|
+
# dates. Bar, line and area charts share it, differing only in ids and
|
|
247
|
+
# position.
|
|
248
|
+
def date_ax_xml(ax_id, cross_ax_id, position)
|
|
249
|
+
<<~XML
|
|
250
|
+
<c:dateAx>
|
|
251
|
+
<c:axId val="#{ax_id}"/>
|
|
252
|
+
<c:scaling>
|
|
253
|
+
<c:orientation val="minMax"/>
|
|
254
|
+
</c:scaling>
|
|
255
|
+
<c:delete val="0"/>
|
|
256
|
+
<c:axPos val="#{position}"/>
|
|
257
|
+
<c:numFmt formatCode="#{chart_data.categories.number_format}" sourceLinked="1"/>
|
|
258
|
+
<c:majorTickMark val="out"/>
|
|
259
|
+
<c:minorTickMark val="none"/>
|
|
260
|
+
<c:tickLblPos val="nextTo"/>
|
|
261
|
+
<c:crossAx val="#{cross_ax_id}"/>
|
|
262
|
+
<c:crosses val="autoZero"/>
|
|
263
|
+
<c:auto val="1"/>
|
|
264
|
+
<c:lblOffset val="100"/>
|
|
265
|
+
<c:baseTimeUnit val="days"/>
|
|
266
|
+
</c:dateAx>
|
|
267
|
+
XML
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def dates? = chart_data.categories.dates?
|
|
271
|
+
|
|
272
|
+
# A chart being written from scratch uses the 1900 date system;
|
|
273
|
+
# rewriting an existing chart's data honours the chart's own setting.
|
|
274
|
+
def date_1904? = @date_1904 || false
|
|
275
|
+
|
|
276
|
+
def point_xml(index, value)
|
|
277
|
+
%(<c:pt idx="#{index}">\n <c:v>#{value}</c:v>\n</c:pt>\n)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Floats keep their trailing ".0" here, unlike in the workbook, because
|
|
281
|
+
# that is what the reference implementation writes.
|
|
282
|
+
def format_value(value) = value.is_a?(String) ? escape(value) : value.to_s
|
|
283
|
+
|
|
284
|
+
def escape(text)
|
|
285
|
+
text.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def all_series_xml(&)
|
|
289
|
+
chart_data.series.map(&).join
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# `c:barChart`: clustered, stacked and 100% stacked bars and columns.
|
|
294
|
+
class BarChartWriter < Base
|
|
295
|
+
CAT_AX_ID = "-2068027336"
|
|
296
|
+
VAL_AX_ID = "-2113994440"
|
|
297
|
+
|
|
298
|
+
BAR_TYPES = %i[BAR_CLUSTERED BAR_STACKED BAR_STACKED_100].freeze
|
|
299
|
+
|
|
300
|
+
GROUPINGS = {
|
|
301
|
+
BAR_CLUSTERED: "clustered", COLUMN_CLUSTERED: "clustered",
|
|
302
|
+
BAR_STACKED: "stacked", COLUMN_STACKED: "stacked",
|
|
303
|
+
BAR_STACKED_100: "percentStacked", COLUMN_STACKED_100: "percentStacked"
|
|
304
|
+
}.freeze
|
|
305
|
+
|
|
306
|
+
STACKED_TYPES = %i[BAR_STACKED BAR_STACKED_100 COLUMN_STACKED COLUMN_STACKED_100].freeze
|
|
307
|
+
|
|
308
|
+
def xml
|
|
309
|
+
<<~XML
|
|
310
|
+
#{Base::DECLARATION}
|
|
311
|
+
<c:chartSpace #{Base::CHART_SPACE_NS}>
|
|
312
|
+
<c:date1904 val="0"/>
|
|
313
|
+
<c:chart>
|
|
314
|
+
<c:autoTitleDeleted val="0"/>
|
|
315
|
+
<c:plotArea>
|
|
316
|
+
<c:barChart>
|
|
317
|
+
<c:barDir val="#{bar_dir}"/>
|
|
318
|
+
<c:grouping val="#{grouping}"/>
|
|
319
|
+
#{indent(series_blocks, 8).chomp}
|
|
320
|
+
#{overlap} <c:axId val="#{CAT_AX_ID}"/>
|
|
321
|
+
<c:axId val="#{VAL_AX_ID}"/>
|
|
322
|
+
</c:barChart>
|
|
323
|
+
#{indent(cat_ax_xml, 6).chomp}
|
|
324
|
+
<c:valAx>
|
|
325
|
+
<c:axId val="#{VAL_AX_ID}"/>
|
|
326
|
+
<c:scaling/>
|
|
327
|
+
<c:delete val="0"/>
|
|
328
|
+
<c:axPos val="#{val_ax_pos}"/>
|
|
329
|
+
<c:majorGridlines/>
|
|
330
|
+
<c:majorTickMark val="out"/>
|
|
331
|
+
<c:minorTickMark val="none"/>
|
|
332
|
+
<c:tickLblPos val="nextTo"/>
|
|
333
|
+
<c:crossAx val="#{CAT_AX_ID}"/>
|
|
334
|
+
<c:crosses val="autoZero"/>
|
|
335
|
+
</c:valAx>
|
|
336
|
+
</c:plotArea>
|
|
337
|
+
<c:dispBlanksAs val="gap"/>
|
|
338
|
+
</c:chart>
|
|
339
|
+
#{indent(Base::TEXT_PROPERTIES, 2).chomp}
|
|
340
|
+
</c:chartSpace>
|
|
341
|
+
XML
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
private
|
|
345
|
+
|
|
346
|
+
def bar? = BAR_TYPES.include?(chart_type.name)
|
|
347
|
+
|
|
348
|
+
def bar_dir = bar? ? "bar" : "col"
|
|
349
|
+
|
|
350
|
+
def grouping = GROUPINGS.fetch(chart_type.name)
|
|
351
|
+
|
|
352
|
+
# A bar chart's category axis runs down the left; a column chart's runs
|
|
353
|
+
# along the bottom, and the value axis takes the other position.
|
|
354
|
+
def cat_ax_pos = bar? ? "l" : "b"
|
|
355
|
+
|
|
356
|
+
def val_ax_pos = bar? ? "b" : "l"
|
|
357
|
+
|
|
358
|
+
def overlap
|
|
359
|
+
STACKED_TYPES.include?(chart_type.name) ? %( <c:overlap val="100"/>\n) : ""
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def series_blocks = all_series_xml { |series| series_xml(series) }
|
|
363
|
+
|
|
364
|
+
def cat_ax_xml
|
|
365
|
+
return date_ax_xml(CAT_AX_ID, VAL_AX_ID, cat_ax_pos) if dates?
|
|
366
|
+
|
|
367
|
+
<<~XML
|
|
368
|
+
<c:catAx>
|
|
369
|
+
<c:axId val="#{CAT_AX_ID}"/>
|
|
370
|
+
<c:scaling>
|
|
371
|
+
<c:orientation val="minMax"/>
|
|
372
|
+
</c:scaling>
|
|
373
|
+
<c:delete val="0"/>
|
|
374
|
+
<c:axPos val="#{cat_ax_pos}"/>
|
|
375
|
+
<c:majorTickMark val="out"/>
|
|
376
|
+
<c:minorTickMark val="none"/>
|
|
377
|
+
<c:tickLblPos val="nextTo"/>
|
|
378
|
+
<c:crossAx val="#{VAL_AX_ID}"/>
|
|
379
|
+
<c:crosses val="autoZero"/>
|
|
380
|
+
<c:auto val="1"/>
|
|
381
|
+
<c:lblAlgn val="ctr"/>
|
|
382
|
+
<c:lblOffset val="100"/>
|
|
383
|
+
<c:noMultiLvlLbl val="0"/>
|
|
384
|
+
</c:catAx>
|
|
385
|
+
XML
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# `c:lineChart`, with or without markers.
|
|
390
|
+
class LineChartWriter < Base
|
|
391
|
+
CAT_AX_ID = "2118791784"
|
|
392
|
+
VAL_AX_ID = "2140495176"
|
|
393
|
+
|
|
394
|
+
MARKER_TYPES = %i[LINE_MARKERS LINE_MARKERS_STACKED LINE_MARKERS_STACKED_100].freeze
|
|
395
|
+
|
|
396
|
+
GROUPINGS = {
|
|
397
|
+
LINE: "standard", LINE_MARKERS: "standard",
|
|
398
|
+
LINE_STACKED: "stacked", LINE_MARKERS_STACKED: "stacked",
|
|
399
|
+
LINE_STACKED_100: "percentStacked", LINE_MARKERS_STACKED_100: "percentStacked"
|
|
400
|
+
}.freeze
|
|
401
|
+
|
|
402
|
+
def xml
|
|
403
|
+
<<~XML
|
|
404
|
+
#{Base::DECLARATION}
|
|
405
|
+
<c:chartSpace #{Base::CHART_SPACE_NS}>
|
|
406
|
+
<c:date1904 val="0"/>
|
|
407
|
+
<c:chart>
|
|
408
|
+
<c:autoTitleDeleted val="0"/>
|
|
409
|
+
<c:plotArea>
|
|
410
|
+
<c:lineChart>
|
|
411
|
+
<c:grouping val="#{grouping}"/>
|
|
412
|
+
<c:varyColors val="0"/>
|
|
413
|
+
#{indent(series_blocks, 8).chomp}
|
|
414
|
+
<c:marker val="1"/>
|
|
415
|
+
<c:smooth val="0"/>
|
|
416
|
+
<c:axId val="#{CAT_AX_ID}"/>
|
|
417
|
+
<c:axId val="#{VAL_AX_ID}"/>
|
|
418
|
+
</c:lineChart>
|
|
419
|
+
#{indent(cat_ax_xml, 6).chomp}
|
|
420
|
+
<c:valAx>
|
|
421
|
+
<c:axId val="#{VAL_AX_ID}"/>
|
|
422
|
+
<c:scaling/>
|
|
423
|
+
<c:delete val="0"/>
|
|
424
|
+
<c:axPos val="l"/>
|
|
425
|
+
<c:majorGridlines/>
|
|
426
|
+
<c:majorTickMark val="out"/>
|
|
427
|
+
<c:minorTickMark val="none"/>
|
|
428
|
+
<c:tickLblPos val="nextTo"/>
|
|
429
|
+
<c:crossAx val="#{CAT_AX_ID}"/>
|
|
430
|
+
<c:crosses val="autoZero"/>
|
|
431
|
+
</c:valAx>
|
|
432
|
+
</c:plotArea>
|
|
433
|
+
#{indent(Base::LEGEND, 4).chomp}
|
|
434
|
+
<c:plotVisOnly val="1"/>
|
|
435
|
+
<c:dispBlanksAs val="gap"/>
|
|
436
|
+
<c:showDLblsOverMax val="0"/>
|
|
437
|
+
</c:chart>
|
|
438
|
+
#{indent(Base::TEXT_PROPERTIES, 2).chomp}
|
|
439
|
+
</c:chartSpace>
|
|
440
|
+
XML
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
private
|
|
444
|
+
|
|
445
|
+
def grouping = GROUPINGS.fetch(chart_type.name)
|
|
446
|
+
|
|
447
|
+
def markers? = MARKER_TYPES.include?(chart_type.name)
|
|
448
|
+
|
|
449
|
+
# A line chart without markers says so per series, by asking for the
|
|
450
|
+
# "none" marker symbol.
|
|
451
|
+
def marker_none_xml
|
|
452
|
+
return "" if markers?
|
|
453
|
+
|
|
454
|
+
<<~XML
|
|
455
|
+
<c:marker>
|
|
456
|
+
<c:symbol val="none"/>
|
|
457
|
+
</c:marker>
|
|
458
|
+
XML
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def smooth_xml = %(<c:smooth val="0"/>\n)
|
|
462
|
+
|
|
463
|
+
def series_blocks
|
|
464
|
+
all_series_xml do |series|
|
|
465
|
+
series_xml(series, extra_before_cat: marker_none_xml, extra_after_val: smooth_xml)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def cat_ax_xml
|
|
470
|
+
return date_ax_xml(CAT_AX_ID, VAL_AX_ID, "b") if dates?
|
|
471
|
+
|
|
472
|
+
<<~XML
|
|
473
|
+
<c:catAx>
|
|
474
|
+
<c:axId val="#{CAT_AX_ID}"/>
|
|
475
|
+
<c:scaling>
|
|
476
|
+
<c:orientation val="minMax"/>
|
|
477
|
+
</c:scaling>
|
|
478
|
+
<c:delete val="0"/>
|
|
479
|
+
<c:axPos val="b"/>
|
|
480
|
+
<c:majorTickMark val="out"/>
|
|
481
|
+
<c:minorTickMark val="none"/>
|
|
482
|
+
<c:tickLblPos val="nextTo"/>
|
|
483
|
+
<c:crossAx val="#{VAL_AX_ID}"/>
|
|
484
|
+
<c:crosses val="autoZero"/>
|
|
485
|
+
<c:auto val="1"/>
|
|
486
|
+
<c:lblAlgn val="ctr"/>
|
|
487
|
+
<c:lblOffset val="100"/>
|
|
488
|
+
<c:noMultiLvlLbl val="0"/>
|
|
489
|
+
</c:catAx>
|
|
490
|
+
XML
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# `c:pieChart`, a single series shown as slices.
|
|
495
|
+
class PieChartWriter < Base
|
|
496
|
+
def xml
|
|
497
|
+
<<~XML
|
|
498
|
+
#{Base::DECLARATION}
|
|
499
|
+
<c:chartSpace #{Base::CHART_SPACE_NS}>
|
|
500
|
+
<c:chart>
|
|
501
|
+
<c:autoTitleDeleted val="0"/>
|
|
502
|
+
<c:plotArea>
|
|
503
|
+
<c:pieChart>
|
|
504
|
+
<c:varyColors val="1"/>
|
|
505
|
+
#{indent(series_blocks, 8).chomp}
|
|
506
|
+
</c:pieChart>
|
|
507
|
+
</c:plotArea>
|
|
508
|
+
<c:dispBlanksAs val="gap"/>
|
|
509
|
+
</c:chart>
|
|
510
|
+
#{indent(Base::TEXT_PROPERTIES, 2).chomp}
|
|
511
|
+
</c:chartSpace>
|
|
512
|
+
XML
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
private
|
|
516
|
+
|
|
517
|
+
# A pie plots one series only; any others live in the workbook but are
|
|
518
|
+
# not drawn.
|
|
519
|
+
def series_blocks = series_xml(chart_data.series.first)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Shared by the two families whose points carry their own x value rather
|
|
523
|
+
# than sitting against a shared category.
|
|
524
|
+
class XyBase < Base
|
|
525
|
+
private
|
|
526
|
+
|
|
527
|
+
# A scatter or bubble series names its own x and y ranges instead of
|
|
528
|
+
# sharing a category axis.
|
|
529
|
+
def xy_series_xml(series, extra_after_tx: "", extra_after_values: "")
|
|
530
|
+
parts = [
|
|
531
|
+
%( <c:idx val="#{series.index}"/>\n),
|
|
532
|
+
%( <c:order val="#{series.index}"/>\n),
|
|
533
|
+
indent(tx_xml(series), 2),
|
|
534
|
+
extra_after_tx.empty? ? "" : indent(extra_after_tx, 2),
|
|
535
|
+
indent(num_ref_xml("c:xVal", series.x_values_ref, series.x_values,
|
|
536
|
+
series.number_format), 2),
|
|
537
|
+
indent(num_ref_xml("c:yVal", series.y_values_ref, series.y_values,
|
|
538
|
+
series.number_format), 2),
|
|
539
|
+
extra_after_values.empty? ? "" : indent(extra_after_values, 2)
|
|
540
|
+
]
|
|
541
|
+
"<c:ser>\n#{parts.join}</c:ser>\n"
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def value_axes_xml(x_ax_id, y_ax_id)
|
|
545
|
+
<<~XML
|
|
546
|
+
<c:valAx>
|
|
547
|
+
<c:axId val="#{x_ax_id}"/>
|
|
548
|
+
<c:scaling>
|
|
549
|
+
<c:orientation val="minMax"/>
|
|
550
|
+
</c:scaling>
|
|
551
|
+
<c:delete val="0"/>
|
|
552
|
+
<c:axPos val="b"/>
|
|
553
|
+
<c:numFmt formatCode="General" sourceLinked="1"/>
|
|
554
|
+
<c:majorTickMark val="out"/>
|
|
555
|
+
<c:minorTickMark val="none"/>
|
|
556
|
+
<c:tickLblPos val="nextTo"/>
|
|
557
|
+
<c:crossAx val="#{y_ax_id}"/>
|
|
558
|
+
<c:crosses val="autoZero"/>
|
|
559
|
+
<c:crossBetween val="midCat"/>
|
|
560
|
+
</c:valAx>
|
|
561
|
+
<c:valAx>
|
|
562
|
+
<c:axId val="#{y_ax_id}"/>
|
|
563
|
+
<c:scaling>
|
|
564
|
+
<c:orientation val="minMax"/>
|
|
565
|
+
</c:scaling>
|
|
566
|
+
<c:delete val="0"/>
|
|
567
|
+
<c:axPos val="l"/>
|
|
568
|
+
<c:majorGridlines/>
|
|
569
|
+
<c:numFmt formatCode="General" sourceLinked="1"/>
|
|
570
|
+
<c:majorTickMark val="out"/>
|
|
571
|
+
<c:minorTickMark val="none"/>
|
|
572
|
+
<c:tickLblPos val="nextTo"/>
|
|
573
|
+
<c:crossAx val="#{x_ax_id}"/>
|
|
574
|
+
<c:crosses val="autoZero"/>
|
|
575
|
+
<c:crossBetween val="midCat"/>
|
|
576
|
+
</c:valAx>
|
|
577
|
+
XML
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# `c:scatterChart`, points at (x, y) with optional lines.
|
|
582
|
+
class XyChartWriter < XyBase
|
|
583
|
+
X_AX_ID = "-2128940872"
|
|
584
|
+
Y_AX_ID = "-2129643912"
|
|
585
|
+
|
|
586
|
+
SMOOTH_TYPES = %i[XY_SCATTER_SMOOTH XY_SCATTER_SMOOTH_NO_MARKERS].freeze
|
|
587
|
+
NO_MARKER_TYPES = %i[XY_SCATTER_LINES_NO_MARKERS XY_SCATTER_SMOOTH_NO_MARKERS].freeze
|
|
588
|
+
|
|
589
|
+
def xml
|
|
590
|
+
<<~XML
|
|
591
|
+
#{Base::DECLARATION}
|
|
592
|
+
<c:chartSpace #{Base::CHART_SPACE_NS}>
|
|
593
|
+
<c:chart>
|
|
594
|
+
<c:plotArea>
|
|
595
|
+
<c:scatterChart>
|
|
596
|
+
<c:scatterStyle val="#{scatter_style}"/>
|
|
597
|
+
<c:varyColors val="0"/>
|
|
598
|
+
#{indent(series_blocks, 8).chomp}
|
|
599
|
+
<c:axId val="#{X_AX_ID}"/>
|
|
600
|
+
<c:axId val="#{Y_AX_ID}"/>
|
|
601
|
+
</c:scatterChart>
|
|
602
|
+
#{indent(value_axes_xml(X_AX_ID, Y_AX_ID), 6).chomp}
|
|
603
|
+
</c:plotArea>
|
|
604
|
+
#{indent(Base::LEGEND, 4).chomp}
|
|
605
|
+
<c:plotVisOnly val="1"/>
|
|
606
|
+
<c:dispBlanksAs val="gap"/>
|
|
607
|
+
<c:showDLblsOverMax val="0"/>
|
|
608
|
+
</c:chart>
|
|
609
|
+
#{indent(Base::TEXT_PROPERTIES, 2).chomp}
|
|
610
|
+
</c:chartSpace>
|
|
611
|
+
XML
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
private
|
|
615
|
+
|
|
616
|
+
def scatter_style
|
|
617
|
+
SMOOTH_TYPES.include?(chart_type.name) ? "smoothMarker" : "lineMarker"
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Plain XY_SCATTER draws markers only, which is said by giving the series
|
|
621
|
+
# an invisible line rather than by any scatterStyle value.
|
|
622
|
+
def line_suppression_xml
|
|
623
|
+
return "" unless chart_type.name == :XY_SCATTER
|
|
624
|
+
|
|
625
|
+
<<~XML
|
|
626
|
+
<c:spPr>
|
|
627
|
+
<a:ln w="47625">
|
|
628
|
+
<a:noFill/>
|
|
629
|
+
</a:ln>
|
|
630
|
+
</c:spPr>
|
|
631
|
+
XML
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def marker_none_xml
|
|
635
|
+
return "" unless NO_MARKER_TYPES.include?(chart_type.name)
|
|
636
|
+
|
|
637
|
+
<<~XML
|
|
638
|
+
<c:marker>
|
|
639
|
+
<c:symbol val="none"/>
|
|
640
|
+
</c:marker>
|
|
641
|
+
XML
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def series_blocks
|
|
645
|
+
all_series_xml do |series|
|
|
646
|
+
xy_series_xml(series,
|
|
647
|
+
extra_after_tx: line_suppression_xml + marker_none_xml,
|
|
648
|
+
extra_after_values: %(<c:smooth val="0"/>\n))
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# `c:bubbleChart`, points at (x, y) sized by a third value.
|
|
654
|
+
class BubbleChartWriter < XyBase
|
|
655
|
+
X_AX_ID = "-2115720072"
|
|
656
|
+
Y_AX_ID = "-2115723560"
|
|
657
|
+
|
|
658
|
+
def xml
|
|
659
|
+
<<~XML
|
|
660
|
+
#{Base::DECLARATION}
|
|
661
|
+
<c:chartSpace #{Base::CHART_SPACE_NS}>
|
|
662
|
+
<c:chart>
|
|
663
|
+
<c:autoTitleDeleted val="0"/>
|
|
664
|
+
<c:plotArea>
|
|
665
|
+
<c:layout/>
|
|
666
|
+
<c:bubbleChart>
|
|
667
|
+
<c:varyColors val="0"/>
|
|
668
|
+
#{indent(series_blocks, 8).chomp}
|
|
669
|
+
<c:dLbls>
|
|
670
|
+
<c:showLegendKey val="0"/>
|
|
671
|
+
<c:showVal val="0"/>
|
|
672
|
+
<c:showCatName val="0"/>
|
|
673
|
+
<c:showSerName val="0"/>
|
|
674
|
+
<c:showPercent val="0"/>
|
|
675
|
+
<c:showBubbleSize val="0"/>
|
|
676
|
+
</c:dLbls>
|
|
677
|
+
<c:bubbleScale val="100"/>
|
|
678
|
+
<c:showNegBubbles val="0"/>
|
|
679
|
+
<c:axId val="#{X_AX_ID}"/>
|
|
680
|
+
<c:axId val="#{Y_AX_ID}"/>
|
|
681
|
+
</c:bubbleChart>
|
|
682
|
+
#{indent(value_axes_xml(X_AX_ID, Y_AX_ID), 6).chomp}
|
|
683
|
+
</c:plotArea>
|
|
684
|
+
#{indent(Base::LEGEND, 4).chomp}
|
|
685
|
+
<c:plotVisOnly val="1"/>
|
|
686
|
+
<c:dispBlanksAs val="gap"/>
|
|
687
|
+
<c:showDLblsOverMax val="0"/>
|
|
688
|
+
</c:chart>
|
|
689
|
+
#{indent(Base::TEXT_PROPERTIES, 2).chomp}
|
|
690
|
+
</c:chartSpace>
|
|
691
|
+
XML
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
private
|
|
695
|
+
|
|
696
|
+
def three_d? = chart_type.name == :BUBBLE_THREE_D_EFFECT
|
|
697
|
+
|
|
698
|
+
def series_blocks
|
|
699
|
+
all_series_xml do |series|
|
|
700
|
+
sizes = indent(num_ref_xml("c:bubbleSize", series.bubble_sizes_ref,
|
|
701
|
+
series.bubble_sizes, series.number_format), 0)
|
|
702
|
+
xy_series_xml(series,
|
|
703
|
+
extra_after_tx: %(<c:invertIfNegative val="0"/>\n),
|
|
704
|
+
extra_after_values: sizes + %(<c:bubble3D val="#{three_d? ? 1 : 0}"/>\n))
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# `c:areaChart`, filled bands under each series.
|
|
710
|
+
class AreaChartWriter < Base
|
|
711
|
+
CAT_AX_ID = "-2101159928"
|
|
712
|
+
VAL_AX_ID = "-2100718248"
|
|
713
|
+
|
|
714
|
+
GROUPINGS = {
|
|
715
|
+
AREA: "standard", AREA_STACKED: "stacked", AREA_STACKED_100: "percentStacked"
|
|
716
|
+
}.freeze
|
|
717
|
+
|
|
718
|
+
def xml
|
|
719
|
+
<<~XML
|
|
720
|
+
#{Base::DECLARATION}
|
|
721
|
+
<c:chartSpace #{Base::CHART_SPACE_NS}>
|
|
722
|
+
<c:date1904 val="0"/>
|
|
723
|
+
<c:roundedCorners val="0"/>
|
|
724
|
+
<c:chart>
|
|
725
|
+
<c:autoTitleDeleted val="0"/>
|
|
726
|
+
<c:plotArea>
|
|
727
|
+
<c:layout/>
|
|
728
|
+
<c:areaChart>
|
|
729
|
+
<c:grouping val="#{grouping}"/>
|
|
730
|
+
<c:varyColors val="0"/>
|
|
731
|
+
#{indent(series_blocks, 8).chomp}
|
|
732
|
+
<c:dLbls>
|
|
733
|
+
<c:showLegendKey val="0"/>
|
|
734
|
+
<c:showVal val="0"/>
|
|
735
|
+
<c:showCatName val="0"/>
|
|
736
|
+
<c:showSerName val="0"/>
|
|
737
|
+
<c:showPercent val="0"/>
|
|
738
|
+
<c:showBubbleSize val="0"/>
|
|
739
|
+
</c:dLbls>
|
|
740
|
+
<c:axId val="#{CAT_AX_ID}"/>
|
|
741
|
+
<c:axId val="#{VAL_AX_ID}"/>
|
|
742
|
+
</c:areaChart>
|
|
743
|
+
#{indent(cat_ax_xml, 6).chomp}
|
|
744
|
+
<c:valAx>
|
|
745
|
+
<c:axId val="#{VAL_AX_ID}"/>
|
|
746
|
+
<c:scaling>
|
|
747
|
+
<c:orientation val="minMax"/>
|
|
748
|
+
</c:scaling>
|
|
749
|
+
<c:delete val="0"/>
|
|
750
|
+
<c:axPos val="l"/>
|
|
751
|
+
<c:majorGridlines/>
|
|
752
|
+
<c:numFmt formatCode="General" sourceLinked="1"/>
|
|
753
|
+
<c:majorTickMark val="out"/>
|
|
754
|
+
<c:minorTickMark val="none"/>
|
|
755
|
+
<c:tickLblPos val="nextTo"/>
|
|
756
|
+
<c:crossAx val="#{CAT_AX_ID}"/>
|
|
757
|
+
<c:crosses val="autoZero"/>
|
|
758
|
+
<c:crossBetween val="midCat"/>
|
|
759
|
+
</c:valAx>
|
|
760
|
+
</c:plotArea>
|
|
761
|
+
#{indent(Base::LEGEND, 4).chomp}
|
|
762
|
+
<c:plotVisOnly val="1"/>
|
|
763
|
+
<c:dispBlanksAs val="zero"/>
|
|
764
|
+
<c:showDLblsOverMax val="0"/>
|
|
765
|
+
</c:chart>
|
|
766
|
+
#{indent(Base::TEXT_PROPERTIES_WITHOUT_LANG, 2).chomp}
|
|
767
|
+
</c:chartSpace>
|
|
768
|
+
XML
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
private
|
|
772
|
+
|
|
773
|
+
def grouping = GROUPINGS.fetch(chart_type.name)
|
|
774
|
+
|
|
775
|
+
def series_blocks = all_series_xml { |series| series_xml(series) }
|
|
776
|
+
|
|
777
|
+
def cat_ax_xml
|
|
778
|
+
return date_ax_xml(CAT_AX_ID, VAL_AX_ID, "b") if dates?
|
|
779
|
+
|
|
780
|
+
<<~XML
|
|
781
|
+
<c:catAx>
|
|
782
|
+
<c:axId val="#{CAT_AX_ID}"/>
|
|
783
|
+
<c:scaling>
|
|
784
|
+
<c:orientation val="minMax"/>
|
|
785
|
+
</c:scaling>
|
|
786
|
+
<c:delete val="0"/>
|
|
787
|
+
<c:axPos val="b"/>
|
|
788
|
+
<c:numFmt formatCode="General" sourceLinked="1"/>
|
|
789
|
+
<c:majorTickMark val="out"/>
|
|
790
|
+
<c:minorTickMark val="none"/>
|
|
791
|
+
<c:tickLblPos val="nextTo"/>
|
|
792
|
+
<c:crossAx val="#{VAL_AX_ID}"/>
|
|
793
|
+
<c:crosses val="autoZero"/>
|
|
794
|
+
<c:auto val="1"/>
|
|
795
|
+
<c:lblAlgn val="ctr"/>
|
|
796
|
+
<c:lblOffset val="100"/>
|
|
797
|
+
<c:noMultiLvlLbl val="0"/>
|
|
798
|
+
</c:catAx>
|
|
799
|
+
XML
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# `c:radarChart`, values plotted on spokes around a centre.
|
|
804
|
+
class RadarChartWriter < Base
|
|
805
|
+
CAT_AX_ID = "2073612648"
|
|
806
|
+
VAL_AX_ID = "-2112772216"
|
|
807
|
+
|
|
808
|
+
# A radar chart carries a style hint PowerPoint 2007 did not understand,
|
|
809
|
+
# so it is written inside an AlternateContent block with a plain
|
|
810
|
+
# fallback.
|
|
811
|
+
ALTERNATE_CONTENT = <<~XML
|
|
812
|
+
<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
|
|
813
|
+
<mc:Choice xmlns:c14="http://schemas.microsoft.com/office/drawing/2007/8/2/chart" Requires="c14">
|
|
814
|
+
<c14:style val="118"/>
|
|
815
|
+
</mc:Choice>
|
|
816
|
+
<mc:Fallback>
|
|
817
|
+
<c:style val="18"/>
|
|
818
|
+
</mc:Fallback>
|
|
819
|
+
</mc:AlternateContent>
|
|
820
|
+
XML
|
|
821
|
+
|
|
822
|
+
def xml
|
|
823
|
+
<<~XML
|
|
824
|
+
#{Base::DECLARATION}
|
|
825
|
+
<c:chartSpace #{Base::CHART_SPACE_NS}>
|
|
826
|
+
<c:date1904 val="0"/>
|
|
827
|
+
<c:roundedCorners val="0"/>
|
|
828
|
+
#{indent(ALTERNATE_CONTENT, 2).chomp}
|
|
829
|
+
<c:chart>
|
|
830
|
+
<c:autoTitleDeleted val="0"/>
|
|
831
|
+
<c:plotArea>
|
|
832
|
+
<c:layout/>
|
|
833
|
+
<c:radarChart>
|
|
834
|
+
<c:radarStyle val="#{radar_style}"/>
|
|
835
|
+
<c:varyColors val="0"/>
|
|
836
|
+
#{indent(series_blocks, 8).chomp}
|
|
837
|
+
<c:axId val="#{CAT_AX_ID}"/>
|
|
838
|
+
<c:axId val="#{VAL_AX_ID}"/>
|
|
839
|
+
</c:radarChart>
|
|
840
|
+
<c:catAx>
|
|
841
|
+
<c:axId val="#{CAT_AX_ID}"/>
|
|
842
|
+
<c:scaling>
|
|
843
|
+
<c:orientation val="minMax"/>
|
|
844
|
+
</c:scaling>
|
|
845
|
+
<c:delete val="0"/>
|
|
846
|
+
<c:axPos val="b"/>
|
|
847
|
+
<c:majorGridlines/>
|
|
848
|
+
<c:numFmt formatCode="m/d/yy" sourceLinked="1"/>
|
|
849
|
+
<c:majorTickMark val="out"/>
|
|
850
|
+
<c:minorTickMark val="none"/>
|
|
851
|
+
<c:tickLblPos val="nextTo"/>
|
|
852
|
+
<c:crossAx val="#{VAL_AX_ID}"/>
|
|
853
|
+
<c:crosses val="autoZero"/>
|
|
854
|
+
<c:auto val="1"/>
|
|
855
|
+
<c:lblAlgn val="ctr"/>
|
|
856
|
+
<c:lblOffset val="100"/>
|
|
857
|
+
<c:noMultiLvlLbl val="0"/>
|
|
858
|
+
</c:catAx>
|
|
859
|
+
<c:valAx>
|
|
860
|
+
<c:axId val="#{VAL_AX_ID}"/>
|
|
861
|
+
<c:scaling>
|
|
862
|
+
<c:orientation val="minMax"/>
|
|
863
|
+
</c:scaling>
|
|
864
|
+
<c:delete val="0"/>
|
|
865
|
+
<c:axPos val="l"/>
|
|
866
|
+
<c:majorGridlines/>
|
|
867
|
+
<c:numFmt formatCode="General" sourceLinked="1"/>
|
|
868
|
+
<c:majorTickMark val="cross"/>
|
|
869
|
+
<c:minorTickMark val="none"/>
|
|
870
|
+
<c:tickLblPos val="nextTo"/>
|
|
871
|
+
<c:crossAx val="#{CAT_AX_ID}"/>
|
|
872
|
+
<c:crosses val="autoZero"/>
|
|
873
|
+
<c:crossBetween val="between"/>
|
|
874
|
+
</c:valAx>
|
|
875
|
+
</c:plotArea>
|
|
876
|
+
<c:plotVisOnly val="1"/>
|
|
877
|
+
<c:dispBlanksAs val="gap"/>
|
|
878
|
+
<c:showDLblsOverMax val="0"/>
|
|
879
|
+
</c:chart>
|
|
880
|
+
#{indent(Base::TEXT_PROPERTIES, 2).chomp}
|
|
881
|
+
</c:chartSpace>
|
|
882
|
+
XML
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
private
|
|
886
|
+
|
|
887
|
+
def radar_style = chart_type.name == :RADAR_FILLED ? "filled" : "marker"
|
|
888
|
+
|
|
889
|
+
# A plain radar draws lines without markers, which is said per series by
|
|
890
|
+
# asking for the "none" symbol. The markers and filled variants do not.
|
|
891
|
+
def marker_none_xml
|
|
892
|
+
return "" unless chart_type.name == :RADAR
|
|
893
|
+
|
|
894
|
+
<<~XML
|
|
895
|
+
<c:marker>
|
|
896
|
+
<c:symbol val="none"/>
|
|
897
|
+
</c:marker>
|
|
898
|
+
XML
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def series_blocks
|
|
902
|
+
all_series_xml do |series|
|
|
903
|
+
series_xml(series, extra_before_cat: marker_none_xml,
|
|
904
|
+
extra_after_val: %(<c:smooth val="0"/>\n))
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
# `c:doughnutChart`, a pie with a hole.
|
|
910
|
+
class DoughnutChartWriter < Base
|
|
911
|
+
def xml
|
|
912
|
+
<<~XML
|
|
913
|
+
#{Base::DECLARATION}
|
|
914
|
+
<c:chartSpace #{Base::CHART_SPACE_NS}>
|
|
915
|
+
<c:date1904 val="0"/>
|
|
916
|
+
<c:roundedCorners val="0"/>
|
|
917
|
+
<c:chart>
|
|
918
|
+
<c:autoTitleDeleted val="0"/>
|
|
919
|
+
<c:plotArea>
|
|
920
|
+
<c:layout/>
|
|
921
|
+
<c:doughnutChart>
|
|
922
|
+
<c:varyColors val="1"/>
|
|
923
|
+
#{indent(series_blocks, 8).chomp}
|
|
924
|
+
<c:dLbls>
|
|
925
|
+
<c:showLegendKey val="0"/>
|
|
926
|
+
<c:showVal val="0"/>
|
|
927
|
+
<c:showCatName val="0"/>
|
|
928
|
+
<c:showSerName val="0"/>
|
|
929
|
+
<c:showPercent val="0"/>
|
|
930
|
+
<c:showBubbleSize val="0"/>
|
|
931
|
+
<c:showLeaderLines val="1"/>
|
|
932
|
+
</c:dLbls>
|
|
933
|
+
<c:firstSliceAng val="0"/>
|
|
934
|
+
<c:holeSize val="50"/>
|
|
935
|
+
</c:doughnutChart>
|
|
936
|
+
</c:plotArea>
|
|
937
|
+
#{indent(Base::LEGEND, 4).chomp}
|
|
938
|
+
<c:plotVisOnly val="1"/>
|
|
939
|
+
<c:dispBlanksAs val="gap"/>
|
|
940
|
+
<c:showDLblsOverMax val="0"/>
|
|
941
|
+
</c:chart>
|
|
942
|
+
#{indent(Base::TEXT_PROPERTIES_WITHOUT_LANG, 2).chomp}
|
|
943
|
+
</c:chartSpace>
|
|
944
|
+
XML
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
private
|
|
948
|
+
|
|
949
|
+
# Unlike a pie, a doughnut draws every series as a concentric ring.
|
|
950
|
+
def series_blocks = all_series_xml { |series| series_xml(series) }
|
|
951
|
+
end
|
|
952
|
+
end
|
|
953
|
+
end
|