diff --git a/Gemfile b/Gemfile index 7005e4f90b..da477d45d1 100644 --- a/Gemfile +++ b/Gemfile @@ -69,9 +69,7 @@ gem 'braintree', '~> 2.93' gem 'bugsnag', '~> 6.11' gem 'cancancan', '~> 2.3.0' gem 'formtastic', '~> 2.3.1' -gem 'gruff', '~>0.3', require: false gem 'htmlentities', '~>4.3', '>= 4.3.4' -gem 'rmagick', '~> 2.15.3', require: false # TODO: Not actively maintained https://github.com/activeadmin/inherited_resources#notice replace with respond_with and fix things the rails way gem 'inherited_resources', '~> 1.12.0' gem 'has_scope', '~> 0.7.2' # remove line after we stop supporting Ruby 2.4 @@ -100,6 +98,7 @@ gem 'json-schema', git: 'https://github.com/3scale/json-schema.git' gem 'paperclip', '~> 6.0' gem 'prawn' gem 'prawn-table', git: "https://github.com/prawnpdf/prawn-table.git", branch: "38b5bdb5dd95237646675c968091706f57a7a641" +gem 'prawn-svg' gem 'rails_event_store', '~> 0.9.0', require: false gem 'ratelimit' gem 'recaptcha', '4.13.1', require: 'recaptcha/rails' @@ -109,6 +108,7 @@ gem 'redis', '~> 4.1.3', require: ['redis', 'redis/connection/hiredis'] gem 'redis-namespace', '~> 1.7.0' gem 'rest-client', '~> 2.0.2' gem 'rubyzip', '~>1.3.0', require: false +gem 'svg-graph', require: false gem 'swagger-ui_rails', git: 'https://github.com/3scale/swagger-ui_rails.git', branch: 'dev' gem 'swagger-ui_rails2', git: 'https://github.com/3scale/swagger-ui_rails.git', branch: 'dev-2.1.3' gem 'thinking-sphinx', '~> 5.4.0' diff --git a/Gemfile.lock b/Gemfile.lock index 3e50cc3047..c5c36c907e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -266,6 +266,8 @@ GEM crack (0.4.5) rexml crass (1.0.6) + css_parser (1.12.0) + addressable cucumber (7.0.0) builder (~> 3.2, >= 3.2.4) cucumber-core (~> 10.0, >= 10.0.1) @@ -365,8 +367,6 @@ GEM github-markdown (0.6.9) globalid (0.4.2) activesupport (>= 4.2.0) - gruff (0.7.0) - rmagick (~> 2.13, >= 2.13.4) has_scope (0.7.2) actionpack (>= 4.1) activesupport (>= 4.1) @@ -528,6 +528,10 @@ GEM prawn (2.4.0) pdf-core (~> 0.9.0) ttfunk (~> 1.7) + prawn-svg (0.32.0) + css_parser (~> 1.6) + prawn (>= 0.11.1, < 3) + rexml (~> 3.2) prometheus-client-mmap (0.11.0) protected_attributes_continued (1.8.2) activemodel (>= 5.0) @@ -653,7 +657,6 @@ GEM netrc (~> 0.8) rexml (3.2.5) riddle (2.4.3) - rmagick (2.15.4) roar (1.0.4) representable (>= 2.0.1, < 2.4.0) roar-rails (1.0.2) @@ -792,6 +795,7 @@ GEM stripe (5.28.0) strong_migrations (0.6.8) activerecord (>= 5) + svg-graph (2.2.1) sys-uname (1.2.2) ffi (~> 1.1) temple (0.8.1) @@ -949,7 +953,6 @@ DEPENDENCIES formtastic (~> 2.3.1) github-markdown globalid (~> 0.4.2) - gruff (~> 0.3) has_scope (~> 0.7.2) hashie hiredis (~> 0.6.3) @@ -987,6 +990,7 @@ DEPENDENCIES pg (~> 0.21.0) pisoni (~> 1.29) prawn + prawn-svg prawn-table! protected_attributes_continued (~> 1.8.2) pry-byebug (>= 3.7.0) @@ -1013,7 +1017,6 @@ DEPENDENCIES reek (= 6.01) reform (~> 2.0.3) rest-client (~> 2.0.2) - rmagick (~> 2.15.3) roar-rails rspec-html-matchers! rspec-rails (~> 4.1) @@ -1045,6 +1048,7 @@ DEPENDENCIES statsd-ruby stripe (~> 5.28.0) strong_migrations (~> 0.6.8) + svg-graph swagger-ui_rails! swagger-ui_rails2! thin diff --git a/app/lib/pdf/data.rb b/app/lib/pdf/data.rb index 6cf6b62079..b5dfed8355 100644 --- a/app/lib/pdf/data.rb +++ b/app/lib/pdf/data.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'gruff' - module Pdf class Data @@ -74,74 +72,11 @@ def metrics end end - def traffic_graph - return if @hit_metric.nil? - - data = @source.usage(@options) - - g = Gruff::Line.new('1200x300') - - g.theme = { - :colors => ['#9172EC', '#306EFF', '#000066', '#B4B4B4'], - :font_color => '#555', - :marker_color => '#eeeeee', - :background_colors => ['#ffffff', '#ffffff'] - } - - g.legend_box_size = 10 - g.hide_title = true - g.legend_font_size = 13 - g.hide_line_markers = false - g.marker_font_size = 13 - g.hide_dots = false - g.dot_radius = 3 - g.line_width = 2 - g.marker_count = 5 - g.margins = 2 - - g.sort = false - g.x_axis_label = @period == :day ? "Hour" : "Week Days" - g.y_axis_label = @hit_metric.friendly_name - - data = data[:values] - g.data(@hit_metric.friendly_name, data) - - max = data.max - g.maximum_value = max + (max / 5) - - g.labels = send("#{@period}_labels") - StringIO.new(g.to_blob("JPG")) + def usage + @source.usage(@options) unless @hit_metric.nil? end - def week_labels - # Week report, there are 28 data points, at 6 hour intervals - # What was the date 1 week ago - date = 1.week.ago.beginning_of_day - - #Gruff expects labels to be presented as a hash - labels= {} - (0..27).each do |point| - # Insert blanks except for every 4th data point (covering a 24hr period) - if (point % 4).zero? - labels[point] = date.strftime("%d %b") - date += 1.day - end - end - - labels - end - - def day_labels - # See week_labels - date = 1.day.ago.beginning_of_day - - labels = {} - (0..23).each do |point| - labels[point] = date.strftime("%k:00") if (point % 4).zero? - date += 1.hour - end - labels - end + private def sanitize_text(text) EscapeUtils.escape_javascript(text.to_s) diff --git a/app/lib/pdf/report.rb b/app/lib/pdf/report.rb index 19e2792ac2..b06c56fe90 100644 --- a/app/lib/pdf/report.rb +++ b/app/lib/pdf/report.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'SVG/Graph/DataPoint' +require 'SVG/Graph/Line' + # REFACTOR: extract abstract Report class, and DRY functionality with InvoiceReporter module Pdf class Report @@ -35,6 +38,7 @@ def generate header + move_down 3 traffic_graph move_down 3 @@ -128,11 +132,158 @@ def traffic_and_users end end + def graph_key_formatter(usage) + if @period == :day + ->(point) { (point % 4).zero? ? "" : sprintf("%02d:00", point) } + else + since = Time.parse(usage.dig(:period, :since)) + granularity = usage.dig(:period, :granularity) + ->(point) { (point % 4).zero? ? "" : (since + point * granularity).strftime("%d %b") } + end + end + def traffic_graph - graph = @data.traffic_graph - return unless graph - subtitle "Traffic" - @pdf.image graph, position: :left, width: 520 + usage = @data.usage + + return unless usage + + options = { + graph_title: "Traffic", + show_graph_title: true, + key: false, + area_fill: false, + show_data_values: false, + add_popups: false, + width: TABLE_FULL_WIDTH, + height: TABLE_FULL_WIDTH / 4, + step_x_labels: 4, + step_include_first_x_label: true, + fields: usage[:values].each_index.map(&:succ).map(&graph_key_formatter(usage)), + show_x_title: false, + x_title: @period == :day ? "Hour" : "Week Days", + show_y_guidelines: true, + scale_integers: true, + scale_divisions: [2, (usage[:values].max - usage[:values].min) / 5].max, + number_format: IntegerWithDelimiterFormatter.new, + show_y_title: false, + y_title: usage.dig(:metric, :name), + y_title_location: :middle, + no_css: false, + } + + graph = SVG::Graph::Line.new(options) + + graph.add_data(data: usage[:values], title: options[:y_title]) + + @pdf.svg traffic_graph_style(graph.burn_svg_only) + end + + def traffic_graph_style(svg) + xml = Nokogiri::XML(svg) + style = xml.at_css("style") + css = CssParser::Parser.new + css.load_string!(style.text.gsub(/ff0000/i, "9273ED")) + traffic_graph_first_data_point(xml) + traffic_graph_y_align(xml) + traffic_graph_style_clean_up(css) + traffic_graph_style_guide_lines(css) + traffic_graph_style_axes(css) + traffic_graph_style_line_width(css) + traffic_graph_style_background(css) + traffic_graph_style_text(css) + + style.content = css.to_s + xml.to_s + end + + # TODO: remove this hack ofter fix is acepted upstream + # https://github.com/lumean/svg-graph2/pull/43 + def traffic_graph_first_data_point(xml) + line = xml.at_css(".line1") + line[:d] = line[:d].sub(/^M.+L\s*(\S+\s+\S+)(.*)$/, 'M\1 L\2') + end + + # TODO: remove this hack after fix is accepted upstream + # https://github.com/lumean/svg-graph2/pull/44 + def traffic_graph_y_align(xml) + xml.css(".yAxisLabels").each do |label| + label[:x] = "-8" + label.delete("style") + label.delete("transform") + end + end + + def traffic_graph_style_background(css) + desired = <<-EOT + .graphBackground { + fill:#ffffff; + } + EOT + + css.remove_rule_set!(css.find_rule_sets([".graphBackground"]).first) + css.add_block!(desired) + end + + def traffic_graph_style_text(css) + desired = <<-EOT + .xAxisLabels,.yAxisLabels { + fill:#909090; + font-size: 10px; + font-family: "#{@style[:font]}", sans-serif; font-weight: normal; + } + .mainTitle { + fill:#505050; + font-family: "#{@style[:font]}", sans-serif; font-weight: normal; + } + EOT + + css.add_block!(desired) + end + + def traffic_graph_style_axes(css) + desired = <<-EOT + .axis{ + stroke: #ffffff; + stroke-width: 0px; + } + EOT + + css.remove_rule_set!(css.find_rule_sets([".axis"]).first) + css.add_block!(desired) + end + + def traffic_graph_style_line_width(css) + desired = <<-EOT + .line1 { + stroke-width: 2px; + } + EOT + + css.add_block!(desired) + end + + def traffic_graph_style_guide_lines(css) + desired = <<-EOT + .guideLines,#yAxis { + stroke: #eeeeee; + stroke-width: 0.3px; + stroke-dasharray: 0.01 1; + stroke-linejoin: round; + stroke-linecap: round; + } + EOT + + css.remove_rule_set!(css.find_rule_sets([".guideLines"]).first) + css.add_block!(desired) + end + + def traffic_graph_style_clean_up(css) + (2..12).each do |num| + %w[line fill key dataPoint].each do |type| + rule = css.find_rule_sets([".#{type}#{num}"]).first + css.remove_rule_set!(rule) if rule.present? + end + end end def metrics @@ -189,5 +340,13 @@ def colorize_num(numstr) { content: numstr, **@style[:green] } end end + + class IntegerWithDelimiterFormatter + include ActionView::Helpers::NumberHelper + + def %(num) + number_with_delimiter(num.to_i) + end + end end end diff --git a/config/initializers/prawn.rb b/config/initializers/prawn.rb index 70f4280822..cb1bf7c55c 100644 --- a/config/initializers/prawn.rb +++ b/config/initializers/prawn.rb @@ -1,7 +1,4 @@ -# require 'prawn' -# require 'prawn/format' require "prawn/measurement_extensions" -require 'gruff' require "open-uri" module Prawn