jirametrics 2.12.1 → 2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/bin/jirametrics-mcp +5 -0
  3. data/lib/jirametrics/aggregate_config.rb +10 -2
  4. data/lib/jirametrics/aging_work_bar_chart.rb +191 -133
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +43 -11
  6. data/lib/jirametrics/aging_work_table.rb +9 -7
  7. data/lib/jirametrics/anonymizer.rb +81 -6
  8. data/lib/jirametrics/atlassian_document_format.rb +160 -0
  9. data/lib/jirametrics/bar_chart_range.rb +17 -0
  10. data/lib/jirametrics/blocked_stalled_change.rb +5 -3
  11. data/lib/jirametrics/board.rb +32 -8
  12. data/lib/jirametrics/board_config.rb +4 -1
  13. data/lib/jirametrics/board_feature.rb +14 -0
  14. data/lib/jirametrics/board_movement_calculator.rb +2 -2
  15. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  16. data/lib/jirametrics/change_item.rb +15 -6
  17. data/lib/jirametrics/chart_base.rb +141 -3
  18. data/lib/jirametrics/css_variable.rb +1 -1
  19. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  20. data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +21 -4
  21. data/lib/jirametrics/cycletime_histogram.rb +15 -101
  22. data/lib/jirametrics/cycletime_scatterplot.rb +17 -83
  23. data/lib/jirametrics/daily_view.rb +90 -61
  24. data/lib/jirametrics/daily_wip_by_age_chart.rb +4 -5
  25. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +14 -4
  26. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
  27. data/lib/jirametrics/daily_wip_chart.rb +30 -8
  28. data/lib/jirametrics/data_quality_report.rb +43 -12
  29. data/lib/jirametrics/dependency_chart.rb +6 -3
  30. data/lib/jirametrics/download_config.rb +15 -0
  31. data/lib/jirametrics/downloader.rb +117 -71
  32. data/lib/jirametrics/downloader_for_cloud.rb +287 -0
  33. data/lib/jirametrics/downloader_for_data_center.rb +95 -0
  34. data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
  35. data/lib/jirametrics/examples/aggregated_project.rb +2 -2
  36. data/lib/jirametrics/examples/standard_project.rb +41 -28
  37. data/lib/jirametrics/expedited_chart.rb +3 -1
  38. data/lib/jirametrics/exporter.rb +26 -6
  39. data/lib/jirametrics/file_config.rb +9 -11
  40. data/lib/jirametrics/file_system.rb +59 -3
  41. data/lib/jirametrics/fix_version.rb +13 -0
  42. data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
  43. data/lib/jirametrics/github_gateway.rb +115 -0
  44. data/lib/jirametrics/groupable_issue_chart.rb +11 -1
  45. data/lib/jirametrics/grouping_rules.rb +26 -4
  46. data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
  47. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
  48. data/lib/jirametrics/html/aging_work_table.erb +5 -0
  49. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  50. data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
  51. data/lib/jirametrics/html/daily_wip_chart.erb +40 -5
  52. data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
  53. data/lib/jirametrics/html/expedited_chart.erb +6 -14
  54. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
  55. data/lib/jirametrics/html/index.css +249 -69
  56. data/lib/jirametrics/html/index.erb +11 -37
  57. data/lib/jirametrics/html/index.js +164 -0
  58. data/lib/jirametrics/html/legacy_colors.css +174 -0
  59. data/lib/jirametrics/html/sprint_burndown.erb +17 -15
  60. data/lib/jirametrics/html/throughput_chart.erb +42 -11
  61. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +61 -59
  62. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +15 -11
  63. data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
  64. data/lib/jirametrics/html_generator.rb +32 -0
  65. data/lib/jirametrics/html_report_config.rb +52 -57
  66. data/lib/jirametrics/issue.rb +305 -102
  67. data/lib/jirametrics/issue_printer.rb +97 -0
  68. data/lib/jirametrics/jira_gateway.rb +81 -17
  69. data/lib/jirametrics/mcp_server.rb +531 -0
  70. data/lib/jirametrics/project_config.rb +128 -12
  71. data/lib/jirametrics/pull_request.rb +30 -0
  72. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  73. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  74. data/lib/jirametrics/pull_request_review.rb +13 -0
  75. data/lib/jirametrics/raw_javascript.rb +17 -0
  76. data/lib/jirametrics/settings.json +5 -1
  77. data/lib/jirametrics/sprint.rb +12 -0
  78. data/lib/jirametrics/sprint_burndown.rb +10 -4
  79. data/lib/jirametrics/status.rb +1 -1
  80. data/lib/jirametrics/status_collection.rb +1 -0
  81. data/lib/jirametrics/stitcher.rb +81 -0
  82. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  83. data/lib/jirametrics/throughput_chart.rb +73 -23
  84. data/lib/jirametrics/time_based_histogram.rb +139 -0
  85. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  86. data/lib/jirametrics/wip_by_column_chart.rb +236 -0
  87. data/lib/jirametrics.rb +83 -68
  88. metadata +61 -6
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jirametrics/groupable_issue_chart'
4
-
5
- class CycletimeScatterplot < ChartBase
6
- include GroupableIssueChart
3
+ require 'jirametrics/time_based_scatterplot'
7
4
 
5
+ class CycletimeScatterplot < TimeBasedScatterplot
8
6
  attr_accessor :possible_statuses
9
7
 
10
8
  def initialize block
@@ -26,6 +24,8 @@ class CycletimeScatterplot < ChartBase
26
24
  </div>
27
25
  #{describe_non_working_days}
28
26
  HTML
27
+ @x_axis_title = 'Date completed'
28
+ @y_axis_title = 'Cycletime in days'
29
29
 
30
30
  init_configuration_block block do
31
31
  grouping_rules do |issue, rule|
@@ -33,95 +33,29 @@ class CycletimeScatterplot < ChartBase
33
33
  rule.color = color_for type: issue.type
34
34
  end
35
35
  end
36
-
37
- @percentage_lines = []
38
- @highest_cycletime = 0
39
36
  end
40
37
 
41
- def run
42
- completed_issues = completed_issues_in_range include_unstarted: false
43
-
44
- data_sets = create_datasets completed_issues
45
- overall_percent_line = calculate_percent_line(completed_issues)
46
- @percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
47
-
48
- return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
49
-
50
- wrap_and_render(binding, __FILE__)
38
+ def minimum_y_value
39
+ 1 # Values under 1 day are data quality problems; they're flagged in the quality report instead
51
40
  end
52
41
 
53
- def create_datasets completed_issues
54
- data_sets = []
55
-
56
- group_issues(completed_issues).each do |rules, completed_issues_by_type|
57
- label = rules.label
58
- color = rules.color
59
- percent_line = calculate_percent_line completed_issues_by_type
60
- data = completed_issues_by_type.filter_map { |issue| data_for_issue(issue) }
61
- data_sets << {
62
- label: "#{label} (85% at #{label_days(percent_line)})",
63
- data: data,
64
- fill: false,
65
- showLine: false,
66
- backgroundColor: color
67
- }
68
-
69
- data_sets << trend_line_data_set(label: label, data: data, color: color)
70
-
71
- @percentage_lines << [percent_line, color]
72
- end
73
- data_sets
42
+ def all_items
43
+ completed_issues_in_range include_unstarted: false
74
44
  end
75
45
 
76
- def show_trend_lines
77
- @show_trend_lines = true
46
+ def x_value item
47
+ item.started_stopped_times.last
78
48
  end
79
49
 
80
- def trend_line_data_set label:, data:, color:
81
- points = data.collect do |hash|
82
- [Time.parse(hash[:x]).to_i, hash[:y]]
83
- end
84
-
85
- # The trend calculation works with numbers only so convert Time to an int and back
86
- calculator = TrendLineCalculator.new(points)
87
- data_points = calculator.chart_datapoints(
88
- range: time_range.begin.to_i..time_range.end.to_i,
89
- max_y: @highest_cycletime
90
- )
91
- data_points.each do |point_hash|
92
- point_hash[:x] = chart_format Time.at(point_hash[:x])
93
- end
94
-
95
- {
96
- type: 'line',
97
- label: "#{label} Trendline",
98
- data: data_points,
99
- fill: false,
100
- borderWidth: 1,
101
- markerType: 'none',
102
- borderColor: color,
103
- borderDash: [6, 3],
104
- pointStyle: 'dash',
105
- hidden: !@show_trend_lines
106
- }
50
+ def y_value item
51
+ item.board.cycletime.cycletime(item)
107
52
  end
108
53
 
109
- def data_for_issue issue
110
- cycle_time = issue.board.cycletime.cycletime(issue)
111
- return nil if cycle_time < 1 # These will get called out on the quality report
112
-
113
- @highest_cycletime = cycle_time if @highest_cycletime < cycle_time
114
-
115
- {
116
- y: cycle_time,
117
- x: chart_format(issue.board.cycletime.started_stopped_times(issue).last),
118
- title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
119
- }
54
+ def title_value item, rules: nil
55
+ hint = @issue_hints&.fetch(item, nil)
56
+ "#{item.key} : #{item.summary} (#{label_days(y_value(item))})#{" #{hint}" if hint}"
120
57
  end
121
58
 
122
- def calculate_percent_line completed_issues
123
- times = completed_issues.collect { |issue| issue.board.cycletime.cycletime(issue) }
124
- index = times.size * 85 / 100
125
- times.sort[index]
126
- end
59
+ # Kept for backwards compatibility with existing callers and specs
60
+ alias data_for_issue data_for_item
127
61
  end
@@ -9,7 +9,8 @@ class DailyView < ChartBase
9
9
  header_text 'Daily View'
10
10
  description_text <<-HTML
11
11
  <div class="p">
12
- This view shows all the items you'll want to discuss during your daily coordination meeting
12
+ This view shows all the items (<%= aging_issues.count %>) you'll want to discuss during your daily
13
+ coordination meeting
13
14
  (aka daily scrum, standup), in the order that you should be discussing them. The most important
14
15
  items are at the top, and the least at the bottom.
15
16
  </div>
@@ -23,7 +24,7 @@ class DailyView < ChartBase
23
24
  def run
24
25
  aging_issues = select_aging_issues
25
26
 
26
- return "<h1>#{@header_text}</h1>There are no items currently in progress" if aging_issues.empty?
27
+ return "<h1 class='foldable'>#{@header_text}</h1><div>There are no items currently in progress</div>" if aging_issues.empty?
27
28
 
28
29
  result = +''
29
30
  result << render_top_text(binding)
@@ -35,7 +36,7 @@ class DailyView < ChartBase
35
36
 
36
37
  def select_aging_issues
37
38
  aging_issues = issues.select do |issue|
38
- started_at, stopped_at = issue.board.cycletime.started_stopped_times(issue)
39
+ started_at, stopped_at = issue.started_stopped_times
39
40
  started_at && !stopped_at
40
41
  end
41
42
 
@@ -72,13 +73,13 @@ class DailyView < ChartBase
72
73
 
73
74
  def make_blocked_stalled_lines issue
74
75
  today = date_range.end
75
- started_date = issue.board.cycletime.started_stopped_times(issue).first&.to_date
76
+ started_date = issue.started_stopped_times.first&.to_date
76
77
  return [] unless started_date
77
78
 
78
79
  blocked_stalled = issue.blocked_stalled_by_date(
79
80
  date_range: today..today, chart_end_time: time_range.end, settings: settings
80
81
  )[today]
81
- return [] unless blocked_stalled
82
+ return [] if blocked_stalled.active?
82
83
 
83
84
  lines = []
84
85
  if blocked_stalled.blocked?
@@ -86,9 +87,15 @@ class DailyView < ChartBase
86
87
  lines << ["#{marker} Blocked by flag"] if blocked_stalled.flag
87
88
  lines << ["#{marker} Blocked by status: #{blocked_stalled.status}"] if blocked_stalled.blocked_by_status?
88
89
  blocked_stalled.blocking_issue_keys&.each do |key|
89
- lines << ["#{marker} Blocked by issue: #{key}"]
90
- blocking_issue = issues.find { |i| i.key == key }
91
- lines << blocking_issue if blocking_issue
90
+ blocking_issue = issues.find_by_key key: key, include_hidden: true
91
+ if blocking_issue
92
+ lines << "<section><div class=\"foldable startFolded\">#{marker} Blocked by issue: " \
93
+ "#{make_issue_label issue: blocking_issue, done: blocking_issue.done?}</div>"
94
+ lines << blocking_issue
95
+ lines << '</section>'
96
+ else
97
+ lines << ["#{marker} Blocked by issue: #{key} (no description found)"]
98
+ end
92
99
  end
93
100
  elsif blocked_stalled.stalled_by_status?
94
101
  lines << ["#{color_block '--stalled-color'} Stalled by status: #{blocked_stalled.status}"]
@@ -98,15 +105,18 @@ class DailyView < ChartBase
98
105
  lines
99
106
  end
100
107
 
101
- def make_issue_label issue
102
- "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> " \
103
- "<b><a href='#{issue.url}'>#{issue.key}</a></b> &nbsp;<i>#{issue.summary}</i>"
108
+ def make_issue_label issue:, done:
109
+ label = "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
110
+ label << '<s>' if done
111
+ label << "<b><a href='#{issue.url}'>#{issue.key}</a></b> &nbsp;<i>#{issue.summary}</i>"
112
+ label << '</s>' if done
113
+ label
104
114
  end
105
115
 
106
- def make_title_line issue
116
+ def make_title_line issue:, done:
107
117
  title_line = +''
108
118
  title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
109
- title_line << make_issue_label(issue)
119
+ title_line << make_issue_label(issue: issue, done: done)
110
120
  title_line
111
121
  end
112
122
 
@@ -115,20 +125,25 @@ class DailyView < ChartBase
115
125
  parent_key = issue.parent_key
116
126
  if parent_key
117
127
  parent = issues.find_by_key key: parent_key, include_hidden: true
118
- text = parent ? make_issue_label(parent) : parent_key
128
+ text = parent ? make_issue_label(issue: parent, done: parent.done?) : parent_key
119
129
  lines << ["Parent: #{text}"]
120
130
  end
121
131
  lines
122
132
  end
123
133
 
124
- def make_stats_lines issue
134
+ def make_stats_lines issue:, done:
125
135
  line = []
126
136
 
127
137
  line << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
128
138
 
129
- age = issue.board.cycletime.age(issue, today: date_range.end)
130
- line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
139
+ if done
140
+ cycletime = issue.board.cycletime.cycletime(issue)
131
141
 
142
+ line << "Cycletime: <b>#{label_days cycletime}</b>"
143
+ else
144
+ age = issue.board.cycletime.age(issue, today: date_range.end)
145
+ line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
146
+ end
132
147
  line << "Status: <b>#{format_status issue.status, board: issue.board}</b>"
133
148
 
134
149
  column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
@@ -138,7 +153,18 @@ class DailyView < ChartBase
138
153
  line << "Assignee: <img src='#{issue.assigned_to_icon_url}' class='icon' /> <b>#{issue.assigned_to}</b>"
139
154
  end
140
155
 
141
- line << "Due: <b>#{issue.due_date}</b>" if issue.due_date
156
+ if issue.due_date
157
+ today = date_range.end
158
+ days = (issue.due_date - today).to_i
159
+ relative =
160
+ if days.zero? then 'today'
161
+ elsif days.positive? then "in #{label_days days}"
162
+ else "#{label_days(-days)} ago"
163
+ end
164
+ content = "#{issue.due_date} (#{relative})"
165
+ content = "<span style='background: var(--warning-banner)'>#{content}</span>" if days.negative?
166
+ line << "Due: <b>#{content}</b>"
167
+ end
142
168
 
143
169
  block = lambda do |collection, label|
144
170
  unless collection.empty?
@@ -154,45 +180,26 @@ class DailyView < ChartBase
154
180
 
155
181
  def make_child_lines issue
156
182
  lines = []
157
- subtasks = issue.subtasks.reject { |i| i.done? }
183
+ subtasks = issue.subtasks
158
184
 
159
- unless subtasks.empty?
160
- icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
161
- lines << (icon_urls << 'Incomplete child issues')
162
- lines += subtasks
163
- end
164
- lines
165
- end
185
+ return lines if subtasks.empty?
166
186
 
167
- def jira_rich_text_to_html text
168
- text
169
- .gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
170
- .gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
171
- .gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
172
- .gsub("\n", '<br />')
173
- end
187
+ lines << "<section><div class=\"foldable startFolded\">Child issues (#{subtasks.count})</div>"
188
+ lines += subtasks
189
+ lines << '</section>'
174
190
 
175
- def expand_account_id account_id
176
- user = @users.find { |u| u.account_id == account_id }
177
- text = account_id
178
- text = "@#{user.display_name}" if user
179
- "<span class='account_id'>#{text}</span>"
191
+ lines
180
192
  end
181
193
 
182
194
  def make_history_lines issue
183
195
  history = issue.changes.reverse
184
196
  lines = []
185
197
 
186
- id = next_id
187
- lines << [
188
- "<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'table#{id}');\">" \
189
- "<span id='open#{id}'>▶ Issue History</span>" \
190
- "<span id='close#{id}' style='display: none'>▼ Issue History</span></a>"
191
- ]
198
+ lines << '<section><div class="foldable startFolded">Issue history</div>'
192
199
  table = +''
193
- table << "<table id='table#{id}' style='display: none'>"
200
+ table << '<table>'
194
201
  history.each do |c|
195
- time = c.time.strftime '%b %d, %I:%M%P'
202
+ time = c.time.strftime '%b %d, %Y @ %I:%M%P'
196
203
 
197
204
  table << '<tr>'
198
205
  table << "<td><span class='time' title='Timestamp: #{c.time}'>#{time}</span></td>"
@@ -203,23 +210,24 @@ class DailyView < ChartBase
203
210
  end
204
211
  table << '</table>'
205
212
  lines << [table]
213
+ lines << '</section>'
206
214
  lines
207
215
  end
208
216
 
209
217
  def history_text change:, board:
210
- if change.comment?
211
- jira_rich_text_to_html(change.value)
212
- elsif change.status?
213
- convertor = ->(id) { format_status(board.possible_statuses.find_by_id(id), board: board) }
214
- to = convertor.call(change.value_id)
218
+ convertor = ->(value, _id) { value.inspect }
219
+ convertor = ->(_value, id) { format_status(board.possible_statuses.find_by_id(id), board: board) } if change.status?
220
+
221
+ if change.comment? || change.description?
222
+ atlassian_document_format.to_html(change.value)
223
+ elsif %w[status priority assignee duedate issuetype].include?(change.field)
224
+ to = convertor.call(change.value, change.value_id)
215
225
  if change.old_value
216
- from = convertor.call(change.old_value_id)
226
+ from = convertor.call(change.old_value, change.old_value_id)
217
227
  "Changed from #{from} to #{to}"
218
228
  else
219
229
  "Set to #{to}"
220
230
  end
221
- elsif %w[priority assignee duedate issuetype].include?(change.field)
222
- "Changed from \"#{change.old_value}\" to \"#{change.value}\""
223
231
  elsif change.flagged?
224
232
  change.value == '' ? 'Off' : 'On'
225
233
  else
@@ -245,16 +253,31 @@ class DailyView < ChartBase
245
253
  .join(' ')]]
246
254
  end
247
255
 
256
+ def make_description_lines issue
257
+ description = issue.raw['fields']['description']
258
+ return [] unless description
259
+
260
+ text = "<div class='foldable startFolded'>Description</div>" \
261
+ "<div>#{atlassian_document_format.to_html(description)}</div>"
262
+ [[text]]
263
+ end
264
+
248
265
  def assemble_issue_lines issue, child:
266
+ done = issue.done?
267
+
249
268
  lines = []
250
- lines << [make_title_line(issue)]
269
+ lines << [make_title_line(issue: issue, done: done)]
270
+ lines << make_not_visible_line(issue)
251
271
  lines += make_parent_lines(issue) unless child
252
- lines += make_stats_lines(issue)
253
- lines += make_sprints_lines(issue)
254
- lines += make_blocked_stalled_lines(issue)
255
- lines += make_child_lines(issue)
256
- lines += make_history_lines(issue)
257
- lines
272
+ lines += make_stats_lines(issue: issue, done: done)
273
+ unless done
274
+ lines += make_description_lines(issue)
275
+ lines += make_sprints_lines(issue)
276
+ lines += make_blocked_stalled_lines(issue)
277
+ lines += make_child_lines(issue)
278
+ lines += make_history_lines(issue)
279
+ end
280
+ lines.compact
258
281
  end
259
282
 
260
283
  def render_issue issue, child:
@@ -264,6 +287,8 @@ class DailyView < ChartBase
264
287
  assemble_issue_lines(issue, child: child).each do |row|
265
288
  if row.is_a? Issue
266
289
  result << render_issue(row, child: true)
290
+ elsif row.is_a?(String)
291
+ result << row
267
292
  else
268
293
  result << '<div class="heading">'
269
294
  row.each do |chunk|
@@ -274,4 +299,8 @@ class DailyView < ChartBase
274
299
  end
275
300
  result << '</div>'
276
301
  end
302
+
303
+ def make_not_visible_line issue
304
+ not_visible_text issue
305
+ end
277
306
  end
@@ -49,9 +49,7 @@ class DailyWipByAgeChart < DailyWipChart
49
49
  end
50
50
 
51
51
  def default_grouping_rules issue:, rules:
52
- started, stopped = issue.board.cycletime.started_stopped_dates(issue)
53
-
54
- rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
52
+ started, stopped = issue.started_stopped_dates
55
53
 
56
54
  if stopped && started.nil? # We can't tell when it started
57
55
  @has_completed_but_not_started = true
@@ -72,7 +70,7 @@ class DailyWipByAgeChart < DailyWipChart
72
70
  rules.label = 'Start date unknown'
73
71
  rules.color = '--body-background'
74
72
  rules.group_priority = 11
75
- created_days = rules.current_date - created + 1
73
+ created_days = rules.current_date - created
76
74
  rules.issue_hint = "(created: #{label_days created_days.to_i} earlier, stopped on #{stopped})"
77
75
  end
78
76
  end
@@ -84,7 +82,8 @@ class DailyWipByAgeChart < DailyWipChart
84
82
  end
85
83
 
86
84
  def group_by_age started:, rules:
87
- age = rules.current_date - started + 1
85
+ age = (rules.current_date - started).to_i + 1
86
+ rules.issue_hint = "(age: #{label_days age})"
88
87
 
89
88
  case age
90
89
  when 1
@@ -39,23 +39,32 @@ class DailyWipByBlockedStalledChart < DailyWipChart
39
39
  end
40
40
 
41
41
  def default_grouping_rules issue:, rules:
42
- started, stopped = issue.board.cycletime.started_stopped_times(issue)
42
+ started, stopped = issue.started_stopped_times
43
43
  stopped_date = stopped&.to_date
44
+ started_date = started&.to_date
44
45
 
45
46
  date = rules.current_date
46
47
  change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
47
-
48
48
  stopped_today = stopped_date == rules.current_date
49
49
 
50
+ days = nil
51
+ if started_date && stopped_date
52
+ days = (stopped_date - started_date).to_i + 1 # cycletime
53
+ elsif started_date
54
+ days = (time_range.end.to_date - started_date).to_i + 1 # age
55
+ end
56
+
50
57
  if stopped_today && started.nil?
51
58
  @has_completed_but_not_started = true
52
59
  rules.label = 'Completed but not started'
53
60
  rules.color = '--wip-chart-completed-but-not-started-color'
54
61
  rules.group_priority = -1
62
+ rules.issue_hint = '(Cycle time: Unknown)'
55
63
  elsif stopped_today
56
64
  rules.label = 'Completed'
57
65
  rules.color = '--wip-chart-completed-color'
58
66
  rules.group_priority = -2
67
+ rules.issue_hint = "(Cycle time: #{label_days days})"
59
68
  elsif started.nil?
60
69
  rules.label = 'Start date unknown'
61
70
  rules.color = '--body-background'
@@ -64,16 +73,17 @@ class DailyWipByBlockedStalledChart < DailyWipChart
64
73
  rules.label = 'Blocked'
65
74
  rules.color = '--blocked-color'
66
75
  rules.group_priority = 1
67
- rules.issue_hint = "(#{change.reasons})"
76
+ rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
68
77
  elsif change&.stalled?
69
78
  rules.label = 'Stalled'
70
79
  rules.color = '--stalled-color'
71
80
  rules.group_priority = 2
72
- rules.issue_hint = "(#{change.reasons})"
81
+ rules.issue_hint = "(Age: #{label_days days}, #{change.reasons})"
73
82
  else
74
83
  rules.label = 'Active'
75
84
  rules.color = '--wip-chart-active-color'
76
85
  rules.group_priority = 3
86
+ rules.issue_hint = "(Age: #{label_days days})"
77
87
  end
78
88
  end
79
89
  end
@@ -26,11 +26,13 @@ class DailyWipByParentChart < DailyWipChart
26
26
  end
27
27
 
28
28
  def default_grouping_rules issue:, rules:
29
- parent = issue.parent&.key
29
+ parent = issue.parent
30
30
  if parent
31
- rules.label = parent
31
+ rules.label = parent.key
32
+ rules.label_hint = "#{parent.key} : #{parent.summary}"
32
33
  else
33
34
  rules.label = 'No parent'
35
+ rules.label_hint = 'No parent'
34
36
  rules.group_priority = 1000
35
37
  rules.color = '--body-background'
36
38
  end
@@ -3,12 +3,16 @@
3
3
  require 'jirametrics/chart_base'
4
4
 
5
5
  class DailyGroupingRules < GroupingRules
6
- attr_accessor :current_date, :group_priority, :issue_hint
6
+ attr_accessor :current_date, :group_priority, :issue_hint, :highlight
7
7
 
8
8
  def initialize
9
9
  super
10
10
  @group_priority = 0
11
11
  end
12
+
13
+ def group
14
+ [@label, @color, @highlight ? true : false]
15
+ end
12
16
  end
13
17
 
14
18
  class DailyWipChart < ChartBase
@@ -19,6 +23,8 @@ class DailyWipChart < ChartBase
19
23
 
20
24
  header_text default_header_text
21
25
  description_text default_description_text
26
+ @x_axis_title = nil
27
+ @y_axis_title = 'Count of items'
22
28
 
23
29
  instance_eval(&block) if block
24
30
 
@@ -33,8 +39,15 @@ class DailyWipChart < ChartBase
33
39
  issue_rules_by_active_date = group_issues_by_active_dates
34
40
  possible_rules = select_possible_rules issue_rules_by_active_date
35
41
 
42
+ conflicting_labels = possible_rules
43
+ .group_by(&:label)
44
+ .select { |_label, rules| rules.any?(&:highlight) && rules.any? { |r| !r.highlight } }
45
+ .keys
46
+
36
47
  data_sets = possible_rules.collect do |grouping_rule|
37
- make_data_set grouping_rule: grouping_rule, issue_rules_by_active_date: issue_rules_by_active_date
48
+ suffix = conflicting_labels.include?(grouping_rule.label) && grouping_rule.highlight ? '*' : ''
49
+ make_data_set grouping_rule: grouping_rule, issue_rules_by_active_date: issue_rules_by_active_date,
50
+ label_suffix: suffix
38
51
  end
39
52
  if @trend_lines
40
53
  data_sets = @trend_lines.filter_map do |group_labels, line_color|
@@ -66,7 +79,7 @@ class DailyWipChart < ChartBase
66
79
  hash = {}
67
80
 
68
81
  @issues.each do |issue|
69
- start, stop = issue.board.cycletime.started_stopped_dates(issue)
82
+ start, stop = cycletime_for_issue(issue).started_stopped_dates(issue)
70
83
  next if start.nil? && stop.nil?
71
84
 
72
85
  # If it stopped but never started then assume it started at creation so the data points
@@ -82,16 +95,17 @@ class DailyWipChart < ChartBase
82
95
  hash
83
96
  end
84
97
 
85
- def make_data_set grouping_rule:, issue_rules_by_active_date:
98
+ def make_data_set grouping_rule:, issue_rules_by_active_date:, label_suffix: ''
86
99
  positive = grouping_rule.group_priority >= 0
100
+ display_label = "#{grouping_rule.label}#{label_suffix}"
87
101
 
88
102
  data = issue_rules_by_active_date.collect do |date, issue_rules|
89
- # issues = []
90
103
  issue_strings = issue_rules
91
104
  .select { |_issue, rules| rules.group == grouping_rule.group }
92
105
  .sort_by { |issue, _rules| issue.key_as_i }
93
106
  .collect { |issue, rules| "#{issue.key} : #{issue.summary.strip} #{rules.issue_hint}" }
94
- title = ["#{grouping_rule.label} (#{label_issues issue_strings.size})"] + issue_strings
107
+ title_label = grouping_rule.label_hint || display_label
108
+ title = ["#{title_label} (#{label_issues issue_strings.size})"] + issue_strings
95
109
 
96
110
  {
97
111
  x: date,
@@ -100,11 +114,19 @@ class DailyWipChart < ChartBase
100
114
  }
101
115
  end
102
116
 
117
+ color = grouping_rule.color || random_color
118
+ background_color = if grouping_rule.highlight
119
+ RawJavascript.new("createDiagonalPattern(#{color.to_json})")
120
+ else
121
+ color
122
+ end
123
+
103
124
  {
104
125
  type: 'bar',
105
- label: grouping_rule.label,
126
+ label: display_label,
127
+ label_hint: grouping_rule.label_hint,
106
128
  data: data,
107
- backgroundColor: grouping_rule.color || random_color,
129
+ backgroundColor: background_color,
108
130
  borderColor: CssVariable['--wip-chart-border-color'],
109
131
  borderWidth: grouping_rule.color.to_s == 'var(--body-background)' ? 1 : 0,
110
132
  borderRadius: positive ? 0 : 5