shadimage
Mapcn
blockanalytics-mapsource: ok · refreshed

Analytics Map

Real-time analytics overview with a world map, breakdown cards, and device stats.

npx shadcn add "https://mapcn.dev/r/analytics-map.json"
free

Where it goes

stats

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
chartsrecharts
iconslucide-react

Registry dependencies

@mapcn/mapcardchart

Files4 · source cached 10/8/2026, 3:33:10 AM

src/registry/blocks/analytics-map/page.tsx
"use client";

import {
  Map,
  MapControls,
  MapGeoJSON,
  MapMarker,
  MarkerContent,
  MarkerTooltip,
} from "@/components/ui/map";
import { OverviewCard } from "./components/overview-card";
import { BreakdownCard } from "./components/breakdown-card";
import {
  locations,
  visitedPagesRows,
  countriesRows,
  referrersRows,
  browsersRows,
} from "./data";

const MAP_HEIGHT = "38rem";

// Country borders from a public CDN, or swap in your own GeoJSON.
const WORLD_GEOJSON =
  "https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@v5.1.2/geojson/ne_110m_admin_0_countries.geojson";

export default function Page() {
  return (
    <div
      className="bg-background relative min-h-screen"
      style={{ "--map-height": MAP_HEIGHT } as React.CSSProperties}
    >
      <div className="bg-card relative h-(--map-height)">
        <Map
          center={[-2, 16]}
          zoom={1.4}
          maxZoom={4}
          minZoom={1.4}
          scrollZoom={false}
          dragRotate={false}
          pitchWithRotate={false}
          renderWorldCopies
          blank
        >
          <MapGeoJSON data={WORLD_GEOJSON} linePaint={false} />
          <MapControls className="bottom-2" />
          {locations.map((location) => (
            <MapMarker
              key={location.city}
              longitude={location.lng}
              latitude={location.lat}
            >
              <MarkerContent className="group">
                <div
                  className="bg-chart-2/80 group-hover:bg-chart-2/90 rounded-full transition-[transform,background-color] group-hover:scale-110"
                  style={{
                    width: location.size * 3,
                    height: location.size * 3,
                  }}
                />
              </MarkerContent>
              <MarkerTooltip
                offset={20}
                className="bg-popover text-popover-foreground border"
              >
                <p className="font-medium">{location.city}</p>
                <p className="text-muted-foreground mt-0.5">
                  {location.size} active users
                </p>
              </MarkerTooltip>
            </MapMarker>
          ))}
        </Map>
        <div
          className="via-background/30 to-background pointer-events-none absolute inset-x-0 bottom-0 h-40 bg-linear-to-b from-transparent"
          aria-hidden
        />
        <OverviewCard />
      </div>

      <div className="grid gap-4 p-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
        <BreakdownCard title="Visited pages" rows={visitedPagesRows} />
        <BreakdownCard title="Referrers" rows={referrersRows} />
        <BreakdownCard title="Countries" rows={countriesRows} />
        <BreakdownCard title="Browsers" rows={browsersRows} />
      </div>
    </div>
  );
}
src/registry/blocks/analytics-map/data.ts
import { type ChartConfig } from "@/components/ui/chart";

export interface LocationPoint {
  city: string;
  lng: number;
  lat: number;
  size: number;
}

export interface BreakdownRow {
  label: string;
  value: number;
}

export const locations: LocationPoint[] = [
  { city: "San Francisco", lng: -122.4194, lat: 37.7749, size: 16 },
  { city: "New York", lng: -74.006, lat: 40.7128, size: 15 },
  { city: "Toronto", lng: -79.3832, lat: 43.6532, size: 11 },
  { city: "Mexico City", lng: -99.1332, lat: 19.4326, size: 10 },
  { city: "Sao Paulo", lng: -46.6333, lat: -23.5505, size: 12 },
  { city: "Buenos Aires", lng: -58.3816, lat: -34.6037, size: 9 },
  { city: "London", lng: -0.1276, lat: 51.5074, size: 14 },
  { city: "Berlin", lng: 13.405, lat: 52.52, size: 11 },
  { city: "Paris", lng: 2.3522, lat: 48.8566, size: 13 },
  { city: "Madrid", lng: -3.7038, lat: 40.4168, size: 10 },
  { city: "Cairo", lng: 31.2357, lat: 30.0444, size: 9 },
  { city: "Lagos", lng: 3.3792, lat: 6.5244, size: 10 },
  { city: "Mumbai", lng: 72.8777, lat: 19.076, size: 13 },
  { city: "Dubai", lng: 55.2708, lat: 25.2048, size: 11 },
  { city: "Seoul", lng: 126.978, lat: 37.5665, size: 12 },
  { city: "Singapore", lng: 103.8198, lat: 1.3521, size: 10 },
  { city: "Tokyo", lng: 139.6917, lat: 35.6895, size: 12 },
  { city: "Sydney", lng: 151.2093, lat: -33.8688, size: 9 },
  { city: "Auckland", lng: 174.7633, lat: -36.8485, size: 8 },
];

export const usersPerDay = [
  { day: "1", users: 420 },
  { day: "2", users: 468 },
  { day: "3", users: 512 },
  { day: "4", users: 390 },
  { day: "5", users: 540 },
  { day: "6", users: 305 },
  { day: "7", users: 268 },
  { day: "8", users: 498 },
  { day: "9", users: 610 },
  { day: "10", users: 472 },
  { day: "11", users: 845 },
  { day: "12", users: 690 },
  { day: "13", users: 360 },
  { day: "14", users: 322 },
  { day: "15", users: 588 },
  { day: "16", users: 654 },
  { day: "17", users: 720 },
  { day: "18", users: 505 },
  { day: "19", users: 932 },
  { day: "20", users: 410 },
  { day: "21", users: 372 },
  { day: "22", users: 640 },
  { day: "23", users: 712 },
  { day: "24", users: 598 },
  { day: "25", users: 1040 },
  { day: "26", users: 805 },
  { day: "27", users: 445 },
  { day: "28", users: 398 },
  { day: "29", users: 702 },
  { day: "30", users: 868 },
];

export const usersPerDayChartConfig = {
  users: {
    label: "Users",
    color: "var(--chart-2)",
  },
} satisfies ChartConfig;

export const deviceCategoryData = [
  { name: "Desktop", value: 73.3, fill: "var(--chart-1)" },
  { name: "Mobile", value: 25.0, fill: "var(--chart-2)" },
  { name: "Tablet", value: 1.7, fill: "var(--chart-3)" },
];

export const deviceCategoryChartConfig = {
  desktop: { label: "Desktop", color: "var(--chart-1)" },
  mobile: { label: "Mobile", color: "var(--chart-2)" },
  tablet: { label: "Tablet", color: "var(--chart-3)" },
} satisfies ChartConfig;

export const visitedPagesRows: BreakdownRow[] = [
  { label: "Home", value: 31 },
  { label: "Pricing", value: 23 },
  { label: "Docs / Basic Map", value: 18 },
  { label: "Installation", value: 12 },
  { label: "Components", value: 9 },
  { label: "Blog", value: 6 },
];

export const countriesRows: BreakdownRow[] = [
  { label: "United States", value: 27 },
  { label: "India", value: 14 },
  { label: "United Kingdom", value: 8 },
  { label: "Germany", value: 6 },
  { label: "Japan", value: 4 },
  { label: "Australia", value: 2 },
];

export const referrersRows: BreakdownRow[] = [
  { label: "google", value: 38 },
  { label: "direct", value: 26 },
  { label: "github.com", value: 19 },
  { label: "x.com", value: 11 },
  { label: "ui.shadcn.com", value: 8 },
  { label: "other", value: 5 },
];

export const browsersRows: BreakdownRow[] = [
  { label: "Chrome", value: 52 },
  { label: "Safari", value: 21 },
  { label: "Firefox", value: 14 },
  { label: "Edge", value: 8 },
  { label: "Other", value: 5 },
];
src/registry/blocks/analytics-map/components/overview-card.tsx
"use client";

import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { ChartContainer } from "@/components/ui/chart";
import { TrendingUp } from "lucide-react";
import { Area, AreaChart, Pie, PieChart } from "recharts";
import {
  deviceCategoryChartConfig,
  deviceCategoryData,
  usersPerDay,
  usersPerDayChartConfig,
} from "../data";

function MetricChart() {
  return (
    <ChartContainer
      config={usersPerDayChartConfig}
      className="aspect-auto h-14 w-full"
    >
      <AreaChart data={usersPerDay}>
        <defs>
          <linearGradient id="usersGradient" x1="0" y1="0" x2="0" y2="1">
            <stop
              offset="0%"
              stopColor="var(--color-users)"
              stopOpacity={0.4}
            />
            <stop
              offset="100%"
              stopColor="var(--color-users)"
              stopOpacity={0}
            />
          </linearGradient>
        </defs>

        <Area
          type="natural"
          dataKey="users"
          stroke="var(--color-users)"
          strokeWidth={1.5}
          fill="url(#usersGradient)"
        />
      </AreaChart>
    </ChartContainer>
  );
}

export function OverviewCard() {
  return (
    <Card className="bg-card/80 absolute top-4 left-4 z-10 w-60 gap-1 backdrop-blur-sm">
      <CardHeader>
        <div>
          <p className="text-muted-foreground pb-2 text-[10px] tracking-wider uppercase">
            Users in last 30 days
          </p>
          <p className="text-3xl leading-none font-semibold">17,234</p>
        </div>
      </CardHeader>

      <CardContent>
        <MetricChart />
        <div className="mt-2 flex items-center gap-1.5 text-xs">
          <TrendingUp className="text-foreground size-3" />
          <span className="text-foreground font-medium">+12.5%</span>
          <span className="text-muted-foreground">vs previous 30 days</span>
        </div>

        <div className="border-border/60 mt-4 border-t pt-4">
          <p className="text-muted-foreground text-[10px] tracking-wider uppercase">
            Device category in last 30 days
          </p>

          <ChartContainer
            config={deviceCategoryChartConfig}
            className="mx-auto mt-3 aspect-square h-32 w-32"
          >
            <PieChart>
              <Pie
                data={deviceCategoryData}
                dataKey="value"
                nameKey="name"
                innerRadius={32}
                outerRadius={52}
                strokeWidth={2}
              />
            </PieChart>
          </ChartContainer>

          <div className="mt-3 grid grid-cols-3 gap-2">
            {deviceCategoryData.map((device) => (
              <div key={device.name} className="text-center">
                <p className="text-muted-foreground flex items-center justify-center gap-1.5 text-[10px] tracking-wide uppercase">
                  <span
                    className="size-2 rounded-full"
                    style={{ backgroundColor: device.fill }}
                  />
                  {device.name}
                </p>
                <p className="text-foreground mt-1 leading-none font-medium tabular-nums">
                  {device.value}%
                </p>
              </div>
            ))}
          </div>
        </div>
      </CardContent>
    </Card>
  );
}
src/registry/blocks/analytics-map/components/breakdown-card.tsx
"use client";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { type BreakdownRow } from "../data";

interface BreakdownCardProps {
  title: string;
  rows: BreakdownRow[];
}

export function BreakdownCard({ title, rows }: BreakdownCardProps) {
  const maxRowValue =
    rows.length > 0 ? Math.max(...rows.map((row) => row.value)) : 0;

  return (
    <Card className="gap-2">
      <CardHeader>
        <CardTitle className="text-sm font-medium">{title}</CardTitle>
      </CardHeader>

      <CardContent>
        <div className="text-muted-foreground mb-2 flex items-center justify-between text-[11px] tracking-wider uppercase">
          <span>{title}</span>
          <span>Visitors</span>
        </div>
        <div className="space-y-1.5">
          {rows.map((row) => {
            const pct = maxRowValue ? (row.value / maxRowValue) * 100 : 0;
            return (
              <div
                key={row.label}
                className="relative flex items-center justify-between overflow-hidden rounded-md px-2 py-1.5 text-xs"
              >
                <div
                  className="bg-chart-2/20 absolute inset-y-0 left-0 rounded-md"
                  style={{ width: `${pct}%` }}
                  aria-hidden
                />
                <span className="text-foreground/90 relative truncate pr-2">
                  {row.label}
                </span>
                <span className="text-foreground relative font-medium tabular-nums">
                  {row.value.toLocaleString()}
                </span>
              </div>
            );
          })}
        </div>
      </CardContent>
    </Card>
  );
}

More from Mapcn

Browse all components in this library →