Skip to Content
Course content

221: Building Custom Admin Dashboards with Charts

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start building an admin dashboard, the temptation is to treat it like any other page in your Rails app. You want a chart showing daily revenue for the last 30 days, so you think: "I'll just grab all the orders from the last month and let the frontend handle the rest." It feels intuitive, and for a few dozen records in development, it works perfectly. But this is where most developers stumble into a performance wall that doesn't show up until the app is actually in production.

The "Fetch Everything and Filter" Trap

The naive approach usually looks something like this: you fetch a collection of ActiveRecord objects, map them into a Ruby array, and pass that massive blob of JSON to a library like Chart.js or Highcharts. I've seen this a dozen times. The code looks clean because it's "pure Ruby," but under the hood, it's a disaster.

# The naive way
def dashboard
  @orders = Order.where('created_at > ?', 30.days.ago)
  @chart_data = @orders.map { |o| { date: o.created_at.to_date, amount: o.total } }
  # This sends thousands of objects to the view/JS
end

The problem here is that you're loading thousands of full ActiveRecord objects into memory just to extract two fields: a date and a number. If you have 10,000 orders a month, you're allocating 10,000 Ruby objects, calculating their attributes, and then sending a massive JSON array over the wire. Your server's RAM will spike, and your browser will likely stutter while trying to parse a 5MB JSON response just to draw a few lines on a graph. It's a classic case of over-fetching.

Letting the Database Do the Heavy Lifting

The better way is to remember that SQL is incredibly good at math—much better than Ruby is at iterating over arrays. Instead of bringing the data to the logic, we bring the logic to the data. We want the database to return only the final coordinates for the chart, not the raw records used to calculate them.

By using GROUP BY and SUM, we can collapse thousands of rows into just 30 rows (one for each day). I prefer wrapping this logic in a Query Object to keep the controller lean and the logic reusable.

# The professional way: A dedicated query object
class DailyRevenueQuery
  def initialize(start_date = 30.days.ago)
    @start_date = start_date
  end

  def call
    Order.where('created_at >= ?', @start_date)
         .group("DATE(created_at)")
         .sum(:total)
    # Returns a Hash: { "2023-10-01" => 450.00, "2023-10-02" => 320.50, ... }
  end
end

Now, your controller is just calling DailyRevenueQuery.new.call. Instead of transferring 10,000 records, you're transferring 30. The memory footprint drops from megabytes to kilobytes, and the response time becomes nearly instantaneous regardless of how many orders you have.

When the Trade-off Hits the Road

Now, you might argue that the naive way is more "flexible." If you suddenly need to filter those orders by a specific category or user attribute on the fly, having the objects in memory feels easier. But that's a trap. As your dataset grows, "flexibility" becomes "unusable."

The real trade-off is between the convenience of ActiveRecord's object-oriented nature and the raw power of SQL. When building dashboards, always lean toward the database. If the query gets too complex for a simple .group().sum(), don't be afraid to write a raw SQL fragment or use a View in your database. Your goal for a dashboard isn't to manipulate objects; it's to aggregate data. The moment you find yourself calling .each or .map on a collection that could be summarized in SQL, you're doing it the hard way.




📋 Practical Task

Build a "User Growth" Aggregation Query

You are tasked with adding a "New User Signups" chart to the admin dashboard. Currently, the app is fetching all users and counting them in the view, which is slowing down the page load.

Create a query object named UserGrowthQuery that returns a hash where the keys are dates (for the last 30 days) and the values are the count of users who signed up on that date. Your solution must use .group() and .count() to ensure the aggregation happens in the database, not in Ruby memory.

Requirements:

  • The query must filter for users created within the last 30 days.
  • The result must be a Hash compatible with a chart library (e.g., {"2023-11-01" => 5, "2023-11-02" => 12}).
  • Avoid loading full ActiveRecord objects into a Ruby array.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.