Transforming Bank Data into a Real-Time Finance Dashboard with Grafana

In my previous post, I showed how to use Playwright to scrape transaction data and store it in a PostgreSQL database. However, raw tables aren’t very helpful for quick insights. In this post, we’ll visualize that data using Grafana.

Using Grafana Variables for Dynamic SQL Queries

Time Grouping

I use a variable called ${time_group} to control the aggregation granularity.

  • Type: Interval or Custom
  • Values: day, week, month

Time Filter

$__timeFilter is a built-in Grafana variable that allows you to dynamically filter data based on the selected dashboard time range.

Spending by Category

WITH base AS (
    SELECT
        COALESCE(record_date, occurrence_datetime, transaction_date) AS effective_date,
        amount,
        CASE
            WHEN category = 'others' THEN split_part(split_part(title, ' ', 1), '-', 1)
            ELSE category
        END AS category_label
    FROM transaction_log
    WHERE category != 'exclusion' AND category != 'income' AND amount > 0
)
SELECT
    DATE_TRUNC('${time_group}', effective_date) AS grouped_time,
    category_label,
    SUM(amount) AS total_amount
FROM base
WHERE $__timeFilter(effective_date)
GROUP BY grouped_time, category_label
ORDER BY grouped_time, category_label;

The result of this query cannot be directly visualized as a standard time series because it returns rows in the format (time, category, total_amount). However, most charts expect a matrix format where each category becomes a separate column.

To solve this, I used Grafana’s Transform → Grouping to Matrix feature to reshape the data.


Balance Trend (Net Worth)

Since our data is stored as a transaction log rather than periodic snapshots, calculating balance trends requires a different approach.

To track how your net worth evolves, we need the last recorded balance for each time period (day/week/month). We use ROW_NUMBER() to select the most recent record within each group.

WITH base AS (
    SELECT
        COALESCE(occurrence_datetime, record_date, transaction_date) AS effective_date,
        balance
    FROM transaction_log
    WHERE amount > 0 AND balance != 0
)
SELECT group_time, balance
FROM (
    SELECT
        DATE_TRUNC('${time_group}', effective_date) AS group_time,
        balance,
        ROW_NUMBER() OVER (
            PARTITION BY DATE_TRUNC('${time_group}', effective_date)
            ORDER BY effective_date DESC
        ) AS rn
    FROM base
) t
WHERE rn = 1
ORDER BY group_time;

Search Records

SELECT *,
       COALESCE(record_date, occurrence_datetime, transaction_date) AS time
FROM transaction_log
WHERE CONCAT_WS(' ', id, pk, title, memo, comment, post_script, category, currency_amount)
      ILIKE '%${search_text}%'
  AND $__timeFilter(COALESCE(transaction_date))
ORDER BY COALESCE(record_date, occurrence_datetime, transaction_date) DESC
LIMIT 1000;


Final Result

This setup turns raw financial transaction logs into a flexible, real-time dashboard, making it much easier to monitor spending patterns and track net worth over time.