Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Initial support for a plot area with multiple different charts on it #341

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions examples/example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
examples << :cached_formula
examples << :page_breaks
examples << :rich_text
examples << :multi_chart

p = Axlsx::Package.new
wb = p.workbook
Expand Down Expand Up @@ -659,7 +660,7 @@

## Book Views
#
## Book views let you specify which sheet the show as active when the user opens the work book as well as a bunch of other
## Book views let you specify which sheet the show as active when the user opens the work book as well as a bunch of other
## tuning values for the UI @see Axlsx::WorkbookView
## ```ruby
if examples.include? :book_view
Expand Down Expand Up @@ -825,4 +826,32 @@
end
p.serialize 'rich_text.xlsx'
end
#```
#```

#```ruby
if examples.include? :multi_chart
p = Axlsx::Package.new
wb = p.workbook
wb.add_worksheet(:name => "Line Chart") do |sheet|
sheet.add_row ["Simple Line Chart"]
sheet.add_row %w(first second)
4.times do
sheet.add_row [ rand(24)+1, rand(24)+1]
end
sheet.add_chart(Axlsx::MultiChart) do |mchart|
mchart.add_sub_chart(Axlsx::BarChart, :title => "Simple 3D Bar Chart", :rotX => 30, :rotY => 20, :barDir => :col) do |chart|
chart.start_at 0, 5
chart.end_at 10, 20
chart.add_series :data => sheet["A3:A6"], :title => sheet["A2"], :color => "0000FF"
chart.catAxis.title = 'X Axis'
chart.valAxis.title = 'Y Axis'
end
mchart.add_sub_chart(Axlsx::LineChart, :title => "Simple Line Chart", :rotX => 30, :rotY => 20) do |chart|
chart.add_series :data => sheet["A3:A6"], :title => sheet["A2"], :labels => [], :color => "FF0000", :show_marker => true, :smooth => true
end

end
end
p.serialize 'multi_chart.xlsx'
end
#```
150 changes: 150 additions & 0 deletions lib/axlsx/drawing/bar_chart.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# encoding: UTF-8
module Axlsx

# The BarChart is a barchart (who would have guessed?) that you can add to your worksheet.
# @see Worksheet#add_chart
# @see Chart#add_series
# @see Package#serialize
# @see README for an example
class BarChart < Chart

# the category axis
# @return [CatAxis]
def cat_axis
axes[:cat_axis]
end
alias :catAxis :cat_axis

# the value axis
# @return [ValAxis]
def val_axis
axes[:val_axis]
end
alias :valAxis :val_axis

# The direction of the bars in the chart
# must be one of [:bar, :col]
# @return [Symbol]
def bar_dir
@bar_dir ||= :bar
end
alias :barDir :bar_dir

# space between bar or column clusters, as a percentage of the bar or column width.
# @return [String]
attr_reader :gap_depth
alias :gapDepth :gap_depth

# space between bar or column clusters, as a percentage of the bar or column width.
# @return [String]
def gap_width
@gap_width ||= 150
end
alias :gapWidth :gap_width

#grouping for a column, line, or area chart.
# must be one of [:percentStacked, :clustered, :standard, :stacked]
# @return [Symbol]
def grouping
@grouping ||= :clustered
end

# The shabe of the bars or columns
# must be one of [:cone, :coneToMax, :box, :cylinder, :pyramid, :pyramidToMax]
# @return [Symbol]
def shape
@shape ||= :box
end

# validation regex for gap amount percent
GAP_AMOUNT_PERCENT = /0*(([0-9])|([1-9][0-9])|([1-4][0-9][0-9])|500)%/

# Creates a new bar chart object
# @param [GraphicFrame] frame The workbook that owns this chart.
# @option options [Cell, String] title
# @option options [Boolean] show_legend
# @option options [Symbol] bar_dir
# @option options [Symbol] grouping
# @option options [String] gap_width
# @option options [String] gap_depth
# @option options [Symbol] shape
# @option options [Integer] rot_x
# @option options [String] h_percent
# @option options [Integer] rot_y
# @option options [String] depth_percent
# @option options [Boolean] r_ang_ax
# @option options [Integer] perspective
# @see Chart
# @see View3D
def initialize(frame, options={})
@vary_colors = true
@gap_width, @gap_depth, @shape = nil, nil, nil
super(frame, options)
@series_type = BarSeries
@d_lbls = nil
end

# The direction of the bars in the chart
# must be one of [:bar, :col]
def bar_dir=(v)
RestrictionValidator.validate "BarChart.bar_dir", [:bar, :col], v
@bar_dir = v
end
alias :barDir= :bar_dir=

#grouping for a column, line, or area chart.
# must be one of [:percentStacked, :clustered, :standard, :stacked]
def grouping=(v)
RestrictionValidator.validate "BarChart.grouping", [:percentStacked, :clustered, :standard, :stacked], v
@grouping = v
end

# space between bar or column clusters, as a percentage of the bar or column width.
def gap_width=(v)
RegexValidator.validate "BarChart.gap_width", GAP_AMOUNT_PERCENT, v
@gap_width=(v)
end
alias :gapWidth= :gap_width=

# space between bar or column clusters, as a percentage of the bar or column width.
def gap_depth=(v)
RegexValidator.validate "BarChart.gap_didth", GAP_AMOUNT_PERCENT, v
@gap_depth=(v)
end
alias :gapDepth= :gap_depth=

# The shabe of the bars or columns
# must be one of [:cone, :coneToMax, :box, :cylinder, :pyramid, :pyramidToMax]
def shape=(v)
RestrictionValidator.validate "BarChart.shape", [:cone, :coneToMax, :box, :cylinder, :pyramid, :pyramidToMax], v
@shape = v
end

# Serializes the object
# @param [String] str
# @return [String]
def to_xml_string(str = '')
super(str) do
str << '<c:barChart>'
str << ('<c:barDir val="' << bar_dir.to_s << '"/>')
str << ('<c:grouping val="' << grouping.to_s << '"/>')
str << ('<c:varyColors val="' << vary_colors.to_s << '"/>')
@series.each { |ser| ser.to_xml_string(str) }
@d_lbls.to_xml_string(str) if @d_lbls
str << ('<c:gapWidth val="' << @gap_width.to_s << '"/>') unless @gap_width.nil?
str << ('<c:gapDepth val="' << @gap_depth.to_s << '"/>') unless @gap_depth.nil?
str << ('<c:shape val="' << @shape.to_s << '"/>') unless @shape.nil?
axes.to_xml_string(str, :ids => true)
str << '</c:barChart>'
axes.to_xml_string(str)
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
7 changes: 6 additions & 1 deletion lib/axlsx/drawing/chart.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def initialize(frame, options={})
@display_blanks_as = :gap
@series_type = Series
@title = Title.new
@d_table = nil
parse_options options
start_at(*options[:start_at]) if options[:start_at]
end_at(*options[:end_at]) if options[:end_at]
Expand All @@ -33,6 +34,8 @@ def initialize(frame, options={})
attr_reader :view_3D
alias :view3D :view_3D

attr_reader :d_table

# A reference to the graphic frame that owns this chart
# @return [GraphicFrame]
attr_reader :graphic_frame
Expand All @@ -53,7 +56,7 @@ def d_lbls
# Indicates that colors should be varied by datum
# @return [Boolean]
attr_reader :vary_colors

# Configures the vary_colors options for this chart
# @param [Boolean] v The value to set
def vary_colors=(v) Axlsx::validate_boolean(v); @vary_colors = v; end
Expand Down Expand Up @@ -164,6 +167,7 @@ def to_xml_string(str = '')
str << '<c:plotArea>'
str << '<c:layout/>'
yield if block_given?
@d_table.to_xml_string(str) if @d_table
str << '</c:plotArea>'
if @show_legend
str << '<c:legend>'
Expand Down Expand Up @@ -227,6 +231,7 @@ def end_at(x=10, y=10)
def view_3D=(v) DataTypeValidator.validate "#{self.class}.view_3D", View3D, v; @view_3D = v; end
alias :view3D= :view_3D=

def d_table=(v) DataTypeValidator.validate "#{self.class}.d_table", DTable, v; @d_table = v; end
end

end
62 changes: 62 additions & 0 deletions lib/axlsx/drawing/d_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Axlsx
# There are more elements in the dTable spec that allow for
# customizations and formatting. For now, I am just implementing the
# basics.
class DTable

include Axlsx::Accessors
include Axlsx::OptionsParser
# creates a new DTable object
def initialize(chart_type, options={})
raise ArgumentError, 'chart_type must inherit from Chart' unless [Chart, LineChart].include?(chart_type.superclass)
@chart_type = chart_type
initialize_defaults
parse_options options
end

# These attributes are all boolean so I'm doing a bit of a hand
# waving magic show to set up the attriubte accessors
# @note
# not all charts support all methods!
#
boolean_attr_accessor :show_horz_border,
:show_vert_border,
:show_outline,
:show_keys

# Initialize all the values to false as Excel requires them to
# explicitly be disabled or all will show.
def initialize_defaults
[:show_horz_border, :show_vert_border,
:show_outline, :show_keys].each do |attr|
self.send("#{attr}=", false)
end
end

# The chart type that is using this data table instance.
# This affects the xml output as not all chart types support the
# same data table attributes.
attr_reader :chart_type

# serializes the data labels
# @return [String]
def to_xml_string(str = '')
# validate_attributes_for_chart_type
str << '<c:dTable>'
%w(show_horz_border show_vert_border show_outline show_keys).each do |key|
next unless instance_values.keys.include?(key) && instance_values[key] != nil
str << "<c:#{Axlsx::camel(key, false)} val='#{instance_values[key]}' />"
end
str << '</c:dTable>'
end

# nills out d_lbl_pos and show_leader_lines as these attributes, while valid in the spec actually chrash excel for any chart type other than pie charts.
# def validate_attributes_for_chart_type
# return if @chart_type == Pie3DChart
# @d_lbl_pos = nil
# @show_leader_lines = nil
# end


end
end
3 changes: 3 additions & 0 deletions lib/axlsx/drawing/drawing.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# encoding: UTF-8
module Axlsx
require 'axlsx/drawing/d_lbls.rb'
require 'axlsx/drawing/d_table.rb'
require 'axlsx/drawing/title.rb'
require 'axlsx/drawing/series_title.rb'
require 'axlsx/drawing/series.rb'
Expand Down Expand Up @@ -33,7 +34,9 @@ module Axlsx

require 'axlsx/drawing/view_3D.rb'
require 'axlsx/drawing/chart.rb'
require 'axlsx/drawing/multi_chart.rb'
require 'axlsx/drawing/pie_3D_chart.rb'
require 'axlsx/drawing/bar_chart.rb'
require 'axlsx/drawing/bar_3D_chart.rb'
require 'axlsx/drawing/line_chart.rb'
require 'axlsx/drawing/line_3D_chart.rb'
Expand Down
65 changes: 65 additions & 0 deletions lib/axlsx/drawing/multi_chart.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# encoding: UTF-8
module Axlsx

# The MultiChart is a wrapper for multiple charts that you can add to your worksheet.
# @see Worksheet#add_chart
# @see Chart#add_series
# @see Package#serialize
# @see README for an example
class MultiChart < Chart

module OverrideToXmlString
def to_xml_string(str = '')
yield if block_given?
str
end
end

# the charts this multi chart contains
# @return [SimpleTypedList]
def sub_charts
@sub_charts
end

# Creates a new multi chart object
# @param [GraphicFrame] frame The workbook that owns this chart.
# @see Chart
def initialize(frame, options = {})
super(frame, options)
@sub_charts ||= SimpleTypedList.new(Chart)
@d_table = DTable.new(self.class)
end

# Add a sub_chart
# @see Worksheet.add_chart
def add_sub_chart(chart_type, options = {})
chart = chart_type.new(@graphic_frame, options)
@graphic_frame.anchor.drawing.worksheet.workbook.charts.pop
yield chart if block_given?
@sub_charts << chart
chart
end

# Serializes the object
# @param [String] str
# @return [String]
def to_xml_string(str = '')
base_index = 0
super(str) do
sub_charts.each do |sub_chart|
# Yes, I went there
sub_chart.instance_eval{ class << self; self; end }.superclass.send(:include, OverrideToXmlString)
# Yes, I really went there
sub_chart.series.each do |ser|
ser.define_singleton_method(:index) do
base_index
end
base_index += 1
end
sub_chart.to_xml_string(str)
end
end
str
end
end
end