diff options
author | Harald Eilertsen <haraldei@anduin.net> | 2023-10-10 21:10:19 +0200 |
---|---|---|
committer | Harald Eilertsen <haraldei@anduin.net> | 2023-10-10 21:10:19 +0200 |
commit | 51ef51bbe3b741a76d1d44b8ce8c1d8a07427e1f (patch) | |
tree | 4f2b4b8a0df7669f6de73496be578f6340ade3c3 /lib | |
parent | 68e89035e51a3fa6d0a54ee7d5f27259090b101e (diff) | |
download | ramaskrik-program-51ef51bbe3b741a76d1d44b8ce8c1d8a07427e1f.tar.gz ramaskrik-program-51ef51bbe3b741a76d1d44b8ce8c1d8a07427e1f.tar.bz2 ramaskrik-program-51ef51bbe3b741a76d1d44b8ce8c1d8a07427e1f.zip |
Updates for Ramaskrik 2023
- Reorg, create lib dir and move room-schedule and events there
- Add .ics template to generate program as an iCalendar stream.
- Add some info to html template, and instructions to html template.
- Add footer to html template, with timestamp for when the view was
generated.
- Fix bug that messed up the layout if the input data was not already
sorted.
- Update readme with usage instructions.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/events.rb | 28 | ||||
-rw-r--r-- | lib/room-schedule.rb | 248 |
2 files changed, 276 insertions, 0 deletions
diff --git a/lib/events.rb b/lib/events.rb new file mode 100644 index 0000000..e647930 --- /dev/null +++ b/lib/events.rb @@ -0,0 +1,28 @@ +require 'time' + +module Events + class Event + + attr_reader :start_time + attr_reader :duration + attr_reader :title + attr_reader :venue + attr_reader :image_url + + def initialize(attrs) + @start_time = Time.parse(attrs['start_time']) + @duration = attrs['duration'].to_i + @title = attrs['title'] + @venue = attrs['venue'] + @image_url = attrs['image'] unless attrs['image'].length < 10 + end + + def date + @start_time.to_date + end + + def slug + "#{start_time.to_i}-#{title.downcase.gsub(/[^a-z0-9_-]/, '')}" + end + end +end diff --git a/lib/room-schedule.rb b/lib/room-schedule.rb new file mode 100644 index 0000000..fd4e282 --- /dev/null +++ b/lib/room-schedule.rb @@ -0,0 +1,248 @@ +require 'date' +require 'time' +require 'SVG/Graph/Plot' + +module SVG + module Graph + class RoomSchedule < Graph + # In addition to the defaults set by Graph::initialize and + # Plot::set_defaults, sets: + # [x_label_format] '%Y-%m-%d %H:%M:%S' + # [popup_format] '%Y-%m-%d %H:%M:%S' + def set_defaults + init_with( + :x_label_format => '%Y-%m-%d %H:%M:%S', + :popup_format => '%Y-%m-%d %H:%M:%S', + :scale_x_divisions => false, + :scale_x_integers => false, + :bar_gap => true + ) + end + + # The format string use do format the X axis labels. + # See Time::strformat + attr_accessor :x_label_format + # Use this to set the spacing between dates on the axis. The value + # must be of the form + # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" + # + # EG: + # + # graph.timescale_divisions = "2 weeks" + # + # will cause the chart to try to divide the X axis up into segments of + # two week periods. + attr_accessor :timescale_divisions + # The formatting used for the popups. See x_label_format + attr_accessor :popup_format + attr_accessor :scale_x_divisions + attr_accessor :scale_x_integers + attr_accessor :bar_gap + + protected + + def format x, y + Time.at( x ).strftime( popup_format ) + end + + def get_x_labels + rv = get_x_values.collect { |v| v.strftime( x_label_format ) } + end + + def y_label_offset( height ) + height / -2.0 + end + + def get_y_labels + @data.map {|room| room[:title] } + end + + def draw_data + bargap = bar_gap ? (field_height < 10 ? field_height / 2 : 10) : 0 + subbar_height = field_height - bargap + + field_count = 1 + y_mod = (subbar_height / 2) + (font_size / 2) + min,max,div = x_range + scale = (@graph_width) / (max - min).to_f + @data.each do |room| + room[:data].each do |entry| + x_start = (entry.start_time - min).to_f + x_end = (entry.end_time - min).to_f + y = @graph_height - (field_height * field_count) + bar_width = (x_end - x_start) * scale + bar_start = x_start * scale + + event = @graph.add_element( "rect", { + "x" => bar_start.to_s, + "y" => y.to_s, + "width" => bar_width.to_s, + "height" => subbar_height.to_s, + "class" => "fill#{field_count+1}" + }) + switch = @graph.add_element("switch") + fo = switch.add_element("foreignObject", { + "x" => (bar_start + @font_size / 2).to_s, + "y" => y.to_s, + "width" => bar_width.to_s, + "height" => subbar_height.to_s, + "requiredExtensions" => "http://www.w3.org/1999/xhtml", + }) + body = fo.add_element("body", { + "xmlns" => "http://www.w3.org/1999/xhtml", + }) + p = body.add_element("p", { + "class" => "event-title", + }) + if entry.link + p = p.add_element("a", { "href" => entry.link }) + end + p.add_text(entry.title) + t = switch.add_element("text", { + "x" => (bar_start + @font_size / 2).to_s, + "y" => (y + 2 * @font_size).to_s, + }) + t.add_text(entry.title) + end + field_count += 1 + end + end + + def get_css + return <<EOL +/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */ +.key1,.fill1{ + fill: #ff0000; + fill-opacity: 0.5; + stroke: none; + stroke-width: 0.5px; +} +.key2,.fill2{ + fill: #0000ff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key3,.fill3{ + fill: #00ff00; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key4,.fill4{ + fill: #ffcc00; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key5,.fill5{ + fill: #00ccff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key6,.fill6{ + fill: #ff00ff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key7,.fill7{ + fill: #00ffff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key8,.fill8{ + fill: #ffff00; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key9,.fill9{ + fill: #cc6666; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key10,.fill10{ + fill: #663399; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key11,.fill11{ + fill: #339900; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key12,.fill12{ + fill: #9966FF; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.event-title { + font-size: 80%; +} +EOL + end + + private + + def timescale_unit + if timescale_divisions + timescale_divisions =~ /(\d+) ?(days|weeks|hours|minutes|seconds)?/ + division_units = $2 ? $2 : "days" + amount = $1.to_i + unit = nil + case division_units + when "weeks" + unit = 7 + when "days" + unit = 1 + when "hours" + unit = (1.0 / 24) + when "minutes" + unit = (1.0 / 1440) + when "seconds" + unit = (1.0 / 86400) + end + end + [ amount, unit ] + end + + def x_range + max_value = @data.map {|room| room[:data].map {|e| e.end_time}.max}.max + min_value = @data.map {|room| room[:data].map {|e| e.start_time}.min}.min + + amount, unit = timescale_unit + max_value += unit * (amount - (max_value.min % amount)) + range = max_value - min_value + right_pad = 0 #range == 0 ? 10 : range / 20.0 + scale_range = (max_value + right_pad) - min_value + + scale_division = scale_x_divisions || (scale_range / 10.0) + + if scale_x_integers + scale_division = scale_division < 1 ? 1 : scale_division.round + end + + [min_value, max_value, scale_division] + end + + def get_x_values + rv = [] + min, max, scale_division = x_range + if timescale_divisions + amount, unit = timescale_unit + min.step( max, amount * unit) {|v| rv << v} if amount + return rv + end + min.step(max, scale_division) {|v| rv << v} + return rv + end + end + end +end |