Module: PredictabilityEngine::VegaVisualizer::CfdLayout

Defined in:
lib/predictability_engine/vega_visualizer/cfd_layout.rb

Overview

Layout and data logic for CFD charts in Vega-Lite.

Class Method Summary collapse

Class Method Details

.area_layer(pcts, legend: true) ⇒ Object



32
33
34
35
36
37
38
39
40
# File 'lib/predictability_engine/vega_visualizer/cfd_layout.rb', line 32

def self.area_layer(pcts, legend: true)
  cfg = { field: 'type', type: 'nominal' }
  cfg[:legend] = { title: 'Flow & Forecast', orient: 'bottom', columns: 3 } if legend && !pcts.empty?
  { mark: { type: 'area' },
    encoding: { y: VegaVisualizer.quantitative_y_axis('count', title: 'Total Items', stack: nil),
                color: cfg,
                order: { field: 'order', type: 'quantitative' },
                tooltip: VegaVisualizer.cfd_tooltip_fields } }
end

.build_unified_data(data, percentiles) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/predictability_engine/vega_visualizer/cfd_layout.rb', line 7

def self.build_unified_data(data, percentiles)
  res = []
  sorted_pcts = percentiles.sort
  data[:dates].each_with_index do |date, i|
    res << { date: PredictabilityEngine.format_date(date), count: data[:arrivals][i], type: 'Arrivals', order: 0 }
    sorted_pcts.each_with_index do |p, pi|
      res << { date: PredictabilityEngine.format_date(date), count: data[:forecasts][p][i],
               type: "#{p}% Confidence", order: pi + 1 }
    end
    if i < data[:departed].size
      res << { date: PredictabilityEngine.format_date(date), count: data[:departed][i], type: 'Departures',
               order: sorted_pcts.size + 1 }
    end
  end
  res
end

.color_scale(pcts) ⇒ Object



24
25
26
27
28
29
30
# File 'lib/predictability_engine/vega_visualizer/cfd_layout.rb', line 24

def self.color_scale(pcts)
  sorted_pcts = pcts.sort
  dom = ['Arrivals'] + sorted_pcts.map { |p| "#{p}% Confidence" } + ['Departures']
  palette = ['#72b7b2', '#e45756', '#b279a2', '#ff9da7', '#ad494a', '#8ca27a']
  range = ['#4c78a8'] + palette.take(sorted_pcts.size) + ['#59a14f']
  [dom, range]
end

.line_layerObject



42
43
44
45
46
47
48
49
50
# File 'lib/predictability_engine/vega_visualizer/cfd_layout.rb', line 42

def self.line_layer
  { mark: { type: 'line' },
    encoding: { y: VegaVisualizer.quantitative_y_axis('count', title: 'Total Items'),
                strokeDash: {
                  condition: { test: "datum.type == 'Arrivals' || datum.type == 'Departures'", value: [] },
                  value: [4, 4]
                },
                tooltip: VegaVisualizer.cfd_tooltip_fields } }
end

.vert_data(forecast, percentiles) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/predictability_engine/vega_visualizer/cfd_layout.rb', line 86

def self.vert_data(forecast, percentiles)
  sorted_pcts = percentiles.sort
  data_by_date = group_pcts_by_date(forecast, sorted_pcts)

  # IMMUTABLE invariant — see CLAUDE.md §"Forecast alignment invariant".
  # Rule height = percentile-surface plateau (departed_so_far + wip), so each
  # vertical rule hits the top-right corner of its p% surface exactly.
  plateau = forecast[:summary][:departed_so_far] + forecast[:summary][:wip]

  data_by_date.sort_by { |date, _| date }.map do |date, p_list|
    date_str = PredictabilityEngine.format_date(date)
    label = "#{p_list.sort.map { |p| "#{p}%" }.join(', ')} (#{date_str})"

    { date: date_str, label: label,
      tooltip: p_list.map { |p| "#{p}% Confidence (#{date_str})" }.join("\n"),
      count: plateau }
  end
end

.vert_layers(forecast, percentiles) ⇒ Object



52
53
54
55
# File 'lib/predictability_engine/vega_visualizer/cfd_layout.rb', line 52

def self.vert_layers(forecast, percentiles)
  data = vert_data(forecast, percentiles)
  [rule_layer(data), text_layer(data)]
end