jirametrics 2.0 → 2.11

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +19 -26
  3. data/lib/jirametrics/aging_work_bar_chart.rb +79 -54
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +106 -40
  5. data/lib/jirametrics/aging_work_table.rb +78 -43
  6. data/lib/jirametrics/anonymizer.rb +6 -5
  7. data/lib/jirametrics/blocked_stalled_change.rb +24 -4
  8. data/lib/jirametrics/board.rb +44 -15
  9. data/lib/jirametrics/board_config.rb +8 -4
  10. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  11. data/lib/jirametrics/change_item.rb +31 -10
  12. data/lib/jirametrics/chart_base.rb +102 -61
  13. data/lib/jirametrics/columns_config.rb +4 -0
  14. data/lib/jirametrics/css_variable.rb +33 -0
  15. data/lib/jirametrics/cycletime_config.rb +59 -8
  16. data/lib/jirametrics/cycletime_histogram.rb +69 -4
  17. data/lib/jirametrics/cycletime_scatterplot.rb +11 -15
  18. data/lib/jirametrics/daily_wip_by_age_chart.rb +44 -20
  19. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +37 -35
  20. data/lib/jirametrics/daily_wip_by_parent_chart.rb +38 -0
  21. data/lib/jirametrics/daily_wip_chart.rb +61 -14
  22. data/lib/jirametrics/data_quality_report.rb +222 -41
  23. data/lib/jirametrics/dependency_chart.rb +54 -23
  24. data/lib/jirametrics/download_config.rb +12 -0
  25. data/lib/jirametrics/downloader.rb +76 -57
  26. data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +48 -33
  27. data/lib/jirametrics/examples/aggregated_project.rb +22 -39
  28. data/lib/jirametrics/examples/standard_project.rb +25 -49
  29. data/lib/jirametrics/expedited_chart.rb +28 -25
  30. data/lib/jirametrics/exporter.rb +59 -32
  31. data/lib/jirametrics/file_config.rb +34 -13
  32. data/lib/jirametrics/file_system.rb +48 -3
  33. data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
  34. data/lib/jirametrics/groupable_issue_chart.rb +2 -6
  35. data/lib/jirametrics/grouping_rules.rb +7 -1
  36. data/lib/jirametrics/hierarchy_table.rb +4 -4
  37. data/lib/jirametrics/html/aging_work_bar_chart.erb +13 -16
  38. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +28 -5
  39. data/lib/jirametrics/html/aging_work_table.erb +19 -25
  40. data/lib/jirametrics/html/cycletime_histogram.erb +83 -3
  41. data/lib/jirametrics/html/cycletime_scatterplot.erb +9 -12
  42. data/lib/jirametrics/html/daily_wip_chart.erb +17 -13
  43. data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +9 -4
  44. data/lib/jirametrics/html/expedited_chart.erb +10 -13
  45. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  46. data/lib/jirametrics/html/hierarchy_table.erb +2 -2
  47. data/lib/jirametrics/html/index.css +209 -0
  48. data/lib/jirametrics/html/index.erb +16 -39
  49. data/lib/jirametrics/html/sprint_burndown.erb +10 -14
  50. data/lib/jirametrics/html/throughput_chart.erb +10 -13
  51. data/lib/jirametrics/html_report_config.rb +108 -86
  52. data/lib/jirametrics/issue.rb +357 -96
  53. data/lib/jirametrics/jira_gateway.rb +29 -11
  54. data/lib/jirametrics/project_config.rb +256 -144
  55. data/lib/jirametrics/rules.rb +2 -2
  56. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  57. data/lib/jirametrics/settings.json +10 -0
  58. data/lib/jirametrics/sprint_burndown.rb +24 -7
  59. data/lib/jirametrics/status.rb +84 -19
  60. data/lib/jirametrics/status_collection.rb +80 -39
  61. data/lib/jirametrics/throughput_chart.rb +12 -4
  62. data/lib/jirametrics/value_equality.rb +2 -2
  63. data/lib/jirametrics.rb +25 -7
  64. metadata +16 -17
  65. data/lib/jirametrics/discard_changes_before.rb +0 -37
  66. data/lib/jirametrics/experimental/generator.rb +0 -210
  67. data/lib/jirametrics/experimental/info.rb +0 -77
  68. data/lib/jirametrics/html/data_quality_report.erb +0 -126
@@ -3,11 +3,14 @@
3
3
  require 'jirametrics/chart_base'
4
4
 
5
5
  class ExpeditedChart < ChartBase
6
- EXPEDITED_SEGMENT = Object.new.tap do |segment|
6
+ EXPEDITED_SEGMENT = ChartBase.new.tap do |segment|
7
7
  def segment.to_json *_args
8
+ expedited = CssVariable.new('--expedited-color').to_json
9
+ not_expedited = CssVariable.new('--expedited-chart-no-longer-expedited').to_json
10
+
8
11
  <<~SNIPPET
9
12
  {
10
- borderColor: ctx => expedited(ctx, 'red') || notExpedited(ctx, 'gray'),
13
+ borderColor: ctx => expedited(ctx, #{expedited}) || notExpedited(ctx, #{not_expedited}),
11
14
  borderDash: ctx => notExpedited(ctx, [6, 6])
12
15
  }
13
16
  SNIPPET
@@ -17,25 +20,26 @@ class ExpeditedChart < ChartBase
17
20
  attr_accessor :issues, :cycletime, :possible_statuses, :date_range
18
21
  attr_reader :expedited_label
19
22
 
20
- def initialize
23
+ def initialize block
21
24
  super()
22
25
 
23
26
  header_text 'Expedited work'
24
27
  description_text <<-HTML
25
- <p>
28
+ <div class="p">
26
29
  This chart only shows issues that have been expedited at some point. We care about these as
27
30
  any form of expedited work will affect the entire system and will slow down non-expedited work.
28
31
  Refer to this article on
29
32
  <a href="https://improvingflow.com/2021/06/16/classes-of-service.html">classes of service</a>
30
33
  for a longer explanation on why we want to avoid expedited work.
31
- </p>
32
- <p>
33
- The lines indicate time that this issue was expedited. When the line is red then the issue was
34
- expedited at that time. When it's gray then it wasn't. Orange dots indicate the date the work
35
- was started and green dots represent the completion date. Lastly, the vertical height of the
36
- lines/dots indicates how long it's been since this issue was created.
37
- </p>
34
+ </div>
35
+ <div class="p">
36
+ The colour of the line indicates time that this issue was #{color_block '--expedited-color'} expedited
37
+ or #{color_block '--expedited-chart-no-longer-expedited'} not expedited.
38
+ </div>
39
+ #{describe_non_working_days}
38
40
  HTML
41
+
42
+ instance_eval(&block)
39
43
  end
40
44
 
41
45
  def run
@@ -53,13 +57,13 @@ class ExpeditedChart < ChartBase
53
57
  def prepare_expedite_data issue
54
58
  expedite_start = nil
55
59
  result = []
56
- expedited_priority_names = issue.board.expedited_priority_names
60
+ expedited_priority_names = issue.board.project_config.settings['expedited_priority_names']
57
61
 
58
62
  issue.changes.each do |change|
59
63
  next unless change.priority?
60
64
 
61
65
  if expedited_priority_names.include? change.value
62
- expedite_start = change.time
66
+ expedite_start = change.time.to_date
63
67
  elsif expedite_start
64
68
  start_date = expedite_start.to_date
65
69
  stop_date = change.time.to_date
@@ -68,7 +72,7 @@ class ExpeditedChart < ChartBase
68
72
  (start_date < date_range.begin && stop_date > date_range.end)
69
73
 
70
74
  result << [expedite_start, :expedite_start]
71
- result << [change.time, :expedite_stop]
75
+ result << [change.time.to_date, :expedite_stop]
72
76
  end
73
77
  expedite_start = nil
74
78
  end
@@ -105,12 +109,11 @@ class ExpeditedChart < ChartBase
105
109
 
106
110
  def make_expedite_lines_data_set issue:, expedite_data:
107
111
  cycletime = issue.board.cycletime
108
- started_time = cycletime.started_time(issue)
109
- stopped_time = cycletime.stopped_time(issue)
112
+ started_date, stopped_date = cycletime.started_stopped_dates(issue)
110
113
 
111
- expedite_data << [started_time, :issue_started] if started_time
112
- expedite_data << [stopped_time, :issue_stopped] if stopped_time
113
- expedite_data.sort_by! { |a| a[0] }
114
+ expedite_data << [started_date, :issue_started] if started_date
115
+ expedite_data << [stopped_date, :issue_stopped] if stopped_date
116
+ expedite_data.sort_by!(&:first)
114
117
 
115
118
  # If none of the data would be visible on the chart then skip it.
116
119
  return nil unless expedite_data.any? { |time, _action| time.to_date >= date_range.begin }
@@ -124,20 +127,20 @@ class ExpeditedChart < ChartBase
124
127
  case action
125
128
  when :issue_started
126
129
  data << make_point(issue: issue, time: time, label: 'Started', expedited: expedited)
127
- dot_colors << 'orange'
130
+ dot_colors << CssVariable['--expedited-chart-dot-issue-started-color']
128
131
  point_styles << 'rect'
129
132
  when :issue_stopped
130
133
  data << make_point(issue: issue, time: time, label: 'Completed', expedited: expedited)
131
- dot_colors << 'green'
134
+ dot_colors << CssVariable['--expedited-chart-dot-issue-stopped-color']
132
135
  point_styles << 'rect'
133
136
  when :expedite_start
134
137
  data << make_point(issue: issue, time: time, label: 'Expedited', expedited: true)
135
- dot_colors << 'red'
138
+ dot_colors << CssVariable['--expedited-chart-dot-expedite-started-color']
136
139
  point_styles << 'circle'
137
140
  expedited = true
138
141
  when :expedite_stop
139
142
  data << make_point(issue: issue, time: time, label: 'Not expedited', expedited: false)
140
- dot_colors << 'gray'
143
+ dot_colors << CssVariable['--expedited-chart-dot-expedite-stopped-color']
141
144
  point_styles << 'circle'
142
145
  expedited = false
143
146
  else
@@ -147,9 +150,9 @@ class ExpeditedChart < ChartBase
147
150
 
148
151
  unless expedite_data.empty?
149
152
  last_change_time = expedite_data[-1][0].to_date
150
- if last_change_time && last_change_time <= date_range.end && stopped_time.nil?
153
+ if last_change_time && last_change_time <= date_range.end && stopped_date.nil?
151
154
  data << make_point(issue: issue, time: date_range.end, label: 'Still ongoing', expedited: expedited)
152
- dot_colors << 'blue' # It won't be visible so it doesn't matter
155
+ dot_colors << '' # It won't be visible so it doesn't matter
153
156
  point_styles << 'dash'
154
157
  end
155
158
  end
@@ -2,21 +2,19 @@
2
2
 
3
3
  require 'fileutils'
4
4
 
5
- class Object
6
- def deprecated message:
7
- text = +''
8
- text << 'Deprecated:'
9
- text << message
10
- text << "\n-> Called from #{caller(1..1).first}"
11
- warn text
12
- end
13
- end
14
-
15
5
  class Exporter
16
- attr_reader :project_configs, :file_system
6
+ attr_reader :project_configs
7
+ attr_accessor :file_system
17
8
 
18
9
  def self.configure &block
19
- exporter = Exporter.new
10
+ logfile_name = 'jirametrics.log'
11
+ logfile = File.open logfile_name, 'w'
12
+ file_system = FileSystem.new
13
+ file_system.logfile = logfile
14
+ file_system.logfile_name = logfile_name
15
+
16
+ exporter = Exporter.new file_system: file_system
17
+
20
18
  exporter.instance_eval(&block)
21
19
  @@instance = exporter
22
20
  end
@@ -25,11 +23,12 @@ class Exporter
25
23
 
26
24
  def initialize file_system: FileSystem.new
27
25
  @project_configs = []
28
- @timezone_offset = '+00:00'
29
26
  @target_path = '.'
30
27
  @holiday_dates = []
31
28
  @downloading = false
32
29
  @file_system = file_system
30
+
31
+ timezone_offset '+00:00'
33
32
  end
34
33
 
35
34
  def export name_filter:
@@ -41,25 +40,49 @@ class Exporter
41
40
 
42
41
  def download name_filter:
43
42
  @downloading = true
44
- logfile_name = 'downloader.log'
45
- File.open logfile_name, 'w' do |logfile|
46
- file_system.logfile = logfile
47
- file_system.logfile_name = logfile_name
48
-
49
- each_project_config(name_filter: name_filter) do |project|
50
- project.evaluate_next_level
51
- next if project.aggregated_project?
52
-
53
- project.download_config.run
54
- downloader = Downloader.new(
55
- download_config: project.download_config,
56
- file_system: file_system,
57
- jira_gateway: JiraGateway.new(file_system: file_system)
58
- )
59
- downloader.run
43
+ each_project_config(name_filter: name_filter) do |project|
44
+ project.evaluate_next_level
45
+ next if project.aggregated_project?
46
+
47
+ unless project.download_config
48
+ raise "Project #{project.name.inspect} is missing a download section in the config. " \
49
+ 'That is required in order to download'
50
+ end
51
+
52
+ project.download_config.run
53
+ downloader = Downloader.new(
54
+ download_config: project.download_config,
55
+ file_system: file_system,
56
+ jira_gateway: JiraGateway.new(file_system: file_system)
57
+ )
58
+ downloader.run
59
+ end
60
+ puts "Full output from downloader in #{file_system.logfile_name}"
61
+ end
62
+
63
+ def info keys, name_filter:
64
+ selected = []
65
+ each_project_config(name_filter: name_filter) do |project|
66
+ project.evaluate_next_level
67
+
68
+ project.run load_only: true
69
+ project.issues.each do |issue|
70
+ selected << [project, issue] if keys.include? issue.key
71
+ end
72
+ rescue => e # rubocop:disable Style/RescueStandardError
73
+ # This happens when we're attempting to load an aggregated project because it hasn't been
74
+ # properly initialized. Since we don't care about aggregated projects, we just ignore it.
75
+ raise unless e.message.start_with? 'This is an aggregated project and issues should have been included'
76
+ end
77
+
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
60
84
  end
61
85
  end
62
- puts "Full output from downloader in #{logfile_name}"
63
86
  end
64
87
 
65
88
  def each_project_config name_filter:
@@ -73,7 +96,6 @@ class Exporter
73
96
  end
74
97
 
75
98
  def project name: nil, &block
76
- raise 'target_path was never set!' if @target_path.nil?
77
99
  raise 'jira_config not set' if @jira_config.nil?
78
100
 
79
101
  @project_configs << ProjectConfig.new(
@@ -93,7 +115,12 @@ class Exporter
93
115
  end
94
116
 
95
117
  def jira_config filename = nil
96
- @jira_config = file_system.load_json(filename) unless filename.nil?
118
+ if 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
+
122
+ @jira_config['url'] = $1 if @jira_config['url'] =~ /^(.+)\/+$/
123
+ end
97
124
  @jira_config
98
125
  end
99
126
 
@@ -5,10 +5,11 @@ require 'csv'
5
5
  class FileConfig
6
6
  attr_reader :project_config, :issues
7
7
 
8
- def initialize project_config:, block:
8
+ def initialize project_config:, block:, today: Date.today
9
9
  @project_config = project_config
10
10
  @block = block
11
11
  @columns = nil
12
+ @today = today
12
13
  end
13
14
 
14
15
  def run
@@ -18,11 +19,8 @@ class FileConfig
18
19
  if @columns
19
20
  all_lines = prepare_grid
20
21
 
21
- File.open(output_filename, 'w') do |file|
22
- all_lines.each do |output_line|
23
- file.puts CSV.generate_line(output_line)
24
- end
25
- end
22
+ content = all_lines.collect { |line| CSV.generate_line line }.join
23
+ project_config.exporter.file_system.save_file content: content, filename: output_filename
26
24
  elsif @html_report
27
25
  @html_report.run
28
26
  else
@@ -58,8 +56,8 @@ class FileConfig
58
56
  def output_filename
59
57
  segments = []
60
58
  segments << project_config.target_path
61
- segments << project_config.file_prefix
62
- segments << (@file_suffix || "-#{Date.today}.csv")
59
+ segments << project_config.get_file_prefix
60
+ segments << (@file_suffix || "-#{@today}.csv")
63
61
  segments.join
64
62
  end
65
63
 
@@ -68,13 +66,20 @@ class FileConfig
68
66
  # is that all empty values in the first column should be at the bottom.
69
67
  def sort_output all_lines
70
68
  all_lines.sort do |a, b|
71
- if a[0].nil?
72
- 1
69
+ result = nil
70
+ if a[0] == b[0]
71
+ result = a[1..] <=> b[1..]
72
+ elsif a[0].nil?
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
 
@@ -85,6 +90,11 @@ class FileConfig
85
90
 
86
91
  def html_report &block
87
92
  assert_only_one_filetype_config_set
93
+ if block.nil?
94
+ project_config.file_system.warning 'No charts were specified for the report. This is almost certainly a mistake.'
95
+ block = ->(_) {}
96
+ end
97
+
88
98
  @html_report = HtmlReportConfig.new file_config: self, block: block
89
99
  end
90
100
 
@@ -103,7 +113,7 @@ class FileConfig
103
113
  def to_datetime object
104
114
  return nil if object.nil?
105
115
 
106
- object = object.to_datetime
116
+ object = object.to_time.to_datetime
107
117
  object = object.new_offset(@timezone_offset) if @timezone_offset
108
118
  object
109
119
  end
@@ -112,8 +122,19 @@ class FileConfig
112
122
  object.to_s
113
123
  end
114
124
 
125
+ def to_integer object
126
+ object.to_i
127
+ end
128
+
115
129
  def file_suffix suffix = nil
116
130
  @file_suffix = suffix unless suffix.nil?
117
131
  @file_suffix
118
132
  end
133
+
134
+ def children
135
+ result = []
136
+ result << @columns if @columns
137
+ result << @html_report if @html_report
138
+ result
139
+ end
119
140
  end
@@ -5,21 +5,48 @@ require 'json'
5
5
  class FileSystem
6
6
  attr_accessor :logfile, :logfile_name
7
7
 
8
+ # Effectively the same as File.read except it forces the encoding to UTF-8
9
+ def load filename, supress_deprecation: false
10
+ if filename.end_with?('.json') && !supress_deprecation
11
+ deprecated(message: 'call load_json instead', date: '2024-11-13')
12
+ end
13
+
14
+ File.read filename, encoding: 'UTF-8'
15
+ end
16
+
8
17
  def load_json filename, fail_on_error: true
9
18
  return nil if fail_on_error == false && File.exist?(filename) == false
10
19
 
11
- JSON.parse File.read(filename)
20
+ JSON.parse load(filename, supress_deprecation: true)
12
21
  end
13
22
 
14
23
  def save_json json:, filename:
24
+ save_file content: JSON.pretty_generate(compress json), filename: filename
25
+ end
26
+
27
+ def save_file content:, filename:
15
28
  file_path = File.dirname(filename)
16
29
  FileUtils.mkdir_p file_path unless File.exist?(file_path)
17
30
 
18
- File.write(filename, JSON.pretty_generate(compress json))
31
+ File.write(filename, content)
32
+ end
33
+
34
+ def warning message, more: nil
35
+ log "Warning: #{message}", more: more, also_write_to_stderr: true
36
+ end
37
+
38
+ def error message, more: nil
39
+ log "Error: #{message}", more: more, also_write_to_stderr: true
19
40
  end
20
41
 
21
- def log message
42
+ def log message, more: nil, also_write_to_stderr: false
43
+ message += " See #{logfile_name} for more details about this message." if more
44
+
22
45
  logfile.puts message
46
+ logfile.puts more if more
47
+ return unless also_write_to_stderr
48
+
49
+ $stderr.puts message # rubocop:disable Style/StderrPuts
23
50
  end
24
51
 
25
52
  # In some Jira instances, a sizeable portion of the JSON is made up of empty fields. I've seen
@@ -33,4 +60,22 @@ class FileSystem
33
60
  end
34
61
  node
35
62
  end
63
+
64
+ def foreach root, &block
65
+ Dir.foreach root, &block
66
+ end
67
+
68
+ def file_exist? filename
69
+ File.exist? filename
70
+ end
71
+
72
+ def deprecated message:, date:, depth: 2
73
+ text = +''
74
+ text << "Deprecated(#{date}): "
75
+ text << message
76
+ caller(1..depth).each do |line|
77
+ text << "\n-> Called from #{line}"
78
+ end
79
+ log text, also_write_to_stderr: true
80
+ end
36
81
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/groupable_issue_chart'
4
+
5
+ class FlowEfficiencyScatterplot < ChartBase
6
+ include GroupableIssueChart
7
+
8
+ attr_accessor :possible_statuses
9
+
10
+ def initialize block
11
+ super()
12
+
13
+ header_text 'Flow Efficiency'
14
+ description_text <<-HTML
15
+ <div class="p">
16
+ This chart shows the active time against the the total time spent on a ticket.
17
+ <a href="https://improvingflow.com/2024/07/06/flow-efficiency.html">Flow efficiency</a> is the ratio
18
+ between these two numbers.
19
+ </div>
20
+ <div class="p">
21
+ <math>
22
+ <mn>Flow efficiency (%)</mn>
23
+ <mo>=</mo>
24
+ <mfrac>
25
+ <mrow><mn>Time adding value</mn></mrow>
26
+ <mrow><mn>Total time</mn></mrow>
27
+ </mfrac>
28
+ </math>
29
+ </div>
30
+ <div style="background: var(--warning-banner)">Note that for this calculation to be accurate, we must be moving items into a
31
+ blocked or stalled state the moment we stop working on it, and most teams don't do that.
32
+ So be aware that your team may have to change their behaviours if you want this chart to be useful.
33
+ </div>
34
+ HTML
35
+
36
+ init_configuration_block block do
37
+ grouping_rules do |issue, rule|
38
+ active_time, total_time = issue.flow_efficiency_numbers end_time: time_range.end
39
+ flow_efficiency = active_time * 100.0 / total_time
40
+
41
+ if flow_efficiency > 99.0
42
+ rule.label = '~100%'
43
+ rule.color = 'green'
44
+ elsif flow_efficiency < 30.0
45
+ rule.label = '< 30%'
46
+ rule.color = 'orange'
47
+ else
48
+ rule.label = 'The rest'
49
+ rule.color = 'black'
50
+ end
51
+ end
52
+ end
53
+
54
+ @percentage_lines = []
55
+ @highest_cycletime = 0
56
+ end
57
+
58
+ def run
59
+ data_sets = group_issues(completed_issues_in_range include_unstarted: false).filter_map do |rules, issues|
60
+ create_dataset(issues: issues, label: rules.label, color: rules.color)
61
+ end
62
+
63
+ return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
64
+
65
+ wrap_and_render(binding, __FILE__)
66
+ end
67
+
68
+ def to_days seconds
69
+ seconds / 60 / 60 / 24
70
+ end
71
+
72
+ def create_dataset issues:, label:, color:
73
+ return nil if issues.empty?
74
+
75
+ data = issues.filter_map do |issue|
76
+ active_time, total_time = issue.flow_efficiency_numbers(
77
+ end_time: time_range.end, settings: settings
78
+ )
79
+
80
+ active_days = to_days(active_time)
81
+ total_days = to_days(total_time)
82
+ flow_efficiency = active_time * 100.0 / total_time
83
+
84
+ if flow_efficiency.nan?
85
+ # If this happens then something is probably misconfigured. We've seen it in production though
86
+ # so we have to handle it.
87
+ file_system.log(
88
+ "Issue(#{issue.key}) flow_efficiency: NaN, active_time: #{active_time}, total_time: #{total_time}"
89
+ )
90
+ flow_efficiency = 0.0
91
+ end
92
+
93
+ {
94
+ y: active_days,
95
+ x: total_days,
96
+ title: [
97
+ "#{issue.key} : #{issue.summary}, flow efficiency: #{flow_efficiency.to_i}%," \
98
+ " total: #{total_days.round(1)} days," \
99
+ " active: #{active_days.round(1)} days"
100
+ ]
101
+ }
102
+ end
103
+ {
104
+ label: label,
105
+ data: data,
106
+ fill: false,
107
+ showLine: false,
108
+ backgroundColor: color
109
+ }
110
+ end
111
+ end
@@ -5,12 +5,8 @@ require 'jirametrics/grouping_rules'
5
5
 
6
6
  module GroupableIssueChart
7
7
  def init_configuration_block user_provided_block, &default_block
8
- if user_provided_block
9
- instance_eval(&user_provided_block)
10
- return if @group_by_block
11
- end
12
-
13
- instance_eval(&default_block)
8
+ instance_eval(&user_provided_block)
9
+ instance_eval(&default_block) unless @group_by_block
14
10
  end
15
11
 
16
12
  def grouping_rules &block
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class GroupingRules < Rules
4
- attr_accessor :label, :color
4
+ attr_accessor :label
5
+ attr_reader :color
5
6
 
6
7
  def eql? other
7
8
  other.label == @label && other.color == @color
@@ -10,4 +11,9 @@ class GroupingRules < Rules
10
11
  def group
11
12
  [@label, @color]
12
13
  end
14
+
15
+ def color= color
16
+ color = CssVariable[color] unless color.is_a?(CssVariable)
17
+ @color = color
18
+ end
13
19
  end
@@ -3,15 +3,15 @@
3
3
  require 'jirametrics/chart_base'
4
4
 
5
5
  class HierarchyTable < ChartBase
6
- def initialize block = nil
6
+ def initialize block
7
7
  super()
8
8
 
9
9
  header_text 'Hierarchy Table'
10
- description_text <<-HTML
11
- <p>content goes here</p>
10
+ description_text <<~HTML
11
+ <p>Shows all issues through this time period and the full hierarchy of their parents.</p>
12
12
  HTML
13
13
 
14
- instance_eval(&block) if block
14
+ instance_eval(&block)
15
15
  end
16
16
 
17
17
  def run
@@ -1,4 +1,4 @@
1
- <div>
1
+ <div class="chart">
2
2
  <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
3
3
  </div>
4
4
  <script>
@@ -19,36 +19,33 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
19
19
  stacked: false,
20
20
  title: {
21
21
  display: false
22
- }
22
+ },
23
+ grid: {
24
+ color: <%= CssVariable['--grid-line-color'].to_json %>
25
+ },
23
26
  },
24
27
  y: {
25
28
  stacked: true,
26
29
  position: 'right',
27
30
  ticks: {
28
31
  display: true
29
- }
32
+ },
33
+ grid: {
34
+ color: <%= CssVariable['--grid-line-color'].to_json %>
35
+ },
30
36
  }
31
37
  },
32
38
  plugins: {
33
39
  annotation: {
34
40
  annotations: {
35
- <% holidays.each_with_index do |range, index| %>
36
- holiday<%= index %>: {
37
- drawTime: 'beforeDraw',
38
- type: 'box',
39
- xMin: '<%= range.begin %>T00:00:00',
40
- xMax: '<%= range.end %>T23:59:59',
41
- backgroundColor: '#F0F0F0',
42
- borderColor: '#F0F0F0'
43
- },
44
- <% end %>
41
+ <%= working_days_annotation %>
45
42
 
46
43
  <% if percentage_line_x %>
47
44
  line: {
48
45
  type: 'line',
49
- xMin: '<%= percentage_line_x %>',
50
- xMax: '<%= percentage_line_x %>',
51
- borderColor: 'red',
46
+ scaleID: 'x',
47
+ value: '<%= percentage_line_x %>',
48
+ borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
52
49
  borderWidth: 1,
53
50
  drawTime: 'afterDraw'
54
51
  }