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

4 comments:

topfunky said...

I like it! I haven't updated Gruff recently but hope to add some other patches and this feature as well.

The source is here:

http://github.com/topfunky/gruff/tree/master

Supriya Surve said...

Thanks for the solution.
It really helped me.
But the only thing I wanted was the labels should go at bottom of the graph as per my requirement.
So I experimented it little more

I changed the values of y_offset from
y_offset = @graph_bottom + LABEL_MARGIN to
y_offset = ((@graph_height / 2) + (@graph_top * 3) - 5 )
which worked for me a bit but not fully functional.

But I don't know if it is a right way to do it as i am newbie to ROR.
If there is any better option it will really help me a lot.

Unknown said...

I just wanted to rotate the x-axis markers -90 degrees to prevent crowding and occlusion with larger datasets, so I upgraded to Gruff 0.3.7, incorporated local copies of base.rb and its dependency deprecated.rb and made the 2 line changes to base.rb/draw_label (@d.gravity = CenterGravity and @d.rotation = -0.90; also tried @d.rotation = 270.0). On calling g.write, I got the inexplicable: "java.lang.IllegalArgumentException: negative width". I tried various ways to get around this error but none of them worked. What am I missing?

Dave

Unknown said...
This comment has been removed by the author.