xlsxrb 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -34
  3. data/README.md +26 -21
  4. data/lib/xlsxrb/chart_builder.rb +18 -18
  5. data/lib/xlsxrb/dsl_helpers.rb +175 -0
  6. data/lib/xlsxrb/elements/cell.rb +56 -22
  7. data/lib/xlsxrb/elements/column.rb +27 -3
  8. data/lib/xlsxrb/elements/row.rb +27 -8
  9. data/lib/xlsxrb/elements/workbook.rb +27 -14
  10. data/lib/xlsxrb/elements/worksheet.rb +17 -1
  11. data/lib/xlsxrb/ooxml/cfb.rb +9 -6
  12. data/lib/xlsxrb/ooxml/crypto/agile.rb +5 -5
  13. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +784 -0
  14. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +3478 -0
  15. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +1796 -0
  16. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +428 -0
  17. data/lib/xlsxrb/ooxml/reader/listeners.rb +4 -6446
  18. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +3 -5
  19. data/lib/xlsxrb/ooxml/utils.rb +5 -6
  20. data/lib/xlsxrb/ooxml/workbook_writer.rb +5 -7
  21. data/lib/xlsxrb/ooxml/worksheet_parser.rb +172 -9
  22. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +1602 -0
  23. data/lib/xlsxrb/ooxml/writer/features_xml.rb +731 -0
  24. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +348 -0
  25. data/lib/xlsxrb/ooxml/writer.rb +9 -2646
  26. data/lib/xlsxrb/ooxml/xml_builder.rb +31 -2
  27. data/lib/xlsxrb/ooxml/zip_generator.rb +26 -5
  28. data/lib/xlsxrb/ooxml/zip_reader.rb +364 -78
  29. data/lib/xlsxrb/ooxml/zip_writer.rb +32 -5
  30. data/lib/xlsxrb/stream_sheet.rb +94 -6
  31. data/lib/xlsxrb/stream_writer.rb +48 -115
  32. data/lib/xlsxrb/style_builder.rb +10 -1
  33. data/lib/xlsxrb/version.rb +1 -1
  34. data/lib/xlsxrb/workbook_builder.rb +1 -1
  35. data/lib/xlsxrb/worksheet_builder.rb +19 -81
  36. data/lib/xlsxrb.rb +449 -171
  37. data/sig/generated/xlsxrb/chart_builder.rbs +36 -36
  38. data/sig/generated/xlsxrb/dsl_helpers.rbs +92 -0
  39. data/sig/generated/xlsxrb/elements/cell.rbs +29 -0
  40. data/sig/generated/xlsxrb/elements/column.rbs +32 -21
  41. data/sig/generated/xlsxrb/elements/row.rbs +94 -14
  42. data/sig/generated/xlsxrb/elements/workbook.rbs +102 -10
  43. data/sig/generated/xlsxrb/elements/worksheet.rbs +9 -0
  44. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +173 -0
  45. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +400 -0
  46. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +442 -0
  47. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +80 -0
  48. data/sig/generated/xlsxrb/ooxml/reader/listeners.rbs +0 -1072
  49. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +0 -2
  50. data/sig/generated/xlsxrb/ooxml/utils.rbs +2 -0
  51. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +5 -4
  52. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +79 -0
  53. data/sig/generated/xlsxrb/ooxml/writer/features_xml.rbs +83 -0
  54. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +53 -0
  55. data/sig/generated/xlsxrb/ooxml/writer.rbs +6 -185
  56. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +18 -0
  57. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +15 -0
  58. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +96 -8
  59. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +5 -1
  60. data/sig/generated/xlsxrb/stream_sheet.rbs +43 -3
  61. data/sig/generated/xlsxrb/style_builder.rbs +7 -0
  62. data/sig/generated/xlsxrb.rbs +18 -11
  63. metadata +22 -144
  64. data/.devcontainer/Dockerfile +0 -65
  65. data/.devcontainer/devcontainer.json +0 -17
  66. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +0 -9
  67. data/.gem_rbs_collection/ast/2.4/ast.rbs +0 -73
  68. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +0 -9
  69. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +0 -4
  70. data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +0 -16
  71. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +0 -96
  72. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +0 -4
  73. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +0 -68
  74. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +0 -249
  75. data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +0 -4
  76. data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +0 -47
  77. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +0 -5
  78. data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +0 -9
  79. data/.gem_rbs_collection/csv/3.3/csv.rbs +0 -3871
  80. data/.gem_rbs_collection/csv/3.3/manifest.yaml +0 -3
  81. data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +0 -9
  82. data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +0 -48
  83. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +0 -9
  84. data/.gem_rbs_collection/listen/3.9/listen.rbs +0 -25
  85. data/.gem_rbs_collection/listen/3.9/listener.rbs +0 -24
  86. data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +0 -9
  87. data/.gem_rbs_collection/logger/1.7/formatter.rbs +0 -45
  88. data/.gem_rbs_collection/logger/1.7/log_device.rbs +0 -100
  89. data/.gem_rbs_collection/logger/1.7/logger.rbs +0 -796
  90. data/.gem_rbs_collection/logger/1.7/manifest.yaml +0 -2
  91. data/.gem_rbs_collection/logger/1.7/period.rbs +0 -17
  92. data/.gem_rbs_collection/logger/1.7/severity.rbs +0 -34
  93. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +0 -9
  94. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +0 -2332
  95. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +0 -4
  96. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +0 -9
  97. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +0 -86
  98. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +0 -9
  99. data/.gem_rbs_collection/parser/3.2/manifest.yaml +0 -7
  100. data/.gem_rbs_collection/parser/3.2/parser.rbs +0 -194
  101. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +0 -4
  102. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +0 -9
  103. data/.gem_rbs_collection/rainbow/3.0/global.rbs +0 -7
  104. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +0 -209
  105. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +0 -5
  106. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +0 -9
  107. data/.gem_rbs_collection/rake/13.0/manifest.yaml +0 -2
  108. data/.gem_rbs_collection/rake/13.0/rake.rbs +0 -39
  109. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +0 -9
  110. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +0 -17
  111. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +0 -9
  112. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +0 -208
  113. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +0 -9
  114. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +0 -903
  115. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +0 -9
  116. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +0 -8
  117. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +0 -42
  118. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +0 -5
  119. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +0 -47
  120. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +0 -30
  121. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +0 -9
  122. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +0 -7
  123. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +0 -19
  124. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +0 -31
  125. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +0 -18
  126. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +0 -12
  127. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +0 -11
  128. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +0 -13
  129. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +0 -95
  130. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +0 -31
  131. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +0 -58
  132. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +0 -24
  133. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +0 -17
  134. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +0 -23
  135. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +0 -22
  136. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +0 -30
  137. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +0 -20
  138. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +0 -15
  139. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +0 -26
  140. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +0 -21
  141. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +0 -131
  142. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +0 -14
  143. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +0 -33
  144. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +0 -21
  145. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +0 -63
  146. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +0 -55
  147. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +0 -35
  148. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +0 -7
  149. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +0 -10
  150. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +0 -22
  151. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +0 -29
  152. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +0 -17
  153. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +0 -13
  154. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +0 -10
  155. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +0 -8
  156. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +0 -6
  157. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +0 -30
  158. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +0 -10
  159. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +0 -10
  160. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +0 -5
  161. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +0 -15
  162. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +0 -3
  163. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +0 -40
  164. data/Rakefile +0 -567
  165. data/Steepfile +0 -18
  166. data/benchmark.rb +0 -406
  167. data/docs/ARCHITECTURE.md +0 -514
  168. data/docs/DEVELOPMENT.md +0 -133
  169. data/docs/PEER_LIBRARIES.md +0 -121
  170. data/docs/QUALITY_ASSURANCE.md +0 -25
  171. data/docs/SPEC_SOURCES.md +0 -48
  172. data/docs/assets/benchmark_results.svg +0 -144
  173. data/docs/assets/lsp_autocompletion.png +0 -0
  174. data/docs/assets/playground_preview.png +0 -0
  175. data/docs/coi-serviceworker.js +0 -82
  176. data/docs/office_thread.js +0 -77
  177. data/docs/preview.html +0 -719
  178. data/docs/visual/VisualGallery.md +0 -4672
  179. data/docs/wasm/ruby.wasm +0 -0
  180. data/docs/wasm/wasm_doc_helper.css +0 -351
  181. data/docs/wasm/wasm_doc_helper.js +0 -439
  182. data/docs/zeta.js +0 -1107
  183. data/rbs_collection.lock.yaml +0 -252
  184. data/rbs_collection.yaml +0 -19
  185. data/vendor/sdk_runner/Program.cs +0 -93
  186. data/vendor/sdk_runner/sdk_runner.csproj +0 -13
@@ -0,0 +1,1602 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Xlsxrb
6
+ module Ooxml
7
+ class Writer
8
+ # Mixin containing DrawingML, Chart, Shape, Comment, and VML XML generation logic.
9
+ module DrawingXml
10
+ # : (untyped drawing_parts) -> untyped
11
+ def generate_drawing_xml(drawing_parts)
12
+ parts = [
13
+ XML_HEADER,
14
+ %(<xdr:wsDr xmlns:xdr="#{XDR_NS}" xmlns:a="#{A_NS}" xmlns:r="#{DOC_REL_NS}">)
15
+ ]
16
+
17
+ drawing_parts.each do |dp|
18
+ case dp[:kind]
19
+ when :pic
20
+ img = dp[:img]
21
+ rid = "rId#{dp[:rid_index]}"
22
+ ea = img[:edit_as] || "oneCell"
23
+ img_pub_attr = img[:published] ? ' fPublished="1"' : ""
24
+ parts << %(<xdr:twoCellAnchor editAs="#{xml_escape(ea)}"#{img_pub_attr}>)
25
+ parts << anchor_xml("from", img[:from_col], img[:from_row], col_off: img[:from_col_off] || 0, row_off: img[:from_row_off] || 0)
26
+ parts << anchor_xml("to", img[:to_col], img[:to_row], col_off: img[:to_col_off] || 0, row_off: img[:to_row_off] || 0)
27
+ macro_attr = img[:macro] ? %( macro="#{xml_escape(img[:macro])}") : ""
28
+ parts << "<xdr:pic#{macro_attr}>"
29
+ descr_attr = img[:description] ? %( descr="#{xml_escape(img[:description])}") : ""
30
+ title_attr = img[:title] ? %( title="#{xml_escape(img[:title])}") : ""
31
+ hidden_attr = img[:hidden] ? ' hidden="1"' : ""
32
+ pic_lock_attrs = +""
33
+ pic_lock_attrs << ' noChangeAspect="1"' if img[:no_change_aspect]
34
+ pic_lock_attrs << ' noCrop="1"' if img[:no_crop]
35
+ pic_locks = pic_lock_attrs.empty? ? "<a:picLocks/>" : "<a:picLocks#{pic_lock_attrs}/>"
36
+ parts << %(<xdr:nvPicPr><xdr:cNvPr id="#{dp[:rid_index] + 1}" name="#{xml_escape(img[:name])}"#{descr_attr}#{title_attr}#{hidden_attr}/><xdr:cNvPicPr>#{pic_locks}</xdr:cNvPicPr></xdr:nvPicPr>)
37
+ src_rect_xml = if img[:src_rect]
38
+ sr = img[:src_rect]
39
+ sr_attrs = +""
40
+ sr_attrs << %( t="#{sr[:top]}") if sr[:top]
41
+ sr_attrs << %( b="#{sr[:bottom]}") if sr[:bottom]
42
+ sr_attrs << %( l="#{sr[:left]}") if sr[:left]
43
+ sr_attrs << %( r="#{sr[:right]}") if sr[:right]
44
+ "<a:srcRect#{sr_attrs}/>"
45
+ else
46
+ ""
47
+ end
48
+ blip_xml = if img[:alpha_mod_fix]
49
+ %(<a:blip r:embed="#{rid}"><a:alphaModFix amt="#{img[:alpha_mod_fix]}"/></a:blip>)
50
+ else
51
+ %(<a:blip r:embed="#{rid}"/>)
52
+ end
53
+ parts << %(<xdr:blipFill>#{blip_xml}#{src_rect_xml}<a:stretch><a:fillRect/></a:stretch></xdr:blipFill>)
54
+ img_line_xml = if img[:line_color]
55
+ ln_w_attr = img[:line_width] ? %( w="#{img[:line_width].to_i}") : ""
56
+ %(<a:ln#{ln_w_attr}><a:solidFill>#{color_xml(img[:line_color])}</a:solidFill></a:ln>)
57
+ else
58
+ ""
59
+ end
60
+ img_xfrm_xml = img[:rotation] ? %(<a:xfrm rot="#{img[:rotation].to_i}"><a:off x="0" y="0"/><a:ext cx="0" cy="0"/></a:xfrm>) : ""
61
+ parts << %(<xdr:spPr>#{img_xfrm_xml}<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>#{img_line_xml}</xdr:spPr>)
62
+ parts << "</xdr:pic>"
63
+ parts << client_data_xml(img)
64
+ parts << "</xdr:twoCellAnchor>"
65
+ when :chart
66
+ chart = dp[:chart]
67
+ rid = "rId#{dp[:rid_index]}"
68
+ chart_ea_attr = chart[:edit_as] ? %( editAs="#{xml_escape(chart[:edit_as])}") : ""
69
+ chart_pub_attr = chart[:published] ? ' fPublished="1"' : ""
70
+ parts << "<xdr:twoCellAnchor#{chart_ea_attr}#{chart_pub_attr}>"
71
+ parts << anchor_xml("from", chart[:from_col], chart[:from_row], col_off: chart[:from_col_off] || 0, row_off: chart[:from_row_off] || 0)
72
+ parts << anchor_xml("to", chart[:to_col], chart[:to_row], col_off: chart[:to_col_off] || 0, row_off: chart[:to_row_off] || 0)
73
+ gf_macro = chart[:frame_macro] ? xml_escape(chart[:frame_macro]) : ""
74
+ parts << %(<xdr:graphicFrame macro="#{gf_macro}">)
75
+ chart_frame_name = chart[:name] || chart[:title] || "Chart"
76
+ chart_descr_attr = chart[:description] ? %( descr="#{xml_escape(chart[:description])}") : ""
77
+ chart_title_attr = chart[:frame_title] ? %( title="#{xml_escape(chart[:frame_title])}") : ""
78
+ chart_hidden_attr = chart[:frame_hidden] ? ' hidden="1"' : ""
79
+ cnv_gf_pr = if chart[:frame_no_grp]
80
+ '<xdr:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></xdr:cNvGraphicFramePr>'
81
+ else
82
+ "<xdr:cNvGraphicFramePr/>"
83
+ end
84
+ parts << %(<xdr:nvGraphicFramePr><xdr:cNvPr id="#{dp[:rid_index] + 1}" name="#{xml_escape(chart_frame_name)}"#{chart_descr_attr}#{chart_title_attr}#{chart_hidden_attr}/>#{cnv_gf_pr}</xdr:nvGraphicFramePr>)
85
+ parts << '<xdr:xfrm><a:off x="0" y="0"/><a:ext cx="5000000" cy="3000000"/></xdr:xfrm>'
86
+ parts << %(<a:graphic><a:graphicData uri="#{C_NS}"><c:chart xmlns:c="#{C_NS}" xmlns:r="#{DOC_REL_NS}" r:id="#{rid}"/></a:graphicData></a:graphic>)
87
+ parts << "</xdr:graphicFrame>"
88
+ parts << client_data_xml(chart)
89
+ parts << "</xdr:twoCellAnchor>"
90
+ when :sp
91
+ shape = dp[:shape]
92
+ shape_ea_attr = shape[:edit_as] ? %( editAs="#{xml_escape(shape[:edit_as])}") : ""
93
+ shape_pub_attr = shape[:published] ? ' fPublished="1"' : ""
94
+ parts << "<xdr:twoCellAnchor#{shape_ea_attr}#{shape_pub_attr}>"
95
+ parts << anchor_xml("from", shape[:from_col], shape[:from_row], col_off: shape[:from_col_off] || 0, row_off: shape[:from_row_off] || 0)
96
+ parts << anchor_xml("to", shape[:to_col], shape[:to_row], col_off: shape[:to_col_off] || 0, row_off: shape[:to_row_off] || 0)
97
+ sp_macro_attr = shape[:macro] ? %( macro="#{xml_escape(shape[:macro])}") : ""
98
+ sp_textlink_attr = shape[:textlink] ? %( textlink="#{xml_escape(shape[:textlink])}") : ""
99
+ parts << "<xdr:sp#{sp_macro_attr}#{sp_textlink_attr}>"
100
+ shape_descr_attr = shape[:description] ? %( descr="#{xml_escape(shape[:description])}") : ""
101
+ shape_title_attr = shape[:title] ? %( title="#{xml_escape(shape[:title])}") : ""
102
+ shape_hidden_attr = shape[:hidden] ? ' hidden="1"' : ""
103
+ sp_lock_attrs = +""
104
+ sp_lock_attrs << ' noGrp="1"' if shape[:no_grp]
105
+ sp_lock_attrs << ' noRot="1"' if shape[:no_rot]
106
+ sp_lock_attrs << ' fLocksText="1"' if shape[:f_locks_text]
107
+ cnv_sp_pr = if sp_lock_attrs.empty?
108
+ "<xdr:cNvSpPr/>"
109
+ else
110
+ "<xdr:cNvSpPr><a:spLocks#{sp_lock_attrs}/></xdr:cNvSpPr>"
111
+ end
112
+ parts << %(<xdr:nvSpPr><xdr:cNvPr id="#{dp[:id]}" name="#{xml_escape(shape[:name])}"#{shape_descr_attr}#{shape_title_attr}#{shape_hidden_attr}/>#{cnv_sp_pr}</xdr:nvSpPr>)
113
+ shape_fill_xml = if shape[:no_fill]
114
+ "<a:noFill/>"
115
+ elsif shape[:fill_color]
116
+ "<a:solidFill>#{srgb_clr_xml(shape[:fill_color], alpha: shape[:fill_alpha], transforms: shape[:fill_color_transforms])}</a:solidFill>"
117
+ elsif shape[:gradient_fill]
118
+ gf = shape[:gradient_fill]
119
+ gf_attrs = +""
120
+ gf_attrs << %( rotWithShape="#{gf[:rot_with_shape] ? 1 : 0}") unless gf[:rot_with_shape].nil?
121
+ gf_attrs << %( flip="#{xml_escape(gf[:flip])}") if gf[:flip]
122
+ gs_xml = (gf[:stops] || []).map { |gs| %(<a:gs pos="#{gs[:pos]}">#{color_xml(gs[:color])}</a:gs>) }.join
123
+ type_xml = if gf[:path]
124
+ %(<a:path path="#{xml_escape(gf[:path])}"/>)
125
+ elsif gf[:angle]
126
+ scaled_attr = gf[:scaled] ? %( scaled="1") : %( scaled="0")
127
+ %(<a:lin ang="#{gf[:angle]}"#{scaled_attr}/>)
128
+ else
129
+ ""
130
+ end
131
+ tile_xml = gf[:tile_rect] ? %(<a:tileRect#{gf[:tile_rect].map { |k, v| %( #{k}="#{v}") }.join}/>) : ""
132
+ "<a:gradFill#{gf_attrs}><a:gsLst>#{gs_xml}</a:gsLst>#{type_xml}#{tile_xml}</a:gradFill>"
133
+ elsif shape[:pattern_fill]
134
+ pf = shape[:pattern_fill]
135
+ pf_children = +""
136
+ pf_children << %(<a:fgClr>#{color_xml(pf[:fg_color])}</a:fgClr>) if pf[:fg_color]
137
+ pf_children << %(<a:bgClr>#{color_xml(pf[:bg_color])}</a:bgClr>) if pf[:bg_color]
138
+ %(<a:pattFill prst="#{xml_escape(pf[:preset])}">#{pf_children}</a:pattFill>)
139
+ else
140
+ ""
141
+ end
142
+ shape_line_xml = if shape[:no_line]
143
+ "<a:ln><a:noFill/></a:ln>"
144
+ elsif shape[:line_color] || shape[:line_dash] || shape[:line_custom_dash] || shape[:head_end] || shape[:tail_end] || shape[:line_cap] || shape[:line_align] || shape[:line_compound] || shape[:line_join]
145
+ ln_attrs = +(shape[:line_width] ? %( w="#{shape[:line_width].to_i}") : "")
146
+ ln_attrs << %( cap="#{xml_escape(shape[:line_cap])}") if shape[:line_cap]
147
+ ln_attrs << %( algn="#{xml_escape(shape[:line_align])}") if shape[:line_align]
148
+ ln_attrs << %( cmpd="#{xml_escape(shape[:line_compound])}") if shape[:line_compound]
149
+ fill_part = if shape[:line_color]
150
+ "<a:solidFill>#{srgb_clr_xml(shape[:line_color], alpha: shape[:line_alpha], transforms: shape[:line_color_transforms])}</a:solidFill>"
151
+ else
152
+ ""
153
+ end
154
+ dash_part = if shape[:line_custom_dash]
155
+ ds_xml = shape[:line_custom_dash].map { |ds| %(<a:ds d="#{ds[:d]}" sp="#{ds[:sp]}"/>) }.join
156
+ "<a:custDash>#{ds_xml}</a:custDash>"
157
+ elsif shape[:line_dash]
158
+ %(<a:prstDash val="#{xml_escape(shape[:line_dash])}"/>)
159
+ else
160
+ ""
161
+ end
162
+ head_part = build_line_end_xml("headEnd", shape[:head_end])
163
+ tail_part = build_line_end_xml("tailEnd", shape[:tail_end])
164
+ join_part = case shape[:line_join]
165
+ when "round" then "<a:round/>"
166
+ when "bevel" then "<a:bevel/>"
167
+ when "miter"
168
+ shape[:line_miter_limit] ? "<a:miter lim=\"#{shape[:line_miter_limit].to_i}\"/>" : "<a:miter/>"
169
+ else ""
170
+ end
171
+ %(<a:ln#{ln_attrs}>#{fill_part}#{dash_part}#{head_part}#{tail_part}#{join_part}</a:ln>)
172
+ else
173
+ ""
174
+ end
175
+ shape_xfrm_xml = shape[:rotation] ? %(<a:xfrm rot="#{shape[:rotation].to_i}"><a:off x="0" y="0"/><a:ext cx="0" cy="0"/></a:xfrm>) : ""
176
+ av_lst_xml = if shape[:adjust_values]&.any?
177
+ gds = shape[:adjust_values].map { |gd| %(<a:gd name="#{xml_escape(gd[:name])}" fmla="#{xml_escape(gd[:fmla])}"/>) }
178
+ "<a:avLst>#{gds.join}</a:avLst>"
179
+ else
180
+ "<a:avLst/>"
181
+ end
182
+ effect_lst_xml = if shape[:outer_shadow] || shape[:inner_shadow] || shape[:glow] || shape[:soft_edge] || shape[:reflection] || shape[:blur]
183
+ effect_children = +""
184
+ if shape[:outer_shadow]
185
+ os = shape[:outer_shadow]
186
+ os_attrs = +""
187
+ os_attrs << %( blurRad="#{os[:blur_rad]}") if os[:blur_rad]
188
+ os_attrs << %( dist="#{os[:dist]}") if os[:dist]
189
+ os_attrs << %( dir="#{os[:dir]}") if os[:dir]
190
+ os_attrs << %( algn="#{xml_escape(os[:algn])}") if os[:algn]
191
+ os_attrs << %( rotWithShape="#{os[:rot_with_shape] ? 1 : 0}") unless os[:rot_with_shape].nil?
192
+ os_color = os[:color] ? srgb_clr_xml(os[:color], alpha: os[:alpha]) : ""
193
+ effect_children << "<a:outerShdw#{os_attrs}>#{os_color}</a:outerShdw>"
194
+ end
195
+ if shape[:glow]
196
+ gl = shape[:glow]
197
+ gl_attrs = +""
198
+ gl_attrs << %( rad="#{gl[:rad]}") if gl[:rad]
199
+ gl_color = gl[:color] ? srgb_clr_xml(gl[:color], alpha: gl[:alpha]) : ""
200
+ effect_children << "<a:glow#{gl_attrs}>#{gl_color}</a:glow>"
201
+ end
202
+ if shape[:inner_shadow]
203
+ is = shape[:inner_shadow]
204
+ is_attrs = +""
205
+ is_attrs << %( blurRad="#{is[:blur_rad]}") if is[:blur_rad]
206
+ is_attrs << %( dist="#{is[:dist]}") if is[:dist]
207
+ is_attrs << %( dir="#{is[:dir]}") if is[:dir]
208
+ is_color = is[:color] ? srgb_clr_xml(is[:color], alpha: is[:alpha]) : ""
209
+ effect_children << "<a:innerShdw#{is_attrs}>#{is_color}</a:innerShdw>"
210
+ end
211
+ if shape[:reflection]
212
+ rf = shape[:reflection]
213
+ rf_attrs = +""
214
+ rf_attrs << %( blurRad="#{rf[:blur_rad]}") if rf[:blur_rad]
215
+ rf_attrs << %( stA="#{rf[:st_a]}") if rf[:st_a]
216
+ rf_attrs << %( endA="#{rf[:end_a]}") if rf[:end_a]
217
+ rf_attrs << %( dist="#{rf[:dist]}") unless rf[:dist].nil?
218
+ rf_attrs << %( dir="#{rf[:dir]}") if rf[:dir]
219
+ rf_attrs << %( fadeDir="#{rf[:fade_dir]}") if rf[:fade_dir]
220
+ rf_attrs << %( sx="#{rf[:sx]}") if rf[:sx]
221
+ rf_attrs << %( sy="#{rf[:sy]}") if rf[:sy]
222
+ rf_attrs << %( kx="#{rf[:kx]}") if rf[:kx]
223
+ rf_attrs << %( ky="#{rf[:ky]}") if rf[:ky]
224
+ rf_attrs << %( algn="#{xml_escape(rf[:algn])}") if rf[:algn]
225
+ rf_attrs << %( rotWithShape="#{rf[:rot_with_shape] ? 1 : 0}") unless rf[:rot_with_shape].nil?
226
+ effect_children << "<a:reflection#{rf_attrs}/>"
227
+ end
228
+ if shape[:soft_edge]
229
+ se = shape[:soft_edge]
230
+ effect_children << %(<a:softEdge rad="#{se[:rad]}"/>)
231
+ end
232
+ if shape[:blur]
233
+ bl = shape[:blur]
234
+ bl_attrs = +""
235
+ bl_attrs << %( rad="#{bl[:rad]}") if bl[:rad]
236
+ bl_attrs << %( grow="#{bl[:grow] ? 1 : 0}") unless bl[:grow].nil?
237
+ effect_children << "<a:blur#{bl_attrs}/>"
238
+ end
239
+ "<a:effectLst>#{effect_children}</a:effectLst>"
240
+ else
241
+ ""
242
+ end
243
+ parts << %(<xdr:spPr>#{shape_xfrm_xml}<a:prstGeom prst="#{xml_escape(shape[:preset])}">#{av_lst_xml}</a:prstGeom>#{shape_fill_xml}#{shape_line_xml}#{effect_lst_xml}</xdr:spPr>)
244
+ if shape[:text] || shape[:text_paragraphs]
245
+ body_pr_attrs = +""
246
+ body_pr_attrs << %( rot="#{shape[:text_rot]}") if shape[:text_rot]
247
+ body_pr_attrs << %( spcFirstLastPara="1") if shape[:text_spc_first_last_para]
248
+ body_pr_attrs << %( wrap="#{xml_escape(shape[:text_wrap])}") if shape[:text_wrap]
249
+ body_pr_attrs << %( anchor="#{xml_escape(shape[:text_anchor])}") if shape[:text_anchor]
250
+ body_pr_attrs << %( anchorCtr="1") if shape[:text_anchor_ctr]
251
+ body_pr_attrs << %( vertOverflow="#{xml_escape(shape[:text_vert_overflow])}") if shape[:text_vert_overflow]
252
+ body_pr_attrs << %( horzOverflow="#{xml_escape(shape[:text_horz_overflow])}") if shape[:text_horz_overflow]
253
+ body_pr_attrs << %( numCol="#{shape[:text_num_col]}") if shape[:text_num_col]
254
+ body_pr_attrs << %( spcCol="#{shape[:text_spc_col]}") if shape[:text_spc_col]
255
+ body_pr_attrs << %( rtlCol="1") if shape[:text_rtl_col]
256
+ body_pr_attrs << %( fromWordArt="1") if shape[:text_from_word_art]
257
+ body_pr_attrs << %( upright="1") if shape[:text_upright]
258
+ body_pr_attrs << %( compatLnSpc="1") if shape[:text_compat_ln_spc]
259
+ body_pr_attrs << %( forceAA="1") if shape[:text_force_aa]
260
+ body_pr_attrs << %( vert="#{xml_escape(shape[:text_vertical])}") if shape[:text_vertical]
261
+ if shape[:text_insets]
262
+ ins = shape[:text_insets]
263
+ body_pr_attrs << %( lIns="#{ins[:left]}") if ins[:left]
264
+ body_pr_attrs << %( tIns="#{ins[:top]}") if ins[:top]
265
+ body_pr_attrs << %( rIns="#{ins[:right]}") if ins[:right]
266
+ body_pr_attrs << %( bIns="#{ins[:bottom]}") if ins[:bottom]
267
+ end
268
+ body_pr_children = +""
269
+ if shape[:text_warp]
270
+ tw = shape[:text_warp]
271
+ body_pr_children << %(<a:prstTxWarp prst="#{xml_escape(tw[:preset])}"><a:avLst/></a:prstTxWarp>)
272
+ end
273
+ autofit_xml = case shape[:autofit]
274
+ when "none" then "<a:noAutofit/>"
275
+ when "shape" then "<a:spAutoFit/>"
276
+ when "normal" then "<a:normAutofit/>"
277
+ when Hash
278
+ af = shape[:autofit]
279
+ case af[:type]
280
+ when "normal"
281
+ na_attrs = +""
282
+ na_attrs << %( fontScale="#{af[:font_scale]}") if af[:font_scale]
283
+ na_attrs << %( lnSpcReduction="#{af[:ln_spc_reduction]}") if af[:ln_spc_reduction]
284
+ "<a:normAutofit#{na_attrs}/>"
285
+ when "none" then "<a:noAutofit/>"
286
+ when "shape" then "<a:spAutoFit/>"
287
+ else ""
288
+ end
289
+ else ""
290
+ end
291
+ body_pr_children << autofit_xml
292
+ body_pr_xml = if body_pr_children.empty?
293
+ "<a:bodyPr#{body_pr_attrs}/>"
294
+ else
295
+ "<a:bodyPr#{body_pr_attrs}>#{body_pr_children}</a:bodyPr>"
296
+ end
297
+ parts << "<xdr:txBody>#{body_pr_xml}<a:lstStyle/>"
298
+ if shape[:text_paragraphs]
299
+ shape[:text_paragraphs].each { |para| parts << paragraph_xml(para) }
300
+ elsif shape[:text]
301
+ parts << paragraph_xml(
302
+ text: shape[:text], font: shape[:text_font],
303
+ align: shape[:text_align], font_align: shape[:text_font_align],
304
+ def_tab_sz: shape[:text_def_tab_sz], rtl: shape[:text_rtl],
305
+ ea_ln_brk: shape[:text_ea_ln_brk], latin_ln_brk: shape[:text_latin_ln_brk],
306
+ hanging_punct: shape[:text_hanging_punct], level: shape[:text_level],
307
+ indent: shape[:text_indent], spacing: shape[:text_spacing],
308
+ tab_stops: shape[:text_tab_stops], bullet: shape[:text_bullet],
309
+ def_rpr: shape[:text_def_rpr], end_para_rpr: shape[:text_end_para_rpr]
310
+ )
311
+ end
312
+ parts << "</xdr:txBody>"
313
+ end
314
+ parts << "</xdr:sp>"
315
+ parts << client_data_xml(shape)
316
+ parts << "</xdr:twoCellAnchor>"
317
+ end
318
+ end
319
+
320
+ parts << "</xdr:wsDr>"
321
+ parts.join
322
+ end
323
+
324
+ # : (untyped para) -> ::String
325
+ def paragraph_xml(para)
326
+ ppr_attrs = +""
327
+ ppr_attrs << %( algn="#{xml_escape(para[:align])}") if para[:align]
328
+ ppr_attrs << %( fontAlgn="#{xml_escape(para[:font_align])}") if para[:font_align]
329
+ ppr_attrs << %( defTabSz="#{para[:def_tab_sz]}") if para[:def_tab_sz]
330
+ ppr_attrs << %( rtl="1") if para[:rtl]
331
+ ppr_attrs << %( eaLnBrk="1") if para[:ea_ln_brk]
332
+ ppr_attrs << %( latinLnBrk="1") if para[:latin_ln_brk]
333
+ ppr_attrs << %( hangingPunct="1") if para[:hanging_punct]
334
+ ppr_attrs << %( lvl="#{para[:level]}") if para[:level]
335
+ if para[:indent]
336
+ ti = para[:indent]
337
+ ppr_attrs << %( marL="#{ti[:left]}") if ti[:left]
338
+ ppr_attrs << %( marR="#{ti[:right]}") if ti[:right]
339
+ ppr_attrs << %( indent="#{ti[:indent]}") if ti[:indent]
340
+ end
341
+ ppr_children = +""
342
+ if para[:spacing]
343
+ ts = para[:spacing]
344
+ if ts[:line]
345
+ ppr_children << %(<a:lnSpc><a:spcPts val="#{ts[:line]}"/></a:lnSpc>)
346
+ elsif ts[:line_pct]
347
+ ppr_children << %(<a:lnSpc><a:spcPct val="#{ts[:line_pct]}"/></a:lnSpc>)
348
+ end
349
+ if ts[:before]
350
+ ppr_children << %(<a:spcBef><a:spcPts val="#{ts[:before]}"/></a:spcBef>)
351
+ elsif ts[:before_pct]
352
+ ppr_children << %(<a:spcBef><a:spcPct val="#{ts[:before_pct]}"/></a:spcBef>)
353
+ end
354
+ if ts[:after]
355
+ ppr_children << %(<a:spcAft><a:spcPts val="#{ts[:after]}"/></a:spcAft>)
356
+ elsif ts[:after_pct]
357
+ ppr_children << %(<a:spcAft><a:spcPct val="#{ts[:after_pct]}"/></a:spcAft>)
358
+ end
359
+ end
360
+ if para[:tab_stops]&.any?
361
+ tabs = para[:tab_stops].map do |tab|
362
+ tab_attrs = %( pos="#{tab[:pos]}")
363
+ tab_attrs << %( algn="#{xml_escape(tab[:align])}") if tab[:align]
364
+ "<a:tab#{tab_attrs}/>"
365
+ end
366
+ ppr_children << "<a:tabLst>#{tabs.join}</a:tabLst>"
367
+ end
368
+ if para[:bullet]
369
+ bu = para[:bullet]
370
+ ppr_children << %(<a:buClr>#{color_xml(bu[:color])}</a:buClr>) if bu[:color]
371
+ ppr_children << %(<a:buSzPts val="#{bu[:size_pts]}"/>) if bu[:size_pts]
372
+ ppr_children << %(<a:buSzPct val="#{bu[:size_pct]}"/>) if bu[:size_pct]
373
+ ppr_children << %(<a:buFont typeface="#{xml_escape(bu[:font])}"/>) if bu[:font]
374
+ case bu[:type]
375
+ when "none"
376
+ ppr_children << "<a:buNone/>"
377
+ when "char"
378
+ ppr_children << %(<a:buChar char="#{xml_escape(bu[:char])}"/>)
379
+ when "auto"
380
+ auto_attrs = %( type="#{xml_escape(bu[:auto_type])}")
381
+ auto_attrs << %( startAt="#{bu[:start_at]}") if bu[:start_at]
382
+ ppr_children << "<a:buAutoNum#{auto_attrs}/>"
383
+ end
384
+ end
385
+ ppr_children << text_char_props_xml("a:defRPr", para[:def_rpr]) if para[:def_rpr]
386
+ ppr_xml = if ppr_attrs.empty? && ppr_children.empty?
387
+ ""
388
+ elsif ppr_children.empty?
389
+ "<a:pPr#{ppr_attrs}/>"
390
+ else
391
+ "<a:pPr#{ppr_attrs}>#{ppr_children}</a:pPr>"
392
+ end
393
+ end_para_rpr_xml = para[:end_para_rpr] ? text_char_props_xml("a:endParaRPr", para[:end_para_rpr]) : ""
394
+ runs_xml = if para[:runs]
395
+ para[:runs].map do |run|
396
+ run_rpr = run[:font] ? text_char_props_xml("a:rPr", run[:font]) : ""
397
+ "<a:r>#{run_rpr}<a:t>#{xml_escape(run[:text] || "")}</a:t></a:r>"
398
+ end.join
399
+ else
400
+ rpr_xml = para[:font] ? text_char_props_xml("a:rPr", para[:font]) : ""
401
+ "<a:r>#{rpr_xml}<a:t>#{xml_escape(para[:text] || "")}</a:t></a:r>"
402
+ end
403
+ "<a:p>#{ppr_xml}#{runs_xml}#{end_para_rpr_xml}</a:p>"
404
+ end
405
+
406
+ # : (untyped tag, untyped font) -> untyped
407
+ def text_char_props_xml(tag, font)
408
+ attrs = +""
409
+ attrs << %( b="1") if font[:bold]
410
+ attrs << %( i="1") if font[:italic]
411
+ attrs << %( noProof="1") if font[:no_proof]
412
+ attrs << %( normalizeH="1") if font[:normalize_h]
413
+ attrs << %( kumimoji="1") if font[:kumimoji]
414
+ attrs << %( sz="#{font[:size]}") if font[:size]
415
+ attrs << %( strike="#{xml_escape(font[:strike])}") if font[:strike]
416
+ attrs << %( u="#{xml_escape(font[:underline])}") if font[:underline]
417
+ attrs << %( baseline="#{font[:baseline]}") if font[:baseline]
418
+ attrs << %( spc="#{font[:spacing]}") if font[:spacing]
419
+ attrs << %( kern="#{font[:kern]}") if font[:kern]
420
+ attrs << %( cap="#{xml_escape(font[:cap])}") if font[:cap]
421
+ attrs << %( lang="#{xml_escape(font[:lang])}") if font[:lang]
422
+ attrs << %( altLang="#{xml_escape(font[:alt_lang])}") if font[:alt_lang]
423
+ attrs << %( dirty="1") if font[:dirty]
424
+ attrs << %( smtClean="1") if font[:smt_clean]
425
+ attrs << %( err="1") if font[:err]
426
+ attrs << %( bmk="#{xml_escape(font[:bmk])}") if font[:bmk]
427
+ children = +""
428
+ children << %(<a:solidFill>#{color_xml(font[:color])}</a:solidFill>) if font[:color]
429
+ children << %(<a:highlight>#{color_xml(font[:highlight])}</a:highlight>) if font[:highlight]
430
+ children << %(<a:latin typeface="#{xml_escape(font[:name])}"/>) if font[:name]
431
+ children << %(<a:ea typeface="#{xml_escape(font[:ea_font])}"/>) if font[:ea_font]
432
+ children << %(<a:cs typeface="#{xml_escape(font[:cs_font])}"/>) if font[:cs_font]
433
+ children << %(<a:sym typeface="#{xml_escape(font[:sym_font])}"/>) if font[:sym_font]
434
+ children << "<a:uFillTx/>" if font[:u_fill_tx]
435
+ children << "<a:uLnTx/>" if font[:u_ln_tx]
436
+ if font[:line_color] || font[:line_width] || font[:line_dash] || font[:line_cap] || font[:line_join]
437
+ ln_attrs = +(font[:line_width] ? %( w="#{font[:line_width].to_i}") : "")
438
+ ln_attrs << %( cap="#{xml_escape(font[:line_cap])}") if font[:line_cap]
439
+ ln_fill = font[:line_color] ? %(<a:solidFill>#{color_xml(font[:line_color])}</a:solidFill>) : ""
440
+ ln_dash = font[:line_dash] ? %(<a:prstDash val="#{xml_escape(font[:line_dash])}"/>) : ""
441
+ ln_join = case font[:line_join]
442
+ when "round" then "<a:round/>"
443
+ when "bevel" then "<a:bevel/>"
444
+ when "miter"
445
+ font[:line_miter_limit] ? "<a:miter lim=\"#{font[:line_miter_limit].to_i}\"/>" : "<a:miter/>"
446
+ else ""
447
+ end
448
+ children << "<a:ln#{ln_attrs}>#{ln_fill}#{ln_dash}#{ln_join}</a:ln>"
449
+ end
450
+ if font[:outer_shadow] || font[:inner_shadow] || font[:glow] || font[:soft_edge] || font[:reflection] || font[:blur]
451
+ eff = +""
452
+ if font[:outer_shadow]
453
+ os = font[:outer_shadow]
454
+ oa = +""
455
+ oa << %( blurRad="#{os[:blur_rad]}") if os[:blur_rad]
456
+ oa << %( dist="#{os[:dist]}") if os[:dist]
457
+ oa << %( dir="#{os[:dir]}") if os[:dir]
458
+ oa << %( algn="#{xml_escape(os[:algn])}") if os[:algn]
459
+ oa << %( rotWithShape="#{os[:rot_with_shape] ? 1 : 0}") unless os[:rot_with_shape].nil?
460
+ oc = os[:color] ? srgb_clr_xml(os[:color], alpha: os[:alpha]) : ""
461
+ eff << "<a:outerShdw#{oa}>#{oc}</a:outerShdw>"
462
+ end
463
+ if font[:glow]
464
+ gl = font[:glow]
465
+ ga = +(gl[:rad] ? %( rad="#{gl[:rad]}") : "")
466
+ gc = gl[:color] ? srgb_clr_xml(gl[:color], alpha: gl[:alpha]) : ""
467
+ eff << "<a:glow#{ga}>#{gc}</a:glow>"
468
+ end
469
+ if font[:inner_shadow]
470
+ is = font[:inner_shadow]
471
+ ia = +""
472
+ ia << %( blurRad="#{is[:blur_rad]}") if is[:blur_rad]
473
+ ia << %( dist="#{is[:dist]}") if is[:dist]
474
+ ia << %( dir="#{is[:dir]}") if is[:dir]
475
+ ic = is[:color] ? srgb_clr_xml(is[:color], alpha: is[:alpha]) : ""
476
+ eff << "<a:innerShdw#{ia}>#{ic}</a:innerShdw>"
477
+ end
478
+ if font[:reflection]
479
+ rf = font[:reflection]
480
+ ra = +""
481
+ ra << %( blurRad="#{rf[:blur_rad]}") if rf[:blur_rad]
482
+ ra << %( stA="#{rf[:st_a]}") if rf[:st_a]
483
+ ra << %( endA="#{rf[:end_a]}") if rf[:end_a]
484
+ ra << %( dist="#{rf[:dist]}") unless rf[:dist].nil?
485
+ ra << %( dir="#{rf[:dir]}") if rf[:dir]
486
+ ra << %( fadeDir="#{rf[:fade_dir]}") if rf[:fade_dir]
487
+ ra << %( sx="#{rf[:sx]}") if rf[:sx]
488
+ ra << %( sy="#{rf[:sy]}") if rf[:sy]
489
+ ra << %( kx="#{rf[:kx]}") if rf[:kx]
490
+ ra << %( ky="#{rf[:ky]}") if rf[:ky]
491
+ ra << %( algn="#{xml_escape(rf[:algn])}") if rf[:algn]
492
+ ra << %( rotWithShape="#{rf[:rot_with_shape] ? 1 : 0}") unless rf[:rot_with_shape].nil?
493
+ eff << "<a:reflection#{ra}/>"
494
+ end
495
+ if font[:soft_edge]
496
+ se = font[:soft_edge]
497
+ eff << %(<a:softEdge rad="#{se[:rad]}"/>)
498
+ end
499
+ if font[:blur]
500
+ bl = font[:blur]
501
+ ba = +(bl[:rad] ? %( rad="#{bl[:rad]}") : "")
502
+ ba << %( grow="#{bl[:grow] ? 1 : 0}") unless bl[:grow].nil?
503
+ eff << "<a:blur#{ba}/>"
504
+ end
505
+ children << "<a:effectLst>#{eff}</a:effectLst>"
506
+ end
507
+ if children.empty?
508
+ "<#{tag}#{attrs}/>"
509
+ else
510
+ "<#{tag}#{attrs}>#{children}</#{tag}>"
511
+ end
512
+ end
513
+
514
+ # : (untyped tag, untyped col, untyped row, ?col_off: ::Integer, ?row_off: ::Integer) -> ::String
515
+ def anchor_xml(tag, col, row, col_off: 0, row_off: 0)
516
+ "<xdr:#{tag}><xdr:col>#{col}</xdr:col><xdr:colOff>#{col_off}</xdr:colOff><xdr:row>#{row}</xdr:row><xdr:rowOff>#{row_off}</xdr:rowOff></xdr:#{tag}>"
517
+ end
518
+
519
+ # : (untyped obj) -> ::String
520
+ def client_data_xml(obj)
521
+ cd_attrs = +""
522
+ cd_attrs << ' fLocksWithSheet="0"' if obj[:locks_with_sheet] == false
523
+ cd_attrs << ' fLocksWithSheet="1"' if obj[:locks_with_sheet] == true
524
+ cd_attrs << ' fPrintsWithSheet="0"' if obj[:prints_with_sheet] == false
525
+ cd_attrs << ' fPrintsWithSheet="1"' if obj[:prints_with_sheet] == true
526
+ "<xdr:clientData#{cd_attrs}/>"
527
+ end
528
+
529
+ # : (untyped rels_data) -> untyped
530
+ def generate_drawing_rels(rels_data)
531
+ parts = [XML_HEADER, %(<Relationships xmlns="#{REL_NS}">)]
532
+ rels_data.each_with_index do |rel, i|
533
+ rel_type = case rel[:type]
534
+ when :image then "#{DOC_REL_NS}/image"
535
+ when :chart then "#{DOC_REL_NS}/chart"
536
+ end
537
+ parts << %(<Relationship Id="rId#{i + 1}" Type="#{rel_type}" Target="#{rel[:target]}"/>)
538
+ end
539
+ parts << "</Relationships>"
540
+ parts.join
541
+ end
542
+
543
+ # : (untyped title_spec, ?overlay: bool) -> untyped
544
+ def build_chart_title_xml(title_spec, overlay: false)
545
+ overlay_val = overlay ? 1 : 0
546
+ if title_spec.is_a?(Hash)
547
+ text = xml_escape(title_spec[:text].to_s)
548
+ rotation = title_spec[:rotation]
549
+ body_pr = rotation ? %(<a:bodyPr rot="#{rotation}"/>) : "<a:bodyPr/>"
550
+ font = title_spec[:font] || {}
551
+ rpr_attrs = +""
552
+ rpr_attrs << %( b="1") if font[:bold]
553
+ rpr_attrs << %( i="1") if font[:italic]
554
+ rpr_attrs << %( sz="#{font[:size]}") if font[:size]
555
+ rpr_children = +""
556
+ rpr_children << %(<a:solidFill>#{color_xml(font[:color])}</a:solidFill>) if font[:color]
557
+ rpr_children << %(<a:latin typeface="#{xml_escape(font[:name])}"/>) if font[:name]
558
+ rpr_xml = if rpr_children.empty?
559
+ "<a:rPr#{rpr_attrs}/>"
560
+ else
561
+ "<a:rPr#{rpr_attrs}>#{rpr_children}</a:rPr>"
562
+ end
563
+ layout_xml = build_title_layout_xml(title_spec[:layout])
564
+ sp_xml = build_title_sp_pr(title_spec)
565
+ "<c:title><c:tx><c:rich>#{body_pr}<a:lstStyle/><a:p><a:r>#{rpr_xml}<a:t>#{text}</a:t></a:r></a:p></c:rich></c:tx>#{layout_xml}<c:overlay val=\"#{overlay_val}\"/>#{sp_xml}</c:title>"
566
+ else
567
+ "<c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>#{xml_escape(title_spec)}</a:t></a:r></a:p></c:rich></c:tx><c:overlay val=\"#{overlay_val}\"/></c:title>"
568
+ end
569
+ end
570
+
571
+ # : (untyped title_spec, untyped chart, untyped prefix) -> untyped
572
+ def merge_flat_title_styling(title_spec, chart, prefix)
573
+ font_key = :"#{prefix}_font"
574
+ fill_key = :"#{prefix}_fill"
575
+ no_fill_key = :"#{prefix}_no_fill"
576
+ lc_key = :"#{prefix}_line_color"
577
+ lw_key = :"#{prefix}_line_width"
578
+ ld_key = :"#{prefix}_line_dash"
579
+ rot_key = :"#{prefix}_rotation"
580
+ return title_spec unless chart[font_key] || chart[fill_key] || chart[no_fill_key] ||
581
+ chart[lc_key] || chart[lw_key] || chart[ld_key] || chart[rot_key]
582
+
583
+ spec = title_spec.is_a?(Hash) ? title_spec : { text: title_spec }
584
+ spec[:font] ||= chart[font_key] if chart[font_key]
585
+ spec[:fill_color] ||= chart[fill_key] if chart[fill_key]
586
+ spec[:no_fill] = chart[no_fill_key] if spec[:no_fill].nil? && !chart[no_fill_key].nil?
587
+ spec[:line_color] ||= chart[lc_key] if chart[lc_key]
588
+ spec[:line_width] ||= chart[lw_key] if chart[lw_key]
589
+ spec[:line_dash] ||= chart[ld_key] if chart[ld_key]
590
+ spec[:rotation] ||= chart[rot_key] if chart[rot_key]
591
+ spec
592
+ end
593
+
594
+ # : (untyped spec) -> untyped
595
+ def build_title_sp_pr(spec)
596
+ children = +""
597
+ children << %(<a:solidFill>#{color_xml(spec[:fill_color])}</a:solidFill>) if spec[:fill_color]
598
+ children << "<a:noFill/>" if spec[:no_fill]
599
+ if spec[:line_color] || spec[:line_width] || spec[:line_dash]
600
+ lw = spec[:line_width] ? %( w="#{(spec[:line_width] * 12_700).to_i}") : ""
601
+ lf = spec[:line_color] ? %(<a:solidFill>#{color_xml(spec[:line_color])}</a:solidFill>) : ""
602
+ ld = spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(spec[:line_dash])}"/>) : ""
603
+ children << "<a:ln#{lw}>#{lf}#{ld}</a:ln>"
604
+ end
605
+ children.empty? ? "" : "<c:spPr>#{children}</c:spPr>"
606
+ end
607
+
608
+ # : (untyped layout) -> ("" | untyped)
609
+ def build_title_layout_xml(layout)
610
+ return "" unless layout.is_a?(Hash)
611
+
612
+ ml = +""
613
+ ml << %(<c:x val="#{layout[:x]}"/>) if layout[:x]
614
+ ml << %(<c:y val="#{layout[:y]}"/>) if layout[:y]
615
+ ml << %(<c:w val="#{layout[:w]}"/>) if layout[:w]
616
+ ml << %(<c:h val="#{layout[:h]}"/>) if layout[:h]
617
+ ml.empty? ? "" : "<c:layout><c:manualLayout>#{ml}</c:manualLayout></c:layout>"
618
+ end
619
+
620
+ # : (untyped tag, untyped spec) -> ("" | untyped)
621
+ def gridlines_xml(tag, spec)
622
+ return "" unless spec
623
+
624
+ if spec.is_a?(Hash)
625
+ sp_children = +""
626
+ if spec[:line_color] || spec[:line_width] || spec[:line_dash]
627
+ lw = spec[:line_width] ? %( w="#{(spec[:line_width] * 12_700).to_i}") : ""
628
+ lf = spec[:line_color] ? %(<a:solidFill>#{color_xml(spec[:line_color])}</a:solidFill>) : ""
629
+ ld = spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(spec[:line_dash])}"/>) : ""
630
+ sp_children << "<a:ln#{lw}>#{lf}#{ld}</a:ln>"
631
+ end
632
+ sp_children.empty? ? "<c:#{tag}/>" : "<c:#{tag}><c:spPr>#{sp_children}</c:spPr></c:#{tag}>"
633
+ else
634
+ "<c:#{tag}/>"
635
+ end
636
+ end
637
+
638
+ # Resolves a cell reference like "Sheet1!$A$1:$A$5" to an array of values from @sheets.
639
+ # : (untyped ref) -> (nil | untyped)
640
+ def resolve_sheet_ref(ref)
641
+ return nil unless ref
642
+
643
+ m = if ref.start_with?("'")
644
+ ref.match(/\A'([^']+)'!(.+)\z/)
645
+
646
+ else
647
+ ref.match(/\A([^!]+)!(.+)\z/)
648
+
649
+ end
650
+ return nil unless m
651
+
652
+ sheet_name = m[1]
653
+ range_part = m[2]
654
+ return nil unless @sheets.key?(sheet_name)
655
+
656
+ range_part = range_part.delete("$")
657
+ cells = if range_part.include?(":")
658
+ start_cell, end_cell = range_part.split(":", 2)
659
+ enumerate_cell_range(start_cell, end_cell)
660
+ else
661
+ [range_part]
662
+ end
663
+ cells.map { |addr| @sheets[sheet_name][addr] }
664
+ end
665
+
666
+ # : (untyped start_cell, untyped end_cell) -> (::Array[untyped] | untyped)
667
+ def enumerate_cell_range(start_cell, end_cell)
668
+ sc = start_cell.match(/\A([A-Z]+)(\d+)\z/)
669
+ ec = end_cell.match(/\A([A-Z]+)(\d+)\z/)
670
+ return [start_cell] unless sc && ec
671
+
672
+ start_col = column_letter_to_index(sc[1])
673
+ start_row = sc[2].to_i
674
+ end_col = column_letter_to_index(ec[1])
675
+ end_row = ec[2].to_i
676
+ cells = []
677
+ (start_row..end_row).each do |row|
678
+ (start_col..end_col).each do |col|
679
+ cells << "#{index_to_column_letter(col)}#{row}"
680
+ end
681
+ end
682
+ cells
683
+ end
684
+
685
+ # : (untyped ref) -> ("" | ::String)
686
+ def num_cache_xml(ref)
687
+ values = resolve_sheet_ref(ref)
688
+ return "" unless values
689
+
690
+ pts = values.each_with_index.filter_map do |v, i|
691
+ next unless v
692
+
693
+ %(<c:pt idx="#{i}"><c:v>#{xml_escape(v.to_s)}</c:v></c:pt>)
694
+ end
695
+ %(<c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="#{values.size}"/>#{pts.join}</c:numCache>)
696
+ end
697
+
698
+ # : (untyped ref) -> ("" | ::String)
699
+ def str_cache_xml(ref)
700
+ values = resolve_sheet_ref(ref)
701
+ return "" unless values
702
+
703
+ pts = values.each_with_index.filter_map do |v, i|
704
+ next unless v
705
+
706
+ %(<c:pt idx="#{i}"><c:v>#{xml_escape(v.to_s)}</c:v></c:pt>)
707
+ end
708
+ %(<c:strCache><c:ptCount val="#{values.size}"/>#{pts.join}</c:strCache>)
709
+ end
710
+
711
+ # : (untyped color, ?alpha: untyped?, ?transforms: untyped?) -> untyped
712
+ def color_xml(color, alpha: nil, transforms: nil)
713
+ if color.is_a?(Hash) && color[:scheme]
714
+ tag = "schemeClr"
715
+ val = xml_escape(color[:scheme])
716
+ transforms = color[:transforms] || transforms
717
+ else
718
+ tag = "srgbClr"
719
+ val = xml_escape(normalize_drawing_rgb(color))
720
+ end
721
+ children = +""
722
+ children << %(<a:alpha val="#{alpha}"/>) if alpha
723
+ (transforms || []).each do |t|
724
+ children << %(<a:#{xml_escape(t[:type])} val="#{t[:val]}"/>)
725
+ end
726
+ if children.empty?
727
+ %(<a:#{tag} val="#{val}"/>)
728
+ else
729
+ %(<a:#{tag} val="#{val}">#{children}</a:#{tag}>)
730
+ end
731
+ end
732
+
733
+ # : (untyped color) -> untyped
734
+ def normalize_drawing_rgb(color)
735
+ str = color.to_s
736
+ hex = str.delete_prefix("#")
737
+
738
+ # Common shorthand (e.g. #f00 -> FF0000)
739
+ return hex.chars.map { |c| c * 2 }.join.upcase if hex.match?(/\A[0-9A-Fa-f]{3}\z/)
740
+
741
+ # ARGB -> RGB for DrawingML srgbClr (expects RRGGBB)
742
+ return hex[-6, 6].upcase if hex.match?(/\A[0-9A-Fa-f]{8}\z/)
743
+
744
+ return hex.upcase if hex.match?(/\A[0-9A-Fa-f]{6}\z/)
745
+
746
+ str
747
+ end
748
+
749
+ alias srgb_clr_xml color_xml
750
+
751
+ # : (untyped rotation, untyped font) -> ("" | ::String)
752
+ def build_axis_txpr(rotation, font)
753
+ return "" unless rotation || font
754
+
755
+ body_attrs = rotation ? %( rot="#{rotation}") : ""
756
+ rpr_attrs = +""
757
+ rpr_children = +""
758
+ if font
759
+ rpr_attrs << %( sz="#{(font[:size] * 100).to_i}") if font[:size]
760
+ rpr_attrs << %( b="1") if font[:bold]
761
+ rpr_attrs << %( i="1") if font[:italic]
762
+ rpr_children << %(<a:solidFill>#{color_xml(font[:color])}</a:solidFill>) if font[:color]
763
+ rpr_children << %(<a:latin typeface="#{xml_escape(font[:name])}"/>) if font[:name]
764
+ end
765
+ def_rpr = if rpr_children.empty?
766
+ "<a:defRPr#{rpr_attrs}/>"
767
+ else
768
+ "<a:defRPr#{rpr_attrs}>#{rpr_children}</a:defRPr>"
769
+ end
770
+ %(<c:txPr><a:bodyPr#{body_attrs}/><a:lstStyle/><a:p><a:pPr>#{def_rpr}</a:pPr><a:endParaRPr/></a:p></c:txPr>)
771
+ end
772
+
773
+ # : (untyped chart) -> untyped
774
+ def generate_chart_xml(chart)
775
+ chart_type = CHART_TYPE_MAP[chart[:type]] || "barChart"
776
+ no_axes = NO_AXIS_CHARTS.include?(chart_type)
777
+ parts = [
778
+ XML_HEADER,
779
+ %(<c:chartSpace xmlns:c="#{C_NS}" xmlns:a="#{A_NS}" xmlns:r="#{DOC_REL_NS}">)
780
+ ]
781
+ rc = chart[:rounded_corners]
782
+ parts << %(<c:roundedCorners val="#{rc ? 1 : 0}"/>) unless rc.nil?
783
+ parts << %(<c:style val="#{chart[:style]}"/>) if chart[:style]
784
+ if (prot = chart[:protection])
785
+ parts << "<c:protection>"
786
+ parts << %(<c:chartObject val="#{prot[:chart_object] ? 1 : 0}"/>) unless prot[:chart_object].nil?
787
+ parts << %(<c:data val="#{prot[:data] ? 1 : 0}"/>) unless prot[:data].nil?
788
+ parts << %(<c:formatting val="#{prot[:formatting] ? 1 : 0}"/>) unless prot[:formatting].nil?
789
+ parts << %(<c:selection val="#{prot[:selection] ? 1 : 0}"/>) unless prot[:selection].nil?
790
+ parts << %(<c:userInterface val="#{prot[:user_interface] ? 1 : 0}"/>) unless prot[:user_interface].nil?
791
+ parts << "</c:protection>"
792
+ end
793
+ parts << "<c:chart>"
794
+
795
+ if chart[:title]
796
+ title_spec = chart[:title]
797
+ if chart[:title_font] || chart[:title_fill_color] || chart[:title_no_fill] ||
798
+ chart[:title_line_color] || chart[:title_line_width] || chart[:title_line_dash] ||
799
+ chart[:title_rotation]
800
+ title_spec = { text: title_spec } unless title_spec.is_a?(Hash)
801
+ title_spec[:font] ||= chart[:title_font] if chart[:title_font]
802
+ title_spec[:fill_color] ||= chart[:title_fill_color] if chart[:title_fill_color]
803
+ title_spec[:no_fill] = chart[:title_no_fill] if title_spec[:no_fill].nil? && !chart[:title_no_fill].nil?
804
+ title_spec[:line_color] ||= chart[:title_line_color] if chart[:title_line_color]
805
+ title_spec[:line_width] ||= chart[:title_line_width] if chart[:title_line_width]
806
+ title_spec[:line_dash] ||= chart[:title_line_dash] if chart[:title_line_dash]
807
+ title_spec[:rotation] ||= chart[:title_rotation] if chart[:title_rotation]
808
+ end
809
+ parts << build_chart_title_xml(title_spec, overlay: chart[:title_overlay])
810
+ end
811
+ atd = chart[:auto_title_deleted]
812
+ parts << %(<c:autoTitleDeleted val="#{atd ? 1 : 0}"/>) unless atd.nil?
813
+
814
+ if (v3d = chart[:view_3d])
815
+ parts << "<c:view3D>"
816
+ parts << %(<c:rotX val="#{v3d[:rot_x]}"/>) if v3d[:rot_x]
817
+ parts << %(<c:hPercent val="#{v3d[:h_percent]}"/>) if v3d[:h_percent]
818
+ parts << %(<c:rotY val="#{v3d[:rot_y]}"/>) if v3d[:rot_y]
819
+ parts << %(<c:depthPercent val="#{v3d[:depth_percent]}"/>) if v3d[:depth_percent]
820
+ r_ang = v3d[:r_ang_ax]
821
+ parts << %(<c:rAngAx val="#{r_ang ? 1 : 0}"/>) unless r_ang.nil?
822
+ parts << %(<c:perspective val="#{v3d[:perspective]}"/>) if v3d[:perspective]
823
+ parts << "</c:view3D>"
824
+ end
825
+
826
+ %i[floor side_wall back_wall].each do |wall_key|
827
+ next unless (wall = chart[wall_key])
828
+
829
+ tag = { floor: "floor", side_wall: "sideWall", back_wall: "backWall" }[wall_key]
830
+ parts << "<c:#{tag}><c:spPr>"
831
+ parts << "<a:solidFill>#{srgb_clr_xml(wall[:fill_color])}</a:solidFill>" if wall[:fill_color]
832
+ parts << "<a:noFill/>" if wall[:no_fill]
833
+ if wall[:line_color] || wall[:line_dash]
834
+ w_attr = wall[:line_width] ? %( w="#{wall[:line_width]}") : ""
835
+ w_fill = wall[:line_color] ? %(<a:solidFill>#{srgb_clr_xml(wall[:line_color])}</a:solidFill>) : ""
836
+ w_dash = wall[:line_dash] ? %(<a:prstDash val="#{xml_escape(wall[:line_dash])}"/>) : ""
837
+ parts << "<a:ln#{w_attr}>#{w_fill}#{w_dash}</a:ln>"
838
+ end
839
+ parts << "</c:spPr></c:#{tag}>"
840
+ end
841
+
842
+ parts << "<c:plotArea>"
843
+ pa_layout = chart[:plot_area_layout]
844
+ if pa_layout.is_a?(Hash)
845
+ ml_parts = +""
846
+ ml_parts << %(<c:layoutTarget val="#{xml_escape(pa_layout[:target])}"/>) if pa_layout[:target]
847
+ ml_parts << %(<c:xMode val="#{xml_escape(pa_layout[:x_mode])}"/>) if pa_layout[:x_mode]
848
+ ml_parts << %(<c:yMode val="#{xml_escape(pa_layout[:y_mode])}"/>) if pa_layout[:y_mode]
849
+ ml_parts << %(<c:wMode val="#{xml_escape(pa_layout[:w_mode])}"/>) if pa_layout[:w_mode]
850
+ ml_parts << %(<c:hMode val="#{xml_escape(pa_layout[:h_mode])}"/>) if pa_layout[:h_mode]
851
+ ml_parts << %(<c:x val="#{pa_layout[:x]}"/>) if pa_layout[:x]
852
+ ml_parts << %(<c:y val="#{pa_layout[:y]}"/>) if pa_layout[:y]
853
+ ml_parts << %(<c:w val="#{pa_layout[:w]}"/>) if pa_layout[:w]
854
+ ml_parts << %(<c:h val="#{pa_layout[:h]}"/>) if pa_layout[:h]
855
+ parts << if ml_parts.empty?
856
+ "<c:layout/>"
857
+ else
858
+ "<c:layout><c:manualLayout>#{ml_parts}</c:manualLayout></c:layout>"
859
+ end
860
+ else
861
+ parts << "<c:layout/>"
862
+ end
863
+ parts << "<c:#{chart_type}>"
864
+ parts << %(<c:ofPieType val="#{xml_escape(chart[:of_pie_type] || "pie")}"/>) if chart_type == "ofPieChart"
865
+ if %w[barChart bar3DChart].include?(chart_type)
866
+ bd = chart[:bar_dir] || "col"
867
+ gr = chart[:grouping] || "clustered"
868
+ parts << %(<c:barDir val="#{bd}"/><c:grouping val="#{gr}"/>)
869
+ elsif GROUPING_CHARTS.include?(chart_type)
870
+ parts << %(<c:grouping val="#{chart[:grouping] || "standard"}"/>)
871
+ end
872
+ parts << %(<c:scatterStyle val="#{chart[:scatter_style] || "lineMarker"}"/>) if chart_type == "scatterChart"
873
+ parts << %(<c:radarStyle val="#{chart[:radar_style] || "standard"}"/>) if chart_type == "radarChart"
874
+ vc = chart[:vary_colors]
875
+ parts << %(<c:varyColors val="#{vc ? 1 : 0}"/>) unless vc.nil?
876
+ wf = chart[:wireframe]
877
+ parts << %(<c:wireframe val="#{wf ? 1 : 0}"/>) unless wf.nil?
878
+
879
+ all_series = chart[:series] || []
880
+ all_series.each_with_index do |ser, idx|
881
+ ser_order = ser[:order] || idx
882
+ parts << "<c:ser><c:idx val=\"#{idx}\"/><c:order val=\"#{ser_order}\"/>"
883
+ parts << "<c:tx><c:strRef><c:f>#{xml_escape(ser[:name])}</c:f>#{str_cache_xml(ser[:name])}</c:strRef></c:tx>" if ser[:name]
884
+ iin = ser[:invert_if_negative]
885
+ parts << %(<c:invertIfNegative val="#{iin ? 1 : 0}"/>) unless iin.nil?
886
+ parts << %(<c:explosion val="#{ser[:explosion]}"/>) if ser[:explosion]
887
+ ser[:data_points]&.each do |dp|
888
+ parts << "<c:dPt><c:idx val=\"#{dp[:idx]}\"/>"
889
+ dp_iin = dp[:invert_if_negative]
890
+ parts << %(<c:invertIfNegative val="#{dp_iin ? 1 : 0}"/>) unless dp_iin.nil?
891
+ if dp[:marker_symbol] || dp[:marker_size] || dp[:marker_fill] || dp[:marker_no_fill] || dp[:marker_line_color] || dp[:marker_line_dash] || dp[:marker_no_line]
892
+ parts << "<c:marker>"
893
+ parts << %(<c:symbol val="#{xml_escape(dp[:marker_symbol])}"/>) if dp[:marker_symbol]
894
+ parts << %(<c:size val="#{dp[:marker_size]}"/>) if dp[:marker_size]
895
+ if dp[:marker_fill] || dp[:marker_no_fill] || dp[:marker_line_color] || dp[:marker_line_dash] || dp[:marker_no_line]
896
+ mk_sp = +""
897
+ mk_sp << "<a:noFill/>" if dp[:marker_no_fill]
898
+ mk_sp << %(<a:solidFill>#{color_xml(dp[:marker_fill])}</a:solidFill>) if dp[:marker_fill]
899
+ if dp[:marker_no_line]
900
+ mk_sp << "<a:ln><a:noFill/></a:ln>"
901
+ elsif dp[:marker_line_color] || dp[:marker_line_dash]
902
+ mk_ln_w = dp[:marker_line_width] ? %( w="#{(dp[:marker_line_width] * 12_700).to_i}") : ""
903
+ mk_ln_f = dp[:marker_line_color] ? %(<a:solidFill>#{color_xml(dp[:marker_line_color])}</a:solidFill>) : ""
904
+ mk_ln_d = dp[:marker_line_dash] ? %(<a:prstDash val="#{xml_escape(dp[:marker_line_dash])}"/>) : ""
905
+ mk_sp << "<a:ln#{mk_ln_w}>#{mk_ln_f}#{mk_ln_d}</a:ln>"
906
+ end
907
+ parts << "<c:spPr>#{mk_sp}</c:spPr>"
908
+ end
909
+ parts << "</c:marker>"
910
+ end
911
+ dp_b3d = dp[:bubble_3d]
912
+ parts << %(<c:bubble3D val="#{dp_b3d ? 1 : 0}"/>) unless dp_b3d.nil?
913
+ parts << %(<c:explosion val="#{dp[:explosion]}"/>) if dp[:explosion]
914
+ dp_sp_children = +""
915
+ dp_sp_children << %(<a:solidFill>#{color_xml(dp[:fill_color])}</a:solidFill>) if dp[:fill_color]
916
+ dp_sp_children << "<a:noFill/>" if dp[:no_fill]
917
+ if dp[:no_line]
918
+ dp_sp_children << "<a:ln><a:noFill/></a:ln>"
919
+ elsif dp[:line_color] || dp[:line_width] || dp[:line_dash]
920
+ dp_ln_attrs = dp[:line_width] ? %( w="#{(dp[:line_width] * 12_700).to_i}") : ""
921
+ dp_ln_fill = dp[:line_color] ? %(<a:solidFill>#{color_xml(dp[:line_color])}</a:solidFill>) : ""
922
+ dp_ln_dash = dp[:line_dash] ? %(<a:prstDash val="#{xml_escape(dp[:line_dash])}"/>) : ""
923
+ dp_sp_children << "<a:ln#{dp_ln_attrs}>#{dp_ln_fill}#{dp_ln_dash}</a:ln>"
924
+ end
925
+ parts << "<c:spPr>#{dp_sp_children}</c:spPr>" unless dp_sp_children.empty?
926
+ parts << "</c:dPt>"
927
+ end
928
+ if ser[:fill_color] || ser[:no_fill] || ser[:line_color] || ser[:no_line] || ser[:line_width] || ser[:line_cap] || ser[:line_join] || ser[:line_dash]
929
+ parts << "<c:spPr>"
930
+ parts << %(<a:solidFill>#{color_xml(ser[:fill_color])}</a:solidFill>) if ser[:fill_color]
931
+ parts << "<a:noFill/>" if ser[:no_fill]
932
+ if ser[:line_color] || ser[:no_line] || ser[:line_width] || ser[:line_cap] || ser[:line_join] || ser[:line_dash]
933
+ lw = ser[:line_width] ? %( w="#{(ser[:line_width] * 12_700).to_i}") : ""
934
+ lc = ser[:line_cap] ? %( cap="#{xml_escape(ser[:line_cap])}") : ""
935
+ parts << "<a:ln#{lw}#{lc}>"
936
+ parts << "<a:noFill/>" if ser[:no_line]
937
+ parts << %(<a:solidFill>#{color_xml(ser[:line_color])}</a:solidFill>) if ser[:line_color]
938
+ parts << %(<a:prstDash val="#{xml_escape(ser[:line_dash])}"/>) if ser[:line_dash]
939
+ case ser[:line_join]
940
+ when "round" then parts << "<a:round/>"
941
+ when "bevel" then parts << "<a:bevel/>"
942
+ when "miter"
943
+ parts << (ser[:line_miter_limit] ? "<a:miter lim=\"#{ser[:line_miter_limit].to_i}\"/>" : "<a:miter/>")
944
+ end
945
+ parts << "</a:ln>"
946
+ end
947
+ parts << "</c:spPr>"
948
+ end
949
+ if ser[:marker_symbol] || ser[:marker_size] || ser[:marker_fill] || ser[:marker_no_fill] || ser[:marker_line_color] || ser[:marker_line_dash] || ser[:marker_no_line]
950
+ parts << "<c:marker>"
951
+ parts << %(<c:symbol val="#{xml_escape(ser[:marker_symbol])}"/>) if ser[:marker_symbol]
952
+ parts << %(<c:size val="#{ser[:marker_size]}"/>) if ser[:marker_size]
953
+ if ser[:marker_fill] || ser[:marker_no_fill] || ser[:marker_line_color] || ser[:marker_line_dash] || ser[:marker_no_line]
954
+ parts << "<c:spPr>"
955
+ parts << "<a:noFill/>" if ser[:marker_no_fill]
956
+ parts << %(<a:solidFill>#{color_xml(ser[:marker_fill])}</a:solidFill>) if ser[:marker_fill]
957
+ if ser[:marker_no_line]
958
+ parts << "<a:ln><a:noFill/></a:ln>"
959
+ elsif ser[:marker_line_color] || ser[:marker_line_dash]
960
+ mk_ln_w = ser[:marker_line_width] ? %( w="#{(ser[:marker_line_width] * 12_700).to_i}") : ""
961
+ mk_ln_f = ser[:marker_line_color] ? %(<a:solidFill>#{color_xml(ser[:marker_line_color])}</a:solidFill>) : ""
962
+ mk_ln_d = ser[:marker_line_dash] ? %(<a:prstDash val="#{xml_escape(ser[:marker_line_dash])}"/>) : ""
963
+ parts << "<a:ln#{mk_ln_w}>#{mk_ln_f}#{mk_ln_d}</a:ln>"
964
+ end
965
+ parts << "</c:spPr>"
966
+ end
967
+ parts << "</c:marker>"
968
+ end
969
+ if (dl = ser[:data_labels] || chart[:data_labels])
970
+ parts << "<c:dLbls>"
971
+ dl[:labels]&.each do |lbl|
972
+ parts << "<c:dLbl>"
973
+ parts << %(<c:idx val="#{lbl[:idx]}"/>)
974
+ if lbl[:delete]
975
+ parts << '<c:delete val="1"/>'
976
+ else
977
+ if (lbl_layout = lbl[:layout])
978
+ ml = +""
979
+ ml << %(<c:x val="#{lbl_layout[:x]}"/>) if lbl_layout[:x]
980
+ ml << %(<c:y val="#{lbl_layout[:y]}"/>) if lbl_layout[:y]
981
+ parts << (ml.empty? ? "<c:layout/>" : "<c:layout><c:manualLayout>#{ml}</c:manualLayout></c:layout>")
982
+ end
983
+ parts << "<c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>#{xml_escape(lbl[:text])}</a:t></a:r></a:p></c:rich></c:tx>" if lbl[:text]
984
+ if lbl[:num_fmt]
985
+ lnf = lbl[:num_fmt]
986
+ lnf_src = lnf[:source_linked] ? ' sourceLinked="1"' : ""
987
+ parts << %(<c:numFmt formatCode="#{xml_escape(lnf[:format_code])}"#{lnf_src}/>)
988
+ end
989
+ lbl_sp = +""
990
+ lbl_sp << %(<a:solidFill>#{color_xml(lbl[:fill_color])}</a:solidFill>) if lbl[:fill_color]
991
+ lbl_sp << "<a:noFill/>" if lbl[:no_fill]
992
+ if lbl[:line_color] || lbl[:line_width] || lbl[:line_dash]
993
+ lbl_lw = lbl[:line_width] ? %( w="#{(lbl[:line_width] * 12_700).to_i}") : ""
994
+ lbl_lf = lbl[:line_color] ? %(<a:solidFill>#{color_xml(lbl[:line_color])}</a:solidFill>) : ""
995
+ lbl_ld = lbl[:line_dash] ? %(<a:prstDash val="#{xml_escape(lbl[:line_dash])}"/>) : ""
996
+ lbl_sp << "<a:ln#{lbl_lw}>#{lbl_lf}#{lbl_ld}</a:ln>"
997
+ end
998
+ parts << "<c:spPr>#{lbl_sp}</c:spPr>" unless lbl_sp.empty?
999
+ parts << build_axis_txpr(nil, lbl[:font]) if lbl[:font]
1000
+ parts << %(<c:dLblPos val="#{lbl[:position]}"/>) if lbl[:position]
1001
+ parts << "<c:showLegendKey val=\"#{lbl[:show_legend_key] ? 1 : 0}\"/>" unless lbl[:show_legend_key].nil?
1002
+ parts << "<c:showVal val=\"#{lbl[:show_val] ? 1 : 0}\"/>" unless lbl[:show_val].nil?
1003
+ parts << "<c:showCatName val=\"#{lbl[:show_cat_name] ? 1 : 0}\"/>" unless lbl[:show_cat_name].nil?
1004
+ parts << "<c:showSerName val=\"#{lbl[:show_ser_name] ? 1 : 0}\"/>" unless lbl[:show_ser_name].nil?
1005
+ parts << "<c:showPercent val=\"#{lbl[:show_percent] ? 1 : 0}\"/>" unless lbl[:show_percent].nil?
1006
+ parts << "<c:showBubbleSize val=\"#{lbl[:show_bubble_size] ? 1 : 0}\"/>" unless lbl[:show_bubble_size].nil?
1007
+ parts << "<c:separator>#{xml_escape(lbl[:separator])}</c:separator>" if lbl[:separator]
1008
+ end
1009
+ parts << "</c:dLbl>"
1010
+ end
1011
+ if dl[:num_fmt]
1012
+ nf = dl[:num_fmt]
1013
+ nf_src = nf[:source_linked] ? ' sourceLinked="1"' : ""
1014
+ parts << %(<c:numFmt formatCode="#{xml_escape(nf[:format_code])}"#{nf_src}/>)
1015
+ end
1016
+ dl_sp_children = +""
1017
+ dl_sp_children << %(<a:solidFill>#{color_xml(dl[:fill_color])}</a:solidFill>) if dl[:fill_color]
1018
+ dl_sp_children << "<a:noFill/>" if dl[:no_fill]
1019
+ if dl[:line_color] || dl[:line_width] || dl[:line_dash]
1020
+ dl_lw = dl[:line_width] ? %( w="#{(dl[:line_width] * 12_700).to_i}") : ""
1021
+ dl_lf = dl[:line_color] ? %(<a:solidFill>#{color_xml(dl[:line_color])}</a:solidFill>) : ""
1022
+ dl_ld = dl[:line_dash] ? %(<a:prstDash val="#{xml_escape(dl[:line_dash])}"/>) : ""
1023
+ dl_sp_children << "<a:ln#{dl_lw}>#{dl_lf}#{dl_ld}</a:ln>"
1024
+ end
1025
+ parts << "<c:spPr>#{dl_sp_children}</c:spPr>" unless dl_sp_children.empty?
1026
+ parts << build_axis_txpr(nil, dl[:font]) if dl[:font]
1027
+ parts << %(<c:dLblPos val="#{dl[:position]}"/>) if dl[:position]
1028
+ parts << "<c:showLegendKey val=\"#{dl[:show_legend_key] ? 1 : 0}\"/>" unless dl[:show_legend_key].nil?
1029
+ parts << "<c:showVal val=\"#{dl[:show_val] ? 1 : 0}\"/>" unless dl[:show_val].nil?
1030
+ parts << "<c:showCatName val=\"#{dl[:show_cat_name] ? 1 : 0}\"/>" unless dl[:show_cat_name].nil?
1031
+ parts << "<c:showSerName val=\"#{dl[:show_ser_name] ? 1 : 0}\"/>" unless dl[:show_ser_name].nil?
1032
+ parts << "<c:showPercent val=\"#{dl[:show_percent] ? 1 : 0}\"/>" unless dl[:show_percent].nil?
1033
+ parts << "<c:showBubbleSize val=\"#{dl[:show_bubble_size] ? 1 : 0}\"/>" unless dl[:show_bubble_size].nil?
1034
+ parts << "<c:separator>#{xml_escape(dl[:separator])}</c:separator>" if dl[:separator]
1035
+ sl = dl[:show_leader_lines]
1036
+ parts << "<c:showLeaderLines val=\"#{sl ? 1 : 0}\"/>" unless sl.nil?
1037
+ if dl[:leader_lines]
1038
+ ll_spec = dl[:leader_lines]
1039
+ if ll_spec.is_a?(Hash)
1040
+ ll_sp = +""
1041
+ if ll_spec[:line_color] || ll_spec[:line_width] || ll_spec[:line_dash]
1042
+ ll_lw = ll_spec[:line_width] ? %( w="#{(ll_spec[:line_width] * 12_700).to_i}") : ""
1043
+ ll_lf = ll_spec[:line_color] ? %(<a:solidFill>#{color_xml(ll_spec[:line_color])}</a:solidFill>) : ""
1044
+ ll_ld = ll_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(ll_spec[:line_dash])}"/>) : ""
1045
+ ll_sp << "<a:ln#{ll_lw}>#{ll_lf}#{ll_ld}</a:ln>"
1046
+ end
1047
+ parts << (ll_sp.empty? ? "<c:leaderLines/>" : "<c:leaderLines><c:spPr>#{ll_sp}</c:spPr></c:leaderLines>")
1048
+ else
1049
+ parts << "<c:leaderLines/>"
1050
+ end
1051
+ end
1052
+ parts << "</c:dLbls>"
1053
+ end
1054
+ trendline_list = ser[:trendlines] || (ser[:trendline] ? [ser[:trendline]] : [])
1055
+ trendline_list.each do |tl|
1056
+ parts << "<c:trendline>"
1057
+ parts << "<c:name>#{xml_escape(tl[:name])}</c:name>" if tl[:name]
1058
+ tl_sp_children = +""
1059
+ if tl[:line_color] || tl[:line_width] || tl[:line_dash]
1060
+ tl_ln_w = tl[:line_width] ? %( w="#{(tl[:line_width] * 12_700).to_i}") : ""
1061
+ tl_ln_f = tl[:line_color] ? %(<a:solidFill>#{color_xml(tl[:line_color])}</a:solidFill>) : ""
1062
+ tl_ln_d = tl[:line_dash] ? %(<a:prstDash val="#{xml_escape(tl[:line_dash])}"/>) : ""
1063
+ tl_sp_children << "<a:ln#{tl_ln_w}>#{tl_ln_f}#{tl_ln_d}</a:ln>"
1064
+ end
1065
+ parts << "<c:spPr>#{tl_sp_children}</c:spPr>" unless tl_sp_children.empty?
1066
+ parts << %(<c:trendlineType val="#{xml_escape(tl[:type] || "linear")}"/>)
1067
+ parts << %(<c:order val="#{tl[:order]}"/>) if tl[:order]
1068
+ parts << %(<c:period val="#{tl[:period]}"/>) if tl[:period]
1069
+ parts << %(<c:forward val="#{tl[:forward]}"/>) if tl[:forward]
1070
+ parts << %(<c:backward val="#{tl[:backward]}"/>) if tl[:backward]
1071
+ parts << %(<c:intercept val="#{tl[:intercept]}"/>) if tl[:intercept]
1072
+ parts << %(<c:dispRSqr val="#{tl[:disp_r_sqr] ? 1 : 0}"/>) unless tl[:disp_r_sqr].nil?
1073
+ parts << %(<c:dispEq val="#{tl[:disp_eq] ? 1 : 0}"/>) unless tl[:disp_eq].nil?
1074
+ if tl[:label]
1075
+ lbl = tl[:label]
1076
+ parts << "<c:trendlineLbl>"
1077
+ parts << build_title_layout_xml(lbl[:layout])
1078
+ parts << "<c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>#{xml_escape(lbl[:text])}</a:t></a:r></a:p></c:rich></c:tx>" if lbl[:text]
1079
+ if lbl[:num_fmt]
1080
+ nf = lbl[:num_fmt]
1081
+ nf_src = nf.is_a?(Hash) ? nf : { format_code: nf }
1082
+ nf_linked = if nf_src.key?(:source_linked)
1083
+ nf_src[:source_linked] ? 1 : 0
1084
+ else
1085
+ 0
1086
+ end
1087
+ parts << %(<c:numFmt formatCode="#{xml_escape(nf_src[:format_code])}" sourceLinked="#{nf_linked}"/>)
1088
+ end
1089
+ tll_sp = +""
1090
+ tll_sp << %(<a:solidFill>#{color_xml(lbl[:fill_color])}</a:solidFill>) if lbl[:fill_color]
1091
+ tll_sp << "<a:noFill/>" if lbl[:no_fill]
1092
+ if lbl[:line_color] || lbl[:line_width] || lbl[:line_dash]
1093
+ tll_lw = lbl[:line_width] ? %( w="#{(lbl[:line_width] * 12_700).to_i}") : ""
1094
+ tll_lf = lbl[:line_color] ? %(<a:solidFill>#{color_xml(lbl[:line_color])}</a:solidFill>) : ""
1095
+ tll_ld = lbl[:line_dash] ? %(<a:prstDash val="#{xml_escape(lbl[:line_dash])}"/>) : ""
1096
+ tll_sp << "<a:ln#{tll_lw}>#{tll_lf}#{tll_ld}</a:ln>"
1097
+ end
1098
+ parts << "<c:spPr>#{tll_sp}</c:spPr>" unless tll_sp.empty?
1099
+ parts << build_axis_txpr(nil, lbl[:font]) if lbl[:font]
1100
+ parts << "</c:trendlineLbl>"
1101
+ end
1102
+ parts << "</c:trendline>"
1103
+ end
1104
+ err_bars_list = ser[:error_bars_list] || (ser[:error_bars] ? [ser[:error_bars]] : [])
1105
+ err_bars_list.each do |eb|
1106
+ parts << "<c:errBars>"
1107
+ parts << %(<c:errDir val="#{xml_escape(eb[:direction])}"/>) if eb[:direction]
1108
+ parts << %(<c:errBarType val="#{xml_escape(eb[:bar_type] || "both")}"/>)
1109
+ parts << %(<c:errValType val="#{xml_escape(eb[:val_type] || "fixedVal")}"/>)
1110
+ parts << %(<c:noEndCap val="#{eb[:no_end_cap] ? 1 : 0}"/>) unless eb[:no_end_cap].nil?
1111
+ parts << "<c:plus><c:numRef><c:f>#{xml_escape(eb[:plus])}</c:f></c:numRef></c:plus>" if eb[:plus]
1112
+ parts << "<c:minus><c:numRef><c:f>#{xml_escape(eb[:minus])}</c:f></c:numRef></c:minus>" if eb[:minus]
1113
+ parts << %(<c:val val="#{eb[:val]}"/>) if eb[:val]
1114
+ eb_sp_children = +""
1115
+ eb_sp_children << %(<a:solidFill>#{color_xml(eb[:fill_color])}</a:solidFill>) if eb[:fill_color]
1116
+ eb_sp_children << "<a:noFill/>" if eb[:no_fill]
1117
+ if eb[:line_color] || eb[:line_width] || eb[:line_dash]
1118
+ eb_ln_w = eb[:line_width] ? %( w="#{(eb[:line_width] * 12_700).to_i}") : ""
1119
+ eb_ln_f = eb[:line_color] ? %(<a:solidFill>#{color_xml(eb[:line_color])}</a:solidFill>) : ""
1120
+ eb_ln_d = eb[:line_dash] ? %(<a:prstDash val="#{xml_escape(eb[:line_dash])}"/>) : ""
1121
+ eb_sp_children << "<a:ln#{eb_ln_w}>#{eb_ln_f}#{eb_ln_d}</a:ln>"
1122
+ end
1123
+ parts << "<c:spPr>#{eb_sp_children}</c:spPr>" unless eb_sp_children.empty?
1124
+ parts << "</c:errBars>"
1125
+ end
1126
+ uses_xy = %w[scatterChart bubbleChart].include?(chart_type)
1127
+ cat_tag = uses_xy ? "xVal" : "cat"
1128
+ val_tag = uses_xy ? "yVal" : "val"
1129
+ if ser[:cat_ref]
1130
+ parts << if uses_xy || ser[:cat_ref_type] == :num
1131
+ "<c:#{cat_tag}><c:numRef><c:f>#{xml_escape(ser[:cat_ref])}</c:f>#{num_cache_xml(ser[:cat_ref])}</c:numRef></c:#{cat_tag}>"
1132
+ else
1133
+ "<c:#{cat_tag}><c:strRef><c:f>#{xml_escape(ser[:cat_ref])}</c:f>#{str_cache_xml(ser[:cat_ref])}</c:strRef></c:#{cat_tag}>"
1134
+ end
1135
+ end
1136
+ parts << "<c:#{val_tag}><c:numRef><c:f>#{xml_escape(ser[:val_ref])}</c:f>#{num_cache_xml(ser[:val_ref])}</c:numRef></c:#{val_tag}>" if ser[:val_ref]
1137
+ parts << "<c:bubbleSize><c:numRef><c:f>#{xml_escape(ser[:bubble_size_ref])}</c:f>#{num_cache_xml(ser[:bubble_size_ref])}</c:numRef></c:bubbleSize>" if ser[:bubble_size_ref]
1138
+ parts << %(<c:smooth val="#{ser[:smooth] ? 1 : 0}"/>) unless ser[:smooth].nil?
1139
+ parts << %(<c:shape val="#{xml_escape(ser[:shape])}"/>) if ser[:shape]
1140
+ parts << "</c:ser>"
1141
+ end
1142
+
1143
+ if chart[:band_fmts]
1144
+ parts << "<c:bandFmts>"
1145
+ chart[:band_fmts].each do |bf|
1146
+ parts << "<c:bandFmt><c:idx val=\"#{bf[:idx]}\"/>"
1147
+ bf_sp = +""
1148
+ bf_sp << %(<a:solidFill>#{color_xml(bf[:fill_color])}</a:solidFill>) if bf[:fill_color]
1149
+ bf_sp << "<a:noFill/>" if bf[:no_fill]
1150
+ if bf[:line_color] || bf[:line_width] || bf[:line_dash]
1151
+ bf_lw = bf[:line_width] ? %( w="#{(bf[:line_width] * 12_700).to_i}") : ""
1152
+ bf_lf = bf[:line_color] ? %(<a:solidFill>#{color_xml(bf[:line_color])}</a:solidFill>) : ""
1153
+ bf_ld = bf[:line_dash] ? %(<a:prstDash val="#{xml_escape(bf[:line_dash])}"/>) : ""
1154
+ bf_sp << "<a:ln#{bf_lw}>#{bf_lf}#{bf_ld}</a:ln>"
1155
+ end
1156
+ parts << "<c:spPr>#{bf_sp}</c:spPr>" unless bf_sp.empty?
1157
+ parts << "</c:bandFmt>"
1158
+ end
1159
+ parts << "</c:bandFmts>"
1160
+ end
1161
+
1162
+ parts << %(<c:gapWidth val="#{chart[:gap_width]}"/>) if chart[:gap_width]
1163
+ parts << %(<c:splitType val="#{xml_escape(chart[:split_type])}"/>) if chart[:split_type]
1164
+ parts << %(<c:splitPos val="#{chart[:split_pos]}"/>) if chart[:split_pos]
1165
+ if chart[:cust_split]&.any?
1166
+ parts << "<c:custSplit>"
1167
+ chart[:cust_split].each { |idx| parts << %(<c:secondPiePt val="#{idx}"/>) }
1168
+ parts << "</c:custSplit>"
1169
+ end
1170
+ parts << %(<c:secondPieSize val="#{chart[:second_pie_size]}"/>) if chart[:second_pie_size]
1171
+ parts << %(<c:gapDepth val="#{chart[:gap_depth]}"/>) if chart[:gap_depth]
1172
+ parts << %(<c:overlap val="#{chart[:overlap]}"/>) if chart[:overlap]
1173
+ parts << %(<c:shape val="#{chart[:bar_shape]}"/>) if chart[:bar_shape]
1174
+ b3d = chart[:bubble_3d]
1175
+ parts << %(<c:bubble3D val="#{b3d ? 1 : 0}"/>) unless b3d.nil?
1176
+ parts << %(<c:bubbleScale val="#{chart[:bubble_scale]}"/>) if chart[:bubble_scale]
1177
+ snb = chart[:show_neg_bubbles]
1178
+ parts << %(<c:showNegBubbles val="#{snb ? 1 : 0}"/>) unless snb.nil?
1179
+ parts << %(<c:sizeRepresents val="#{chart[:size_represents]}"/>) if chart[:size_represents]
1180
+ parts << %(<c:firstSliceAng val="#{chart[:first_slice_ang]}"/>) if chart[:first_slice_ang]
1181
+ parts << %(<c:holeSize val="#{chart[:hole_size]}"/>) if chart[:hole_size]
1182
+ if chart[:ser_lines]
1183
+ sl_spec = chart[:ser_lines]
1184
+ if sl_spec.is_a?(Hash)
1185
+ sl_sp = +""
1186
+ if sl_spec[:line_color] || sl_spec[:line_width] || sl_spec[:line_dash]
1187
+ sl_lw = sl_spec[:line_width] ? %( w="#{(sl_spec[:line_width] * 12_700).to_i}") : ""
1188
+ sl_lf = sl_spec[:line_color] ? %(<a:solidFill>#{color_xml(sl_spec[:line_color])}</a:solidFill>) : ""
1189
+ sl_ld = sl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(sl_spec[:line_dash])}"/>) : ""
1190
+ sl_sp << "<a:ln#{sl_lw}>#{sl_lf}#{sl_ld}</a:ln>"
1191
+ end
1192
+ parts << (sl_sp.empty? ? "<c:serLines/>" : "<c:serLines><c:spPr>#{sl_sp}</c:spPr></c:serLines>")
1193
+ else
1194
+ parts << "<c:serLines/>"
1195
+ end
1196
+ end
1197
+ if chart[:drop_lines]
1198
+ dl_spec = chart[:drop_lines]
1199
+ if dl_spec.is_a?(Hash)
1200
+ dl_sp = +""
1201
+ if dl_spec[:line_color] || dl_spec[:line_width] || dl_spec[:line_dash]
1202
+ dl_lw = dl_spec[:line_width] ? %( w="#{(dl_spec[:line_width] * 12_700).to_i}") : ""
1203
+ dl_lf = dl_spec[:line_color] ? %(<a:solidFill>#{color_xml(dl_spec[:line_color])}</a:solidFill>) : ""
1204
+ dl_ld = dl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(dl_spec[:line_dash])}"/>) : ""
1205
+ dl_sp << "<a:ln#{dl_lw}>#{dl_lf}#{dl_ld}</a:ln>"
1206
+ end
1207
+ parts << (dl_sp.empty? ? "<c:dropLines/>" : "<c:dropLines><c:spPr>#{dl_sp}</c:spPr></c:dropLines>")
1208
+ else
1209
+ parts << "<c:dropLines/>"
1210
+ end
1211
+ end
1212
+ if chart[:hi_low_lines]
1213
+ hl_spec = chart[:hi_low_lines]
1214
+ if hl_spec.is_a?(Hash)
1215
+ hl_sp = +""
1216
+ if hl_spec[:line_color] || hl_spec[:line_width] || hl_spec[:line_dash]
1217
+ hl_lw = hl_spec[:line_width] ? %( w="#{(hl_spec[:line_width] * 12_700).to_i}") : ""
1218
+ hl_lf = hl_spec[:line_color] ? %(<a:solidFill>#{color_xml(hl_spec[:line_color])}</a:solidFill>) : ""
1219
+ hl_ld = hl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(hl_spec[:line_dash])}"/>) : ""
1220
+ hl_sp << "<a:ln#{hl_lw}>#{hl_lf}#{hl_ld}</a:ln>"
1221
+ end
1222
+ parts << (hl_sp.empty? ? "<c:hiLowLines/>" : "<c:hiLowLines><c:spPr>#{hl_sp}</c:spPr></c:hiLowLines>")
1223
+ else
1224
+ parts << "<c:hiLowLines/>"
1225
+ end
1226
+ end
1227
+ if chart[:up_down_bars]
1228
+ udb = chart[:up_down_bars]
1229
+ parts << "<c:upDownBars>"
1230
+ parts << %(<c:gapWidth val="#{udb[:gap_width]}"/>) if udb.is_a?(Hash) && udb[:gap_width]
1231
+ %i[up_bars down_bars].each do |bar_key|
1232
+ tag = bar_key == :up_bars ? "upBars" : "downBars"
1233
+ bar = udb.is_a?(Hash) ? udb[bar_key] : nil
1234
+ if bar
1235
+ bar_sp = +""
1236
+ bar_sp << %(<a:solidFill>#{color_xml(bar[:fill_color])}</a:solidFill>) if bar[:fill_color]
1237
+ bar_sp << "<a:noFill/>" if bar[:no_fill]
1238
+ if bar[:line_color] || bar[:line_width] || bar[:line_dash]
1239
+ b_lw = bar[:line_width] ? %( w="#{(bar[:line_width] * 12_700).to_i}") : ""
1240
+ b_lf = bar[:line_color] ? %(<a:solidFill>#{color_xml(bar[:line_color])}</a:solidFill>) : ""
1241
+ b_ld = bar[:line_dash] ? %(<a:prstDash val="#{xml_escape(bar[:line_dash])}"/>) : ""
1242
+ bar_sp << "<a:ln#{b_lw}>#{b_lf}#{b_ld}</a:ln>"
1243
+ end
1244
+ parts << (bar_sp.empty? ? "<c:#{tag}/>" : "<c:#{tag}><c:spPr>#{bar_sp}</c:spPr></c:#{tag}>")
1245
+ else
1246
+ parts << "<c:#{tag}/>"
1247
+ end
1248
+ end
1249
+ parts << "</c:upDownBars>"
1250
+ end
1251
+ mk = chart[:marker]
1252
+ parts << %(<c:marker val="#{mk ? 1 : 0}"/>) unless mk.nil?
1253
+ sm = chart[:smooth]
1254
+ parts << %(<c:smooth val="#{sm ? 1 : 0}"/>) unless sm.nil?
1255
+ three_axes = THREE_AXIS_CHARTS.include?(chart_type)
1256
+ unless no_axes
1257
+ parts << '<c:axId val="1"/><c:axId val="2"/>'
1258
+ parts << '<c:axId val="3"/>' if three_axes
1259
+ end
1260
+ parts << "</c:#{chart_type}>"
1261
+
1262
+ unless no_axes
1263
+ cat_del = chart[:cat_axis_delete] ? 1 : 0
1264
+ cat_orient = chart[:cat_axis_orientation] || "minMax"
1265
+ cat_ax_tag = chart[:cat_axis_type] == :date ? "dateAx" : "catAx"
1266
+ parts << %(<c:#{cat_ax_tag}><c:axId val="1"/><c:scaling>)
1267
+ parts << %(<c:logBase val="#{chart[:cat_axis_log_base]}"/>) if chart[:cat_axis_log_base]
1268
+ parts << %(<c:orientation val="#{cat_orient}"/>)
1269
+ parts << %(<c:max val="#{chart[:cat_axis_scaling_max]}"/>) if chart[:cat_axis_scaling_max]
1270
+ parts << %(<c:min val="#{chart[:cat_axis_scaling_min]}"/>) if chart[:cat_axis_scaling_min]
1271
+ parts << %(</c:scaling><c:delete val="#{cat_del}"/><c:axPos val="#{chart[:cat_axis_pos] || "b"}"/>)
1272
+ parts << gridlines_xml("majorGridlines", chart[:cat_axis_major_gridlines])
1273
+ parts << gridlines_xml("minorGridlines", chart[:cat_axis_minor_gridlines])
1274
+ if chart[:cat_axis_title]
1275
+ cat_title_spec = merge_flat_title_styling(chart[:cat_axis_title], chart, :cat_axis_title)
1276
+ parts << build_chart_title_xml(cat_title_spec)
1277
+ end
1278
+ if (cnf = chart[:cat_axis_num_fmt])
1279
+ sl = cnf[:source_linked] ? 1 : 0
1280
+ parts << %(<c:numFmt formatCode="#{xml_escape(cnf[:format_code])}" sourceLinked="#{sl}"/>)
1281
+ end
1282
+ parts << %(<c:majorTickMark val="#{chart[:cat_axis_major_tick_mark]}"/>) if chart[:cat_axis_major_tick_mark]
1283
+ parts << %(<c:minorTickMark val="#{chart[:cat_axis_minor_tick_mark]}"/>) if chart[:cat_axis_minor_tick_mark]
1284
+ parts << %(<c:tickLblPos val="#{chart[:cat_axis_tick_lbl_pos]}"/>) if chart[:cat_axis_tick_lbl_pos]
1285
+ if chart[:cat_axis_line_color] || chart[:cat_axis_fill] || chart[:cat_axis_no_fill] || chart[:cat_axis_line_dash]
1286
+ parts << "<c:spPr>"
1287
+ parts << "<a:noFill/>" if chart[:cat_axis_no_fill]
1288
+ parts << %(<a:solidFill>#{color_xml(chart[:cat_axis_fill])}</a:solidFill>) if chart[:cat_axis_fill]
1289
+ if chart[:cat_axis_line_color] || chart[:cat_axis_line_dash]
1290
+ w_attr = chart[:cat_axis_line_width] ? %( w="#{chart[:cat_axis_line_width]}") : ""
1291
+ ca_ln_f = chart[:cat_axis_line_color] ? %(<a:solidFill>#{srgb_clr_xml(chart[:cat_axis_line_color])}</a:solidFill>) : ""
1292
+ ca_ln_d = chart[:cat_axis_line_dash] ? %(<a:prstDash val="#{xml_escape(chart[:cat_axis_line_dash])}"/>) : ""
1293
+ parts << "<a:ln#{w_attr}>#{ca_ln_f}#{ca_ln_d}</a:ln>"
1294
+ end
1295
+ parts << "</c:spPr>"
1296
+ end
1297
+ parts << build_axis_txpr(chart[:cat_axis_label_rotation], chart[:cat_axis_font])
1298
+ parts << '<c:crossAx val="2"/>'
1299
+ parts << %(<c:crosses val="#{chart[:cat_axis_crosses]}"/>) if chart[:cat_axis_crosses]
1300
+ parts << %(<c:crossesAt val="#{chart[:cat_axis_crosses_at]}"/>) if !chart[:cat_axis_crosses] && chart[:cat_axis_crosses_at]
1301
+ ca_auto = chart[:cat_axis_auto]
1302
+ parts << %(<c:auto val="#{ca_auto ? 1 : 0}"/>) unless ca_auto.nil?
1303
+ parts << %(<c:lblAlgn val="#{xml_escape(chart[:cat_axis_lbl_algn])}"/>) if chart[:cat_axis_lbl_algn]
1304
+ parts << %(<c:lblOffset val="#{chart[:cat_axis_lbl_offset]}"/>) if chart[:cat_axis_lbl_offset]
1305
+ parts << %(<c:tickLblSkip val="#{chart[:cat_axis_tick_lbl_skip]}"/>) if chart[:cat_axis_tick_lbl_skip]
1306
+ parts << %(<c:tickMarkSkip val="#{chart[:cat_axis_tick_mark_skip]}"/>) if chart[:cat_axis_tick_mark_skip]
1307
+ nml = chart[:cat_axis_no_multi_lvl_lbl]
1308
+ parts << %(<c:noMultiLvlLbl val="#{nml ? 1 : 0}"/>) unless nml.nil?
1309
+ if cat_ax_tag == "dateAx"
1310
+ parts << %(<c:baseTimeUnit val="#{chart[:cat_axis_base_time_unit]}"/>) if chart[:cat_axis_base_time_unit]
1311
+ parts << %(<c:majorUnit val="#{chart[:cat_axis_major_unit]}"/>) if chart[:cat_axis_major_unit]
1312
+ parts << %(<c:majorTimeUnit val="#{chart[:cat_axis_major_time_unit]}"/>) if chart[:cat_axis_major_time_unit]
1313
+ parts << %(<c:minorUnit val="#{chart[:cat_axis_minor_unit]}"/>) if chart[:cat_axis_minor_unit]
1314
+ parts << %(<c:minorTimeUnit val="#{chart[:cat_axis_minor_time_unit]}"/>) if chart[:cat_axis_minor_time_unit]
1315
+ end
1316
+ parts << "</c:#{cat_ax_tag}>"
1317
+ val_del = chart[:val_axis_delete] ? 1 : 0
1318
+ val_orient = chart[:val_axis_orientation] || "minMax"
1319
+ parts << %(<c:valAx><c:axId val="2"/><c:scaling>)
1320
+ parts << %(<c:logBase val="#{chart[:val_axis_log_base]}"/>) if chart[:val_axis_log_base]
1321
+ parts << %(<c:orientation val="#{val_orient}"/>)
1322
+ parts << %(<c:max val="#{chart[:val_axis_scaling_max]}"/>) if chart[:val_axis_scaling_max]
1323
+ parts << %(<c:min val="#{chart[:val_axis_scaling_min]}"/>) if chart[:val_axis_scaling_min]
1324
+ parts << %(</c:scaling><c:delete val="#{val_del}"/><c:axPos val="#{chart[:val_axis_pos] || "l"}"/>)
1325
+ parts << gridlines_xml("majorGridlines", chart[:val_axis_major_gridlines])
1326
+ parts << gridlines_xml("minorGridlines", chart[:val_axis_minor_gridlines])
1327
+ if chart[:val_axis_title]
1328
+ val_title_spec = merge_flat_title_styling(chart[:val_axis_title], chart, :val_axis_title)
1329
+ parts << build_chart_title_xml(val_title_spec)
1330
+ end
1331
+ if (vnf = chart[:val_axis_num_fmt])
1332
+ sl = vnf[:source_linked] ? 1 : 0
1333
+ parts << %(<c:numFmt formatCode="#{xml_escape(vnf[:format_code])}" sourceLinked="#{sl}"/>)
1334
+ end
1335
+ parts << %(<c:majorTickMark val="#{chart[:val_axis_major_tick_mark]}"/>) if chart[:val_axis_major_tick_mark]
1336
+ parts << %(<c:minorTickMark val="#{chart[:val_axis_minor_tick_mark]}"/>) if chart[:val_axis_minor_tick_mark]
1337
+ parts << %(<c:tickLblPos val="#{chart[:val_axis_tick_lbl_pos]}"/>) if chart[:val_axis_tick_lbl_pos]
1338
+ if chart[:val_axis_line_color] || chart[:val_axis_fill] || chart[:val_axis_no_fill] || chart[:val_axis_line_dash]
1339
+ parts << "<c:spPr>"
1340
+ parts << "<a:noFill/>" if chart[:val_axis_no_fill]
1341
+ parts << %(<a:solidFill>#{color_xml(chart[:val_axis_fill])}</a:solidFill>) if chart[:val_axis_fill]
1342
+ if chart[:val_axis_line_color] || chart[:val_axis_line_dash]
1343
+ w_attr = chart[:val_axis_line_width] ? %( w="#{chart[:val_axis_line_width]}") : ""
1344
+ va_ln_f = chart[:val_axis_line_color] ? %(<a:solidFill>#{srgb_clr_xml(chart[:val_axis_line_color])}</a:solidFill>) : ""
1345
+ va_ln_d = chart[:val_axis_line_dash] ? %(<a:prstDash val="#{xml_escape(chart[:val_axis_line_dash])}"/>) : ""
1346
+ parts << "<a:ln#{w_attr}>#{va_ln_f}#{va_ln_d}</a:ln>"
1347
+ end
1348
+ parts << "</c:spPr>"
1349
+ end
1350
+ parts << build_axis_txpr(chart[:val_axis_label_rotation], chart[:val_axis_font])
1351
+ parts << '<c:crossAx val="1"/>'
1352
+ parts << %(<c:crosses val="#{chart[:val_axis_crosses]}"/>) if chart[:val_axis_crosses]
1353
+ parts << %(<c:crossesAt val="#{chart[:val_axis_crosses_at]}"/>) if !chart[:val_axis_crosses] && chart[:val_axis_crosses_at]
1354
+ parts << %(<c:crossBetween val="#{chart[:val_axis_cross_between]}"/>) if chart[:val_axis_cross_between]
1355
+ parts << %(<c:majorUnit val="#{chart[:val_axis_major_unit]}"/>) if chart[:val_axis_major_unit]
1356
+ parts << %(<c:minorUnit val="#{chart[:val_axis_minor_unit]}"/>) if chart[:val_axis_minor_unit]
1357
+ if chart[:val_axis_disp_units]
1358
+ du = chart[:val_axis_disp_units]
1359
+ if du.is_a?(Hash)
1360
+ parts << "<c:dispUnits>"
1361
+ parts << %(<c:builtInUnit val="#{xml_escape(du[:built_in_unit])}"/>) if du[:built_in_unit]
1362
+ parts << %(<c:custUnit val="#{du[:cust_unit]}"/>) if du[:cust_unit]
1363
+ if du[:label]
1364
+ dul = du[:label]
1365
+ parts << "<c:dispUnitsLbl>"
1366
+ if dul[:num_fmt]
1367
+ nf = dul[:num_fmt]
1368
+ nf_src = nf.is_a?(Hash) ? nf : { format_code: nf }
1369
+ nf_linked = if nf_src.key?(:source_linked)
1370
+ nf_src[:source_linked] ? 1 : 0
1371
+ else
1372
+ 0
1373
+ end
1374
+ parts << %(<c:numFmt formatCode="#{xml_escape(nf_src[:format_code])}" sourceLinked="#{nf_linked}"/>)
1375
+ end
1376
+ dul_sp = +""
1377
+ dul_sp << %(<a:solidFill>#{color_xml(dul[:fill_color])}</a:solidFill>) if dul[:fill_color]
1378
+ dul_sp << "<a:noFill/>" if dul[:no_fill]
1379
+ if dul[:line_color] || dul[:line_width] || dul[:line_dash]
1380
+ dul_lw = dul[:line_width] ? %( w="#{(dul[:line_width] * 12_700).to_i}") : ""
1381
+ dul_lf = dul[:line_color] ? %(<a:solidFill>#{color_xml(dul[:line_color])}</a:solidFill>) : ""
1382
+ dul_ld = dul[:line_dash] ? %(<a:prstDash val="#{xml_escape(dul[:line_dash])}"/>) : ""
1383
+ dul_sp << "<a:ln#{dul_lw}>#{dul_lf}#{dul_ld}</a:ln>"
1384
+ end
1385
+ parts << "<c:spPr>#{dul_sp}</c:spPr>" unless dul_sp.empty?
1386
+ parts << build_axis_txpr(nil, dul[:font]) if dul[:font]
1387
+ parts << "</c:dispUnitsLbl>"
1388
+ end
1389
+ parts << "</c:dispUnits>"
1390
+ else
1391
+ parts << %(<c:dispUnits><c:builtInUnit val="#{xml_escape(du.to_s)}"/></c:dispUnits>)
1392
+ end
1393
+ end
1394
+ parts << "</c:valAx>"
1395
+
1396
+ if three_axes
1397
+ parts << '<c:serAx><c:axId val="3"/><c:scaling><c:orientation val="minMax"/></c:scaling>'
1398
+ parts << '<c:delete val="0"/><c:axPos val="b"/><c:crossAx val="2"/></c:serAx>'
1399
+ end
1400
+ end
1401
+
1402
+ if chart[:data_table]
1403
+ dt = chart[:data_table]
1404
+ parts << "<c:dTable>"
1405
+ parts << %(<c:showHorzBorder val="#{dt[:show_horz_border] ? 1 : 0}"/>) unless dt[:show_horz_border].nil?
1406
+ parts << %(<c:showVertBorder val="#{dt[:show_vert_border] ? 1 : 0}"/>) unless dt[:show_vert_border].nil?
1407
+ parts << %(<c:showOutline val="#{dt[:show_outline] ? 1 : 0}"/>) unless dt[:show_outline].nil?
1408
+ parts << %(<c:showKeys val="#{dt[:show_keys] ? 1 : 0}"/>) unless dt[:show_keys].nil?
1409
+ dt_sp_children = +""
1410
+ dt_sp_children << %(<a:solidFill>#{color_xml(dt[:fill_color])}</a:solidFill>) if dt[:fill_color]
1411
+ dt_sp_children << "<a:noFill/>" if dt[:no_fill]
1412
+ if dt[:line_color] || dt[:line_width] || dt[:line_dash]
1413
+ dt_ln_w = dt[:line_width] ? %( w="#{(dt[:line_width] * 12_700).to_i}") : ""
1414
+ dt_ln_f = dt[:line_color] ? %(<a:solidFill>#{color_xml(dt[:line_color])}</a:solidFill>) : ""
1415
+ dt_ln_d = dt[:line_dash] ? %(<a:prstDash val="#{xml_escape(dt[:line_dash])}"/>) : ""
1416
+ dt_sp_children << "<a:ln#{dt_ln_w}>#{dt_ln_f}#{dt_ln_d}</a:ln>"
1417
+ end
1418
+ parts << "<c:spPr>#{dt_sp_children}</c:spPr>" unless dt_sp_children.empty?
1419
+ parts << build_axis_txpr(nil, dt[:font]) if dt[:font]
1420
+ parts << "</c:dTable>"
1421
+ end
1422
+
1423
+ if chart[:plot_area_fill] || chart[:plot_area_no_fill] || chart[:plot_area_line_color] || chart[:plot_area_line_dash]
1424
+ pa_sp = +""
1425
+ pa_sp << %(<a:solidFill>#{color_xml(chart[:plot_area_fill])}</a:solidFill>) if chart[:plot_area_fill]
1426
+ pa_sp << "<a:noFill/>" if chart[:plot_area_no_fill]
1427
+ if chart[:plot_area_line_color] || chart[:plot_area_line_dash]
1428
+ pa_ln_w = chart[:plot_area_line_width] ? %( w="#{chart[:plot_area_line_width].to_i}") : ""
1429
+ pa_ln_f = chart[:plot_area_line_color] ? %(<a:solidFill>#{color_xml(chart[:plot_area_line_color])}</a:solidFill>) : ""
1430
+ pa_ln_d = chart[:plot_area_line_dash] ? %(<a:prstDash val="#{xml_escape(chart[:plot_area_line_dash])}"/>) : ""
1431
+ pa_sp << "<a:ln#{pa_ln_w}>#{pa_ln_f}#{pa_ln_d}</a:ln>"
1432
+ end
1433
+ parts << "<c:spPr>#{pa_sp}</c:spPr>"
1434
+ end
1435
+
1436
+ parts << "</c:plotArea>"
1437
+ legend_pos = chart.dig(:legend, :position) || "r"
1438
+ legend_overlay = chart.dig(:legend, :overlay)
1439
+ parts << %(<c:legend><c:legendPos val="#{legend_pos}"/>)
1440
+ legend_entries = chart.dig(:legend, :entries)
1441
+ legend_entries&.each do |entry|
1442
+ parts << %(<c:legendEntry><c:idx val="#{entry[:idx]}"/>)
1443
+ if !entry[:delete].nil?
1444
+ parts << %(<c:delete val="#{entry[:delete] ? 1 : 0}"/>)
1445
+ elsif entry[:font]
1446
+ parts << build_axis_txpr(nil, entry[:font])
1447
+ end
1448
+ parts << "</c:legendEntry>"
1449
+ end
1450
+ legend_layout = chart.dig(:legend, :layout)
1451
+ if legend_layout.is_a?(Hash)
1452
+ ml_parts = +""
1453
+ ml_parts << %(<c:layoutTarget val="#{xml_escape(legend_layout[:target])}"/>) if legend_layout[:target]
1454
+ ml_parts << %(<c:xMode val="#{xml_escape(legend_layout[:x_mode])}"/>) if legend_layout[:x_mode]
1455
+ ml_parts << %(<c:yMode val="#{xml_escape(legend_layout[:y_mode])}"/>) if legend_layout[:y_mode]
1456
+ ml_parts << %(<c:wMode val="#{xml_escape(legend_layout[:w_mode])}"/>) if legend_layout[:w_mode]
1457
+ ml_parts << %(<c:hMode val="#{xml_escape(legend_layout[:h_mode])}"/>) if legend_layout[:h_mode]
1458
+ ml_parts << %(<c:x val="#{legend_layout[:x]}"/>) if legend_layout[:x]
1459
+ ml_parts << %(<c:y val="#{legend_layout[:y]}"/>) if legend_layout[:y]
1460
+ ml_parts << %(<c:w val="#{legend_layout[:w]}"/>) if legend_layout[:w]
1461
+ ml_parts << %(<c:h val="#{legend_layout[:h]}"/>) if legend_layout[:h]
1462
+ parts << "<c:layout><c:manualLayout>#{ml_parts}</c:manualLayout></c:layout>" unless ml_parts.empty?
1463
+ end
1464
+ parts << %(<c:overlay val="#{legend_overlay ? 1 : 0}"/>) unless legend_overlay.nil?
1465
+ leg_sp_children = +""
1466
+ leg_fill = chart.dig(:legend, :fill_color)
1467
+ leg_sp_children << %(<a:solidFill>#{color_xml(leg_fill)}</a:solidFill>) if leg_fill
1468
+ leg_no_fill = chart.dig(:legend, :no_fill)
1469
+ leg_sp_children << "<a:noFill/>" if leg_no_fill
1470
+ leg_lc = chart.dig(:legend, :line_color)
1471
+ leg_lw = chart.dig(:legend, :line_width)
1472
+ leg_ld = chart.dig(:legend, :line_dash)
1473
+ if leg_lc || leg_lw || leg_ld
1474
+ lw_attr = leg_lw ? %( w="#{(leg_lw * 12_700).to_i}") : ""
1475
+ lf = leg_lc ? %(<a:solidFill>#{color_xml(leg_lc)}</a:solidFill>) : ""
1476
+ ld = leg_ld ? %(<a:prstDash val="#{xml_escape(leg_ld)}"/>) : ""
1477
+ leg_sp_children << "<a:ln#{lw_attr}>#{lf}#{ld}</a:ln>"
1478
+ end
1479
+ parts << "<c:spPr>#{leg_sp_children}</c:spPr>" unless leg_sp_children.empty?
1480
+ if (lfont = chart.dig(:legend, :font))
1481
+ parts << build_axis_txpr(nil, lfont)
1482
+ end
1483
+ parts << %(</c:legend>)
1484
+ pvo = chart[:plot_vis_only]
1485
+ parts << %(<c:plotVisOnly val="#{pvo ? 1 : 0}"/>) unless pvo.nil?
1486
+ parts << %(<c:dispBlanksAs val="#{chart[:disp_blanks_as]}"/>) if chart[:disp_blanks_as]
1487
+ sdom = chart[:show_d_lbls_over_max]
1488
+ parts << %(<c:showDLblsOverMax val="#{sdom ? 1 : 0}"/>) unless sdom.nil?
1489
+ parts << "</c:chart>"
1490
+ if chart[:chart_fill] || chart[:chart_no_fill] || chart[:chart_line_color] || chart[:chart_line_dash]
1491
+ cs_sp = +""
1492
+ cs_sp << %(<a:solidFill>#{color_xml(chart[:chart_fill])}</a:solidFill>) if chart[:chart_fill]
1493
+ cs_sp << "<a:noFill/>" if chart[:chart_no_fill]
1494
+ if chart[:chart_line_color] || chart[:chart_line_dash]
1495
+ cs_lw = chart[:chart_line_width] ? %( w="#{(chart[:chart_line_width] * 12_700).to_i}") : ""
1496
+ cs_ln_f = chart[:chart_line_color] ? %(<a:solidFill>#{color_xml(chart[:chart_line_color])}</a:solidFill>) : ""
1497
+ cs_ln_d = chart[:chart_line_dash] ? %(<a:prstDash val="#{xml_escape(chart[:chart_line_dash])}"/>) : ""
1498
+ cs_sp << "<a:ln#{cs_lw}>#{cs_ln_f}#{cs_ln_d}</a:ln>"
1499
+ end
1500
+ parts << "<c:spPr>#{cs_sp}</c:spPr>"
1501
+ end
1502
+ parts << build_axis_txpr(nil, chart[:chart_font]) if chart[:chart_font]
1503
+ if (ps = chart[:print_settings])
1504
+ parts << "<c:printSettings>"
1505
+ if (hf = ps[:header_footer])
1506
+ parts << "<c:headerFooter>"
1507
+ parts << "<c:oddHeader>#{xml_escape(hf[:odd_header])}</c:oddHeader>" if hf[:odd_header]
1508
+ parts << "<c:oddFooter>#{xml_escape(hf[:odd_footer])}</c:oddFooter>" if hf[:odd_footer]
1509
+ parts << "<c:evenHeader>#{xml_escape(hf[:even_header])}</c:evenHeader>" if hf[:even_header]
1510
+ parts << "<c:evenFooter>#{xml_escape(hf[:even_footer])}</c:evenFooter>" if hf[:even_footer]
1511
+ parts << "<c:firstHeader>#{xml_escape(hf[:first_header])}</c:firstHeader>" if hf[:first_header]
1512
+ parts << "<c:firstFooter>#{xml_escape(hf[:first_footer])}</c:firstFooter>" if hf[:first_footer]
1513
+ parts << "</c:headerFooter>"
1514
+ end
1515
+ if (pm = ps[:page_margins])
1516
+ pm_attrs = %w[b l r t header footer].filter_map { |a| pm[a.to_sym] ? %( #{a}="#{pm[a.to_sym]}") : nil }.join
1517
+ parts << "<c:pageMargins#{pm_attrs}/>"
1518
+ end
1519
+ if (psu = ps[:page_setup])
1520
+ psu_parts = +""
1521
+ psu_parts << %( paperSize="#{psu[:paper_size]}") if psu[:paper_size]
1522
+ psu_parts << %( firstPageNumber="#{psu[:first_page_number]}") if psu[:first_page_number]
1523
+ psu_parts << %( orientation="#{xml_escape(psu[:orientation])}") if psu[:orientation]
1524
+ psu_parts << %( horizontalDpi="#{psu[:horizontal_dpi]}") if psu[:horizontal_dpi]
1525
+ psu_parts << %( verticalDpi="#{psu[:vertical_dpi]}") if psu[:vertical_dpi]
1526
+ psu_parts << %( copies="#{psu[:copies]}") if psu[:copies]
1527
+ parts << "<c:pageSetup#{psu_parts}/>"
1528
+ end
1529
+ parts << "</c:printSettings>"
1530
+ end
1531
+ parts << "</c:chartSpace>"
1532
+ parts.join
1533
+ end
1534
+
1535
+ # : (untyped sheet_comments) -> untyped
1536
+ def generate_comments_xml(sheet_comments)
1537
+ authors = sheet_comments.map { |c| c[:author] }.uniq
1538
+ parts = [
1539
+ XML_HEADER,
1540
+ %(<comments xmlns="#{SSML_NS}">),
1541
+ "<authors>"
1542
+ ]
1543
+ authors.each { |a| parts << "<author>#{xml_escape(a)}</author>" }
1544
+ parts << "</authors><commentList>"
1545
+ sheet_comments.each do |c|
1546
+ aid = authors.index(c[:author]) || 0
1547
+ text_xml = if c[:text].is_a?(Xlsxrb::Elements::RichText)
1548
+ rich_text_xml(c[:text])
1549
+ else
1550
+ "<r><t>#{xml_escape(c[:text])}</t></r>"
1551
+ end
1552
+ comment_attrs = %(ref="#{c[:ref]}" authorId="#{aid}")
1553
+ comment_attrs << %( guid="#{c[:guid]}") if c[:guid]
1554
+ comment_attrs << %( shapeId="#{c[:shape_id]}") if c[:shape_id]
1555
+ parts << "<comment #{comment_attrs}><text>#{text_xml}</text></comment>"
1556
+ end
1557
+ parts << "</commentList></comments>"
1558
+ parts.join
1559
+ end
1560
+
1561
+ # : (untyped sheet_comments) -> untyped
1562
+ def generate_vml_drawing_xml(sheet_comments)
1563
+ parts = [
1564
+ '<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">',
1565
+ '<o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout>',
1566
+ '<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe">',
1567
+ '<v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/>',
1568
+ "</v:shapetype>"
1569
+ ]
1570
+ sheet_comments.each_with_index do |c, idx|
1571
+ col, row = cell_to_col_row(c[:ref])
1572
+ shape_id = 1025 + idx
1573
+ parts << %(<v:shape id="_x0000_s#{shape_id}" type="#_x0000_t202" style="position:absolute;margin-left:59.25pt;margin-top:1.5pt;width:108pt;height:59.25pt;z-index:#{idx + 1};visibility:hidden" fillcolor="#ffffe1" o:insetmode="auto">)
1574
+ parts << '<v:fill color2="#ffffe1"/>'
1575
+ parts << '<v:shadow on="t" color="black" obscured="t"/>'
1576
+ parts << '<v:path o:connecttype="none"/>'
1577
+ parts << '<v:textbox style="mso-direction-alt:auto"><div style="text-align:left"></div></v:textbox>'
1578
+ parts << '<x:ClientData ObjectType="Note">'
1579
+ parts << "<x:MoveWithCells/><x:SizeWithCells/>"
1580
+ parts << "<x:Anchor>#{col + 1}, 15, #{row}, 10, #{col + 3}, 15, #{row + 4}, 4</x:Anchor>"
1581
+ parts << "<x:AutoFill>False</x:AutoFill>"
1582
+ parts << "<x:Row>#{row}</x:Row>"
1583
+ parts << "<x:Column>#{col}</x:Column>"
1584
+ parts << "</x:ClientData></v:shape>"
1585
+ end
1586
+ parts << "</xml>"
1587
+ parts.join
1588
+ end
1589
+
1590
+ # : (untyped cell_ref) -> (::Array[0] | ::Array[untyped])
1591
+ def cell_to_col_row(cell_ref)
1592
+ m = cell_ref.match(/\A([A-Z]+)(\d+)\z/)
1593
+ return [0, 0] unless m
1594
+
1595
+ col = column_letter_to_index(m[1]) - 1
1596
+ row = m[2].to_i - 1
1597
+ [col, row]
1598
+ end
1599
+ end
1600
+ end
1601
+ end
1602
+ end