Skip to content

Commit

Permalink
Refactor estimated series + show estimate end date along with release…
Browse files Browse the repository at this point in the history
…s when showing multiview.
  • Loading branch information
bohansen committed Oct 15, 2013
1 parent 50a1482 commit 798fb09
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 41 deletions.
55 changes: 48 additions & 7 deletions app/models/rb_release_multiview_burnchart.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
# Responsible for calculating data for a release multiview burnchart
# Takes care of limiting presentation of estimates within reasonable time frame.
class RbReleaseMultiviewBurnchart

# Number of days to forecast
ESTIMATE_DAYS = 120

def initialize(multiview)

@releases = multiview.releases

# TODO Should the last date for closed data be removed?
# Idea was to avoid showing closed points in the future which will
# mess up with the estimates. Could also be that estimates should
# make sure not to use new data than today for estimates?
# make sure not to use newer data than today for estimates ?
@stacked_graph = RbStackedData.new(Date.today + 30)
@releases.each{|r|
if r.has_burndown?
release_data = {}
release_data[:days] = r.days
release_data[:total_points] = r.burndown[:total_points]
release_data[:closed_points] = r.burndown[:closed_points]
@stacked_graph.add(release_data,r.name,r.has_open_stories?)
@stacked_graph.add(release_data,r,r.has_open_stories?)
end
}

Expand All @@ -35,28 +40,64 @@ def total_series
def total_series_names
names = []
@stacked_graph.total_data.each{|s|
names << s[:name]
names << s[:object].name
}
names
end

def estimate_series
series = []
@stacked_graph.estimate_data.each{|s|
series << s[:line]
return series if @stacked_graph.closed_estimate.nil?
date_forecast_closed = Date.today
@stacked_graph.total_estimates.each{|k,l|
date_forecast = Date.today + ESTIMATE_DAYS
date_forecast = l[:end_date_estimate] + 5.days if l[:end_date_estimate].nil? == false && (l[:end_date_estimate] > Date.today && l[:end_date_estimate] < date_forecast)
date_forecast_closed = date_forecast if date_forecast > date_forecast_closed
series << l[:trendline].predict_line(date_forecast)
}
series << @stacked_graph.closed_estimate.predict_line(date_forecast_closed)
series
end

def estimate_series_names
names = []
@stacked_graph.estimate_data.each{|s|
names << s[:name]
@stacked_graph.total_estimates.each_key{|k|
names << k.name + " estimate"
}
names << "Estimated accepted points"
names
end

def closed_series
@stacked_graph.closed_data[:days].zip(@stacked_graph.closed_data[:closed_points])
end

def trend_end_dates
@stacked_graph.trend_end_dates
end

# Return all releases including trend information if available
def releases_estimate
releases_with_trends = []
@releases.each{|r|
trend_end_date = @stacked_graph.total_estimates[r][:end_date_estimate] unless @stacked_graph.total_estimates[r].nil?
releases_with_trends << { :release => r,
:trend_end_date => _estimate_text(r.has_open_stories?,trend_end_date)}
}
releases_with_trends
end

private
def _estimate_text(open_stories, end_date)
if (open_stories === false)
"Finished"
elsif (end_date.nil?)
"Not Available"
elsif (end_date < Date.today)
"Estimate in the past!"
else
end_date
end
end

end
48 changes: 33 additions & 15 deletions app/models/rb_stacked_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@
# * Points are accumulated with current closed series.
# * No data beyond closed_day_limit is accepted into the closed series
# This is to avoid initial days with closed points of future releases to
# affect the graph.
# affect the estimates.
class RbStackedData

attr_reader :closed_data
attr_reader :total_data
attr_reader :estimate_data
attr_reader :total_estimates
attr_reader :closed_estimate

# Number of days to forecast
ESTIMATE_DAYS = 60
# Number of history datapoints used for calculating estimate
ESTIMATE_POINTS = 5

Expand All @@ -29,13 +28,20 @@ def initialize(closed_day_limit)
@closed_data[:days]= []
@closed_data[:closed_points] = []
@estimate_data = []
@total_estimates = Hash.new
end

def add(arrays,name,create_estimate = false)
# Add a series to the stacked graph.
# arrays: Expected to be a hash with elements :days, :total_points and :closed_points.
# All three of them are expected to be arrays of equal size.
# object: Associated object for identification.
# create_estimate: Choose whether to create trendline or not.
# Typically enabled if series contain open stories.
def add(arrays,object,create_estimate = false)
if @total_data.size == 0
add_first(arrays,name,create_estimate)
add_first(arrays,object,create_estimate)
else
stack_total(arrays,name,create_estimate)
stack_total(arrays,object,create_estimate)
merge_closed(arrays)
end
end
Expand All @@ -44,9 +50,14 @@ def [](i)
return @total_data[i]
end

# Adds overlapping days between stacked series to make the graph look
# smoother when the points are changing in each series.
# create_closed_estimate: Enable/disable closed points trendline.
# Typically enabled when there are open stories in one or more releases.
def finalize(create_closed_estimate = false)
add_overlapping_days
calculate_closed_estimate if create_closed_estimate == true
calculate_estimate_end_days if create_closed_estimate == true
end

private
Expand All @@ -65,16 +76,23 @@ def add_overlapping_days
_ripple_overlapping_days(idx_bottom,idx_top);
end


# Calculate a trendline for closed points
def calculate_closed_estimate
return unless @closed_data[:days].size > 1
@closed_estimate = _linear_regression(@closed_data[:days],@closed_data[:closed_points],ESTIMATE_POINTS)
end

est_closed = _linear_regression(@closed_data[:days],@closed_data[:closed_points],ESTIMATE_POINTS)
@estimate_data << { :line => est_closed.predict_line(@closed_day_limit + ESTIMATE_DAYS), :name => "Estimated accepted points"}
# Calculate estimated end dates for all stacked series trendlines crossing closed series trendline
def calculate_estimate_end_days
@total_estimates.each{|k,l|
@total_estimates[k][:end_date_estimate] = l[:trendline].crossing_date(@closed_estimate)
}
end

def add_first(arrays,name,create_estimate)
@total_data << {:days => arrays[:days], :total_points => arrays[:total_points], :name => name}
# Used when adding first stacked series
def add_first(arrays,object,create_estimate)
@total_data << {:days => arrays[:days], :total_points => arrays[:total_points], :object => object}
# Need to duplicate array of days. Otherwise ruby references falsely.
days_within_limit = arrays[:days].select{|day| day <= @closed_day_limit}
return if days_within_limit.size() == 0
Expand All @@ -83,11 +101,11 @@ def add_first(arrays,name,create_estimate)

if create_estimate
est_total = _linear_regression(@total_data[-1][:days],@total_data[-1][:total_points],ESTIMATE_POINTS)
@estimate_data << { :line => est_total.predict_line(@closed_day_limit + ESTIMATE_DAYS), :name => name + " estimate"}
@total_estimates[object] = {:trendline => est_total}
end
end

def stack_total(arrays,name,create_estimate)
def stack_total(arrays,object,create_estimate)
# Have last stacked series ready when stacking the next
last = @total_data.last

Expand All @@ -112,11 +130,11 @@ def stack_total(arrays,name,create_estimate)
tmp_total << arrays[:total_points][i] + last[:total_points][idx]
}
# Add the new stacked total series
@total_data << {:days => arrays[:days], :total_points => tmp_total, :name => name}
@total_data << {:days => arrays[:days], :total_points => tmp_total, :object => object}

if create_estimate
est_total = _linear_regression(@total_data[-1][:days],@total_data[-1][:total_points],ESTIMATE_POINTS)
@estimate_data << { :line => est_total.predict_line(@closed_day_limit + ESTIMATE_DAYS), :name => name + " estimate"}
@total_estimates[object] = {:trendline => est_total }
end
end

Expand Down
15 changes: 15 additions & 0 deletions app/views/rb_releases_multiview/_releases.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<table class="list releases">
<tr>
<th>Release</th>
<th><%= l(:field_release_start_date) %></th>
<th><%= l(:field_release_end_date) %></th>
</tr>
<% releases.each do |r|
klass=r.closed? ? 'closed':'open' %>
<tr class="<%= klass %>">
<td><%= release_link_or_empty(r) %></td>
<td><%= r.release_start_date %></td>
<td><%= r.release_end_date %></td>
</tr>
<% end %>
</table>
17 changes: 17 additions & 0 deletions app/views/rb_releases_multiview/_releases_estimate.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<table class="list releases">
<tr>
<th>Release</th>
<th><%= l(:field_release_start_date) %></th>
<th><%= l(:field_release_end_date) %></th>
<th>Estimated end date</th>
</tr>
<% releases.each do |r|
klass=r[:release].closed? ? 'closed':'open' %>
<tr class="<%= klass %>">
<td><%= release_link_or_empty(r[:release]) %></td>
<td><%= r[:release].release_start_date %></td>
<td><%= r[:release].release_end_date %></td>
<td><%= r[:trend_end_date] %></td>
</tr>
<% end %>
</table>
8 changes: 6 additions & 2 deletions app/views/rb_releases_multiview/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@
<% end %>

<h3>Releases</h3>
<% @release_multiview.releases.each do |release| %>
<%= render :partial => 'rb_releases/release', :object => release %>
<% unless @release_multiview.has_burnchart? %>
<%= render :partial => 'releases', :locals => { :releases => @release_multiview.releases } %>
<% else %>
<%= render :partial => 'releases_estimate', :locals => { :releases => @release_multiview.burnchart.releases_estimate } %>
<% end %>




<% content_for :sidebar do %>
<h3><%= l(:label_release_multiview) %></h3>


<% end %>
Loading

0 comments on commit 798fb09

Please sign in to comment.