jirametrics 2.0.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +1 -1
  3. data/lib/jirametrics/aging_work_bar_chart.rb +18 -13
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +8 -6
  5. data/lib/jirametrics/aging_work_table.rb +21 -16
  6. data/lib/jirametrics/anonymizer.rb +6 -5
  7. data/lib/jirametrics/chart_base.rb +35 -19
  8. data/lib/jirametrics/css_variable.rb +33 -0
  9. data/lib/jirametrics/cycletime_scatterplot.rb +8 -9
  10. data/lib/jirametrics/daily_wip_by_age_chart.rb +43 -17
  11. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +32 -17
  12. data/lib/jirametrics/daily_wip_by_parent_chart.rb +42 -0
  13. data/lib/jirametrics/daily_wip_chart.rb +67 -6
  14. data/lib/jirametrics/data_quality_report.rb +1 -1
  15. data/lib/jirametrics/dependency_chart.rb +18 -14
  16. data/lib/jirametrics/downloader.rb +13 -11
  17. data/lib/jirametrics/examples/aggregated_project.rb +17 -20
  18. data/lib/jirametrics/examples/standard_project.rb +10 -18
  19. data/lib/jirametrics/expedited_chart.rb +17 -15
  20. data/lib/jirametrics/exporter.rb +26 -20
  21. data/lib/jirametrics/grouping_rules.rb +7 -1
  22. data/lib/jirametrics/html/aging_work_bar_chart.erb +12 -6
  23. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +8 -2
  24. data/lib/jirametrics/html/aging_work_table.erb +11 -19
  25. data/lib/jirametrics/html/cycletime_histogram.erb +9 -3
  26. data/lib/jirametrics/html/cycletime_scatterplot.erb +10 -4
  27. data/lib/jirametrics/html/daily_wip_chart.erb +18 -5
  28. data/lib/jirametrics/html/expedited_chart.erb +11 -5
  29. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  30. data/lib/jirametrics/html/index.css +186 -0
  31. data/lib/jirametrics/html/index.erb +8 -36
  32. data/lib/jirametrics/html/sprint_burndown.erb +11 -6
  33. data/lib/jirametrics/html/story_point_accuracy_chart.erb +9 -4
  34. data/lib/jirametrics/html/throughput_chart.erb +11 -5
  35. data/lib/jirametrics/html_report_config.rb +28 -3
  36. data/lib/jirametrics/issue.rb +5 -3
  37. data/lib/jirametrics/jira_gateway.rb +5 -2
  38. data/lib/jirametrics/project_config.rb +14 -19
  39. data/lib/jirametrics/settings.json +7 -0
  40. data/lib/jirametrics/sprint_burndown.rb +10 -4
  41. data/lib/jirametrics/status_collection.rb +1 -1
  42. data/lib/jirametrics/story_point_accuracy_chart.rb +20 -10
  43. data/lib/jirametrics/throughput_chart.rb +10 -2
  44. data/lib/jirametrics.rb +2 -0
  45. metadata +7 -3
@@ -20,13 +20,16 @@ class DailyWipChart < ChartBase
20
20
  header_text default_header_text
21
21
  description_text default_description_text
22
22
 
23
- if block
24
- instance_eval(&block)
25
- else
23
+ instance_eval(&block) if block
24
+
25
+ unless @group_by_block
26
26
  grouping_rules do |issue, rules|
27
27
  default_grouping_rules issue: issue, rules: rules
28
28
  end
29
29
  end
30
+
31
+ # Because this one will size itself as needed, we start with a smaller default size
32
+ # @canvas_height = 80
30
33
  end
31
34
 
32
35
  def run
@@ -36,6 +39,13 @@ class DailyWipChart < ChartBase
36
39
  data_sets = possible_rules.collect do |grouping_rule|
37
40
  make_data_set grouping_rule: grouping_rule, issue_rules_by_active_date: issue_rules_by_active_date
38
41
  end
42
+ if @trend_lines
43
+ data_sets = @trend_lines.filter_map do |group_labels, line_color|
44
+ trend_line_data_set(data: data_sets, group_labels: group_labels, color: line_color)
45
+ end + data_sets
46
+ end
47
+
48
+ # grow_chart_height_if_too_many_data_sets data_sets.size
39
49
 
40
50
  wrap_and_render(binding, __FILE__)
41
51
  end
@@ -102,14 +112,14 @@ class DailyWipChart < ChartBase
102
112
  label: grouping_rule.label,
103
113
  data: data,
104
114
  backgroundColor: grouping_rule.color || random_color,
105
- borderColor: 'gray',
106
- borderWidth: grouping_rule.color == 'white' ? 1 : 0,
115
+ borderColor: CssVariable['--wip-chart-border-color'],
116
+ borderWidth: grouping_rule.color.to_s == 'var(--body-background)' ? 1 : 0,
107
117
  borderRadius: positive ? 0 : 5
108
118
  }
109
119
  end
110
120
 
111
121
  def configure_rule issue:, date:
112
- raise 'grouping_rules must be set' if @group_by_block.nil?
122
+ raise "#{self.class}: grouping_rules must be set" if @group_by_block.nil?
113
123
 
114
124
  rules = DailyGroupingRules.new
115
125
  rules.current_date = date
@@ -120,4 +130,55 @@ class DailyWipChart < ChartBase
120
130
  def grouping_rules &block
121
131
  @group_by_block = block
122
132
  end
133
+
134
+ def add_trend_line group_labels:, line_color:
135
+ (@trend_lines ||= []) << [group_labels, line_color]
136
+ end
137
+
138
+ def trend_line_data_set data:, group_labels:, color:
139
+ day_wip_hash = {}
140
+ data.each do |top_level|
141
+ next unless group_labels.include? top_level[:label]
142
+
143
+ top_level[:data].each do |datapoint|
144
+ date = datapoint[:x]
145
+ day_wip_hash[date] = (day_wip_hash[date] || 0) + datapoint[:y]
146
+ end
147
+ end
148
+
149
+ points = day_wip_hash
150
+ .collect { |date, wip| [date.jd, wip] }
151
+ .sort_by(&:first)
152
+
153
+ calculator = TrendLineCalculator.new(points)
154
+ return nil unless calculator.valid?
155
+
156
+ data_points = calculator.chart_datapoints(
157
+ range: date_range.begin.jd..date_range.end.jd,
158
+ max_y: points.collect { |_date, wip| wip }.max
159
+ )
160
+ data_points.each do |point_hash|
161
+ point_hash[:x] = chart_format Date.jd(point_hash[:x])
162
+ end
163
+
164
+ {
165
+ type: 'line',
166
+ label: "Trendline",
167
+ data: data_points,
168
+ fill: false,
169
+ borderWidth: 1,
170
+ markerType: 'none',
171
+ borderColor: CssVariable[color],
172
+ borderDash: [6, 3],
173
+ pointStyle: 'dash',
174
+ hidden: false
175
+ }
176
+ end
177
+
178
+ def grow_chart_height_if_too_many_data_sets count
179
+ px_per_bar = 8
180
+ bars_per_issue = 0.5
181
+ preferred_height = count * px_per_bar * bars_per_issue
182
+ @canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
183
+ end
123
184
  end
@@ -271,7 +271,7 @@ class DataQualityReport < ChartBase
271
271
  board_names = entry_list.collect { |entry| entry.issue.board.name.inspect }
272
272
  entry_list.first.report(
273
273
  problem_key: :issue_on_multiple_boards,
274
- detail: "Found on boards: #{board_names.join(', ')}"
274
+ detail: "Found on boards: #{board_names.sort.join(', ')}"
275
275
  )
276
276
  end
277
277
  end
@@ -84,7 +84,8 @@ class DependencyChart < ChartBase
84
84
  result << issue_link.other_issue.key.inspect
85
85
  result << '['
86
86
  result << 'label=' << (link_rules.label || issue_link.label).inspect
87
- result << ',color=' << (link_rules.line_color || 'black').inspect
87
+ result << ',color=' << (link_rules.line_color || 'gray').inspect
88
+ result << ',fontcolor=' << (link_rules.line_color || 'gray').inspect
88
89
  result << ',dir=both' if link_rules.bidirectional_arrows?
89
90
  result << '];'
90
91
  result
@@ -101,12 +102,26 @@ class DependencyChart < ChartBase
101
102
  tooltip = "#{issue.key}: #{issue.summary}"
102
103
  result << ",tooltip=#{tooltip[0..80].inspect}"
103
104
  unless issue_rules.color == :none
104
- result << %(,style=filled,fillcolor="#{issue_rules.color || color_for(type: issue.type, shade: :light)}")
105
+ result << %(,style=filled,fillcolor="#{issue_rules.color || color_for(type: issue.type)}")
105
106
  end
106
107
  result << ']'
107
108
  result
108
109
  end
109
110
 
111
+ # This used to pull colours from chart_base but the migration to CSS colours kept breaking
112
+ # this chart so we moved it here, until we're finished with the rest. TODO: Revisit whether
113
+ # this can also use customizable CSS colours
114
+ def color_for type:
115
+ @chart_colors = {
116
+ 'Story' => '#90EE90',
117
+ 'Task' => '#87CEFA',
118
+ 'Bug' => '#ffdab9',
119
+ 'Defect' => '#ffdab9',
120
+ 'Epic' => '#fafad2',
121
+ 'Spike' => '#DDA0DD' # light purple
122
+ }[type] ||= random_color
123
+ end
124
+
110
125
  def build_dot_graph
111
126
  issue_links = find_links
112
127
 
@@ -148,6 +163,7 @@ class DependencyChart < ChartBase
148
163
  dot_graph = []
149
164
  dot_graph << 'digraph mygraph {'
150
165
  dot_graph << 'rankdir=LR'
166
+ dot_graph << 'bgcolor="transparent"'
151
167
 
152
168
  # Sort the keys so they are proccessed in a deterministic order.
153
169
  visible_issues.values.sort_by(&:key_as_i).each do |issue|
@@ -177,18 +193,6 @@ class DependencyChart < ChartBase
177
193
  message
178
194
  end
179
195
 
180
- def default_color_for_issue issue
181
- {
182
- 'Story' => '#90EE90',
183
- 'Task' => '#87CEFA',
184
- 'Bug' => '#f08080',
185
- 'Defect' => '#f08080',
186
- 'Epic' => '#fafad2',
187
- 'Spike' => '#7fffd4',
188
- 'Sub-task' => '#dcdcdc'
189
- }[issue.type]
190
- end
191
-
192
196
  def shrink_svg svg
193
197
  scale = 0.8
194
198
  svg.sub(/width="([\d.]+)pt" height="([\d.]+)pt"/) do
@@ -10,7 +10,7 @@ class Downloader
10
10
  attr_reader :file_system
11
11
 
12
12
  # For testing only
13
- attr_reader :start_date_in_query
13
+ attr_reader :start_date_in_query, :board_id_to_filter_id
14
14
 
15
15
  def initialize download_config:, file_system:, jira_gateway:
16
16
  @metadata = {}
@@ -55,7 +55,7 @@ class Downloader
55
55
 
56
56
  def log text, both: false
57
57
  @file_system.log text
58
- puts text if both
58
+ puts text if both && !@quiet_mode
59
59
  end
60
60
 
61
61
  def find_board_ids
@@ -93,7 +93,7 @@ class Downloader
93
93
  intercept_jql = @download_config.project_config.settings['intercept_jql']
94
94
  jql = intercept_jql.call jql if intercept_jql
95
95
 
96
- log " #{jql}"
96
+ log " JQL: #{jql}"
97
97
  escaped_jql = CGI.escape jql
98
98
 
99
99
  max_results = 100
@@ -148,19 +148,19 @@ class Downloader
148
148
 
149
149
  def exit_if_call_failed json
150
150
  # Sometimes Jira returns the singular form of errorMessage and sometimes the plural. Consistency FTW.
151
- return unless json['errorMessages'] || json['errorMessage']
151
+ return unless json['error'] || json['errorMessages'] || json['errorMessage']
152
152
 
153
- log "Download failed. See #{@logfile_name} for details.", both: true
153
+ log "Download failed. See #{@file_system.logfile_name} for details.", both: true
154
154
  log " #{JSON.pretty_generate(json)}"
155
155
  exit 1
156
156
  end
157
157
 
158
158
  def download_statuses
159
159
  log ' Downloading all statuses', both: true
160
- json = @jira_gateway.call_url relative_url: "/rest/api/2/status"
160
+ json = @jira_gateway.call_url relative_url: '/rest/api/2/status'
161
161
 
162
162
  @file_system.save_json(
163
- json: json,
163
+ json: json,
164
164
  filename: "#{@target_path}#{@download_config.project_config.file_prefix}_statuses.json"
165
165
  )
166
166
  end
@@ -168,13 +168,16 @@ class Downloader
168
168
  def download_board_configuration board_id:
169
169
  log " Downloading board configuration for board #{board_id}", both: true
170
170
  json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration"
171
- exit_if_call_failed json
172
171
 
173
- @board_id_to_filter_id[board_id] = json['filter']['id'].to_i
172
+ exit_if_call_failed json
174
173
 
175
174
  file_prefix = @download_config.project_config.file_prefix
176
175
  @file_system.save_json json: json, filename: "#{@target_path}#{file_prefix}_board_#{board_id}_configuration.json"
177
176
 
177
+ # We have a reported bug that blew up on this line. Moved it after the save so we can
178
+ # actually look at the returned json.
179
+ @board_id_to_filter_id[board_id] = json['filter']['id'].to_i
180
+
178
181
  download_sprints board_id: board_id if json['type'] == 'scrum'
179
182
  end
180
183
 
@@ -282,9 +285,8 @@ class Downloader
282
285
 
283
286
  # Pick up any issues that had a status change in the range
284
287
  start_date_text = @start_date_in_query.strftime '%Y-%m-%d'
285
- end_date_text = today.strftime '%Y-%m-%d'
286
288
  # find_in_range = %((status changed DURING ("#{start_date_text} 00:00","#{end_date_text} 23:59")))
287
- find_in_range = %((updated >= "#{start_date_text} 00:00" AND updated <= "#{end_date_text} 23:59"))
289
+ find_in_range = %(updated >= "#{start_date_text} 00:00")
288
290
 
289
291
  segments << "(#{find_in_range} OR #{catch_all})"
290
292
  end
@@ -10,9 +10,11 @@
10
10
  # single team. For that reason, we look at slightly different things that we would on a single team board.
11
11
 
12
12
  class Exporter
13
- def aggregated_project name:, project_names:
13
+ def aggregated_project name:, project_names:, settings: {}
14
14
  project name: name do
15
15
  puts name
16
+ self.settings.merge! settings
17
+
16
18
  aggregate do
17
19
  project_names.each do |project_name|
18
20
  include_issues_from project_name
@@ -28,32 +30,27 @@ class Exporter
28
30
  end
29
31
 
30
32
  html_report do
33
+ html '<h1>Boards included in this report</h1><ul>', type: :header
34
+ board_lines = []
35
+ included_projects.each do |project|
36
+ project.all_boards.values.each do |board|
37
+ board_lines << "<a href='#{project.file_prefix}.html'>#{board.name}</a> from project #{project.name}"
38
+ end
39
+ end
40
+ board_lines.sort.each { |line| html "<li>#{line}</li>", type: :header }
41
+ html '</ul>', type: :header
42
+
31
43
  cycletime_scatterplot do
32
44
  show_trend_lines
45
+ # For an aggregated report we group by board rather than by type
33
46
  grouping_rules do |issue, rules|
34
47
  rules.label = issue.board.name
35
48
  end
36
49
  end
37
50
  # aging_work_in_progress_chart
38
- daily_wip_chart do
39
- header_text 'Daily WIP by Parent'
40
- description_text <<-TEXT
41
- <p>How much work is in progress, grouped by the parent of the issue. This will give us an
42
- indication of how focused we are on higher level objectives. If there are many parent
43
- tickets in progress at the same time, either this team has their focus scattered or we
44
- aren't doing a good job of
45
- <a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
46
- tickets</a>. Neither of those is desirable.</p>
47
- <p>If you're expecting all work items to have parents and there are a lot that don't,
48
- that's also something to look at. Consider whether there is even value in aggregating
49
- these projects if they don't share parent dependencies. Aggregation helps us when we're
50
- looking at related work and if there aren't parent dependencies then the work may not
51
- be related.</p>
52
- TEXT
53
- grouping_rules do |issue, rules|
54
- rules.label = issue.parent&.key || 'No parent'
55
- rules.color = 'white' if rules.label == 'No parent'
56
- end
51
+ daily_wip_by_parent_chart do
52
+ # When aggregating, the chart tends to need more vertical space
53
+ canvas height: 400, width: 800
57
54
  end
58
55
  aging_work_table do
59
56
  # In an aggregated report, we likely only care about items that are old so exclude anything
@@ -6,13 +6,19 @@
6
6
  # See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more
7
7
  class Exporter
8
8
  def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
9
- default_board: nil, anonymize: false
9
+ default_board: nil, anonymize: false, settings: {}, status_category_mappings: {}
10
10
 
11
11
  project name: name do
12
12
  puts name
13
13
  self.anonymize if anonymize
14
14
 
15
15
  settings['blocked_link_text'] = ['is blocked by']
16
+ self.settings.merge! settings
17
+
18
+ status_category_mappings.each do |status, category|
19
+ status_category_mapping status: status, category: category
20
+ end
21
+
16
22
  file_prefix file_prefix
17
23
  download do
18
24
  rolling_date_count 90
@@ -50,7 +56,7 @@ class Exporter
50
56
  type: :header
51
57
  end
52
58
 
53
- discard_changes_before status_becomes: (starting_status || :backlog)
59
+ discard_changes_before status_becomes: (starting_status || :backlog) # rubocop:disable Style/RedundantParentheses
54
60
 
55
61
  cycletime_scatterplot do
56
62
  show_trend_lines
@@ -77,21 +83,7 @@ class Exporter
77
83
  aging_work_table
78
84
  daily_wip_by_age_chart
79
85
  daily_wip_by_blocked_stalled_chart
80
- daily_wip_chart do
81
- header_text 'Daily WIP by Parent'
82
- description_text <<-TEXT
83
- How much work is in progress, grouped by the parent of the issue. This will give us an
84
- indication of how focused we are on higher level objectives. If there are many parent
85
- tickets in progress at the same time, either this team has their focus scattered or we
86
- aren't doing a good job of
87
- <a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
88
- tickets</a>. Neither of those is desirable.
89
- TEXT
90
- grouping_rules do |issue, rules|
91
- rules.label = issue.parent&.key || 'No parent'
92
- rules.color = 'white' if rules.label == 'No parent'
93
- end
94
- end
86
+ daily_wip_by_parent_chart
95
87
  expedited_chart
96
88
  sprint_burndown
97
89
  story_point_accuracy_chart
@@ -107,7 +99,7 @@ class Exporter
107
99
  when 'Sync'
108
100
  rules.use_bidirectional_arrows
109
101
  else
110
- # This is a link type that we don't recognized. Dump it to standard out to draw attention
102
+ # This is a link type that we don't recognize. Dump it to standard out to draw attention
111
103
  # to it.
112
104
  puts "name=#{link.name}, label=#{link.label}"
113
105
  end
@@ -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
@@ -22,19 +25,18 @@ class ExpeditedChart < ChartBase
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
39
41
  end
40
42
 
@@ -124,20 +126,20 @@ class ExpeditedChart < ChartBase
124
126
  case action
125
127
  when :issue_started
126
128
  data << make_point(issue: issue, time: time, label: 'Started', expedited: expedited)
127
- dot_colors << 'orange'
129
+ dot_colors << CssVariable['--expedited-chart-dot-issue-started-color']
128
130
  point_styles << 'rect'
129
131
  when :issue_stopped
130
132
  data << make_point(issue: issue, time: time, label: 'Completed', expedited: expedited)
131
- dot_colors << 'green'
133
+ dot_colors << CssVariable['--expedited-chart-dot-issue-stopped-color']
132
134
  point_styles << 'rect'
133
135
  when :expedite_start
134
136
  data << make_point(issue: issue, time: time, label: 'Expedited', expedited: true)
135
- dot_colors << 'red'
137
+ dot_colors << CssVariable['--expedited-chart-dot-expedite-started-color']
136
138
  point_styles << 'circle'
137
139
  expedited = true
138
140
  when :expedite_stop
139
141
  data << make_point(issue: issue, time: time, label: 'Not expedited', expedited: false)
140
- dot_colors << 'gray'
142
+ dot_colors << CssVariable['--expedited-chart-dot-expedite-stopped-color']
141
143
  point_styles << 'circle'
142
144
  expedited = false
143
145
  else
@@ -149,7 +151,7 @@ class ExpeditedChart < ChartBase
149
151
  last_change_time = expedite_data[-1][0].to_date
150
152
  if last_change_time && last_change_time <= date_range.end && stopped_time.nil?
151
153
  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
154
+ dot_colors << '' # It won't be visible so it doesn't matter
153
155
  point_styles << 'dash'
154
156
  end
155
157
  end
@@ -3,9 +3,9 @@
3
3
  require 'fileutils'
4
4
 
5
5
  class Object
6
- def deprecated message:
6
+ def deprecated message:, date:
7
7
  text = +''
8
- text << 'Deprecated:'
8
+ text << "Deprecated(#{date}):"
9
9
  text << message
10
10
  text << "\n-> Called from #{caller(1..1).first}"
11
11
  warn text
@@ -16,7 +16,14 @@ class Exporter
16
16
  attr_reader :project_configs, :file_system
17
17
 
18
18
  def self.configure &block
19
- exporter = Exporter.new
19
+ logfile_name = 'jirametrics.log'
20
+ logfile = File.open logfile_name, 'w'
21
+ file_system = FileSystem.new
22
+ file_system.logfile = logfile
23
+ file_system.logfile_name = logfile_name
24
+
25
+ exporter = Exporter.new file_system: file_system
26
+
20
27
  exporter.instance_eval(&block)
21
28
  @@instance = exporter
22
29
  end
@@ -41,25 +48,24 @@ class Exporter
41
48
 
42
49
  def download name_filter:
43
50
  @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
51
+ each_project_config(name_filter: name_filter) do |project|
52
+ project.evaluate_next_level
53
+ next if project.aggregated_project?
54
+
55
+ unless project.download_config
56
+ raise "Project #{project.name.inspect} is missing a download section in the config. " \
57
+ 'That is required in order to download'
60
58
  end
59
+
60
+ project.download_config.run
61
+ downloader = Downloader.new(
62
+ download_config: project.download_config,
63
+ file_system: file_system,
64
+ jira_gateway: JiraGateway.new(file_system: file_system)
65
+ )
66
+ downloader.run
61
67
  end
62
- puts "Full output from downloader in #{logfile_name}"
68
+ puts "Full output from downloader in #{file_system.logfile_name}"
63
69
  end
64
70
 
65
71
  def each_project_config name_filter:
@@ -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
@@ -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,14 +19,20 @@ 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: {
@@ -38,8 +44,8 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
38
44
  type: 'box',
39
45
  xMin: '<%= range.begin %>T00:00:00',
40
46
  xMax: '<%= range.end %>T23:59:59',
41
- backgroundColor: '#F0F0F0',
42
- borderColor: '#F0F0F0'
47
+ backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
48
+ borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
43
49
  },
44
50
  <% end %>
45
51
 
@@ -48,7 +54,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
48
54
  type: 'line',
49
55
  xMin: '<%= percentage_line_x %>',
50
56
  xMax: '<%= percentage_line_x %>',
51
- borderColor: 'red',
57
+ borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
52
58
  borderWidth: 1,
53
59
  drawTime: 'afterDraw'
54
60
  }
@@ -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>
@@ -20,7 +20,10 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
20
20
  scaleLabel: {
21
21
  display: true,
22
22
  labelString: 'Date Completed'
23
- }
23
+ },
24
+ grid: {
25
+ color: <%= CssVariable['--grid-line-color'].to_json %>
26
+ },
24
27
  },
25
28
  y: {
26
29
  scaleLabel: {
@@ -31,6 +34,9 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
31
34
  display: true,
32
35
  text: 'Age in days'
33
36
  },
37
+ grid: {
38
+ color: <%= CssVariable['--grid-line-color'].to_json %>
39
+ },
34
40
  }
35
41
  },
36
42
  plugins: {