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 · mediumchartsrecharts
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>
);
}