anfra docs

Add interactions

Add a filter, a date drill and a cross-filter to a data app, one step at a time

This guide grows a two-chart app into an interactive one: a product filter, a week/month switch, and a bar chart that filters a trend line when clicked. Each step adds a few lines of declarations. None of them changes a query's AQL, because anfra rewrites each query from the reader's choices. See Semantic UI for how that works.

Explore interaction patterns

For ideas on which interactions to add, browse Interaction patterns for examples of how readers can explore data.

It uses the sales dataset from Model your data. Replace its names with your own from anfra show. Write the page as a file under apps/ and keep it open under anfra serve, as in Build a data app.

Build it with a coding agent

The anfra-build-data-app skill knows these steps. Name the interactions you want in your prompt:

Use the anfra-build-data-app skill. Show revenue over time and revenue by order status. Add a product filter for both charts and a week/month switch for the trend. Clicking a status should filter the trend.

1. Start with two queries

Declare the data each chart needs, subscribe a render function, and run the app. drawLine and drawBars stand for your own charting code, with any library:

apps/sales.html
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Sales</title>
</head>
<body>
  <div id="by-status"></div>
  <div id="trend"></div>

  <script>
    const app = Anfra.createApp({ title: 'Sales' })

    const trend = app.createQuery('trend', {
      dataset: 'sales',
      aql: `explore {
        dimensions { period: orders.created_at | month() }
        measures { revenue: revenue }
        sorts { period asc }
      }`,
    })

    const byStatus = app.createQuery('byStatus', {
      dataset: 'sales',
      aql: `explore {
        dimensions { status: orders.status }
        measures { revenue: revenue }
        sorts { revenue desc }
      }`,
    })

    function render () {
      if (trend.result) drawLine('#trend', trend.result.rows)          // [{ period, revenue }]
      if (byStatus.result) drawBars('#by-status', byStatus.result.rows) // [{ status, revenue }]
    }

    app.subscribe(render)
    app.execute()
  </script>
</body>
</html>

Name the time dimension period, not month. Step 3 lets the reader change its grain, and the row key stays the same whatever the grain.

2. Add a filter

Add a dropdown of products that filters both charts. Create a filter on the field, load its options into ordinary HTML, and set the filter's condition when the reader picks one:

<select id="product"><option value="">All products</option></select>
const product = app.createFilter('product', { dataset: 'sales', field: 'products.name' })

app.mapControl(product, trend, { field: 'products.name' })
app.mapControl(product, byStatus, { field: 'products.name' })

const productSelect = document.querySelector('#product')
product.loadOptions().then((options) => {
  productSelect.append(...options.map((value) => new Option(String(value), String(value))))
})
productSelect.addEventListener('change', () => {
  const value = productSelect.value
  product.setCondition({ operator: 'is', values: value ? [value] : [] })
  app.execute()
})
  • A filter only reaches the queries it is mapped to. Map it once per query. Leave a mapping out to keep a chart unfiltered.
  • The mapped field needn't be in the query. Neither chart shows the product, but both are filtered by it.
  • An empty values array removes the filter.

See Filters for the other operators, defaults and filters with fixed options.

3. Add a date drill

Let the reader switch the trend between weeks and months:

<select id="grain">
  <option value="week">Week</option>
  <option value="month" selected>Month</option>
</select>
const grain = app.createDateDrill('grain', { default: 'month' })
app.mapControl(grain, trend, { field: 'orders.created_at' })

document.querySelector('#grain').addEventListener('change', (event) => {
  grain.setGrain(event.target.value)
  app.execute()
})

Map the drill to the field, orders.created_at, never to the alias period. anfra finds the dimension built on that field and redraws it at the new grain. See Date drill.

4. Add a cross-filter

Make a click on a status bar filter the trend to that status:

app.mapCrossFilter(byStatus, trend)

// Your chart library's click handler: pass the clicked row from the result
onBarClick('#by-status', (index) => {
  byStatus.select([byStatus.result.rows[index]])
  app.execute()
})

Add a way to clear it:

<button id="clear" type="button">Clear selection</button>
document.querySelector('#clear').addEventListener('click', () => {
  app.clearSelection()
  app.execute()
})
  • Pass row objects from query.result.rows. The SDK reads the dimension values off them.
  • The source chart is never filtered by its own selection. Use byStatus.selectedRows in render() to highlight the selected bars instead.
  • Selections are per app. Selecting in another query replaces this one.

To let a click toggle several bars, or both charts filter each other, see Cross-filtering.

5. Show loading, errors and empty results

Each interaction re-runs the affected queries, so render() is now called while they load. Draw every state, not just the data:

function renderState (query, selector) {
  const el = document.querySelector(selector)
  if (query.state === 'error') el.textContent = `Couldn't load: ${query.error.message}`
  else if (query.state === 'success' && query.result.rows.length === 0) el.textContent = 'No sales for this selection'
  // While executing, query.result keeps the previous rows: draw them dimmed under a spinner
}

See Results and Errors.

6. Check what ran

Pick a product, switch to weeks and click a status. Then open the Inspect panel in anfra serve: it shows the AQL each query actually ran, with the filter, the grain and the selection applied, and the SQL generated from it. The same is on each result as query.result.debug. See Debug information.

This is how you verify an interactive app: every number on the page is a semantic query you can read, not code in the page.

Next steps

On this page