Basis provides a set of classes for easily plotting and transforming arbitrary 2D coordinate systems by specifying their basis vectors in Ruby-Processing.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
lib
.gitignore
README
basis.gemspec
redeploy.sh
test.txt

README

Basis provides a set of classes for easily plotting and transforming arbitrary 2D coordinate systems by specifying their basis vectors in Ruby-Processing.

Please send bug reports to (avishek.sen.gupta at gmail.com)

**NOTE: Starting from version 0.5.1, Basis has been ported to Ruby 1.9.2, because of the kd-tree library dependency. Currently, there are no plans of maintaining Basis compatibility with Ruby 1.8.x.**

**Basis** provides a set of classes for easily plotting and transforming arbitrary 2D coordinate systems by specifying their basis vectors. Originally developed to work with Ruby-Processing, it now supports any class which supports Processing-like semantics.

**UPDATE:** Starting from version 0.6.0, Basis allows you to specify axis labels. Additionally, you can specify arrays of points instead of plotting points one at a time. When you do this, you can also specify a corresponding legend string, which will show up in a legend guide. See below for more details.

**UPDATE:** Starting from version 0.5.9, you can turn grid lines on or off. Additionally, the matrix operations implementation has been ported to use the Matrix class in Ruby's stdlib.

**UPDATE:** Starting from version 0.5.8, you can customise axis labels, draw arbitrary shapes/text/plot custom graphics at any point in your coordinate system. See below for more details.

**UPDATE:** With version 0.5.7, experimental support has been added for drawing objects which aren’t points. Interactions with such objects is currently not supported. Additional support for drawing markers/highlighting in custom bases is now in.

**UPDATE:** Starting from version 0.5.1, Basis has been ported to Ruby 1.9.2, because of the kd-tree library dependency. Currently, there are no plans of maintaining Basis compatibility with Ruby 1.8.x. As an aside, I personally recommend using RVM to manage the mess of Ruby/JRuby installations that you’re likely to have on your machine.

UPDATE: Basis has hit version 0.5.0 with experimental support for mouseover interactivity. More work is incoming, but the demo code below is up-to-date, for now. Starting from version 0.5.0, experimental support has been added for mouseover interactivity without (too much) extra effort on your part. This is still a work in progress, though.

## Installation

To install the Basis gem, first head to the location where the jruby-complete.jar is located, for Ruby-Processing. There, do this:

    java -jar jruby-complete.jar -S gem install basis-processing --user-install

Alternatively, if you're using a conventional JRuby installation, do this:

    sudo jruby -S gem install basis-processing

To use Basis functionality in your code, it is enough to:

    require 'basis_processing'

## Example Code

Here's some example code, which plots random points.

	require 'rubygems'

	Gem.clear_paths
	ENV['GEM_HOME'] = '/home/avishek/jruby/jruby-1.6.4/lib/ruby/gems/1.8'
	ENV['GEM_PATH'] = '/home/avishek/jruby/jruby-1.6.4/lib/ruby/gems/1.8'

	require 'basis_processing'

	class Demo < Processing::App
		app = self
		def setup
			render_mode(P2D)
			no_loop
			background(0,0,0)
			color_mode(RGB, 1.0)
			stroke(1,1,0,1)
			@highlight_block = lambda do |original, mapped, s|
						s.in_basis do
							rect_mode(CENTER)
							stroke(1,0,0)
							fill(1,0,0)
							rect(original[:x], original[:y], 3, 3)
						end
					   end

			points = []
			200.times {|n|points << {:x => n, :y => random(300)}}

	#		Long-winded way of setting up a coordinate system explicitly

	#		@x_unit_vector = {:x => 1.0, :y => 0.0}
	#		@y_unit_vector = {:x => 0.0, :y => 1.0}
	#		x_range = ContinuousRange.new({:minimum => 0, :maximum => 200})
	#		y_range = ContinuousRange.new({:minimum => 0, :maximum => 300})
	#		@basis = CoordinateSystem.new(Axis.new(@x_unit_vector,x_range), Axis.new(@y_unit_vector,y_range), self, [[1,0],[0,1]])

	#		Accomplish the above in a single line below...
			@basis = CoordinateSystem.standard({:minimum => 0, :maximum => 200}, {:minimum => 0, :maximum => 300}, self)

			screen_transform = Transform.new({:x => 2, :y => -2}, {:x => 300, :y => 900})
			@screen = Screen.new(screen_transform, self, @basis)
			@screen.draw_axes(10,10)
			stroke(1,1,0,1)
			fill(1,1,0)
			rect_mode(CENTER)
			points.each do |p|
				@screen.plot(p, :track => true) do |original, mapped, s|
					s.in_basis do
						rect(original[:x], original[:y], 3, 3)
					end
				end
			end
		end
	
		def draw
		end
	end

	w = 1200
	h = 1000

	Demo.send :include, Interactive
	Demo.new(:title => "My Sketch", :width => w, :height => h)

You may choose to plot each point individually, as in the example above. However, you could have achieved the same thing by writing the following directly.

		@screen.plot(points, :legend => 'Some Distribution', :track => true) do |original, mapped, s|
			s.in_basis do
				rect(original[:x], original[:y], 3, 3)
			end
		end

If you choose this route, you can specify a legend, as shown above. This will show up in a legend guide at a default position. You can customise the positioning of the legend box, while creating the Screen object, like so:

		@screen = Screen.new(screen_transform, self, @basis, LegendBox.new(self, {:x => 1300, :y => 30}))

You have a few options when plotting a point. If you specify ':bar => true', like the line below:

	screen.plot(p, basis, :bar => true) {|p| rect(p[:x], p[:y], 5, 5)}

it will connect the point with the x-axis. If you omit `:bar => true` or don't specify any options, Basis will plot the points without connecting them to the X-axis.

If you omit the block at the end of the call, Basis will plot the point using a circle. Use the block to customise how you wish to represent the point graphically.

You can toggle the joining of the points with lines by setting the join attribute of Screen to on (joins points)/off (no joining). The default is false.

## Plotting markers inside/outside your custom basis

You now have more options for drawing markers for your data points on screen, either in the default basis (sans any transformation), or in the basis you've specified. If you're using your specified basis, all shapes you draw (rectangles, ellipses, etc.) will be draw as they'd appear in the custom basis.

For example, in the code below, we are specifying that all our drawing (in this case, a filled rectangle) will be drawn using the basis specified while defining the Screen object.

			@highlight_block = lambda do |original, mapped, s|
						s.in_basis do
							rect_mode(CENTER)
							stroke(1,0,0)
							fill(1,0,0)
							rect(original[:x], original[:y], 3, 3)
						end
					   end

Of course, in this case, the basis vectors are the standard ones, so you'll not really notice anything. But let's consider the follwing example.

	require 'rubygems'

	Gem.clear_paths
	ENV['GEM_HOME'] = '/home/avishek/jruby/jruby-1.6.4/lib/ruby/gems/1.8'
	ENV['GEM_PATH'] = '/home/avishek/jruby/jruby-1.6.4/lib/ruby/gems/1.8'

	require 'basis_processing'

	class Demo < Processing::App
		app = self
		def setup
			render_mode(P2D)
			no_loop
			background(0,0,0)
			color_mode(RGB, 1.0)
			stroke(1,1,0,1)
			@highlight_block = lambda do |original, mapped, s|
						s.in_basis do
							rect_mode(CENTER)
							stroke(1,0,0)
							fill(1,0,0)
							rect(original[:x], original[:y], 3, 3)
						end
					   end

			points = []
			200.times {|n|points << {:x => n, :y => random(300)}}

			@x_unit_vector = {:x => 1.0, :y => 1.0}
			@y_unit_vector = {:x => -1.0, :y => 1.0}
			x_range = ContinuousRange.new({:minimum => 0, :maximum => 200})
			y_range = ContinuousRange.new({:minimum => 0, :maximum => 300})
			@basis = CoordinateSystem.new(Axis.new(@x_unit_vector,x_range), Axis.new(@y_unit_vector,y_range), self, [[1,0],[0,1]])

			screen_transform = Transform.new({:x => 2, :y => -2}, {:x => 300, :y => 900})
			@screen = Screen.new(screen_transform, self, @basis)
			@screen.draw_axes(10,10)
			stroke(1,1,0,1)
			fill(1,1,0)
			rect_mode(CENTER)
			points.each do |p|
				@screen.plot(p, :track => true) do |original, mapped, s|
					s.in_basis do
						rect(original[:x], original[:y], 3, 3)
					end
				end
			end
		end
	
		def draw
		end
	end

	w = 1200
	h = 1000

	Demo.send :include, Interactive
	Demo.new(:title => "My Sketch", :width => w, :height => h)

In the above code, the basis vectors are (1,1) and (-1,1). The @highlight_block is the same as in the previous example. But if you run the above example, the highlighted rectangle will also appear rotated. Contrast this with an alternate definition of @highlight_block, like so:

		@highlight_block = lambda do |original, mapped, s|
					s.outside_basis do
						rect_mode(CENTER)
						stroke(1,0,0)
						fill(1,0,0)
						rect(mapped[:x], mapped[:y], 3, 3)
					end
				   end

This will draw the highlighted rectangle unrotated. You can make use of the in_basis() and the outside_basis() methods to use one or the other. The default is to draw it in the default, untransformed, basis. For your convenience, the first two parameters passed to your block will be the original, and the mapped points, respectively.

## Plotting objects which are not points

Some graphical representations of data might require more than simply plotting a single point. For example, a box plot is a concise way of summarising the distribution of a dataset; it is drawn not as a single point, but as a combination of boxes and 'whiskers'. There is some support now in Basis to make this easier for you to do.
Consider the following code:

	require 'rubygems'
	Gem.clear_paths
	ENV['GEM_HOME'] = '/home/avishek/jruby/jruby-1.6.4/lib/ruby/gems/1.8'
	ENV['GEM_PATH'] = '/home/avishek/jruby/jruby-1.6.4/lib/ruby/gems/1.8'

	require 'basis_processing'

	class BoxPlotSketch < Processing::App
		def setup
			@screen_height = 900
			@width = width
			@height = height
			no_loop
			smooth
			background(0,0,0)
			color_mode(HSB, 1.0)
			box = {:minimum => 20, :maximum => 70, :q1 => 30, :q2 => 40, :q3 => 50}
			@x_unit_vector = {:x => 1.0, :y => 0.15, :legend => 'x-axis'}
			@y_unit_vector = {:x => 0.2, :y => 1.0, :legend => 'y-axis'}
			@screen_transform = Transform.new({:x => 5.0, :y => -5.0}, {:x => @width/2, :y => @screen_height})
			x_range = ContinuousRange.new({:minimum => 0.0, :maximum => 80.0})
			y_range = ContinuousRange.new({:minimum => 0.0, :maximum => box[:maximum]})
			@c = CoordinateSystem.new(Axis.new(@x_unit_vector,x_range), Axis.new(@y_unit_vector,y_range), self, [[1,0],[0,1]])
			@screen = Screen.new(@screen_transform, self, @c)
			stroke(0.3,1,1)
			no_fill
			position = 20
			box_width = 20
			whisker_width = 10
			@screen.plot(box) do |o,s|
				s.in_basis do
					rect(position - box_width/2, o[:q1], box_width, o[:q2] - o[:q1])
					rect(position - box_width/2, o[:q2], box_width, o[:q3] - o[:q2])
					line(position, o[:q3], position, o[:maximum])
					line(position, o[:q1], position, o[:minimum])
					line(position - whisker_width/2, o[:minimum], position + whisker_width/2, o[:minimum])
					line(position - whisker_width/2, o[:maximum], position + whisker_width/2, o[:maximum])
				end
			end
		
			@screen.draw_axes(10, 4)
		end
	end

	h = 1000
	w = 1400
	BoxPlotSketch.new(:title => "Box Plot", :width => w, :height => h)

The first interesting thing in the code above is that the box hash does not have any values for the keys :x, :y. In such a situation, where Basis cannot identify a valid point, it defers the entirety of the plotting to the block that is passed to plot(). If no block is specified, it does nothing else. At this point, it is not possible to interact with objects which are not points, like in the above example.

The other interesting thing about the above code will become evident if we specify a different set of basis vectors like so:

		@x_unit_vector = {:x => 1.0, :y => 0.15, :label => 'x-axis'}
		@y_unit_vector = {:x => 0.2, :y => 1.0, :label => 'y-axis'}

Specifying the :label key is optional, Basis will generate its own legends if you don't specify anything.

The box plot in this modified basis acquires the properties of skewness, rotation, etc. of the host coordinate system. This lets you get on with drawing the object without really worrying about the specific properties of the coordinate space you are plotting in.

The other variation you can use is the at() method of a Screen object. at() behaves like plot(), except that it never plots anything by itself, instead deferring full control to the block that you pass to it. Think of it as a simple way of accessing any point in your custom coordinate system, without having to perform any of the messy transformations yourself. So, for example, the following code fragment, puts some text at whichever point on screen corresponds to (some_x, some_y) in your custom basis.

	@screen.at({:x => some_x, :y => some_y}) {|o,m,s| text(o[:y], m[:x] + 5, m[:y] + 14)}

## Axis drawing options

Starting from version 0.6.0, you can specify the axis labels. There are two ways to do this. If you're explicitly specifying your own basis vectors, you can specify the labels there, like so:

		@x_unit_vector = {:x => 1.0, :y => 0.15, :label => 'x-axis'}
		@y_unit_vector = {:x => 0.2, :y => 1.0, :label => 'y-axis'}

If you're using the convenience method to initialise the standard CoordinateSystem, you can specify the labels like so:

		@c = CoordinateSystem.standard(x_range, y_range, self, {:x => 'Score', :y => 'Probability P(score)'})

In the code above, :x maps to 'Score' for the x-axis, and :y maps to 'Probability P(score)' for the y-axis.

Starting from version 0.5.8, you can customise labels, if you pass two blocks to the draw_axes() method, one for the x-axis, the other for the y-axis. For example, in the fragment below, we're not outputting anything for the x-axis labels (returning an empty string), and converting the y-axis value to an integer.

	@screen.draw_axes(10, 10, :x => ->(p){''}, :y => ->(p){p.to_i})

Grid lines are drawn by default. To turn them off, set the :gridlines key to false in the call to draw_axes(), like so:

	@screen.draw_axes(10, 10, :x => ->(p){''}, :y => ->(p){p.to_i}, :gridlines => false)

## The default CoordinateSystem

If you're doing a simple plot, with standard basis vectors with no transformations, you can use the convenience method in CoordinateSystem to set up a default system.

		@basis = CoordinateSystem.standard({:minimum => 0, :maximum => 200}, {:minimum => 0, :maximum => 300}, self)
If you want more control over your transformations and/or are using custom (possibly non-perpendicular) basis vectors, you'll want to specify all of that explicitly. In the example below, we are setting up a CoordinateSystem with basis vectors (1,1) and (-1,1), with a nonuniform scaling transformation ((2,0),(0,3)).

		@x_unit_vector = {:x => 1.0, :y => 1.0}
		@y_unit_vector = {:x => -1.0, :y => 1.0}
		x_range = ContinuousRange.new({:minimum => 0, :maximum => 200})
		y_range = ContinuousRange.new({:minimum => 0, :maximum => 300})
		@basis = CoordinateSystem.new(Axis.new(@x_unit_vector,x_range), Axis.new(@y_unit_vector,y_range), [[2,0],[0,3]], self)


## Running the Code (with extra notes for versions >= 0.5.1)

Running the code is as simple as typing:

`rp5 run demo.rb`

If you're not using the Gems-in-a-Jar approach, you might have to use:

`rp5 run --jruby demo.rb`

The reason for the `--jruby` switch is to force Ruby-Processing to use the installed version of JRuby, instead of it's own jruby-complete.jar

Additionally, starting with version 0.5.1, you will have to specify the environment variable JRUBY_OPTS to force JRuby to use the 1.9 version of the interpreter, like so:

`export JRUBY_OPTS=--1.9`

## Renderer Compatibility

Basis has been tested with the Java2D and the Processing2D renderer. The OpenGL render mode needs some more work, because the cache behaves strangely in this mode.

## Notes on Interactivity

Experimental support exists for mouseover interactivity without (too much) extra effort on your part. To allow interactions to happen, you must specify ':track => true' while plotting a point, as in the line below:

			@screen.plot(p, :track => true) do |original, mapped, s|
				s.in_basis do
					rect(original[:x], original[:y], 3, 3)
				end
			end

To actually enable interactivity, you must do a few things:

* 'include' the Interactive module in your sketch class, as in the demonstration code above, namely the line `Demo.send :include, Interactive`
* `@highlight_block` specifies what to plot when the data point is hovered upon.

Interactivity is a work in progress at the moment; some of the above steps may change or be removed.