jirametrics 3.1.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: 46c9c370181743889ae079b5c6ed872644110c23bf5113118dd1fbb9bac217cd
4
- data.tar.gz: 1b40c073695046589fe0d8ca996d0337eb81fda0ffb44ac7875f238369a88d11
3
+ metadata.gz: 9952ccca62cfc09c062633e0ee50828016681b77b537e736e77046a0365a47ca
4
+ data.tar.gz: 577b27edc9b07c42285339995ea3a23f2082d62feaa34a1d225cf7c61300fda0
5
5
  SHA512:
6
- metadata.gz: 79995d870fb4282b57919bcf05cdc77bc8a7c45d83a142fef60ae3313382a098b204f810fe1515b4ac2e799c015a068cff55cd68e0e6affb4834148f6be52739
7
- data.tar.gz: bba299cd94305c9bd0cac2f271235eca287b44b33a77e7e86597aecb662373e71b82b91a074b1ba1e773a4011620a7335cf33b1540810ffe283b89be6554b18f
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: <%= JSON.generate(data_sets) %>
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
- @highest_y_value = y if @highest_y_value < y
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: 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
- times = items.collect { |item| y_value(item) }
112
- times.reject! { |y| min && y < min }
113
- index = times.size * 85 / 100
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.1.1
4
+ version: '3.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler