class Blazer::Result

Attributes

cached_at[RW]
columns[R]
data_source[R]
error[R]
forecast_error[R]
just_cached[RW]
rows[R]

Public Class Methods

new(data_source, columns, rows, error, cached_at, just_cached) click to toggle source
# File lib/blazer/result.rb, line 6
def initialize(data_source, columns, rows, error, cached_at, just_cached)
  @data_source = data_source
  @columns = columns
  @rows = rows
  @error = error
  @cached_at = cached_at
  @just_cached = just_cached
end

Public Instance Methods

anomaly?(series) click to toggle source
# File lib/blazer/result.rb, line 171
def anomaly?(series)
  series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }

  anomaly_detector = Blazer.anomaly_detectors.fetch(Blazer.anomaly_checks)
  anomaly_detector.call(series)
end
cached?() click to toggle source
# File lib/blazer/result.rb, line 19
def cached?
  cached_at.present?
end
chart_type() click to toggle source
# File lib/blazer/result.rb, line 69
def chart_type
  @chart_type ||= begin
    if column_types.compact.size >= 2 && column_types.compact == ["time"] + (column_types.compact.size - 1).times.map { "numeric" }
      "line"
    elsif column_types == ["time", "string", "numeric"]
      "line2"
    elsif column_types == ["string", "numeric"] && @columns.last == "pie"
      "pie"
    elsif column_types.compact.size >= 2 && column_types == ["string"] + (column_types.compact.size - 1).times.map { "numeric" }
      "bar"
    elsif column_types == ["string", "string", "numeric"]
      "bar2"
    elsif column_types == ["numeric", "numeric"]
      "scatter"
    end
  end
end
column_types() click to toggle source
# File lib/blazer/result.rb, line 48
def column_types
  @column_types ||= begin
    columns.each_with_index.map do |k, i|
      v = (rows.find { |r| r[i] } || {})[i]
      if smart_values[k]
        "string"
      elsif v.is_a?(Numeric)
        "numeric"
      elsif v.is_a?(Time) || v.is_a?(Date)
        "time"
      elsif v.nil?
        nil
      elsif v.is_a?(String) && v.encoding == Encoding::BINARY
        "binary"
      else
        "string"
      end
    end
  end
end
detect_anomaly() click to toggle source
# File lib/blazer/result.rb, line 122
def detect_anomaly
  anomaly = nil
  message = nil

  if rows.empty?
    message = "No data"
  else
    if chart_type == "line" || chart_type == "line2"
      series = []

      if chart_type == "line"
        columns[1..-1].each_with_index.each do |k, i|
          series << {name: k, data: rows.map { |r| [r[0], r[i + 1]] }}
        end
      else
        rows.group_by { |r| v = r[1]; (smart_values[columns[1]] || {})[v.to_s] || v }.each_with_index.map do |(name, v), i|
          series << {name: name, data: v.map { |v2| [v2[0], v2[2]] }}
        end
      end

      current_series = nil
      begin
        anomalies = []
        series.each do |s|
          current_series = s[:name]
          anomalies << s[:name] if anomaly?(s[:data])
        end
        anomaly = anomalies.any?
        if anomaly
          if anomalies.size == 1
            message = "Anomaly detected in #{anomalies.first}"
          else
            message = "Anomalies detected in #{anomalies.to_sentence}"
          end
        else
          message = "No anomalies detected"
        end
      rescue => e
        message = "#{current_series}: #{e.message}"
        raise e if Rails.env.development?
      end
    else
      message = "Bad format"
    end
  end

  [anomaly, message]
end
forecast() click to toggle source

TODO cache it? don’t want to put result data (even hashed version) into cache without developer opt-in

# File lib/blazer/result.rb, line 94
def forecast
  count = (@rows.size * 0.25).round.clamp(30, 365)

  forecaster = Blazer.forecasters.fetch(Blazer.forecasting)
  forecast = forecaster.call(@rows.to_h, count: count)

  # round integers
  if @rows[0][1].is_a?(Integer)
    forecast = forecast.map { |k, v| [k, v.round] }.to_h
  end

  @rows.each do |row|
    row[2] = nil
  end
  @rows.unshift(*forecast.map { |k, v| [k, nil, v] })
  @columns << "forecast"

  # reset cache
  @column_types = nil
  @chart_type = nil

  forecast
rescue => e
  @forecast_error = String.new("Error generating forecast")
  @forecast_error << ": #{e.message.sub("Invalid parameter: ", "")}"
  nil
end
forecastable?() click to toggle source
# File lib/blazer/result.rb, line 87
def forecastable?
  @forecastable ||= Blazer.forecasting && column_types == ["time", "numeric"] && @rows.size >= 10
end
smart_values() click to toggle source
# File lib/blazer/result.rb, line 23
def smart_values
  @smart_values ||= begin
    smart_values = {}
    columns.each_with_index do |key, i|
      smart_columns_data_source =
        ([data_source] + Array(data_source.settings["inherit_smart_settings"]).map { |ds| Blazer.data_sources[ds] }).find { |ds| ds.smart_columns[key] }

      if smart_columns_data_source
        query = smart_columns_data_source.smart_columns[key]
        res =
          if query.is_a?(Hash)
            query
          else
            values = rows.map { |r| r[i] }.compact.uniq
            result = smart_columns_data_source.run_statement(ActiveRecord::Base.send(:sanitize_sql_array, [query.sub("{value}", "(?)"), values]))
            result.rows
          end

        smart_values[key] = res.to_h { |k, v| [k.nil? ? k : k.to_s, v] }
      end
    end
    smart_values
  end
end
timed_out?() click to toggle source
# File lib/blazer/result.rb, line 15
def timed_out?
  error == Blazer::TIMEOUT_MESSAGE
end