Interactive Geospatial Storytelling

Bring maps to life with motion and meaning.

A declarative animation library built on MapLibre GL. Design journeys, not just maps.

Vintage cartography
Explore Capabilities
01

Everything geospatial, animated.

From simple points to complex temporal timelines. Kartograf extends GeoJSON into a storytelling format.

Geometry

Native & Extended

Point, LineString, Polygon, Multi-geometries, plus 14 custom patches: LinePointString, MultiLinePointString, LineArc, Timeline, Circle, Extrusion, Donut, FlowLine, Arc, Cluster, Heatmap, IconPoint, Label.

Animation

Frame-Perfect Motion

Progressive line drawing, ghost path preview, particle flow, breathing polygons, point bounce, camera orbit, and synchronized multi-geometry sequences.

Three-Dimensional

3D Model & Extrusion

Render GLTF models as map markers or animate fill-extrusion polygons with height rise, wave oscillation, and color sweep effects.

Temporal

Timeline & Video

Fetch time-series GeoJSON data and animate it frame-by-frame, or overlay video directly on the map with playback sync and fade transitions.

Declarative

Story Architecture

Compose scenes with fade transitions, audio-synced narration, play/pause controls, and branching paths. Let Kartograf handle the choreography.

02

See it in motion.

Simulated map viewport showing animated routes, pulsing waypoints, and a live timeline scrubber.

Xi'an — 109 AD
Samarkand
Constantinople
109 — 1453
03

Compose with clean syntax.

Define your narrative as a story — scenes, cameras, features, and transitions — then play it through Kartograf.

Story Configuration
const story = kartograf.createStory({
  title: "The Silk Road",
  scenes: [
    {
      id: "departure",
      camera: { zoomLevel: 6 },
      features: [{
        geometry: { type: "Point", coordinates: [28.9, 41.0] },
        marker: { type: "PULSING", popup: { title: "Istanbul" } }
      }],
      transition: { delay: 3 }
    },
    {
      id: "route",
      camera: { zoomLevel: 5, pitch: 30 },
      features: [{
        geometry: {
          type: "LineString",
          coordinates: [[28.9, 41.0], [32.8, 39.9]]
        },
        line: { color: "#c9a87c", width: 3, animate: true }
      }],
      transition: { delay: 5 }
    },
    {
      id: "arrival",
      camera: { zoomLevel: 8 },
      features: [{
        geometry: {
          type: "Polygon",
          coordinates: [[[32.8, 39.8], [32.9, 39.9], [32.7, 39.9], [32.8, 39.8]]]
        }
      }],
      transition: { delay: 2 }
    }
  ]
});

// Play from start to finish
await story.play();
04

Branching narratives.

Stories aren't always linear. Define conditional paths that respond to your data or user state.

// Branch based on a function
story.story.addBranch(
  "crossroads",
  (sceneData) => sceneData.sceneIndex === 0,
  "northern_route"
);

// Branch based on a key-value condition
story.story.addBranch(
  "northern_route",
  { key: "completed", value: true },
  "finale"
);

// Listen to lifecycle events
story.story.on("sceneStart", (s) => {
  console.log("Now showing:", s.id);
});

story.story.on("complete", () => {
  console.log("Journey finished");
});

The Crossroads

Decision point where the narrative splits based on data conditions or user interaction state.

Northern Route

Conditional branch taken only when the predicate function returns true. Otherwise continues linearly.

The Finale

Convergence point. Branches can merge back, loop, or terminate independently.

05

Extend the canvas.

Register custom geometry types with the plugin system. Kartograf handles the lifecycle; you handle the rendering.

class HeatmapLayer extends Layer {
  add() {
    const { map, element, index } = this;
    map.addSource("heatmap-source", {
      type: "geojson", data: { type: "FeatureCollection", features: [element] }
    });
    map.addLayer({
      id: `heatmap-${index}`,
      type: "heatmap",
      source: "heatmap-source",
      paint: {
        "heatmap-radius": 30,
        "heatmap-color": [...]
      }
    });
  }
  remove() { /* cleanup */ }
  animate() { /* optional */ }
}

kartograf.registerLayerType("Heatmap", HeatmapLayer);

// Now use type: "Heatmap" in your GeoJSON
06

Every geometry, elevated.

From a single marker to a temporally animated polygon. The same API surface, infinite expression.

// 3D Model Marker
{
  geometry: { type: "Point", coordinates: [lng, lat] },
  marker: {
    type: "THREED",
    model: { url: "ship.gltf", size: 10, angle: 0 }
  }
}

// LineArc — curved bezier route
{
  geometry: {
    type: "LineArc",
    coordinates: [[startLng, startLat], [endLng, endLat]]
  },
  line: { color: "#c9a87c", width: 3, animate: true }
}

// Timeline — temporal data animation
{
  geometry: { type: "Timeline", coordinates: [bounds] },
  timeline: {
    url: "events.geojson",
    start: "2024-01-01",
    end: "2024-12-31",
    step: 7,
    qualifier: "DATE",
    fps: 100
  }
}
Point
LineString
Polygon
3D Model
07

Five stories, live.

Real MapLibre GL maps rendered with Kartograf's declarative Story API. Each loops to show the full animation cycle.

Standard GeoJSON

The Departure

A single Point with a pulsing animated marker. The camera glides in with flyTo, then holds while the pulse draws the eye.

const story = kartograf.createStory({
  title: "The Departure",
  scenes: [
    {
      id: "departure",
      camera: { zoomLevel: 10 },
      features: [{
        geometry: { type: "Point", coordinates: [28.978, 41.008] },
        marker: { type: "PULSING", popup: { title: "Istanbul" } }
      }]
    }
  ]
});

await story.play();
Standard GeoJSON

The Route

A LineString that draws itself coordinate by coordinate. Camera fits to bounds as the path grows. Dashed or solid, your choice.

const story = kartograf.createStory({
  title: "The Route",
  scenes: [
    {
      id: "route",
      camera: { zoomLevel: 6, pitch: 25 },
      features: [{
        geometry: {
          type: "LineString",
          coordinates: [
            [28.978, 41.008],
            [32.860, 39.933],
            [27.138, 38.423]
          ]
        },
        line: { color: "#c9a87c", width: 3, animate: true }
      }]
    }
  ]
});

await story.play();
Custom Patch Type

The Traveler

LinePointString draws a line progressively and animates a point marker along the path simultaneously. The signature Kartograf experience.

const story = kartograf.createStory({
  title: "The Traveler",
  scenes: [
    {
      id: "traveler",
      camera: { zoomLevel: 6 },
      features: [{
        geometry: {
          type: "LinePointString",
          coordinates: [
            [28.978, 41.008],
            [30.520, 39.800],
            [32.860, 39.933]
          ]
        },
        marker: { type: "PULSING" },
        line: { color: "#c9a87c", width: 3, animate: true }
      }]
    }
  ]
});

await story.play();
Custom Patch Type

The Great Circle

LineArc takes only two points and generates a smooth quadratic bezier curve between them. Great for flight paths and trade routes.

const story = kartograf.createStory({
  title: "The Great Circle",
  scenes: [
    {
      id: "arc",
      camera: { zoomLevel: 5 },
      features: [{
        geometry: {
          type: "LineArc",
          coordinates: [
            [28.978, 41.008],
            [51.390, 35.689]
          ]
        },
        line: { color: "#88c0d0", width: 3, animate: true }
      }]
    }
  ]
});

await story.play();
Custom Patch Type

Through the Ages

Timeline defines a viewing polygon and animates time-series point data frame by frame. A temporal lens on geographic events.

const story = kartograf.createStory({
  title: "Through the Ages",
  scenes: [
    {
      id: "era",
      camera: { zoomLevel: 5 },
      features: [{
        geometry: {
          type: "Timeline",
          coordinates: [[[26, 42], [45, 42],
            [45, 35], [26, 35],
            [26, 42]]]
        },
        timeline: {
          url: "events.geojson",
          start: "1200-01-01",
          end: "1453-05-29",
          step: 30,
          qualifier: "DATE",
          displayType: "CIRCLE",
          fps: 120,
          paint: { color: "#c9a87c", radius: 5, opacity: 0.7 }
        }
      }]
    }
  ]
});

await story.play();
1200 — 1453
Get Started

Start building your story.

Install via npm, compose a story with scenes and features, then call play. Or explore all 21 geometry types in the showroom or build your own compositions in the playground.

npm install kartograf
Read the Docs View on GitHub