jirametrics 2.22 → 2.30
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/bin/jirametrics-mcp +5 -0
- data/lib/jirametrics/aggregate_config.rb +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +26 -10
- data/lib/jirametrics/aging_work_in_progress_chart.rb +43 -11
- data/lib/jirametrics/aging_work_table.rb +9 -7
- 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 +28 -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 +107 -3
- 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 +38 -13
- 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 +107 -22
- data/lib/jirametrics/downloader_for_data_center.rb +3 -2
- 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 +32 -19
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +15 -2
- data/lib/jirametrics/file_config.rb +9 -11
- 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 +228 -60
- data/lib/jirametrics/html/index.erb +6 -0
- data/lib/jirametrics/html/index.js +53 -3
- data/lib/jirametrics/html/legacy_colors.css +174 -0
- 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/wip_by_column_chart.erb +250 -0
- data/lib/jirametrics/html_generator.rb +2 -1
- data/lib/jirametrics/html_report_config.rb +45 -33
- data/lib/jirametrics/issue.rb +197 -99
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +32 -10
- data/lib/jirametrics/mcp_server.rb +531 -0
- data/lib/jirametrics/project_config.rb +87 -8
- 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 +4 -2
- 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/wip_by_column_chart.rb +236 -0
- data/lib/jirametrics.rb +58 -0
- metadata +52 -5
|
@@ -22,10 +22,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
22
22
|
time: {
|
|
23
23
|
format: 'YYYY-MM-DD'
|
|
24
24
|
},
|
|
25
|
-
|
|
26
|
-
display: true,
|
|
27
|
-
labelString: 'Date'
|
|
28
|
-
},
|
|
25
|
+
<%= render_axis_title :x %>
|
|
29
26
|
min: "<%= date_range.begin.to_s %>",
|
|
30
27
|
max: "<%= (date_range.end + 1).to_s %>",
|
|
31
28
|
grid: {
|
|
@@ -33,14 +30,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
33
30
|
},
|
|
34
31
|
},
|
|
35
32
|
y: {
|
|
36
|
-
|
|
37
|
-
display: true,
|
|
38
|
-
labelString: 'Items remaining'
|
|
39
|
-
},
|
|
40
|
-
title: {
|
|
41
|
-
display: true,
|
|
42
|
-
text: "<%= y_axis_title %>"
|
|
43
|
-
},
|
|
33
|
+
<%= render_axis_title :y %>
|
|
44
34
|
min: 0.0,
|
|
45
35
|
grid: {
|
|
46
36
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -77,7 +67,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
77
67
|
<table class='standard' style="margin-left: 1em;">
|
|
78
68
|
<thead>
|
|
79
69
|
<th>Sprint</th>
|
|
80
|
-
<th>
|
|
70
|
+
<th>Started</th>
|
|
71
|
+
<th>Completed</th>
|
|
72
|
+
<th>Days</th>
|
|
81
73
|
<th>State</th>
|
|
82
74
|
<th>Started</th>
|
|
83
75
|
<th>Completed</th>
|
|
@@ -90,6 +82,8 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
90
82
|
<% @summary_stats.keys.sort_by(&:start_time).each do |sprint| %>
|
|
91
83
|
<tr>
|
|
92
84
|
<td><%= sprint.name %></td>
|
|
85
|
+
<td><%= sprint.start_time.to_date %></td>
|
|
86
|
+
<td><%= sprint.completed_time&.to_date %></td>
|
|
93
87
|
<td><%= sprint.day_count %></td>
|
|
94
88
|
<td><%= sprint.raw['state'] %></td>
|
|
95
89
|
<% stats = @summary_stats[sprint] %>
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
4
4
|
</div>
|
|
5
5
|
<script>
|
|
6
|
+
if (!Chart.Tooltip.positioners.legendItem) {
|
|
7
|
+
Chart.Tooltip.positioners.legendItem = function(items) {
|
|
8
|
+
return this.chart._legendHoverPosition || Chart.Tooltip.positioners.average.call(this, items);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
6
11
|
new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
7
12
|
type: 'scatter',
|
|
8
13
|
data: {
|
|
@@ -20,10 +25,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
20
25
|
time: {
|
|
21
26
|
format: 'YYYY-MM-DD'
|
|
22
27
|
},
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
28
|
+
min: "<%= date_range.begin.to_s %>",
|
|
29
|
+
max: "<%= (date_range.end + 1).to_s %>",
|
|
30
|
+
<%= render_axis_title :x %>
|
|
27
31
|
grid: {
|
|
28
32
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
29
33
|
},
|
|
@@ -32,10 +36,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
32
36
|
scaleLabel: {
|
|
33
37
|
display: true,
|
|
34
38
|
},
|
|
35
|
-
|
|
36
|
-
display: true,
|
|
37
|
-
text: 'Count of items'
|
|
38
|
-
},
|
|
39
|
+
<%= render_axis_title :y %>
|
|
39
40
|
min: 0,
|
|
40
41
|
grid: {
|
|
41
42
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
@@ -44,15 +45,45 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
44
45
|
},
|
|
45
46
|
plugins: {
|
|
46
47
|
tooltip: {
|
|
48
|
+
position: 'legendItem',
|
|
47
49
|
callbacks: {
|
|
50
|
+
title: function(contexts) {
|
|
51
|
+
if (contexts[0]?.chart._legendHoverIndex != null) return '';
|
|
52
|
+
},
|
|
48
53
|
label: function(context) {
|
|
49
|
-
|
|
54
|
+
if (context.chart._legendHoverIndex != null) {
|
|
55
|
+
return context.dataset.label_hint || '';
|
|
56
|
+
}
|
|
57
|
+
return context.dataset.data[context.dataIndex].title;
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
},
|
|
53
61
|
annotation: {
|
|
54
62
|
annotations: {
|
|
55
63
|
<%= working_days_annotation %>
|
|
64
|
+
<%= date_annotation %>
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
legend: {
|
|
68
|
+
onHover: function(event, legendItem, legend) {
|
|
69
|
+
const chart = legend.chart;
|
|
70
|
+
const dataset = chart.data.datasets[legendItem.datasetIndex];
|
|
71
|
+
if (!dataset?.label_hint) return;
|
|
72
|
+
chart._legendHoverIndex = legendItem.datasetIndex;
|
|
73
|
+
chart._legendHoverPosition = { x: event.x, y: event.y };
|
|
74
|
+
const firstNonZero = dataset.data.findIndex(d => d?.y !== 0);
|
|
75
|
+
if (firstNonZero === -1) return;
|
|
76
|
+
chart.tooltip.setActiveElements(
|
|
77
|
+
[{ datasetIndex: legendItem.datasetIndex, index: firstNonZero }],
|
|
78
|
+
{ x: event.x, y: event.y }
|
|
79
|
+
);
|
|
80
|
+
chart.update();
|
|
81
|
+
},
|
|
82
|
+
onLeave: function(event, legendItem, legend) {
|
|
83
|
+
legend.chart._legendHoverIndex = null;
|
|
84
|
+
legend.chart._legendHoverPosition = null;
|
|
85
|
+
legend.chart.tooltip.setActiveElements([], { x: 0, y: 0 });
|
|
86
|
+
legend.chart.update();
|
|
56
87
|
}
|
|
57
88
|
}
|
|
58
89
|
}
|
|
@@ -1,58 +1,8 @@
|
|
|
1
|
+
<div>
|
|
1
2
|
<%= seam_start %>
|
|
2
3
|
<div class="chart">
|
|
3
4
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
4
5
|
</div>
|
|
5
|
-
<%
|
|
6
|
-
if show_stats
|
|
7
|
-
link_id = next_id
|
|
8
|
-
issues_id = next_id
|
|
9
|
-
%>
|
|
10
|
-
<div class='foldable' style="padding-left: 1em;">Statistics</div>
|
|
11
|
-
<div id="<%= issues_id %>" style="padding-left: 1em;">
|
|
12
|
-
<div>
|
|
13
|
-
<table class="standard">
|
|
14
|
-
<tr>
|
|
15
|
-
<th>Issue Type</th>
|
|
16
|
-
<th>Min</th>
|
|
17
|
-
<th>Max</th>
|
|
18
|
-
<th>Avg</th>
|
|
19
|
-
<th>Mode</th>
|
|
20
|
-
<% percentiles.each do |p| %>
|
|
21
|
-
<th><%= p %>th</th>
|
|
22
|
-
<% end %>
|
|
23
|
-
</tr>
|
|
24
|
-
<% the_stats.each do |k, v| %>
|
|
25
|
-
<tr>
|
|
26
|
-
<td><%= k %></td>
|
|
27
|
-
<td style="text-align: right;"><%= v[:min] %></td>
|
|
28
|
-
<td style="text-align: right;"><%= v[:max] %></td>
|
|
29
|
-
<td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
|
|
30
|
-
<td><%= v[:mode].join(', ') %></td>
|
|
31
|
-
<% percentiles.each do |p| %>
|
|
32
|
-
<td style="text-align: right;"><%= v[:percentiles][p] %></td>
|
|
33
|
-
<% end %>
|
|
34
|
-
</tr>
|
|
35
|
-
<% end %>
|
|
36
|
-
</table>
|
|
37
|
-
</div>
|
|
38
|
-
<div>
|
|
39
|
-
<p>These statistics help understand the <i>"shape"</i> of the cycletime histogram distribution, to help us with predictions.</p>
|
|
40
|
-
<ul>
|
|
41
|
-
<li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
|
|
42
|
-
<li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
|
|
43
|
-
<li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
|
|
44
|
-
<li><b>Percentiles:</b> they partition the data set. If X is the Nth percentile, it means that N% of cycletime values are X or less. Typical percentiles of interest are:</li>
|
|
45
|
-
<ul>
|
|
46
|
-
<li><b>50%</b>: also known as the <b>Median</b>. Useful to establish short feedback loops, to monitor that it's not drifting to the right.</li>
|
|
47
|
-
<li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
|
|
48
|
-
<li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
|
|
49
|
-
</ul>
|
|
50
|
-
</ul>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
<%
|
|
54
|
-
end
|
|
55
|
-
%>
|
|
56
6
|
<script>
|
|
57
7
|
new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
58
8
|
{
|
|
@@ -66,22 +16,17 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
66
16
|
x: {
|
|
67
17
|
type: 'linear',
|
|
68
18
|
stacked: true,
|
|
69
|
-
|
|
70
|
-
display: true,
|
|
71
|
-
text: 'Cycletime in days'
|
|
72
|
-
},
|
|
19
|
+
<%= render_axis_title :x %>
|
|
73
20
|
grid: {
|
|
74
21
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
75
22
|
},
|
|
76
23
|
min: 0,
|
|
24
|
+
<%= @max_x_value.nil? ? '' : "max: #{@max_x_value}," %>
|
|
77
25
|
offset: false, // Gets rid of the ugly padding on left.
|
|
78
26
|
},
|
|
79
27
|
y: {
|
|
80
28
|
stacked: true,
|
|
81
|
-
|
|
82
|
-
display: true,
|
|
83
|
-
text: 'Number of items that had that cycletime'
|
|
84
|
-
},
|
|
29
|
+
<%= render_axis_title :y %>
|
|
85
30
|
grid: {
|
|
86
31
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
87
32
|
},
|
|
@@ -121,3 +66,58 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
121
66
|
});
|
|
122
67
|
</script>
|
|
123
68
|
<%= seam_end %>
|
|
69
|
+
|
|
70
|
+
<%= seam_start 'stats_table' %>
|
|
71
|
+
<%
|
|
72
|
+
if show_stats
|
|
73
|
+
link_id = next_id
|
|
74
|
+
issues_id = next_id
|
|
75
|
+
%>
|
|
76
|
+
<div class='foldable' style="padding-left: 1em;">Statistics</div>
|
|
77
|
+
<div id="<%= issues_id %>" style="padding-left: 1em;">
|
|
78
|
+
<div>
|
|
79
|
+
<table class="standard">
|
|
80
|
+
<tr>
|
|
81
|
+
<th>Type</th>
|
|
82
|
+
<th>Min</th>
|
|
83
|
+
<th>Max</th>
|
|
84
|
+
<th>Avg</th>
|
|
85
|
+
<th>Mode</th>
|
|
86
|
+
<% percentiles.each do |p| %>
|
|
87
|
+
<th><%= p %>th</th>
|
|
88
|
+
<% end %>
|
|
89
|
+
</tr>
|
|
90
|
+
<% the_stats.each do |k, v| %>
|
|
91
|
+
<tr>
|
|
92
|
+
<td><%= k %></td>
|
|
93
|
+
<td style="text-align: right;"><%= v[:min] %></td>
|
|
94
|
+
<td style="text-align: right;"><%= v[:max] %></td>
|
|
95
|
+
<td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
|
|
96
|
+
<td><%= v[:mode].join(', ') %></td>
|
|
97
|
+
<% percentiles.each do |p| %>
|
|
98
|
+
<td style="text-align: right;"><%= v[:percentiles][p] %></td>
|
|
99
|
+
<% end %>
|
|
100
|
+
</tr>
|
|
101
|
+
<% end %>
|
|
102
|
+
</table>
|
|
103
|
+
</div>
|
|
104
|
+
<div>
|
|
105
|
+
<p>These statistics help understand the <i>"shape"</i> of the histogram distribution, to help us with predictions.</p>
|
|
106
|
+
<ul>
|
|
107
|
+
<li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
|
|
108
|
+
<li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
|
|
109
|
+
<li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
|
|
110
|
+
<li><b>Percentiles:</b> they partition the data set. If X is the Nth percentile, it means that N% of values are X or less. Typical percentiles of interest are:</li>
|
|
111
|
+
<ul>
|
|
112
|
+
<li><b>50%</b>: also known as the <b>Median</b>. Useful to establish short feedback loops, to monitor that it's not drifting to the right.</li>
|
|
113
|
+
<li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
|
|
114
|
+
<li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
|
|
115
|
+
</ul>
|
|
116
|
+
</ul>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<%
|
|
120
|
+
end
|
|
121
|
+
%>
|
|
122
|
+
<%= seam_end 'stats_table' %>
|
|
123
|
+
</div>
|
|
@@ -23,22 +23,25 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
23
23
|
grid: {
|
|
24
24
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
25
25
|
},
|
|
26
|
+
<%= render_axis_title :x %>
|
|
26
27
|
min: "<%= date_range.begin.to_s %>",
|
|
27
28
|
max: "<%= (date_range.end + 1).to_s %>"
|
|
28
29
|
},
|
|
29
30
|
y: {
|
|
31
|
+
min: 0,
|
|
32
|
+
max: <%= (@highest_y_value * 1.1).ceil %>,
|
|
30
33
|
scaleLabel: {
|
|
31
|
-
display: true
|
|
32
|
-
min: 0,
|
|
33
|
-
max: <%= @highest_y_value %>
|
|
34
|
-
},
|
|
35
|
-
title: {
|
|
36
|
-
display: true,
|
|
37
|
-
text: '<%= y_axis_heading %>'
|
|
34
|
+
display: true
|
|
38
35
|
},
|
|
36
|
+
<%= render_axis_title :y %>
|
|
39
37
|
grid: {
|
|
40
38
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
41
39
|
},
|
|
40
|
+
ticks: {
|
|
41
|
+
callback: function(value, index, ticks) {
|
|
42
|
+
return index === ticks.length - 1 ? null : value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
42
45
|
}
|
|
43
46
|
},
|
|
44
47
|
plugins: {
|
|
@@ -53,6 +56,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
53
56
|
annotation: {
|
|
54
57
|
annotations: {
|
|
55
58
|
<%= working_days_annotation %>
|
|
59
|
+
<%= date_annotation %>
|
|
56
60
|
|
|
57
61
|
<% @percentage_lines.each_with_index do |args, index| %>
|
|
58
62
|
<% percent, color = args %>
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
2
|
+
<div class="chart" style="position:relative;">
|
|
3
|
+
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
4
|
+
<div id="<%= chart_id %>-tooltip" style="
|
|
5
|
+
display:none; position:absolute; pointer-events:none;
|
|
6
|
+
background:rgba(0,0,0,0.75); color:#fff; border-radius:4px;
|
|
7
|
+
padding:4px 8px; font:12px sans-serif; white-space:nowrap;
|
|
8
|
+
"></div>
|
|
9
|
+
</div>
|
|
10
|
+
<script>
|
|
11
|
+
(function() {
|
|
12
|
+
var wipData = <%= @wip_data.to_json %>;
|
|
13
|
+
var wipLimits = <%= @wip_limits.to_json %>;
|
|
14
|
+
var recommendations = <%= @recommendations.to_json %>;
|
|
15
|
+
var maxWip = <%= @max_wip %>;
|
|
16
|
+
var gridColor = <%= CssVariable['--grid-line-color'].to_json %>;
|
|
17
|
+
var barFillColor = <%= CssVariable['--wip-by-column-chart-bar-fill-color'].to_json %>;
|
|
18
|
+
var barTextColor = <%= CssVariable['--wip-by-column-chart-bar-text-color'].to_json %>;
|
|
19
|
+
var limitColor = <%= CssVariable['--wip-by-column-chart-limit-line-color'].to_json %>;
|
|
20
|
+
var recColor = <%= CssVariable['--wip-by-column-chart-recommendation-color'].to_json %>;
|
|
21
|
+
var tooltipEl = document.getElementById(<%= "#{chart_id}-tooltip".inspect %>);
|
|
22
|
+
|
|
23
|
+
var hitAreas = [];
|
|
24
|
+
|
|
25
|
+
var rectPlugin = {
|
|
26
|
+
id: 'wipRects',
|
|
27
|
+
afterDraw: function(chart) {
|
|
28
|
+
var ctx = chart.ctx;
|
|
29
|
+
var xScale = chart.scales['x'];
|
|
30
|
+
var yScale = chart.scales['y'];
|
|
31
|
+
var slotWidth = xScale.width / Math.max(xScale.ticks.length, 1);
|
|
32
|
+
|
|
33
|
+
hitAreas = [];
|
|
34
|
+
|
|
35
|
+
// 1. Draw y-axis gridlines at integer band boundaries
|
|
36
|
+
ctx.save();
|
|
37
|
+
ctx.beginPath();
|
|
38
|
+
ctx.rect(xScale.left, yScale.top, xScale.right - xScale.left, yScale.bottom - yScale.top);
|
|
39
|
+
ctx.clip();
|
|
40
|
+
ctx.strokeStyle = gridColor;
|
|
41
|
+
ctx.lineWidth = 1;
|
|
42
|
+
ctx.setLineDash([]);
|
|
43
|
+
for (var gi = 0; gi <= maxWip + 1; gi++) {
|
|
44
|
+
var gy = yScale.getPixelForValue(gi);
|
|
45
|
+
ctx.beginPath();
|
|
46
|
+
ctx.moveTo(xScale.left, gy);
|
|
47
|
+
ctx.lineTo(xScale.right, gy);
|
|
48
|
+
ctx.stroke();
|
|
49
|
+
}
|
|
50
|
+
ctx.restore();
|
|
51
|
+
|
|
52
|
+
// 2. Draw WIP limit lines (behind rectangles)
|
|
53
|
+
wipLimits.forEach(function(limits, colIndex) {
|
|
54
|
+
var xCenter = xScale.getPixelForValue(colIndex);
|
|
55
|
+
var halfSlot = slotWidth * 0.45;
|
|
56
|
+
|
|
57
|
+
[['min', 'bottom'], ['max', 'top']].forEach(function(pair) {
|
|
58
|
+
var type = pair[0];
|
|
59
|
+
var baseline = pair[1];
|
|
60
|
+
var val = limits[type];
|
|
61
|
+
if (val === null || val === undefined) return;
|
|
62
|
+
|
|
63
|
+
var y = yScale.getPixelForValue(val + 0.5);
|
|
64
|
+
|
|
65
|
+
ctx.save();
|
|
66
|
+
ctx.strokeStyle = limitColor;
|
|
67
|
+
ctx.lineWidth = 2;
|
|
68
|
+
ctx.setLineDash([5, 3]);
|
|
69
|
+
ctx.beginPath();
|
|
70
|
+
ctx.moveTo(xCenter - halfSlot, y);
|
|
71
|
+
ctx.lineTo(xCenter + halfSlot, y);
|
|
72
|
+
ctx.stroke();
|
|
73
|
+
ctx.restore();
|
|
74
|
+
|
|
75
|
+
ctx.save();
|
|
76
|
+
ctx.fillStyle = limitColor;
|
|
77
|
+
ctx.font = 'bold 10px sans-serif';
|
|
78
|
+
ctx.textAlign = 'right';
|
|
79
|
+
ctx.textBaseline = baseline;
|
|
80
|
+
ctx.fillText(type + ': ' + val, xCenter + halfSlot, baseline === 'bottom' ? y - 2 : y + 2);
|
|
81
|
+
ctx.restore();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
<% if @show_recommendations %>
|
|
86
|
+
// 3. Draw recommendation lines (behind rectangles, label on left)
|
|
87
|
+
recommendations.forEach(function(rec, colIndex) {
|
|
88
|
+
if (rec === null || rec === undefined || rec === 0) return;
|
|
89
|
+
var xCenter = xScale.getPixelForValue(colIndex);
|
|
90
|
+
var halfSlot = slotWidth * 0.45;
|
|
91
|
+
var y = yScale.getPixelForValue(rec + 0.5);
|
|
92
|
+
|
|
93
|
+
ctx.save();
|
|
94
|
+
ctx.strokeStyle = recColor;
|
|
95
|
+
ctx.lineWidth = 2;
|
|
96
|
+
ctx.setLineDash([5, 3]);
|
|
97
|
+
ctx.beginPath();
|
|
98
|
+
ctx.moveTo(xCenter - halfSlot, y);
|
|
99
|
+
ctx.lineTo(xCenter + halfSlot, y);
|
|
100
|
+
ctx.stroke();
|
|
101
|
+
ctx.restore();
|
|
102
|
+
|
|
103
|
+
ctx.save();
|
|
104
|
+
ctx.fillStyle = recColor;
|
|
105
|
+
ctx.font = 'bold 10px sans-serif';
|
|
106
|
+
ctx.textAlign = 'left';
|
|
107
|
+
ctx.textBaseline = 'top';
|
|
108
|
+
ctx.fillText('rec: ' + rec, xCenter - halfSlot, y + 2);
|
|
109
|
+
ctx.restore();
|
|
110
|
+
});
|
|
111
|
+
<% end %>
|
|
112
|
+
|
|
113
|
+
// 4. Draw WIP rectangles centered in their bands (wip + 0.5)
|
|
114
|
+
var yStep = Math.abs(yScale.getPixelForValue(0.5) - yScale.getPixelForValue(1.5));
|
|
115
|
+
|
|
116
|
+
wipData.forEach(function(colData, colIndex) {
|
|
117
|
+
var xCenter = xScale.getPixelForValue(colIndex);
|
|
118
|
+
|
|
119
|
+
colData.forEach(function(entry) {
|
|
120
|
+
var wip = entry['wip'];
|
|
121
|
+
var pct = entry['pct'];
|
|
122
|
+
var rectWidth = slotWidth * pct / 100;
|
|
123
|
+
var rectHeight = yStep * 0.8;
|
|
124
|
+
var yCenter = yScale.getPixelForValue(wip + 0.5);
|
|
125
|
+
var x1 = xCenter - rectWidth / 2;
|
|
126
|
+
var y1 = yCenter - rectHeight / 2;
|
|
127
|
+
|
|
128
|
+
ctx.save();
|
|
129
|
+
ctx.fillStyle = barFillColor;
|
|
130
|
+
ctx.strokeStyle = barFillColor;
|
|
131
|
+
ctx.lineWidth = 1;
|
|
132
|
+
ctx.fillRect(x1, y1, rectWidth, rectHeight);
|
|
133
|
+
ctx.strokeRect(x1, y1, rectWidth, rectHeight);
|
|
134
|
+
|
|
135
|
+
ctx.fillStyle = barTextColor;
|
|
136
|
+
ctx.font = '11px sans-serif';
|
|
137
|
+
ctx.textAlign = 'center';
|
|
138
|
+
ctx.textBaseline = 'middle';
|
|
139
|
+
if (rectWidth > 25) {
|
|
140
|
+
ctx.fillText(pct + '%', xCenter, yCenter);
|
|
141
|
+
}
|
|
142
|
+
ctx.restore();
|
|
143
|
+
|
|
144
|
+
var hitWidth = Math.max(rectWidth, slotWidth);
|
|
145
|
+
hitAreas.push({
|
|
146
|
+
x1: xCenter - hitWidth / 2, y1: y1,
|
|
147
|
+
x2: xCenter + hitWidth / 2, y2: y1 + rectHeight,
|
|
148
|
+
label: 'WIP ' + wip + ': ' + pct + '%'
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
var canvas = document.getElementById(<%= chart_id.inspect %>);
|
|
156
|
+
|
|
157
|
+
canvas.addEventListener('mousemove', function(e) {
|
|
158
|
+
var rect = canvas.getBoundingClientRect();
|
|
159
|
+
var mx = e.clientX - rect.left;
|
|
160
|
+
var my = e.clientY - rect.top;
|
|
161
|
+
|
|
162
|
+
var hit = null;
|
|
163
|
+
for (var i = 0; i < hitAreas.length; i++) {
|
|
164
|
+
var a = hitAreas[i];
|
|
165
|
+
if (mx >= a.x1 && mx <= a.x2 && my >= a.y1 && my <= a.y2) {
|
|
166
|
+
hit = a;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (hit) {
|
|
172
|
+
tooltipEl.textContent = hit.label;
|
|
173
|
+
tooltipEl.style.display = 'block';
|
|
174
|
+
tooltipEl.style.left = (mx + 10) + 'px';
|
|
175
|
+
tooltipEl.style.top = (my - 20) + 'px';
|
|
176
|
+
} else {
|
|
177
|
+
tooltipEl.style.display = 'none';
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
canvas.addEventListener('mouseleave', function() {
|
|
182
|
+
tooltipEl.style.display = 'none';
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
new Chart(canvas.getContext('2d'),
|
|
186
|
+
{
|
|
187
|
+
type: 'bar',
|
|
188
|
+
plugins: [rectPlugin],
|
|
189
|
+
data: {
|
|
190
|
+
labels: <%= @column_names.to_json %>,
|
|
191
|
+
datasets: [{
|
|
192
|
+
data: [],
|
|
193
|
+
backgroundColor: 'transparent'
|
|
194
|
+
}]
|
|
195
|
+
},
|
|
196
|
+
options: {
|
|
197
|
+
responsive: <%= canvas_responsive? %>,
|
|
198
|
+
scales: {
|
|
199
|
+
x: {
|
|
200
|
+
grid: {
|
|
201
|
+
color: gridColor,
|
|
202
|
+
z: 1
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
y: {
|
|
206
|
+
title: {
|
|
207
|
+
display: true,
|
|
208
|
+
text: 'WIP'
|
|
209
|
+
},
|
|
210
|
+
grid: {
|
|
211
|
+
display: false
|
|
212
|
+
},
|
|
213
|
+
min: 0,
|
|
214
|
+
max: <%= @max_wip + 1 %>,
|
|
215
|
+
afterBuildTicks: function(scale) {
|
|
216
|
+
scale.ticks = [];
|
|
217
|
+
for (var i = 0; i <= maxWip; i++) {
|
|
218
|
+
scale.ticks.push({ value: i + 0.5 });
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
ticks: {
|
|
222
|
+
callback: function(value) {
|
|
223
|
+
return Math.round(value - 0.5);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
plugins: {
|
|
229
|
+
legend: {
|
|
230
|
+
display: false
|
|
231
|
+
},
|
|
232
|
+
tooltip: {
|
|
233
|
+
enabled: false
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
})();
|
|
239
|
+
</script>
|
|
240
|
+
<%= seam_end %>
|
|
241
|
+
<% unless @recommendation_texts.empty? %>
|
|
242
|
+
<div style="margin-top: 0.5em;">
|
|
243
|
+
<strong>WIP limit recommendations</strong>
|
|
244
|
+
<ul style="margin: 0.3em 0 0 0; padding-left: 1.5em;">
|
|
245
|
+
<% @recommendation_texts.each do |text| %>
|
|
246
|
+
<li><%= text %></li>
|
|
247
|
+
<% end %>
|
|
248
|
+
</ul>
|
|
249
|
+
</div>
|
|
250
|
+
<% end %>
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
class HtmlGenerator
|
|
4
4
|
attr_accessor :file_system, :settings
|
|
5
5
|
|
|
6
|
-
def create_html output_filename:, settings:
|
|
6
|
+
def create_html output_filename:, settings:, project_name: ''
|
|
7
7
|
@settings = settings
|
|
8
|
+
project_name = project_name.to_s
|
|
8
9
|
html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
|
|
9
10
|
css = load_css html_directory: html_directory
|
|
10
11
|
javascript = file_system.load(File.join(html_directory, 'index.js'))
|