Thursday, July 24, 2008

vertical labels in gruff


It's been a while since I blogged anything, so I decided to post how to create vertical labels in gruff.

First of all - the bitching. Even though gruff has been around for 3 years now, it is still pretty basic. I don't want to sound ungrateful; if it wasn't for Geoffrey Grosenbach, it would have taken me months to come up with a decent charting library. So, thanks Geoffrey!

The first problem that came up was the issue of long labels. If the labels text is too long, gruff will draw them on top of each other. There is no option to specify the orientation (horizontal vs vertical), so I decided to take a look at the code and make the necessary modifications. I'm using stacked bars, so I'm only documenting how to modify label orientation for StackedBar; the other classes should be similar.

You only need to modify Gruff::Base::draw_label and Gruff::StackedBar::draw. These functions are defined in the lib/gruff/base.rb and lib/gruff/stacked_bar.rb files in the gruff gems directory. Here are the modified versions:


class Gruff::Base
def draw_label(x_offset, index, y_offset = @graph_bottom + LABEL_MARGIN)
return if @hide_line_markers

if !@labels[index].nil? && @labels_seen[index].nil?
@d.fill = @font_color
@d.font = @font if @font
@d.stroke('transparent')
@d.font_weight = NormalWeight
@d.pointsize = scale_fontsize(@marker_font_size)
@d.gravity = CenterGravity
@d.rotation = -90
@d = @d.annotate_scaled(@base_image,
1.0, 1.0,
x_offset, y_offset,
@labels[index], @scale)
@d.rotation = 90
@labels_seen[index] = 1
debug { @d.line 0.0, y_offset, @raw_columns, y_offset }
end
end
end

class Gruff::StackedBar
# Draws a bar graph, but multiple sets are stacked on top of each other.
def draw
get_maximum_by_stack
super
return unless @has_data

# Setup spacing.
#
# Columns sit stacked.
spacing_factor = 0.9
@bar_width = @graph_width / @column_count.to_f

@d = @d.stroke_opacity 0.0

height = Array.new(@column_count, 0)

@norm_data.each_with_index do |data_row, row_index|
@d = @d.fill data_row[DATA_COLOR_INDEX]

data_row[1].each_with_index do |data_point, point_index|
next if (data_point == 0)
# Use incremented x and scaled y
left_x = @graph_left + (@bar_width * point_index)
left_y = @graph_top + (@graph_height -
data_point * @graph_height -
height[point_index]) + 1
right_x = left_x + @bar_width * spacing_factor
right_y = @graph_top + @graph_height - height[point_index] - 1

# update the total height of the current stacked bar
height[point_index] += (data_point * @graph_height )

@d = @d.rectangle(left_x, left_y, right_x, right_y)

end
end

@norm_data.each_with_index do |data_row, row_index|
data_row[1].each_with_index do |data_point, point_index|
next if (data_point == 0)
label_center = @graph_left + (@bar_width * point_index) + (@bar_width * spacing_factor / 2.0)
label_width = calculate_width(@marker_font_size, @labels[point_index])
draw_label(label_center, point_index, @graph_top + @graph_height - height[point_index] + 1 - label_width / 2 - 10)
end
end

@d.draw(@base_image)
end

end


Little code as been changed. The @d.rotation lines in Base::draw_label() do the magic of rotating the labels. The tricky thing is that we gotta pass the height at which the labels should be drawn, since the height is not constant any more. All that is calculated in StackedBar::draw().

The best thing is to copy this code into a new file (e.g. gruff_fix.rb) and require it into your charting code (after require 'gruff'). For Rails, save it to the lib/ directory.

If your labels are incredibly long (like mine), you might want to modify Gruff:Base::setup_graph_measurements to account for the labels ('s height). Modify the calculation of the @graph_top instance variable (see last line):


@graph_top = @top_margin +
(@hide_title ? TITLE_MARGIN : @title_caps_height + TITLE_MARGIN * 2) +
(@hide_legend ? LEGEND_MARGIN : @legend_caps_height + LEGEND_MARGIN * 2) +
calculate_width(@marker_font_size, @labels.max { |x, y| x.length <=> y.length }) / 2


It took me a few hours to get this to work nicely, because I was to lazy to understand the coordinate system. Anyway...

Tuesday, April 29, 2008

Sync-ing Mongrel Cluster

I've been looking for a solution for implementing Mongrel cluster synchronization. My application writes and updates XML files on disk. The possibility of having 2 (or more) users update the same file at the same time is very real. I'm using a Mongrel cluster for scalability.

I can't believe I haven't found an answer for this on the Internet. I thought this is a common problem for people in production. I did come across this problem before. I solved it by creating a separate bottle-neck process which performed the region which had to be sync-ed. The code was wrapped in a Mutex.synchronize block. All Mongrel instances called the bottle-neck process' only function through DRb.

I don't want to use this solution again. It is difficult to maintain, especially in production environments. So, without finding anything else on the Internet, the only other thing I could think of was using database locking. I will use the locking features of MySQL to create a synchronized region in a Mongrel cluster setup.

Rails provides optimistic and pessimistic locking through MySQL (see http://api.rubyonrails.com/classes/ActiveRecord/Locking/Pessimistic.html and http://api.rubyonrails.com/classes/ActiveRecord/Locking/Optimistic.html). In my case I didn't even consider optimistic locking because it requires conflict resolution. For my specific case conflict resolution was too difficult to do, so I looked into pessimistic locking. Here is a step-by-step breakdown of implementing it:
1. Create a model (no fields are necessary; mine is called lockers)
2. Migrate it (and insert one record in it - in the migrations script, e.g. 0)
3. Implement the synchronization region in your controller. Here's my code:

class DoerController < ApplicationController
def do
Locker.transaction do
r = Locker.find(1, :lock => true)
sleep 10
r.save!
end
redirect_to :action => :index
end
end


Now, the code inside the Locker.transaction block is synchronized. You can test it by opening X Firefox/Explorer tabs to X instances of Mongrel and click the "do" link at the same time. The first Mongrel will takes 10 seconds to finish, the second Mongrel 20 seconds, etc.

One word of advice: make sure you don't create the lockers table manually. The default "CREATE TABLE" statement in MySQL creates a tables with the myisam engine type, which does not support locking!!! Migrations in Rails crate tables with the innodb engine type which supports locking!!! I created the sync table manually and it took me half day to understand why the hell it wasn't sync-ing.