class Svg_Plot_Gen
Public Class Methods
gen()
click to toggle source
# File lib/svg_plot_gen.rb, line 16 def self.gen # Command line options opts = Trollop::options do opt :template, "Output a template SVG file that contains [SPLIT] markers where a path and display attribute can be inserted. See README.md", :default => false opt :width, "The width of the output plot in pixels", :default => 960 opt :height, "The height of the output plot in pixels", :default => 380 opt :y_first, "Sets the value at which the y axis starts", :default => 0.to_f opt :y_step, "Sets the height of each division on the y axis", :default => 1.to_f opt :y_last, "Sets the value at which the y axis ends", :default => 5.to_f opt :log, "Sets the y axis to a logarithmic scale. `y_first` and `y_last` now refer to powers of 10", :default => false opt :x_text, "The x-axis label", :default => 'x-axis' opt :y_text, "The y-axis label", :default => 'y-axis' opt :cursor_line, "Adds a vertical line that follows the cursor to the graph", :default => false opt :x_margin, "The distance that is left between the edge of the image and the x-axis", :default => 70 opt :y_margin, "The distance that is left between the edge of the image and the y-axis", :default => 100 opt :margins, "The distance that is left between the edge of the image and the plot on sides that do not have labels", :default => 25 opt :long_tick_len, "The length of the long ticks on the graph", :default => 10 opt :short_tick_len, "The length of the short ticks on the graph", :default => 5 opt :font_size, "The size of font used for the plot", :default => 12 opt :label_font_size, "The size of font used for the axis labels", :default => 14 end axis_padding = 6 # Define the solarized colours base03 = Color::RGB.from_html('002b36') base02 = Color::RGB.from_html('073642') base01 = Color::RGB.from_html('586e75') base00 = Color::RGB.from_html('657b83') base0 = Color::RGB.from_html('839496') base1 = Color::RGB.from_html('93a1a1') base2 = Color::RGB.from_html('eee8d5') base3 = Color::RGB.from_html('fdf6e3') yellow = Color::RGB.from_html('b58900') orange = Color::RGB.from_html('cb4b16') red = Color::RGB.from_html('dc322f') magenta = Color::RGB.from_html('d33682') violet = Color::RGB.from_html('6c71c4') blue = Color::RGB.from_html('268bd2') cyan = Color::RGB.from_html('2aa198') green = Color::RGB.from_html('859900') # Calculate the coordinates for the actual graph itself xstart = opts.y_margin xend = opts.width - opts.margins xlen = xend - xstart ystart = opts.margins yend = opts.height - opts.x_margin ylen = yend - ystart # Read in the CSS for our font font_css = File.read(File.dirname(__FILE__) + '/font.css') if opts.cursor_line # Load in the javascript for the cursor line script cursor_line_js = File.read(File.dirname(__FILE__) + '/cursor_line.js') # Replace the [XSTART] and [XEND] placeholders in the javascript cursor_line_js = cursor_line_js.gsub(/\[XSTART\]/, xstart.to_s).gsub(/\[XEND\]/, xend.to_s) end # Set the template marker if specified if opts.template # Put in a split marker when the data actully goes plot = '[SPLIT]' no_data_display = '[SPLIT]' else plot = '' no_data_display = 'none' end # Work out the x-positions of our vertical lines and ticks xlines_step = xlen.to_f / 24 xlines = Hash[(xstart..xend).step(xlines_step).map { |x| x.round(1) }.zip((0..24).map { |x| (x%2 == 0) })] # Build up our x-axis labels for a 24 hour period xlabels = (0..24).step(2).map { |hour| [['00', (hour%24).to_s].join[-2..-1], ':00'].join }.zip(xlines.select { |k, v| v }.map { |k, v| k }) y_range = (opts.y_first..opts.y_last).step(opts.y_step) # Generates the values for a logarithmic scale if opts.log # Work out the y-positions of the horizonal lines and ticks ylines = Hash[y_range .map { |y| (y < opts.y_last ? [1, 2.5, 5, 7.5].map { |i| i*opts.y_step } : [1]) .map { |m| [Math.log10((10**y)*m), m==1] } } .flatten(1) .map { |y| [(yend - (y[0] - opts.y_first)*(ylen.to_f/(opts.y_last - opts.y_first))).round(1), y[1]] }] # Build up our y-axis labels ylabels = y_range .map{ |i| 10**i } .map{ |i| i <= 10000 ? ("%d" % i) : ("%.0e" % i) } .zip(ylines .select { |k ,v| v } # Select only major ticks .map { |k, v| k }) else # Generate the values for a linear scale # Work out the y-positions of the horizonal lines and ticks ylines = Hash[y_range .map { |y| (y < opts.y_last ? [0, 0.25, 0.5, 0.75].map { |i| i*opts.y_step } : [0]) .map { |m| [y+m, m==0] } } .flatten(1) .map { |y| [(yend - (y[0] - opts.y_first)*(ylen.to_f/(opts.y_last - opts.y_first))).round(1), y[1]] }] # Build up our y-axis labels ylabels = y_range .map{ |i| i.whole_to_i } .zip(ylines .select { |k, v| v } # Select only major ticks .map { |k, v| k }) end # Output some useful info about the plot we're generating to stderr $stderr.puts ["X Origin ", xstart].join $stderr.puts ["Y Origin ", yend].join $stderr.puts ["X Length ", xlen].join $stderr.puts ["Y Length ", ylen].join $stderr.puts ["Px per x division ", (xlen.to_f/12).round(2)].join $stderr.puts ["Px per y division ", (ylen.to_f/(opts.y_last - opts.y_first)).round(2)].join # Create the XML object builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml| xml.doc.create_internal_subset( # Create the DOCTYPE 'svg', '-//W3C//DTD SVG 1.1//EN', 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' ) xml.svg(:viewbox => [0, 0, opts.width, opts.height].join(' '), :width => opts.width, :height => opts.height, :xmlns => "http://www.w3.org/2000/svg", 'xmlns:xlink' => "http://www.w3.org/1999/xlink", :onmousemove => "on_mouse_move(event)", :style => "font-family: 'Exo'") do # Font xml.defs do xml.style(:type => 'text/css') do xml.cdata(font_css) end end if opts.cursor_line # Cursor Line Script xml.script(:type => 'text/javascript') do xml.cdata(cursor_line_js) end # The cursor line itself xml.path(:id => 'cursor_line', :d => ['M0,', ystart, 'l0,', ylen].join, :stroke => orange.html, 'stroke-width' => '1.5', :display => 'none') end # X-Axis - Vertical Lines xml.g(:style => "color:grey", :stroke => "currentColor", :transform => ['translate(0,', ystart, ')'].join) do xlines.each do |x, tl| xml.path(:d => ['m', x, ' 0v', ylen].join) end end # X-Axis - Ticks xml.g(:style => "color:black", :stroke => "currentColor", :transform => ['translate(0,', ystart, ')'].join) do xlines.each do |x, mm| tl = mm ? opts.long_tick_len : opts.short_tick_len # The tick length to set based on if the division is a major one xml.path(:d => ['m', x, ' 0v', tl, 'm0 ', ylen-(2*tl), 'v', tl].join) end end # X-Axis - Labels xml.g(:style => 'text-anchor:middle', :stroke => 'none', :transform => ['translate(0,', yend+2*opts.font_size, ')'].join, 'font-size' => [opts.font_size, 'pt'].join, :fill => "#000") do xlabels.each do |label| xml.g(:transform => ['translate(', label[1], ')'].join) do xml.text_ label[0] end end end # X-Axis Text Label xml.g(:style => 'text-anchor: middle', :transform => ['translate(', xstart + xlen/2, ' ', opts.height - (opts.margins - opts.font_size), ')'].join, 'font-size' => [opts.label_font_size, 'pt'].join, :fill => '#000') do xml.text_ opts.x_text end # Y-Axis - Horizontal Lines xml.g(:style => "color:grey", :stroke => "currentColor", :transform => ['translate(', xstart, ',0)'].join) do ylines.each do |y, mm| if mm # Only draw lines for major divisions xml.path(:d => ['m0 ', y, 'h', xlen].join) end end end # Y-Axis - Ticks xml.g(:style => "color:black", :stroke => "currentColor", :transform => ['translate(', xstart, ',0)'].join) do ylines.each do |y, mm| tl = mm ? opts.long_tick_len : opts.short_tick_len # The tick length to set based on if the division is a major one xml.path(:d => ['m0 ', y, 'h', tl, 'm', xlen-(2*tl), ' 0h', tl].join) end end # Y-Axis - Labels xml.g(:style => 'text-anchor:end', :stroke => 'none', :transform => ['translate(', xstart-axis_padding, ',0)'].join, 'font-size' => [opts.font_size, 'pt'].join, :fill => "#000") do ylabels.each do |label| xml.g(:transform => ['translate(0,', label[1]+(opts.font_size/2), ')'].join) do xml.text_ label[0] end end end # Y-Axis text label xml.g(:style => 'text-anchor:middle', :transform => ['translate(', opts.margins, ' ', ystart + ylen/2, ') rotate(-90)'].join, 'font-size' => [opts.label_font_size, 'pt'].join, :fill => '#000') do xml.text_ opts.y_text end # Bounding Box xml.g(:stroke => "#333", :fill => "none") do xml.path(:d => ['m', xstart, ' ', ystart, 'v', ylen, 'h', xlen, 'v', -ylen , 'h', -xlen, 'z'].join) end # Plots #xml.g(:transform => ['translate(', xstart, ',', yend, ') scale(', 1000.to_f/xlen, ',', -(1000.to_f/ylen), ')'].join) do xml.g(:transform => ['translate(', xstart, ',', yend, ') scale(1, -1)'].join) do xml.a('xlink:title' => 'Plot #1') do xml.g('stroke-width' => 1.4, 'stroke-linejoin' => 'bevel', :fill => 'none') do xml.path(:stroke => red.html, :d => plot) end end end # No data marker xml.g(:style => 'text-anchor:middle', :stroke => red.html, :transform => ['translate(', xstart + xlen/2, ',', ystart + ylen/2, ')'].join, 'font-size' => [opts.label_font_size+16, 'pt'].join, :display => no_data_display, :fill => red.html) do xml.text_ 'No Data Found' end # An invisible rectangle to stop the final graph being highlighted and so on xml.rect(:x => 0, :y => 0, :width => opts.width, :height => opts.height, :fill => 'white', 'fill-opacity' => 0, :stroke => 'none', 'stroke-width' => 0, :onclick => 'null_handler') end end # Output the XML puts builder.to_xml end