Here's my solution, using _why's wonderful little shoes toolkit. Run like

shoes graph.rb "x*x; -5; 5; 0; 25"

The args are "fn; xmin; xmax; ymin; ymax"

I tried to make the code as straightforward as possible, to recapture
the feel of the old Basic days. A few inevitable complications, due to
shoes being a work in progress: the Grapher class is to work around
scoping quirks, and the range check is due to a bug that doesn't let
the exception handler catch an overflowing Bignum -> long conversion
in the canvas code (it would've been nice to simply draw to a point
off-canvas and let the drawing engine cope). Also shoes's argv
handling code puts the filename in ARGV[0] and doesn't like negative
numbers as plain command line args (they are treated as shoes args),
so a single quoted string was the simplest thing that worked.

#------------------------------------------------------------------------------------------------------
# ARGV[0] is the filename, since we launch via shoes graph.rb args
ARGV.shift
args = ARGV[0].split(";").map {|i| i.chomp}
F = args.shift
Xmin, Xmax, Ymin, Ymax = args.map {|i| i.to_f}

X = Y = 800

XScale = X * 1.0/(Xmax - Xmin)
YScale = Y * 1.0/(Ymax - Ymin)

class Grapher
	def at(x,y)
		[((x -Xmin)* XScale).to_i, Y - ((y - Ymin) * YScale).to_i] rescue nil
	end

	def bounded?(x,y)
		x && y && 0 <= x && x <= X && 0 <= y && y <= Y
	end

	def fn(x)
		y = eval(F)
	end
end

Shoes.app :height => Y, :width => X do
	g = Grapher.new
	background rgb(255, 255, 255)

	fill white
	stroke black
	strokewidth 1
	u, v = nil
	Xmin.step(Xmax, (Xmax - Xmin)/(X*1.0)) {|i|
		begin
			x0, y0 = g.at(u,v)
			u, v = i, g.fn(i)
			x, y = g.at(u,v)
			if g.bounded?(x,y) and g.bounded?(x0,y0)
				line(x0, y0, x, y)
			end
		rescue
		end
	}
end