##// END OF EJS Templates
Gantt chart can now be exported to a graphic file (png)....
Jean-Philippe Lang -
r660:edba1f692b5a
parent child
Show More
@@ -38,6 +38,7 class ProjectsController < ApplicationController
38 include QueriesHelper
38 include QueriesHelper
39 helper :repositories
39 helper :repositories
40 include RepositoriesHelper
40 include RepositoriesHelper
41 include ProjectsHelper
41
42
42 def index
43 def index
43 list
44 list
@@ -614,10 +615,14 class ProjectsController < ApplicationController
614 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
615 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
615 @events.sort! {|x,y| x.start_date <=> y.start_date }
616 @events.sort! {|x,y| x.start_date <=> y.start_date }
616
617
617 if params[:output]=='pdf'
618 if params[:format]=='pdf'
618 @options_for_rfpdf ||= {}
619 @options_for_rfpdf ||= {}
619 @options_for_rfpdf[:file_name] = "gantt.pdf"
620 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
620 render :template => "projects/gantt.rfpdf", :layout => false
621 render :template => "projects/gantt.rfpdf", :layout => false
622 elsif params[:format]=='png' && respond_to?('gantt_image')
623 image = gantt_image(@events, @date_from, @months, @zoom)
624 image.format = 'PNG'
625 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
621 else
626 else
622 render :template => "projects/gantt.rhtml"
627 render :template => "projects/gantt.rhtml"
623 end
628 end
@@ -25,4 +25,154 module ProjectsHelper
25 :anchor => version.name
25 :anchor => version.name
26 }, options
26 }, options
27 end
27 end
28
29 # Generates a gantt image
30 # Only defined if RMagick is avalaible
31 def gantt_image(events, date_from, months, zoom)
32 date_to = (date_from >> months)-1
33 show_weeks = zoom > 1
34 show_days = zoom > 2
35
36 subject_width = 320
37 header_heigth = 18
38 # width of one day in pixels
39 zoom = zoom*2
40 g_width = (date_to - date_from + 1)*zoom
41 g_height = 20 * events.length + 20
42 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
43 height = g_height + headers_heigth
44
45 imgl = Magick::ImageList.new
46 imgl.new_image(subject_width+g_width+1, height)
47 gc = Magick::Draw.new
48
49 # Subjects
50 top = headers_heigth + 20
51 gc.fill('black')
52 gc.stroke('transparent')
53 gc.stroke_width(1)
54 events.each do |i|
55 gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name))
56 top = top + 20
57 end
58
59 # Months headers
60 month_f = date_from
61 left = subject_width
62 months.times do
63 width = ((month_f >> 1) - month_f) * zoom
64 gc.fill('white')
65 gc.stroke('grey')
66 gc.stroke_width(1)
67 gc.rectangle(left, 0, left + width, height)
68 gc.fill('black')
69 gc.stroke('transparent')
70 gc.stroke_width(1)
71 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
72 left = left + width
73 month_f = month_f >> 1
74 end
75
76 # Weeks headers
77 if show_weeks
78 left = subject_width
79 height = header_heigth
80 if date_from.cwday == 1
81 # date_from is monday
82 week_f = date_from
83 else
84 # find next monday after date_from
85 week_f = date_from + (7 - date_from.cwday + 1)
86 width = (7 - date_from.cwday + 1) * zoom
87 gc.fill('white')
88 gc.stroke('grey')
89 gc.stroke_width(1)
90 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
91 left = left + width
92 end
93 while week_f <= date_to
94 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
95 gc.fill('white')
96 gc.stroke('grey')
97 gc.stroke_width(1)
98 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
99 gc.fill('black')
100 gc.stroke('transparent')
101 gc.stroke_width(1)
102 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
103 left = left + width
104 week_f = week_f+7
105 end
106 end
107
108 # Days details (week-end in grey)
109 if show_days
110 left = subject_width
111 height = g_height + header_heigth - 1
112 wday = date_from.cwday
113 (date_to - date_from + 1).to_i.times do
114 width = zoom
115 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
116 gc.stroke('grey')
117 gc.stroke_width(1)
118 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
119 left = left + width
120 wday = wday + 1
121 wday = 1 if wday > 7
122 end
123 end
124
125 # border
126 gc.fill('transparent')
127 gc.stroke('grey')
128 gc.stroke_width(1)
129 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
130 gc.stroke('black')
131 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
132
133 # content
134 top = headers_heigth + 20
135 gc.stroke('transparent')
136 events.each do |i|
137 if i.is_a?(Issue)
138 i_start_date = (i.start_date >= date_from ? i.start_date : date_from )
139 i_end_date = (i.due_date <= date_to ? i.due_date : date_to )
140 i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
141 i_done_date = (i_done_date <= date_from ? date_from : i_done_date )
142 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
143 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
144
145 i_left = subject_width + ((i_start_date - date_from)*zoom).floor
146 i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
147 d_width = ((i_done_date - i_start_date)*zoom).floor # done width
148 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
149
150 gc.fill('grey')
151 gc.rectangle(i_left, top, i_left + i_width, top - 6)
152 gc.fill('red')
153 gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
154 gc.fill('blue')
155 gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
156 gc.fill('black')
157 gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
158 else
159 i_left = subject_width + ((i.start_date - date_from)*zoom).floor
160 gc.fill('green')
161 gc.rectangle(i_left, top, i_left + 6, top - 6)
162 gc.fill('black')
163 gc.text(i_left + 11, top + 1, i.name)
164 end
165 top = top + 20
166 end
167
168 # today red line
169 if Date.today >= @date_from and Date.today <= @date_to
170 gc.stroke('red')
171 x = (Date.today-@date_from+1)*zoom + subject_width
172 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
173 end
174
175 gc.draw(imgl)
176 imgl
177 end if Object.const_defined?(:Magick)
28 end
178 end
@@ -22,10 +22,10 g_height = [(20 * @events.length + 6)+150, 206].max
22 t_height = g_height + headers_height
22 t_height = g_height + headers_height
23 %>
23 %>
24
24
25 <% cache(:year => @year_from, :month => @month_from, :months => @months, :zoom => @zoom, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %>
26 <div class="contextual">
25 <div class="contextual">
27 <%= l(:label_export_to) %>
26 <%= l(:label_export_to) %>
28 <%= link_to 'PDF', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :output => 'pdf'}, :class => 'icon icon-pdf' %>
27 <%= link_to 'PDF', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'pdf'}, :class => 'icon icon-pdf' %>
28 <%= link_to 'PNG', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'png'}, :class => 'icon icon-image' if respond_to?('gantt_image') %>
29 </div>
29 </div>
30
30
31 <h2><%= l(:label_gantt) %></h2>
31 <h2><%= l(:label_gantt) %></h2>
@@ -72,6 +72,8 t_height = g_height + headers_height
72 </table>
72 </table>
73 <% end %>
73 <% end %>
74
74
75 <% cache(:year => @year_from, :month => @month_from, :months => @months, :zoom => @zoom, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %>
76
75 <table width="100%" style="border:0; border-collapse: collapse;">
77 <table width="100%" style="border:0; border-collapse: collapse;">
76 <tr>
78 <tr>
77 <td style="width:<%= subject_width %>px;">
79 <td style="width:<%= subject_width %>px;">
@@ -2,4 +2,10 require 'redmine/version'
2 require 'redmine/mime_type'
2 require 'redmine/mime_type'
3 require 'redmine/acts_as_watchable/init'
3 require 'redmine/acts_as_watchable/init'
4
4
5 begin
6 require_library_or_gem 'rmagick' unless Object.const_defined?(:Magick)
7 rescue LoadError
8 # RMagick is not available
9 end
10
5 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs )
11 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs )
@@ -142,6 +142,7 vertical-align: middle;
142 .icon-pdf { background-image: url(../images/pdf.png); }
142 .icon-pdf { background-image: url(../images/pdf.png); }
143 .icon-csv { background-image: url(../images/csv.png); }
143 .icon-csv { background-image: url(../images/csv.png); }
144 .icon-html { background-image: url(../images/html.png); }
144 .icon-html { background-image: url(../images/html.png); }
145 .icon-image { background-image: url(../images/image.png); }
145 .icon-txt { background-image: url(../images/txt.png); }
146 .icon-txt { background-image: url(../images/txt.png); }
146 .icon-file { background-image: url(../images/file.png); }
147 .icon-file { background-image: url(../images/file.png); }
147 .icon-folder { background-image: url(../images/folder.png); }
148 .icon-folder { background-image: url(../images/folder.png); }
General Comments 0
You need to be logged in to leave comments. Login now