Skip to content
Gravity Tables
tutorial

Build an analytics dashboard from your Gravity Forms entries

A 10-minute pattern for turning the data your Gravity Forms already collect into a chart-and-table dashboard. Bar charts, donut charts, totals, and a filtered detail table, all from the same form, no separate BI tool, no JavaScript framework.

· 8 min read · By Fahd Murtaza

The data is already in your forms. The chart most teams need is one bar chart and one summary table, not Looker, not Tableau, not a $200/mo BI subscription. The reason teams pay for those tools usually isn’t the analytics; it’s the convenience of “the data was already in this product”.

If your data is in Gravity Forms, the same convenience is available, and free with Pro. This guide ships the analytics dashboard pattern in three shortcodes and about ten minutes.

What you’ll build#

A /dashboard page that combines:

  • A summary stats strip, a few high-level numbers (total revenue, count, average) computed live from the entries
  • One or two charts, bar chart of revenue by category, donut chart of share-of-pipeline by status
  • A filtered detail table, the same data as a sortable, exportable table for “show me the underlying rows” questions

Same Gravity Form, three shortcodes, no separate BI tool.

Step 1: confirm your form has the right field types#

Charts read the same fields the table reads. For aggregations (sum, avg, min, max), the value column needs to be a numeric type (Number, Currency, Calculation). For grouping (group_by), the column can be any field type, text, dropdown, hidden status field.

Common dashboard-shaped forms:

  • Sales pipeline: deal value (currency), status (dropdown), owner (text), closed_at (date)
  • Order intake: total (currency), category (dropdown), customer_email (email)
  • Subscription billing: mrr (number), plan (dropdown), status (dropdown)
  • Event registration: ticket_type (dropdown), paid_amount (currency), status (dropdown)

If your form already has these fields, skip to Step 2. If a numeric field is currently a Single-line Text field, switch the type to Number or Currency in the form builder, the chart’s aggregation requires it.

Step 2: build the chart row#

A [gravity_chart] shortcode at the top of /dashboard, immediately under your hero or H1:

## Revenue by category

[gravity_chart id="orders"
  type="bar"
  group_by="category"
  aggregate="sum"
  value="total"
  height="320"
  filter="status:paid"]

Five parameters doing the work:

  • type="bar", vertical bar chart (donut also available; line is on the 4.3 roadmap)
  • group_by="category", one bar per unique value in the category field
  • aggregate="sum", bar height = sum of the value field per group. Other modes: count (default), avg, min, max.
  • value="total", which numeric column drives the aggregation
  • filter="status:paid", narrow the chart to a subset (only paid orders, not cancelled or pending). Same syntax as the table shortcode.

The output is inline SVG, server-rendered, with no JavaScript. The chart loads with the page, prints correctly, gets indexed by Google, and renders the same in Reader Mode.

Step 3: add a second chart for status share#

A donut chart next to (or below) the bar chart for a share-of-total view:

## Pipeline by status

[gravity_chart id="orders"
  type="donut"
  group_by="status"
  aggregate="sum"
  value="total"
  height="280"]

Donut shows each status as a colour-cycled wedge with a centred total in the middle. Hover tooltips show the per-segment value and percentage. The donut renders without filter, the entire pipeline is the point of a status-share view.

To put the bar and donut side by side on desktop, wrap them in a 2-column responsive grid (use your page builder’s columns, or a simple inline-flex if you write HTML directly):

<div class="dashboard-charts" style="display:grid; gap:2rem; grid-template-columns:repeat(2, minmax(0, 1fr));">
  <div>
    <h2>Revenue by category</h2>
    [gravity_chart id="orders" type="bar" group_by="category" aggregate="sum" value="total" filter="status:paid"]
  </div>
  <div>
    <h2>Pipeline by status</h2>
    [gravity_chart id="orders" type="donut" group_by="status" aggregate="sum" value="total"]
  </div>
</div>

(Adjust the markup for whichever page builder you’re using; the exact wrapper isn’t important, just that the two [gravity_chart] calls live in adjacent columns.)

Step 4: add the detail table#

Charts answer “what’s the shape of the data?” Tables answer “show me the rows behind that shape.” Drop the same form’s data as a sortable detail table below the charts:

## All orders

[gravity_table id="orders"
  columns="created,customer_email,category,total,status,owner"
  filters="category,status,owner"
  sort="created:desc"
  per_page="25"
  totals="total"
  allow_edit=""
  bulk="export"
  search_field="customer_email,category"]

The five things this table earns:

  • filters="category,status,owner", chip-style filters above the table for the same fields the charts group by. Customers narrowing by category see the chart and table both responding to the same filter intent.
  • sort="created:desc", newest orders float to the top. Most-recent context is most useful.
  • totals="total", a footer row sums the visible orders’ total field. As filters narrow the table, the totals recalculate. Use a chart for distribution; use the totals row for aggregate.
  • bulk="export", leadership wants the slice as Excel; one click, no manual download.
  • search_field restricts global search to two columns, keeps the search box focused, prevents matching against fields that aren’t useful (like internal notes).

For a /dashboard page that staff visit daily, this combo (charts + filtered table) gives both the bird’s-eye view and the on-the-ground detail in one URL.

Step 5: a top-line stats strip (optional, marketing-strong)#

For external-facing dashboards (e.g. a public progress board), a row of big numbers above the charts adds polish. Use multiple [gravity_chart] shortcodes with aggregate="count" or aggregate="sum" and no group_by:

[gravity_chart id="orders" aggregate="count" filter="status:paid" type="number" label="Paid orders this month"]
[gravity_chart id="orders" aggregate="sum" value="total" filter="status:paid" type="number" label="Revenue this month"]
[gravity_chart id="orders" aggregate="avg" value="total" filter="status:paid" type="number" label="Average order value"]

type="number" is the simplest “chart”, it renders as a single big number with the supplied label underneath. Three of them in a row read as a stat strip.

For time-window filters like “this month”, combine with the dynamic placeholder tokens (shipped 4.1.63):

filter="status:paid AND created:>{current_month_start}"

Where {current_month_start} is one of the placeholder tokens the renderer expands server-side.

Step 6: role gates for sensitive dashboards#

If your dashboard shows revenue or pipeline value, you probably don’t want it visible to logged-out users. Add allowed_roles to every chart and table shortcode on the page:

[gravity_chart id="orders" ... allowed_roles="finance,management,administrator"]
[gravity_table id="orders" ... allowed_roles="finance,management,administrator"]

The roles are checked on the server before any data is fetched, visitors without the role never see the data on the wire, not just in the rendered output. The fallback is configurable (the table can show “log in to view”, redirect, or render hidden).

For mixed-audience dashboards (a public stat strip + a staff-only detail table), gate the table only:

[gravity_chart id="orders" ...]                    <!-- public, no role gate -->
[gravity_table id="orders" ... allowed_roles="..."] <!-- staff-only -->

Step 7: auto-refresh for live ops dashboards#

Sales floors and event ops rooms benefit from a dashboard that refreshes itself. Charts and tables both support auto_refresh:

[gravity_chart id="orders" ... auto_refresh="true" refresh_interval="60"]
[gravity_table id="orders" ... auto_refresh="true" refresh_interval="60"]

Both poll the server on the same interval, picking up new entries as they’re submitted. The refresh_interval="60" (seconds) is balanced for “dashboard on a TV in the office”, frequent enough to feel live, infrequent enough that the network impact is negligible. Drop to 30 for a high-pace event; raise to 300 for a once-a-day check-in dashboard.

What this gives you#

For a small team running off a Gravity Form:

  • No BI tool subscription, replace Looker / Tableau / Geckoboard / Klipfolio with the plugin you already have
  • Same source-of-truth data, no syncing, no warehouse load, no stale-by-an-hour copy
  • Loads fast, server-rendered SVG charts, one DB query per shortcode, ETag-aware auto-refresh polling
  • Indexable, the chart’s data is in the rendered HTML, so a public /dashboard page actually shows up in Google with the data, not a “Loading…” placeholder
  • Honest scope, bar, donut, and number tiles only. No line charts, no scatter, no sankey. For complex visualisations, export to your tool of choice.

Recipe variations#

Sales pipeline dashboard#

Bar by owner (sum of value), donut by stage, table filtered to active deals. Add top_n_count="10" to the bar chart for a “top 10 reps” view (shipped v4.2.54).

Subscription metrics dashboard#

Bar by plan (sum of mrr), donut by status (active vs trial vs cancelled), totals row on mrr. Auto-refresh on a 5-minute interval for a near-live MRR display.

Event registration dashboard#

Bar by session_choice (count of attendees), donut by status (confirmed / checked-in / waitlist), per-session capacity warnings via the event-registration guide’s pattern.

Public progress board (non-profit / fundraising)#

Three number tiles up top (donations count, total raised, average donation), one bar chart by campaign, no table (donor data is private). Public-readable, no login required.

Don’ts#

Don’t put 4+ charts on one page#

Past 3, the page becomes “a wall of charts” that nobody actually reads. Pick the chart that answers the most-frequently-asked question; let the table cover the long tail.

Don’t filter the chart and table differently#

If [gravity_chart filter="status:paid"] and the table shows all statuses, visitors get conflicting narratives. Apply the same filter parameter to both, or be explicit in the heading (“Paid orders” vs. “All orders”) so the difference is intentional.

Don’t use aggregate="avg" on tiny groups#

Average of 1 is a meaningless data point. If most groups have fewer than 5 entries, aggregate="count" or aggregate="sum" reads more honestly. Or use top_n_count to focus on groups that have enough data to support an average.