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);
}
});
});