def tag(type, body, args = {}) if args[:style].is_a? Hash args[:style] = args[:style].map { |k, v| "#{k}: #{v};" }.join(" ") end "<#{type} #{args.map { |k, v| "#{k}='#{v}'" }.join(" ")}>#{body}" end def data_table(schema, data, params = {}) tablename_style = params.fetch(:tablename_style, { "font-weight" => "bold", "text-decoration" => "underline" }) rownum_style = params.fetch(:rownum_style, { "font-size" => "50%", "font-face" => "Courier, fixed-width", "vertical-align"=> "middle" }) annotation_style = params.fetch(:annotation_style, rownum_style.merge("text-align" => :left, "font-size" => "70%")) separator_style = params.fetch(:separator_style, { "border-right" => "1px solid black" }) num_cols = data.map { |row| row.size if row.is_a? Array }.compact.max data = data.map do |row| case row when Array row.map do |cell| tag("td", cell) end when String [ tag("td", row, colspan: num_cols) ] else raise "Unknown rowtype: #{row}" end end if params.has_key? :annotations and not params[:annotations].nil? data = data.zip(params[:annotations]).map do |row, annot| if annot.nil? then row else row+[tag("td", "→ "+annot.to_s, style: annotation_style)] end end num_cols += 1 end data = [schema.map { |cell| tag("th", cell) }] + data if params.fetch(:rowids, false) data = data.map.with_index do |row, idx| if idx == 0 then [tag("th", "#", style: separator_style)]+row else [tag("td", idx+params.fetch(:rowid_offset, 0), style: rownum_style.merge(separator_style))]+row end end elsif params.has_key? :name data = data.map { |row| [tag("td", "", style: separator_style)]+row } end if params.has_key? :name data[0][0] = tag("th", params[:name], style: tablename_style.merge(separator_style)) end row_args = [{}] * data.size case params.fetch(:build_in, :none) when :none then when :by_row then row_args = [{}] + [{ "class" => "fragment" }]*(data.size-1) when Array then row_args = [{}] + params[:build_in].map do |order| if order <= 0 then {} else { "class" => "fragment", "data-fragment-index" => order } end end end return tag("table", data.zip(row_args).map { |row, args| tag("tr", row.join, args) }.join("\n"), params.fetch(:table_args, {}) ) end class RATreeNode def initialize(type, params, children = []) @type = type @params = params @children = children @self_width = 100 @self_height = 100 case @type when :table then @self_width = 40*params[:name].length @self_height = 50 when :select, :join then @self_width += 15*params[:pred].length when :project then @self_width += 15*params[:attrs].length end @height_above_children = 100 end def subscript(x) "#{x}" end def symbol case @type when :select then " 𝛔#{subscript @params[:pred]}" when :project then " 𝛑#{subscript @params[:attrs]}" when :aggregate then "#{subscript @params[:groupby] if @params.has_key? :groupby}𝛄#{subscript @params[:aggregates]}" when :join then "⋈#{subscript @params[:pred]}" when :cross then "" when :diff then " -" when :union then "" when :table then "#{@params[:name]}" else type.to_s end end def height(config = {}) unless @height if @children.nil? @height = @self_height else @height = @children.map { |c| c.height(config) }.max + (@self_height + @height_above_children) end end @height end def child_width(config = {}) return 0 if @children.nil? unless @child_width separator_x = 20 @child_width = @children.map { |c| c.width(config) }.sum + separator_x * (@children.size-1) end @child_width end def width(config = {}) unless @width if @children.nil? @width = @self_width else @width = [ child_width, @self_width ].max end end @width end def symbol_text(config) symbol_x = width(config) / 2 - (@self_width / 2) symbol_y = 0 debug = "#{config.fetch(:indent, "")}\n" if config.fetch(:debug, false) "#{debug}#{config.fetch(:indent, "")}#{symbol}\n" end def render(config = {}) return symbol_text(config) if @children.nil? indent = config.fetch(:indent, "") separator_x = 20 separator_y = @height_above_children children_x = [0] children_x = [(width(config) - child_width(config)) / 2] if width(config) > child_width(config) (1..@children.length).each { |i| children_x[i] = children_x[i-1] + @children[i-1].width + separator_x } children_y = separator_y + @self_height child_blobs = @children.map.with_index do |c, i| rendered = c.render(config.merge( indent: indent+" " )) p rendered if config.fetch(:debug, false) p children_x[i] if config.fetch(:debug, false) "#{indent} \n#{rendered}\n" end line_x = width(config) / 2 line_y = (@self_height) * 1.1 target_y = line_y + @height_above_children child_lines = @children.map.with_index do |c, i| target_x = (children_x[i] + children_x[i+1] - separator_x) / 2 "#{indent} \n" end symbol_text(config)+child_blobs.join+child_lines.join end end def ra_table(name) RATreeNode.new(:table, { name: name }, nil) end def ra_union(*children) RATreeNode.new(:union, {}, children) end def ra_diff(*children) RATreeNode.new(:diff, {}, children) end def ra_join(predicate, lhs, rhs) RATreeNode.new(:table, { pred: predicate }, [lhs, rhs]) end def ra_aggregate(groupby, aggregates, input) RATreeNode.new(:aggregate, { groupby: groupby, aggregates: aggregates}, [input]) end def ra_select(predicate, input) RATreeNode.new(:select, { pred: predicate }, [input]) end def ra_project(attrs, input) attrs = attrs.map { |k, v| "#{k} ← #{v}"}.join("; ") if attrs.is_a? Hash RATreeNode.new(:project, { attrs: attrs }, [input]) end def relational_algebra(params = {}) indent = params.fetch(:indent, "") ra = yield scale = if ra.height > 500 then 500.0 / ra.height else 1 end return ( "#{indent}\n"+ "#{indent}"+ ra.render(params.merge( indent: indent+" " ))+ "#{indent}\n" ) end