← Back to Data Analysis & Visualizations Under the Hood

How the live dashboard actually works.

The dashboard on the Data Analysis page isn't a screenshot or a static mockup — it's pulling live records from a real Airtable base, aggregating them in JavaScript, and rendering charts and KPIs in real time. Here's the full technical stack, layer by layer.

Architecture Overview

Four components working together.

The dashboard connects a WordPress frontend to Airtable as the data source, with all aggregation and visualization logic running entirely in the browser. The Airtable API key never touches the browser — all data routes through a server-side PHP proxy, identical to the one powering the Process Automation demo.

01

WordPress Frontend

A hand-coded PHP page template renders the filter controls, KPI cards, bar chart, donut chart, trend bars, and data table. Vanilla JavaScript handles all data fetching, aggregation, filtering, and DOM rendering — no charting library, no framework, no build step.

02

SO Airtable Connector Plugin

The same custom WordPress plugin that powers the Process Automation demo registers the so_airtable_proxy read endpoint. It holds the API key in wp-config.php as a PHP constant — never in the database, never visible in the browser or network traffic.

03

Airtable REST API

The plugin makes server-side wp_remote_get calls to Airtable's v0 API across four tables in the Northshore Community Services demo base: Clients, Programs, Service Requests, and Monthly Metrics. Each fetch returns a full record set which JavaScript then processes locally.

04

Client-Side Aggregation & Rendering

Once data arrives in the browser, JavaScript takes over completely — counting records, grouping by status or program, calculating percentages, and rendering every chart and KPI card from scratch. Filters update the view instantly without another round trip to the server.

Dashboard Components

What the dashboard actually renders.

Each component is built from raw Airtable record data — no pre-aggregated fields, no summary tables. The math happens in the browser on every filter change.

📊 KPI Cards Total Clients, Active Clients, Open Service Requests, and Programs Offered — each a live count from the relevant Airtable table, recalculated on every filter change.
📈 Bar Chart — Clients by Status Groups Clients records by their Status field value and renders proportional bars using pure CSS width percentages. No canvas, no SVG library — just divs and math.
🍩 Donut Chart — Programs Distribution Counts Service Requests by Program and renders a CSS conic-gradient donut with a legend. The gradient stop positions are calculated in JavaScript from the record counts.
📉 Trend Bars — Monthly Metrics Renders Monthly Metrics records as a horizontal bar series sorted by month. Each bar's width is proportional to its value relative to the maximum in the current dataset.
📋 Data Table — Client Records A filterable, sortable table of Clients records showing Name, Status, and Created Time. Filter controls at the top narrow the visible rows without re-fetching from Airtable.
🔍 Filter Controls Dropdown filters for Status and Program scope the entire dashboard simultaneously — KPIs, charts, trend bars, and table all update from the same filtered dataset in a single render pass.
Production Code

Fetching data — proxy to dashboard.

The dashboard fires parallel fetches to the proxy endpoint for each table on page load. Once all four resolve, the render pipeline runs once with the complete dataset. This is the actual JavaScript running on this page right now.

page-data-analysis.php — inline JS (parallel data fetch) JavaScript
function fetchTable(table, params) {
    var url = ajaxUrl
        + "?action=so_airtable_proxy"
        + "&table=" + encodeURIComponent(table)
        + (params ? "&" + params : "");
    return fetch(url).then(function(r) { return r.json(); });
}

// Fire all four fetches in parallel — render only after all resolve
Promise.all([
    fetchTable("Clients"),
    fetchTable("Programs"),
    fetchTable("tblfrY1tR2ZMg3MHT"),   // Service Requests — ID required, name fails
    fetchTable("tblSJUdMl5UgfrP2m")    // Monthly Metrics  — ID required, name fails
]).then(function(results) {
    dashboardData.clients        = results[0].records || [];
    dashboardData.programs       = results[1].records || [];
    dashboardData.serviceReqs    = results[2].records || [];
    dashboardData.monthlyMetrics = results[3].records || [];
    renderDashboard();  // single render pass with complete dataset
}).catch(function(err) {
    showError("Dashboard failed to load. Please refresh.");
});
page-data-analysis.php — inline JS (donut chart render) JavaScript
function renderDonut(serviceReqs, programs) {
    // Count requests per program
    var counts = {};
    serviceReqs.forEach(function(r) {
        var prog = r.fields["Programs"];   // "Programs" plural — actual API field name
        if (Array.isArray(prog)) {
            prog.forEach(function(p) {
                counts[p] = (counts[p] || 0) + 1;
            });
        }
    });

    var total = Object.values(counts).reduce(function(a, b) { return a + b; }, 0);
    if (total === 0) return;

    // Build conic-gradient string from percentage slices
    var gradient = "";
    var cursor = 0;
    var colors = ["#e882a5","#333333","#fef2f5","#d46f92","#a0a0a0"];
    var i = 0;
    Object.keys(counts).forEach(function(prog) {
        var pct = (counts[prog] / total) * 100;
        gradient += colors[i % colors.length]
            + " " + cursor + "% "
            + (cursor + pct) + "%,";
        cursor += pct;
        i++;
    });

    donutEl.style.background =
        "conic-gradient(" + gradient.slice(0,-1) + ")";
}
Real-World Notes

What the Airtable API actually requires.

The Northshore demo base has two tables that don't respond to name-based API calls — they require their table IDs instead. This is a real quirk of how Airtable assigns internal names, and it's exactly the kind of thing that only shows up in production.

⚠️ Table ID vs. Name Service Requests (tblfrY1tR2ZMg3MHT) and Monthly Metrics (tblSJUdMl5UgfrP2m) must be called by ID. Using their display names returns a 404 from the Airtable API. Clients and Programs work fine by name.
🔗 Linked Record Arrays The Programs field on Service Requests returns an array of linked record IDs, not display names. The dashboard resolves these against the Programs table client-side to show human-readable labels.
📝 Field Name: "Programs" (plural) The API field name for the program relationship on Service Requests is Programs with an S — not Program. A mismatch here returns undefined silently, not an error.
📞 Field Name: "Phone Number" The phone field on the Clients table is Phone Number (two words) in the API, not Phone. These naming inconsistencies are worth documenting — they're invisible until something breaks.
Data Flow

Page load to dashboard render.

From the moment the page loads to the moment charts appear, here's every step the data takes.

Browser — Four Parallel Fetches

On page load, JavaScript fires four simultaneous fetch() calls to the so_airtable_proxy AJAX endpoint — one per Airtable table. Promise.all() holds the render until all four resolve.

WordPress — Table Allowlist Check

The proxy plugin checks each requested table against its hardcoded allowlist. Permitted tables proceed; anything not on the list returns a 403. The API key is read from wp-config.php — never from the request.

PHP — Server-Side Airtable Request

wp_remote_get() calls api.airtable.com/v0/{base}/{table} with the Bearer token in the Authorization header. The full JSON response is passed back through the AJAX endpoint to the browser.

Browser — Aggregation & Render

Once all four datasets arrive, JavaScript runs a single render pass: counting records, building chart data structures, calculating percentages, and injecting HTML into every dashboard component simultaneously.

Filter Interaction — Local Only

When a filter changes, no new network request fires. JavaScript re-runs the aggregation and render functions against the already-fetched dataset in memory. Filters are instant because all the data is already in the browser.

Full Stack

Everything used to build this dashboard.

No charting libraries, no data processing frameworks. Every visualization is built from raw record counts and CSS — which means it loads fast, works everywhere, and requires no dependencies to maintain.

🐘 PHP 8 / WordPress Custom page template, SO Airtable Connector plugin proxy endpoint, wp_remote_get, table allowlist, API key in wp-config.php
🟨 Vanilla JavaScript (ES5+) Parallel Promise.all fetches, client-side aggregation, filter logic, DOM rendering, conic-gradient calculation — no libraries
🗄️ Airtable REST API v0 Four tables: Clients, Programs, Service Requests (ID only), Monthly Metrics (ID only). Bearer token auth server-side only.
🎨 Pure CSS All charts rendered with CSS — width percentages for bars, conic-gradient for donut, flexbox for layout. Zero canvas, zero SVG libraries.
🏠 GoDaddy / nginx Shared hosting, nginx server. Deployed via VS Code SFTP — Ctrl+S auto-uploads on every save.
Want This for Your Organization?

I can build a live dashboard from your data.

Whether your data lives in Airtable, Google Sheets, or a spreadsheet you've outgrown — this same pattern can surface it as a real-time dashboard your team actually uses. Book a free consult to talk through your setup.

Hiring or Partnering?

Need someone who turns raw data into clear insight?

I'm available for short-term projects and part-time engagements — dashboard builds, data pipeline design, and Airtable architecture without the overhead of a full-time hire.