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?, #chart_format, #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, #format_integer, #format_status, #header_text, #holidays, #html_directory, #icon_span, #link_to_issue, #next_id, #next_palette_color, #non_working_day?, #normalize_annotation_datetime, #not_visible_icon, #not_visible_text, #ordinal, #percentile_of, #render, #render_axis_title, #render_top_text, #resolve_status, #stagger_label_positions, #status_category_color, #to_human_readable, #working_days_annotation, #wrap_and_render
Constructor Details
#initialize ⇒ TimeBasedScatterplot
Returns a new instance of TimeBasedScatterplot.
22 23 24 25 26 27 28 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 22 def initialize super @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
35 36 37 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 35 def cap_y_axis percentile: 98 @y_axis_cap_percentile = percentile end |
#compute_cap(items) ⇒ Object
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 217 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
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 97 98 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 67 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
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 181 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.
142 143 144 145 146 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 142 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
177 178 179 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 177 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.
101 102 103 104 105 106 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 101 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.
206 207 208 209 210 211 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 206 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
213 214 215 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 213 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.
42 43 44 45 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 42 def percentiles list = nil @percentiles = validate_percentiles(list) unless list.nil? @percentiles end |
#run ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 47 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 if data_sets.empty? return "<h1 class='foldable'>#{@header_text}</h1>" \ '<div>No data matched the selected criteria. Nothing to show.</div>' end wrap_and_render(binding, __FILE__) end |
#show_trend_lines ⇒ Object
108 109 110 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 108 def show_trend_lines @show_trend_lines = true end |
#trend_line_data_set(label:, data:, color:) ⇒ Object
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 174 175 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 148 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.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 116 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.
31 32 33 |
# File 'lib/jirametrics/time_based_scatterplot.rb', line 31 def value_axis_title= title @y_axis_title = title end |