summaryrefslogtreecommitdiffhomepage
path: root/lib
diff options
context:
space:
mode:
authorJean Jacques Warmerdam <[email protected]>2013-07-24 12:32:54 +0200
committerJean Jacques Warmerdam <[email protected]>2013-07-24 12:32:54 +0200
commit88ee2b1ca8aee6bd14c838f247654f43c073fd2e (patch)
treef5414ad991971477ac7b0286ebb3067dc5ca2cf7 /lib
parent35d3cc8b21bce0c4ce7b9ec4e29d52df4b5f9cc4 (diff)
parent7fb6629b6f1e56b3e012613ec8cfcda8628c0ca5 (diff)
downloadcaxlsx-88ee2b1ca8aee6bd14c838f247654f43c073fd2e.tar.gz
caxlsx-88ee2b1ca8aee6bd14c838f247654f43c073fd2e.zip
Merge branch 'master' of https://github.com/randym/axlsx
Diffstat (limited to 'lib')
-rw-r--r--lib/axlsx/doc_props/core.rb7
-rw-r--r--lib/axlsx/drawing/axes.rb61
-rw-r--r--lib/axlsx/drawing/axis.rb29
-rw-r--r--lib/axlsx/drawing/bar_3D_chart.rb28
-rw-r--r--lib/axlsx/drawing/cat_axis.rb12
-rw-r--r--lib/axlsx/drawing/chart.rb24
-rw-r--r--lib/axlsx/drawing/d_lbls.rb2
-rw-r--r--lib/axlsx/drawing/drawing.rb18
-rw-r--r--lib/axlsx/drawing/graphic_frame.rb11
-rw-r--r--lib/axlsx/drawing/hyperlink.rb17
-rw-r--r--lib/axlsx/drawing/line_3D_chart.rb97
-rw-r--r--lib/axlsx/drawing/line_chart.rb99
-rw-r--r--lib/axlsx/drawing/line_series.rb22
-rw-r--r--lib/axlsx/drawing/pic.rb13
-rw-r--r--lib/axlsx/drawing/scatter_chart.rb44
-rw-r--r--lib/axlsx/drawing/ser_axis.rb31
-rw-r--r--lib/axlsx/drawing/val_axis.rb27
-rw-r--r--lib/axlsx/drawing/vml_shape.rb95
-rw-r--r--lib/axlsx/package.rb42
-rw-r--r--lib/axlsx/rels/relationship.rb81
-rw-r--r--lib/axlsx/rels/relationships.rb9
-rw-r--r--lib/axlsx/stylesheet/styles.rb6
-rw-r--r--lib/axlsx/util/serialized_attributes.rb1
-rw-r--r--lib/axlsx/util/simple_typed_list.rb46
-rw-r--r--lib/axlsx/util/validators.rb7
-rw-r--r--lib/axlsx/version.rb2
-rw-r--r--lib/axlsx/workbook/shared_strings_table.rb14
-rw-r--r--lib/axlsx/workbook/workbook.rb33
-rw-r--r--lib/axlsx/workbook/worksheet/comment.rb44
-rw-r--r--lib/axlsx/workbook/worksheet/comments.rb6
-rw-r--r--lib/axlsx/workbook/worksheet/conditional_formatting_rule.rb8
-rw-r--r--lib/axlsx/workbook/worksheet/pivot_table.rb31
-rw-r--r--lib/axlsx/workbook/worksheet/pivot_table_cache_definition.rb5
-rw-r--r--lib/axlsx/workbook/worksheet/pivot_tables.rb2
-rw-r--r--lib/axlsx/workbook/worksheet/table.rb5
-rw-r--r--lib/axlsx/workbook/worksheet/tables.rb2
-rw-r--r--lib/axlsx/workbook/worksheet/worksheet.rb39
-rw-r--r--lib/axlsx/workbook/worksheet/worksheet_comments.rb11
-rw-r--r--lib/axlsx/workbook/worksheet/worksheet_drawing.rb12
-rw-r--r--lib/axlsx/workbook/worksheet/worksheet_hyperlink.rb15
-rw-r--r--lib/schema/sml.xsd4
41 files changed, 639 insertions, 423 deletions
diff --git a/lib/axlsx/doc_props/core.rb b/lib/axlsx/doc_props/core.rb
index d71300e0..a74031f2 100644
--- a/lib/axlsx/doc_props/core.rb
+++ b/lib/axlsx/doc_props/core.rb
@@ -8,14 +8,19 @@ module Axlsx
# Creates a new Core object.
# @option options [String] creator
+ # @option options [Time] created
def initialize(options={})
@creator = options[:creator] || 'axlsx'
+ @created = options[:created]
end
# The author of the document. By default this is 'axlsx'
# @return [String]
attr_accessor :creator
+ # Creation time of the document. If nil, the current time will be used.
+ attr_accessor :created
+
# serializes the core.xml document
# @return [String]
def to_xml_string(str = '')
@@ -24,7 +29,7 @@ module Axlsx
str << 'xmlns:dcmitype="' << CORE_NS_DCMIT << '" xmlns:dcterms="' << CORE_NS_DCT << '" '
str << 'xmlns:xsi="' << CORE_NS_XSI << '">'
str << '<dc:creator>' << self.creator << '</dc:creator>'
- str << '<dcterms:created xsi:type="dcterms:W3CDTF">' << Time.now.strftime('%Y-%m-%dT%H:%M:%S') << 'Z</dcterms:created>'
+ str << '<dcterms:created xsi:type="dcterms:W3CDTF">' << (created || Time.now).strftime('%Y-%m-%dT%H:%M:%S') << 'Z</dcterms:created>'
str << '<cp:revision>0</cp:revision>'
str << '</cp:coreProperties>'
end
diff --git a/lib/axlsx/drawing/axes.rb b/lib/axlsx/drawing/axes.rb
new file mode 100644
index 00000000..bc40e532
--- /dev/null
+++ b/lib/axlsx/drawing/axes.rb
@@ -0,0 +1,61 @@
+module Axlsx
+
+ # The Axes class creates and manages axis information and
+ # serialization for charts.
+ class Axes
+
+ # @param [Hash] options options used to generate axis each key
+ # should be an axis name like :val_axis and its value should be the
+ # class of the axis type to construct. The :cat_axis, if there is one,
+ # must come first (we assume a Ruby 1.9+ Hash or an OrderedHash).
+ def initialize(options={})
+ raise(ArgumentError, "CatAxis must come first") if options.keys.include?(:cat_axis) && options.keys.first != :cat_axis
+ options.each do |name, axis_class|
+ add_axis(name, axis_class)
+ end
+ end
+
+ # [] provides assiciative access to a specic axis store in an axes
+ # instance.
+ # @return [Axis]
+ def [](name)
+ axes.assoc(name)[1]
+ end
+
+ # Serializes the object
+ # @param [String] str
+ # @param [Hash] options
+ # @option options ids
+ # If the ids option is specified only the axis identifier is
+ # serialized. Otherwise, each axis is serialized in full.
+ def to_xml_string(str = '', options = {})
+ if options[:ids]
+ # CatAxis must come first in the XML (for Microsoft Excel at least)
+ sorted = axes.sort_by { |name, axis| axis.kind_of?(CatAxis) ? 0 : 1 }
+ sorted.inject(str) { |string, axis| string << '<c:axId val="' << axis[1].id.to_s << '"/>' }
+ else
+ axes.each { |axis| axis[1].to_xml_string(str) }
+ end
+ end
+
+ # Adds an axis to the collection
+ # @param [Symbol] name The name of the axis
+ # @param [Axis] axis_class The axis class to generate
+ def add_axis(name, axis_class)
+ axis = axis_class.new
+ set_cross_axis(axis)
+ axes << [name, axis]
+ end
+
+ private
+
+ def axes
+ @axes ||= []
+ end
+
+ def set_cross_axis(axis)
+ axes.first[1].cross_axis = axis if axes.size == 1
+ axis.cross_axis = axes.first[1] unless axes.empty?
+ end
+ end
+end
diff --git a/lib/axlsx/drawing/axis.rb b/lib/axlsx/drawing/axis.rb
index 1b55bece..32e40373 100644
--- a/lib/axlsx/drawing/axis.rb
+++ b/lib/axlsx/drawing/axis.rb
@@ -7,17 +7,13 @@ module Axlsx
include Axlsx::OptionsParser
# Creates an Axis object
- # @param [Integer] ax_id the id of this axis
- # @param [Integer] cross_ax the id of the perpendicular axis
+ # @option options [Axis] cross_axis the perpendicular axis
# @option options [Symbol] ax_pos
# @option options [Symbol] crosses
# @option options [Symbol] tick_lbl_pos
# @raise [ArgumentError] If axi_id or cross_ax are not unsigned integers
- def initialize(ax_id, cross_ax, options={})
- Axlsx::validate_unsigned_int(ax_id)
- Axlsx::validate_unsigned_int(cross_ax)
- @ax_id = ax_id
- @cross_ax = cross_ax
+ def initialize(options={})
+ @id = rand(8 ** 8)
@format_code = "General"
@delete = @label_rotation = 0
@scaling = Scaling.new(:orientation=>:minMax)
@@ -37,13 +33,13 @@ module Axlsx
# the id of the axis.
# @return [Integer]
- attr_reader :ax_id
- alias :axID :ax_id
+ attr_reader :id
+ alias :axID :id
# The perpendicular axis
# @return [Integer]
- attr_reader :cross_ax
- alias :crossAx :cross_ax
+ attr_reader :cross_axis
+ alias :crossAx :cross_axis
# The scaling of the axis
# @see Scaling
@@ -94,6 +90,13 @@ module Axlsx
def color=(color_rgb)
@color = color_rgb
end
+
+ # The crossing axis for this axis
+ # @param [Axis] axis
+ def cross_axis=(axis)
+ DataTypeValidator.validate "#{self.class}.cross_axis", [Axis], axis
+ @cross_axis = axis
+ end
# The position of the axis
# must be one of [:l, :r, :t, :b]
@@ -147,7 +150,7 @@ module Axlsx
# @param [String] str
# @return [String]
def to_xml_string(str = '')
- str << '<c:axId val="' << @ax_id.to_s << '"/>'
+ str << '<c:axId val="' << @id.to_s << '"/>'
@scaling.to_xml_string str
str << '<c:delete val="'<< @delete.to_s << '"/>'
str << '<c:axPos val="' << @ax_pos.to_s << '"/>'
@@ -175,7 +178,7 @@ module Axlsx
end
# some potential value in implementing this in full. Very detailed!
str << '<c:txPr><a:bodyPr rot="' << @label_rotation.to_s << '"/><a:lstStyle/><a:p><a:pPr><a:defRPr/></a:pPr><a:endParaRPr/></a:p></c:txPr>'
- str << '<c:crossAx val="' << @cross_ax.to_s << '"/>'
+ str << '<c:crossAx val="' << @cross_axis.id.to_s << '"/>'
str << '<c:crosses val="' << @crosses.to_s << '"/>'
end
diff --git a/lib/axlsx/drawing/bar_3D_chart.rb b/lib/axlsx/drawing/bar_3D_chart.rb
index 6f69f703..755f334c 100644
--- a/lib/axlsx/drawing/bar_3D_chart.rb
+++ b/lib/axlsx/drawing/bar_3D_chart.rb
@@ -10,12 +10,16 @@ module Axlsx
# the category axis
# @return [CatAxis]
- attr_reader :cat_axis
+ def cat_axis
+ axes[:cat_axis]
+ end
alias :catAxis :cat_axis
# the value axis
# @return [ValAxis]
- attr_reader :val_axis
+ def val_axis
+ axes[:val_axis]
+ end
alias :valAxis :val_axis
# The direction of the bars in the chart
@@ -75,10 +79,6 @@ module Axlsx
def initialize(frame, options={})
@vary_colors = true
@gap_width, @gap_depth, @shape = nil, nil, nil
- @cat_ax_id = rand(8 ** 8)
- @val_ax_id = rand(8 ** 8)
- @cat_axis = CatAxis.new(@cat_ax_id, @val_ax_id)
- @val_axis = ValAxis.new(@val_ax_id, @cat_ax_id, :tick_lbl_pos => :low, :ax_pos => :l)
super(frame, options)
@series_type = BarSeries
@view_3D = View3D.new({:r_ang_ax=>1}.merge(options))
@@ -131,17 +131,21 @@ module Axlsx
str_inner << '<c:grouping val="' << grouping.to_s << '"/>'
str_inner << '<c:varyColors val="' << vary_colors.to_s << '"/>'
@series.each { |ser| ser.to_xml_string(str_inner) }
- @d_lbls.to_xml_string(str) if @d_lbls
+ @d_lbls.to_xml_string(str_inner) if @d_lbls
str_inner << '<c:gapWidth val="' << @gap_width.to_s << '"/>' unless @gap_width.nil?
str_inner << '<c:gapDepth val="' << @gap_depth.to_s << '"/>' unless @gap_depth.nil?
str_inner << '<c:shape val="' << @shape.to_s << '"/>' unless @shape.nil?
- str_inner << '<c:axId val="' << @cat_ax_id.to_s << '"/>'
- str_inner << '<c:axId val="' << @val_ax_id.to_s << '"/>'
- str_inner << '<c:axId val="0"/>'
+ axes.to_xml_string(str_inner, :ids => true)
str_inner << '</c:bar3DChart>'
- @cat_axis.to_xml_string str_inner
- @val_axis.to_xml_string str_inner
+ axes.to_xml_string(str_inner)
end
end
+
+ # A hash of axes used by this chart. Bar charts have a value and
+ # category axes specified via axes[:val_axes] and axes[:cat_axis]
+ # @return [Axes]
+ def axes
+ @axes ||= Axes.new(:cat_axis => CatAxis, :val_axis => ValAxis)
+ end
end
end
diff --git a/lib/axlsx/drawing/cat_axis.rb b/lib/axlsx/drawing/cat_axis.rb
index ba392ce6..f32c23c9 100644
--- a/lib/axlsx/drawing/cat_axis.rb
+++ b/lib/axlsx/drawing/cat_axis.rb
@@ -4,23 +4,15 @@ module Axlsx
class CatAxis < Axis
# Creates a new CatAxis object
- # @param [Integer] ax_id the id of this axis. Inherited
- # @param [Integer] cross_ax the id of the perpendicular axis. Inherited
- # @option options [Symbol] ax_pos. Inherited
- # @option options [Symbol] tick_lbl_pos. Inherited
- # @option options [Symbol] crosses. Inherited
- # @option options [Boolean] auto
- # @option options [Symbol] lbl_algn
- # @option options [Integer] lbl_offset
# @option options [Integer] tick_lbl_skip
# @option options [Integer] tick_mark_skip
- def initialize(ax_id, cross_ax, options={})
+ def initialize(options={})
@tick_lbl_skip = 1
@tick_mark_skip = 1
self.auto = 1
self.lbl_algn = :ctr
self.lbl_offset = "100"
- super(ax_id, cross_ax, options)
+ super(options)
end
# From the docs: This element specifies that this axis is a date or text axis based on the data that is used for the axis labels, not a specific choice.
diff --git a/lib/axlsx/drawing/chart.rb b/lib/axlsx/drawing/chart.rb
index 79019a0a..c1d408e6 100644
--- a/lib/axlsx/drawing/chart.rb
+++ b/lib/axlsx/drawing/chart.rb
@@ -20,6 +20,7 @@ module Axlsx
@graphic_frame.anchor.drawing.worksheet.workbook.charts << self
@series = SimpleTypedList.new Series
@show_legend = true
+ @display_blanks_as = :gap
@series_type = Series
@title = Title.new
parse_options options
@@ -70,10 +71,19 @@ module Axlsx
# @return [Boolean]
attr_reader :show_legend
- # returns a relationship object for the chart
- # @return [Axlsx::Relationship]
+ # How to display blank values
+ # Options are
+ # * gap: Display nothing
+ # * span: Not sure what this does
+ # * zero: Display as if the value were zero, not blank
+ # @return [Symbol]
+ # Default :gap (although this really should vary by chart type and grouping)
+ attr_reader :display_blanks_as
+
+ # The relationship object for this chart.
+ # @return [Relationship]
def relationship
- Relationship.new(CHART_R, "../#{pn}")
+ Relationship.new(self, CHART_R, "../#{pn}")
end
# The index of this chart in the workbooks charts collection
@@ -105,6 +115,12 @@ module Axlsx
# @return [Boolean]
def show_legend=(v) Axlsx::validate_boolean(v); @show_legend = v; end
+ # How to display blank values
+ # @see display_blanks_as
+ # @param [Symbol] v
+ # @return [Symbol]
+ def display_blanks_as=(v) Axlsx::validate_display_blanks_as(v); @display_blanks_as = v; end
+
# The style for the chart.
# see ECMA Part 1 §21.2.2.196
# @param [Integer] v must be between 1 and 48
@@ -157,7 +173,7 @@ module Axlsx
str << '</c:legend>'
end
str << '<c:plotVisOnly val="1"/>'
- str << '<c:dispBlanksAs val="zero"/>'
+ str << '<c:dispBlanksAs val="' << display_blanks_as.to_s << '"/>'
str << '<c:showDLblsOverMax val="1"/>'
str << '</c:chart>'
str << '<c:printSettings>'
diff --git a/lib/axlsx/drawing/d_lbls.rb b/lib/axlsx/drawing/d_lbls.rb
index 3685e7d2..74c01350 100644
--- a/lib/axlsx/drawing/d_lbls.rb
+++ b/lib/axlsx/drawing/d_lbls.rb
@@ -10,7 +10,7 @@ module Axlsx
include Axlsx::OptionsParser
# creates a new DLbls object
def initialize(chart_type, options={})
- raise ArgumentError, 'chart_type must inherit from Chart' unless chart_type.superclass == Chart
+ raise ArgumentError, 'chart_type must inherit from Chart' unless [Chart, LineChart].include?(chart_type.superclass)
@chart_type = chart_type
initialize_defaults
parse_options options
diff --git a/lib/axlsx/drawing/drawing.rb b/lib/axlsx/drawing/drawing.rb
index 46d5a88b..1748e9f4 100644
--- a/lib/axlsx/drawing/drawing.rb
+++ b/lib/axlsx/drawing/drawing.rb
@@ -22,6 +22,7 @@ module Axlsx
require 'axlsx/drawing/ser_axis.rb'
require 'axlsx/drawing/cat_axis.rb'
require 'axlsx/drawing/val_axis.rb'
+ require 'axlsx/drawing/axes.rb'
require 'axlsx/drawing/marker.rb'
@@ -33,6 +34,7 @@ module Axlsx
require 'axlsx/drawing/chart.rb'
require 'axlsx/drawing/pie_3D_chart.rb'
require 'axlsx/drawing/bar_3D_chart.rb'
+ require 'axlsx/drawing/line_chart.rb'
require 'axlsx/drawing/line_3D_chart.rb'
require 'axlsx/drawing/scatter_chart.rb'
@@ -119,12 +121,6 @@ module Axlsx
@worksheet.workbook.drawings.index(self)
end
- # The relation reference id for this drawing
- # @return [String]
- def rId
- "rId#{index+1}"
- end
-
# The part name for this drawing
# @return [String]
def pn
@@ -138,15 +134,7 @@ module Axlsx
"#{DRAWING_RELS_PN % (index+1)}"
end
- # The index of a chart, image or hyperlink object this drawing contains
- def index_of(object)
- child_objects.index(object)
- end
-
-
- # An ordered list of objects this drawing holds
- # It is important that the objects are returned in the same order each time for
- # releationship indexing in the package
+ # A list of objects this drawing holds.
# @return [Array]
def child_objects
charts + images + hyperlinks
diff --git a/lib/axlsx/drawing/graphic_frame.rb b/lib/axlsx/drawing/graphic_frame.rb
index 943caeae..9b921275 100644
--- a/lib/axlsx/drawing/graphic_frame.rb
+++ b/lib/axlsx/drawing/graphic_frame.rb
@@ -22,15 +22,10 @@ module Axlsx
@chart = chart_type.new(self, options)
end
- # The relationship id for this graphic
+ # The relationship id for this graphic frame.
# @return [String]
- #
- # NOTE: Discontinued. This should not be part of GraphicFrame.
- # The drawing object maintains relationships and needs to be queried to determine the relationship id of any given graphic data child object.
- #
def rId
- warn('axlsx::DEPRECIATED: GraphicFrame#rId has been depreciated. relationship id is determed by the drawing object')
- "rId#{@anchor.index+1}"
+ @anchor.drawing.relationships.for(chart).Id
end
# Serializes the object
@@ -49,7 +44,7 @@ module Axlsx
str << '</xdr:xfrm>'
str << '<a:graphic>'
str << '<a:graphicData uri="' << XML_NS_C << '">'
- str << '<c:chart xmlns:c="' << XML_NS_C << '" xmlns:r="' << XML_NS_R << '" r:id="rId' << (@anchor.drawing.index_of(@chart)+1).to_s << '"/>'
+ str << '<c:chart xmlns:c="' << XML_NS_C << '" xmlns:r="' << XML_NS_R << '" r:id="' << rId << '"/>'
str << '</a:graphicData>'
str << '</a:graphic>'
str << '</xdr:graphicFrame>'
diff --git a/lib/axlsx/drawing/hyperlink.rb b/lib/axlsx/drawing/hyperlink.rb
index e886f766..850baeef 100644
--- a/lib/axlsx/drawing/hyperlink.rb
+++ b/lib/axlsx/drawing/hyperlink.rb
@@ -83,27 +83,20 @@ module Axlsx
# @return [String]
attr_accessor :tooltip
- # Returns a relationship object for this hyperlink
- # @return [Axlsx::Relationship]
+ # The relationship object for this hyperlink.
+ # @return [Relationship]
def relationship
- Relationship.new(HYPERLINK_R, href, :target_mode => :External)
+ Relationship.new(self, HYPERLINK_R, href, :target_mode => :External)
end
+
# Serializes the object
# @param [String] str
# @return [String]
def to_xml_string(str = '')
str << '<a:hlinkClick '
- serialized_attributes str, {:'r:id' => "rId#{id}", :'xmlns:r' => XML_NS_R }
+ serialized_attributes str, {:'r:id' => relationship.Id, :'xmlns:r' => XML_NS_R }
str << '/>'
end
- private
-
- # The relational ID for this hyperlink
- # @return [Integer]
- def id
- @parent.anchor.drawing.index_of(self)+1
- end
-
end
end
diff --git a/lib/axlsx/drawing/line_3D_chart.rb b/lib/axlsx/drawing/line_3D_chart.rb
index b843f48e..183f3250 100644
--- a/lib/axlsx/drawing/line_3D_chart.rb
+++ b/lib/axlsx/drawing/line_3D_chart.rb
@@ -11,7 +11,7 @@ module Axlsx
# ws = p.workbook.add_worksheet
# ws.add_row ["This is a chart with no data in the sheet"]
#
- # chart = ws.add_chart(Axlsx::Line3DChart, :start_at=> [0,1], :end_at=>[0,6], :title=>"Most Popular Pets")
+ # chart = ws.add_chart(Axlsx::Line3DChart, :start_at=> [0,1], :end_at=>[0,6], :t#itle=>"Most Popular Pets")
# chart.add_series :data => [1, 9, 10], :labels => ["Slimy Reptiles", "Fuzzy Bunnies", "Rottweiler"]
#
# @see Worksheet#add_chart
@@ -19,93 +19,50 @@ module Axlsx
# @see Chart#add_series
# @see Series
# @see Package#serialize
- class Line3DChart < Chart
-
- # the category axis
- # @return [CatAxis]
- attr_reader :catAxis
-
- # the category axis
- # @return [ValAxis]
- attr_reader :valAxis
-
- # the category axis
- # @return [Axis]
- attr_reader :serAxis
+ class Line3DChart < Axlsx::LineChart
# space between bar or column clusters, as a percentage of the bar or column width.
# @return [String]
- attr_reader :gapDepth
-
- #grouping for a column, line, or area chart.
- # must be one of [:percentStacked, :clustered, :standard, :stacked]
- # @return [Symbol]
- attr_reader :grouping
+ attr_reader :gap_depth
+ alias :gapDepth :gap_depth
# validation regex for gap amount percent
GAP_AMOUNT_PERCENT = /0*(([0-9])|([1-9][0-9])|([1-4][0-9][0-9])|500)%/
+ # the category axis
+ # @return [Axis]
+ def ser_axis
+ axes[:ser_axis]
+ end
+ alias :serAxis :ser_axis
+
# Creates a new line chart object
- # @param [GraphicFrame] frame The workbook that owns this chart.
- # @option options [Cell, String] title
- # @option options [Boolean] show_legend
- # @option options [Symbol] grouping
- # @option options [String] gapDepth
- # @option options [Integer] rotX
- # @option options [String] hPercent
- # @option options [Integer] rotY
- # @option options [String] depthPercent
- # @option options [Boolean] rAngAx
- # @option options [Integer] perspective
+ # @option options [String] gap_depth
# @see Chart
+ # @see lineChart
# @see View3D
def initialize(frame, options={})
- @vary_colors = false
- @gapDepth = nil
- @grouping = :standard
- @catAxId = rand(8 ** 8)
- @valAxId = rand(8 ** 8)
- @serAxId = rand(8 ** 8)
- @catAxis = CatAxis.new(@catAxId, @valAxId)
- @valAxis = ValAxis.new(@valAxId, @catAxId)
- @serAxis = SerAxis.new(@serAxId, @valAxId)
+ @gap_depth = nil
+ @view_3D = View3D.new({:r_ang_ax=>1}.merge(options))
super(frame, options)
- @series_type = LineSeries
- @view_3D = View3D.new({:perspective=>30}.merge(options))
- @d_lbls = nil
+ axes.add_axis :ser_axis, SerAxis
end
- # @see grouping
- def grouping=(v)
- RestrictionValidator.validate "Bar3DChart.grouping", [:percentStacked, :standard, :stacked], v
- @grouping = v
- end
# @see gapDepth
- def gapDepth=(v)
- RegexValidator.validate "Bar3DChart.gapWidth", GAP_AMOUNT_PERCENT, v
- @gapDepth=(v)
+ def gap_depth=(v)
+ RegexValidator.validate "Line3DChart.gapWidth", GAP_AMOUNT_PERCENT, v
+ @gap_depth=(v)
end
+ alias :gapDepth= :gap_depth=
- # Serializes the object
- # @param [String] str
- # @return [String]
- def to_xml_string(str = '')
- super(str) do |str_inner|
- str_inner << '<c:line3DChart>'
- str_inner << '<c:grouping val="' << grouping.to_s << '"/>'
- str_inner << '<c:varyColors val="' << vary_colors.to_s << '"/>'
- @series.each { |ser| ser.to_xml_string(str_inner) }
- @d_lbls.to_xml_string(str) if @d_lbls
- str_inner << '<c:gapDepth val="' << @gapDepth.to_s << '"/>' unless @gapDepth.nil?
- str_inner << '<c:axId val="' << @catAxId.to_s << '"/>'
- str_inner << '<c:axId val="' << @valAxId.to_s << '"/>'
- str_inner << '<c:axId val="' << @serAxId.to_s << '"/>'
- str_inner << '</c:line3DChart>'
- @catAxis.to_xml_string str_inner
- @valAxis.to_xml_string str_inner
- @serAxis.to_xml_string str_inner
+ # Serializes the object
+ # @param [String] str
+ # @return [String]
+ def to_xml_string(str = '')
+ super(str) do |str_inner|
+ str_inner << '<c:gapDepth val="' << @gap_depth.to_s << '"/>' unless @gap_depth.nil?
+ end
end
- end
end
end
diff --git a/lib/axlsx/drawing/line_chart.rb b/lib/axlsx/drawing/line_chart.rb
new file mode 100644
index 00000000..699050dc
--- /dev/null
+++ b/lib/axlsx/drawing/line_chart.rb
@@ -0,0 +1,99 @@
+# encoding: UTF-8
+module Axlsx
+
+ # The LineChart is a two dimentional line chart (who would have guessed?) that you can add to your worksheet.
+ # @example Creating a chart
+ # # This example creates a line in a single sheet.
+ # require "rubygems" # if that is your preferred way to manage gems!
+ # require "axlsx"
+ #
+ # p = Axlsx::Package.new
+ # ws = p.workbook.add_worksheet
+ # ws.add_row ["This is a chart with no data in the sheet"]
+ #
+ # chart = ws.add_chart(Axlsx::LineChart, :start_at=> [0,1], :end_at=>[0,6], :title=>"Most Popular Pets")
+ # chart.add_series :data => [1, 9, 10], :labels => ["Slimy Reptiles", "Fuzzy Bunnies", "Rottweiler"]
+ #
+ # @see Worksheet#add_chart
+ # @see Worksheet#add_row
+ # @see Chart#add_series
+ # @see Series
+ # @see Package#serialize
+ class LineChart < Chart
+
+ # the category axis
+ # @return [CatAxis]
+ def cat_axis
+ axes[:cat_axis]
+ end
+ alias :catAxis :cat_axis
+
+ # the category axis
+ # @return [ValAxis]
+ def val_axis
+ axes[:val_axis]
+ end
+ alias :valAxis :val_axis
+
+ # must be one of [:percentStacked, :clustered, :standard, :stacked]
+ # @return [Symbol]
+ attr_reader :grouping
+
+ # Creates a new line chart object
+ # @param [GraphicFrame] frame The workbook that owns this chart.
+ # @option options [Cell, String] title
+ # @option options [Boolean] show_legend
+ # @option options [Symbol] grouping
+ # @see Chart
+ def initialize(frame, options={})
+ @vary_colors = false
+ @grouping = :standard
+ super(frame, options)
+ @series_type = LineSeries
+ @d_lbls = nil
+ end
+
+ # @see grouping
+ def grouping=(v)
+ RestrictionValidator.validate "LineChart.grouping", [:percentStacked, :standard, :stacked], v
+ @grouping = v
+ end
+
+ # The node name to use in serialization. As LineChart is used as the
+ # base class for Liine3DChart we need to be sure to serialize the
+ # chart based on the actual class type and not a fixed node name.
+ # @return [String]
+ def node_name
+ path = self.class.to_s
+ if i = path.rindex('::')
+ path = path[(i+2)..-1]
+ end
+ path[0] = path[0].chr.downcase
+ path
+ end
+
+ # Serializes the object
+ # @param [String] str
+ # @return [String]
+ def to_xml_string(str = '')
+ super(str) do |str_inner|
+ str_inner << "<c:" << node_name << ">"
+ str_inner << '<c:grouping val="' << grouping.to_s << '"/>'
+ str_inner << '<c:varyColors val="' << vary_colors.to_s << '"/>'
+ @series.each { |ser| ser.to_xml_string(str_inner) }
+ @d_lbls.to_xml_string(str_inner) if @d_lbls
+ yield str_inner if block_given?
+ axes.to_xml_string(str_inner, :ids => true)
+ str_inner << "</c:" << node_name << ">"
+ axes.to_xml_string(str_inner)
+ end
+ end
+
+ # The axes for this chart. LineCharts have a category and value
+ # axis.
+ # @return [Axes]
+ def axes
+ @axes ||= Axes.new(:cat_axis => CatAxis, :val_axis => ValAxis)
+ end
+ end
+end
diff --git a/lib/axlsx/drawing/line_series.rb b/lib/axlsx/drawing/line_series.rb
index 017822ed..b011e0b6 100644
--- a/lib/axlsx/drawing/line_series.rb
+++ b/lib/axlsx/drawing/line_series.rb
@@ -19,11 +19,16 @@ module Axlsx
# @return [String]
attr_reader :color
+ # show markers on values
+ # @return [Boolean]
+ attr_reader :show_marker
+
# Creates a new series
# @option options [Array, SimpleTypedList] data
# @option options [Array, SimpleTypedList] labels
# @param [Chart] chart
def initialize(chart, options={})
+ @show_marker = false
@labels, @data = nil, nil
super(chart, options)
@labels = AxDataSource.new(:data => options[:labels]) unless options[:labels].nil?
@@ -35,6 +40,12 @@ module Axlsx
@color = v
end
+ # @see show_marker
+ def show_marker=(v)
+ Axlsx::validate_boolean(v)
+ @show_marker = v
+ end
+
# Serializes the object
# @param [String] str
# @return [String]
@@ -43,9 +54,16 @@ module Axlsx
if color
str << '<c:spPr><a:solidFill>'
str << '<a:srgbClr val="' << color << '"/>'
- str << '</a:solidFill></c:spPr>'
+ str << '</a:solidFill>'
+ str << '<a:ln w="28800">'
+ str << '<a:solidFill>'
+ str << '<a:srgbClr val="' << color << '"/>'
+ str << '</a:solidFill>'
+ str << '</a:ln>'
+ str << '<a:round/>'
+ str << '</c:spPr>'
end
-
+ str << '<c:marker><c:symbol val="none"/></c:marker>' unless @show_marker
@labels.to_xml_string(str) unless @labels.nil?
@data.to_xml_string(str) unless @data.nil?
end
diff --git a/lib/axlsx/drawing/pic.rb b/lib/axlsx/drawing/pic.rb
index 8ed8d042..89f4c1ea 100644
--- a/lib/axlsx/drawing/pic.rb
+++ b/lib/axlsx/drawing/pic.rb
@@ -102,15 +102,10 @@ module Axlsx
"#{IMAGE_PN % [(index+1), extname]}"
end
- # The relational id withing the drawing's relationships
- def id
- @anchor.drawing.charts.size + @anchor.drawing.images.index(self) + 1
- end
-
- # Returns a relationship object for this object
- # @return Axlsx::Relationship
+ # The relationship object for this pic.
+ # @return [Relationship]
def relationship
- Relationship.new(IMAGE_R, "../#{pn}")
+ Relationship.new(self, IMAGE_R, "../#{pn}")
end
# providing access to the anchor's width attribute
@@ -177,7 +172,7 @@ module Axlsx
picture_locking.to_xml_string(str)
str << '</xdr:cNvPicPr></xdr:nvPicPr>'
str << '<xdr:blipFill>'
- str << '<a:blip xmlns:r ="' << XML_NS_R << '" r:embed="rId' << id.to_s << '"/>'
+ str << '<a:blip xmlns:r ="' << XML_NS_R << '" r:embed="' << relationship.Id << '"/>'
str << '<a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr>'
str << '<a:xfrm><a:off x="0" y="0"/><a:ext cx="2336800" cy="2161540"/></a:xfrm>'
str << '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic>'
diff --git a/lib/axlsx/drawing/scatter_chart.rb b/lib/axlsx/drawing/scatter_chart.rb
index 2b801642..526bd6d5 100644
--- a/lib/axlsx/drawing/scatter_chart.rb
+++ b/lib/axlsx/drawing/scatter_chart.rb
@@ -12,35 +12,40 @@ module Axlsx
# The Style for the scatter chart
# must be one of :none | :line | :lineMarker | :marker | :smooth | :smoothMarker
# return [Symbol]
- attr_reader :scatterStyle
+ attr_reader :scatter_style
+ alias :scatterStyle :scatter_style
# the x value axis
# @return [ValAxis]
- attr_reader :xValAxis
+ def x_val_axis
+ axes[:x_val_axis]
+ end
+ alias :xValAxis :x_val_axis
# the y value axis
# @return [ValAxis]
- attr_reader :yValAxis
+ def y_val_axis
+ axes[:x_val_axis]
+ end
+ alias :yValAxis :y_val_axis
# Creates a new scatter chart
def initialize(frame, options={})
@vary_colors = 0
- @scatterStyle = :lineMarker
- @xValAxId = rand(8 ** 8)
- @yValAxId = rand(8 ** 8)
- @xValAxis = ValAxis.new(@xValAxId, @yValAxId)
- @yValAxis = ValAxis.new(@yValAxId, @xValAxId)
- super(frame, options)
+ @scatter_style = :lineMarker
+
+ super(frame, options)
@series_type = ScatterSeries
@d_lbls = nil
parse_options options
end
# see #scatterStyle
- def scatterStyle=(v)
+ def scatter_style=(v)
Axlsx.validate_scatter_style(v)
- @scatterStyle = v
+ @scatter_style = v
end
+ alias :scatterStyle= :scatter_style=
# Serializes the object
# @param [String] str
@@ -48,17 +53,22 @@ module Axlsx
def to_xml_string(str = '')
super(str) do |str_inner|
str_inner << '<c:scatterChart>'
- str_inner << '<c:scatterStyle val="' << scatterStyle.to_s << '"/>'
+ str_inner << '<c:scatterStyle val="' << scatter_style.to_s << '"/>'
str_inner << '<c:varyColors val="' << vary_colors.to_s << '"/>'
@series.each { |ser| ser.to_xml_string(str_inner) }
- d_lbls.to_xml_string(str) if @d_lbls
- str_inner << '<c:axId val="' << @xValAxId.to_s << '"/>'
- str_inner << '<c:axId val="' << @yValAxId.to_s << '"/>'
+ d_lbls.to_xml_string(str_inner) if @d_lbls
+ axes.to_xml_string(str_inner, :ids => true)
str_inner << '</c:scatterChart>'
- @xValAxis.to_xml_string str_inner
- @yValAxis.to_xml_string str_inner
+ axes.to_xml_string(str_inner)
end
str
end
+
+ # The axes for the scatter chart. ScatterChart has an x_val_axis and
+ # a y_val_axis
+ # @return [Axes]
+ def axes
+ @axes ||= Axes.new(:x_val_axis => ValAxis, :y_val_axis => ValAxis)
+ end
end
end
diff --git a/lib/axlsx/drawing/ser_axis.rb b/lib/axlsx/drawing/ser_axis.rb
index 00b04989..54e2c60e 100644
--- a/lib/axlsx/drawing/ser_axis.rb
+++ b/lib/axlsx/drawing/ser_axis.rb
@@ -5,30 +5,29 @@ module Axlsx
# The number of tick lables to skip between labels
# @return [Integer]
- attr_reader :tickLblSkip
+ attr_reader :tick_lbl_skip
+ alias :tickLblSkip :tick_lbl_skip
# The number of tickmarks to be skipped before the next one is rendered.
# @return [Boolean]
- attr_reader :tickMarkSkip
+ attr_reader :tick_mark_skip
+ alias :tickMarkSkip :tick_mark_skip
# Creates a new SerAxis object
- # @param [Integer] axId the id of this axis. Inherited
- # @param [Integer] crossAx the id of the perpendicular axis. Inherited
- # @option options [Symbol] axPos. Inherited
- # @option options [Symbol] tickLblPos. Inherited
- # @option options [Symbol] crosses. Inherited
- # @option options [Integer] tickLblSkip
- # @option options [Integer] tickMarkSkip
- def initialize(axId, crossAx, options={})
- @tickLblSkip, @tickMarkSkip = 1, 1
- super(axId, crossAx, options)
+ # @option options [Integer] tick_lbl_skip
+ # @option options [Integer] tick_mark_skip
+ def initialize(options={})
+ @tick_lbl_skip, @tick_mark_skip = 1, 1
+ super(options)
end
# @see tickLblSkip
- def tickLblSkip=(v) Axlsx::validate_unsigned_int(v); @tickLblSkip = v; end
+ def tick_lbl_skip=(v) Axlsx::validate_unsigned_int(v); @tick_lbl_skip = v; end
+ alias :tickLblSkip= :tick_lbl_skip=
# @see tickMarkSkip
- def tickMarkSkip=(v) Axlsx::validate_unsigned_int(v); @tickMarkSkip = v; end
+ def tick_mark_skip=(v) Axlsx::validate_unsigned_int(v); @tick_mark_skip = v; end
+ alias :tickMarkSkip= :tick_mark_skip=
# Serializes the object
# @param [String] str
@@ -36,8 +35,8 @@ module Axlsx
def to_xml_string(str = '')
str << '<c:serAx>'
super(str)
- str << '<c:tickLblSkip val="' << @tickLblSkip.to_s << '"/>' unless @tickLblSkip.nil?
- str << '<c:tickMarkSkip val="' << @tickMarkSkip.to_s << '"/>' unless @tickMarkSkip.nil?
+ str << '<c:tickLblSkip val="' << @tick_lbl_skip.to_s << '"/>' unless @tick_lbl_skip.nil?
+ str << '<c:tickMarkSkip val="' << @tick_mark_skip.to_s << '"/>' unless @tick_mark_skip.nil?
str << '</c:serAx>'
end
end
diff --git a/lib/axlsx/drawing/val_axis.rb b/lib/axlsx/drawing/val_axis.rb
index 6e55c8ea..0e7a0800 100644
--- a/lib/axlsx/drawing/val_axis.rb
+++ b/lib/axlsx/drawing/val_axis.rb
@@ -6,21 +6,22 @@ module Axlsx
# This element specifies how the value axis crosses the category axis.
# must be one of [:between, :midCat]
# @return [Symbol]
- attr_reader :crossBetween
+ attr_reader :cross_between
+ alias :crossBetween :cross_between
# Creates a new ValAxis object
- # @param [Integer] axId the id of this axis
- # @param [Integer] crossAx the id of the perpendicular axis
- # @option options [Symbol] axPos
- # @option options [Symbol] tickLblPos
- # @option options [Symbol] crosses
- # @option options [Symbol] crossesBetween
- def initialize(axId, crossAx, options={})
- self.crossBetween = :between
- super(axId, crossAx, options)
+ # @option options [Symbol] crosses_between
+ def initialize(options={})
+ self.cross_between = :between
+ super(options)
end
- # @see crossBetween
- def crossBetween=(v) RestrictionValidator.validate "ValAxis.crossBetween", [:between, :midCat], v; @crossBetween = v; end
+
+ # @see cross_between
+ def cross_between=(v)
+ RestrictionValidator.validate "ValAxis.cross_between", [:between, :midCat], v
+ @cross_between = v
+ end
+ alias :crossBetween= :cross_between=
# Serializes the object
# @param [String] str
@@ -28,7 +29,7 @@ module Axlsx
def to_xml_string(str = '')
str << '<c:valAx>'
super(str)
- str << '<c:crossBetween val="' << @crossBetween.to_s << '"/>'
+ str << '<c:crossBetween val="' << @cross_between.to_s << '"/>'
str << '</c:valAx>'
end
diff --git a/lib/axlsx/drawing/vml_shape.rb b/lib/axlsx/drawing/vml_shape.rb
index 701456f3..21cac911 100644
--- a/lib/axlsx/drawing/vml_shape.rb
+++ b/lib/axlsx/drawing/vml_shape.rb
@@ -4,95 +4,35 @@ module Axlsx
class VmlShape
include Axlsx::OptionsParser
+ include Axlsx::Accessors
# Creates a new VmlShape
- # @option options [Integer|String] left_column
- # @option options [Integer|String] left_offset
- # @option options [Integer|String] top_row
- # @option options [Integer|String] top_offset
- # @option options [Integer|String] right_column
- # @option options [Integer|String] right_offset
- # @option options [Integer|String] bottom_row
- # @option options [Integer|String] bottom_offset
+ # @option options [Integer] row
+ # @option options [Integer] column
+ # @option options [Integer] left_column
+ # @option options [Integer] left_offset
+ # @option options [Integer] top_row
+ # @option options [Integer] top_offset
+ # @option options [Integer] right_column
+ # @option options [Integer] right_offset
+ # @option options [Integer] bottom_row
+ # @option options [Integer] bottom_offset
def initialize(options={})
@row = @column = @left_column = @top_row = @right_column = @bottom_row = 0
@left_offset = 15
@top_offset = 2
@right_offset = 50
@bottom_offset = 5
+ @visible = true
@id = (0...8).map{65.+(rand(25)).chr}.join
parse_options options
yield self if block_given?
end
- # The row anchor position for this shape determined by the comment's ref value
- # @return [Integer]
- attr_reader :row
+ unsigned_int_attr_accessor :row, :column, :left_column, :left_offset, :top_row, :top_offset,
+ :right_column, :right_offset, :bottom_row, :bottom_offset
- # The column anchor position for this shape determined by the comment's ref value
- # @return [Integer]
- attr_reader :column
-
- # The left column for this shape
- # @return [Integer]
- attr_reader :left_column
-
- # The left offset for this shape
- # @return [Integer]
- attr_reader :left_offset
-
- # The top row for this shape
- # @return [Integer]
- attr_reader :top_row
-
- # The top offset for this shape
- # @return [Integer]
- attr_reader :top_offset
-
- # The right column for this shape
- # @return [Integer]
- attr_reader :right_column
-
- # The right offset for this shape
- # @return [Integer]
- attr_reader :right_offset
-
- # The botttom row for this shape
- # @return [Integer]
- attr_reader :bottom_row
-
- # The bottom offset for this shape
- # @return [Integer]
- attr_reader :bottom_offset
-
- # @see column
- def column=(v); Axlsx::validate_integerish(v); @column = v.to_i end
-
- # @see row
- def row=(v); Axlsx::validate_integerish(v); @row = v.to_i end
- # @see left_column
- def left_column=(v); Axlsx::validate_integerish(v); @left_column = v.to_i end
-
- # @see left_offset
- def left_offset=(v); Axlsx::validate_integerish(v); @left_offset = v.to_i end
-
- # @see top_row
- def top_row=(v); Axlsx::validate_integerish(v); @top_row = v.to_i end
-
- # @see top_offset
- def top_offset=(v); Axlsx::validate_integerish(v); @top_offset = v.to_i end
-
- # @see right_column
- def right_column=(v); Axlsx::validate_integerish(v); @right_column = v.to_i end
-
- # @see right_offset
- def right_offset=(v); Axlsx::validate_integerish(v); @right_offset = v.to_i end
-
- # @see bottom_row
- def bottom_row=(v); Axlsx::validate_integerish(v); @bottom_row = v.to_i end
-
- # @see bottom_offset
- def bottom_offset=(v); Axlsx::validate_integerish(v); @bottom_offset = v.to_i end
+ boolean_attr_accessor :visible
# serialize the shape to a string
# @param [String] str
@@ -100,7 +40,8 @@ module Axlsx
def to_xml_string(str ='')
str << <<SHAME_ON_YOU
-<v:shape id="#{@id}" type="#_x0000_t202" fillcolor="#ffffa1 [80]" o:insetmode="auto">
+<v:shape id="#{@id}" type="#_x0000_t202" fillcolor="#ffffa1 [80]" o:insetmode="auto"
+ style="visibility:#{@visible ? 'visible' : 'hidden'}">
<v:fill color2="#ffffa1 [80]"/>
<v:shadow on="t" obscured="t"/>
<v:path o:connecttype="none"/>
@@ -115,7 +56,7 @@ str << <<SHAME_ON_YOU
<x:AutoFill>False</x:AutoFill>
<x:Row>#{row}</x:Row>
<x:Column>#{column}</x:Column>
- <x:Visible/>
+ #{@visible ? '<x:Visible/>' : ''}
</x:ClientData>
</v:shape>
SHAME_ON_YOU
diff --git a/lib/axlsx/package.rb b/lib/axlsx/package.rb
index df87ed12..0a1bceaa 100644
--- a/lib/axlsx/package.rb
+++ b/lib/axlsx/package.rb
@@ -17,12 +17,14 @@ module Axlsx
#
# @param [Hash] options A hash that you can use to specify the author and workbook for this package.
# @option options [String] :author The author of the document
+ # @option options [Time] :created_at Timestamp in the document properties (defaults to current time).
# @option options [Boolean] :use_shared_strings This is passed to the workbook to specify that shared strings should be used when serializing the package.
# @example Package.new :author => 'you!', :workbook => Workbook.new
def initialize(options={})
@workbook = nil
@core, @app = Core.new, App.new
@core.creator = options[:author] || @core.creator
+ @core.created = options[:created_at]
parse_options options
yield self if block_given?
end
@@ -35,12 +37,6 @@ module Axlsx
end
- # Shortcut to specify that the workbook should use shared strings
- # @see Workbook#use_shared_strings
- def use_shared_strings=(v)
- Axlsx::validate_boolean(v);
- workbook.use_shared_strings = v
- end
# Shortcut to determine if the workbook is configured to use shared strings
# @see Workbook#use_shared_strings
@@ -48,6 +44,12 @@ module Axlsx
workbook.use_shared_strings
end
+ # Shortcut to specify that the workbook should use shared strings
+ # @see Workbook#use_shared_strings
+ def use_shared_strings=(v)
+ Axlsx::validate_boolean(v);
+ workbook.use_shared_strings = v
+ end
# The workbook this package will serialize or validate.
# @return [Workbook] If no workbook instance has been assigned with this package a new Workbook instance is returned.
# @raise ArgumentError if workbook parameter is not a Workbook instance.
@@ -98,6 +100,7 @@ module Axlsx
# File.open('example_streamed.xlsx', 'w') { |f| f.write(s.read) }
def serialize(output, confirm_valid=false)
return false unless !confirm_valid || self.validate.empty?
+ Relationship.clear_cached_instances
Zip::ZipOutputStream.open(output) do |zip|
write_parts(zip)
end
@@ -110,6 +113,7 @@ module Axlsx
# @return [StringIO|Boolean] False if confirm_valid and validation errors exist. rewound string IO if not.
def to_stream(confirm_valid=false)
return false unless !confirm_valid || self.validate.empty?
+ Relationship.clear_cached_instances
zip = write_parts(Zip::ZipOutputStream.new("streamed", true))
stream = zip.close_buffer
stream.rewind
@@ -156,12 +160,12 @@ module Axlsx
p = parts
p.each do |part|
unless part[:doc].nil?
- zip.put_next_entry(part[:entry])
+ zip.put_next_entry(zip_entry_for_part(part))
entry = ['1.9.2', '1.9.3'].include?(RUBY_VERSION) ? part[:doc].force_encoding('BINARY') : part[:doc]
zip.puts(entry)
end
unless part[:path].nil?
- zip.put_next_entry(part[:entry]);
+ zip.put_next_entry(zip_entry_for_part(part))
# binread for 1.9.3
zip.write IO.respond_to?(:binread) ? IO.binread(part[:path]) : IO.read(part[:path])
end
@@ -169,6 +173,22 @@ module Axlsx
zip
end
+ # Generate a ZipEntry for the given package part.
+ # The important part here is to explicitly set the timestamp for the zip entry: Serializing axlsx packages
+ # with identical contents should result in identical zip files – however, the timestamp of a zip entry
+ # defaults to the time of serialization and therefore the zip file contents would be different every time
+ # the package is serialized.
+ #
+ # Note: {Core#created} also defaults to the current time – so to generate identical axlsx packages you have
+ # to set this explicitly, too (eg. with `Package.new(created_at: Time.local(2013, 1, 1))`).
+ #
+ # @param part A hash describing a part of this pacakge (see {#parts})
+ # @return [Zip::ZipEntry]
+ def zip_entry_for_part(part)
+ timestamp = Zip::DOSTime.at(@core.created.to_i)
+ Zip::ZipEntry.new("", part[:entry], "", "", 0, 0, Zip::ZipEntry::DEFLATED, 0, timestamp)
+ end
+
# The parts of a package
# @return [Array] An array of hashes that define the entry, document and schema for each part of the package.
# @private
@@ -321,9 +341,9 @@ module Axlsx
# @private
def relationships
rels = Axlsx::Relationships.new
- rels << Relationship.new(WORKBOOK_R, WORKBOOK_PN)
- rels << Relationship.new(CORE_R, CORE_PN)
- rels << Relationship.new(APP_R, APP_PN)
+ rels << Relationship.new(self, WORKBOOK_R, WORKBOOK_PN)
+ rels << Relationship.new(self, CORE_R, CORE_PN)
+ rels << Relationship.new(self, APP_R, APP_PN)
rels.lock
rels
end
diff --git a/lib/axlsx/rels/relationship.rb b/lib/axlsx/rels/relationship.rb
index 385059f1..a6b7bdd2 100644
--- a/lib/axlsx/rels/relationship.rb
+++ b/lib/axlsx/rels/relationship.rb
@@ -3,7 +3,43 @@ module Axlsx
# A relationship defines a reference between package parts.
# @note Packages automatically manage relationships.
class Relationship
+
+ class << self
+ # Keeps track of all instances of this class.
+ # @return [Array]
+ def instances
+ @instances ||= []
+ end
+
+ # Clear cached instances.
+ #
+ # This should be called before serializing a package (see {Package#serialize} and
+ # {Package#to_stream}) to make sure that serialization is idempotent (i.e.
+ # Relationship instances are generated with the same IDs everytime the package
+ # is serialized).
+ #
+ # Also, calling this avoids memory leaks (cached instances lingering around
+ # forever).
+ def clear_cached_instances
+ @instances = []
+ end
+
+ # Generate and return a unique id (eg. `rId123`) Used for setting {#Id}.
+ #
+ # The generated id depends on the number of cached instances, so using
+ # {clear_cached_instances} will automatically reset the generated ids, too.
+ # @return [String]
+ def next_free_id
+ "rId#{@instances.size + 1}"
+ end
+ end
+ # The id of the relationship (eg. "rId123"). Most instances get their own unique id.
+ # However, some instances need to share the same id – see {#should_use_same_id_as?}
+ # for details.
+ # @return [String]
+ attr_reader :Id
+
# The location of the relationship target
# @return [String]
attr_reader :Target
@@ -30,14 +66,26 @@ module Axlsx
# Target mode must be :external for now.
attr_reader :TargetMode
- # creates a new relationship
+ # The source object the relations belongs to (e.g. a hyperlink, drawing, ...). Needed when
+ # looking up the relationship for a specific object (see {Relationships#for}).
+ attr_reader :source_obj
+
+ # Initializes a new relationship.
+ # @param [Object] source_obj see {#source_obj}
# @param [String] type The type of the relationship
# @param [String] target The target for the relationship
# @option [Symbol] :target_mode only accepts :external.
- def initialize(type, target, options={})
+ def initialize(source_obj, type, target, options={})
+ @source_obj = source_obj
self.Target=target
self.Type=type
- self.TargetMode = options.delete(:target_mode) if options[:target_mode]
+ self.TargetMode = options[:target_mode] if options[:target_mode]
+ @Id = if (existing = self.class.instances.find{ |i| should_use_same_id_as?(i) })
+ existing.Id
+ else
+ self.class.next_free_id
+ end
+ self.class.instances << self
end
# @see Target
@@ -50,15 +98,32 @@ module Axlsx
# serialize relationship
# @param [String] str
- # @param [Integer] rId the id for this relationship
# @return [String]
- def to_xml_string(rId, str = '')
- h = self.instance_values
- h[:Id] = 'rId' << rId.to_s
+ def to_xml_string(str = '')
+ h = self.instance_values.reject{|k, _| k == "source_obj"}
str << '<Relationship '
str << h.map { |key, value| '' << key.to_s << '="' << Axlsx::coder.encode(value.to_s) << '"'}.join(' ')
str << '/>'
end
-
+
+ # Whether this relationship should use the same id as `other`.
+ #
+ # Instances designating the same relationship need to use the same id. We can not simply
+ # compare the {#Target} attribute, though: `foo/bar.xml`, `../foo/bar.xml`,
+ # `../../foo/bar.xml` etc. are all different but probably mean the same file (this
+ # is especially an issue for relationships in the context of pivot tables). So lets
+ # just ignore this attribute for now (except when {#TargetMode} is set to `:External` –
+ # then {#Target} will be an absolute URL and thus can safely be compared).
+ #
+ # @todo Implement comparison of {#Target} based on normalized path names.
+ # @param other [Relationship]
+ def should_use_same_id_as?(other)
+ result = self.source_obj == other.source_obj && self.Type == other.Type && self.TargetMode == other.TargetMode
+ if self.TargetMode == :External
+ result &&= self.Target == other.Target
+ end
+ result
+ end
+
end
end
diff --git a/lib/axlsx/rels/relationships.rb b/lib/axlsx/rels/relationships.rb
index 521d7689..a9c73f7d 100644
--- a/lib/axlsx/rels/relationships.rb
+++ b/lib/axlsx/rels/relationships.rb
@@ -11,10 +11,17 @@ require 'axlsx/rels/relationship.rb'
super Relationship
end
+ # The relationship instance for the given source object, or nil if none exists.
+ # @see Relationship#source_obj
+ # @return [Relationship]
+ def for(source_obj)
+ @list.find{ |rel| rel.source_obj == source_obj }
+ end
+
def to_xml_string(str = '')
str << '<?xml version="1.0" encoding="UTF-8"?>'
str << '<Relationships xmlns="' << RELS_R << '">'
- each_with_index { |rel, index| rel.to_xml_string(index+1, str) }
+ each{ |rel| rel.to_xml_string(str) }
str << '</Relationships>'
end
diff --git a/lib/axlsx/stylesheet/styles.rb b/lib/axlsx/stylesheet/styles.rb
index 8cd2275c..44ccb752 100644
--- a/lib/axlsx/stylesheet/styles.rb
+++ b/lib/axlsx/stylesheet/styles.rb
@@ -296,9 +296,11 @@ module Axlsx
def parse_fill_options(options={})
return unless options[:bg_color]
color = Color.new(:rgb=>options[:bg_color])
- pattern = PatternFill.new(:patternType =>:solid, :fgColor=>color)
+ dxf = options[:type] == :dxf
+ color_key = dxf ? :bgColor : :fgColor
+ pattern = PatternFill.new(:patternType =>:solid, color_key=>color)
fill = Fill.new(pattern)
- options[:type] == :dxf ? fill : fills << fill
+ dxf ? fill : fills << fill
end
# parses Style#add_style options for borders.
diff --git a/lib/axlsx/util/serialized_attributes.rb b/lib/axlsx/util/serialized_attributes.rb
index 5519f843..e421984b 100644
--- a/lib/axlsx/util/serialized_attributes.rb
+++ b/lib/axlsx/util/serialized_attributes.rb
@@ -59,7 +59,6 @@ module Axlsx
# break the xml and 1.8.7 does not support ordered hashes.
# @param [String] str The string instance to which serialized data is appended
# @param [Array] additional_attributes An array of additional attribute names.
- # @param [Proc] block A which will be called with the value for each element.
# @return [String] The serialized output.
def serialized_element_attributes(str='', additional_attributes=[], &block)
attrs = self.class.xml_element_attributes + additional_attributes
diff --git a/lib/axlsx/util/simple_typed_list.rb b/lib/axlsx/util/simple_typed_list.rb
index 4d188ffd..9a5f2e3d 100644
--- a/lib/axlsx/util/simple_typed_list.rb
+++ b/lib/axlsx/util/simple_typed_list.rb
@@ -1,21 +1,9 @@
# encoding: UTF-8
module Axlsx
+
# A SimpleTypedList is a type restrictive collection that allows some of the methods from Array and supports basic xml serialization.
# @private
class SimpleTypedList
- # The class constants of allowed types
- # @return [Array]
- attr_reader :allowed_types
-
- # The index below which items cannot be removed
- # @return [Integer]
- attr_reader :locked_at
-
- # The tag name to use when serializing this object
- # by default the parent node for all items in the list is the classname of the first allowed type with the first letter in lowercase.
- # @return [String]
- attr_reader :serialize_as
-
# Creats a new typed list
# @param [Array, Class] type An array of Class objects or a single Class object
# @param [String] serialize_as The tag name to use in serialization
@@ -33,6 +21,38 @@ module Axlsx
@serialize_as = serialize_as
end
+ # The class constants of allowed types
+ # @return [Array]
+ attr_reader :allowed_types
+
+ # The index below which items cannot be removed
+ # @return [Integer]
+ attr_reader :locked_at
+
+ # The tag name to use when serializing this object
+ # by default the parent node for all items in the list is the classname of the first allowed type with the first letter in lowercase.
+ # @return [String]
+ attr_reader :serialize_as
+
+ # Transposes the list (without blowing up like ruby does)
+ # any non populated cell in the matrix will be a nil value
+ def transpose
+ return @list.clone if @list.size == 0
+ row_count = @list.size
+ max_column_count = @list.map{|row| row.cells.size}.max
+ result = Array.new(max_column_count) { Array.new(row_count) }
+ 0..row_count.times do |row_index|
+ 0..max_column_count.times do |column_index|
+ datum = if @list[row_index].cells.size >= max_column_count
+ @list[row_index].cells[column_index]
+ elsif block_given?
+ yield(column_index, row_index)
+ end
+ result[column_index][row_index] = datum
+ end
+ end
+ result
+ end
# Lock this list at the current size
# @return [self]
def lock
diff --git a/lib/axlsx/util/validators.rb b/lib/axlsx/util/validators.rb
index 739a4de2..a161f0d9 100644
--- a/lib/axlsx/util/validators.rb
+++ b/lib/axlsx/util/validators.rb
@@ -290,4 +290,11 @@ module Axlsx
def self.validate_split_state_type(v)
RestrictionValidator.validate :split_state_type, [:frozen, :frozen_split, :split], v
end
+
+ # Requires that the value is a valid "display blanks as" type.
+ # valid types must be one of gap, span, zero
+ # @param [Any] v The value validated
+ def self.validate_display_blanks_as(v)
+ RestrictionValidator.validate :display_blanks_as, [:gap, :span, :zero], v
+ end
end
diff --git a/lib/axlsx/version.rb b/lib/axlsx/version.rb
index 6b0df7df..6c9907ab 100644
--- a/lib/axlsx/version.rb
+++ b/lib/axlsx/version.rb
@@ -1,5 +1,5 @@
module Axlsx
# The current version
- VERSION = "1.3.5"
+ VERSION = "1.3.7"
end
diff --git a/lib/axlsx/workbook/shared_strings_table.rb b/lib/axlsx/workbook/shared_strings_table.rb
index 43e8a1a8..7c08205e 100644
--- a/lib/axlsx/workbook/shared_strings_table.rb
+++ b/lib/axlsx/workbook/shared_strings_table.rb
@@ -26,10 +26,16 @@ module Axlsx
# @see Cell#sharable
attr_reader :unique_cells
+ # The xml:space attribute
+ # @see Workbook#xml_space
+ attr_reader :xml_space
+
# Creates a new Shared Strings Table agains an array of cells
# @param [Array] cells This is an array of all of the cells in the workbook
- def initialize(cells)
+ # @param [Symbol] xml_space The xml:space behavior for the shared string table.
+ def initialize(cells, xml_space=:preserve)
@index = 0
+ @xml_space = xml_space
@unique_cells = {}
@shared_xml_string = ""
shareable_cells = cells.flatten.select{ |cell| cell.plain_string? }
@@ -40,8 +46,10 @@ module Axlsx
# Serializes the object
# @param [String] str
# @return [String]
- def to_xml_string
- '<?xml version="1.0" encoding="UTF-8"?><sst xmlns="' << XML_NS << '" count="' << @count.to_s << '" uniqueCount="' << unique_count.to_s << '">' << @shared_xml_string << '</sst>'
+ def to_xml_string(str='')
+ str << '<?xml version="1.0" encoding="UTF-8"?><sst xmlns="' << XML_NS << '"'
+ str << ' count="' << @count.to_s << '" uniqueCount="' << unique_count.to_s << '"'
+ str << ' xml:space="' << xml_space.to_s << '">' << @shared_xml_string << '</sst>'
end
private
diff --git a/lib/axlsx/workbook/workbook.rb b/lib/axlsx/workbook/workbook.rb
index e329ae74..d122f253 100644
--- a/lib/axlsx/workbook/workbook.rb
+++ b/lib/axlsx/workbook/workbook.rb
@@ -270,14 +270,14 @@ require 'axlsx/workbook/worksheet/selection.rb'
def relationships
r = Relationships.new
@worksheets.each do |sheet|
- r << Relationship.new(WORKSHEET_R, WORKSHEET_PN % (r.size+1))
+ r << Relationship.new(sheet, WORKSHEET_R, WORKSHEET_PN % (r.size+1))
end
pivot_tables.each_with_index do |pivot_table, index|
- r << Relationship.new(PIVOT_TABLE_CACHE_DEFINITION_R, PIVOT_TABLE_CACHE_DEFINITION_PN % (index+1))
+ r << Relationship.new(pivot_table.cache_definition, PIVOT_TABLE_CACHE_DEFINITION_R, PIVOT_TABLE_CACHE_DEFINITION_PN % (index+1))
end
- r << Relationship.new(STYLES_R, STYLES_PN)
+ r << Relationship.new(self, STYLES_R, STYLES_PN)
if use_shared_strings
- r << Relationship.new(SHARED_STRINGS_R, SHARED_STRINGS_PN)
+ r << Relationship.new(self, SHARED_STRINGS_R, SHARED_STRINGS_PN)
end
r
end
@@ -285,7 +285,25 @@ require 'axlsx/workbook/worksheet/selection.rb'
# generates a shared string object against all cells in all worksheets.
# @return [SharedStringTable]
def shared_strings
- SharedStringsTable.new(worksheets.collect { |ws| ws.cells })
+ SharedStringsTable.new(worksheets.collect { |ws| ws.cells }, xml_space)
+ end
+
+ # The xml:space attribute for the worksheet.
+ # This determines how whitespace is handled withing the document.
+ # The most relevant part being whitespace in the cell text.
+ # allowed values are :preserve and :default. Axlsx uses :preserve unless
+ # you explicily set this to :default.
+ # @return Symbol
+ def xml_space
+ @xml_space ||= :preserve
+ end
+
+ # Sets the xml:space attribute for the worksheet
+ # @see Worksheet#xml_space
+ # @param [Symbol] space must be one of :preserve or :default
+ def xml_space=(space)
+ Axlsx::RestrictionValidator.validate(:xml_space, [:preserve, :default], space)
+ @xml_space = space;
end
# returns a range of cells in a worksheet
@@ -318,9 +336,8 @@ require 'axlsx/workbook/worksheet/selection.rb'
defined_names.to_xml_string(str)
unless pivot_tables.empty?
str << '<pivotCaches>'
- pivot_tables.each_with_index do |pivot_table, index|
- rId = "rId#{@worksheets.size + index + 1 }"
- str << '<pivotCache cacheId="' << pivot_table.cache_definition.cache_id.to_s << '" r:id="' << rId << '"/>'
+ pivot_tables.each do |pivot_table|
+ str << '<pivotCache cacheId="' << pivot_table.cache_definition.cache_id.to_s << '" r:id="' << pivot_table.cache_definition.rId << '"/>'
end
str << '</pivotCaches>'
end
diff --git a/lib/axlsx/workbook/worksheet/comment.rb b/lib/axlsx/workbook/worksheet/comment.rb
index 3e54d2b3..7035f4cf 100644
--- a/lib/axlsx/workbook/worksheet/comment.rb
+++ b/lib/axlsx/workbook/worksheet/comment.rb
@@ -4,35 +4,33 @@ module Axlsx
class Comment
include Axlsx::OptionsParser
+ include Axlsx::Accessors
# Creates a new comment object
- # @param [Comments] comments
+ # @param [Comments] comments The comment collection this comment belongs to
# @param [Hash] options
# @option [String] author the author of the comment
# @option [String] text The text for the comment
+ # @option [String] ref The refence (e.g. 'A3' where this comment will be anchored.
+ # @option [Boolean] visible This controls the visiblity of the associated vml_shape.
def initialize(comments, options={})
raise ArgumentError, "A comment needs a parent comments object" unless comments.is_a?(Comments)
+ @visible = true
@comments = comments
parse_options options
yield self if block_given?
end
- # The text to render
- # @return [String]
- attr_reader :text
-
- # The author of this comment
- # @see Comments
- # @return [String]
- attr_reader :author
+ string_attr_accessor :text, :author
+ boolean_attr_accessor :visible
- # The owning Comments object
+ # The owning Comments object
# @return [Comments]
attr_reader :comments
# The string based cell position reference (e.g. 'A1') that determines the positioning of this comment
- # @return [String]
+ # @return [String|Cell]
attr_reader :ref
# TODO
@@ -60,30 +58,20 @@ module Axlsx
@ref = v.r if v.is_a?(Cell)
end
- # @see text
- def text=(v)
- Axlsx::validate_string(v)
- @text = v
- end
-
- # @see author
- def author=(v)
- @author = v
- end
-
# serialize the object
# @param [String] str
# @return [String]
def to_xml_string(str = "")
author = @comments.authors[author_index]
str << '<comment ref="' << ref << '" authorId="' << author_index.to_s << '">'
- str << '<text><r>'
- str << '<rPr> <b/><color indexed="81"/></rPr>'
- str << '<t>' << author.to_s << ':
-</t></r>'
+ str << '<text>'
+ unless author.to_s == ""
+ str << '<r><rPr><b/><color indexed="81"/></rPr>'
+ str << "<t>" << ::CGI.escapeHTML(author.to_s) << ":\n</t></r>"
+ end
str << '<r>'
str << '<rPr><color indexed="81"/></rPr>'
- str << '<t>' << text << '</t></r></text>'
+ str << '<t>' << ::CGI.escapeHTML(text) << '</t></r></text>'
str << '</comment>'
end
@@ -93,7 +81,7 @@ module Axlsx
# by default, all columns are 5 columns wide and 5 rows high
def initialize_vml_shape
pos = Axlsx::name_to_indices(ref)
- @vml_shape = VmlShape.new(:row => pos[1], :column => pos[0]) do |vml|
+ @vml_shape = VmlShape.new(:row => pos[1], :column => pos[0], :visible => @visible) do |vml|
vml.left_column = vml.column
vml.right_column = vml.column + 2
vml.top_row = vml.row
diff --git a/lib/axlsx/workbook/worksheet/comments.rb b/lib/axlsx/workbook/worksheet/comments.rb
index 25da9de2..03cce339 100644
--- a/lib/axlsx/workbook/worksheet/comments.rb
+++ b/lib/axlsx/workbook/worksheet/comments.rb
@@ -56,9 +56,9 @@ module Axlsx
# The relationships required by this object
# @return [Array]
def relationships
- [Relationship.new(VML_DRAWING_R, "../#{vml_drawing.pn}"),
- Relationship.new(COMMENT_R, "../#{pn}"),
- Relationship.new(COMMENT_R_NULL, "NULL")]
+ [Relationship.new(self, VML_DRAWING_R, "../#{vml_drawing.pn}"),
+ Relationship.new(self, COMMENT_R, "../#{pn}"),
+ Relationship.new(self, COMMENT_R_NULL, "NULL")]
end
# serialize the object
diff --git a/lib/axlsx/workbook/worksheet/conditional_formatting_rule.rb b/lib/axlsx/workbook/worksheet/conditional_formatting_rule.rb
index a0ce6a41..916b31c2 100644
--- a/lib/axlsx/workbook/worksheet/conditional_formatting_rule.rb
+++ b/lib/axlsx/workbook/worksheet/conditional_formatting_rule.rb
@@ -25,7 +25,7 @@ module Axlsx
# @option options [Integer] stdDev The number of standard deviations above or below the average to match
# @option options [Boolean] stopIfTrue Stop evaluating rules after this rule matches
# @option options [Symbol] timePeriod The time period in a date occuring... rule
- # @option options [String] formula The formula to match against in i.e. an equal rule
+ # @option options [String] formula The formula to match against in i.e. an equal rule. Use a [minimum, maximum] array for cellIs between/notBetween conditionals.
def initialize(options={})
@color_scale = @data_bar = @icon_set = @formula = nil
parse_options options
@@ -36,6 +36,8 @@ module Axlsx
:stopIfTrue, :timePeriod
# Formula
+ # The formula or value to match against (e.g. 5 with an operator of :greaterThan to specify cell_value > 5).
+ # If the operator is :between or :notBetween, use an array to specify [minimum, maximum]
# @return [String]
attr_reader :formula
@@ -180,7 +182,7 @@ module Axlsx
# @see timePeriod
def timePeriod=(v); Axlsx::validate_time_period_type(v); @timePeriod = v end
# @see formula
- def formula=(v); Axlsx::validate_string(v); @formula = v end
+ def formula=(v); [*v].each {|x| Axlsx::validate_string(x) }; @formula = [*v].map { |form| ::CGI.escapeHTML(form) } end
# @see color_scale
def color_scale=(v)
@@ -208,7 +210,7 @@ module Axlsx
str << '<cfRule '
serialized_attributes str
str << '>'
- str << '<formula>' << self.formula << '</formula>' if @formula
+ str << '<formula>' << [*self.formula].join('</formula><formula>') << '</formula>' if @formula
@color_scale.to_xml_string(str) if @color_scale && @type == :colorScale
@data_bar.to_xml_string(str) if @data_bar && @type == :dataBar
@icon_set.to_xml_string(str) if @icon_set && @type == :iconSet
diff --git a/lib/axlsx/workbook/worksheet/pivot_table.rb b/lib/axlsx/workbook/worksheet/pivot_table.rb
index 94edf80e..0de22f8f 100644
--- a/lib/axlsx/workbook/worksheet/pivot_table.rb
+++ b/lib/axlsx/workbook/worksheet/pivot_table.rb
@@ -95,10 +95,17 @@ module Axlsx
# (see #data)
def data=(v)
DataTypeValidator.validate "#{self.class}.data", [Array], v
- v.each do |ref|
- DataTypeValidator.validate "#{self.class}.data[]", [String], ref
+ @data = []
+ v.each do |data_field|
+ if data_field.is_a? String
+ data_field = {:ref => data_field}
+ end
+ data_field.values.each do |value|
+ DataTypeValidator.validate "#{self.class}.data[]", [String], value
+ end
+ @data << data_field
end
- @data = v
+ @data
end
# The pages
@@ -138,20 +145,14 @@ module Axlsx
@cache_definition ||= PivotTableCacheDefinition.new(self)
end
- # The worksheet relationships. This is managed automatically by the worksheet
+ # The relationships for this pivot table.
# @return [Relationships]
def relationships
r = Relationships.new
- r << Relationship.new(PIVOT_TABLE_CACHE_DEFINITION_R, "../#{cache_definition.pn}")
+ r << Relationship.new(cache_definition, PIVOT_TABLE_CACHE_DEFINITION_R, "../#{cache_definition.pn}")
r
end
- # The relation reference id for this table
- # @return [String]
- def rId
- "rId#{index+1}"
- end
-
# Serializes the object
# @param [String] str
# @return [String]
@@ -196,11 +197,11 @@ module Axlsx
str << '</pageFields>'
end
unless data.empty?
- str << '<dataFields count="' << data.size.to_s << '">'
+ str << "<dataFields count=\"#{data.size}\">"
data.each do |datum_value|
- str << '<dataField name="Sum of ' << datum_value << '" ' <<
- 'fld="' << header_index_of(datum_value).to_s << '" ' <<
- 'baseField="0" baseItem="0"/>'
+ str << "<dataField name='#{@subtotal} of #{datum_value[:ref]}' fld='#{header_index_of(datum_value[:ref])}' baseField='0' baseItem='0'"
+ str << " subtotal='#{datum_value[:subtotal]}' " if datum_value[:subtotal]
+ str << "/>"
end
str << '</dataFields>'
end
diff --git a/lib/axlsx/workbook/worksheet/pivot_table_cache_definition.rb b/lib/axlsx/workbook/worksheet/pivot_table_cache_definition.rb
index 37f46c51..665384f4 100644
--- a/lib/axlsx/workbook/worksheet/pivot_table_cache_definition.rb
+++ b/lib/axlsx/workbook/worksheet/pivot_table_cache_definition.rb
@@ -35,10 +35,11 @@ module Axlsx
index + 1
end
- # The relation reference id for this table
+ # The relationship id for this pivot table cache definition.
+ # @see Relationship#Id
# @return [String]
def rId
- "rId#{index + 1}"
+ pivot_table.relationships.for(self).Id
end
# Serializes the object
diff --git a/lib/axlsx/workbook/worksheet/pivot_tables.rb b/lib/axlsx/workbook/worksheet/pivot_tables.rb
index f5625fc0..912d9f41 100644
--- a/lib/axlsx/workbook/worksheet/pivot_tables.rb
+++ b/lib/axlsx/workbook/worksheet/pivot_tables.rb
@@ -17,7 +17,7 @@ module Axlsx
# returns the relationships required by this collection
def relationships
return [] if empty?
- map{ |pivot_table| Relationship.new(PIVOT_TABLE_R, "../#{pivot_table.pn}") }
+ map{ |pivot_table| Relationship.new(pivot_table, PIVOT_TABLE_R, "../#{pivot_table.pn}") }
end
end
diff --git a/lib/axlsx/workbook/worksheet/table.rb b/lib/axlsx/workbook/worksheet/table.rb
index dce1a40e..94474c57 100644
--- a/lib/axlsx/workbook/worksheet/table.rb
+++ b/lib/axlsx/workbook/worksheet/table.rb
@@ -47,10 +47,11 @@ module Axlsx
"#{TABLE_PN % (index+1)}"
end
- # The relation reference id for this table
+ # The relationship id for this table.
+ # @see Relationship#Id
# @return [String]
def rId
- "rId#{index+1}"
+ @sheet.relationships.for(self).Id
end
# The name of the Table.
diff --git a/lib/axlsx/workbook/worksheet/tables.rb b/lib/axlsx/workbook/worksheet/tables.rb
index 2d9a1f3c..814f995f 100644
--- a/lib/axlsx/workbook/worksheet/tables.rb
+++ b/lib/axlsx/workbook/worksheet/tables.rb
@@ -17,7 +17,7 @@ module Axlsx
# returns the relationships required by this collection
def relationships
return [] if empty?
- map{ |table| Relationship.new(TABLE_R, "../#{table.pn}") }
+ map{ |table| Relationship.new(table, TABLE_R, "../#{table.pn}") }
end
def to_xml_string(str = "")
diff --git a/lib/axlsx/workbook/worksheet/worksheet.rb b/lib/axlsx/workbook/worksheet/worksheet.rb
index 5f650263..ecba9254 100644
--- a/lib/axlsx/workbook/worksheet/worksheet.rb
+++ b/lib/axlsx/workbook/worksheet/worksheet.rb
@@ -114,14 +114,19 @@ module Axlsx
@rows ||= SimpleTypedList.new Row
end
- # returns the sheet data as columnw
- def cols
- @rows.transpose
+ # returns the sheet data as columns
+ # If you pass a block, it will be evaluated whenever a row does not have a
+ # cell at a specific index. The block will be called with the row and column
+ # index in the missing cell was found.
+ # @example
+ # cols { |row_index, column_index| p "warn - row #{row_index} is does not have a cell at #{column_index}
+ def cols(&block)
+ @rows.transpose(&block)
end
- # An range that excel will apply an autfilter to "A1:B3"
+ # An range that excel will apply an auto-filter to "A1:B3"
# This will turn filtering on for the cells in the range.
- # The first row is considered the header, while subsequent rows are considerd to be data.
+ # The first row is considered the header, while subsequent rows are considered to be data.
# @return String
def auto_filter
@auto_filter ||= AutoFilter.new self
@@ -327,6 +332,10 @@ module Axlsx
auto_filter.range = v
end
+ # Accessor for controlling whether leading and trailing spaces in cells are
+ # preserved or ignored. The default is to preserve spaces.
+ attr_accessor :preserve_spaces
+
# The part name of this worksheet
# @return [String]
def pn
@@ -339,10 +348,11 @@ module Axlsx
"#{WORKSHEET_RELS_PN % (index+1)}"
end
- # The relationship Id of thiw worksheet
+ # The relationship id of this worksheet.
# @return [String]
+ # @see Relationship#Id
def rId
- "rId#{index+1}"
+ @workbook.relationships.for(self).Id
end
# The index of this worksheet in the owning Workbook's worksheets list.
@@ -565,14 +575,6 @@ module Axlsx
r
end
- # identifies the index of an object withing the collections used in generating relationships for the worksheet
- # @param [Any] object the object to search for
- # @return [Integer] The index of the object
- def relationships_index_of(object)
- objects = [tables.to_a, worksheet_comments.comments.to_a, hyperlinks.to_a, worksheet_drawing.drawing].flatten.compact || []
- objects.index(object)
- end
-
# Returns the cell or cells defined using excel style A1:B3 references.
# @param [String|Integer] cell_def the string defining the cell or range of cells, or the rownumber
# @return [Cell, Array]
@@ -629,6 +631,11 @@ module Axlsx
end
private
+
+ def xml_space
+ workbook.xml_space
+ end
+
def outline(collection, range, level = 1, collapsed = true)
range.each do |index|
unless (item = collection[index]).nil?
@@ -699,7 +706,7 @@ module Axlsx
# Helper method for parsingout the root node for worksheet
# @return [String]
def worksheet_node
- "<worksheet xmlns=\"%s\" xmlns:r=\"%s\">" % [XML_NS, XML_NS_R]
+ "<worksheet xmlns=\"%s\" xmlns:r=\"%s\" xml:space=\"#{xml_space}\">" % [XML_NS, XML_NS_R]
end
def sheet_data
diff --git a/lib/axlsx/workbook/worksheet/worksheet_comments.rb b/lib/axlsx/workbook/worksheet/worksheet_comments.rb
index 8c700aa0..f9e4c8cd 100644
--- a/lib/axlsx/workbook/worksheet/worksheet_comments.rb
+++ b/lib/axlsx/workbook/worksheet/worksheet_comments.rb
@@ -40,10 +40,11 @@ module Axlsx
!comments.empty?
end
- # The index in the worksheet's relationships for the VML drawing that will render the comments
- # @return [Integer]
- def index
- worksheet.relationships.index { |r| r.Type == VML_DRAWING_R } + 1
+ # The relationship id of the VML drawing that will render the comments.
+ # @see Relationship#Id
+ # @return [String]
+ def drawing_rId
+ comments.relationships.find{ |r| r.Type == VML_DRAWING_R }.Id
end
# Seraalize the object
@@ -51,7 +52,7 @@ module Axlsx
# @return [String]
def to_xml_string(str = '')
return unless has_comments?
- str << "<legacyDrawing r:id='rId#{index}' />"
+ str << "<legacyDrawing r:id='#{drawing_rId}' />"
end
end
end
diff --git a/lib/axlsx/workbook/worksheet/worksheet_drawing.rb b/lib/axlsx/workbook/worksheet/worksheet_drawing.rb
index 9deeeec3..08cad1f7 100644
--- a/lib/axlsx/workbook/worksheet/worksheet_drawing.rb
+++ b/lib/axlsx/workbook/worksheet/worksheet_drawing.rb
@@ -41,24 +41,18 @@ module Axlsx
@drawing.is_a? Drawing
end
- # The relationship required by this object
+ # The relationship instance for this drawing.
# @return [Relationship]
def relationship
return unless has_drawing?
- Relationship.new(DRAWING_R, "../#{drawing.pn}")
- end
-
- # returns the index of the worksheet releationship that defines this drawing.
- # @return [Integer]
- def index
- worksheet.relationships.index{ |r| r.Type == DRAWING_R } +1
+ Relationship.new(self, DRAWING_R, "../#{drawing.pn}")
end
# Serialize the drawing for the worksheet
# @param [String] str
def to_xml_string(str = '')
return unless has_drawing?
- str << "<drawing r:id='rId#{index}'/>"
+ str << "<drawing r:id='#{relationship.Id}'/>"
end
end
end
diff --git a/lib/axlsx/workbook/worksheet/worksheet_hyperlink.rb b/lib/axlsx/workbook/worksheet/worksheet_hyperlink.rb
index d352ec90..2a241287 100644
--- a/lib/axlsx/workbook/worksheet/worksheet_hyperlink.rb
+++ b/lib/axlsx/workbook/worksheet/worksheet_hyperlink.rb
@@ -45,18 +45,13 @@ module Axlsx
@ref = cell_reference
end
- # The relationship required by this hyperlink when the taget is :external
+ # The relationship instance for this hyperlink.
+ # A relationship is only required if `@target` is `:external`. If not, this method will simply return `nil`.
+ # @see #target=
# @return [Relationship]
def relationship
return unless @target == :external
- Relationship.new HYPERLINK_R, location, :target_mode => :External
- end
-
- # The id of the relationship for this object
- # @return [String]
- def id
- return unless @target == :external
- "rId#{(@worksheet.relationships_index_of(self)+1)}"
+ Relationship.new(self, HYPERLINK_R, location, :target_mode => :External)
end
# Seralize the object
@@ -73,7 +68,7 @@ module Axlsx
# r:id should only be specified for external targets.
# @return [Hash]
def location_or_id
- @target == :external ? { :"r:id" => id } : { :location => Axlsx::coder.encode(location) }
+ @target == :external ? { :"r:id" => relationship.Id } : { :location => Axlsx::coder.encode(location) }
end
end
end
diff --git a/lib/schema/sml.xsd b/lib/schema/sml.xsd
index fa396e80..3a67d0f2 100644
--- a/lib/schema/sml.xsd
+++ b/lib/schema/sml.xsd
@@ -13,6 +13,8 @@
<xsd:import
namespace="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
schemaLocation="dml-spreadsheetDrawing.xsd"/>
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="xml.xsd"/>
+
<xsd:complexType name="CT_AutoFilter">
<xsd:sequence>
<xsd:element name="filterColumn" minOccurs="0" maxOccurs="unbounded" type="CT_FilterColumn"/>
@@ -1797,6 +1799,7 @@
</xsd:sequence>
<xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/>
<xsd:attribute name="uniqueCount" type="xsd:unsignedInt" use="optional"/>
+ <xsd:attribute ref="xml:space"/>
</xsd:complexType>
<xsd:simpleType name="ST_PhoneticType">
<xsd:restriction base="xsd:string">
@@ -2213,6 +2216,7 @@
<xsd:element name="tableParts" type="CT_TableParts" minOccurs="0" maxOccurs="1"/>
<xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
+ <xsd:attribute ref="xml:space"/>
</xsd:complexType>
<xsd:complexType name="CT_SheetData">
<xsd:sequence>