207 lines
7.1 KiB
Ruby
207 lines
7.1 KiB
Ruby
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%'> 𝛔#{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'> -</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 |