M
MetricUI
Guide

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:

  1. 1Give users a clean UI to select filters (period, comparison, dimensions)
  2. 2Store those selections in a React context
  3. 3Let you read the selections and pass them to your own data-fetching logic
  4. 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>
  );
}
PropTypeDescription
defaultPresetPeriodPresetInitial period preset (e.g. "30d", "quarter", "ytd")
childrenReactNodeYour 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:

ModeLogicExample
previousShifts the range backward by its own durationLast 30d → the 30 days before that
year-over-yearShifts the range back one yearMar 1-31, 2026 → Mar 1-31, 2025
noneNo 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
}
PropertyTypeDescription
selection{ field, value } | nullCurrent selection or null
isActivebooleanTrue when a selection is active
select(sel)(sel) => voidSet or toggle a selection
clear()() => voidClear the current selection

Behavior

Toggle:Clicking the same value again deselects it
Escape key:Press Escape to clear the selection from anywhere
Signal only:The crossFilter prop only emits clicks into context — it never changes the chart's data or appearance
No provider:Without a CrossFilterProvider, the crossFilter prop is silently ignored — safe to leave on
Stable colors:DonutChart remembers color assignments — filtering won't change a slice's color
Drill-down priority:When both drillDown and crossFilter are set on the same component, drillDown wins. The click opens the drill panel instead of emitting a cross-filter signal