jirametrics 3.1 → 3.2
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9952ccca62cfc09c062633e0ee50828016681b77b537e736e77046a0365a47ca
|
|
4
|
+
data.tar.gz: 577b27edc9b07c42285339995ea3a23f2082d62feaa34a1d225cf7c61300fda0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29ec03cf3689cd29d772ca781dd750fe33c54d203307b1ce0bb378b35b211a70b33c7d81503b688fad0cd1f7204b14b3fdaebfcc7fd2375ff3dbbc42ed6615c5
|
|
7
|
+
data.tar.gz: b73780c0e68abea384a151d2f194c17e9705f9e507fd1587809d7ddb6691b9157aa82b6ab3a3cdd86ca513acd489b7c5dac733184cd1774887f0e2d6c6e5d990
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
--warning-banner: yellow;
|
|
6
6
|
|
|
7
7
|
--cycletime-scatterplot-overall-trendline-color: gray;
|
|
8
|
+
--cycletime-scatterplot-cap-rule-color: #555555;
|
|
9
|
+
--cycletime-scatterplot-cap-gutter-color: rgba(0, 0, 0, 0.05);
|
|
8
10
|
|
|
9
11
|
--non-working-days-color: #F0F0F0;
|
|
10
12
|
--expedited-color: #D55E00; /* Okabe-Ito vermilion */
|
|
@@ -238,6 +240,8 @@ html[data-theme="dark"] {
|
|
|
238
240
|
--body-background: #343434;
|
|
239
241
|
--default-text-color: #aaa;
|
|
240
242
|
--grid-line-color: #424242;
|
|
243
|
+
--cycletime-scatterplot-cap-rule-color: #b8b8b8;
|
|
244
|
+
--cycletime-scatterplot-cap-gutter-color: rgba(255, 255, 255, 0.08);
|
|
241
245
|
--expedited-color: #E69F00; /* Okabe-Ito orange for dark bg */
|
|
242
246
|
--blocked-color: #E69F00; /* Okabe-Ito orange for dark bg */
|
|
243
247
|
--stalled-color: #F0E442; /* Okabe-Ito yellow - distinct from blocked */
|
|
@@ -294,6 +298,8 @@ html[data-theme="light"] {
|
|
|
294
298
|
--body-background: white;
|
|
295
299
|
--default-text-color: black;
|
|
296
300
|
--grid-line-color: lightgray;
|
|
301
|
+
--cycletime-scatterplot-cap-rule-color: #555555;
|
|
302
|
+
--cycletime-scatterplot-cap-gutter-color: rgba(0, 0, 0, 0.05);
|
|
297
303
|
--expedited-color: #D55E00; /* Okabe-Ito vermilion */
|
|
298
304
|
--blocked-color: #D55E00; /* Okabe-Ito vermilion */
|
|
299
305
|
--stalled-color: #E69F00; /* Okabe-Ito orange */
|
|
@@ -377,6 +383,8 @@ html[data-theme="light"] {
|
|
|
377
383
|
--body-background: #343434;
|
|
378
384
|
--default-text-color: #aaa;
|
|
379
385
|
--grid-line-color: #424242;
|
|
386
|
+
--cycletime-scatterplot-cap-rule-color: #b8b8b8;
|
|
387
|
+
--cycletime-scatterplot-cap-gutter-color: rgba(255, 255, 255, 0.08);
|
|
380
388
|
|
|
381
389
|
--expedited-color: #E69F00; /* Okabe-Ito orange for dark bg */
|
|
382
390
|
--blocked-color: #E69F00; /* Okabe-Ito orange for dark bg */
|
|
@@ -5,8 +5,52 @@
|
|
|
5
5
|
<script>
|
|
6
6
|
new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
7
7
|
type: 'scatter',
|
|
8
|
+
<% if @cap %>
|
|
9
|
+
plugins: [{
|
|
10
|
+
id: 'capRule<%= chart_id %>',
|
|
11
|
+
afterDatasetsDraw: function(chart) {
|
|
12
|
+
const yPixel = chart.scales.y.getPixelForValue(<%= @cap[:sep] %>);
|
|
13
|
+
const area = chart.chartArea;
|
|
14
|
+
const pen = chart.ctx;
|
|
15
|
+
pen.save();
|
|
16
|
+
pen.strokeStyle = <%= CssVariable['--cycletime-scatterplot-cap-rule-color'].to_json %>;
|
|
17
|
+
pen.lineWidth = 1.5;
|
|
18
|
+
[0, 3].forEach(function(offset) {
|
|
19
|
+
pen.beginPath();
|
|
20
|
+
pen.moveTo(area.left, yPixel - offset);
|
|
21
|
+
pen.lineTo(area.right, yPixel - offset);
|
|
22
|
+
pen.stroke();
|
|
23
|
+
});
|
|
24
|
+
pen.restore();
|
|
25
|
+
}
|
|
26
|
+
}],
|
|
27
|
+
<% end %>
|
|
8
28
|
data: {
|
|
9
|
-
datasets:
|
|
29
|
+
datasets: (function() {
|
|
30
|
+
const dataSets = <%= JSON.generate(data_sets) %>;
|
|
31
|
+
<% if @cap %>
|
|
32
|
+
// draw an up-arrow glyph in the given colour, usable as a Chart.js pointStyle.
|
|
33
|
+
// The arrow says "the real value continues upward beyond the cap".
|
|
34
|
+
function capArrow(color) {
|
|
35
|
+
const size = 18, glyph = document.createElement('canvas');
|
|
36
|
+
glyph.width = size; glyph.height = size;
|
|
37
|
+
const pen = glyph.getContext('2d');
|
|
38
|
+
pen.strokeStyle = color; pen.lineWidth = 2.5; pen.lineCap = 'round'; pen.lineJoin = 'round';
|
|
39
|
+
pen.beginPath();
|
|
40
|
+
pen.moveTo(size / 2, size - 3); pen.lineTo(size / 2, 3);
|
|
41
|
+
pen.moveTo(size / 2 - 5, 8); pen.lineTo(size / 2, 3); pen.lineTo(size / 2 + 5, 8);
|
|
42
|
+
pen.stroke();
|
|
43
|
+
return glyph;
|
|
44
|
+
}
|
|
45
|
+
dataSets.forEach(function(dataSet) {
|
|
46
|
+
if (dataSet.type === 'line') return; // skip the interleaved trend lines
|
|
47
|
+
const arrow = capArrow(dataSet.backgroundColor);
|
|
48
|
+
dataSet.pointStyle = dataSet.data.map(function(point) { return point.over ? arrow : 'circle'; });
|
|
49
|
+
dataSet.pointRadius = dataSet.data.map(function(point) { return point.over ? 9 : 3; });
|
|
50
|
+
});
|
|
51
|
+
<% end %>
|
|
52
|
+
return dataSets;
|
|
53
|
+
})()
|
|
10
54
|
},
|
|
11
55
|
options: {
|
|
12
56
|
title: {
|
|
@@ -29,7 +73,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
29
73
|
},
|
|
30
74
|
y: {
|
|
31
75
|
min: 0,
|
|
32
|
-
max: <%= (@highest_y_value * 1.1).ceil %>,
|
|
76
|
+
max: <%= @cap ? @cap[:axis_max] : (@highest_y_value * 1.1).ceil %>,
|
|
33
77
|
scaleLabel: {
|
|
34
78
|
display: true
|
|
35
79
|
},
|
|
@@ -39,6 +83,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
39
83
|
},
|
|
40
84
|
ticks: {
|
|
41
85
|
callback: function(value, index, ticks) {
|
|
86
|
+
<% if @cap %>if (value > <%= @cap[:cutoff] %>) return null;<% end %>
|
|
42
87
|
return index === ticks.length - 1 ? null : value;
|
|
43
88
|
}
|
|
44
89
|
}
|
|
@@ -58,6 +103,32 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
58
103
|
<%= working_days_annotation %>
|
|
59
104
|
<%= date_annotation %>
|
|
60
105
|
|
|
106
|
+
<% if @cap %>
|
|
107
|
+
capGutter: {
|
|
108
|
+
type: 'box',
|
|
109
|
+
yMin: <%= @cap[:sep] %>,
|
|
110
|
+
yMax: <%= @cap[:axis_max] %>,
|
|
111
|
+
backgroundColor: <%= CssVariable['--cycletime-scatterplot-cap-gutter-color'].to_json %>,
|
|
112
|
+
borderWidth: 0
|
|
113
|
+
},
|
|
114
|
+
capLabel: {
|
|
115
|
+
type: 'line',
|
|
116
|
+
yMin: <%= @cap[:sep] %>,
|
|
117
|
+
yMax: <%= @cap[:sep] %>,
|
|
118
|
+
borderWidth: 0,
|
|
119
|
+
drawTime: 'afterDraw',
|
|
120
|
+
label: {
|
|
121
|
+
display: true,
|
|
122
|
+
content: '<%= @cap[:label] %>',
|
|
123
|
+
position: 'start',
|
|
124
|
+
yAdjust: 0,
|
|
125
|
+
backgroundColor: 'rgba(0,0,0,0.85)',
|
|
126
|
+
color: '#fff',
|
|
127
|
+
font: { size: 11 }
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
<% end %>
|
|
131
|
+
|
|
61
132
|
<% @percentage_lines.each_with_index do |args, index| %>
|
|
62
133
|
<% percent, color = args %>
|
|
63
134
|
line<%= index %>: {
|
|
@@ -6,6 +6,8 @@ require 'jirametrics/time_based_chart'
|
|
|
6
6
|
class TimeBasedScatterplot < TimeBasedChart
|
|
7
7
|
include GroupableIssueChart
|
|
8
8
|
|
|
9
|
+
attr_reader :y_axis_cap_percentile
|
|
10
|
+
|
|
9
11
|
def initialize
|
|
10
12
|
super
|
|
11
13
|
|
|
@@ -18,6 +20,10 @@ class TimeBasedScatterplot < TimeBasedChart
|
|
|
18
20
|
@y_axis_title = title
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
def cap_y_axis percentile: 98
|
|
24
|
+
@y_axis_cap_percentile = percentile
|
|
25
|
+
end
|
|
26
|
+
|
|
21
27
|
def run
|
|
22
28
|
items = all_items
|
|
23
29
|
data_sets = create_datasets items
|
|
@@ -33,6 +39,7 @@ class TimeBasedScatterplot < TimeBasedChart
|
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
def create_datasets items
|
|
42
|
+
@cap = compute_cap items
|
|
36
43
|
data_sets = []
|
|
37
44
|
|
|
38
45
|
group_issues(items).each do |rules, items_by_type|
|
|
@@ -61,14 +68,14 @@ class TimeBasedScatterplot < TimeBasedChart
|
|
|
61
68
|
|
|
62
69
|
def trend_line_data_set label:, data:, color:
|
|
63
70
|
points = data.collect do |hash|
|
|
64
|
-
[Time.parse(hash[:x]).to_i, hash[:y]]
|
|
71
|
+
[Time.parse(hash[:x]).to_i, hash[:true_y] || hash[:y]]
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
# The trend calculation works with numbers only so convert Time to an int and back
|
|
68
75
|
calculator = TrendLineCalculator.new(points)
|
|
69
76
|
data_points = calculator.chart_datapoints(
|
|
70
77
|
range: time_range.begin.to_i..time_range.end.to_i,
|
|
71
|
-
max_y: @highest_y_value
|
|
78
|
+
max_y: (@cap ? @cap[:cutoff] : @highest_y_value)
|
|
72
79
|
)
|
|
73
80
|
data_points.each do |point_hash|
|
|
74
81
|
point_hash[:x] = chart_format Time.at(point_hash[:x])
|
|
@@ -97,20 +104,68 @@ class TimeBasedScatterplot < TimeBasedChart
|
|
|
97
104
|
min = minimum_y_value
|
|
98
105
|
return nil if min && y < min
|
|
99
106
|
|
|
100
|
-
|
|
107
|
+
over = @cap && y > @cap[:cutoff]
|
|
108
|
+
plotted_y = over ? @cap[:pin_row] : y
|
|
109
|
+
@highest_y_value = plotted_y if @highest_y_value < plotted_y
|
|
101
110
|
|
|
102
|
-
{
|
|
103
|
-
y:
|
|
111
|
+
point = {
|
|
112
|
+
y: plotted_y,
|
|
104
113
|
x: chart_format(x_value(item)),
|
|
105
114
|
title: [title_value(item, rules: rules)]
|
|
106
115
|
}
|
|
116
|
+
if over
|
|
117
|
+
point[:over] = true
|
|
118
|
+
point[:true_y] = y
|
|
119
|
+
end
|
|
120
|
+
point
|
|
107
121
|
end
|
|
108
122
|
|
|
109
123
|
def calculate_percent_line items
|
|
124
|
+
percentile_value items, 85
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def percentile_value items, percentile
|
|
128
|
+
values = filtered_values(items)
|
|
129
|
+
return nil if values.empty?
|
|
130
|
+
|
|
131
|
+
index = [values.size * percentile / 100, values.size - 1].min
|
|
132
|
+
values.sort[index]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def compute_cap items
|
|
136
|
+
return nil unless @y_axis_cap_percentile
|
|
137
|
+
|
|
138
|
+
cutoff = percentile_value items, @y_axis_cap_percentile
|
|
139
|
+
return nil unless cutoff
|
|
140
|
+
|
|
141
|
+
values = filtered_values(items)
|
|
142
|
+
outlier_count = values.count { |value| value > cutoff }
|
|
143
|
+
return nil if outlier_count.zero?
|
|
144
|
+
|
|
145
|
+
pad = cutoff * 0.06 # breathing room so the top real dot does not touch the break
|
|
146
|
+
gutter_height = cutoff * 0.15
|
|
147
|
+
sep = cutoff + pad
|
|
148
|
+
{
|
|
149
|
+
cutoff: cutoff,
|
|
150
|
+
sep: sep,
|
|
151
|
+
pin_row: sep + (gutter_height * 0.55),
|
|
152
|
+
axis_max: (sep + gutter_height).ceil,
|
|
153
|
+
outlier_count: outlier_count,
|
|
154
|
+
label: cap_label(outlier_count: outlier_count, cutoff: cutoff)
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def cap_label outlier_count:, cutoff:
|
|
161
|
+
item_word = outlier_count == 1 ? 'item' : 'items'
|
|
162
|
+
"#{outlier_count} #{item_word} above #{cutoff.round} days"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def filtered_values items
|
|
110
166
|
min = minimum_y_value
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
times.sort[index]
|
|
167
|
+
values = items.collect { |item| y_value(item) }
|
|
168
|
+
values.reject! { |value| min && value < min }
|
|
169
|
+
values
|
|
115
170
|
end
|
|
116
171
|
end
|
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: '3.
|
|
4
|
+
version: '3.2'
|
|
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: csv
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
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: json-schema
|
|
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: mcp
|
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -203,7 +231,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
203
231
|
- !ruby/object:Gem::Version
|
|
204
232
|
version: '0'
|
|
205
233
|
requirements: []
|
|
206
|
-
rubygems_version: 4.0.
|
|
234
|
+
rubygems_version: 4.0.17
|
|
207
235
|
specification_version: 4
|
|
208
236
|
summary: Extract Jira metrics
|
|
209
237
|
test_files: []
|