module Rgviz module ViewHelper def rgviz(options = {}) def get_package(name) down = name.downcase case down when 'areachart', 'barchart', 'bubblechart', 'candlestickchart', 'columnchart', 'combochart', 'linechart', 'piechart', 'scatterchart', 'steppedareachart' then 'corechart' else down end end options = options.with_indifferent_access id = options[:id] kind = options[:kind] url = options[:url] query = options[:query] || '' events = options[:events] || {} html = options[:html] || {} hidden = options[:hidden] conditions = options[:conditions] virtual_columns = options[:virtual_columns] package = options[:package] load = options[:load] rgviz_events, google_events = events.partition{|x| x[0].to_s.start_with? 'rgviz'} rgviz_events = rgviz_events.inject(Hash.new){|h, y| h[y[0]] = y[1]; h} rgviz_events = rgviz_events.with_indifferent_access ajax = options.has_key?(:ajax) && options[:ajax] load_package = !ajax && (!options.has_key?(:load_package) || options[:load_package]) load_google = !ajax && (!options.has_key?(:load_google) || options[:load_google]) html_prefix = (options[:html_prefix] || 'html').to_s js_prefix = (options[:js_prefix] || 'js').to_s param_prefix = (options[:param_prefix] || 'param').to_s html_prefix += '_' js_prefix += '_' param_prefix += '_' debug = options[:debug] opts = options[:options] || {} opts[:width] = 640 unless opts[:width] opts[:height] = 480 unless opts[:height] params = [] uses_rgviz_get_value = false uses_rgviz_append = false visitor = MagicNamesVisitor.new(html_prefix, js_prefix, param_prefix) opts.each do |key, value| next unless value.kind_of?(String) source = visitor.get_source(value, false) next unless source[:source] case source[:source] when :html s = "rgviz_get_value('#{source[:id]}')" def s.encode_json(options = {}) self end opts[key] = s uses_rgviz_get_value = true when :js s = "#{source[:id]}()" def s.encode_json(options = {}) self end opts[key] = s when :param s = "param_#{source[:id]}" def s.encode_json(options = {}) self end opts[key] = s params << source[:id].to_i unless params.include?(source[:id]) end end opts = opts.to_json raise "Must specify an :id" unless id raise "Must specify a :kind" unless kind raise "Must specify a :url" unless url custom_executor = RgvizRails.inherits_from_active_record(url) || url.respond_to?(:execute) || url.is_a?(Rgviz::Table) || url.is_a?(Array) url = url_for url unless custom_executor # Parse the query query = RgvizRails::Parser.parse query # And replace the html_ and javascript_ magic names query.accept visitor query_builder = visitor.query_builder query_builder_var = visitor.query_builder_var uses_rgviz_get_value |= visitor.uses_rgviz_get_value? uses_rgviz_append |= visitor.uses_rgviz_append? visitor.params.each{|p| params << p unless params.include?(p)} params = params.sort!.map{|i| "param_#{i}"} callback = "rgviz_draw_#{id}" out = '' # Write the div out << "
\n" # Output the google jsapi tag the first time @first_time ||= 1 if load != :auto && load_google && @first_time == 1 out << "\n" end # Now the real script out << "\n" if load == :auto && !ajax && !hidden && params.empty? require 'cgi' package ||= get_package(kind) autoload = {'modules' => [{'name' => 'visualization', 'version' => '1', 'packages' => ["#{package}"], 'callback' => callback}]} out << "\n" end @first_time = 0 if Rails::VERSION::MAJOR == 2 out else raw out end end module_function :rgviz end class MagicNamesVisitor < Visitor def initialize(html_prefix, js_prefix, param_prefix) @html_prefix = html_prefix @js_prefix = js_prefix @param_prefix = param_prefix @s = '' @params = [] end def query_builder @s.strip end def query_builder_var 'q' end def params @params end def uses_rgviz_get_value? @uses_rgviz_get_value end def uses_rgviz_append? @uses_rgviz_append end def visit_query(node) @s << "var q = '" if node.select && node.select.columns && node.select.columns.length > 0 node.select.accept self else @s << 'select * ' end node.where.accept self if node.where node.group_by.accept self if node.group_by node.pivot.accept self if node.pivot node.order_by.accept self if node.order_by @s << "limit #{node.limit} " if node.limit @s << "offset #{node.offset} " if node.offset if node.labels @s << "label " node.labels.each_with_index do |l, i| @s << ', ' if i > 0 l.accept self end end if node.formats @s << "format " node.formats.each_with_index do |f, i| @s << ', ' if i > 0 f.accept self end end if node.options @s << "options " @s << "no_values " if node.options.no_values @s << "no_format " if node.options.no_format end @s << "';\n" false end def visit_select(node) @s << "select "; print_columns node @s << " " false end def visit_where(node) @s << "where " node.expression.accept self @s << " " false end def visit_group_by(node) @s << "group by " print_columns node @s << " " false end def visit_pivot(node) @s << "pivot " print_columns node @s << " " false end def visit_order_by(node) @s << "order by " node.sorts.each_with_index do |s, i| @s << ', ' if i > 0 s.column.accept self @s << ' ' @s << s.order.to_s end @s << " " false end def visit_label(node) node.column.accept self @s << ' ' if node.label.include?("'") val = node.label.gsub("'", "\\'") @s << "\"#{val}\"" else @s << "\\'#{node.label}\\'" end false end def visit_format(node) node.column.accept self @s << ' ' if node.pattern.include?("'") @s << "\"#{node.pattern}\"" else @s << "\\'#{node.pattern}\\'" end false end def visit_logical_expression(node) @s += "(" node.operands.each_with_index do |operand, i| @s += " #{node.operator} " if i > 0 operand.accept self end @s += ")" false end def visit_binary_expression(node) if node.operator == BinaryExpression::Eq source = has_magic_name?(node.right) if source @s << "';\n" case source[:source] when :html @s << "var s = rgviz_get_value('#{source[:id]}');\n" append_selections node, source @uses_rgviz_get_value = true when :js @s << "var s = #{source[:id]}();\n" @s << "if (typeof(s) != 'object') s = [s];\n" append_selections node, source when :param @s << "var s = param_#{source[:id]};\n" @s << "if (typeof(s) != 'object') s = [s];\n" append_selections node, source @params << source[:id].to_i unless @params.include?(source[:id]) end @s << "q += '" return false end end node.left.accept self @s << " #{node.operator} " node.right.accept self false end def visit_unary_expression(node) if node.operator == UnaryExpression::Not @s << "not " node.operand.accept self else node.operand.accept self @s << " #{node.operator}" end false end def visit_id_column(node) source = get_source node.name case source[:source] when nil @s << "`#{node.name}`" when :html append_before_source_type source[:type] @s << " + rgviz_get_value('#{source[:id]}') + " append_after_source_type source[:type] @uses_rgviz_get_value = true when :js append_before_source_type source[:type] @s << " + #{source[:id]}() + " append_after_source_type source[:type] when :param append_before_source_type source[:type] @s << " + param_#{source[:id]} + " append_after_source_type source[:type] @params << source[:id].to_i unless @params.include?(source[:id]) end end def visit_number_column(node) @s << node.value.to_s end def visit_string_column(node) value = node.value.gsub('"', '\"') @s << "\"#{value}\"" end def visit_boolean_column(node) @s << node.value.to_s end def visit_date_column(node) @s << "date \"#{node.value.to_s}\"" end def visit_date_time_column(node) @s << "date \"#{node.value.strftime('%Y-%m-%d %H:%M:%S')}\"" end def visit_time_of_day_column(node) @s << "date \"#{node.value.strftime('%H:%M:%S')}\"" end def visit_scalar_function_column(node) case node.function when ScalarFunctionColumn::Sum, ScalarFunctionColumn::Difference, ScalarFunctionColumn::Product, ScalarFunctionColumn::Quotient node.arguments[0].accept self @s << " #{node.function} " node.arguments[1].accept self else @s << "#{node.function}(" node.arguments.each_with_index do |a, i| @s << ', ' if i > 0 a.accept self end @s << ")" end false end def visit_aggregate_column(node) @s << "#{node.function}(" node.argument.accept self @s << ")" false end def print_columns(node) node.columns.each_with_index do |c, i| @s << ', ' if i > 0 c.accept self end end def get_source(name, include_type = true) if name.start_with?(@html_prefix) if include_type get_source_type :html, name[@html_prefix.length .. -1] else {:source => :html, :id => name[@html_prefix.length .. -1]} end elsif name.start_with?(@js_prefix) if include_type get_source_type :js, name[@js_prefix.length .. -1] else {:source => :js, :id => name[@js_prefix.length .. -1]} end elsif name.start_with?(@param_prefix) if include_type get_source_type :param, name[@param_prefix.length .. -1] else {:source => :param, :id => name[@param_prefix.length .. -1]} end else {} end end def get_source_type(source, name) if name.start_with?('number_') {:source => source, :id => name[7 .. -1], :type => :number} elsif name.start_with?('string_') {:source => source, :id => name[7 .. -1], :type => :string} elsif name.start_with?('date_') {:source => source, :id => name[5 .. -1], :type => :date} elsif name.start_with?('datetime_') {:source => source, :id => name[9 .. -1], :type => :datetime} elsif name.start_with?('timeofday_') {:source => source, :id => name[10 .. -1], :type => :timeofday} else {:source => source, :id => name, :type => :string} end end def append_before_source_type(type) case type when :number return when :string @s << "\"'" when :date @s << "date \"'" when :datetime @s << "datetime \"'" when :timeofday @s << "timeofday \"'" end end def append_after_source_type(type) case type when :number return else @s << "'\"" end end def append_selections(node, source) @s << "q += rgviz_append(s, '"; node.left.accept self @s << " #{node.operator} " append_before_source_type source[:type] @s << ", " append_after_source_type source[:type] @s << "');\n" @uses_rgviz_append = true end def has_magic_name?(node) return false unless node.kind_of?(IdColumn) source = get_source node.name return false unless source[:source] return source end end end