xlsxrb 0.1.16 → 0.1.17

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/README.md +2 -2
  4. data/lib/xlsxrb/chart_builder.rb +41 -2
  5. data/lib/xlsxrb/dsl_helpers.rb +522 -6
  6. data/lib/xlsxrb/elements/cell.rb +34 -9
  7. data/lib/xlsxrb/elements/column.rb +2 -2
  8. data/lib/xlsxrb/elements/types.rb +1 -0
  9. data/lib/xlsxrb/elements/workbook.rb +48 -0
  10. data/lib/xlsxrb/elements/worksheet.rb +158 -11
  11. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +22 -1
  12. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +187 -24
  13. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +108 -1
  14. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +16 -11
  15. data/lib/xlsxrb/ooxml/reader.rb +88 -0
  16. data/lib/xlsxrb/ooxml/styles_parser.rb +19 -11
  17. data/lib/xlsxrb/ooxml/workbook_parser.rb +18 -3
  18. data/lib/xlsxrb/ooxml/workbook_writer.rb +266 -46
  19. data/lib/xlsxrb/ooxml/worksheet_parser.rb +18 -4
  20. data/lib/xlsxrb/ooxml/worksheet_writer.rb +289 -88
  21. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +579 -441
  22. data/lib/xlsxrb/ooxml/writer/features_xml.rb +26 -9
  23. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +23 -9
  24. data/lib/xlsxrb/ooxml/writer.rb +268 -31
  25. data/lib/xlsxrb/ooxml/zip_writer.rb +15 -5
  26. data/lib/xlsxrb/stream_sheet.rb +157 -3
  27. data/lib/xlsxrb/stream_writer.rb +946 -74
  28. data/lib/xlsxrb/style_builder.rb +59 -6
  29. data/lib/xlsxrb/utils.rb +65 -0
  30. data/lib/xlsxrb/version.rb +1 -1
  31. data/lib/xlsxrb/workbook_builder.rb +197 -14
  32. data/lib/xlsxrb/worksheet_builder.rb +1137 -44
  33. data/lib/xlsxrb.rb +302 -47
  34. data/sig/generated/xlsxrb/chart_builder.rbs +31 -3
  35. data/sig/generated/xlsxrb/dsl_helpers.rbs +119 -4
  36. data/sig/generated/xlsxrb/elements/cell.rbs +20 -3
  37. data/sig/generated/xlsxrb/elements/column.rbs +3 -3
  38. data/sig/generated/xlsxrb/elements/types.rbs +2 -0
  39. data/sig/generated/xlsxrb/elements/workbook.rbs +37 -0
  40. data/sig/generated/xlsxrb/elements/worksheet.rbs +82 -2
  41. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +2 -0
  42. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +74 -2
  43. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +36 -0
  44. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +3 -0
  45. data/sig/generated/xlsxrb/ooxml/reader.rbs +17 -0
  46. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +3 -0
  47. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +8 -0
  48. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +8 -4
  49. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +1 -1
  50. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +12 -1
  51. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +16 -0
  52. data/sig/generated/xlsxrb/ooxml/writer.rbs +42 -8
  53. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +8 -2
  54. data/sig/generated/xlsxrb/stream_sheet.rbs +101 -1
  55. data/sig/generated/xlsxrb/stream_writer.rbs +592 -23
  56. data/sig/generated/xlsxrb/style_builder.rbs +24 -6
  57. data/sig/generated/xlsxrb/utils.rbs +20 -0
  58. data/sig/generated/xlsxrb/workbook_builder.rbs +87 -5
  59. data/sig/generated/xlsxrb/worksheet_builder.rbs +554 -10
  60. data/sig/generated/xlsxrb.rbs +77 -16
  61. metadata +1 -1
@@ -774,10 +774,121 @@ module Xlsxrb
774
774
  %(<c:txPr><a:bodyPr#{body_attrs}/><a:lstStyle/><a:p><a:pPr>#{def_rpr}</a:pPr><a:endParaRPr/></a:p></c:txPr>)
775
775
  end
776
776
 
777
+ def build_val_ax_xml(ax_id:, prefix:, chart:, default_pos: "l", default_crosses: nil)
778
+ del_key = :"#{prefix}_delete"
779
+ orient_key = :"#{prefix}_orientation"
780
+ log_key = :"#{prefix}_log_base"
781
+ max_key = :"#{prefix}_scaling_max"
782
+ min_key = :"#{prefix}_scaling_min"
783
+ pos_key = :"#{prefix}_pos"
784
+ major_grid_key = :"#{prefix}_major_gridlines"
785
+ minor_grid_key = :"#{prefix}_minor_gridlines"
786
+ title_key = :"#{prefix}_title"
787
+ num_fmt_key = :"#{prefix}_num_fmt"
788
+ major_tick_key = :"#{prefix}_major_tick_mark"
789
+ minor_tick_key = :"#{prefix}_minor_tick_mark"
790
+ tick_lbl_pos_key = :"#{prefix}_tick_lbl_pos"
791
+ line_color_key = :"#{prefix}_line_color"
792
+ fill_key = :"#{prefix}_fill"
793
+ no_fill_key = :"#{prefix}_no_fill"
794
+ line_dash_key = :"#{prefix}_line_dash"
795
+ line_width_key = :"#{prefix}_line_width"
796
+ rotation_key = :"#{prefix}_label_rotation"
797
+ font_key = :"#{prefix}_font"
798
+ crosses_key = :"#{prefix}_crosses"
799
+ crosses_at_key = :"#{prefix}_crosses_at"
800
+ cross_between_key = :"#{prefix}_cross_between"
801
+ major_unit_key = :"#{prefix}_major_unit"
802
+ minor_unit_key = :"#{prefix}_minor_unit"
803
+ disp_units_key = :"#{prefix}_disp_units"
804
+
805
+ parts = +""
806
+ val_del = chart[del_key] ? 1 : 0
807
+ val_orient = chart[orient_key] || "minMax"
808
+ parts << %(<c:valAx><c:axId val="#{ax_id}"/><c:scaling>)
809
+ parts << %(<c:logBase val="#{chart[log_key]}"/>) if chart[log_key]
810
+ parts << %(<c:orientation val="#{val_orient}"/>)
811
+ parts << %(<c:max val="#{chart[max_key]}"/>) if chart[max_key]
812
+ parts << %(<c:min val="#{chart[min_key]}"/>) if chart[min_key]
813
+ parts << %(</c:scaling><c:delete val="#{val_del}"/><c:axPos val="#{chart[pos_key] || default_pos}"/>)
814
+ parts << gridlines_xml("majorGridlines", chart[major_grid_key])
815
+ parts << gridlines_xml("minorGridlines", chart[minor_grid_key])
816
+ if chart[title_key]
817
+ val_title_spec = merge_flat_title_styling(chart[title_key], chart, title_key)
818
+ parts << build_chart_title_xml(val_title_spec)
819
+ end
820
+ if (vnf = chart[num_fmt_key])
821
+ vnf_hash = vnf.is_a?(Hash) ? vnf : { format_code: vnf }
822
+ sl = vnf_hash[:source_linked] ? 1 : 0
823
+ parts << %(<c:numFmt formatCode="#{xml_escape(vnf_hash[:format_code])}" sourceLinked="#{sl}"/>)
824
+ end
825
+ parts << %(<c:majorTickMark val="#{chart[major_tick_key]}"/>) if chart[major_tick_key]
826
+ parts << %(<c:minorTickMark val="#{chart[minor_tick_key]}"/>) if chart[minor_tick_key]
827
+ parts << %(<c:tickLblPos val="#{chart[tick_lbl_pos_key]}"/>) if chart[tick_lbl_pos_key]
828
+ if chart[line_color_key] || chart[fill_key] || chart[no_fill_key] || chart[line_dash_key]
829
+ parts << "<c:spPr>"
830
+ parts << "<a:noFill/>" if chart[no_fill_key]
831
+ parts << %(<a:solidFill>#{color_xml(chart[fill_key])}</a:solidFill>) if chart[fill_key]
832
+ if chart[line_color_key] || chart[line_dash_key]
833
+ w_attr = chart[line_width_key] ? %( w="#{chart[line_width_key]}") : ""
834
+ va_ln_f = chart[line_color_key] ? %(<a:solidFill>#{srgb_clr_xml(chart[line_color_key])}</a:solidFill>) : ""
835
+ va_ln_d = chart[line_dash_key] ? %(<a:prstDash val="#{xml_escape(chart[line_dash_key])}"/>) : ""
836
+ parts << "<a:ln#{w_attr}>#{va_ln_f}#{va_ln_d}</a:ln>"
837
+ end
838
+ parts << "</c:spPr>"
839
+ end
840
+ parts << build_axis_txpr(chart[rotation_key], chart[font_key])
841
+ parts << '<c:crossAx val="1"/>'
842
+ crosses_val = chart[crosses_key] || default_crosses
843
+ parts << %(<c:crosses val="#{crosses_val}"/>) if crosses_val
844
+ parts << %(<c:crossesAt val="#{chart[crosses_at_key]}"/>) if !crosses_val && chart[crosses_at_key]
845
+ parts << %(<c:crossBetween val="#{chart[cross_between_key]}"/>) if chart[cross_between_key]
846
+ parts << %(<c:majorUnit val="#{chart[major_unit_key]}"/>) if chart[major_unit_key]
847
+ parts << %(<c:minorUnit val="#{chart[minor_unit_key]}"/>) if chart[minor_unit_key]
848
+ if chart[disp_units_key]
849
+ du = chart[disp_units_key]
850
+ if du.is_a?(Hash)
851
+ parts << "<c:dispUnits>"
852
+ parts << %(<c:builtInUnit val="#{xml_escape(du[:built_in_unit])}"/>) if du[:built_in_unit]
853
+ parts << %(<c:custUnit val="#{du[:cust_unit]}"/>) if du[:cust_unit]
854
+ if du[:label]
855
+ dul = du[:label]
856
+ parts << "<c:dispUnitsLbl>"
857
+ if dul[:num_fmt]
858
+ nf = dul[:num_fmt]
859
+ nf_src = nf.is_a?(Hash) ? nf : { format_code: nf }
860
+ nf_linked = if nf_src.key?(:source_linked)
861
+ nf_src[:source_linked] ? 1 : 0
862
+ else
863
+ 0
864
+ end
865
+ parts << %(<c:numFmt formatCode="#{xml_escape(nf_src[:format_code])}" sourceLinked="#{nf_linked}"/>)
866
+ end
867
+ dul_sp = +""
868
+ dul_sp << %(<a:solidFill>#{color_xml(dul[:fill_color])}</a:solidFill>) if dul[:fill_color]
869
+ dul_sp << "<a:noFill/>" if dul[:no_fill]
870
+ if dul[:line_color] || dul[:line_width] || dul[:line_dash]
871
+ dul_lw = dul[:line_width] ? %( w="#{(dul[:line_width] * 12_700).to_i}") : ""
872
+ dul_lf = dul[:line_color] ? %(<a:solidFill>#{color_xml(dul[:line_color])}</a:solidFill>) : ""
873
+ dul_ld = dul[:line_dash] ? %(<a:prstDash val="#{xml_escape(dul[:line_dash])}"/>) : ""
874
+ dul_sp << "<a:ln#{dul_lw}>#{dul_lf}#{dul_ld}</a:ln>"
875
+ end
876
+ parts << "<c:spPr>#{dul_sp}</c:spPr>" unless dul_sp.empty?
877
+ parts << build_axis_txpr(nil, dul[:font]) if dul[:font]
878
+ parts << "</c:dispUnitsLbl>"
879
+ end
880
+ parts << "</c:dispUnits>"
881
+ else
882
+ parts << %(<c:dispUnits><c:builtInUnit val="#{xml_escape(du.to_s)}"/></c:dispUnits>)
883
+ end
884
+ end
885
+ parts << "</c:valAx>"
886
+ parts
887
+ end
888
+
777
889
  # : (untyped chart) -> untyped
778
890
  def generate_chart_xml(chart)
779
891
  chart_type = CHART_TYPE_MAP[chart[:type]] || "barChart"
780
- no_axes = NO_AXIS_CHARTS.include?(chart_type)
781
892
  parts = [
782
893
  XML_HEADER,
783
894
  %(<c:chartSpace xmlns:c="#{C_NS}" xmlns:a="#{A_NS}" xmlns:r="#{DOC_REL_NS}">)
@@ -864,406 +975,446 @@ module Xlsxrb
864
975
  else
865
976
  parts << "<c:layout/>"
866
977
  end
867
- parts << "<c:#{chart_type}>"
868
- parts << %(<c:ofPieType val="#{xml_escape(chart[:of_pie_type] || "pie")}"/>) if chart_type == "ofPieChart"
869
- if %w[barChart bar3DChart].include?(chart_type)
870
- bd = chart[:bar_dir] || "col"
871
- gr = chart[:grouping] || "clustered"
872
- parts << %(<c:barDir val="#{bd}"/><c:grouping val="#{gr}"/>)
873
- elsif GROUPING_CHARTS.include?(chart_type)
874
- parts << %(<c:grouping val="#{chart[:grouping] || "standard"}"/>)
978
+ all_series = chart[:series] || []
979
+ all_series.each do |s|
980
+ s[:cat_ref] ||= s[:categories] if s[:categories]
981
+ s[:val_ref] ||= s[:values] if s[:values]
875
982
  end
876
- parts << %(<c:scatterStyle val="#{chart[:scatter_style] || "lineMarker"}"/>) if chart_type == "scatterChart"
877
- parts << %(<c:radarStyle val="#{chart[:radar_style] || "standard"}"/>) if chart_type == "radarChart"
878
- vc = chart[:vary_colors]
879
- parts << %(<c:varyColors val="#{vc ? 1 : 0}"/>) unless vc.nil?
880
- wf = chart[:wireframe]
881
- parts << %(<c:wireframe val="#{wf ? 1 : 0}"/>) unless wf.nil?
882
983
 
883
- all_series = chart[:series] || []
984
+ has_secondary = !chart[:sec_val_axis_title].nil? ||
985
+ !chart[:secondary_value_axis].nil? ||
986
+ all_series.any? { |s| s[:secondary_axis] || s[:axis] == :secondary || s[:axis] == "secondary" }
987
+
988
+ groups = []
884
989
  all_series.each_with_index do |ser, idx|
885
- ser_order = ser[:order] || idx
886
- parts << "<c:ser><c:idx val=\"#{idx}\"/><c:order val=\"#{ser_order}\"/>"
887
- 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]
888
- iin = ser[:invert_if_negative]
889
- parts << %(<c:invertIfNegative val="#{iin ? 1 : 0}"/>) unless iin.nil?
890
- parts << %(<c:explosion val="#{ser[:explosion]}"/>) if ser[:explosion]
891
- ser[:data_points]&.each do |dp|
892
- parts << "<c:dPt><c:idx val=\"#{dp[:idx]}\"/>"
893
- dp_iin = dp[:invert_if_negative]
894
- parts << %(<c:invertIfNegative val="#{dp_iin ? 1 : 0}"/>) unless dp_iin.nil?
895
- 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]
896
- parts << "<c:marker>"
897
- parts << %(<c:symbol val="#{xml_escape(dp[:marker_symbol])}"/>) if dp[:marker_symbol]
898
- parts << %(<c:size val="#{dp[:marker_size]}"/>) if dp[:marker_size]
899
- if dp[:marker_fill] || dp[:marker_no_fill] || dp[:marker_line_color] || dp[:marker_line_dash] || dp[:marker_no_line]
900
- mk_sp = +""
901
- mk_sp << "<a:noFill/>" if dp[:marker_no_fill]
902
- mk_sp << %(<a:solidFill>#{color_xml(dp[:marker_fill])}</a:solidFill>) if dp[:marker_fill]
903
- if dp[:marker_no_line]
904
- mk_sp << "<a:ln><a:noFill/></a:ln>"
905
- elsif dp[:marker_line_color] || dp[:marker_line_dash]
906
- mk_ln_w = dp[:marker_line_width] ? %( w="#{(dp[:marker_line_width] * 12_700).to_i}") : ""
907
- mk_ln_f = dp[:marker_line_color] ? %(<a:solidFill>#{color_xml(dp[:marker_line_color])}</a:solidFill>) : ""
908
- mk_ln_d = dp[:marker_line_dash] ? %(<a:prstDash val="#{xml_escape(dp[:marker_line_dash])}"/>) : ""
909
- mk_sp << "<a:ln#{mk_ln_w}>#{mk_ln_f}#{mk_ln_d}</a:ln>"
990
+ s_type = if ser[:type]
991
+ CHART_TYPE_MAP[ser[:type]] || ser[:type].to_s
992
+ elsif chart_type == "comboChart"
993
+ "barChart"
994
+ else
995
+ chart_type
996
+ end
997
+ s_sec = ser[:secondary_axis] || ser[:axis] == :secondary || ser[:axis] == "secondary"
998
+ s_val_ax = s_sec ? 3 : 2
999
+
1000
+ group = groups.find { |g| g[:type] == s_type && g[:val_ax_id] == s_val_ax }
1001
+ if group
1002
+ group[:series] << [ser, idx]
1003
+ else
1004
+ groups << { type: s_type, val_ax_id: s_val_ax, series: [[ser, idx]] }
1005
+ end
1006
+ end
1007
+
1008
+ if groups.empty?
1009
+ groups << { type: (chart_type == "comboChart" ? "barChart" : chart_type), val_ax_id: 2, series: [] }
1010
+ end
1011
+
1012
+ groups.each do |grp|
1013
+ g_type = grp[:type]
1014
+ g_val_ax = grp[:val_ax_id]
1015
+ g_no_axes = NO_AXIS_CHARTS.include?(g_type)
1016
+
1017
+ parts << "<c:#{g_type}>"
1018
+ parts << %(<c:ofPieType val="#{xml_escape(chart[:of_pie_type] || "pie")}"/>) if g_type == "ofPieChart"
1019
+ if %w[barChart bar3DChart].include?(g_type)
1020
+ bd = chart[:bar_dir] || "col"
1021
+ gr = chart[:grouping] || "clustered"
1022
+ parts << %(<c:barDir val="#{bd}"/><c:grouping val="#{gr}"/>)
1023
+ elsif GROUPING_CHARTS.include?(g_type)
1024
+ parts << %(<c:grouping val="#{chart[:grouping] || "standard"}"/>)
1025
+ end
1026
+ parts << %(<c:scatterStyle val="#{chart[:scatter_style] || "lineMarker"}"/>) if g_type == "scatterChart"
1027
+ parts << %(<c:radarStyle val="#{chart[:radar_style] || "standard"}"/>) if g_type == "radarChart"
1028
+ vc = chart[:vary_colors]
1029
+ parts << %(<c:varyColors val="#{vc ? 1 : 0}"/>) unless vc.nil?
1030
+ wf = chart[:wireframe]
1031
+ parts << %(<c:wireframe val="#{wf ? 1 : 0}"/>) unless wf.nil?
1032
+
1033
+ grp[:series].each do |ser, idx|
1034
+ ser_order = ser[:order] || idx
1035
+ parts << "<c:ser><c:idx val=\"#{idx}\"/><c:order val=\"#{ser_order}\"/>"
1036
+ 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]
1037
+ iin = ser[:invert_if_negative]
1038
+ parts << %(<c:invertIfNegative val="#{iin ? 1 : 0}"/>) unless iin.nil?
1039
+ parts << %(<c:explosion val="#{ser[:explosion]}"/>) if ser[:explosion]
1040
+ ser[:data_points]&.each do |dp|
1041
+ parts << "<c:dPt><c:idx val=\"#{dp[:idx]}\"/>"
1042
+ dp_iin = dp[:invert_if_negative]
1043
+ parts << %(<c:invertIfNegative val="#{dp_iin ? 1 : 0}"/>) unless dp_iin.nil?
1044
+ 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]
1045
+ parts << "<c:marker>"
1046
+ parts << %(<c:symbol val="#{xml_escape(dp[:marker_symbol])}"/>) if dp[:marker_symbol]
1047
+ parts << %(<c:size val="#{dp[:marker_size]}"/>) if dp[:marker_size]
1048
+ if dp[:marker_fill] || dp[:marker_no_fill] || dp[:marker_line_color] || dp[:marker_line_dash] || dp[:marker_no_line]
1049
+ mk_sp = +""
1050
+ mk_sp << "<a:noFill/>" if dp[:marker_no_fill]
1051
+ mk_sp << %(<a:solidFill>#{color_xml(dp[:marker_fill])}</a:solidFill>) if dp[:marker_fill]
1052
+ if dp[:marker_no_line]
1053
+ mk_sp << "<a:ln><a:noFill/></a:ln>"
1054
+ elsif dp[:marker_line_color] || dp[:marker_line_dash]
1055
+ mk_ln_w = dp[:marker_line_width] ? %( w="#{(dp[:marker_line_width] * 12_700).to_i}") : ""
1056
+ mk_ln_f = dp[:marker_line_color] ? %(<a:solidFill>#{color_xml(dp[:marker_line_color])}</a:solidFill>) : ""
1057
+ mk_ln_d = dp[:marker_line_dash] ? %(<a:prstDash val="#{xml_escape(dp[:marker_line_dash])}"/>) : ""
1058
+ mk_sp << "<a:ln#{mk_ln_w}>#{mk_ln_f}#{mk_ln_d}</a:ln>"
1059
+ end
1060
+ parts << "<c:spPr>#{mk_sp}</c:spPr>"
910
1061
  end
911
- parts << "<c:spPr>#{mk_sp}</c:spPr>"
1062
+ parts << "</c:marker>"
912
1063
  end
913
- parts << "</c:marker>"
914
- end
915
- dp_b3d = dp[:bubble_3d]
916
- parts << %(<c:bubble3D val="#{dp_b3d ? 1 : 0}"/>) unless dp_b3d.nil?
917
- parts << %(<c:explosion val="#{dp[:explosion]}"/>) if dp[:explosion]
918
- dp_sp_children = +""
919
- dp_sp_children << %(<a:solidFill>#{color_xml(dp[:fill_color])}</a:solidFill>) if dp[:fill_color]
920
- dp_sp_children << "<a:noFill/>" if dp[:no_fill]
921
- if dp[:no_line]
922
- dp_sp_children << "<a:ln><a:noFill/></a:ln>"
923
- elsif dp[:line_color] || dp[:line_width] || dp[:line_dash]
924
- dp_ln_attrs = dp[:line_width] ? %( w="#{(dp[:line_width] * 12_700).to_i}") : ""
925
- dp_ln_fill = dp[:line_color] ? %(<a:solidFill>#{color_xml(dp[:line_color])}</a:solidFill>) : ""
926
- dp_ln_dash = dp[:line_dash] ? %(<a:prstDash val="#{xml_escape(dp[:line_dash])}"/>) : ""
927
- dp_sp_children << "<a:ln#{dp_ln_attrs}>#{dp_ln_fill}#{dp_ln_dash}</a:ln>"
928
- end
929
- parts << "<c:spPr>#{dp_sp_children}</c:spPr>" unless dp_sp_children.empty?
930
- parts << "</c:dPt>"
931
- end
932
- 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]
933
- parts << "<c:spPr>"
934
- parts << %(<a:solidFill>#{color_xml(ser[:fill_color])}</a:solidFill>) if ser[:fill_color]
935
- parts << "<a:noFill/>" if ser[:no_fill]
936
- if ser[:line_color] || ser[:no_line] || ser[:line_width] || ser[:line_cap] || ser[:line_join] || ser[:line_dash]
937
- lw = ser[:line_width] ? %( w="#{(ser[:line_width] * 12_700).to_i}") : ""
938
- lc = ser[:line_cap] ? %( cap="#{xml_escape(ser[:line_cap])}") : ""
939
- parts << "<a:ln#{lw}#{lc}>"
940
- parts << "<a:noFill/>" if ser[:no_line]
941
- parts << %(<a:solidFill>#{color_xml(ser[:line_color])}</a:solidFill>) if ser[:line_color]
942
- parts << %(<a:prstDash val="#{xml_escape(ser[:line_dash])}"/>) if ser[:line_dash]
943
- case ser[:line_join]
944
- when "round" then parts << "<a:round/>"
945
- when "bevel" then parts << "<a:bevel/>"
946
- when "miter"
947
- parts << (ser[:line_miter_limit] ? "<a:miter lim=\"#{ser[:line_miter_limit].to_i}\"/>" : "<a:miter/>")
1064
+ dp_b3d = dp[:bubble_3d]
1065
+ parts << %(<c:bubble3D val="#{dp_b3d ? 1 : 0}"/>) unless dp_b3d.nil?
1066
+ parts << %(<c:explosion val="#{dp[:explosion]}"/>) if dp[:explosion]
1067
+ dp_sp_children = +""
1068
+ dp_sp_children << %(<a:solidFill>#{color_xml(dp[:fill_color])}</a:solidFill>) if dp[:fill_color]
1069
+ dp_sp_children << "<a:noFill/>" if dp[:no_fill]
1070
+ if dp[:no_line]
1071
+ dp_sp_children << "<a:ln><a:noFill/></a:ln>"
1072
+ elsif dp[:line_color] || dp[:line_width] || dp[:line_dash]
1073
+ dp_ln_attrs = dp[:line_width] ? %( w="#{(dp[:line_width] * 12_700).to_i}") : ""
1074
+ dp_ln_fill = dp[:line_color] ? %(<a:solidFill>#{color_xml(dp[:line_color])}</a:solidFill>) : ""
1075
+ dp_ln_dash = dp[:line_dash] ? %(<a:prstDash val="#{xml_escape(dp[:line_dash])}"/>) : ""
1076
+ dp_sp_children << "<a:ln#{dp_ln_attrs}>#{dp_ln_fill}#{dp_ln_dash}</a:ln>"
948
1077
  end
949
- parts << "</a:ln>"
1078
+ parts << "<c:spPr>#{dp_sp_children}</c:spPr>" unless dp_sp_children.empty?
1079
+ parts << "</c:dPt>"
950
1080
  end
951
- parts << "</c:spPr>"
952
- end
953
- 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]
954
- parts << "<c:marker>"
955
- parts << %(<c:symbol val="#{xml_escape(ser[:marker_symbol])}"/>) if ser[:marker_symbol]
956
- parts << %(<c:size val="#{ser[:marker_size]}"/>) if ser[:marker_size]
957
- if ser[:marker_fill] || ser[:marker_no_fill] || ser[:marker_line_color] || ser[:marker_line_dash] || ser[:marker_no_line]
1081
+ 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]
958
1082
  parts << "<c:spPr>"
959
- parts << "<a:noFill/>" if ser[:marker_no_fill]
960
- parts << %(<a:solidFill>#{color_xml(ser[:marker_fill])}</a:solidFill>) if ser[:marker_fill]
961
- if ser[:marker_no_line]
962
- parts << "<a:ln><a:noFill/></a:ln>"
963
- elsif ser[:marker_line_color] || ser[:marker_line_dash]
964
- mk_ln_w = ser[:marker_line_width] ? %( w="#{(ser[:marker_line_width] * 12_700).to_i}") : ""
965
- mk_ln_f = ser[:marker_line_color] ? %(<a:solidFill>#{color_xml(ser[:marker_line_color])}</a:solidFill>) : ""
966
- mk_ln_d = ser[:marker_line_dash] ? %(<a:prstDash val="#{xml_escape(ser[:marker_line_dash])}"/>) : ""
967
- parts << "<a:ln#{mk_ln_w}>#{mk_ln_f}#{mk_ln_d}</a:ln>"
1083
+ parts << %(<a:solidFill>#{color_xml(ser[:fill_color])}</a:solidFill>) if ser[:fill_color]
1084
+ parts << "<a:noFill/>" if ser[:no_fill]
1085
+ if ser[:line_color] || ser[:no_line] || ser[:line_width] || ser[:line_cap] || ser[:line_join] || ser[:line_dash]
1086
+ lw = ser[:line_width] ? %( w="#{(ser[:line_width] * 12_700).to_i}") : ""
1087
+ lc = ser[:line_cap] ? %( cap="#{xml_escape(ser[:line_cap])}") : ""
1088
+ parts << "<a:ln#{lw}#{lc}>"
1089
+ parts << "<a:noFill/>" if ser[:no_line]
1090
+ parts << %(<a:solidFill>#{color_xml(ser[:line_color])}</a:solidFill>) if ser[:line_color]
1091
+ parts << %(<a:prstDash val="#{xml_escape(ser[:line_dash])}"/>) if ser[:line_dash]
1092
+ case ser[:line_join]
1093
+ when "round" then parts << "<a:round/>"
1094
+ when "bevel" then parts << "<a:bevel/>"
1095
+ when "miter"
1096
+ parts << (ser[:line_miter_limit] ? "<a:miter lim=\"#{ser[:line_miter_limit].to_i}\"/>" : "<a:miter/>")
1097
+ end
1098
+ parts << "</a:ln>"
968
1099
  end
969
1100
  parts << "</c:spPr>"
970
1101
  end
971
- parts << "</c:marker>"
972
- end
973
- if (dl = ser[:data_labels] || chart[:data_labels])
974
- parts << "<c:dLbls>"
975
- dl[:labels]&.each do |lbl|
976
- parts << "<c:dLbl>"
977
- parts << %(<c:idx val="#{lbl[:idx]}"/>)
978
- if lbl[:delete]
979
- parts << '<c:delete val="1"/>'
980
- else
981
- if (lbl_layout = lbl[:layout])
982
- ml = +""
983
- ml << %(<c:x val="#{lbl_layout[:x]}"/>) if lbl_layout[:x]
984
- ml << %(<c:y val="#{lbl_layout[:y]}"/>) if lbl_layout[:y]
985
- parts << (ml.empty? ? "<c:layout/>" : "<c:layout><c:manualLayout>#{ml}</c:manualLayout></c:layout>")
1102
+ 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]
1103
+ parts << "<c:marker>"
1104
+ parts << %(<c:symbol val="#{xml_escape(ser[:marker_symbol])}"/>) if ser[:marker_symbol]
1105
+ parts << %(<c:size val="#{ser[:marker_size]}"/>) if ser[:marker_size]
1106
+ if ser[:marker_fill] || ser[:marker_no_fill] || ser[:marker_line_color] || ser[:marker_line_dash] || ser[:marker_no_line]
1107
+ parts << "<c:spPr>"
1108
+ parts << "<a:noFill/>" if ser[:marker_no_fill]
1109
+ parts << %(<a:solidFill>#{color_xml(ser[:marker_fill])}</a:solidFill>) if ser[:marker_fill]
1110
+ if ser[:marker_no_line]
1111
+ parts << "<a:ln><a:noFill/></a:ln>"
1112
+ elsif ser[:marker_line_color] || ser[:marker_line_dash]
1113
+ mk_ln_w = ser[:marker_line_width] ? %( w="#{(ser[:marker_line_width] * 12_700).to_i}") : ""
1114
+ mk_ln_f = ser[:marker_line_color] ? %(<a:solidFill>#{color_xml(ser[:marker_line_color])}</a:solidFill>) : ""
1115
+ mk_ln_d = ser[:marker_line_dash] ? %(<a:prstDash val="#{xml_escape(ser[:marker_line_dash])}"/>) : ""
1116
+ parts << "<a:ln#{mk_ln_w}>#{mk_ln_f}#{mk_ln_d}</a:ln>"
1117
+ end
1118
+ parts << "</c:spPr>"
1119
+ end
1120
+ parts << "</c:marker>"
1121
+ end
1122
+ if (dl = ser[:data_labels] || chart[:data_labels])
1123
+ parts << "<c:dLbls>"
1124
+ dl[:labels]&.each do |lbl|
1125
+ parts << "<c:dLbl>"
1126
+ parts << %(<c:idx val="#{lbl[:idx]}"/>)
1127
+ if lbl[:delete]
1128
+ parts << '<c:delete val="1"/>'
1129
+ else
1130
+ if (lbl_layout = lbl[:layout])
1131
+ ml = +""
1132
+ ml << %(<c:x val="#{lbl_layout[:x]}"/>) if lbl_layout[:x]
1133
+ ml << %(<c:y val="#{lbl_layout[:y]}"/>) if lbl_layout[:y]
1134
+ parts << (ml.empty? ? "<c:layout/>" : "<c:layout><c:manualLayout>#{ml}</c:manualLayout></c:layout>")
1135
+ end
1136
+ 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]
1137
+ if lbl[:num_fmt]
1138
+ lnf = lbl[:num_fmt]
1139
+ lnf_src = lnf[:source_linked] ? ' sourceLinked="1"' : ""
1140
+ parts << %(<c:numFmt formatCode="#{xml_escape(lnf[:format_code])}"#{lnf_src}/>)
1141
+ end
1142
+ lbl_sp = +""
1143
+ lbl_sp << %(<a:solidFill>#{color_xml(lbl[:fill_color])}</a:solidFill>) if lbl[:fill_color]
1144
+ lbl_sp << "<a:noFill/>" if lbl[:no_fill]
1145
+ if lbl[:line_color] || lbl[:line_width] || lbl[:line_dash]
1146
+ lbl_lw = lbl[:line_width] ? %( w="#{(lbl[:line_width] * 12_700).to_i}") : ""
1147
+ lbl_lf = lbl[:line_color] ? %(<a:solidFill>#{color_xml(lbl[:line_color])}</a:solidFill>) : ""
1148
+ lbl_ld = lbl[:line_dash] ? %(<a:prstDash val="#{xml_escape(lbl[:line_dash])}"/>) : ""
1149
+ lbl_sp << "<a:ln#{lbl_lw}>#{lbl_lf}#{lbl_ld}</a:ln>"
1150
+ end
1151
+ parts << "<c:spPr>#{lbl_sp}</c:spPr>" unless lbl_sp.empty?
1152
+ parts << build_axis_txpr(nil, lbl[:font]) if lbl[:font]
1153
+ parts << %(<c:dLblPos val="#{lbl[:position]}"/>) if lbl[:position]
1154
+ parts << "<c:showLegendKey val=\"#{lbl[:show_legend_key] ? 1 : 0}\"/>" unless lbl[:show_legend_key].nil?
1155
+ parts << "<c:showVal val=\"#{lbl[:show_val] ? 1 : 0}\"/>" unless lbl[:show_val].nil?
1156
+ parts << "<c:showCatName val=\"#{lbl[:show_cat_name] ? 1 : 0}\"/>" unless lbl[:show_cat_name].nil?
1157
+ parts << "<c:showSerName val=\"#{lbl[:show_ser_name] ? 1 : 0}\"/>" unless lbl[:show_ser_name].nil?
1158
+ parts << "<c:showPercent val=\"#{lbl[:show_percent] ? 1 : 0}\"/>" unless lbl[:show_percent].nil?
1159
+ parts << "<c:showBubbleSize val=\"#{lbl[:show_bubble_size] ? 1 : 0}\"/>" unless lbl[:show_bubble_size].nil?
1160
+ parts << "<c:separator>#{xml_escape(lbl[:separator])}</c:separator>" if lbl[:separator]
1161
+ end
1162
+ parts << "</c:dLbl>"
1163
+ end
1164
+ if dl[:num_fmt]
1165
+ nf = dl[:num_fmt]
1166
+ nf_src = nf[:source_linked] ? ' sourceLinked="1"' : ""
1167
+ parts << %(<c:numFmt formatCode="#{xml_escape(nf[:format_code])}"#{nf_src}/>)
1168
+ end
1169
+ dl_sp_children = +""
1170
+ dl_sp_children << %(<a:solidFill>#{color_xml(dl[:fill_color])}</a:solidFill>) if dl[:fill_color]
1171
+ dl_sp_children << "<a:noFill/>" if dl[:no_fill]
1172
+ if dl[:line_color] || dl[:line_width] || dl[:line_dash]
1173
+ dl_lw = dl[:line_width] ? %( w="#{(dl[:line_width] * 12_700).to_i}") : ""
1174
+ dl_lf = dl[:line_color] ? %(<a:solidFill>#{color_xml(dl[:line_color])}</a:solidFill>) : ""
1175
+ dl_ld = dl[:line_dash] ? %(<a:prstDash val="#{xml_escape(dl[:line_dash])}"/>) : ""
1176
+ dl_sp_children << "<a:ln#{dl_lw}>#{dl_lf}#{dl_ld}</a:ln>"
1177
+ end
1178
+ parts << "<c:spPr>#{dl_sp_children}</c:spPr>" unless dl_sp_children.empty?
1179
+ parts << build_axis_txpr(nil, dl[:font]) if dl[:font]
1180
+ parts << %(<c:dLblPos val="#{dl[:position]}"/>) if dl[:position]
1181
+ parts << "<c:showLegendKey val=\"#{dl[:show_legend_key] ? 1 : 0}\"/>" unless dl[:show_legend_key].nil?
1182
+ parts << "<c:showVal val=\"#{dl[:show_val] ? 1 : 0}\"/>" unless dl[:show_val].nil?
1183
+ parts << "<c:showCatName val=\"#{dl[:show_cat_name] ? 1 : 0}\"/>" unless dl[:show_cat_name].nil?
1184
+ parts << "<c:showSerName val=\"#{dl[:show_ser_name] ? 1 : 0}\"/>" unless dl[:show_ser_name].nil?
1185
+ parts << "<c:showPercent val=\"#{dl[:show_percent] ? 1 : 0}\"/>" unless dl[:show_percent].nil?
1186
+ parts << "<c:showBubbleSize val=\"#{dl[:show_bubble_size] ? 1 : 0}\"/>" unless dl[:show_bubble_size].nil?
1187
+ parts << "<c:separator>#{xml_escape(dl[:separator])}</c:separator>" if dl[:separator]
1188
+ sl = dl[:show_leader_lines]
1189
+ parts << "<c:showLeaderLines val=\"#{sl ? 1 : 0}\"/>" unless sl.nil?
1190
+ if dl[:leader_lines]
1191
+ ll_spec = dl[:leader_lines]
1192
+ if ll_spec.is_a?(Hash)
1193
+ ll_sp = +""
1194
+ if ll_spec[:line_color] || ll_spec[:line_width] || ll_spec[:line_dash]
1195
+ ll_lw = ll_spec[:line_width] ? %( w="#{(ll_spec[:line_width] * 12_700).to_i}") : ""
1196
+ ll_lf = ll_spec[:line_color] ? %(<a:solidFill>#{color_xml(ll_spec[:line_color])}</a:solidFill>) : ""
1197
+ ll_ld = ll_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(ll_spec[:line_dash])}"/>) : ""
1198
+ ll_sp << "<a:ln#{ll_lw}>#{ll_lf}#{ll_ld}</a:ln>"
1199
+ end
1200
+ parts << (ll_sp.empty? ? "<c:leaderLines/>" : "<c:leaderLines><c:spPr>#{ll_sp}</c:spPr></c:leaderLines>")
1201
+ else
1202
+ parts << "<c:leaderLines/>"
986
1203
  end
1204
+ end
1205
+ parts << "</c:dLbls>"
1206
+ end
1207
+ trendline_list = ser[:trendlines] || (ser[:trendline] ? [ser[:trendline]] : [])
1208
+ trendline_list.each do |tl|
1209
+ parts << "<c:trendline>"
1210
+ parts << "<c:name>#{xml_escape(tl[:name])}</c:name>" if tl[:name]
1211
+ tl_sp_children = +""
1212
+ if tl[:line_color] || tl[:line_width] || tl[:line_dash]
1213
+ tl_ln_w = tl[:line_width] ? %( w="#{(tl[:line_width] * 12_700).to_i}") : ""
1214
+ tl_ln_f = tl[:line_color] ? %(<a:solidFill>#{color_xml(tl[:line_color])}</a:solidFill>) : ""
1215
+ tl_ln_d = tl[:line_dash] ? %(<a:prstDash val="#{xml_escape(tl[:line_dash])}"/>) : ""
1216
+ tl_sp_children << "<a:ln#{tl_ln_w}>#{tl_ln_f}#{tl_ln_d}</a:ln>"
1217
+ end
1218
+ parts << "<c:spPr>#{tl_sp_children}</c:spPr>" unless tl_sp_children.empty?
1219
+ parts << %(<c:trendlineType val="#{xml_escape(tl[:type] || "linear")}"/>)
1220
+ parts << %(<c:order val="#{tl[:order]}"/>) if tl[:order]
1221
+ parts << %(<c:period val="#{tl[:period]}"/>) if tl[:period]
1222
+ parts << %(<c:forward val="#{tl[:forward]}"/>) if tl[:forward]
1223
+ parts << %(<c:backward val="#{tl[:backward]}"/>) if tl[:backward]
1224
+ parts << %(<c:intercept val="#{tl[:intercept]}"/>) if tl[:intercept]
1225
+ parts << %(<c:dispRSqr val="#{tl[:disp_r_sqr] ? 1 : 0}"/>) unless tl[:disp_r_sqr].nil?
1226
+ parts << %(<c:dispEq val="#{tl[:disp_eq] ? 1 : 0}"/>) unless tl[:disp_eq].nil?
1227
+ if tl[:label]
1228
+ lbl = tl[:label]
1229
+ parts << "<c:trendlineLbl>"
1230
+ parts << build_title_layout_xml(lbl[:layout])
987
1231
  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]
988
1232
  if lbl[:num_fmt]
989
- lnf = lbl[:num_fmt]
990
- lnf_src = lnf[:source_linked] ? ' sourceLinked="1"' : ""
991
- parts << %(<c:numFmt formatCode="#{xml_escape(lnf[:format_code])}"#{lnf_src}/>)
1233
+ nf = lbl[:num_fmt]
1234
+ nf_src = nf.is_a?(Hash) ? nf : { format_code: nf }
1235
+ nf_linked = if nf_src.key?(:source_linked)
1236
+ nf_src[:source_linked] ? 1 : 0
1237
+ else
1238
+ 0
1239
+ end
1240
+ parts << %(<c:numFmt formatCode="#{xml_escape(nf_src[:format_code])}" sourceLinked="#{nf_linked}"/>)
992
1241
  end
993
- lbl_sp = +""
994
- lbl_sp << %(<a:solidFill>#{color_xml(lbl[:fill_color])}</a:solidFill>) if lbl[:fill_color]
995
- lbl_sp << "<a:noFill/>" if lbl[:no_fill]
1242
+ tll_sp = +""
1243
+ tll_sp << %(<a:solidFill>#{color_xml(lbl[:fill_color])}</a:solidFill>) if lbl[:fill_color]
1244
+ tll_sp << "<a:noFill/>" if lbl[:no_fill]
996
1245
  if lbl[:line_color] || lbl[:line_width] || lbl[:line_dash]
997
- lbl_lw = lbl[:line_width] ? %( w="#{(lbl[:line_width] * 12_700).to_i}") : ""
998
- lbl_lf = lbl[:line_color] ? %(<a:solidFill>#{color_xml(lbl[:line_color])}</a:solidFill>) : ""
999
- lbl_ld = lbl[:line_dash] ? %(<a:prstDash val="#{xml_escape(lbl[:line_dash])}"/>) : ""
1000
- lbl_sp << "<a:ln#{lbl_lw}>#{lbl_lf}#{lbl_ld}</a:ln>"
1246
+ tll_lw = lbl[:line_width] ? %( w="#{(lbl[:line_width] * 12_700).to_i}") : ""
1247
+ tll_lf = lbl[:line_color] ? %(<a:solidFill>#{color_xml(lbl[:line_color])}</a:solidFill>) : ""
1248
+ tll_ld = lbl[:line_dash] ? %(<a:prstDash val="#{xml_escape(lbl[:line_dash])}"/>) : ""
1249
+ tll_sp << "<a:ln#{tll_lw}>#{tll_lf}#{tll_ld}</a:ln>"
1001
1250
  end
1002
- parts << "<c:spPr>#{lbl_sp}</c:spPr>" unless lbl_sp.empty?
1251
+ parts << "<c:spPr>#{tll_sp}</c:spPr>" unless tll_sp.empty?
1003
1252
  parts << build_axis_txpr(nil, lbl[:font]) if lbl[:font]
1004
- parts << %(<c:dLblPos val="#{lbl[:position]}"/>) if lbl[:position]
1005
- parts << "<c:showLegendKey val=\"#{lbl[:show_legend_key] ? 1 : 0}\"/>" unless lbl[:show_legend_key].nil?
1006
- parts << "<c:showVal val=\"#{lbl[:show_val] ? 1 : 0}\"/>" unless lbl[:show_val].nil?
1007
- parts << "<c:showCatName val=\"#{lbl[:show_cat_name] ? 1 : 0}\"/>" unless lbl[:show_cat_name].nil?
1008
- parts << "<c:showSerName val=\"#{lbl[:show_ser_name] ? 1 : 0}\"/>" unless lbl[:show_ser_name].nil?
1009
- parts << "<c:showPercent val=\"#{lbl[:show_percent] ? 1 : 0}\"/>" unless lbl[:show_percent].nil?
1010
- parts << "<c:showBubbleSize val=\"#{lbl[:show_bubble_size] ? 1 : 0}\"/>" unless lbl[:show_bubble_size].nil?
1011
- parts << "<c:separator>#{xml_escape(lbl[:separator])}</c:separator>" if lbl[:separator]
1253
+ parts << "</c:trendlineLbl>"
1012
1254
  end
1013
- parts << "</c:dLbl>"
1014
- end
1015
- if dl[:num_fmt]
1016
- nf = dl[:num_fmt]
1017
- nf_src = nf[:source_linked] ? ' sourceLinked="1"' : ""
1018
- parts << %(<c:numFmt formatCode="#{xml_escape(nf[:format_code])}"#{nf_src}/>)
1255
+ parts << "</c:trendline>"
1019
1256
  end
1020
- dl_sp_children = +""
1021
- dl_sp_children << %(<a:solidFill>#{color_xml(dl[:fill_color])}</a:solidFill>) if dl[:fill_color]
1022
- dl_sp_children << "<a:noFill/>" if dl[:no_fill]
1023
- if dl[:line_color] || dl[:line_width] || dl[:line_dash]
1024
- dl_lw = dl[:line_width] ? %( w="#{(dl[:line_width] * 12_700).to_i}") : ""
1025
- dl_lf = dl[:line_color] ? %(<a:solidFill>#{color_xml(dl[:line_color])}</a:solidFill>) : ""
1026
- dl_ld = dl[:line_dash] ? %(<a:prstDash val="#{xml_escape(dl[:line_dash])}"/>) : ""
1027
- dl_sp_children << "<a:ln#{dl_lw}>#{dl_lf}#{dl_ld}</a:ln>"
1028
- end
1029
- parts << "<c:spPr>#{dl_sp_children}</c:spPr>" unless dl_sp_children.empty?
1030
- parts << build_axis_txpr(nil, dl[:font]) if dl[:font]
1031
- parts << %(<c:dLblPos val="#{dl[:position]}"/>) if dl[:position]
1032
- parts << "<c:showLegendKey val=\"#{dl[:show_legend_key] ? 1 : 0}\"/>" unless dl[:show_legend_key].nil?
1033
- parts << "<c:showVal val=\"#{dl[:show_val] ? 1 : 0}\"/>" unless dl[:show_val].nil?
1034
- parts << "<c:showCatName val=\"#{dl[:show_cat_name] ? 1 : 0}\"/>" unless dl[:show_cat_name].nil?
1035
- parts << "<c:showSerName val=\"#{dl[:show_ser_name] ? 1 : 0}\"/>" unless dl[:show_ser_name].nil?
1036
- parts << "<c:showPercent val=\"#{dl[:show_percent] ? 1 : 0}\"/>" unless dl[:show_percent].nil?
1037
- parts << "<c:showBubbleSize val=\"#{dl[:show_bubble_size] ? 1 : 0}\"/>" unless dl[:show_bubble_size].nil?
1038
- parts << "<c:separator>#{xml_escape(dl[:separator])}</c:separator>" if dl[:separator]
1039
- sl = dl[:show_leader_lines]
1040
- parts << "<c:showLeaderLines val=\"#{sl ? 1 : 0}\"/>" unless sl.nil?
1041
- if dl[:leader_lines]
1042
- ll_spec = dl[:leader_lines]
1043
- if ll_spec.is_a?(Hash)
1044
- ll_sp = +""
1045
- if ll_spec[:line_color] || ll_spec[:line_width] || ll_spec[:line_dash]
1046
- ll_lw = ll_spec[:line_width] ? %( w="#{(ll_spec[:line_width] * 12_700).to_i}") : ""
1047
- ll_lf = ll_spec[:line_color] ? %(<a:solidFill>#{color_xml(ll_spec[:line_color])}</a:solidFill>) : ""
1048
- ll_ld = ll_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(ll_spec[:line_dash])}"/>) : ""
1049
- ll_sp << "<a:ln#{ll_lw}>#{ll_lf}#{ll_ld}</a:ln>"
1050
- end
1051
- parts << (ll_sp.empty? ? "<c:leaderLines/>" : "<c:leaderLines><c:spPr>#{ll_sp}</c:spPr></c:leaderLines>")
1052
- else
1053
- parts << "<c:leaderLines/>"
1257
+ err_bars_list = ser[:error_bars_list] || (ser[:error_bars] ? [ser[:error_bars]] : [])
1258
+ err_bars_list.each do |eb|
1259
+ parts << "<c:errBars>"
1260
+ parts << %(<c:errDir val="#{xml_escape(eb[:direction])}"/>) if eb[:direction]
1261
+ parts << %(<c:errBarType val="#{xml_escape(eb[:bar_type] || "both")}"/>)
1262
+ parts << %(<c:errValType val="#{xml_escape(eb[:val_type] || "fixedVal")}"/>)
1263
+ parts << %(<c:noEndCap val="#{eb[:no_end_cap] ? 1 : 0}"/>) unless eb[:no_end_cap].nil?
1264
+ parts << "<c:plus><c:numRef><c:f>#{xml_escape(eb[:plus])}</c:f></c:numRef></c:plus>" if eb[:plus]
1265
+ parts << "<c:minus><c:numRef><c:f>#{xml_escape(eb[:minus])}</c:f></c:numRef></c:minus>" if eb[:minus]
1266
+ parts << %(<c:val val="#{eb[:val]}"/>) if eb[:val]
1267
+ eb_sp_children = +""
1268
+ eb_sp_children << %(<a:solidFill>#{color_xml(eb[:fill_color])}</a:solidFill>) if eb[:fill_color]
1269
+ eb_sp_children << "<a:noFill/>" if eb[:no_fill]
1270
+ if eb[:line_color] || eb[:line_width] || eb[:line_dash]
1271
+ eb_ln_w = eb[:line_width] ? %( w="#{(eb[:line_width] * 12_700).to_i}") : ""
1272
+ eb_ln_f = eb[:line_color] ? %(<a:solidFill>#{color_xml(eb[:line_color])}</a:solidFill>) : ""
1273
+ eb_ln_d = eb[:line_dash] ? %(<a:prstDash val="#{xml_escape(eb[:line_dash])}"/>) : ""
1274
+ eb_sp_children << "<a:ln#{eb_ln_w}>#{eb_ln_f}#{eb_ln_d}</a:ln>"
1054
1275
  end
1276
+ parts << "<c:spPr>#{eb_sp_children}</c:spPr>" unless eb_sp_children.empty?
1277
+ parts << "</c:errBars>"
1055
1278
  end
1056
- parts << "</c:dLbls>"
1057
- end
1058
- trendline_list = ser[:trendlines] || (ser[:trendline] ? [ser[:trendline]] : [])
1059
- trendline_list.each do |tl|
1060
- parts << "<c:trendline>"
1061
- parts << "<c:name>#{xml_escape(tl[:name])}</c:name>" if tl[:name]
1062
- tl_sp_children = +""
1063
- if tl[:line_color] || tl[:line_width] || tl[:line_dash]
1064
- tl_ln_w = tl[:line_width] ? %( w="#{(tl[:line_width] * 12_700).to_i}") : ""
1065
- tl_ln_f = tl[:line_color] ? %(<a:solidFill>#{color_xml(tl[:line_color])}</a:solidFill>) : ""
1066
- tl_ln_d = tl[:line_dash] ? %(<a:prstDash val="#{xml_escape(tl[:line_dash])}"/>) : ""
1067
- tl_sp_children << "<a:ln#{tl_ln_w}>#{tl_ln_f}#{tl_ln_d}</a:ln>"
1068
- end
1069
- parts << "<c:spPr>#{tl_sp_children}</c:spPr>" unless tl_sp_children.empty?
1070
- parts << %(<c:trendlineType val="#{xml_escape(tl[:type] || "linear")}"/>)
1071
- parts << %(<c:order val="#{tl[:order]}"/>) if tl[:order]
1072
- parts << %(<c:period val="#{tl[:period]}"/>) if tl[:period]
1073
- parts << %(<c:forward val="#{tl[:forward]}"/>) if tl[:forward]
1074
- parts << %(<c:backward val="#{tl[:backward]}"/>) if tl[:backward]
1075
- parts << %(<c:intercept val="#{tl[:intercept]}"/>) if tl[:intercept]
1076
- parts << %(<c:dispRSqr val="#{tl[:disp_r_sqr] ? 1 : 0}"/>) unless tl[:disp_r_sqr].nil?
1077
- parts << %(<c:dispEq val="#{tl[:disp_eq] ? 1 : 0}"/>) unless tl[:disp_eq].nil?
1078
- if tl[:label]
1079
- lbl = tl[:label]
1080
- parts << "<c:trendlineLbl>"
1081
- parts << build_title_layout_xml(lbl[:layout])
1082
- 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]
1083
- if lbl[:num_fmt]
1084
- nf = lbl[:num_fmt]
1085
- nf_src = nf.is_a?(Hash) ? nf : { format_code: nf }
1086
- nf_linked = if nf_src.key?(:source_linked)
1087
- nf_src[:source_linked] ? 1 : 0
1088
- else
1089
- 0
1090
- end
1091
- parts << %(<c:numFmt formatCode="#{xml_escape(nf_src[:format_code])}" sourceLinked="#{nf_linked}"/>)
1092
- end
1093
- tll_sp = +""
1094
- tll_sp << %(<a:solidFill>#{color_xml(lbl[:fill_color])}</a:solidFill>) if lbl[:fill_color]
1095
- tll_sp << "<a:noFill/>" if lbl[:no_fill]
1096
- if lbl[:line_color] || lbl[:line_width] || lbl[:line_dash]
1097
- tll_lw = lbl[:line_width] ? %( w="#{(lbl[:line_width] * 12_700).to_i}") : ""
1098
- tll_lf = lbl[:line_color] ? %(<a:solidFill>#{color_xml(lbl[:line_color])}</a:solidFill>) : ""
1099
- tll_ld = lbl[:line_dash] ? %(<a:prstDash val="#{xml_escape(lbl[:line_dash])}"/>) : ""
1100
- tll_sp << "<a:ln#{tll_lw}>#{tll_lf}#{tll_ld}</a:ln>"
1101
- end
1102
- parts << "<c:spPr>#{tll_sp}</c:spPr>" unless tll_sp.empty?
1103
- parts << build_axis_txpr(nil, lbl[:font]) if lbl[:font]
1104
- parts << "</c:trendlineLbl>"
1105
- end
1106
- parts << "</c:trendline>"
1107
- end
1108
- err_bars_list = ser[:error_bars_list] || (ser[:error_bars] ? [ser[:error_bars]] : [])
1109
- err_bars_list.each do |eb|
1110
- parts << "<c:errBars>"
1111
- parts << %(<c:errDir val="#{xml_escape(eb[:direction])}"/>) if eb[:direction]
1112
- parts << %(<c:errBarType val="#{xml_escape(eb[:bar_type] || "both")}"/>)
1113
- parts << %(<c:errValType val="#{xml_escape(eb[:val_type] || "fixedVal")}"/>)
1114
- parts << %(<c:noEndCap val="#{eb[:no_end_cap] ? 1 : 0}"/>) unless eb[:no_end_cap].nil?
1115
- parts << "<c:plus><c:numRef><c:f>#{xml_escape(eb[:plus])}</c:f></c:numRef></c:plus>" if eb[:plus]
1116
- parts << "<c:minus><c:numRef><c:f>#{xml_escape(eb[:minus])}</c:f></c:numRef></c:minus>" if eb[:minus]
1117
- parts << %(<c:val val="#{eb[:val]}"/>) if eb[:val]
1118
- eb_sp_children = +""
1119
- eb_sp_children << %(<a:solidFill>#{color_xml(eb[:fill_color])}</a:solidFill>) if eb[:fill_color]
1120
- eb_sp_children << "<a:noFill/>" if eb[:no_fill]
1121
- if eb[:line_color] || eb[:line_width] || eb[:line_dash]
1122
- eb_ln_w = eb[:line_width] ? %( w="#{(eb[:line_width] * 12_700).to_i}") : ""
1123
- eb_ln_f = eb[:line_color] ? %(<a:solidFill>#{color_xml(eb[:line_color])}</a:solidFill>) : ""
1124
- eb_ln_d = eb[:line_dash] ? %(<a:prstDash val="#{xml_escape(eb[:line_dash])}"/>) : ""
1125
- eb_sp_children << "<a:ln#{eb_ln_w}>#{eb_ln_f}#{eb_ln_d}</a:ln>"
1279
+ uses_xy = %w[scatterChart bubbleChart].include?(g_type)
1280
+ cat_tag = uses_xy ? "xVal" : "cat"
1281
+ val_tag = uses_xy ? "yVal" : "val"
1282
+ if ser[:cat_ref]
1283
+ parts << if uses_xy || ser[:cat_ref_type] == :num
1284
+ "<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}>"
1285
+ else
1286
+ "<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}>"
1287
+ end
1126
1288
  end
1127
- parts << "<c:spPr>#{eb_sp_children}</c:spPr>" unless eb_sp_children.empty?
1128
- parts << "</c:errBars>"
1129
- end
1130
- uses_xy = %w[scatterChart bubbleChart].include?(chart_type)
1131
- cat_tag = uses_xy ? "xVal" : "cat"
1132
- val_tag = uses_xy ? "yVal" : "val"
1133
- if ser[:cat_ref]
1134
- parts << if uses_xy || ser[:cat_ref_type] == :num
1135
- "<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}>"
1136
- else
1137
- "<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}>"
1138
- end
1289
+ 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]
1290
+ 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]
1291
+ parts << %(<c:smooth val="#{ser[:smooth] ? 1 : 0}"/>) unless ser[:smooth].nil?
1292
+ parts << %(<c:shape val="#{xml_escape(ser[:shape])}"/>) if ser[:shape]
1293
+ parts << "</c:ser>"
1139
1294
  end
1140
- 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]
1141
- 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]
1142
- parts << %(<c:smooth val="#{ser[:smooth] ? 1 : 0}"/>) unless ser[:smooth].nil?
1143
- parts << %(<c:shape val="#{xml_escape(ser[:shape])}"/>) if ser[:shape]
1144
- parts << "</c:ser>"
1145
- end
1146
1295
 
1147
- if chart[:band_fmts]
1148
- parts << "<c:bandFmts>"
1149
- chart[:band_fmts].each do |bf|
1150
- parts << "<c:bandFmt><c:idx val=\"#{bf[:idx]}\"/>"
1151
- bf_sp = +""
1152
- bf_sp << %(<a:solidFill>#{color_xml(bf[:fill_color])}</a:solidFill>) if bf[:fill_color]
1153
- bf_sp << "<a:noFill/>" if bf[:no_fill]
1154
- if bf[:line_color] || bf[:line_width] || bf[:line_dash]
1155
- bf_lw = bf[:line_width] ? %( w="#{(bf[:line_width] * 12_700).to_i}") : ""
1156
- bf_lf = bf[:line_color] ? %(<a:solidFill>#{color_xml(bf[:line_color])}</a:solidFill>) : ""
1157
- bf_ld = bf[:line_dash] ? %(<a:prstDash val="#{xml_escape(bf[:line_dash])}"/>) : ""
1158
- bf_sp << "<a:ln#{bf_lw}>#{bf_lf}#{bf_ld}</a:ln>"
1296
+ if chart[:band_fmts]
1297
+ parts << "<c:bandFmts>"
1298
+ chart[:band_fmts].each do |bf|
1299
+ parts << "<c:bandFmt><c:idx val=\"#{bf[:idx]}\"/>"
1300
+ bf_sp = +""
1301
+ bf_sp << %(<a:solidFill>#{color_xml(bf[:fill_color])}</a:solidFill>) if bf[:fill_color]
1302
+ bf_sp << "<a:noFill/>" if bf[:no_fill]
1303
+ if bf[:line_color] || bf[:line_width] || bf[:line_dash]
1304
+ bf_lw = bf[:line_width] ? %( w="#{(bf[:line_width] * 12_700).to_i}") : ""
1305
+ bf_lf = bf[:line_color] ? %(<a:solidFill>#{color_xml(bf[:line_color])}</a:solidFill>) : ""
1306
+ bf_ld = bf[:line_dash] ? %(<a:prstDash val="#{xml_escape(bf[:line_dash])}"/>) : ""
1307
+ bf_sp << "<a:ln#{bf_lw}>#{bf_lf}#{bf_ld}</a:ln>"
1308
+ end
1309
+ parts << "<c:spPr>#{bf_sp}</c:spPr>" unless bf_sp.empty?
1310
+ parts << "</c:bandFmt>"
1159
1311
  end
1160
- parts << "<c:spPr>#{bf_sp}</c:spPr>" unless bf_sp.empty?
1161
- parts << "</c:bandFmt>"
1312
+ parts << "</c:bandFmts>"
1162
1313
  end
1163
- parts << "</c:bandFmts>"
1164
- end
1165
1314
 
1166
- parts << %(<c:gapWidth val="#{chart[:gap_width]}"/>) if chart[:gap_width]
1167
- parts << %(<c:splitType val="#{xml_escape(chart[:split_type])}"/>) if chart[:split_type]
1168
- parts << %(<c:splitPos val="#{chart[:split_pos]}"/>) if chart[:split_pos]
1169
- if chart[:cust_split]&.any?
1170
- parts << "<c:custSplit>"
1171
- chart[:cust_split].each { |idx| parts << %(<c:secondPiePt val="#{idx}"/>) }
1172
- parts << "</c:custSplit>"
1173
- end
1174
- parts << %(<c:secondPieSize val="#{chart[:second_pie_size]}"/>) if chart[:second_pie_size]
1175
- parts << %(<c:gapDepth val="#{chart[:gap_depth]}"/>) if chart[:gap_depth]
1176
- parts << %(<c:overlap val="#{chart[:overlap]}"/>) if chart[:overlap]
1177
- parts << %(<c:shape val="#{chart[:bar_shape]}"/>) if chart[:bar_shape]
1178
- b3d = chart[:bubble_3d]
1179
- parts << %(<c:bubble3D val="#{b3d ? 1 : 0}"/>) unless b3d.nil?
1180
- parts << %(<c:bubbleScale val="#{chart[:bubble_scale]}"/>) if chart[:bubble_scale]
1181
- snb = chart[:show_neg_bubbles]
1182
- parts << %(<c:showNegBubbles val="#{snb ? 1 : 0}"/>) unless snb.nil?
1183
- parts << %(<c:sizeRepresents val="#{chart[:size_represents]}"/>) if chart[:size_represents]
1184
- parts << %(<c:firstSliceAng val="#{chart[:first_slice_ang]}"/>) if chart[:first_slice_ang]
1185
- parts << %(<c:holeSize val="#{chart[:hole_size]}"/>) if chart[:hole_size]
1186
- if chart[:ser_lines]
1187
- sl_spec = chart[:ser_lines]
1188
- if sl_spec.is_a?(Hash)
1189
- sl_sp = +""
1190
- if sl_spec[:line_color] || sl_spec[:line_width] || sl_spec[:line_dash]
1191
- sl_lw = sl_spec[:line_width] ? %( w="#{(sl_spec[:line_width] * 12_700).to_i}") : ""
1192
- sl_lf = sl_spec[:line_color] ? %(<a:solidFill>#{color_xml(sl_spec[:line_color])}</a:solidFill>) : ""
1193
- sl_ld = sl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(sl_spec[:line_dash])}"/>) : ""
1194
- sl_sp << "<a:ln#{sl_lw}>#{sl_lf}#{sl_ld}</a:ln>"
1195
- end
1196
- parts << (sl_sp.empty? ? "<c:serLines/>" : "<c:serLines><c:spPr>#{sl_sp}</c:spPr></c:serLines>")
1197
- else
1198
- parts << "<c:serLines/>"
1315
+ parts << %(<c:gapWidth val="#{chart[:gap_width]}"/>) if chart[:gap_width]
1316
+ parts << %(<c:splitType val="#{xml_escape(chart[:split_type])}"/>) if chart[:split_type]
1317
+ parts << %(<c:splitPos val="#{chart[:split_pos]}"/>) if chart[:split_pos]
1318
+ if chart[:cust_split]&.any?
1319
+ parts << "<c:custSplit>"
1320
+ chart[:cust_split].each { |idx| parts << %(<c:secondPiePt val="#{idx}"/>) }
1321
+ parts << "</c:custSplit>"
1199
1322
  end
1200
- end
1201
- if chart[:drop_lines]
1202
- dl_spec = chart[:drop_lines]
1203
- if dl_spec.is_a?(Hash)
1204
- dl_sp = +""
1205
- if dl_spec[:line_color] || dl_spec[:line_width] || dl_spec[:line_dash]
1206
- dl_lw = dl_spec[:line_width] ? %( w="#{(dl_spec[:line_width] * 12_700).to_i}") : ""
1207
- dl_lf = dl_spec[:line_color] ? %(<a:solidFill>#{color_xml(dl_spec[:line_color])}</a:solidFill>) : ""
1208
- dl_ld = dl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(dl_spec[:line_dash])}"/>) : ""
1209
- dl_sp << "<a:ln#{dl_lw}>#{dl_lf}#{dl_ld}</a:ln>"
1323
+ parts << %(<c:secondPieSize val="#{chart[:second_pie_size]}"/>) if chart[:second_pie_size]
1324
+ parts << %(<c:gapDepth val="#{chart[:gap_depth]}"/>) if chart[:gap_depth]
1325
+ parts << %(<c:overlap val="#{chart[:overlap]}"/>) if chart[:overlap]
1326
+ parts << %(<c:shape val="#{chart[:bar_shape]}"/>) if chart[:bar_shape]
1327
+ b3d = chart[:bubble_3d]
1328
+ parts << %(<c:bubble3D val="#{b3d ? 1 : 0}"/>) unless b3d.nil?
1329
+ parts << %(<c:bubbleScale val="#{chart[:bubble_scale]}"/>) if chart[:bubble_scale]
1330
+ snb = chart[:show_neg_bubbles]
1331
+ parts << %(<c:showNegBubbles val="#{snb ? 1 : 0}"/>) unless snb.nil?
1332
+ parts << %(<c:sizeRepresents val="#{chart[:size_represents]}"/>) if chart[:size_represents]
1333
+ parts << %(<c:firstSliceAng val="#{chart[:first_slice_ang]}"/>) if chart[:first_slice_ang]
1334
+ parts << %(<c:holeSize val="#{chart[:hole_size]}"/>) if chart[:hole_size]
1335
+ if chart[:ser_lines]
1336
+ sl_spec = chart[:ser_lines]
1337
+ if sl_spec.is_a?(Hash)
1338
+ sl_sp = +""
1339
+ if sl_spec[:line_color] || sl_spec[:line_width] || sl_spec[:line_dash]
1340
+ sl_lw = sl_spec[:line_width] ? %( w="#{(sl_spec[:line_width] * 12_700).to_i}") : ""
1341
+ sl_lf = sl_spec[:line_color] ? %(<a:solidFill>#{color_xml(sl_spec[:line_color])}</a:solidFill>) : ""
1342
+ sl_ld = sl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(sl_spec[:line_dash])}"/>) : ""
1343
+ sl_sp << "<a:ln#{sl_lw}>#{sl_lf}#{sl_ld}</a:ln>"
1344
+ end
1345
+ parts << (sl_sp.empty? ? "<c:serLines/>" : "<c:serLines><c:spPr>#{sl_sp}</c:spPr></c:serLines>")
1346
+ else
1347
+ parts << "<c:serLines/>"
1210
1348
  end
1211
- parts << (dl_sp.empty? ? "<c:dropLines/>" : "<c:dropLines><c:spPr>#{dl_sp}</c:spPr></c:dropLines>")
1212
- else
1213
- parts << "<c:dropLines/>"
1214
1349
  end
1215
- end
1216
- if chart[:hi_low_lines]
1217
- hl_spec = chart[:hi_low_lines]
1218
- if hl_spec.is_a?(Hash)
1219
- hl_sp = +""
1220
- if hl_spec[:line_color] || hl_spec[:line_width] || hl_spec[:line_dash]
1221
- hl_lw = hl_spec[:line_width] ? %( w="#{(hl_spec[:line_width] * 12_700).to_i}") : ""
1222
- hl_lf = hl_spec[:line_color] ? %(<a:solidFill>#{color_xml(hl_spec[:line_color])}</a:solidFill>) : ""
1223
- hl_ld = hl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(hl_spec[:line_dash])}"/>) : ""
1224
- hl_sp << "<a:ln#{hl_lw}>#{hl_lf}#{hl_ld}</a:ln>"
1350
+ if chart[:drop_lines]
1351
+ dl_spec = chart[:drop_lines]
1352
+ if dl_spec.is_a?(Hash)
1353
+ dl_sp = +""
1354
+ if dl_spec[:line_color] || dl_spec[:line_width] || dl_spec[:line_dash]
1355
+ dl_lw = dl_spec[:line_width] ? %( w="#{(dl_spec[:line_width] * 12_700).to_i}") : ""
1356
+ dl_lf = dl_spec[:line_color] ? %(<a:solidFill>#{color_xml(dl_spec[:line_color])}</a:solidFill>) : ""
1357
+ dl_ld = dl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(dl_spec[:line_dash])}"/>) : ""
1358
+ dl_sp << "<a:ln#{dl_lw}>#{dl_lf}#{dl_ld}</a:ln>"
1359
+ end
1360
+ parts << (dl_sp.empty? ? "<c:dropLines/>" : "<c:dropLines><c:spPr>#{dl_sp}</c:spPr></c:dropLines>")
1361
+ else
1362
+ parts << "<c:dropLines/>"
1225
1363
  end
1226
- parts << (hl_sp.empty? ? "<c:hiLowLines/>" : "<c:hiLowLines><c:spPr>#{hl_sp}</c:spPr></c:hiLowLines>")
1227
- else
1228
- parts << "<c:hiLowLines/>"
1229
1364
  end
1230
- end
1231
- if chart[:up_down_bars]
1232
- udb = chart[:up_down_bars]
1233
- parts << "<c:upDownBars>"
1234
- parts << %(<c:gapWidth val="#{udb[:gap_width]}"/>) if udb.is_a?(Hash) && udb[:gap_width]
1235
- %i[up_bars down_bars].each do |bar_key|
1236
- tag = bar_key == :up_bars ? "upBars" : "downBars"
1237
- bar = udb.is_a?(Hash) ? udb[bar_key] : nil
1238
- if bar
1239
- bar_sp = +""
1240
- bar_sp << %(<a:solidFill>#{color_xml(bar[:fill_color])}</a:solidFill>) if bar[:fill_color]
1241
- bar_sp << "<a:noFill/>" if bar[:no_fill]
1242
- if bar[:line_color] || bar[:line_width] || bar[:line_dash]
1243
- b_lw = bar[:line_width] ? %( w="#{(bar[:line_width] * 12_700).to_i}") : ""
1244
- b_lf = bar[:line_color] ? %(<a:solidFill>#{color_xml(bar[:line_color])}</a:solidFill>) : ""
1245
- b_ld = bar[:line_dash] ? %(<a:prstDash val="#{xml_escape(bar[:line_dash])}"/>) : ""
1246
- bar_sp << "<a:ln#{b_lw}>#{b_lf}#{b_ld}</a:ln>"
1365
+ if chart[:hi_low_lines]
1366
+ hl_spec = chart[:hi_low_lines]
1367
+ if hl_spec.is_a?(Hash)
1368
+ hl_sp = +""
1369
+ if hl_spec[:line_color] || hl_spec[:line_width] || hl_spec[:line_dash]
1370
+ hl_lw = hl_spec[:line_width] ? %( w="#{(hl_spec[:line_width] * 12_700).to_i}") : ""
1371
+ hl_lf = hl_spec[:line_color] ? %(<a:solidFill>#{color_xml(hl_spec[:line_color])}</a:solidFill>) : ""
1372
+ hl_ld = hl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(hl_spec[:line_dash])}"/>) : ""
1373
+ hl_sp << "<a:ln#{hl_lw}>#{hl_lf}#{hl_ld}</a:ln>"
1247
1374
  end
1248
- parts << (bar_sp.empty? ? "<c:#{tag}/>" : "<c:#{tag}><c:spPr>#{bar_sp}</c:spPr></c:#{tag}>")
1375
+ parts << (hl_sp.empty? ? "<c:hiLowLines/>" : "<c:hiLowLines><c:spPr>#{hl_sp}</c:spPr></c:hiLowLines>")
1249
1376
  else
1250
- parts << "<c:#{tag}/>"
1377
+ parts << "<c:hiLowLines/>"
1251
1378
  end
1252
1379
  end
1253
- parts << "</c:upDownBars>"
1254
- end
1255
- mk = chart[:marker]
1256
- parts << %(<c:marker val="#{mk ? 1 : 0}"/>) unless mk.nil?
1257
- sm = chart[:smooth]
1258
- parts << %(<c:smooth val="#{sm ? 1 : 0}"/>) unless sm.nil?
1259
- three_axes = THREE_AXIS_CHARTS.include?(chart_type)
1260
- unless no_axes
1261
- parts << '<c:axId val="1"/><c:axId val="2"/>'
1262
- parts << '<c:axId val="3"/>' if three_axes
1380
+ if chart[:up_down_bars]
1381
+ udb = chart[:up_down_bars]
1382
+ parts << "<c:upDownBars>"
1383
+ parts << %(<c:gapWidth val="#{udb[:gap_width]}"/>) if udb.is_a?(Hash) && udb[:gap_width]
1384
+ %i[up_bars down_bars].each do |bar_key|
1385
+ tag = bar_key == :up_bars ? "upBars" : "downBars"
1386
+ bar = udb.is_a?(Hash) ? udb[bar_key] : nil
1387
+ if bar
1388
+ bar_sp = +""
1389
+ bar_sp << %(<a:solidFill>#{color_xml(bar[:fill_color])}</a:solidFill>) if bar[:fill_color]
1390
+ bar_sp << "<a:noFill/>" if bar[:no_fill]
1391
+ if bar[:line_color] || bar[:line_width] || bar[:line_dash]
1392
+ b_lw = bar[:line_width] ? %( w="#{(bar[:line_width] * 12_700).to_i}") : ""
1393
+ b_lf = bar[:line_color] ? %(<a:solidFill>#{color_xml(bar[:line_color])}</a:solidFill>) : ""
1394
+ b_ld = bar[:line_dash] ? %(<a:prstDash val="#{xml_escape(bar[:line_dash])}"/>) : ""
1395
+ bar_sp << "<a:ln#{b_lw}>#{b_lf}#{b_ld}</a:ln>"
1396
+ end
1397
+ parts << (bar_sp.empty? ? "<c:#{tag}/>" : "<c:#{tag}><c:spPr>#{bar_sp}</c:spPr></c:#{tag}>")
1398
+ else
1399
+ parts << "<c:#{tag}/>"
1400
+ end
1401
+ end
1402
+ parts << "</c:upDownBars>"
1403
+ end
1404
+ mk = chart[:marker]
1405
+ parts << %(<c:marker val="#{mk ? 1 : 0}"/>) unless mk.nil?
1406
+ sm = chart[:smooth]
1407
+ parts << %(<c:smooth val="#{sm ? 1 : 0}"/>) unless sm.nil?
1408
+ three_axes = THREE_AXIS_CHARTS.include?(g_type)
1409
+ unless g_no_axes
1410
+ parts << %(<c:axId val="1"/><c:axId val="#{g_val_ax}"/>)
1411
+ parts << '<c:axId val="3"/>' if three_axes
1412
+ end
1413
+ parts << "</c:#{g_type}>"
1263
1414
  end
1264
- parts << "</c:#{chart_type}>"
1265
1415
 
1266
- unless no_axes
1416
+ has_axes = groups.any? { |g| !NO_AXIS_CHARTS.include?(g[:type]) }
1417
+ if has_axes
1267
1418
  cat_del = chart[:cat_axis_delete] ? 1 : 0
1268
1419
  cat_orient = chart[:cat_axis_orientation] || "minMax"
1269
1420
  cat_ax_tag = chart[:cat_axis_type] == :date ? "dateAx" : "catAx"
@@ -1318,86 +1469,11 @@ module Xlsxrb
1318
1469
  parts << %(<c:minorTimeUnit val="#{chart[:cat_axis_minor_time_unit]}"/>) if chart[:cat_axis_minor_time_unit]
1319
1470
  end
1320
1471
  parts << "</c:#{cat_ax_tag}>"
1321
- val_del = chart[:val_axis_delete] ? 1 : 0
1322
- val_orient = chart[:val_axis_orientation] || "minMax"
1323
- parts << %(<c:valAx><c:axId val="2"/><c:scaling>)
1324
- parts << %(<c:logBase val="#{chart[:val_axis_log_base]}"/>) if chart[:val_axis_log_base]
1325
- parts << %(<c:orientation val="#{val_orient}"/>)
1326
- parts << %(<c:max val="#{chart[:val_axis_scaling_max]}"/>) if chart[:val_axis_scaling_max]
1327
- parts << %(<c:min val="#{chart[:val_axis_scaling_min]}"/>) if chart[:val_axis_scaling_min]
1328
- parts << %(</c:scaling><c:delete val="#{val_del}"/><c:axPos val="#{chart[:val_axis_pos] || "l"}"/>)
1329
- parts << gridlines_xml("majorGridlines", chart[:val_axis_major_gridlines])
1330
- parts << gridlines_xml("minorGridlines", chart[:val_axis_minor_gridlines])
1331
- if chart[:val_axis_title]
1332
- val_title_spec = merge_flat_title_styling(chart[:val_axis_title], chart, :val_axis_title)
1333
- parts << build_chart_title_xml(val_title_spec)
1334
- end
1335
- if (vnf = chart[:val_axis_num_fmt])
1336
- sl = vnf[:source_linked] ? 1 : 0
1337
- parts << %(<c:numFmt formatCode="#{xml_escape(vnf[:format_code])}" sourceLinked="#{sl}"/>)
1338
- end
1339
- parts << %(<c:majorTickMark val="#{chart[:val_axis_major_tick_mark]}"/>) if chart[:val_axis_major_tick_mark]
1340
- parts << %(<c:minorTickMark val="#{chart[:val_axis_minor_tick_mark]}"/>) if chart[:val_axis_minor_tick_mark]
1341
- parts << %(<c:tickLblPos val="#{chart[:val_axis_tick_lbl_pos]}"/>) if chart[:val_axis_tick_lbl_pos]
1342
- if chart[:val_axis_line_color] || chart[:val_axis_fill] || chart[:val_axis_no_fill] || chart[:val_axis_line_dash]
1343
- parts << "<c:spPr>"
1344
- parts << "<a:noFill/>" if chart[:val_axis_no_fill]
1345
- parts << %(<a:solidFill>#{color_xml(chart[:val_axis_fill])}</a:solidFill>) if chart[:val_axis_fill]
1346
- if chart[:val_axis_line_color] || chart[:val_axis_line_dash]
1347
- w_attr = chart[:val_axis_line_width] ? %( w="#{chart[:val_axis_line_width]}") : ""
1348
- va_ln_f = chart[:val_axis_line_color] ? %(<a:solidFill>#{srgb_clr_xml(chart[:val_axis_line_color])}</a:solidFill>) : ""
1349
- va_ln_d = chart[:val_axis_line_dash] ? %(<a:prstDash val="#{xml_escape(chart[:val_axis_line_dash])}"/>) : ""
1350
- parts << "<a:ln#{w_attr}>#{va_ln_f}#{va_ln_d}</a:ln>"
1351
- end
1352
- parts << "</c:spPr>"
1353
- end
1354
- parts << build_axis_txpr(chart[:val_axis_label_rotation], chart[:val_axis_font])
1355
- parts << '<c:crossAx val="1"/>'
1356
- parts << %(<c:crosses val="#{chart[:val_axis_crosses]}"/>) if chart[:val_axis_crosses]
1357
- parts << %(<c:crossesAt val="#{chart[:val_axis_crosses_at]}"/>) if !chart[:val_axis_crosses] && chart[:val_axis_crosses_at]
1358
- parts << %(<c:crossBetween val="#{chart[:val_axis_cross_between]}"/>) if chart[:val_axis_cross_between]
1359
- parts << %(<c:majorUnit val="#{chart[:val_axis_major_unit]}"/>) if chart[:val_axis_major_unit]
1360
- parts << %(<c:minorUnit val="#{chart[:val_axis_minor_unit]}"/>) if chart[:val_axis_minor_unit]
1361
- if chart[:val_axis_disp_units]
1362
- du = chart[:val_axis_disp_units]
1363
- if du.is_a?(Hash)
1364
- parts << "<c:dispUnits>"
1365
- parts << %(<c:builtInUnit val="#{xml_escape(du[:built_in_unit])}"/>) if du[:built_in_unit]
1366
- parts << %(<c:custUnit val="#{du[:cust_unit]}"/>) if du[:cust_unit]
1367
- if du[:label]
1368
- dul = du[:label]
1369
- parts << "<c:dispUnitsLbl>"
1370
- if dul[:num_fmt]
1371
- nf = dul[:num_fmt]
1372
- nf_src = nf.is_a?(Hash) ? nf : { format_code: nf }
1373
- nf_linked = if nf_src.key?(:source_linked)
1374
- nf_src[:source_linked] ? 1 : 0
1375
- else
1376
- 0
1377
- end
1378
- parts << %(<c:numFmt formatCode="#{xml_escape(nf_src[:format_code])}" sourceLinked="#{nf_linked}"/>)
1379
- end
1380
- dul_sp = +""
1381
- dul_sp << %(<a:solidFill>#{color_xml(dul[:fill_color])}</a:solidFill>) if dul[:fill_color]
1382
- dul_sp << "<a:noFill/>" if dul[:no_fill]
1383
- if dul[:line_color] || dul[:line_width] || dul[:line_dash]
1384
- dul_lw = dul[:line_width] ? %( w="#{(dul[:line_width] * 12_700).to_i}") : ""
1385
- dul_lf = dul[:line_color] ? %(<a:solidFill>#{color_xml(dul[:line_color])}</a:solidFill>) : ""
1386
- dul_ld = dul[:line_dash] ? %(<a:prstDash val="#{xml_escape(dul[:line_dash])}"/>) : ""
1387
- dul_sp << "<a:ln#{dul_lw}>#{dul_lf}#{dul_ld}</a:ln>"
1388
- end
1389
- parts << "<c:spPr>#{dul_sp}</c:spPr>" unless dul_sp.empty?
1390
- parts << build_axis_txpr(nil, dul[:font]) if dul[:font]
1391
- parts << "</c:dispUnitsLbl>"
1392
- end
1393
- parts << "</c:dispUnits>"
1394
- else
1395
- parts << %(<c:dispUnits><c:builtInUnit val="#{xml_escape(du.to_s)}"/></c:dispUnits>)
1396
- end
1397
- end
1398
- parts << "</c:valAx>"
1399
1472
 
1400
- if three_axes
1473
+ parts << build_val_ax_xml(ax_id: 2, prefix: :val_axis, chart: chart, default_pos: "l")
1474
+ parts << build_val_ax_xml(ax_id: 3, prefix: :sec_val_axis, chart: chart, default_pos: "r", default_crosses: "max") if has_secondary
1475
+
1476
+ if groups.any? { |g| THREE_AXIS_CHARTS.include?(g[:type]) }
1401
1477
  parts << '<c:serAx><c:axId val="3"/><c:scaling><c:orientation val="minMax"/></c:scaling>'
1402
1478
  parts << '<c:delete val="0"/><c:axPos val="b"/><c:crossAx val="2"/></c:serAx>'
1403
1479
  end
@@ -1600,6 +1676,68 @@ module Xlsxrb
1600
1676
  row = m[2].to_i - 1
1601
1677
  [col, row]
1602
1678
  end
1679
+
1680
+ THREADED_COMMENTS_NS = "http://schemas.microsoft.com/office/spreadsheetml/2018/threadedcomments"
1681
+
1682
+ # Formats or generates a GUID in standard uppercase braced format {UUID}.
1683
+ # : (?String? val) -> String
1684
+ def format_guid(val = nil)
1685
+ return "{#{SecureRandom.uuid.upcase}}" if val.nil? || val.to_s.empty?
1686
+
1687
+ str = val.to_s.strip
1688
+ str = "{#{str}}" unless str.start_with?("{") && str.end_with?("}")
1689
+ str.upcase
1690
+ end
1691
+
1692
+ # Generates XML for xl/threadedComments/threadedComment{N}.xml.
1693
+ # : (Array[Hash[Symbol, untyped]] sheet_threaded_comments) -> String
1694
+ def generate_threaded_comments_xml(sheet_threaded_comments)
1695
+ parts = [
1696
+ XML_HEADER,
1697
+ %(<ThreadedComments xmlns="#{THREADED_COMMENTS_NS}">)
1698
+ ]
1699
+ sheet_threaded_comments.each do |c|
1700
+ ref = c[:ref] || c[:cell]
1701
+ dt = c[:timestamp] || c[:dt]
1702
+ dt_str = case dt
1703
+ when Time, DateTime
1704
+ dt.to_time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
1705
+ when String
1706
+ dt
1707
+ else
1708
+ Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
1709
+ end
1710
+ person_id = c[:person_id] || c[:author_id] || "{00000000-0000-0000-0000-000000000000}"
1711
+ comment_id = c[:id] || format_guid(nil)
1712
+
1713
+ attrs = %(ref="#{ref}" dT="#{dt_str}" personId="#{person_id}" id="#{comment_id}")
1714
+ attrs << %( parentId="#{c[:parent_id]}") if c[:parent_id]
1715
+ attrs << %( done="true") if [true, "true", "1"].include?(c[:done])
1716
+
1717
+ text_val = c[:text].to_s
1718
+ parts << "<threadedComment #{attrs}><text>#{xml_escape(text_val)}</text></threadedComment>"
1719
+ end
1720
+ parts << "</ThreadedComments>"
1721
+ parts.join
1722
+ end
1723
+
1724
+ # Generates XML for xl/persons/person.xml.
1725
+ # : (Array[Hash[Symbol, untyped]] persons) -> String
1726
+ def generate_persons_xml(persons)
1727
+ parts = [
1728
+ XML_HEADER,
1729
+ %(<personList xmlns="#{THREADED_COMMENTS_NS}">)
1730
+ ]
1731
+ persons.each do |p|
1732
+ p_id = p[:id] || format_guid(nil)
1733
+ name = xml_escape(p[:display_name] || p[:name] || "Author")
1734
+ user_id = xml_escape(p[:user_id] || "AD:#{p[:display_name] || p[:name] || "Author"}")
1735
+ provider_id = xml_escape(p[:provider_id] || "AD")
1736
+ parts << %(<person displayName="#{name}" id="#{p_id}" userId="#{user_id}" providerId="#{provider_id}"/>)
1737
+ end
1738
+ parts << "</personList>"
1739
+ parts.join
1740
+ end
1603
1741
  end
1604
1742
  end
1605
1743
  end