jirametrics 2.9 → 2.12.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +1 -1
  3. data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
  4. data/lib/jirametrics/aging_work_table.rb +56 -13
  5. data/lib/jirametrics/board.rb +38 -10
  6. data/lib/jirametrics/board_config.rb +1 -0
  7. data/lib/jirametrics/board_movement_calculator.rb +155 -0
  8. data/lib/jirametrics/change_item.rb +37 -16
  9. data/lib/jirametrics/chart_base.rb +22 -5
  10. data/lib/jirametrics/css_variable.rb +1 -1
  11. data/lib/jirametrics/cycletime_config.rb +1 -1
  12. data/lib/jirametrics/cycletime_histogram.rb +65 -2
  13. data/lib/jirametrics/daily_view.rb +277 -0
  14. data/lib/jirametrics/data_quality_report.rb +1 -1
  15. data/lib/jirametrics/downloader.rb +11 -14
  16. data/lib/jirametrics/estimate_accuracy_chart.rb +34 -10
  17. data/lib/jirametrics/estimation_configuration.rb +25 -0
  18. data/lib/jirametrics/examples/standard_project.rb +2 -0
  19. data/lib/jirametrics/exporter.rb +10 -8
  20. data/lib/jirametrics/file_config.rb +10 -5
  21. data/lib/jirametrics/file_system.rb +4 -0
  22. data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
  23. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
  24. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
  25. data/lib/jirametrics/html/aging_work_table.erb +7 -3
  26. data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
  27. data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
  28. data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
  29. data/lib/jirametrics/html/expedited_chart.erb +1 -10
  30. data/lib/jirametrics/html/index.css +82 -2
  31. data/lib/jirametrics/html/index.erb +25 -1
  32. data/lib/jirametrics/html/sprint_burndown.erb +1 -10
  33. data/lib/jirametrics/html/throughput_chart.erb +1 -10
  34. data/lib/jirametrics/html_report_config.rb +2 -0
  35. data/lib/jirametrics/issue.rb +68 -27
  36. data/lib/jirametrics/issue_collection.rb +33 -0
  37. data/lib/jirametrics/jira_gateway.rb +20 -4
  38. data/lib/jirametrics/project_config.rb +25 -8
  39. data/lib/jirametrics/settings.json +2 -1
  40. data/lib/jirametrics/sprint.rb +1 -0
  41. data/lib/jirametrics/sprint_burndown.rb +35 -33
  42. data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
  43. data/lib/jirametrics/status.rb +3 -6
  44. data/lib/jirametrics/status_collection.rb +6 -0
  45. data/lib/jirametrics/user.rb +12 -0
  46. data/lib/jirametrics.rb +4 -0
  47. metadata +7 -2
@@ -22,15 +22,18 @@ class EstimateAccuracyChart < ChartBase
22
22
  </div>
23
23
  HTML
24
24
 
25
- @y_axis_label = 'Story Point Estimates'
26
25
  @y_axis_type = 'linear'
27
- @y_axis_block = ->(issue, start_time) { story_points_at(issue: issue, start_time: start_time)&.to_f }
26
+ @y_axis_block = ->(issue, start_time) { estimate_at(issue: issue, start_time: start_time)&.to_f }
28
27
  @y_axis_sort_order = nil
29
28
 
30
29
  instance_eval(&configuration_block)
31
30
  end
32
31
 
33
32
  def run
33
+ if @y_axis_label.nil?
34
+ text = current_board.estimation_configuration.units == :story_points ? 'Story Points' : 'Days'
35
+ @y_axis_label = "Estimated #{text}"
36
+ end
34
37
  data_sets = scan_issues
35
38
 
36
39
  return '' if data_sets.empty?
@@ -41,6 +44,7 @@ class EstimateAccuracyChart < ChartBase
41
44
  def scan_issues
42
45
  completed_hash, aging_hash = split_into_completed_and_aging issues: issues
43
46
 
47
+ estimation_units = current_board.estimation_configuration.units
44
48
  @has_aging_data = !aging_hash.empty?
45
49
 
46
50
  [
@@ -53,9 +57,13 @@ class EstimateAccuracyChart < ChartBase
53
57
  # We sort so that the smaller circles are in front of the bigger circles.
54
58
  data = hash.sort(&hash_sorter).collect do |key, values|
55
59
  estimate, cycle_time = *key
56
- estimate_label = "#{estimate}#{'pts' if @y_axis_type == 'linear'}"
57
- title = ["Estimate: #{estimate_label}, Cycletime: #{label_days(cycle_time)}, #{values.size} issues"] +
58
- values.collect { |issue| "#{issue.key}: #{issue.summary}" }
60
+
61
+ title = [
62
+ "Estimate: #{estimate_label(estimate: estimate, estimation_units: estimation_units)}, " \
63
+ "Cycletime: #{label_days(cycle_time)}, " \
64
+ "#{values.size} issues"
65
+ ] + values.collect { |issue| "#{issue.key}: #{issue.summary}" }
66
+
59
67
  {
60
68
  'x' => cycle_time,
61
69
  'y' => estimate,
@@ -77,6 +85,18 @@ class EstimateAccuracyChart < ChartBase
77
85
  end
78
86
  end
79
87
 
88
+ def estimate_label estimate:, estimation_units:
89
+ if @y_axis_type == 'linear'
90
+ if estimation_units == :story_points
91
+ estimate_label = "#{estimate}pts"
92
+ elsif estimation_units == :seconds
93
+ estimate_label = label_days estimate
94
+ end
95
+ end
96
+ estimate_label = estimate.to_s if estimate_label.nil?
97
+ estimate_label
98
+ end
99
+
80
100
  def split_into_completed_and_aging issues:
81
101
  aging_hash = {}
82
102
  completed_hash = {}
@@ -126,14 +146,18 @@ class EstimateAccuracyChart < ChartBase
126
146
  end
127
147
  end
128
148
 
129
- def story_points_at issue:, start_time:
130
- story_points = nil
149
+ def estimate_at issue:, start_time:, estimation_configuration: current_board.estimation_configuration
150
+ estimate = nil
151
+
131
152
  issue.changes.each do |change|
132
- return story_points if change.time >= start_time
153
+ return estimate if change.time >= start_time
133
154
 
134
- story_points = change.value if change.story_points?
155
+ if change.field == estimation_configuration.display_name || change.field == estimation_configuration.field_id
156
+ estimate = change.value
157
+ estimate = estimate.to_f / (24 * 60 * 60) if estimation_configuration.units == :seconds
158
+ end
135
159
  end
136
- story_points
160
+ estimate
137
161
  end
138
162
 
139
163
  def y_axis label:, sort_order: nil, &block
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EstimationConfiguration
4
+ attr_reader :units, :display_name, :field_id
5
+
6
+ def initialize raw:
7
+ @units = :story_points
8
+ @display_name = 'Story Points'
9
+
10
+ # If there wasn't an estimation section they rely on all defaults
11
+ return if raw.nil?
12
+
13
+ if raw['type'] == 'field'
14
+ @field_id = raw['field']['fieldId']
15
+ @display_name = raw['field']['displayName']
16
+ if @field_id == 'timeoriginalestimate'
17
+ @units = :seconds
18
+ @display_name = 'Original estimate'
19
+ end
20
+ elsif raw['type'] == 'issueCount'
21
+ @display_name = 'Issue Count'
22
+ @units = :issue_count
23
+ end
24
+ end
25
+ end
@@ -58,6 +58,8 @@ class Exporter
58
58
  type: :header
59
59
  end
60
60
 
61
+ daily_view
62
+
61
63
  cycletime_scatterplot do
62
64
  show_trend_lines
63
65
  end
@@ -64,12 +64,8 @@ class Exporter
64
64
  selected = []
65
65
  each_project_config(name_filter: name_filter) do |project|
66
66
  project.evaluate_next_level
67
- # next if project.aggregated_project?
68
67
 
69
68
  project.run load_only: true
70
- project.board_configs.each do |board_config|
71
- board_config.run
72
- end
73
69
  project.issues.each do |issue|
74
70
  selected << [project, issue] if keys.include? issue.key
75
71
  end
@@ -79,9 +75,13 @@ class Exporter
79
75
  raise unless e.message.start_with? 'This is an aggregated project and issues should have been included'
80
76
  end
81
77
 
82
- selected.each do |project, issue|
83
- puts "\nProject #{project.name}"
84
- puts issue.dump
78
+ if selected.empty?
79
+ file_system.log "No issues found to match #{keys.collect(&:inspect).join(', ')}"
80
+ else
81
+ selected.each do |project, issue|
82
+ file_system.log "\nProject #{project.name}", also_write_to_stderr: true
83
+ file_system.log issue.dump, also_write_to_stderr: true
84
+ end
85
85
  end
86
86
  end
87
87
 
@@ -116,7 +116,9 @@ class Exporter
116
116
 
117
117
  def jira_config filename = nil
118
118
  if filename
119
- @jira_config = file_system.load_json(filename)
119
+ @jira_config = file_system.load_json(filename, fail_on_error: false)
120
+ raise "Unable to load Jira configuration file and cannot continue: #{filename.inspect}" if @jira_config.nil?
121
+
120
122
  @jira_config['url'] = $1 if @jira_config['url'] =~ /^(.+)\/+$/
121
123
  end
122
124
  @jira_config
@@ -13,7 +13,7 @@ class FileConfig
13
13
  end
14
14
 
15
15
  def run
16
- @issues = project_config.issues.dup
16
+ @issues = project_config.issues
17
17
  instance_eval(&@block)
18
18
 
19
19
  if @columns
@@ -66,15 +66,20 @@ class FileConfig
66
66
  # is that all empty values in the first column should be at the bottom.
67
67
  def sort_output all_lines
68
68
  all_lines.sort do |a, b|
69
+ result = nil
69
70
  if a[0] == b[0]
70
- a[1..] <=> b[1..]
71
+ result = a[1..] <=> b[1..]
71
72
  elsif a[0].nil?
72
- 1
73
+ result = 1
73
74
  elsif b[0].nil?
74
- -1
75
+ result = -1
75
76
  else
76
- a[0] <=> b[0]
77
+ result = a[0] <=> b[0]
77
78
  end
79
+
80
+ # This will only happen if one of the objects isn't comparable. Seen in production.
81
+ result = -1 if result.nil?
82
+ result
78
83
  end
79
84
  end
80
85
 
@@ -35,6 +35,10 @@ class FileSystem
35
35
  log "Warning: #{message}", more: more, also_write_to_stderr: true
36
36
  end
37
37
 
38
+ def error message, more: nil
39
+ log "Error: #{message}", more: more, also_write_to_stderr: true
40
+ end
41
+
38
42
  def log message, more: nil, also_write_to_stderr: false
39
43
  message += " See #{logfile_name} for more details about this message." if more
40
44
 
@@ -27,7 +27,7 @@ class FlowEfficiencyScatterplot < ChartBase
27
27
  </mfrac>
28
28
  </math>
29
29
  </div>
30
- <div style="background: yellow">Note that for this calculation to be accurate, we must be moving items into a
30
+ <div style="background: var(--warning-banner)">Note that for this calculation to be accurate, we must be moving items into a
31
31
  blocked or stalled state the moment we stop working on it, and most teams don't do that.
32
32
  So be aware that your team may have to change their behaviours if you want this chart to be useful.
33
33
  </div>
@@ -38,22 +38,13 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
38
38
  plugins: {
39
39
  annotation: {
40
40
  annotations: {
41
- <% holidays.each_with_index do |range, index| %>
42
- holiday<%= index %>: {
43
- drawTime: 'beforeDraw',
44
- type: 'box',
45
- xMin: '<%= range.begin %>T00:00:00',
46
- xMax: '<%= range.end %>T23:59:59',
47
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
48
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
49
- },
50
- <% end %>
41
+ <%= working_days_annotation %>
51
42
 
52
43
  <% if percentage_line_x %>
53
44
  line: {
54
45
  type: 'line',
55
- xMin: '<%= percentage_line_x %>',
56
- xMax: '<%= percentage_line_x %>',
46
+ scaleID: 'x',
47
+ value: '<%= percentage_line_x %>',
57
48
  borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
58
49
  borderWidth: 1,
59
50
  drawTime: 'afterDraw'
@@ -6,7 +6,7 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
6
6
  {
7
7
  type: 'bar',
8
8
  data: {
9
- labels: [<%= column_headings.collect(&:inspect).join(',') %>],
9
+ labels: [<%= @board_columns.collect { |c| c.name.inspect }.join(',') %>],
10
10
  datasets: <%= JSON.generate(data_sets) %>
11
11
  },
12
12
  options: {
@@ -22,8 +22,10 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
22
22
  labelString: 'Date Completed'
23
23
  },
24
24
  grid: {
25
- color: <%= CssVariable['--grid-line-color'].to_json %>
25
+ color: <%= CssVariable['--grid-line-color'].to_json %>,
26
+ z: 1 // draw the grid lines on top of the bars
26
27
  },
28
+ stacked: true
27
29
  },
28
30
  y: {
29
31
  scaleLabel: {
@@ -35,8 +37,11 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
35
37
  text: 'Age in days'
36
38
  },
37
39
  grid: {
38
- color: <%= CssVariable['--grid-line-color'].to_json %>
40
+ color: <%= CssVariable['--grid-line-color'].to_json %>,
41
+ z: 1 // draw the grid lines on top of the bars
39
42
  },
43
+ stacked: true,
44
+ max: <%= (@max_age * 1.1).to_i %>
40
45
  }
41
46
  },
42
47
  plugins: {
@@ -44,14 +49,26 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
44
49
  callbacks: {
45
50
  label: function(context) {
46
51
  if( typeof(context.dataset.data[context.dataIndex]) == "number" ) {
47
- return "85% of the issues, leave this column in "+context.dataset.data[context.dataIndex]+" days";
52
+ let full_data = <%= @bar_data.inspect %>;
53
+ let columnIndex = context.dataIndex;
54
+ let rowIndex = context.datasetIndex - <%= @row_index_offset %>;
55
+ return context.dataset.label + " of completed work items left this column in " +full_data[rowIndex][columnIndex] + " days or less";
48
56
  }
49
57
  else {
50
- return context.dataset.data[context.dataIndex].title
58
+ return context.dataset.data[context.dataIndex].title;
51
59
  }
52
60
  }
53
61
  }
62
+ },
63
+ legend: {
64
+ labels: {
65
+ filter: function(item, chart) {
66
+ // Logic to remove a particular legend item goes here
67
+ return !item.text.includes('%');
68
+ }
69
+ }
54
70
  }
71
+
55
72
  }
56
73
  }
57
74
  });
@@ -1,11 +1,13 @@
1
1
  <table class='standard'>
2
2
  <thead>
3
3
  <tr>
4
- <th>Age (days)</th>
5
- <th>E</th>
6
- <th>B</th>
4
+ <th title="Age in days">Age</th>
5
+ <th title="Expedited">E</th>
6
+ <th title="Blocked / Stalled">B/S</th>
7
+ <th title="Priority">P</th>
7
8
  <th>Issue</th>
8
9
  <th>Status</th>
10
+ <th>Forecast</th>
9
11
  <th>Fix versions</th>
10
12
  <% if any_scrum_boards %>
11
13
  <th>Sprints</th>
@@ -28,6 +30,7 @@
28
30
  <td style="text-align: right;"><%= issue_age || 'Not started' %></td>
29
31
  <td><%= expedited_text(issue) %></td>
30
32
  <td><%= blocked_text(issue) %></td>
33
+ <td><%= priority_text(issue) %></td>
31
34
  <td>
32
35
  <% parent_hierarchy(issue).each_with_index do |parent, index| %>
33
36
  <% color = parent != issue ? "var(--hierarchy-table-inactive-item-text-color)" : 'var(--default-text-color)' %>
@@ -41,6 +44,7 @@
41
44
  <% end %>
42
45
  </td>
43
46
  <td><%= format_status issue.status, board: issue.board %></td>
47
+ <td><%= dates_text(issue) %></td>
44
48
  <td><%= fix_versions_text(issue) %></td>
45
49
  <% if any_scrum_boards %>
46
50
  <td><%= sprints_text(issue) %></td>
@@ -1,6 +1,57 @@
1
1
  <div class="chart">
2
2
  <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
3
3
  </div>
4
+ <%
5
+ if show_stats
6
+ link_id = next_id
7
+ issues_id = next_id
8
+ %>
9
+ [<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
10
+ <div id="<%= issues_id %>" style="display: none;">
11
+ <div>
12
+ <table class="standard">
13
+ <tr>
14
+ <th>Issue Type</th>
15
+ <th>Min</th>
16
+ <th>Max</th>
17
+ <th>Avg</th>
18
+ <th>Mode</th>
19
+ <% percentiles.each do |p| %>
20
+ <th><%= p %>th</th>
21
+ <% end %>
22
+ </tr>
23
+ <% the_stats.each do |k, v| %>
24
+ <tr>
25
+ <td><%= k %></td>
26
+ <td style="text-align: right;"><%= v[:min] %></td>
27
+ <td style="text-align: right;"><%= v[:max] %></td>
28
+ <td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
29
+ <td><%= v[:mode].join(', ') %></td>
30
+ <% percentiles.each do |p| %>
31
+ <td style="text-align: right;"><%= v[:percentiles][p] %></td>
32
+ <% end %>
33
+ </tr>
34
+ <% end %>
35
+ </table>
36
+ </div>
37
+ <div>
38
+ <p>These statistics help understand the <i>"shape"</i> of the cycletime histogram distribution, to help us with predictions.</p>
39
+ <ul>
40
+ <li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
41
+ <li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
42
+ <li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
43
+ <li><b>Percentiles:</b> they partition the data set. If X is the Nth percentile, it means that N% of cycletime values are X or less. Typical percentiles of interest are:</li>
44
+ <ul>
45
+ <li><b>50%</b>: also known as the <b>Median</b>. Useful to establish short feedback loops, to monitor that it's not drifting to the right.</li>
46
+ <li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
47
+ <li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
48
+ </ul>
49
+ </ul>
50
+ </div>
51
+ </div>
52
+ <%
53
+ end
54
+ %>
4
55
  <script>
5
56
  new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
6
57
  {
@@ -21,6 +72,8 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
21
72
  grid: {
22
73
  color: <%= CssVariable['--grid-line-color'].to_json %>
23
74
  },
75
+ min: 0,
76
+ offset: false, // Gets rid of the ugly padding on left.
24
77
  },
25
78
  y: {
26
79
  stacked: true,
@@ -34,6 +87,27 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
34
87
  }
35
88
  },
36
89
  plugins: {
90
+ annotation: {
91
+ annotations: {
92
+ <%
93
+ results = the_stats[:all][:percentiles]
94
+ results.each do |percentile, value|
95
+ %>
96
+ percentile<%= percentile.to_s %>: {
97
+ type: 'line',
98
+ scaleID: 'x',
99
+ value: <%= value %>,
100
+ borderWidth: 1,
101
+ drawTime: 'beforeDatasetsDraw',
102
+ label: {
103
+ enabled: true,
104
+ content: '<%= "#{percentile}%" %>',
105
+ position: 'start',
106
+ }
107
+ },
108
+ <% end %>
109
+ },
110
+ },
37
111
  tooltip: {
38
112
  callbacks: {
39
113
  label: function(context) {
@@ -53,16 +53,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
53
53
  autocolors: false,
54
54
  annotation: {
55
55
  annotations: {
56
- <% holidays.each_with_index do |range, index| %>
57
- holiday<%= index %>: {
58
- drawTime: 'beforeDraw',
59
- type: 'box',
60
- xMin: '<%= range.begin %>T00:00:00',
61
- xMax: '<%= range.end %>T23:59:59',
62
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
63
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
64
- },
65
- <% end %>
56
+ <%= working_days_annotation %>
66
57
 
67
58
  <% @percentage_lines.each_with_index do |args, index| %>
68
59
  <% percent, color = args %>
@@ -50,16 +50,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
50
50
  },
51
51
  annotation: {
52
52
  annotations: {
53
- <% holidays.each_with_index do |range, index| %>
54
- holiday<%= index %>: {
55
- drawTime: 'beforeDraw',
56
- type: 'box',
57
- xMin: '<%= range.begin %>T00:00:00',
58
- xMax: '<%= range.end %>T23:59:59',
59
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
60
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
61
- },
62
- <% end %>
53
+ <%= working_days_annotation %>
63
54
  }
64
55
  },
65
56
  legend: {
@@ -55,16 +55,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
55
55
  autocolors: false,
56
56
  annotation: {
57
57
  annotations: {
58
- <% holidays.each_with_index do |range, index| %>
59
- holiday<%= index %>: {
60
- drawTime: 'beforeDraw',
61
- type: 'box',
62
- xMin: '<%= range.begin %>T00:00:00',
63
- xMax: '<%= range.end %>T23:59:59',
64
- backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
65
- borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
66
- },
67
- <% end %>
58
+ <%= working_days_annotation %>
68
59
  }
69
60
  }
70
61
  }
@@ -2,6 +2,7 @@
2
2
  --body-background: white;
3
3
  --default-text-color: black;
4
4
  --grid-line-color: lightgray;
5
+ --warning-banner: yellow;
5
6
 
6
7
  --cycletime-scatterplot-overall-trendline-color: gray;
7
8
 
@@ -27,8 +28,16 @@
27
28
  --throughput_chart_total_line_color: gray;
28
29
 
29
30
  --aging-work-in-progress-chart-shading-color: lightgray;
31
+ --aging-work-in-progress-chart-shading-50-color: #2E8BC0; // green;
32
+ --aging-work-in-progress-chart-shading-85-color: #ADD8E6; // yellow;
33
+ --aging-work-in-progress-chart-shading-98-color: #FF8A8A; // orange;
34
+ --aging-work-in-progress-chart-shading-100-color: #FF2E2E; // red;
35
+
30
36
  --aging-work-in-progress-by-age-trend-line-color: gray;
31
37
 
38
+ --aging-work-table-date-in-jeopardy: yellow;
39
+ --aging-work-table-date-overdue: red;
40
+
32
41
  --hierarchy-table-inactive-item-text-color: gray;
33
42
 
34
43
  --wip-chart-completed-color: #00ff00;
@@ -58,6 +67,9 @@
58
67
  --sprint-burndown-sprint-color-4: red;
59
68
  --sprint-burndown-sprint-color-5: brown;
60
69
 
70
+ --daily-view-selected-issue-background: lightgray;
71
+ --daily-view-issue-border: green;
72
+ --daily-view-selected-issue-border: red;
61
73
 
62
74
  }
63
75
 
@@ -133,8 +145,68 @@ ul.quality_report {
133
145
  border-top: 1px solid gray;
134
146
  }
135
147
 
148
+ div.daily_issue:hover {
149
+ background: var(--daily-view-selected-issue-background);
150
+ border-color: var(--daily-view-selected-issue-border);
151
+ }
152
+
153
+ div.daily_issue {
154
+ border: 1px solid var(--daily-view-issue-border);
155
+ padding: 0.5em;
156
+ .heading {
157
+ vertical-align: middle;
158
+ display: flex;
159
+ flex-wrap: wrap;
160
+ column-gap: 0.5em;
161
+ align-items: center;
162
+ }
163
+ table {
164
+ margin-left: 1em;
165
+ td {
166
+ vertical-align: top;
167
+ }
168
+ .time {
169
+ white-space: nowrap;
170
+ font-size: 0.8em;
171
+ }
172
+ }
173
+ .icon {
174
+ width: 1em;
175
+ height: 1em;
176
+ }
177
+ .account_id {
178
+ font-weight: bold;
179
+ }
180
+ .field {
181
+ border: 1px solid black;
182
+ color: white;
183
+ background: black;
184
+ padding-left: 0.2em;
185
+ padding-right: 0.2em;
186
+ border-radius: 0.2em;
187
+ }
188
+ .label {
189
+ border: 1px solid black;
190
+ padding-left: 0.2em;
191
+ padding-right: 0.2em;
192
+ border-radius: 0.2em;
193
+ }
194
+ margin-bottom: 0.5em;
195
+ }
196
+ div.child_issue:hover {
197
+ background: var(--body-background);
198
+ }
199
+ div.child_issue {
200
+ border: 1px dashed green;
201
+ margin: 0.2em;
202
+ margin-left: 1.5em;
203
+ padding: 0.5em;
204
+ }
205
+
136
206
  @media screen and (prefers-color-scheme: dark) {
137
207
  :root {
208
+ --warning-banner: #9F2B00;
209
+
138
210
  --non-working-days-color: #2f2f2f;
139
211
  --type-story-color: #6fb86f;
140
212
  --type-task-color: #0021b3;
@@ -150,8 +222,6 @@ ul.quality_report {
150
222
  --dead-color: black;
151
223
  --wip-chart-active-color: #2551c1;
152
224
 
153
- --aging-work-in-progress-chart-shading-color: #b4b4b4;
154
-
155
225
  --status-category-inprogress-color: #1c49bb;
156
226
 
157
227
  --cycletime-scatterplot-overall-trendline-color: gray;
@@ -165,6 +235,8 @@ ul.quality_report {
165
235
  --wip-chart-duration-two-weeks-or-less-color: #cf9400;
166
236
  --wip-chart-duration-four-weeks-or-less-color: #c25e00;
167
237
  --wip-chart-duration-more-than-four-weeks-color: #8e0000;
238
+
239
+ --daily-view-selected-issue-background: #474747;
168
240
  }
169
241
 
170
242
  h1 {
@@ -197,4 +269,12 @@ ul.quality_report {
197
269
  div.color_block {
198
270
  border: 1px solid lightgray;
199
271
  }
272
+
273
+ div.daily_issue {
274
+ .field {
275
+ color: var(--default-text-color);
276
+ }
277
+ }
278
+ }
279
+
200
280
  }
@@ -18,18 +18,42 @@
18
18
  document.getElementById(issues_id).style.display = 'none'
19
19
  }
20
20
  }
21
+
22
+ function toggle_visibility(open_link_id, close_link_id, toggleable_id) {
23
+ let open_link = document.getElementById(open_link_id)
24
+ let close_link = document.getElementById(close_link_id)
25
+ let toggleable_element = document.getElementById(toggleable_id)
26
+
27
+ if(open_link.style.display == 'none') {
28
+ open_link.style.display = 'block'
29
+ close_link.style.display = 'none'
30
+ toggleable_element.style.display = 'none'
31
+ }
32
+ else {
33
+ open_link.style.display = 'none'
34
+ close_link.style.display = 'block'
35
+ toggleable_element.style.display = 'block'
36
+ }
37
+ }
21
38
  // If we switch between light/dark mode then force a refresh so all charts will redraw correctly
22
39
  // in the other colour scheme.
23
40
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
24
41
  location.reload()
25
42
  })
26
-
27
43
  </script>
28
44
  <style>
29
45
  <%= css %>
30
46
  </style>
47
+ <script type="text/javascript">
48
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--default-text-color');
49
+ </script>
31
50
  </head>
32
51
  <body>
52
+ <noscript>
53
+ <div style="padding: 1em; background: gray; color: white; font-size: 2em;">
54
+ Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you'd loaded this from a folder on SharePoint then save it locally and load it again.
55
+ </div>
56
+ </noscript>
33
57
  <%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
34
58
  <%= "\n" + @sections.collect { |text, type| text if type == :body }.compact.join("\n\n") %>
35
59
  </body>