jirametrics 2.22 → 2.27
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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +20 -6
- data/lib/jirametrics/aging_work_table.rb +4 -5
- data/lib/jirametrics/anonymizer.rb +74 -1
- data/lib/jirametrics/atlassian_document_format.rb +93 -93
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +20 -8
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/board_movement_calculator.rb +2 -2
- data/lib/jirametrics/cfd_data_builder.rb +108 -0
- data/lib/jirametrics/change_item.rb +4 -3
- data/lib/jirametrics/chart_base.rb +94 -2
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
- data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
- data/lib/jirametrics/cycletime_histogram.rb +15 -103
- data/lib/jirametrics/cycletime_scatterplot.rb +13 -98
- data/lib/jirametrics/daily_view.rb +36 -12
- data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
- data/lib/jirametrics/daily_wip_chart.rb +29 -7
- data/lib/jirametrics/data_quality_report.rb +38 -12
- data/lib/jirametrics/dependency_chart.rb +2 -2
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +87 -5
- data/lib/jirametrics/downloader_for_cloud.rb +52 -10
- data/lib/jirametrics/downloader_for_data_center.rb +2 -1
- data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
- data/lib/jirametrics/examples/aggregated_project.rb +2 -2
- data/lib/jirametrics/examples/standard_project.rb +29 -19
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +3 -1
- data/lib/jirametrics/file_system.rb +35 -2
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
- data/lib/jirametrics/github_gateway.rb +115 -0
- data/lib/jirametrics/groupable_issue_chart.rb +4 -0
- data/lib/jirametrics/grouping_rules.rb +26 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
- data/lib/jirametrics/html/aging_work_table.erb +3 -0
- data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +38 -5
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
- data/lib/jirametrics/html/expedited_chart.erb +3 -13
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
- data/lib/jirametrics/html/index.css +117 -0
- data/lib/jirametrics/html/index.erb +6 -0
- data/lib/jirametrics/html/index.js +52 -2
- data/lib/jirametrics/html/sprint_burndown.erb +7 -13
- data/lib/jirametrics/html/throughput_chart.erb +40 -9
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +59 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +11 -7
- data/lib/jirametrics/html_generator.rb +2 -1
- data/lib/jirametrics/html_report_config.rb +23 -16
- data/lib/jirametrics/issue.rb +101 -96
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +6 -3
- data/lib/jirametrics/mcp_server.rb +305 -0
- data/lib/jirametrics/project_config.rb +80 -7
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
- data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
- data/lib/jirametrics/pull_request_review.rb +13 -0
- data/lib/jirametrics/raw_javascript.rb +4 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint_burndown.rb +3 -1
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/stitcher.rb +7 -1
- data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
- data/lib/jirametrics/throughput_chart.rb +73 -23
- data/lib/jirametrics/time_based_histogram.rb +139 -0
- data/lib/jirametrics/time_based_scatterplot.rb +107 -0
- data/lib/jirametrics.rb +28 -0
- metadata +47 -5
|
@@ -29,6 +29,8 @@ class SprintBurndown < ChartBase
|
|
|
29
29
|
</div>
|
|
30
30
|
#{describe_non_working_days}
|
|
31
31
|
TEXT
|
|
32
|
+
@x_axis_title = 'Date'
|
|
33
|
+
@y_axis_title = 'Items remaining'
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def options= arg
|
|
@@ -132,7 +134,7 @@ class SprintBurndown < ChartBase
|
|
|
132
134
|
|
|
133
135
|
estimate_display_name = current_board.estimation_configuration.display_name
|
|
134
136
|
|
|
135
|
-
issue_completed_time = issue.
|
|
137
|
+
issue_completed_time = issue.started_stopped_times.last
|
|
136
138
|
completed_has_been_tracked = false
|
|
137
139
|
|
|
138
140
|
issue.changes.each do |change|
|
data/lib/jirametrics/status.rb
CHANGED
|
@@ -36,7 +36,7 @@ class Status
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def self.from_raw raw
|
|
39
|
-
raise
|
|
39
|
+
raise 'raw cannot be nil' if raw.nil?
|
|
40
40
|
|
|
41
41
|
category_config = raw['statusCategory']
|
|
42
42
|
raise "statusCategory can't be nil in #{category_config.inspect}" if category_config.nil?
|
data/lib/jirametrics/stitcher.rb
CHANGED
|
@@ -44,7 +44,8 @@ class Stitcher < HtmlGenerator
|
|
|
44
44
|
stitch_content = @all_stitches.find { |s| s.file == from_file && s.title == title && s.type == type }
|
|
45
45
|
return stitch_content.content if stitch_content
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
file_system.error "Unable to find content in file #{from_file.inspect} matching title: #{title.inspect}"
|
|
48
|
+
''
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def parse_file filename
|
|
@@ -59,6 +60,11 @@ class Stitcher < HtmlGenerator
|
|
|
59
60
|
if matches[:seam] == 'start'
|
|
60
61
|
content = +''
|
|
61
62
|
else
|
|
63
|
+
if content.nil? || content.strip.empty?
|
|
64
|
+
file_system.warning "Seam found with no content in #{filename.inspect}: " \
|
|
65
|
+
"id=#{matches[:id].strip.inspect}, class=#{matches[:clazz].strip.inspect}, " \
|
|
66
|
+
"title=#{matches[:title].strip.inspect}"
|
|
67
|
+
end
|
|
62
68
|
@all_stitches << Stitcher::StitchContent.new(
|
|
63
69
|
file: filename, title: matches[:title], type: matches[:type], content: content
|
|
64
70
|
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jirametrics/throughput_chart'
|
|
4
|
+
|
|
5
|
+
class ThroughputByCompletedResolutionChart < ThroughputChart
|
|
6
|
+
def initialize block
|
|
7
|
+
super
|
|
8
|
+
header_text 'Throughput, grouped by completion status and resolution'
|
|
9
|
+
description_text nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def default_grouping_rules issue, rules
|
|
13
|
+
status, resolution = issue.status_resolution_at_done
|
|
14
|
+
if resolution
|
|
15
|
+
rules.label = "#{status.name}:#{resolution}"
|
|
16
|
+
rules.label_hint = "Status: #{status.name.inspect}:#{status.id}, resolution: #{resolution.inspect}"
|
|
17
|
+
else
|
|
18
|
+
rules.label = status.name
|
|
19
|
+
rules.label_hint = "Status: #{status.name.inspect}:#{status.id}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'cgi'
|
|
4
|
+
|
|
3
5
|
class ThroughputChart < ChartBase
|
|
4
6
|
include GroupableIssueChart
|
|
5
7
|
|
|
@@ -10,42 +12,54 @@ class ThroughputChart < ChartBase
|
|
|
10
12
|
|
|
11
13
|
header_text 'Throughput Chart'
|
|
12
14
|
description_text <<-TEXT
|
|
13
|
-
<div
|
|
14
|
-
|
|
15
|
+
<div>Throughput data is very useful for#{' '}
|
|
16
|
+
<a href="https://blog.mikebowler.ca/2024/06/02/probabilistic-forecasting/">probabilistic forecasting</a>,
|
|
17
|
+
to determine when we'll be done. Try it now with the
|
|
18
|
+
<a href="<%= throughput_forecaster_url %>" target="_blank" rel="noopener noreferrer">
|
|
19
|
+
Focused Objective throughput forecaster,</a> to see how long it would take to complete all of the
|
|
20
|
+
<%= @not_started_count %> items you currently have in your backlog.
|
|
15
21
|
</div>
|
|
16
22
|
#{describe_non_working_days}
|
|
17
23
|
TEXT
|
|
24
|
+
@x_axis_title = nil
|
|
25
|
+
@y_axis_title = 'Count of items'
|
|
18
26
|
|
|
19
27
|
init_configuration_block(block) do
|
|
20
|
-
grouping_rules
|
|
21
|
-
rule.label = issue.type
|
|
22
|
-
rule.color = color_for type: issue.type
|
|
23
|
-
end
|
|
28
|
+
grouping_rules { |issue, rule| default_grouping_rules(issue, rule) }
|
|
24
29
|
end
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def run
|
|
33
|
+
# This is saved as an instance variable so that it's accessible later when rendering the description text
|
|
34
|
+
@not_started_count = issues.count { |issue| issue.started_stopped_times.first.nil? }
|
|
35
|
+
|
|
28
36
|
completed_issues = completed_issues_in_range include_unstarted: true
|
|
29
37
|
rules_to_issues = group_issues completed_issues
|
|
30
38
|
data_sets = []
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
total_data_set = weekly_throughput_dataset(
|
|
40
|
+
completed_issues: completed_issues,
|
|
41
|
+
label: 'Totals',
|
|
42
|
+
color: CssVariable['--throughput_chart_total_line_color'],
|
|
43
|
+
dashed: true
|
|
44
|
+
)
|
|
45
|
+
@throughput_samples = total_data_set[:data].collect { |d| d[:y] }
|
|
46
|
+
data_sets << total_data_set if rules_to_issues.size > 1
|
|
39
47
|
|
|
40
48
|
rules_to_issues.each_key do |rules|
|
|
41
49
|
data_sets << weekly_throughput_dataset(
|
|
42
|
-
completed_issues: rules_to_issues[rules], label: rules.label, color: rules.color
|
|
50
|
+
completed_issues: rules_to_issues[rules], label: rules.label, color: rules.color,
|
|
51
|
+
label_hint: rules.label_hint
|
|
43
52
|
)
|
|
44
53
|
end
|
|
45
54
|
|
|
46
55
|
wrap_and_render(binding, __FILE__)
|
|
47
56
|
end
|
|
48
57
|
|
|
58
|
+
def default_grouping_rules issue, rule
|
|
59
|
+
rule.label = issue.type
|
|
60
|
+
rule.color = color_for type: issue.type
|
|
61
|
+
end
|
|
62
|
+
|
|
49
63
|
def calculate_time_periods
|
|
50
64
|
first_day = @date_range.begin
|
|
51
65
|
first_day = case first_day.wday
|
|
@@ -65,10 +79,22 @@ class ThroughputChart < ChartBase
|
|
|
65
79
|
end
|
|
66
80
|
end
|
|
67
81
|
|
|
68
|
-
def
|
|
82
|
+
def calculate_custom_periods
|
|
83
|
+
last_days = @issue_periods.values.compact.uniq.sort
|
|
84
|
+
last_days.each_with_index.map do |last_day, i|
|
|
85
|
+
first_day = i.zero? ? @date_range.begin : last_days[i - 1] + 1
|
|
86
|
+
first_day..last_day
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def weekly_throughput_dataset completed_issues:, label:, color:, dashed: false, label_hint: nil
|
|
91
|
+
periods = @issue_periods&.values&.any? ? calculate_custom_periods : calculate_time_periods
|
|
69
92
|
result = {
|
|
70
93
|
label: label,
|
|
71
|
-
|
|
94
|
+
label_hint: label_hint,
|
|
95
|
+
data: throughput_dataset(
|
|
96
|
+
periods: periods, completed_issues: completed_issues, label_hint: label_hint
|
|
97
|
+
),
|
|
72
98
|
fill: false,
|
|
73
99
|
showLine: true,
|
|
74
100
|
borderColor: color,
|
|
@@ -79,20 +105,44 @@ class ThroughputChart < ChartBase
|
|
|
79
105
|
result
|
|
80
106
|
end
|
|
81
107
|
|
|
82
|
-
def
|
|
108
|
+
def throughput_forecaster_url
|
|
109
|
+
params = {
|
|
110
|
+
throughputMode: 'data',
|
|
111
|
+
samplesText: @throughput_samples.join(','),
|
|
112
|
+
storyLow: @not_started_count,
|
|
113
|
+
storyHigh: @not_started_count
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
query = params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
|
|
117
|
+
"https://focusedobjective.com/throughput?#{query}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def throughput_dataset periods:, completed_issues:, label_hint: nil
|
|
121
|
+
custom_mode = @issue_periods&.values&.any?
|
|
83
122
|
periods.collect do |period|
|
|
84
123
|
closed_issues = completed_issues.filter_map do |issue|
|
|
85
|
-
stop_date = issue.
|
|
86
|
-
|
|
124
|
+
stop_date = issue.started_stopped_dates.last
|
|
125
|
+
next unless stop_date
|
|
126
|
+
|
|
127
|
+
if custom_mode
|
|
128
|
+
[stop_date, issue] if @issue_periods[issue] == period.end
|
|
129
|
+
elsif period.include?(stop_date)
|
|
130
|
+
[stop_date, issue]
|
|
131
|
+
end
|
|
87
132
|
end
|
|
88
133
|
|
|
89
134
|
date_label = "on #{period.end}"
|
|
90
135
|
date_label = "between #{period.begin} and #{period.end}" unless period.begin == period.end
|
|
91
136
|
|
|
92
|
-
{
|
|
137
|
+
with_label_hint = label_hint ? " with #{label_hint}" : ''
|
|
138
|
+
{
|
|
139
|
+
y: closed_issues.size,
|
|
93
140
|
x: "#{period.end}T23:59:59",
|
|
94
|
-
title: ["#{closed_issues.size} items
|
|
95
|
-
closed_issues.collect
|
|
141
|
+
title: ["#{closed_issues.size} items closed#{with_label_hint} #{date_label}"] +
|
|
142
|
+
closed_issues.collect do |_stop_date, issue|
|
|
143
|
+
hint = @issue_hints&.fetch(issue, nil)
|
|
144
|
+
"#{issue.key} : #{issue.summary}#{" #{hint}" if hint}"
|
|
145
|
+
end
|
|
96
146
|
}
|
|
97
147
|
end
|
|
98
148
|
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jirametrics/groupable_issue_chart'
|
|
4
|
+
|
|
5
|
+
class TimeBasedHistogram < ChartBase
|
|
6
|
+
include GroupableIssueChart
|
|
7
|
+
|
|
8
|
+
attr_reader :show_stats
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
super
|
|
12
|
+
|
|
13
|
+
percentiles [50, 85, 98]
|
|
14
|
+
@show_stats = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def percentiles percs = nil
|
|
18
|
+
@percentiles = percs unless percs.nil?
|
|
19
|
+
@percentiles
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def disable_stats
|
|
23
|
+
@show_stats = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run
|
|
27
|
+
histogram_items = all_items
|
|
28
|
+
rules_to_items = group_issues histogram_items
|
|
29
|
+
|
|
30
|
+
the_stats = {}
|
|
31
|
+
|
|
32
|
+
overall_histogram = histogram_data_for(items: histogram_items).transform_values(&:size)
|
|
33
|
+
the_stats[:all] = stats_for histogram_data: overall_histogram, percentiles: @percentiles
|
|
34
|
+
data_sets = rules_to_items.keys.collect do |rules|
|
|
35
|
+
the_label = rules.label
|
|
36
|
+
the_histogram = histogram_data_for(items: rules_to_items[rules])
|
|
37
|
+
if @show_stats
|
|
38
|
+
the_stats[the_label] = stats_for(
|
|
39
|
+
histogram_data: the_histogram.transform_values(&:size), percentiles: @percentiles
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
data_set_for(
|
|
44
|
+
histogram_data: the_histogram,
|
|
45
|
+
label: the_label,
|
|
46
|
+
color: rules.color
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if data_sets.empty?
|
|
51
|
+
return "<h1 class='foldable'>#{@header_text}</h1>" \
|
|
52
|
+
'<div>No data matched the selected criteria. Nothing to show.</div>'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
wrap_and_render(binding, __FILE__)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def histogram_data_for items:
|
|
59
|
+
items_hash = {}
|
|
60
|
+
items.each do |item|
|
|
61
|
+
days = value_for_item item
|
|
62
|
+
(items_hash[days] ||= []) << item if days.positive?
|
|
63
|
+
end
|
|
64
|
+
items_hash
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stats_for histogram_data:, percentiles:
|
|
68
|
+
return {} if histogram_data.empty?
|
|
69
|
+
|
|
70
|
+
total_values = histogram_data.values.sum
|
|
71
|
+
|
|
72
|
+
# Calculate the average
|
|
73
|
+
weighted_sum = histogram_data.reduce(0) { |sum, (value, frequency)| sum + (value * frequency) }
|
|
74
|
+
average = total_values.zero? ? 0 : weighted_sum.to_f / total_values
|
|
75
|
+
|
|
76
|
+
# Find the mode (or modes!) and the spread of the distribution
|
|
77
|
+
sorted_histogram = histogram_data.sort_by { |_value, frequency| frequency }
|
|
78
|
+
max_freq = sorted_histogram[-1][1]
|
|
79
|
+
mode = sorted_histogram.select { |_v, f| f == max_freq }
|
|
80
|
+
|
|
81
|
+
minmax = histogram_data.keys.minmax
|
|
82
|
+
|
|
83
|
+
# Calculate percentiles
|
|
84
|
+
sorted_values = histogram_data.keys.sort
|
|
85
|
+
cumulative_counts = {}
|
|
86
|
+
cumulative_sum = 0
|
|
87
|
+
|
|
88
|
+
sorted_values.each do |value|
|
|
89
|
+
cumulative_sum += histogram_data[value]
|
|
90
|
+
cumulative_counts[value] = cumulative_sum
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
percentile_results = {}
|
|
94
|
+
percentiles.each do |percentile|
|
|
95
|
+
rank = (percentile / 100.0) * total_values
|
|
96
|
+
percentile_value = sorted_values.find { |value| cumulative_counts[value] >= rank }
|
|
97
|
+
percentile_results[percentile] = percentile_value
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
average: average,
|
|
102
|
+
mode: mode.collect(&:first).sort,
|
|
103
|
+
min: minmax[0],
|
|
104
|
+
max: minmax[1],
|
|
105
|
+
percentiles: percentile_results
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def sort_items items
|
|
110
|
+
items
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def label_for_item item, hint:
|
|
114
|
+
raise NotImplementedError, "#{self.class} must implement label_for_item"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def data_set_for histogram_data:, label:, color:
|
|
118
|
+
{
|
|
119
|
+
type: 'bar',
|
|
120
|
+
label: label,
|
|
121
|
+
data: histogram_data.keys.sort.filter_map do |days|
|
|
122
|
+
items = histogram_data[days]
|
|
123
|
+
next if items.empty?
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
x: days,
|
|
127
|
+
y: items.size,
|
|
128
|
+
title: [title_for_item(count: items.size, value: days)] +
|
|
129
|
+
sort_items(items).collect do |item|
|
|
130
|
+
hint = @issue_hints&.fetch(item, nil)
|
|
131
|
+
label_for_item(item, hint: hint)
|
|
132
|
+
end
|
|
133
|
+
}
|
|
134
|
+
end,
|
|
135
|
+
backgroundColor: color,
|
|
136
|
+
borderRadius: 0
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jirametrics/groupable_issue_chart'
|
|
4
|
+
|
|
5
|
+
class TimeBasedScatterplot < ChartBase
|
|
6
|
+
include GroupableIssueChart
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
super
|
|
10
|
+
|
|
11
|
+
@percentage_lines = []
|
|
12
|
+
@highest_y_value = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
items = all_items
|
|
17
|
+
data_sets = create_datasets items
|
|
18
|
+
overall_percent_line = calculate_percent_line(items)
|
|
19
|
+
@percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
|
|
20
|
+
|
|
21
|
+
return "<h1 class='foldable'>#{@header_text}</h1><div>No data matched the selected criteria. Nothing to show.</div>" if data_sets.empty?
|
|
22
|
+
|
|
23
|
+
wrap_and_render(binding, __FILE__)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_datasets items
|
|
27
|
+
data_sets = []
|
|
28
|
+
|
|
29
|
+
group_issues(items).each do |rules, items_by_type|
|
|
30
|
+
label = rules.label
|
|
31
|
+
color = rules.color
|
|
32
|
+
percent_line = calculate_percent_line items_by_type
|
|
33
|
+
data = items_by_type.filter_map { |item| data_for_item(item, rules: rules) }
|
|
34
|
+
data_sets << {
|
|
35
|
+
label: "#{label} (85% at #{label_days(percent_line)})",
|
|
36
|
+
data: data,
|
|
37
|
+
fill: false,
|
|
38
|
+
showLine: false,
|
|
39
|
+
backgroundColor: color
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
data_sets << trend_line_data_set(label: label, data: data, color: color)
|
|
43
|
+
|
|
44
|
+
@percentage_lines << [percent_line, color]
|
|
45
|
+
end
|
|
46
|
+
data_sets
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def show_trend_lines
|
|
50
|
+
@show_trend_lines = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def trend_line_data_set label:, data:, color:
|
|
54
|
+
points = data.collect do |hash|
|
|
55
|
+
[Time.parse(hash[:x]).to_i, hash[:y]]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The trend calculation works with numbers only so convert Time to an int and back
|
|
59
|
+
calculator = TrendLineCalculator.new(points)
|
|
60
|
+
data_points = calculator.chart_datapoints(
|
|
61
|
+
range: time_range.begin.to_i..time_range.end.to_i,
|
|
62
|
+
max_y: @highest_y_value
|
|
63
|
+
)
|
|
64
|
+
data_points.each do |point_hash|
|
|
65
|
+
point_hash[:x] = chart_format Time.at(point_hash[:x])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
type: 'line',
|
|
70
|
+
label: "#{label} Trendline",
|
|
71
|
+
data: data_points,
|
|
72
|
+
fill: false,
|
|
73
|
+
borderWidth: 1,
|
|
74
|
+
markerType: 'none',
|
|
75
|
+
borderColor: color,
|
|
76
|
+
borderDash: [6, 3],
|
|
77
|
+
pointStyle: 'dash',
|
|
78
|
+
hidden: !@show_trend_lines
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def minimum_y_value
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def data_for_item item, rules: nil
|
|
87
|
+
y = y_value(item)
|
|
88
|
+
min = minimum_y_value
|
|
89
|
+
return nil if min && y < min
|
|
90
|
+
|
|
91
|
+
@highest_y_value = y if @highest_y_value < y
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
y: y,
|
|
95
|
+
x: chart_format(x_value(item)),
|
|
96
|
+
title: [title_value(item, rules: rules)]
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def calculate_percent_line items
|
|
101
|
+
min = minimum_y_value
|
|
102
|
+
times = items.collect { |item| y_value(item) }
|
|
103
|
+
times.reject! { |y| min && y < min }
|
|
104
|
+
index = times.size * 85 / 100
|
|
105
|
+
times.sort[index]
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/jirametrics.rb
CHANGED
|
@@ -52,6 +52,34 @@ class JiraMetrics < Thor
|
|
|
52
52
|
Exporter.instance.info(key, name_filter: options[:name] || '*')
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
option :config
|
|
56
|
+
option :name
|
|
57
|
+
desc 'mcp', 'Start in MCP (Model Context Protocol) server mode'
|
|
58
|
+
def mcp
|
|
59
|
+
load_config options[:config]
|
|
60
|
+
require 'jirametrics/mcp_server'
|
|
61
|
+
|
|
62
|
+
Exporter.instance.file_system.log_only = true
|
|
63
|
+
|
|
64
|
+
projects = {}
|
|
65
|
+
Exporter.instance.each_project_config(name_filter: options[:name] || '*') do |project|
|
|
66
|
+
project.evaluate_next_level
|
|
67
|
+
project.run load_only: true
|
|
68
|
+
projects[project.name || 'default'] = {
|
|
69
|
+
issues: project.issues,
|
|
70
|
+
today: project.time_range.end.to_date,
|
|
71
|
+
end_time: project.time_range.end
|
|
72
|
+
}
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
next if e.message.start_with? 'This is an aggregated project'
|
|
75
|
+
next if e.message.start_with? 'No data found'
|
|
76
|
+
|
|
77
|
+
raise
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
McpServer.new(projects: projects, timezone_offset: Exporter.instance.timezone_offset).run
|
|
81
|
+
end
|
|
82
|
+
|
|
55
83
|
option :config
|
|
56
84
|
desc 'stitch', 'Dump information about one issue'
|
|
57
85
|
def stitch stitch_file = 'stitcher.erb'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jirametrics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '2.
|
|
4
|
+
version: '2.27'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Bowler
|
|
@@ -9,6 +9,34 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: mutant-rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: mcp
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
12
40
|
- !ruby/object:Gem::Dependency
|
|
13
41
|
name: random-word
|
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -71,12 +99,15 @@ files:
|
|
|
71
99
|
- lib/jirametrics/board.rb
|
|
72
100
|
- lib/jirametrics/board_column.rb
|
|
73
101
|
- lib/jirametrics/board_config.rb
|
|
102
|
+
- lib/jirametrics/board_feature.rb
|
|
74
103
|
- lib/jirametrics/board_movement_calculator.rb
|
|
104
|
+
- lib/jirametrics/cfd_data_builder.rb
|
|
75
105
|
- lib/jirametrics/change_item.rb
|
|
76
106
|
- lib/jirametrics/chart_base.rb
|
|
77
107
|
- lib/jirametrics/columns_config.rb
|
|
78
108
|
- lib/jirametrics/css_variable.rb
|
|
79
|
-
- lib/jirametrics/
|
|
109
|
+
- lib/jirametrics/cumulative_flow_diagram.rb
|
|
110
|
+
- lib/jirametrics/cycle_time_config.rb
|
|
80
111
|
- lib/jirametrics/cycletime_histogram.rb
|
|
81
112
|
- lib/jirametrics/cycletime_scatterplot.rb
|
|
82
113
|
- lib/jirametrics/daily_view.rb
|
|
@@ -100,6 +131,7 @@ files:
|
|
|
100
131
|
- lib/jirametrics/file_system.rb
|
|
101
132
|
- lib/jirametrics/fix_version.rb
|
|
102
133
|
- lib/jirametrics/flow_efficiency_scatterplot.rb
|
|
134
|
+
- lib/jirametrics/github_gateway.rb
|
|
103
135
|
- lib/jirametrics/groupable_issue_chart.rb
|
|
104
136
|
- lib/jirametrics/grouping_rules.rb
|
|
105
137
|
- lib/jirametrics/hierarchy_table.rb
|
|
@@ -107,8 +139,7 @@ files:
|
|
|
107
139
|
- lib/jirametrics/html/aging_work_in_progress_chart.erb
|
|
108
140
|
- lib/jirametrics/html/aging_work_table.erb
|
|
109
141
|
- lib/jirametrics/html/collapsible_issues_panel.erb
|
|
110
|
-
- lib/jirametrics/html/
|
|
111
|
-
- lib/jirametrics/html/cycletime_scatterplot.erb
|
|
142
|
+
- lib/jirametrics/html/cumulative_flow_diagram.erb
|
|
112
143
|
- lib/jirametrics/html/daily_wip_chart.erb
|
|
113
144
|
- lib/jirametrics/html/estimate_accuracy_chart.erb
|
|
114
145
|
- lib/jirametrics/html/expedited_chart.erb
|
|
@@ -119,13 +150,21 @@ files:
|
|
|
119
150
|
- lib/jirametrics/html/index.js
|
|
120
151
|
- lib/jirametrics/html/sprint_burndown.erb
|
|
121
152
|
- lib/jirametrics/html/throughput_chart.erb
|
|
153
|
+
- lib/jirametrics/html/time_based_histogram.erb
|
|
154
|
+
- lib/jirametrics/html/time_based_scatterplot.erb
|
|
122
155
|
- lib/jirametrics/html_generator.rb
|
|
123
156
|
- lib/jirametrics/html_report_config.rb
|
|
124
157
|
- lib/jirametrics/issue.rb
|
|
125
158
|
- lib/jirametrics/issue_collection.rb
|
|
126
159
|
- lib/jirametrics/issue_link.rb
|
|
160
|
+
- lib/jirametrics/issue_printer.rb
|
|
127
161
|
- lib/jirametrics/jira_gateway.rb
|
|
162
|
+
- lib/jirametrics/mcp_server.rb
|
|
128
163
|
- lib/jirametrics/project_config.rb
|
|
164
|
+
- lib/jirametrics/pull_request.rb
|
|
165
|
+
- lib/jirametrics/pull_request_cycle_time_histogram.rb
|
|
166
|
+
- lib/jirametrics/pull_request_cycle_time_scatterplot.rb
|
|
167
|
+
- lib/jirametrics/pull_request_review.rb
|
|
129
168
|
- lib/jirametrics/raw_javascript.rb
|
|
130
169
|
- lib/jirametrics/rules.rb
|
|
131
170
|
- lib/jirametrics/self_or_issue_dispatcher.rb
|
|
@@ -136,7 +175,10 @@ files:
|
|
|
136
175
|
- lib/jirametrics/status.rb
|
|
137
176
|
- lib/jirametrics/status_collection.rb
|
|
138
177
|
- lib/jirametrics/stitcher.rb
|
|
178
|
+
- lib/jirametrics/throughput_by_completed_resolution_chart.rb
|
|
139
179
|
- lib/jirametrics/throughput_chart.rb
|
|
180
|
+
- lib/jirametrics/time_based_histogram.rb
|
|
181
|
+
- lib/jirametrics/time_based_scatterplot.rb
|
|
140
182
|
- lib/jirametrics/tree_organizer.rb
|
|
141
183
|
- lib/jirametrics/trend_line_calculator.rb
|
|
142
184
|
- lib/jirametrics/user.rb
|
|
@@ -163,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
163
205
|
- !ruby/object:Gem::Version
|
|
164
206
|
version: '0'
|
|
165
207
|
requirements: []
|
|
166
|
-
rubygems_version:
|
|
208
|
+
rubygems_version: 4.0.8
|
|
167
209
|
specification_version: 4
|
|
168
210
|
summary: Extract Jira metrics
|
|
169
211
|
test_files: []
|