NAME
Rose::DBx::Object::Renderer - Web UI Rendering for Rose::DB::Object
SYNOPSIS
use Rose::DBx::Object::Renderer;
use CGI;
my $query = new CGI;
print $query->header();
# Load all tables in the local MySQL database named 'company'
my $renderer = Rose::DBx::Object::Renderer->new(
config => {
db => {name => 'company', username => 'root', password => 'password'}
},
load => 1
);
# Render a form to add new employee
Company::Employee->render_as_form();
# Render an object as a form
my $e = Company::Employee->new(id => 1);
$e->load;
$e->render_as_form();
# Render a link to a google map for the 'address' column
print $e->address_for_view();
# Render a table
Company::Employee::Manager->render_as_table();
# Render a table for all the employees who love 'Coding' with create, copy, edit, and delete access
Company::Employee::Manager->render_as_table(
get => {query => [hobby => 'Coding']}
order => ['first_name', 'email', 'address', 'phone'],
create => 1,
copy => 1,
edit => 1,
delete => 1,
searchable => ['first_name', 'address']
);
# Render a menu
my $menu = Company::Employee::Manager->render_as_menu(
order => ['Company::Employee', 'Company::Position'],
edit => 1,
delete => 1,
);
# Render a pie chart via Google Chart API
Company::Employee::Manager->render_as_chart(
type => 'pie',
values => ['Coding', 'Cooking'],
column => 'hobby',
);
# Render a bar chart
Company::Employee::Manager->render_as_chart(
type => 'bar',
title => 'The Employee Bar Chart',
description => 'A useful bar chart.',
columns => ['salary', 'tax'],
objects => [1, 2, 3],
options => {chco => 'ff6600,ffcc00'} # the color for each bar
);
DESCRIPTION
Rose::DBx::Object::Renderer generates web UIs for Rose::DB::Object. It encapsulates many web conventions in the generated UIs as default behaviours. For example, email addresses are by default rendered as mailto
links in tables and appropiate validation is enforced automatically in forms. These behaviours are highly extensible.
Renderer uses CGI::FormBuilder to generate forms and the Google Chart API to render charts. Template::Toolkit is used for template processing, however, Renderer can dynamically generate a full set of UIs without any templates.
RESTRICTIONS
Must follow the default conventions in Rose::DB::Object.
Limited support for database tables with multiple primary keys.
METHODS
new
To instantiate a new Renderer object:
my $renderer = Rose::DBx::Object::Renderer->new(config => {db => {name => 'company', username => 'root', password => 'root'}}, load => 1);
Since Renderer inherits from Rose::Object, the above line is equivalent to:
my $renderer = Rose::DBx::Object::Renderer->new();
$renderer->config({db => {name => 'company', username => 'root', password => 'root'}});
$renderer->load();
config
A Renderer instance inherits the default configurations in Renderer, which is accessible by:
my $config = $renderer->config();
config
accepts a hashref for configuring the Renderer object.
db
The db
option is for configuring database related settings, for instance:
$renderer->config({
db => {
name => 'product',
type => 'Pg', # defaulted to 'mysql'
host => '10.0.0.1',
port => '5432',
username => 'admin',
password => 'password',
tables_are_singular => 1, # defines table name conventions, defaulted to undef
like_operator => 'ilike', # to perform case-insensitive LIKE pattern matching in PostgreSQL, defaulted to 'like'
table_prefix => 'app_', # specificies the prefix used in your table names if any, defaulted to undef
}
});
template
The template
option specifies the template toolkit INCLUDE_PATH
and the base URL for static contents, such as javascript libraries or images:
$renderer->config({
...
template => {
path => '../templates:../alternative', # TT INCLUDE_PATH, defaulted to 'templates'
url => '../images', # defaulted to 'templates'
},
});
upload
Renderer needs a directory with write access to upload files. The upload
option defines file upload related settings:
$renderer->config({
...
upload => {
path => '../files', # the upload directory path, defaulted to 'uploads'
url => '../files', # the corresponding URL path, defaulted to 'uploads'
keep_old_files => 1, # defaulted to undef
},
});
form
The form
option defines the default behaviours of render_as_form
:
$renderer->config({
...
form => {
download_message => 'Get Current File', # the name of the link for uploaded files
cancel => 'Back', # the name of the built-in 'Cancel' controller
delimiter => ' ' # the delimiter for handling column with muliple values, defaulted to ','
},
});
table
The table
option defines the default behaviours of render_as_table
:
$renderer->config({
...
table => {
search_result_title => 'Looking for "[% q %]"?',
empty_message => 'No matching records.',
per_page => 25, # number of records per table, defaulted to 15
pages => 5, # the amount of page numbers in the table pagination, defaulted to 9
no_pagination => 1, # do not display pagination, defaulted to undef
or_filter => 1, # column filtering is joined by 'OR', defaulted to undef
delimiter => '/', # the delimiter for joining foreign objects in relationship columns, defaulted to ', '
keyword_delimiter => '\s+', # the delimiter for search keywords, defaulted to ','
},
});
columns
Renderer has a built-in list of column definitions that encapsulate web-oriented conventions and behaviours. A column definition is a collection of column options. Column definitions are used by the rendering methods to generate web UIs. The built-in column definitions are stored inside columns
:
my $config = $renderer->config();
print join (', ', keys %{$config->{columns}});
For example, the column definition for 'email' would be:
...
'email' => {
required => 1,
validate => 'EMAIL',
sortopts => 'LABELNAME',
comment => 'e.g. your.name@work.com',
format => {
for_view => sub {
my ($self, $column) = @_;
my $value = $self->$column;
return unless $value;
return qq(<a href="mailto:$value">$value</a>);}
}
},
...
We can also define new column definitions:
$renderer->config({
...
columns => {
hobby => {
label => 'Your Favourite Hobby',
sortopts => 'LABELNAME',
required => 1,
options => ['Reading', 'Coding', 'Shopping']
}
},
});
The options in each column definition are CGI::FormBuilder
field definitions, with the following additions:
format
-
The
format
option is a hash of coderefs which get injected as object methods byload
. For instance, based on the 'email' column definition, we can print a 'mailto' link for the email address by calling:print $object->email_for_view;
Similarly, based on other column definitions, we can:
# Print the date in 'DD/MM/YYYY' format print $object->date_for_view; # Store a password in MD5 hash $object->password_for_update('p@$$W0rD'); $object->save(); # Display an image formatted in HTML <img> tags print $object->image_for_view; # Print the url of the image print $object->image_url; # Prints the file path of the image print $object->image_path;
We can overwrite the existing formatting methods or define new ones. For instance, we can use the HTML::Strip module to strip out HTML tags for the 'description' column type:
use HTML::Strip; ... $renderer->config({ ... columns => { description => { format => { for_update => sub { my ($self, $column, $value) = @_; return unless $value; my $hs = HTML::Strip->new(emit_spaces => 0); my $clean_text = $hs->parse($value); return $self->$column($clean_text); } } } }, }); $renderer->load(); ... $object->description_for_update('<html>The Lightweight UI Generator.</html>'); $p->save(); print $p->description; # prints 'The Lightweight UI Generator.'
Formatting methods are utilised by rendering methods. They take preference over the default CRUD methods. The
for_create
,for_edit
, andfor_update
methods are used byrender_as_form
. When creating new objects,render_as_form
triggers thefor_create
methods to format the default value of each column. When rendering an existing object as a form, however, thefor_edit
methods are triggered to format column values. During form submissions, thefor_update
methods are triggered to format the submitted form field values. Thefor_view
,for_search
, andfor_filter
methods are used byrender_as_table
. Thefor_view
methods are triggered to format column values for data presentation, thefor_filter
methods are triggered during data filtering, and thefor_search
methods are triggered during keyword searches. unsortable
-
This option defines whether a column is sortable. For instance, the 'password' column definition has the
unsortable
option set to 1. This option is used byrender_as_table
. Custom columns are always unsortable. stringify
-
This option specifies whether a column will be stringified by the
stringify_me
object method.
misc
Other miscellaneous options are defined in misc
:
my $custom_config = $renderer->config();
# Print the built-in doctype and CSS
print $custom_config->{misc}->{html_head};
# Change the object stringify delimiter
$custom_config->{misc}->{stringify_delimiter} = ', '; # defaulted to space
# Change time zone
$custom_config->{misc}->{time_zone} = 'Asia/Hong_Kong'; # defaulted to Australia/Sydney
$renderer->config($custom_config);
$renderer->load();
load
load
uses Rose::DB::Object::Loader to load Rose::DB::Object classes dynamically. In order to take advantage of the built-in column definitions, load
employs the following logic to auto-assign column definitions to database columns:
Column name exists in the Renderer object's config?
Yes: Use that column definition.
No: Is the column a foreign key?
Yes: Apply the column options designed for foreign keys.
No: Column name matches (regex) a column definition name?
Yes: Use the first matching column definition.
No: Column's metadata object type exists as column definition name?
Yes: Use that column definition.
No: Create a custom column definition by aggregating database column information.
load
accepts a hashref to pass parameters to the new
and make_classes
methods in Rose::DB::Object::Loader.
loader
-
The
loader
option is a hashref that gets passed to thenew
method in Rose::DB::Object::Loader.$renderer->load({ loader => { class_prefix => 'MyCompany', } });
make_classes
-
Similarly, the
make_classes
option is passed to themake_classes
method.$renderer->load({ make_classes => { include_tables => ['customer', 'product'], } });
load
returns an array of the loaded classes via the make_classes
method in Rose::DB::Object::Loader. However, if the Rose::DB::Object base_class
for the particular database already exists, which most likely happens in a persistent environment, load
will simply skip the loading process and return undef.
load
generates CGI::FormBuilder validation subrefs to validate unique keys in forms. However, since column definitions are identified by column names, custom validation subrefs are required when there are multiple unique keys with the same table column name across different tables loaded via Renderer.
RENDERING METHODS
Rendering methods are exported for Rose::DB::Object subclasses to generate web UIs. Rose::DB::Object subclasses generated by calling load
will import the rendering methods automatically. However, we can also import the rendering methods into custom Rose::DB::Object subclasses:
# For object classes
package Company::Employee
use Rose::DBx::Object::Renderer qw(:object);
...
# For manager classes
package Company::Employee::Manager
use Rose::DBx::Object::Renderer qw(:manager);
...
Obviously, the rendering methods in the custom subclasses do not take advantages of the built-in column definitions and formatting methods. However, you can still replicate those behaviours by using the fields
option in render_as_form
, or the columns
option in render_as_table
, as well as hand crafting the formatting methods.
The following is a list of common parameters for the rendering methods:
template
-
The template file name. When it is set to 1, rendering methods will try to find the default template based on the rendering method name. For example:
Company::Employee->render_as_form(template => 1); # tries to use the template 'form.tt' Company::Employee::Manager->render_as_table(template => 1); # tries to use the template 'table.tt'
template_path
-
The "Template Toolkit"'s
INCLUDE_PATH
. template_url
-
An URL path variable that is passed to templates.
template_options
-
Optional parameters to be passed to "Template Toolkit". This is not applicable to
render_as_form
. template_data
-
A hashref to overwrite the variables passed to the template.
html_head
-
This is specifying custom DOCTYPE, CSS, or Javascript for the particular rendering method.
prefix
-
Define a prefix for the UI, e.g.:
Company::Employee::Manager->render_as_table(prefix => 'employee_admin');
A prefix should be URL friendly. Adding a
prefix
can prevent CGI param conflicts when rendering multiple UIs of the same class on the same web page. title
-
Define a title for the UI, e.g.:
Company::Employee::Manager->render_as_table(title => 'Employee Directory');
description
-
Define a short description for the UI, e.g.:
Company::Employee::Manager->render_as_table(description => 'Here you can view, search, and manage your employee details.');
no_head
-
When set to 1, rendering methods will not include the default DOCTYPE and CSS. This is useful when rendering multiple UIs in the same page.
output
-
When set to 1, the rendering methods will return the rendered UI instead of printing it directly. For example:
my $form = Company::Employee->render_as_form(output => 1); print $form->{output};
extra
-
A hashref of additional template variables. For example:
Company::Employee->render_as_form(extra => {hobby => 'basketball'}); # to access it within a template: [% extra.hobby %]
render_as_form
render_as_form
renders forms and handles their submission.
# Render a form for creating a new object
Company::Employee->render_as_form();
# Render a form for updating an existing object
my $e = Company::Employee->new(id => 1);
$e->load;
$e->render_as_form();
order
-
order
is an arrayref for the order of the form fields. fields
-
A hashref to specify the CGI::FormBuilder field definitions for this particular
render_as_form
call. Any custom fields must be included in theorder
arrayref in order to be shown.Company::Employee->render_as_form( order => ['username', 'favourite_cuisine'], fields => { favourite_cuisine => {required => 1, options => ['Chinese', 'French', 'Japanese']} } );
copy
-
Instead of updating the calling object, we can clone the object by setting
copy
to 1.... $e->render_as_form(copy => 1);
queries
-
An arrayref of query parameters to be converted as hidden fields.
Company::Employee->render_as_form( queries => { 'rm' => 'edit', 'favourite_cuisine' => ['French', 'Japanese'] });
Please note that when a prefix is used, all fields are renamed to '
prefix_fieldname
'. controllers
andcontroller_order
-
Controllers are essentially callbacks. We can add multiple custom controllers to a form. They are rendered as submit buttons.
controller_order
defines the order of the controllers, in other words, the order of the submit buttons.my $form = Company::Employee->render_as_form( output => 1, controller_order => ['Hello', 'Good Bye'], controllers => { 'Hello' => { create => sub { return if DateTime->now->day_name eq 'Sunday'; return 1; }, callback => sub { my $self = shift; if (ref $self) { return 'Welcome ' . $self->first_name; } else { return 'Employees cannot be added on Sundays'; } } }, 'Good Bye' => sub {return 'Have fun!'} } ); if (exists $form->{controller}) { print $form->{controller}; } else { print $form->{output}; }
Within the
controllers
hashref, we can set thecreate
parameter to 1 so that the object is always inserted into the database before running the custom callback. We can also pointcreate
to a coderef, in which case, the object is inserted into the database only if the coderef returns true.When rendering an object instance as a form, we can use the same mechanism to 'copy' or 'update' the object before running the custom callback, for instance:
... $e->render_as_form( controllers => { 'Hello' => { update => 1, callback => sub{...} } } );
Another parameter within the
controllers
hashref ishide_form
, which informsrender_as_form
not to render the form after executing the controller. form
-
A hashref that gets passed to the CGI::FormBuilder constructor.
validate
-
Parameters for the CGI::FormBuilder's
validate
method. jserror
-
When a template is used,
render_as_form
sets CGI::FormBuilder'sjserror
function name to 'notify_error
' so that we can always customise the error alert mechanism within the template (see the included 'form.tt' template). javascript_code
-
A string with javascript code to be added to the template
render_as_form
passes the following list of variables to a template:
[% self %] - the calling object instance or class
[% form %] - CGI::FormBuilder's form object
[% field_order %] - the order of the form fields
[% form_id %] - the form id
[% form_submit %] - the form submit buttons with a custom 'Cancel' button
[% title %] - the form title
[% description %] - the form description
[% doctype %] - the default html doctype
[% html_head %] - the default html doctype and css
[% no_head %] - the 'no_head' option
[% cancel %] - the name of the 'Cancel' controller
[% javascript_code %] - javascript code
[% template_url %] - the default template URL
[% extra %] - extra template variables
render_as_table
render_as_table
renders tables for CRUD operations.
columns
-
The
columns
parameter can be used to set the label and value of a column, as well as whether the column is sortable. It can also be used to create custom columns, which do not exist in the underlying database.Company::Employee::Manager->render_as_table( order => ['first_name', 'custom_column'], columns => { 'first_name' => { unsortable => 1 }, 'custom_column' => { label => 'Favourite Drink', value => { 1 => 'Tea', # 1 is the primary key of the object 2 => 'Coffee' }, } } );
We can also nominate a custom
accessor
, such that the table column values are populated via the nominated accessor, as opposed to the default column one. For example:Company::Employee::Manager->render_as_table( order => ['first_name', 'salary'], columns => { 'salary' => { accessor => 'salary_with_bonus' }, } );
In this case, the values of the 'salary' column in the table are populated by calling
salary_with_bonus
, instead ofsalary
. order
-
order
accepts an arrayref to define the order of the columns to be shown. Theorder
parameter also determines which columns are allowed to be filtered via URL whenfilterable
is not defined. or_filter
-
render_as_table
allows columns to be filtered via URL. For example:http://www.yoursite.com/yourscript.pl?first_name=Danny&last_name=Liang
returns the records where 'first_name' is 'Danny' and 'Last_name' is 'liang'. By default, column queries are joined by "AND", unless
or_filter
is set to 1. filterable
-
This specifies an arrayref of columns that are filterable via URL. This can be used to filter data in columns that are not shown, e.g.:
Company::Employee::Manager->render_as_table( order => ['first_name', 'last_name', 'email'], filterable => ['first_name', 'last_name', 'email', 'state'], );
searchable
-
The
searchable
option allows keyword search in multiple columns, including the columns of foreign objects:Company::Employee::Manager->render_as_table( get => {with_objects => [ 'position' ]}, searchable => ['first_name', 'last_name', 'position.title'], );
This option adds a text field named 'q' in the rendered table for entering keywords.
render_as_table()
grabs the value of theq
parameter if it exists, otherwise it pulls the 'q' value from querystring. Thesearchable
option constructs SQL queries using the 'LIKE' operator (configurable vialike_operator
).Since PostgreSQL does not like mixing table aliases with real table names in queries, and disabled auto type casting in 8.3,
render_as_table()
tries to perform basic table aliasing and type casting for non-character based columns automatically for PostgreSQL. Please note that the corresponding tables in chained relationships defined via 'with_objects' and 'require_objects', such as 'vendor.region', will require manual table aliasing if they are specified in thesearchable
array.By default, comma is the delimiter for seperating multiple keywords. This is configurable via
config()
. get
-
get
accepts a hashref to construct database queries.get
is directly passed to theget
method of the manager class.Company::Employee::Manager->render_as_table( get => { per_page = 5, require_objects => [ 'position' ], query => ['position.title' => 'Manager'], });
get_from_sql
-
get_from_sql
accepts arguments, such as an SQL statement, for theget_objects_from_sql
method from Rose::DB::Object::Manager.Company::Employee::Manager->render_as_table( order => ['id', 'first_name', 'email'], get_from_sql => 'SELECT id, first_name, email FROM employee WHERE id % 2 = 0 ORDER BY id', );
get_from_sql
takes precedence overget
. The default table pagination will be also disabled. controllers
andcontroller_order
-
The
controllers
parameter works very similar torender_as_form
.controller_order
defines the order of the controllers.Company::Employee::Manager->render_as_table( controller_order => ['edit', 'Review', 'approve'], controllers => { 'Review' => sub{my $self = shift; do_something_with($self);} 'approve' => { label => 'Approve', hide_table => 1, queries => {approve => '1'}, callback => sub {my $self = shift; do_something_else_with($self); } } );
Within the
controllers
hashref, thequeries
parameter allows us to define custom query strings for the controller. Thehide_table
parameter informsrender_as_table
not to render the table after executing the controller. create
-
This enables the built-in 'create' controller when set to 1.
Company::Employee::Manager->render_as_table(create => 1);
Since
render_as_form
is used to render the form, we can also pass a hashref to manipulate the generated form.Company::Employee::Manager->render_as_table( create => {title => 'Add New Employee', fields => {...}} );
edit
-
Similar to
create
,edit
enables the built-in 'edit' controller for updating objects. copy
-
copy
enables the built-in 'copy' controller for cloning objects. delete
-
When set to 1,
delete
enables the built-in 'delete' controller for removing objects. queries
-
Similar to the
queries
parameter inrender_as_form
,queries
is an arrayref of query parameters, which will be converted to query strings. Please note that when a prefix is used, all query strings are renamed to 'prefix_querystring
'. url
-
Unless a url is specified in
url
,render_as_table
will resolve the self url using CGI. javascript_code
-
A string with javascript code to be added to the template
ajax
andajax_template
-
These two parameters are designed for rendering Ajax-enabled tables. When
ajax
is set to 1,render_as_table
tries to use the template 'table_ajax.tt' for rendering, unless it is defined viaajax_template
.render_as_table
also passes a variable called 'ajax' to the template and sets it to 1 when a CGI param named 'ajax' (assuming no prefix is in use) is found, indicating the current request is an ajax request.
Within a template, we can loop through objects using the [% table %]
variable. Alternatively, we can use the [% objects %]
variable.
render_as_table
passes the following list of variables to a template:
[% table %] - the hash for the formatted table, see the sample template 'table.tt'
[% objects %] - the raw objects returned by the 'get_object' method
[% column_order %] - the order of the columns
[% template_url %] - the default template URL
[% table_id %] - the table id
[% title %] - the table title
[% description %] - the table description
[% no_pagination %] - the 'no_pagination' option
[% q %] - the keyword query for search
[% query_string %] - a hash of URL encoded query strings
[% query_hidden_fields %] - CGI queries converted into hidden fields; it is used by the keyword search form
[% param_list %] - a list of CGI param names with the table prefix, e.g. the name of the keyword search box is [% param_list.q %]
[% searchable %] - the 'searchable' option
[% sort_by_column %] - the column to be sorted
[% doctype %] - the default html doctype
[% html_head %] - the default html doctype and css
[% no_head %] - the 'no_head' option
[% javascript_code %] - javascript code
[% ajax %] - the ajax variable for checking whether the current CGI request is a ajax request
[% url %] - the base url
[% extra %] - extra template variables
render_as_menu
render_as_menu
generates a menu with the given list of classes and renders a table for the current class. We can have fine-grained control over each table within the menu. For example, we can alter the 'date_of_birth' field inside the 'create' form of the 'Company::Employee' table inside the menu:
Company::Employee::Manager->render_as_menu (
order => ['Company::Employee', 'Company::Position'],
items => {
'Company::Employee' => {
create => {
fields => {date_of_birth => {required => 1}}
}
}
'Company::Position' => {
title => 'Current Positions',
description => 'important positions in the company'
}},
create => 1,
edit => 1,
delete => 1,
);
order
-
The
order
parameter defines the list of classes to be shown in the menu as well as their order. The current item of the menu is always the calling class, i.e.Company::Employee::Manager
in the example. items
-
The
items
parameter is a hashref of parameters to control each table within the menu. create
,copy
,edit
,delete
,template
, andajax
-
These parameters are shortcuts which get passed to all the underlying tables rendered by the menu.
render_as_menu
passes the following list of variables to a template:
[% template_url %] - the default template URL
[% menu_id %] - the menu id
[% title %] - the menu title
[% description %] - the menu description
[% items %] - the hash for the menu items
[% item_order %] - the order of the menu items
[% current %] - the current menu item
[% content %] - the output of the table
[% hide %] - whether the menu should be hidden
[% doctype %] - the default html doctype
[% html_head %] - the default html doctype and css
[% no_head %] - the 'no_head' option
[% extra %] - extra template variables
render_as_chart
render_as_chart
renders pie, line, and vertical bar charts via the Google Chart API.
type
-
This can be 'pie', 'bar', or 'line', which maps to the Google chart type (cht) 'p', 'bvg', and 'ls' respectively.
column
andvalues
-
These two parameters are only applicable to pie charts.
column
defines the column of the table in which the values are compared. Thevalues
parameter is a list of values to be compared in that column, i.e. the slices. columns
andobjects
-
These two parameters are only applicable to bar and line charts.
columns
defines the columns of the object to be compared. Theobjects
parameter is a list of object IDs representing the objects to be compared. options
-
A hashref for specifying Google Chart API options, such as the chart type, size, labels, or data. This hashref is serialised into a query string.
engine
-
Accepts a coderef to plug in your own charting engine.
render_as_chart
passes the following list of variables to a template:
[% template_url %] - the default template URL
[% chart_id %] - the chart id
[% title %] - the chart title
[% description %] - the chart description
[% chart %] - the chart
[% options %] - the 'options' hash
[% doctype %] - the default html doctype
[% html_head %] - the default html doctype and css
[% no_head %] - the 'no_head' option
[% extra %] - extra template variables
OBJECT METHODS
Apart from the formatting methods injected by load
, there are several lesser-used object methods:
delete_with_file
This is a wrapper of the object's delete
method to remove any uploaded files associated:
$object->delete_with_file();
stringify_me
Stringifies the object instance, e.g.:
$object->first_name('John');
$object->last_name('Smith');
print $object->stringify_me;
# prints 'John Smith';
stringify_class
Stringifies the class name:
print Company::Employee->stringify_class;
# prints 'company_employee'
SAMPLE TEMPLATES
There are four sample templates: 'form.tt', 'table.tt', 'menu.tt', and 'chart.tt' in the 'templates' folder of the TAR archive.
SEE ALSO
Rose::DB::Object, CGI::FormBuilder, Template::Toolkit, http://code.google.com/apis/chart/
AUTHOR
Xufeng (Danny) Liang (danny.glue@gmail.com)
COPYRIGHT & LICENSE
Copyright 2008 Xufeng (Danny) Liang, All Rights Reserved.
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.