annotate .svn/pristine/b7/b714455dd810b3c0cdf70f4f3c6401f3cc46d1f3.svn-base @ 1628:9c5f8e24dadc live tip

Quieten this cron script
author Chris Cannam
date Tue, 25 Aug 2020 11:38:49 +0100
parents dffacf8a6908
children
rev   line source
Chris@1517 1 # Redmine - project management software
Chris@1517 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
Chris@1517 3 #
Chris@1517 4 # This program is free software; you can redistribute it and/or
Chris@1517 5 # modify it under the terms of the GNU General Public License
Chris@1517 6 # as published by the Free Software Foundation; either version 2
Chris@1517 7 # of the License, or (at your option) any later version.
Chris@1517 8 #
Chris@1517 9 # This program is distributed in the hope that it will be useful,
Chris@1517 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@1517 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@1517 12 # GNU General Public License for more details.
Chris@1517 13 #
Chris@1517 14 # You should have received a copy of the GNU General Public License
Chris@1517 15 # along with this program; if not, write to the Free Software
Chris@1517 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@1517 17
Chris@1517 18 module Redmine
Chris@1517 19 module Helpers
Chris@1517 20 # Simple class to handle gantt chart data
Chris@1517 21 class Gantt
Chris@1517 22 include ERB::Util
Chris@1517 23 include Redmine::I18n
Chris@1517 24 include Redmine::Utils::DateCalculation
Chris@1517 25
Chris@1517 26 # Relation types that are rendered
Chris@1517 27 DRAW_TYPES = {
Chris@1517 28 IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' },
Chris@1517 29 IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
Chris@1517 30 }.freeze
Chris@1517 31
Chris@1517 32 # :nodoc:
Chris@1517 33 # Some utility methods for the PDF export
Chris@1517 34 class PDF
Chris@1517 35 MaxCharactorsForSubject = 45
Chris@1517 36 TotalWidth = 280
Chris@1517 37 LeftPaneWidth = 100
Chris@1517 38
Chris@1517 39 def self.right_pane_width
Chris@1517 40 TotalWidth - LeftPaneWidth
Chris@1517 41 end
Chris@1517 42 end
Chris@1517 43
Chris@1517 44 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
Chris@1517 45 attr_accessor :query
Chris@1517 46 attr_accessor :project
Chris@1517 47 attr_accessor :view
Chris@1517 48
Chris@1517 49 def initialize(options={})
Chris@1517 50 options = options.dup
Chris@1517 51 if options[:year] && options[:year].to_i >0
Chris@1517 52 @year_from = options[:year].to_i
Chris@1517 53 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
Chris@1517 54 @month_from = options[:month].to_i
Chris@1517 55 else
Chris@1517 56 @month_from = 1
Chris@1517 57 end
Chris@1517 58 else
Chris@1517 59 @month_from ||= Date.today.month
Chris@1517 60 @year_from ||= Date.today.year
Chris@1517 61 end
Chris@1517 62 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
Chris@1517 63 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
Chris@1517 64 months = (options[:months] || User.current.pref[:gantt_months]).to_i
Chris@1517 65 @months = (months > 0 && months < 25) ? months : 6
Chris@1517 66 # Save gantt parameters as user preference (zoom and months count)
Chris@1517 67 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] ||
Chris@1517 68 @months != User.current.pref[:gantt_months]))
Chris@1517 69 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
Chris@1517 70 User.current.preference.save
Chris@1517 71 end
Chris@1517 72 @date_from = Date.civil(@year_from, @month_from, 1)
Chris@1517 73 @date_to = (@date_from >> @months) - 1
Chris@1517 74 @subjects = ''
Chris@1517 75 @lines = ''
Chris@1517 76 @number_of_rows = nil
Chris@1517 77 @issue_ancestors = []
Chris@1517 78 @truncated = false
Chris@1517 79 if options.has_key?(:max_rows)
Chris@1517 80 @max_rows = options[:max_rows]
Chris@1517 81 else
Chris@1517 82 @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
Chris@1517 83 end
Chris@1517 84 end
Chris@1517 85
Chris@1517 86 def common_params
Chris@1517 87 { :controller => 'gantts', :action => 'show', :project_id => @project }
Chris@1517 88 end
Chris@1517 89
Chris@1517 90 def params
Chris@1517 91 common_params.merge({:zoom => zoom, :year => year_from,
Chris@1517 92 :month => month_from, :months => months})
Chris@1517 93 end
Chris@1517 94
Chris@1517 95 def params_previous
Chris@1517 96 common_params.merge({:year => (date_from << months).year,
Chris@1517 97 :month => (date_from << months).month,
Chris@1517 98 :zoom => zoom, :months => months})
Chris@1517 99 end
Chris@1517 100
Chris@1517 101 def params_next
Chris@1517 102 common_params.merge({:year => (date_from >> months).year,
Chris@1517 103 :month => (date_from >> months).month,
Chris@1517 104 :zoom => zoom, :months => months})
Chris@1517 105 end
Chris@1517 106
Chris@1517 107 # Returns the number of rows that will be rendered on the Gantt chart
Chris@1517 108 def number_of_rows
Chris@1517 109 return @number_of_rows if @number_of_rows
Chris@1517 110 rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
Chris@1517 111 rows > @max_rows ? @max_rows : rows
Chris@1517 112 end
Chris@1517 113
Chris@1517 114 # Returns the number of rows that will be used to list a project on
Chris@1517 115 # the Gantt chart. This will recurse for each subproject.
Chris@1517 116 def number_of_rows_on_project(project)
Chris@1517 117 return 0 unless projects.include?(project)
Chris@1517 118 count = 1
Chris@1517 119 count += project_issues(project).size
Chris@1517 120 count += project_versions(project).size
Chris@1517 121 count
Chris@1517 122 end
Chris@1517 123
Chris@1517 124 # Renders the subjects of the Gantt chart, the left side.
Chris@1517 125 def subjects(options={})
Chris@1517 126 render(options.merge(:only => :subjects)) unless @subjects_rendered
Chris@1517 127 @subjects
Chris@1517 128 end
Chris@1517 129
Chris@1517 130 # Renders the lines of the Gantt chart, the right side
Chris@1517 131 def lines(options={})
Chris@1517 132 render(options.merge(:only => :lines)) unless @lines_rendered
Chris@1517 133 @lines
Chris@1517 134 end
Chris@1517 135
Chris@1517 136 # Returns issues that will be rendered
Chris@1517 137 def issues
Chris@1517 138 @issues ||= @query.issues(
Chris@1517 139 :include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
Chris@1517 140 :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
Chris@1517 141 :limit => @max_rows
Chris@1517 142 )
Chris@1517 143 end
Chris@1517 144
Chris@1517 145 # Returns a hash of the relations between the issues that are present on the gantt
Chris@1517 146 # and that should be displayed, grouped by issue ids.
Chris@1517 147 def relations
Chris@1517 148 return @relations if @relations
Chris@1517 149 if issues.any?
Chris@1517 150 issue_ids = issues.map(&:id)
Chris@1517 151 @relations = IssueRelation.
Chris@1517 152 where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys).
Chris@1517 153 group_by(&:issue_from_id)
Chris@1517 154 else
Chris@1517 155 @relations = {}
Chris@1517 156 end
Chris@1517 157 end
Chris@1517 158
Chris@1517 159 # Return all the project nodes that will be displayed
Chris@1517 160 def projects
Chris@1517 161 return @projects if @projects
Chris@1517 162 ids = issues.collect(&:project).uniq.collect(&:id)
Chris@1517 163 if ids.any?
Chris@1517 164 # All issues projects and their visible ancestors
Chris@1517 165 @projects = Project.visible.
Chris@1517 166 joins("LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt").
Chris@1517 167 where("child.id IN (?)", ids).
Chris@1517 168 order("#{Project.table_name}.lft ASC").
Chris@1517 169 uniq.
Chris@1517 170 all
Chris@1517 171 else
Chris@1517 172 @projects = []
Chris@1517 173 end
Chris@1517 174 end
Chris@1517 175
Chris@1517 176 # Returns the issues that belong to +project+
Chris@1517 177 def project_issues(project)
Chris@1517 178 @issues_by_project ||= issues.group_by(&:project)
Chris@1517 179 @issues_by_project[project] || []
Chris@1517 180 end
Chris@1517 181
Chris@1517 182 # Returns the distinct versions of the issues that belong to +project+
Chris@1517 183 def project_versions(project)
Chris@1517 184 project_issues(project).collect(&:fixed_version).compact.uniq
Chris@1517 185 end
Chris@1517 186
Chris@1517 187 # Returns the issues that belong to +project+ and are assigned to +version+
Chris@1517 188 def version_issues(project, version)
Chris@1517 189 project_issues(project).select {|issue| issue.fixed_version == version}
Chris@1517 190 end
Chris@1517 191
Chris@1517 192 def render(options={})
Chris@1517 193 options = {:top => 0, :top_increment => 20,
Chris@1517 194 :indent_increment => 20, :render => :subject,
Chris@1517 195 :format => :html}.merge(options)
Chris@1517 196 indent = options[:indent] || 4
Chris@1517 197 @subjects = '' unless options[:only] == :lines
Chris@1517 198 @lines = '' unless options[:only] == :subjects
Chris@1517 199 @number_of_rows = 0
Chris@1517 200 Project.project_tree(projects) do |project, level|
Chris@1517 201 options[:indent] = indent + level * options[:indent_increment]
Chris@1517 202 render_project(project, options)
Chris@1517 203 break if abort?
Chris@1517 204 end
Chris@1517 205 @subjects_rendered = true unless options[:only] == :lines
Chris@1517 206 @lines_rendered = true unless options[:only] == :subjects
Chris@1517 207 render_end(options)
Chris@1517 208 end
Chris@1517 209
Chris@1517 210 def render_project(project, options={})
Chris@1517 211 subject_for_project(project, options) unless options[:only] == :lines
Chris@1517 212 line_for_project(project, options) unless options[:only] == :subjects
Chris@1517 213 options[:top] += options[:top_increment]
Chris@1517 214 options[:indent] += options[:indent_increment]
Chris@1517 215 @number_of_rows += 1
Chris@1517 216 return if abort?
Chris@1517 217 issues = project_issues(project).select {|i| i.fixed_version.nil?}
Chris@1517 218 self.class.sort_issues!(issues)
Chris@1517 219 if issues
Chris@1517 220 render_issues(issues, options)
Chris@1517 221 return if abort?
Chris@1517 222 end
Chris@1517 223 versions = project_versions(project)
Chris@1517 224 self.class.sort_versions!(versions)
Chris@1517 225 versions.each do |version|
Chris@1517 226 render_version(project, version, options)
Chris@1517 227 end
Chris@1517 228 # Remove indent to hit the next sibling
Chris@1517 229 options[:indent] -= options[:indent_increment]
Chris@1517 230 end
Chris@1517 231
Chris@1517 232 def render_issues(issues, options={})
Chris@1517 233 @issue_ancestors = []
Chris@1517 234 issues.each do |i|
Chris@1517 235 subject_for_issue(i, options) unless options[:only] == :lines
Chris@1517 236 line_for_issue(i, options) unless options[:only] == :subjects
Chris@1517 237 options[:top] += options[:top_increment]
Chris@1517 238 @number_of_rows += 1
Chris@1517 239 break if abort?
Chris@1517 240 end
Chris@1517 241 options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
Chris@1517 242 end
Chris@1517 243
Chris@1517 244 def render_version(project, version, options={})
Chris@1517 245 # Version header
Chris@1517 246 subject_for_version(version, options) unless options[:only] == :lines
Chris@1517 247 line_for_version(version, options) unless options[:only] == :subjects
Chris@1517 248 options[:top] += options[:top_increment]
Chris@1517 249 @number_of_rows += 1
Chris@1517 250 return if abort?
Chris@1517 251 issues = version_issues(project, version)
Chris@1517 252 if issues
Chris@1517 253 self.class.sort_issues!(issues)
Chris@1517 254 # Indent issues
Chris@1517 255 options[:indent] += options[:indent_increment]
Chris@1517 256 render_issues(issues, options)
Chris@1517 257 options[:indent] -= options[:indent_increment]
Chris@1517 258 end
Chris@1517 259 end
Chris@1517 260
Chris@1517 261 def render_end(options={})
Chris@1517 262 case options[:format]
Chris@1517 263 when :pdf
Chris@1517 264 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
Chris@1517 265 end
Chris@1517 266 end
Chris@1517 267
Chris@1517 268 def subject_for_project(project, options)
Chris@1517 269 case options[:format]
Chris@1517 270 when :html
Chris@1517 271 html_class = ""
Chris@1517 272 html_class << 'icon icon-projects '
Chris@1517 273 html_class << (project.overdue? ? 'project-overdue' : '')
Chris@1517 274 s = view.link_to_project(project).html_safe
Chris@1517 275 subject = view.content_tag(:span, s,
Chris@1517 276 :class => html_class).html_safe
Chris@1517 277 html_subject(options, subject, :css => "project-name")
Chris@1517 278 when :image
Chris@1517 279 image_subject(options, project.name)
Chris@1517 280 when :pdf
Chris@1517 281 pdf_new_page?(options)
Chris@1517 282 pdf_subject(options, project.name)
Chris@1517 283 end
Chris@1517 284 end
Chris@1517 285
Chris@1517 286 def line_for_project(project, options)
Chris@1517 287 # Skip versions that don't have a start_date or due date
Chris@1517 288 if project.is_a?(Project) && project.start_date && project.due_date
Chris@1517 289 options[:zoom] ||= 1
Chris@1517 290 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
Chris@1517 291 coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
Chris@1517 292 label = h(project)
Chris@1517 293 case options[:format]
Chris@1517 294 when :html
Chris@1517 295 html_task(options, coords, :css => "project task", :label => label, :markers => true)
Chris@1517 296 when :image
Chris@1517 297 image_task(options, coords, :label => label, :markers => true, :height => 3)
Chris@1517 298 when :pdf
Chris@1517 299 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
Chris@1517 300 end
Chris@1517 301 else
Chris@1517 302 ''
Chris@1517 303 end
Chris@1517 304 end
Chris@1517 305
Chris@1517 306 def subject_for_version(version, options)
Chris@1517 307 case options[:format]
Chris@1517 308 when :html
Chris@1517 309 html_class = ""
Chris@1517 310 html_class << 'icon icon-package '
Chris@1517 311 html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " "
Chris@1517 312 html_class << (version.overdue? ? 'version-overdue' : '')
Chris@1517 313 html_class << ' version-closed' unless version.open?
Chris@1517 314 if version.start_date && version.due_date && version.completed_percent
Chris@1517 315 progress_date = calc_progress_date(version.start_date,
Chris@1517 316 version.due_date, version.completed_percent)
Chris@1517 317 html_class << ' behind-start-date' if progress_date < self.date_from
Chris@1517 318 html_class << ' over-end-date' if progress_date > self.date_to
Chris@1517 319 end
Chris@1517 320 s = view.link_to_version(version).html_safe
Chris@1517 321 subject = view.content_tag(:span, s,
Chris@1517 322 :class => html_class).html_safe
Chris@1517 323 html_subject(options, subject, :css => "version-name",
Chris@1517 324 :id => "version-#{version.id}")
Chris@1517 325 when :image
Chris@1517 326 image_subject(options, version.to_s_with_project)
Chris@1517 327 when :pdf
Chris@1517 328 pdf_new_page?(options)
Chris@1517 329 pdf_subject(options, version.to_s_with_project)
Chris@1517 330 end
Chris@1517 331 end
Chris@1517 332
Chris@1517 333 def line_for_version(version, options)
Chris@1517 334 # Skip versions that don't have a start_date
Chris@1517 335 if version.is_a?(Version) && version.due_date && version.start_date
Chris@1517 336 options[:zoom] ||= 1
Chris@1517 337 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
Chris@1517 338 coords = coordinates(version.start_date,
Chris@1517 339 version.due_date, version.completed_percent,
Chris@1517 340 options[:zoom])
Chris@1517 341 label = "#{h version} #{h version.completed_percent.to_i.to_s}%"
Chris@1517 342 label = h("#{version.project} -") + label unless @project && @project == version.project
Chris@1517 343 case options[:format]
Chris@1517 344 when :html
Chris@1517 345 html_task(options, coords, :css => "version task",
Chris@1517 346 :label => label, :markers => true, :version => version)
Chris@1517 347 when :image
Chris@1517 348 image_task(options, coords, :label => label, :markers => true, :height => 3)
Chris@1517 349 when :pdf
Chris@1517 350 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
Chris@1517 351 end
Chris@1517 352 else
Chris@1517 353 ''
Chris@1517 354 end
Chris@1517 355 end
Chris@1517 356
Chris@1517 357 def subject_for_issue(issue, options)
Chris@1517 358 while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
Chris@1517 359 @issue_ancestors.pop
Chris@1517 360 options[:indent] -= options[:indent_increment]
Chris@1517 361 end
Chris@1517 362 output = case options[:format]
Chris@1517 363 when :html
Chris@1517 364 css_classes = ''
Chris@1517 365 css_classes << ' issue-overdue' if issue.overdue?
Chris@1517 366 css_classes << ' issue-behind-schedule' if issue.behind_schedule?
Chris@1517 367 css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
Chris@1517 368 css_classes << ' issue-closed' if issue.closed?
Chris@1517 369 if issue.start_date && issue.due_before && issue.done_ratio
Chris@1517 370 progress_date = calc_progress_date(issue.start_date,
Chris@1517 371 issue.due_before, issue.done_ratio)
Chris@1517 372 css_classes << ' behind-start-date' if progress_date < self.date_from
Chris@1517 373 css_classes << ' over-end-date' if progress_date > self.date_to
Chris@1517 374 end
Chris@1517 375 s = "".html_safe
Chris@1517 376 if issue.assigned_to.present?
Chris@1517 377 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
Chris@1517 378 s << view.avatar(issue.assigned_to,
Chris@1517 379 :class => 'gravatar icon-gravatar',
Chris@1517 380 :size => 10,
Chris@1517 381 :title => assigned_string).to_s.html_safe
Chris@1517 382 end
Chris@1517 383 s << view.link_to_issue(issue).html_safe
Chris@1517 384 subject = view.content_tag(:span, s, :class => css_classes).html_safe
Chris@1517 385 html_subject(options, subject, :css => "issue-subject",
Chris@1517 386 :title => issue.subject, :id => "issue-#{issue.id}") + "\n"
Chris@1517 387 when :image
Chris@1517 388 image_subject(options, issue.subject)
Chris@1517 389 when :pdf
Chris@1517 390 pdf_new_page?(options)
Chris@1517 391 pdf_subject(options, issue.subject)
Chris@1517 392 end
Chris@1517 393 unless issue.leaf?
Chris@1517 394 @issue_ancestors << issue
Chris@1517 395 options[:indent] += options[:indent_increment]
Chris@1517 396 end
Chris@1517 397 output
Chris@1517 398 end
Chris@1517 399
Chris@1517 400 def line_for_issue(issue, options)
Chris@1517 401 # Skip issues that don't have a due_before (due_date or version's due_date)
Chris@1517 402 if issue.is_a?(Issue) && issue.due_before
Chris@1517 403 coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
Chris@1517 404 label = "#{issue.status.name} #{issue.done_ratio}%"
Chris@1517 405 case options[:format]
Chris@1517 406 when :html
Chris@1517 407 html_task(options, coords,
Chris@1517 408 :css => "task " + (issue.leaf? ? 'leaf' : 'parent'),
Chris@1517 409 :label => label, :issue => issue,
Chris@1517 410 :markers => !issue.leaf?)
Chris@1517 411 when :image
Chris@1517 412 image_task(options, coords, :label => label)
Chris@1517 413 when :pdf
Chris@1517 414 pdf_task(options, coords, :label => label)
Chris@1517 415 end
Chris@1517 416 else
Chris@1517 417 ''
Chris@1517 418 end
Chris@1517 419 end
Chris@1517 420
Chris@1517 421 # Generates a gantt image
Chris@1517 422 # Only defined if RMagick is avalaible
Chris@1517 423 def to_image(format='PNG')
Chris@1517 424 date_to = (@date_from >> @months) - 1
Chris@1517 425 show_weeks = @zoom > 1
Chris@1517 426 show_days = @zoom > 2
Chris@1517 427 subject_width = 400
Chris@1517 428 header_height = 18
Chris@1517 429 # width of one day in pixels
Chris@1517 430 zoom = @zoom * 2
Chris@1517 431 g_width = (@date_to - @date_from + 1) * zoom
Chris@1517 432 g_height = 20 * number_of_rows + 30
Chris@1517 433 headers_height = (show_weeks ? 2 * header_height : header_height)
Chris@1517 434 height = g_height + headers_height
Chris@1517 435 imgl = Magick::ImageList.new
Chris@1517 436 imgl.new_image(subject_width + g_width + 1, height)
Chris@1517 437 gc = Magick::Draw.new
Chris@1517 438 gc.font = Redmine::Configuration['rmagick_font_path'] || ""
Chris@1517 439 # Subjects
Chris@1517 440 gc.stroke('transparent')
Chris@1517 441 subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image)
Chris@1517 442 # Months headers
Chris@1517 443 month_f = @date_from
Chris@1517 444 left = subject_width
Chris@1517 445 @months.times do
Chris@1517 446 width = ((month_f >> 1) - month_f) * zoom
Chris@1517 447 gc.fill('white')
Chris@1517 448 gc.stroke('grey')
Chris@1517 449 gc.stroke_width(1)
Chris@1517 450 gc.rectangle(left, 0, left + width, height)
Chris@1517 451 gc.fill('black')
Chris@1517 452 gc.stroke('transparent')
Chris@1517 453 gc.stroke_width(1)
Chris@1517 454 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
Chris@1517 455 left = left + width
Chris@1517 456 month_f = month_f >> 1
Chris@1517 457 end
Chris@1517 458 # Weeks headers
Chris@1517 459 if show_weeks
Chris@1517 460 left = subject_width
Chris@1517 461 height = header_height
Chris@1517 462 if @date_from.cwday == 1
Chris@1517 463 # date_from is monday
Chris@1517 464 week_f = date_from
Chris@1517 465 else
Chris@1517 466 # find next monday after date_from
Chris@1517 467 week_f = @date_from + (7 - @date_from.cwday + 1)
Chris@1517 468 width = (7 - @date_from.cwday + 1) * zoom
Chris@1517 469 gc.fill('white')
Chris@1517 470 gc.stroke('grey')
Chris@1517 471 gc.stroke_width(1)
Chris@1517 472 gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1)
Chris@1517 473 left = left + width
Chris@1517 474 end
Chris@1517 475 while week_f <= date_to
Chris@1517 476 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
Chris@1517 477 gc.fill('white')
Chris@1517 478 gc.stroke('grey')
Chris@1517 479 gc.stroke_width(1)
Chris@1517 480 gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1)
Chris@1517 481 gc.fill('black')
Chris@1517 482 gc.stroke('transparent')
Chris@1517 483 gc.stroke_width(1)
Chris@1517 484 gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s)
Chris@1517 485 left = left + width
Chris@1517 486 week_f = week_f + 7
Chris@1517 487 end
Chris@1517 488 end
Chris@1517 489 # Days details (week-end in grey)
Chris@1517 490 if show_days
Chris@1517 491 left = subject_width
Chris@1517 492 height = g_height + header_height - 1
Chris@1517 493 wday = @date_from.cwday
Chris@1517 494 (date_to - @date_from + 1).to_i.times do
Chris@1517 495 width = zoom
Chris@1517 496 gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white')
Chris@1517 497 gc.stroke('#ddd')
Chris@1517 498 gc.stroke_width(1)
Chris@1517 499 gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1)
Chris@1517 500 left = left + width
Chris@1517 501 wday = wday + 1
Chris@1517 502 wday = 1 if wday > 7
Chris@1517 503 end
Chris@1517 504 end
Chris@1517 505 # border
Chris@1517 506 gc.fill('transparent')
Chris@1517 507 gc.stroke('grey')
Chris@1517 508 gc.stroke_width(1)
Chris@1517 509 gc.rectangle(0, 0, subject_width + g_width, headers_height)
Chris@1517 510 gc.stroke('black')
Chris@1517 511 gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1)
Chris@1517 512 # content
Chris@1517 513 top = headers_height + 20
Chris@1517 514 gc.stroke('transparent')
Chris@1517 515 lines(:image => gc, :top => top, :zoom => zoom,
Chris@1517 516 :subject_width => subject_width, :format => :image)
Chris@1517 517 # today red line
Chris@1517 518 if Date.today >= @date_from and Date.today <= date_to
Chris@1517 519 gc.stroke('red')
Chris@1517 520 x = (Date.today - @date_from + 1) * zoom + subject_width
Chris@1517 521 gc.line(x, headers_height, x, headers_height + g_height - 1)
Chris@1517 522 end
Chris@1517 523 gc.draw(imgl)
Chris@1517 524 imgl.format = format
Chris@1517 525 imgl.to_blob
Chris@1517 526 end if Object.const_defined?(:Magick)
Chris@1517 527
Chris@1517 528 def to_pdf
Chris@1517 529 pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
Chris@1517 530 pdf.SetTitle("#{l(:label_gantt)} #{project}")
Chris@1517 531 pdf.alias_nb_pages
Chris@1517 532 pdf.footer_date = format_date(Date.today)
Chris@1517 533 pdf.AddPage("L")
Chris@1517 534 pdf.SetFontStyle('B', 12)
Chris@1517 535 pdf.SetX(15)
Chris@1517 536 pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s)
Chris@1517 537 pdf.Ln
Chris@1517 538 pdf.SetFontStyle('B', 9)
Chris@1517 539 subject_width = PDF::LeftPaneWidth
Chris@1517 540 header_height = 5
Chris@1517 541 headers_height = header_height
Chris@1517 542 show_weeks = false
Chris@1517 543 show_days = false
Chris@1517 544 if self.months < 7
Chris@1517 545 show_weeks = true
Chris@1517 546 headers_height = 2 * header_height
Chris@1517 547 if self.months < 3
Chris@1517 548 show_days = true
Chris@1517 549 headers_height = 3 * header_height
Chris@1517 550 end
Chris@1517 551 end
Chris@1517 552 g_width = PDF.right_pane_width
Chris@1517 553 zoom = (g_width) / (self.date_to - self.date_from + 1)
Chris@1517 554 g_height = 120
Chris@1517 555 t_height = g_height + headers_height
Chris@1517 556 y_start = pdf.GetY
Chris@1517 557 # Months headers
Chris@1517 558 month_f = self.date_from
Chris@1517 559 left = subject_width
Chris@1517 560 height = header_height
Chris@1517 561 self.months.times do
Chris@1517 562 width = ((month_f >> 1) - month_f) * zoom
Chris@1517 563 pdf.SetY(y_start)
Chris@1517 564 pdf.SetX(left)
Chris@1517 565 pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
Chris@1517 566 left = left + width
Chris@1517 567 month_f = month_f >> 1
Chris@1517 568 end
Chris@1517 569 # Weeks headers
Chris@1517 570 if show_weeks
Chris@1517 571 left = subject_width
Chris@1517 572 height = header_height
Chris@1517 573 if self.date_from.cwday == 1
Chris@1517 574 # self.date_from is monday
Chris@1517 575 week_f = self.date_from
Chris@1517 576 else
Chris@1517 577 # find next monday after self.date_from
Chris@1517 578 week_f = self.date_from + (7 - self.date_from.cwday + 1)
Chris@1517 579 width = (7 - self.date_from.cwday + 1) * zoom-1
Chris@1517 580 pdf.SetY(y_start + header_height)
Chris@1517 581 pdf.SetX(left)
Chris@1517 582 pdf.RDMCell(width + 1, height, "", "LTR")
Chris@1517 583 left = left + width + 1
Chris@1517 584 end
Chris@1517 585 while week_f <= self.date_to
Chris@1517 586 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
Chris@1517 587 pdf.SetY(y_start + header_height)
Chris@1517 588 pdf.SetX(left)
Chris@1517 589 pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
Chris@1517 590 left = left + width
Chris@1517 591 week_f = week_f + 7
Chris@1517 592 end
Chris@1517 593 end
Chris@1517 594 # Days headers
Chris@1517 595 if show_days
Chris@1517 596 left = subject_width
Chris@1517 597 height = header_height
Chris@1517 598 wday = self.date_from.cwday
Chris@1517 599 pdf.SetFontStyle('B', 7)
Chris@1517 600 (self.date_to - self.date_from + 1).to_i.times do
Chris@1517 601 width = zoom
Chris@1517 602 pdf.SetY(y_start + 2 * header_height)
Chris@1517 603 pdf.SetX(left)
Chris@1517 604 pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C")
Chris@1517 605 left = left + width
Chris@1517 606 wday = wday + 1
Chris@1517 607 wday = 1 if wday > 7
Chris@1517 608 end
Chris@1517 609 end
Chris@1517 610 pdf.SetY(y_start)
Chris@1517 611 pdf.SetX(15)
Chris@1517 612 pdf.RDMCell(subject_width + g_width - 15, headers_height, "", 1)
Chris@1517 613 # Tasks
Chris@1517 614 top = headers_height + y_start
Chris@1517 615 options = {
Chris@1517 616 :top => top,
Chris@1517 617 :zoom => zoom,
Chris@1517 618 :subject_width => subject_width,
Chris@1517 619 :g_width => g_width,
Chris@1517 620 :indent => 0,
Chris@1517 621 :indent_increment => 5,
Chris@1517 622 :top_increment => 5,
Chris@1517 623 :format => :pdf,
Chris@1517 624 :pdf => pdf
Chris@1517 625 }
Chris@1517 626 render(options)
Chris@1517 627 pdf.Output
Chris@1517 628 end
Chris@1517 629
Chris@1517 630 private
Chris@1517 631
Chris@1517 632 def coordinates(start_date, end_date, progress, zoom=nil)
Chris@1517 633 zoom ||= @zoom
Chris@1517 634 coords = {}
Chris@1517 635 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
Chris@1517 636 if start_date > self.date_from
Chris@1517 637 coords[:start] = start_date - self.date_from
Chris@1517 638 coords[:bar_start] = start_date - self.date_from
Chris@1517 639 else
Chris@1517 640 coords[:bar_start] = 0
Chris@1517 641 end
Chris@1517 642 if end_date < self.date_to
Chris@1517 643 coords[:end] = end_date - self.date_from
Chris@1517 644 coords[:bar_end] = end_date - self.date_from + 1
Chris@1517 645 else
Chris@1517 646 coords[:bar_end] = self.date_to - self.date_from + 1
Chris@1517 647 end
Chris@1517 648 if progress
Chris@1517 649 progress_date = calc_progress_date(start_date, end_date, progress)
Chris@1517 650 if progress_date > self.date_from && progress_date > start_date
Chris@1517 651 if progress_date < self.date_to
Chris@1517 652 coords[:bar_progress_end] = progress_date - self.date_from
Chris@1517 653 else
Chris@1517 654 coords[:bar_progress_end] = self.date_to - self.date_from + 1
Chris@1517 655 end
Chris@1517 656 end
Chris@1517 657 if progress_date < Date.today
Chris@1517 658 late_date = [Date.today, end_date].min
Chris@1517 659 if late_date > self.date_from && late_date > start_date
Chris@1517 660 if late_date < self.date_to
Chris@1517 661 coords[:bar_late_end] = late_date - self.date_from + 1
Chris@1517 662 else
Chris@1517 663 coords[:bar_late_end] = self.date_to - self.date_from + 1
Chris@1517 664 end
Chris@1517 665 end
Chris@1517 666 end
Chris@1517 667 end
Chris@1517 668 end
Chris@1517 669 # Transforms dates into pixels witdh
Chris@1517 670 coords.keys.each do |key|
Chris@1517 671 coords[key] = (coords[key] * zoom).floor
Chris@1517 672 end
Chris@1517 673 coords
Chris@1517 674 end
Chris@1517 675
Chris@1517 676 def calc_progress_date(start_date, end_date, progress)
Chris@1517 677 start_date + (end_date - start_date + 1) * (progress / 100.0)
Chris@1517 678 end
Chris@1517 679
Chris@1517 680 def self.sort_issues!(issues)
Chris@1517 681 issues.sort! {|a, b| sort_issue_logic(a) <=> sort_issue_logic(b)}
Chris@1517 682 end
Chris@1517 683
Chris@1517 684 def self.sort_issue_logic(issue)
Chris@1517 685 julian_date = Date.new()
Chris@1517 686 ancesters_start_date = []
Chris@1517 687 current_issue = issue
Chris@1517 688 begin
Chris@1517 689 ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id])
Chris@1517 690 current_issue = current_issue.parent
Chris@1517 691 end while (current_issue)
Chris@1517 692 ancesters_start_date
Chris@1517 693 end
Chris@1517 694
Chris@1517 695 def self.sort_versions!(versions)
Chris@1517 696 versions.sort!
Chris@1517 697 end
Chris@1517 698
Chris@1517 699 def current_limit
Chris@1517 700 if @max_rows
Chris@1517 701 @max_rows - @number_of_rows
Chris@1517 702 else
Chris@1517 703 nil
Chris@1517 704 end
Chris@1517 705 end
Chris@1517 706
Chris@1517 707 def abort?
Chris@1517 708 if @max_rows && @number_of_rows >= @max_rows
Chris@1517 709 @truncated = true
Chris@1517 710 end
Chris@1517 711 end
Chris@1517 712
Chris@1517 713 def pdf_new_page?(options)
Chris@1517 714 if options[:top] > 180
Chris@1517 715 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
Chris@1517 716 options[:pdf].AddPage("L")
Chris@1517 717 options[:top] = 15
Chris@1517 718 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
Chris@1517 719 end
Chris@1517 720 end
Chris@1517 721
Chris@1517 722 def html_subject(params, subject, options={})
Chris@1517 723 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
Chris@1517 724 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
Chris@1517 725 output = view.content_tag(:div, subject,
Chris@1517 726 :class => options[:css], :style => style,
Chris@1517 727 :title => options[:title],
Chris@1517 728 :id => options[:id])
Chris@1517 729 @subjects << output
Chris@1517 730 output
Chris@1517 731 end
Chris@1517 732
Chris@1517 733 def pdf_subject(params, subject, options={})
Chris@1517 734 params[:pdf].SetY(params[:top])
Chris@1517 735 params[:pdf].SetX(15)
Chris@1517 736 char_limit = PDF::MaxCharactorsForSubject - params[:indent]
Chris@1517 737 params[:pdf].RDMCell(params[:subject_width] - 15, 5,
Chris@1517 738 (" " * params[:indent]) +
Chris@1517 739 subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'),
Chris@1517 740 "LR")
Chris@1517 741 params[:pdf].SetY(params[:top])
Chris@1517 742 params[:pdf].SetX(params[:subject_width])
Chris@1517 743 params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
Chris@1517 744 end
Chris@1517 745
Chris@1517 746 def image_subject(params, subject, options={})
Chris@1517 747 params[:image].fill('black')
Chris@1517 748 params[:image].stroke('transparent')
Chris@1517 749 params[:image].stroke_width(1)
Chris@1517 750 params[:image].text(params[:indent], params[:top] + 2, subject)
Chris@1517 751 end
Chris@1517 752
Chris@1517 753 def issue_relations(issue)
Chris@1517 754 rels = {}
Chris@1517 755 if relations[issue.id]
Chris@1517 756 relations[issue.id].each do |relation|
Chris@1517 757 (rels[relation.relation_type] ||= []) << relation.issue_to_id
Chris@1517 758 end
Chris@1517 759 end
Chris@1517 760 rels
Chris@1517 761 end
Chris@1517 762
Chris@1517 763 def html_task(params, coords, options={})
Chris@1517 764 output = ''
Chris@1517 765 # Renders the task bar, with progress and late
Chris@1517 766 if coords[:bar_start] && coords[:bar_end]
Chris@1517 767 width = coords[:bar_end] - coords[:bar_start] - 2
Chris@1517 768 style = ""
Chris@1517 769 style << "top:#{params[:top]}px;"
Chris@1517 770 style << "left:#{coords[:bar_start]}px;"
Chris@1517 771 style << "width:#{width}px;"
Chris@1517 772 html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
Chris@1517 773 html_id = "task-todo-version-#{options[:version].id}" if options[:version]
Chris@1517 774 content_opt = {:style => style,
Chris@1517 775 :class => "#{options[:css]} task_todo",
Chris@1517 776 :id => html_id}
Chris@1517 777 if options[:issue]
Chris@1517 778 rels = issue_relations(options[:issue])
Chris@1517 779 if rels.present?
Chris@1517 780 content_opt[:data] = {"rels" => rels.to_json}
Chris@1517 781 end
Chris@1517 782 end
Chris@1517 783 output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
Chris@1517 784 if coords[:bar_late_end]
Chris@1517 785 width = coords[:bar_late_end] - coords[:bar_start] - 2
Chris@1517 786 style = ""
Chris@1517 787 style << "top:#{params[:top]}px;"
Chris@1517 788 style << "left:#{coords[:bar_start]}px;"
Chris@1517 789 style << "width:#{width}px;"
Chris@1517 790 output << view.content_tag(:div, '&nbsp;'.html_safe,
Chris@1517 791 :style => style,
Chris@1517 792 :class => "#{options[:css]} task_late")
Chris@1517 793 end
Chris@1517 794 if coords[:bar_progress_end]
Chris@1517 795 width = coords[:bar_progress_end] - coords[:bar_start] - 2
Chris@1517 796 style = ""
Chris@1517 797 style << "top:#{params[:top]}px;"
Chris@1517 798 style << "left:#{coords[:bar_start]}px;"
Chris@1517 799 style << "width:#{width}px;"
Chris@1517 800 html_id = "task-done-issue-#{options[:issue].id}" if options[:issue]
Chris@1517 801 html_id = "task-done-version-#{options[:version].id}" if options[:version]
Chris@1517 802 output << view.content_tag(:div, '&nbsp;'.html_safe,
Chris@1517 803 :style => style,
Chris@1517 804 :class => "#{options[:css]} task_done",
Chris@1517 805 :id => html_id)
Chris@1517 806 end
Chris@1517 807 end
Chris@1517 808 # Renders the markers
Chris@1517 809 if options[:markers]
Chris@1517 810 if coords[:start]
Chris@1517 811 style = ""
Chris@1517 812 style << "top:#{params[:top]}px;"
Chris@1517 813 style << "left:#{coords[:start]}px;"
Chris@1517 814 style << "width:15px;"
Chris@1517 815 output << view.content_tag(:div, '&nbsp;'.html_safe,
Chris@1517 816 :style => style,
Chris@1517 817 :class => "#{options[:css]} marker starting")
Chris@1517 818 end
Chris@1517 819 if coords[:end]
Chris@1517 820 style = ""
Chris@1517 821 style << "top:#{params[:top]}px;"
Chris@1517 822 style << "left:#{coords[:end] + params[:zoom]}px;"
Chris@1517 823 style << "width:15px;"
Chris@1517 824 output << view.content_tag(:div, '&nbsp;'.html_safe,
Chris@1517 825 :style => style,
Chris@1517 826 :class => "#{options[:css]} marker ending")
Chris@1517 827 end
Chris@1517 828 end
Chris@1517 829 # Renders the label on the right
Chris@1517 830 if options[:label]
Chris@1517 831 style = ""
Chris@1517 832 style << "top:#{params[:top]}px;"
Chris@1517 833 style << "left:#{(coords[:bar_end] || 0) + 8}px;"
Chris@1517 834 style << "width:15px;"
Chris@1517 835 output << view.content_tag(:div, options[:label],
Chris@1517 836 :style => style,
Chris@1517 837 :class => "#{options[:css]} label")
Chris@1517 838 end
Chris@1517 839 # Renders the tooltip
Chris@1517 840 if options[:issue] && coords[:bar_start] && coords[:bar_end]
Chris@1517 841 s = view.content_tag(:span,
Chris@1517 842 view.render_issue_tooltip(options[:issue]).html_safe,
Chris@1517 843 :class => "tip")
Chris@1517 844 style = ""
Chris@1517 845 style << "position: absolute;"
Chris@1517 846 style << "top:#{params[:top]}px;"
Chris@1517 847 style << "left:#{coords[:bar_start]}px;"
Chris@1517 848 style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
Chris@1517 849 style << "height:12px;"
Chris@1517 850 output << view.content_tag(:div, s.html_safe,
Chris@1517 851 :style => style,
Chris@1517 852 :class => "tooltip")
Chris@1517 853 end
Chris@1517 854 @lines << output
Chris@1517 855 output
Chris@1517 856 end
Chris@1517 857
Chris@1517 858 def pdf_task(params, coords, options={})
Chris@1517 859 height = options[:height] || 2
Chris@1517 860 # Renders the task bar, with progress and late
Chris@1517 861 if coords[:bar_start] && coords[:bar_end]
Chris@1517 862 params[:pdf].SetY(params[:top] + 1.5)
Chris@1517 863 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
Chris@1517 864 params[:pdf].SetFillColor(200, 200, 200)
Chris@1517 865 params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
Chris@1517 866 if coords[:bar_late_end]
Chris@1517 867 params[:pdf].SetY(params[:top] + 1.5)
Chris@1517 868 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
Chris@1517 869 params[:pdf].SetFillColor(255, 100, 100)
Chris@1517 870 params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
Chris@1517 871 end
Chris@1517 872 if coords[:bar_progress_end]
Chris@1517 873 params[:pdf].SetY(params[:top] + 1.5)
Chris@1517 874 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
Chris@1517 875 params[:pdf].SetFillColor(90, 200, 90)
Chris@1517 876 params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
Chris@1517 877 end
Chris@1517 878 end
Chris@1517 879 # Renders the markers
Chris@1517 880 if options[:markers]
Chris@1517 881 if coords[:start]
Chris@1517 882 params[:pdf].SetY(params[:top] + 1)
Chris@1517 883 params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
Chris@1517 884 params[:pdf].SetFillColor(50, 50, 200)
Chris@1517 885 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
Chris@1517 886 end
Chris@1517 887 if coords[:end]
Chris@1517 888 params[:pdf].SetY(params[:top] + 1)
Chris@1517 889 params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
Chris@1517 890 params[:pdf].SetFillColor(50, 50, 200)
Chris@1517 891 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
Chris@1517 892 end
Chris@1517 893 end
Chris@1517 894 # Renders the label on the right
Chris@1517 895 if options[:label]
Chris@1517 896 params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
Chris@1517 897 params[:pdf].RDMCell(30, 2, options[:label])
Chris@1517 898 end
Chris@1517 899 end
Chris@1517 900
Chris@1517 901 def image_task(params, coords, options={})
Chris@1517 902 height = options[:height] || 6
Chris@1517 903 # Renders the task bar, with progress and late
Chris@1517 904 if coords[:bar_start] && coords[:bar_end]
Chris@1517 905 params[:image].fill('#aaa')
Chris@1517 906 params[:image].rectangle(params[:subject_width] + coords[:bar_start],
Chris@1517 907 params[:top],
Chris@1517 908 params[:subject_width] + coords[:bar_end],
Chris@1517 909 params[:top] - height)
Chris@1517 910 if coords[:bar_late_end]
Chris@1517 911 params[:image].fill('#f66')
Chris@1517 912 params[:image].rectangle(params[:subject_width] + coords[:bar_start],
Chris@1517 913 params[:top],
Chris@1517 914 params[:subject_width] + coords[:bar_late_end],
Chris@1517 915 params[:top] - height)
Chris@1517 916 end
Chris@1517 917 if coords[:bar_progress_end]
Chris@1517 918 params[:image].fill('#00c600')
Chris@1517 919 params[:image].rectangle(params[:subject_width] + coords[:bar_start],
Chris@1517 920 params[:top],
Chris@1517 921 params[:subject_width] + coords[:bar_progress_end],
Chris@1517 922 params[:top] - height)
Chris@1517 923 end
Chris@1517 924 end
Chris@1517 925 # Renders the markers
Chris@1517 926 if options[:markers]
Chris@1517 927 if coords[:start]
Chris@1517 928 x = params[:subject_width] + coords[:start]
Chris@1517 929 y = params[:top] - height / 2
Chris@1517 930 params[:image].fill('blue')
Chris@1517 931 params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
Chris@1517 932 end
Chris@1517 933 if coords[:end]
Chris@1517 934 x = params[:subject_width] + coords[:end] + params[:zoom]
Chris@1517 935 y = params[:top] - height / 2
Chris@1517 936 params[:image].fill('blue')
Chris@1517 937 params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
Chris@1517 938 end
Chris@1517 939 end
Chris@1517 940 # Renders the label on the right
Chris@1517 941 if options[:label]
Chris@1517 942 params[:image].fill('black')
Chris@1517 943 params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,
Chris@1517 944 params[:top] + 1,
Chris@1517 945 options[:label])
Chris@1517 946 end
Chris@1517 947 end
Chris@1517 948 end
Chris@1517 949 end
Chris@1517 950 end