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...