Domphy

dottedMap

A Core block/component from Magic UI — clean-room reimplemented for Domphy (see methodology). Call dottedMap() with no arguments for a working demo, or edit the code below live.

Implementation notes

Fully implements the described architecture: a pre-computed (non-animated) equirectangular grid of SVG dots forming a stippled world silhouette, plus a marker layer supporting per-marker color/pulse overrides, a repeating CSS @keyframes radar-ping ring, and a custom overlay renderer (via SVG <foreignObject> + CSS clip-path circle) for e.g. avatar images — all exactly as specified, with zero stubs. The one deliberate fidelity gap, called out because it affects the VISUAL result: land/water classification uses a small, hand-authored set of ellipse approximations per continent (with a per-point pseudo-random jitter so edges look stippled rather than geometrically smooth) instead of a real geographic point-set or polygon containment test — this is the exact substitution the spec's own researchNote sanctions ('a clean-room build can substitute any lightweight equirectangular projection plus a pre-baked landmass point set... to avoid heavy geo-boundary dependencies'), but it means continent silhouettes are stylized/decorative rather than coastline-accurate. Default grid density (80 columns / 40 rows ≈ 3200 samples) is intentionally lower than the spec's illustrative ~5000-sample default, traded off for render/test performance — both counts are explicitly framed by the spec as non-normative starting points, not requirements.

Status: ported · Reference: Magic UI original

// magicui "Dotted Map" — clean-room reimplementation from the public
// behavior/visual spec only (no upstream source viewed or copied). An SVG
// world map rendered as a grid of small dots tracing continent silhouettes,
// with optional pulsing marker overlays at specific latitude/longitude
// locations.
//
// The upstream technique tests every sampled grid point against real
// geographic polygon boundaries, which needs a geo-boundary dependency this
// package intentionally avoids (per the port's own research note). Instead,
// each landmass is approximated as one or more ellipses in
// (longitude, latitude) space, with a small per-point pseudo-random jitter
// on the ellipse boundary so coastlines read as a stippled fringe instead of
// a perfectly smooth curve. This is a stylized, hand-authored approximation
// suitable for a decorative/illustrative backdrop — not a geographic dataset.

import type { DomphyElement, Listener, StyleObject } from "@domphy/core";
import { hashString } from "@domphy/core";
import { type ThemeColor, themeColor } from "@domphy/theme";

export interface DottedMapMarker {
  latitude: number;
  longitude: number;
  /** Accessible label announced for this marker (also used as the visible label when no `renderOverlay`). */
  label?: string;
  /** Marker color. Defaults to the map's `markerColor`. */
  color?: ThemeColor;
  /** Overrides the map's global `pulse` setting for just this marker. */
  pulse?: boolean;
  /** Renders custom overlay content (e.g. an avatar image) anchored at the marker's projected position, circular-clipped, instead of the default dot. */
  renderOverlay?: () => DomphyElement;
}

export interface DottedMapProps {
  /** SVG viewBox width (user units). Defaults to `150`. */
  width?: number;
  /** SVG viewBox height (user units). Defaults to `75`. */
  height?: number;
  /** Grid columns sampled across the map width (rows derive from the 2:1 aspect ratio) — more columns = denser dot grid. Defaults to `80`. */
  columns?: number;
  markers?: DottedMapMarker[];
  /** Background land-dot radius (SVG units). Defaults to `0.3`. */
  dotRadius?: number;
  /** Marker dot radius (SVG units). Defaults to `1.6`. */
  markerRadius?: number;
  /** Background dot color — dots render with `fill: currentColor`, so this sets the map's `color`. Defaults to `"neutral"`. */
  dotColor?: ThemeColor;
  /** Default marker color, overridable per marker. Defaults to `"primary"`. */
  markerColor?: ThemeColor;
  /** Default pulse setting, overridable per marker. Defaults to `true`. */
  pulse?: boolean;
  /** Offsets alternating rows horizontally by half a column width for a more organic dot layout. Defaults to `true`. */
  staggerRows?: boolean;
  style?: StyleObject;
}

const DEFAULT_WIDTH = 150;
const DEFAULT_HEIGHT = 75;
const DEFAULT_COLUMNS = 80;
const DEFAULT_DOT_RADIUS = 0.3;
const DEFAULT_MARKER_RADIUS = 1.6;

const DEFAULT_MARKERS: DottedMapMarker[] = [
  { latitude: 40.7128, longitude: -74.006, label: "New York" },
  { latitude: 51.5072, longitude: -0.1276, label: "London" },
  { latitude: 35.6762, longitude: 139.6503, label: "Tokyo" },
  { latitude: -33.8688, longitude: 151.2093, label: "Sydney" },
];

interface LandRegion {
  longitude: number;
  latitude: number;
  radiusLongitude: number;
  radiusLatitude: number;
}

// Hand-authored, coarse ellipse approximation of each continent's rough
// footprint — not a traced geographic boundary. Ordering/values are a
// stylized artistic approximation for a decorative backdrop.
const LAND_REGIONS: LandRegion[] = [
  { longitude: -105, latitude: 50, radiusLongitude: 32, radiusLatitude: 22 }, // North America
  { longitude: -85, latitude: 68, radiusLongitude: 18, radiusLatitude: 10 }, // Canadian Arctic
  { longitude: -42, latitude: 72, radiusLongitude: 11, radiusLatitude: 9 }, // Greenland
  { longitude: -90, latitude: 17, radiusLongitude: 8, radiusLatitude: 7 }, // Central America
  { longitude: -60, latitude: -15, radiusLongitude: 20, radiusLatitude: 38 }, // South America
  { longitude: 10, latitude: 52, radiusLongitude: 20, radiusLatitude: 15 }, // Europe
  { longitude: 35, latitude: 63, radiusLongitude: 22, radiusLatitude: 11 }, // Scandinavia / NW Russia
  { longitude: 20, latitude: 5, radiusLongitude: 22, radiusLatitude: 33 }, // Africa
  { longitude: 90, latitude: 58, radiusLongitude: 58, radiusLatitude: 18 }, // Siberia / Central Asia
  { longitude: 48, latitude: 27, radiusLongitude: 12, radiusLatitude: 12 }, // Middle East
  { longitude: 78, latitude: 22, radiusLongitude: 11, radiusLatitude: 13 }, // Indian subcontinent
  { longitude: 108, latitude: 32, radiusLongitude: 16, radiusLatitude: 14 }, // East Asia
  { longitude: 138, latitude: 38, radiusLongitude: 5, radiusLatitude: 7 }, // Japan
  { longitude: 112, latitude: 2, radiusLongitude: 22, radiusLatitude: 13 }, // Maritime Southeast Asia
  { longitude: 133, latitude: -25, radiusLongitude: 19, radiusLatitude: 13 }, // Australia
  { longitude: 172, latitude: -41, radiusLongitude: 3, radiusLatitude: 5 }, // New Zealand
  { longitude: 47, latitude: -19, radiusLongitude: 3, radiusLatitude: 7 }, // Madagascar
];

const PULSE_KEYFRAMES = {
  "0%": { transform: "scale(1)", opacity: 0.7 },
  "100%": { transform: "scale(2.6)", opacity: 0 },
};
const PULSE_ANIMATION_NAME = `dotted-map-pulse-${hashString(JSON.stringify(PULSE_KEYFRAMES))}`;

/** Cheap deterministic pseudo-random value in [0, 1) from a numeric seed — no external RNG dependency. */
function pseudoRandom(seed: number): number {
  const value = Math.sin(seed * 12.9898) * 43758.5453;
  return value - Math.floor(value);
}

/** Whether a lat/long point falls inside any hand-authored landmass ellipse, with a soft
 * jittered boundary (0.85–1.15 instead of a hard 1.0 cutoff) so edges look stippled. */
function isLandApprox(latitude: number, longitude: number): boolean {
  for (const region of LAND_REGIONS) {
    const normalizedLongitude = (longitude - region.longitude) / region.radiusLongitude;
    const normalizedLatitude = (latitude - region.latitude) / region.radiusLatitude;
    const distance = normalizedLongitude * normalizedLongitude + normalizedLatitude * normalizedLatitude;
    const jitter = 0.85 + pseudoRandom(latitude * 1000 + longitude) * 0.3;
    if (distance <= jitter) return true;
  }
  return false;
}

/** Equirectangular projection: lat/long → SVG (x, y) in the map's viewBox space. */
function projectLatLong(latitude: number, longitude: number, width: number, height: number): { x: number; y: number } {
  return { x: ((longitude + 180) / 360) * width, y: ((90 - latitude) / 180) * height };
}

interface DotPoint {
  x: number;
  y: number;
}

/** Pre-computes the static background dot grid once: samples an equirectangular grid,
 * keeps only points classified as land, and (optionally) staggers alternating rows. */
function buildDotGrid(columns: number, width: number, height: number, staggerRows: boolean): DotPoint[] {
  const rows = Math.max(1, Math.round(columns / 2));
  const columnStep = width / columns;
  const rowStep = height / rows;
  const points: DotPoint[] = [];

  for (let row = 0; row < rows; row += 1) {
    const latitude = 90 - ((row + 0.5) / rows) * 180;
    const staggerOffset = staggerRows && row % 2 === 1 ? columnStep / 2 : 0;
    for (let column = 0; column < columns; column += 1) {
      const longitude = ((column + 0.5) / columns) * 360 - 180;
      if (!isLandApprox(latitude, longitude)) continue;
      const x = column * columnStep + columnStep / 2 + staggerOffset;
      if (x > width) continue; // dropped off the right edge by the stagger offset
      const y = row * rowStep + rowStep / 2;
      points.push({ x, y });
    }
  }
  return points;
}

function dotElement(point: DotPoint, index: number, radius: number): DomphyElement {
  return {
    circle: null,
    _key: `dot-${index}`,
    cx: point.x,
    cy: point.y,
    r: radius,
    style: { fill: "currentColor" } as StyleObject,
  } as DomphyElement;
}

function markerElement(
  marker: DottedMapMarker,
  index: number,
  width: number,
  height: number,
  defaultColor: ThemeColor,
  defaultPulse: boolean,
  markerRadius: number,
): DomphyElement {
  const { x, y } = projectLatLong(marker.latitude, marker.longitude, width, height);
  const color = marker.color ?? defaultColor;
  const pulseEnabled = marker.pulse ?? defaultPulse;
  const children: DomphyElement[] = [];

  if (pulseEnabled) {
    children.push({
      circle: null,
      _key: "pulse",
      cx: x,
      cy: y,
      r: markerRadius,
      ariaHidden: "true",
      _doctorDisable: "missing-color",
      style: {
        transformBox: "fill-box",
        transformOrigin: "center",
        fill: (listener: Listener) => themeColor(listener, "shift-9", color),
        animation: `${PULSE_ANIMATION_NAME} 1.8s ease-out ${index * 0.15}s infinite`,
        [`@keyframes ${PULSE_ANIMATION_NAME}`]: PULSE_KEYFRAMES,
      } as StyleObject,
    } as DomphyElement);
  }

  if (marker.renderOverlay) {
    const overlaySize = markerRadius * 2.4;
    children.push({
      foreignObject: [
        {
          div: [marker.renderOverlay()],
          style: { width: "100%", height: "100%", overflow: "hidden", borderRadius: "50%", clipPath: "circle(50%)" },
        },
      ],
      _key: "overlay",
      x: x - overlaySize / 2,
      y: y - overlaySize / 2,
      width: overlaySize,
      height: overlaySize,
    } as DomphyElement);
  } else {
    children.push({
      circle: null,
      _key: "dot",
      cx: x,
      cy: y,
      r: markerRadius,
      ariaHidden: "true",
      _doctorDisable: "missing-color",
      style: {
        fill: (listener: Listener) => themeColor(listener, "shift-9", color),
        stroke: (listener: Listener) => themeColor(listener, "inherit"),
        strokeWidth: markerRadius * 0.5,
      } as StyleObject,
    } as DomphyElement);
  }

  return {
    g: children,
    _key: `marker-${index}`,
    role: marker.label ? "img" : undefined,
    ariaLabel: marker.label,
  } as DomphyElement;
}

/**
 * SVG world map rendered as a stippled grid of dots, with optional pulsing
 * marker overlays at specific latitude/longitude coordinates. Purely static
 * — the base dot grid never animates; only pulsing markers (opt-in, on by
 * default) loop a radar-ping ring via CSS `@keyframes`. Call with no
 * arguments for a working demo — four pulsing city markers.
 */
function dottedMap(props: DottedMapProps = {}): DomphyElement<"svg"> {
  const width = props.width ?? DEFAULT_WIDTH;
  const height = props.height ?? DEFAULT_HEIGHT;
  const columns = Math.max(10, Math.round(props.columns ?? DEFAULT_COLUMNS));
  const dotRadius = props.dotRadius ?? DEFAULT_DOT_RADIUS;
  const markerRadius = props.markerRadius ?? DEFAULT_MARKER_RADIUS;
  const dotColor = props.dotColor ?? "neutral";
  const markerColor = props.markerColor ?? "primary";
  const globalPulse = props.pulse ?? true;
  const staggerRows = props.staggerRows ?? true;
  const markers = props.markers ?? DEFAULT_MARKERS;

  const dots = buildDotGrid(columns, width, height, staggerRows);

  return {
    svg: [
      { g: dots.map((point, index) => dotElement(point, index, dotRadius)), _key: "dot-layer" } as DomphyElement,
      {
        g: markers.map((marker, index) =>
          markerElement(marker, index, width, height, markerColor, globalPulse, markerRadius),
        ),
        _key: "marker-layer",
      } as DomphyElement,
    ],
    viewBox: `0 0 ${width} ${height}`,
    role: "img",
    ariaLabel: "World map with highlighted locations",
    style: {
      display: "block",
      width: "100%",
      height: "auto",
      color: (listener: Listener) => themeColor(listener, "shift-7", dotColor),
      ...(props.style ?? {}),
    } as StyleObject,
  } as DomphyElement<"svg">;
}

export { dottedMap };

← Back to Magic UI catalog