class DataTable::Table

Config Options

id: the html id title: the title of the data table subtitle: the subtitle of the data table css_class: an extra css class to get applied to the table empty_text: the text to display of the collection is empty display_header => false: hide the column headers for the data table alternate_rows => false: turn off alternating of row css classes alternate_cols => true: turn on alternating of column classes, defaults to false

columns: an array of hashes of the column specs for this table

group_by: an array of columns to group on

subtotals: an array of hashes that contain the subtotal information for each column that should be subtotaled totals: an array of hashes that contain the total information for each column that should be totaled

Attributes

alternate_cols[RW]
alternate_rows[RW]
collection[R]
columns[R]
css_class[RW]
custom_headers[RW]
display_header[RW]
empty_text[RW]
grouped_data[R]
hide_if_empty[RW]
id[RW]
repeat_headers_for_groups[RW]
subtotal_calculations[R]
subtotals[R]
title[RW]
total_calculations[R]
totals[R]

Public Class Methods

new(collection) click to toggle source
# File lib/data-table/table.rb, line 30
def initialize(collection)
  @collection = collection
  @grouped_collection = nil
  default_options!
  @columns = []
  @groupings = []
  @grouped_data = false
  @subtotals = []
  @totals = []
end

Public Instance Methods

calculate(data, column_name, function, path = nil) click to toggle source

TODO: Write test for this

# File lib/data-table/table.rb, line 381
def calculate(data, column_name, function, path = nil)
  column = @columns.select { |col| col.name == column_name }
  if function.is_a?(Proc)
    calculate_with_proc(function, data, column, path)
  elsif function.is_a?(Array) && function[1].is_a?(Proc)
    calculate_array_and_proc(function, data, column_name, path)
  elsif function.is_a?(Array)
    calculate_many(function, data, column_name, path)
  else
    send("calculate_#{function}", data, column_name)
  end
end
calculate_array_and_proc(function, data, column = nil, path = nil) click to toggle source
# File lib/data-table/table.rb, line 402
def calculate_array_and_proc(function, data, column = nil, path = nil)
  result = send("calculate_#{function[0]}", data, column)
  case function[1].arity
  when 1 then function[1].call(result)
  when 2 then function[1].call(result, column.first)
  when 3 then function[1].call(result, column.first, path.last)
  end
end
calculate_avg(collection, column_name) click to toggle source
# File lib/data-table/table.rb, line 425
def calculate_avg(collection, column_name)
  return 0 if collection.empty?

  sum = calculate_sum(collection, column_name)
  sum / collection.size
end
calculate_many(function, data, column_name, _path = nil) click to toggle source
# File lib/data-table/table.rb, line 411
def calculate_many(function, data, column_name, _path = nil)
  function.each do |func|
    if func.is_a? Array
      send("calculate_#{func[0]}", data, column_name)
    else
      send("calculate_#{func}", data, column_name)
    end
  end
end
calculate_max(collection, column_name) click to toggle source
# File lib/data-table/table.rb, line 432
def calculate_max(collection, column_name)
  collection.collect { |r| r[column_name].to_f }.max
end
calculate_min(collection, column_name) click to toggle source
# File lib/data-table/table.rb, line 436
def calculate_min(collection, column_name)
  collection.collect { |r| r[column_name].to_f }.min
end
calculate_parent_subtotals() click to toggle source
# File lib/data-table/table.rb, line 360
def calculate_parent_subtotals
  @parent_subtotals = Hash.new { |h, k| h[k] = [] }
  # Iterate over all the parent groups
  parent_groups = @groupings.slice(0, @groupings.count - 1).compact
  parent_groups.count.times do
    # Group by each parent on the fly
    @subtotals.each_with_index do |subtotal, index|
      @collection.group_by_recursive(parent_groups).each_pair_with_parents do |group_name, data, parents|
        subtotal.each do |s|
          path = parents + [group_name]
          result = calculate(data, s[0], s[1], path)
          @parent_subtotals[path][index] ||= {} if @parent_subtotals[path][index].nil?
          @parent_subtotals[path][index][s[0]] = {s[1] => result}
        end
      end
    end
    parent_groups.pop
  end
end
calculate_subtotals!() click to toggle source
# File lib/data-table/table.rb, line 343
def calculate_subtotals!
  raise 'Subtotals only work with grouped results sets' unless @grouped_data
  @subtotal_calculations ||= Hash.new { |h, k| h[k] = [] }
  @subtotals.each_with_index do |subtotal_type, index|
    subtotal_type.each do |subtotal|
      @collection.each_pair_with_parents(@groupings.count) do |group_name, group_data, parents|
        path = parents + [group_name]
        result = calculate(group_data, subtotal[0], subtotal[1], path)
        (0..index).each do |index|
          @subtotal_calculations[path][index] ||= {}
        end
        @subtotal_calculations[path][index][subtotal[0]] = {subtotal[1] => result}
      end
    end
  end
end
calculate_sum(collection, column_name) click to toggle source
# File lib/data-table/table.rb, line 421
def calculate_sum(collection, column_name)
  collection.inject(0) { |sum, row| sum + row[column_name].to_f }
end
calculate_totals!() click to toggle source

TODO: Refactor to shorten method. Also revise tests.

# File lib/data-table/table.rb, line 326
def calculate_totals!
  @total_calculations = []
  @totals.each_with_index do |row, index|
    next if row.nil?

    if @collection.is_a?(Hash)
      collection = []
      @collection.each_pair_recursive { |_k, v| collection.concat(v) }
    end
    collection = @collection if @collection.is_a? Array
    @total_calculations[index] = {} if @total_calculations[index].nil?
    row.each do |item|
      @total_calculations[index][item[0]] = calculate(collection, item[0], item[1])
    end
  end
end
calculate_with_proc(function, data, column = nil, path = nil) click to toggle source
# File lib/data-table/table.rb, line 394
def calculate_with_proc(function, data, column = nil, path = nil)
  case function.arity
  when 1 then function.call(data)
  when 2 then function.call(data, column.first)
  when 3 then function.call(data, column.first, path.last)
  end
end
column(id, title = '', opts = {}, &b) click to toggle source

Define a new column for the table

# File lib/data-table/table.rb, line 59
def column(id, title = '', opts = {}, &b)
  @columns << DataTable::Column.new(id, title, opts, &b)
end
custom_header(&blk) click to toggle source
# File lib/data-table/table.rb, line 156
def custom_header(&blk)
  instance_eval(&blk)
end
default_options!() click to toggle source
# File lib/data-table/table.rb, line 41
def default_options!
  @id = ''
  @title = ''
  @subtitle = ''
  @css_class = ''
  @empty_text = 'No records found'
  @hide_if_empty = false
  @display_header = true
  @alternate_rows = true
  @alternate_cols = false
  @subtotal_title = 'Subtotal:'
  @total_title = 'Total:'
  @repeat_headers_for_groups = false
  @custom_headers = []
  @row_attributes = nil
end
group_by(group_column, level = {level: 0}, &_blk) click to toggle source

TODO: allow for group column only, block only and group column and block

# File lib/data-table/table.rb, line 173
def group_by(group_column, level = {level: 0}, &_blk)
  if level.nil? && @groupings.count >= 1
    raise 'a level designation is required when using multiple groupings.'
  end
  @grouped_data = true
  @groupings[level ? level[:level] : 0] = group_column
  @columns.reject! { |c| c.name == group_column }
end
group_data!() click to toggle source
# File lib/data-table/table.rb, line 182
def group_data!
  @groupings.compact!
  @collection = if @groupings.count > 1
                  collection.group_by_recursive(@groupings)
                else
                  collection.group_by { |row| row[@groupings.first] }
                end
end
prepare_data() click to toggle source
# File lib/data-table/table.rb, line 63
def prepare_data
  calculate_parent_subtotals if @groupings.count > 1
  group_data! if @grouped_data
  calculate_subtotals! if subtotals?
  calculate_totals! if totals?
end
render() click to toggle source

GENERAL RENDERING

# File lib/data-table/table.rb, line 73
def render
  render_data_table
end
render_custom_table_header() click to toggle source
# File lib/data-table/table.rb, line 102
def render_custom_table_header
  html = "<tr class='custom-header'>"
  @custom_headers.each do |h|
    html << "<th class=\"#{h[:css]}\" colspan=\"#{h[:colspan]}\" style=\"#{h[:style]}\">#{h[:text]}</th>"
  end
  html << '</tr>'
end
render_data_table() click to toggle source
# File lib/data-table/table.rb, line 77
def render_data_table
  html = "<table id='#{@id}' class='data_table #{@css_class}' cellspacing='0' cellpadding='0'>"
  html << "<caption>#{@title}</caption>" if @title
  html << render_data_table_header if @display_header
  if @collection.any?
    html << render_data_table_body(@collection)
    html << render_totals if totals?
  else
    html << "<tr><td class='empty_data_table' colspan='#{@columns.size}'>#{@empty_text}</td></tr>"
  end
  html << '</table>'
end
render_data_table_body(collection) click to toggle source
# File lib/data-table/table.rb, line 110
def render_data_table_body(collection)
  if @grouped_data
    render_grouped_data_table_body(collection)
  else
    "<tbody>#{render_rows(collection)}</tbody>"
  end
end
render_data_table_header() click to toggle source
# File lib/data-table/table.rb, line 90
def render_data_table_header
  html = '<thead>'

  html << render_custom_table_header unless @custom_headers.empty?

  html << '<tr>'
  @columns.each do |col|
    html << col.render_column_header
  end
  html << '</tr></thead>'
end
render_group(group_header, group_data) click to toggle source
# File lib/data-table/table.rb, line 217
def render_group(group_header, group_data)
  html = "<tbody class='#{group_header.to_s.downcase.gsub(/[^A-Za-z0-9]+/, '_')}'>"
  html << render_group_header(group_header, 0)
  if group_data.is_a? Array
    html << render_rows(group_data)
    html << render_subtotals(group_header, group_data) if subtotals?
  elsif group_data.is_a? Hash
    html << render_group_recursive(group_data, 1, group_header)
  end
  html << '</tbody>'
end
render_group_header(group_header, index = nil) click to toggle source
# File lib/data-table/table.rb, line 199
def render_group_header(group_header, index = nil)
  css_classes = ['group_header']
  css_classes << ["level_#{index}"] unless index.nil?
  html =  "<tr class='#{css_classes.join(' ')}'>"
  html << "<th colspan='#{@columns.size}'>#{group_header}</th>"
  html << '</tr>'
  repeat_headers(html) if @repeat_headers_for_groups
  html
end
render_group_recursive(collection, index = 1, group_parent = nil, ancestors = nil) click to toggle source
# File lib/data-table/table.rb, line 229
def render_group_recursive(collection, index = 1, group_parent = nil, ancestors = nil)
  html = ''
  ancestors ||= []
  collection.each_pair do |group_name, group_data|
    ancestors << group_parent unless ancestors[0] == group_parent
    ancestors << group_name unless ancestors.length == @groupings.length
    if group_data.is_a?(Hash)
      html << render_group_header(group_name, index)
      html << render_group_recursive(group_data, index + 1, nil, ancestors)
    elsif group_data.is_a?(Array)
      html << render_group_header(group_name, index)
      html << render_rows(group_data)
      ancestors.pop
      html << render_subtotals(group_name, group_data, ancestors) if subtotals?
    end
  end
  html << render_parent_subtotals(ancestors) if @parent_subtotals
  ancestors.pop
  html
end
render_grouped_data_table_body(collection) click to toggle source
# File lib/data-table/table.rb, line 191
def render_grouped_data_table_body(collection)
  html = ''
  collection.keys.each do |group_name|
    html << render_group(group_name, collection[group_name])
  end
  html
end
render_parent_subtotals(group_array) click to toggle source
# File lib/data-table/table.rb, line 268
def render_parent_subtotals(group_array)
  html = ''
  @parent_subtotals[group_array].each_with_index do |group, index|
    next if group.nil?

    html << "<tr class='parent_subtotal "
    html << "index_#{index} #{group_array.join('_').gsub(/\s/, '_').downcase}'>"
    @columns.each do |col|
      value = group[col.name] ? group[col.name].values[0] : nil
      html << col.render_cell(value)
    end
    html << '</tr>'
  end
  html
end
render_row(row, row_index, css_class = '', row_attributes = {}) click to toggle source
# File lib/data-table/table.rb, line 132
def render_row(row, row_index, css_class = '', row_attributes = {})
  attributes = if row_attributes.nil?
                 ''
               else
                 row_attributes.map { |attr, val| "#{attr}='#{val}'" }.join ' '
               end

  html = "<tr class='row_#{row_index} #{css_class}' #{attributes}>"
  @columns.each_with_index do |col, col_index|
    cell = begin
             row[col.name]
           rescue
             nil
           end
    html << col.render_cell(cell, row, row_index, col_index)
  end
  html << '</tr>'
end
render_rows(collection) click to toggle source
# File lib/data-table/table.rb, line 118
def render_rows(collection)
  html = ''
  collection.each_with_index do |row, row_index|
    css_class = @alternate_rows && row_index.odd? ? 'alt ' : ''
    if @row_style && style = @row_style.call(row, row_index)
      css_class << style
    end

    attributes = @row_attributes.nil? ? {} : @row_attributes.call(row)
    html << render_row(row, row_index, css_class, attributes)
  end
  html
end
render_subtotals(group_header, _group_data = nil, ancestors = nil) click to toggle source

ancestors should be an array

# File lib/data-table/table.rb, line 285
def render_subtotals(group_header, _group_data = nil, ancestors = nil)
  html = ''
  path = ancestors.nil? ? [] : ancestors.dup
  path << group_header

  is_first_subtotal = true

  @subtotal_calculations[path].each_with_index do |group, index|
    next if group.empty?
    
    html << "<tr class='subtotal index_#{index} #{'first' if is_first_subtotal}'>"
    @columns.each do |col|
      value = group[col.name] ? group[col.name].values[0] : nil
      html << col.render_cell(value)
    end
    html << '</tr>'

    is_first_subtotal = false
  end
  html
end
render_totals() click to toggle source

TOTALS AND SUBTOTALS

# File lib/data-table/table.rb, line 253
def render_totals
  html = '<tfoot>'
  @total_calculations.each_with_index do |totals_row, index|
    next if totals_row.nil?
    
    html << "<tr class='total index_#{index}'>"
    @columns.each do |col|
      value = totals_row[col.name] ||= nil
      html << col.render_cell(value)
    end
    html << '</tr>'
  end
  html << '</tfoot>'
end
repeat_headers(html) click to toggle source
# File lib/data-table/table.rb, line 209
def repeat_headers(html)
  html << "<tr class='col_headers'>"
  @columns.each_with_index do |col, _i|
    html << col.render_column_header
  end
  html << '</tr>'
end
row_attributes(&b) click to toggle source
# File lib/data-table/table.rb, line 164
def row_attributes(&b)
  @row_attributes = b
end
row_style(&b) click to toggle source

define a custom block to be used to determine the css class for a row.

# File lib/data-table/table.rb, line 152
def row_style(&b)
  @row_style = b
end
subtotal(column_name, function = nil, index = 0, &block) click to toggle source
# File lib/data-table/table.rb, line 307
def subtotal(column_name, function = nil, index = 0, &block)
  raise 'You must supply an index value' if @subtotals.count >= 1 && index.nil?
  total_row @subtotals, column_name, function, index, &block
end
subtotals?() click to toggle source
# File lib/data-table/table.rb, line 312
def subtotals?
  !@subtotals.empty?
end
th(header_text, options) click to toggle source
# File lib/data-table/table.rb, line 160
def th(header_text, options)
  @custom_headers << options.merge(text: header_text)
end
total(column_name, function = nil, index = 0, &block) click to toggle source
# File lib/data-table/table.rb, line 316
def total(column_name, function = nil, index = 0, &block)
  raise 'You must supply an index value' if @totals.count >= 1 && index.nil?
  total_row @totals, column_name, function, index, &block
end
totals?() click to toggle source
# File lib/data-table/table.rb, line 321
def totals?
  !@totals.empty?
end

Private Instance Methods

total_row(collection, column_name, function = nil, index = nil, &block) click to toggle source

Define a new total column definition. total columns take the name of the column that should be totaled they also take a default aggregate function name and/or a block if only a default function is given, then it is used to calculate the total if only a block is given then only it is used to calculated the total if both a block and a function are given then the default aggregate function is called first then its result is passed into the block for further processing.

# File lib/data-table/table.rb, line 449
def total_row(collection, column_name, function = nil, index = nil, &block)
  function_or_block = function || block
  f = function && block_given? ? [function, block] : function_or_block
  (0..index).each do |index|
    collection[index] = {} if collection[index].nil?
  end
  collection[index][column_name] = f
end