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.
Project
basis-processing
Basis provides a set of classes for easily plotting and transforming arbitrary 2D coordinate systems by specifying their basis vectors in Ruby-Processing.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
Development
Dependencies
Runtime
>= 0.0.6
Project Readme