Website/lib/slide_utils.rb

207 lines
7.1 KiB
Ruby
Raw Normal View History

2019-04-24 00:25:35 -04:00
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" })
2019-05-01 01:51:31 -04:00
annotation_style = params.fetch(:annotation_style, rownum_style.merge("text-align" => :left, "font-size" => "70%"))
2019-04-24 00:25:35 -04:00
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
2019-05-01 01:51:31 -04:00
if params.has_key? :annotations and not params[:annotations].nil?
2019-04-26 01:07:03 -04:00
data = data.zip(params[:annotations]).map do |row, annot|
if annot.nil? then row
2019-05-01 01:51:31 -04:00
else row+[tag("td", ""+annot.to_s, style: annotation_style)] end
2019-04-26 01:07:03 -04:00
end
2019-04-24 00:25:35 -04:00
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, {})
)
2019-05-03 02:59:22 -04:00
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"
)
2019-04-24 00:25:35 -04:00
end