dot_globe

Spin a dotted 3D globe, pin any widget to a latitude/longitude, and connect the dots with great-circle arcs. Pure Dart, zero dependencies, nine presets.

pub package Flutter License: MIT Platforms

Globe with markers and arcs

World Cup Presets Neon Routes

dot_globe renders ~6,300 halftone land dots and lets you pin any Flutter widget (flags, labels, price bubbles) to geographic coordinates β€” with automatic depth sorting, back-face culling, and edge fade. Visually inspired by Polymarket's world-cup map and cobe.js.

🌐 Live demo β†’  Β·  δΈ­ζ–‡ζ–‡ζ‘£ β†’


Features

  • Any widget as a marker β€” not just dots. Pin images, flags, bubbles, price charts, or custom widgets to lat/lng coordinates. Markers fade and hide automatically when they rotate to the back.
  • Great-circle arcs β€” connect coordinates with flight-path lines that bow off the sphere. Per-arc altitude, color, width, dash, and back-side opacity; solid on the near side, dashed as they curve around the back.
  • Halftone (dotted) rendering β€” ~6,300 land dots sampled from Natural Earth 110m data. Cobe-style aesthetics with strong visual depth.
  • Pure Dart, zero dependencies β€” no plugins, no shader code, no texture assets. Import and go on iOS, Android, Web, macOS, Windows, Linux.
  • High performance β€” rotation never rebuilds widgets; markers re-position via Flow (transform-only, no relayout). Consistent 60 fps on Impeller.
  • Full gesture control β€” drag to rotate with inertia decay, auto-spin when idle, configurable tilt limits, elastic spring-back to rest pitch.
  • Nine color presets β€” light, dark, polymarket, neon, sunset, mono, emerald, pastel, midnight. One-line styling or customize with copyWith.
  • Imperative controller β€” spin to a coordinate, observe the current facing point, drive animations from outside the widget tree.

Install

dependencies:
  dot_globe: ^0.1.0
flutter pub add dot_globe

Quick start

Minimal: one-line globe

import 'package:dot_globe/dot_globe.dart';

DotGlobe(style: DotGlobeStyle.dark)

Tap and drag to rotate, swipe for momentum.

With markers

DotGlobe(
  style: DotGlobeStyle.polymarket,
  markers: [
    DotGlobeMarker(
      latitude: 46, longitude: 2,
      anchor: Alignment.bottomCenter,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Text('πŸ‡«πŸ‡·', style: TextStyle(fontSize: 28)),
          Container(
            decoration: BoxDecoration(
              color: Colors.blue,
              borderRadius: BorderRadius.circular(6),
            ),
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
            child: const Text('16%', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
    ),
    DotGlobeMarker(
      latitude: -23, longitude: -47,
      anchor: Alignment.bottomCenter,
      child: const Icon(Icons.location_on, color: Colors.red),
    ),
  ],
)

Each marker auto-projects to its lat/lng, fades near the edge, and vanishes when it rotates behind the sphere.

Connection arcs

DotGlobe(
  style: DotGlobeStyle.midnight,
  // Shrink the globe so tall arcs stay in bounds: radiusFactor <= 1/(1+altitude)
  radiusFactor: 0.55,
  arcs: const [
    DotGlobeArc(
      startLatitude: 35.7, startLongitude: 139.7, // Tokyo
      endLatitude: 51.5, endLongitude: -0.1,       // London
      color: Color(0xFF4ED7F2),
      altitude: 0.45,   // how high it bows off the sphere (0 = hugs it)
    ),
    DotGlobeArc(
      startLatitude: 40.7, startLongitude: -74.0,  // New York
      endLatitude: -23.5, endLongitude: -46.6,     // SΓ£o Paulo
      color: Color(0xFFFF5A5A),
      altitude: 0.3,
      dashed: true,       // dash the near side too (back is dashed by default)
      backOpacity: 0.4,   // fade of the half curving behind the globe
    ),
  ],
)

Programmatic control

final controller = DotGlobeController();

DotGlobe(
  style: DotGlobeStyle.neon,
  controller: controller,
);

// Spin to a coordinate over 600ms
controller.animateTo(latitude: 46, longitude: 2);

// Or snap instantly
controller.jumpTo(latitude: -23, longitude: -47);

// Listen to rotations (fires every frame while spinning)
controller.addListener(() {
  final facing = controller.facing;
  print('Facing: ${facing?.latitude}, ${facing?.longitude}');
});

// Dispose when done
@override
void dispose() {
  controller.dispose();
  super.dispose();
}

Custom styling

DotGlobe(
  style: DotGlobeStyle.dark.copyWith(
    dotRadius: 1.8,
    depthFade: 0.8,
  ),
)

Or build your own palette:

DotGlobe(
  style: DotGlobeStyle(
    dotColor: const Color(0xFF00FF88),
    sphereColor: const Color(0xFF0A0A20),
    glowColor: const Color(0xFF00FF44),
    sphereLight: true,
    depthFade: 0.7,
    backgroundColor: const Color(0xFF000000),
  ),
)

API at a glance

Widget / Class Purpose
DotGlobe Root globe widget; holds markers, gestures, and animation state.
DotGlobeMarker A widget pinned to a lat/lng coordinate on the sphere.
DotGlobeArc A great-circle arc (flight path) between two coordinates.
DotGlobeStyle Color, lighting, and dot-size config; 9 presets included.
DotGlobeController Spin the globe and read the facing coordinate.
DotGlobeFacing The lat/lng currently facing the viewer.

DotGlobe parameters

Parameter Default Notes
style DotGlobeStyle.light Visual config: colors, lighting, dot size. Pick a preset or customize.
markers [] Widgets pinned to the globe.
arcs [] Great-circle arcs drawn over the globe, beneath the markers.
controller null Optional handle to drive rotation and read facing point.
radiusFactor 0.92 Sphere radius as a fraction of half the widget's shortest side.
autoRotateSpeed 0.12 Idle rotation speed in radians per second; 0 = static.
initialLatitude 18 Pitch (latitude in degrees) at rest; also the spring-back target after a vertical drag.
initialLongitude 10 Yaw (longitude in degrees) facing the viewer at first build.
maxTilt 0.6 Maximum pitch deviation from rest, in radians (~34Β°).
dragSensitivity 1.0 Drag-to-rotation multiplier; higher = faster spin.
inertiaDecay 0.94 Per-frame (60 fps) momentum retention [0, 1); higher = longer spin after release.
tiltReturn 0.92 Per-frame pitch spring-back retention [0, 1); higher = slower return to rest.
interactive true Whether drags rotate the globe.
paused false Pause the frame loop (auto-rotation, inertia) for off-screen globes to save power. Pair with VisibilityDetector.

DotGlobeStyle fields & presets

Field Type Notes
dotColor Color Required. Land dot color; far dots dim when depthFade > 0.
sphereColor Color Required. Sphere base; flat or radial gradient if sphereLight = true.
glowColor Color? Rim glow at the silhouette; null disables. Best on dark backgrounds.
sphereLight bool false = flat fill; true = faked 3D radial gradient from upper-left.
depthFade double Depth intensity 0–1; 1 = strong (Polymarket look), 0 = flat dots.
dotRadius double Land dot size in logical pixels (at the sphere front).
backgroundColor Color? Optional fill behind the globe; null = transparent.

Built-in presets:

  • light β€” Pale blue dots on faint blue, flat. Light-themed apps.
  • dark β€” Deep blue luminous dots on navy, flat. Dark-themed apps.
  • polymarket β€” 3D-lit blue globe with strong depth. Homage to the Polymarket world-cup map.
  • neon β€” Cyan dots with magenta rim glow. Cyber / Web3 energy.
  • sunset β€” Amber dots on a lit ember sphere. Warm, brand-forward.
  • mono β€” White dots on near-black, flat, no glow. Premium, editorial.
  • emerald β€” Turquoise dots on a lit forest-green sphere. Organic.
  • pastel β€” Mauve dots on cream lavender, flat, on light background. Cute.
  • midnight β€” Cool off-white dots with faint blue glow. Quieter dark default.

Iterate all presets programmatically:

for (final name in DotGlobeStyle.presets.entries) {
  print('${name.key}: ${name.value}');
}

DotGlobeController methods

Method Returns Notes
animateTo({latitude?, longitude?, duration, curve}) Future<void> Spin to a coordinate with easing; omitted axes keep their current value. Pitch is clamped to maxTilt.
jumpTo({latitude?, longitude?}) void Snap instantly; omitted axes unchanged.
facing DotGlobeFacing? Current facing coordinate; null if not attached.
isAttached bool Whether the controller is bound to a mounted globe.
addListener, removeListener, dispose Standard ChangeNotifier API; facing updates fire on every rotation frame.

DotGlobeMarker parameters

Parameter Type Notes
latitude double Degrees north; south is negative.
longitude double Degrees east; west is negative.
child Widget The widget rendered at this coordinate. Can be anything: Text, Image, Icon, custom widgets.
anchor Alignment Which point of child pins to the projected coordinate. Default center; use bottomCenter for bubbles with a tail.

DotGlobeArc parameters

Parameter Default Notes
startLatitude, startLongitude β€” Required. Start coordinate (degrees).
endLatitude, endLongitude β€” Required. End coordinate (degrees).
color 0xFF6B8AE8 Stroke color.
width 2.0 Stroke width in logical pixels.
altitude 0.35 Peak height above the sphere as a fraction of radius; 0 hugs the surface.
dashed false Dash the near (front) side.
backDashed true Dash the half curving around the back.
backOpacity 0.38 Opacity of the back half, 0–1; 0 hides it, 1 matches the front.
dashLength, dashGap 6, 5 Dash pattern in logical pixels.
glow true Soft glow underlay on the front half.

Tall arcs bow outside the globe's circle, so shrink the sphere to keep them in frame: radiusFactor <= 1 / (1 + maxAltitude).

How it works

Geometry:

  • ~6,300 land dots are sampled from Natural Earth's 110m coastline data using a Fibonacci-sphere distribution.
  • The globe is a unit sphere in the [-1, 1]^3 coordinate space.
  • Each frame, the sphere rotates by two angles (yaw / longitude, pitch / latitude).

Rendering:

  • The canvas layer (CustomPaint with DotGlobePainter) draws dots in one batched drawRawPoints call per depth band. Rotation never triggers a widget rebuild β€” only a repaint via a shared ChangeNotifier.
  • The widget layer uses a Flow to re-position markers every frame via matrix transforms (no relayout, no rebuild). Back-facing markers are skipped entirely, so they stop receiving taps.
  • Markers fade (opacity) as they approach the sphere's silhouette and disappear when they pass behind.

Performance:

  • ~0.1 ms per frame for coordinate projection and layout.
  • 60 fps sustained on Impeller (iOS 15+, Android 12+).
  • Dragging the globe costs only a repaint, not a full rebuild.

Why dot_globe?

vs. cobe_flutter

cobe_flutter is a solid WebGL port, but markers are limited to small circles or text labels baked into the shader. dot_globe lets you use any Flutter widget β€” images, custom bubbles, charts β€” with full layout freedom.

vs. textured-globe packages

Packages using earth.png + 3D mesh require asset bundles, GPU shaders, and plugin code. dot_globe is pure Dart, zero assets, zero plugins β€” lighter, simpler, faster to ship.

vs. plain map libraries

Maps are designed for geographic data. If you want a pretty, animated, tactile globe as a UI component β€” to show global stats, highlight regions, or just delight users β€” a dotted globe is lower overhead and more visually distinctive.

Platform support

iOS Android Web macOS Windows Linux
βœ… βœ… βœ… βœ… βœ… βœ…

dot_globe is pure Dart; everything renders through Flutter's native Canvas and Transform APIs. There are no plugin channels, native code, or platform-specific code paths.

FAQ

Q. Do markers need to be small? No. A marker can be as large as you want β€” a full Card, a chart widget, or a ListView. It will clip to the sphere's silhouette and fade automatically.

Q. Can I tap markers? Yes. Markers are regular Widgets, so they receive taps, gestures, and state changes like any other Flutter widget. Back-facing markers are invisible and skip hit-testing.

Q. How many markers can I add? Go crazy. Each marker is just a widget in a Flow. Layout cost is negligible; the paint cost depends on the marker complexity, not the count. Tested with 100+ markers at 60 fps.

Q. What if I want the globe paused to save power? Set paused: true. The frame loop stops entirely. When you set paused: false, it resumes. Pair it with VisibilityDetector to pause when the globe scrolls off-screen.

VisibilityDetector(
  key: Key('my_globe'),
  onVisibilityChanged: (info) {
    setState(() => _globePaused = info.visibleFraction < 0.1);
  },
  child: DotGlobe(paused: _globePaused, ...),
)

Q. Can I customize the dot pattern? Not directly β€” the land geometry is baked into assets/land_dots.bin at build time. It's based on Natural Earth 110m data, which is a good balance of detail and performance. Custom geometries are a future enhancement.

Q. What about haptics or sound? dot_globe is a pure rendering widget. Wrap it with your haptic / audio layer as needed β€” e.g., trigger haptics in controller.addListener().

Example

A full showcase app lives in example/. To run it:

cd example
flutter run

Contributing

Issues and PRs welcome β€” especially for new presets, performance tuning, and test coverage. Run the test suite:

flutter test
cd example && flutter test

License

MIT Β© 2026 cccmax.

Libraries

dot_globe
A dotted (halftone) 3D globe for Flutter.