Module: RailsuiCharts::ChartHelper

Defined in:
lib/railsui_charts/chart_helper.rb

Constant Summary collapse

CIRCULAR_TYPES =

Forms whose placeholder should be a disc rather than a plotting area.

%i[pie donut polar_area radar].freeze

Instance Method Summary collapse

Instance Method Details

#railsui_chart(data, type: :line, **options) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/railsui_charts/chart_helper.rb', line 8

def railsui_chart(data, type: :line, **options)
  # A chart whose series are not the numbers a reader wants can say so.
  # Deleted rather than read, so it never reaches the builder and never
  # gets serialised into the Stimulus value alongside the real config.
  explicit_table = options.delete(:accessible_table)

  # An empty dataset is a normal day one, not an error. Rendering axes
  # around nothing looks like a chart that failed rather than a chart with
  # nothing to show yet.
  return railsui_chart_empty(**empty_options(options)) if ApexOptionsBuilder.blank?(data)

  config = ApexOptionsBuilder.new(data, type: type, **options).build
  id = options[:id] || "rui-chart-#{SecureRandom.hex(4)}"

  table =
    if options[:accessible] == false
      nil
    elsif explicit_table.present?
      explicit_accessibility_table(explicit_table, id: id)
    else
      accessibility_table(config, id: id)
    end

  (:div,
              id: id,
              class: "railsui-chart",
              # Charts draw when they scroll into view, so the space has to
              # be held from the start. Without it the page is short on
              # load and grows under the reader as they scroll.
              style: reserved_height(config),
              data: { controller: "railsui-chart", "railsui-chart-options-value": config.to_json }) do
    table
  end
end

#railsui_chart_empty(title: nil, description: nil, height: nil) ⇒ Object

Holds the chart's footprint so a card keeps its shape whether or not the query came back with anything.



86
87
88
# File 'lib/railsui_charts/chart_helper.rb', line 86

def railsui_chart_empty(title: nil, description: nil, height: nil)
  chart_state(:empty, title: title || "No data", description: description, height: height)
end

#railsui_chart_error(title: nil, description: nil, height: nil) ⇒ Object



90
91
92
# File 'lib/railsui_charts/chart_helper.rb', line 90

def railsui_chart_error(title: nil, description: nil, height: nil)
  chart_state(:error, title: title || "Couldn't load this chart", description: description, height: height)
end

#railsui_chart_skeleton(height: nil, type: :line, label: "Loading chart", align: :left) ⇒ Object

Server-rendered placeholder for a chart whose data has not arrived yet — the thing a Turbo frame shows before it swaps in the real one. It keeps the chart footprint and avoids drawing fake chart geometry before data exists.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/railsui_charts/chart_helper.rb', line 98

def railsui_chart_skeleton(height: nil, type: :line, label: "Loading chart", align: :left)
  alignment = skeleton_alignment(align)

  (:div,
              class: "railsui-chart-state railsui-chart-state--loading railsui-chart-state--loading-#{alignment}",
              style: state_height(height),
              role: "status",
              aria: { label: label, busy: true }) do
    (:div, class: "railsui-chart-skeleton", aria: { hidden: true }) do
      safe_join([
        (:span, "", class: "railsui-skeleton-bar railsui-skeleton-bar--label"),
        (:span, "", class: "railsui-skeleton-bar railsui-skeleton-bar--value"),
        (:span, "", class: "railsui-skeleton-bar railsui-skeleton-bar--meta"),
        (:span, "", class: "railsui-chart-skeleton__plot"),
        (:span, "", class: "railsui-skeleton-bar railsui-skeleton-bar--footer")
      ])
    end
  end
end

#railsui_small_multiples(series, type: :line, height: 120, columns: 3, **options) ⇒ Object

One small chart per series, sharing a y-scale.

This is the honest answer when there are more categories than a single chart can hold. Eight lines on one axis is a plate of spaghetti, and a ninth colour is not distinguishable from the others anyway — facets scale where colour does not.

<%= railsui_small_multiples @plans, type: :area, columns: 3 %>


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/railsui_charts/chart_helper.rb', line 51

def railsui_small_multiples(series, type: :line, height: 120, columns: 3, **options)
  return railsui_chart_empty(**empty_options(options)) if ApexOptionsBuilder.blank?(series)

  entries = Array(series)
  shared = shared_scale(entries)

  (:div, class: "railsui-small-multiples", style: "--rui-small-multiple-columns: #{columns.to_i}") do
    safe_join(entries.each_with_index.map do |entry, index|
      (:div, class: "railsui-small-multiple") do
        safe_join([
          (:p, entry[:name] || entry["name"] || "Series #{index + 1}", class: "railsui-small-multiple__title"),
          # Every facet takes the same colour. The title carries identity
          # here, so spending a hue on it would say nothing extra.
          railsui_chart(entry[:data] || entry["data"], type: type, height: height,
                                                       yaxis: shared.merge(tickAmount: 3), **options)
        ])
      end
    end)
  end
end

#shared_scale(entries) ⇒ Object

Facets only compare if they share a scale. Left to themselves each one would fit its own data and a small series would look like a large one.



74
75
76
77
78
79
80
81
82
# File 'lib/railsui_charts/chart_helper.rb', line 74

def shared_scale(entries)
  values = entries.flat_map { |entry| Array(entry[:data] || entry["data"]).map { |point| ApexOptionsBuilder.value_of(point) } }.compact
  return {} if values.empty?

  min, max = values.minmax
  return {} if min == max

  { min: min, max: max }
end