write_xlsx 1.13.0 → 1.15.1

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +25 -4
  3. data/Changes +23 -0
  4. data/lib/write_xlsx/chart/area.rb +2 -2
  5. data/lib/write_xlsx/chart/axis.rb +55 -32
  6. data/lib/write_xlsx/chart/axis_writer.rb +528 -0
  7. data/lib/write_xlsx/chart/bar.rb +2 -2
  8. data/lib/write_xlsx/chart/caption.rb +16 -9
  9. data/lib/write_xlsx/chart/chart_area.rb +121 -0
  10. data/lib/write_xlsx/chart/column.rb +2 -2
  11. data/lib/write_xlsx/chart/d_pt_point_writer.rb +14 -0
  12. data/lib/write_xlsx/chart/doughnut.rb +0 -3
  13. data/lib/write_xlsx/chart/formatting_writer.rb +652 -0
  14. data/lib/write_xlsx/chart/initialization.rb +100 -0
  15. data/lib/write_xlsx/chart/line.rb +4 -3
  16. data/lib/write_xlsx/chart/pie.rb +6 -2
  17. data/lib/write_xlsx/chart/radar.rb +2 -2
  18. data/lib/write_xlsx/chart/scatter.rb +4 -3
  19. data/lib/write_xlsx/chart/series.rb +35 -15
  20. data/lib/write_xlsx/chart/series_data.rb +132 -0
  21. data/lib/write_xlsx/chart/series_writer.rb +318 -0
  22. data/lib/write_xlsx/chart/settings.rb +226 -0
  23. data/lib/write_xlsx/chart/stock.rb +2 -2
  24. data/lib/write_xlsx/chart/table.rb +50 -0
  25. data/lib/write_xlsx/chart/xml_writer.rb +305 -0
  26. data/lib/write_xlsx/chart.rb +286 -2477
  27. data/lib/write_xlsx/chartsheet.rb +31 -82
  28. data/lib/write_xlsx/constants.rb +11 -0
  29. data/lib/write_xlsx/drawing.rb +5 -3
  30. data/lib/write_xlsx/format/alignment_state.rb +39 -0
  31. data/lib/write_xlsx/format/alignment_style.rb +92 -0
  32. data/lib/write_xlsx/format/border_state.rb +47 -0
  33. data/lib/write_xlsx/format/border_style.rb +116 -0
  34. data/lib/write_xlsx/format/fill_state.rb +26 -0
  35. data/lib/write_xlsx/format/fill_style.rb +52 -0
  36. data/lib/write_xlsx/format/font_state.rb +74 -0
  37. data/lib/write_xlsx/format/font_style.rb +172 -0
  38. data/lib/write_xlsx/format/format_state.rb +65 -0
  39. data/lib/write_xlsx/format/number_format_state.rb +20 -0
  40. data/lib/write_xlsx/format/number_format_style.rb +28 -0
  41. data/lib/write_xlsx/format/protection_state.rb +20 -0
  42. data/lib/write_xlsx/format/protection_style.rb +28 -0
  43. data/lib/write_xlsx/format.rb +1093 -426
  44. data/lib/write_xlsx/formats.rb +0 -2
  45. data/lib/write_xlsx/image_property.rb +4 -1
  46. data/lib/write_xlsx/inserted_chart.rb +1 -1
  47. data/lib/write_xlsx/object_positioning.rb +15 -1
  48. data/lib/write_xlsx/package/app.rb +2 -2
  49. data/lib/write_xlsx/package/button.rb +6 -2
  50. data/lib/write_xlsx/package/comments.rb +11 -3
  51. data/lib/write_xlsx/package/conditional_format.rb +7 -3
  52. data/lib/write_xlsx/package/content_types.rb +2 -2
  53. data/lib/write_xlsx/package/core.rb +2 -2
  54. data/lib/write_xlsx/package/custom.rb +3 -2
  55. data/lib/write_xlsx/package/metadata.rb +2 -2
  56. data/lib/write_xlsx/package/packager.rb +0 -3
  57. data/lib/write_xlsx/package/relationships.rb +2 -2
  58. data/lib/write_xlsx/package/rich_value.rb +4 -2
  59. data/lib/write_xlsx/package/rich_value_rel.rb +2 -2
  60. data/lib/write_xlsx/package/rich_value_structure.rb +2 -2
  61. data/lib/write_xlsx/package/rich_value_types.rb +3 -3
  62. data/lib/write_xlsx/package/shared_strings.rb +2 -2
  63. data/lib/write_xlsx/package/styles.rb +13 -9
  64. data/lib/write_xlsx/package/table.rb +8 -2
  65. data/lib/write_xlsx/package/theme.rb +0 -3
  66. data/lib/write_xlsx/package/vml.rb +2 -2
  67. data/lib/write_xlsx/page_setup.rb +3 -1
  68. data/lib/write_xlsx/shape.rb +97 -100
  69. data/lib/write_xlsx/sheets.rb +6 -1
  70. data/lib/write_xlsx/sparkline.rb +2 -2
  71. data/lib/write_xlsx/utility/cell_reference.rb +124 -0
  72. data/lib/write_xlsx/utility/chart_formatting.rb +262 -0
  73. data/lib/write_xlsx/utility/common.rb +44 -0
  74. data/lib/write_xlsx/utility/date_time.rb +113 -0
  75. data/lib/write_xlsx/utility/dimensions.rb +40 -0
  76. data/lib/write_xlsx/utility/drawing.rb +136 -0
  77. data/lib/write_xlsx/utility/rich_text.rb +184 -0
  78. data/lib/write_xlsx/utility/sheetname_quoting.rb +73 -0
  79. data/lib/write_xlsx/utility/string_width.rb +45 -0
  80. data/lib/write_xlsx/utility/url.rb +27 -0
  81. data/lib/write_xlsx/utility/xml_primitives.rb +32 -0
  82. data/lib/write_xlsx/version.rb +1 -1
  83. data/lib/write_xlsx/workbook/chart_data.rb +188 -0
  84. data/lib/write_xlsx/workbook/format_preparation.rb +199 -0
  85. data/lib/write_xlsx/workbook/initialization.rb +223 -0
  86. data/lib/write_xlsx/workbook/package_preparation.rb +231 -0
  87. data/lib/write_xlsx/workbook/workbook_writer.rb +164 -0
  88. data/lib/write_xlsx/workbook.rb +143 -981
  89. data/lib/write_xlsx/worksheet/autofilter.rb +3 -1
  90. data/lib/write_xlsx/worksheet/cell_data.rb +5 -1
  91. data/lib/write_xlsx/worksheet/columns.rb +8 -3
  92. data/lib/write_xlsx/worksheet/data_validation.rb +9 -1
  93. data/lib/write_xlsx/worksheet/data_writing.rb +42 -15
  94. data/lib/write_xlsx/worksheet/drawing_preparation.rb +1 -1
  95. data/lib/write_xlsx/worksheet/formatting.rb +3 -1
  96. data/lib/write_xlsx/worksheet/hyperlink.rb +9 -1
  97. data/lib/write_xlsx/worksheet/row_col_sizing.rb +3 -1
  98. data/lib/write_xlsx/worksheet/xml_writer.rb +9 -4
  99. data/lib/write_xlsx/worksheet.rb +19 -2
  100. metadata +42 -3
  101. data/lib/write_xlsx/utility.rb +0 -1034
@@ -1,1034 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # frozen_string_literal: true
3
-
4
- require 'write_xlsx/col_name'
5
-
6
- module Writexlsx
7
- module Utility
8
- ROW_MAX = 1048576 # :nodoc:
9
- COL_MAX = 16384 # :nodoc:
10
- STR_MAX = 32767 # :nodoc:
11
- SHEETNAME_MAX = 31 # :nodoc:
12
- CHAR_WIDTHS = {
13
- ' ' => 3, '!' => 5, '"' => 6, '#' => 7, '$' => 7, '%' => 11,
14
- '&' => 10, "'" => 3, '(' => 5, ')' => 5, '*' => 7, '+' => 7,
15
- ',' => 4, '-' => 5, '.' => 4, '/' => 6, '0' => 7, '1' => 7,
16
- '2' => 7, '3' => 7, '4' => 7, '5' => 7, '6' => 7, '7' => 7,
17
- '8' => 7, '9' => 7, ':' => 4, ';' => 4, '<' => 7, '=' => 7,
18
- '>' => 7, '?' => 7, '@' => 13, 'A' => 9, 'B' => 8, 'C' => 8,
19
- 'D' => 9, 'E' => 7, 'F' => 7, 'G' => 9, 'H' => 9, 'I' => 4,
20
- 'J' => 5, 'K' => 8, 'L' => 6, 'M' => 12, 'N' => 10, 'O' => 10,
21
- 'P' => 8, 'Q' => 10, 'R' => 8, 'S' => 7, 'T' => 7, 'U' => 9,
22
- 'V' => 9, 'W' => 13, 'X' => 8, 'Y' => 7, 'Z' => 7, '[' => 5,
23
- '\\' => 6, ']' => 5, '^' => 7, '_' => 7, '`' => 4, 'a' => 7,
24
- 'b' => 8, 'c' => 6, 'd' => 8, 'e' => 8, 'f' => 5, 'g' => 7,
25
- 'h' => 8, 'i' => 4, 'j' => 4, 'k' => 7, 'l' => 4, 'm' => 12,
26
- 'n' => 8, 'o' => 8, 'p' => 8, 'q' => 8, 'r' => 5, 's' => 6,
27
- 't' => 5, 'u' => 8, 'v' => 7, 'w' => 11, 'x' => 7, 'y' => 7,
28
- 'z' => 6, '{' => 5, '|' => 7, '}' => 5, '~' => 7
29
- }.freeze
30
- MAX_DIGIT_WIDTH = 7 # For Calabri 11. # :nodoc:
31
- PADDING = 5 # :nodoc:
32
- DEFAULT_COL_PIXELS = 64
33
-
34
- #
35
- # xl_rowcol_to_cell($row, col, row_absolute, col_absolute)
36
- #
37
- def xl_rowcol_to_cell(row_or_name, col, row_absolute = false, col_absolute = false)
38
- if row_or_name.is_a?(Integer)
39
- row_or_name += 1 # Change from 0-indexed to 1 indexed.
40
- end
41
- col_str = xl_col_to_name(col, col_absolute)
42
- "#{col_str}#{absolute_char(row_absolute)}#{row_or_name}"
43
- end
44
-
45
- #
46
- # Returns: [row, col, row_absolute, col_absolute]
47
- #
48
- # The row_absolute and col_absolute parameters aren't documented because they
49
- # mainly used internally and aren't very useful to the user.
50
- #
51
- def xl_cell_to_rowcol(cell)
52
- cell =~ /(\$?)([A-Z]{1,3})(\$?)(\d+)/
53
-
54
- col_abs = ::Regexp.last_match(1) != ""
55
- col = ::Regexp.last_match(2)
56
- row_abs = ::Regexp.last_match(3) != ""
57
- row = ::Regexp.last_match(4).to_i
58
-
59
- # Convert base26 column string to number
60
- # All your Base are belong to us.
61
- chars = col.split("")
62
- expn = 0
63
- col = 0
64
-
65
- chars.reverse.each do |char|
66
- col += (char.ord - 'A'.ord + 1) * (26**expn)
67
- expn += 1
68
- end
69
-
70
- # Convert 1-index to zero-index
71
- row -= 1
72
- col -= 1
73
-
74
- [row, col, row_abs, col_abs]
75
- end
76
-
77
- def xl_col_to_name(col, col_absolute)
78
- col_str = ColName.instance.col_str(col)
79
- if col_absolute
80
- "#{absolute_char(col_absolute)}#{col_str}"
81
- else
82
- # Do not allocate new string
83
- col_str
84
- end
85
- end
86
-
87
- def xl_range(row_1, row_2, col_1, col_2,
88
- row_abs_1 = false, row_abs_2 = false, col_abs_1 = false, col_abs_2 = false)
89
- range1 = xl_rowcol_to_cell(row_1, col_1, row_abs_1, col_abs_1)
90
- range2 = xl_rowcol_to_cell(row_2, col_2, row_abs_2, col_abs_2)
91
-
92
- if range1 == range2
93
- range1
94
- else
95
- "#{range1}:#{range2}"
96
- end
97
- end
98
-
99
- def xl_range_formula(sheetname, row_1, row_2, col_1, col_2)
100
- # Use Excel's conventions and quote the sheet name if it contains any
101
- # non-word character or if it isn't already quoted.
102
- sheetname = quote_sheetname(sheetname)
103
-
104
- range1 = xl_rowcol_to_cell(row_1, col_1, 1, 1)
105
- range2 = xl_rowcol_to_cell(row_2, col_2, 1, 1)
106
-
107
- "=#{sheetname}!#{range1}:#{range2}"
108
- end
109
-
110
- #
111
- # xl_string_pixel_width($string)
112
- #
113
- # Get the pixel width of a string based on individual character widths taken
114
- # from Excel. UTF8 characters are given a default width of 8.
115
- #
116
- # Note, Excel adds an additional 7 pixels padding to a cell.
117
- #
118
- def xl_string_pixel_width(string)
119
- length = 0
120
- string.to_s.split("").each { |char| length += CHAR_WIDTHS[char] || 8 }
121
-
122
- length
123
- end
124
-
125
- #
126
- # Sheetnames used in references should be quoted if they contain any spaces,
127
- # special characters or if the look like something that isn't a sheet name.
128
- # TODO. We need to handle more special cases.
129
- #
130
- def quote_sheetname(sheetname) # :nodoc:
131
- name = sheetname.dup
132
- return name if already_quoted_sheetname?(name)
133
- return name unless sheetname_needs_quoting?(name)
134
-
135
- "'#{escape_sheetname(name)}'"
136
- end
137
-
138
- def check_dimensions(row, col)
139
- raise WriteXLSXDimensionError if !row || row >= ROW_MAX || !col || col >= COL_MAX
140
-
141
- 0
142
- end
143
-
144
- #
145
- # convert_date_time(date_time_string)
146
- #
147
- # The function takes a date and time in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format
148
- # and converts it to a decimal number representing a valid Excel date.
149
- #
150
- def convert_date_time(date_time_string) # :nodoc:
151
- date_time = date_time_string.to_s.sub(/^\s+/, '').sub(/\s+$/, '').sub(/Z$/, '')
152
-
153
- # Check for invalid date char.
154
- return nil if date_time =~ /[^0-9T:\-.Z]/
155
-
156
- # Check for "T" after date or before time.
157
- return nil unless date_time =~ /\dT|T\d/
158
-
159
- days = 0 # Number of days since epoch
160
- seconds = 0 # Time expressed as fraction of 24h hours in seconds
161
-
162
- # Split into date and time.
163
- date, time = date_time.split("T")
164
-
165
- # We allow the time portion of the input DateTime to be optional.
166
- if time
167
- # Match hh:mm:ss.sss+ where the seconds are optional
168
- if time =~ /^(\d\d):(\d\d)(:(\d\d(\.\d+)?))?/
169
- hour = ::Regexp.last_match(1).to_i
170
- min = ::Regexp.last_match(2).to_i
171
- sec = ::Regexp.last_match(4).to_f
172
- else
173
- return nil # Not a valid time format.
174
- end
175
-
176
- # Some boundary checks
177
- return nil if hour >= 24
178
- return nil if min >= 60
179
- return nil if sec >= 60
180
-
181
- # Excel expresses seconds as a fraction of the number in 24 hours.
182
- seconds = ((hour * 60 * 60) + (min * 60) + sec) / (24.0 * 60 * 60)
183
- end
184
-
185
- # We allow the date portion of the input DateTime to be optional.
186
- return seconds if date == ''
187
-
188
- # Match date as yyyy-mm-dd.
189
- if date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/
190
- year = ::Regexp.last_match(1).to_i
191
- month = ::Regexp.last_match(2).to_i
192
- day = ::Regexp.last_match(3).to_i
193
- else
194
- return nil # Not a valid date format.
195
- end
196
-
197
- # Set the epoch as 1900 or 1904. Defaults to 1900.
198
- # Special cases for Excel.
199
- unless date_1904?
200
- return seconds if date == '1899-12-31' # Excel 1900 epoch
201
- return seconds if date == '1900-01-00' # Excel 1900 epoch
202
- return 60 + seconds if date == '1900-02-29' # Excel false leapday
203
- end
204
-
205
- # We calculate the date by calculating the number of days since the epoch
206
- # and adjust for the number of leap days. We calculate the number of leap
207
- # days by normalising the year in relation to the epoch. Thus the year 2000
208
- # becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
209
- #
210
- epoch = date_1904? ? 1904 : 1900
211
- offset = date_1904? ? 4 : 0
212
- norm = 300
213
- range = year - epoch
214
-
215
- # Set month days and check for leap year.
216
- mdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
217
- leap = 0
218
- leap = 1 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
219
- mdays[1] = 29 if leap != 0
220
-
221
- # Some boundary checks
222
- return nil if year < epoch or year > 9999
223
- return nil if month < 1 or month > 12
224
- return nil if day < 1 or day > mdays[month - 1]
225
-
226
- # Accumulate the number of days since the epoch.
227
- days = day # Add days for current month
228
- (0..(month - 2)).each do |m|
229
- days += mdays[m] # Add days for past months
230
- end
231
- days += range * 365 # Add days for past years
232
- days += (range / 4) # Add leapdays
233
- days -= ((range + offset) / 100) # Subtract 100 year leapdays
234
- days += ((range + offset + norm) / 400) # Add 400 year leapdays
235
- days -= leap # Already counted above
236
-
237
- # Adjust for Excel erroneously treating 1900 as a leap year.
238
- days += 1 if !date_1904? and days > 59
239
-
240
- date_time = sprintf("%0.10f", days + seconds)
241
- date_time = date_time.sub(/\.?0+$/, '') if date_time =~ /\./
242
- if date_time =~ /\./
243
- date_time.to_f
244
- else
245
- date_time.to_i
246
- end
247
- end
248
-
249
- def escape_url(url)
250
- unless url =~ /%[0-9a-fA-F]{2}/
251
- # Escape the URL escape symbol.
252
- url = url.gsub("%", "%25")
253
-
254
- # Escape whitespae in URL.
255
- url = url.gsub(/[\s\x00]/, '%20')
256
-
257
- # Escape other special characters in URL.
258
- re = /(["<>\[\]`^{}])/
259
- while re =~ url
260
- match = $~[1]
261
- url = url.sub(re, sprintf("%%%x", match.ord))
262
- end
263
- end
264
-
265
- url
266
- end
267
-
268
- def absolute_char(absolute)
269
- absolute ? '$' : ''
270
- end
271
-
272
- def xml_str
273
- @writer.string
274
- end
275
-
276
- def self.delete_files(path)
277
- if FileTest.file?(path)
278
- File.delete(path)
279
- elsif FileTest.directory?(path)
280
- Dir.foreach(path) do |file|
281
- next if file =~ /^\.\.?$/ # '.' or '..'
282
-
283
- delete_files(path.sub(%r{/+$}, "") + '/' + file)
284
- end
285
- Dir.rmdir(path)
286
- end
287
- end
288
-
289
- def put_deprecate_message(method)
290
- warn("Warning: calling deprecated method #{method}. This method will be removed in a future release.")
291
- end
292
-
293
- # Check for a cell reference in A1 notation and substitute row and column
294
- def row_col_notation(row_or_a1) # :nodoc:
295
- substitute_cellref(row_or_a1) if row_or_a1.respond_to?(:match) && row_or_a1.to_s =~ /^\D/
296
- end
297
-
298
- #
299
- # Substitute an Excel cell reference in A1 notation for zero based row and
300
- # column values in an argument list.
301
- #
302
- # Ex: ("A4", "Hello") is converted to (3, 0, "Hello").
303
- #
304
- def substitute_cellref(cell, *args) # :nodoc:
305
- # return [*args] if cell.respond_to?(:coerce) # Numeric
306
-
307
- normalized_cell = cell.upcase
308
-
309
- case normalized_cell
310
- # Convert a column range: 'A:A' or 'B:G'.
311
- # A range such as A:A is equivalent to A1:65536, so add rows as required
312
- when /\$?([A-Z]{1,3}):\$?([A-Z]{1,3})/
313
- row1, col1 = xl_cell_to_rowcol(::Regexp.last_match(1) + '1')
314
- row2, col2 = xl_cell_to_rowcol(::Regexp.last_match(2) + ROW_MAX.to_s)
315
- [row1, col1, row2, col2, *args]
316
- # Convert a cell range: 'A1:B7'
317
- when /\$?([A-Z]{1,3}\$?\d+):\$?([A-Z]{1,3}\$?\d+)/
318
- row1, col1 = xl_cell_to_rowcol(::Regexp.last_match(1))
319
- row2, col2 = xl_cell_to_rowcol(::Regexp.last_match(2))
320
- [row1, col1, row2, col2, *args]
321
- # Convert a cell reference: 'A1' or 'AD2000'
322
- when /\$?([A-Z]{1,3}\$?\d+)/
323
- row1, col1 = xl_cell_to_rowcol(::Regexp.last_match(1))
324
- [row1, col1, *args]
325
- else
326
- raise("Unknown cell reference #{normalized_cell}")
327
- end
328
- end
329
-
330
- def underline_attributes(underline)
331
- if underline == 2
332
- [%w[val double]]
333
- elsif underline == 33
334
- [%w[val singleAccounting]]
335
- elsif underline == 34
336
- [%w[val doubleAccounting]]
337
- else
338
- [] # Default to single underline.
339
- end
340
- end
341
-
342
- #
343
- # Write the <color> element.
344
- #
345
- def write_color(name, value, writer = @writer) # :nodoc:
346
- attributes = [[name, value]]
347
-
348
- writer.empty_tag('color', attributes)
349
- end
350
-
351
- PERL_TRUE_VALUES = [false, nil, 0, "0", "", [], {}].freeze
352
- #
353
- # return perl's boolean result
354
- #
355
- def ptrue?(value)
356
- if PERL_TRUE_VALUES.include?(value)
357
- false
358
- else
359
- true
360
- end
361
- end
362
-
363
- def check_parameter(params, valid_keys, method)
364
- invalids = params.keys - valid_keys
365
- unless invalids.empty?
366
- raise WriteXLSXOptionParameterError,
367
- "Unknown parameter '#{invalids.join(", ")}' in #{method}."
368
- end
369
- true
370
- end
371
-
372
- #
373
- # Check that row and col are valid and store max and min values for use in
374
- # other methods/elements.
375
- #
376
- def check_dimensions_and_update_max_min_values(row, col, ignore_row = 0, ignore_col = 0) # :nodoc:
377
- check_dimensions(row, col)
378
- store_row_max_min_values(row) if ignore_row == 0
379
- store_col_max_min_values(col) if ignore_col == 0
380
-
381
- 0
382
- end
383
-
384
- def store_row_max_min_values(row)
385
- @dim_rowmin = row if !@dim_rowmin || (row < @dim_rowmin)
386
- @dim_rowmax = row if !@dim_rowmax || (row > @dim_rowmax)
387
- end
388
-
389
- def store_col_max_min_values(col)
390
- @dim_colmin = col if !@dim_colmin || (col < @dim_colmin)
391
- @dim_colmax = col if !@dim_colmax || (col > @dim_colmax)
392
- end
393
-
394
- def float_to_str(float)
395
- return '' unless float
396
-
397
- if float == float.to_i
398
- float.to_i.to_s
399
- else
400
- float.to_s
401
- end
402
- end
403
-
404
- #
405
- # Convert user defined legend properties to the structure required internally.
406
- #
407
- def legend_properties(params)
408
- legend = Writexlsx::Chart::Legend.new
409
-
410
- legend.position = params[:position] || 'right'
411
- legend.delete_series = params[:delete_series]
412
- legend.font = convert_font_args(params[:font])
413
-
414
- # Set the legend layout.
415
- legend.layout = layout_properties(params[:layout])
416
-
417
- # Turn off the legend.
418
- legend.position = 'none' if params[:none]
419
-
420
- # Set the line properties for the legend.
421
- line = line_properties(params[:line])
422
-
423
- # Allow 'border' as a synonym for 'line'.
424
- line = line_properties(params[:border]) if params[:border]
425
-
426
- # Set the fill properties for the legend.
427
- fill = fill_properties(params[:fill])
428
-
429
- # Set the pattern properties for the legend.
430
- pattern = pattern_properties(params[:pattern])
431
-
432
- # Set the gradient fill properties for the legend.
433
- gradient = gradient_properties(params[:gradient])
434
-
435
- # Pattern fill overrides solid fill.
436
- fill = nil if pattern
437
-
438
- # Gradient fill overrides solid and pattern fills.
439
- if gradient
440
- pattern = nil
441
- fill = nil
442
- end
443
-
444
- # Set the legend layout.
445
- layout = layout_properties(params[:layout])
446
-
447
- legend.line = line
448
- legend.fill = fill
449
- legend.pattern = pattern
450
- legend.gradient = gradient
451
- legend.layout = layout
452
-
453
- @legend = legend
454
- end
455
-
456
- #
457
- # Convert user defined layout properties to the format required internally.
458
- #
459
- def layout_properties(args, is_text = false)
460
- return unless ptrue?(args)
461
-
462
- properties = is_text ? %i[x y] : %i[x y width height]
463
-
464
- # Check for valid properties.
465
- args.keys.each do |key|
466
- raise "Property '#{key}' not allowed in layout options\n" unless properties.include?(key.to_sym)
467
- end
468
-
469
- # Set the layout properties
470
- layout = {}
471
- properties.each do |property|
472
- value = args[property]
473
- # Convert to the format used by Excel for easier testing.
474
- layout[property] = sprintf("%.17g", value)
475
- end
476
-
477
- layout
478
- end
479
-
480
- #
481
- # Convert vertices from pixels to points.
482
- #
483
- def pixels_to_points(vertices)
484
- _col_start, _row_start, _x1, _y1,
485
- _col_end, _row_end, _x2, _y2,
486
- left, top, width, height = vertices.flatten
487
-
488
- left *= 0.75
489
- top *= 0.75
490
- width *= 0.75
491
- height *= 0.75
492
-
493
- [left, top, width, height]
494
- end
495
-
496
- def v_shape_attributes_base(id)
497
- [
498
- ['id', "_x0000_s#{id}"],
499
- ['type', type]
500
- ]
501
- end
502
-
503
- def v_shape_style_base(z_index, vertices)
504
- left, top, width, height = pixels_to_points(vertices)
505
-
506
- left_str = float_to_str(left)
507
- top_str = float_to_str(top)
508
- width_str = float_to_str(width)
509
- height_str = float_to_str(height)
510
- z_index_str = float_to_str(z_index)
511
-
512
- shape_style_base(left_str, top_str, width_str, height_str, z_index_str)
513
- end
514
-
515
- def shape_style_base(left_str, top_str, width_str, height_str, z_index_str)
516
- [
517
- 'position:absolute;',
518
- 'margin-left:',
519
- left_str, 'pt;',
520
- 'margin-top:',
521
- top_str, 'pt;',
522
- 'width:',
523
- width_str, 'pt;',
524
- 'height:',
525
- height_str, 'pt;',
526
- 'z-index:',
527
- z_index_str, ';'
528
- ]
529
- end
530
-
531
- #
532
- # Write the <v:fill> element.
533
- #
534
- def write_fill
535
- @writer.empty_tag('v:fill', fill_attributes)
536
- end
537
-
538
- #
539
- # Write the <v:path> element.
540
- #
541
- def write_comment_path(gradientshapeok, connecttype)
542
- attributes = []
543
-
544
- attributes << %w[gradientshapeok t] if gradientshapeok
545
- attributes << ['o:connecttype', connecttype]
546
-
547
- @writer.empty_tag('v:path', attributes)
548
- end
549
-
550
- #
551
- # Write the <x:Anchor> element.
552
- #
553
- def write_anchor
554
- col_start, row_start, x1, y1, col_end, row_end, x2, y2 = @vertices
555
- data = [col_start, x1, row_start, y1, col_end, x2, row_end, y2].join(', ')
556
-
557
- @writer.data_element('x:Anchor', data)
558
- end
559
-
560
- #
561
- # Write the <x:AutoFill> element.
562
- #
563
- def write_auto_fill
564
- @writer.data_element('x:AutoFill', 'False')
565
- end
566
-
567
- #
568
- # Write the <div> element.
569
- #
570
- def write_div(align, font = nil)
571
- style = "text-align:#{align}"
572
- attributes = [['style', style]]
573
-
574
- @writer.tag_elements('div', attributes) do
575
- if font
576
- # Write the font element.
577
- write_font(font)
578
- end
579
- end
580
- end
581
-
582
- #
583
- # Write the <font> element.
584
- #
585
- def write_font(font)
586
- caption = font[:_caption]
587
- face = 'Calibri'
588
- size = 220
589
- color = '#000000'
590
-
591
- attributes = [
592
- ['face', face],
593
- ['size', size],
594
- ['color', color]
595
- ]
596
- @writer.data_element('font', caption, attributes)
597
- end
598
-
599
- #
600
- # Write the <v:stroke> element.
601
- #
602
- def write_stroke
603
- attributes = [%w[joinstyle miter]]
604
-
605
- @writer.empty_tag('v:stroke', attributes)
606
- end
607
-
608
- def r_id_attributes(id)
609
- ['r:id', "rId#{id}"]
610
- end
611
-
612
- def write_xml_declaration
613
- @writer.xml_decl
614
- yield
615
- @writer.crlf
616
- @writer.close
617
- end
618
-
619
- #
620
- # Convert user defined line properties to the structure required internally.
621
- #
622
- def line_properties(line) # :nodoc:
623
- line_fill_properties(line) do
624
- value_or_raise(dash_types, line[:dash_type], 'dash type')
625
- end
626
- end
627
-
628
- #
629
- # Convert user defined fill properties to the structure required internally.
630
- #
631
- def fill_properties(fill) # :nodoc:
632
- line_fill_properties(fill)
633
- end
634
-
635
- #
636
- # Convert user defined pattern properties to the structure required internally.
637
- #
638
- def pattern_properties(args) # :nodoc:
639
- pattern = {}
640
-
641
- return nil unless args
642
-
643
- # Check the pattern type is present.
644
- return nil unless args.has_key?(:pattern)
645
-
646
- # Check the foreground color is present.
647
- retuen nil unless args.has_key?(:fg_color)
648
-
649
- types = {
650
- 'percent_5' => 'pct5',
651
- 'percent_10' => 'pct10',
652
- 'percent_20' => 'pct20',
653
- 'percent_25' => 'pct25',
654
- 'percent_30' => 'pct30',
655
- 'percent_40' => 'pct40',
656
-
657
- 'percent_50' => 'pct50',
658
- 'percent_60' => 'pct60',
659
- 'percent_70' => 'pct70',
660
- 'percent_75' => 'pct75',
661
- 'percent_80' => 'pct80',
662
- 'percent_90' => 'pct90',
663
-
664
- 'light_downward_diagonal' => 'ltDnDiag',
665
- 'light_upward_diagonal' => 'ltUpDiag',
666
- 'dark_downward_diagonal' => 'dkDnDiag',
667
- 'dark_upward_diagonal' => 'dkUpDiag',
668
- 'wide_downward_diagonal' => 'wdDnDiag',
669
- 'wide_upward_diagonal' => 'wdUpDiag',
670
-
671
- 'light_vertical' => 'ltVert',
672
- 'light_horizontal' => 'ltHorz',
673
- 'narrow_vertical' => 'narVert',
674
- 'narrow_horizontal' => 'narHorz',
675
- 'dark_vertical' => 'dkVert',
676
- 'dark_horizontal' => 'dkHorz',
677
-
678
- 'dashed_downward_diagonal' => 'dashDnDiag',
679
- 'dashed_upward_diagonal' => 'dashUpDiag',
680
- 'dashed_horizontal' => 'dashHorz',
681
- 'dashed_vertical' => 'dashVert',
682
- 'small_confetti' => 'smConfetti',
683
- 'large_confetti' => 'lgConfetti',
684
-
685
- 'zigzag' => 'zigZag',
686
- 'wave' => 'wave',
687
- 'diagonal_brick' => 'diagBrick',
688
- 'horizontal_brick' => 'horzBrick',
689
- 'weave' => 'weave',
690
- 'plaid' => 'plaid',
691
-
692
- 'divot' => 'divot',
693
- 'dotted_grid' => 'dotGrid',
694
- 'dotted_diamond' => 'dotDmnd',
695
- 'shingle' => 'shingle',
696
- 'trellis' => 'trellis',
697
- 'sphere' => 'sphere',
698
-
699
- 'small_grid' => 'smGrid',
700
- 'large_grid' => 'lgGrid',
701
- 'small_check' => 'smCheck',
702
- 'large_check' => 'lgCheck',
703
- 'outlined_diamond' => 'openDmnd',
704
- 'solid_diamond' => 'solidDmnd'
705
- }
706
-
707
- # Check for valid types.
708
- if types[args[:pattern]]
709
- pattern[:pattern] = types[args[:pattern]]
710
- else
711
- raise "Unknown pattern type '#{args[:pattern]}'"
712
- end
713
-
714
- pattern[:bg_color] = args[:bg_color] || '#FFFFFF'
715
- pattern[:fg_color] = args[:fg_color]
716
-
717
- pattern
718
- end
719
-
720
- def line_fill_properties(params)
721
- return { _defined: 0 } unless params
722
-
723
- ret = params.dup
724
- ret[:dash_type] = yield if block_given? && ret[:dash_type]
725
- ret[:_defined] = 1
726
- ret
727
- end
728
-
729
- def dash_types
730
- {
731
- solid: 'solid',
732
- round_dot: 'sysDot',
733
- square_dot: 'sysDash',
734
- dash: 'dash',
735
- dash_dot: 'dashDot',
736
- long_dash: 'lgDash',
737
- long_dash_dot: 'lgDashDot',
738
- long_dash_dot_dot: 'lgDashDotDot',
739
- dot: 'dot',
740
- system_dash_dot: 'sysDashDot',
741
- system_dash_dot_dot: 'sysDashDotDot'
742
- }
743
- end
744
-
745
- def value_or_raise(hash, key, msg)
746
- raise "Unknown #{msg} '#{key}'" if hash[key.to_sym].nil?
747
-
748
- hash[key.to_sym]
749
- end
750
-
751
- def palette_color_from_index(index)
752
- # Adjust the colour index.
753
- idx = index - 8
754
-
755
- r, g, b = @palette[idx]
756
- sprintf("%02X%02X%02X", r, g, b)
757
- end
758
-
759
- #
760
- # Convert user defined font values into private hash values.
761
- #
762
- def convert_font_args(params)
763
- return unless params
764
-
765
- font = params_to_font(params)
766
-
767
- # Convert font size units.
768
- font[:_size] *= 100 if font[:_size] && font[:_size] != 0
769
-
770
- # Convert rotation into 60,000ths of a degree.
771
- font[:_rotation] = 60_000 * font[:_rotation].to_i if ptrue?(font[:_rotation])
772
-
773
- font
774
- end
775
-
776
- def params_to_font(params)
777
- {
778
- _name: params[:name],
779
- _color: params[:color],
780
- _size: params[:size],
781
- _bold: params[:bold],
782
- _italic: params[:italic],
783
- _underline: params[:underline],
784
- _pitch_family: params[:pitch_family],
785
- _charset: params[:charset],
786
- _baseline: params[:baseline] || 0,
787
- _rotation: params[:rotation]
788
- }
789
- end
790
-
791
- #
792
- # Write the <c:txPr> element.
793
- #
794
- def write_tx_pr(font, is_y_axis = nil) # :nodoc:
795
- rotation = nil
796
- rotation = font[:_rotation] if font && font.respond_to?(:[]) && font[:_rotation]
797
- @writer.tag_elements('c:txPr') do
798
- # Write the a:bodyPr element.
799
- write_a_body_pr(rotation, is_y_axis)
800
- # Write the a:lstStyle element.
801
- write_a_lst_style
802
- # Write the a:p element.
803
- write_a_p_formula(font)
804
- end
805
- end
806
-
807
- #
808
- # Write the <a:bodyPr> element.
809
- #
810
- def write_a_body_pr(rot, is_y_axis = nil) # :nodoc:
811
- rot = -5400000 if !rot && ptrue?(is_y_axis)
812
- attributes = []
813
- if rot
814
- if rot == 16_200_000
815
- # 270 deg/stacked angle.
816
- attributes << ['rot', 0]
817
- attributes << %w[vert wordArtVert]
818
- elsif rot == 16_260_000
819
- # 271 deg/stacked angle.
820
- attributes << ['rot', 0]
821
- attributes << %w[vert eaVert]
822
- else
823
- attributes << ['rot', rot]
824
- attributes << %w[vert horz]
825
- end
826
- end
827
-
828
- @writer.empty_tag('a:bodyPr', attributes)
829
- end
830
-
831
- #
832
- # Write the <a:lstStyle> element.
833
- #
834
- def write_a_lst_style # :nodoc:
835
- @writer.empty_tag('a:lstStyle')
836
- end
837
-
838
- #
839
- # Write the <a:p> element for formula titles.
840
- #
841
- def write_a_p_formula(font = nil) # :nodoc:
842
- @writer.tag_elements('a:p') do
843
- # Write the a:pPr element.
844
- write_a_p_pr_formula(font)
845
- # Write the a:endParaRPr element.
846
- write_a_end_para_rpr
847
- end
848
- end
849
-
850
- #
851
- # Write the <a:pPr> element for formula titles.
852
- #
853
- def write_a_p_pr_formula(font) # :nodoc:
854
- @writer.tag_elements('a:pPr') { write_a_def_rpr(font) }
855
- end
856
-
857
- #
858
- # Write the <a:defRPr> element.
859
- #
860
- def write_a_def_rpr(font = nil) # :nodoc:
861
- write_def_rpr_r_pr_common(
862
- font,
863
- get_font_style_attributes(font),
864
- 'a:defRPr'
865
- )
866
- end
867
-
868
- def write_def_rpr_r_pr_common(font, style_attributes, tag) # :nodoc:
869
- latin_attributes = get_font_latin_attributes(font)
870
- has_color = ptrue?(font) && ptrue?(font[:_color])
871
-
872
- if !latin_attributes.empty? || has_color
873
- @writer.tag_elements(tag, style_attributes) do
874
- write_a_solid_fill(color: font[:_color]) if has_color
875
- write_a_latin(latin_attributes) unless latin_attributes.empty?
876
- end
877
- else
878
- @writer.empty_tag(tag, style_attributes)
879
- end
880
- end
881
-
882
- #
883
- # Get the font latin attributes from a font hash.
884
- #
885
- def get_font_latin_attributes(font)
886
- return [] unless font
887
- return [] unless font.respond_to?(:[])
888
-
889
- attributes = []
890
- attributes << ['typeface', font[:_name]] if ptrue?(font[:_name])
891
- attributes << ['pitchFamily', font[:_pitch_family]] if font[:_pitch_family]
892
- attributes << ['charset', font[:_charset]] if font[:_charset]
893
-
894
- attributes
895
- end
896
-
897
- #
898
- # Write the <a:solidFill> element.
899
- #
900
- def write_a_solid_fill(fill) # :nodoc:
901
- @writer.tag_elements('a:solidFill') do
902
- if fill[:color]
903
- # Write the a:srgbClr element.
904
- write_a_srgb_clr(color(fill[:color]), fill[:transparency])
905
- end
906
- end
907
- end
908
-
909
- #
910
- # Write the <a:srgbClr> element.
911
- #
912
- def write_a_srgb_clr(color, transparency = nil) # :nodoc:
913
- tag = 'a:srgbClr'
914
- attributes = [['val', color]]
915
-
916
- if ptrue?(transparency)
917
- @writer.tag_elements(tag, attributes) do
918
- write_a_alpha(transparency)
919
- end
920
- else
921
- @writer.empty_tag(tag, attributes)
922
- end
923
- end
924
-
925
- #
926
- # Convert the user specified colour index or string to a rgb colour.
927
- #
928
- def color(color_code) # :nodoc:
929
- if color_code and color_code =~ /^#[0-9a-fA-F]{6}$/
930
- # Convert a HTML style #RRGGBB color.
931
- color_code.sub(/^#/, '').upcase
932
- else
933
- index = Format.color(color_code)
934
- raise "Unknown color '#{color_code}' used in chart formatting." unless index
935
-
936
- palette_color_from_index(index)
937
- end
938
- end
939
-
940
- #
941
- # Get the font style attributes from a font hash.
942
- #
943
- def get_font_style_attributes(font)
944
- return [] unless font
945
- return [] unless font.respond_to?(:[])
946
-
947
- attributes = []
948
- attributes << ['sz', font[:_size]] if ptrue?(font[:_size])
949
- attributes << ['b', font[:_bold]] if font[:_bold]
950
- attributes << ['i', font[:_italic]] if font[:_italic]
951
- attributes << %w[u sng] if font[:_underline]
952
-
953
- # Turn off baseline when testing fonts that don't have it.
954
- attributes << ['baseline', font[:_baseline]] if font[:_baseline] != -1
955
- attributes
956
- end
957
-
958
- #
959
- # Write the <a:endParaRPr> element.
960
- #
961
- def write_a_end_para_rpr # :nodoc:
962
- @writer.empty_tag('a:endParaRPr', [%w[lang en-US]])
963
- end
964
-
965
- private
966
-
967
- def already_quoted_sheetname?(name)
968
- name.start_with?("'")
969
- end
970
-
971
- def escape_sheetname(name)
972
- name.gsub("'", "''")
973
- end
974
-
975
- def sheetname_needs_quoting?(name)
976
- contains_non_identifier_chars?(name) ||
977
- starts_with_digit_or_dot?(name) ||
978
- valid_a1_reference_name?(name) ||
979
- starts_with_rc_reference?(name) ||
980
- single_rc_reference?(name)
981
- end
982
-
983
- def contains_non_identifier_chars?(name)
984
- name.match?(/[^\p{L}\p{N}_.]/)
985
- end
986
-
987
- def starts_with_digit_or_dot?(name)
988
- name.match?(/^[\p{N}.]/)
989
- end
990
-
991
- def valid_a1_reference_name?(name)
992
- upcased = name.upcase
993
- return false unless upcased.match?(/^[A-Z]{1,3}\d+$/)
994
-
995
- row, col = xl_cell_to_rowcol(upcased)
996
- row.between?(0, 1_048_575) && col.between?(0, 16_383)
997
- end
998
-
999
- def starts_with_rc_reference?(name)
1000
- upcased = name.upcase
1001
-
1002
- if (match = upcased.match(/^R(\d+)/))
1003
- return match[1].to_i.between?(1, 1_048_576)
1004
- end
1005
-
1006
- if (match = upcased.match(/^R?C(\d+)/))
1007
- return match[1].to_i.between?(1, 16_384)
1008
- end
1009
-
1010
- false
1011
- end
1012
-
1013
- def single_rc_reference?(name)
1014
- %w[R C RC].include?(name.upcase)
1015
- end
1016
- end
1017
-
1018
- module WriteDPtPoint
1019
- #
1020
- # Write an individual <c:dPt> element. Override the parent method to add
1021
- # markers.
1022
- #
1023
- def write_d_pt_point(index, point)
1024
- @writer.tag_elements('c:dPt') do
1025
- # Write the c:idx element.
1026
- write_idx(index)
1027
- @writer.tag_elements('c:marker') do
1028
- # Write the c:spPr element.
1029
- write_sp_pr(point)
1030
- end
1031
- end
1032
- end
1033
- end
1034
- end