Class: TimeBasedScatterplot
- Inherits:
-
TimeBasedChart
- Object
- ChartBase
- TimeBasedChart
- TimeBasedScatterplot
- Includes:
- GroupableIssueChart, PercentileValidation
- Defined in:
- lib/jirametrics/time_based_scatterplot.rb
Direct Known Subclasses
Constant Summary collapse
- OVERALL_LABEL =
What the whole-data-set percentile lines call themselves when you hover them. They have no legend entry, so without this there is nothing identifying them at all. "items" rather than "data" or "everything" because it matches the description prose and because anything a grouping rule ignored has already been dropped by the time these lines are calculated.
'All items'
Constants inherited from TimeBasedChart
TimeBasedChart::VALUE_AXIS_LABELS
Constants inherited from ChartBase
ChartBase::LABEL_POSITIONS, ChartBase::OKABE_ITO_PALETTE
Instance Attribute Summary collapse
-
#percentage_lines ⇒ Object
readonly
percentage_lines is internal, not part of the documented config DSL.
-
#y_axis_cap_percentile ⇒ Object
readonly
percentage_lines is internal, not part of the documented config DSL.
Attributes included from GroupableIssueChart
#group_by_block, #issue_hints, #issue_periods
Attributes inherited from ChartBase
#aggregated_project, #all_boards, #atlassian_document_format, #board_id, #canvas_height, #canvas_width, #color_palette, #data_quality, #date_range, #file_system, #fix_versions, #holiday_dates, #issues, #settings, #time_range, #timezone_offset, #x_axis_title, #y_axis_title
Instance Method Summary collapse
- #cap_y_axis(percentile: 98) ⇒ Object
- #compute_cap(items) ⇒ Object
- #create_datasets(items) ⇒ Object
- #data_for_item(item, rules: nil) ⇒ Object
-
#initialize ⇒ TimeBasedScatterplot
constructor
A new instance of TimeBasedScatterplot.
-
#legend_annotation_map ⇒ Object
Dataset index to the annotation ids belonging to that dataset's group, so the legend handler can toggle all of a group's lines.
- #minimum_y_value ⇒ Object
-
#percentile_label(label, lines) ⇒ Object
"Story (85% at 81 days)" for one, comma separated for several, bare label for none.
-
#percentile_lines_for(items, percentiles) ⇒ Object
Returns [[percentile, value], ...] for the requested percentiles, sorted ascending by percentile and dropping any that have no value because the item list is empty after filtering.
- #percentile_value(items, percentile) ⇒ Object
-
#percentiles(list = nil) ⇒ Object
Percentile reference lines.
- #run ⇒ Object
- #show_trend_lines ⇒ Object
- #trend_line_data_set(label:, data:, color:) ⇒ Object
-
#trend_line_description ⇒ Object
The lines are always built but drawn hidden unless asked for, so this stays empty until they are actually switched on.
-
#value_axis_title=(title) ⇒ Object
On a scatterplot the cycle time is plotted up the y-axis.
Methods included from PercentileValidation
validate_percentile, validate_percentiles
Methods included from GroupableIssueChart
#accumulate_issue_for_group, #group_issues, #grouping_rules, #init_configuration_block, #reconcile_percentiles
Methods inherited from TimeBasedChart
#cycletime_unit, #duration_in_unit, #label_cycletime
Methods inherited from ChartBase
#aggregated_project?, #before_run, #call_before_run, #canvas, #canvas_responsive?, #collapsible_issues_panel, #color_block, #color_for, #comma_and, #completed_issues_in_range, #current_board, #cycletime, #cycletime_for_issue, #daily_chart_dataset, #date_annotation, #describe_non_working_days, #description_text, #expand_template, #expanded_header_text, #format_integer, #format_status, #header_text, #holidays, #html_directory, #icon_span, #link_to_issue, #next_id, #next_palette_color, #no_data_text, #non_working_day?, #normalize_annotation_datetime, #not_visible_icon, #not_visible_text, #ordinal, #percentile_of, #render, #render_axis_title, #render_binding, #render_header, #render_no_data, #render_top_text, #resolve_status, #stagger_label_positions, #status_category_color, #to_human_readable, #working_days_annotation, #wrap_and_render
Methods included from ChartFormat
Constructor Details
#initialize ⇒ TimeBasedScatterplot
Returns a new instance of TimeBasedScatterplot.
22 23 24 25 26 27 28 29 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 22 def initialize super no_data_text '<%= render_header %><div>No data matched the selected criteria. Nothing to show.</div>' @percentage_lines = [] @highest_y_value = 0 @percentiles = [85] end |
Instance Attribute Details
#percentage_lines ⇒ Object (readonly)
percentage_lines is internal, not part of the documented config DSL. It exists so that specs and the ERB can see the computed lines without reaching into instance variables. Its shape, including the :id strings that encode positional group indices, is free to change.
20 21 22 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 20 def percentage_lines @percentage_lines end |
#y_axis_cap_percentile ⇒ Object (readonly)
percentage_lines is internal, not part of the documented config DSL. It exists so that specs and the ERB can see the computed lines without reaching into instance variables. Its shape, including the :id strings that encode positional group indices, is free to change.
20 21 22 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 20 def y_axis_cap_percentile @y_axis_cap_percentile end |
Instance Method Details
#cap_y_axis(percentile: 98) ⇒ Object
36 37 38 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 36 def cap_y_axis percentile: 98 @y_axis_cap_percentile = percentile end |
#compute_cap(items) ⇒ Object
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 215 def compute_cap items return nil unless @y_axis_cap_percentile cutoff = percentile_value items, @y_axis_cap_percentile return nil unless cutoff values = filtered_values(items) outlier_count = values.count { |value| value > cutoff } return nil if outlier_count.zero? pad = cutoff * 0.06 # breathing room so the top real dot does not touch the break gutter_height = cutoff * 0.15 sep = cutoff + pad { cutoff: cutoff, sep: sep, pin_row: sep + (gutter_height * 0.55), axis_max: (sep + gutter_height).ceil, outlier_count: outlier_count, label: cap_label(outlier_count: outlier_count, cutoff: cutoff) } end |
#create_datasets(items) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 65 def create_datasets items @cap = compute_cap items data_sets = [] group_issues(items).each_with_index do |(rules, items_by_type), group_index| label = rules.label color = rules.color lines = percentile_lines_for items_by_type, (rules.percentiles || @percentiles) data = items_by_type.filter_map { |item| data_for_item(item, rules: rules) } # Where this group's scatter set is about to land. The legend handler knows the clicked # dataset by index, so that's what the annotation map is keyed by. dataset_index = data_sets.size data_sets << { label: percentile_label(label, lines), data: data, fill: false, showLine: false, backgroundColor: color } data_sets << trend_line_data_set(label: label, data: data, color: color) lines.each do |percentile, value| @percentage_lines << { percentile: percentile, value: value, color: color, id: "group#{group_index}_#{percentile}", dataset_index: dataset_index, label: label } end end data_sets end |
#data_for_item(item, rules: nil) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 179 def data_for_item item, rules: nil y = y_value(item) min = minimum_y_value return nil if min && y < min over = @cap && y > @cap[:cutoff] plotted_y = over ? @cap[:pin_row] : y @highest_y_value = plotted_y if @highest_y_value < plotted_y point = { y: plotted_y, x: chart_format(x_value(item)), title: [title_value(item, rules: rules)] } if over point[:over] = true point[:true_y] = y end point end |
#legend_annotation_map ⇒ Object
Dataset index to the annotation ids belonging to that dataset's group, so the legend handler can toggle all of a group's lines. Keyed by index rather than by label because two groups may legitimately share a label while differing in colour, and keying by label would then toggle both of them at once. Overall lines are deliberately absent; they are not owned by any group and stay visible when a group is switched off.
140 141 142 143 144 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 140 def legend_annotation_map @percentage_lines.reject { |line| line[:dataset_index].nil? } .group_by { |line| line[:dataset_index] } .transform_values { |lines| lines.collect { |line| line[:id] } } end |
#minimum_y_value ⇒ Object
175 176 177 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 175 def minimum_y_value nil end |
#percentile_label(label, lines) ⇒ Object
"Story (85% at 81 days)" for one, comma separated for several, bare label for none.
99 100 101 102 103 104 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 99 def percentile_label label, lines return label if lines.empty? parts = lines.collect { |percentile, value| "#{percentile}% at #{label_days value}" } "#{label} (#{parts.join ', '})" end |
#percentile_lines_for(items, percentiles) ⇒ Object
Returns [[percentile, value], ...] for the requested percentiles, sorted ascending by percentile and dropping any that have no value because the item list is empty after filtering. Sorting happens here, not in the caller, because GroupingRules#percentiles is user-assigned with no ordering guarantee.
204 205 206 207 208 209 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 204 def percentile_lines_for items, percentiles percentiles.sort.filter_map do |percentile| value = percentile_value items, percentile [percentile, value] unless value.nil? end end |
#percentile_value(items, percentile) ⇒ Object
211 212 213 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 211 def percentile_value items, percentile percentile_of filtered_values(items), percentile end |
#percentiles(list = nil) ⇒ Object
Percentile reference lines. The chart level value defines the lines drawn across the whole data set AND the default for each group; a group can override with rule.percentiles. An empty list switches the lines off.
43 44 45 46 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 43 def percentiles list = nil @percentiles = validate_percentiles(list) unless list.nil? @percentiles end |
#run ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 48 def run items = all_items data_sets = create_datasets items overall_color = CssVariable['--cycletime-scatterplot-overall-trendline-color'] percentile_lines_for(items, @percentiles).each do |percentile, value| @percentage_lines << { percentile: percentile, value: value, color: overall_color, id: "overall_#{percentile}", dataset_index: nil, label: OVERALL_LABEL } end return render_no_data if data_sets.empty? wrap_and_render(binding, __FILE__) end |
#show_trend_lines ⇒ Object
106 107 108 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 106 def show_trend_lines @show_trend_lines = true end |
#trend_line_data_set(label:, data:, color:) ⇒ Object
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 146 def trend_line_data_set label:, data:, color: points = data.collect do |hash| [Time.parse(hash[:x]).to_i, hash[:true_y] || hash[:y]] end # The trend calculation works with numbers only so convert Time to an int and back calculator = TrendLineCalculator.new(points) data_points = calculator.chart_datapoints( range: time_range.begin.to_i..time_range.end.to_i, max_y: (@cap ? @cap[:cutoff] : @highest_y_value) ) data_points.each do |point_hash| point_hash[:x] = chart_format Time.at(point_hash[:x]) end { type: 'line', label: "#{label} Trendline", data: data_points, fill: false, borderWidth: 1, markerType: 'none', borderColor: color, borderDash: [6, 3], pointStyle: 'dash', hidden: !@show_trend_lines } end |
#trend_line_description ⇒ Object
The lines are always built but drawn hidden unless asked for, so this stays empty until they are actually switched on. Note the caller must be the ERB tag <%= trend_line_description %>: description_text is built during initialize, before the config block has called show_trend_lines, so interpolation would freeze "off" in permanently.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 114 def trend_line_description return '' unless @show_trend_lines <<-HTML <div class="p"> The dashed lines are trend lines, one per group in that group's colour. Each is a straight line fitted through that group's dots, so the slope tells you whether cycle times have been getting longer or shorter across the period shown. A line sloping up means work of that kind has been taking progressively longer to finish. </div> <div class="p"> Read the slope as a description of this window rather than a prediction. A line is drawn whenever a group has at least three dots and nothing checks how well it actually fits them, so a scattered cloud with no real trend in it still gets a confident looking line. It is a straight line, so it cannot show a trend that changed direction partway through, and a handful of unusually long items will tilt it noticeably. If the slope surprises you, look at the dots before you believe it. </div> HTML end |
#value_axis_title=(title) ⇒ Object
On a scatterplot the cycle time is plotted up the y-axis.
32 33 34 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 32 def value_axis_title= title @y_axis_title = title end |