I love gem 'chartkick' for building quick and simple charting.

An optimization I found is to render charts when they are on screen. Sometimes we’ll have long dashboards with a dozen charts, which would normally fire off a dozen async requests.

I introduced a file onscreen_chart_helper.rb

module OnscreenChartHelper
  include Chartkick::Helper

  def onscreen_chart(chart_type, data_source, **options)
    options = Chartkick::Utils.deep_merge(Chartkick.options, options)

    @chartkick_chart_id ||= 0
    element_id = options.delete(:id) || "chart-#{@chartkick_chart_id += 1}"

    height = (options.delete(:height) || "300px").to_s
    width = (options.delete(:width) || "100%").to_s

    html_vars = {
      id: element_id,
      height: height,
      width: width,
      loading: options[:loading] || "Loading..."
    }

    [:height, :width].each do |k|
      raise ArgumentError, "Invalid #{k}" unless html_vars[k] =~ /\A[a-zA-Z0-9%.]*\z/
    end

    html_vars.each_key do |k|
      html_vars[k] = ERB::Util.html_escape(html_vars[k])
    end

    html = %(<div id="%{id}" class="onscreen_chart" style="height: %{height}; width: %{width}; text-align: center; color: #999; line-height: %{height}; font-size: 14px; font-family: 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, Helvetica, sans-serif;">%{loading}</div>) % html_vars

    js_vars = {
      type: chart_type.to_json,
      data: data_source.respond_to?(:chart_json) ? data_source.chart_json : data_source.to_json,
      options: options.to_json
    }

    js_vars.each_key do |k|
      js_vars[k] = Chartkick::Utils.json_escape(js_vars[k])
    end

    js = <<-JS
      <script>
        window.lazyChartConfigs = window.lazyChartConfigs || {};
        window.lazyChartConfigs["#{element_id}"] = {
          type: #{js_vars[:type]},
          data: #{js_vars[:data]},
          options: #{js_vars[:options]}
        };
      </script>
    JS

    (html + js).html_safe
  end

  def onscreen_line_chart(data_source, **options)
    onscreen_chart("LineChart", data_source, **options)
  end

  def onscreen_pie_chart(data_source, **options)
    onscreen_chart("PieChart", data_source, **options)
  end

  def onscreen_column_chart(data_source, **options)
    onscreen_chart("ColumnChart", data_source, **options)
  end

  def onscreen_bar_chart(data_source, **options)
    onscreen_chart("BarChart", data_source, **options)
  end

  def onscreen_area_chart(data_source, **options)
    onscreen_chart("AreaChart", data_source, **options)
  end

  def onscreen_scatter_chart(data_source, **options)
    onscreen_chart("ScatterChart", data_source, **options)
  end

  def onscreen_geo_chart(data_source, **options)
    onscreen_chart("GeoChart", data_source, **options)
  end

  def onscreen_timeline(data_source, **options)
    onscreen_chart("Timeline", data_source, **options)
  end

  # Alias Chartkick methods to use onscreen versions
  alias_method :line_chart, :onscreen_line_chart
  alias_method :pie_chart, :onscreen_pie_chart
  alias_method :column_chart, :onscreen_column_chart
  alias_method :bar_chart, :onscreen_bar_chart
  alias_method :area_chart, :onscreen_area_chart
  alias_method :scatter_chart, :onscreen_scatter_chart
  alias_method :geo_chart, :onscreen_geo_chart
  alias_method :timeline, :onscreen_timeline
end

Then add javascript observer file:

// Optimized Intersection Observer setup
function initIntersectionObserver(className = 'onscreen_chart', callback) {
    if ("IntersectionObserver" in window) {
        setupIntersectionObserver(className, callback);
    } else {
        setupLegacyObserver(className, callback);
    }
}

function setupIntersectionObserver(className, callback) {
    const observer = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                handleIntersection(entry.target, className, callback);
                observer.unobserve(entry.target);
            }
        });
    });

    document.querySelectorAll(`.${className}`).forEach(target => {
        observer.observe(target);
    });
}

function setupLegacyObserver(className, callback) {
    let throttleTimeout;

    function lazyload() {
        if (throttleTimeout) {
            clearTimeout(throttleTimeout);
        }

        throttleTimeout = setTimeout(() => {
            const scrollTop = window.scrollY;
            document.querySelectorAll(`.${className}`).forEach(target => {
                if (target.offsetTop < window.innerHeight + scrollTop) {
                    handleIntersection(target, className, callback);
                }
            });

            if (document.querySelectorAll(`.${className}`).length === 0) {
                document.removeEventListener("scroll", lazyload);
                window.removeEventListener("resize", lazyload);
                window.removeEventListener("orientationChange", lazyload);
            }
        }, 20);
    }

    document.addEventListener("scroll", lazyload);
    window.addEventListener("resize", lazyload);
    window.addEventListener("orientationChange", lazyload);
}

function handleIntersection(element, className, callback) {
    element.classList.remove(className);
    if (callback) {
        callback(element);
    }
}

// Usage
document.addEventListener("DOMContentLoaded", () => {
    initIntersectionObserver('onscreen_chart', (element) => {
        // This callback will be called when the element is in view
        const chartId = element.id;
        if (window.lazyChartConfigs && window.lazyChartConfigs[chartId]) {
            const config = window.lazyChartConfigs[chartId];
            new Chartkick[config.type](chartId, config.data, config.options);
        }
    });
});