Filtering
MetricUI filter components are pure UI. They capture what the user selected — a date range, a comparison mode, a dimension value — and expose it via context. They never touch, fetch, or transform your data.
Philosophy
Most dashboard frameworks try to own your data pipeline — fetching, caching, filtering, and re-rendering. MetricUI doesn't. Your data comes from your API, your database, your state management. MetricUI's job is to:
- 1Give users a clean UI to select filters (period, comparison, dimensions)
- 2Store those selections in a React context
- 3Let you read the selections and pass them to your own data-fetching logic
- 4Render whatever data you give it
This means MetricUI works with any backend, any data layer, any state manager. REST, GraphQL, tRPC, Supabase, raw SQL — doesn't matter. The filter components are a UI layer, not a data layer.
Architecture
FilterProvider ← Holds filter state (period, comparison, dimensions)
├── PeriodSelector ← UI: user picks a date range
├── YourDashboard ← Reads filters via useMetricFilters()
│ ├── KpiCard ← You pass data based on the active period
│ ├── AreaChart ← You pass data based on the active period
│ └── DataTable ← You pass data based on the active period
└── (future filters) ← Dimension filters, search, etc.The arrow of data flows one way: user selects → context updates → you fetch → components render. MetricUI never fetches for you.
FilterProvider
Wrap your dashboard in a FilterProviderto enable filter state. It's separate from MetricProvider — MetricProvider handles theming/formatting, FilterProvider handles filter state.
import { MetricProvider, FilterProvider, PeriodSelector } from "metricui";
function App() {
return (
<MetricProvider theme="indigo">
<FilterProvider defaultPreset="30d">
<PeriodSelector comparison />
<Dashboard />
</FilterProvider>
</MetricProvider>
);
}| Prop | Type | Description |
|---|---|---|
defaultPreset | PeriodPreset | Initial period preset (e.g. "30d", "quarter", "ytd") |
children | ReactNode | Your dashboard content |
Reading Filters
Call useMetricFilters() from any component inside a FilterProvider to read the current filter state.
import { useMetricFilters } from "metricui";
function Dashboard() {
const filters = useMetricFilters();
// filters.period → { start: Date, end: Date } | null
// filters.preset → "30d" | "quarter" | null (null = custom range)
// filters.comparisonMode → "none" | "previous" | "year-over-year"
// filters.comparisonPeriod → { start: Date, end: Date } | null
// filters.dimensions → Record<string, string[]>
// Actions:
// filters.setPeriod(range, preset?)
// filters.setPreset("7d")
// filters.setCustomRange(startDate, endDate)
// filters.setComparisonMode("previous")
// filters.setDimension("region", ["US", "EU"])
// filters.clearDimension("region")
// filters.clearAll()
}Wiring to Your Data
This is where you connect MetricUI filters to your data layer. Read the active period from the context, pass it to your fetch, render the result:
import { useMetricFilters, KpiCard, AreaChart } from "metricui";
import useSWR from "swr"; // or React Query, or fetch, or anything
function Dashboard() {
const filters = useMetricFilters();
const period = filters?.period;
// Your data fetching — MetricUI doesn't care how you do it
const { data, isLoading, error } = useSWR(
period ? `/api/metrics?start=${period.start.toISOString()}&end=${period.end.toISOString()}` : null
);
return (
<>
<KpiCard
title="Revenue"
value={data?.revenue ?? 0}
format="currency"
loading={isLoading}
error={error ? { message: error.message } : undefined}
/>
<AreaChart
data={data?.revenueOverTime ?? []}
title="Revenue Over Time"
format="currency"
loading={isLoading}
/>
</>
);
}Notice: MetricUI components receive the resultof your fetch. They don't know about your API, your database, or your caching strategy. The filter context is just a React state that tells you what the user wants to see.
Comparison Periods
When the user enables comparison mode (via PeriodSelector's comparison toggle), the context auto-computes a comparison period:
| Mode | Logic | Example |
|---|---|---|
previous | Shifts the range backward by its own duration | Last 30d → the 30 days before that |
year-over-year | Shifts the range back one year | Mar 1-31, 2026 → Mar 1-31, 2025 |
none | No comparison | — |
const filters = useMetricFilters();
if (filters?.comparisonPeriod) {
// Fetch comparison data too
const compData = await fetch(
`/api/metrics?start=${filters.comparisonPeriod.start.toISOString()}&end=${filters.comparisonPeriod.end.toISOString()}`
);
}Dimension Filters
FilterProvider also holds dimension filters (region, plan, status, etc.) as a key-value map. Set them programmatically — UI components for dimension filtering are coming soon.
const filters = useMetricFilters();
// Set a dimension filter
filters?.setDimension("region", ["US", "EU"]);
// Read it
filters?.dimensions.region // → ["US", "EU"]
// Clear it
filters?.clearDimension("region");
// Clear everything
filters?.clearAll();Without a Provider
FilterProvider is optional. PeriodSelector works standalone with an onChangecallback for simple cases where you don't need shared context:
<PeriodSelector
onChange={(period, preset) => {
// period.start, period.end — use these to fetch
}}
/>Use FilterProvider when multiple components need to read the same filter state. Use onChange when you just need a date picker in one spot.
Cross-Filtering
Cross-filtering captures click selections from charts and tables into a shared context. When a user clicks “Chrome” in a bar chart, the selection is stored — your code reads it, filters your data, and the charts re-render with the filtered data. Same philosophy as FilterProvider: MetricUI handles the signal, you handle the data.
Setup
Three pieces: a provider, the crossFilter prop on clickable components, and your data filtering logic:
import {
CrossFilterProvider,
useCrossFilteredData,
BarChart,
DonutChart,
} from "metricui";
function App() {
return (
<CrossFilterProvider>
<Dashboard />
</CrossFilterProvider>
);
}
function Dashboard() {
// One line — filters data when a selection is active
const chartData = useCrossFilteredData(allData, "browser");
return (
<>
{/* Source chart: keeps full data so user sees what they clicked */}
<BarChart
data={allData}
index="browser"
categories={["visitors"]}
crossFilter
/>
{/* Sibling charts: get filtered data */}
<DonutChart
data={chartData}
index="browser"
categories={["revenue"]}
crossFilter
/>
</>
);
}The crossFilter prop tells a component to call crossFilter.select() when clicked. useCrossFilteredData reads the selection and returns filtered data — one line per chart. For advanced multi-filter scenarios, use useCrossFilter() directly.
Reading Cross-Filter State
Use useCrossFilter() from any component inside a CrossFilterProvider:
import { useCrossFilter } from "metricui";
function Dashboard() {
const cf = useCrossFilter();
// cf.isActive → true when something is selected
// cf.selection.field → "browser"
// cf.selection.value → "Chrome"
// cf.clear() → deselect
// cf.select(...) → programmatic selection
}| Property | Type | Description |
|---|---|---|
selection | { field, value } | null | Current selection or null |
isActive | boolean | True when a selection is active |
select(sel) | (sel) => void | Set or toggle a selection |
clear() | () => void | Clear the current selection |