Your app becomes a BI tool: dashboards, SQL in the browser and natural-language questions
A Reactive app could already collect data, show it in views and charts, map it, run machine learning on it and let an AI reason over it. What was missing was the signature gesture of real business-intelligence tools: click a chart and watch everything else filter itself. And, for those who want to go further, a real query engine. Both arrive today — plus six more features that complete the picture. As always: no server, no account, the data never leaves the device.
Cross-filtering: :::dashboard
Wrap charts and views on the same collection in one container:
::::dashboard{path="sales"}
::chart-bar{data="sales" x="region" y="amount"}
:::table{path="sales" headers="Region,Channel,Amount"}
{region} | {channel} | {amount}
:::
::::
Click a bar: the table narrows to that region, a chip at the top shows the active filter, and the chart highlights the selection by dimming the rest — without filtering itself, because the context must stay visible (the same choice Tableau and Power BI make). Click again: filter cleared. It works with tables, lists, cards, derived views and maps, and every chart in the dashboard is a different filter: one by region, one by channel.
Real SQL, in the browser: :::sql
For the analyses Markdown cannot express there is now a full SQL engine — DuckDB compiled to WebAssembly — running entirely in the browser. The app’s collections become tables, and you write SQL:
:::sql{data="clients,orders" into="revenue"}
```sql
SELECT c.name, sum(o.amount) AS total
FROM orders o JOIN clients c ON c.name = o.client
GROUP BY 1 ORDER BY 2 DESC
```
:::
::chart-bar{data="revenue" x="name" y="total"}
JOINs across collections, GROUP BY, window functions: the result is a
collection like any other, so it goes straight into a table, a chart, a map.
{#key} placeholders are reactive parameters — a slider that re-runs the
query — and read_parquet('https://…') reads remote files in place:
worldwide open data published as Parquet or CSV enters the app with no
middleman service. Even attachments uploaded with ::file can be queried
with read_csv('sales.csv'). The engine (~10 MB) downloads once, on the
first click of Run.
Ask your data: ::ai-query
The most requested feature of modern BI, in one line:
::ai-query{data="expenses" into="answer"}
“How much did I spend on transport in June?”, “top 5 clients by revenue”: the
model — in the browser, on Ollama or via API — neither sees the whole
collection nor writes free-form code. It produces a constrained query
plan (the allowed fields are a closed list: it cannot invent columns)
executed locally. And if the app also uses :::sql, it upgrades itself: the
question becomes a real DuckDB SELECT — joins, dates, expressions — shown in
the widget for transparency, falling back to the plan automatically if
anything goes wrong.
Open data beyond Italy: ::api-query
Reactive’s data service covers Italy; the new REST connector covers the rest of the world. Exchange rates, weather, stocks, Eurostat — any public JSON API lands in a collection:
::input[base]{value="EUR"}
::api-query{url="https://api.frankfurter.dev/v1/latest?base={#base}" into="rates" pick="rates" as="pairs"}
::chart-bar{data="rates" x="key" y="value"}
Change the base currency and the call re-runs by itself. pick descends into
the JSON, and the common shapes become rows with no glue: even Open-Meteo’s
columnar format zips itself into a time series ready for a chart.
From addresses to the map: ::geocode
Business data — clients, branches, points of sale — has addresses, not GPS. Now it takes one button:
::geocode{path="clients" from="address" to="coords"}
:::map{path="clients" coords="coords"}
**{name}** — {address}
:::
It only geocodes the rows that don’t have coordinates yet (a second click
picks up where it left off), respects the Nominatim policy — one request per
second, never automatic — and url= points to a self-hosted instance,
consistent with the privacy-first stance.
Forecasts, rankings and percentiles
Three bricks that complete the analytical side:
::ml-forecast— the time-series forecast that was missing next toml-predict: a trend over income by year, population, accumulated prices. It writes the history with its fit plus the future rows, so::chart-line{y="value,previsione"}shows the series and its extension together.:rankand:percentile— benchmarking in one line: “this town is at the 12th percentile for income”, “3rd of 42”. Reactive twice: on the group’s data and on the compared value.
Everything composes
The strength is not in the eight features taken one by one, but in the fact
that they all speak the same language: a collection. api-query fetches the
exchange rates, :::sql joins them with your orders, the result lands in a
:::dashboard where one click filters the table, and ::ai-query answers
your questions — while :rank tells you where you stand. Each piece is one
line of Markdown, and the whole app remains a text file you share with a
link.
Try it now: open the app or browse the catalog — the open data apps are already being updated with dashboards, benchmarks and forecasts.