jirametrics 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/bin/jirametrics +4 -0
  3. data/lib/jirametrics/aggregate_config.rb +89 -0
  4. data/lib/jirametrics/aging_work_bar_chart.rb +235 -0
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +148 -0
  6. data/lib/jirametrics/aging_work_table.rb +149 -0
  7. data/lib/jirametrics/anonymizer.rb +186 -0
  8. data/lib/jirametrics/blocked_stalled_change.rb +43 -0
  9. data/lib/jirametrics/board.rb +85 -0
  10. data/lib/jirametrics/board_column.rb +14 -0
  11. data/lib/jirametrics/board_config.rb +31 -0
  12. data/lib/jirametrics/change_item.rb +80 -0
  13. data/lib/jirametrics/chart_base.rb +239 -0
  14. data/lib/jirametrics/columns_config.rb +42 -0
  15. data/lib/jirametrics/cycletime_config.rb +69 -0
  16. data/lib/jirametrics/cycletime_histogram.rb +74 -0
  17. data/lib/jirametrics/cycletime_scatterplot.rb +128 -0
  18. data/lib/jirametrics/daily_wip_by_age_chart.rb +88 -0
  19. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +77 -0
  20. data/lib/jirametrics/daily_wip_chart.rb +123 -0
  21. data/lib/jirametrics/data_quality_report.rb +278 -0
  22. data/lib/jirametrics/dependency_chart.rb +217 -0
  23. data/lib/jirametrics/discard_changes_before.rb +37 -0
  24. data/lib/jirametrics/download_config.rb +41 -0
  25. data/lib/jirametrics/downloader.rb +337 -0
  26. data/lib/jirametrics/examples/aggregated_project.rb +36 -0
  27. data/lib/jirametrics/examples/standard_project.rb +111 -0
  28. data/lib/jirametrics/expedited_chart.rb +169 -0
  29. data/lib/jirametrics/experimental/generator.rb +209 -0
  30. data/lib/jirametrics/experimental/info.rb +77 -0
  31. data/lib/jirametrics/exporter.rb +127 -0
  32. data/lib/jirametrics/file_config.rb +119 -0
  33. data/lib/jirametrics/fix_version.rb +21 -0
  34. data/lib/jirametrics/groupable_issue_chart.rb +44 -0
  35. data/lib/jirametrics/grouping_rules.rb +13 -0
  36. data/lib/jirametrics/hierarchy_table.rb +31 -0
  37. data/lib/jirametrics/html/aging_work_bar_chart.erb +72 -0
  38. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +52 -0
  39. data/lib/jirametrics/html/aging_work_table.erb +60 -0
  40. data/lib/jirametrics/html/collapsible_issues_panel.erb +32 -0
  41. data/lib/jirametrics/html/cycletime_histogram.erb +41 -0
  42. data/lib/jirametrics/html/cycletime_scatterplot.erb +103 -0
  43. data/lib/jirametrics/html/daily_wip_chart.erb +63 -0
  44. data/lib/jirametrics/html/data_quality_report.erb +126 -0
  45. data/lib/jirametrics/html/expedited_chart.erb +67 -0
  46. data/lib/jirametrics/html/hierarchy_table.erb +29 -0
  47. data/lib/jirametrics/html/index.erb +66 -0
  48. data/lib/jirametrics/html/sprint_burndown.erb +116 -0
  49. data/lib/jirametrics/html/story_point_accuracy_chart.erb +57 -0
  50. data/lib/jirametrics/html/throughput_chart.erb +65 -0
  51. data/lib/jirametrics/html_report_config.rb +217 -0
  52. data/lib/jirametrics/issue.rb +521 -0
  53. data/lib/jirametrics/issue_link.rb +60 -0
  54. data/lib/jirametrics/json_file_loader.rb +9 -0
  55. data/lib/jirametrics/project_config.rb +442 -0
  56. data/lib/jirametrics/rules.rb +34 -0
  57. data/lib/jirametrics/self_or_issue_dispatcher.rb +15 -0
  58. data/lib/jirametrics/sprint.rb +43 -0
  59. data/lib/jirametrics/sprint_burndown.rb +335 -0
  60. data/lib/jirametrics/sprint_issue_change_data.rb +31 -0
  61. data/lib/jirametrics/status.rb +26 -0
  62. data/lib/jirametrics/status_collection.rb +67 -0
  63. data/lib/jirametrics/story_point_accuracy_chart.rb +139 -0
  64. data/lib/jirametrics/throughput_chart.rb +91 -0
  65. data/lib/jirametrics/tree_organizer.rb +96 -0
  66. data/lib/jirametrics/trend_line_calculator.rb +74 -0
  67. data/lib/jirametrics.rb +85 -0
  68. metadata +167 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/self_or_issue_dispatcher'
4
+
5
+ class ColumnsConfig
6
+ include SelfOrIssueDispatcher
7
+
8
+ attr_reader :columns, :file_config
9
+
10
+ def initialize file_config:, block:
11
+ @columns = []
12
+ @file_config = file_config
13
+ @block = block
14
+ end
15
+
16
+ def run
17
+ instance_eval(&@block)
18
+ end
19
+
20
+ def write_headers headers = nil
21
+ @write_headers = headers unless headers.nil?
22
+ @write_headers
23
+ end
24
+
25
+ def date label, proc
26
+ @columns << [:date, label, proc]
27
+ end
28
+
29
+ def datetime label, proc
30
+ @columns << [:datetime, label, proc]
31
+ end
32
+
33
+ def string label, proc
34
+ @columns << [:string, label, proc]
35
+ end
36
+
37
+ def column_entry_times board_id: nil
38
+ @file_config.project_config.find_board_by_id(board_id).visible_columns.each do |column|
39
+ date column.name, first_time_in_status(*column.status_ids)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/self_or_issue_dispatcher'
4
+ require 'date'
5
+
6
+ class CycleTimeConfig
7
+ include SelfOrIssueDispatcher
8
+
9
+ attr_reader :label, :parent_config
10
+
11
+ def initialize parent_config:, label:, block:, today: Date.today
12
+ @parent_config = parent_config
13
+ @label = label
14
+ @today = today
15
+ instance_eval(&block) unless block.nil?
16
+ end
17
+
18
+ def start_at block = nil
19
+ @start_at = block unless block.nil?
20
+ @start_at
21
+ end
22
+
23
+ def stop_at block = nil
24
+ @stop_at = block unless block.nil?
25
+ @stop_at
26
+ end
27
+
28
+ def in_progress? issue
29
+ started_time(issue) && stopped_time(issue).nil?
30
+ end
31
+
32
+ def done? issue
33
+ stopped_time(issue)
34
+ end
35
+
36
+ def started_time issue
37
+ @start_at.call(issue)
38
+ end
39
+
40
+ def stopped_time issue
41
+ @stop_at.call(issue)
42
+ end
43
+
44
+ def cycletime issue
45
+ start = started_time(issue)
46
+ stop = stopped_time(issue)
47
+ return nil if start.nil? || stop.nil?
48
+
49
+ (stop.to_date - start.to_date).to_i + 1
50
+ end
51
+
52
+ def age issue, today: nil
53
+ start = started_time(issue)
54
+ stop = today || @today || Date.today
55
+ return nil if start.nil? || stop.nil?
56
+
57
+ (stop.to_date - start.to_date).to_i + 1
58
+ end
59
+
60
+ def possible_statuses
61
+ if parent_config.is_a? BoardConfig
62
+ project_config = parent_config.project_config
63
+ else
64
+ # TODO: This will go away when cycletimes are no longer supported inside html_reports
65
+ project_config = parent_config.file_config.project_config
66
+ end
67
+ project_config.possible_statuses
68
+ end
69
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/groupable_issue_chart'
4
+
5
+ class CycletimeHistogram < ChartBase
6
+ include GroupableIssueChart
7
+ attr_accessor :possible_statuses
8
+
9
+ def initialize block = nil
10
+ super()
11
+
12
+ header_text 'Cycletime Histogram'
13
+ description_text <<-HTML
14
+ <p>
15
+ The Cycletime Histogram shows how many items completed in a certain timeframe. This can be
16
+ useful for determining how many different types of work are flowing through, based on the
17
+ lengths of time they take.
18
+ </p>
19
+ HTML
20
+
21
+ init_configuration_block(block) do
22
+ grouping_rules do |issue, rule|
23
+ rule.label = issue.type
24
+ rule.color = color_for type: issue.type
25
+ end
26
+ end
27
+ end
28
+
29
+ def run
30
+ stopped_issues = completed_issues_in_range include_unstarted: true
31
+
32
+ # For the histogram, we only want to consider items that have both a start and a stop time.
33
+ histogram_issues = stopped_issues.select { |issue| issue.board.cycletime.started_time(issue) }
34
+ rules_to_issues = group_issues histogram_issues
35
+
36
+ data_sets = rules_to_issues.keys.collect do |rules|
37
+ data_set_for(
38
+ histogram_data: histogram_data_for(issues: rules_to_issues[rules]),
39
+ label: rules.label,
40
+ color: rules.color
41
+ )
42
+ end
43
+
44
+ wrap_and_render(binding, __FILE__)
45
+ end
46
+
47
+ def histogram_data_for issues:
48
+ count_hash = {}
49
+ issues.each do |issue|
50
+ days = issue.board.cycletime.cycletime(issue)
51
+ count_hash[days] = (count_hash[days] || 0) + 1 if days.positive?
52
+ end
53
+ count_hash
54
+ end
55
+
56
+ def data_set_for histogram_data:, label:, color:
57
+ keys = histogram_data.keys.sort
58
+ {
59
+ type: 'bar',
60
+ label: label,
61
+ data: keys.sort.collect do |key|
62
+ next if histogram_data[key].zero?
63
+
64
+ {
65
+ x: key,
66
+ y: histogram_data[key],
67
+ title: "#{histogram_data[key]} items completed in #{label_days key}"
68
+ }
69
+ end.compact,
70
+ backgroundColor: color,
71
+ borderRadius: 0
72
+ }
73
+ end
74
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/groupable_issue_chart'
4
+
5
+ class CycletimeScatterplot < ChartBase
6
+ include GroupableIssueChart
7
+
8
+ attr_accessor :possible_statuses
9
+
10
+ def initialize block = nil
11
+ super()
12
+
13
+ header_text 'Cycletime Scatterplot'
14
+ description_text <<-HTML
15
+ <p>
16
+ This chart shows only completed work and indicates both what day it completed as well as
17
+ how many days it took to get done. Hovering over a dot will show you the ID of the work item.
18
+ </p>
19
+ <p>
20
+ The gray line indicates the 85th percentile (<%= overall_percent_line %> days). 85% of all
21
+ items on this chart fall on or below the line and the remaining 15% are above the line. 85%
22
+ is a reasonable proxy for "most" so that we can say that based on this data set, we can
23
+ predict that most work of this type will complete in <%= overall_percent_line %> days or
24
+ less. The other lines reflect the 85% line for that respective type of work.
25
+ </p>
26
+ <p>
27
+ The gray vertical bars indicate weekends, when theoretically we aren't working.
28
+ </p>
29
+ HTML
30
+
31
+ init_configuration_block block do
32
+ grouping_rules do |issue, rule|
33
+ rule.label = issue.type
34
+ rule.color = color_for type: issue.type
35
+ end
36
+ end
37
+
38
+ @percentage_lines = []
39
+ @highest_cycletime = 0
40
+ end
41
+
42
+ def run
43
+ completed_issues = completed_issues_in_range include_unstarted: false
44
+
45
+ data_sets = create_datasets completed_issues
46
+ overall_percent_line = calculate_percent_line(completed_issues)
47
+ @percentage_lines << [overall_percent_line, 'gray']
48
+
49
+ return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if data_sets.empty?
50
+
51
+ wrap_and_render(binding, __FILE__)
52
+ end
53
+
54
+ def create_datasets completed_issues
55
+ data_sets = []
56
+
57
+ groups = group_issues completed_issues
58
+
59
+ groups.each_key do |rules|
60
+ completed_issues_by_type = groups[rules]
61
+ label = rules.label
62
+ color = rules.color
63
+ percent_line = calculate_percent_line completed_issues_by_type
64
+ data = completed_issues_by_type.collect { |issue| data_for_issue(issue) }.compact
65
+ data_sets << {
66
+ label: "#{label} (85% at #{label_days(percent_line)})",
67
+ data: data,
68
+ fill: false,
69
+ showLine: false,
70
+ backgroundColor: color
71
+ }
72
+
73
+ data_sets << trend_line_data_set(label: label, data: data, color: color)
74
+
75
+ @percentage_lines << [percent_line, color]
76
+ end
77
+ data_sets
78
+ end
79
+
80
+ def show_trend_lines
81
+ @show_trend_lines = true
82
+ end
83
+
84
+ def trend_line_data_set label:, data:, color:
85
+ points = data.collect do |hash|
86
+ [Time.parse(hash[:x]).to_i, hash[:y]]
87
+ end
88
+
89
+ # The trend calculation works with numbers only so convert Time to an int and back
90
+ calculator = TrendLineCalculator.new(points)
91
+ data_points = calculator.chart_datapoints range: time_range.begin.to_i..time_range.end.to_i, max_y: @highest_cycletime
92
+ data_points.each do |point_hash|
93
+ point_hash[:x] = chart_format Time.at(point_hash[:x])
94
+ end
95
+
96
+ {
97
+ type: 'line',
98
+ label: "#{label} Trendline",
99
+ data: data_points,
100
+ fill: false,
101
+ borderWidth: 1,
102
+ markerType: 'none',
103
+ borderColor: color,
104
+ borderDash: [6, 3],
105
+ pointStyle: 'dash',
106
+ hidden: !@show_trend_lines
107
+ }
108
+ end
109
+
110
+ def data_for_issue issue
111
+ cycle_time = issue.board.cycletime.cycletime(issue)
112
+ return nil if cycle_time < 1 # These will get called out on the quality report
113
+
114
+ @highest_cycletime = cycle_time if @highest_cycletime < cycle_time
115
+
116
+ {
117
+ y: cycle_time,
118
+ x: chart_format(issue.board.cycletime.stopped_time(issue)),
119
+ title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
120
+ }
121
+ end
122
+
123
+ def calculate_percent_line completed_issues
124
+ times = completed_issues.collect { |issue| issue.board.cycletime.cycletime(issue) }
125
+ index = times.size * 85 / 100
126
+ times.sort[index]
127
+ end
128
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/daily_wip_chart'
4
+
5
+ class DailyWipByAgeChart < DailyWipChart
6
+ def default_header_text
7
+ 'Daily WIP grouped by Age'
8
+ end
9
+
10
+ def default_description_text
11
+ <<-HTML
12
+ <p>
13
+ This chart shows the highest WIP on each given day. The WIP is color coded so you can see
14
+ how old it is and hovering over the bar will show you exactly which work items it relates
15
+ to. The green bar underneath, shows how many items completed on that day.
16
+ </p>
17
+ <p>
18
+ "Completed without being started" reflects the fact that while we know that it completed
19
+ that day, we were unable to determine when it had started. These items will show up in
20
+ white at the top. Note that the white is approximate because we don't know exactly when
21
+ it started so we're guessing.
22
+ </p>
23
+ HTML
24
+ end
25
+
26
+ def default_grouping_rules issue:, rules:
27
+ cycletime = issue.board.cycletime
28
+ started = cycletime.started_time(issue)&.to_date
29
+ stopped = cycletime.stopped_time(issue)&.to_date
30
+
31
+ rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
32
+
33
+ if stopped && started.nil? # We can't tell when it started
34
+ not_started stopped: stopped, rules: rules, created: issue.created.to_date
35
+ elsif stopped == rules.current_date
36
+ stopped_today rules: rules
37
+ else
38
+ group_by_age started: started, rules: rules
39
+ end
40
+ end
41
+
42
+ def not_started stopped:, rules:, created:
43
+ if stopped == rules.current_date
44
+ rules.label = 'Completed but not started'
45
+ rules.color = '#66FF66'
46
+ rules.group_priority = -1
47
+ else
48
+ rules.label = 'Start date unknown'
49
+ rules.color = 'white'
50
+ rules.group_priority = 11
51
+ created_days = rules.current_date - created + 1
52
+ rules.issue_hint = "(created: #{label_days created_days.to_i} earlier, stopped on #{stopped})"
53
+ end
54
+ end
55
+
56
+ def stopped_today rules:
57
+ rules.label = 'Completed'
58
+ rules.color = '#009900'
59
+ rules.group_priority = -2
60
+ end
61
+
62
+ def group_by_age started:, rules:
63
+ age = rules.current_date - started + 1
64
+
65
+ case age
66
+ when 1
67
+ rules.label = 'Less than a day'
68
+ rules.color = '#aaaaaa'
69
+ rules.group_priority = 10 # Highest is top
70
+ when 2..7
71
+ rules.label = 'A week or less'
72
+ rules.color = '#80bfff'
73
+ rules.group_priority = 9
74
+ when 8..14
75
+ rules.label = 'Two weeks or less'
76
+ rules.color = '#ffd700'
77
+ rules.group_priority = 8
78
+ when 15..28
79
+ rules.label = 'Four weeks or less'
80
+ rules.color = '#ce6300'
81
+ rules.group_priority = 7
82
+ else
83
+ rules.label = 'More than four weeks'
84
+ rules.color = '#990000'
85
+ rules.group_priority = 6
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/daily_wip_chart'
4
+
5
+ class DailyWipByBlockedStalledChart < DailyWipChart
6
+ def default_header_text
7
+ 'Daily WIP, grouped by Blocked and Stalled statuses'
8
+ end
9
+
10
+ def default_description_text
11
+ <<-HTML
12
+ <p>
13
+ This chart highlights work that is blocked or stalled on each given day. In Jira terms, blocked
14
+ means that the issue has been "flagged". Stalled indicates that the item hasn't had any updates in 5 days.
15
+ </p>
16
+ <p>
17
+ Note that if an item tracks as both blocked and stalled, it will only show up in the flagged totals.
18
+ It will not be double counted.
19
+ </p>
20
+ <p>
21
+ The white section reflects items that have stopped but for which we can't identify the start date. As
22
+ a result, we are unable to properly show the WIP for these items.
23
+ </p>
24
+ HTML
25
+ end
26
+
27
+ def key_blocked_stalled_change issue:, date:, end_time:
28
+ stalled_change = nil
29
+ blocked_change = nil
30
+
31
+ issue.blocked_stalled_changes_on_date(date: date, end_time: end_time) do |change|
32
+ blocked_change = change if change.blocked?
33
+ stalled_change = change if change.stalled?
34
+ end
35
+
36
+ return blocked_change if blocked_change
37
+ return stalled_change if stalled_change
38
+
39
+ nil
40
+ end
41
+
42
+ def default_grouping_rules issue:, rules:
43
+ started = issue.board.cycletime.started_time(issue)
44
+ stopped_date = issue.board.cycletime.stopped_time(issue)&.to_date
45
+ change = key_blocked_stalled_change issue: issue, date: rules.current_date, end_time: time_range.end
46
+
47
+ stopped_today = stopped_date == rules.current_date
48
+
49
+ if stopped_today && started.nil?
50
+ rules.label = 'Completed but not started'
51
+ rules.color = '#66FF66'
52
+ rules.group_priority = -1
53
+ elsif stopped_today
54
+ rules.label = 'Completed'
55
+ rules.color = '#009900'
56
+ rules.group_priority = -2
57
+ elsif started.nil?
58
+ rules.label = 'Start date unknown'
59
+ rules.color = 'white'
60
+ rules.group_priority = 4
61
+ elsif change&.blocked?
62
+ rules.label = 'Blocked'
63
+ rules.color = 'red'
64
+ rules.group_priority = 1
65
+ rules.issue_hint = "(#{change.reasons})"
66
+ elsif change&.stalled?
67
+ rules.label = 'Stalled'
68
+ rules.color = 'orange'
69
+ rules.group_priority = 2
70
+ rules.issue_hint = "(#{change.reasons})"
71
+ else
72
+ rules.label = 'Active'
73
+ rules.color = 'lightgray'
74
+ rules.group_priority = 3
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/chart_base'
4
+
5
+ class DailyGroupingRules < GroupingRules
6
+ attr_accessor :current_date, :group_priority, :issue_hint
7
+
8
+ def initialize
9
+ super()
10
+ @group_priority = 0
11
+ end
12
+ end
13
+
14
+ class DailyWipChart < ChartBase
15
+ attr_accessor :possible_statuses
16
+
17
+ def initialize block = nil
18
+ super()
19
+
20
+ header_text default_header_text
21
+ description_text default_description_text
22
+
23
+ if block
24
+ instance_eval(&block)
25
+ else
26
+ grouping_rules do |issue, rules|
27
+ default_grouping_rules issue: issue, rules: rules
28
+ end
29
+ end
30
+ end
31
+
32
+ def run
33
+ issue_rules_by_active_date = group_issues_by_active_dates
34
+ possible_rules = select_possible_rules issue_rules_by_active_date
35
+
36
+ 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
38
+ end
39
+
40
+ wrap_and_render(binding, __FILE__)
41
+ end
42
+
43
+ def default_header_text = 'Daily WIP'
44
+ def default_description_text = ''
45
+
46
+ def default_grouping_rules issue:, rules: # rubocop:disable Lint/UnusedMethodArgument
47
+ raise 'If you use this class directly then you must provide grouping_rules'
48
+ end
49
+
50
+ def select_possible_rules issue_rules_by_active_date
51
+ possible_rules = []
52
+ issue_rules_by_active_date.each_pair do |_date, issues_rules_list|
53
+ issues_rules_list.each do |_issue, rules|
54
+ possible_rules << rules unless possible_rules.any? { |r| r.group == rules.group }
55
+ end
56
+ end
57
+ possible_rules.sort_by!(&:group_priority)
58
+ end
59
+
60
+ def group_issues_by_active_dates
61
+ hash = {}
62
+
63
+ @issues.each do |issue|
64
+ cycletime = issue.board.cycletime
65
+ start = cycletime.started_time(issue)&.to_date
66
+ stop = cycletime.stopped_time(issue)&.to_date
67
+ next if start.nil? && stop.nil?
68
+
69
+ # If it stopped but never started then assume it started at creation so the data points
70
+ # will be available for the config.
71
+ start = issue.created.to_date if stop && start.nil?
72
+ start = @date_range.begin if start < @date_range.begin
73
+
74
+ start.upto(stop || @date_range.end) do |date|
75
+ rule = configure_rule issue: issue, date: date
76
+ (hash[date] ||= []) << [issue, rule] unless rule.ignored?
77
+ end
78
+ end
79
+ hash
80
+ end
81
+
82
+ def make_data_set grouping_rule:, issue_rules_by_active_date:
83
+ positive = grouping_rule.group_priority >= 0
84
+
85
+ data = issue_rules_by_active_date.collect do |date, issue_rules|
86
+ # issues = []
87
+ issue_strings = issue_rules
88
+ .select { |_issue, rules| rules.group == grouping_rule.group }
89
+ .sort_by { |issue, _rules| issue.key_as_i }
90
+ .collect { |issue, rules| "#{issue.key} : #{issue.summary.strip} #{rules.issue_hint}" }
91
+ title = ["#{grouping_rule.label} (#{label_issues issue_strings.size})"] + issue_strings
92
+
93
+ {
94
+ x: date,
95
+ y: positive ? issue_strings.size : -issue_strings.size,
96
+ title: title
97
+ }
98
+ end
99
+
100
+ {
101
+ type: 'bar',
102
+ label: grouping_rule.label,
103
+ data: data,
104
+ backgroundColor: grouping_rule.color || random_color,
105
+ borderColor: 'gray',
106
+ borderWidth: grouping_rule.color == 'white' ? 1 : 0,
107
+ borderRadius: positive ? 0 : 5
108
+ }
109
+ end
110
+
111
+ def configure_rule issue:, date:
112
+ raise 'grouping_rules must be set' if @group_by_block.nil?
113
+
114
+ rules = DailyGroupingRules.new
115
+ rules.current_date = date
116
+ @group_by_block.call issue, rules
117
+ rules
118
+ end
119
+
120
+ def grouping_rules &block
121
+ @group_by_block = block
122
+ end
123
+ end