Class: SprintBurndown
- Defined in:
- lib/jirametrics/sprint_burndown.rb
Constant Summary
Constants inherited from ChartBase
ChartBase::LABEL_POSITIONS, ChartBase::OKABE_ITO_PALETTE
Instance Attribute Summary collapse
-
#board_id ⇒ Object
Returns the value of attribute board_id.
-
#summary_stats ⇒ Object
readonly
Returns the value of attribute summary_stats.
-
#use_story_counts ⇒ Object
readonly
Returns the value of attribute use_story_counts.
-
#use_story_points ⇒ Object
readonly
Returns the value of attribute use_story_points.
Attributes inherited from ChartBase
#aggregated_project, #all_boards, #atlassian_document_format, #canvas_height, #canvas_width, #data_quality, #date_range, #file_system, #fix_versions, #holiday_dates, #issues, #settings, #time_range, #timezone_offset, #x_axis_title, #y_axis_title
Instance Method Summary collapse
- #append_closing_points(data_set, measure, sprint, start_written) ⇒ Object
-
#build_sprint_data_set(sprint:, change_data_for_sprint:, measure:) ⇒ Object
Walks the sprint's change stream once.
- #change_point(measure, change_data, message) ⇒ Object
-
#changes_for_one_issue(issue:, sprint:) ⇒ Object
select all the changes that are relevant for the sprint.
-
#charts_to_generate ⇒ Object
The [data-builder method, y-axis label] pairs for whichever burndown measures are turned on.
- #data_set_by_story_counts(sprint:, change_data_for_sprint:) ⇒ Object
- #data_set_by_story_points(sprint:, change_data_for_sprint:) ⇒ Object
- #estimate_change_before_completion?(change, estimate_display_name, issue_completed_time) ⇒ Boolean
- #ever_entered_sprint?(change_data) ⇒ Boolean
-
#gather_change_data_by_sprint(sprints) ⇒ Object
Every sprint's changes, flattened across all issues and sorted into time order.
-
#initialize ⇒ SprintBurndown
constructor
A new instance of SprintBurndown.
- #legend_for(data_method) ⇒ Object
- #options=(arg) ⇒ Object
- #past_sprint_end?(change_data, sprint) ⇒ Boolean
- #reached_completion?(completed_has_been_tracked, change, issue_completed_time) ⇒ Boolean
- #run ⇒ Object
-
#sprint_change_action(in_change_item, currently_in_sprint, estimate) ⇒ Object
Two sprint changes in a row can say the same thing, so an event only fires when the membership actually flips: entering (was out, now in) or leaving (was in, now out).
-
#sprint_data_sets(data_method:, sprints:, change_data_by_sprint:) ⇒ Object
One Chart.js data set per sprint, each in its own colour from the palette.
- #sprint_in_change_item(sprint, change_item) ⇒ Object
-
#sprint_in_time_range?(sprint) ⇒ Boolean
A sprint counts when it has actually started (future and never-started sprints are just holding areas, so we ignore them) and its active span overlaps the chart's time range.
- #sprint_start_point(measure, sprint) ⇒ Object
- #sprints_in_time_range(board) ⇒ Object
- #start_pending?(start_written, change_data, sprint) ⇒ Boolean
Methods inherited from ChartBase
#aggregated_project?, #before_run, #call_before_run, #canvas, #canvas_responsive?, #chart_format, #collapsible_issues_panel, #color_block, #color_for, #completed_issues_in_range, #current_board, #cycletime, #cycletime_for_issue, #daily_chart_dataset, #date_annotation, #describe_non_working_days, #description_text, #format_integer, #format_status, #header_text, #holidays, #html_directory, #icon_span, #link_to_issue, #next_id, #non_working_day?, #normalize_annotation_datetime, #not_visible_icon, #not_visible_text, #random_color, #render, #render_axis_title, #render_top_text, #resolve_status, #stagger_label_positions, #status_category_color, #to_human_readable, #working_days_annotation, #wrap_and_render
Constructor Details
#initialize ⇒ SprintBurndown
Returns a new instance of SprintBurndown.
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# File 'lib/jirametrics/sprint_burndown.rb', line 7 def initialize super @summary_stats = {} header_text 'Sprint burndown' description_text <<-TEXT <div class="p"> Burndowns for all sprints in this time period. The different colours are only to differentiate one sprint from another as they may overlap time periods. </div> #{describe_non_working_days} TEXT @x_axis_title = 'Date' @y_axis_title = 'Items remaining' end |
Instance Attribute Details
#board_id ⇒ Object
Returns the value of attribute board_id.
5 6 7 |
# File 'lib/jirametrics/sprint_burndown.rb', line 5 def board_id @board_id end |
#summary_stats ⇒ Object (readonly)
Returns the value of attribute summary_stats.
4 5 6 |
# File 'lib/jirametrics/sprint_burndown.rb', line 4 def summary_stats @summary_stats end |
#use_story_counts ⇒ Object (readonly)
Returns the value of attribute use_story_counts.
4 5 6 |
# File 'lib/jirametrics/sprint_burndown.rb', line 4 def use_story_counts @use_story_counts end |
#use_story_points ⇒ Object (readonly)
Returns the value of attribute use_story_points.
4 5 6 |
# File 'lib/jirametrics/sprint_burndown.rb', line 4 def use_story_points @use_story_points end |
Instance Method Details
#append_closing_points(data_set, measure, sprint, start_written) ⇒ Object
249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/jirametrics/sprint_burndown.rb', line 249 def append_closing_points data_set, measure, sprint, start_written # Nothing in the change stream triggered the sprint-start marker, so write it now. data_set << sprint_start_point(measure, sprint) unless start_written if sprint.completed_time data_set << { y: measure.value, x: chart_format(sprint.completed_time), title: measure.ended_title } measure.summary_stats.remaining = measure.value end return if sprint.completed_at?(time_range.end) data_set << { y: measure.value, x: chart_format(time_range.end), title: measure.active_title } end |
#build_sprint_data_set(sprint:, change_data_for_sprint:, measure:) ⇒ Object
Walks the sprint's change stream once. The measure decides how each change accumulates and reads out; the loop structure is shared by both the story-points and story-counts views.
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/jirametrics/sprint_burndown.rb', line 209 def build_sprint_data_set sprint:, change_data_for_sprint:, measure: data_set = [] start_written = false change_data_for_sprint.each do |change_data| if start_pending?(start_written, change_data, sprint) data_set << sprint_start_point(measure, sprint) start_written = true end break if past_sprint_end?(change_data, sprint) measure.update_state change_data next unless change_data.time >= sprint.start_time = measure.record change_data data_set << change_point(measure, change_data, ) if end append_closing_points data_set, measure, sprint, start_written @summary_stats[sprint] = measure.summary_stats data_set end |
#change_point(measure, change_data, message) ⇒ Object
245 246 247 |
# File 'lib/jirametrics/sprint_burndown.rb', line 245 def change_point measure, change_data, { y: measure.value, x: chart_format(change_data.time), title: "#{change_data.issue.key} #{}" } end |
#changes_for_one_issue(issue:, sprint:) ⇒ Object
select all the changes that are relevant for the sprint. If this issue never appears in this sprint then return [].
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/jirametrics/sprint_burndown.rb', line 133 def changes_for_one_issue issue:, sprint: estimate = 0.0 currently_in_sprint = false completed_has_been_tracked = false change_data = [] estimate_display_name = current_board.estimation_configuration.display_name issue_completed_time = issue.started_stopped_times.last issue.changes.each do |change| action = nil value = nil if change.sprint? in_change_item = sprint_in_change_item(sprint, change) action, value = sprint_change_action(in_change_item, currently_in_sprint, estimate) currently_in_sprint = in_change_item elsif estimate_change_before_completion?(change, estimate_display_name, issue_completed_time) action = :story_points estimate = change.value.to_f value = estimate - change.old_value.to_f elsif reached_completion?(completed_has_been_tracked, change, issue_completed_time) completed_has_been_tracked = true action = :issue_stopped value = -estimate end next unless action change_data << SprintIssueChangeData.new( time: change.time, issue: issue, action: action, value: value, estimate: estimate ) end ever_entered_sprint?(change_data) ? change_data : [] end |
#charts_to_generate ⇒ Object
The [data-builder method, y-axis label] pairs for whichever burndown measures are turned on.
71 72 73 74 75 76 |
# File 'lib/jirametrics/sprint_burndown.rb', line 71 def charts_to_generate charts = [] charts << [:data_set_by_story_points, 'Story Points'] if @use_story_points charts << [:data_set_by_story_counts, 'Story Count'] if @use_story_counts charts end |
#data_set_by_story_counts(sprint:, change_data_for_sprint:) ⇒ Object
201 202 203 204 205 |
# File 'lib/jirametrics/sprint_burndown.rb', line 201 def data_set_by_story_counts sprint:, change_data_for_sprint: build_sprint_data_set( sprint: sprint, change_data_for_sprint: change_data_for_sprint, measure: SprintCountMeasure.new ) end |
#data_set_by_story_points(sprint:, change_data_for_sprint:) ⇒ Object
195 196 197 198 199 |
# File 'lib/jirametrics/sprint_burndown.rb', line 195 def data_set_by_story_points sprint:, change_data_for_sprint: build_sprint_data_set( sprint: sprint, change_data_for_sprint: change_data_for_sprint, measure: SprintPointsMeasure.new ) end |
#estimate_change_before_completion?(change, estimate_display_name, issue_completed_time) ⇒ Boolean
179 180 181 |
# File 'lib/jirametrics/sprint_burndown.rb', line 179 def estimate_change_before_completion? change, estimate_display_name, issue_completed_time change.field == estimate_display_name && (issue_completed_time.nil? || change.time < issue_completed_time) end |
#ever_entered_sprint?(change_data) ⇒ Boolean
187 188 189 |
# File 'lib/jirametrics/sprint_burndown.rb', line 187 def ever_entered_sprint? change_data change_data.any? { |data| data.action == :enter_sprint } end |
#gather_change_data_by_sprint(sprints) ⇒ Object
Every sprint's changes, flattened across all issues and sorted into time order.
63 64 65 66 67 68 |
# File 'lib/jirametrics/sprint_burndown.rb', line 63 def gather_change_data_by_sprint sprints sprints.to_h do |sprint| change_data = issues.flat_map { |issue| changes_for_one_issue(issue: issue, sprint: sprint) } [sprint, change_data.sort_by(&:time)] end end |
#legend_for(data_method) ⇒ Object
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/jirametrics/sprint_burndown.rb', line 98 def legend_for data_method case data_method when :data_set_by_story_counts ['<b>Started</b>: Number of issues already in the sprint, when the sprint was started.', '<b>Completed</b>: Number of issues, completed during the sprint', '<b>Added</b>: Number of issues added in the middle of the sprint', '<b>Removed</b>: Number of issues removed while the sprint was in progress'] when :data_set_by_story_points ['<b>Started</b>: Total count of story points when the sprint was started', '<b>Completed</b>: Count of story points completed during the sprint', '<b>Added</b>: Count of story points added in the middle of the sprint', '<b>Removed</b>: Count of story points removed while the sprint was in progress'] else raise "Unexpected method #{data_method}" end end |
#options=(arg) ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/jirametrics/sprint_burndown.rb', line 23 def arg case arg when :points_only @use_story_points = true @use_story_counts = false when :counts_only @use_story_points = false @use_story_counts = true when :points_and_counts @use_story_points = true @use_story_counts = true else raise "Unexpected option: #{arg}" end end |
#past_sprint_end?(change_data, sprint) ⇒ Boolean
236 237 238 |
# File 'lib/jirametrics/sprint_burndown.rb', line 236 def past_sprint_end? change_data, sprint sprint.completed_time && change_data.time > sprint.completed_time end |
#reached_completion?(completed_has_been_tracked, change, issue_completed_time) ⇒ Boolean
183 184 185 |
# File 'lib/jirametrics/sprint_burndown.rb', line 183 def reached_completion? completed_has_been_tracked, change, issue_completed_time completed_has_been_tracked == false && change.time == issue_completed_time end |
#run ⇒ Object
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/jirametrics/sprint_burndown.rb', line 39 def run return nil unless current_board.scrum? sprints = sprints_in_time_range current_board change_data_by_sprint = gather_change_data_by_sprint sprints result = +'' result << render_top_text(binding) # HashEachMethods misreads this array of [method, title] pairs as a hash and thinks y_axis_title # is unused; in fact the ERB template reads it (and data_method) from the binding we pass to render. charts_to_generate.each do |data_method, y_axis_title| # rubocop:disable Style/HashEachMethods @summary_stats.clear data_sets = sprint_data_sets( data_method: data_method, sprints: sprints, change_data_by_sprint: change_data_by_sprint ) legend = legend_for data_method result << render(binding, __FILE__) end result end |
#sprint_change_action(in_change_item, currently_in_sprint, estimate) ⇒ Object
Two sprint changes in a row can say the same thing, so an event only fires when the membership actually flips: entering (was out, now in) or leaving (was in, now out).
172 173 174 175 176 177 |
# File 'lib/jirametrics/sprint_burndown.rb', line 172 def sprint_change_action in_change_item, currently_in_sprint, estimate return [:enter_sprint, estimate] if currently_in_sprint == false && in_change_item return [:leave_sprint, -estimate] if currently_in_sprint && in_change_item == false [nil, nil] end |
#sprint_data_sets(data_method:, sprints:, change_data_by_sprint:) ⇒ Object
One Chart.js data set per sprint, each in its own colour from the palette.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/jirametrics/sprint_burndown.rb', line 79 def sprint_data_sets data_method:, sprints:, change_data_by_sprint: possible_colours = (1..ChartBase::OKABE_ITO_PALETTE.size).collect do |i| CssVariable["--sprint-burndown-sprint-color-#{i}"] end sprints.each_with_index.collect do |sprint, index| color = possible_colours[index % possible_colours.size] { label: sprint.name, data: send(data_method, sprint: sprint, change_data_for_sprint: change_data_by_sprint[sprint]), fill: false, showLine: true, borderColor: color, backgroundColor: color, stepped: true, pointStyle: %w[rect circle] # First dot is visually different from the rest } end end |
#sprint_in_change_item(sprint, change_item) ⇒ Object
191 192 193 |
# File 'lib/jirametrics/sprint_burndown.rb', line 191 def sprint_in_change_item sprint, change_item change_item.raw['to'].split(/\s*,\s*/).any? { |id| id.to_i == sprint.id } end |
#sprint_in_time_range?(sprint) ⇒ Boolean
A sprint counts when it has actually started (future and never-started sprints are just holding areas, so we ignore them) and its active span overlaps the chart's time range.
121 122 123 124 125 126 127 128 129 130 |
# File 'lib/jirametrics/sprint_burndown.rb', line 121 def sprint_in_time_range? sprint return false if sprint.future? sprint_start_time = sprint.start_time return false if sprint_start_time.nil? sprint_end_time = sprint.completed_time || sprint.end_time time_range.include?(sprint_start_time) || time_range.include?(sprint_end_time) || (sprint_start_time < time_range.begin && sprint_end_time > time_range.end) end |
#sprint_start_point(measure, sprint) ⇒ Object
240 241 242 243 |
# File 'lib/jirametrics/sprint_burndown.rb', line 240 def sprint_start_point measure, sprint measure.summary_stats.started = measure.value { y: measure.value, x: chart_format(sprint.start_time), title: measure.started_title } end |
#sprints_in_time_range(board) ⇒ Object
115 116 117 |
# File 'lib/jirametrics/sprint_burndown.rb', line 115 def sprints_in_time_range board board.sprints.select { |sprint| sprint_in_time_range? sprint } end |
#start_pending?(start_written, change_data, sprint) ⇒ Boolean
232 233 234 |
# File 'lib/jirametrics/sprint_burndown.rb', line 232 def start_pending? start_written, change_data, sprint !start_written && change_data.time >= sprint.start_time end |