Class: ChartBase
- Inherits:
-
Object
- Object
- ChartBase
- Defined in:
- lib/jirametrics/chart_base.rb
Direct Known Subclasses
AgingWorkBarChart, AgingWorkInProgressChart, AgingWorkTable, Anonymizer, CumulativeFlowDiagram, DailyView, DailyWipChart, DataQualityReport, DependencyChart, EstimateAccuracyChart, ExpeditedChart, FlowEfficiencyScatterplot, HierarchyTable, SprintBurndown, ThroughputChart, TimeBasedChart, WipByColumnChart
Constant Summary collapse
- OKABE_ITO_PALETTE =
Okabe-Ito palette - perceptually distinct under the most common forms of colour blindness. Ordered from most- to least-commonly useful for chart series.
%w[ #0072B2 #E69F00 #009E73 #56B4E9 #D55E00 #CC79A7 #F0E442 ].freeze
- LABEL_POSITIONS =
%w[5% 25% 45% 65%].freeze
- @@chart_counter =
0
Instance Attribute Summary collapse
-
#aggregated_project ⇒ Object
writeonly
Sets the attribute aggregated_project.
-
#all_boards ⇒ Object
Returns the value of attribute all_boards.
-
#atlassian_document_format ⇒ Object
Returns the value of attribute atlassian_document_format.
-
#board_id ⇒ Object
Returns the value of attribute board_id.
-
#canvas_height ⇒ Object
readonly
Returns the value of attribute canvas_height.
-
#canvas_width ⇒ Object
readonly
Returns the value of attribute canvas_width.
-
#data_quality ⇒ Object
Returns the value of attribute data_quality.
-
#date_range ⇒ Object
Returns the value of attribute date_range.
-
#file_system ⇒ Object
Returns the value of attribute file_system.
-
#fix_versions ⇒ Object
Returns the value of attribute fix_versions.
-
#holiday_dates ⇒ Object
Returns the value of attribute holiday_dates.
-
#issues ⇒ Object
Returns the value of attribute issues.
-
#settings ⇒ Object
Returns the value of attribute settings.
-
#time_range ⇒ Object
Returns the value of attribute time_range.
-
#timezone_offset ⇒ Object
Returns the value of attribute timezone_offset.
-
#x_axis_title ⇒ Object
Returns the value of attribute x_axis_title.
-
#y_axis_title ⇒ Object
Returns the value of attribute y_axis_title.
Instance Method Summary collapse
- #aggregated_project? ⇒ Boolean
- #before_run ⇒ Object
- #call_before_run(&proc) ⇒ Object
- #canvas(width:, height:, responsive: true) ⇒ Object
- #canvas_responsive? ⇒ Boolean
- #chart_format(object) ⇒ Object
- #collapsible_issues_panel(issue_descriptions, *args) ⇒ Object
- #color_block(color, title: nil) ⇒ Object
- #color_for(type:) ⇒ Object
- #completed_issues_in_range(include_unstarted: false) ⇒ Object
-
#current_board ⇒ Object
Return only the board columns for the current board.
-
#cycletime(&block) ⇒ Object
Set a cycletime for just this one chart, overriding the one for the report.
-
#cycletime_for_issue(issue) ⇒ Object
Returns the cycletime in use right now, which may be specific to the chart or across the report.
- #daily_chart_dataset(date_issues_list:, color:, label:, positive: true) ⇒ Object
- #date_annotation ⇒ Object
- #describe_non_working_days ⇒ Object
- #description_text(text = :none) ⇒ Object
-
#format_integer(number) ⇒ Object
Convert a number like 1234567 into the string "1,234,567".
-
#format_status(object, board:, is_category: false, use_old_status: false) ⇒ Object
object will be either a Status or a ChangeItem if it's a ChangeItem then use_old_status will specify whether we're using the new or old Either way, is_category will format the category rather than the status.
- #header_text(text = :none) ⇒ Object
-
#holidays(date_range: @date_range) ⇒ Object
Returns the non-working stretches (weekends and holidays) within the range, each as a Date range of consecutive days.
- #html_directory ⇒ Object
- #icon_span(title:, icon:) ⇒ Object
-
#initialize ⇒ ChartBase
constructor
A new instance of ChartBase.
- #link_to_issue(issue, args = {}) ⇒ Object
- #next_id ⇒ Object
- #non_working_day?(date) ⇒ Boolean
- #normalize_annotation_datetime(value) ⇒ Object
-
#not_visible_icon(status, board) ⇒ Object
The 👀 marker for a status that isn't mapped to any column, or '' when it is visible.
- #not_visible_text(issue) ⇒ Object
- #random_color ⇒ Object
- #render(caller_binding, file) ⇒ Object
- #render_axis_title(axis_direction) ⇒ Object
- #render_top_text(caller_binding) ⇒ Object
-
#resolve_status(object, board:, use_old_status:) ⇒ Object
Resolves the object being formatted into [status, error_message].
- #stagger_label_positions(datetimes) ⇒ Object
- #status_category_color(status) ⇒ Object
- #to_human_readable(number) ⇒ Object
- #working_days_annotation ⇒ Object
-
#wrap_and_render(caller_binding, file) ⇒ Object
Render the file and then wrap it with standard headers and quality checks.
Constructor Details
#initialize ⇒ ChartBase
Returns a new instance of ChartBase.
23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/jirametrics/chart_base.rb', line 23 def initialize @chart_colors = { 'Story' => CssVariable['--type-story-color'], 'Task' => CssVariable['--type-task-color'], 'Bug' => CssVariable['--type-bug-color'], 'Defect' => CssVariable['--type-bug-color'], 'Spike' => CssVariable['--type-spike-color'] } @canvas_width = 800 @canvas_height = 200 @canvas_responsive = true end |
Instance Attribute Details
#aggregated_project=(value) ⇒ Object (writeonly)
Sets the attribute aggregated_project
18 19 20 |
# File 'lib/jirametrics/chart_base.rb', line 18 def aggregated_project=(value) @aggregated_project = value end |
#all_boards ⇒ Object
Returns the value of attribute all_boards.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def all_boards @all_boards end |
#atlassian_document_format ⇒ Object
Returns the value of attribute atlassian_document_format.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def atlassian_document_format @atlassian_document_format end |
#board_id ⇒ Object
Returns the value of attribute board_id.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def board_id @board_id end |
#canvas_height ⇒ Object (readonly)
Returns the value of attribute canvas_height.
19 20 21 |
# File 'lib/jirametrics/chart_base.rb', line 19 def canvas_height @canvas_height end |
#canvas_width ⇒ Object (readonly)
Returns the value of attribute canvas_width.
19 20 21 |
# File 'lib/jirametrics/chart_base.rb', line 19 def canvas_width @canvas_width end |
#data_quality ⇒ Object
Returns the value of attribute data_quality.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def data_quality @data_quality end |
#date_range ⇒ Object
Returns the value of attribute date_range.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def date_range @date_range end |
#file_system ⇒ Object
Returns the value of attribute file_system.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def file_system @file_system end |
#fix_versions ⇒ Object
Returns the value of attribute fix_versions.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def fix_versions @fix_versions end |
#holiday_dates ⇒ Object
Returns the value of attribute holiday_dates.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def holiday_dates @holiday_dates end |
#issues ⇒ Object
Returns the value of attribute issues.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def issues @issues end |
#settings ⇒ Object
Returns the value of attribute settings.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def settings @settings end |
#time_range ⇒ Object
Returns the value of attribute time_range.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def time_range @time_range end |
#timezone_offset ⇒ Object
Returns the value of attribute timezone_offset.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def timezone_offset @timezone_offset end |
#x_axis_title ⇒ Object
Returns the value of attribute x_axis_title.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def x_axis_title @x_axis_title end |
#y_axis_title ⇒ Object
Returns the value of attribute y_axis_title.
15 16 17 |
# File 'lib/jirametrics/chart_base.rb', line 15 def y_axis_title @y_axis_title end |
Instance Method Details
#aggregated_project? ⇒ Boolean
44 45 46 |
# File 'lib/jirametrics/chart_base.rb', line 44 def aggregated_project? @aggregated_project end |
#before_run ⇒ Object
40 41 42 |
# File 'lib/jirametrics/chart_base.rb', line 40 def before_run @call_before_run_procs&.each(&:call) end |
#call_before_run(&proc) ⇒ Object
36 37 38 |
# File 'lib/jirametrics/chart_base.rb', line 36 def call_before_run &proc (@call_before_run_procs ||= []) << proc end |
#canvas(width:, height:, responsive: true) ⇒ Object
332 333 334 335 336 |
# File 'lib/jirametrics/chart_base.rb', line 332 def canvas width:, height:, responsive: true @canvas_width = width @canvas_height = height @canvas_responsive = responsive end |
#canvas_responsive? ⇒ Boolean
338 339 340 |
# File 'lib/jirametrics/chart_base.rb', line 338 def canvas_responsive? @canvas_responsive end |
#chart_format(object) ⇒ Object
243 244 245 246 247 248 249 250 |
# File 'lib/jirametrics/chart_base.rb', line 243 def chart_format object if object.is_a? Time # "2022-04-09T11:38:30-07:00" object.strftime '%Y-%m-%dT%H:%M:%S%z' else object.to_s end end |
#collapsible_issues_panel(issue_descriptions, *args) ⇒ Object
129 130 131 132 133 134 135 136 |
# File 'lib/jirametrics/chart_base.rb', line 129 def collapsible_issues_panel issue_descriptions, *args link_id = next_id issues_id = next_id issue_descriptions.sort! { |a, b| a[0].key_as_i <=> b[0].key_as_i } erb = ERB.new file_system.load File.join(html_directory, 'collapsible_issues_panel.erb') erb.result(binding) end |
#color_block(color, title: nil) ⇒ Object
342 343 344 345 346 347 348 349 350 351 |
# File 'lib/jirametrics/chart_base.rb', line 342 def color_block color, title: nil result = +'' result << "<div class='color_block' style='" result << "background: #{CssVariable[color]};" if color result << 'visibility: hidden;' unless color result << "'" result << " title=#{title.inspect}" if title result << '></div>' result end |
#color_for(type:) ⇒ Object
85 86 87 |
# File 'lib/jirametrics/chart_base.rb', line 85 def color_for type: @chart_colors[type] ||= random_color end |
#completed_issues_in_range(include_unstarted: false) ⇒ Object
232 233 234 235 236 237 238 239 240 241 |
# File 'lib/jirametrics/chart_base.rb', line 232 def completed_issues_in_range include_unstarted: false issues.select do |issue| cycletime = issue.board.cycletime started_time, stopped_time = cycletime.started_stopped_times(issue) stopped_time && date_range.include?(stopped_time.to_date) && # Remove outside range (include_unstarted || (started_time && (stopped_time >= started_time))) end end |
#current_board ⇒ Object
Return only the board columns for the current board.
217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/jirametrics/chart_base.rb', line 217 def current_board if @board_id.nil? case @all_boards.size when 0 raise 'Couldn\'t find any board configurations. Ensure one is set' when 1 return @all_boards.values[0] else raise "Must set board_id so we know which to use. Multiple boards found: #{@all_boards.keys.inspect}" end end @all_boards[@board_id] end |
#cycletime(&block) ⇒ Object
Set a cycletime for just this one chart, overriding the one for the report.
363 364 365 366 367 368 369 370 |
# File 'lib/jirametrics/chart_base.rb', line 363 def cycletime &block call_before_run do @cycletime = CycleTimeConfig.new( possible_statuses: possible_statuses, label: nil, block: block, file_system: file_system, settings: settings ) end end |
#cycletime_for_issue(issue) ⇒ Object
Returns the cycletime in use right now, which may be specific to the chart or across the report.
373 374 375 |
# File 'lib/jirametrics/chart_base.rb', line 373 def cycletime_for_issue issue @cycletime || issue.board.cycletime end |
#daily_chart_dataset(date_issues_list:, color:, label:, positive: true) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/jirametrics/chart_base.rb', line 103 def daily_chart_dataset date_issues_list:, color:, label:, positive: true { type: 'bar', label: label, data: date_issues_list.collect do |date, issues| issues.sort_by!(&:key_as_i) title = "#{label} (#{label_issues issues.size})" { x: date, y: positive ? issues.size : -issues.size, title: [title] + issues.collect { |i| "#{i.key} : #{i.summary.strip}#{" #{yield date, i}" if block_given?}" } } end, backgroundColor: color, borderRadius: positive ? 0 : 5 } end |
#date_annotation ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/jirametrics/chart_base.rb', line 168 def date_annotation annotations = settings['date_annotations'] || [] in_range = annotations .map { |a| [a, normalize_annotation_datetime(a['date'])] } .select { |(_, dt)| date_range.cover?(Date.parse(dt)) } .sort_by { |(_, dt)| dt } positions = stagger_label_positions(in_range.map { |(_, dt)| dt }) in_range.each_with_index.collect do |(a, normalized), index| <<~TEXT dateAnnotation#{index}: { type: 'line', xMin: #{normalized.to_json}, xMax: #{normalized.to_json}, borderColor: 'rgba(0,0,0,0.7)', borderWidth: 1, label: { display: true, content: #{a['label'].to_json}, position: #{positions[index].to_json} } }, TEXT end.join end |
#describe_non_working_days ⇒ Object
353 354 355 356 357 358 359 360 |
# File 'lib/jirametrics/chart_base.rb', line 353 def describe_non_working_days <<-TEXT <div class='p'> The #{color_block '--non-working-days-color'} vertical bars indicate non-working days; weekends and any other holidays mentioned in the configuration. </div> TEXT end |
#description_text(text = :none) ⇒ Object
257 258 259 260 |
# File 'lib/jirametrics/chart_base.rb', line 257 def description_text text = :none @description_text = text unless text == :none @description_text end |
#format_integer(number) ⇒ Object
Convert a number like 1234567 into the string "1,234,567"
263 264 265 |
# File 'lib/jirametrics/chart_base.rb', line 263 def format_integer number number.to_s.reverse.scan(/.{1,3}/).join(',').reverse end |
#format_status(object, board:, is_category: false, use_old_status: false) ⇒ Object
object will be either a Status or a ChangeItem if it's a ChangeItem then use_old_status will specify whether we're using the new or old Either way, is_category will format the category rather than the status
270 271 272 273 274 275 276 277 278 |
# File 'lib/jirametrics/chart_base.rb', line 270 def format_status object, board:, is_category: false, use_old_status: false status, = resolve_status object, board: board, use_old_status: use_old_status return "<span style='color: red'>#{}</span>" if color = status_category_color status visibility = is_category ? '' : not_visible_icon(status, board) text = is_category ? status.category : status "<span title='Category: #{status.category}'>#{color_block color.name} #{text}</span>#{visibility}" end |
#header_text(text = :none) ⇒ Object
252 253 254 255 |
# File 'lib/jirametrics/chart_base.rb', line 252 def header_text text = :none @header_text = text unless text == :none @header_text end |
#holidays(date_range: @date_range) ⇒ Object
Returns the non-working stretches (weekends and holidays) within the range, each as a Date range of consecutive days.
140 141 142 143 144 145 |
# File 'lib/jirametrics/chart_base.rb', line 140 def holidays date_range: @date_range date_range .select { |date| non_working_day?(date) } .slice_when { |previous_date, date| date != previous_date + 1 } .collect { |run| run.first..run.last } end |
#html_directory ⇒ Object
48 49 50 51 |
# File 'lib/jirametrics/chart_base.rb', line 48 def html_directory pathname = Pathname.new(File.realpath(__FILE__)) "#{pathname.dirname}/html" end |
#icon_span(title:, icon:) ⇒ Object
307 308 309 |
# File 'lib/jirametrics/chart_base.rb', line 307 def icon_span title:, icon: "<span title='#{title}' style='font-size: 0.8em;'>#{icon}</span>" end |
#link_to_issue(issue, args = {}) ⇒ Object
121 122 123 124 125 126 127 |
# File 'lib/jirametrics/chart_base.rb', line 121 def link_to_issue issue, args = {} attributes = { class: 'issue_key' } .merge(args) .collect { |key, value| "#{key}='#{value}'" } .join(' ') "<a href='#{issue.url}' #{attributes}>#{issue.key}</a>" end |
#next_id ⇒ Object
81 82 83 |
# File 'lib/jirametrics/chart_base.rb', line 81 def next_id @@chart_counter += 1 end |
#non_working_day?(date) ⇒ Boolean
147 148 149 |
# File 'lib/jirametrics/chart_base.rb', line 147 def non_working_day? date date.saturday? || date.sunday? || holiday_dates.include?(date) end |
#normalize_annotation_datetime(value) ⇒ Object
207 208 209 210 211 212 213 214 |
# File 'lib/jirametrics/chart_base.rb', line 207 def normalize_annotation_datetime value offset = timezone_offset || '+00:00' if value.include?('T') value.match?(/([+-]\d{2}:\d{2}|Z)$/) ? value : "#{value}#{offset}" else "#{value}T00:00:00#{offset}" end end |
#not_visible_icon(status, board) ⇒ Object
The 👀 marker for a status that isn't mapped to any column, or '' when it is visible.
298 299 300 301 302 303 304 305 |
# File 'lib/jirametrics/chart_base.rb', line 298 def not_visible_icon status, board return '' if board.visible_columns.any? { |column| column.status_ids.include? status.id } icon_span( title: "Not visible: The status #{status.name.inspect} is not mapped to any column and will not be visible", icon: ' 👀' ) end |
#not_visible_text(issue) ⇒ Object
311 312 313 314 315 316 |
# File 'lib/jirametrics/chart_base.rb', line 311 def not_visible_text issue reasons = issue.reasons_not_visible_on_board return nil if reasons.empty? "<span style='background: var(--warning-banner)'>Not visible on board: #{reasons.join(', ')}</span>" end |
#random_color ⇒ Object
327 328 329 330 |
# File 'lib/jirametrics/chart_base.rb', line 327 def random_color @palette_index = (@palette_index || -1) + 1 OKABE_ITO_PALETTE[@palette_index % OKABE_ITO_PALETTE.size] end |
#render(caller_binding, file) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/jirametrics/chart_base.rb', line 53 def render caller_binding, file pathname = Pathname.new(File.realpath(file)) basename = pathname.basename.to_s match = basename.match(/^(?<template_name>.+)\.rb$/) raise "Unexpected filename #{basename.inspect}" unless match # Insert a incrementing chart_id so that all the chart names on the page are unique caller_binding.eval "chart_id='chart#{next_id}'" # chart_id=chart3 erb = ERB.new file_system.load "#{html_directory}/#{match[:template_name]}.erb" erb.result(caller_binding) end |
#render_axis_title(axis_direction) ⇒ Object
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/jirametrics/chart_base.rb', line 385 def render_axis_title axis_direction text = case axis_direction when :x x_axis_title when :y y_axis_title else raise "Unexpected axis_direction: #{axis_direction}" end return '' unless text <<~CONTENT title: { display: true, text: "#{text}" }, CONTENT end |
#render_top_text(caller_binding) ⇒ Object
66 67 68 69 70 71 |
# File 'lib/jirametrics/chart_base.rb', line 66 def render_top_text caller_binding result = +'' result << "<h1 class='foldable'>#{@header_text}</h1>" if @header_text result << ERB.new(@description_text).result(caller_binding) if @description_text result end |
#resolve_status(object, board:, use_old_status:) ⇒ Object
Resolves the object being formatted into [status, error_message]. A Status is itself; a ChangeItem is looked up by id, yielding an error message (the raw value) when the id isn't a known status.
282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/jirametrics/chart_base.rb', line 282 def resolve_status object, board:, use_old_status: case object when ChangeItem id = use_old_status ? object.old_value_id : object.value_id status = board.possible_statuses.find_by_id(id) return [status, nil] unless status.nil? [nil, use_old_status ? object.old_value : object.value] when Status [object, nil] else raise "Unexpected type: #{object.class}" end end |
#stagger_label_positions(datetimes) ⇒ Object
195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/jirametrics/chart_base.rb', line 195 def stagger_label_positions datetimes return [] if datetimes.empty? threshold_days = (date_range.end - date_range.begin).to_f / 5.0 slot = 0 [LABEL_POSITIONS[0]] + datetimes.each_cons(2).map do |a, b| days_apart = (Date.parse(b) - Date.parse(a)).to_f.abs slot = days_apart < threshold_days ? slot + 1 : 0 LABEL_POSITIONS[slot % LABEL_POSITIONS.size] end end |
#status_category_color(status) ⇒ Object
318 319 320 321 322 323 324 325 |
# File 'lib/jirametrics/chart_base.rb', line 318 def status_category_color status case status.category.key when 'new' then CssVariable['--status-category-todo-color'] when 'indeterminate' then CssVariable['--status-category-inprogress-color'] when 'done' then CssVariable['--status-category-done-color'] else CssVariable['--status-category-unknown-color'] # Theoretically impossible but seen in prod. end end |
#to_human_readable(number) ⇒ Object
99 100 101 |
# File 'lib/jirametrics/chart_base.rb', line 99 def to_human_readable number number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse end |
#working_days_annotation ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/jirametrics/chart_base.rb', line 151 def working_days_annotation holidays.each_with_index.collect do |range, index| <<~TEXT holiday#{index}: { drawTime: 'beforeDraw', type: 'box', xMin: '#{range.begin}T00:00:00', xMax: '#{range.end}T23:59:59', backgroundColor: #{CssVariable.new('--non-working-days-color').to_json}, borderColor: #{CssVariable.new('--non-working-days-color').to_json} }, TEXT end.join end |
#wrap_and_render(caller_binding, file) ⇒ Object
Render the file and then wrap it with standard headers and quality checks.
74 75 76 77 78 79 |
# File 'lib/jirametrics/chart_base.rb', line 74 def wrap_and_render caller_binding, file result = +'' result << render_top_text(caller_binding) result << render(caller_binding, file) result end |