Class: CafeCar::ChartBuilder
- Inherits:
-
Object
- Object
- CafeCar::ChartBuilder
- Defined in:
- lib/cafe_car/chart_builder.rb
Overview
Aggregates an index collection into time buckets for the index "chart" view. Given a datetime/date column (the x-axis) and a granularity it GROUP BYs a DB-portable truncation of that column and COUNTs each bucket, then renders an inline SVG bar chart. The collection passed in is already policy-scoped and filtered (it is the same relation the table view renders), so the chart never counts rows the user can't see and always honors the active filters.
Constant Summary collapse
- FORMATS =
strftime patterns, shared by SQLite's
strftimeand Ruby'sTime#strftimeso a bucket label is identical whichever adapter produced the key. { day: "%Y-%m-%d", week: "%Y-%W", month: "%Y-%m" }.freeze
- DEFAULT_BUCKET =
:month- OPERATIONS =
The y-axis aggregations.
countis a plain row count (no column);sum/avgaggregate a numeric column and map to the portable ActiveRecord calculation. { "sum" => :sum, "avg" => :average }.freeze
- NUMERIC_TYPES =
%i[integer decimal float].freeze
- DEFAULT_METRIC =
"count"- METRIC_DEFAULTS =
{ "sum" => "Sum of %{attribute}", "avg" => "Average %{attribute}" }.freeze
- WIDTH =
SVG geometry (user units; the viewBox scales it to fit its container). WIDTH and HEIGHT are FIXED so the chart keeps one landscape shape at ANY bucket count. The CSS gives
.Charta full-width block withheight: auto, so the rendered height follows the viewBox aspect ratio — a column-per-bucket layout made that viewBox near-square for a few buckets (rendered tall and narrow) and very wide for many (rendered flat and short). Distributing the bars across a fixed WIDTH instead holds a steady ~3.5:1 landscape that always fills the column. 1000- HEIGHT =
280- PAD_TOP =
room for the value label above the tallest bar
14- PAD_BOTTOM =
room for the x-axis (bucket) labels
22- MAX_BAR =
cap bar width so a handful of buckets don't balloon
72- BAR_RATIO =
bar width as a fraction of its slot
0.6
Instance Attribute Summary collapse
-
#bucket ⇒ Object
readonly
Returns the value of attribute bucket.
-
#column ⇒ Object
readonly
Returns the value of attribute column.
-
#metric ⇒ Object
readonly
Returns the value of attribute metric.
Instance Method Summary collapse
- #bucket_options ⇒ Object
- #column_options ⇒ Object
-
#columns ⇒ Object
The date/datetime columns offered as x-axis choices: the model's displayable attributes (policy-respecting) whose type is a date.
-
#data ⇒ Object
Ordered
{ "2026-01" => count }over the policy-scoped, filtered collection. - #html_safe? ⇒ Boolean
-
#initialize(template, objects:, column: nil, bucket: nil, metric: nil) ⇒ ChartBuilder
constructor
A new instance of ChartBuilder.
- #metric_label(op, attribute) ⇒ Object
-
#metric_options ⇒ Object
[label, encoding]pairs for the y-metric select: a plain count plus a sum + average per numeric column. -
#metrics? ⇒ Boolean
Whether a y-metric selector is worth showing — only when there's a numeric column to aggregate (otherwise
countis the only choice). - #to_s ⇒ Object
-
#value_columns ⇒ Object
The numeric columns offered as y-axis aggregates: the model's displayable attributes (policy-respecting, the SAME source of truth as the x-axis) whose type is numeric.
- #~@ ⇒ Object
Constructor Details
#initialize(template, objects:, column: nil, bucket: nil, metric: nil) ⇒ ChartBuilder
Returns a new instance of ChartBuilder.
37 38 39 40 41 42 43 |
# File 'lib/cafe_car/chart_builder.rb', line 37 def initialize(template, objects:, column: nil, bucket: nil, metric: nil) @template = template @objects = objects @column = pick_column(column) @bucket = FORMATS.key?(bucket.to_s.to_sym) ? bucket.to_s.to_sym : DEFAULT_BUCKET @metric = pick_metric(metric) end |
Instance Attribute Details
#bucket ⇒ Object (readonly)
Returns the value of attribute bucket.
45 46 47 |
# File 'lib/cafe_car/chart_builder.rb', line 45 def bucket @bucket end |
#column ⇒ Object (readonly)
Returns the value of attribute column.
45 46 47 |
# File 'lib/cafe_car/chart_builder.rb', line 45 def column @column end |
#metric ⇒ Object (readonly)
Returns the value of attribute metric.
45 46 47 |
# File 'lib/cafe_car/chart_builder.rb', line 45 def metric @metric end |
Instance Method Details
#bucket_options ⇒ Object
70 |
# File 'lib/cafe_car/chart_builder.rb', line 70 def = FORMATS.keys.map { [ _1.to_s.capitalize, _1.to_s ] } |
#column_options ⇒ Object
69 |
# File 'lib/cafe_car/chart_builder.rb', line 69 def = columns.map { [ info(_1).label, _1 ] } |
#columns ⇒ Object
The date/datetime columns offered as x-axis choices: the model's displayable
attributes (policy-respecting) whose type is a date. Column NAMES only reach
the query through this allowlist — a ?chart_x= param outside it is dropped
by #pick_column, so a raw param can never be interpolated as a column name.
51 52 53 54 55 56 |
# File 'lib/cafe_car/chart_builder.rb', line 51 def columns @columns ||= policy.attributes.displayable .map { info(_1) } .select { _1.type.in?(%i[date datetime]) } .map { _1.method.to_s } end |
#data ⇒ Object
Ordered { "2026-01" => count } over the policy-scoped, filtered collection.
92 |
# File 'lib/cafe_car/chart_builder.rb', line 92 def data = @data ||= aggregate |
#html_safe? ⇒ Boolean
94 |
# File 'lib/cafe_car/chart_builder.rb', line 94 def html_safe? = true |
#metric_label(op, attribute) ⇒ Object
87 88 89 |
# File 'lib/cafe_car/chart_builder.rb', line 87 def metric_label(op, attribute) t("chart.metric.#{op}", attribute:, default: METRIC_DEFAULTS.fetch(op) % { attribute: }) end |
#metric_options ⇒ Object
[label, encoding] pairs for the y-metric select: a plain count plus a
sum + average per numeric column. Labels come from the locale.
78 79 80 81 82 83 84 85 |
# File 'lib/cafe_car/chart_builder.rb', line 78 def count = [ t("chart.metric.count", default: "Count"), DEFAULT_METRIC ] aggs = value_columns.flat_map do |col| attribute = info(col).label OPERATIONS.each_key.map { |op| [ metric_label(op, attribute), "#{op}:#{col}" ] } end [ count, *aggs ] end |
#metrics? ⇒ Boolean
Whether a y-metric selector is worth showing — only when there's a numeric
column to aggregate (otherwise count is the only choice).
74 |
# File 'lib/cafe_car/chart_builder.rb', line 74 def metrics? = value_columns.any? |
#to_s ⇒ Object
95 |
# File 'lib/cafe_car/chart_builder.rb', line 95 def to_s = svg.to_s |
#value_columns ⇒ Object
The numeric columns offered as y-axis aggregates: the model's displayable
attributes (policy-respecting, the SAME source of truth as the x-axis) whose
type is numeric. A chart_y=sum:<col> param's column is validated against
this list by #pick_metric, so a raw param never becomes a column reference.
62 63 64 65 66 67 |
# File 'lib/cafe_car/chart_builder.rb', line 62 def value_columns @value_columns ||= policy.attributes.displayable .map { info(_1) } .select { _1.type.in?(NUMERIC_TYPES) } .map { _1.method.to_s } end |
#~@ ⇒ Object
96 |
# File 'lib/cafe_car/chart_builder.rb', line 96 def ~@ = @template.concat(to_s) |