# # mingchart - a library to generate a flash-based chart using ming. # # Copyright (C) 2003 Satoru Takabayashi # All rights reserved. # This is free software with ABSOLUTELY NO WARRANTY. # # You can redistribute it and/or modify it under the terms of # the GNU General Public License version 2. # require 'ming/ming' require 'time' include Ming module Math module_function def golden_ratio 1 + (Math.sqrt(5) - 1) / 2 end # returns n * 10^m def frexp10 (x, i = 0) if x == 0 return 0, 0 elsif x.abs < 1 return frexp10(x * 10, i - 1) elsif x.abs < 10 return x, i else return frexp10(x.to_f / 10, i + 1) end end end class MingChart Color = Struct.new("Color", :r, :g, :b, :a) class ChartData def initialize (data, side) @data = data @side = side end attr_reader :data attr_reader :side end def initialize (config = {}) raise "flash font must be specified" if config[:flash_font].nil? @font = SWFFont.new(config[:flash_font]) @canvas_width = 1000 @canvas_height = @canvas_width / Math.golden_ratio @canvas_x_margin = @canvas_width * 0.16 @canvas_y_margin = @canvas_height * 0.12 @scale_margin = @canvas_width * 0.01 @label_margin = @canvas_width * 0.018 @x_length = @canvas_width - @canvas_x_margin * 2 @y_length = @canvas_height - @canvas_y_margin * 2 @scale_font_size = (config[:font_size] or 28) @dot_size = 8 @line_width = 2 @x_min = 0 @x_max = 100 @x_nscales = nil @left_y_nscales = nil @right_y_nscales = nil @x_float_p = nil @left_y_float_p = nil @right_y_float_p = nil @left_y_direction = (config[:left_y_direction] or :normal) @right_y_direction = (config[:right_y_direction] or :normal) @background_color = (config[:background_color] or Color.new(0xff, 0xff, 0xff)) @left_y_min = 0 @left_y_max = 100 @right_y_min = 0 @right_y_max = 100 @y_axes = config[:y_axes] @x_time_scale = (config[:x_time_scale] or false) @x_time_scale_format = (config[:x_time_scale_format] or "%Y-%m-%d") @grid_p = (config[:grid] or false) @axis_line_width = 2 @axis_line_color = Color.new(0x40, 0x40, 0x40, 0xff) @scale_text_color = @axis_line_color @scale_line_color = @axis_line_color @scale_line_width = @axis_line_width @scale_line_length = @canvas_width * 0.01 @grid_line_color = Color.new(0xaa, 0xaa, 0xaa, 0xcc) @grid_line_width = @scale_line_width if config[:drawing_methods] @drawing_methods = config[:drawing_methods] else @drawing_methods = [:draw_dotted_line] end if config[:line_colors] @line_colors = config[:line_colors] else @line_colors = [ Color.new(0xff, 0x44, 0x44, 0xcc), Color.new(0x44, 0x44, 0xff, 0xcc), Color.new(0x44, 0xaa, 0x44, 0xcc), Color.new(0xaa, 0xaa, 0x44, 0xcc), Color.new(0xaa, 0x44, 0xaa, 0xcc), Color.new(0x44, 0xaa, 0xaa, 0xcc), Color.new(0x44, 0x44, 0x44, 0xcc) ] end @x_label = (config[:x_label] or "") @left_y_label = (config[:left_y_label] or "") @right_y_label = (config[:right_y_label] or "") @label_text_color = @scale_text_color @label_font_size = @scale_font_size * 1.2 @line_title_text_color = @label_text_color @line_title_font_size = @scale_font_size @line_title_x_margin = @x_length * 0.04 @line_title_y_margin = @y_length * 0.02 @line_title_line_length = @x_length * 0.08 @line_title_line_margin = @line_title_line_length * 0.1 @line_title_background_color = Color.new(0xff, 0xff, 0xff, 0x80) @line_title_line_height_ratio = 1.2 @line_titles = (config[:line_titles] or []) @chart_data = [] @data_id = 0 @left_line_indicator_x = nil @left_line_indicator_y = nil @right_line_indicator_x = nil @right_line_indicator_y = nil @movie = nil end private def convert_x (x) raise if x < @x_min or x > @x_max x_diff = @x_max - @x_min (x - @x_min) * (@x_length / x_diff) + @canvas_x_margin end def convert_y (y, side) if side == :left y_min = @left_y_min y_max = @left_y_max else y_min = @right_y_min y_max = @right_y_max end raise if y < y_min or y > y_max y_diff = y_max - y_min if (side == :left and @left_y_direction == :upside_down) or (side == :right and @right_y_direction == :upside_down) (y_diff - (y_max - y)) * (@y_length / y_diff) + @canvas_y_margin else (y_diff - (y - y_min)) * (@y_length / y_diff) + @canvas_y_margin end end def convert (x, y, side) xx = convert_x(x) yy = convert_y(y, side) return xx, yy end def commify (number, float_p) numstr = if float_p then sprintf("%.2f", number) else number.to_i.to_s end true while numstr.sub!(/^([-+]?\d+)(\d{3})/, '\1,\2') return numstr end def add_axises shape = SWFShape.new shape.set_line(@axis_line_width, *@axis_line_color.to_a) shape.move_pen(@canvas_x_margin, @canvas_y_margin) shape.draw_line(0, @y_length) shape.draw_line(@x_length, 0) shape.draw_line(0, -@y_length) shape.draw_line(-@x_length, 0) @movie.add(shape) end def create_text (string, color, font_size) text = SWFText.new text.set_font(@font) text.set_color(*color) text.set_height(font_size) width = text.get_width(string) height = text.get_ascent text.add_string(string) return text, width, height end def add_y_label (string, x, side) text, width, height = create_text(string, @label_text_color, @label_font_size) y = @canvas_height / 2 x_margin = if side == :left @label_margin + height else -@label_margin end y_margin = width / 2 item = @movie.add(text) item.rotate(90) item.move_to(x + x_margin, y + y_margin) if side == :left @left_line_indicator_x = x + @label_margin + height / 2 @left_line_indicator_y = y - y_margin - @dot_size * 4 else @right_line_indicator_x = x - @label_margin - height / 2 @right_line_indicator_y = y - y_margin - @dot_size * 4 end end def add_left_y_label add_y_label(@left_y_label, 0, :left) end def add_right_y_label add_y_label(@right_y_label, @canvas_width, :right) end def add_x_label text, width, height = create_text(@x_label, @label_text_color, @label_font_size) x = @canvas_width / 2 y = @canvas_height x_margin = width / 2 y_margin = @label_margin item = @movie.add(text) item.move_to(x - x_margin, y - y_margin) end def add_labels add_x_label add_left_y_label add_right_y_label end def adjust_time_scale (min, max) diff = max - min days = diff / 86400 if days < 1 ndivisions = 6 @x_time_scale_format = "%H:%M" elsif days <= 4 ndivisions = days else ndivisions = 4 end return ndivisions, min, max end def adjust_scale (min, max) diff = max - min raise if diff == 0 n, m = Math.frexp10(diff) diff2 = (n * 10).round.to_f # 10-99 ndivisions = nil interval = nil [20, 10, 5, 2].each {|divider| tmp = diff2 / divider if (tmp >=4 and tmp < 8) or divider == 2 ndivisions = tmp.ceil interval = (divider * 10 ** (m - 1)).to_f break end } adjusted_min = (min / interval).floor * interval adjusted_max = adjusted_min + interval * ndivisions while adjusted_max < max adjusted_max += interval ndivisions += 1 end float_p = if interval.floor != interval then true else false end raise if adjusted_max < max raise if adjusted_min > min return ndivisions, adjusted_min, adjusted_max, float_p end def add_scale (n, at, float_p) string = if at == :bottom and @x_time_scale Time.at(n).strftime(@x_time_scale_format) else commify(n, float_p) end text, width, height = create_text(string, @scale_text_color, @scale_font_size) if at == :bottom x_margin = - width / 2 y_margin = height + @scale_margin xx = convert_x(n) yy = @canvas_height - @canvas_y_margin else if at == :left x_margin = - width - @scale_margin xx = @canvas_x_margin else x_margin = @scale_margin xx = @canvas_width - @canvas_x_margin end y_margin = height / 2 yy = convert_y(n, at) end item = @movie.add(text) item.move_to(xx + x_margin, yy + y_margin) line = SWFShape.new line.set_line(@scale_line_width, *@scale_line_color.to_a) line.move_pen_to(xx, yy) if at == :bottom line.draw_line(0, -@scale_line_length) else line.draw_line(@scale_line_length, 0) if at == :left line.draw_line(-@scale_line_length, 0) if at == :right end @movie.add(line) end def add_x_scale (x, float_p) add_scale(x, :bottom, float_p) end def add_y_scale (y, side, float_p) add_scale(y, side, float_p) end def integer_dividable? (a, b) (a.to_f / b) % 1.0 == 0.0 end def add_grid @x_nscales.times {|i| x = @x_min + ((@x_max - @x_min).to_f / @x_nscales) * (i + 1) xx = convert_x(x) yy = @canvas_height - @canvas_y_margin line = SWFShape.new line.set_line(@grid_line_width, *@grid_line_color.to_a) line.move_pen_to(xx, yy) line.draw_line(0, -@y_length) @movie.add(line) } y_nscales = (@left_y_nscales or @right_y_nscales) y_min = (@left_y_min or @right_y_min) y_max = (@left_y_max or @right_y_max) side = if @left_y_nscales then :left else :right end y_nscales.times {|i| y = y_min + ((y_max - y_min).to_f / y_nscales) * (i + 1) xx = @canvas_x_margin yy = convert_y(y, side) line = SWFShape.new line.set_line(@grid_line_width, *@grid_line_color.to_a) line.move_pen_to(xx, yy) line.draw_line(@x_length, 0) @movie.add(line) } end def add_scales add_x_scale(@x_min, @x_float_p) @x_nscales.times {|i| x = @x_min + ((@x_max - @x_min).to_f / @x_nscales) * (i + 1) add_x_scale(x, @x_float_p) } if @left_y_min and @left_y_max add_y_scale(@left_y_min, :left, @left_y_float_p) @left_y_nscales.times {|i| y = @left_y_min + ((@left_y_max - @left_y_min).to_f / @left_y_nscales) * (i + 1) add_y_scale(y, :left, @left_y_float_p) } end if @right_y_min and @right_y_max add_y_scale(@right_y_min, :right, @right_y_float_p) @right_y_nscales.times {|i| y = @right_y_min + ((@right_y_max - @right_y_min).to_f / @right_y_nscales) * (i + 1) add_y_scale(y, :right, @right_y_float_p) } end end def create_rectangle (width, height, color) rect = SWFShape.new rect.set_right_fill(rect.add_fill(*color)) rect.draw_line(0, height) rect.draw_line(width, 0) rect.draw_line(0, -height) rect.draw_line(-width, 0) return rect end def draw_dots (data, color, side) data.each {|x, y| xx, yy = convert(x, y, side) rect = create_rectangle(@dot_size, @dot_size, color) item = @movie.add(rect) item.move(xx - @dot_size / 2, yy - @dot_size / 2) } end def draw_line (data, color, side, fill_p = false) line = SWFShape.new line.set_line(@line_width, *color.to_a) first_time = true if fill_p line.set_right_fill(line.add_fill(color.r, color.g, color.b, color.a / 3)) end xx = nil yy = nil data.each {|x, y| xx, yy = convert(x, y, side) if first_time line.move_pen_to(xx, yy) first_time = false else line.draw_line_to(xx, yy) end } if fill_p line.set_line(@line_width, color.r, color.g, color.b, 0x00) line.draw_line_to(xx, @canvas_height - @canvas_y_margin) line.draw_line_to(convert_x(data.first.first), @canvas_height - @canvas_y_margin) line.draw_line_to(convert_x(data.first.first), convert_y(data.first.last, side)) end @movie.add(line) end def draw_dotted_line (data, color, side) draw_dots(data, color, side) draw_line(data, color, side) end def draw_dotted_filled_line (data, color, side) draw_dots(data, color, side) draw_line(data, color, side, true) end def choose_x (lines_data, method) lines_data.find_all {|data| !data.data.empty? }.map{|data| data.data.map {|x, y| x}.send(method) }.send(method) end def choose_y (lines_data, method) lines_data.find_all {|data| !data.data.empty? }.map {|data| data.data.map {|x, y| y}.send(method) }.send(method) end def adjust_min_max (min, max) if min == 0 min = -1 max = +1 else min = min.to_f / 2 max = max.to_f * 1.5 end return min, max end def configure left_data = @chart_data.find_all {|x| x.side == :left and !x.data.empty? } right_data = @chart_data.find_all {|x| x.side == :right and !x.data.empty? } @left_y_min = choose_y(left_data, :min) @left_y_max = choose_y(left_data, :max) @right_y_min = choose_y(right_data, :min) @right_y_max = choose_y(right_data, :max) if !left_data.empty? and !right_data.empty? @x_min = [choose_x(left_data, :min), choose_x(right_data, :min)].min @x_max = [choose_x(left_data, :max), choose_x(right_data, :max)].max else @x_min = (choose_x(left_data, :min) or choose_x(right_data, :min)) @x_max = (choose_x(left_data, :max) or choose_x(right_data, :max)) end if @x_min == @x_max if @x_time_scale @x_max += 86400 else @x_min, @x_max = adjust_min_max(@x_min, @x_max) end end if@left_y_min and @left_y_max and @left_y_min == @left_y_max @left_y_min, @left_y_max = adjust_min_max(@left_y_min, @left_y_max) end if@right_y_min and @right_y_max and @right_y_min == @right_y_max @right_y_min, @right_y_max = adjust_min_max(@right_y_min, @right_y_max) end @x_nscales, @x_min, @x_max, @x_float_p = if @x_time_scale adjust_time_scale(@x_min, @x_max) else adjust_scale(@x_min, @x_max) end if !left_data.empty? @left_y_nscales, @left_y_min, @left_y_max, @left_y_float_p = adjust_scale(@left_y_min, @left_y_max) end if !right_data.empty? @right_y_nscales, @right_y_min, @right_y_max, @right_y_float_p = adjust_scale(@right_y_min, @right_y_max) end end def add_line_indicator (color, side) left_data = @chart_data.find_all {|x| x.side == :left and !x.data.empty? } right_data = @chart_data.find_all {|x| x.side == :right and !x.data.empty?} return if left_data.empty? or right_data.empty? rect = create_rectangle(@dot_size * 2, @dot_size * 2, color) item = @movie.add(rect) if side == :left return if @left_line_indicator_x.nil? or @left_line_indicator_y.nil? item.move_to(@left_line_indicator_x, @left_line_indicator_y) @left_line_indicator_y -= @dot_size * 4 else return if @right_line_indicator_x.nil? or @right_line_indicator_y.nil? item.move_to(@right_line_indicator_x, @right_line_indicator_y) @right_line_indicator_y -= @dot_size * 4 end end def add_line_titles return if @line_titles.empty? return if @chart_data.empty? total_height = 0 texts = [] @chart_data.each_with_index {|data, lineno| next if data.data.empty? or @line_titles[lineno].nil? text, width, height = create_text(@line_titles[lineno], @line_title_text_color, @line_title_font_size) texts.push([text, width, height, lineno]) } total_height = 0 texts.map {|x| x[2] }.each {|v| total_height += v * @line_title_line_height_ratio } max_width = texts.map {|x| x[1] }.max base_x = @canvas_width - @canvas_x_margin - @line_title_x_margin - @line_title_line_length - @line_title_line_margin * 2 base_y = @canvas_y_margin + @line_title_y_margin rect = create_rectangle(max_width + @line_title_line_length + @line_title_line_margin * 2, total_height, @line_title_background_color) item = @movie.add(rect) item.move_to(base_x - max_width, base_y) texts.each_with_index {|x, i| text, width, height, lineno = x[0], x[1], x[2], x[3] item = @movie.add(text) item.move_to(base_x - width, base_y + height + height * i * @line_title_line_height_ratio) line = SWFShape.new line.set_line(@line_width, *@line_colors[lineno].to_a) line.draw_line(@line_title_line_length, 0) item = @movie.add(line) item.move_to(base_x + @line_title_line_margin, base_y + height * 3 / 4 + height * i * @line_title_line_height_ratio) } end public def draw return if @chart_data.find_all {|data| !data.data.empty? }.empty? @movie = SWFMovie.new @movie.set_dimension(@canvas_width, @canvas_height) background = create_rectangle(@canvas_width, @canvas_height, @background_color) @movie.add(background) configure add_grid if @grid_p add_scales add_axises add_labels i = 0 @chart_data.each {|data| color = @line_colors[i % @line_colors.length] method = @drawing_methods[i % @drawing_methods.length] add_line_indicator(color, data.side) send(method, data.data, color, data.side) i += 1 } add_line_titles end def add_data (data, index = 1) tmp = data.map {|line| x = line.first y = line[index] [x, y] }.find_all {|x, y| x and y } side = if @y_axes and @y_axes[@data_id] @y_axes[@data_id] else :left end @chart_data.push(ChartData.new(tmp, side)) @data_id += 1 end def save (file_name) return if @movie.nil? @movie.next_frame @movie.save(file_name) end end