Class: CumulativeFlowDiagram

Inherits:
ChartBase show all
Defined in:
lib/jirametrics/cumulative_flow_diagram.rb

Constant Summary

Constants inherited from ChartBase

ChartBase::LABEL_POSITIONS

Instance Attribute Summary

Attributes inherited from ChartBase

#aggregated_project, #all_boards, #atlassian_document_format, #board_id, #canvas_height, #canvas_width, #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

Methods inherited from ChartBase

#aggregated_project?, #before_run, #call_before_run, #canvas, #canvas_responsive?, #chart_format, #collapsible_issues_panel, #color_block, #color_for, #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, #label_days, #label_hours, #label_issues, #label_minutes, #link_to_issue, #next_id, #normalize_annotation_datetime, #not_visible_text, #random_color, #render, #render_axis_title, #render_top_text, #seam_end, #seam_start, #stagger_label_positions, #status_category_color, #to_human_readable, #working_days_annotation, #wrap_and_render

Constructor Details

#initialize(block) ⇒ CumulativeFlowDiagram

Returns a new instance of CumulativeFlowDiagram.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/jirametrics/cumulative_flow_diagram.rb', line 38

def initialize block
  super()
  header_text 'Cumulative Flow Diagram'
  description_text <<~HTML
    <div class="p">
      A Cumulative Flow Diagram (CFD) shows how work accumulates across board columns over time.
      Each coloured band represents a workflow stage. The top edge of the leftmost band shows
      total work entered; the top edge of the rightmost band shows total work completed.
    </div>
    <div class="p">
      A widening band means work is piling up in that stage — a bottleneck. Parallel top edges
      (bands staying the same width) indicate smooth flow. Steep rises in the leftmost band
      without corresponding rises on the right mean new work is arriving faster than it is
      being finished.
    </div>
    <div class="p">
      Dashed lines and hatched regions indicate periods where an item moved backwards through
      the workflow (a correction). These highlight rework or process irregularities worth
      investigating.
    </div>
    <div class="p">
      The chart also overlays two trend lines and an interactive triangle. The <b>arrival rate</b>
      trend line shows how fast work is entering the system; the <b>departure rate</b> trend line
      shows how fast it is leaving. Move the mouse over the chart to see a Little's Law triangle
      at that point in time, labelled with three derived metrics: <b>Work In Progress (WIP)</b> (items started
      but not finished), <b>approximate average cycle time (CT)</b> (roughly how long an average item takes to complete), and
      <b>average throughput (TP)</b> (items completed per day). Use the checkbox above the chart to toggle
      between the triangle and the normal data tooltips.
    </div>
    <div class="p">
      CT and TP require a future point C where cumulative completions catch up to current arrivals.
      When the cursor is near the right edge and that point falls outside the visible date range,
      CT and TP cannot be calculated and are hidden; only WIP is shown.
    </div>
    <div class="p">
      See also: This article on <a href="https://blog.mikebowler.ca/2026/03/27/cumulative-flow-diagram/">how to read a CFD</a>.
    </div>      
  HTML
  instance_eval(&block)
end

Instance Method Details

#arrival_rate_line_color(color) ⇒ Object



87
88
89
# File 'lib/jirametrics/cumulative_flow_diagram.rb', line 87

def arrival_rate_line_color color
  @arrival_rate_line_color = parse_theme_color(color)
end

#column_rules(&block) ⇒ Object



79
80
81
# File 'lib/jirametrics/cumulative_flow_diagram.rb', line 79

def column_rules &block
  @column_rules_block = block
end

#departure_rate_line_color(color) ⇒ Object



91
92
93
# File 'lib/jirametrics/cumulative_flow_diagram.rb', line 91

def departure_rate_line_color color
  @departure_rate_line_color = parse_theme_color(color)
end

#runObject



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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
# File 'lib/jirametrics/cumulative_flow_diagram.rb', line 95

def run
  all_columns = current_board.visible_columns

  column_rules_list = all_columns.map do |column|
    rules = CfdColumnRules.new
    @column_rules_block&.call(column, rules)
    rules
  end

  active_pairs   = all_columns.zip(column_rules_list).reject { |_, rules| rules.ignored? }
  active_columns = active_pairs.map(&:first)
  active_rules   = active_pairs.map(&:last)

  cfd = CfdDataBuilder.new(
    board: current_board,
    issues: issues,
    date_range: date_range,
    columns: active_columns
  ).run

  columns            = cfd[:columns]
  daily_counts       = cfd[:daily_counts]
  correction_windows = cfd[:correction_windows]
  column_count       = columns.size

  # Convert cumulative totals to marginal band heights for Chart.js stacking.
  # cumulative[i] = issues that reached column i or further.
  # marginal[i]   = cumulative[i] - cumulative[i+1]  (last column: marginal = cumulative)
  daily_marginals = daily_counts.transform_values do |cumulative|
    cumulative.each_with_index.map do |count, i|
      i < column_count - 1 ? count - cumulative[i + 1] : count
    end
  end

  border_colors = active_rules.map { |rules| rules.color || random_color }

  fill_colors = active_rules.zip(border_colors).map { |rules, border| fill_color_for(rules, border) }

  # Datasets in reversed order: rightmost column first (bottom of stack), leftmost last (top).
  data_sets = columns.each_with_index.map do |name, col_index|
    col_windows = correction_windows
      .select { |w| w[:column_index] == col_index }
      .map { |w| { start_date: w[:start_date].to_s, end_date: w[:end_date].to_s } }

    {
      label: active_rules[col_index].label || name,
      label_hint: active_rules[col_index].label_hint,
      data: date_range.map { |date| { x: date.to_s, y: daily_marginals[date][col_index] } },
      backgroundColor: fill_colors[col_index],
      borderColor: border_colors[col_index],
      fill: true,
      tension: 0,
      segment: Segment.new(col_windows)
    }
  end.reverse

  # Correction windows for the afterDraw hatch plugin, with dataset index in
  # Chart.js dataset array (reversed: done column = index 0).
  hatch_windows = correction_windows.map do |w|
    {
      dataset_index: column_count - 1 - w[:column_index],
      start_date: w[:start_date].to_s,
      end_date: w[:end_date].to_s,
      color: border_colors[w[:column_index]],
      fill_color: fill_colors[w[:column_index]]
    }
  end

  @triangle_color = parse_theme_color(['#333333', '#ffffff']) unless instance_variable_defined?(:@triangle_color)
  unless instance_variable_defined?(:@arrival_rate_line_color)
    @arrival_rate_line_color = 'rgba(255,138,101,0.85)'
  end
  unless instance_variable_defined?(:@departure_rate_line_color)
    @departure_rate_line_color = 'rgba(128,203,196,0.85)'
  end

  wrap_and_render(binding, __FILE__)
end

#triangle_color(color) ⇒ Object



83
84
85
# File 'lib/jirametrics/cumulative_flow_diagram.rb', line 83

def triangle_color color
  @triangle_color = parse_theme_color(color)
end