Website/lib/slide_utils.rb

207 lines
7.1 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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}</#{type}>"
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)
"<tspan style='font-size: 40%; vertical-align: sub;'>#{x}</tspan>"
end
def symbol
case @type
when :select then "<tspan style='font-size: 200%'>&nbsp;𝛔#{subscript @params[:pred]}</tspan>"
when :project then "<tspan style='font-size: 200%'> 𝛑#{subscript @params[:attrs]}</tspan>"
when :aggregate then "<tspan style='font-size: 200%'>#{subscript @params[:groupby] if @params.has_key? :groupby}𝛄#{subscript @params[:aggregates]}</tspan>"
when :join then "<tspan style='font-size: 400%'>⋈#{subscript @params[:pred]}</tspan>"
when :cross then "<tspan style='font-size: 400%'>⨉</tspan>"
when :diff then "<tspan style='font-size: 300%; font-weight: bold'>&nbsp-</tspan>"
when :union then "<tspan style='font-size: 400%'>⊎</tspan>"
when :table then "<tspan style='font-weight: bold; font-family: Courier, fixed-width; font-size: 150%'>#{@params[:name]}</tspan>"
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, "")}<rect x='#{symbol_x}' y='#{symbol_y}' width='#{@self_width}' height='#{@self_height}' style='fill: red'/>\n" if config.fetch(:debug, false)
"#{debug}#{config.fetch(:indent, "")}<text x='#{symbol_x}' y='#{symbol_y+@self_height}'>#{symbol}</text>\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} <g transform='translate(#{children_x[i]}, #{children_y})'>\n#{rendered}</g>\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} <line x1='#{line_x}' y1='#{line_y}' x2='#{target_x}' y2='#{target_y}' stroke='black' stroke-width='4'/>\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}<svg height='#{(ra.height+20)*scale}' width='#{(ra.width+20)*scale}'>\n"+
"#{indent}<g transform='scale(#{scale})'>"+
ra.render(params.merge( indent: indent+" " ))+
"#{indent}</g></svg>\n"
)
end