Skip to content

Analytics Dashboard Examples

Real-world examples of filtering for analytics and reporting.

User Analytics

User Activity Dashboard

typescript
import { useFilter, useDebouncedFilter } from '@mcabreradev/filter/react';

interface UserActivity {
  userId: number;
  userName: string;
  lastActive: Date;
  sessionsCount: number;
  pageViews: number;
  avgSessionDuration: number;
  country: string;
  device: 'desktop' | 'mobile' | 'tablet';
  browser: string;
}

const UserActivityDashboard = ({ activities }: { activities: UserActivity[] }) => {
  const [dateRange, setDateRange] = useState<[Date, Date]>([
    new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
    new Date()
  ]);
  const [minSessions, setMinSessions] = useState(0);
  const [selectedCountries, setSelectedCountries] = useState<string[]>([]);
  const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
  const [searchTerm, setSearchTerm] = useState('');

  const baseExpression = useMemo(() => {
    const conditions = [];

    conditions.push({
      $and: [
        { lastActive: { $gte: dateRange[0] } },
        { lastActive: { $lte: dateRange[1] } }
      ]
    });

    if (minSessions > 0) {
      conditions.push({ sessionsCount: { $gte: minSessions } });
    }

    if (selectedCountries.length > 0) {
      conditions.push({ country: { $in: selectedCountries } });
    }

    if (selectedDevices.length > 0) {
      conditions.push({ device: { $in: selectedDevices } });
    }

    return conditions.length > 0 ? { $and: conditions } : {};
  }, [dateRange, minSessions, selectedCountries, selectedDevices]);

  const { filtered: baseFiltered } = useFilter(activities, baseExpression, {
    memoize: true
  });

  const searchExpression = useMemo(() => ({
    userName: { $regex: new RegExp(searchTerm, 'i') }
  }), [searchTerm]);

  const { filtered, isPending } = useDebouncedFilter(
    baseFiltered,
    searchTerm ? searchExpression : {},
    { delay: 300 }
  );

  const stats = useMemo(() => ({
    totalUsers: filtered.length,
    totalSessions: filtered.reduce((sum, a) => sum + a.sessionsCount, 0),
    totalPageViews: filtered.reduce((sum, a) => sum + a.pageViews, 0),
    avgSessionDuration: filtered.length > 0
      ? filtered.reduce((sum, a) => sum + a.avgSessionDuration, 0) / filtered.length
      : 0
  }), [filtered]);

  return (
    <div>
      <div className="grid grid-cols-4 gap-4 mb-6">
        <MetricCard title="Total Users" value={stats.totalUsers} />
        <MetricCard title="Total Sessions" value={stats.totalSessions} />
        <MetricCard title="Page Views" value={stats.totalPageViews} />
        <MetricCard
          title="Avg Session Duration"
          value={`${Math.round(stats.avgSessionDuration)}s`}
        />
      </div>

      <div className="mb-6 space-y-4">
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Search users..."
        />
        {isPending && <Spinner size="sm" />}

        <DateRangePicker value={dateRange} onChange={setDateRange} />

        <input
          type="number"
          value={minSessions}
          onChange={(e) => setMinSessions(Number(e.target.value))}
          placeholder="Minimum sessions"
        />

        <CountryMultiSelect
          selected={selectedCountries}
          onChange={setSelectedCountries}
        />

        <DeviceMultiSelect
          selected={selectedDevices}
          onChange={setSelectedDevices}
        />
      </div>

      <UserActivityTable activities={filtered} />
    </div>
  );
};

Sales Analytics

Sales Performance Dashboard

typescript
interface SaleRecord {
  id: number;
  date: Date;
  amount: number;
  product: string;
  category: string;
  region: string;
  salesPerson: string;
  status: 'completed' | 'pending' | 'refunded';
}

const SalesAnalyticsDashboard = ({ sales }: { sales: SaleRecord[] }) => {
  const [period, setPeriod] = useState<'today' | 'week' | 'month' | 'year'>('month');
  const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
  const [selectedRegions, setSelectedRegions] = useState<string[]>([]);
  const [minAmount, setMinAmount] = useState(0);
  const [status, setStatus] = useState<string>('');

  const expression = useMemo(() => {
    const conditions = [];

    const now = new Date();
    let startDate: Date;

    switch (period) {
      case 'today':
        startDate = new Date(now.setHours(0, 0, 0, 0));
        break;
      case 'week':
        startDate = new Date(now.setDate(now.getDate() - 7));
        break;
      case 'month':
        startDate = new Date(now.setMonth(now.getMonth() - 1));
        break;
      case 'year':
        startDate = new Date(now.setFullYear(now.getFullYear() - 1));
        break;
    }

    conditions.push({ date: { $gte: startDate } });

    if (selectedCategories.length > 0) {
      conditions.push({ category: { $in: selectedCategories } });
    }

    if (selectedRegions.length > 0) {
      conditions.push({ region: { $in: selectedRegions } });
    }

    if (minAmount > 0) {
      conditions.push({ amount: { $gte: minAmount } });
    }

    if (status) {
      conditions.push({ status: { $eq: status } });
    }

    return { $and: conditions };
  }, [period, selectedCategories, selectedRegions, minAmount, status]);

  const { filtered } = useFilter(sales, expression, {
    memoize: true
  });

  const analytics = useMemo(() => {
    const totalRevenue = filtered.reduce((sum, s) => sum + s.amount, 0);
    const avgOrderValue = filtered.length > 0 ? totalRevenue / filtered.length : 0;

    const categoryBreakdown = filtered.reduce((acc, s) => {
      acc[s.category] = (acc[s.category] || 0) + s.amount;
      return acc;
    }, {} as Record<string, number>);

    const regionBreakdown = filtered.reduce((acc, s) => {
      acc[s.region] = (acc[s.region] || 0) + s.amount;
      return acc;
    }, {} as Record<string, number>);

    return {
      totalSales: filtered.length,
      totalRevenue,
      avgOrderValue,
      categoryBreakdown,
      regionBreakdown
    };
  }, [filtered]);

  return (
    <div>
      <div className="grid grid-cols-3 gap-4 mb-6">
        <MetricCard
          title="Total Sales"
          value={analytics.totalSales}
        />
        <MetricCard
          title="Total Revenue"
          value={`$${analytics.totalRevenue.toLocaleString()}`}
        />
        <MetricCard
          title="Avg Order Value"
          value={`$${analytics.avgOrderValue.toFixed(2)}`}
        />
      </div>

      <div className="grid grid-cols-2 gap-6 mb-6">
        <ChartCard title="Revenue by Category">
          <BarChart data={analytics.categoryBreakdown} />
        </ChartCard>
        <ChartCard title="Revenue by Region">
          <PieChart data={analytics.regionBreakdown} />
        </ChartCard>
      </div>

      <div className="mb-6 space-y-4">
        <PeriodSelector value={period} onChange={setPeriod} />

        <CategoryMultiSelect
          selected={selectedCategories}
          onChange={setSelectedCategories}
        />

        <RegionMultiSelect
          selected={selectedRegions}
          onChange={setSelectedRegions}
        />

        <input
          type="number"
          value={minAmount}
          onChange={(e) => setMinAmount(Number(e.target.value))}
          placeholder="Minimum amount"
        />

        <select value={status} onChange={(e) => setStatus(e.target.value)}>
          <option value="">All Statuses</option>
          <option value="completed">Completed</option>
          <option value="pending">Pending</option>
          <option value="refunded">Refunded</option>
        </select>
      </div>

      <SalesTable sales={filtered} />
    </div>
  );
};

Event Analytics

Event Tracking Dashboard

typescript
interface Event {
  id: number;
  name: string;
  category: string;
  timestamp: Date;
  userId: number;
  properties: Record<string, any>;
  duration?: number;
  value?: number;
}

const EventAnalyticsDashboard = ({ events }: { events: Event[] }) => {
  const [eventName, setEventName] = useState('');
  const [category, setCategory] = useState('');
  const [dateRange, setDateRange] = useState<[Date, Date]>([
    new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
    new Date()
  ]);
  const [minDuration, setMinDuration] = useState(0);
  const [hasValue, setHasValue] = useState(false);

  const expression = useMemo(() => {
    const conditions = [];

    if (eventName) {
      conditions.push({
        name: { $regex: new RegExp(eventName, 'i') }
      });
    }

    if (category) {
      conditions.push({ category: { $eq: category } });
    }

    conditions.push({
      $and: [
        { timestamp: { $gte: dateRange[0] } },
        { timestamp: { $lte: dateRange[1] } }
      ]
    });

    if (minDuration > 0) {
      conditions.push({
        duration: { $gte: minDuration }
      });
    }

    if (hasValue) {
      conditions.push({
        value: { $ne: null }
      });
    }

    return conditions.length > 0 ? { $and: conditions } : {};
  }, [eventName, category, dateRange, minDuration, hasValue]);

  const { filtered } = useDebouncedFilter(events, expression, {
    delay: 300,
    memoize: true
  });

  const metrics = useMemo(() => {
    const totalEvents = filtered.length;
    const uniqueUsers = new Set(filtered.map(e => e.userId)).size;
    const avgDuration = filtered.filter(e => e.duration).length > 0
      ? filtered.reduce((sum, e) => sum + (e.duration || 0), 0) /
        filtered.filter(e => e.duration).length
      : 0;
    const totalValue = filtered.reduce((sum, e) => sum + (e.value || 0), 0);

    const eventsByCategory = filtered.reduce((acc, e) => {
      acc[e.category] = (acc[e.category] || 0) + 1;
      return acc;
    }, {} as Record<string, number>);

    const topEvents = Object.entries(
      filtered.reduce((acc, e) => {
        acc[e.name] = (acc[e.name] || 0) + 1;
        return acc;
      }, {} as Record<string, number>)
    )
      .sort(([, a], [, b]) => b - a)
      .slice(0, 10);

    return {
      totalEvents,
      uniqueUsers,
      avgDuration,
      totalValue,
      eventsByCategory,
      topEvents
    };
  }, [filtered]);

  return (
    <div>
      <div className="grid grid-cols-4 gap-4 mb-6">
        <MetricCard title="Total Events" value={metrics.totalEvents} />
        <MetricCard title="Unique Users" value={metrics.uniqueUsers} />
        <MetricCard
          title="Avg Duration"
          value={`${metrics.avgDuration.toFixed(2)}s`}
        />
        <MetricCard
          title="Total Value"
          value={`$${metrics.totalValue.toLocaleString()}`}
        />
      </div>

      <div className="grid grid-cols-2 gap-6 mb-6">
        <ChartCard title="Events by Category">
          <DonutChart data={metrics.eventsByCategory} />
        </ChartCard>
        <ChartCard title="Top 10 Events">
          <HorizontalBarChart data={Object.fromEntries(metrics.topEvents)} />
        </ChartCard>
      </div>

      <div className="mb-6 space-y-4">
        <input
          type="text"
          value={eventName}
          onChange={(e) => setEventName(e.target.value)}
          placeholder="Search events..."
        />

        <select value={category} onChange={(e) => setCategory(e.target.value)}>
          <option value="">All Categories</option>
          <option value="user_action">User Action</option>
          <option value="system">System</option>
          <option value="error">Error</option>
        </select>

        <DateRangePicker value={dateRange} onChange={setDateRange} />

        <input
          type="number"
          value={minDuration}
          onChange={(e) => setMinDuration(Number(e.target.value))}
          placeholder="Minimum duration (seconds)"
        />

        <label>
          <input
            type="checkbox"
            checked={hasValue}
            onChange={(e) => setHasValue(e.target.checked)}
          />
          Has monetary value
        </label>
      </div>

      <EventTable events={filtered} />
    </div>
  );
};

Funnel Analysis

Conversion Funnel

typescript
interface FunnelStep {
  userId: number;
  step: 'landing' | 'signup' | 'activation' | 'purchase';
  timestamp: Date;
  metadata: Record<string, any>;
}

const ConversionFunnel = ({ steps }: { steps: FunnelStep[] }) => {
  const [dateRange, setDateRange] = useState<[Date, Date]>([
    new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
    new Date()
  ]);
  const [source, setSource] = useState('');

  const expression = useMemo(() => ({
    $and: [
      { timestamp: { $gte: dateRange[0] } },
      { timestamp: { $lte: dateRange[1] } },
      source && { 'metadata.source': { $eq: source } }
    ].filter(Boolean)
  }), [dateRange, source]);

  const { filtered } = useFilter(steps, expression, {
    memoize: true
  });

  const funnelData = useMemo(() => {
    const usersByStep = {
      landing: new Set<number>(),
      signup: new Set<number>(),
      activation: new Set<number>(),
      purchase: new Set<number>()
    };

    filtered.forEach(step => {
      usersByStep[step.step].add(step.userId);
    });

    const counts = {
      landing: usersByStep.landing.size,
      signup: usersByStep.signup.size,
      activation: usersByStep.activation.size,
      purchase: usersByStep.purchase.size
    };

    const conversionRates = {
      signupRate: counts.landing > 0
        ? (counts.signup / counts.landing) * 100
        : 0,
      activationRate: counts.signup > 0
        ? (counts.activation / counts.signup) * 100
        : 0,
      purchaseRate: counts.activation > 0
        ? (counts.purchase / counts.activation) * 100
        : 0,
      overallRate: counts.landing > 0
        ? (counts.purchase / counts.landing) * 100
        : 0
    };

    return { counts, conversionRates };
  }, [filtered]);

  return (
    <div>
      <div className="mb-6 space-y-4">
        <DateRangePicker value={dateRange} onChange={setDateRange} />

        <select value={source} onChange={(e) => setSource(e.target.value)}>
          <option value="">All Sources</option>
          <option value="organic">Organic</option>
          <option value="paid">Paid</option>
          <option value="referral">Referral</option>
        </select>
      </div>

      <div className="space-y-4">
        <FunnelStepCard
          title="Landing Page"
          count={funnelData.counts.landing}
          percentage={100}
        />
        <FunnelStepCard
          title="Sign Up"
          count={funnelData.counts.signup}
          percentage={funnelData.conversionRates.signupRate}
        />
        <FunnelStepCard
          title="Activation"
          count={funnelData.counts.activation}
          percentage={funnelData.conversionRates.activationRate}
        />
        <FunnelStepCard
          title="Purchase"
          count={funnelData.counts.purchase}
          percentage={funnelData.conversionRates.purchaseRate}
        />
      </div>

      <div className="mt-6">
        <MetricCard
          title="Overall Conversion Rate"
          value={`${funnelData.conversionRates.overallRate.toFixed(2)}%`}
          size="large"
        />
      </div>
    </div>
  );
};

Performance Monitoring

Performance Metrics Dashboard

typescript
interface PerformanceMetric {
  id: number;
  timestamp: Date;
  page: string;
  metric: 'FCP' | 'LCP' | 'FID' | 'CLS' | 'TTFB';
  value: number;
  deviceType: 'desktop' | 'mobile' | 'tablet';
  connection: '4g' | '3g' | 'slow-2g' | 'wifi';
}

const PerformanceMonitoring = ({ metrics }: { metrics: PerformanceMetric[] }) => {
  const [selectedMetric, setSelectedMetric] = useState<string>('');
  const [selectedPage, setSelectedPage] = useState('');
  const [deviceType, setDeviceType] = useState('');
  const [dateRange, setDateRange] = useState<[Date, Date]>([
    new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
    new Date()
  ]);

  const expression = useMemo(() => {
    const conditions = [];

    conditions.push({
      $and: [
        { timestamp: { $gte: dateRange[0] } },
        { timestamp: { $lte: dateRange[1] } }
      ]
    });

    if (selectedMetric) {
      conditions.push({ metric: { $eq: selectedMetric } });
    }

    if (selectedPage) {
      conditions.push({ page: { $eq: selectedPage } });
    }

    if (deviceType) {
      conditions.push({ deviceType: { $eq: deviceType } });
    }

    return { $and: conditions };
  }, [selectedMetric, selectedPage, deviceType, dateRange]);

  const { filtered } = useFilter(metrics, expression, {
    memoize: true
  });

  const stats = useMemo(() => {
    const avgByMetric = filtered.reduce((acc, m) => {
      if (!acc[m.metric]) {
        acc[m.metric] = { sum: 0, count: 0 };
      }
      acc[m.metric].sum += m.value;
      acc[m.metric].count += 1;
      return acc;
    }, {} as Record<string, { sum: number; count: number }>);

    return Object.entries(avgByMetric).reduce((acc, [metric, data]) => {
      acc[metric] = data.sum / data.count;
      return acc;
    }, {} as Record<string, number>);
  }, [filtered]);

  return (
    <div>
      <div className="grid grid-cols-5 gap-4 mb-6">
        <MetricCard title="FCP" value={`${stats.FCP?.toFixed(0) || 0}ms`} />
        <MetricCard title="LCP" value={`${stats.LCP?.toFixed(0) || 0}ms`} />
        <MetricCard title="FID" value={`${stats.FID?.toFixed(0) || 0}ms`} />
        <MetricCard title="CLS" value={stats.CLS?.toFixed(3) || 0} />
        <MetricCard title="TTFB" value={`${stats.TTFB?.toFixed(0) || 0}ms`} />
      </div>

      <div className="mb-6 space-y-4">
        <DateRangePicker value={dateRange} onChange={setDateRange} />

        <select
          value={selectedMetric}
          onChange={(e) => setSelectedMetric(e.target.value)}
        >
          <option value="">All Metrics</option>
          <option value="FCP">First Contentful Paint</option>
          <option value="LCP">Largest Contentful Paint</option>
          <option value="FID">First Input Delay</option>
          <option value="CLS">Cumulative Layout Shift</option>
          <option value="TTFB">Time to First Byte</option>
        </select>

        <select
          value={selectedPage}
          onChange={(e) => setSelectedPage(e.target.value)}
        >
          <option value="">All Pages</option>
          <option value="/">Home</option>
          <option value="/products">Products</option>
          <option value="/checkout">Checkout</option>
        </select>

        <select
          value={deviceType}
          onChange={(e) => setDeviceType(e.target.value)}
        >
          <option value="">All Devices</option>
          <option value="desktop">Desktop</option>
          <option value="mobile">Mobile</option>
          <option value="tablet">Tablet</option>
        </select>
      </div>

      <PerformanceChart data={filtered} />
    </div>
  );
};

Released under the MIT License.