added simple svn statistics graphs, rendered using SVG::Graph

git-svn-id: http://redmine.rubyforge.org/svn/trunk@380 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang 2007-03-25 17:11:46 +00:00
parent 887f11435b
commit 0ef114e006
25 changed files with 3897 additions and 3 deletions

View File

@ -15,10 +15,15 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'SVG/Graph/Bar'
require 'SVG/Graph/BarHorizontal'
class RepositoriesController < ApplicationController class RepositoriesController < ApplicationController
layout 'base' layout 'base'
before_filter :find_project, :authorize before_filter :find_project
before_filter :authorize, :except => [:stats, :graph]
before_filter :check_project_privacy, :only => [:stats, :graph]
def show def show
# get entries for the browse frame # get entries for the browse frame
@entries = @repository.scm.entries('') @entries = @repository.scm.entries('')
@ -64,6 +69,25 @@ class RepositoriesController < ApplicationController
show_error and return unless @diff show_error and return unless @diff
end end
def stats
end
def graph
data = nil
case params[:graph]
when "commits_per_month"
data = graph_commits_per_month(@repository)
when "commits_per_author"
data = graph_commits_per_author(@repository)
end
if data
headers["Content-Type"] = "image/svg+xml"
send_data(data, :type => "image/svg+xml", :disposition => "inline")
else
render_404
end
end
private private
def find_project def find_project
@project = Project.find(params[:id]) @project = Project.find(params[:id])
@ -80,4 +104,85 @@ private
flash.now[:notice] = l(:notice_scm_error) flash.now[:notice] = l(:notice_scm_error)
render :nothing => true, :layout => true render :nothing => true, :layout => true
end end
def graph_commits_per_month(repository)
@date_to = Date.today
@date_from = @date_to << 12
commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
commits_by_month = [0] * 12
commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
changes_by_day = repository.changes.count(:all, :group => :commit_date)
changes_by_month = [0] * 12
changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
fields = []
month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
graph = SVG::Graph::Bar.new(
:height => 300,
:width => 500,
:fields => fields.reverse,
:stack => :side,
:scale_integers => true,
:step_x_labels => 2,
:show_data_values => false,
:graph_title => l(:label_commits_per_month),
:show_graph_title => true
)
graph.add_data(
:data => commits_by_month[0..11].reverse,
:title => l(:label_revision_plural)
)
graph.add_data(
:data => changes_by_month[0..11].reverse,
:title => l(:label_change_plural)
)
graph.burn
end
def graph_commits_per_author(repository)
commits_by_author = repository.changesets.count(:all, :group => :committer)
commits_by_author.sort! {|x, y| x.last <=> y.last}
fields = commits_by_author.collect {|r| r.first}
data = commits_by_author.collect {|r| r.last}
fields = fields + [""]*(10 - fields.length) if fields.length<10
data = data + [0]*(10 - data.length) if data.length<10
graph = SVG::Graph::BarHorizontal.new(
:height => 300,
:width => 500,
:fields => fields,
:stack => :side,
:scale_integers => true,
:show_data_values => false,
:rotate_y_labels => false,
:graph_title => l(:label_commits_per_author),
:show_graph_title => true
)
graph.add_data(
:data => data,
:title => l(:label_revision_plural)
)
graph.burn
end
end end
class Date
def months_ago(date = Date.today)
(date.year - self.year)*12 + (date.month - self.month)
end
def weeks_ago(date = Date.today)
(date.year - self.year)*52 + (date.cweek - self.cweek)
end
end

View File

@ -19,7 +19,12 @@ class Changeset < ActiveRecord::Base
belongs_to :repository belongs_to :repository
has_many :changes, :dependent => :delete_all has_many :changes, :dependent => :delete_all
validates_presence_of :repository_id, :revision, :committed_on validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_numericality_of :revision, :only_integer => true validates_numericality_of :revision, :only_integer => true
validates_uniqueness_of :revision, :scope => :repository_id validates_uniqueness_of :revision, :scope => :repository_id
def committed_on=(date)
self.commit_date = date
super
end
end end

View File

@ -18,6 +18,7 @@
class Repository < ActiveRecord::Base class Repository < ActiveRecord::Base
belongs_to :project belongs_to :project
has_many :changesets, :dependent => :destroy, :order => 'revision DESC' has_many :changesets, :dependent => :destroy, :order => 'revision DESC'
has_many :changes, :through => :changesets
has_one :latest_changeset, :class_name => 'Changeset', :foreign_key => :repository_id, :order => 'revision DESC' has_one :latest_changeset, :class_name => 'Changeset', :foreign_key => :repository_id, :order => 'revision DESC'
attr_protected :root_url attr_protected :root_url

View File

@ -1,3 +1,7 @@
<div class="contextual">
<%= link_to l(:label_statistics), {:action => 'stats', :id => @project}, :class => 'icon icon-stats' %>
</div>
<h2><%= l(:label_repository) %></h2> <h2><%= l(:label_repository) %></h2>
<h3><%= l(:label_browse) %></h3> <h3><%= l(:label_browse) %></h3>

View File

@ -0,0 +1,11 @@
<h2><%= l(:label_statistics) %></h2>
<table width="100%">
<tr><td>
<%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %>
</td><td>
<%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %>
</td></tr>
</table>
<br />
<p><%= link_to l(:button_back), :action => 'show', :id => @project %></p>

View File

@ -0,0 +1,10 @@
class AddChangesetCommitDate < ActiveRecord::Migration
def self.up
add_column :changesets, :commit_date, :date, :null => false
Changeset.update_all "commit_date = committed_on"
end
def self.down
remove_column :changesets, :commit_date
end
end

View File

@ -344,6 +344,10 @@ label_spent_time: Spent time
label_f_hour: %.2f hour label_f_hour: %.2f hour
label_f_hour_plural: %.2f hours label_f_hour_plural: %.2f hours
label_time_tracking: Time tracking label_time_tracking: Time tracking
label_change_plural: Changes
label_statistics: Statistics
label_commits_per_month: Commits per month
label_commits_per_author: Commits per author
button_login: Einloggen button_login: Einloggen
button_submit: OK button_submit: OK

View File

@ -344,6 +344,10 @@ label_spent_time: Spent time
label_f_hour: %.2f hour label_f_hour: %.2f hour
label_f_hour_plural: %.2f hours label_f_hour_plural: %.2f hours
label_time_tracking: Time tracking label_time_tracking: Time tracking
label_change_plural: Changes
label_statistics: Statistics
label_commits_per_month: Commits per month
label_commits_per_author: Commits per author
button_login: Login button_login: Login
button_submit: Submit button_submit: Submit

View File

@ -344,6 +344,10 @@ label_spent_time: Spent time
label_f_hour: %.2f hour label_f_hour: %.2f hour
label_f_hour_plural: %.2f hours label_f_hour_plural: %.2f hours
label_time_tracking: Time tracking label_time_tracking: Time tracking
label_change_plural: Changes
label_statistics: Statistics
label_commits_per_month: Commits per month
label_commits_per_author: Commits per author
button_login: Conexión button_login: Conexión
button_submit: Someter button_submit: Someter

View File

@ -344,6 +344,10 @@ label_spent_time: Temps passé
label_f_hour: %.2f heure label_f_hour: %.2f heure
label_f_hour_plural: %.2f heures label_f_hour_plural: %.2f heures
label_time_tracking: Suivi du temps label_time_tracking: Suivi du temps
label_change_plural: Changements
label_statistics: Statistiques
label_commits_per_month: Commits par mois
label_commits_per_author: Commits par auteur
button_login: Connexion button_login: Connexion
button_submit: Soumettre button_submit: Soumettre

View File

@ -344,6 +344,10 @@ label_spent_time: Spent time
label_f_hour: %.2f hour label_f_hour: %.2f hour
label_f_hour_plural: %.2f hours label_f_hour_plural: %.2f hours
label_time_tracking: Time tracking label_time_tracking: Time tracking
label_change_plural: Changes
label_statistics: Statistics
label_commits_per_month: Commits per month
label_commits_per_author: Commits per author
button_login: Login button_login: Login
button_submit: Invia button_submit: Invia

View File

@ -345,6 +345,10 @@ label_spent_time: Spent time
label_f_hour: %.2f hour label_f_hour: %.2f hour
label_f_hour_plural: %.2f hours label_f_hour_plural: %.2f hours
label_time_tracking: Time tracking label_time_tracking: Time tracking
label_change_plural: Changes
label_statistics: Statistics
label_commits_per_month: Commits per month
label_commits_per_author: Commits per author
button_login: ログイン button_login: ログイン
button_submit: 変更 button_submit: 変更

340
lib/SVG/GPL.txt Normal file
View File

@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.

137
lib/SVG/Graph/Bar.rb Normal file
View File

@ -0,0 +1,137 @@
require 'rexml/document'
require 'SVG/Graph/Graph'
require 'SVG/Graph/BarBase'
module SVG
module Graph
# === Create presentation quality SVG bar graphs easily
#
# = Synopsis
#
# require 'SVG/Graph/Bar'
#
# fields = %w(Jan Feb Mar);
# data_sales_02 = [12, 45, 21]
#
# graph = SVG::Graph::Bar.new(
# :height => 500,
# :width => 300,
# :fields => fields
# )
#
# graph.add_data(
# :data => data_sales_02,
# :title => 'Sales 2002'
# )
#
# print "Content-type: image/svg+xml\r\n\r\n"
# print graph.burn
#
# = Description
#
# This object aims to allow you to easily create high quality
# SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
# style sheet or supply your own. Either way there are many options which
# can be configured to give you control over how the graph is generated -
# with or without a key, data elements at each point, title, subtitle etc.
#
# = Notes
#
# The default stylesheet handles upto 12 data sets, if you
# use more you must create your own stylesheet and add the
# additional settings for the extra data sets. You will know
# if you go over 12 data sets as they will have no style and
# be in black.
#
# = Examples
#
# * http://germane-software.com/repositories/public/SVG/test/test.rb
#
# = See also
#
# * SVG::Graph::Graph
# * SVG::Graph::BarHorizontal
# * SVG::Graph::Line
# * SVG::Graph::Pie
# * SVG::Graph::Plot
# * SVG::Graph::TimeSeries
class Bar < BarBase
include REXML
# See Graph::initialize and BarBase::set_defaults
def set_defaults
super
self.top_align = self.top_font = 1
end
protected
def get_x_labels
@config[:fields]
end
def get_y_labels
maxvalue = max_value
minvalue = min_value
range = maxvalue - minvalue
top_pad = range == 0 ? 10 : range / 20.0
scale_range = (maxvalue + top_pad) - minvalue
scale_division = scale_divisions || (scale_range / 10.0)
if scale_integers
scale_division = scale_division < 1 ? 1 : scale_division.round
end
rv = []
maxvalue = maxvalue%scale_division == 0 ?
maxvalue : maxvalue + scale_division
minvalue.step( maxvalue, scale_division ) {|v| rv << v}
return rv
end
def x_label_offset( width )
width / 2.0
end
def draw_data
fieldwidth = field_width
maxvalue = max_value
minvalue = min_value
fieldheight = (@graph_height.to_f - font_size*2*top_font) /
(get_y_labels.max - get_y_labels.min)
bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
subbar_width = fieldwidth - bargap
subbar_width /= @data.length if stack == :side
x_mod = (@graph_width-bargap)/2 - (stack==:side ? subbar_width/2 : 0)
# Y1
p2 = @graph_height
# to X2
field_count = 0
@config[:fields].each_index { |i|
dataset_count = 0
for dataset in @data
# X1
p1 = (fieldwidth * field_count)
# to Y2
p3 = @graph_height - ((dataset[:data][i] - minvalue) * fieldheight)
p1 += subbar_width * dataset_count if stack == :side
@graph.add_element( "path", {
"class" => "fill#{dataset_count+1}",
"d" => "M#{p1} #{p2} V#{p3} h#{subbar_width} V#{p2} Z"
})
make_datapoint_text(
p1 + subbar_width/2.0,
p3 - 6,
dataset[:data][i].to_s)
dataset_count += 1
end
field_count += 1
}
end
end
end
end

140
lib/SVG/Graph/BarBase.rb Normal file
View File

@ -0,0 +1,140 @@
require 'rexml/document'
require 'SVG/Graph/Graph'
module SVG
module Graph
# = Synopsis
#
# A superclass for bar-style graphs. Do not attempt to instantiate
# directly; use one of the subclasses instead.
#
# = Author
#
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
#
# Copyright 2004 Sean E. Russell
# This software is available under the Ruby license[LICENSE.txt]
#
class BarBase < SVG::Graph::Graph
# Ensures that :fields are provided in the configuration.
def initialize config
raise "fields was not supplied or is empty" unless config[:fields] &&
config[:fields].kind_of?(Array) &&
config[:fields].length > 0
super
end
# In addition to the defaults set in Graph::initialize, sets
# [bar_gap] true
# [stack] :overlap
def set_defaults
init_with( :bar_gap => true, :stack => :overlap )
end
# Whether to have a gap between the bars or not, default
# is true, set to false if you don't want gaps.
attr_accessor :bar_gap
# How to stack data sets. :overlap overlaps bars with
# transparent colors, :top stacks bars on top of one another,
# :side stacks the bars side-by-side. Defaults to :overlap.
attr_accessor :stack
protected
def max_value
return @data.collect{|x| x[:data].max}.max
end
def min_value
min = 0
if (min_scale_value.nil? == false) then
min = min_scale_value
else
min = @data.collect{|x| x[:data].min}.min
end
return min
end
def get_css
return <<EOL
/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
.key1,.fill1{
fill: #ff0000;
fill-opacity: 0.5;
stroke: none;
stroke-width: 0.5px;
}
.key2,.fill2{
fill: #0000ff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key3,.fill3{
fill: #00ff00;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key4,.fill4{
fill: #ffcc00;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key5,.fill5{
fill: #00ccff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key6,.fill6{
fill: #ff00ff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key7,.fill7{
fill: #00ffff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key8,.fill8{
fill: #ffff00;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key9,.fill9{
fill: #cc6666;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key10,.fill10{
fill: #663399;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key11,.fill11{
fill: #339900;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key12,.fill12{
fill: #9966FF;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
EOL
end
end
end
end

View File

@ -0,0 +1,136 @@
require 'rexml/document'
require 'SVG/Graph/BarBase'
module SVG
module Graph
# === Create presentation quality SVG horitonzal bar graphs easily
#
# = Synopsis
#
# require 'SVG/Graph/BarHorizontal'
#
# fields = %w(Jan Feb Mar)
# data_sales_02 = [12, 45, 21]
#
# graph = SVG::Graph::BarHorizontal.new({
# :height => 500,
# :width => 300,
# :fields => fields,
# })
#
# graph.add_data({
# :data => data_sales_02,
# :title => 'Sales 2002',
# })
#
# print "Content-type: image/svg+xml\r\n\r\n"
# print graph.burn
#
# = Description
#
# This object aims to allow you to easily create high quality
# SVG horitonzal bar graphs. You can either use the default style sheet
# or supply your own. Either way there are many options which can
# be configured to give you control over how the graph is
# generated - with or without a key, data elements at each point,
# title, subtitle etc.
#
# = Examples
#
# * http://germane-software.com/repositories/public/SVG/test/test.rb
#
# = See also
#
# * SVG::Graph::Graph
# * SVG::Graph::Bar
# * SVG::Graph::Line
# * SVG::Graph::Pie
# * SVG::Graph::Plot
# * SVG::Graph::TimeSeries
#
# == Author
#
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
#
# Copyright 2004 Sean E. Russell
# This software is available under the Ruby license[LICENSE.txt]
#
class BarHorizontal < BarBase
# In addition to the defaults set in BarBase::set_defaults, sets
# [rotate_y_labels] true
# [show_x_guidelines] true
# [show_y_guidelines] false
def set_defaults
super
init_with(
:rotate_y_labels => true,
:show_x_guidelines => true,
:show_y_guidelines => false
)
self.right_align = self.right_font = 1
end
protected
def get_x_labels
maxvalue = max_value
minvalue = min_value
range = maxvalue - minvalue
top_pad = range == 0 ? 10 : range / 20.0
scale_range = (maxvalue + top_pad) - minvalue
scale_division = scale_divisions || (scale_range / 10.0)
if scale_integers
scale_division = scale_division < 1 ? 1 : scale_division.round
end
rv = []
maxvalue = maxvalue%scale_division == 0 ?
maxvalue : maxvalue + scale_division
minvalue.step( maxvalue, scale_division ) {|v| rv << v}
return rv
end
def get_y_labels
@config[:fields]
end
def y_label_offset( height )
height / -2.0
end
def draw_data
minvalue = min_value
fieldheight = field_height
fieldwidth = (@graph_width.to_f - font_size*2*right_font ) /
(get_x_labels.max - get_x_labels.min )
bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
subbar_height = fieldheight - bargap
subbar_height /= @data.length if stack == :side
field_count = 1
y_mod = (subbar_height / 2) + (font_size / 2)
@config[:fields].each_index { |i|
dataset_count = 0
for dataset in @data
y = @graph_height - (fieldheight * field_count)
y += (subbar_height * dataset_count) if stack == :side
x = (dataset[:data][i] - minvalue) * fieldwidth
@graph.add_element( "path", {
"d" => "M0 #{y} H#{x} v#{subbar_height} H0 Z",
"class" => "fill#{dataset_count+1}"
})
make_datapoint_text(
x+5, y+y_mod, dataset[:data][i], "text-anchor: start; "
)
dataset_count += 1
end
field_count += 1
}
end
end
end
end

977
lib/SVG/Graph/Graph.rb Normal file
View File

@ -0,0 +1,977 @@
begin
require 'zlib'
@@__have_zlib = true
rescue
@@__have_zlib = false
end
require 'rexml/document'
module SVG
module Graph
VERSION = '@ANT_VERSION@'
# === Base object for generating SVG Graphs
#
# == Synopsis
#
# This class is only used as a superclass of specialized charts. Do not
# attempt to use this class directly, unless creating a new chart type.
#
# For examples of how to subclass this class, see the existing specific
# subclasses, such as SVG::Graph::Pie.
#
# == Examples
#
# For examples of how to use this package, see either the test files, or
# the documentation for the specific class you want to use.
#
# * file:test/plot.rb
# * file:test/single.rb
# * file:test/test.rb
# * file:test/timeseries.rb
#
# == Description
#
# This package should be used as a base for creating SVG graphs.
#
# == Acknowledgements
#
# Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
# port is based on.
#
# Stephen Morgan for creating the TT template and SVG.
#
# == See
#
# * SVG::Graph::BarHorizontal
# * SVG::Graph::Bar
# * SVG::Graph::Line
# * SVG::Graph::Pie
# * SVG::Graph::Plot
# * SVG::Graph::TimeSeries
#
# == Author
#
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
#
# Copyright 2004 Sean E. Russell
# This software is available under the Ruby license[LICENSE.txt]
#
class Graph
include REXML
# Initialize the graph object with the graph settings. You won't
# instantiate this class directly; see the subclass for options.
# [width] 500
# [height] 300
# [show_x_guidelines] false
# [show_y_guidelines] true
# [show_data_values] true
# [min_scale_value] 0
# [show_x_labels] true
# [stagger_x_labels] false
# [rotate_x_labels] false
# [step_x_labels] 1
# [step_include_first_x_label] true
# [show_y_labels] true
# [rotate_y_labels] false
# [scale_integers] false
# [show_x_title] false
# [x_title] 'X Field names'
# [show_y_title] false
# [y_title_text_direction] :bt
# [y_title] 'Y Scale'
# [show_graph_title] false
# [graph_title] 'Graph Title'
# [show_graph_subtitle] false
# [graph_subtitle] 'Graph Sub Title'
# [key] true,
# [key_position] :right, # bottom or righ
# [font_size] 12
# [title_font_size] 16
# [subtitle_font_size] 14
# [x_label_font_size] 12
# [x_title_font_size] 14
# [y_label_font_size] 12
# [y_title_font_size] 14
# [key_font_size] 10
# [no_css] false
# [add_popups] false
def initialize( config )
@config = config
self.top_align = self.top_font = self.right_align = self.right_font = 0
init_with({
:width => 500,
:height => 300,
:show_x_guidelines => false,
:show_y_guidelines => true,
:show_data_values => true,
:min_scale_value => 0,
:show_x_labels => true,
:stagger_x_labels => false,
:rotate_x_labels => false,
:step_x_labels => 1,
:step_include_first_x_label => true,
:show_y_labels => true,
:rotate_y_labels => false,
:stagger_y_labels => false,
:scale_integers => false,
:show_x_title => false,
:x_title => 'X Field names',
:show_y_title => false,
:y_title_text_direction => :bt,
:y_title => 'Y Scale',
:show_graph_title => false,
:graph_title => 'Graph Title',
:show_graph_subtitle => false,
:graph_subtitle => 'Graph Sub Title',
:key => true,
:key_position => :right, # bottom or right
:font_size =>11,
:title_font_size =>12,
:subtitle_font_size =>14,
:x_label_font_size =>12,
:x_title_font_size =>14,
:y_label_font_size =>12,
:y_title_font_size =>14,
:key_font_size =>10,
:no_css =>false,
:add_popups =>false,
})
set_defaults if methods.include? "set_defaults"
init_with config
end
# This method allows you do add data to the graph object.
# It can be called several times to add more data sets in.
#
# data_sales_02 = [12, 45, 21];
#
# graph.add_data({
# :data => data_sales_02,
# :title => 'Sales 2002'
# })
def add_data conf
@data = [] unless defined? @data
if conf[:data] and conf[:data].kind_of? Array
@data << conf
else
raise "No data provided by #{conf.inspect}"
end
end
# This method removes all data from the object so that you can
# reuse it to create a new graph but with the same config options.
#
# graph.clear_data
def clear_data
@data = []
end
# This method processes the template with the data and
# config which has been set and returns the resulting SVG.
#
# This method will croak unless at least one data set has
# been added to the graph object.
#
# print graph.burn
def burn
raise "No data available" unless @data.size > 0
calculations if methods.include? 'calculations'
start_svg
calculate_graph_dimensions
@foreground = Element.new( "g" )
draw_graph
draw_titles
draw_legend
draw_data
@graph.add_element( @foreground )
style
data = ""
@doc.write( data, 0 )
if @config[:compress]
if @@__have_zlib
inp, out = IO.pipe
gz = Zlib::GzipWriter.new( out )
gz.write data
gz.close
data = inp.read
else
data << "<!-- Ruby Zlib not available for SVGZ -->";
end
end
return data
end
# Set the height of the graph box, this is the total height
# of the SVG box created - not the graph it self which auto
# scales to fix the space.
attr_accessor :height
# Set the width of the graph box, this is the total width
# of the SVG box created - not the graph it self which auto
# scales to fix the space.
attr_accessor :width
# Set the path to an external stylesheet, set to '' if
# you want to revert back to using the defaut internal version.
#
# To create an external stylesheet create a graph using the
# default internal version and copy the stylesheet section to
# an external file and edit from there.
attr_accessor :style_sheet
# (Bool) Show the value of each element of data on the graph
attr_accessor :show_data_values
# The point at which the Y axis starts, defaults to '0',
# if set to nil it will default to the minimum data value.
attr_accessor :min_scale_value
# Whether to show labels on the X axis or not, defaults
# to true, set to false if you want to turn them off.
attr_accessor :show_x_labels
# This puts the X labels at alternative levels so if they
# are long field names they will not overlap so easily.
# Default it false, to turn on set to true.
attr_accessor :stagger_x_labels
# This puts the Y labels at alternative levels so if they
# are long field names they will not overlap so easily.
# Default it false, to turn on set to true.
attr_accessor :stagger_y_labels
# This turns the X axis labels by 90 degrees.
# Default it false, to turn on set to true.
attr_accessor :rotate_x_labels
# This turns the Y axis labels by 90 degrees.
# Default it false, to turn on set to true.
attr_accessor :rotate_y_labels
# How many "steps" to use between displayed X axis labels,
# a step of one means display every label, a step of two results
# in every other label being displayed (label <gap> label <gap> label),
# a step of three results in every third label being displayed
# (label <gap> <gap> label <gap> <gap> label) and so on.
attr_accessor :step_x_labels
# Whether to (when taking "steps" between X axis labels) step from
# the first label (i.e. always include the first label) or step from
# the X axis origin (i.e. start with a gap if step_x_labels is greater
# than one).
attr_accessor :step_include_first_x_label
# Whether to show labels on the Y axis or not, defaults
# to true, set to false if you want to turn them off.
attr_accessor :show_y_labels
# Ensures only whole numbers are used as the scale divisions.
# Default it false, to turn on set to true. This has no effect if
# scale divisions are less than 1.
attr_accessor :scale_integers
# This defines the gap between markers on the Y axis,
# default is a 10th of the max_value, e.g. you will have
# 10 markers on the Y axis. NOTE: do not set this too
# low - you are limited to 999 markers, after that the
# graph won't generate.
attr_accessor :scale_divisions
# Whether to show the title under the X axis labels,
# default is false, set to true to show.
attr_accessor :show_x_title
# What the title under X axis should be, e.g. 'Months'.
attr_accessor :x_title
# Whether to show the title under the Y axis labels,
# default is false, set to true to show.
attr_accessor :show_y_title
# Aligns writing mode for Y axis label.
# Defaults to :bt (Bottom to Top).
# Change to :tb (Top to Bottom) to reverse.
attr_accessor :y_title_text_direction
# What the title under Y axis should be, e.g. 'Sales in thousands'.
attr_accessor :y_title
# Whether to show a title on the graph, defaults
# to false, set to true to show.
attr_accessor :show_graph_title
# What the title on the graph should be.
attr_accessor :graph_title
# Whether to show a subtitle on the graph, defaults
# to false, set to true to show.
attr_accessor :show_graph_subtitle
# What the subtitle on the graph should be.
attr_accessor :graph_subtitle
# Whether to show a key, defaults to false, set to
# true if you want to show it.
attr_accessor :key
# Where the key should be positioned, defaults to
# :right, set to :bottom if you want to move it.
attr_accessor :key_position
# Set the font size (in points) of the data point labels
attr_accessor :font_size
# Set the font size of the X axis labels
attr_accessor :x_label_font_size
# Set the font size of the X axis title
attr_accessor :x_title_font_size
# Set the font size of the Y axis labels
attr_accessor :y_label_font_size
# Set the font size of the Y axis title
attr_accessor :y_title_font_size
# Set the title font size
attr_accessor :title_font_size
# Set the subtitle font size
attr_accessor :subtitle_font_size
# Set the key font size
attr_accessor :key_font_size
# Show guidelines for the X axis
attr_accessor :show_x_guidelines
# Show guidelines for the Y axis
attr_accessor :show_y_guidelines
# Do not use CSS if set to true. Many SVG viewers do not support CSS, but
# not using CSS can result in larger SVGs as well as making it impossible to
# change colors after the chart is generated. Defaults to false.
attr_accessor :no_css
# Add popups for the data points on some graphs
attr_accessor :add_popups
protected
def sort( *arrys )
sort_multiple( arrys )
end
# Overwrite configuration options with supplied options. Used
# by subclasses.
def init_with config
config.each { |key, value|
self.send( key.to_s+"=", value ) if methods.include? key.to_s
}
end
attr_accessor :top_align, :top_font, :right_align, :right_font
KEY_BOX_SIZE = 12
# Override this (and call super) to change the margin to the left
# of the plot area. Results in @border_left being set.
def calculate_left_margin
@border_left = 7
# Check for Y labels
max_y_label_height_px = rotate_y_labels ?
y_label_font_size :
get_y_labels.max{|a,b|
a.to_s.length<=>b.to_s.length
}.to_s.length * y_label_font_size * 0.6
@border_left += max_y_label_height_px if show_y_labels
@border_left += max_y_label_height_px + 10 if stagger_y_labels
@border_left += y_title_font_size + 5 if show_y_title
end
# Calculates the width of the widest Y label. This will be the
# character height if the Y labels are rotated
def max_y_label_width_px
return font_size if rotate_y_labels
end
# Override this (and call super) to change the margin to the right
# of the plot area. Results in @border_right being set.
def calculate_right_margin
@border_right = 7
if key and key_position == :right
val = keys.max { |a,b| a.length <=> b.length }
@border_right += val.length * key_font_size * 0.6
@border_right += KEY_BOX_SIZE
@border_right += 10 # Some padding around the box
end
end
# Override this (and call super) to change the margin to the top
# of the plot area. Results in @border_top being set.
def calculate_top_margin
@border_top = 5
@border_top += title_font_size if show_graph_title
@border_top += 5
@border_top += subtitle_font_size if show_graph_subtitle
end
# Adds pop-up point information to a graph.
def add_popup( x, y, label )
txt_width = label.length * font_size * 0.6 + 10
tx = (x+txt_width > width ? x-5 : x+5)
t = @foreground.add_element( "text", {
"x" => tx.to_s,
"y" => (y - font_size).to_s,
"visibility" => "hidden",
})
t.attributes["style"] = "fill: #000; "+
(x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
t.text = label.to_s
t.attributes["id"] = t.id.to_s
@foreground.add_element( "circle", {
"cx" => x.to_s,
"cy" => y.to_s,
"r" => "10",
"style" => "opacity: 0",
"onmouseover" =>
"document.getElementById(#{t.id}).setAttribute('visibility', 'visible' )",
"onmouseout" =>
"document.getElementById(#{t.id}).setAttribute('visibility', 'hidden' )",
})
end
# Override this (and call super) to change the margin to the bottom
# of the plot area. Results in @border_bottom being set.
def calculate_bottom_margin
@border_bottom = 7
if key and key_position == :bottom
@border_bottom += @data.size * (font_size + 5)
@border_bottom += 10
end
if show_x_labels
max_x_label_height_px = rotate_x_labels ?
get_x_labels.max{|a,b|
a.length<=>b.length
}.length * x_label_font_size * 0.6 :
x_label_font_size
@border_bottom += max_x_label_height_px
@border_bottom += max_x_label_height_px + 10 if stagger_x_labels
end
@border_bottom += x_title_font_size + 5 if show_x_title
end
# Draws the background, axis, and labels.
def draw_graph
@graph = @root.add_element( "g", {
"transform" => "translate( #@border_left #@border_top )"
})
# Background
@graph.add_element( "rect", {
"x" => "0",
"y" => "0",
"width" => @graph_width.to_s,
"height" => @graph_height.to_s,
"class" => "graphBackground"
})
# Axis
@graph.add_element( "path", {
"d" => "M 0 0 v#@graph_height",
"class" => "axis",
"id" => "xAxis"
})
@graph.add_element( "path", {
"d" => "M 0 #@graph_height h#@graph_width",
"class" => "axis",
"id" => "yAxis"
})
draw_x_labels
draw_y_labels
end
# Where in the X area the label is drawn
# Centered in the field, should be width/2. Start, 0.
def x_label_offset( width )
0
end
def make_datapoint_text( x, y, value, style="" )
if show_data_values
@foreground.add_element( "text", {
"x" => x.to_s,
"y" => y.to_s,
"class" => "dataPointLabel",
"style" => "#{style} stroke: #fff; stroke-width: 2;"
}).text = value.to_s
text = @foreground.add_element( "text", {
"x" => x.to_s,
"y" => y.to_s,
"class" => "dataPointLabel"
})
text.text = value.to_s
text.attributes["style"] = style if style.length > 0
end
end
# Draws the X axis labels
def draw_x_labels
stagger = x_label_font_size + 5
if show_x_labels
label_width = field_width
count = 0
for label in get_x_labels
if step_include_first_x_label == true then
step = count % step_x_labels
else
step = (count + 1) % step_x_labels
end
if step == 0 then
text = @graph.add_element( "text" )
text.attributes["class"] = "xAxisLabels"
text.text = label.to_s
x = count * label_width + x_label_offset( label_width )
y = @graph_height + x_label_font_size + 3
t = 0 - (font_size / 2)
if stagger_x_labels and count % 2 == 1
y += stagger
@graph.add_element( "path", {
"d" => "M#{x} #@graph_height v#{stagger}",
"class" => "staggerGuideLine"
})
end
text.attributes["x"] = x.to_s
text.attributes["y"] = y.to_s
if rotate_x_labels
text.attributes["transform"] =
"rotate( 90 #{x} #{y-x_label_font_size} )"+
" translate( 0 -#{x_label_font_size/4} )"
text.attributes["style"] = "text-anchor: start"
else
text.attributes["style"] = "text-anchor: middle"
end
end
draw_x_guidelines( label_width, count ) if show_x_guidelines
count += 1
end
end
end
# Where in the Y area the label is drawn
# Centered in the field, should be width/2. Start, 0.
def y_label_offset( height )
0
end
def field_width
(@graph_width.to_f - font_size*2*right_font) /
(get_x_labels.length - right_align)
end
def field_height
(@graph_height.to_f - font_size*2*top_font) /
(get_y_labels.length - top_align)
end
# Draws the Y axis labels
def draw_y_labels
stagger = y_label_font_size + 5
if show_y_labels
label_height = field_height
count = 0
y_offset = @graph_height + y_label_offset( label_height )
y_offset += font_size/1.2 unless rotate_y_labels
for label in get_y_labels
y = y_offset - (label_height * count)
x = rotate_y_labels ? 0 : -3
if stagger_y_labels and count % 2 == 1
x -= stagger
@graph.add_element( "path", {
"d" => "M#{x} #{y} h#{stagger}",
"class" => "staggerGuideLine"
})
end
text = @graph.add_element( "text", {
"x" => x.to_s,
"y" => y.to_s,
"class" => "yAxisLabels"
})
text.text = label.to_s
if rotate_y_labels
text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
"rotate( 90 #{x} #{y} ) "
text.attributes["style"] = "text-anchor: middle"
else
text.attributes["y"] = (y - (y_label_font_size/2)).to_s
text.attributes["style"] = "text-anchor: end"
end
draw_y_guidelines( label_height, count ) if show_y_guidelines
count += 1
end
end
end
# Draws the X axis guidelines
def draw_x_guidelines( label_height, count )
if count != 0
@graph.add_element( "path", {
"d" => "M#{label_height*count} 0 v#@graph_height",
"class" => "guideLines"
})
end
end
# Draws the Y axis guidelines
def draw_y_guidelines( label_height, count )
if count != 0
@graph.add_element( "path", {
"d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
"class" => "guideLines"
})
end
end
# Draws the graph title and subtitle
def draw_titles
if show_graph_title
@root.add_element( "text", {
"x" => (width / 2).to_s,
"y" => (title_font_size).to_s,
"class" => "mainTitle"
}).text = graph_title.to_s
end
if show_graph_subtitle
y_subtitle = show_graph_title ?
title_font_size + 10 :
subtitle_font_size
@root.add_element("text", {
"x" => (width / 2).to_s,
"y" => (y_subtitle).to_s,
"class" => "subTitle"
}).text = graph_subtitle.to_s
end
if show_x_title
y = @graph_height + @border_top + x_title_font_size
if show_x_labels
y += x_label_font_size + 5 if stagger_x_labels
y += x_label_font_size + 5
end
x = width / 2
@root.add_element("text", {
"x" => x.to_s,
"y" => y.to_s,
"class" => "xAxisTitle",
}).text = x_title.to_s
end
if show_y_title
x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
y = height / 2
text = @root.add_element("text", {
"x" => x.to_s,
"y" => y.to_s,
"class" => "yAxisTitle",
})
text.text = y_title.to_s
if y_title_text_direction == :bt
text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
else
text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
end
end
end
def keys
return @data.collect{ |d| d[:title] }
end
# Draws the legend on the graph
def draw_legend
if key
group = @root.add_element( "g" )
key_count = 0
for key_name in keys
y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
group.add_element( "rect", {
"x" => 0.to_s,
"y" => y_offset.to_s,
"width" => KEY_BOX_SIZE.to_s,
"height" => KEY_BOX_SIZE.to_s,
"class" => "key#{key_count+1}"
})
group.add_element( "text", {
"x" => (KEY_BOX_SIZE + 5).to_s,
"y" => (y_offset + KEY_BOX_SIZE).to_s,
"class" => "keyText"
}).text = key_name.to_s
key_count += 1
end
case key_position
when :right
x_offset = @graph_width + @border_left + 10
y_offset = @border_top + 20
when :bottom
x_offset = @border_left + 20
y_offset = @border_top + @graph_height + 5
if show_x_labels
max_x_label_height_px = rotate_x_labels ?
get_x_labels.max{|a,b|
a.length<=>b.length
}.length * x_label_font_size :
x_label_font_size
y_offset += max_x_label_height_px
y_offset += max_x_label_height_px + 5 if stagger_x_labels
end
y_offset += x_title_font_size + 5 if show_x_title
end
group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
end
end
private
def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
if lo < hi
p = partition(arrys,lo,hi)
sort_multiple(arrys, lo, p-1)
sort_multiple(arrys, p+1, hi)
end
arrys
end
def partition( arrys, lo, hi )
p = arrys[0][lo]
l = lo
z = lo+1
while z <= hi
if arrys[0][z] < p
l += 1
arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
end
z += 1
end
arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
l
end
def style
if no_css
styles = parse_css
@root.elements.each("//*[@class]") { |el|
cl = el.attributes["class"]
style = styles[cl]
style += el.attributes["style"] if el.attributes["style"]
el.attributes["style"] = style
}
end
end
def parse_css
css = get_style
rv = {}
while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
names_orig = names = $1
css = $'
css =~ /([^}]+)\}/m
content = $1
css = $'
nms = []
while names =~ /^\s*,?\s*\.(\w+)/
nms << $1
names = $'
end
content = content.tr( "\n\t", " ")
for name in nms
current = rv[name]
current = current ? current+"; "+content : content
rv[name] = current.strip.squeeze(" ")
end
end
return rv
end
# Override and place code to add defs here
def add_defs defs
end
def start_svg
# Base document
@doc = Document.new
@doc << XMLDecl.new
@doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
%q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
if style_sheet && style_sheet != ''
@doc << ProcessingInstruction.new( "xml-stylesheet",
%Q{href="#{style_sheet}" type="text/css"} )
end
@root = @doc.add_element( "svg", {
"width" => width.to_s,
"height" => height.to_s,
"viewBox" => "0 0 #{width} #{height}",
"xmlns" => "http://www.w3.org/2000/svg",
"xmlns:xlink" => "http://www.w3.org/1999/xlink",
"xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
"a3:scriptImplementation" => "Adobe"
})
@root << Comment.new( " "+"\\"*66 )
@root << Comment.new( " Created with SVG::Graph " )
@root << Comment.new( " SVG::Graph by Sean E. Russell " )
@root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
" Leo Lapworth & Stephan Morgan " )
@root << Comment.new( " "+"/"*66 )
defs = @root.add_element( "defs" )
add_defs defs
if not(style_sheet && style_sheet != '') and !no_css
@root << Comment.new(" include default stylesheet if none specified ")
style = defs.add_element( "style", {"type"=>"text/css"} )
style << CData.new( get_style )
end
@root << Comment.new( "SVG Background" )
@root.add_element( "rect", {
"width" => width.to_s,
"height" => height.to_s,
"x" => "0",
"y" => "0",
"class" => "svgBackground"
})
end
def calculate_graph_dimensions
calculate_left_margin
calculate_right_margin
calculate_bottom_margin
calculate_top_margin
@graph_width = width - @border_left - @border_right
@graph_height = height - @border_top - @border_bottom
end
def get_style
return <<EOL
/* Copy from here for external style sheet */
.svgBackground{
fill:#ffffff;
}
.graphBackground{
fill:#f0f0f0;
}
/* graphs titles */
.mainTitle{
text-anchor: middle;
fill: #555555;
font-size: #{title_font_size}px;
font-family: "Verdana", sans-serif;
font-weight: bold;
}
.subTitle{
text-anchor: middle;
fill: #999999;
font-size: #{subtitle_font_size}px;
font-family: "Arial", sans-serif;
font-weight: normal;
}
.axis{
stroke: #000000;
stroke-width: 1px;
}
.guideLines{
stroke: #666666;
stroke-width: 1px;
stroke-dasharray: 5 5;
}
.xAxisLabels{
text-anchor: middle;
fill: #000000;
font-size: #{x_label_font_size}px;
font-family: "Arial", sans-serif;
font-weight: normal;
}
.yAxisLabels{
text-anchor: end;
fill: #000000;
font-size: #{y_label_font_size}px;
font-family: "Arial", sans-serif;
font-weight: normal;
}
.xAxisTitle{
text-anchor: middle;
fill: #ff0000;
font-size: #{x_title_font_size}px;
font-family: "Arial", sans-serif;
font-weight: normal;
}
.yAxisTitle{
fill: #ff0000;
text-anchor: middle;
font-size: #{y_title_font_size}px;
font-family: "Arial", sans-serif;
font-weight: normal;
}
.dataPointLabel{
fill: #000000;
text-anchor:middle;
font-size: 10px;
font-family: "Arial", sans-serif;
font-weight: normal;
}
.staggerGuideLine{
fill: none;
stroke: #000000;
stroke-width: 0.5px;
}
#{get_css}
.keyText{
fill: #000000;
text-anchor:start;
font-size: #{key_font_size}px;
font-family: "Arial", sans-serif;
font-weight: normal;
}
/* End copy for external style sheet */
EOL
end
end
end
end

444
lib/SVG/Graph/Line.rb Normal file
View File

@ -0,0 +1,444 @@
require 'SVG/Graph/Graph'
module SVG
module Graph
# === Create presentation quality SVG line graphs easily
#
# = Synopsis
#
# require 'SVG/Graph/Line'
#
# fields = %w(Jan Feb Mar);
# data_sales_02 = [12, 45, 21]
# data_sales_03 = [15, 30, 40]
#
# graph = SVG::Graph::Line.new({
# :height => 500,
# :width => 300,
# :fields => fields,
# })
#
# graph.add_data({
# :data => data_sales_02,
# :title => 'Sales 2002',
# })
#
# graph.add_data({
# :data => data_sales_03,
# :title => 'Sales 2003',
# })
#
# print "Content-type: image/svg+xml\r\n\r\n";
# print graph.burn();
#
# = Description
#
# This object aims to allow you to easily create high quality
# SVG line graphs. You can either use the default style sheet
# or supply your own. Either way there are many options which can
# be configured to give you control over how the graph is
# generated - with or without a key, data elements at each point,
# title, subtitle etc.
#
# = Examples
#
# http://www.germane-software/repositories/public/SVG/test/single.rb
#
# = Notes
#
# The default stylesheet handles upto 10 data sets, if you
# use more you must create your own stylesheet and add the
# additional settings for the extra data sets. You will know
# if you go over 10 data sets as they will have no style and
# be in black.
#
# = See also
#
# * SVG::Graph::Graph
# * SVG::Graph::BarHorizontal
# * SVG::Graph::Bar
# * SVG::Graph::Pie
# * SVG::Graph::Plot
# * SVG::Graph::TimeSeries
#
# == Author
#
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
#
# Copyright 2004 Sean E. Russell
# This software is available under the Ruby license[LICENSE.txt]
#
class Line < SVG::Graph::Graph
# Show a small circle on the graph where the line
# goes from one point to the next.
attr_accessor :show_data_points
# Accumulates each data set. (i.e. Each point increased by sum of
# all previous series at same point). Default is 0, set to '1' to show.
attr_accessor :stacked
# Fill in the area under the plot if true
attr_accessor :area_fill
# The constructor takes a hash reference, fields (the names for each
# field on the X axis) MUST be set, all other values are defaulted to
# those shown above - with the exception of style_sheet which defaults
# to using the internal style sheet.
def initialize config
raise "fields was not supplied or is empty" unless config[:fields] &&
config[:fields].kind_of?(Array) &&
config[:fields].length > 0
super
end
# In addition to the defaults set in Graph::initialize, sets
# [show_data_points] true
# [show_data_values] true
# [stacked] false
# [area_fill] false
def set_defaults
init_with(
:show_data_points => true,
:show_data_values => true,
:stacked => false,
:area_fill => false
)
self.top_align = self.top_font = self.right_align = self.right_font = 1
end
protected
def max_value
max = 0
if (stacked == true) then
sums = Array.new(@config[:fields].length).fill(0)
@data.each do |data|
sums.each_index do |i|
sums[i] += data[:data][i].to_f
end
end
max = sums.max
else
max = @data.collect{|x| x[:data].max}.max
end
return max
end
def min_value
min = 0
if (min_scale_value.nil? == false) then
min = min_scale_value
elsif (stacked == true) then
min = @data[-1][:data].min
else
min = @data.collect{|x| x[:data].min}.min
end
return min
end
def get_x_labels
@config[:fields]
end
def calculate_left_margin
super
label_left = @config[:fields][0].length / 2 * font_size * 0.6
@border_left = label_left if label_left > @border_left
end
def get_y_labels
maxvalue = max_value
minvalue = min_value
range = maxvalue - minvalue
top_pad = range == 0 ? 10 : range / 20.0
scale_range = (maxvalue + top_pad) - minvalue
scale_division = scale_divisions || (scale_range / 10.0)
if scale_integers
scale_division = scale_division < 1 ? 1 : scale_division.round
end
rv = []
maxvalue = maxvalue%scale_division == 0 ?
maxvalue : maxvalue + scale_division
minvalue.step( maxvalue, scale_division ) {|v| rv << v}
return rv
end
def calc_coords(field, value, width = field_width, height = field_height)
coords = {:x => 0, :y => 0}
coords[:x] = width * field
coords[:y] = @graph_height - value * height
return coords
end
def draw_data
minvalue = min_value
fieldheight = (@graph_height.to_f - font_size*2*top_font) /
(get_y_labels.max - get_y_labels.min)
fieldwidth = field_width
line = @data.length
prev_sum = Array.new(@config[:fields].length).fill(0)
cum_sum = Array.new(@config[:fields].length).fill(-minvalue)
for data in @data.reverse
lpath = ""
apath = ""
if not stacked then cum_sum.fill(-minvalue) end
data[:data].each_index do |i|
cum_sum[i] += data[:data][i]
c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
lpath << "#{c[:x]} #{c[:y]} "
end
if area_fill
if stacked then
(prev_sum.length - 1).downto 0 do |i|
c = calc_coords(i, prev_sum[i], fieldwidth, fieldheight)
apath << "#{c[:x]} #{c[:y]} "
end
c = calc_coords(0, prev_sum[0], fieldwidth, fieldheight)
else
apath = "V#@graph_height"
c = calc_coords(0, 0, fieldwidth, fieldheight)
end
@graph.add_element("path", {
"d" => "M#{c[:x]} #{c[:y]} L" + lpath + apath + "Z",
"class" => "fill#{line}"
})
end
@graph.add_element("path", {
"d" => "M0 #@graph_height L" + lpath,
"class" => "line#{line}"
})
if show_data_points || show_data_values
cum_sum.each_index do |i|
if show_data_points
@graph.add_element( "circle", {
"cx" => (fieldwidth * i).to_s,
"cy" => (@graph_height - cum_sum[i] * fieldheight).to_s,
"r" => "2.5",
"class" => "dataPoint#{line}"
})
end
make_datapoint_text(
fieldwidth * i,
@graph_height - cum_sum[i] * fieldheight - 6,
cum_sum[i] + minvalue
)
end
end
prev_sum = cum_sum.dup
line -= 1
end
end
def get_css
return <<EOL
/* default line styles */
.line1{
fill: none;
stroke: #ff0000;
stroke-width: 1px;
}
.line2{
fill: none;
stroke: #0000ff;
stroke-width: 1px;
}
.line3{
fill: none;
stroke: #00ff00;
stroke-width: 1px;
}
.line4{
fill: none;
stroke: #ffcc00;
stroke-width: 1px;
}
.line5{
fill: none;
stroke: #00ccff;
stroke-width: 1px;
}
.line6{
fill: none;
stroke: #ff00ff;
stroke-width: 1px;
}
.line7{
fill: none;
stroke: #00ffff;
stroke-width: 1px;
}
.line8{
fill: none;
stroke: #ffff00;
stroke-width: 1px;
}
.line9{
fill: none;
stroke: #ccc6666;
stroke-width: 1px;
}
.line10{
fill: none;
stroke: #663399;
stroke-width: 1px;
}
.line11{
fill: none;
stroke: #339900;
stroke-width: 1px;
}
.line12{
fill: none;
stroke: #9966FF;
stroke-width: 1px;
}
/* default fill styles */
.fill1{
fill: #cc0000;
fill-opacity: 0.2;
stroke: none;
}
.fill2{
fill: #0000cc;
fill-opacity: 0.2;
stroke: none;
}
.fill3{
fill: #00cc00;
fill-opacity: 0.2;
stroke: none;
}
.fill4{
fill: #ffcc00;
fill-opacity: 0.2;
stroke: none;
}
.fill5{
fill: #00ccff;
fill-opacity: 0.2;
stroke: none;
}
.fill6{
fill: #ff00ff;
fill-opacity: 0.2;
stroke: none;
}
.fill7{
fill: #00ffff;
fill-opacity: 0.2;
stroke: none;
}
.fill8{
fill: #ffff00;
fill-opacity: 0.2;
stroke: none;
}
.fill9{
fill: #cc6666;
fill-opacity: 0.2;
stroke: none;
}
.fill10{
fill: #663399;
fill-opacity: 0.2;
stroke: none;
}
.fill11{
fill: #339900;
fill-opacity: 0.2;
stroke: none;
}
.fill12{
fill: #9966FF;
fill-opacity: 0.2;
stroke: none;
}
/* default line styles */
.key1,.dataPoint1{
fill: #ff0000;
stroke: none;
stroke-width: 1px;
}
.key2,.dataPoint2{
fill: #0000ff;
stroke: none;
stroke-width: 1px;
}
.key3,.dataPoint3{
fill: #00ff00;
stroke: none;
stroke-width: 1px;
}
.key4,.dataPoint4{
fill: #ffcc00;
stroke: none;
stroke-width: 1px;
}
.key5,.dataPoint5{
fill: #00ccff;
stroke: none;
stroke-width: 1px;
}
.key6,.dataPoint6{
fill: #ff00ff;
stroke: none;
stroke-width: 1px;
}
.key7,.dataPoint7{
fill: #00ffff;
stroke: none;
stroke-width: 1px;
}
.key8,.dataPoint8{
fill: #ffff00;
stroke: none;
stroke-width: 1px;
}
.key9,.dataPoint9{
fill: #cc6666;
stroke: none;
stroke-width: 1px;
}
.key10,.dataPoint10{
fill: #663399;
stroke: none;
stroke-width: 1px;
}
.key11,.dataPoint11{
fill: #339900;
stroke: none;
stroke-width: 1px;
}
.key12,.dataPoint12{
fill: #9966FF;
stroke: none;
stroke-width: 1px;
}
EOL
end
end
end
end

394
lib/SVG/Graph/Pie.rb Normal file
View File

@ -0,0 +1,394 @@
require 'SVG/Graph/Graph'
module SVG
module Graph
# === Create presentation quality SVG pie graphs easily
#
# == Synopsis
#
# require 'SVG/Graph/Pie'
#
# fields = %w(Jan Feb Mar)
# data_sales_02 = [12, 45, 21]
#
# graph = SVG::Graph::Pie.new({
# :height => 500,
# :width => 300,
# :fields => fields,
# })
#
# graph.add_data({
# :data => data_sales_02,
# :title => 'Sales 2002',
# })
#
# print "Content-type: image/svg+xml\r\n\r\n"
# print graph.burn();
#
# == Description
#
# This object aims to allow you to easily create high quality
# SVG pie graphs. You can either use the default style sheet
# or supply your own. Either way there are many options which can
# be configured to give you control over how the graph is
# generated - with or without a key, display percent on pie chart,
# title, subtitle etc.
#
# = Examples
#
# http://www.germane-software/repositories/public/SVG/test/single.rb
#
# == See also
#
# * SVG::Graph::Graph
# * SVG::Graph::BarHorizontal
# * SVG::Graph::Bar
# * SVG::Graph::Line
# * SVG::Graph::Plot
# * SVG::Graph::TimeSeries
#
# == Author
#
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
#
# Copyright 2004 Sean E. Russell
# This software is available under the Ruby license[LICENSE.txt]
#
class Pie < Graph
# Defaults are those set by Graph::initialize, and
# [show_shadow] true
# [shadow_offset] 10
# [show_data_labels] false
# [show_actual_values] false
# [show_percent] true
# [show_key_data_labels] true
# [show_key_actual_values] true
# [show_key_percent] false
# [expanded] false
# [expand_greatest] false
# [expand_gap] 10
# [show_x_labels] false
# [show_y_labels] false
# [datapoint_font_size] 12
def set_defaults
init_with(
:show_shadow => true,
:shadow_offset => 10,
:show_data_labels => false,
:show_actual_values => false,
:show_percent => true,
:show_key_data_labels => true,
:show_key_actual_values => true,
:show_key_percent => false,
:expanded => false,
:expand_greatest => false,
:expand_gap => 10,
:show_x_labels => false,
:show_y_labels => false,
:datapoint_font_size => 12
)
@data = []
end
# Adds a data set to the graph.
#
# graph.add_data( { :data => [1,2,3,4] } )
#
# Note that the :title is not necessary. If multiple
# data sets are added to the graph, the pie chart will
# display the +sums+ of the data. EG:
#
# graph.add_data( { :data => [1,2,3,4] } )
# graph.add_data( { :data => [2,3,5,9] } )
#
# is the same as:
#
# graph.add_data( { :data => [3,5,8,13] } )
def add_data arg
arg[:data].each_index {|idx|
@data[idx] = 0 unless @data[idx]
@data[idx] += arg[:data][idx]
}
end
# If true, displays a drop shadow for the chart
attr_accessor :show_shadow
# Sets the offset of the shadow from the pie chart
attr_accessor :shadow_offset
# If true, display the data labels on the chart
attr_accessor :show_data_labels
# If true, display the actual field values in the data labels
attr_accessor :show_actual_values
# If true, display the percentage value of each pie wedge in the data
# labels
attr_accessor :show_percent
# If true, display the labels in the key
attr_accessor :show_key_data_labels
# If true, display the actual value of the field in the key
attr_accessor :show_key_actual_values
# If true, display the percentage value of the wedges in the key
attr_accessor :show_key_percent
# If true, "explode" the pie (put space between the wedges)
attr_accessor :expanded
# If true, expand the largest pie wedge
attr_accessor :expand_greatest
# The amount of space between expanded wedges
attr_accessor :expand_gap
# The font size of the data point labels
attr_accessor :datapoint_font_size
protected
def add_defs defs
gradient = defs.add_element( "filter", {
"id"=>"dropshadow",
"width" => "1.2",
"height" => "1.2",
} )
gradient.add_element( "feGaussianBlur", {
"stdDeviation" => "4",
"result" => "blur"
})
end
# We don't need the graph
def draw_graph
end
def get_y_labels
[""]
end
def get_x_labels
[""]
end
def keys
total = 0
max_value = 0
@data.each {|x| total += x }
percent_scale = 100.0 / total
count = -1
a = @config[:fields].collect{ |x|
count += 1
v = @data[count]
perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : ""
x + " [" + v.to_s + "]" + perc
}
end
RADIANS = Math::PI/180
def draw_data
@graph = @root.add_element( "g" )
background = @graph.add_element("g")
midground = @graph.add_element("g")
diameter = @graph_height > @graph_width ? @graph_width : @graph_height
diameter -= expand_gap if expanded or expand_greatest
diameter -= datapoint_font_size if show_data_labels
diameter -= 10 if show_shadow
radius = diameter / 2.0
xoff = (width - diameter) / 2
yoff = (height - @border_bottom - diameter)
yoff -= 10 if show_shadow
@graph.attributes['transform'] = "translate( #{xoff} #{yoff} )"
wedge_text_pad = 5
wedge_text_pad = 20 if show_percent and show_data_labels
total = 0
max_value = 0
@data.each {|x|
max_value = max_value < x ? x : max_value
total += x
}
percent_scale = 100.0 / total
prev_percent = 0
rad_mult = 3.6 * RADIANS
@config[:fields].each_index { |count|
value = @data[count]
percent = percent_scale * value
radians = prev_percent * rad_mult
x_start = radius+(Math.sin(radians) * radius)
y_start = radius-(Math.cos(radians) * radius)
radians = (prev_percent+percent) * rad_mult
x_end = radius+(Math.sin(radians) * radius)
y_end = radius-(Math.cos(radians) * radius)
path = "M#{radius},#{radius} L#{x_start},#{y_start} "+
"A#{radius},#{radius} "+
"0, #{percent >= 50 ? '1' : '0'},1, "+
"#{x_end} #{y_end} Z"
wedge = @foreground.add_element( "path", {
"d" => path,
"class" => "fill#{count+1}"
})
translate = nil
tx = 0
ty = 0
half_percent = prev_percent + percent / 2
radians = half_percent * rad_mult
if show_shadow
shadow = background.add_element( "path", {
"d" => path,
"filter" => "url(#dropshadow)",
"style" => "fill: #ccc; stroke: none;"
})
clear = midground.add_element( "path", {
"d" => path,
"style" => "fill: #fff; stroke: none;"
})
end
if expanded or (expand_greatest && value == max_value)
tx = (Math.sin(radians) * expand_gap)
ty = -(Math.cos(radians) * expand_gap)
translate = "translate( #{tx} #{ty} )"
wedge.attributes["transform"] = translate
clear.attributes["transform"] = translate
end
if show_shadow
shadow.attributes["transform"] =
"translate( #{tx+shadow_offset} #{ty+shadow_offset} )"
end
if show_data_labels and value != 0
label = ""
label += @config[:fields][count] if show_key_data_labels
label += " ["+value.to_s+"]" if show_actual_values
label += " "+percent.round.to_s+"%" if show_percent
msr = Math.sin(radians)
mcr = Math.cos(radians)
tx = radius + (msr * radius)
ty = radius -(mcr * radius)
if expanded or (expand_greatest && value == max_value)
tx += (msr * expand_gap)
ty -= (mcr * expand_gap)
end
@foreground.add_element( "text", {
"x" => tx.to_s,
"y" => ty.to_s,
"class" => "dataPointLabel",
"style" => "stroke: #fff; stroke-width: 2;"
}).text = label.to_s
@foreground.add_element( "text", {
"x" => tx.to_s,
"y" => ty.to_s,
"class" => "dataPointLabel",
}).text = label.to_s
end
prev_percent += percent
}
end
def round val, to
up = 10**to.to_f
(val * up).to_i / up
end
def get_css
return <<EOL
.dataPointLabel{
fill: #000000;
text-anchor:middle;
font-size: #{datapoint_font_size}px;
font-family: "Arial", sans-serif;
font-weight: normal;
}
/* key - MUST match fill styles */
.key1,.fill1{
fill: #ff0000;
fill-opacity: 0.7;
stroke: none;
stroke-width: 1px;
}
.key2,.fill2{
fill: #0000ff;
fill-opacity: 0.7;
stroke: none;
stroke-width: 1px;
}
.key3,.fill3{
fill-opacity: 0.7;
fill: #00ff00;
stroke: none;
stroke-width: 1px;
}
.key4,.fill4{
fill-opacity: 0.7;
fill: #ffcc00;
stroke: none;
stroke-width: 1px;
}
.key5,.fill5{
fill-opacity: 0.7;
fill: #00ccff;
stroke: none;
stroke-width: 1px;
}
.key6,.fill6{
fill-opacity: 0.7;
fill: #ff00ff;
stroke: none;
stroke-width: 1px;
}
.key7,.fill7{
fill-opacity: 0.7;
fill: #00ff99;
stroke: none;
stroke-width: 1px;
}
.key8,.fill8{
fill-opacity: 0.7;
fill: #ffff00;
stroke: none;
stroke-width: 1px;
}
.key9,.fill9{
fill-opacity: 0.7;
fill: #cc6666;
stroke: none;
stroke-width: 1px;
}
.key10,.fill10{
fill-opacity: 0.7;
fill: #663399;
stroke: none;
stroke-width: 1px;
}
.key11,.fill11{
fill-opacity: 0.7;
fill: #339900;
stroke: none;
stroke-width: 1px;
}
.key12,.fill12{
fill-opacity: 0.7;
fill: #9966FF;
stroke: none;
stroke-width: 1px;
}
EOL
end
end
end
end

494
lib/SVG/Graph/Plot.rb Normal file
View File

@ -0,0 +1,494 @@
require 'SVG/Graph/Graph'
module SVG
module Graph
# === For creating SVG plots of scalar data
#
# = Synopsis
#
# require 'SVG/Graph/Plot'
#
# # Data sets are x,y pairs
# # Note that multiple data sets can differ in length, and that the
# # data in the datasets needn't be in order; they will be ordered
# # by the plot along the X-axis.
# projection = [
# 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13,
# 7, 9
# ]
# actual = [
# 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12,
# 15, 6, 4, 17, 2, 12
# ]
#
# graph = SVG::Graph::Plot.new({
# :height => 500,
# :width => 300,
# :key => true,
# :scale_x_integers => true,
# :scale_y_integerrs => true,
# })
#
# graph.add_data({
# :data => projection
# :title => 'Projected',
# })
#
# graph.add_data({
# :data => actual,
# :title => 'Actual',
# })
#
# print graph.burn()
#
# = Description
#
# Produces a graph of scalar data.
#
# This object aims to allow you to easily create high quality
# SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the
# default style sheet or supply your own. Either way there are many options
# which can be configured to give you control over how the graph is
# generated - with or without a key, data elements at each point, title,
# subtitle etc.
#
# = Examples
#
# http://www.germane-software/repositories/public/SVG/test/plot.rb
#
# = Notes
#
# The default stylesheet handles upto 10 data sets, if you
# use more you must create your own stylesheet and add the
# additional settings for the extra data sets. You will know
# if you go over 10 data sets as they will have no style and
# be in black.
#
# Unlike the other types of charts, data sets must contain x,y pairs:
#
# [ 1, 2 ] # A data set with 1 point: (1,2)
# [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
#
# = See also
#
# * SVG::Graph::Graph
# * SVG::Graph::BarHorizontal
# * SVG::Graph::Bar
# * SVG::Graph::Line
# * SVG::Graph::Pie
# * SVG::Graph::TimeSeries
#
# == Author
#
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
#
# Copyright 2004 Sean E. Russell
# This software is available under the Ruby license[LICENSE.txt]
#
class Plot < Graph
# In addition to the defaults set by Graph::initialize, sets
# [show_data_points] true
# [area_fill] false
# [stacked] false
def set_defaults
init_with(
:show_data_points => true,
:area_fill => false,
:stacked => false
)
self.top_align = self.right_align = self.top_font = self.right_font = 1
end
# Determines the scaling for the X axis divisions.
#
# graph.scale_x_divisions = 2
#
# would cause the graph to attempt to generate labels stepped by 2; EG:
# 0,2,4,6,8...
attr_accessor :scale_x_divisions
# Determines the scaling for the Y axis divisions.
#
# graph.scale_y_divisions = 0.5
#
# would cause the graph to attempt to generate labels stepped by 0.5; EG:
# 0, 0.5, 1, 1.5, 2, ...
attr_accessor :scale_y_divisions
# Make the X axis labels integers
attr_accessor :scale_x_integers
# Make the Y axis labels integers
attr_accessor :scale_y_integers
# Fill the area under the line
attr_accessor :area_fill
# Show a small circle on the graph where the line
# goes from one point to the next.
attr_accessor :show_data_points
# Set the minimum value of the X axis
attr_accessor :min_x_value
# Set the minimum value of the Y axis
attr_accessor :min_y_value
# Adds data to the plot. The data must be in X,Y pairs; EG
# [ 1, 2 ] # A data set with 1 point: (1,2)
# [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
def add_data data
@data = [] unless @data
raise "No data provided by #{conf.inspect}" unless data[:data] and
data[:data].kind_of? Array
raise "Data supplied must be x,y pairs! "+
"The data provided contained an odd set of "+
"data points" unless data[:data].length % 2 == 0
return if data[:data].length == 0
x = []
y = []
data[:data].each_index {|i|
(i%2 == 0 ? x : y) << data[:data][i]
}
sort( x, y )
data[:data] = [x,y]
@data << data
end
protected
def keys
@data.collect{ |x| x[:title] }
end
def calculate_left_margin
super
label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6
@border_left = label_left if label_left > @border_left
end
def calculate_right_margin
super
label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6
@border_right = label_right if label_right > @border_right
end
X = 0
Y = 1
def x_range
max_value = @data.collect{|x| x[:data][X][-1] }.max
min_value = @data.collect{|x| x[:data][X][0] }.min
min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
range = max_value - min_value
right_pad = range == 0 ? 10 : range / 20.0
scale_range = (max_value + right_pad) - min_value
scale_division = scale_x_divisions || (scale_range / 10.0)
if scale_x_integers
scale_division = scale_division < 1 ? 1 : scale_division.round
end
[min_value, max_value, scale_division]
end
def get_x_values
min_value, max_value, scale_division = x_range
rv = []
min_value.step( max_value, scale_division ) {|v| rv << v}
return rv
end
alias :get_x_labels :get_x_values
def field_width
values = get_x_values
max = @data.collect{|x| x[:data][X][-1]}.max
dx = (max - values[-1]).to_f / (values[-1] - values[-2])
(@graph_width.to_f - font_size*2*right_font) /
(values.length + dx - right_align)
end
def y_range
max_value = @data.collect{|x| x[:data][Y].max }.max
min_value = @data.collect{|x| x[:data][Y].min }.min
min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
range = max_value - min_value
top_pad = range == 0 ? 10 : range / 20.0
scale_range = (max_value + top_pad) - min_value
scale_division = scale_y_divisions || (scale_range / 10.0)
if scale_y_integers
scale_division = scale_division < 1 ? 1 : scale_division.round
end
return [min_value, max_value, scale_division]
end
def get_y_values
min_value, max_value, scale_division = y_range
rv = []
min_value.step( max_value, scale_division ) {|v| rv << v}
return rv
end
alias :get_y_labels :get_y_values
def field_height
values = get_y_values
max = @data.collect{|x| x[:data][Y].max }.max
dx = (max - values[-1]).to_f / (values[-1] - values[-2])
(@graph_height.to_f - font_size*2*top_font) /
(values.length + dx - top_align)
end
def draw_data
line = 1
x_min, x_max, x_div = x_range
y_min, y_max, y_div = y_range
x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min)
for data in @data
x_points = data[:data][X]
y_points = data[:data][Y]
lpath = "L"
x_start = 0
y_start = 0
x_points.each_index { |idx|
x = (x_points[idx] - x_min) * x_step
y = @graph_height - (y_points[idx] - y_min) * y_step
x_start, y_start = x,y if idx == 0
lpath << "#{x} #{y} "
}
if area_fill
@graph.add_element( "path", {
"d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z",
"class" => "fill#{line}"
})
end
@graph.add_element( "path", {
"d" => "M#{x_start} #{y_start} #{lpath}",
"class" => "line#{line}"
})
if show_data_points || show_data_values
x_points.each_index { |idx|
x = (x_points[idx] - x_min) * x_step
y = @graph_height - (y_points[idx] - y_min) * y_step
if show_data_points
@graph.add_element( "circle", {
"cx" => x.to_s,
"cy" => y.to_s,
"r" => "2.5",
"class" => "dataPoint#{line}"
})
add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
end
make_datapoint_text( x, y-6, y_points[idx] )
}
end
line += 1
end
end
def format x, y
"(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
end
def get_css
return <<EOL
/* default line styles */
.line1{
fill: none;
stroke: #ff0000;
stroke-width: 1px;
}
.line2{
fill: none;
stroke: #0000ff;
stroke-width: 1px;
}
.line3{
fill: none;
stroke: #00ff00;
stroke-width: 1px;
}
.line4{
fill: none;
stroke: #ffcc00;
stroke-width: 1px;
}
.line5{
fill: none;
stroke: #00ccff;
stroke-width: 1px;
}
.line6{
fill: none;
stroke: #ff00ff;
stroke-width: 1px;
}
.line7{
fill: none;
stroke: #00ffff;
stroke-width: 1px;
}
.line8{
fill: none;
stroke: #ffff00;
stroke-width: 1px;
}
.line9{
fill: none;
stroke: #ccc6666;
stroke-width: 1px;
}
.line10{
fill: none;
stroke: #663399;
stroke-width: 1px;
}
.line11{
fill: none;
stroke: #339900;
stroke-width: 1px;
}
.line12{
fill: none;
stroke: #9966FF;
stroke-width: 1px;
}
/* default fill styles */
.fill1{
fill: #cc0000;
fill-opacity: 0.2;
stroke: none;
}
.fill2{
fill: #0000cc;
fill-opacity: 0.2;
stroke: none;
}
.fill3{
fill: #00cc00;
fill-opacity: 0.2;
stroke: none;
}
.fill4{
fill: #ffcc00;
fill-opacity: 0.2;
stroke: none;
}
.fill5{
fill: #00ccff;
fill-opacity: 0.2;
stroke: none;
}
.fill6{
fill: #ff00ff;
fill-opacity: 0.2;
stroke: none;
}
.fill7{
fill: #00ffff;
fill-opacity: 0.2;
stroke: none;
}
.fill8{
fill: #ffff00;
fill-opacity: 0.2;
stroke: none;
}
.fill9{
fill: #cc6666;
fill-opacity: 0.2;
stroke: none;
}
.fill10{
fill: #663399;
fill-opacity: 0.2;
stroke: none;
}
.fill11{
fill: #339900;
fill-opacity: 0.2;
stroke: none;
}
.fill12{
fill: #9966FF;
fill-opacity: 0.2;
stroke: none;
}
/* default line styles */
.key1,.dataPoint1{
fill: #ff0000;
stroke: none;
stroke-width: 1px;
}
.key2,.dataPoint2{
fill: #0000ff;
stroke: none;
stroke-width: 1px;
}
.key3,.dataPoint3{
fill: #00ff00;
stroke: none;
stroke-width: 1px;
}
.key4,.dataPoint4{
fill: #ffcc00;
stroke: none;
stroke-width: 1px;
}
.key5,.dataPoint5{
fill: #00ccff;
stroke: none;
stroke-width: 1px;
}
.key6,.dataPoint6{
fill: #ff00ff;
stroke: none;
stroke-width: 1px;
}
.key7,.dataPoint7{
fill: #00ffff;
stroke: none;
stroke-width: 1px;
}
.key8,.dataPoint8{
fill: #ffff00;
stroke: none;
stroke-width: 1px;
}
.key9,.dataPoint9{
fill: #cc6666;
stroke: none;
stroke-width: 1px;
}
.key10,.dataPoint10{
fill: #663399;
stroke: none;
stroke-width: 1px;
}
.key11,.dataPoint11{
fill: #339900;
stroke: none;
stroke-width: 1px;
}
.key12,.dataPoint12{
fill: #9966FF;
stroke: none;
stroke-width: 1px;
}
EOL
end
end
end
end

373
lib/SVG/Graph/Schedule.rb Normal file
View File

@ -0,0 +1,373 @@
require 'SVG/Graph/Plot'
require 'parsedate'
module SVG
module Graph
# === For creating SVG plots of scalar temporal data
#
# = Synopsis
#
# require 'SVG/Graph/Schedule'
#
# # Data sets are label, start, end tripples.
# data1 = [
# "Housesitting", "6/17/04", "6/19/04",
# "Summer Session", "6/15/04", "8/15/04",
# ]
#
# graph = SVG::Graph::Schedule.new( {
# :width => 640,
# :height => 480,
# :graph_title => title,
# :show_graph_title => true,
# :no_css => true,
# :scale_x_integers => true,
# :scale_y_integers => true,
# :min_x_value => 0,
# :min_y_value => 0,
# :show_data_labels => true,
# :show_x_guidelines => true,
# :show_x_title => true,
# :x_title => "Time",
# :stagger_x_labels => true,
# :stagger_y_labels => true,
# :x_label_format => "%m/%d/%y",
# })
#
# graph.add_data({
# :data => data1,
# :title => 'Data',
# })
#
# print graph.burn()
#
# = Description
#
# Produces a graph of temporal scalar data.
#
# = Examples
#
# http://www.germane-software/repositories/public/SVG/test/schedule.rb
#
# = Notes
#
# The default stylesheet handles upto 10 data sets, if you
# use more you must create your own stylesheet and add the
# additional settings for the extra data sets. You will know
# if you go over 10 data sets as they will have no style and
# be in black.
#
# Note that multiple data sets within the same chart can differ in
# length, and that the data in the datasets needn't be in order;
# they will be ordered by the plot along the X-axis.
#
# The dates must be parseable by ParseDate, but otherwise can be
# any order of magnitude (seconds within the hour, or years)
#
# = See also
#
# * SVG::Graph::Graph
# * SVG::Graph::BarHorizontal
# * SVG::Graph::Bar
# * SVG::Graph::Line
# * SVG::Graph::Pie
# * SVG::Graph::Plot
# * SVG::Graph::TimeSeries
#
# == Author
#
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
#
# Copyright 2004 Sean E. Russell
# This software is available under the Ruby license[LICENSE.txt]
#
class Schedule < Graph
# In addition to the defaults set by Graph::initialize and
# Plot::set_defaults, sets:
# [x_label_format] '%Y-%m-%d %H:%M:%S'
# [popup_format] '%Y-%m-%d %H:%M:%S'
def set_defaults
init_with(
:x_label_format => '%Y-%m-%d %H:%M:%S',
:popup_format => '%Y-%m-%d %H:%M:%S',
:scale_x_divisions => false,
:scale_x_integers => false,
:bar_gap => true
)
end
# The format string use do format the X axis labels.
# See Time::strformat
attr_accessor :x_label_format
# Use this to set the spacing between dates on the axis. The value
# must be of the form
# "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
#
# EG:
#
# graph.timescale_divisions = "2 weeks"
#
# will cause the chart to try to divide the X axis up into segments of
# two week periods.
attr_accessor :timescale_divisions
# The formatting used for the popups. See x_label_format
attr_accessor :popup_format
attr_accessor :min_x_value
attr_accessor :scale_x_divisions
attr_accessor :scale_x_integers
attr_accessor :bar_gap
# Add data to the plot.
#
# # A data set with 1 point: Lunch from 12:30 to 14:00
# d1 = [ "Lunch", "12:30", "14:00" ]
# # A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and
# # "Henry V" runs from 6/12/03 to 8/20/03
# d2 = [ "Cats", "5/11/03", "7/15/04",
# "Henry V", "6/12/03", "8/20/03" ]
#
# graph.add_data(
# :data => d1,
# :title => 'Meetings'
# )
# graph.add_data(
# :data => d2,
# :title => 'Plays'
# )
#
# Note that the data must be in time,value pairs, and that the date format
# may be any date that is parseable by ParseDate.
# Also note that, in this example, we're mixing scales; the data from d1
# will probably not be discernable if both data sets are plotted on the same
# graph, since d1 is too granular.
def add_data data
@data = [] unless @data
raise "No data provided by #{conf.inspect}" unless data[:data] and
data[:data].kind_of? Array
raise "Data supplied must be title,from,to tripples! "+
"The data provided contained an odd set of "+
"data points" unless data[:data].length % 3 == 0
return if data[:data].length == 0
y = []
x_start = []
x_end = []
data[:data].each_index {|i|
im3 = i%3
if im3 == 0
y << data[:data][i]
else
arr = ParseDate.parsedate( data[:data][i] )
t = Time.local( *arr[0,6].compact )
(im3 == 1 ? x_start : x_end) << t.to_i
end
}
sort( x_start, x_end, y )
@data = [x_start, x_end, y ]
end
protected
def min_x_value=(value)
arr = ParseDate.parsedate( value )
@min_x_value = Time.local( *arr[0,6].compact ).to_i
end
def format x, y
Time.at( x ).strftime( popup_format )
end
def get_x_labels
rv = get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
end
def y_label_offset( height )
height / -2.0
end
def get_y_labels
@data[2]
end
def draw_data
fieldheight = field_height
fieldwidth = field_width
bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
subbar_height = fieldheight - bargap
field_count = 1
y_mod = (subbar_height / 2) + (font_size / 2)
min,max,div = x_range
scale = (@graph_width.to_f - font_size*2) / (max-min)
@data[0].each_index { |i|
x_start = @data[0][i]
x_end = @data[1][i]
y = @graph_height - (fieldheight * field_count)
bar_width = (x_end-x_start) * scale
bar_start = x_start * scale - (min * scale)
@graph.add_element( "rect", {
"x" => bar_start.to_s,
"y" => y.to_s,
"width" => bar_width.to_s,
"height" => subbar_height.to_s,
"class" => "fill#{field_count+1}"
})
field_count += 1
}
end
def get_css
return <<EOL
/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
.key1,.fill1{
fill: #ff0000;
fill-opacity: 0.5;
stroke: none;
stroke-width: 0.5px;
}
.key2,.fill2{
fill: #0000ff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key3,.fill3{
fill: #00ff00;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key4,.fill4{
fill: #ffcc00;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key5,.fill5{
fill: #00ccff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key6,.fill6{
fill: #ff00ff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key7,.fill7{
fill: #00ffff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key8,.fill8{
fill: #ffff00;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key9,.fill9{
fill: #cc6666;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key10,.fill10{
fill: #663399;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key11,.fill11{
fill: #339900;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key12,.fill12{
fill: #9966FF;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
EOL
end
private
def x_range
max_value = [ @data[0][-1], @data[1].max ].max
min_value = [ @data[0][0], @data[1].min ].min
min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
range = max_value - min_value
right_pad = range == 0 ? 10 : range / 20.0
scale_range = (max_value + right_pad) - min_value
scale_division = scale_x_divisions || (scale_range / 10.0)
if scale_x_integers
scale_division = scale_division < 1 ? 1 : scale_division.round
end
[min_value, max_value, scale_division]
end
def get_x_values
rv = []
min, max, scale_division = x_range
if timescale_divisions
timescale_divisions =~ /(\d+) ?(days|weeks|months|years|hours|minutes|seconds)?/
division_units = $2 ? $2 : "days"
amount = $1.to_i
if amount
step = nil
case division_units
when "months"
cur = min
while cur < max
rv << cur
arr = Time.at( cur ).to_a
arr[4] += amount
if arr[4] > 12
arr[5] += (arr[4] / 12).to_i
arr[4] = (arr[4] % 12)
end
cur = Time.local(*arr).to_i
end
when "years"
cur = min
while cur < max
rv << cur
arr = Time.at( cur ).to_a
arr[5] += amount
cur = Time.local(*arr).to_i
end
when "weeks"
step = 7 * 24 * 60 * 60 * amount
when "days"
step = 24 * 60 * 60 * amount
when "hours"
step = 60 * 60 * amount
when "minutes"
step = 60 * amount
when "seconds"
step = amount
end
min.step( max, step ) {|v| rv << v} if step
return rv
end
end
min.step( max, scale_division ) {|v| rv << v}
return rv
end
end
end
end

241
lib/SVG/Graph/TimeSeries.rb Normal file
View File

@ -0,0 +1,241 @@
require 'SVG/Graph/Plot'
require 'parsedate'
module SVG
module Graph
# === For creating SVG plots of scalar temporal data
#
# = Synopsis
#
# require 'SVG/Graph/TimeSeriess'
#
# # Data sets are x,y pairs
# data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11,
# "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13]
# data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4,
# "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6,
# "5/1/84", 17, "10/1/80", 12]
#
# graph = SVG::Graph::TimeSeries.new( {
# :width => 640,
# :height => 480,
# :graph_title => title,
# :show_graph_title => true,
# :no_css => true,
# :key => true,
# :scale_x_integers => true,
# :scale_y_integers => true,
# :min_x_value => 0,
# :min_y_value => 0,
# :show_data_labels => true,
# :show_x_guidelines => true,
# :show_x_title => true,
# :x_title => "Time",
# :show_y_title => true,
# :y_title => "Ice Cream Cones",
# :y_title_text_direction => :bt,
# :stagger_x_labels => true,
# :x_label_format => "%m/%d/%y",
# })
#
# graph.add_data({
# :data => projection
# :title => 'Projected',
# })
#
# graph.add_data({
# :data => actual,
# :title => 'Actual',
# })
#
# print graph.burn()
#
# = Description
#
# Produces a graph of temporal scalar data.
#
# = Examples
#
# http://www.germane-software/repositories/public/SVG/test/timeseries.rb
#
# = Notes
#
# The default stylesheet handles upto 10 data sets, if you
# use more you must create your own stylesheet and add the
# additional settings for the extra data sets. You will know
# if you go over 10 data sets as they will have no style and
# be in black.
#
# Unlike the other types of charts, data sets must contain x,y pairs:
#
# [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
# [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
# # ("14:20",6)
#
# Note that multiple data sets within the same chart can differ in length,
# and that the data in the datasets needn't be in order; they will be ordered
# by the plot along the X-axis.
#
# The dates must be parseable by ParseDate, but otherwise can be
# any order of magnitude (seconds within the hour, or years)
#
# = See also
#
# * SVG::Graph::Graph
# * SVG::Graph::BarHorizontal
# * SVG::Graph::Bar
# * SVG::Graph::Line
# * SVG::Graph::Pie
# * SVG::Graph::Plot
#
# == Author
#
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
#
# Copyright 2004 Sean E. Russell
# This software is available under the Ruby license[LICENSE.txt]
#
class TimeSeries < Plot
# In addition to the defaults set by Graph::initialize and
# Plot::set_defaults, sets:
# [x_label_format] '%Y-%m-%d %H:%M:%S'
# [popup_format] '%Y-%m-%d %H:%M:%S'
def set_defaults
super
init_with(
#:max_time_span => '',
:x_label_format => '%Y-%m-%d %H:%M:%S',
:popup_format => '%Y-%m-%d %H:%M:%S'
)
end
# The format string use do format the X axis labels.
# See Time::strformat
attr_accessor :x_label_format
# Use this to set the spacing between dates on the axis. The value
# must be of the form
# "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
#
# EG:
#
# graph.timescale_divisions = "2 weeks"
#
# will cause the chart to try to divide the X axis up into segments of
# two week periods.
attr_accessor :timescale_divisions
# The formatting used for the popups. See x_label_format
attr_accessor :popup_format
# Add data to the plot.
#
# d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
# d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
# # ("14:20",6)
# graph.add_data(
# :data => d1,
# :title => 'One'
# )
# graph.add_data(
# :data => d2,
# :title => 'Two'
# )
#
# Note that the data must be in time,value pairs, and that the date format
# may be any date that is parseable by ParseDate.
def add_data data
@data = [] unless @data
raise "No data provided by #{conf.inspect}" unless data[:data] and
data[:data].kind_of? Array
raise "Data supplied must be x,y pairs! "+
"The data provided contained an odd set of "+
"data points" unless data[:data].length % 2 == 0
return if data[:data].length == 0
x = []
y = []
data[:data].each_index {|i|
if i%2 == 0
arr = ParseDate.parsedate( data[:data][i] )
t = Time.local( *arr[0,6].compact )
x << t.to_i
else
y << data[:data][i]
end
}
sort( x, y )
data[:data] = [x,y]
@data << data
end
protected
def min_x_value=(value)
arr = ParseDate.parsedate( value )
@min_x_value = Time.local( *arr[0,6].compact ).to_i
end
def format x, y
Time.at( x ).strftime( popup_format )
end
def get_x_labels
get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
end
private
def get_x_values
rv = []
min, max, scale_division = x_range
if timescale_divisions
timescale_divisions =~ /(\d+) ?(days|weeks|months|years|hours|minutes|seconds)?/
division_units = $2 ? $2 : "days"
amount = $1.to_i
if amount
step = nil
case division_units
when "months"
cur = min
while cur < max
rv << cur
arr = Time.at( cur ).to_a
arr[4] += amount
if arr[4] > 12
arr[5] += (arr[4] / 12).to_i
arr[4] = (arr[4] % 12)
end
cur = Time.local(*arr).to_i
end
when "years"
cur = min
while cur < max
rv << cur
arr = Time.at( cur ).to_a
arr[5] += amount
cur = Time.local(*arr).to_i
end
when "weeks"
step = 7 * 24 * 60 * 60 * amount
when "days"
step = 24 * 60 * 60 * amount
when "hours"
step = 60 * 60 * amount
when "minutes"
step = 60 * amount
when "seconds"
step = amount
end
min.step( max, step ) {|v| rv << v} if step
return rv
end
end
min.step( max, scale_division ) {|v| rv << v}
return rv
end
end
end
end

57
lib/SVG/LICENSE.txt Normal file
View File

@ -0,0 +1,57 @@
SVG::Graph is copyrighted free software by Sean Russell <ser@germane-software.com>.
You can redistribute it and/or modify it under either the terms of the GPL
(see GPL.txt file), or the conditions below:
1. You may make and give away verbatim copies of the source form of the
software without restriction, provided that you duplicate all of the
original copyright notices and associated disclaimers.
2. You may modify your copy of the software in any way, provided that
you do at least ONE of the following:
a) place your modifications in the Public Domain or otherwise
make them Freely Available, such as by posting said
modifications to Usenet or an equivalent medium, or by allowing
the author to include your modifications in the software.
b) use the modified software only within your corporation or
organization.
c) rename any non-standard executables so the names do not conflict
with standard executables, which must also be provided.
d) make other distribution arrangements with the author.
3. You may distribute the software in object code or executable
form, provided that you do at least ONE of the following:
a) distribute the executables and library files of the software,
together with instructions (in the manual page or equivalent)
on where to get the original distribution.
b) accompany the distribution with the machine-readable source of
the software.
c) give non-standard executables non-standard names, with
instructions on where to get the original software distribution.
d) make other distribution arrangements with the author.
4. You may modify and include the part of the software into any other
software (possibly commercial). But some files in the distribution
are not written by the author, so that they are not under this terms.
All files of this sort are located under the contrib/ directory.
See each file for the copying condition.
5. The scripts and library files supplied as input to or produced as
output from the software do not automatically fall under the
copyright of the software, but belong to whomever generated them,
and may be sold commercially, and may be aggregated with this
software.
6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.

BIN
public/images/stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -156,6 +156,7 @@ vertical-align: middle;
.icon-history { background-image: url(../images/history.png); } .icon-history { background-image: url(../images/history.png); }
.icon-feed { background-image: url(../images/feed.png); } .icon-feed { background-image: url(../images/feed.png); }
.icon-time { background-image: url(../images/time.png); } .icon-time { background-image: url(../images/time.png); }
.icon-stats { background-image: url(../images/stats.png); }
.icon22-projects { background-image: url(../images/22x22/projects.png); } .icon22-projects { background-image: url(../images/22x22/projects.png); }
.icon22-users { background-image: url(../images/22x22/users.png); } .icon22-users { background-image: url(../images/22x22/users.png); }